Repository: e-breuninger/terraform-provider-netbox Branch: master Commit: 2f471e19ab4b Files: 570 Total size: 1.8 MB Directory structure: gitextract_85p5o3oc/ ├── .github/ │ ├── CODE_OF_CONDUCT.md │ ├── ISSUE_TEMPLATE.md │ ├── allowed-subcategories.txt │ ├── dependabot.yml │ └── workflows/ │ ├── check-allowed-subcategories.yml │ ├── ci-testing.yml │ ├── ensure-docs-examples.yml │ ├── golangci-lint.yml │ ├── pre-commit.yml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── .markdownlint.yaml ├── .pre-commit-config.yaml ├── .prettierignore ├── .terraform-registry ├── .yamllint.yaml ├── CHANGELOG.md ├── GNUmakefile ├── LICENSE ├── README.md ├── docker/ │ ├── Dockerfile-wait │ ├── docker-compose.yml │ └── wait-for ├── docs/ │ ├── data-sources/ │ │ ├── asn.md │ │ ├── asns.md │ │ ├── available_prefix.md │ │ ├── cluster.md │ │ ├── cluster_group.md │ │ ├── cluster_type.md │ │ ├── clusters.md │ │ ├── config_context.md │ │ ├── contact.md │ │ ├── contact_group.md │ │ ├── contact_role.md │ │ ├── device_interfaces.md │ │ ├── device_power_ports.md │ │ ├── device_render_config.md │ │ ├── device_role.md │ │ ├── device_type.md │ │ ├── devices.md │ │ ├── interfaces.md │ │ ├── ip_address.md │ │ ├── ip_addresses.md │ │ ├── ip_range.md │ │ ├── ip_ranges.md │ │ ├── ipam_role.md │ │ ├── location.md │ │ ├── locations.md │ │ ├── platform.md │ │ ├── prefix.md │ │ ├── prefixes.md │ │ ├── rack_role.md │ │ ├── racks.md │ │ ├── region.md │ │ ├── rir.md │ │ ├── route_target.md │ │ ├── site.md │ │ ├── site_group.md │ │ ├── tag.md │ │ ├── tags.md │ │ ├── tenant.md │ │ ├── tenant_group.md │ │ ├── tenants.md │ │ ├── virtual_disk.md │ │ ├── virtual_machines.md │ │ ├── vlan.md │ │ ├── vlan_group.md │ │ ├── vlan_groups.md │ │ ├── vlans.md │ │ ├── vrf.md │ │ └── vrfs.md │ ├── index.md │ └── resources/ │ ├── aggregate.md │ ├── asn.md │ ├── available_ip_address.md │ ├── available_prefix.md │ ├── available_vlan.md │ ├── cable.md │ ├── circuit.md │ ├── circuit_provider.md │ ├── circuit_termination.md │ ├── circuit_type.md │ ├── cluster.md │ ├── cluster_group.md │ ├── cluster_type.md │ ├── config_context.md │ ├── config_template.md │ ├── contact.md │ ├── contact_assignment.md │ ├── contact_group.md │ ├── contact_role.md │ ├── custom_field.md │ ├── custom_field_choice_set.md │ ├── device.md │ ├── device_bay.md │ ├── device_bay_template.md │ ├── device_console_port.md │ ├── device_console_server_port.md │ ├── device_front_port.md │ ├── device_interface.md │ ├── device_module_bay.md │ ├── device_power_outlet.md │ ├── device_power_port.md │ ├── device_primary_ip.md │ ├── device_rear_port.md │ ├── device_role.md │ ├── device_type.md │ ├── event_rule.md │ ├── group.md │ ├── interface.md │ ├── interface_template.md │ ├── inventory_item.md │ ├── inventory_item_role.md │ ├── ip_address.md │ ├── ip_range.md │ ├── ipam_role.md │ ├── location.md │ ├── mac_address.md │ ├── manufacturer.md │ ├── module.md │ ├── module_type.md │ ├── permission.md │ ├── platform.md │ ├── power_feed.md │ ├── power_panel.md │ ├── prefix.md │ ├── primary_ip.md │ ├── rack.md │ ├── rack_reservation.md │ ├── rack_role.md │ ├── rack_type.md │ ├── region.md │ ├── rir.md │ ├── route_target.md │ ├── service.md │ ├── site.md │ ├── site_group.md │ ├── tag.md │ ├── tenant.md │ ├── tenant_group.md │ ├── token.md │ ├── user.md │ ├── virtual_chassis.md │ ├── virtual_disk.md │ ├── virtual_machine.md │ ├── vlan.md │ ├── vlan_group.md │ ├── vpn_tunnel.md │ ├── vpn_tunnel_group.md │ ├── vpn_tunnel_termination.md │ ├── vrf.md │ ├── webhook.md │ ├── wireless_lan.md │ └── wireless_lan_group.md ├── example/ │ ├── README.md │ └── main.tf ├── examples/ │ ├── data-sources/ │ │ ├── netbox_asn/ │ │ │ └── data-source.tf │ │ ├── netbox_asns/ │ │ │ └── data-source.tf │ │ ├── netbox_cluster/ │ │ │ └── data-source.tf │ │ ├── netbox_cluster_group/ │ │ │ └── data-source.tf │ │ ├── netbox_clusters/ │ │ │ └── data-source.tf │ │ ├── netbox_device_render_config/ │ │ │ └── data-source.tf │ │ ├── netbox_device_role/ │ │ │ └── data-source.tf │ │ ├── netbox_device_type/ │ │ │ └── data-source.tf │ │ ├── netbox_interfaces/ │ │ │ └── data-source.tf │ │ ├── netbox_ip_address/ │ │ │ └── data-source.tf │ │ ├── netbox_ip_range/ │ │ │ └── data-source.tf │ │ ├── netbox_platform/ │ │ │ └── data-source.tf │ │ ├── netbox_rir/ │ │ │ └── data-source.tf │ │ ├── netbox_site/ │ │ │ └── data-source.tf │ │ ├── netbox_site_group/ │ │ │ └── data-source.tf │ │ ├── netbox_tag/ │ │ │ └── data-source.tf │ │ ├── netbox_tags/ │ │ │ └── data-source.tf │ │ ├── netbox_tenant/ │ │ │ └── data-source.tf │ │ ├── netbox_virtual_disk/ │ │ │ └── data-source.tf │ │ ├── netbox_virtual_machines/ │ │ │ └── data-source.tf │ │ ├── netbox_vlan/ │ │ │ └── data-source.tf │ │ ├── netbox_vlan_group/ │ │ │ └── data-source.tf │ │ └── netbox_vrf/ │ │ └── data-source.tf │ ├── provider/ │ │ └── provider.tf │ └── resources/ │ ├── netbox_aggregate/ │ │ └── resource.tf │ ├── netbox_asn/ │ │ └── resource.tf │ ├── netbox_available_ip_address/ │ │ ├── assign_to_interface.tf │ │ ├── prefix.tf │ │ └── range.tf │ ├── netbox_available_prefix/ │ │ └── resource.tf │ ├── netbox_available_vlan/ │ │ ├── multiple.tf │ │ ├── prefix.tf │ │ ├── range.tf │ │ └── site.tf │ ├── netbox_cable/ │ │ └── resource.tf │ ├── netbox_circuit/ │ │ └── resource.tf │ ├── netbox_circuit_provider/ │ │ └── resource.tf │ ├── netbox_circuit_termination/ │ │ └── resource.tf │ ├── netbox_circuit_type/ │ │ └── resource.tf │ ├── netbox_cluster/ │ │ └── resource.tf │ ├── netbox_cluster_group/ │ │ └── resource.tf │ ├── netbox_cluster_type/ │ │ └── resource.tf │ ├── netbox_config_context/ │ │ └── resource.tf │ ├── netbox_config_template/ │ │ └── resource.tf │ ├── netbox_contact/ │ │ └── resource.tf │ ├── netbox_contact_assignment/ │ │ └── resource.tf │ ├── netbox_contact_group/ │ │ └── resource.tf │ ├── netbox_contact_role/ │ │ └── resource.tf │ ├── netbox_custom_field/ │ │ └── resource.tf │ ├── netbox_custom_field_choice_set/ │ │ └── resource.tf │ ├── netbox_device/ │ │ └── resource.tf │ ├── netbox_device_bay/ │ │ └── resource.tf │ ├── netbox_device_bay_template/ │ │ └── resource.tf │ ├── netbox_device_console_port/ │ │ └── resource.tf │ ├── netbox_device_console_server_port/ │ │ └── resource.tf │ ├── netbox_device_front_port/ │ │ └── resource.tf │ ├── netbox_device_interface/ │ │ └── resource.tf │ ├── netbox_device_module_bay/ │ │ └── resource.tf │ ├── netbox_device_power_outlet/ │ │ └── resource.tf │ ├── netbox_device_power_port/ │ │ └── resource.tf │ ├── netbox_device_primary_ip/ │ │ └── resource.tf │ ├── netbox_device_rear_port/ │ │ └── resource.tf │ ├── netbox_device_role/ │ │ └── resource.tf │ ├── netbox_device_type/ │ │ └── resource.tf │ ├── netbox_event_rule/ │ │ └── resource.tf │ ├── netbox_group/ │ │ └── resource.tf │ ├── netbox_interface/ │ │ └── resource.tf │ ├── netbox_interface_template/ │ │ └── resource.tf │ ├── netbox_inventory_item/ │ │ └── resource.tf │ ├── netbox_inventory_item_role/ │ │ └── resource.tf │ ├── netbox_ip_address/ │ │ ├── device_interface_id.tf │ │ ├── object_type_device.tf │ │ ├── object_type_virtual_machine.tf │ │ ├── standalone.tf │ │ └── virtual_machine_interface_id.tf │ ├── netbox_ip_range/ │ │ └── resource.tf │ ├── netbox_ipam_role/ │ │ └── resource.tf │ ├── netbox_location/ │ │ └── resource.tf │ ├── netbox_mac_address/ │ │ ├── device_interface_id.tf │ │ ├── object_type_device.tf │ │ ├── object_type_virtual_machine.tf │ │ ├── standalone.tf │ │ └── virtual_machine_interface_id.tf │ ├── netbox_manufacturer/ │ │ └── resource.tf │ ├── netbox_module/ │ │ └── resource.tf │ ├── netbox_module_type/ │ │ └── resource.tf │ ├── netbox_permission/ │ │ └── resource.tf │ ├── netbox_platform/ │ │ └── resource.tf │ ├── netbox_power_feed/ │ │ └── resource.tf │ ├── netbox_power_panel/ │ │ └── resource.tf │ ├── netbox_prefix/ │ │ └── resource.tf │ ├── netbox_primary_ip/ │ │ └── resource.tf │ ├── netbox_rack/ │ │ └── resource.tf │ ├── netbox_rack_reservation/ │ │ └── resource.tf │ ├── netbox_rack_role/ │ │ └── resource.tf │ ├── netbox_rack_type/ │ │ └── resource.tf │ ├── netbox_region/ │ │ └── resource.tf │ ├── netbox_rir/ │ │ └── resource.tf │ ├── netbox_route_target/ │ │ └── resource.tf │ ├── netbox_service/ │ │ └── resource.tf │ ├── netbox_site/ │ │ └── resource.tf │ ├── netbox_site_group/ │ │ └── resource.tf │ ├── netbox_tag/ │ │ └── resource.tf │ ├── netbox_tenant/ │ │ └── resource.tf │ ├── netbox_tenant_group/ │ │ └── resource.tf │ ├── netbox_token/ │ │ └── resource.tf │ ├── netbox_user/ │ │ └── resource.tf │ ├── netbox_virtual_chassis/ │ │ └── resource.tf │ ├── netbox_virtual_disk/ │ │ └── resource.tf │ ├── netbox_virtual_machine/ │ │ └── resource.tf │ ├── netbox_vlan/ │ │ └── resource.tf │ ├── netbox_vlan_group/ │ │ └── resource.tf │ ├── netbox_vpn_tunnel/ │ │ └── resource.tf │ ├── netbox_vpn_tunnel_group/ │ │ └── resource.tf │ ├── netbox_vpn_tunnel_termination/ │ │ └── resource.tf │ ├── netbox_vrf/ │ │ └── resource.tf │ ├── netbox_webhook/ │ │ └── resource.tf │ ├── netbox_wireless_lan/ │ │ └── resource.tf │ └── netbox_wireless_lan_group/ │ └── resource.tf ├── go.mod ├── go.sum ├── main.go ├── netbox/ │ ├── client.go │ ├── client_test.go │ ├── custom_fields.go │ ├── custom_fields_test.go │ ├── data_source_netbox_asn.go │ ├── data_source_netbox_asn_test.go │ ├── data_source_netbox_asns.go │ ├── data_source_netbox_asns_test.go │ ├── data_source_netbox_available_prefix.go │ ├── data_source_netbox_available_prefix_test.go │ ├── data_source_netbox_cluster.go │ ├── data_source_netbox_cluster_group.go │ ├── data_source_netbox_cluster_group_test.go │ ├── data_source_netbox_cluster_test.go │ ├── data_source_netbox_cluster_type.go │ ├── data_source_netbox_cluster_type_test.go │ ├── data_source_netbox_clusters.go │ ├── data_source_netbox_clusters_test.go │ ├── data_source_netbox_config_context.go │ ├── data_source_netbox_config_context_test.go │ ├── data_source_netbox_contact.go │ ├── data_source_netbox_contact_group.go │ ├── data_source_netbox_contact_group_test.go │ ├── data_source_netbox_contact_role.go │ ├── data_source_netbox_contact_role_test.go │ ├── data_source_netbox_contact_test.go │ ├── data_source_netbox_device_interfaces.go │ ├── data_source_netbox_device_interfaces_test.go │ ├── data_source_netbox_device_power_ports.go │ ├── data_source_netbox_device_power_ports_test.go │ ├── data_source_netbox_device_render_config.go │ ├── data_source_netbox_device_render_config_test.go │ ├── data_source_netbox_device_role.go │ ├── data_source_netbox_device_role_test.go │ ├── data_source_netbox_device_type.go │ ├── data_source_netbox_device_type_test.go │ ├── data_source_netbox_devices.go │ ├── data_source_netbox_devices_pagination_test.go │ ├── data_source_netbox_devices_test.go │ ├── data_source_netbox_interfaces.go │ ├── data_source_netbox_interfaces_test.go │ ├── data_source_netbox_ip_address.go │ ├── data_source_netbox_ip_address_test.go │ ├── data_source_netbox_ip_addresses.go │ ├── data_source_netbox_ip_addresses_test.go │ ├── data_source_netbox_ip_range.go │ ├── data_source_netbox_ip_range_test.go │ ├── data_source_netbox_ip_ranges.go │ ├── data_source_netbox_ip_ranges_test.go │ ├── data_source_netbox_ipam_role.go │ ├── data_source_netbox_ipam_role_test.go │ ├── data_source_netbox_location.go │ ├── data_source_netbox_location_test.go │ ├── data_source_netbox_locations.go │ ├── data_source_netbox_locations_test.go │ ├── data_source_netbox_platform.go │ ├── data_source_netbox_platform_test.go │ ├── data_source_netbox_prefix.go │ ├── data_source_netbox_prefix_test.go │ ├── data_source_netbox_prefixes.go │ ├── data_source_netbox_prefixes_test.go │ ├── data_source_netbox_rack_role.go │ ├── data_source_netbox_rack_role_test.go │ ├── data_source_netbox_racks.go │ ├── data_source_netbox_racks_test.go │ ├── data_source_netbox_region.go │ ├── data_source_netbox_region_test.go │ ├── data_source_netbox_rir.go │ ├── data_source_netbox_rir_test.go │ ├── data_source_netbox_route_target.go │ ├── data_source_netbox_route_target_test.go │ ├── data_source_netbox_site.go │ ├── data_source_netbox_site_group.go │ ├── data_source_netbox_site_group_test.go │ ├── data_source_netbox_site_test.go │ ├── data_source_netbox_tag.go │ ├── data_source_netbox_tags.go │ ├── data_source_netbox_tags_test.go │ ├── data_source_netbox_tenant.go │ ├── data_source_netbox_tenant_group.go │ ├── data_source_netbox_tenant_group_test.go │ ├── data_source_netbox_tenant_test.go │ ├── data_source_netbox_tenants.go │ ├── data_source_netbox_tenants_test.go │ ├── data_source_netbox_virtual_disk.go │ ├── data_source_netbox_virtual_disk_test.go │ ├── data_source_netbox_virtual_machines.go │ ├── data_source_netbox_virtual_machines_test.go │ ├── data_source_netbox_vlan.go │ ├── data_source_netbox_vlan_group.go │ ├── data_source_netbox_vlan_group_test.go │ ├── data_source_netbox_vlan_groups.go │ ├── data_source_netbox_vlan_groups_test.go │ ├── data_source_netbox_vlan_test.go │ ├── data_source_netbox_vlans.go │ ├── data_source_netbox_vlans_test.go │ ├── data_source_netbox_vrf.go │ ├── data_source_netbox_vrf_test.go │ ├── data_source_netbox_vrfs.go │ ├── data_source_netbox_vrfs_test.go │ ├── generic_objects.go │ ├── netbox_sweeper_test.go │ ├── pagination.go │ ├── pagination_test.go │ ├── provider.go │ ├── provider_test.go │ ├── resource_netbox_aggregate.go │ ├── resource_netbox_aggregate_test.go │ ├── resource_netbox_asn.go │ ├── resource_netbox_asn_test.go │ ├── resource_netbox_available_ip_address.go │ ├── resource_netbox_available_ip_address_test.go │ ├── resource_netbox_available_prefix.go │ ├── resource_netbox_available_prefix_test.go │ ├── resource_netbox_available_vlan.go │ ├── resource_netbox_available_vlan_test.go │ ├── resource_netbox_cable.go │ ├── resource_netbox_cable_test.go │ ├── resource_netbox_circuit.go │ ├── resource_netbox_circuit_provider.go │ ├── resource_netbox_circuit_provider_test.go │ ├── resource_netbox_circuit_termination.go │ ├── resource_netbox_circuit_termination_test.go │ ├── resource_netbox_circuit_test.go │ ├── resource_netbox_circuit_type.go │ ├── resource_netbox_circuit_type_test.go │ ├── resource_netbox_cluster.go │ ├── resource_netbox_cluster_group.go │ ├── resource_netbox_cluster_group_test.go │ ├── resource_netbox_cluster_test.go │ ├── resource_netbox_cluster_type.go │ ├── resource_netbox_cluster_type_test.go │ ├── resource_netbox_config_context.go │ ├── resource_netbox_config_context_test.go │ ├── resource_netbox_config_template.go │ ├── resource_netbox_config_template_test.go │ ├── resource_netbox_contact.go │ ├── resource_netbox_contact_assignment.go │ ├── resource_netbox_contact_assignment_test.go │ ├── resource_netbox_contact_group.go │ ├── resource_netbox_contact_group_test.go │ ├── resource_netbox_contact_role.go │ ├── resource_netbox_contact_role_test.go │ ├── resource_netbox_contact_test.go │ ├── resource_netbox_custom_field.go │ ├── resource_netbox_custom_field_choice_set.go │ ├── resource_netbox_custom_field_choice_set_test.go │ ├── resource_netbox_custom_field_test.go │ ├── resource_netbox_device.go │ ├── resource_netbox_device_bay.go │ ├── resource_netbox_device_bay_template.go │ ├── resource_netbox_device_bay_template_test.go │ ├── resource_netbox_device_bay_test.go │ ├── resource_netbox_device_console_port.go │ ├── resource_netbox_device_console_port_test.go │ ├── resource_netbox_device_console_server_port.go │ ├── resource_netbox_device_console_server_port_test.go │ ├── resource_netbox_device_front_port.go │ ├── resource_netbox_device_front_port_test.go │ ├── resource_netbox_device_interface.go │ ├── resource_netbox_device_interface_test.go │ ├── resource_netbox_device_module_bay.go │ ├── resource_netbox_device_module_bay_test.go │ ├── resource_netbox_device_power_feed.go │ ├── resource_netbox_device_power_feed_test.go │ ├── resource_netbox_device_power_outlet.go │ ├── resource_netbox_device_power_outlet_test.go │ ├── resource_netbox_device_power_port.go │ ├── resource_netbox_device_power_port_test.go │ ├── resource_netbox_device_primary_ip.go │ ├── resource_netbox_device_primary_ip_test.go │ ├── resource_netbox_device_rear_port.go │ ├── resource_netbox_device_rear_port_test.go │ ├── resource_netbox_device_role.go │ ├── resource_netbox_device_role_test.go │ ├── resource_netbox_device_test.go │ ├── resource_netbox_device_type.go │ ├── resource_netbox_device_type_test.go │ ├── resource_netbox_event_rule.go │ ├── resource_netbox_event_rule_test.go │ ├── resource_netbox_group.go │ ├── resource_netbox_group_test.go │ ├── resource_netbox_interface.go │ ├── resource_netbox_interface_template.go │ ├── resource_netbox_interface_template_test.go │ ├── resource_netbox_interface_test.go │ ├── resource_netbox_inventory_item.go │ ├── resource_netbox_inventory_item_role.go │ ├── resource_netbox_inventory_item_role_test.go │ ├── resource_netbox_inventory_item_test.go │ ├── resource_netbox_ip_address.go │ ├── resource_netbox_ip_address_test.go │ ├── resource_netbox_ip_range.go │ ├── resource_netbox_ip_range_test.go │ ├── resource_netbox_ipam_role.go │ ├── resource_netbox_ipam_role_test.go │ ├── resource_netbox_location.go │ ├── resource_netbox_location_test.go │ ├── resource_netbox_mac_address.go │ ├── resource_netbox_mac_address_test.go │ ├── resource_netbox_manufacturer.go │ ├── resource_netbox_manufacturer_test.go │ ├── resource_netbox_module.go │ ├── resource_netbox_module_test.go │ ├── resource_netbox_module_type.go │ ├── resource_netbox_module_type_test.go │ ├── resource_netbox_permission.go │ ├── resource_netbox_permission_test.go │ ├── resource_netbox_platform.go │ ├── resource_netbox_platform_test.go │ ├── resource_netbox_power_panel.go │ ├── resource_netbox_power_panel_test.go │ ├── resource_netbox_prefix.go │ ├── resource_netbox_prefix_test.go │ ├── resource_netbox_primary_ip.go │ ├── resource_netbox_primary_ip_test.go │ ├── resource_netbox_rack.go │ ├── resource_netbox_rack_reservation.go │ ├── resource_netbox_rack_reservation_test.go │ ├── resource_netbox_rack_role.go │ ├── resource_netbox_rack_role_test.go │ ├── resource_netbox_rack_test.go │ ├── resource_netbox_rack_type.go │ ├── resource_netbox_rack_type_test.go │ ├── resource_netbox_region.go │ ├── resource_netbox_region_test.go │ ├── resource_netbox_rir.go │ ├── resource_netbox_rir_test.go │ ├── resource_netbox_route_target.go │ ├── resource_netbox_route_target_test.go │ ├── resource_netbox_service.go │ ├── resource_netbox_service_test.go │ ├── resource_netbox_site.go │ ├── resource_netbox_site_group.go │ ├── resource_netbox_site_group_test.go │ ├── resource_netbox_site_test.go │ ├── resource_netbox_tag.go │ ├── resource_netbox_tag_test.go │ ├── resource_netbox_tenant.go │ ├── resource_netbox_tenant_group.go │ ├── resource_netbox_tenant_group_test.go │ ├── resource_netbox_tenant_test.go │ ├── resource_netbox_token.go │ ├── resource_netbox_token_test.go │ ├── resource_netbox_user.go │ ├── resource_netbox_user_test.go │ ├── resource_netbox_virtual_chassis.go │ ├── resource_netbox_virtual_chassis_test.go │ ├── resource_netbox_virtual_disk.go │ ├── resource_netbox_virtual_disk_migrate_v0.go │ ├── resource_netbox_virtual_disk_test.go │ ├── resource_netbox_virtual_machine.go │ ├── resource_netbox_virtual_machine_migrate_v0.go │ ├── resource_netbox_virtual_machine_migrate_v0_test.go │ ├── resource_netbox_virtual_machine_migrate_v1.go │ ├── resource_netbox_virtual_machine_migrate_v1_test.go │ ├── resource_netbox_virtual_machine_test.go │ ├── resource_netbox_vlan.go │ ├── resource_netbox_vlan_group.go │ ├── resource_netbox_vlan_group_test.go │ ├── resource_netbox_vlan_test.go │ ├── resource_netbox_vpn_tunnel.go │ ├── resource_netbox_vpn_tunnel_group.go │ ├── resource_netbox_vpn_tunnel_group_test.go │ ├── resource_netbox_vpn_tunnel_termination.go │ ├── resource_netbox_vpn_tunnel_termination_test.go │ ├── resource_netbox_vpn_tunnel_test.go │ ├── resource_netbox_vrf.go │ ├── resource_netbox_vrf_test.go │ ├── resource_netbox_webhook.go │ ├── resource_netbox_webhook_test.go │ ├── resource_netbox_wireless_helpers.go │ ├── resource_netbox_wireless_lan.go │ ├── resource_netbox_wireless_lan_group.go │ ├── resource_netbox_wireless_lan_group_test.go │ ├── resource_netbox_wireless_lan_test.go │ ├── slug_test.go │ ├── slugs.go │ ├── tags.go │ ├── tags_test.go │ ├── util.go │ ├── util_test.go │ └── validation.go ├── scripts/ │ ├── allowed_subcategories.sh │ └── ensure_docs_examples.sh ├── templates/ │ ├── index.md.tmpl │ └── resources/ │ ├── available_ip_address.md.tmpl │ ├── ip_address.md.tmpl │ └── mac_address.md.tmpl └── tools/ └── tools.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ # Code of Conduct HashiCorp Community Guidelines apply to you when interacting with the community here on GitHub and contributing code. Please read the full text at ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ # Issue Reporting Guide Hi there, Thank you for opening an issue. Please note that we try to keep the Terraform issue tracker reserved for bug reports and feature requests. For general usage questions, please see: . ## Terraform Version Run `terraform -v` to show the version. If you are not running the latest version of Terraform, please upgrade because your issue may have already been fixed. ## Affected Resource(s) Please list the resources as a list, for example: - opc_instance - opc_storage_volume If this issue appears to affect multiple resources, it may be an issue with Terraform's core, so please mention this. ## Terraform Configuration Files ```hcl # Copy-paste your Terraform configurations here - for large Terraform configs, # please use a service like Dropbox and share a link to the ZIP file. For # security, you can also encrypt the files using our GPG public key. ``` ## Debug Output Please provide a link to a GitHub Gist containing the complete debug output: . Please do NOT paste the debug output in the issue; just paste a link to the Gist. ## Panic Output If Terraform produced a panic, please provide a link to a GitHub Gist containing the output of the `crash.log`. ## Expected Behavior What should have happened? ## Actual Behavior What actually happened? ## Steps to Reproduce Please list the steps required to reproduce the issue, for example: 1. `terraform apply` ## Important Factoids Are there anything atypical about your accounts that we should know? For example: Running in EC2 Classic? Custom version of OpenStack? Tight ACLs? ## References Are there any other GitHub issues (open or closed) or Pull Requests that should be linked here? For example: - GH-1234 ================================================ FILE: .github/allowed-subcategories.txt ================================================ Authentication Circuits Data Center Inventory Management (DCIM) Extras IP Address Management (IPAM) Tenancy Virtualization VPN Tunnels Wireless ================================================ FILE: .github/dependabot.yml ================================================ --- version: 2 updates: - package-ecosystem: gomod directory: "/" schedule: interval: daily time: "04:00" open-pull-requests-limit: 10 - package-ecosystem: github-actions directory: "/" schedule: interval: daily time: "04:00" open-pull-requests-limit: 10 ================================================ FILE: .github/workflows/check-allowed-subcategories.yml ================================================ --- name: check-allowed-subcategories on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v6 - name: check-allowed-subcategories run: bash scripts/allowed_subcategories.sh ================================================ FILE: .github/workflows/ci-testing.yml ================================================ --- name: ci-testing on: push: branches: - master pull_request: branches: - master workflow_dispatch: jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: test run: make test testacc: runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: netbox-version: - v4.3.0 # - v4.3.1 # with this version, we get {"scope":["Please select a site."]} for prefixes - v4.3.2 - v4.3.3 - v4.3.4 - v4.3.5 - v4.3.6 - v4.3.7 - v4.4.0 - v4.4.1 - v4.4.2 # - v4.4.3 # this version cannot be spun up for testing due to TypeError: issubclass() arg 1 must be a class - v4.4.4 - v4.4.5 - v4.4.6 - v4.4.7 - v4.4.8 - v4.4.9 - v4.4.10 steps: - uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: testacc run: make -e testacc env: NETBOX_VERSION: ${{ matrix.netbox-version }} ================================================ FILE: .github/workflows/ensure-docs-examples.yml ================================================ --- name: ensure-docs-examples on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v6 - name: ensure-docs-examples run: bash scripts/ensure_docs_examples.sh ================================================ FILE: .github/workflows/golangci-lint.yml ================================================ --- name: linting on: push: branches: - master pull_request: branches: - master permissions: contents: read # Optional: allow read access to pull request. Use with `only-new-issues` option. # pull-requests: read jobs: golangci: name: golangci runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod cache: false - name: golangci-lint uses: golangci/golangci-lint-action@v9.2.0 with: version: v2.11 doc-check: name: docs check runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod - run: | make docs git add --intent-to-add --all git diff --exit-code || (echo "::error title=Doc generation changes detected::Please update the docs and commit them to the PR" && exit 1) ================================================ FILE: .github/workflows/pre-commit.yml ================================================ --- name: pre-commit on: pull_request: push: branches: [master] permissions: read-all jobs: pre-commit: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v6 - name: Setup pre-commit run: python -m pip install pre-commit - name: Run pre-commit checks uses: pre-commit/action@v3.0.1 ================================================ FILE: .github/workflows/release.yml ================================================ --- # This GitHub action can publish assets for release when a tag is created. # Currently its setup to run on any tag that matches the pattern "v*" (ie. v0.1.0). # # This uses an action (paultyng/ghaction-import-gpg) that assumes you set your # private key in the `GPG_PRIVATE_KEY` secret and passphrase in the `PASSPHRASE` # secret. If you would rather own your own GPG handling, please fork this action # or use an alternative one for key handling. # # You will need to pass the `--batch` flag to `gpg` in your signing step # in `goreleaser` to indicate this is being used in a non-interactive mode. # name: release on: push: tags: - "v*" jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Unshallow run: git fetch --prune --unshallow - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Import GPG key id: import_gpg uses: crazy-max/ghaction-import-gpg@v7.0.0 with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.PASSPHRASE }} - name: Run GoReleaser uses: goreleaser/goreleaser-action@v7.2.1 with: version: latest args: release --clean env: GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ *.dll *.exe .envrc .DS_Store example.tf terraform.tfplan terraform.tfstate bin/ dist/ modules-dev/ /pkg/ website/.vagrant website/.bundle website/build website/node_modules .vagrant/ *.backup ./*.tfstate .terraform/ *.log *.bak *~ .*.swp .idea *.iml *.test *.iml website/vendor # Test exclusions !command/test-fixtures/**/*.tfstate !command/test-fixtures/**/.terraform/ # Keep windows files with windows line endings *.winfile eol=crlf # Locally compiled provider terraform-provider-netbox ================================================ FILE: .golangci.yml ================================================ --- version: "2" linters: enable: - staticcheck - whitespace disable: - errcheck exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling paths: - third_party$ - builtin$ - examples$ formatters: exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ ================================================ FILE: .goreleaser.yml ================================================ --- # Visit https://goreleaser.com for documentation on how to customize this # behavior. version: 2 before: hooks: # this is just an example and not a requirement for provider building/publishing - go mod tidy builds: - env: # goreleaser does not work with CGO, it could also complicate # usage by users in CI/CD systems like Terraform Cloud where # they are unable to install libraries. - CGO_ENABLED=0 mod_timestamp: "{{ .CommitTimestamp }}" flags: - -trimpath ldflags: - "-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}" goos: # - freebsd - windows - linux - darwin goarch: - amd64 - "386" # - arm - arm64 ignore: - goos: darwin goarch: "386" binary: "{{ .ProjectName }}_v{{ .Version }}" archives: - format: zip name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" checksum: name_template: "{{ .ProjectName }}_{{ .Version }}_SHA256SUMS" algorithm: sha256 signs: - artifacts: checksum args: # if you are using this is a GitHub action or some other automated pipeline, you # need to pass the batch flag to indicate its not interactive. - "--batch" - "--local-user" - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key - "--output" - "${signature}" - "--detach-sign" - "${artifact}" release: # If you want to manually examine the release before its live, uncomment this line: # draft: true changelog: disable: true ================================================ FILE: .markdownlint.yaml ================================================ --- MD013: false ================================================ FILE: .pre-commit-config.yaml ================================================ --- default_stages: - commit default_install_hook_types: - commit-msg - pre-commit - prepare-commit-msg exclude: "^docs/|^CHANGELOG.md$" repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: "v4.5.0" hooks: - id: check-added-large-files - id: check-case-conflict - id: check-executables-have-shebangs - id: check-json - id: check-merge-conflict - id: check-shebang-scripts-are-executable - id: check-symlinks - id: check-toml - id: check-vcs-permalinks - id: check-xml - id: check-yaml - id: destroyed-symlinks - id: detect-private-key - id: end-of-file-fixer - id: fix-byte-order-marker - id: forbid-new-submodules - id: trailing-whitespace - repo: https://github.com/pre-commit/mirrors-prettier rev: "v3.1.0" hooks: - id: prettier exclude: "^project/" - repo: https://github.com/igorshubovych/markdownlint-cli rev: "v0.38.0" hooks: - id: markdownlint-fix name: markdownlint - repo: https://github.com/adrienverge/yamllint rev: "v1.33.0" hooks: - id: yamllint entry: yamllint --strict - repo: https://github.com/pecigonzalo/pre-commit-shfmt rev: "v2.1.0" hooks: - id: shell-fmt-go - repo: https://github.com/shellcheck-py/shellcheck-py rev: "v0.9.0.6" hooks: - id: shellcheck - repo: https://github.com/rhysd/actionlint rev: "v1.6.26" hooks: - id: actionlint ================================================ FILE: .prettierignore ================================================ # ignore generated documentation docs/ ================================================ FILE: .terraform-registry ================================================ Request: change owner to fbreckle Registry Link: https://registry.terraform.io/providers/e-breuninger/netbox Request by: fabian.breckle@breuninger.de ================================================ FILE: .yamllint.yaml ================================================ --- extends: default rules: comments: min-spaces-from-content: 1 line-length: disable truthy: allowed-values: ["true", "false"] check-keys: false comments-indentation: ignore: | .goreleaser.yml ================================================ FILE: CHANGELOG.md ================================================ ## 5.3.0 (April 9th, 2026) ENHANCEMENTS * provider: Now supports using [v2 API tokens](https://netboxlabs.com/docs/netbox/integrations/rest-api/#authenticating-to-the-api) ([#862](https://github.com/e-breuninger/terraform-provider-netbox/pull/862) by [@MrKeiKun](https://github.com/MrKeiKun)) Note that this is a NetBox v4.5 feature. This provider does not officially support v4.5 versions yet. * **New Resource:** `netbox_wireless_lan` ([#859](https://github.com/e-breuninger/terraform-provider-netbox/pull/859) by [@JonasKop](https://github.com/JonasKop)) * **New Resource:** `netbox_wireless_lan_group` ([#859](https://github.com/e-breuninger/terraform-provider-netbox/pull/859) by [@JonasKop](https://github.com/JonasKop)) * resource/netbox_virtual_machine: Support auto-calculation of `disk_size_mb` attribute ([#854](https://github.com/e-breuninger/terraform-provider-netbox/pull/854) by [@keshy7](https://github.com/keshy7)) * resource/netbox_ip_range: Add `custom_fields` attribute ([#842](https://github.com/e-breuninger/terraform-provider-netbox/pull/842) by [@jonas-sjodin-stegra](https://github.com/jonas-sjodin-stegra)) * resource/netbox_available_ip_address: Add `custom_fields` attribute ([#841](https://github.com/e-breuninger/terraform-provider-netbox/pull/841) by [@jonas-sjodin-stegra](https://github.com/jonas-sjodin-stegra)) * resource/netbox_interface: Add `bridge_interface_id` attribute ([#856](https://github.com/e-breuninger/terraform-provider-netbox/pull/856) by [@mraerino](https://github.com/mraerino)) * data-source/netbox_ip_addresses: Add `custom_fields` attribute ([#848](https://github.com/e-breuninger/terraform-provider-netbox/pull/848) by [@jonas-sjodin-stegra](https://github.com/jonas-sjodin-stegra)) * data-source/netbox_vlans: Add `id` attribute ([#662](https://github.com/e-breuninger/terraform-provider-netbox/pull/662) by [@rypti](https://github.com/rypti)) * resource/netbox_cable: Add `usb` as option for `type` attribute ([#858](https://github.com/e-breuninger/terraform-provider-netbox/pull/858) by [@JonasKop](https://github.com/JonasKop)) BUG FIXES * provider: use better error checking in many read functions ([#864](https://github.com/e-breuninger/terraform-provider-netbox/pull/864) by [@MrKeiKun](https://github.com/MrKeiKun)) * resource/netbox_token: Handle `expires` field correctly ([#853](https://github.com/e-breuninger/terraform-provider-netbox/pull/853) by [@Oceaneyes123](https://github.com/Oceaneyes123)) ## 5.2.1 (March 12th, 2026) ENHANCEMENTS * All plural data sources now use pagination ([#846](https://github.com/e-breuninger/terraform-provider-netbox/pull/846) by [@mcanevet](https://github.com/mcanevet)) * Ignore unset custom fields ([#844](https://github.com/e-breuninger/terraform-provider-netbox/pull/844) by [@mcanevet](https://github.com/mcanevet)) ## 5.2.0 (March 11th, 2026) ENHANCEMENTS * **New Data Source:** `netbox_vlan_groups` ([#787](https://github.com/e-breuninger/terraform-provider-netbox/pull/787) by [@hp197](https://github.com/hp197)) * data-source/netbox_vlan_group: Add `id` attribute ([#787](https://github.com/e-breuninger/terraform-provider-netbox/pull/787) by [@hp197](https://github.com/hp197)) * data-source/netbox_ip_range: Add many missing filters ([#827](https://github.com/e-breuninger/terraform-provider-netbox/pull/827) by [@oliverwiegers](https://github.com/oliverwiegers)) BUG FIXES * resource/netbox_available_ip_address: Force replacement when `prefix_id` or `ip_range_id` changes ([#837](https://github.com/e-breuninger/terraform-provider-netbox/pull/837) by [@bl0way](https://github.com/bl0way)) ## 5.1.0 (January 26th, 2026) ENHANCEMENTS * **New Data Source:** `netbox_device_render_config` ([#816](https://github.com/e-breuninger/terraform-provider-netbox/pull/816) by [@lgogolin](https://github.com/lgogolin)) * **New Data Source:** `netbox_clusters` ([#817](https://github.com/e-breuninger/terraform-provider-netbox/pull/817) by [@sasler](https://github.com/sasler)) * data-source/netbox_prefix: Add support for filtering netbox_prefix by custom fields ([#819](https://github.com/e-breuninger/terraform-provider-netbox/pull/819) by [@christopher-svensson-stegra](https://github.com/christopher-svensson-stegra)) BUG FIXES * Fix an error where datasources with boolean custom fields returned 'expected type string, got unconvertible type bool' ([#814](https://github.com/e-breuninger/terraform-provider-netbox/pull/814) by [@mcanevet](https://github.com/mcanevet)) ## 5.0.1 Ho-Ho-Ho (December 25th, 2025) ENHANCEMENTS * resource/netbox_circuit: Add `description` attribute ([#766](https://github.com/e-breuninger/terraform-provider-netbox/pull/766) by [@jakegroves](https://github.com/jakegroves)) * resource/netbox_circuit_provider: Add `description` attribute ([#766](https://github.com/e-breuninger/terraform-provider-netbox/pull/766) by [@jakegroves](https://github.com/jakegroves)) * resource/netbox_circuit_termination: Add `description` attribute ([#766](https://github.com/e-breuninger/terraform-provider-netbox/pull/766) by [@jakegroves](https://github.com/jakegroves)) * resource/netbox_circuit_type: Add `description` attribute ([#766](https://github.com/e-breuninger/terraform-provider-netbox/pull/766) by [@jakegroves](https://github.com/jakegroves)) * data-source/netbox_site: Add `physical_address` attribute to result ([#781](https://github.com/e-breuninger/terraform-provider-netbox/pull/781) by [@sytmd](https://github.com/sytmd)) * data_source/netbox_ip_addresses: Add `description` attribute to results ([#784](https://github.com/e-breuninger/terraform-provider-netbox/pull/784) by [@christopher-svensson-stegra](https://github.com/christopher-svensson-stegra)) * resource/netbox_event_rule: Allow `script` action type ([#799](https://github.com/e-breuninger/terraform-provider-netbox/pull/799) by [@jorgenspange](https://github.com/jorgenspange)) * data-source/netbox_prefixes: Add `custom_fields` to results ([#779](https://github.com/e-breuninger/terraform-provider-netbox/pull/779) by [@lgogolin](https://github.com/lgogolin)) * resource/netbox_webhook: Add `ca_file_path` attribute ([#797](https://github.com/e-breuninger/terraform-provider-netbox/pull/797) by [@jorgenspange](https://github.com/jorgenspange)) * data-source/netbox_virtual_machines: Add `platform_name` to results ([#810](https://github.com/e-breuninger/terraform-provider-netbox/pull/810) by [@Slartibartfast1](https://github.com/Slartibartfast1)) BUG FIXES * resource/netbox_user: Fix date_joined datetime out of range error ([#796](https://github.com/e-breuninger/terraform-provider-netbox/pull/796) by [@luispcoutinho](https://github.com/luispcoutinho)) * resource/netbox_ip_address: Fix IP Address Assignment being removed despite being in ignore_changes ([#790](https://github.com/e-breuninger/terraform-provider-netbox/pull/790) by [@chuyang-wang-dev](https://github.com/chuyang-wang-dev)) ## 5.0.0 (September 12th, 2025) ENHANCEMENTS provider: Now supports NetBox 4.3.x and 4.4.0 ## 4.3.1 (September 12th, 2025) ENHANCEMENTS * provider: Allow setting a CA file in the provider ([#765](https://github.com/e-breuninger/terraform-provider-netbox/pull/765) by [@bodgit](https://github.com/bodgit)) ## 4.3.0 (September 11th, 2025) ENHANCEMENTS * **New Data Source:** `netbox_rir` ([#759](https://github.com/e-breuninger/terraform-provider-netbox/pull/759) by [@MrKeiKun](https://github.com/MrKeiKun)) * data-source/netbox_prefixes: Allow filtering by `description` ([#756](https://github.com/e-breuninger/terraform-provider-netbox/pull/756) by [@jonas-sjodin-h2gs](https://github.com/jonas-sjodin-h2gs)) * resource/netbox_contact: Add `link` and `description` attributes ([#753](https://github.com/e-breuninger/terraform-provider-netbox/pull/753) by [@hikhvar](https://github.com/hikhvar)) * resource/netbox_ip_range: Add `size` attribute ([#761](https://github.com/e-breuninger/terraform-provider-netbox/pull/761) by [@MrKeiKun](https://github.com/MrKeiKun)) * data-source/netbox_device_interfaces: Add `type` attribute to results ([#764](https://github.com/e-breuninger/terraform-provider-netbox/pull/764) by [@xvzf](https://github.com/xvzf)) BUG FIXES * resource/netbox_available_ip_address: Prevent provider crash when no IP is available ([#760](https://github.com/e-breuninger/terraform-provider-netbox/pull/760) by [@MrKeiKun](https://github.com/MrKeiKun)) ## 4.2.0 (August 20th, 2025) ENHANCEMENTS * **New Resource:** `netbox_device_bay` ([#734](https://github.com/e-breuninger/terraform-provider-netbox/pull/734) by [@kyleprice](https://github.com/kyleprice)) * **New Resource:** `netbox_device_bay_template` ([#734](https://github.com/e-breuninger/terraform-provider-netbox/pull/734) by [@kyleprice](https://github.com/kyleprice)) * **New Data Source:** `netbox_ip_address` ([#743](https://github.com/e-breuninger/terraform-provider-netbox/pull/743) by [@c24holdingit](https://github.com/c24holdingit)) * resource/netbox_device_type: Add `subdevice_role` field ([#734](https://github.com/e-breuninger/terraform-provider-netbox/pull/734) by [@kyleprice](https://github.com/kyleprice)) * data-source/netbox_device_type: Add `subdevice_role` field ([#734](https://github.com/e-breuninger/terraform-provider-netbox/pull/734) by [@kyleprice](https://github.com/kyleprice)) * resource/netbox_token: Add `expires` field ([#742](https://github.com/e-breuninger/terraform-provider-netbox/pull/742) by [@thomas-VIGINUM](https://github.com/thomas-VIGINUM)) ## 4.1.0 (July 2nd, 2025) ENHANCEMENTS * **New Data Source:** `netbox_virtual_disk` ([#731](https://github.com/e-breuninger/terraform-provider-netbox/pull/731) by [@lgogolin](https://github.com/lgogolin)) * resource/netbox_available_prefix: Add `custom_fields` attribute ([#679](https://github.com/e-breuninger/terraform-provider-netbox/pull/679) by [@rs-hock](https://github.com/rs-hock)) * data-source/netbox_devices: Add `oob_ip` attribute to output([#705](https://github.com/e-breuninger/terraform-provider-netbox/pull/705) by [@sempervictus](https://github.com/sempervictus)) BUG FIXES * provider: Now properly detects netbox-docker versions ## 4.0.0 (June 12th, 2025) **BREAKING CHANGES** NetBox 4.2 came with some breaking changes and these are reflected in the provider. * resource/netbox_device_interface: Make `mac_address` attribute read-only * resource/netbox_device_interface: Add read-only `mac_addresses` attribute ENHANCEMENTS provider: Now supports NetBox 4.2.x * **New Resource:** `netbox_mac_address` * resource/netbox_cluster: Add `location_id`, `site_group_id`, `region_id`, `scope_id` and `scope_type` attributes * resource/netbox_prefix: Add `location_id`, `site_group_id` and `region_id` attributes * resource/netbox_device_interface: Allow `q-in-q` value in `mode` attribute * resource/netbox_circuit_termination: Add `location_id`, `site_group_id`, `region_id`, `site_id` and `provider_network_id` attributes BUG FIXES * resource/netbox_location: Mark `site_id` as required ## 3.11.1 (June 12th, 2025) ENHANCEMENTS * **New Data Source:** `netbox_device_power_ports` ([#721](https://github.com/e-breuninger/terraform-provider-netbox/pull/721) by [@mraerino](https://github.com/mraerino)) * **New Resource:** `netbox_available_vlan` ([#717](https://github.com/e-breuninger/terraform-provider-netbox/pull/717) by [@MacherelR](https://github.com/MacherelR)) ## 3.11.0 (May 27th, 2025) * provider: Add `default_tags` attribute ([#711](https://github.com/e-breuninger/terraform-provider-netbox/pull/711) by [@mraerino](https://github.com/mraerino)) ENHANCEMENTS * **New Data Source:** `netbox_ip_ranges` ([#719](https://github.com/e-breuninger/terraform-provider-netbox/pull/719) by [@waza-ari](https://github.com/waza-ari)) * resource/netbox_device: Allow `decommissioning` value in `status` attribute ([#767](https://github.com/e-breuninger/terraform-provider-netbox/pull/767) by [@sboschman](https://github.com/sboschman)) * resource/netbox_site: Add `comments` attribute ([#710](https://github.com/e-breuninger/terraform-provider-netbox/pull/710) by [@mraerino](https://github.com/mraerino)) * resource/netbox_user: Add `email`, `first_name` and `last_name` attributes ([#693](https://github.com/e-breuninger/terraform-provider-netbox/pull/693) by [@mraerino](https://github.com/mraerino)) * data-source/netbox_device_interfaces: Add `limit` attribute ([#695](https://github.com/e-breuninger/terraform-provider-netbox/pull/695) by [@sempervictus](https://github.com/sempervictus)) * resource/netbox_group: Add `description` attribute ([#694](https://github.com/e-breuninger/terraform-provider-netbox/pull/694) by [@mraerino](https://github.com/mraerino)) * resource/netbox_location: Add `facility` attribute ([#718](https://github.com/e-breuninger/terraform-provider-netbox/pull/718) by [@mraerino](https://github.com/mraerino)) * data-source/netbox_locations: Add `facility` attribute ([#718](https://github.com/e-breuninger/terraform-provider-netbox/pull/718) by [@mraerino](https://github.com/mraerino)) ## 3.10.0 (January 9th, 2025) **BREAKING CHANGES** NetBox 4.1 came with some breaking changes and these are reflected in the provider. * resource/netbox_event_rule: Replace `trigger_on_X` attributes with `event_types` list attribute * resource/netbox_racks: Remove `type` attribute * resource/netbox_virtual_disk: Change `size_gb` attribute to `size_mb` * resource/netbox_virtual_machine: Change `disk_size_gb` attribute to `disk_size_mb` * resource/netbox_vlan_group: Remove `min_vid` and `max_vid` attributes in favor of `vid_ranges` attribute ENHANCEMENTS provider: Now supports NetBox 4.1.x * **New Resource:** `netbox_rack_type` * resource/netbox_racks: Add `form_factor` attribute ## 3.9.3 (January 9th, 2025) ENHANCEMENTS * resource/netbox_custom_field: Add `default` attribute ([#647](https://github.com/e-breuninger/terraform-provider-netbox/pull/647) by [@jenxie](https://github.com/jenxie)) * data-source/netbox_vlans: Allow filtering by `site_id` ([#654](https://github.com/e-breuninger/terraform-provider-netbox/pull/654) by [@i-am-smolli](https://github.com/i-am-smolli)) * data-source/netbox_vlan_group: Make `name` and `slug` definitions optional when `scope_type` is defined ([#657](https://github.com/e-breuninger/terraform-provider-netbox/pull/657) by [@TGM](https://github.com/TGM)) * resource/netbox_asn: Add `description` and `comments` attributes ([#664](https://github.com/e-breuninger/terraform-provider-netbox/pull/664) by [@ymylei](https://github.com/ymylei)) * data-source/netbox_prefix: Allow searching by `tenant_id` and `status` ([#666](https://github.com/e-breuninger/terraform-provider-netbox/pull/666) by [@xabinapal](https://github.com/xabinapal)) * data-source/netbox_prefixes: Allow searching by `tenant_id` ([#666](https://github.com/e-breuninger/terraform-provider-netbox/pull/666) by [@xabinapal](https://github.com/xabinapal)) ## 3.9.2 (October 10th, 2024) ENHANCEMENTS * provider: Include 4.0.11 in supported versions * resource/netbox_ip_address: Add `custom_fields` attribute ([#638](https://github.com/e-breuninger/terraform-provider-netbox/pull/638) by [@greatman](https://github.com/greatman)) * resource/netbox_service: Add `device_id`, `description` and `tags` attributes ([#637](https://github.com/e-breuninger/terraform-provider-netbox/pull/637) by [@STANIAC](https://github.com/STANIAC)) * data-source/netbox_vrf: Fix a bug where `tenant_id` was not used ([#643](https://github.com/e-breuninger/terraform-provider-netbox/pull/643) by [@c3JpbmkK](https://github.com/c3JpbmkK)) ## 3.9.1 (September 2nd, 2024) ENHANCEMENTS provider: Include 4.0.9 and 4.0.10 in supported versions ## 3.9.0 (August 10th, 2024) ENHANCEMENTS provider: Now is tested against (= supports) the NetBox 4.0.x range ## 3.8.9 (July 31st, 2024) ENHANCEMENTS * data-source/netbox_virtual_machines: Add `status` attribute ([#612](https://github.com/e-breuninger/terraform-provider-netbox/pull/612) by [@twink0r](https://github.com/twink0r)) * data-source/netbox_vlans: Add `tag_ids` attribute ([#621](https://github.com/e-breuninger/terraform-provider-netbox/pull/621) by [@Piethan](https://github.com/Piethan)) * data-source/netbox_vlans: Add `status` attribute ([#622](https://github.com/e-breuninger/terraform-provider-netbox/pull/622) by [@Piethan](https://github.com/Piethan)) * data-source/netbox_devices: Add `device_type_id` attribute ([#624](https://github.com/e-breuninger/terraform-provider-netbox/pull/624) by [@Piethan](https://github.com/Piethan)) ## 3.8.8 (July 22th, 2024) ENHANCEMENTS * data-source/netbox_prefixes: Add `contains` and `site_id` attributes ([#617](https://github.com/e-breuninger/terraform-provider-netbox/pull/617) by [@tagur87](https://github.com/tagur87)) BUG FIXES * resource/netbox_vpn_tunnel_termination: Fix a interface conversion panic when updating tunnel terminations ([#616](https://github.com/e-breuninger/terraform-provider-netbox/pull/616) by [@mraerino](https://github.com/mraerino)) ## 3.8.7 (June 28th, 2024) ENHANCEMENTS * **New Resource:** `netbox_interface_template` ([#588](https://github.com/e-breuninger/terraform-provider-netbox/pull/588) by [@thibaultbustarret-ovhcloud](https://github.com/thibaultbustarret-ovhcloud)) * **New Resource:** `netbox_config_context` ([#590](https://github.com/e-breuninger/terraform-provider-netbox/pull/590) by [@diogenxs](https://github.com/diogenxs)) * **New Data Source:** `netbox_config_context` ([#590](https://github.com/e-breuninger/terraform-provider-netbox/pull/590) by [@diogenxs](https://github.com/diogenxs)) * data-source/netbox_devices: Add `config_context` and `local_context_data` attributes ([#590](https://github.com/e-breuninger/terraform-provider-netbox/pull/590) by [@diogenxs](https://github.com/diogenxs)) * resource/netbox_device_interface: Add `label` attribute ([#605](https://github.com/e-breuninger/terraform-provider-netbox/pull/605) by [@thibaultbustarret-ovhcloud](https://github.com/thibaultbustarret-ovhcloud)) * **New Resource:** `netbox_config_template` ([#604](https://github.com/e-breuninger/terraform-provider-netbox/pull/604) by [@thibaultbustarret-ovhcloud](https://github.com/thibaultbustarret-ovhcloud)) * resource/netbox_device: Add `config_template_id` attribute ([#604](https://github.com/e-breuninger/terraform-provider-netbox/pull/604) by [@thibaultbustarret-ovhcloud](https://github.com/thibaultbustarret-ovhcloud)) * data-source/netbox_prefix: Add `role_id` and `custom_fields` attributes ([#607](https://github.com/e-breuninger/terraform-provider-netbox/pull/607) by [@ad8lmondy](https://github.com/ad8lmondy)) * resource/netbox_platform: Add `manufacturer_id` attribute ([#608](https://github.com/e-breuninger/terraform-provider-netbox/pull/608) by [@ad8lmondy](https://github.com/ad8lmondy)) * data-source/netbox_platform: Add `manufacturer_id` attribute ([#608](https://github.com/e-breuninger/terraform-provider-netbox/pull/608) by [@ad8lmondy](https://github.com/ad8lmondy)) ## 3.8.6 (May 17th, 2024) ENHANCEMENTS * resource/netbox_rir: Add `is_private` attribute ([#594](https://github.com/e-breuninger/terraform-provider-netbox/pull/594) by [@thibaultbustarret-ovhcloud](https://github.com/thibaultbustarret-ovhcloud)) * resource/netbox_vrf: Add `rd` and `enforce_unique` attributes ([#585](https://github.com/e-breuninger/terraform-provider-netbox/pull/585) by [@thibaultbustarret-ovhcloud](https://github.com/thibaultbustarret-ovhcloud)) * **New Resource:** `netbox_group` ([#584](https://github.com/e-breuninger/terraform-provider-netbox/pull/584) by [@thibaultbustarret-ovhcloud](https://github.com/thibaultbustarret-ovhcloud)) * resource/netbox_user: Add `group_ids` attribute ([#584](https://github.com/e-breuninger/terraform-provider-netbox/pull/584) by [@thibaultbustarret-ovhcloud](https://github.com/thibaultbustarret-ovhcloud)) ## 3.8.5 (March 18th, 2024) BUG FIXES * All resources with `slug` attributes now properly allow for up to 100 characters in that attribute ## 3.8.4 (March 11th, 2024) ENHANCEMENTS * data-source/netbox_interfaces: Add `limit` attribute ## 3.8.3 (March 8th, 2024) ENHANCEMENTS * **New Resource:** `netbox_vpn_tunnel_termination` ## 3.8.2 (March 4th, 2024) ENHANCEMENTS * **New Resource:** `netbox_virtual_disk` ([#558](https://github.com/e-breuninger/terraform-provider-netbox/pull/558) by [@Ikke](https://github.com/Ikke)) * resource/netbox_prefix: Add `custom_fields` attribute ([#553](https://github.com/e-breuninger/terraform-provider-netbox/pull/553) by [@nothinux](https://github.com/nothinux)) ## 3.8.1 (February 16th, 2024) ENHANCEMENTS * **New Resource:** `netbox_vpn_tunnel_group` * **New Resource:** `netbox_vpn_tunnel` * data-source/netbox_virtual_machines: Add `platform_slug` attribute * data-source/netbox_locations: Add `parent_id` attribute ([#548](https://github.com/e-breuninger/terraform-provider-netbox/pull/548) by [@GennadySpb](https://github.com/GennadySpb)) * data-source/netbox_location: Add `parent_id` attribute ([#548](https://github.com/e-breuninger/terraform-provider-netbox/pull/548) by [@GennadySpb](https://github.com/GennadySpb)) * resource/netbox_location: Add `parent_id` attribute ([#548](https://github.com/e-breuninger/terraform-provider-netbox/pull/548) by [@GennadySpb](https://github.com/GennadySpb)) * resource/device_type: Add `is_full_depth` attribute ## 3.8.0 (January 30th, 2024) **BREAKING CHANGES** Due to a change in NetBox 3.7's behavior regarding Webhooks and the corresponding changes in the API, the `netbox_webhook` resource might cause problems with NetBox versions older than 3.7.0. For all other resources and data sources, the provider should still perform fine with older NetBox versions. * resource/netbox_webhook: Removed `enabled`, `trigger_on_create`, `trigger_on_update`, `trigger_on_delete`, `content_types` and `conditions` ENHANCEMENTS * provider: Now officially supports NetBox 3.7 * **New Resource:** `netbox_event_rule` ## 3.7.7 (January 30th, 2024) BUG FIXES * resource/netbox_device: Fix Virtual Chassis Master Update Function for Tag Input ([#532](https://github.com/e-breuninger/terraform-provider-netbox/pull/532) by [@adelekanley](https://github.com/adelekanley)) ## 3.7.6 (January 2nd, 2024) ENHANCEMENTS * resource/netbox_webhook: Add `additional_headers` and `conditions` attributes ([#505](https://github.com/e-breuninger/terraform-provider-netbox/pull/505) by [@Ikke](https://github.com/Ikke)) * data-source/netbox_vrfs: Allow filtering by `tag` ([#513](https://github.com/e-breuninger/terraform-provider-netbox/pull/513) by [@sjurtf](https://github.com/sjurtf)) * data-source/netbox_virtual_machines: Allow filtering by `tenant_id` ([#511](https://github.com/e-breuninger/terraform-provider-netbox/pull/511) by [@sjurtf](https://github.com/sjurtf)) * data-source/netbox_ip_addresses: Allow filtering by `tag` ([#510](https://github.com/e-breuninger/terraform-provider-netbox/pull/510) by [@sjurtf](https://github.com/sjurtf)) * data-source/netbox_cluster: Allow filtering by `cluster_group_id` ([#528](https://github.com/e-breuninger/terraform-provider-netbox/pull/528) by [@Ikke](https://github.com/Ikke)) BUG FIXES * resources/netbox_webhook: Fix a bug where JSON encoding would break drift detection ([#505](https://github.com/e-breuninger/terraform-provider-netbox/pull/505) by [@Ikke](https://github.com/Ikke)) * data-source/netbox_site: Mark optionally searchable attributes as `computed` as well ([#520](https://github.com/e-breuninger/terraform-provider-netbox/pull/520) by [@tagur87](https://github.com/tagur87)) ## 3.7.5 (November 27th, 2023) * **New Data Source:** `netbox_locations` ([#503](https://github.com/e-breuninger/terraform-provider-netbox/pull/503) by [@Ikke](https://github.com/Ikke)) ## 3.7.4 (November 22nd, 2023) ENHANCEMENTS * **New Resource:** `netbox_virtual_chassis` ([#497](https://github.com/e-breuninger/terraform-provider-netbox/pull/497) by [@Ikke](https://github.com/Ikke)) * resource/netbox_device: Add `virtual_chassis_id`, `virtual_chassis_master`, `virtual_chassis_position` and `virtual_chassis_priority` attributes ([#500](https://github.com/e-breuninger/terraform-provider-netbox/pull/500) by [@Ikke](https://github.com/Ikke)) ## 3.7.3 (November 3rd, 2023) ENHANCEMENTS * resource/netbox_site: Allow unsetting the `latitude` and `longitude` attributes ([#480](https://github.com/e-breuninger/terraform-provider-netbox/pull/480) by [@haipersuccor02](https://github.com/haipersuccor02)) * **New Data Source:** `netbox_tags` ([#484](https://github.com/e-breuninger/terraform-provider-netbox/pull/484) by [@zeddD1abl0](https://github.com/zeddD1abl0)) * data-source/netbox_ip_addresses: Allow filtering by `parent_prefix` ([#485](https://github.com/e-breuninger/terraform-provider-netbox/pull/485) by [@sjurtf](https://github.com/sjurtf)) * data-source/netbox_devices: Allow filtering by `tags` and `status` ([#491](https://github.com/e-breuninger/terraform-provider-netbox/pull/491) by [@Kenterfie](https://github.com/Kenterfie)) * **New Data Source:** `netbox_available_prefixes` ([#489](https://github.com/e-breuninger/terraform-provider-netbox/pull/489) by [@theochita](https://github.com/theochita)) * resource/netbox_device: Add `local_context_data` attribute ([#493](https://github.com/e-breuninger/terraform-provider-netbox/pull/493) by [@RickyRajinder](https://github.com/RickyRajinder)) ## 3.7.2 (October 10th, 2023) ENHANCEMENTS * data-source/netbox_location: Allow searching by `site_id` ([#482](https://github.com/e-breuninger/terraform-provider-netbox/pull/482) by [@w87x](https://github.com/w87x)) * data-source/netbox_ip_addresses: Allow searching by `role`, `status`, `vrf` and `tenant` ([#479](https://github.com/e-breuninger/terraform-provider-netbox/pull/479) by [@sjurtf](https://github.com/sjurtf)) * data-source/netbox_site: Allow Searching by `facility` ([#483](https://github.com/e-breuninger/terraform-provider-netbox/pull/483) by [@ikke](https://github.com/ikke)) ## 3.7.1 (September 25th, 2023) ENHANCEMENTS * **New Data Source:** `netbox_device_interfaces` ([#476](https://github.com/e-breuninger/terraform-provider-netbox/pull/476) by [@w87x](https://github.com/w87x)) * resource/netbox_token: Add `description` attribute ([#473](https://github.com/e-breuninger/terraform-provider-netbox/pull/473) by [@twink0r](https://github.com/twink0r)) * data-source/netbox_virtual_machines: Include `device_id` and `device_name` attributes in result ([#477](https://github.com/e-breuninger/terraform-provider-netbox/pull/477) by [@zeddD1abl0](https://github.com/zeddD1abl0)) ## 3.7.0 (September 14th, 2023) **BREAKING CHANGES** * resource/netbox_custom_field: Replace `choices` attribute with `choice_set_id` attribute ENHANCEMENTS * provider: Now officially supports NetBox 3.6 * **New Resource:** `netbox_custom_field_choice_set` ## 3.6.2 (September 14th, 2023) FEATURES * **New Data Source:** `netbox_location` ([#467](https://github.com/e-breuninger/terraform-provider-netbox/pull/467) by [@w87x](https://github.com/w87x)) * data-source/netbox_virtual_machines: Allow searching by tag ([#466](https://github.com/e-breuninger/terraform-provider-netbox/pull/466) by [@twink0r](https://github.com/twink0r)) * resource/netbox_device: Add `asset_tag` attribute ([#470](https://github.com/e-breuninger/terraform-provider-netbox/pull/470) by [@bebehei](https://github.com/bebehei)) * resource/netbox_device_interface: Add `speed`, `lag_device_interface_id` and `parent_device_interface_id` attributes ([#469](https://github.com/e-breuninger/terraform-provider-netbox/pull/469) by [@bebehei](https://github.com/bebehei)) BUG FIXES * resource/netbox_custom_field: Allow correct value `json` instead of `JSON` ([#459](https://github.com/e-breuninger/terraform-provider-netbox/pull/459) by [@menselman](https://github.com/menselman)) ## 3.6.1 (August 31th, 2023) FEATURES * **New Resource:** `netbox_webhook` ([#438](https://github.com/e-breuninger/terraform-provider-netbox/pull/438) by [@haipersuccor02](https://github.com/haipersuccor02)) ## 3.6.0 (August 18th, 2023) **BREAKING CHANGES** Due to a change in NetBox 3.5's behavior regarding ASN and the corresponding required changes in the go library that is used in this provider, the `netbox_asn` resource and the `netbox_asns` data source are no longer supported in versions older than 3.5. For all other resources and data sources, the provider should still perform fine with older NetBox versions. ENHANCEMENTS * provider: Now officially supports NetBox 3.5 ## 3.5.5 (August 18th, 2023) ENHANCEMENTS * data-source/netbox_cluster: Allow searching by `id` field and include `custom_fields` attribute in output ([#457](https://github.com/e-breuninger/terraform-provider-netbox/pull/457) by [@fred-clement-91](https://github.com/fred-clement-91)) BUG FIXES * resource/netbox_device_interface: Changing `mac_address` no longer needlessly forces a recreate of the resource ([#454](https://github.com/e-breuninger/terraform-provider-netbox/pull/454) by [@hamzazaman](https://github.com/hamzazaman)) ## 3.5.4 (August 7th, 2023) FEATURES * **New Resource:** `netbox_cable` ([#450](https://github.com/e-breuninger/terraform-provider-netbox/pull/450) by [@joeyberkovitz](https://github.com/joeyberkovitz)) * **New Resource:** `netbox_device_console_port` ([#450](https://github.com/e-breuninger/terraform-provider-netbox/pull/450) by [@joeyberkovitz](https://github.com/joeyberkovitz)) * **New Resource:** `netbox_device_console_server_port` ([#450](https://github.com/e-breuninger/terraform-provider-netbox/pull/450) by [@joeyberkovitz](https://github.com/joeyberkovitz)) * **New Resource:** `netbox_device_power_port` ([#450](https://github.com/e-breuninger/terraform-provider-netbox/pull/450) by [@joeyberkovitz](https://github.com/joeyberkovitz)) * **New Resource:** `netbox_device_power_outlet` ([#450](https://github.com/e-breuninger/terraform-provider-netbox/pull/450) by [@joeyberkovitz](https://github.com/joeyberkovitz)) * **New Resource:** `netbox_device_front_port` ([#450](https://github.com/e-breuninger/terraform-provider-netbox/pull/450) by [@joeyberkovitz](https://github.com/joeyberkovitz)) * **New Resource:** `netbox_device_rear_port` ([#450](https://github.com/e-breuninger/terraform-provider-netbox/pull/450) by [@joeyberkovitz](https://github.com/joeyberkovitz)) * **New Resource:** `netbox_device_module_bay` ([#450](https://github.com/e-breuninger/terraform-provider-netbox/pull/450) by [@joeyberkovitz](https://github.com/joeyberkovitz)) * **New Resource:** `netbox_module` ([#450](https://github.com/e-breuninger/terraform-provider-netbox/pull/450) by [@joeyberkovitz](https://github.com/joeyberkovitz)) * **New Resource:** `netbox_module_type` ([#450](https://github.com/e-breuninger/terraform-provider-netbox/pull/450) by [@joeyberkovitz](https://github.com/joeyberkovitz)) * **New Resource:** `netbox_power_feed` ([#450](https://github.com/e-breuninger/terraform-provider-netbox/pull/450) by [@joeyberkovitz](https://github.com/joeyberkovitz)) * **New Resource:** `netbox_power_panel` ([#450](https://github.com/e-breuninger/terraform-provider-netbox/pull/450) by [@joeyberkovitz](https://github.com/joeyberkovitz)) * **New Resource:** `netbox_inventory_item_role` ([#450](https://github.com/e-breuninger/terraform-provider-netbox/pull/450) by [@joeyberkovitz](https://github.com/joeyberkovitz)) * **New Resource:** `netbox_inventory_item` ([#450](https://github.com/e-breuninger/terraform-provider-netbox/pull/450) by [@joeyberkovitz](https://github.com/joeyberkovitz)) ## 3.5.3 (August 4th, 2023) ENHANCEMENTS * resource/netbox_service: Add `custom_fields` attribute ([#448](https://github.com/e-breuninger/terraform-provider-netbox/pull/448) by [@sebastianreloaded](https://github.com/sebastianreloaded)) * data-source/netbox_site: Allow searching by `id` field BUG FIXES * resource/netbox_interface: Allow setting `enabled` to `false` * resource/netbox_device_interface: Allow setting `enabled` to `false` ## 3.5.2 (August 3rd, 2023) FEATURES * **New Data Source:** `netbox_vrfs` ([#441](https://github.com/e-breuninger/terraform-provider-netbox/pull/441) by [@robvand](https://github.com/robvand)) ENHANCEMENTS * Added `description` attribute to - data-source/netbox_cluster - data-source/netbox_devices - data-source/netbox_virtual_machines - resource/netbox_cluster - resource/netbox_device - resource/virtual_machine ([#401](https://github.com/e-breuninger/terraform-provider-netbox/pull/401) by [@tagur87](https://github.com/tagur87)) * data-source/netbox_devices: Return `tags` attribute * resource/netbox_interface: Ignore drift when providing lowercase MAC addresses ([#446](https://github.com/e-breuninger/terraform-provider-netbox/pull/446) by [@bebehei](https://github.com/bebehei)) * resource/netbox_device_interface: Ignore drift when providing lowercase MAC addresses ([#446](https://github.com/e-breuninger/terraform-provider-netbox/pull/446) by [@bebehei](https://github.com/bebehei)) ## 3.5.1 (July 24th, 2023) BUG FIXES * resource/netbox_ip_address: Use correct attribute when using the `device_interface_id` attribute ([#437](https://github.com/e-breuninger/terraform-provider-netbox/pull/437) by [@switchcorp](https://github.com/switchcorp)) * resource/netbox_primary_ip: Fix a bug where setting a primary IP unsets the `local_context_data` attribute ([#435](https://github.com/e-breuninger/terraform-provider-netbox/pull/435) by [@tagur87](https://github.com/tagur87)) ## 3.5.0 (July 20th, 2023) **BREAKING CHANGES** Historically, this provider primarily handled virtual machines, so when linking a `netbox_ip_address` resource to an interface, the interface was initially assumed to always be a virtual machine interface. In [v3.1.0](https://github.com/e-breuninger/terraform-provider-netbox/commit/76f11292a162d88eb1616d9a5b7d70d986b2db3f), support was added for device interfaces by setting the newly introduced `object_type` attribute, once again defaulting to virtual machine interfaces. The valid values for `object_type` directly reflect the API values of NetBox, which are very unintuitive. In this version, we make the type of connection between IP addresses and interfaces explicit: We introduce two new attributes: `virtual_machine_interface_id` and `device_interface_id` to the `netbox_ip_address` resource. These fields are easier to use and convey their meaning directly to the user. The `object_type` and `interface_id` method is still supported, but `object_type` no longer has a default value and is now mandatory when `interface_id` is used. **Migration guide** In your existing codebase: * replace `interface_id` with `virtual_machine_interface_id` if `object_type` is currently unset or set to `virtualization.vminterface` * replace `interface_id` with `device_interface_id` if `object_type` is currently set to `dcim.interface` ENHANCEMENTS * resource/netbox_ip_address: Add `virtual_machine_interface_id` and `device_interface_id` attributes * resource/netbox_ip_address: Add `slaac` to the list of valid statuses * resource/netbox_ip_address: Add `nat_inside_address_id` and `nat_outside_addresses` attributes BUG FIXES * resource/netbox_permission: Fix perpetual drift when `constraints` is nil ([#432](https://github.com/e-breuninger/terraform-provider-netbox/pull/432) by [@tagur87](https://github.com/tagur87)) ## 3.4.1 (July 19th, 2023) ENHANCEMENTS * resource/netbox_cluster: Add `comments` attribute ([#429](https://github.com/e-breuninger/terraform-provider-netbox/pull/429) by [@edwin-bruurs](https://github.com/edwin-bruurs)) * data-source/netbox_prefix: Add `family` attribute ([#431](https://github.com/e-breuninger/terraform-provider-netbox/pull/431) by [@tagur87](https://github.com/tagur87)) BUG FIXES * resource/netbox_virtual_machine: Fix `local_context_data` attribute ([#430](https://github.com/e-breuninger/terraform-provider-netbox/pull/430) by [@zeddD1abl0](https://github.com/zeddD1abl0)) ## 3.4.0 (July 10th, 2023) ENHANCEMENTS * **New Resource:** `netbox_device_primary_ip` ([#424](https://github.com/e-breuninger/terraform-provider-netbox/pull/424) by [@Ikke](https://github.com/Ikke)) * resource/netbox_virtual_machine: Add `local_context_data` attribute ([#421](https://github.com/e-breuninger/terraform-provider-netbox/pull/421) by [@zeddD1abl0](https://github.com/zeddD1abl0)) BUG FIXES * resource/netbox_primary_ip: Fix a bug where setting the primary ip of a VM unsets the device id ## 3.3.3 (June 28th, 2023) ENHANCEMENTS * **New Data Source:** `netbox_vlans` ([#420](https://github.com/e-breuninger/terraform-provider-netbox/pull/420) by [@danischm](https://github.com/danischm)) * resource/netbox_contact_assignment: Add `priority` attribute ([#418](https://github.com/e-breuninger/terraform-provider-netbox/pull/418) by [@Ikke](https://github.com/Ikke)) ## 3.3.2 (June 12th, 2023) ENHANCEMENTS * **New Data Source:** `netbox_contact_role` ([#414](https://github.com/e-breuninger/terraform-provider-netbox/pull/414) by [@Ikke](https://github.com/Ikke)) ## 3.3.1 (May 31th, 2023) ENHANCEMENTS * data-source/netbox_prefixes: Allow filtering by `site_id` ([#397](https://github.com/e-breuninger/terraform-provider-netbox/pull/397) by [@tagur87](https://github.com/tagur87)) * data-source/netbox_ip_addresses: Add `tags` attributes to output ([#406](https://github.com/e-breuninger/terraform-provider-netbox/pull/406) by [@pier-nl](https://github.com/pier-nl)) * Improved error handling for tags ([#400](https://github.com/e-breuninger/terraform-provider-netbox/pull/400) by [@tagur87](https://github.com/tagur87)) ## 3.3.0 (May 7th, 2023) ENHANCEMENTS * **New Resource:** `netbox_permission` ([#390](https://github.com/e-breuninger/terraform-provider-netbox/pull/390) by [@tagur87](https://github.com/tagur87)) * **New Resource:** `netbox_contact_group` ([#366](https://github.com/e-breuninger/terraform-provider-netbox/pull/366) by [@leasley199](https://github.com/leasley199)) * **New Data Source:** `netbox_contact_group` ([#366](https://github.com/e-breuninger/terraform-provider-netbox/pull/366) by [@leasley199](https://github.com/leasley199)) * **New Data Source:** `netbox_contact` ([#366](https://github.com/e-breuninger/terraform-provider-netbox/pull/366) by [@leasley199](https://github.com/leasley199)) * data-source/netbox_cluster: Allow searching by `site_id` BUG FIXES * resource/netbox_prefix: Allow unsetting `description` attribute ([#382](https://github.com/e-breuninger/terraform-provider-netbox/pull/382) by [@DevOpsFu](https://github.com/DevOpsFu)) ## 3.2.1 (April 27th, 2023) ENHANCEMENTS * **New Resource:** `netbox_vlan_group` ([#377](https://github.com/e-breuninger/terraform-provider-netbox/pull/377) by [@zeddD1abl0](https://github.com/zeddD1abl0)) * **New Data Source:** `netbox_vlan_group` ([#377](https://github.com/e-breuninger/terraform-provider-netbox/pull/377) by [@zeddD1abl0](https://github.com/zeddD1abl0)) * resource/netbox_vlan: Add `group_id` attribute ([#377](https://github.com/e-breuninger/terraform-provider-netbox/pull/377) by [@zeddD1abl0](https://github.com/zeddD1abl0)) BUG FIXES * data-source/netbox_prefixes: Fix error when filtering by `vlan_vid` ([#381](https://github.com/e-breuninger/terraform-provider-netbox/pull/381) by [@zeddD1abl0](https://github.com/zeddD1abl0)) ## 3.2.0 (March 26th, 2023) ENHANCEMENTS * **New Resource:** `netbox_route_target` ([#344](https://github.com/e-breuninger/terraform-provider-netbox/pull/344) by [@imdhruva](https://github.com/imdhruva)) * **New Resource:** `netbox_rack` ([#358](https://github.com/e-breuninger/terraform-provider-netbox/pull/358) by [@joeyberkovitz](https://github.com/joeyberkovitz)) * **New Resource:** `netbox_rack_reservation` ([#358](https://github.com/e-breuninger/terraform-provider-netbox/pull/358) by [@joeyberkovitz](https://github.com/joeyberkovitz)) * **New Resource:** `netbox_rack_role` ([#358](https://github.com/e-breuninger/terraform-provider-netbox/pull/358) by [@joeyberkovitz](https://github.com/joeyberkovitz)) * **New Data Source:** `netbox_ipam_role` ([#344](https://github.com/e-breuninger/terraform-provider-netbox/pull/344) by [@imdhruva](https://github.com/imdhruva)) * **New Data Source:** `netbox_route_target` ([#344](https://github.com/e-breuninger/terraform-provider-netbox/pull/344) by [@imdhruva](https://github.com/imdhruva)) * **New Data Source:** `netbox_racks` ([#358](https://github.com/e-breuninger/terraform-provider-netbox/pull/358) by [@joeyberkovitz](https://github.com/joeyberkovitz)) * **New Data Source:** `netbox_rack_role` ([#358](https://github.com/e-breuninger/terraform-provider-netbox/pull/358) by [@joeyberkovitz](https://github.com/joeyberkovitz)) * resource/netbox_device: Add `rack_face`, `rack_id` and `rack_position` attributes ([#358](https://github.com/e-breuninger/terraform-provider-netbox/pull/358) by [@joeyberkovitz](https://github.com/joeyberkovitz)) * data-source/netbox_device: Add `rack_face`, `rack_id` and `rack_position` attributes ([#358](https://github.com/e-breuninger/terraform-provider-netbox/pull/358) by [@joeyberkovitz](https://github.com/joeyberkovitz)) * data-source/netbox_prefixes: Add support for filtering by `status` and `tag` ([#367](https://github.com/e-breuninger/terraform-provider-netbox/pull/367) by [@kyle-burnett](https://github.com/kyle-burnett)) * resource/netbox_location: Add `description` attribute * resource/netbox_rir: Add `description` attribute * resource/netbox_vrf: Add `description` attribute * data-source/netbox_prefixes: Include `description` attribute in search results * data-source/netbox_ip_addresses: Add `limit` attribute (default 1000) ## 3.1.0 (February 19th, 2023) CHANGES * provider: `slug` fields are now generated to match the netbox GUI behavior ENHANCEMENTS * resource/netbox_interface: Updating `mac_address` no longer forces resource recreation ([#336](https://github.com/e-breuninger/terraform-provider-netbox/pull/336) by [@johann8384](https://github.com/johann8384)) * resource/netbox_site: Add `physical_address` and `shipping_address` ([#337](https://github.com/e-breuninger/terraform-provider-netbox/pull/337) by [@Ikke](https://github.com/Ikke)) * resource/netbox_ip_address: IP addresses can now be assigned to devices via the `object_type` field ([#341](https://github.com/e-breuninger/terraform-provider-netbox/pull/341) by [@arjenvri](https://github.com/arjenvri)) ## 3.0.13 (January 24th, 2023) ENHANCEMENTS * data-source/netbox_prefix: Add `site_id` attribute ([#320](https://github.com/e-breuninger/terraform-provider-netbox/pull/320) by [@TGM](https://github.com/TGM)) ## 3.0.12 (January 3rd, 2023) ENHANCEMENTS * resource/netbox_token: Add `write_enabled` attribute ([#309](https://github.com/e-breuninger/terraform-provider-netbox/pull/309) by [@keshy7](https://github.com/keshy7)) * data-source/netbox_interfaces: The resulting interfaces now have their interface ID set BUG FIXES * resource/site: Allow unsetting `description` attribute ([#314](https://github.com/e-breuninger/terraform-provider-netbox/pull/314) by [@keshy7](https://github.com/keshy7)) * resource/site: Set max length of the `slug` attribute to 100 ([#317](https://github.com/e-breuninger/terraform-provider-netbox/pull/317) by [@keshy7](https://github.com/keshy7)) ## 3.0.11 (December 13th, 2022) ENHANCEMENTS * resource/netbox_available_ip_address: Add `role` attribute BUG FIXES * resource/netbox_location: Fix updates of the `site_id` attribute ([#307](https://github.com/e-breuninger/terraform-provider-netbox/pull/307) by [@nneul](https://github.com/nneul)) ## 3.0.10 (November 24th, 2022) ENHANCEMENTS * provider: Add `strip_trailing_slashes_from_url` attribute BUG FIXES * data-source/netbox_region: Use correct field for slug attribute ([#302](https://github.com/e-breuninger/terraform-provider-netbox/pull/302) by [@paulexyz](https://github.com/paulexyz)) ## 3.0.9 (November 17th, 2022) ENHANCEMENTS * data-source/netbox_vlan: Allow querying by `group_id`, `role` and `tenant` ([#287](https://github.com/e-breuninger/terraform-provider-netbox/pull/287) by [@tstarck](https://github.com/tstarck)) * data-source/netbox_prefix: Allow querying by `description` ([#298](https://github.com/e-breuninger/terraform-provider-netbox/pull/298) by [@luispcoutinho](https://github.com/luispcoutinho)) ## 3.0.8 (November 9th, 2022) ENHANCEMENTS * **New Resource:** `netbox_device_interface` ([#286](https://github.com/e-breuninger/terraform-provider-netbox/pull/286) by [@arjenvri](https://github.com/arjenvri)) * **New Data Source:** `netbox_asn` ([#285](https://github.com/e-breuninger/terraform-provider-netbox/pull/285) by [@kyle-burnett](https://github.com/kyle-burnett)) * **New Data Source:** `netbox_asns` ([#292](https://github.com/e-breuninger/terraform-provider-netbox/pull/292) by [@kyle-burnett](https://github.com/kyle-burnett)) * data-source/netbox_prefix: Add `tags` and tag filter attributes ([#284](https://github.com/e-breuninger/terraform-provider-netbox/pull/284) by [@kyle-burnett](https://github.com/kyle-burnett)) BUG FIXES * data-source/netbox_prefixes: Fix kernel panic when finding prefixes without vlan or vrf ## 3.0.7 (November 3rd, 2022) ENHANCEMENTS * **New Resource:** `netbox_contact_role` ([#279](https://github.com/e-breuninger/terraform-provider-netbox/pull/279) by [@arjenvri](https://github.com/arjenvri)) * **New Resource:** `netbox_contact_assignment` ([#279](https://github.com/e-breuninger/terraform-provider-netbox/pull/279) by [@arjenvri](https://github.com/arjenvri)) * resource/netbox_device: Add `primary_ipv6` attribute ([#282](https://github.com/e-breuninger/terraform-provider-netbox/pull/282) by [@arjenvri](https://github.com/arjenvri)) * resource/netbox_virtual_machine: Add `primary_ipv6` attribute ([#283](https://github.com/e-breuninger/terraform-provider-netbox/pull/283) by [@arjenvri](https://github.com/arjenvri)) * resource/netbox_custom_field: Add `group_name` atribute ([#280](https://github.com/e-breuninger/terraform-provider-netbox/pull/280) by [@arjenvri](https://github.com/arjenvri)) ## 3.0.6 (October 21st, 2022) ENHANCEMENTS * **New Resource:** `netbox_contact` ([#273](https://github.com/e-breuninger/terraform-provider-netbox/pull/273) by [@arjenvri](https://github.com/arjenvri)) * data-source/netbox_prefix: Add `description` attribute ([#277](https://github.com/e-breuninger/terraform-provider-netbox/pull/277) by [@holmesb](https://github.com/holmesb)) * resource/netbox_cluster: Add `tenant_id` attribute ([#275](https://github.com/e-breuninger/terraform-provider-netbox/pull/275) by [@arjenvri](https://github.com/arjenvri)) ## 3.0.5 (October 18th, 2022) ENHANCEMENTS * resource/netbox_device_role: Add `tags` attribute ([#269](https://github.com/e-breuninger/terraform-provider-netbox/pull/269) by [@hollow](https://github.com/hollow)) * data-source/netbox_device_role: Add `tags` attribute ([#269](https://github.com/e-breuninger/terraform-provider-netbox/pull/269) by [@hollow](https://github.com/hollow)) CHANGES * resource/netbox_service: Implement provider-side validation on allowed values. Valid values are `tcp`, `udp` and `sctp`. ## 3.0.4 (October 11th, 2022) ENHANCEMENTS * resource/netbox_device: Add `platform_id` attribute ([#264](https://github.com/e-breuninger/terraform-provider-netbox/pull/264) by [@mifrost](https://github.com/mifrost)) * **New Data Source:** `netbox_prefixes ` ([#253](https://github.com/e-breuninger/terraform-provider-netbox/pull/253) by [@ironashram](https://github.com/ironashram)) * data-source/netbox_prefix: Add `prefix`, `status`, `vlan_id`, `vlan_vid` attributes ([#253](https://github.com/e-breuninger/terraform-provider-netbox/pull/253) by [@ironashram](https://github.com/ironashram)) * resource/netbox_device: Add `status` attribute [#266](https://github.com/e-breuninger/terraform-provider-netbox/pull/266) by [@mifrost](https://github.com/mifrost)) CHANGES * resource/netbox_prefix: Deprecate `cidr` attribute in favor of new canonical `prefix` attribute ## 3.0.3 (October 4th, 2022) ENHANCEMENTS * resource/netbox_site: Add `group_id` attribute ([#255](https://github.com/e-breuninger/terraform-provider-netbox/pull/255) by [@arjenvri](https://github.com/arjenvri)) ## 3.0.2 (September 30th, 2022) ENHANCEMENTS * data-source/netbox_cluster: Add `site_id`, `cluster_type_id`, `cluster_group_id` and `tags` attribute ([#251](https://github.com/e-breuninger/terraform-provider-netbox/pull/251) by [@ns1pelle](https://github.com/ns1pelle)) ## 3.0.1 (September 25th, 2022) This is a re-release of 3.0.0 because there seem to be some issues with the checksums in the 3.0.0 version. ## 3.0.0 (September 23th, 2022) FEATURES * provider: Now supports NetBox v3.3 ENHANCEMENTS * resource/netbox_virtual_machine: In accordance with upstream API changes, VMs can now have `site_id` set directly * resource/netbox_virtual_machine: Add `device_id` attribute ([#238](https://github.com/e-breuninger/terraform-provider-netbox/pull/238) by [@ns1pelle](https://github.com/ns1pelle)) * resource/netbox_circuit_termination: Add `tags` and `custom_fields` attributes ([#238](https://github.com/e-breuninger/terraform-provider-netbox/pull/238) by [@ns1pelle](https://github.com/ns1pelle)) * resource/netbox_token: Add `allowed_ips`, `last_used` and `expires` attributes ([#238](https://github.com/e-breuninger/terraform-provider-netbox/pull/238) by [@ns1pelle](https://github.com/ns1pelle)) * resource/netbox_device: Add `cluster_id` attribute ([#238](https://github.com/e-breuninger/terraform-provider-netbox/pull/238) by [@ns1pelle](https://github.com/ns1pelle)) ## 2.0.7 (September 23th, 2022) ENHANCEMENTS * **New Data Source:** `netbox_devices` ([#236](https://github.com/e-breuninger/terraform-provider-netbox/pull/236) by [@dipeshsharma](https://github.com/dipeshsharma)) * provider: Add `request_timeout` attribute ([#227](https://github.com/e-breuninger/terraform-provider-netbox/pull/227) by [@twink0r](https://github.com/twink0r)) * data-source/netbox_tenants: Add `limit` attribute to allow for larger queries ## 2.0.6 (September 9th, 2022) ENHANCEMENTS * **New Resource:** `netbox_site_group` * **New Data Source:** `netbox_site_group` * resource/netbox_virtual_machine: Add `status` attribute. The `status` attribute will default to `active`, which matches the implicit behavior of NetBox. If you manually changed the status of your terraform-managed NetBox VMs, be cautious * data-source/netbox_tenant: Allow searching by `slug` attribute ## 2.0.5 (August 10th, 2022) ENHANCEMENTS * provider: Update list of supported versions * docs: Add all missing docs and update existing ones * docs: Add subcategories ## 2.0.4 (August 1st, 2022) ENHANCEMENTS * resource/netbox_ip_address: Add `role` attribute * resource/netbox_available_ip_address: improve documentation ([#220](https://github.com/e-breuninger/terraform-provider-netbox/pull/220) by [@holmesb](https://github.com/holmesb)) BUG FIXES * resource/netbox_device: Set correct attribute for `device_type_id` ([#219](https://github.com/e-breuninger/terraform-provider-netbox/pull/219) by [@BegBlev](https://github.com/BegBlev)) * data-source/netbox_ip_addresses: Use correct attribute for `role` ([#217](https://github.com/e-breuninger/terraform-provider-netbox/pull/217) by [@twink0r](https://github.com/twink0r)) ## 2.0.3 (July 8th, 2022) ENHANCEMENTS * data-source/netbox_prefix: Add `vrf_id` attribute BUG FIXES * resource/netbox_prefix: Allow unsetting mark_utilized and is_pool attributes * resource/netbox_available_prefix: Allow unsetting mark_utilized and is_pool attributes ## 2.0.2 (July 6th, 2022) BUG FIXES * resource/netbox_device: Make `role_id` and `site_id` attributes mandatory * resource/netbox_device_type: Make `manufacturer_id` attribute mandatory ## 2.0.1 (June 25th, 2022) ENHANCEMENTS * **New Resource:** `netbox_location` ([#195](https://github.com/e-breuninger/terraform-provider-netbox/pull/195) by [@arjenvri](https://github.com/arjenvri)) * resource/netbox_device: Add `location_id` attribute ([#195](https://github.com/e-breuninger/terraform-provider-netbox/pull/195) by [@arjenvri](https://github.com/arjenvri)) ## 2.0.0 (June 16th, 2022) **BREAKING CHANGES** NetBox 3.2.0 came with [breaking changes](https://docs.netbox.dev/en/stable/release-notes/version-3.2/#breaking-changes). In accordance with the upstream API, the `netbox_site` resource and data source now have an `asn_ids` attribute that replaces the `asn` attriute. Note that `asn_ids` contains **IDs** of ASN objects, not numbers. ENHANCEMENTS * **New Resource:** `netbox_asn` ## 1.6.7 (June 14th, 2022) ENHANCEMENTS * resource/netbox_site: Make `status` attribute optional and default to `active` ([#187](https://github.com/e-breuninger/terraform-provider-netbox/pull/187) by [@tstarck](https://github.com/tstarck)) * data-source/netbox_site: Add `slug` parameter to allow searching for a slug ([#187](https://github.com/e-breuninger/terraform-provider-netbox/pull/187) by [@tstarck](https://github.com/tstarck)) * data-source/netbox_site: Include `asn`, `slug`, `comments`, `description`, `group_id`, `status`, `region_id`, `tenant_id` and `time_zone` attributes in the search result ([#187](https://github.com/e-breuninger/terraform-provider-netbox/pull/187) by [@tstarck](https://github.com/tstarck)) * resource/netbox_vlan: Add default values to `status` and `description` attributes ([#184](https://github.com/e-breuninger/terraform-provider-netbox/pull/184) by [@tstarck](https://github.com/tstarck)) * resource/netbox_interface: Add `enabled`, `mtu`, `mode`, `tagged_vlans` and `untagged_vlans` attributes ([#183](https://github.com/e-breuninger/terraform-provider-netbox/pull/183) by [@tstarck](https://github.com/tstarck)) ## 1.6.6 (May 27th, 2022) ENHANCEMENTS * **New Data Source:** `netbox_device_type` ([#179](https://github.com/e-breuninger/terraform-provider-netbox/pull/179) by [@tstarck](https://github.com/tstarck)) * **New Data Source:** `netbox_vlan` ([#180](https://github.com/e-breuninger/terraform-provider-netbox/pull/180) by [@tstarck](https://github.com/tstarck)) * provider: Add `skip_version_check` attribute * provider: Update list of officially supported versions * resource/netbox_device_type: Add `part_number` attribute ([#179](https://github.com/e-breuninger/terraform-provider-netbox/pull/179) by [@tstarck](https://github.com/tstarck)) BUG FIXES * resource/netbox_circuit: Fix bug that prevented updates from being made * resource/netbox_circuit_provider: Fix bug that prevented updates from being made ## 1.6.5 (May 18th, 2022) ENHANCEMENTS * docs: Fix critical error in usage documentation ## 1.6.4 (May 18th, 2022) FEATURES * **New Resource:** `netbox_user` ([#169](https://github.com/e-breuninger/terraform-provider-netbox/pull/169) by [@arjenvri](https://github.com/arjenvri)) * **New Resource:** `netbox_token` ([#169](https://github.com/e-breuninger/terraform-provider-netbox/pull/169) by [@arjenvri](https://github.com/arjenvri)) ENHANCEMENTS * resource/netbox_site: Add `timezone`, `latitude`, `longitude` and `custom_fields` attributes ([#168](https://github.com/e-breuninger/terraform-provider-netbox/pull/168) by [@arjenvri](https://github.com/arjenvri)) * docs: Regenerate docs with updated tooling ([#165](https://github.com/e-breuninger/terraform-provider-netbox/pull/165) by [@d-strobel](https://github.com/d-strobel)) ## 1.6.3 (May 6th, 2022) FEATURES * **New Data Source:** `netbox_ip_addresses` ([#159](https://github.com/e-breuninger/terraform-provider-netbox/pull/159) by [@twink0r](https://github.com/twink0r)) * **New Resource:** `netbox_circuit` ([#160](https://github.com/e-breuninger/terraform-provider-netbox/pull/160) by [@arjenvri](https://github.com/arjenvri)) * **New Resource:** `netbox_circuit_provider` ([#160](https://github.com/e-breuninger/terraform-provider-netbox/pull/160) by [@arjenvri](https://github.com/arjenvri)) * **New Resource:** `netbox_circuit_termination` ([#160](https://github.com/e-breuninger/terraform-provider-netbox/pull/160) by [@arjenvri](https://github.com/arjenvri)) * **New Resource:** `netbox_circuit_type` ([#160](https://github.com/e-breuninger/terraform-provider-netbox/pull/160) by [@arjenvri](https://github.com/arjenvri)) * **New Resource:** `netbox_custom_field` ([#158](https://github.com/e-breuninger/terraform-provider-netbox/pull/158) by [@chapsuk](https://github.com/chapsuk)) ENHANCEMENTS * resource/netbox_ip_address: Add `description` attribute ([#156](https://github.com/e-breuninger/terraform-provider-netbox/pull/156) by [@fbreckle](https://github.com/fbreckle)) * resource/netbox_virtual_machine: Add `custom_fields` attribute ([#158](https://github.com/e-breuninger/terraform-provider-netbox/pull/158) by [@chapsuk](https://github.com/chapsuk)) ## 1.6.2 (Apr 11, 2022) FEATURES * **New Resource:** `netbox_rir` ([#153](https://github.com/e-breuninger/terraform-provider-netbox/pull/153) by [@arjenvri](https://github.com/arjenvri)) * **New Resource:** `netbox_aggregate` ([#153](https://github.com/e-breuninger/terraform-provider-netbox/pull/153) by [@arjenvri](https://github.com/arjenvri)) ## 1.6.1 (Apr 8, 2022) ENHANCEMENTS * resource/netbox_site: Add `tags` and `tenant_id` attributes ([#149](https://github.com/e-breuninger/terraform-provider-netbox/pull/149) by [@arjenvri](https://github.com/arjenvri)) ## 1.6.0 (Apr 8, 2022) FEATURES * **New Resource:** `netbox_device` ([#142](https://github.com/e-breuninger/terraform-provider-netbox/pull/142) by [@arjenvri](https://github.com/arjenvri)) * **New Resource:** `netbox_device_type` ([#142](https://github.com/e-breuninger/terraform-provider-netbox/pull/142) by [@arjenvri](https://github.com/arjenvri)) * **New Resource:** `netbox_manufacturer` ([#142](https://github.com/e-breuninger/terraform-provider-netbox/pull/142) by [@arjenvri](https://github.com/arjenvri)) ## 1.5.2 (Mar 4, 2022) ENHANCEMENTS * data-source/netbox_tenants: Add `tenant_group` attribute ([#129](https://github.com/e-breuninger/terraform-provider-netbox/pull/129) by [@twink0r](https://github.com/twink0r)) ## 1.5.1 (Feb 24, 2022) ENHANCEMENTS * No longer crashes if netbox is unreachable when initialising the provider [#126](https://github.com/e-breuninger/terraform-provider-netbox/pull/126) by [@twink0r](https://github.com/twink0r) ## 1.5.0 (Feb 23, 2022) FEATURES * **New Data Source:** `netbox_tenants` [#124](https://github.com/e-breuninger/terraform-provider-netbox/pull/124) by [@twink0r](https://github.com/twink0r) ## 1.4.0 (Feb 21, 2022) FEATURES * **New Data Source:** `netbox_cluster_type` [#122](https://github.com/e-breuninger/terraform-provider-netbox/pull/122) by [@madnutter56](https://github.com/madnutter56) * **New Data Source:** `netbox_site` [#122](https://github.com/e-breuninger/terraform-provider-netbox/pull/122) by [@madnutter56](https://github.com/madnutter56) ## 1.3.0 (Feb 17, 2022) FEATURES * **New Resource:** `netbox_region` ([#121](https://github.com/e-breuninger/terraform-provider-netbox/pull/121) by [@gerl1ng](https://github.com/gerl1ng)) * **New Data Source:** `netbox_region` [#121](https://github.com/e-breuninger/terraform-provider-netbox/pull/121) by [@gerl1ng](https://github.com/gerl1ng) ## 1.2.2 (Feb 9, 2022) ENHANCEMENTS * resource/netbox_virtual_machine: Now has a state migration for the `vcpus` attribute ([#120](https://github.com/e-breuninger/terraform-provider-netbox/pull/120) by [@pascal-hofmann](https://github.com/pascal-hofmann)) ## 1.2.1 (Jan 31, 2022) FEATURES * provider: Can now optionally pass custom HTTP headers for every request ([#116](https://github.com/e-breuninger/terraform-provider-netbox/pull/116) by [@mariuskiessling](https://github.com/mariuskiessling)) ## 1.2.0 (Jan 20, 2022) FEATURES * resource/netbox_available_ip_address: Can now be created in netbox_ip_ranges ([#106](https://github.com/e-breuninger/terraform-provider-netbox/pull/106) by [@holmesb](https://github.com/holmesb)) ENHANCEMENTS * resource/netbox_available_ip_address: fixed duplicates [#59](https://github.com/e-breuninger/terraform-provider-netbox/issues/59) ([#106](https://github.com/e-breuninger/terraform-provider-netbox/pull/106) by [@holmesb](https://github.com/holmesb)) * resource/netbox_available_ip_address: Add `description` argument ([#106](https://github.com/e-breuninger/terraform-provider-netbox/pull/106) by [@holmesb](https://github.com/holmesb)) * resource/netbox_available_ip_address: `status` argument is now optional ([#106](https://github.com/e-breuninger/terraform-provider-netbox/pull/106) by [@holmesb](https://github.com/holmesb)) * resource/netbox_vrf: Add `tenant_id` attribute ([#112](https://github.com/e-breuninger/terraform-provider-netbox/pull/112) by [@cova-fe](https://github.com/cova-fe)) * data-source/netbox_vrf: Add `tenant_id` attribute ([#112](https://github.com/e-breuninger/terraform-provider-netbox/pull/112) by [@cova-fe](https://github.com/cova-fe)) * resource/available_prefix: Add `mark_utilized` attribute ([#111](https://github.com/e-breuninger/terraform-provider-netbox/pull/111) by [@cova-fe](https://github.com/cova-fe)) ## 1.1.0 (Jan 3, 2022) FEATURES * provider: Now supports NetBox v3.1.3 ## 1.0.2 Ho-Ho-Ho (Dec 24, 2021) ENHANCEMENTS * resource/tag: Add `description` attribute ([#98](https://github.com/e-breuninger/terraform-provider-netbox/pull/98)) by [@lu1as](https://github.com/lu1as)) ## 1.0.1 (Dec 23, 2021) FEATURES * **New Resource:** `netbox_ip_range` ([#101](https://github.com/e-breuninger/terraform-provider-netbox/pull/100) by [@holmesb](https://github.com/holmesb)) * **New Data Source:** `netbox_ip_range` ([#101](https://github.com/e-breuninger/terraform-provider-netbox/pull/100) by [@holmesb](https://github.com/holmesb)) ## 1.0.0 (Nov 8, 2021) FEATURES * provider: Now supports NetBox v3.0.9 BREAKING CHANGES * resource/virtual_machine: `vcpus` is now a float to match upstream API ## 0.3.2 (Nov 2, 2021) ENHANCEMENTS * resource/primary_ip: Support both v4 and v6 primary IP ([#87](https://github.com/e-breuninger/terraform-provider-netbox/pull/87) by [@t-tran](https://github.com/t-tran)) ## 0.3.1 (Oct 27, 2021) FEATURES * **New Resource:** `netbox_vlan` ([#83](https://github.com/e-breuninger/terraform-provider-netbox/pull/83) by [@Sanverik](https://github.com/Sanverik)) * **New Resource:** `netbox_ipam_role` ([#86](https://github.com/e-breuninger/terraform-provider-netbox/pull/86) by [@Sanverik](https://github.com/Sanverik)) ENHANCEMENTS * resource/prefix: Add `site_id`, `vlan_id` and `role_id` attributes ([#85](https://github.com/e-breuninger/terraform-provider-netbox/pull/85) and [#85](https://github.com/e-breuninger/terraform-provider-netbox/pull/85)) by [@Sanverik](https://github.com/Sanverik)) ## 0.3.0 (Oct 19, 2021) FEATURES * provider: Now supports NetBox v2.11.12 BREAKING CHANGES * resource/virtual_machine: `vcpus` is now a string to match upstream API ## 0.2.5 (Oct 8, 2021) ENHANCEMENTS * **New Resource:** `netbox_site` ([#78](https://github.com/e-breuninger/terraform-provider-netbox/pull/78)) BUG FIXES * resource/cluster: Properly set tags when updating ([#69](https://github.com/e-breuninger/terraform-provider-netbox/issues/69)) ## 0.2.4 (Sep 20, 2021) CHANGES * Use go 1.17 to fix some builds ## 0.2.3 (Sep 20, 2021) ENHANCEMENTS * Add arm64 builds ([#71](https://github.com/e-breuninger/terraform-provider-netbox/pull/71) by [@richardklose](https://github.com/richardklose)) ## 0.2.2 (Aug 23, 2021) ENHANCEMENTS * resource/interface: Add `mac_address` attribute ([#65](https://github.com/e-breuninger/terraform-provider-netbox/pull/65) by [@holmesb](https://github.com/holmesb)) ## 0.2.1 (Jul 26, 2021) ENHANCEMENTS * resource/prefix: Add `vrf` and `tenant` attribute ([#61](https://github.com/e-breuninger/terraform-provider-netbox/pull/61) by [@jeansebastienh](https://github.com/jeansebastienh)) BUG FIXES * resource/prefix: Correctly read `prefix` and `status` ([#60](https://github.com/e-breuninger/terraform-provider-netbox/pull/60) by [@jeansebastienh](https://github.com/jeansebastienh)) ## 0.2.0 (May 31, 2021) FEATURES * provider: Now supports NetBox v2.10.10 CHANGES * resource/service: `port` field is now deprecated in favor of `ports` field. ## 0.1.3 (May 17, 2021) ENHANCEMENTS * **New Resource:** `netbox_tenant_group` ([#48](https://github.com/e-breuninger/terraform-provider-netbox/pull/48) by [@pezhore](https://github.com/pezhore)) * **New Data Source:** `netbox_tenant_group` ([#48](https://github.com/e-breuninger/terraform-provider-netbox/pull/48) by [@pezhore](https://github.com/pezhore)) * data-source/tenant: Add `group_id` attribute ([#48](https://github.com/e-breuninger/terraform-provider-netbox/pull/48) by [@pezhore](https://github.com/pezhore)) ## 1.5.1 (Feb 24, 2022) ENHANCEMENTS * No longer crashes if netbox is unreachable when initialising the provider [#126](https://github.com/e-breuninger/terraform-provider-netbox/pull/126) by [@twink0r](https://github.com/twink0r) ## 1.5.0 (Feb 23, 2022) FEATURES * **New Data Source:** `netbox_tenants` [#124](https://github.com/e-breuninger/terraform-provider-netbox/pull/124) by [@twink0r](https://github.com/twink0r) ## 1.4.0 (Feb 21, 2022) FEATURES * **New Data Source:** `netbox_cluster_type` [#122](https://github.com/e-breuninger/terraform-provider-netbox/pull/122) by [@madnutter56](https://github.com/madnutter56) * **New Data Source:** `netbox_site` [#122](https://github.com/e-breuninger/terraform-provider-netbox/pull/122) by [@madnutter56](https://github.com/madnutter56) ## 1.3.0 (Feb 17, 2022) FEATURES * **New Resource:** `netbox_region` ([#121](https://github.com/e-breuninger/terraform-provider-netbox/pull/121) by [@gerl1ng](https://github.com/gerl1ng)) * **New Data Source:** `netbox_region` [#121](https://github.com/e-breuninger/terraform-provider-netbox/pull/121) by [@gerl1ng](https://github.com/gerl1ng) ## 1.2.2 (Feb 9, 2022) ENHANCEMENTS * resource/netbox_virtual_machine: Now has a state migration for the `vcpus` attribute ([#120](https://github.com/e-breuninger/terraform-provider-netbox/pull/120) by [@pascal-hofmann](https://github.com/pascal-hofmann)) ## 1.2.1 (Jan 31, 2022) FEATURES * provider: Can now optionally pass custom HTTP headers for every request ([#116](https://github.com/e-breuninger/terraform-provider-netbox/pull/116) by [@mariuskiessling](https://github.com/mariuskiessling)) ## 1.2.0 (Jan 20, 2022) FEATURES * resource/netbox_available_ip_address: Can now be created in netbox_ip_ranges ([#106](https://github.com/e-breuninger/terraform-provider-netbox/pull/106) by [@holmesb](https://github.com/holmesb)) ENHANCEMENTS * resource/netbox_available_ip_address: fixed duplicates [#59](https://github.com/e-breuninger/terraform-provider-netbox/issues/59) ([#106](https://github.com/e-breuninger/terraform-provider-netbox/pull/106) by [@holmesb](https://github.com/holmesb)) * resource/netbox_available_ip_address: Add `description` argument ([#106](https://github.com/e-breuninger/terraform-provider-netbox/pull/106) by [@holmesb](https://github.com/holmesb)) * resource/netbox_available_ip_address: `status` argument is now optional ([#106](https://github.com/e-breuninger/terraform-provider-netbox/pull/106) by [@holmesb](https://github.com/holmesb)) * resource/netbox_vrf: Add `tenant_id` attribute ([#112](https://github.com/e-breuninger/terraform-provider-netbox/pull/112) by [@cova-fe](https://github.com/cova-fe)) * data-source/netbox_vrf: Add `tenant_id` attribute ([#112](https://github.com/e-breuninger/terraform-provider-netbox/pull/112) by [@cova-fe](https://github.com/cova-fe)) * resource/available_prefix: Add `mark_utilized` attribute ([#111](https://github.com/e-breuninger/terraform-provider-netbox/pull/111) by [@cova-fe](https://github.com/cova-fe)) ## 1.1.0 (Jan 3, 2022) FEATURES * provider: Now supports NetBox v3.1.3 ## 1.0.2 Ho-Ho-Ho (Dec 24, 2021) ENHANCEMENTS * resource/tag: Add `description` attribute ([#98](https://github.com/e-breuninger/terraform-provider-netbox/pull/98)) by [@lu1as](https://github.com/lu1as)) ## 1.0.1 (Dec 23, 2021) FEATURES * **New Resource:** `netbox_ip_range` ([#101](https://github.com/e-breuninger/terraform-provider-netbox/pull/100) by [@holmesb](https://github.com/holmesb)) * **New Data Source:** `netbox_ip_range` ([#101](https://github.com/e-breuninger/terraform-provider-netbox/pull/100) by [@holmesb](https://github.com/holmesb)) ## 1.0.0 (Nov 8, 2021) FEATURES * provider: Now supports NetBox v3.0.9 BREAKING CHANGES * resource/virtual_machine: `vcpus` is now a float to match upstream API ## 0.3.2 (Nov 2, 2021) ENHANCEMENTS * resource/primary_ip: Support both v4 and v6 primary IP ([#87](https://github.com/e-breuninger/terraform-provider-netbox/pull/87) by [@t-tran](https://github.com/t-tran)) ## 0.3.1 (Oct 27, 2021) FEATURES * **New Resource:** `netbox_vlan` ([#83](https://github.com/e-breuninger/terraform-provider-netbox/pull/83) by [@Sanverik](https://github.com/Sanverik)) * **New Resource:** `netbox_ipam_role` ([#86](https://github.com/e-breuninger/terraform-provider-netbox/pull/86) by [@Sanverik](https://github.com/Sanverik)) ENHANCEMENTS * resource/prefix: Add `site_id`, `vlan_id` and `role_id` attributes ([#85](https://github.com/e-breuninger/terraform-provider-netbox/pull/85) and [#85](https://github.com/e-breuninger/terraform-provider-netbox/pull/85)) by [@Sanverik](https://github.com/Sanverik)) ## 0.3.0 (Oct 19, 2021) FEATURES * provider: Now supports NetBox v2.11.12 BREAKING CHANGES * resource/virtual_machine: `vcpus` is now a string to match upstream API ## 0.2.5 (Oct 8, 2021) ENHANCEMENTS * **New Resource:** `netbox_site` ([#78](https://github.com/e-breuninger/terraform-provider-netbox/pull/78)) BUG FIXES * resource/cluster: Properly set tags when updating ([#69](https://github.com/e-breuninger/terraform-provider-netbox/issues/69)) ## 0.2.4 (Sep 20, 2021) CHANGES * Use go 1.17 to fix some builds ## 0.2.3 (Sep 20, 2021) ENHANCEMENTS * Add arm64 builds ([#71](https://github.com/e-breuninger/terraform-provider-netbox/pull/71) by [@richardklose](https://github.com/richardklose)) ## 0.2.2 (Aug 23, 2021) ENHANCEMENTS * resource/interface: Add `mac_address` attribute ([#65](https://github.com/e-breuninger/terraform-provider-netbox/pull/65) by [@holmesb](https://github.com/holmesb)) ## 0.2.1 (Jul 26, 2021) ENHANCEMENTS * resource/prefix: Add `vrf` and `tenant` attribute ([#61](https://github.com/e-breuninger/terraform-provider-netbox/pull/61) by [@jeansebastienh](https://github.com/jeansebastienh)) BUG FIXES * resource/prefix: Correctly read `prefix` and `status` ([#60](https://github.com/e-breuninger/terraform-provider-netbox/pull/60) by [@jeansebastienh](https://github.com/jeansebastienh)) ## 0.2.0 (May 31, 2021) FEATURES * provider: Now supports NetBox v2.10.10 CHANGES * resource/service: `port` field is now deprecated in favor of `ports` field. ## 0.1.3 (May 17, 2021) ENHANCEMENTS * **New Resource:** `netbox_tenant_group` ([#48](https://github.com/e-breuninger/terraform-provider-netbox/pull/48) by [@pezhore](https://github.com/pezhore)) * **New Data Source:** `netbox_tenant_group` ([#48](https://github.com/e-breuninger/terraform-provider-netbox/pull/48) by [@pezhore](https://github.com/pezhore)) * data-source/tenant: Add `group_id` attribute ([#48](https://github.com/e-breuninger/terraform-provider-netbox/pull/48) by [@pezhore](https://github.com/pezhore)) * resource/tenant: Add `group_id` attribute ([#48](https://github.com/e-breuninger/terraform-provider-netbox/pull/48) by [@pezhore](https://github.com/pezhore)) * Documentation ([#46](https://github.com/e-breuninger/terraform-provider-netbox/pull/46) by [@pezhore](https://github.com/pezhore)) ## 0.1.2 (May 4, 2021) ENHANCEMENTS * **New Resource:** `netbox_prefix` ([#43](https://github.com/e-breuninger/terraform-provider-netbox/pull/43) by [@pezhore](https://github.com/pezhore)) * **New Data Source:** `netbox_prefix` ([#43](https://github.com/e-breuninger/terraform-provider-netbox/pull/43) by [@pezhore](https://github.com/pezhore)) * **New Resource:** `netbox_available_ip_address` ([#43](https://github.com/e-breuninger/terraform-provider-netbox/pull/43) by [@pezhore](https://github.com/pezhore)) ## 0.1.1 (February 15, 2021) ENHANCEMENTS * data-source/netbox_virtual_machines: Add `limit` attribute ([#33](https://github.com/e-breuninger/terraform-provider-netbox/pull/33) by [@jake2184](https://github.com/jake2184)) ## 0.1.0 Ho-Ho-Ho (December 24, 2020) FEATURES * **New Resource:** `netbox_vrf` ([#26](https://github.com/e-breuninger/terraform-provider-netbox/pull/26) by [@rthomson](https://github.com/rthomson)) * **New Data Source:** `netbox_vrf` ([#26](https://github.com/e-breuninger/terraform-provider-netbox/pull/26) by [@rthomson](https://github.com/rthomson)) * **New Resource:** `netbox_cluster_group` * **New Data Source:** `netbox_cluster_group` ENHANCEMENTS * resource/netbox_ip_address: Add `tenant_id` attribute * resource/netbox_cluster: Add `cluster_group_id` attribute ## 0.0.9 (November 20, 2020) FEATURES * **New Data Source:** `netbox_interfaces` ([#9](https://github.com/e-breuninger/terraform-provider-netbox/pull/9) by [@jake2184](https://github.com/jake2184)) BUG FIXES * provider: Honor Sub-Paths in netbox URL ([#15](https://github.com/e-breuninger/terraform-provider-netbox/pull/15) by [@kasimon](https://github.com/kasimon)) ## 0.0.8 (November 19, 2020) FEATURES * **New Data Source:** `netbox_virtual_machines` ([#8](https://github.com/e-breuninger/terraform-provider-netbox/pull/8) by [@jake2184](https://github.com/jake2184)) ================================================ FILE: GNUmakefile ================================================ TEST?=netbox/*.go TEST_FUNC?=TestAccNetboxMACAddr* GOFMT_FILES?=$$(find . -name '*.go' | grep -v vendor) DOCKER_COMPOSE=docker compose export NETBOX_VERSION=v4.4.10 export NETBOX_SERVER_URL=http://localhost:8001 export NETBOX_API_TOKEN=0123456789abcdef0123456789abcdef01234567 export NETBOX_TOKEN=$(NETBOX_API_TOKEN) default: testacc # Run acceptance tests .PHONY: testacc testacc: docker-up @echo "⌛ Startup acceptance tests on $(NETBOX_SERVER_URL) with version $(NETBOX_VERSION)" TF_ACC=1 go test -timeout 20m -v -cover $(TEST) .PHONY: testacc-specific-test testacc-specific-test: # docker-up @echo "⌛ Startup acceptance tests on $(NETBOX_SERVER_URL) with version $(NETBOX_VERSION)" @echo "⌛ Testing function $(TEST_FUNC)" TF_ACC=1 go test -timeout 20m -v -cover $(TEST) -run $(TEST_FUNC) .PHONY: test test: go test $(TEST) $(TESTARGS) -timeout=120s -parallel=4 -cover # Run dockerized Netbox for acceptance testing .PHONY: docker-up docker-up: @echo "⌛ Startup Netbox $(NETBOX_VERSION) and wait for service to become ready" $(DOCKER_COMPOSE) -f docker/docker-compose.yml up --build wait $(DOCKER_COMPOSE) -f docker/docker-compose.yml logs @echo "🚀 Netbox is up and running!" .PHONY: docker-logs docker-logs: $(DOCKER_COMPOSE) -f docker/docker-compose.yml logs .PHONY: docker-down docker-down: $(DOCKER_COMPOSE) -f docker/docker-compose.yml down --volumes .PHONY: docs docs: NETBOX_API_TOKEN="" NETBOX_SERVER_URL="" go generate ./... #! Development # The following make goals are only for local usage .PHONY: fmt fmt: go fmt go fmt netbox/*.go ================================================ FILE: LICENSE ================================================ Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ================================================ FILE: README.md ================================================ # terraform-provider-netbox The Terraform Netbox provider is a plugin for Terraform that allows for the full lifecycle management of [Netbox](https://netboxlabs.com/docs/netbox/) resources. This provider is maintained by E. Breuninger. See: [Official documentation](https://registry.terraform.io/providers/e-breuninger/netbox/latest/docs) in the Terraform registry. ## Requirements - [Terraform](https://www.terraform.io/downloads.html) >= 0.12.x ## Supported netbox versions Netbox often makes breaking API changes even in non-major releases. Check the table below to see which version a provider was tested against. It is generally recommended to use the provider version matching your Netbox version. We aim to always support the latest minor version of Netbox. Since version [1.6.6](https://github.com/e-breuninger/terraform-provider-netbox/commit/0b0b2fffa54d4ab2e5f1677e948b01e56ba211c8), each version of the provider has a built-in list of all Netbox versions it supports at release time. Upon initialization, the provider will probe your Netbox version and include a (non-blocking) warning if the used Netbox version is not supported. | Netbox version | Provider version | | --------------- | ---------------- | | v4.3.0 - 4.4.10 | v5.0.0 and up | | v4.2.2 - 4.2.9 | v4.0.0 - 4.3.1 | | v4.1.0 - 4.1.11 | v3.10.0 - 3.11.1 | | v4.0.0 - 4.0.11 | v3.9.0 - 3.9.2 | | v3.7.0 - 3.7.8 | v3.8.0 - 3.8.9 | | v3.6.0 - 3.6.9 | v3.7.0 - 3.7.7 | | v3.5.1 - 3.5.9 | v3.6.x | | v3.4.3 - 3.4.10 | v3.5.x | | v3.3.0 - 3.4.2 | v3.0.x - 3.5.1 | | v3.2.0 - 3.2.9 | v2.0.x | | v3.1.9 | v1.6.0 - 1.6.7 | | v3.1.3 | v1.1.x - 1.5.2 | | v3.0.9 | v1.0.x | | v2.11.12 | v0.3.x | | v2.10.10 | v0.2.x | | v2.9 | v0.1.x | ## Building The Provider 1. Clone the repository 1. Enter the repository directory 1. Build the provider using the Go `install` command: ```sh go install ``` ## Installation Starting with Terraform 0.13, you can download the provider via the Terraform registry. For further information on how to use third party providers, see the [Terraform documentation](https://www.terraform.io/docs/configuration/providers.html) Releases for all major plattforms are available on the release page. ## Using the provider Here is a short example on how to use this provider: ```hcl provider "netbox" { server_url = "https://demo.netbox.dev" api_token = "" } resource "netbox_platform" "testplatform" { name = "my-test-platform" } ``` For a more examples, see the [provider documentation](https://registry.terraform.io/providers/e-breuninger/netbox/latest/docs). ## Developing the Provider If you wish to work on the provider, you need [Go](http://www.golang.org) installed on your machine. To compile the provider, run `go install`. This will build the provider and put the provider binary in the `$GOPATH/bin` directory. To generate or update documentation, run `make docs`. In order to run the suite of unit tests, run `make test`. In order to run the full suite of acceptance tests, run `make testacc`. In order to run a single specific acceptance test, run ```sh TEST_FUNC= make testacc-specific-test ``` For example: ```sh TEST_FUNC=TestAccNetboxLocationDataSource_basic make testacc-specific-test ``` _Note:_ Acceptance tests create a docker compose stack on port 8001. ```sh make testacc ``` If you notice a failed test, it might be due to a stale netbox data volume. Before concluding there is a problem, refresh the docker containers by running `docker-compose down --volumes` in the `docker` directory. Then run the tests again. If you get `too many open files` errors when running the acceptance test suite locally on Linux, your user limit for open file descriptors might be too low. You can increase that limit with `ulimit -n 2048`. ## Contribution We focus on virtual machine management and IPAM. If you want to contribute more resources to this provider, feel free to make a PR. ================================================ FILE: docker/Dockerfile-wait ================================================ FROM alpine COPY wait-for /usr/local/bin ================================================ FILE: docker/docker-compose.yml ================================================ --- services: postgres: image: postgres:18-alpine environment: - POSTGRES_USER=netbox - POSTGRES_PASSWORD=netbox - POSTGRES_DB=netbox redis: image: docker.io/valkey/valkey:9.0-alpine netbox: image: netboxcommunity/netbox:${NETBOX_VERSION} depends_on: - postgres - redis ports: - 8001:8080 environment: - CORS_ORIGIN_ALLOW_ALL=True - DB_NAME=netbox - DB_USER=netbox - DB_PASSWORD=netbox - DB_HOST=postgres - REDIS_HOST=redis - REDIS_DATABASE=0 - REDIS_SSL=false - REDIS_CACHE_HOST=redis - REDIS_CACHE_DATABASE=1 - REDIS_CACHE_SSL=false - SECRET_KEY=0123456789abcdefghij0123456789abcdefghij0123456789 - SKIP_STARTUP_SCRIPTS=false - SKIP_SUPERUSER=false - SUPERUSER_NAME=admin - SUPERUSER_EMAIL=admin@example.com - SUPERUSER_PASSWORD=admin - SUPERUSER_API_TOKEN=${NETBOX_API_TOKEN} - METRICS_ENABLED=true - API_TOKEN_PEPPER_1=D1)T@tb@cRf(guHO1q+rs&zrXKKA=96uQU3bT_N8t0NNo5mHy2 healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/metrics"] interval: 10s timeout: 10s retries: 10 start_period: 5s wait: build: context: . dockerfile: Dockerfile-wait depends_on: - netbox command: wait-for netbox:8080 --timeout 240 -- echo "Netbox is up and running" ================================================ FILE: docker/wait-for ================================================ #!/bin/sh # Source: https://raw.githubusercontent.com/eficode/wait-for/master/wait-for # The MIT License (MIT) # # Copyright (c) 2017 Eficode Oy # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. VERSION="2.2.4" set -- "$@" -- "$TIMEOUT" "$QUIET" "$PROTOCOL" "$HOST" "$PORT" "$result" TIMEOUT=15 QUIET=0 # The protocol to make the request with, either "tcp" or "http" PROTOCOL="tcp" echoerr() { if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi } usage() { exitcode="$1" cat <&2 Usage: $0 host:port|url [-t timeout] [-- command args] -q | --quiet Do not output any status messages -t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout Defaults to 15 seconds -v | --version Show the version of this tool -- COMMAND ARGS Execute command with args after the test finishes USAGE exit "$exitcode" } wait_for() { case "$PROTOCOL" in tcp) if ! command -v nc >/dev/null; then echoerr 'nc command is missing!' exit 1 fi ;; http) if ! command -v wget >/dev/null; then echoerr 'wget command is missing!' exit 1 fi ;; esac TIMEOUT_END=$(($(date +%s) + TIMEOUT)) while :; do case "$PROTOCOL" in tcp) nc -w 1 -z "$HOST" "$PORT" >/dev/null 2>&1 ;; http) wget --timeout=1 --tries=1 -q "$HOST" -O /dev/null >/dev/null 2>&1 ;; *) echoerr "Unknown protocol '$PROTOCOL'" exit 1 ;; esac result=$? if [ $result -eq 0 ]; then if [ $# -gt 7 ]; then for result in $(seq $(($# - 7))); do result=$1 shift set -- "$@" "$result" done TIMEOUT=$2 QUIET=$3 PROTOCOL=$4 HOST=$5 PORT=$6 result=$7 shift 7 exec "$@" fi exit 0 fi if [ "$TIMEOUT" -ne 0 ] && [ "$(date +%s)" -ge "$TIMEOUT_END" ]; then echo "Operation timed out" >&2 exit 1 fi sleep 1 done } while :; do case "$1" in http://* | https://*) HOST="$1" PROTOCOL="http" shift 1 ;; *:*) HOST=$(printf "%s\n" "$1" | cut -d : -f 1) PORT=$(printf "%s\n" "$1" | cut -d : -f 2) shift 1 ;; -v | --version) echo $VERSION exit ;; -q | --quiet) QUIET=1 shift 1 ;; -q-*) QUIET=0 echoerr "Unknown option: $1" usage 1 ;; -q*) QUIET=1 result=$1 shift 1 set -- -"${result#-q}" "$@" ;; -t | --timeout) TIMEOUT="$2" shift 2 ;; -t*) TIMEOUT="${1#-t}" shift 1 ;; --timeout=*) TIMEOUT="${1#*=}" shift 1 ;; --) shift break ;; --help) usage 0 ;; -*) QUIET=0 echoerr "Unknown option: $1" usage 1 ;; *) QUIET=0 echoerr "Unknown argument: $1" usage 1 ;; esac done if ! [ "$TIMEOUT" -ge 0 ] 2>/dev/null; then echoerr "Error: invalid timeout '$TIMEOUT'" usage 3 fi case "$PROTOCOL" in tcp) if [ "$HOST" = "" ] || [ "$PORT" = "" ]; then echoerr "Error: you need to provide a host and port to test." usage 2 fi ;; http) if [ "$HOST" = "" ]; then echoerr "Error: you need to provide a host to test." usage 2 fi ;; esac wait_for "$@" ================================================ FILE: docs/data-sources/asn.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_asn Data Source - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- --- # netbox_asn (Data Source) ## Example Usage ```terraform data "netbox_asn" "asn_1" { asn = "1111" tag = "tag-1" } data "netbox_asn" "asn_2" { tag = "tag-1" tag__n = "tag-2" } ``` ## Schema ### Optional - `asn` (String) At least one of `asn` or `tag` must be given. - `tag` (String) Tag to include in the data source filter (must match the tag's slug). At least one of `asn` or `tag` must be given. - `tag__n` (String) Tag to exclude from the data source filter (must match the tag's slug). Refer to [Netbox's documentation](https://demo.netbox.dev/static/docs/rest-api/filtering/#lookup-expressions) for more information on available lookup expressions. ### Read-Only - `description` (String) - `id` (Number) The ID of this resource. - `tags` (Set of String) ================================================ FILE: docs/data-sources/asns.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_asns Data Source - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- --- # netbox_asns (Data Source) ## Example Usage ```terraform data "netbox_asns" "asns" { filter { name = "asn__gte" value = "1000" } filter { name = "asn__lte" value = "2000" } } ``` ## Schema ### Optional - `filter` (Block Set) (see [below for nested schema](#nestedblock--filter)) - `limit` (Number) Defaults to `0`. ### Read-Only - `asns` (List of Object) (see [below for nested schema](#nestedatt--asns)) - `id` (String) The ID of this resource. ### Nested Schema for `filter` Required: - `name` (String) - `value` (String) ### Nested Schema for `asns` Read-Only: - `asn` (Number) - `id` (Number) - `rir_id` (Number) - `tags` (Set of String) ================================================ FILE: docs/data-sources/available_prefix.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_available_prefix Data Source - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- --- # netbox_available_prefix (Data Source) ## Schema ### Required - `prefix_id` (Number) ### Read-Only - `id` (String) The ID of this resource. - `prefixes_available` (List of Object) (see [below for nested schema](#nestedatt--prefixes_available)) ### Nested Schema for `prefixes_available` Read-Only: - `family` (Number) - `prefix` (String) - `vrf_id` (Number) ================================================ FILE: docs/data-sources/cluster.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_cluster Data Source - terraform-provider-netbox" subcategory: "Virtualization" description: |- --- # netbox_cluster (Data Source) ## Example Usage ```terraform data "netbox_cluster" "vmw_cluster_01" { name = "vmw-cluster-01" } ``` ## Schema ### Optional - `cluster_group_id` (Number) - `id` (String) At least one of `name`, `site_id` or `id` must be given. - `name` (String) At least one of `name`, `site_id` or `id` must be given. - `site_id` (Number) At least one of `name`, `site_id` or `id` must be given. ### Read-Only - `cluster_id` (Number) - `cluster_type_id` (Number) - `comments` (String) - `custom_fields` (Map of String) - `description` (String) - `location_id` (Number) - `region_id` (Number) - `scope_id` (Number) - `scope_type` (String) - `site_group_id` (Number) - `tags` (Set of String) ================================================ FILE: docs/data-sources/cluster_group.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_cluster_group Data Source - terraform-provider-netbox" subcategory: "Virtualization" description: |- --- # netbox_cluster_group (Data Source) ## Example Usage ```terraform data "netbox_cluster_group" "dc_west" { name = "dc-west" } ``` ## Schema ### Required - `name` (String) ### Read-Only - `cluster_group_id` (Number) - `id` (String) The ID of this resource. ================================================ FILE: docs/data-sources/cluster_type.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_cluster_type Data Source - terraform-provider-netbox" subcategory: "Virtualization" description: |- --- # netbox_cluster_type (Data Source) ## Schema ### Required - `name` (String) ### Read-Only - `cluster_type_id` (Number) - `id` (String) The ID of this resource. ================================================ FILE: docs/data-sources/clusters.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_clusters Data Source - terraform-provider-netbox" subcategory: "Virtualization" description: |- --- # netbox_clusters (Data Source) ## Example Usage ```terraform // Get all clusters of a specific type data "netbox_cluster_type" "vmware" { name = "VMware ESXi" } data "netbox_clusters" "vmware_clusters" { filter { name = "cluster_type_id" value = data.netbox_cluster_type.vmware.id } } // Get clusters by name regex data "netbox_clusters" "prod_clusters" { name_regex = "prod-.*" } // Get clusters at a specific site data "netbox_clusters" "site_clusters" { filter { name = "site_id" value = data.netbox_site.main.id } } ``` ## Schema ### Optional - `filter` (Block Set) (see [below for nested schema](#nestedblock--filter)) - `limit` (Number) Defaults to `0`. - `name_regex` (String) ### Read-Only - `clusters` (List of Object) (see [below for nested schema](#nestedatt--clusters)) - `id` (String) The ID of this resource. ### Nested Schema for `filter` Required: - `name` (String) - `value` (String) ### Nested Schema for `clusters` Read-Only: - `cluster_group_id` (Number) - `cluster_id` (Number) - `cluster_type_id` (Number) - `comments` (String) - `custom_fields` (Map of String) - `description` (String) - `location_id` (Number) - `name` (String) - `region_id` (Number) - `scope_id` (Number) - `scope_type` (String) - `site_group_id` (Number) - `site_id` (Number) - `tags` (Set of String) - `tenant_id` (Number) ================================================ FILE: docs/data-sources/config_context.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_config_context Data Source - terraform-provider-netbox" subcategory: "Extras" description: |- --- # netbox_config_context (Data Source) ## Schema ### Required - `name` (String) ### Read-Only - `cluster_groups` (List of Number) - `cluster_types` (List of Number) - `clusters` (List of Number) - `data` (String) - `description` (String) - `device_types` (List of Number) - `id` (String) The ID of this resource. - `locations` (List of Number) - `platforms` (List of Number) - `regions` (List of Number) - `roles` (List of Number) - `site_groups` (List of Number) - `sites` (List of Number) - `tags` (List of String) - `tenant_groups` (List of Number) - `tenants` (List of Number) - `weight` (Number) ================================================ FILE: docs/data-sources/contact.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_contact Data Source - terraform-provider-netbox" subcategory: "Tenancy" description: |- --- # netbox_contact (Data Source) ## Schema ### Optional - `description` (String) - `name` (String) At least one of `name` or `slug` must be given. - `slug` (String) At least one of `name` or `slug` must be given. ### Read-Only - `group_id` (Number) - `id` (String) The ID of this resource. ================================================ FILE: docs/data-sources/contact_group.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_contact_group Data Source - terraform-provider-netbox" subcategory: "Tenancy" description: |- --- # netbox_contact_group (Data Source) ## Schema ### Required - `name` (String) ### Read-Only - `description` (String) - `id` (String) The ID of this resource. - `parent_id` (Number) - `slug` (String) ================================================ FILE: docs/data-sources/contact_role.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_contact_role Data Source - terraform-provider-netbox" subcategory: "Tenancy" description: |- --- # netbox_contact_role (Data Source) ## Schema ### Optional - `name` (String) At least one of `name` or `slug` must be given. - `slug` (String) At least one of `name` or `slug` must be given. ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/data-sources/device_interfaces.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_device_interfaces Data Source - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- --- # netbox_device_interfaces (Data Source) ## Schema ### Optional - `filter` (Block Set) (see [below for nested schema](#nestedblock--filter)) - `limit` (Number) Defaults to `0`. - `name_regex` (String) ### Read-Only - `id` (String) The ID of this resource. - `interfaces` (List of Object) (see [below for nested schema](#nestedatt--interfaces)) ### Nested Schema for `filter` Required: - `name` (String) - `value` (String) ### Nested Schema for `interfaces` Read-Only: - `description` (String) - `device_id` (Number) - `enabled` (Boolean) - `id` (Number) - `mac_address` (String) - `mac_addresses` (Set of Object) (see [below for nested schema](#nestedobjatt--interfaces--mac_addresses)) - `mode` (Map of String) - `mtu` (Number) - `name` (String) - `tag_ids` (List of Number) - `tagged_vlans` (List of Object) (see [below for nested schema](#nestedobjatt--interfaces--tagged_vlans)) - `type` (String) - `untagged_vlan` (List of Object) (see [below for nested schema](#nestedobjatt--interfaces--untagged_vlan)) ### Nested Schema for `interfaces.mac_addresses` Read-Only: - `description` (String) - `id` (Number) - `mac_address` (String) ### Nested Schema for `interfaces.tagged_vlans` Read-Only: - `id` (Number) - `name` (String) - `vid` (Number) ### Nested Schema for `interfaces.untagged_vlan` Read-Only: - `id` (Number) - `name` (String) - `vid` (Number) ================================================ FILE: docs/data-sources/device_power_ports.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_device_power_ports Data Source - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- --- # netbox_device_power_ports (Data Source) ## Schema ### Optional - `filter` (Block Set) (see [below for nested schema](#nestedblock--filter)) - `limit` (Number) Defaults to `0`. - `name_regex` (String) ### Read-Only - `id` (String) The ID of this resource. - `power_ports` (List of Object) (see [below for nested schema](#nestedatt--power_ports)) ### Nested Schema for `filter` Required: - `name` (String) - `value` (String) ### Nested Schema for `power_ports` Read-Only: - `allocated_draw` (Number) - `description` (String) - `device_id` (Number) - `id` (Number) - `maximum_draw` (Number) - `module_id` (Number) - `name` (String) - `tag_ids` (List of Number) - `type` (String) ================================================ FILE: docs/data-sources/device_render_config.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_device_render_config Data Source - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- Render the configuration template assigned to a device using the device's config context. --- # netbox_device_render_config (Data Source) Render the configuration template assigned to a device using the device's config context. ## Example Usage ```terraform # Get the rendered configuration for a device data "netbox_device_render_config" "server_config" { device_id = 60 } # Use the rendered configuration output "rendered_config" { value = data.netbox_device_render_config.server_config.content } output "template_used" { value = data.netbox_device_render_config.server_config.config_template_name } # Example: Write the config to a file using local_file resource # resource "local_file" "kickstart" { # content = data.netbox_device_render_config.server_config.content # filename = "${path.module}/kickstart.cfg" # } ``` ## Schema ### Required - `device_id` (Number) The ID of the device to render configuration for. ### Read-Only - `config_template_id` (Number) The ID of the config template that was used for rendering. - `config_template_name` (String) The name of the config template that was used for rendering. - `content` (String) The rendered configuration content. - `id` (String) The ID of this resource. ================================================ FILE: docs/data-sources/device_role.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_device_role Data Source - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- --- # netbox_device_role (Data Source) ## Example Usage ```terraform data "netbox_device_role" "core_sw" { name = "core-sw" } ``` ## Schema ### Required - `name` (String) ### Read-Only - `color_hex` (String) - `id` (String) The ID of this resource. - `slug` (String) - `tags` (Set of String) ================================================ FILE: docs/data-sources/device_type.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_device_type Data Source - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- --- # netbox_device_type (Data Source) ## Example Usage ```terraform # Get device type by model name data "netbox_device_type" "ex1" { model = "7210 SAS-Sx 10/100GE" } # Get device type by slug data "netbox_device_type" "ex2" { slug = "7210-sas-sx-10-100GE" } # Get device type by manufacturer and part number information data "netbox_device_type" "ex3" { manufacturer = "Nokia" part_number = "3HE11597AARB01" } ``` ## Schema ### Optional - `manufacturer` (String) - `model` (String) - `part_number` (String) - `slug` (String) - `subdevice_role` (String) ### Read-Only - `id` (String) The ID of this resource. - `is_full_depth` (Boolean) - `manufacturer_id` (Number) - `u_height` (Number) ================================================ FILE: docs/data-sources/devices.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_devices Data Source - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- --- # netbox_devices (Data Source) ## Schema ### Optional - `filter` (Block Set) (see [below for nested schema](#nestedblock--filter)) - `limit` (Number) Defaults to `0`. - `name_regex` (String) ### Read-Only - `devices` (List of Object) (see [below for nested schema](#nestedatt--devices)) - `id` (String) The ID of this resource. ### Nested Schema for `filter` Required: - `name` (String) - `value` (String) ### Nested Schema for `devices` Read-Only: - `asset_tag` (String) - `cluster_id` (Number) - `comments` (String) - `config_context` (String) - `custom_fields` (Map of String) - `description` (String) - `device_id` (Number) - `device_type_id` (Number) - `local_context_data` (String) - `location_id` (Number) - `manufacturer_id` (Number) - `model` (String) - `name` (String) - `oob_ip` (String) - `platform_id` (Number) - `primary_ipv4` (String) - `primary_ipv6` (String) - `rack_face` (String) - `rack_id` (Number) - `rack_position` (Number) - `role_id` (Number) - `serial` (String) - `site_id` (Number) - `status` (String) - `tags` (Set of String) - `tenant_id` (Number) ================================================ FILE: docs/data-sources/interfaces.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_interfaces Data Source - terraform-provider-netbox" subcategory: "Virtualization" description: |- --- # netbox_interfaces (Data Source) ## Example Usage ```terraform data "netbox_interfaces" "myvm_eth0" { name_regex = "eth0" filter { name = "name" value = "myvm" } } ``` ## Schema ### Optional - `filter` (Block Set) (see [below for nested schema](#nestedblock--filter)) - `limit` (Number) The limit of objects to return from the API lookup. Defaults to `0`. - `name_regex` (String) ### Read-Only - `id` (String) The ID of this resource. - `interfaces` (List of Object) (see [below for nested schema](#nestedatt--interfaces)) ### Nested Schema for `filter` Required: - `name` (String) - `value` (String) ### Nested Schema for `interfaces` Read-Only: - `description` (String) - `enabled` (Boolean) - `id` (Number) - `mac_address` (String) - `mode` (Map of String) - `mtu` (Number) - `name` (String) - `tag_ids` (List of Number) - `tagged_vlans` (List of Object) (see [below for nested schema](#nestedobjatt--interfaces--tagged_vlans)) - `untagged_vlan` (List of Object) (see [below for nested schema](#nestedobjatt--interfaces--untagged_vlan)) - `vm_id` (Number) ### Nested Schema for `interfaces.tagged_vlans` Read-Only: - `id` (Number) - `name` (String) - `vid` (Number) ### Nested Schema for `interfaces.untagged_vlan` Read-Only: - `id` (Number) - `name` (String) - `vid` (Number) ================================================ FILE: docs/data-sources/ip_address.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_ip_address Data Source - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- --- # netbox_ip_address (Data Source) ## Example Usage ```terraform data "netbox_ip_address" "ip_address" { id = 1001 } ``` ## Schema ### Read-Only - `address_family` (String) - `created` (String) - `custom_fields` (Map of String) - `description` (String) - `dns_name` (String) - `id` (Number) The ID of this resource. - `ip_address` (String) - `last_updated` (String) - `role` (String) - `status` (String) - `tags` (List of Object) (see [below for nested schema](#nestedatt--tags)) - `tenant` (List of Object) (see [below for nested schema](#nestedatt--tenant)) ### Nested Schema for `tags` Read-Only: - `display` (String) - `id` (Number) - `name` (String) - `slug` (String) ### Nested Schema for `tenant` Read-Only: - `id` (Number) - `name` (String) - `slug` (String) ================================================ FILE: docs/data-sources/ip_addresses.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_ip_addresses Data Source - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- --- # netbox_ip_addresses (Data Source) ## Schema ### Optional - `custom_fields` (Map of String) - `filter` (Block Set) (see [below for nested schema](#nestedblock--filter)) - `limit` (Number) Defaults to `0`. ### Read-Only - `id` (String) The ID of this resource. - `ip_addresses` (List of Object) (see [below for nested schema](#nestedatt--ip_addresses)) ### Nested Schema for `filter` Required: - `name` (String) - `value` (String) ### Nested Schema for `ip_addresses` Read-Only: - `address_family` (String) - `created` (String) - `custom_fields` (Map of String) - `description` (String) - `dns_name` (String) - `id` (Number) - `ip_address` (String) - `last_updated` (String) - `role` (String) - `status` (String) - `tags` (List of Object) (see [below for nested schema](#nestedobjatt--ip_addresses--tags)) - `tenant` (List of Object) (see [below for nested schema](#nestedobjatt--ip_addresses--tenant)) ### Nested Schema for `ip_addresses.tags` Read-Only: - `display` (String) - `id` (Number) - `name` (String) - `slug` (String) ### Nested Schema for `ip_addresses.tenant` Read-Only: - `id` (Number) - `name` (String) - `slug` (String) ================================================ FILE: docs/data-sources/ip_range.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_ip_range Data Source - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- --- # netbox_ip_range (Data Source) ## Example Usage ```terraform data "netbox_ip_range" "cust_a_prod" { contains = "10.0.0.1/24" } ``` ## Schema ### Optional - `contains` (String) At least one of `contains`, `family`, `vrf_id`, `tenant_id`, `status`, `role_id`, `description` or `tag` must be given. - `description` (String) Description to include in the data source filter. At least one of `contains`, `family`, `vrf_id`, `tenant_id`, `status`, `role_id`, `description` or `tag` must be given. - `family` (Number) The IP family of the IP range. One of 4 or 6. At least one of `contains`, `family`, `vrf_id`, `tenant_id`, `status`, `role_id`, `description` or `tag` must be given. - `role_id` (Number) At least one of `contains`, `family`, `vrf_id`, `tenant_id`, `status`, `role_id`, `description` or `tag` must be given. - `status` (String) At least one of `contains`, `family`, `vrf_id`, `tenant_id`, `status`, `role_id`, `description` or `tag` must be given. - `tag` (String) Tag to include in the data source filter (must match the tag's slug). At least one of `contains`, `family`, `vrf_id`, `tenant_id`, `status`, `role_id`, `description` or `tag` must be given. - `tag__n` (String) Tag to exclude from the data source filter (must match the tag's slug). Refer to [Netbox's documentation](https://netboxlabs.com/docs/netbox/reference/filtering/#lookup-expressions) for more information on available lookup expressions. - `tenant_id` (Number) At least one of `contains`, `family`, `vrf_id`, `tenant_id`, `status`, `role_id`, `description` or `tag` must be given. - `vrf_id` (Number) At least one of `contains`, `family`, `vrf_id`, `tenant_id`, `status`, `role_id`, `description` or `tag` must be given. ### Read-Only - `end_address` (String) - `id` (Number) The ID of this resource. - `start_address` (String) - `tags` (Set of String) ================================================ FILE: docs/data-sources/ip_ranges.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_ip_ranges Data Source - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- --- # netbox_ip_ranges (Data Source) ## Schema ### Optional - `filter` (Block Set) (see [below for nested schema](#nestedblock--filter)) - `limit` (Number) Defaults to `0`. ### Read-Only - `id` (String) The ID of this resource. - `ip_ranges` (List of Object) (see [below for nested schema](#nestedatt--ip_ranges)) ### Nested Schema for `filter` Required: - `name` (String) - `value` (String) ### Nested Schema for `ip_ranges` Read-Only: - `address_family` (String) - `created` (String) - `custom_fields` (Map of String) - `description` (String) - `end_address` (String) - `id` (Number) - `last_updated` (String) - `role` (String) - `start_address` (String) - `status` (String) - `tags` (List of Object) (see [below for nested schema](#nestedobjatt--ip_ranges--tags)) - `tenant` (List of Object) (see [below for nested schema](#nestedobjatt--ip_ranges--tenant)) ### Nested Schema for `ip_ranges.tags` Read-Only: - `display` (String) - `id` (Number) - `name` (String) - `slug` (String) ### Nested Schema for `ip_ranges.tenant` Read-Only: - `id` (Number) - `name` (String) - `slug` (String) ================================================ FILE: docs/data-sources/ipam_role.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_ipam_role Data Source - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- --- # netbox_ipam_role (Data Source) ## Schema ### Required - `name` (String) ### Read-Only - `description` (String) - `id` (String) The ID of this resource. - `slug` (String) - `weight` (Number) ================================================ FILE: docs/data-sources/location.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_location Data Source - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- --- # netbox_location (Data Source) ## Schema ### Optional - `name` (String) - `parent_id` (Number) - `site_id` (Number) - `slug` (String) ### Read-Only - `description` (String) - `id` (String) The ID of this resource. - `status` (String) - `tenant_id` (Number) ================================================ FILE: docs/data-sources/locations.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_locations Data Source - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- --- # netbox_locations (Data Source) ## Schema ### Optional - `filter` (Block Set) A list of filter to apply to the API query when requesting locations. (see [below for nested schema](#nestedblock--filter)) - `limit` (Number) The limit of objects to return from the API lookup. Defaults to `0`. - `tags` (Set of String) A list of tags to filter on. ### Read-Only - `id` (String) The ID of this resource. - `locations` (List of Object) (see [below for nested schema](#nestedatt--locations)) ### Nested Schema for `filter` Required: - `name` (String) The name of the field to filter on. Supported fields are: . - `value` (String) The value to pass to the specified filter. ### Nested Schema for `locations` Read-Only: - `description` (String) - `facility` (String) - `id` (String) - `name` (String) - `parent_id` (Number) - `site_id` (Number) - `slug` (String) - `status` (String) - `tenant_id` (Number) ================================================ FILE: docs/data-sources/platform.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_platform Data Source - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- --- # netbox_platform (Data Source) ## Example Usage ```terraform data "netbox_platform" "PANOS" { name = "PANOS" } ``` ## Schema ### Required - `name` (String) ### Optional - `manufacturer_id` (Number) ### Read-Only - `id` (String) The ID of this resource. - `slug` (String) ================================================ FILE: docs/data-sources/prefix.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_prefix Data Source - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- --- # netbox_prefix (Data Source) ## Schema ### Optional - `cidr` (String, Deprecated) At least one of `description`, `family`, `prefix`, `vlan_vid`, `vrf_id`, `vlan_id`, `tenant_id`, `site_id`, `role_id`, `cidr`, `custom_fields`, `tag` or `status` must be given. Conflicts with `prefix`. - `custom_fields` (Map of String) At least one of `description`, `family`, `prefix`, `vlan_vid`, `vrf_id`, `vlan_id`, `tenant_id`, `site_id`, `role_id`, `cidr`, `custom_fields`, `tag` or `status` must be given. - `description` (String) Description to include in the data source filter. At least one of `description`, `family`, `prefix`, `vlan_vid`, `vrf_id`, `vlan_id`, `tenant_id`, `site_id`, `role_id`, `cidr`, `custom_fields`, `tag` or `status` must be given. - `family` (Number) The IP family of the prefix. One of 4 or 6. At least one of `description`, `family`, `prefix`, `vlan_vid`, `vrf_id`, `vlan_id`, `tenant_id`, `site_id`, `role_id`, `cidr`, `custom_fields`, `tag` or `status` must be given. - `prefix` (String) At least one of `description`, `family`, `prefix`, `vlan_vid`, `vrf_id`, `vlan_id`, `tenant_id`, `site_id`, `role_id`, `cidr`, `custom_fields`, `tag` or `status` must be given. Conflicts with `cidr`. - `role_id` (Number) At least one of `description`, `family`, `prefix`, `vlan_vid`, `vrf_id`, `vlan_id`, `tenant_id`, `site_id`, `role_id`, `cidr`, `custom_fields`, `tag` or `status` must be given. - `site_id` (Number) At least one of `description`, `family`, `prefix`, `vlan_vid`, `vrf_id`, `vlan_id`, `tenant_id`, `site_id`, `role_id`, `cidr`, `custom_fields`, `tag` or `status` must be given. - `status` (String) At least one of `description`, `family`, `prefix`, `vlan_vid`, `vrf_id`, `vlan_id`, `tenant_id`, `site_id`, `role_id`, `cidr`, `custom_fields`, `tag` or `status` must be given. - `tag` (String) Tag to include in the data source filter (must match the tag's slug). At least one of `description`, `family`, `prefix`, `vlan_vid`, `vrf_id`, `vlan_id`, `tenant_id`, `site_id`, `role_id`, `cidr`, `custom_fields`, `tag` or `status` must be given. - `tag__n` (String) Tag to exclude from the data source filter (must match the tag's slug). Refer to [Netbox's documentation](https://demo.netbox.dev/static/docs/rest-api/filtering/#lookup-expressions) for more information on available lookup expressions. - `tenant_id` (Number) At least one of `description`, `family`, `prefix`, `vlan_vid`, `vrf_id`, `vlan_id`, `tenant_id`, `site_id`, `role_id`, `cidr`, `custom_fields`, `tag` or `status` must be given. - `vlan_id` (Number) At least one of `description`, `family`, `prefix`, `vlan_vid`, `vrf_id`, `vlan_id`, `tenant_id`, `site_id`, `role_id`, `cidr`, `custom_fields`, `tag` or `status` must be given. - `vlan_vid` (Number) At least one of `description`, `family`, `prefix`, `vlan_vid`, `vrf_id`, `vlan_id`, `tenant_id`, `site_id`, `role_id`, `cidr`, `custom_fields`, `tag` or `status` must be given. - `vrf_id` (Number) At least one of `description`, `family`, `prefix`, `vlan_vid`, `vrf_id`, `vlan_id`, `tenant_id`, `site_id`, `role_id`, `cidr`, `custom_fields`, `tag` or `status` must be given. ### Read-Only - `id` (Number) The ID of this resource. - `location_id` (Number) - `region_id` (Number) - `site_group_id` (Number) - `tags` (Set of String) ================================================ FILE: docs/data-sources/prefixes.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_prefixes Data Source - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- --- # netbox_prefixes (Data Source) ## Schema ### Optional - `filter` (Block Set) A list of filters to apply to the API query when requesting prefixes. (see [below for nested schema](#nestedblock--filter)) - `limit` (Number) The limit of objects to return from the API lookup. Defaults to `0`. ### Read-Only - `id` (String) The ID of this resource. - `prefixes` (List of Object) (see [below for nested schema](#nestedatt--prefixes)) ### Nested Schema for `filter` Required: - `name` (String) The name of the field to filter on. Supported fields are: `prefix`, `contains`, `vlan_vid`, `vrf_id`, `vlan_id`, `status`, `tenant_id`, `site_id`, `description` & `tag`. - `value` (String) The value to pass to the specified filter. ### Nested Schema for `prefixes` Read-Only: - `custom_fields` (Map of String) - `description` (String) - `id` (Number) - `location_id` (Number) - `prefix` (String) - `region_id` (Number) - `site_group_id` (Number) - `site_id` (Number) - `status` (String) - `tags` (Set of String) - `tenant_id` (Number) - `vlan_id` (Number) - `vlan_vid` (Number) - `vrf_id` (Number) ================================================ FILE: docs/data-sources/rack_role.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_rack_role Data Source - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- --- # netbox_rack_role (Data Source) ## Schema ### Required - `name` (String) ### Read-Only - `color_hex` (String) - `description` (String) - `id` (String) The ID of this resource. - `slug` (String) - `tags` (Set of String) ================================================ FILE: docs/data-sources/racks.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_racks Data Source - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- --- # netbox_racks (Data Source) ## Schema ### Optional - `filter` (Block Set) (see [below for nested schema](#nestedblock--filter)) - `limit` (Number) Defaults to `0`. ### Read-Only - `id` (String) The ID of this resource. - `racks` (List of Object) (see [below for nested schema](#nestedatt--racks)) ### Nested Schema for `filter` Required: - `name` (String) - `value` (String) ### Nested Schema for `racks` Read-Only: - `asset_tag` (String) - `comments` (String) - `custom_fields` (Map of String) - `desc_units` (Boolean) - `description` (String) - `facility_id` (String) - `id` (Number) - `location_id` (Number) - `max_weight` (Number) - `mounting_depth` (Number) - `name` (String) - `outer_depth` (Number) - `outer_unit` (String) - `outer_width` (Number) - `role_id` (Number) - `serial` (String) - `site_id` (Number) - `status` (String) - `tags` (Set of String) - `tenant_id` (Number) - `type_id` (Number) - `u_height` (Number) - `weight` (Number) - `weight_unit` (String) - `width` (Number) ================================================ FILE: docs/data-sources/region.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_region Data Source - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- --- # netbox_region (Data Source) ## Schema ### Optional - `filter` (Block Set) (see [below for nested schema](#nestedblock--filter)) ### Read-Only - `description` (String) - `id` (Number) The ID of this resource. - `name` (String) - `parent_region_id` (Number) - `slug` (String) ### Nested Schema for `filter` Optional: - `name` (String) - `slug` (String) Read-Only: - `id` (Number) The ID of this resource. ================================================ FILE: docs/data-sources/rir.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_rir Data Source - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/ipam/#regional-internet-registries-rirs: Regional Internet registries are responsible for the allocation of globally-routable address space. The five RIRs are ARIN, RIPE, APNIC, LACNIC, and AFRINIC. However, some address space has been set aside for internal use, such as defined in RFCs 1918 and 6598. NetBox considers these RFCs as a sort of RIR as well; that is, an authority which "owns" certain address space. --- # netbox_rir (Data Source) From the [official documentation](https://docs.netbox.dev/en/stable/features/ipam/#regional-internet-registries-rirs): > Regional Internet registries are responsible for the allocation of globally-routable address space. The five RIRs are ARIN, RIPE, APNIC, LACNIC, and AFRINIC. However, some address space has been set aside for internal use, such as defined in RFCs 1918 and 6598. NetBox considers these RFCs as a sort of RIR as well; that is, an authority which "owns" certain address space. ## Example Usage ```terraform data "netbox_rir" "rir_1" { name = "ARIN" } data "netbox_rir" "rir_2" { slug = "arin" } ``` ## Schema ### Optional - `name` (String) At least one of `name` or `slug` must be given. - `slug` (String) At least one of `name` or `slug` must be given. ### Read-Only - `description` (String) - `id` (Number) The ID of this resource. - `is_private` (Boolean) ================================================ FILE: docs/data-sources/route_target.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_route_target Data Source - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- --- # netbox_route_target (Data Source) ## Schema ### Required - `name` (String) ### Optional - `tags` (Set of String) ### Read-Only - `description` (String) - `id` (String) The ID of this resource. - `tenant_id` (Number) ================================================ FILE: docs/data-sources/site.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_site Data Source - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- --- # netbox_site (Data Source) ## Example Usage ```terraform data "netbox_site" "get_by_name" { name = "Example Site 1" } data "netbox_site" "get_by_slug" { slug = "example-site-1" } ``` ## Schema ### Optional - `facility` (String) - `name` (String) - `slug` (String) ### Read-Only - `asn_ids` (Set of Number) - `comments` (String) - `description` (String) - `group_id` (Number) - `id` (String) The ID of this resource. - `physical_address` (String) - `region_id` (Number) - `site_id` (Number) - `status` (String) - `tenant_id` (Number) - `time_zone` (String) ================================================ FILE: docs/data-sources/site_group.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_site_group Data Source - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- --- # netbox_site_group (Data Source) ## Example Usage ```terraform // Assumes the corresponding site groups exist data "netbox_site_group" "get_by_name" { name = "example-sitegroup-1" } data "netbox_site_group" "get_by_slug" { slug = "sitegrp" } ``` ## Schema ### Optional - `name` (String) At least one of `name` or `slug` must be given. - `slug` (String) At least one of `name` or `slug` must be given. ### Read-Only - `description` (String) - `id` (String) The ID of this resource. ================================================ FILE: docs/data-sources/tag.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_tag Data Source - terraform-provider-netbox" subcategory: "Extras" description: |- --- # netbox_tag (Data Source) ## Example Usage ```terraform data "netbox_tag" "dmz" { name = "DMZ" } ``` ## Schema ### Required - `name` (String) ### Optional - `description` (String) ### Read-Only - `id` (String) The ID of this resource. - `slug` (String) ================================================ FILE: docs/data-sources/tags.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_tags Data Source - terraform-provider-netbox" subcategory: "Extras" description: |- --- # netbox_tags (Data Source) ## Example Usage ```terraform data "netbox_tags" "all_tags" { } data "netbox_tags" "ansible_tags" { filter { name = "name__isw" value = "ansible_" } } data "netbox_tags" "not_ansible_tags" { filter { name = "name__nisw" value = "ansible_" } } ``` ## Schema ### Optional - `filter` (Block Set) (see [below for nested schema](#nestedblock--filter)) - `limit` (Number) Defaults to `0`. ### Read-Only - `id` (String) The ID of this resource. - `tags` (List of Object) (see [below for nested schema](#nestedatt--tags)) ### Nested Schema for `filter` Required: - `name` (String) - `value` (String) ### Nested Schema for `tags` Read-Only: - `color` (String) - `description` (String) - `name` (String) - `slug` (String) - `tag_id` (Number) ================================================ FILE: docs/data-sources/tenant.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_tenant Data Source - terraform-provider-netbox" subcategory: "Tenancy" description: |- --- # netbox_tenant (Data Source) ## Example Usage ```terraform data "netbox_tenant" "customer_a" { name = "Customer A" } ``` ## Schema ### Optional - `description` (String) - `name` (String) At least one of `name` or `slug` must be given. - `slug` (String) At least one of `name` or `slug` must be given. ### Read-Only - `group_id` (Number) - `id` (String) The ID of this resource. ================================================ FILE: docs/data-sources/tenant_group.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_tenant_group Data Source - terraform-provider-netbox" subcategory: "Tenancy" description: |- --- # netbox_tenant_group (Data Source) ## Schema ### Required - `name` (String) ### Read-Only - `description` (String) - `id` (String) The ID of this resource. - `parent_id` (Number) - `slug` (String) ================================================ FILE: docs/data-sources/tenants.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_tenants Data Source - terraform-provider-netbox" subcategory: "Tenancy" description: |- --- # netbox_tenants (Data Source) ## Schema ### Optional - `filter` (Block Set) (see [below for nested schema](#nestedblock--filter)) - `limit` (Number) Defaults to `0`. ### Read-Only - `id` (String) The ID of this resource. - `tenants` (List of Object) (see [below for nested schema](#nestedatt--tenants)) ### Nested Schema for `filter` Required: - `name` (String) - `value` (String) ### Nested Schema for `tenants` Read-Only: - `circuit_count` (Number) - `cluster_count` (Number) - `comments` (String) - `created` (String) - `custom_fields` (Map of String) - `description` (String) - `device_count` (Number) - `id` (Number) - `ip_address_count` (Number) - `last_updated` (String) - `name` (String) - `prefix_count` (Number) - `rack_count` (Number) - `site_count` (Number) - `slug` (String) - `tenant_group` (List of Object) (see [below for nested schema](#nestedobjatt--tenants--tenant_group)) - `vlan_count` (Number) - `vm_count` (Number) - `vrf_count` (Number) ### Nested Schema for `tenants.tenant_group` Read-Only: - `id` (Number) - `name` (String) - `slug` (String) - `tenant_count` (Number) ================================================ FILE: docs/data-sources/virtual_disk.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_virtual_disk Data Source - terraform-provider-netbox" subcategory: "Virtualization" description: |- --- # netbox_virtual_disk (Data Source) ## Example Usage ```terraform # Filter by name data "netbox_virtual_disk" "disk_by_name" { filter { name = "name" value = "disk1" } } # Filter by tag data "netbox_virtual_disk" "disk_by_tag" { filter { name = "tag" value = "production" } } # Multiple filters data "netbox_virtual_disk" "disk_filtered" { filter { name = "name" value = "disk1" } filter { name = "tag" value = "production" } } # Filter with name regex data "netbox_virtual_disk" "disk_regex" { name_regex = "^disk[0-9]+" limit = 10 } ``` ## Schema ### Optional - `filter` (Block List) (see [below for nested schema](#nestedblock--filter)) - `limit` (Number) Defaults to `0`. - `name_regex` (String) ### Read-Only - `id` (String) The ID of this resource. - `virtual_disks` (List of Object) (see [below for nested schema](#nestedatt--virtual_disks)) ### Nested Schema for `filter` Required: - `name` (String) - `value` (String) ### Nested Schema for `virtual_disks` Read-Only: - `custom_fields` (Map of String) - `description` (String) - `id` (Number) - `name` (String) - `size_mb` (Number) - `tags` (List of String) - `virtual_machine_id` (Number) ================================================ FILE: docs/data-sources/virtual_machines.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_virtual_machines Data Source - terraform-provider-netbox" subcategory: "Virtualization" description: |- --- # netbox_virtual_machines (Data Source) ## Example Usage ```terraform // Assumes vmw-cluster-01 exists as a cluster in Netbox data "netbox_cluster" "vmw_cluster_01" { name = "vmw-cluster-01" } data "netbox_virtual_machines" "base_vm" { name_regex = "myvm-1" filter { name = "cluster_id" value = data.netbox_cluster.vmw_cluster_01.id } } ``` ## Schema ### Optional - `filter` (Block Set) (see [below for nested schema](#nestedblock--filter)) - `limit` (Number) Defaults to `0`. - `name_regex` (String) ### Read-Only - `id` (String) The ID of this resource. - `vms` (List of Object) (see [below for nested schema](#nestedatt--vms)) ### Nested Schema for `filter` Required: - `name` (String) - `value` (String) ### Nested Schema for `vms` Read-Only: - `cluster_id` (Number) - `comments` (String) - `config_context` (String) - `custom_fields` (Map of String) - `description` (String) - `device_id` (Number) - `device_name` (String) - `disk_size_mb` (Number) - `local_context_data` (String) - `memory_mb` (Number) - `name` (String) - `platform_id` (Number) - `platform_name` (String) - `platform_slug` (String) - `primary_ip` (String) - `primary_ip4` (String) - `primary_ip6` (String) - `role_id` (Number) - `site_id` (Number) - `status` (String) - `tag_ids` (List of Number) - `tenant_id` (Number) - `vcpus` (Number) - `vm_id` (Number) ================================================ FILE: docs/data-sources/vlan.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_vlan Data Source - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- --- # netbox_vlan (Data Source) ## Example Usage ```terraform # Get VLAN by name data "netbox_vlan" "vlan1" { name = "vlan-1" } # Get VLAN by VID and IPAM role ID data "netbox_vlan" "vlan2" { vid = 1234 role = netbox_ipam_role.example.id } # Get VLAN by name and tenant ID data "netbox_vlan" "vlan3" { name = "vlan-3" tenant = netbox_tenant.example.id } ``` ## Schema ### Optional - `group_id` (Number) - `name` (String) - `role` (Number) - `tenant` (Number) - `vid` (Number) ### Read-Only - `description` (String) - `id` (String) The ID of this resource. - `site` (Number) - `status` (String) ================================================ FILE: docs/data-sources/vlan_group.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_vlan_group Data Source - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- --- # netbox_vlan_group (Data Source) ## Example Usage ```terraform # Get VLAN group by name data "netbox_vlan_group" "example1" { name = "example1" } # Get VLAN group by stub data "netbox_vlan_group" "example2" { slug = "example2" } # Get VLAN group by name and scope_type/id data "netbox_vlan_group" "example3" { name = "example" scope_type = "dcim.site" scope_id = netbox_site.example.id } ``` ## Schema ### Optional - `id` (String) At least one of `id`, `name`, `slug` or `scope_type` must be given. - `name` (String) At least one of `id`, `name`, `slug` or `scope_type` must be given. - `scope_id` (String) Required when `scope_type` is set. - `scope_type` (String) Valid values are `dcim.location`, `dcim.site`, `dcim.sitegroup`, `dcim.region`, `dcim.rack`, `virtualization.cluster` and `virtualization.clustergroup`. At least one of `id`, `name`, `slug` or `scope_type` must be given. - `slug` (String) At least one of `id`, `name`, `slug` or `scope_type` must be given. ### Read-Only - `description` (String) - `vlan_count` (Number) ================================================ FILE: docs/data-sources/vlan_groups.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_vlan_groups Data Source - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- --- # netbox_vlan_groups (Data Source) ## Schema ### Optional - `filter` (Block Set) (see [below for nested schema](#nestedblock--filter)) - `limit` (Number) Defaults to `0`. ### Read-Only - `id` (String) The ID of this resource. - `vlan_groups` (List of Object) (see [below for nested schema](#nestedatt--vlan_groups)) ### Nested Schema for `filter` Required: - `name` (String) - `value` (String) ### Nested Schema for `vlan_groups` Read-Only: - `description` (String) - `id` (Number) - `name` (String) - `ranges` (List of Object) (see [below for nested schema](#nestedobjatt--vlan_groups--ranges)) - `slug` (String) - `tag_ids` (List of Number) - `used` (Number) ### Nested Schema for `vlan_groups.ranges` Read-Only: - `end` (Number) - `start` (Number) ================================================ FILE: docs/data-sources/vlans.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_vlans Data Source - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- --- # netbox_vlans (Data Source) ## Schema ### Optional - `filter` (Block Set) (see [below for nested schema](#nestedblock--filter)) - `limit` (Number) Defaults to `0`. ### Read-Only - `id` (String) The ID of this resource. - `vlans` (List of Object) (see [below for nested schema](#nestedatt--vlans)) ### Nested Schema for `filter` Required: - `name` (String) - `value` (String) ### Nested Schema for `vlans` Read-Only: - `description` (String) - `group_id` (Number) - `id` (Number) - `name` (String) - `role` (Number) - `site` (Number) - `status` (String) - `tag_ids` (List of Number) - `tenant` (Number) - `vid` (Number) ================================================ FILE: docs/data-sources/vrf.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_vrf Data Source - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- --- # netbox_vrf (Data Source) ## Example Usage ```terraform data "netbox_vrf" "cust_a_prod" { name = "cust-a-prod" } ``` ## Schema ### Required - `name` (String) ### Optional - `tenant_id` (Number) ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/data-sources/vrfs.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_vrfs Data Source - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- --- # netbox_vrfs (Data Source) ## Schema ### Optional - `filter` (Block Set) (see [below for nested schema](#nestedblock--filter)) - `limit` (Number) Defaults to `0`. ### Read-Only - `id` (String) The ID of this resource. - `vrfs` (List of Object) (see [below for nested schema](#nestedatt--vrfs)) ### Nested Schema for `filter` Required: - `name` (String) - `value` (String) ### Nested Schema for `vrfs` Read-Only: - `description` (String) - `id` (Number) - `name` (String) - `rd` (String) - `tenant` (Number) ================================================ FILE: docs/index.md ================================================ --- layout: "" page_title: "Provider: Netbox" description: |- The netbox provider provides resources and data sources to interact with Netbox. --- # Netbox Provider The Terraform Netbox provider is a plugin for Terraform that allows for the full lifecycle management of [Netbox](https://netboxlabs.com/docs/netbox/) resources. Use the navigation to the left to read about the available resources. ## Supported Netbox versions Netbox often makes breaking API changes even in non-major releases. Check the table below to see which version this provider is compatible with your Netbox version. It is generally recommended to use the provider version matching your Netbox version. | Netbox version | Provider version | | --------------- | ---------------- | | v4.3.0 - 4.4.10 | v5.0.0 and up | | v4.2.2 - 4.2.9 | v4.0.0 - 4.3.1 | | v4.1.0 - 4.1.11 | v3.10.0 - 3.11.1 | | v4.0.0 - 4.0.11 | v3.9.0 - 3.9.2 | | v3.7.0 - 3.7.8 | v3.8.0 - 3.8.9 | | v3.6.0 - 3.6.9 | v3.7.0 - 3.7.7 | | v3.5.1 - 3.5.9 | v3.6.x | | v3.4.3 - 3.4.10 | v3.5.x | | v3.3.0 - 3.4.2 | v3.0.x - 3.5.1 | | v3.2.0 - 3.2.9 | v2.0.x | | v3.1.9 | v1.6.0 - 1.6.7 | | v3.1.3 | v1.1.x - 1.5.2 | | v3.0.9 | v1.0.x | | v2.11.12 | v0.3.x | | v2.10.10 | v0.2.x | | v2.9 | v0.1.x | Additionally, since version [1.6.6](https://github.com/e-breuninger/terraform-provider-netbox/commit/0b0b2fffa54d4ab2e5f1677e948b01e56ba211c8), each version of the provider has a built-in list of all Netbox versions it supports at release time. Upon initialization, the provider will probe your Netbox version and include a (non-blocking) warning if the used Netbox version is not supported. ## Configuration You must configure the provider with proper credentials before you can use it. You can configure the provider via attributes in the provider block or via environment variables. See [Schema](#schema) for all configuration options ## Example Usage ```terraform terraform { required_providers { netbox = { source = "e-breuninger/netbox" version = "~> 3.2.1" } } } # example provider configuration for https://demo.netbox.dev provider "netbox" { server_url = "https://demo.netbox.dev" api_token = "" } ``` ## Schema ### Required - `api_token` (String) Netbox API authentication token. Supports both v1 tokens (`Authorization: Token `) and v2 tokens (`Authorization: Bearer nbt_.`). V2 tokens are auto-detected by their `nbt_` prefix. Can be set via the `NETBOX_API_TOKEN` environment variable. - `server_url` (String) Location of Netbox server including scheme (http or https) and optional port. Can be set via the `NETBOX_SERVER_URL` environment variable. ### Optional - `allow_insecure_https` (Boolean) Flag to set whether to allow https with invalid certificates. Can be set via the `NETBOX_ALLOW_INSECURE_HTTPS` environment variable. Defaults to `false`. - `ca_cert_file` (String) Path to a PEM-encoded CA certificate for verifying the Netbox server certificate. Can be set via the `NETBOX_CA_CERT_FILE` environment variable. - `default_tags` (Set of String) Tags to add to every resource managed by this provider. - `headers` (Map of String) Set these header on all requests to Netbox. Can be set via the `NETBOX_HEADERS` environment variable. - `request_timeout` (Number) Netbox API HTTP request timeout in seconds. Can be set via the `NETBOX_REQUEST_TIMEOUT` environment variable. - `skip_version_check` (Boolean) If true, do not try to determine the running Netbox version at provider startup. Disables warnings about possibly unsupported Netbox version. Also useful for local testing on terraform plans. Can be set via the `NETBOX_SKIP_VERSION_CHECK` environment variable. Defaults to `false`. - `strip_trailing_slashes_from_url` (Boolean) If true, strip trailing slashes from the `server_url` parameter and print a warning when doing so. Note that using trailing slashes in the `server_url` parameter will usually lead to errors. Can be set via the `NETBOX_STRIP_TRAILING_SLASHES_FROM_URL` environment variable. Defaults to `true`. ================================================ FILE: docs/resources/aggregate.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_aggregate Resource - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/ipam/#aggregates: NetBox allows us to specify the portions of IP space that are interesting to us by defining aggregates. Typically, an aggregate will correspond to either an allocation of public (globally routable) IP space granted by a regional authority, or a private (internally-routable) designation. --- # netbox_aggregate (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/ipam/#aggregates): > NetBox allows us to specify the portions of IP space that are interesting to us by defining aggregates. Typically, an aggregate will correspond to either an allocation of public (globally routable) IP space granted by a regional authority, or a private (internally-routable) designation. ## Example Usage ```terraform resource "netbox_rir" "test" { name = "testrir" } resource "netbox_aggregate" "test" { prefix = "1.1.1.0/25" description = "my description" rir_id = netbox_rir.test.id } ``` ## Schema ### Required - `prefix` (String) ### Optional - `description` (String) - `rir_id` (Number) - `tags` (Set of String) - `tenant_id` (Number) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/asn.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_asn Resource - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/ipam/#asn: > ASN is short for Autonomous System Number. This identifier is used in the BGP protocol to identify which "autonomous system" a particular prefix is originating and transiting through. > > The AS number model within NetBox allows you to model some of this real-world relationship. --- # netbox_asn (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/ipam/#asn): > ASN is short for Autonomous System Number. This identifier is used in the BGP protocol to identify which "autonomous system" a particular prefix is originating and transiting through. > > The AS number model within NetBox allows you to model some of this real-world relationship. ## Example Usage ```terraform resource "netbox_rir" "test" { name = "testrir" } resource "netbox_asn" "test" { asn = 1337 rir_id = netbox_rir.test.id description = "test" comments = "test" } ``` ## Schema ### Required - `asn` (Number) Value for the AS Number record. - `rir_id` (Number) ID for the RIR for the AS Number record. ### Optional - `comments` (String) Comments field for the AS Number record. - `description` (String) Description field for the AS Number record. - `tags` (Set of String) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/available_ip_address.md ================================================ --- page_title: "netbox_available_ip_address Resource - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- Per the docs https://netbox.readthedocs.io/en/stable/models/ipam/ipaddress/: An IP address comprises a single host address (either IPv4 or IPv6) and its subnet mask. Its mask should match exactly how the IP address is configured on an interface in the real world. Like a prefix, an IP address can optionally be assigned to a VRF (otherwise, it will appear in the "global" table). IP addresses are automatically arranged under parent prefixes within their respective VRFs according to the IP hierarchya. Each IP address can also be assigned an operational status and a functional role. Statuses are hard-coded in NetBox and include the following: * Active * Reserved * Deprecated * DHCP * SLAAC (IPv6 Stateless Address Autoconfiguration) This resource will retrieve the next available IP address from a given prefix or IP range (specified by ID) --- # netbox_available_ip_address (Resource) Per [the docs](https://netbox.readthedocs.io/en/stable/models/ipam/ipaddress/): > An IP address comprises a single host address (either IPv4 or IPv6) and its subnet mask. Its mask should match exactly how the IP address is configured on an interface in the real world. > Like a prefix, an IP address can optionally be assigned to a VRF (otherwise, it will appear in the "global" table). IP addresses are automatically arranged under parent prefixes within their respective VRFs according to the IP hierarchya. > > Each IP address can also be assigned an operational status and a functional role. Statuses are hard-coded in NetBox and include the following: > * Active > * Reserved > * Deprecated > * DHCP > * SLAAC (IPv6 Stateless Address Autoconfiguration) This resource will retrieve the next available IP address from a given prefix or IP range (specified by ID) ## Example Usage ### Creating an IP in a prefix ```terraform data "netbox_prefix" "test" { cidr = "10.0.0.0/24" } resource "netbox_available_ip_address" "test" { prefix_id = data.netbox_prefix.test.id } ``` ### Creating an IP in an IP range ```terraform data "netbox_ip_range" "test" { start_address = "10.0.0.1/24" end_address = "10.0.0.50/24" } resource "netbox_available_ip_address" "test" { ip_range_id = data.netbox_ip_range.test.id } ``` ### Marking an IP active and assigning to interface ```terraform // Assumes Netbox already has a VM whos name matches 'dc-west-myvm-20' data "netbox_virtual_machine" "myvm" { name_regex = "dc-west-myvm-20" } data "netbox_prefix" "test" { cidr = "10.0.0.0/24" } resource "netbox_interface" "myvm-eth0" { name = "eth0" virtual_machine_id = data.netbox_virtual_machine.myvm.id } resource "netbox_available_ip_address" "myvm-ip" { prefix_id = data.netbox_prefix.test.id status = "active" interface_id = netbox_interface.myvm-eth0.id } ``` ## Schema ### Optional - `custom_fields` (Map of String) - `description` (String) - `device_interface_id` (Number) Conflicts with `interface_id` and `virtual_machine_interface_id`. - `dns_name` (String) - `interface_id` (Number) Required when `object_type` is set. - `ip_range_id` (Number) Exactly one of `prefix_id` or `ip_range_id` must be given. - `object_type` (String) Valid values are `virtualization.vminterface` and `dcim.interface`. Required when `interface_id` is set. - `prefix_id` (Number) Exactly one of `prefix_id` or `ip_range_id` must be given. - `role` (String) Valid values are `loopback`, `secondary`, `anycast`, `vip`, `vrrp`, `hsrp`, `glbp` and `carp`. - `status` (String) Valid values are `active`, `reserved`, `deprecated`, `dhcp` and `slaac`. Defaults to `active`. - `tags` (Set of String) - `tenant_id` (Number) - `virtual_machine_interface_id` (Number) Conflicts with `interface_id` and `device_interface_id`. - `vrf_id` (Number) ### Read-Only - `id` (String) The ID of this resource. - `ip_address` (String) - `tags_all` (Set of String) ================================================ FILE: docs/resources/available_prefix.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_available_prefix Resource - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- --- # netbox_available_prefix (Resource) ## Example Usage ```terraform data "netbox_prefix" "test" { cidr = "10.0.0.0/24" } resource "netbox_available_prefix" "test" { parent_prefix_id = data.netbox_prefix.test.id prefix_length = 25 status = "active" } ``` ## Schema ### Required - `parent_prefix_id` (Number) - `prefix_length` (Number) - `status` (String) Valid values are `active`, `container`, `reserved` and `deprecated`. ### Optional - `custom_fields` (Map of String) - `description` (String) - `is_pool` (Boolean) - `location_id` (Number) Conflicts with `site_id`, `site_group_id` and `region_id`. - `mark_utilized` (Boolean) - `region_id` (Number) Conflicts with `location_id`, `site_id` and `site_group_id`. - `role_id` (Number) - `site_group_id` (Number) Conflicts with `location_id`, `site_id` and `region_id`. - `site_id` (Number) Conflicts with `location_id`, `site_group_id` and `region_id`. - `tags` (Set of String) - `tenant_id` (Number) - `vlan_id` (Number) - `vrf_id` (Number) ### Read-Only - `id` (String) The ID of this resource. - `prefix` (String) - `tags_all` (Set of String) ================================================ FILE: docs/resources/available_vlan.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_available_vlan Resource - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- Per the docs https://netbox.readthedocs.io/en/stable/models/ipam/vlan/: A VLAN represents an isolated Layer 2 domain identified by a numeric ID (1–4094). VLANs may be assigned to specific sites or marked as global. Optionally, they can be organized within VLAN groups to define scope and enforce uniqueness. Each VLAN can also be assigned an operational status and a functional role. Statuses are hard-coded in NetBox and include the following: * Active * Reserved * Deprecated This resource will retrieve the next available VLAN ID from a given VLAN group (specified by ID). --- # netbox_available_vlan (Resource) Per [the docs](https://netbox.readthedocs.io/en/stable/models/ipam/vlan/): > A VLAN represents an isolated Layer 2 domain identified by a numeric ID (1–4094). VLANs may be assigned to specific sites or marked as global. > Optionally, they can be organized within VLAN groups to define scope and enforce uniqueness. > > Each VLAN can also be assigned an operational status and a functional role. Statuses are hard-coded in NetBox and include the following: > * Active > * Reserved > * Deprecated This resource will retrieve the next available VLAN ID from a given VLAN group (specified by ID). ## Schema ### Required - `name` (String) ### Optional - `description` (String) - `group_id` (Number) - `role_id` (Number) - `site_id` (Number) - `status` (String) - `tags` (Set of String) - `tenant_id` (Number) ### Read-Only - `comments` (String) - `id` (String) The ID of this resource. - `tags_all` (Set of String) - `vid` (Number) ================================================ FILE: docs/resources/cable.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_cable Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/dcim/cable/: All connections between device components in NetBox are represented using cables. A cable represents a direct physical connection between two sets of endpoints (A and B), such as a console port and a patch panel port, or between two network interfaces. --- # netbox_cable (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/cable/): > All connections between device components in NetBox are represented using cables. A cable represents a direct physical connection between two sets of endpoints (A and B), such as a console port and a patch panel port, or between two network interfaces. ## Example Usage ```terraform // assumes that the referenced console port resources exist resource "netbox_cable" "test" { a_termination { object_type = "dcim.consoleserverport" object_id = netbox_device_console_server_port.kvm1.id } a_termination { object_type = "dcim.consoleserverport" object_id = netbox_device_console_server_port.kvm2.id } b_termination { object_type = "dcim.consoleport" object_id = netbox_device_console_port.server1.id } b_termination { object_type = "dcim.consoleport" object_id = netbox_device_console_port.server2.id } status = "connected" label = "KVM cable" type = "cat8" color_hex = "123456" length = 10 length_unit = "m" } ``` ## Schema ### Required - `a_termination` (Block Set, Min: 1) (see [below for nested schema](#nestedblock--a_termination)) - `b_termination` (Block Set, Min: 1) (see [below for nested schema](#nestedblock--b_termination)) - `status` (String) One of [connected, planned, decommissioning]. ### Optional - `color_hex` (String) - `comments` (String) - `custom_fields` (Map of String) - `description` (String) - `label` (String) - `length` (Number) - `length_unit` (String) One of [km, m, cm, mi, ft, in]. Required when `length` is set. - `tags` (Set of String) - `tenant_id` (Number) - `type` (String) One of [cat3, cat5, cat5e, cat6, cat6a, cat7, cat7a, cat8, dac-active, dac-passive, mrj21-trunk, coaxial, mmf, mmf-om1, mmf-om2, mmf-om3, mmf-om4, mmf-om5, smf, smf-os1, smf-os2, aoc, power, usb]. ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ### Nested Schema for `a_termination` Required: - `object_id` (Number) - `object_type` (String) ### Nested Schema for `b_termination` Required: - `object_id` (Number) - `object_type` (String) ================================================ FILE: docs/resources/circuit.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_circuit Resource - terraform-provider-netbox" subcategory: "Circuits" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/circuits/#circuits_1: A communications circuit represents a single physical link connecting exactly two endpoints, commonly referred to as its A and Z terminations. A circuit in NetBox may have zero, one, or two terminations defined. It is common to have only one termination defined when you don't necessarily care about the details of the provider side of the circuit, e.g. for Internet access circuits. Both terminations would likely be modeled for circuits which connect one customer site to another. Each circuit is associated with a provider and a user-defined type. For example, you might have Internet access circuits delivered to each site by one provider, and private MPLS circuits delivered by another. Each circuit must be assigned a circuit ID, each of which must be unique per provider. --- # netbox_circuit (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/circuits/#circuits_1): > A communications circuit represents a single physical link connecting exactly two endpoints, commonly referred to as its A and Z terminations. A circuit in NetBox may have zero, one, or two terminations defined. It is common to have only one termination defined when you don't necessarily care about the details of the provider side of the circuit, e.g. for Internet access circuits. Both terminations would likely be modeled for circuits which connect one customer site to another. > > Each circuit is associated with a provider and a user-defined type. For example, you might have Internet access circuits delivered to each site by one provider, and private MPLS circuits delivered by another. Each circuit must be assigned a circuit ID, each of which must be unique per provider. ## Example Usage ```terraform resource "netbox_tenant" "test" { name = "test" } resource "netbox_circuit_provider" "test" { name = "test" } resource "netbox_circuit_type" "test" { name = "test" } resource "netbox_circuit" "test" { cid = "test" status = "active" provider_id = netbox_circuit_provider.test.id type_id = netbox_circuit_type.test.id } ``` ## Schema ### Required - `cid` (String) - `provider_id` (Number) - `status` (String) Valid values are `planned`, `provisioning`, `active`, `offline`, `deprovisioning` and `decommissioning`. - `type_id` (Number) ### Optional - `description` (String) - `tenant_id` (Number) ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/circuit_provider.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_circuit_provider Resource - terraform-provider-netbox" subcategory: "Circuits" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/circuits/#providers: A circuit provider is any entity which provides some form of connectivity of among sites or organizations within a site. While this obviously includes carriers which offer Internet and private transit service, it might also include Internet exchange (IX) points and even organizations with whom you peer directly. Each circuit within NetBox must be assigned a provider and a circuit ID which is unique to that provider. Each provider may be assigned an autonomous system number (ASN), an account number, and contact information. --- # netbox_circuit_provider (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/circuits/#providers): > A circuit provider is any entity which provides some form of connectivity of among sites or organizations within a site. While this obviously includes carriers which offer Internet and private transit service, it might also include Internet exchange (IX) points and even organizations with whom you peer directly. Each circuit within NetBox must be assigned a provider and a circuit ID which is unique to that provider. > > Each provider may be assigned an autonomous system number (ASN), an account number, and contact information. ## Example Usage ```terraform resource "netbox_circuit_provider" "test" { name = "test" } ``` ## Schema ### Required - `name` (String) ### Optional - `description` (String) - `slug` (String) ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/circuit_termination.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_circuit_termination Resource - terraform-provider-netbox" subcategory: "Circuits" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/circuits/#circuit-terminations: The association of a circuit with a particular site and/or device is modeled separately as a circuit termination. A circuit may have up to two terminations, labeled A and Z. A single-termination circuit can be used when you don't know (or care) about the far end of a circuit (for example, an Internet access circuit which connects to a transit provider). A dual-termination circuit is useful for tracking circuits which connect two sites. Each circuit termination is attached to either a site or to a provider network. Site terminations may optionally be connected via a cable to a specific device interface or port within that site. Each termination must be assigned a port speed, and can optionally be assigned an upstream speed if it differs from the downstream speed (a common scenario with e.g. DOCSIS cable modems). Fields are also available to track cross-connect and patch panel details. --- # netbox_circuit_termination (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/circuits/#circuit-terminations): > The association of a circuit with a particular site and/or device is modeled separately as a circuit termination. A circuit may have up to two terminations, labeled A and Z. A single-termination circuit can be used when you don't know (or care) about the far end of a circuit (for example, an Internet access circuit which connects to a transit provider). A dual-termination circuit is useful for tracking circuits which connect two sites. > > Each circuit termination is attached to either a site or to a provider network. Site terminations may optionally be connected via a cable to a specific device interface or port within that site. Each termination must be assigned a port speed, and can optionally be assigned an upstream speed if it differs from the downstream speed (a common scenario with e.g. DOCSIS cable modems). Fields are also available to track cross-connect and patch panel details. ## Example Usage ```terraform resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_circuit_provider" "test" { name = "%[1]s" } resource "netbox_circuit_type" "test" { name = "%[1]s" } resource "netbox_circuit" "test" { cid = "%[1]s" status = "active" provider_id = netbox_circuit_provider.test.id type_id = netbox_circuit_type.test.id } resource "netbox_circuit_termination" "test" { circuit_id = netbox_circuit.test.id term_side = "A" site_id = netbox_site.test.id port_speed = 100000 upstream_speed = 50000 } ``` ## Schema ### Required - `circuit_id` (Number) - `term_side` (String) Valid values are `A` and `Z`. ### Optional - `custom_fields` (Map of String) - `description` (String) - `location_id` (Number) Exactly one of `site_id`, `site_group_id`, `region_id` or `provider_network_id` must be given. - `port_speed` (Number) - `provider_network_id` (Number) Exactly one of `location_id`, `site_id`, `site_group_id` or `region_id` must be given. - `region_id` (Number) Exactly one of `location_id`, `site_id`, `site_group_id` or `provider_network_id` must be given. - `site_group_id` (Number) Exactly one of `location_id`, `site_id`, `region_id` or `provider_network_id` must be given. - `site_id` (Number) Exactly one of `location_id`, `site_group_id`, `region_id` or `provider_network_id` must be given. - `tags` (Set of String) - `upstream_speed` (Number) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/circuit_type.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_circuit_type Resource - terraform-provider-netbox" subcategory: "Circuits" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/circuits/#circuit-types: Circuits are classified by functional type. These types are completely customizable, and are typically used to convey the type of service being delivered over a circuit. --- # netbox_circuit_type (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/circuits/#circuit-types): > Circuits are classified by functional type. These types are completely customizable, and are typically used to convey the type of service being delivered over a circuit. ## Example Usage ```terraform resource "netbox_circuit_type" "test" { name = "test" } ``` ## Schema ### Required - `name` (String) ### Optional - `description` (String) - `slug` (String) ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/cluster.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_cluster Resource - terraform-provider-netbox" subcategory: "Virtualization" description: |- From the official documentation https://netboxlabs.com/docs/netbox/models/virtualization/cluster/: > A cluster is a logical grouping of physical resources within which virtual machines run. Physical devices may be associated with clusters as hosts. This allows users to track on which host(s) a particular virtual machine may reside. --- # netbox_cluster (Resource) From the [official documentation](https://netboxlabs.com/docs/netbox/models/virtualization/cluster/): > A cluster is a logical grouping of physical resources within which virtual machines run. Physical devices may be associated with clusters as hosts. This allows users to track on which host(s) a particular virtual machine may reside. ## Example Usage ```terraform // Assumes the 'dc-west' cluster group already exists data "netbox_cluster_group" "dc_west" { name = "dc-west" } resource "netbox_cluster_type" "vmw_vsphere" { name = "VMware vSphere 6" } resource "netbox_cluster" "vmw_cluster_01" { cluster_type_id = netbox_cluster_type.vmw_vsphere.id name = "vmw-cluster-01" cluster_group_id = data.netbox_cluster_group.dc_west.id } ``` ## Schema ### Required - `cluster_type_id` (Number) - `name` (String) ### Optional - `cluster_group_id` (Number) - `comments` (String) - `description` (String) - `location_id` (Number) Conflicts with `site_id`, `site_group_id` and `region_id`. - `region_id` (Number) Conflicts with `location_id`, `site_id` and `site_group_id`. - `site_group_id` (Number) Conflicts with `location_id`, `site_id` and `region_id`. - `site_id` (Number) Conflicts with `location_id`, `site_group_id` and `region_id`. - `tags` (Set of String) - `tenant_id` (Number) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/cluster_group.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_cluster_group Resource - terraform-provider-netbox" subcategory: "Virtualization" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/virtualization/#cluster-groups: Cluster groups may be created for the purpose of organizing clusters. The arrangement of clusters into groups is optional. --- # netbox_cluster_group (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/virtualization/#cluster-groups): > Cluster groups may be created for the purpose of organizing clusters. The arrangement of clusters into groups is optional. ## Example Usage ```terraform resource "netbox_cluster_group" "dc_west" { description = "West Datacenter Cluster" name = "dc-west" } ``` ## Schema ### Required - `name` (String) ### Optional - `description` (String) - `slug` (String) ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/cluster_type.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_cluster_type Resource - terraform-provider-netbox" subcategory: "Virtualization" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/virtualization/#cluster-types: A cluster type represents a technology or mechanism by which a cluster is formed. For example, you might create a cluster type named "VMware vSphere" for a locally hosted cluster or "DigitalOcean NYC3" for one hosted by a cloud provider. --- # netbox_cluster_type (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/virtualization/#cluster-types): > A cluster type represents a technology or mechanism by which a cluster is formed. For example, you might create a cluster type named "VMware vSphere" for a locally hosted cluster or "DigitalOcean NYC3" for one hosted by a cloud provider. ## Example Usage ```terraform resource "netbox_cluster_type" "vmw_vsphere" { name = "VMware vSphere 6" } ``` ## Schema ### Required - `name` (String) ### Optional - `slug` (String) ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/config_context.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_config_context Resource - terraform-provider-netbox" subcategory: "Extras" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/extras/configcontext/: Context data is made available to devices and/or virtual machines based on their relationships to other objects in NetBox. For example, context data can be associated only with devices assigned to a particular site, or only to virtual machines in a certain cluster. --- # netbox_config_context (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/extras/configcontext/): > Context data is made available to devices and/or virtual machines based on their relationships to other objects in NetBox. For example, context data can be associated only with devices assigned to a particular site, or only to virtual machines in a certain cluster. ## Example Usage ```terraform resource "netbox_config_context" "test" { name = "%s" data = jsonencode({ "testkey" = "testval" }) } ``` ## Schema ### Required - `data` (String) - `name` (String) ### Optional - `cluster_groups` (Set of Number) - `cluster_types` (Set of Number) - `clusters` (Set of Number) - `description` (String) - `device_types` (Set of Number) - `locations` (Set of Number) - `platforms` (Set of Number) - `regions` (Set of Number) - `roles` (Set of Number) - `site_groups` (Set of Number) - `sites` (Set of Number) - `tags` (Set of String) - `tenant_groups` (Set of Number) - `tenants` (Set of Number) - `weight` (Number) Defaults to `1000`. ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/config_template.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_config_template Resource - terraform-provider-netbox" subcategory: "Extras" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/extras/configtemplate/: Configuration templates can be used to render device configurations from context data. Templates are written in the Jinja2 language and can be associated with devices roles, platforms, and/or individual devices. Context data is made available to devices and/or virtual machines based on their relationships to other objects in NetBox. For example, context data can be associated only with devices assigned to a particular site, or only to virtual machines in a certain cluster. --- # netbox_config_template (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/extras/configtemplate/): > Configuration templates can be used to render device configurations from context data. Templates are written in the Jinja2 language and can be associated with devices roles, platforms, and/or individual devices. > Context data is made available to devices and/or virtual machines based on their relationships to other objects in NetBox. For example, context data can be associated only with devices assigned to a particular site, or only to virtual machines in a certain cluster. ## Example Usage ```terraform resource "netbox_config_template" "test" { name = "test" description = "test description" template_code = "hostname {{ name }}" environment_params = jsonencode({ "name" = "my-hostname" }) } ``` ## Schema ### Required - `name` (String) - `template_code` (String) ### Optional - `description` (String) - `environment_params` (String) Defaults to `{}`. - `tags` (Set of String) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/contact.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_contact Resource - terraform-provider-netbox" subcategory: "Tenancy" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/contacts/#contacts_1: A contact should represent an individual or permanent point of contact. Each contact must define a name, and may optionally include a title, phone number, email address, and related details. Contacts are reused for assignments, so each unique contact must be created only once and can be assigned to any number of NetBox objects, and there is no limit to the number of assigned contacts an object may have. Most core objects in NetBox can have contacts assigned to them. --- # netbox_contact (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/contacts/#contacts_1): > A contact should represent an individual or permanent point of contact. Each contact must define a name, and may optionally include a title, phone number, email address, and related details. > > Contacts are reused for assignments, so each unique contact must be created only once and can be assigned to any number of NetBox objects, and there is no limit to the number of assigned contacts an object may have. Most core objects in NetBox can have contacts assigned to them. ## Example Usage ```terraform resource "netbox_contact" "test" { name = "John Doe" email = "test@example.com" phone = "123-123123" } ``` ## Schema ### Required - `name` (String) ### Optional - `description` (String) - `email` (String) - `group_id` (Number) - `link` (String) - `phone` (String) - `tags` (Set of String) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/contact_assignment.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_contact_assignment Resource - terraform-provider-netbox" subcategory: "Tenancy" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/contacts#contactassignments_1: Much like tenancy, contact assignment enables you to track ownership of resources modeled in NetBox. --- # netbox_contact_assignment (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/contacts#contactassignments_1): > Much like tenancy, contact assignment enables you to track ownership of resources modeled in NetBox. ## Example Usage ```terraform resource "netbox_contact" "test" { name = "test" } resource "netbox_contact_role" "test" { name = "test" } // Assumes that a device with id 123 exists resource "netbox_contact_assignment" "test" { content_type = "dcim.device" object_id = 123 contact_id = netbox_contact.test.id role_id = netbox_contact_role.test.id priority = "primary" } ``` ## Schema ### Required - `contact_id` (Number) - `content_type` (String) - `object_id` (Number) - `role_id` (Number) ### Optional - `priority` (String) Valid values are `primary`, `secondary`, `tertiary` and `inactive`. ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/contact_group.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_contact_group Resource - terraform-provider-netbox" subcategory: "Tenancy" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/contacts/#contact-groups: Contacts can be grouped arbitrarily into a recursive hierarchy, and a contact can be assigned to a group at any level within the hierarchy. --- # netbox_contact_group (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/contacts/#contact-groups): > Contacts can be grouped arbitrarily into a recursive hierarchy, and a contact can be assigned to a group at any level within the hierarchy. ## Example Usage ```terraform resource "netbox_contact_group" "test" { name = "test" } ``` ## Schema ### Required - `name` (String) ### Optional - `description` (String) - `parent_id` (Number) - `slug` (String) ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/contact_role.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_contact_role Resource - terraform-provider-netbox" subcategory: "Tenancy" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/contacts/#contactroles: A contact role defines the relationship of a contact to an assigned object. For example, you might define roles for administrative, operational, and emergency contacts --- # netbox_contact_role (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/contacts/#contactroles): > A contact role defines the relationship of a contact to an assigned object. For example, you might define roles for administrative, operational, and emergency contacts ## Example Usage ```terraform resource "netbox_contact_role" "test" { name = "test" } ``` ## Schema ### Required - `name` (String) ### Optional - `slug` (String) ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/custom_field.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_custom_field Resource - terraform-provider-netbox" subcategory: "Extras" description: |- From the official documentation https://docs.netbox.dev/en/stable/customization/custom-fields/#custom-fields: Each model in NetBox is represented in the database as a discrete table, and each attribute of a model exists as a column within its table. For example, sites are stored in the dcimsite table, which has columns named name, facility, physicaladdress, and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows. However, some users might want to store additional object attributes that are somewhat esoteric in nature, and that would not make sense to include in the core NetBox database schema. For instance, suppose your organization needs to associate each device with a ticket number correlating it with an internal support system record. This is certainly a legitimate use for NetBox, but it's not a common enough need to warrant including a field for every NetBox installation. Instead, you can create a custom field to hold this data. --- # netbox_custom_field (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/customization/custom-fields/#custom-fields): > Each model in NetBox is represented in the database as a discrete table, and each attribute of a model exists as a column within its table. For example, sites are stored in the dcim_site table, which has columns named name, facility, physical_address, and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows. > > However, some users might want to store additional object attributes that are somewhat esoteric in nature, and that would not make sense to include in the core NetBox database schema. For instance, suppose your organization needs to associate each device with a ticket number correlating it with an internal support system record. This is certainly a legitimate use for NetBox, but it's not a common enough need to warrant including a field for every NetBox installation. Instead, you can create a custom field to hold this data. ## Example Usage ```terraform resource "netbox_custom_field" "test" { name = "test" type = "text" content_types = ["virtualization.vminterface"] weight = 100 validation_regex = "^.*$" } ``` ## Schema ### Required - `content_types` (Set of String) - `name` (String) - `type` (String) ### Optional - `choice_set_id` (Number) - `default` (String) - `description` (String) - `group_name` (String) - `label` (String) - `required` (Boolean) - `validation_maximum` (Number) - `validation_minimum` (Number) - `validation_regex` (String) - `weight` (Number) ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/custom_field_choice_set.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_custom_field_choice_set Resource - terraform-provider-netbox" subcategory: "Extras" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/extras/customfieldchoiceset/: Single- and multi-selection custom fields must define a set of valid choices from which the user may choose when defining the field value. These choices are defined as sets that may be reused among multiple custom fields. A choice set must define a base choice set and/or a set of arbitrary extra choices. --- # netbox_custom_field_choice_set (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/extras/customfieldchoiceset/): Single- and multi-selection custom fields must define a set of valid choices from which the user may choose when defining the field value. These choices are defined as sets that may be reused among multiple custom fields. A choice set must define a base choice set and/or a set of arbitrary extra choices. ## Example Usage ```terraform resource "netbox_custom_field_choice_set" "test" { name = "my-custom-field-set" description = "Description" extra_choices = [ ["choice1", "label1"], # label and choice are different ["choice2", "choice2"] # label and choice are the same ] } ``` ## Schema ### Required - `name` (String) ### Optional - `base_choices` (String) Valid values are `IATA`, `ISO_3166` and `UN_LOCODE`. At least one of `base_choices` or `extra_choices` must be given. - `custom_fields` (Map of String) - `description` (String) - `extra_choices` (List of List of String) This length of the inner lists must be exactly two, where the first value is the value of a choice and the second value is the label of the choice. At least one of `base_choices` or `extra_choices` must be given. - `order_alphabetically` (Boolean) experimental. Defaults to `false`. ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/device.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_device Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/devices/#devices: Every piece of hardware which is installed within a site or rack exists in NetBox as a device. Devices are measured in rack units (U) and can be half depth or full depth. A device may have a height of 0U: These devices do not consume vertical rack space and cannot be assigned to a particular rack unit. A common example of a 0U device is a vertically-mounted PDU. --- # netbox_device (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/devices/#devices): > Every piece of hardware which is installed within a site or rack exists in NetBox as a device. Devices are measured in rack units (U) and can be half depth or full depth. A device may have a height of 0U: These devices do not consume vertical rack space and cannot be assigned to a particular rack unit. A common example of a 0U device is a vertically-mounted PDU. ## Example Usage ```terraform resource "netbox_site" "test" { name = "%[1]s" } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_manufacturer" "test" { name = "test" } resource "netbox_device_type" "test" { model = "test" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id local_context_data = jsonencode({ "setting_a" = "Some Setting" "setting_b" = 42 }) } ``` ## Schema ### Required - `device_type_id` (Number) - `name` (String) - `role_id` (Number) - `site_id` (Number) ### Optional - `asset_tag` (String) - `cluster_id` (Number) - `comments` (String) - `config_template_id` (Number) - `custom_fields` (Map of String) - `description` (String) - `local_context_data` (String) This is best managed through the use of `jsonencode` and a map of settings. - `location_id` (Number) - `platform_id` (Number) - `rack_face` (String) Valid values are `front` and `rear`. Required when `rack_position` is set. - `rack_id` (Number) - `rack_position` (Number) - `serial` (String) - `status` (String) Valid values are `offline`, `active`, `planned`, `staged`, `failed`, `inventory` and `decommissioning`. Defaults to `active`. - `tags` (Set of String) - `tenant_id` (Number) - `virtual_chassis_id` (Number) Required when `virtual_chassis_master` and `virtual_chassis_id` is set. - `virtual_chassis_master` (Boolean) Required when `virtual_chassis_master` and `virtual_chassis_id` is set. - `virtual_chassis_position` (Number) - `virtual_chassis_priority` (Number) ### Read-Only - `id` (String) The ID of this resource. - `primary_ipv4` (Number) - `primary_ipv6` (Number) - `tags_all` (Set of String) ================================================ FILE: docs/resources/device_bay.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_device_bay Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/dcim/devicebay/: Device bays represent a space or slot within a device in which a field-replaceable device may be installed. A common example is that of a chassis-based server that holds a number of blades which may contain device components that don't fit the module pattern. Devices in turn hold additional components that become available to the parent device. --- # netbox_device_bay (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/devicebay/): > Device bays represent a space or slot within a device in which a field-replaceable device may be installed. A common example is that of a chassis-based server that holds a number of blades which may contain device components that don't fit the module pattern. Devices in turn hold additional components that become available to the parent device. ## Example Usage ```terraform resource "netbox_tenant" "example" { name = "example_tenant" } resource "netbox_site" "example" { name = "example_site" status = "active" } resource "netbox_manufacturer" "example" { name = "example_manufacturer" } resource "netbox_device_type" "example" { model = "example_device_type" manufacturer_id = netbox_manufacturer.example.id subdevice_role = "parent" } resource "netbox_device_type" "example_installed" { model = "example_device_type_installed" manufacturer_id = netbox_manufacturer.example.id u_height = 0 subdevice_role = "child" } resource "netbox_device_role" "example" { name = "example_role" color_hex = "123456" } resource "netbox_device" "example" { name = "example_device" device_type_id = netbox_device_type.example.id tenant_id = netbox_tenant.example.id role_id = netbox_device_role.example.id site_id = netbox_site.example.id } resource "netbox_device" "example_installed" { name = "example_device_installed" device_type_id = netbox_device_type.example_installed.id tenant_id = netbox_tenant.example.id role_id = netbox_device_role.example.id site_id = netbox_site.example.id } resource "netbox_device_bay" "example" { device_id = netbox_device.example.id name = "example_device_bay" label = "example_label" description = "example_description" installed_device_id = netbox_device.example_installed.id } ``` ## Schema ### Required - `device_id` (Number) - `name` (String) ### Optional - `custom_fields` (Map of String) - `description` (String) - `installed_device_id` (Number) - `label` (String) - `tags` (Set of String) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/device_bay_template.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_device_bay_template Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/dcim/devicebaytemplate/: A template for a device bay that will be created on all instantiations of the parent device type. --- # netbox_device_bay_template (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/devicebaytemplate/): > A template for a device bay that will be created on all instantiations of the parent device type. ## Example Usage ```terraform resource "netbox_manufacturer" "example" { name = "example_manufacturer" } resource "netbox_device_type" "example" { model = "example_device_model" slug = "example_device_slug" part_number = "example_part_number" manufacturer_id = netbox_manufacturer.example.id subdevice_role = "parent" } resource "netbox_device_bay_template" "example" { name = "example_device_bay_template" device_type_id = netbox_device_type.example.id } ``` ## Schema ### Required - `device_type_id` (Number) - `name` (String) ### Optional - `description` (String) - `label` (String) ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/device_console_port.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_device_console_port Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/dcim/consoleport/: A console port provides connectivity to the physical console of a device. These are typically used for temporary access by someone who is physically near the device, or for remote out-of-band access provided via a networked console server. --- # netbox_device_console_port (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/consoleport/): > A console port provides connectivity to the physical console of a device. These are typically used for temporary access by someone who is physically near the device, or for remote out-of-band access provided via a networked console server. ## Example Usage ```terraform # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_console_port" "test" { device_id = netbox_device.test.id name = "console port" type = "de-9" speed = 1200 mark_connected = true } ``` ## Schema ### Required - `device_id` (Number) - `name` (String) ### Optional - `custom_fields` (Map of String) - `description` (String) - `label` (String) - `mark_connected` (Boolean) Defaults to `false`. - `module_id` (Number) - `speed` (Number) One of [1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200]. - `tags` (Set of String) - `type` (String) One of [de-9, db-25, rj-11, rj-12, rj-45, mini-din-8, usb-a, usb-b, usb-c, usb-mini-a, usb-mini-b, usb-micro-a, usb-micro-b, usb-micro-ab, other]. ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/device_console_server_port.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_device_console_server_port Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/dcim/consoleserverport/: A console server is a device which provides remote access to the local consoles of connected devices. They are typically used to provide remote out-of-band access to network devices, and generally connect to console ports. --- # netbox_device_console_server_port (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/consoleserverport/): > A console server is a device which provides remote access to the local consoles of connected devices. They are typically used to provide remote out-of-band access to network devices, and generally connect to console ports. ## Example Usage ```terraform # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_console_server_port" "test" { device_id = netbox_device.test.id name = "console server port" type = "de-9" speed = 1200 mark_connected = true } ``` ## Schema ### Required - `device_id` (Number) - `name` (String) ### Optional - `custom_fields` (Map of String) - `description` (String) - `label` (String) - `mark_connected` (Boolean) Defaults to `false`. - `module_id` (Number) - `speed` (Number) One of [1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200]. - `tags` (Set of String) - `type` (String) One of [de-9, db-25, rj-11, rj-12, rj-45, mini-din-8, usb-a, usb-b, usb-c, usb-mini-a, usb-mini-b, usb-micro-a, usb-micro-b, usb-micro-ab, other]. ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/device_front_port.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_device_front_port Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/dcim/frontport/: Front ports are pass-through ports which represent physical cable connections that comprise part of a longer path. For example, the ports on the front face of a UTP patch panel would be modeled in NetBox as front ports. Each port is assigned a physical type, and must be mapped to a specific rear port on the same device. A single rear port may be mapped to multiple front ports, using numeric positions to annotate the specific alignment of each. --- # netbox_device_front_port (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/frontport/): > Front ports are pass-through ports which represent physical cable connections that comprise part of a longer path. For example, the ports on the front face of a UTP patch panel would be modeled in NetBox as front ports. Each port is assigned a physical type, and must be mapped to a specific rear port on the same device. A single rear port may be mapped to multiple front ports, using numeric positions to annotate the specific alignment of each. ## Example Usage ```terraform # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_rear_port" "test" { device_id = netbox_device.test.id name = "rear port 1" type = "8p8c" positions = 2 mark_connected = true } resource "netbox_device_front_port" "test" { device_id = netbox_device.test.id name = "front port 1" type = "8p8c" rear_port_id = netbox_device_rear_port.test.id rear_port_position = 2 } ``` ## Schema ### Required - `device_id` (Number) - `name` (String) - `rear_port_id` (Number) - `rear_port_position` (Number) - `type` (String) One of [8p8c, 8p6c, 8p4c, 8p2c, 6p6c, 6p4c, 6p2c, 4p4c, 4p2c, gg45, tera-4p, tera-2p, tera-1p, 110-punch, bnc, f, n, mrj21, fc, lc, lc-pc, lc-upc, lc-apc, lsh, lsh-pc, lsh-upc, lsh-apc, mpo, mtrj, sc, sc-pc, sc-upc, sc-apc, st, cs, sn, sma-905, sma-906, urm-p2, urm-p4, urm-p8, splice, other]. ### Optional - `color_hex` (String) - `custom_fields` (Map of String) - `description` (String) - `label` (String) - `mark_connected` (Boolean) Defaults to `false`. - `module_id` (Number) - `tags` (Set of String) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/device_interface.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_device_interface Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/device/#interface: Interfaces in NetBox represent network interfaces used to exchange data with connected devices. On modern networks, these are most commonly Ethernet, but other types are supported as well. IP addresses and VLANs can be assigned to interfaces. --- # netbox_device_interface (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/device/#interface): > Interfaces in NetBox represent network interfaces used to exchange data with connected devices. On modern networks, these are most commonly Ethernet, but other types are supported as well. IP addresses and VLANs can be assigned to interfaces. ## Example Usage ```terraform // Assumes a device with ID 123 exists resource "netbox_device_interface" "test" { name = "testinterface" device_id = 123 type = "1000base-t" } ``` ## Schema ### Required - `device_id` (Number) - `name` (String) - `type` (String) ### Optional - `description` (String) - `enabled` (Boolean) Defaults to `true`. - `label` (String) - `lag_device_interface_id` (Number) If this device is a member of a LAG group, you can reference the LAG interface here. - `mgmtonly` (Boolean) - `mode` (String) Valid values are `access`, `tagged`, `tagged-all` and `q-in-q`. - `mtu` (Number) - `parent_device_interface_id` (Number) The netbox_device_interface id of the parent interface. Useful if this interface is a logical interface. - `speed` (Number) - `tagged_vlans` (Set of Number) - `tags` (Set of String) - `untagged_vlan` (Number) ### Read-Only - `id` (String) The ID of this resource. - `mac_address` (String) The MAC address as string from the first MAC address assigned to this interface, if any. - `mac_addresses` (Set of Object) (see [below for nested schema](#nestedatt--mac_addresses)) - `tags_all` (Set of String) ### Nested Schema for `mac_addresses` Read-Only: - `description` (String) - `id` (Number) - `mac_address` (String) ================================================ FILE: docs/resources/device_module_bay.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_device_module_bay Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/dcim/modulebay/: Module bays represent a space or slot within a device in which a field-replaceable module may be installed. A common example is that of a chassis-based switch such as the Cisco Nexus 9000 or Juniper EX9200. Modules in turn hold additional components that become available to the parent device. --- # netbox_device_module_bay (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/modulebay/): > Module bays represent a space or slot within a device in which a field-replaceable module may be installed. A common example is that of a chassis-based switch such as the Cisco Nexus 9000 or Juniper EX9200. Modules in turn hold additional components that become available to the parent device. ## Example Usage ```terraform # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_module_bay" "test" { device_id = netbox_device.test.id name = "module bay 1" } ``` ## Schema ### Required - `device_id` (Number) - `name` (String) ### Optional - `custom_fields` (Map of String) - `description` (String) - `label` (String) - `position` (String) - `tags` (Set of String) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/device_power_outlet.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_device_power_outlet Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/dcim/poweroutlet/: Power outlets represent the outlets on a power distribution unit (PDU) or other device that supplies power to dependent devices. Each power port may be assigned a physical type, and may be associated with a specific feed leg (where three-phase power is used) and/or a specific upstream power port. This association can be used to model the distribution of power within a device. For example, imagine a PDU with one power port which draws from a three-phase feed and 48 power outlets arranged into three banks of 16 outlets each. Outlets 1-16 would be associated with leg A on the port, and outlets 17-32 and 33-48 would be associated with legs B and C, respectively. --- # netbox_device_power_outlet (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/poweroutlet/): > Power outlets represent the outlets on a power distribution unit (PDU) or other device that supplies power to dependent devices. Each power port may be assigned a physical type, and may be associated with a specific feed leg (where three-phase power is used) and/or a specific upstream power port. This association can be used to model the distribution of power within a device. For example, imagine a PDU with one power port which draws from a three-phase feed and 48 power outlets arranged into three banks of 16 outlets each. Outlets 1-16 would be associated with leg A on the port, and outlets 17-32 and 33-48 would be associated with legs B and C, respectively. ## Example Usage ```terraform # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_power_outlet" "test" { device_id = netbox_device.test.id name = "power outlet" type = "iec-60320-c5" feed_leg = "A" } ``` ## Schema ### Required - `device_id` (Number) - `name` (String) ### Optional - `custom_fields` (Map of String) - `description` (String) - `feed_leg` (String) One of [A, B, C]. - `label` (String) - `mark_connected` (Boolean) Defaults to `false`. - `module_id` (Number) - `power_port_id` (Number) - `tags` (Set of String) - `type` (String) One of [iec-60320-c5, iec-60320-c7, iec-60320-c13, iec-60320-c15, iec-60320-c19, iec-60320-c21, iec-60309-p-n-e-4h, iec-60309-p-n-e-6h, iec-60309-p-n-e-9h, iec-60309-2p-e-4h, iec-60309-2p-e-6h, iec-60309-2p-e-9h, iec-60309-3p-e-4h, iec-60309-3p-e-6h, iec-60309-3p-e-9h, iec-60309-3p-n-e-4h, iec-60309-3p-n-e-6h, iec-60309-3p-n-e-9h, nema-1-15r, nema-5-15r, nema-5-20r, nema-5-30r, nema-5-50r, nema-6-15r, nema-6-20r, nema-6-30r, nema-6-50r, nema-10-30r, nema-10-50r, nema-14-20r, nema-14-30r, nema-14-50r, nema-14-60r, nema-15-15r, nema-15-20r, nema-15-30r, nema-15-50r, nema-15-60r, nema-l1-15r, nema-l5-15r, nema-l5-20r, nema-l5-30r, nema-l5-50r, nema-l6-15r, nema-l6-20r, nema-l6-30r, nema-l6-50r, nema-l10-30r, nema-l14-20r, nema-l14-30r, nema-l14-50r, nema-l14-60r, nema-l15-20r, nema-l15-30r, nema-l15-50r, nema-l15-60r, nema-l21-20r, nema-l21-30r, nema-l22-30r, CS6360C, CS6364C, CS8164C, CS8264C, CS8364C, CS8464C, ita-e, ita-f, ita-g, ita-h, ita-i, ita-j, ita-k, ita-l, ita-m, ita-n, ita-o, ita-multistandard, usb-a, usb-micro-b, usb-c, dc-terminal, hdot-cx, saf-d-grid, neutrik-powercon-20a, neutrik-powercon-32a, neutrik-powercon-true1, neutrik-powercon-true1-top, ubiquiti-smartpower, hardwired, other]. ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/device_power_port.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_device_power_port Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/dcim/powerport/: A power port is a device component which draws power from some external source (e.g. an upstream power outlet), and generally represents a power supply internal to a device. --- # netbox_device_power_port (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/powerport/): > A power port is a device component which draws power from some external source (e.g. an upstream power outlet), and generally represents a power supply internal to a device. ## Example Usage ```terraform # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_power_port" "test" { device_id = netbox_device.test.id name = "power port" maximum_draw = 750 allocated_draw = 500 type = "iec-60320-c6" } ``` ## Schema ### Required - `device_id` (Number) - `name` (String) ### Optional - `allocated_draw` (Number) - `custom_fields` (Map of String) - `description` (String) - `label` (String) - `mark_connected` (Boolean) Defaults to `false`. - `maximum_draw` (Number) - `module_id` (Number) - `tags` (Set of String) - `type` (String) One of [iec-60320-c6, iec-60320-c8, iec-60320-c14, iec-60320-c16, iec-60320-c20, iec-60320-c22, iec-60309-p-n-e-4h, iec-60309-p-n-e-6h, iec-60309-p-n-e-9h, iec-60309-2p-e-4h, iec-60309-2p-e-6h, iec-60309-2p-e-9h, iec-60309-3p-e-4h, iec-60309-3p-e-6h, iec-60309-3p-e-9h, iec-60309-3p-n-e-4h, iec-60309-3p-n-e-6h, iec-60309-3p-n-e-9h, nema-1-15p, nema-5-15p, nema-5-20p, nema-5-30p, nema-5-50p, nema-6-15p, nema-6-20p, nema-6-30p, nema-6-50p, nema-10-30p, nema-10-50p, nema-14-20p, nema-14-30p, nema-14-50p, nema-14-60p, nema-15-15p, nema-15-20p, nema-15-30p, nema-15-50p, nema-15-60p, nema-l1-15p, nema-l5-15p, nema-l5-20p, nema-l5-30p, nema-l5-50p, nema-l6-15p, nema-l6-20p, nema-l6-30p, nema-l6-50p, nema-l10-30p, nema-l14-20p, nema-l14-30p, nema-l14-50p, nema-l14-60p, nema-l15-20p, nema-l15-30p, nema-l15-50p, nema-l15-60p, nema-l21-20p, nema-l21-30p, nema-l22-30p, cs6361c, cs6365c, cs8165c, cs8265c, cs8365c, cs8465c, ita-c, ita-e, ita-f, ita-ef, ita-g, ita-h, ita-i, ita-j, ita-k, ita-l, ita-m, ita-n, ita-o, usb-a, usb-b, usb-c, usb-mini-a, usb-mini-b, usb-micro-a, usb-micro-b, usb-micro-ab, usb-3-b, usb-3-micro-b, dc-terminal, saf-d-grid, neutrik-powercon-20, neutrik-powercon-32, neutrik-powercon-true1, neutrik-powercon-true1-top, ubiquiti-smartpower, hardwired, other]. ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/device_primary_ip.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_device_primary_ip Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- This resource is used to define the primary IP for a given device. The primary IP is reflected in the device Netbox UI, which identifies the Primary IPv4 and IPv6 addresses. --- # netbox_device_primary_ip (Resource) This resource is used to define the primary IP for a given device. The primary IP is reflected in the device Netbox UI, which identifies the Primary IPv4 and IPv6 addresses. ## Example Usage ```terraform # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_ip_address" "test_v4" { ip_address = "1.1.1.1/32" status = "active" device_interface_id = netbox_device_interface.test.id } resource "netbox_device_primary_ip" "test_v4" { device_id = netbox_device.test.id ip_address_id = netbox_ip_address.test.id } ``` ## Schema ### Required - `device_id` (Number) - `ip_address_id` (Number) ### Optional - `ip_address_version` (Number) Defaults to `4`. ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/device_rear_port.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_device_rear_port Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/dcim/rearport/: Like front ports, rear ports are pass-through ports which represent the continuation of a path from one cable to the next. Each rear port is defined with its physical type and a number of positions: Rear ports with more than one position can be mapped to multiple front ports. This can be useful for modeling instances where multiple paths share a common cable (for example, six discrete two-strand fiber connections sharing a 12-strand MPO cable). --- # netbox_device_rear_port (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/rearport/): > Like front ports, rear ports are pass-through ports which represent the continuation of a path from one cable to the next. Each rear port is defined with its physical type and a number of positions: Rear ports with more than one position can be mapped to multiple front ports. This can be useful for modeling instances where multiple paths share a common cable (for example, six discrete two-strand fiber connections sharing a 12-strand MPO cable). ## Example Usage ```terraform # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_rear_port" "test" { device_id = netbox_device.test.id name = "rear port 1" type = "8p8c" positions = 2 mark_connected = true } ``` ## Schema ### Required - `device_id` (Number) - `name` (String) - `positions` (Number) - `type` (String) One of [8p8c, 8p6c, 8p4c, 8p2c, 6p6c, 6p4c, 6p2c, 4p4c, 4p2c, gg45, tera-4p, tera-2p, tera-1p, 110-punch, bnc, f, n, mrj21, fc, lc, lc-pc, lc-upc, lc-apc, lsh, lsh-pc, lsh-upc, lsh-apc, mpo, mtrj, sc, sc-pc, sc-upc, sc-apc, st, cs, sn, sma-905, sma-906, urm-p2, urm-p4, urm-p8, splice, other]. ### Optional - `color_hex` (String) - `custom_fields` (Map of String) - `description` (String) - `label` (String) - `mark_connected` (Boolean) Defaults to `false`. - `module_id` (Number) - `tags` (Set of String) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/device_role.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_device_role Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/devices/#device-roles: Devices can be organized by functional roles, which are fully customizable by the user. For example, you might create roles for core switches, distribution switches, and access switches within your network. --- # netbox_device_role (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/devices/#device-roles): > Devices can be organized by functional roles, which are fully customizable by the user. For example, you might create roles for core switches, distribution switches, and access switches within your network. ## Example Usage ```terraform resource "netbox_device_role" "core_sw" { color_hex = "ff00ff" name = "core-sw" } ``` ## Schema ### Required - `color_hex` (String) - `name` (String) ### Optional - `description` (String) - `slug` (String) - `tags` (Set of String) - `vm_role` (Boolean) Defaults to `true`. ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/device_type.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_device_type Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/device-types/#device-types_1: A device type represents a particular make and model of hardware that exists in the real world. Device types define the physical attributes of a device (rack height and depth) and its individual components (console, power, network interfaces, and so on). --- # netbox_device_type (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/device-types/#device-types_1): > A device type represents a particular make and model of hardware that exists in the real world. Device types define the physical attributes of a device (rack height and depth) and its individual components (console, power, network interfaces, and so on). ## Example Usage ```terraform resource "netbox_manufacturer" "test" { name = "test" } resource "netbox_device_type" "test" { model = "test" part_number = "123" manufacturer_id = netbox_manufacturer.test.id } ``` ## Schema ### Required - `manufacturer_id` (Number) - `model` (String) ### Optional - `is_full_depth` (Boolean) - `part_number` (String) - `slug` (String) - `subdevice_role` (String) - `tags` (Set of String) - `u_height` (Number) Defaults to `1.0`. ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/event_rule.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_event_rule Resource - terraform-provider-netbox" subcategory: "Extras" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/event-rules/: NetBox can be configured via Event Rules to transmit outgoing webhooks to remote systems in response to internal object changes. The receiver can act on the data in these webhook messages to perform related tasks. Event rules can also execute custom scripts, enabling NetBox to run defined logic locally in response to object changes. --- # netbox_event_rule (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/event-rules/): > NetBox can be configured via Event Rules to transmit outgoing webhooks to remote systems in response to internal object changes. The receiver can act on the data in these webhook messages to perform related tasks. Event rules can also execute custom scripts, enabling NetBox to run defined logic locally in response to object changes. ## Example Usage ```terraform resource "netbox_webhook" "test" { name = "my-webhook" payload_url = "https://example.com/webhook" } resource "netbox_event_rule" "webhook" { name = "my-event-rule" content_types = ["dcim.site", "virtualization.cluster"] action_type = "webhook" action_object_id = netbox_webhook.test.id event_types = [ "object_created", "object_updated", "object_deleted", "job_started", "job_completed", "job_failed", "job_errored" ] } resource "netbox_event_rule" "script" { name = "my-script-event-rule" content_types = ["dcim.site"] action_type = "script" action_object_id = 42 # existing NetBox Script ID event_types = ["object_created"] } ``` ## Schema ### Required - `action_object_id` (Number) - `action_type` (String) Valid values are `webhook` and `script`. - `content_types` (Set of String) - `event_types` (Set of String) The types of event which will trigger this rule. By default, valid values are `object_created`, `oject_updated`, `object_deleted`, `job_started`, `job_completed`, `job_failed` and `job_errored`. - `name` (String) ### Optional - `conditions` (String) - `description` (String) - `enabled` (Boolean) Defaults to `true`. - `tags` (Set of String) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/group.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_group Resource - terraform-provider-netbox" subcategory: "Authentication" description: |- This resource is used to manage groups. --- # netbox_group (Resource) This resource is used to manage groups. ## Example Usage ```terraform resource "netbox_group" "test" { name = "test-group" } ``` ## Schema ### Required - `name` (String) ### Optional - `description` (String) Defaults to `""`. ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/interface.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_interface Resource - terraform-provider-netbox" subcategory: "Virtualization" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/virtualization/#interfaces: Virtual machine interfaces behave similarly to device interfaces, and can be assigned to VRFs, and may have IP addresses, VLANs, and services attached to them. However, given their virtual nature, they lack properties pertaining to physical attributes. For example, VM interfaces do not have a physical type and cannot have cables attached to them. --- # netbox_interface (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/virtualization/#interfaces): > Virtual machine interfaces behave similarly to device interfaces, and can be assigned to VRFs, and may have IP addresses, VLANs, and services attached to them. However, given their virtual nature, they lack properties pertaining to physical attributes. For example, VM interfaces do not have a physical type and cannot have cables attached to them. ## Example Usage ```terraform // Assume Netbox already has a VM whos name matches 'dc-west-myvm-20' data "netbox_virtual_machine" "myvm" { name_regex = "dc-west-myvm-20" } resource "netbox_interface" "myvm_eth0" { name = "eth0" virtual_machine_id = data.netbox_virtual_machine.myvm.id } // Assume existing VLAN resources 'test1' and 'test2' resource "netbox_interface" "myvm_eth1" { name = "eth1" enabled = true mac_address = "00:16:3E:A8:B5:D7" mode = "tagged" mtu = 1440 tagged_vlans = [netbox_vlan.test1.id] untagged_vlan = netbox_vlan.test2.id virtual_machine_id = netbox_virtual_machine.test.id } ``` ## Schema ### Required - `name` (String) - `virtual_machine_id` (Number) ### Optional - `bridge_interface_id` (Number) ID of the bridge interface this interface belongs to. - `description` (String) - `enabled` (Boolean) Defaults to `true`. - `mac_address` (String) - `mode` (String) Valid values are `access`, `tagged` and `tagged-all`. - `mtu` (Number) - `tagged_vlans` (Set of Number) - `tags` (Set of String) - `type` (String, Deprecated) - `untagged_vlan` (Number) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/interface_template.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_interface_template Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/dcim/interfacetemplate/: A template for a network interface that will be created on all instantiations of the parent device type. See the interface documentation for more detail. --- # netbox_interface_template (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/interfacetemplate/): > A template for a network interface that will be created on all instantiations of the parent device type. See the interface documentation for more detail. ## Example Usage ```terraform resource "netbox_manufacturer" "test" { name = "my-manufacturer" } resource "netbox_device_type" "test" { model = "test-model" slug = "test-model" part_number = "test-part-number" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_interface_template" "test" { name = "eth0" description = "eth0 description" label = "eth0 label" device_type_id = netbox_device_type.test.id type = "100base-tx" mgmt_only = true } ``` ## Schema ### Required - `name` (String) - `type` (String) ### Optional - `description` (String) - `device_type_id` (Number) Exactly one of `device_type_id` or `module_type_id` must be given. - `label` (String) - `mgmt_only` (Boolean) - `module_type_id` (Number) Exactly one of `device_type_id` or `module_type_id` must be given. ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/inventory_item.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_inventory_item Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/dcim/inventoryitem/: Inventory items represent hardware components installed within a device, such as a power supply or CPU or line card. They are intended to be used primarily for inventory purposes. --- # netbox_inventory_item (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/inventoryitem/): > Inventory items represent hardware components installed within a device, such as a power supply or CPU or line card. They are intended to be used primarily for inventory purposes. ## Example Usage ```terraform # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_rear_port" "test" { device_id = netbox_device.test.id name = "rear port" type = "8p8c" positions = 1 mark_connected = true } resource "netbox_inventory_item" "parent" { device_id = netbox_device.test.id name = "Parent Item" } resource "netbox_inventory_item" "test" { device_id = netbox_device.test.id name = "Child Item" parent_id = netbox_inventory_item.parent.id component_type = "dcim.rearport" component_id = netbox_device_rear_port.test.id } ``` ## Schema ### Required - `device_id` (Number) - `name` (String) ### Optional - `asset_tag` (String) - `component_id` (Number) Required when `component_type` is set. - `component_type` (String) - `custom_fields` (Map of String) - `description` (String) - `discovered` (Boolean) Defaults to `false`. - `label` (String) - `manufacturer_id` (Number) - `parent_id` (Number) - `part_id` (String) - `role_id` (Number) - `serial` (String) - `tags` (Set of String) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/inventory_item_role.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_inventory_item_role Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/dcim/inventoryitemrole/: Inventory items can be organized by functional roles, which are fully customizable by the user. For example, you might create roles for power supplies, fans, interface optics, etc. --- # netbox_inventory_item_role (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/inventoryitemrole/): > Inventory items can be organized by functional roles, which are fully customizable by the user. For example, you might create roles for power supplies, fans, interface optics, etc. ## Example Usage ```terraform # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_inventory_item_role" "test" { name = "Role 1" slug = "role-1-slug" color_hex = "123456" } resource "netbox_inventory_item" "parent" { device_id = netbox_device.test.id name = "Inventory Item 1" role_id = netbox_inventory_item_role.test.id } ``` ## Schema ### Required - `color_hex` (String) - `name` (String) - `slug` (String) ### Optional - `custom_fields` (Map of String) - `description` (String) - `tags` (Set of String) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/ip_address.md ================================================ --- page_title: "netbox_ip_address Resource - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/ipam/#ip-addresses: An IP address comprises a single host address (either IPv4 or IPv6) and its subnet mask. Its mask should match exactly how the IP address is configured on an interface in the real world. Like a prefix, an IP address can optionally be assigned to a VRF (otherwise, it will appear in the "global" table). IP addresses are automatically arranged under parent prefixes within their respective VRFs according to the IP hierarchy. --- # netbox_ip_address (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/ipam/#ip-addresses): > An IP address comprises a single host address (either IPv4 or IPv6) and its subnet mask. Its mask should match exactly how the IP address is configured on an interface in the real world. > > Like a prefix, an IP address can optionally be assigned to a VRF (otherwise, it will appear in the "global" table). IP addresses are automatically arranged under parent prefixes within their respective VRFs according to the IP hierarchy. ## Example Usage ### Creating an IP address that is assigned to a virtual machine interface Starting with provider version 3.5.0, you can use the `virtual_machine_interface_id` attribute to assign an IP address to a virtual machine interface. You can also use the `interface_id` and `object_type` attributes instead. With `virtual_machine_interface_id`: ```terraform // Assuming a virtual machine with the id `123` exists resource "netbox_interface" "this" { name = "eth0" virtual_machine_id = 123 } resource "netbox_ip_address" "this" { ip_address = "10.0.0.60/24" status = "active" virtual_machine_interface_id = netbox_interface.this.id } ``` With `object_type` and `interface_id`: ```terraform // Assuming a virtual machine with the id `123` exists resource "netbox_interface" "this" { name = "eth0" virtual_machine_id = 123 } resource "netbox_ip_address" "this" { ip_address = "10.0.0.60/24" status = "active" interface_id = netbox_interface.this.id object_type = "virtualization.vminterface" } ``` ### Creating an IP address that is assigned to a device interface Starting with provider version 3.5.0, you can use the `device_interface_id` attribute to assign an IP address to a device interface. You can also use the `interface_id` and `object_type` attributes instead. With `device_interface_id`: ```terraform // Assuming a device with the id `123` exists resource "netbox_device_interface" "this" { name = "eth0" device_id = 123 type = "1000base-t" } resource "netbox_ip_address" "this" { ip_address = "10.0.0.60/24" status = "active" device_interface_id = netbox_device_interface.this.id } ``` With `object_type` and `interface_id`: ```terraform // Assuming a device with the id `123` exists resource "netbox_device_interface" "this" { name = "eth0" device_id = 123 type = "1000base-t" } resource "netbox_ip_address" "this" { ip_address = "10.0.0.60/24" status = "active" interface_id = netbox_device_interface.this.id object_type = "dcim.interface" } ``` ### Creating an IP address that is not assigned to anything You can create an IP address that is not assigned to anything by omitting the attributes mentioned above. ```terraform resource "netbox_ip_address" "this" { ip_address = "10.0.0.50/24" status = "reserved" } ``` ## Schema ### Required - `ip_address` (String) - `status` (String) Valid values are `active`, `reserved`, `deprecated`, `dhcp` and `slaac`. ### Optional - `custom_fields` (Map of String) - `description` (String) - `device_interface_id` (Number) Conflicts with `interface_id` and `virtual_machine_interface_id`. - `dns_name` (String) - `interface_id` (Number) Required when `object_type` is set. - `nat_inside_address_id` (Number) - `object_type` (String) Valid values are `virtualization.vminterface` and `dcim.interface`. Required when `interface_id` is set. - `role` (String) Valid values are `loopback`, `secondary`, `anycast`, `vip`, `vrrp`, `hsrp`, `glbp` and `carp`. - `tags` (Set of String) - `tenant_id` (Number) - `virtual_machine_interface_id` (Number) Conflicts with `interface_id` and `device_interface_id`. - `vrf_id` (Number) ### Read-Only - `id` (String) The ID of this resource. - `nat_outside_addresses` (List of Object) (see [below for nested schema](#nestedatt--nat_outside_addresses)) - `tags_all` (Set of String) ### Nested Schema for `nat_outside_addresses` Read-Only: - `address_family` (Number) - `id` (Number) - `ip_address` (String) ================================================ FILE: docs/resources/ip_range.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_ip_range Resource - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/ipam/#ip-ranges: This model represents an arbitrary range of individual IPv4 or IPv6 addresses, inclusive of its starting and ending addresses. For instance, the range 192.0.2.10 to 192.0.2.20 has eleven members. (The total member count is available as the size property on an IPRange instance.) Like prefixes and IP addresses, each IP range may optionally be assigned to a VRF and/or tenant. --- # netbox_ip_range (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/ipam/#ip-ranges): > This model represents an arbitrary range of individual IPv4 or IPv6 addresses, inclusive of its starting and ending addresses. For instance, the range 192.0.2.10 to 192.0.2.20 has eleven members. (The total member count is available as the size property on an IPRange instance.) Like prefixes and IP addresses, each IP range may optionally be assigned to a VRF and/or tenant. ## Example Usage ```terraform resource "netbox_ip_range" "cust_a_prod" { start_address = "10.0.0.1/24" end_address = "10.0.0.50/24" tags = ["customer-a", "prod"] } ``` ## Schema ### Required - `end_address` (String) - `start_address` (String) ### Optional - `custom_fields` (Map of String) - `description` (String) - `role_id` (Number) - `status` (String) Valid values are `active`, `reserved` and `deprecated`. Defaults to `active`. - `tags` (Set of String) - `tenant_id` (Number) - `vrf_id` (Number) ### Read-Only - `id` (String) The ID of this resource. - `size` (Number) The total member count of the IP range. - `tags_all` (Set of String) ================================================ FILE: docs/resources/ipam_role.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_ipam_role Resource - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/ipam/#prefixvlan-roles: A role indicates the function of a prefix or VLAN. For example, you might define Data, Voice, and Security roles. Generally, a prefix will be assigned the same functional role as the VLAN to which it is assigned (if any). --- # netbox_ipam_role (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/ipam/#prefixvlan-roles): > A role indicates the function of a prefix or VLAN. For example, you might define Data, Voice, and Security roles. Generally, a prefix will be assigned the same functional role as the VLAN to which it is assigned (if any). ## Example Usage ```terraform resource "netbox_ipam_role" "test_basic" { name = "test" } ``` ## Schema ### Required - `name` (String) ### Optional - `description` (String) - `slug` (String) - `weight` (Number) ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/location.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_location Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/sites-and-racks/#locations: Racks and devices can be grouped by location within a site. A location may represent a floor, room, cage, or similar organizational unit. Locations can be nested to form a hierarchy. For example, you may have floors within a site, and rooms within a floor. Each location must have a name that is unique within its parent site and location, if any. --- # netbox_location (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/sites-and-racks/#locations): > Racks and devices can be grouped by location within a site. A location may represent a floor, room, cage, or similar organizational unit. Locations can be nested to form a hierarchy. For example, you may have floors within a site, and rooms within a floor. Each location must have a name that is unique within its parent site and location, if any. ## Example Usage ```terraform resource "netbox_site" "test" { name = "test" } resource "netbox_tenant" "test" { name = "test" } resource "netbox_location" "test" { name = "test" description = "my description" site_id = netbox_site.test.id tenant_id = netbox_tenant.test.id } ``` ## Schema ### Required - `name` (String) - `site_id` (Number) ### Optional - `custom_fields` (Map of String) - `description` (String) - `facility` (String) - `parent_id` (Number) - `slug` (String) - `tags` (Set of String) - `tenant_id` (Number) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/mac_address.md ================================================ --- page_title: "netbox_mac_address Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://netboxlabs.com/docs/netbox/models/dcim/macaddress/: A MAC address object in NetBox comprises a single Ethernet link layer address, and represents a MAC address as reported by or assigned to a network interface. MAC addresses can be assigned to device and virtual machine interfaces. A MAC address can be specified as the primary MAC address for a given device or VM interface. --- # netbox_mac_address (Resource) From the [official documentation](https://netboxlabs.com/docs/netbox/models/dcim/macaddress/): > A MAC address object in NetBox comprises a single Ethernet link layer address, and represents a MAC address as reported by or assigned to a network interface. MAC addresses can be assigned to device and virtual machine interfaces. A MAC address can be specified as the primary MAC address for a given device or VM interface. ## Example Usage ### Creating a MAC address that is assigned to a virtual machine interface With `virtual_machine_interface_id`: ```terraform // Assuming a virtual machine with the id `123` exists resource "netbox_interface" "this" { name = "eth0" virtual_machine_id = 123 } resource "netbox_mac_address" "this" { mac_address = "00:1A:2B:3C:4D:5E" virtual_machine_interface_id = netbox_interface.this.id } ``` With `object_type` and `interface_id`: ```terraform // Assuming a virtual machine with the id `123` exists resource "netbox_interface" "this" { name = "eth0" virtual_machine_id = 123 } resource "netbox_mac_address" "this" { mac_address = "00:1A:2B:3C:4D:5E" interface_id = netbox_interface.this.id object_type = "virtualization.vminterface" } ``` ### Creating a MAC address that is assigned to a device interface With `device_interface_id`: ```terraform // Assuming a device with the id `123` exists resource "netbox_device_interface" "this" { name = "eth0" device_id = 123 type = "1000base-t" } resource "netbox_mac_address" "this" { mac_address = "00:1A:2B:3C:4D:5E" device_interface_id = netbox_device_interface.this.id } ``` With `object_type` and `interface_id`: ```terraform // Assuming a device with the id `123` exists resource "netbox_device_interface" "this" { name = "eth0" device_id = 123 type = "1000base-t" } resource "netbox_mac_address" "this" { mac_address = "00:1A:2B:3C:4D:5E" interface_id = netbox_device_interface.this.id object_type = "dcim.interface" } ``` ### Creating a MAC address that is not assigned to anything You can create a MAC address that is not assigned to anything by omitting the attributes mentioned above. ```terraform resource "netbox_mac_address" "this" { mac_address = "00:1A:2B:3C:4D:5E" } ``` ## Schema ### Required - `mac_address` (String) ### Optional - `comments` (String) - `custom_fields` (Map of String) - `description` (String) - `device_interface_id` (Number) Conflicts with `interface_id` and `virtual_machine_interface_id`. - `interface_id` (Number) Required when `object_type` is set. - `object_type` (String) Valid values are `virtualization.vminterface` and `dcim.interface`. Required when `interface_id` is set. - `tags` (Set of String) - `virtual_machine_interface_id` (Number) Conflicts with `interface_id` and `device_interface_id`. ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/manufacturer.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_manufacturer Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/device-types/#manufacturers: A manufacturer represents the "make" of a device; e.g. Cisco or Dell. Each device type must be assigned to a manufacturer. (Inventory items and platforms may also be associated with manufacturers.) Each manufacturer must have a unique name and may have a description assigned to it. --- # netbox_manufacturer (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/device-types/#manufacturers): > A manufacturer represents the "make" of a device; e.g. Cisco or Dell. Each device type must be assigned to a manufacturer. (Inventory items and platforms may also be associated with manufacturers.) Each manufacturer must have a unique name and may have a description assigned to it. ## Example Usage ```terraform resource "netbox_manufacturer" "test" { name = "testmanufacturer" } ``` ## Schema ### Required - `name` (String) ### Optional - `slug` (String) ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/module.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_module Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/dcim/module/: A module is a field-replaceable hardware component installed within a device which houses its own child components. The most common example is a chassis-based router or switch. Similar to devices, modules are instantiated from module types, and any components associated with the module type are automatically instantiated on the new model. Each module must be installed within a module bay on a device, and each module bay may have only one module installed in it. --- # netbox_module (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/module/): > A module is a field-replaceable hardware component installed within a device which houses its own child components. The most common example is a chassis-based router or switch. Similar to devices, modules are instantiated from module types, and any components associated with the module type are automatically instantiated on the new model. Each module must be installed within a module bay on a device, and each module bay may have only one module installed in it. ## Example Usage ```terraform # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_module_bay" "test" { device_id = netbox_device.test.id name = "SFP" } resource "netbox_manufacturer" "test" { name = "Dell" } resource "netbox_module_type" "test" { manufacturer_id = netbox_manufacturer.test.id model = "Networking" } resource "netbox_module" "test" { device_id = netbox_device.test.id module_bay_id = netbox_device_module_bay.test.id module_type_id = netbox_module_type.test.id status = "active" description = "SFP card" } ``` ## Schema ### Required - `device_id` (Number) - `module_bay_id` (Number) - `module_type_id` (Number) - `status` (String) One of [offline, active, planned, staged, failed, decommissioning]. ### Optional - `asset_tag` (String) - `comments` (String) - `custom_fields` (Map of String) - `description` (String) - `serial` (String) - `tags` (Set of String) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/module_type.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_module_type Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/dcim/moduletype/: A module type represents a specific make and model of hardware component which is installable within a device's module bay and has its own child components. For example, consider a chassis-based switch or router with a number of field-replaceable line cards. Each line card has its own model number and includes a certain set of components such as interfaces. Each module type may have a manufacturer, model number, and part number assigned to it. --- # netbox_module_type (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/moduletype/): > A module type represents a specific make and model of hardware component which is installable within a device's module bay and has its own child components. For example, consider a chassis-based switch or router with a number of field-replaceable line cards. Each line card has its own model number and includes a certain set of components such as interfaces. Each module type may have a manufacturer, model number, and part number assigned to it. ## Example Usage ```terraform resource "netbox_manufacturer" "test" { name = "Dell" } resource "netbox_module_type" "test" { manufacturer_id = netbox_manufacturer.test.id model = "Networking" } ``` ## Schema ### Required - `manufacturer_id` (Number) - `model` (String) ### Optional - `comments` (String) - `custom_fields` (Map of String) - `description` (String) - `part_number` (String) - `tags` (Set of String) - `weight` (Number) - `weight_unit` (String) One of [kg, g, lb, oz]. Required when `weight` is set. ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/permission.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_permission Resource - terraform-provider-netbox" subcategory: "Authentication" description: |- This resource manages the object-based permissions for Netbox users, built into the application. Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type. For more information, see the Netbox Object-Based Permissions Docs. https://docs.netbox.dev/en/stable/administration/permissions/ --- # netbox_permission (Resource) This resource manages the object-based permissions for Netbox users, built into the application. > Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type. > For more information, see the [Netbox Object-Based Permissions Docs.](https://docs.netbox.dev/en/stable/administration/permissions/) ## Example Usage ```terraform resource "netbox_user" "test" { username = "johndoe" password = "Abcdefghijkl1" active = true staff = true } resource "netbox_permission" "test" { name = "test" description = "my description" enabled = true object_types = ["ipam.prefix"] actions = ["add", "change"] users = [netbox_user.test.id] constraints = jsonencode([{ "status" = "active" }]) } ``` ## Schema ### Required - `actions` (Set of String) A list actions that are allowed on the object types. Acceptable values are `view`, `add`, `change`, or `delete`. - `name` (String) The name of the permission object. - `object_types` (Set of String) A list of object types that the permission object allows access to. Should be in a form the API can accept. For example: `circuits.provider`, `dcim.inventoryitem`, etc. ### Optional - `constraints` (String) A JSON string of an arbitrary filter used to limit the granted action(s) to a specific subset of objects. For more information on correct syntax, see https://docs.netbox.dev/en/stable/administration/permissions/#constraints. - `description` (String) The description of the permission object. - `enabled` (Boolean) Whether the permission object is enabled or not. Defaults to `true`. - `groups` (Set of Number) A list of group IDs that have been assigned to this permission object. - `users` (Set of Number) A list of user IDs that have been assigned to this permission object. ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/platform.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_platform Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/devices/#platforms: A platform defines the type of software running on a device or virtual machine. This can be helpful to model when it is necessary to distinguish between different versions or feature sets. Note that two devices of the same type may be assigned different platforms: For example, one Juniper MX240 might run Junos 14 while another runs Junos 15. --- # netbox_platform (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/devices/#platforms): > A platform defines the type of software running on a device or virtual machine. This can be helpful to model when it is necessary to distinguish between different versions or feature sets. Note that two devices of the same type may be assigned different platforms: For example, one Juniper MX240 might run Junos 14 while another runs Junos 15. ## Example Usage ```terraform // Resource for PanOS (e.g. Panorama from Palo Alto) resource "netbox_platform" "PANOS" { name = "PANOS" } ``` ## Schema ### Required - `name` (String) ### Optional - `manufacturer_id` (Number) - `slug` (String) ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/power_feed.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_power_feed Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/dcim/powerfeed/: A power feed represents the distribution of power from a power panel to a particular device, typically a power distribution unit (PDU). The power port (inlet) on a device can be connected via a cable to a power feed. A power feed may optionally be assigned to a rack to allow more easily tracking the distribution of power among racks. --- # netbox_power_feed (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/powerfeed/): > A power feed represents the distribution of power from a power panel to a particular device, typically a power distribution unit (PDU). The power port (inlet) on a device can be connected via a cable to a power feed. A power feed may optionally be assigned to a rack to allow more easily tracking the distribution of power among racks. ## Example Usage ```terraform resource "netbox_site" "test" { name = "Site 1" status = "active" } resource "netbox_location" "test" { name = "Location 1" site_id = netbox_site.test.id } resource "netbox_power_panel" "test" { name = "Power Panel 1" site_id = netbox_site.test.id location_id = netbox_location.test.id } resource "netbox_power_feed" "test" { power_panel_id = netbox_power_panel.test.id name = "Power Feed 1" status = "active" type = "primary" supply = "ac" phase = "single-phase" voltage = 250 amperage = 100 max_percent_utilization = 80 } ``` ## Schema ### Required - `amperage` (Number) - `max_percent_utilization` (Number) - `name` (String) - `phase` (String) One of [single-phase, three-phase]. - `power_panel_id` (Number) - `status` (String) One of [offline, active, planned, failed]. - `supply` (String) One of [ac, dc]. - `type` (String) One of [primary, redundant]. - `voltage` (Number) ### Optional - `comments` (String) - `custom_fields` (Map of String) - `description` (String) - `mark_connected` (Boolean) Defaults to `false`. - `rack_id` (Number) - `tags` (Set of String) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/power_panel.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_power_panel Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/dcim/powerpanel/: A power panel represents the origin point in NetBox for electrical power being disseminated by one or more power feeds. In a data center environment, one power panel often serves a group of racks, with an individual power feed extending to each rack, though this is not always the case. It is common to have two sets of panels and feeds arranged in parallel to provide redundant power to each rack. --- # netbox_power_panel (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/powerpanel/): > A power panel represents the origin point in NetBox for electrical power being disseminated by one or more power feeds. In a data center environment, one power panel often serves a group of racks, with an individual power feed extending to each rack, though this is not always the case. It is common to have two sets of panels and feeds arranged in parallel to provide redundant power to each rack. ## Example Usage ```terraform resource "netbox_site" "test" { name = "Site 1" status = "active" } resource "netbox_location" "test" { name = "Location 1" site_id = netbox_site.test.id } resource "netbox_power_panel" "test" { name = "Power Panel 1" site_id = netbox_site.test.id location_id = netbox_location.test.id } ``` ## Schema ### Required - `name` (String) - `site_id` (Number) ### Optional - `comments` (String) - `custom_fields` (Map of String) - `description` (String) - `location_id` (Number) - `tags` (Set of String) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/prefix.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_prefix Resource - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/ipam/#prefixes: A prefix is an IPv4 or IPv6 network and mask expressed in CIDR notation (e.g. 192.0.2.0/24). A prefix entails only the "network portion" of an IP address: All bits in the address not covered by the mask must be zero. (In other words, a prefix cannot be a specific IP address.) Prefixes are automatically organized by their parent aggregates. Additionally, each prefix can be assigned to a particular site and virtual routing and forwarding instance (VRF). Each VRF represents a separate IP space or routing table. All prefixes not assigned to a VRF are considered to be in the "global" table. --- # netbox_prefix (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/ipam/#prefixes): > A prefix is an IPv4 or IPv6 network and mask expressed in CIDR notation (e.g. 192.0.2.0/24). A prefix entails only the "network portion" of an IP address: All bits in the address not covered by the mask must be zero. (In other words, a prefix cannot be a specific IP address.) > > Prefixes are automatically organized by their parent aggregates. Additionally, each prefix can be assigned to a particular site and virtual routing and forwarding instance (VRF). Each VRF represents a separate IP space or routing table. All prefixes not assigned to a VRF are considered to be in the "global" table. ## Example Usage ```terraform resource "netbox_prefix" "my_prefix" { prefix = "10.0.0.0/24" status = "active" description = "test prefix" } ``` ## Schema ### Required - `prefix` (String) - `status` (String) Valid values are `active`, `container`, `reserved` and `deprecated`. ### Optional - `custom_fields` (Map of String) - `description` (String) - `is_pool` (Boolean) - `location_id` (Number) Conflicts with `site_id`, `site_group_id` and `region_id`. - `mark_utilized` (Boolean) - `region_id` (Number) Conflicts with `location_id`, `site_id` and `site_group_id`. - `role_id` (Number) - `site_group_id` (Number) Conflicts with `location_id`, `site_id` and `region_id`. - `site_id` (Number) Conflicts with `location_id`, `site_group_id` and `region_id`. - `tags` (Set of String) - `tenant_id` (Number) - `vlan_id` (Number) - `vrf_id` (Number) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/primary_ip.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_primary_ip Resource - terraform-provider-netbox" subcategory: "Virtualization" description: |- This resource is used to define the primary IP for a given virtual machine. The primary IP is reflected in the Virtual machine Netbox UI, which identifies the Primary IPv4 and IPv6 addresses. --- # netbox_primary_ip (Resource) This resource is used to define the primary IP for a given virtual machine. The primary IP is reflected in the Virtual machine Netbox UI, which identifies the Primary IPv4 and IPv6 addresses. ## Example Usage ```terraform // Assumes Netbox already has a VM whos name matches 'dc-west-myvm-20' data "netbox_virtual_machine" "myvm" { name_regex = "dc-west-myvm-20" } resource "netbox_interface" "myvm_eth0" { name = "eth0" virtual_machine_id = data.netbox_virtual_machine.myvm.id } resource "netbox_ip_address" "myvm_ip" { ip_address = "10.0.0.60/24" status = "active" interface_id = netbox_interface.myvm_eth0.id } resource "netbox_primary_ip" "myvm_primary_ip" { ip_address_id = netbox_ip_address.myvm_ip.id virtual_machine_id = data.netbox_virtual_machine.myvm.id } ``` ## Schema ### Required - `ip_address_id` (Number) - `virtual_machine_id` (Number) ### Optional - `ip_address_version` (Number) Defaults to `4`. ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/rack.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_rack Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/dcim/rack/: The rack model represents a physical two- or four-post equipment rack in which devices can be installed. Each rack must be assigned to a site, and may optionally be assigned to a location within that site. Racks can also be organized by user-defined functional roles. The name and facility ID of each rack within a location must be unique. Rack height is measured in rack units (U); racks are commonly between 42U and 48U tall, but NetBox allows you to define racks of arbitrary height. A toggle is provided to indicate whether rack units are in ascending (from the ground up) or descending order. Each rack is assigned a name and (optionally) a separate facility ID. This is helpful when leasing space in a data center your organization does not own: The facility will often assign a seemingly arbitrary ID to a rack (for example, "M204.313") whereas internally you refer to is simply as "R113." A unique serial number and asset tag may also be associated with each rack. --- # netbox_rack (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/rack/): > The rack model represents a physical two- or four-post equipment rack in which devices can be installed. Each rack must be assigned to a site, and may optionally be assigned to a location within that site. Racks can also be organized by user-defined functional roles. The name and facility ID of each rack within a location must be unique. Rack height is measured in rack units (U); racks are commonly between 42U and 48U tall, but NetBox allows you to define racks of arbitrary height. A toggle is provided to indicate whether rack units are in ascending (from the ground up) or descending order. Each rack is assigned a name and (optionally) a separate facility ID. This is helpful when leasing space in a data center your organization does not own: The facility will often assign a seemingly arbitrary ID to a rack (for example, "M204.313") whereas internally you refer to is simply as "R113." A unique serial number and asset tag may also be associated with each rack. ## Example Usage ```terraform resource "netbox_site" "test" { name = "test" status = "active" } resource "netbox_rack" "test" { name = "test" site_id = netbox_site.test.id status = "reserved" width = 19 u_height = 48 } ``` ## Schema ### Required - `name` (String) - `site_id` (Number) - `status` (String) Valid values are `reserved`, `available`, `planned`, `active` and `deprecated`. ### Optional - `asset_tag` (String) - `comments` (String) - `custom_fields` (Map of String) - `desc_units` (Boolean) If rack units are descending. Defaults to `false`. - `description` (String) - `facility_id` (String) - `form_factor` (String) Valid values are `2-post-frame`, `4-post-frame`, `4-post-cabinet`, `wall-frame`, `wall-frame-vertical`, `wall-cabinet` and `wall-cabinet-vertical`. - `location_id` (Number) - `max_weight` (Number) - `mounting_depth` (Number) - `outer_depth` (Number) - `outer_unit` (String) Valid values are `mm` and `in`. Required when `outer_width` and `outer_depth` is set. - `outer_width` (Number) - `role_id` (Number) - `serial` (String) - `tags` (Set of String) - `tenant_id` (Number) - `u_height` (Number) - `weight` (Number) - `weight_unit` (String) Valid values are `kg`, `g`, `lb` and `oz`. Required when `weight` and `max_weight` is set. - `width` (Number) Valid values are `10`, `19`, `21` and `23`. ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/rack_reservation.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_rack_reservation Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/dcim/rackreservation/: Users can reserve specific units within a rack for future use. An arbitrary set of units within a rack can be associated with a single reservation, but reservations cannot span multiple racks. A description is required for each reservation, reservations may optionally be associated with a specific tenant. --- # netbox_rack_reservation (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/rackreservation/): > Users can reserve specific units within a rack for future use. An arbitrary set of units within a rack can be associated with a single reservation, but reservations cannot span multiple racks. A description is required for each reservation, reservations may optionally be associated with a specific tenant. ## Example Usage ```terraform resource "netbox_site" "test" { name = "test" status = "active" } resource "netbox_rack" "test" { name = "test" site_id = netbox_site.test.id status = "active" width = 10 u_height = 40 } resource "netbox_rack_reservation" "test" { rack_id = netbox_rack.test.id units = [1, 2, 3, 4, 5] user_id = 1 description = "my description" } ``` ## Schema ### Required - `description` (String) - `rack_id` (Number) - `units` (Set of Number) - `user_id` (Number) ### Optional - `comments` (String) - `tags` (Set of String) - `tenant_id` (Number) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/rack_role.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_rack_role Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/dcim/rackrole/: Each rack can optionally be assigned a user-defined functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices. --- # netbox_rack_role (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/rackrole/): > Each rack can optionally be assigned a user-defined functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices. ## Example Usage ```terraform resource "netbox_rack_role" "test" { name = "test" color_hex = "111111" } ``` ## Schema ### Required - `color_hex` (String) - `name` (String) ### Optional - `description` (String) - `slug` (String) - `tags` (Set of String) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/rack_type.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_rack_type Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://netboxlabs.com/docs/netbox/en/stable/models/dcim/racktype/: A rack type defines the physical characteristics of a particular model of rack. --- # netbox_rack_type (Resource) From the [official documentation](https://netboxlabs.com/docs/netbox/en/stable/models/dcim/racktype/): > A rack type defines the physical characteristics of a particular model of rack. ## Example Usage ```terraform resource "netbox_manufacturer" "test" { name = "my-manufacturer" } resource "netbox_rack_type" "test" { model = "mymodel" manufacturer_id = netbox_manufacturer.test.id width = 19 u_height = 48 starting_unit = 1 form_factor = "2-post-frame" description = "My description" outer_width = 10 outer_depth = 15 outer_unit = "mm" weight = 15 max_weight = 20 weight_unit = "kg" mounting_depth_mm = 21 comments = "My comments" } ``` ## Schema ### Required - `form_factor` (String) Valid values are `2-post-frame`, `4-post-frame`, `4-post-cabinet`, `wall-frame`, `wall-frame-vertical`, `wall-cabinet` and `wall-cabinet-vertical`. - `model` (String) - `starting_unit` (Number) - `u_height` (Number) - `width` (Number) Valid values are `10`, `19`, `21` and `23`. ### Optional - `comments` (String) - `description` (String) - `manufacturer_id` (Number) - `max_weight` (Number) - `mounting_depth_mm` (Number) - `outer_depth` (Number) - `outer_unit` (String) Valid values are `mm` and `in`. Required when `outer_width` and `outer_depth` is set. - `outer_width` (Number) - `slug` (String) - `tags` (Set of String) - `weight` (Number) - `weight_unit` (String) Valid values are `kg`, `g`, `lb` and `oz`. Required when `weight` and `max_weight` is set. ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/region.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_region Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/sites-and-racks/#regions: Sites can be arranged geographically using regions. A region might represent a continent, country, city, campus, or other area depending on your use case. Regions can be nested recursively to construct a hierarchy. For example, you might define several country regions, and within each of those several state or city regions to which sites are assigned. Each region must have a name that is unique within its parent region, if any. --- # netbox_region (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/sites-and-racks/#regions): > Sites can be arranged geographically using regions. A region might represent a continent, country, city, campus, or other area depending on your use case. Regions can be nested recursively to construct a hierarchy. For example, you might define several country regions, and within each of those several state or city regions to which sites are assigned. > > Each region must have a name that is unique within its parent region, if any. ## Example Usage ```terraform resource "netbox_region" "test" { name = "test" description = "test description" } ``` ## Schema ### Required - `name` (String) ### Optional - `description` (String) - `parent_region_id` (Number) - `slug` (String) ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/rir.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_rir Resource - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/ipam/#regional-internet-registries-rirs: Regional Internet registries are responsible for the allocation of globally-routable address space. The five RIRs are ARIN, RIPE, APNIC, LACNIC, and AFRINIC. However, some address space has been set aside for internal use, such as defined in RFCs 1918 and 6598. NetBox considers these RFCs as a sort of RIR as well; that is, an authority which "owns" certain address space. There also exist lower-tier registries which serve particular geographic areas. --- # netbox_rir (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/ipam/#regional-internet-registries-rirs): > Regional Internet registries are responsible for the allocation of globally-routable address space. The five RIRs are ARIN, RIPE, APNIC, LACNIC, and AFRINIC. However, some address space has been set aside for internal use, such as defined in RFCs 1918 and 6598. NetBox considers these RFCs as a sort of RIR as well; that is, an authority which "owns" certain address space. There also exist lower-tier registries which serve particular geographic areas. ## Example Usage ```terraform resource "netbox_rir" "test" { name = "test" description = "my description" } ``` ## Schema ### Required - `name` (String) ### Optional - `description` (String) - `is_private` (Boolean) Defaults to `false`. - `slug` (String) ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/route_target.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_route_target Resource - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/ipam/routetarget/: A route target is a particular type of extended BGP community used to control the redistribution of routes among VRF tables in a network. Route targets can be assigned to individual VRFs in NetBox as import or export targets (or both) to model this exchange in an L3VPN. Each route target must be given a unique name, which should be in a format prescribed by RFC 4364, similar to a VR route distinguisher. --- # netbox_route_target (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/ipam/routetarget/): > A route target is a particular type of extended BGP community used to control the redistribution of routes among VRF tables in a network. Route targets can be assigned to individual VRFs in NetBox as import or export targets (or both) to model this exchange in an L3VPN. Each route target must be given a unique name, which should be in a format prescribed by RFC 4364, similar to a VR route distinguisher. ## Example Usage ```terraform resource "netbox_tenant" "test" { name = "test" } resource "netbox_route_target" "test" { name = "test" description = "my description" tenant_id = netbox_tenant.test.id } ``` ## Schema ### Required - `name` (String) ### Optional - `description` (String) - `tags` (Set of String) - `tenant_id` (Number) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/service.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_service Resource - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/services/#services: A service represents a layer four TCP or UDP service available on a device or virtual machine. For example, you might want to document that an HTTP service is running on a device. Each service includes a name, protocol, and port number; for example, "SSH (TCP/22)" or "DNS (UDP/53)." A service may optionally be bound to one or more specific IP addresses belonging to its parent device or VM. (If no IP addresses are bound, the service is assumed to be reachable via any assigned IP address. --- # netbox_service (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/services/#services): > A service represents a layer four TCP or UDP service available on a device or virtual machine. For example, you might want to document that an HTTP service is running on a device. Each service includes a name, protocol, and port number; for example, "SSH (TCP/22)" or "DNS (UDP/53)." > > A service may optionally be bound to one or more specific IP addresses belonging to its parent device or VM. (If no IP addresses are bound, the service is assumed to be reachable via any assigned IP address. ## Example Usage ```terraform // Assumes Netbox already has a VM whos name matches 'dc-west-myvm-20' data "netbox_virtual_machine" "myvm" { name_regex = "dc-west-myvm-20" } resource "netbox_service" "ssh" { name = "ssh" ports = [22] protocol = "tcp" virtual_machine_id = data.netbox_virtual_machine.myvm.id } ``` ## Schema ### Required - `name` (String) - `protocol` (String) Valid values are `tcp`, `udp` and `sctp`. ### Optional - `custom_fields` (Map of String) - `description` (String) - `device_id` (Number) Exactly one of `virtual_machine_id` or `device_id` must be given. - `port` (Number, Deprecated) Exactly one of `port` or `ports` must be given. - `ports` (Set of Number) Exactly one of `port` or `ports` must be given. - `tags` (Set of String) - `virtual_machine_id` (Number) Exactly one of `virtual_machine_id` or `device_id` must be given. ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/site.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_site Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/sites-and-racks/#sites: How you choose to employ sites when modeling your network may vary depending on the nature of your organization, but generally a site will equate to a building or campus. For example, a chain of banks might create a site to represent each of its branches, a site for its corporate headquarters, and two additional sites for its presence in two colocation facilities. Each site must be assigned a unique name and may optionally be assigned to a region and/or tenant. --- # netbox_site (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/sites-and-racks/#sites): > How you choose to employ sites when modeling your network may vary depending on the nature of your organization, but generally a site will equate to a building or campus. For example, a chain of banks might create a site to represent each of its branches, a site for its corporate headquarters, and two additional sites for its presence in two colocation facilities. > > Each site must be assigned a unique name and may optionally be assigned to a region and/or tenant. ## Example Usage ```terraform resource "netbox_site" "example1" { name = "Example site 1" asn = 1337 facility = "Data center" latitude = "-45.4085" longitude = "30.1496" status = "staging" timezone = "Africa/Johannesburg" } ``` ## Schema ### Required - `name` (String) ### Optional - `asn_ids` (Set of Number) - `comments` (String) - `custom_fields` (Map of String) - `description` (String) - `facility` (String) - `group_id` (Number) - `latitude` (Number) - `longitude` (Number) - `physical_address` (String) - `region_id` (Number) - `shipping_address` (String) - `slug` (String) - `status` (String) Valid values are `planned`, `staging`, `active`, `decommissioning` and `retired`. Defaults to `active`. - `tags` (Set of String) - `tenant_id` (Number) - `timezone` (String) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/site_group.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_site_group Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/facilities/#site-groups: Like regions, site groups can be arranged in a recursive hierarchy for grouping sites. However, whereas regions are intended for geographic organization, site groups may be used for functional grouping. For example, you might classify sites as corporate, branch, or customer sites in addition to where they are physically located. The use of both regions and site groups affords to independent but complementary dimensions across which sites can be organized. --- # netbox_site_group (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/facilities/#site-groups): > Like regions, site groups can be arranged in a recursive hierarchy for grouping sites. However, whereas regions are intended for geographic organization, site groups may be used for functional grouping. For example, you might classify sites as corporate, branch, or customer sites in addition to where they are physically located. > > The use of both regions and site groups affords to independent but complementary dimensions across which sites can be organized. ## Example Usage ```terraform resource "netbox_site_group" "parent" { name = "parent" description = "sample description" } resource "netbox_site_group" "child" { name = "child" description = "sample description" parent_id = netbox_site_group.parent.id } ``` ## Schema ### Required - `name` (String) ### Optional - `description` (String) - `parent_id` (Number) - `slug` (String) ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/tag.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_tag Resource - terraform-provider-netbox" subcategory: "Extras" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/extras/tag/: > Tags are user-defined labels which can be applied to a variety of objects within NetBox. They can be used to establish dimensions of organization beyond the relationships built into NetBox. For example, you might create a tag to identify a particular ownership or condition across several types of objects. > > Each tag has a label, color, and a URL-friendly slug. For example, the slug for a tag named "Dunder Mifflin, Inc." would be dunder-mifflin-inc. The slug is generated automatically and makes tags easier to work with as URL parameters. Each tag can also be assigned a description indicating its purpose. --- # netbox_tag (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/extras/tag/): > Tags are user-defined labels which can be applied to a variety of objects within NetBox. They can be used to establish dimensions of organization beyond the relationships built into NetBox. For example, you might create a tag to identify a particular ownership or condition across several types of objects. > > Each tag has a label, color, and a URL-friendly slug. For example, the slug for a tag named "Dunder Mifflin, Inc." would be dunder-mifflin-inc. The slug is generated automatically and makes tags easier to work with as URL parameters. Each tag can also be assigned a description indicating its purpose. ## Example Usage ```terraform resource "netbox_tag" "dmz" { name = "DMZ" color_hex = "ff00ff" } ``` ## Schema ### Required - `name` (String) ### Optional - `color_hex` (String) Defaults to `9e9e9e`. - `description` (String) - `slug` (String) - `tags` (Set of String) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/tenant.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_tenant Resource - terraform-provider-netbox" subcategory: "Tenancy" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/tenancy/#tenants: A tenant represents a discrete grouping of resources used for administrative purposes. Typically, tenants are used to represent individual customers or internal departments within an organization. Tenant assignment is used to signify the ownership of an object in NetBox. As such, each object may only be owned by a single tenant. For example, if you have a firewall dedicated to a particular customer, you would assign it to the tenant which represents that customer. However, if the firewall serves multiple customers, it doesn't belong to any particular customer, so tenant assignment would not be appropriate. --- # netbox_tenant (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/tenancy/#tenants): > A tenant represents a discrete grouping of resources used for administrative purposes. Typically, tenants are used to represent individual customers or internal departments within an organization. > > Tenant assignment is used to signify the ownership of an object in NetBox. As such, each object may only be owned by a single tenant. For example, if you have a firewall dedicated to a particular customer, you would assign it to the tenant which represents that customer. However, if the firewall serves multiple customers, it doesn't belong to any particular customer, so tenant assignment would not be appropriate. ## Example Usage ```terraform resource "netbox_tenant" "customer_a" { name = "Customer A" } ``` ## Schema ### Required - `name` (String) ### Optional - `description` (String) - `group_id` (Number) - `slug` (String) - `tags` (Set of String) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/tenant_group.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_tenant_group Resource - terraform-provider-netbox" subcategory: "Tenancy" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/tenancy/#tenant-groups: Tenants can be organized by custom groups. For instance, you might create one group called "Customers" and one called "Departments." The assignment of a tenant to a group is optional. Tenant groups may be nested recursively to achieve a multi-level hierarchy. For example, you might have a group called "Customers" containing subgroups of individual tenants grouped by product or account team. --- # netbox_tenant_group (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/tenancy/#tenant-groups): > Tenants can be organized by custom groups. For instance, you might create one group called "Customers" and one called "Departments." The assignment of a tenant to a group is optional. > > Tenant groups may be nested recursively to achieve a multi-level hierarchy. For example, you might have a group called "Customers" containing subgroups of individual tenants grouped by product or account team. ## Example Usage ```terraform resource "netbox_tenant_group" "test" { name = "test-tenant-group" } ``` ## Schema ### Required - `name` (String) ### Optional - `description` (String) - `parent_id` (Number) - `slug` (String) ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/token.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_token Resource - terraform-provider-netbox" subcategory: "Authentication" description: |- From the official documentation https://docs.netbox.dev/en/stable/rest-api/authentication/#tokens: A token is a unique identifier mapped to a NetBox user account. Each user may have one or more tokens which he or she can use for authentication when making REST API requests. To create a token, navigate to the API tokens page under your user profile. --- # netbox_token (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/rest-api/authentication/#tokens): > A token is a unique identifier mapped to a NetBox user account. Each user may have one or more tokens which he or she can use for authentication when making REST API requests. To create a token, navigate to the API tokens page under your user profile. ## Example Usage ```terraform resource "netbox_user" "test" { username = "johndoe" password = "Abcdefghijkl1" } resource "netbox_token" "test_basic" { user_id = netbox_user.test.id key = "0123456789012345678901234567890123456789" allowed_ips = ["2.4.8.16/32"] write_enabled = false expires = "2036-01-02T15:04:05.000Z" } ``` ## Schema ### Required - `user_id` (Number) ### Optional - `allowed_ips` (List of String) - `description` (String) - `expires` (String) - `key` (String, Sensitive) - `write_enabled` (Boolean) ### Read-Only - `id` (String) The ID of this resource. - `last_used` (String) ================================================ FILE: docs/resources/user.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_user Resource - terraform-provider-netbox" subcategory: "Authentication" description: |- This resource is used to manage users. --- # netbox_user (Resource) This resource is used to manage users. ## Example Usage ```terraform resource "netbox_user" "test" { username = "johndoe" password = "Abcdefghijkl1" active = true staff = true } ``` ## Schema ### Required - `password` (String, Sensitive) - `username` (String) ### Optional - `active` (Boolean) Defaults to `true`. - `email` (String) Defaults to `""`. - `first_name` (String) Defaults to `""`. - `group_ids` (Set of Number) - `last_name` (String) Defaults to `""`. - `staff` (Boolean) Defaults to `false`. ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/virtual_chassis.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_virtual_chassis Resource - terraform-provider-netbox" subcategory: "Data Center Inventory Management (DCIM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/devices-cabling/#virtual-chassis: > Sometimes it is necessary to model a set of physical devices as sharing a single management plane. Perhaps the most common example of such a scenario is stackable switches. These can be modeled as virtual chassis in NetBox, with one device acting as the chassis master and the rest as members. All components of member devices will appear on the master. --- # netbox_virtual_chassis (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/devices-cabling/#virtual-chassis): > Sometimes it is necessary to model a set of physical devices as sharing a single management plane. Perhaps the most common example of such a scenario is stackable switches. These can be modeled as virtual chassis in NetBox, with one device acting as the chassis master and the rest as members. All components of member devices will appear on the master. ## Example Usage ```terraform resource "netbox_virtual_chassis" "example" { name = "chassis" domain = "domain" description = "virtual chassis" } ``` ## Schema ### Required - `name` (String) ### Optional - `comments` (String) - `custom_fields` (Map of String) - `description` (String) - `domain` (String) - `tags` (Set of String) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/virtual_disk.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_virtual_disk Resource - terraform-provider-netbox" subcategory: "Virtualization" description: |- From the official documentation https://docs.netbox.dev/en/stable/models/virtualization/virtualdisk/: > A virtual disk is used to model discrete virtual hard disks assigned to virtual machines. --- # netbox_virtual_disk (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/models/virtualization/virtualdisk/): > A virtual disk is used to model discrete virtual hard disks assigned to virtual machines. ## Example Usage ```terraform // Assumes vmw-cluster-01 exists in Netbox data "netbox_cluster" "vmw_cluster_01" { name = "vmw-cluster-01" } resource "netbox_virtual_machine" "base_vm" { cluster_id = data.netbox_cluster.vmw_cluster_01.id name = "myvm-1" } resource "netbox_virtual_disk" "example" { name = "disk-01" description = "Main disk" size_mb = 50 virtual_machine_id = netbox_virtual_machine.base_vm.id } ``` ## Schema ### Required - `name` (String) - `size_mb` (Number) - `virtual_machine_id` (Number) ### Optional - `custom_fields` (Map of String) - `description` (String) - `tags` (Set of String) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/virtual_machine.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_virtual_machine Resource - terraform-provider-netbox" subcategory: "Virtualization" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/virtualization/#virtual-machines: A virtual machine is a virtualized compute instance. These behave in NetBox very similarly to device objects, but without any physical attributes. For example, a VM may have interfaces assigned to it with IP addresses and VLANs, however its interfaces cannot be connected via cables (because they are virtual). Each VM may also define its compute, memory, and storage resources as well. --- # netbox_virtual_machine (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/virtualization/#virtual-machines): > A virtual machine is a virtualized compute instance. These behave in NetBox very similarly to device objects, but without any physical attributes. For example, a VM may have interfaces assigned to it with IP addresses and VLANs, however its interfaces cannot be connected via cables (because they are virtual). Each VM may also define its compute, memory, and storage resources as well. ## Example Usage ```terraform // Assumes vmw-cluster-01 exists in Netbox data "netbox_cluster" "vmw_cluster_01" { name = "vmw-cluster-01" } resource "netbox_virtual_machine" "base_vm" { cluster_id = data.netbox_cluster.vmw_cluster_01.id name = "myvm-1" } // Assumes vmw-cluster-01 exists in Netbox data "netbox_cluster" "vmw_cluster_01" { name = "vmw-cluster-01" } resource "netbox_virtual_machine" "basic_vm" { cluster_id = data.netbox_cluster.vmw_cluster_01.id name = "myvm-2" disk_size_mb = 40000 memory_mb = 4092 vcpus = "2" } // Assumes vmw-cluster-01 exists as a cluster in Netbox data "netbox_cluster" "vmw_cluster_01" { name = "vmw-cluster-01" } // Assumes customer-a exists as a tenant in Netbox data "netbox_tenant" "customer_a" { name = "Customer A" } resource "netbox_virtual_machine" "full_vm" { cluster_id = data.netbox_cluster.vmw_cluster_01.id name = "myvm-3" disk_size_mb = 40000 memory_mb = 4092 vcpus = "2" role_id = 31 // This corresponds to the Netbox ID for a given role tenant_id = data.netbox_tenant.customer_a.id local_context_data = jsonencode({ "setting_a" = "Some Setting" "setting_b" = 42 }) } ``` ## Schema ### Required - `name` (String) ### Optional - `cluster_id` (Number) At least one of `site_id` or `cluster_id` must be given. - `comments` (String) - `custom_fields` (Map of String) - `description` (String) - `device_id` (Number) - `disk_size_mb` (Number) The disk size in MB. When virtual disks are attached to this VM, NetBox automatically computes this as the aggregate of those disks and rejects manual values. In that case, omit this field from the configuration and let it be computed. - `local_context_data` (String) This is best managed through the use of `jsonencode` and a map of settings. - `memory_mb` (Number) - `platform_id` (Number) - `role_id` (Number) - `site_id` (Number) At least one of `site_id` or `cluster_id` must be given. - `status` (String) Valid values are `offline`, `active`, `planned`, `staged`, `failed` and `decommissioning`. Defaults to `active`. - `tags` (Set of String) - `tenant_id` (Number) - `vcpus` (Number) ### Read-Only - `id` (String) The ID of this resource. - `primary_ipv4` (Number) - `primary_ipv6` (Number) - `tags_all` (Set of String) ================================================ FILE: docs/resources/vlan.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_vlan Resource - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/vlans/#vlans: A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in IEEE 802.1Q. VLANs are arranged into VLAN groups to define scope and to enforce uniqueness. --- # netbox_vlan (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/vlans/#vlans): > A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in IEEE 802.1Q. VLANs are arranged into VLAN groups to define scope and to enforce uniqueness. ## Example Usage ```terraform resource "netbox_vlan" "example1" { name = "VLAN 1" vid = 1777 tags = [] } # Assume netbox_tenant, netbox_site, and netbox_tag resources exist resource "netbox_vlan" "example2" { name = "VLAN 2" vid = 1778 status = "reserved" description = "Reserved example VLAN" tenant_id = netbox_tenant.ex.id site_id = netbox_site.ex.id group_id = netbox_vlan_group.ex.id tags = [netbox_tag.ex.name] } ``` ## Schema ### Required - `name` (String) - `vid` (Number) ### Optional - `description` (String) Defaults to `""`. - `group_id` (Number) - `role_id` (Number) - `site_id` (Number) - `status` (String) Valid values are `active`, `reserved` and `deprecated`. Defaults to `active`. - `tags` (Set of String) - `tenant_id` (Number) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/vlan_group.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_vlan_group Resource - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- A VLAN Group represents a collection of VLANs. Generally, these are limited by one of a number of scopes such as "Site" or "Virtualization Cluster". --- # netbox_vlan_group (Resource) > A VLAN Group represents a collection of VLANs. Generally, these are limited by one of a number of scopes such as "Site" or "Virtualization Cluster". ## Example Usage ```terraform #Basic VLAN Group example resource "netbox_vlan_group" "example1" { name = "example1" slug = "example1" } #Full VLAN Group example resource "netbox_vlan_group" "example2" { name = "Second Example" slug = "example2" scope_type = "dcim.site" scope_id = netbox_site.example.id description = "Second Example VLAN Group" tags = [netbox_tag.example.id] vid_ranges = [[1, 2], [3, 4]] } ``` ## Schema ### Required - `name` (String) - `slug` (String) - `vid_ranges` (List of List of Number) ### Optional - `description` (String) Defaults to `""`. - `scope_id` (Number) Required when `scope_type` is set. - `scope_type` (String) Valid values are `dcim.location`, `dcim.site`, `dcim.sitegroup`, `dcim.region`, `dcim.rack`, `virtualization.cluster` and `virtualization.clustergroup`. - `tags` (Set of String) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/vpn_tunnel.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_vpn_tunnel Resource - terraform-provider-netbox" subcategory: "VPN Tunnels" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/vpn-tunnels/: NetBox can model private tunnels formed among virtual termination points across your network. Typical tunnel implementations include GRE, IP-in-IP, and IPSec. A tunnel may be terminated to two or more device or virtual machine interfaces. For convenient organization, tunnels may be assigned to user-defined groups. --- # netbox_vpn_tunnel (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/vpn-tunnels/): > NetBox can model private tunnels formed among virtual termination points across your network. Typical tunnel implementations include GRE, IP-in-IP, and IPSec. A tunnel may be terminated to two or more device or virtual machine interfaces. For convenient organization, tunnels may be assigned to user-defined groups. ## Example Usage ```terraform resource "netbox_vpn_tunnel_group" "test" { name = "my-tunnel-group" } resource "netbox_vpn_tunnel" "test" { name = "my-tunnel" encapsulation = "ipsec-transport" status = "active" tunnel_group_id = netbox_vpn_tunnel_group.test.id description = "This is a description." tunnel_id = 3 tenant_id = 2 } ``` ## Schema ### Required - `encapsulation` (String) Valid values are `ipsec-transport`, `ipsec-tunnel`, `ip-ip` and `gre`. - `name` (String) - `status` (String) Valid values are `planned`, `active` and `disabled`. - `tunnel_group_id` (Number) ### Optional - `description` (String) - `tags` (Set of String) - `tenant_id` (Number) - `tunnel_id` (Number) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/vpn_tunnel_group.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_vpn_tunnel_group Resource - terraform-provider-netbox" subcategory: "VPN Tunnels" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/vpn-tunnels/: NetBox can model private tunnels formed among virtual termination points across your network. Typical tunnel implementations include GRE, IP-in-IP, and IPSec. A tunnel may be terminated to two or more device or virtual machine interfaces. For convenient organization, tunnels may be assigned to user-defined groups. --- # netbox_vpn_tunnel_group (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/vpn-tunnels/): > NetBox can model private tunnels formed among virtual termination points across your network. Typical tunnel implementations include GRE, IP-in-IP, and IPSec. A tunnel may be terminated to two or more device or virtual machine interfaces. For convenient organization, tunnels may be assigned to user-defined groups. ## Example Usage ```terraform resource "netbox_vpn_tunnel_group" "test" { name = "my-tunnel-group" description = "My description" } ``` ## Schema ### Required - `name` (String) ### Optional - `description` (String) - `slug` (String) ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/vpn_tunnel_termination.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_vpn_tunnel_termination Resource - terraform-provider-netbox" subcategory: "VPN Tunnels" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/vpn-tunnels/: NetBox can model private tunnels formed among virtual termination points across your network. Typical tunnel implementations include GRE, IP-in-IP, and IPSec. A tunnel may be terminated to two or more device or virtual machine interfaces. For convenient organization, tunnels may be assigned to user-defined groups. --- # netbox_vpn_tunnel_termination (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/vpn-tunnels/): > NetBox can model private tunnels formed among virtual termination points across your network. Typical tunnel implementations include GRE, IP-in-IP, and IPSec. A tunnel may be terminated to two or more device or virtual machine interfaces. For convenient organization, tunnels may be assigned to user-defined groups. ## Example Usage ```terraform resource "netbox_vpn_tunnel_group" "test" { name = "my-tunnel-group" description = "description" } resource "netbox_vpn_tunnel" "test" { name = "my-tunnel" encapsulation = "ipsec-transport" status = "active" tunnel_group_id = netbox_vpn_tunnel_group.test.id } resource "netbox_vpn_tunnel_termination" "device" { role = "peer" tunnel_id = netbox_vpn_tunnel.test.id device_interface_id = 123 } resource "netbox_vpn_tunnel_termination" "vm" { role = "peer" tunnel_id = netbox_vpn_tunnel.test.id virtual_machine_interface_id = 234 } ``` ## Schema ### Required - `role` (String) Valid values are `peer`, `hub` and `spoke`. - `tunnel_id` (Number) ### Optional - `device_interface_id` (Number) Exactly one of `virtual_machine_interface_id` or `device_interface_id` must be given. - `outside_ip_address_id` (Number) - `tags` (Set of String) - `virtual_machine_interface_id` (Number) Exactly one of `virtual_machine_interface_id` or `device_interface_id` must be given. ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/vrf.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_vrf Resource - terraform-provider-netbox" subcategory: "IP Address Management (IPAM)" description: |- From the official documentation https://docs.netbox.dev/en/stable/features/ipam/#virtual-routing-and-forwarding-vrf: A VRF object in NetBox represents a virtual routing and forwarding (VRF) domain. Each VRF is essentially a separate routing table. VRFs are commonly used to isolate customers or organizations from one another within a network, or to route overlapping address space (e.g. multiple instances of the 10.0.0.0/8 space). Each VRF may be assigned to a specific tenant to aid in organizing the available IP space by customer or internal user. --- # netbox_vrf (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/features/ipam/#virtual-routing-and-forwarding-vrf): > A VRF object in NetBox represents a virtual routing and forwarding (VRF) domain. Each VRF is essentially a separate routing table. VRFs are commonly used to isolate customers or organizations from one another within a network, or to route overlapping address space (e.g. multiple instances of the 10.0.0.0/8 space). Each VRF may be assigned to a specific tenant to aid in organizing the available IP space by customer or internal user. ## Example Usage ```terraform resource "netbox_vrf" "cust_a_prod" { name = "cust-a-prod" tags = ["customer-a", "prod"] } ``` ## Schema ### Required - `name` (String) ### Optional - `description` (String) - `enforce_unique` (Boolean) Defaults to `true`. - `rd` (String) - `tags` (Set of String) - `tenant_id` (Number) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/webhook.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_webhook Resource - terraform-provider-netbox" subcategory: "Extras" description: |- From the official documentation https://docs.netbox.dev/en/stable/integrations/webhooks/: A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. --- # netbox_webhook (Resource) From the [official documentation](https://docs.netbox.dev/en/stable/integrations/webhooks/): > A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. ## Example Usage ```terraform resource "netbox_webhook" "test" { name = "test" payload_url = "https://example.com/webhook" bodytemplate = "Sample body" } ``` ## Schema ### Required - `name` (String) - `payload_url` (String) ### Optional - `additional_headers` (String) - `body_template` (String) - `ca_file_path` (String) - `http_content_type` (String) The complete list of official content types is available [here](https://www.iana.org/assignments/media-types/media-types.xhtml). Defaults to `application/json`. - `http_method` (String) Valid values are `GET`, `POST`, `PUT`, `PATCH` and `DELETE`. Defaults to `POST`. ### Read-Only - `id` (String) The ID of this resource. ================================================ FILE: docs/resources/wireless_lan.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_wireless_lan Resource - terraform-provider-netbox" subcategory: "Wireless" description: |- A Wireless LAN represents a broadcast wireless network, identified by its SSID and optional authentication settings. --- # netbox_wireless_lan (Resource) > A Wireless LAN represents a broadcast wireless network, identified by its SSID and optional authentication settings. ## Example Usage ```terraform resource "netbox_wireless_lan" "guest" { ssid = "guest-wifi" status = "active" } ``` ## Schema ### Required - `ssid` (String) ### Optional - `auth_cipher` (String) Valid values are `auto`, `tkip` and `aes`. - `auth_psk` (String, Sensitive) - `auth_type` (String) Valid values are `open`, `wep`, `wpa-personal` and `wpa-enterprise`. - `comments` (String) - `custom_fields` (Map of String) - `description` (String) - `group_id` (Number) - `status` (String) Valid values are `active`, `reserved`, `disabled` and `deprecated`. Defaults to `active`. - `tags` (Set of String) - `tenant_id` (Number) - `vlan_id` (Number) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: docs/resources/wireless_lan_group.md ================================================ --- # generated by https://github.com/fbreckle/terraform-plugin-docs page_title: "netbox_wireless_lan_group Resource - terraform-provider-netbox" subcategory: "Wireless" description: |- A Wireless LAN Group is used to organize wireless LANs into a recursive hierarchy. --- # netbox_wireless_lan_group (Resource) > A Wireless LAN Group is used to organize wireless LANs into a recursive hierarchy. ## Example Usage ```terraform resource "netbox_wireless_lan_group" "parent" { name = "campus" } resource "netbox_wireless_lan_group" "child" { name = "building-a" parent_id = netbox_wireless_lan_group.parent.id } ``` ## Schema ### Required - `name` (String) ### Optional - `custom_fields` (Map of String) - `description` (String) - `parent_id` (Number) - `slug` (String) - `tags` (Set of String) ### Read-Only - `id` (String) The ID of this resource. - `tags_all` (Set of String) ================================================ FILE: example/README.md ================================================ # example This folder is not actively maintained, but can be used for local testing and as a small reference for using this provider. ================================================ FILE: example/main.tf ================================================ terraform { required_providers { netbox = { source = "e-breuninger/netbox" } } } # example provider configuration for a local netbox deployment # e.g. https://github.com/netbox-community/netbox-docker #provider "netbox" { # server_url = "http://localhost:8000" # api_token = "0123456789abcdef0123456789abcdef01234567" #} # example provider configuration for https://netboxdemo.om provider "netbox" { server_url = "https://demo.netbox.dev" api_token = "" } resource "netbox_tag" "foo" { name = "foo" color_hex = "00ff00" # green } resource "netbox_tag" "bar" { name = "bar" } resource "netbox_device_role" "testdevicerole" { name = "my-device-role" vm_role = true color_hex = "ff0000" } resource "netbox_site" "testsite" { name = "my-test-site" status = "active" } resource "netbox_platform" "testplatform" { name = "my-test-platform" } resource "netbox_cluster_type" "testclustertype" { name = "my-test-cluster-type" } resource "netbox_cluster_group" "testclustergroup" { name = "my-test-cluster-group" description = "test cluster group description" } resource "netbox_vrf" "testvrf" { name = "my-test-vrf" } resource "netbox_cluster" "testcluster" { name = "my-test-cluster" cluster_type_id = netbox_cluster_type.testclustertype.id cluster_group_id = netbox_cluster_group.testclustergroup.id site_id = netbox_site.testsite.id # tags can be referenced by name but have to be created first .. tags = ["foo"] # .. or explicitly depended upon, unless created separately depends_on = [netbox_tag.foo] } resource "netbox_tenant" "testtenant" { name = "my-test-tenant" } resource "netbox_virtual_machine" "testvm" { name = "my-test-vm" comments = "my-test-comment" memory_mb = 1024 vcpus = 4 disk_size_mb = 512 cluster_id = netbox_cluster.testcluster.id tenant_id = netbox_tenant.testtenant.id platform_id = netbox_platform.testplatform.id role_id = netbox_device_role.testdevicerole.id tags = [netbox_tag.foo.name, netbox_tag.bar.name] } resource "netbox_interface" "testinterface" { virtual_machine_id = netbox_virtual_machine.testvm.id name = "my-test-interface" description = "description" tags = [netbox_tag.foo.name] } resource "netbox_ip_address" "testip" { ip_address = "1.2.3.4/32" dns_name = "test.example.com" interface_id = netbox_interface.testinterface.id status = "active" tenant_id = netbox_tenant.testtenant.id vrf_id = netbox_vrf.testvrf.id tags = [netbox_tag.foo.name] } resource "netbox_primary_ip" "testprimaryip" { virtual_machine_id = netbox_virtual_machine.testvm.id ip_address_id = netbox_ip_address.testip.id } resource "netbox_service" "testservice" { name = "my-test-service" virtual_machine_id = netbox_virtual_machine.testvm.id protocol = "tcp" ports = [80] } resource "netbox_webhook" "testwebhook" { name = "My Webhook" enabled = "true" type_create = "true" payload_url = "https://example.com/webhook" content_types = "dcim.site" body_template = "Sample Body" } resource "netbox_config_context" "test" { name = "My Config Context" data = jsonencode( { "testkey" = "testval" } ) } ================================================ FILE: examples/data-sources/netbox_asn/data-source.tf ================================================ data "netbox_asn" "asn_1" { asn = "1111" tag = "tag-1" } data "netbox_asn" "asn_2" { tag = "tag-1" tag__n = "tag-2" } ================================================ FILE: examples/data-sources/netbox_asns/data-source.tf ================================================ data "netbox_asns" "asns" { filter { name = "asn__gte" value = "1000" } filter { name = "asn__lte" value = "2000" } } ================================================ FILE: examples/data-sources/netbox_cluster/data-source.tf ================================================ data "netbox_cluster" "vmw_cluster_01" { name = "vmw-cluster-01" } ================================================ FILE: examples/data-sources/netbox_cluster_group/data-source.tf ================================================ data "netbox_cluster_group" "dc_west" { name = "dc-west" } ================================================ FILE: examples/data-sources/netbox_clusters/data-source.tf ================================================ // Get all clusters of a specific type data "netbox_cluster_type" "vmware" { name = "VMware ESXi" } data "netbox_clusters" "vmware_clusters" { filter { name = "cluster_type_id" value = data.netbox_cluster_type.vmware.id } } // Get clusters by name regex data "netbox_clusters" "prod_clusters" { name_regex = "prod-.*" } // Get clusters at a specific site data "netbox_clusters" "site_clusters" { filter { name = "site_id" value = data.netbox_site.main.id } } ================================================ FILE: examples/data-sources/netbox_device_render_config/data-source.tf ================================================ # Get the rendered configuration for a device data "netbox_device_render_config" "server_config" { device_id = 60 } # Use the rendered configuration output "rendered_config" { value = data.netbox_device_render_config.server_config.content } output "template_used" { value = data.netbox_device_render_config.server_config.config_template_name } # Example: Write the config to a file using local_file resource # resource "local_file" "kickstart" { # content = data.netbox_device_render_config.server_config.content # filename = "${path.module}/kickstart.cfg" # } ================================================ FILE: examples/data-sources/netbox_device_role/data-source.tf ================================================ data "netbox_device_role" "core_sw" { name = "core-sw" } ================================================ FILE: examples/data-sources/netbox_device_type/data-source.tf ================================================ # Get device type by model name data "netbox_device_type" "ex1" { model = "7210 SAS-Sx 10/100GE" } # Get device type by slug data "netbox_device_type" "ex2" { slug = "7210-sas-sx-10-100GE" } # Get device type by manufacturer and part number information data "netbox_device_type" "ex3" { manufacturer = "Nokia" part_number = "3HE11597AARB01" } ================================================ FILE: examples/data-sources/netbox_interfaces/data-source.tf ================================================ data "netbox_interfaces" "myvm_eth0" { name_regex = "eth0" filter { name = "name" value = "myvm" } } ================================================ FILE: examples/data-sources/netbox_ip_address/data-source.tf ================================================ data "netbox_ip_address" "ip_address" { id = 1001 } ================================================ FILE: examples/data-sources/netbox_ip_range/data-source.tf ================================================ data "netbox_ip_range" "cust_a_prod" { contains = "10.0.0.1/24" } ================================================ FILE: examples/data-sources/netbox_platform/data-source.tf ================================================ data "netbox_platform" "PANOS" { name = "PANOS" } ================================================ FILE: examples/data-sources/netbox_rir/data-source.tf ================================================ data "netbox_rir" "rir_1" { name = "ARIN" } data "netbox_rir" "rir_2" { slug = "arin" } ================================================ FILE: examples/data-sources/netbox_site/data-source.tf ================================================ data "netbox_site" "get_by_name" { name = "Example Site 1" } data "netbox_site" "get_by_slug" { slug = "example-site-1" } ================================================ FILE: examples/data-sources/netbox_site_group/data-source.tf ================================================ // Assumes the corresponding site groups exist data "netbox_site_group" "get_by_name" { name = "example-sitegroup-1" } data "netbox_site_group" "get_by_slug" { slug = "sitegrp" } ================================================ FILE: examples/data-sources/netbox_tag/data-source.tf ================================================ data "netbox_tag" "dmz" { name = "DMZ" } ================================================ FILE: examples/data-sources/netbox_tags/data-source.tf ================================================ data "netbox_tags" "all_tags" { } data "netbox_tags" "ansible_tags" { filter { name = "name__isw" value = "ansible_" } } data "netbox_tags" "not_ansible_tags" { filter { name = "name__nisw" value = "ansible_" } } ================================================ FILE: examples/data-sources/netbox_tenant/data-source.tf ================================================ data "netbox_tenant" "customer_a" { name = "Customer A" } ================================================ FILE: examples/data-sources/netbox_virtual_disk/data-source.tf ================================================ # Filter by name data "netbox_virtual_disk" "disk_by_name" { filter { name = "name" value = "disk1" } } # Filter by tag data "netbox_virtual_disk" "disk_by_tag" { filter { name = "tag" value = "production" } } # Multiple filters data "netbox_virtual_disk" "disk_filtered" { filter { name = "name" value = "disk1" } filter { name = "tag" value = "production" } } # Filter with name regex data "netbox_virtual_disk" "disk_regex" { name_regex = "^disk[0-9]+" limit = 10 } ================================================ FILE: examples/data-sources/netbox_virtual_machines/data-source.tf ================================================ // Assumes vmw-cluster-01 exists as a cluster in Netbox data "netbox_cluster" "vmw_cluster_01" { name = "vmw-cluster-01" } data "netbox_virtual_machines" "base_vm" { name_regex = "myvm-1" filter { name = "cluster_id" value = data.netbox_cluster.vmw_cluster_01.id } } ================================================ FILE: examples/data-sources/netbox_vlan/data-source.tf ================================================ # Get VLAN by name data "netbox_vlan" "vlan1" { name = "vlan-1" } # Get VLAN by VID and IPAM role ID data "netbox_vlan" "vlan2" { vid = 1234 role = netbox_ipam_role.example.id } # Get VLAN by name and tenant ID data "netbox_vlan" "vlan3" { name = "vlan-3" tenant = netbox_tenant.example.id } ================================================ FILE: examples/data-sources/netbox_vlan_group/data-source.tf ================================================ # Get VLAN group by name data "netbox_vlan_group" "example1" { name = "example1" } # Get VLAN group by stub data "netbox_vlan_group" "example2" { slug = "example2" } # Get VLAN group by name and scope_type/id data "netbox_vlan_group" "example3" { name = "example" scope_type = "dcim.site" scope_id = netbox_site.example.id } ================================================ FILE: examples/data-sources/netbox_vrf/data-source.tf ================================================ data "netbox_vrf" "cust_a_prod" { name = "cust-a-prod" } ================================================ FILE: examples/provider/provider.tf ================================================ terraform { required_providers { netbox = { source = "e-breuninger/netbox" version = "~> 3.2.1" } } } # example provider configuration for https://demo.netbox.dev provider "netbox" { server_url = "https://demo.netbox.dev" api_token = "" } ================================================ FILE: examples/resources/netbox_aggregate/resource.tf ================================================ resource "netbox_rir" "test" { name = "testrir" } resource "netbox_aggregate" "test" { prefix = "1.1.1.0/25" description = "my description" rir_id = netbox_rir.test.id } ================================================ FILE: examples/resources/netbox_asn/resource.tf ================================================ resource "netbox_rir" "test" { name = "testrir" } resource "netbox_asn" "test" { asn = 1337 rir_id = netbox_rir.test.id description = "test" comments = "test" } ================================================ FILE: examples/resources/netbox_available_ip_address/assign_to_interface.tf ================================================ // Assumes Netbox already has a VM whos name matches 'dc-west-myvm-20' data "netbox_virtual_machine" "myvm" { name_regex = "dc-west-myvm-20" } data "netbox_prefix" "test" { cidr = "10.0.0.0/24" } resource "netbox_interface" "myvm-eth0" { name = "eth0" virtual_machine_id = data.netbox_virtual_machine.myvm.id } resource "netbox_available_ip_address" "myvm-ip" { prefix_id = data.netbox_prefix.test.id status = "active" interface_id = netbox_interface.myvm-eth0.id } ================================================ FILE: examples/resources/netbox_available_ip_address/prefix.tf ================================================ data "netbox_prefix" "test" { cidr = "10.0.0.0/24" } resource "netbox_available_ip_address" "test" { prefix_id = data.netbox_prefix.test.id } ================================================ FILE: examples/resources/netbox_available_ip_address/range.tf ================================================ data "netbox_ip_range" "test" { start_address = "10.0.0.1/24" end_address = "10.0.0.50/24" } resource "netbox_available_ip_address" "test" { ip_range_id = data.netbox_ip_range.test.id } ================================================ FILE: examples/resources/netbox_available_prefix/resource.tf ================================================ data "netbox_prefix" "test" { cidr = "10.0.0.0/24" } resource "netbox_available_prefix" "test" { parent_prefix_id = data.netbox_prefix.test.id prefix_length = 25 status = "active" } ================================================ FILE: examples/resources/netbox_available_vlan/multiple.tf ================================================ resource "netbox_site" "testSite" { name = "test site" slug = "test-site" } resource "netbox_vlan_group" "group1" { name = "Group One" slug = "group-one" scope_id = netbox_site.testSite.id scope_type = "dcim.site" description = "First VLAN group" vid_ranges = [[1, 2], [7, 17]] } resource "netbox_available_vlan" "vlan1" { name = "vlan1" status = "active" description = "Virtual network for team 1" group_id = netbox_vlan_group.group1.id site_id = netbox_vlan_group.group1.scope_id } resource "netbox_available_vlan" "vlan2" { name = "vlan2" status = "active" description = "Virtual network for team 2" group_id = netbox_vlan_group.group1.id site_id = netbox_vlan_group.group1.scope_id } resource "netbox_available_vlan" "vlan3" { name = "vlan3" status = "active" description = "Virtual network for team 3" group_id = netbox_vlan_group.group1.id site_id = netbox_vlan_group.group1.scope_id } ================================================ FILE: examples/resources/netbox_available_vlan/prefix.tf ================================================ resource "netbox_site" "testSite" { name = "test site" slug = "test-site" } resource "netbox_vlan_group" "testGroup" { name = "Group One" slug = "group-one" scope_id = netbox_site.testSite.id scope_type = "dcim.site" description = "First VLAN group" vid_ranges = [[1, 20]] } resource "netbox_available_vlan" "testVlan" { name = "test-vlan" status = "active" description = "Virtual network for testing purposes" group_id = netbox_vlan_group.testGroup.id site_id = netbox_vlan_group.testGroup.scope_id } resource "netbox_prefix" "testPrefix" { prefix = "192.168.40.0/24" status = "active" description = "test prefix" site_id = netbox_site.testSite.id vlan_id = netbox_available_vlan.testVlan.id } ================================================ FILE: examples/resources/netbox_available_vlan/range.tf ================================================ resource "netbox_site" "testSite" { name = "test site" slug = "test-site" } resource "netbox_vlan_group" "testGroup" { name = "Group One" slug = "group-one" scope_id = netbox_site.testSite.id scope_type = "dcim.site" description = "First VLAN group" vid_ranges = [[1, 2], [7, 17]] } resource "netbox_available_vlan" "testVlan" { name = "test-vlan" status = "active" description = "Virtual network for testing purposes" group_id = netbox_vlan_group.testGroup.id site_id = netbox_vlan_group.testGroup.scope_id } resource "netbox_available_vlan" "testVlan2" { name = "test-vlan2" status = "active" description = "Virtual network for testing purposes" group_id = netbox_vlan_group.testGroup.id site_id = netbox_vlan_group.testGroup.scope_id } resource "netbox_available_vlan" "testVlan3" { name = "test-vlan3" status = "active" description = "Virtual network for testing purposes" group_id = netbox_vlan_group.testGroup.id site_id = netbox_vlan_group.testGroup.scope_id } ================================================ FILE: examples/resources/netbox_available_vlan/site.tf ================================================ resource "netbox_site" "testSite" { name = "test site" slug = "test-site" } resource "netbox_vlan_group" "testGroup" { name = "Group One" slug = "group-one" scope_id = netbox_site.testSite.id scope_type = "dcim.site" description = "First VLAN group" vid_ranges = [[1, 20]] } resource "netbox_available_vlan" "testVlan" { name = "test-vlan" status = "active" description = "Virtual network for testing purposes" group_id = netbox_vlan_group.testGroup.id site_id = netbox_vlan_group.testGroup.scope_id } ================================================ FILE: examples/resources/netbox_cable/resource.tf ================================================ // assumes that the referenced console port resources exist resource "netbox_cable" "test" { a_termination { object_type = "dcim.consoleserverport" object_id = netbox_device_console_server_port.kvm1.id } a_termination { object_type = "dcim.consoleserverport" object_id = netbox_device_console_server_port.kvm2.id } b_termination { object_type = "dcim.consoleport" object_id = netbox_device_console_port.server1.id } b_termination { object_type = "dcim.consoleport" object_id = netbox_device_console_port.server2.id } status = "connected" label = "KVM cable" type = "cat8" color_hex = "123456" length = 10 length_unit = "m" } ================================================ FILE: examples/resources/netbox_circuit/resource.tf ================================================ resource "netbox_tenant" "test" { name = "test" } resource "netbox_circuit_provider" "test" { name = "test" } resource "netbox_circuit_type" "test" { name = "test" } resource "netbox_circuit" "test" { cid = "test" status = "active" provider_id = netbox_circuit_provider.test.id type_id = netbox_circuit_type.test.id } ================================================ FILE: examples/resources/netbox_circuit_provider/resource.tf ================================================ resource "netbox_circuit_provider" "test" { name = "test" } ================================================ FILE: examples/resources/netbox_circuit_termination/resource.tf ================================================ resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_circuit_provider" "test" { name = "%[1]s" } resource "netbox_circuit_type" "test" { name = "%[1]s" } resource "netbox_circuit" "test" { cid = "%[1]s" status = "active" provider_id = netbox_circuit_provider.test.id type_id = netbox_circuit_type.test.id } resource "netbox_circuit_termination" "test" { circuit_id = netbox_circuit.test.id term_side = "A" site_id = netbox_site.test.id port_speed = 100000 upstream_speed = 50000 } ================================================ FILE: examples/resources/netbox_circuit_type/resource.tf ================================================ resource "netbox_circuit_type" "test" { name = "test" } ================================================ FILE: examples/resources/netbox_cluster/resource.tf ================================================ // Assumes the 'dc-west' cluster group already exists data "netbox_cluster_group" "dc_west" { name = "dc-west" } resource "netbox_cluster_type" "vmw_vsphere" { name = "VMware vSphere 6" } resource "netbox_cluster" "vmw_cluster_01" { cluster_type_id = netbox_cluster_type.vmw_vsphere.id name = "vmw-cluster-01" cluster_group_id = data.netbox_cluster_group.dc_west.id } ================================================ FILE: examples/resources/netbox_cluster_group/resource.tf ================================================ resource "netbox_cluster_group" "dc_west" { description = "West Datacenter Cluster" name = "dc-west" } ================================================ FILE: examples/resources/netbox_cluster_type/resource.tf ================================================ resource "netbox_cluster_type" "vmw_vsphere" { name = "VMware vSphere 6" } ================================================ FILE: examples/resources/netbox_config_context/resource.tf ================================================ resource "netbox_config_context" "test" { name = "%s" data = jsonencode({ "testkey" = "testval" }) } ================================================ FILE: examples/resources/netbox_config_template/resource.tf ================================================ resource "netbox_config_template" "test" { name = "test" description = "test description" template_code = "hostname {{ name }}" environment_params = jsonencode({ "name" = "my-hostname" }) } ================================================ FILE: examples/resources/netbox_contact/resource.tf ================================================ resource "netbox_contact" "test" { name = "John Doe" email = "test@example.com" phone = "123-123123" } ================================================ FILE: examples/resources/netbox_contact_assignment/resource.tf ================================================ resource "netbox_contact" "test" { name = "test" } resource "netbox_contact_role" "test" { name = "test" } // Assumes that a device with id 123 exists resource "netbox_contact_assignment" "test" { content_type = "dcim.device" object_id = 123 contact_id = netbox_contact.test.id role_id = netbox_contact_role.test.id priority = "primary" } ================================================ FILE: examples/resources/netbox_contact_group/resource.tf ================================================ resource "netbox_contact_group" "test" { name = "test" } ================================================ FILE: examples/resources/netbox_contact_role/resource.tf ================================================ resource "netbox_contact_role" "test" { name = "test" } ================================================ FILE: examples/resources/netbox_custom_field/resource.tf ================================================ resource "netbox_custom_field" "test" { name = "test" type = "text" content_types = ["virtualization.vminterface"] weight = 100 validation_regex = "^.*$" } ================================================ FILE: examples/resources/netbox_custom_field_choice_set/resource.tf ================================================ resource "netbox_custom_field_choice_set" "test" { name = "my-custom-field-set" description = "Description" extra_choices = [ ["choice1", "label1"], # label and choice are different ["choice2", "choice2"] # label and choice are the same ] } ================================================ FILE: examples/resources/netbox_device/resource.tf ================================================ resource "netbox_site" "test" { name = "%[1]s" } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_manufacturer" "test" { name = "test" } resource "netbox_device_type" "test" { model = "test" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id local_context_data = jsonencode({ "setting_a" = "Some Setting" "setting_b" = 42 }) } ================================================ FILE: examples/resources/netbox_device_bay/resource.tf ================================================ resource "netbox_tenant" "example" { name = "example_tenant" } resource "netbox_site" "example" { name = "example_site" status = "active" } resource "netbox_manufacturer" "example" { name = "example_manufacturer" } resource "netbox_device_type" "example" { model = "example_device_type" manufacturer_id = netbox_manufacturer.example.id subdevice_role = "parent" } resource "netbox_device_type" "example_installed" { model = "example_device_type_installed" manufacturer_id = netbox_manufacturer.example.id u_height = 0 subdevice_role = "child" } resource "netbox_device_role" "example" { name = "example_role" color_hex = "123456" } resource "netbox_device" "example" { name = "example_device" device_type_id = netbox_device_type.example.id tenant_id = netbox_tenant.example.id role_id = netbox_device_role.example.id site_id = netbox_site.example.id } resource "netbox_device" "example_installed" { name = "example_device_installed" device_type_id = netbox_device_type.example_installed.id tenant_id = netbox_tenant.example.id role_id = netbox_device_role.example.id site_id = netbox_site.example.id } resource "netbox_device_bay" "example" { device_id = netbox_device.example.id name = "example_device_bay" label = "example_label" description = "example_description" installed_device_id = netbox_device.example_installed.id } ================================================ FILE: examples/resources/netbox_device_bay_template/resource.tf ================================================ resource "netbox_manufacturer" "example" { name = "example_manufacturer" } resource "netbox_device_type" "example" { model = "example_device_model" slug = "example_device_slug" part_number = "example_part_number" manufacturer_id = netbox_manufacturer.example.id subdevice_role = "parent" } resource "netbox_device_bay_template" "example" { name = "example_device_bay_template" device_type_id = netbox_device_type.example.id } ================================================ FILE: examples/resources/netbox_device_console_port/resource.tf ================================================ # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_console_port" "test" { device_id = netbox_device.test.id name = "console port" type = "de-9" speed = 1200 mark_connected = true } ================================================ FILE: examples/resources/netbox_device_console_server_port/resource.tf ================================================ # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_console_server_port" "test" { device_id = netbox_device.test.id name = "console server port" type = "de-9" speed = 1200 mark_connected = true } ================================================ FILE: examples/resources/netbox_device_front_port/resource.tf ================================================ # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_rear_port" "test" { device_id = netbox_device.test.id name = "rear port 1" type = "8p8c" positions = 2 mark_connected = true } resource "netbox_device_front_port" "test" { device_id = netbox_device.test.id name = "front port 1" type = "8p8c" rear_port_id = netbox_device_rear_port.test.id rear_port_position = 2 } ================================================ FILE: examples/resources/netbox_device_interface/resource.tf ================================================ // Assumes a device with ID 123 exists resource "netbox_device_interface" "test" { name = "testinterface" device_id = 123 type = "1000base-t" } ================================================ FILE: examples/resources/netbox_device_module_bay/resource.tf ================================================ # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_module_bay" "test" { device_id = netbox_device.test.id name = "module bay 1" } ================================================ FILE: examples/resources/netbox_device_power_outlet/resource.tf ================================================ # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_power_outlet" "test" { device_id = netbox_device.test.id name = "power outlet" type = "iec-60320-c5" feed_leg = "A" } ================================================ FILE: examples/resources/netbox_device_power_port/resource.tf ================================================ # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_power_port" "test" { device_id = netbox_device.test.id name = "power port" maximum_draw = 750 allocated_draw = 500 type = "iec-60320-c6" } ================================================ FILE: examples/resources/netbox_device_primary_ip/resource.tf ================================================ # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_ip_address" "test_v4" { ip_address = "1.1.1.1/32" status = "active" device_interface_id = netbox_device_interface.test.id } resource "netbox_device_primary_ip" "test_v4" { device_id = netbox_device.test.id ip_address_id = netbox_ip_address.test.id } ================================================ FILE: examples/resources/netbox_device_rear_port/resource.tf ================================================ # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_rear_port" "test" { device_id = netbox_device.test.id name = "rear port 1" type = "8p8c" positions = 2 mark_connected = true } ================================================ FILE: examples/resources/netbox_device_role/resource.tf ================================================ resource "netbox_device_role" "core_sw" { color_hex = "ff00ff" name = "core-sw" } ================================================ FILE: examples/resources/netbox_device_type/resource.tf ================================================ resource "netbox_manufacturer" "test" { name = "test" } resource "netbox_device_type" "test" { model = "test" part_number = "123" manufacturer_id = netbox_manufacturer.test.id } ================================================ FILE: examples/resources/netbox_event_rule/resource.tf ================================================ resource "netbox_webhook" "test" { name = "my-webhook" payload_url = "https://example.com/webhook" } resource "netbox_event_rule" "webhook" { name = "my-event-rule" content_types = ["dcim.site", "virtualization.cluster"] action_type = "webhook" action_object_id = netbox_webhook.test.id event_types = [ "object_created", "object_updated", "object_deleted", "job_started", "job_completed", "job_failed", "job_errored" ] } resource "netbox_event_rule" "script" { name = "my-script-event-rule" content_types = ["dcim.site"] action_type = "script" action_object_id = 42 # existing NetBox Script ID event_types = ["object_created"] } ================================================ FILE: examples/resources/netbox_group/resource.tf ================================================ resource "netbox_group" "test" { name = "test-group" } ================================================ FILE: examples/resources/netbox_interface/resource.tf ================================================ // Assume Netbox already has a VM whos name matches 'dc-west-myvm-20' data "netbox_virtual_machine" "myvm" { name_regex = "dc-west-myvm-20" } resource "netbox_interface" "myvm_eth0" { name = "eth0" virtual_machine_id = data.netbox_virtual_machine.myvm.id } // Assume existing VLAN resources 'test1' and 'test2' resource "netbox_interface" "myvm_eth1" { name = "eth1" enabled = true mac_address = "00:16:3E:A8:B5:D7" mode = "tagged" mtu = 1440 tagged_vlans = [netbox_vlan.test1.id] untagged_vlan = netbox_vlan.test2.id virtual_machine_id = netbox_virtual_machine.test.id } ================================================ FILE: examples/resources/netbox_interface_template/resource.tf ================================================ resource "netbox_manufacturer" "test" { name = "my-manufacturer" } resource "netbox_device_type" "test" { model = "test-model" slug = "test-model" part_number = "test-part-number" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_interface_template" "test" { name = "eth0" description = "eth0 description" label = "eth0 label" device_type_id = netbox_device_type.test.id type = "100base-tx" mgmt_only = true } ================================================ FILE: examples/resources/netbox_inventory_item/resource.tf ================================================ # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_rear_port" "test" { device_id = netbox_device.test.id name = "rear port" type = "8p8c" positions = 1 mark_connected = true } resource "netbox_inventory_item" "parent" { device_id = netbox_device.test.id name = "Parent Item" } resource "netbox_inventory_item" "test" { device_id = netbox_device.test.id name = "Child Item" parent_id = netbox_inventory_item.parent.id component_type = "dcim.rearport" component_id = netbox_device_rear_port.test.id } ================================================ FILE: examples/resources/netbox_inventory_item_role/resource.tf ================================================ # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_inventory_item_role" "test" { name = "Role 1" slug = "role-1-slug" color_hex = "123456" } resource "netbox_inventory_item" "parent" { device_id = netbox_device.test.id name = "Inventory Item 1" role_id = netbox_inventory_item_role.test.id } ================================================ FILE: examples/resources/netbox_ip_address/device_interface_id.tf ================================================ // Assuming a device with the id `123` exists resource "netbox_device_interface" "this" { name = "eth0" device_id = 123 type = "1000base-t" } resource "netbox_ip_address" "this" { ip_address = "10.0.0.60/24" status = "active" device_interface_id = netbox_device_interface.this.id } ================================================ FILE: examples/resources/netbox_ip_address/object_type_device.tf ================================================ // Assuming a device with the id `123` exists resource "netbox_device_interface" "this" { name = "eth0" device_id = 123 type = "1000base-t" } resource "netbox_ip_address" "this" { ip_address = "10.0.0.60/24" status = "active" interface_id = netbox_device_interface.this.id object_type = "dcim.interface" } ================================================ FILE: examples/resources/netbox_ip_address/object_type_virtual_machine.tf ================================================ // Assuming a virtual machine with the id `123` exists resource "netbox_interface" "this" { name = "eth0" virtual_machine_id = 123 } resource "netbox_ip_address" "this" { ip_address = "10.0.0.60/24" status = "active" interface_id = netbox_interface.this.id object_type = "virtualization.vminterface" } ================================================ FILE: examples/resources/netbox_ip_address/standalone.tf ================================================ resource "netbox_ip_address" "this" { ip_address = "10.0.0.50/24" status = "reserved" } ================================================ FILE: examples/resources/netbox_ip_address/virtual_machine_interface_id.tf ================================================ // Assuming a virtual machine with the id `123` exists resource "netbox_interface" "this" { name = "eth0" virtual_machine_id = 123 } resource "netbox_ip_address" "this" { ip_address = "10.0.0.60/24" status = "active" virtual_machine_interface_id = netbox_interface.this.id } ================================================ FILE: examples/resources/netbox_ip_range/resource.tf ================================================ resource "netbox_ip_range" "cust_a_prod" { start_address = "10.0.0.1/24" end_address = "10.0.0.50/24" tags = ["customer-a", "prod"] } ================================================ FILE: examples/resources/netbox_ipam_role/resource.tf ================================================ resource "netbox_ipam_role" "test_basic" { name = "test" } ================================================ FILE: examples/resources/netbox_location/resource.tf ================================================ resource "netbox_site" "test" { name = "test" } resource "netbox_tenant" "test" { name = "test" } resource "netbox_location" "test" { name = "test" description = "my description" site_id = netbox_site.test.id tenant_id = netbox_tenant.test.id } ================================================ FILE: examples/resources/netbox_mac_address/device_interface_id.tf ================================================ // Assuming a device with the id `123` exists resource "netbox_device_interface" "this" { name = "eth0" device_id = 123 type = "1000base-t" } resource "netbox_mac_address" "this" { mac_address = "00:1A:2B:3C:4D:5E" device_interface_id = netbox_device_interface.this.id } ================================================ FILE: examples/resources/netbox_mac_address/object_type_device.tf ================================================ // Assuming a device with the id `123` exists resource "netbox_device_interface" "this" { name = "eth0" device_id = 123 type = "1000base-t" } resource "netbox_mac_address" "this" { mac_address = "00:1A:2B:3C:4D:5E" interface_id = netbox_device_interface.this.id object_type = "dcim.interface" } ================================================ FILE: examples/resources/netbox_mac_address/object_type_virtual_machine.tf ================================================ // Assuming a virtual machine with the id `123` exists resource "netbox_interface" "this" { name = "eth0" virtual_machine_id = 123 } resource "netbox_mac_address" "this" { mac_address = "00:1A:2B:3C:4D:5E" interface_id = netbox_interface.this.id object_type = "virtualization.vminterface" } ================================================ FILE: examples/resources/netbox_mac_address/standalone.tf ================================================ resource "netbox_mac_address" "this" { mac_address = "00:1A:2B:3C:4D:5E" } ================================================ FILE: examples/resources/netbox_mac_address/virtual_machine_interface_id.tf ================================================ // Assuming a virtual machine with the id `123` exists resource "netbox_interface" "this" { name = "eth0" virtual_machine_id = 123 } resource "netbox_mac_address" "this" { mac_address = "00:1A:2B:3C:4D:5E" virtual_machine_interface_id = netbox_interface.this.id } ================================================ FILE: examples/resources/netbox_manufacturer/resource.tf ================================================ resource "netbox_manufacturer" "test" { name = "testmanufacturer" } ================================================ FILE: examples/resources/netbox_module/resource.tf ================================================ # Note that some terraform code is not included in the example for brevity resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_module_bay" "test" { device_id = netbox_device.test.id name = "SFP" } resource "netbox_manufacturer" "test" { name = "Dell" } resource "netbox_module_type" "test" { manufacturer_id = netbox_manufacturer.test.id model = "Networking" } resource "netbox_module" "test" { device_id = netbox_device.test.id module_bay_id = netbox_device_module_bay.test.id module_type_id = netbox_module_type.test.id status = "active" description = "SFP card" } ================================================ FILE: examples/resources/netbox_module_type/resource.tf ================================================ resource "netbox_manufacturer" "test" { name = "Dell" } resource "netbox_module_type" "test" { manufacturer_id = netbox_manufacturer.test.id model = "Networking" } ================================================ FILE: examples/resources/netbox_permission/resource.tf ================================================ resource "netbox_user" "test" { username = "johndoe" password = "Abcdefghijkl1" active = true staff = true } resource "netbox_permission" "test" { name = "test" description = "my description" enabled = true object_types = ["ipam.prefix"] actions = ["add", "change"] users = [netbox_user.test.id] constraints = jsonencode([{ "status" = "active" }]) } ================================================ FILE: examples/resources/netbox_platform/resource.tf ================================================ // Resource for PanOS (e.g. Panorama from Palo Alto) resource "netbox_platform" "PANOS" { name = "PANOS" } ================================================ FILE: examples/resources/netbox_power_feed/resource.tf ================================================ resource "netbox_site" "test" { name = "Site 1" status = "active" } resource "netbox_location" "test" { name = "Location 1" site_id = netbox_site.test.id } resource "netbox_power_panel" "test" { name = "Power Panel 1" site_id = netbox_site.test.id location_id = netbox_location.test.id } resource "netbox_power_feed" "test" { power_panel_id = netbox_power_panel.test.id name = "Power Feed 1" status = "active" type = "primary" supply = "ac" phase = "single-phase" voltage = 250 amperage = 100 max_percent_utilization = 80 } ================================================ FILE: examples/resources/netbox_power_panel/resource.tf ================================================ resource "netbox_site" "test" { name = "Site 1" status = "active" } resource "netbox_location" "test" { name = "Location 1" site_id = netbox_site.test.id } resource "netbox_power_panel" "test" { name = "Power Panel 1" site_id = netbox_site.test.id location_id = netbox_location.test.id } ================================================ FILE: examples/resources/netbox_prefix/resource.tf ================================================ resource "netbox_prefix" "my_prefix" { prefix = "10.0.0.0/24" status = "active" description = "test prefix" } ================================================ FILE: examples/resources/netbox_primary_ip/resource.tf ================================================ // Assumes Netbox already has a VM whos name matches 'dc-west-myvm-20' data "netbox_virtual_machine" "myvm" { name_regex = "dc-west-myvm-20" } resource "netbox_interface" "myvm_eth0" { name = "eth0" virtual_machine_id = data.netbox_virtual_machine.myvm.id } resource "netbox_ip_address" "myvm_ip" { ip_address = "10.0.0.60/24" status = "active" interface_id = netbox_interface.myvm_eth0.id } resource "netbox_primary_ip" "myvm_primary_ip" { ip_address_id = netbox_ip_address.myvm_ip.id virtual_machine_id = data.netbox_virtual_machine.myvm.id } ================================================ FILE: examples/resources/netbox_rack/resource.tf ================================================ resource "netbox_site" "test" { name = "test" status = "active" } resource "netbox_rack" "test" { name = "test" site_id = netbox_site.test.id status = "reserved" width = 19 u_height = 48 } ================================================ FILE: examples/resources/netbox_rack_reservation/resource.tf ================================================ resource "netbox_site" "test" { name = "test" status = "active" } resource "netbox_rack" "test" { name = "test" site_id = netbox_site.test.id status = "active" width = 10 u_height = 40 } resource "netbox_rack_reservation" "test" { rack_id = netbox_rack.test.id units = [1, 2, 3, 4, 5] user_id = 1 description = "my description" } ================================================ FILE: examples/resources/netbox_rack_role/resource.tf ================================================ resource "netbox_rack_role" "test" { name = "test" color_hex = "111111" } ================================================ FILE: examples/resources/netbox_rack_type/resource.tf ================================================ resource "netbox_manufacturer" "test" { name = "my-manufacturer" } resource "netbox_rack_type" "test" { model = "mymodel" manufacturer_id = netbox_manufacturer.test.id width = 19 u_height = 48 starting_unit = 1 form_factor = "2-post-frame" description = "My description" outer_width = 10 outer_depth = 15 outer_unit = "mm" weight = 15 max_weight = 20 weight_unit = "kg" mounting_depth_mm = 21 comments = "My comments" } ================================================ FILE: examples/resources/netbox_region/resource.tf ================================================ resource "netbox_region" "test" { name = "test" description = "test description" } ================================================ FILE: examples/resources/netbox_rir/resource.tf ================================================ resource "netbox_rir" "test" { name = "test" description = "my description" } ================================================ FILE: examples/resources/netbox_route_target/resource.tf ================================================ resource "netbox_tenant" "test" { name = "test" } resource "netbox_route_target" "test" { name = "test" description = "my description" tenant_id = netbox_tenant.test.id } ================================================ FILE: examples/resources/netbox_service/resource.tf ================================================ // Assumes Netbox already has a VM whos name matches 'dc-west-myvm-20' data "netbox_virtual_machine" "myvm" { name_regex = "dc-west-myvm-20" } resource "netbox_service" "ssh" { name = "ssh" ports = [22] protocol = "tcp" virtual_machine_id = data.netbox_virtual_machine.myvm.id } ================================================ FILE: examples/resources/netbox_site/resource.tf ================================================ resource "netbox_site" "example1" { name = "Example site 1" asn = 1337 facility = "Data center" latitude = "-45.4085" longitude = "30.1496" status = "staging" timezone = "Africa/Johannesburg" } ================================================ FILE: examples/resources/netbox_site_group/resource.tf ================================================ resource "netbox_site_group" "parent" { name = "parent" description = "sample description" } resource "netbox_site_group" "child" { name = "child" description = "sample description" parent_id = netbox_site_group.parent.id } ================================================ FILE: examples/resources/netbox_tag/resource.tf ================================================ resource "netbox_tag" "dmz" { name = "DMZ" color_hex = "ff00ff" } ================================================ FILE: examples/resources/netbox_tenant/resource.tf ================================================ resource "netbox_tenant" "customer_a" { name = "Customer A" } ================================================ FILE: examples/resources/netbox_tenant_group/resource.tf ================================================ resource "netbox_tenant_group" "test" { name = "test-tenant-group" } ================================================ FILE: examples/resources/netbox_token/resource.tf ================================================ resource "netbox_user" "test" { username = "johndoe" password = "Abcdefghijkl1" } resource "netbox_token" "test_basic" { user_id = netbox_user.test.id key = "0123456789012345678901234567890123456789" allowed_ips = ["2.4.8.16/32"] write_enabled = false expires = "2036-01-02T15:04:05.000Z" } ================================================ FILE: examples/resources/netbox_user/resource.tf ================================================ resource "netbox_user" "test" { username = "johndoe" password = "Abcdefghijkl1" active = true staff = true } ================================================ FILE: examples/resources/netbox_virtual_chassis/resource.tf ================================================ resource "netbox_virtual_chassis" "example" { name = "chassis" domain = "domain" description = "virtual chassis" } ================================================ FILE: examples/resources/netbox_virtual_disk/resource.tf ================================================ // Assumes vmw-cluster-01 exists in Netbox data "netbox_cluster" "vmw_cluster_01" { name = "vmw-cluster-01" } resource "netbox_virtual_machine" "base_vm" { cluster_id = data.netbox_cluster.vmw_cluster_01.id name = "myvm-1" } resource "netbox_virtual_disk" "example" { name = "disk-01" description = "Main disk" size_mb = 50 virtual_machine_id = netbox_virtual_machine.base_vm.id } ================================================ FILE: examples/resources/netbox_virtual_machine/resource.tf ================================================ // Assumes vmw-cluster-01 exists in Netbox data "netbox_cluster" "vmw_cluster_01" { name = "vmw-cluster-01" } resource "netbox_virtual_machine" "base_vm" { cluster_id = data.netbox_cluster.vmw_cluster_01.id name = "myvm-1" } // Assumes vmw-cluster-01 exists in Netbox data "netbox_cluster" "vmw_cluster_01" { name = "vmw-cluster-01" } resource "netbox_virtual_machine" "basic_vm" { cluster_id = data.netbox_cluster.vmw_cluster_01.id name = "myvm-2" disk_size_mb = 40000 memory_mb = 4092 vcpus = "2" } // Assumes vmw-cluster-01 exists as a cluster in Netbox data "netbox_cluster" "vmw_cluster_01" { name = "vmw-cluster-01" } // Assumes customer-a exists as a tenant in Netbox data "netbox_tenant" "customer_a" { name = "Customer A" } resource "netbox_virtual_machine" "full_vm" { cluster_id = data.netbox_cluster.vmw_cluster_01.id name = "myvm-3" disk_size_mb = 40000 memory_mb = 4092 vcpus = "2" role_id = 31 // This corresponds to the Netbox ID for a given role tenant_id = data.netbox_tenant.customer_a.id local_context_data = jsonencode({ "setting_a" = "Some Setting" "setting_b" = 42 }) } ================================================ FILE: examples/resources/netbox_vlan/resource.tf ================================================ resource "netbox_vlan" "example1" { name = "VLAN 1" vid = 1777 tags = [] } # Assume netbox_tenant, netbox_site, and netbox_tag resources exist resource "netbox_vlan" "example2" { name = "VLAN 2" vid = 1778 status = "reserved" description = "Reserved example VLAN" tenant_id = netbox_tenant.ex.id site_id = netbox_site.ex.id group_id = netbox_vlan_group.ex.id tags = [netbox_tag.ex.name] } ================================================ FILE: examples/resources/netbox_vlan_group/resource.tf ================================================ #Basic VLAN Group example resource "netbox_vlan_group" "example1" { name = "example1" slug = "example1" } #Full VLAN Group example resource "netbox_vlan_group" "example2" { name = "Second Example" slug = "example2" scope_type = "dcim.site" scope_id = netbox_site.example.id description = "Second Example VLAN Group" tags = [netbox_tag.example.id] vid_ranges = [[1, 2], [3, 4]] } ================================================ FILE: examples/resources/netbox_vpn_tunnel/resource.tf ================================================ resource "netbox_vpn_tunnel_group" "test" { name = "my-tunnel-group" } resource "netbox_vpn_tunnel" "test" { name = "my-tunnel" encapsulation = "ipsec-transport" status = "active" tunnel_group_id = netbox_vpn_tunnel_group.test.id description = "This is a description." tunnel_id = 3 tenant_id = 2 } ================================================ FILE: examples/resources/netbox_vpn_tunnel_group/resource.tf ================================================ resource "netbox_vpn_tunnel_group" "test" { name = "my-tunnel-group" description = "My description" } ================================================ FILE: examples/resources/netbox_vpn_tunnel_termination/resource.tf ================================================ resource "netbox_vpn_tunnel_group" "test" { name = "my-tunnel-group" description = "description" } resource "netbox_vpn_tunnel" "test" { name = "my-tunnel" encapsulation = "ipsec-transport" status = "active" tunnel_group_id = netbox_vpn_tunnel_group.test.id } resource "netbox_vpn_tunnel_termination" "device" { role = "peer" tunnel_id = netbox_vpn_tunnel.test.id device_interface_id = 123 } resource "netbox_vpn_tunnel_termination" "vm" { role = "peer" tunnel_id = netbox_vpn_tunnel.test.id virtual_machine_interface_id = 234 } ================================================ FILE: examples/resources/netbox_vrf/resource.tf ================================================ resource "netbox_vrf" "cust_a_prod" { name = "cust-a-prod" tags = ["customer-a", "prod"] } ================================================ FILE: examples/resources/netbox_webhook/resource.tf ================================================ resource "netbox_webhook" "test" { name = "test" payload_url = "https://example.com/webhook" bodytemplate = "Sample body" } ================================================ FILE: examples/resources/netbox_wireless_lan/resource.tf ================================================ resource "netbox_wireless_lan" "guest" { ssid = "guest-wifi" status = "active" } ================================================ FILE: examples/resources/netbox_wireless_lan_group/resource.tf ================================================ resource "netbox_wireless_lan_group" "parent" { name = "campus" } resource "netbox_wireless_lan_group" "child" { name = "building-a" parent_id = netbox_wireless_lan_group.parent.id } ================================================ FILE: go.mod ================================================ module github.com/e-breuninger/terraform-provider-netbox go 1.25.8 require ( github.com/fbreckle/go-netbox v0.0.0-20260127172814-8429073254cc github.com/fbreckle/terraform-plugin-docs v0.0.0-20220812121758-a828466500d3 github.com/go-openapi/runtime v0.29.4 github.com/go-openapi/strfmt v0.26.2 github.com/go-viper/mapstructure/v2 v2.5.0 github.com/goware/urlx v0.3.2 github.com/hashicorp/terraform-plugin-sdk/v2 v2.40.1 github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 ) require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.0 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/ProtonMail/go-crypto v1.4.1 // indirect github.com/PuerkitoBio/purell v1.2.1 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/armon/go-radix v1.0.0 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fatih/color v1.19.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.25.0 // indirect github.com/go-openapi/errors v0.22.7 // indirect github.com/go-openapi/jsonpointer v0.22.5 // indirect github.com/go-openapi/jsonreference v0.21.5 // indirect github.com/go-openapi/loads v0.23.3 // indirect github.com/go-openapi/spec v0.22.4 // indirect github.com/go-openapi/swag v0.25.5 // indirect github.com/go-openapi/swag/cmdutils v0.25.5 // indirect github.com/go-openapi/swag/conv v0.26.0 // indirect github.com/go-openapi/swag/fileutils v0.26.0 // indirect github.com/go-openapi/swag/jsonname v0.25.5 // indirect github.com/go-openapi/swag/jsonutils v0.26.0 // indirect github.com/go-openapi/swag/loading v0.25.5 // indirect github.com/go-openapi/swag/mangling v0.25.5 // indirect github.com/go-openapi/swag/netutils v0.25.5 // indirect github.com/go-openapi/swag/stringutils v0.26.0 // indirect github.com/go-openapi/swag/typeutils v0.26.0 // indirect github.com/go-openapi/swag/yamlutils v0.25.5 // indirect github.com/go-openapi/validate v0.25.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cty v1.5.0 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.7.0 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/go-version v1.9.0 // indirect github.com/hashicorp/hc-install v0.9.5 // indirect github.com/hashicorp/hcl/v2 v2.24.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-exec v0.25.1 // indirect github.com/hashicorp/terraform-json v0.27.2 // indirect github.com/hashicorp/terraform-plugin-go v0.31.0 // indirect github.com/hashicorp/terraform-plugin-log v0.10.0 // indirect github.com/hashicorp/terraform-registry-address v0.4.0 // indirect github.com/hashicorp/terraform-svchost v0.2.1 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/huandu/xstrings v1.4.0 // indirect github.com/imdario/mergo v0.3.15 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/mitchellh/cli v1.1.5 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/oklog/run v1.2.0 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/posener/complete v1.2.3 // indirect github.com/russross/blackfriday v1.6.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/zclconf/go-cty v1.18.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/mod v0.35.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect golang.org/x/tools v0.43.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect google.golang.org/grpc v1.80.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28= github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/fbreckle/go-netbox v0.0.0-20260127172814-8429073254cc h1:hD25F2J/e992lL1qXa94qM+Hv1ieqn6evPWaJM8WN9c= github.com/fbreckle/go-netbox v0.0.0-20260127172814-8429073254cc/go.mod h1:3U3/m/hna9Ntd3sbHBYwZ1IqbP2+coRzoXw3mCfu3kM= github.com/fbreckle/terraform-plugin-docs v0.0.0-20220812121758-a828466500d3 h1:DMSpM0btVedE2Tt1vfDHWQhf2obzjAe1F0/j8/CyfW4= github.com/fbreckle/terraform-plugin-docs v0.0.0-20220812121758-a828466500d3/go.mod h1:j3HmJySEjx6hOAOPDjGzmzpVNDQq9SNnnF+Vm22d2rs= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM= github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/analysis v0.25.0 h1:EnjAq1yO8wEO9HbPmY8vLPEIkdZuuFhCAKBPvCB7bCs= github.com/go-openapi/analysis v0.25.0/go.mod h1:5WFTRE43WLkPG9r9OtlMfqkkvUTYLVVCIxLlEpyF8kE= github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= github.com/go-openapi/runtime v0.29.4 h1:k2lDxrGoSAJRdhFG2tONKMpkizY/4X1cciSdtzk4Jjo= github.com/go-openapi/runtime v0.29.4/go.mod h1:K0k/2raY6oqXJnZAgWJB2i/12QKrhUKpZcH4PfV9P18= github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= github.com/go-openapi/strfmt v0.26.2 h1:ysjheCh4i1rmFEo2LanhELDNucNzfWTZhUDKgWWPaFM= github.com/go-openapi/strfmt v0.26.2/go.mod h1:fXh1e449cyUn2NYuz+wb3wARBUdMl7qPEZwX00nqivY= github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU= github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA= github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c= github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= github.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU= github.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc= github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM= github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y= github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU= github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14= github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= github.com/go-openapi/testify/enable/yaml/v2 v2.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0= github.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE= github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4= github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/goware/urlx v0.3.2 h1:gdoo4kBHlkqZNaf6XlQ12LGtQOmpKJrR04Rc3RnpJEo= github.com/goware/urlx v0.3.2/go.mod h1:h8uwbJy68o+tQXCGZNa9D73WN8n0r9OBae5bUnLcgjw= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cty v1.5.0 h1:EkQ/v+dDNUqnuVpmS5fPqyY71NXVgT5gf32+57xY8g0= github.com/hashicorp/go-cty v1.5.0/go.mod h1:lFUCG5kd8exDobgSfyj4ONE/dc822kiYMguVKdHGMLM= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA= github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hc-install v0.9.5 h1:XHCjcMn2563ysuaQ9v9ec2FNc7c2PJOIEEGobAFeIx4= github.com/hashicorp/hc-install v0.9.5/go.mod h1:ihEW4LshrNkxq2bU/MpVbKyn+yt1is2hYqUTHDGhG84= github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/terraform-exec v0.25.1 h1:PRutYRGM8pixV3B8812NYoBK5O+yuf3qcB/70KFKGiU= github.com/hashicorp/terraform-exec v0.25.1/go.mod h1:+izOYrs9sKMQK4OYvGDnrSSJHY/pm4e4eXFqSL2Q5mA= github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= github.com/hashicorp/terraform-plugin-go v0.31.0 h1:0Fz2r9DQ+kNNl6bx8HRxFd1TfMKUvnrOtvJPmp3Z0q8= github.com/hashicorp/terraform-plugin-go v0.31.0/go.mod h1:A88bDhd/cW7FnwqxQRz3slT+QY6yzbHKc6AOTtmdeS8= github.com/hashicorp/terraform-plugin-log v0.10.0 h1:eu2kW6/QBVdN4P3Ju2WiB2W3ObjkAsyfBsL3Wh1fj3g= github.com/hashicorp/terraform-plugin-log v0.10.0/go.mod h1:/9RR5Cv2aAbrqcTSdNmY1NRHP4E3ekrXRGjqORpXyB0= github.com/hashicorp/terraform-plugin-sdk/v2 v2.40.1 h1:2yPUd7esMOpuTaG3y1iEla1iw+tla+3ZEkkBnmOAre4= github.com/hashicorp/terraform-plugin-sdk/v2 v2.40.1/go.mod h1:sq8qsxh+PwdvTQFcd17kfCoBgQo46ADNMvCpKE7t/gY= github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk= github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE= github.com/hashicorp/terraform-svchost v0.2.1 h1:ubvrTFw3Q7CsoEaX7V06PtCTKG3wu7GyyobAoN4eF3Q= github.com/hashicorp/terraform-svchost v0.2.1/go.mod h1:zDMheBLvNzu7Q6o9TBvPqiZToJcSuCLXjAXxBslSky4= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zclconf/go-cty v1.18.1 h1:yEGE8M4iIZlyKQURZNb2SnEyZlZHUcBCnx6KF81KuwM= github.com/zclconf/go-cty v1.18.1/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: main.go ================================================ package main import ( "flag" "github.com/e-breuninger/terraform-provider-netbox/netbox" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" ) // Run "go generate" to format example terraform files and generate the docs for the registry/website // Run the docs generation tool, check its repository for more information on how it works and how docs // can be customized. //go:generate go run github.com/fbreckle/terraform-plugin-docs/cmd/tfplugindocs func main() { var debug bool flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve") flag.Parse() plugin.Serve(&plugin.ServeOpts{ Debug: debug, ProviderAddr: "registry.terraform.io/e-breuninger/netbox", ProviderFunc: func() *schema.Provider { return netbox.Provider() }, }) } ================================================ FILE: netbox/client.go ================================================ package netbox import ( "fmt" "net/http" "strings" "time" netboxclient "github.com/fbreckle/go-netbox/netbox/client" httptransport "github.com/go-openapi/runtime/client" "github.com/goware/urlx" log "github.com/sirupsen/logrus" ) // Config struct for the netbox provider type Config struct { APIToken string ServerURL string AllowInsecureHTTPS bool Headers map[string]interface{} RequestTimeout int StripTrailingSlashesFromURL bool CACertFile string } // customHeaderTransport is a transport that adds the specified headers on // every request. type customHeaderTransport struct { original http.RoundTripper headers map[string]interface{} } // Client does the heavy lifting of establishing a base Open API client to Netbox. func (cfg *Config) Client() (*netboxclient.NetBoxAPI, error) { log.WithFields(log.Fields{ "server_url": cfg.ServerURL, }).Debug("Initializing Netbox client") if cfg.APIToken == "" { return nil, fmt.Errorf("missing netbox API key") } // parse serverUrl parsedURL, urlParseError := urlx.Parse(cfg.ServerURL) if urlParseError != nil { return nil, fmt.Errorf("error while trying to parse URL: %s", urlParseError) } desiredRuntimeClientSchemes := []string{parsedURL.Scheme} log.WithFields(log.Fields{ "host": parsedURL.Host, "schemes": desiredRuntimeClientSchemes, }).Debug("Initializing Netbox Open API runtime client") // build http client clientOpts := httptransport.TLSClientOptions{ CA: cfg.CACertFile, InsecureSkipVerify: cfg.AllowInsecureHTTPS, } trans, err := httptransport.TLSTransport(clientOpts) if err != nil { return nil, err } trans.(*http.Transport).Proxy = http.ProxyFromEnvironment if len(cfg.Headers) > 0 { log.WithFields(log.Fields{ "custom_headers": cfg.Headers, }).Debug("Setting custom headers on every request to Netbox") trans = customHeaderTransport{ original: trans, headers: cfg.Headers, } } httpClient := &http.Client{ Transport: trans, Timeout: time.Second * time.Duration(cfg.RequestTimeout), } transport := httptransport.NewWithClient(parsedURL.Host, parsedURL.Path+netboxclient.DefaultBasePath, desiredRuntimeClientSchemes, httpClient) authScheme := "Token" if strings.HasPrefix(cfg.APIToken, "nbt_") { authScheme = "Bearer" } transport.DefaultAuthentication = httptransport.APIKeyAuth("Authorization", "header", fmt.Sprintf("%s %v", authScheme, cfg.APIToken)) transport.SetLogger(log.StandardLogger()) netboxClient := netboxclient.New(transport, nil) return netboxClient, nil } // RoundTrip adds the headers specified in the transport on every request. func (t customHeaderTransport) RoundTrip(r *http.Request) (*http.Response, error) { for key, value := range t.headers { r.Header.Add(key, fmt.Sprintf("%v", value)) } resp, err := t.original.RoundTrip(r) return resp, err } ================================================ FILE: netbox/client_test.go ================================================ package netbox import ( "net/http" "net/http/httptest" "testing" "github.com/fbreckle/go-netbox/netbox/client/status" "github.com/stretchr/testify/assert" ) func TestValidClientWithAllData(t *testing.T) { config := Config{ APIToken: "07b12b765127747e4afd56cb531b7bf9c61f3c30", ServerURL: "https://localhost:8080", } client, err := config.Client() assert.NotNil(t, client) assert.NoError(t, err) } func TestURLMissingSchemaShouldWork(t *testing.T) { config := Config{ APIToken: "07b12b765127747e4afd56cb531b7bf9c61f3c30", ServerURL: "localhost:8080", } client, err := config.Client() assert.NotNil(t, client) assert.NoError(t, err) } func TestURLMaleformedUrlShouldFail(t *testing.T) { config := Config{ APIToken: "07b12b765127747e4afd56cb531b7bf9c61f3c30", ServerURL: "xyz:/localhost:8080", } _, err := config.Client() assert.Error(t, err) } func TestURLMissingPortShouldWork(t *testing.T) { config := Config{ APIToken: "07b12b765127747e4afd56cb531b7bf9c61f3c30", ServerURL: "http://localhost", } client, err := config.Client() assert.NotNil(t, client) assert.NoError(t, err) } func TestURLMissingAccessKey(t *testing.T) { config := Config{ APIToken: "", ServerURL: "http://localhost", } _, err := config.Client() assert.Error(t, err) } func TestAdditionalHeadersSet(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { vals, ok := r.Header["Hello"] assert.True(t, ok) assert.Len(t, vals, 1) assert.Equal(t, vals[0], "World!") })) defer ts.Close() config := Config{ APIToken: "07b12b765127747e4afd56cb531b7bf9c61f3c30", ServerURL: ts.URL, Headers: map[string]interface{}{ "Hello": "World!", }, } client, err := config.Client() assert.NoError(t, err) req := status.NewStatusListParams() client.Status.StatusList(req, nil) } func TestV1TokenUsesTokenScheme(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") assert.Equal(t, "Token 07b12b765127747e4afd56cb531b7bf9c61f3c30", auth) })) defer ts.Close() config := Config{ APIToken: "07b12b765127747e4afd56cb531b7bf9c61f3c30", ServerURL: ts.URL, } client, err := config.Client() assert.NoError(t, err) req := status.NewStatusListParams() client.Status.StatusList(req, nil) } func TestV2TokenUsesBearerScheme(t *testing.T) { v2Token := "nbt_abc1234567890abcdef.checksum1234" ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") assert.Equal(t, "Bearer "+v2Token, auth) })) defer ts.Close() config := Config{ APIToken: v2Token, ServerURL: ts.URL, } client, err := config.Client() assert.NoError(t, err) req := status.NewStatusListParams() client.Status.StatusList(req, nil) } /* TODO func TestInvalidHttpsCertificate(t *testing.T) {} */ ================================================ FILE: netbox/custom_fields.go ================================================ package netbox import ( "encoding/json" "fmt" "net/url" "github.com/go-openapi/runtime" "github.com/go-openapi/strfmt" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) const customFieldsKey = "custom_fields" var customFieldsSchema = &schema.Schema{ Type: schema.TypeMap, Optional: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeString, Default: nil, }, } func getCustomFields(cf interface{}) map[string]interface{} { cfm, ok := cf.(map[string]interface{}) if !ok || len(cfm) == 0 { return nil } result := make(map[string]interface{}) for key, value := range cfm { if value != nil { result[key] = value } } if len(result) == 0 { return nil } return result } // flattenCustomFields converts custom fields to a map where all values are strings. // Complex nested objects (like IP address references) are converted to JSON strings. func flattenCustomFields(cf interface{}) map[string]interface{} { cfm, ok := cf.(map[string]interface{}) if !ok || len(cfm) == 0 { return nil } result := make(map[string]interface{}) for key, value := range cfm { if value == nil { result[key] = "" continue } // Check if the value is a simple type (string, number, bool) switch v := value.(type) { case string: result[key] = v case float64, int, int64, bool: result[key] = fmt.Sprintf("%v", v) default: // For complex types (maps, arrays, objects), convert to JSON string if jsonBytes, err := json.Marshal(value); err == nil { result[key] = string(jsonBytes) } else { // Fallback to string representation result[key] = fmt.Sprintf("%v", value) } } } return result } type CustomFieldParams struct { params runtime.ClientRequestWriter cfm map[string]interface{} } func (o *CustomFieldParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { if err := o.params.WriteToRequest(r, reg); err != nil { return err } for k, v := range o.cfm { if vs, ok := v.(string); ok { if err := r.SetQueryParam(fmt.Sprintf("cf_%s", url.QueryEscape(k)), vs); err != nil { return err } } } return nil } func WithCustomFieldParamsOption(cfm map[string]interface{}) func(*runtime.ClientOperation) { if cfm == nil { cfm = make(map[string]interface{}) } return func(co *runtime.ClientOperation) { co.Params = &CustomFieldParams{ params: co.Params, cfm: cfm, } } } ================================================ FILE: netbox/custom_fields_test.go ================================================ package netbox import ( "encoding/json" "testing" ) func TestFlattenCustomFields(t *testing.T) { tests := []struct { name string input interface{} expected map[string]interface{} }{ { name: "nil input", input: nil, expected: nil, }, { name: "empty map", input: map[string]interface{}{}, expected: nil, }, { name: "simple string value", input: map[string]interface{}{ "field1": "value1", }, expected: map[string]interface{}{ "field1": "value1", }, }, { name: "numeric values", input: map[string]interface{}{ "int_field": 42, "int64_field": int64(100), "float_field": 3.14, }, expected: map[string]interface{}{ "int_field": "42", "int64_field": "100", "float_field": "3.14", }, }, { name: "boolean value", input: map[string]interface{}{ "bool_field": true, }, expected: map[string]interface{}{ "bool_field": "true", }, }, { name: "null value", input: map[string]interface{}{ "null_field": nil, }, expected: map[string]interface{}{ "null_field": "", }, }, { name: "complex nested object (IP address reference)", input: map[string]interface{}{ "gateway": map[string]interface{}{ "id": 9, "address": "10.21.10.254/24", "display": "10.21.10.254/24", "family": map[string]interface{}{ "value": 4, "label": "IPv4", }, "url": "https://netbox.example.com/api/ipam/ip-addresses/9/", }, }, expected: map[string]interface{}{ "gateway": `{"address":"10.21.10.254/24","display":"10.21.10.254/24","family":{"label":"IPv4","value":4},"id":9,"url":"https://netbox.example.com/api/ipam/ip-addresses/9/"}`, }, }, { name: "array value", input: map[string]interface{}{ "tags": []string{"tag1", "tag2", "tag3"}, }, expected: map[string]interface{}{ "tags": `["tag1","tag2","tag3"]`, }, }, { name: "mixed types", input: map[string]interface{}{ "text_field": "simple text", "number_field": 123, "bool_field": false, "null_field": nil, "object_field": map[string]interface{}{ "nested": "value", }, }, expected: map[string]interface{}{ "text_field": "simple text", "number_field": "123", "bool_field": "false", "null_field": "", "object_field": `{"nested":"value"}`, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := flattenCustomFields(tt.input) if tt.expected == nil { if result != nil { t.Errorf("expected nil, got %v", result) } return } if result == nil { t.Errorf("expected non-nil result, got nil") return } if len(result) != len(tt.expected) { t.Errorf("expected %d fields, got %d", len(tt.expected), len(result)) } for key, expectedValue := range tt.expected { actualValue, ok := result[key] if !ok { t.Errorf("expected field %q not found in result", key) continue } // For JSON strings, compare the parsed objects to avoid formatting differences expectedStr, expectedIsStr := expectedValue.(string) actualStr, actualIsStr := actualValue.(string) if expectedIsStr && actualIsStr { // Try to parse as JSON var expectedJSON, actualJSON interface{} expectedIsJSON := json.Unmarshal([]byte(expectedStr), &expectedJSON) == nil actualIsJSON := json.Unmarshal([]byte(actualStr), &actualJSON) == nil if expectedIsJSON && actualIsJSON { // Compare as JSON objects expectedJSONStr, _ := json.Marshal(expectedJSON) actualJSONStr, _ := json.Marshal(actualJSON) if string(expectedJSONStr) != string(actualJSONStr) { t.Errorf("field %q: expected JSON %q, got %q", key, expectedStr, actualStr) } continue } } // Compare as strings if actualValue != expectedValue { t.Errorf("field %q: expected %q, got %q", key, expectedValue, actualValue) } } }) } } func TestFlattenCustomFields_ComplexRealWorldExample(t *testing.T) { // Simulate a real NetBox custom field response with an IP address object reference input := map[string]interface{}{ "gateway": map[string]interface{}{ "id": 9, "url": "https://netbox.zonda.systems/api/ipam/ip-addresses/9/", "display": "10.21.10.254/24", "address": "10.21.10.254/24", "description": "", "family": map[string]interface{}{ "value": float64(4), "label": "IPv4", }, }, "vlan_purpose": "management", "monitoring": true, "priority": 10, } result := flattenCustomFields(input) if result == nil { t.Fatal("expected non-nil result") } // Check that gateway is a JSON string gateway, ok := result["gateway"].(string) if !ok { t.Errorf("expected gateway to be a string, got %T", result["gateway"]) } // Verify we can parse the gateway JSON var gatewayObj map[string]interface{} if err := json.Unmarshal([]byte(gateway), &gatewayObj); err != nil { t.Errorf("failed to parse gateway JSON: %v", err) } // Verify the gateway object has expected fields if gatewayObj["address"] != "10.21.10.254/24" { t.Errorf("expected address 10.21.10.254/24, got %v", gatewayObj["address"]) } // Check simple fields if result["vlan_purpose"] != "management" { t.Errorf("expected vlan_purpose=management, got %v", result["vlan_purpose"]) } if result["monitoring"] != "true" { t.Errorf("expected monitoring=true, got %v", result["monitoring"]) } if result["priority"] != "10" { t.Errorf("expected priority=10, got %v", result["priority"]) } } func TestGetCustomFields(t *testing.T) { tests := []struct { name string input interface{} expected map[string]interface{} }{ { name: "nil input", input: nil, expected: nil, }, { name: "empty map", input: map[string]interface{}{}, expected: nil, }, { name: "valid map", input: map[string]interface{}{ "field1": "value1", "field2": 123, }, expected: map[string]interface{}{ "field1": "value1", "field2": 123, }, }, { name: "invalid type", input: "not a map", expected: nil, }, { name: "map with nil values excluded", input: map[string]interface{}{ "set_field": "value1", "unset_field": nil, }, expected: map[string]interface{}{ "set_field": "value1", }, }, { name: "map with only nil values returns nil", input: map[string]interface{}{ "unset1": nil, "unset2": nil, }, expected: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := getCustomFields(tt.input) if tt.expected == nil { if result != nil { t.Errorf("expected nil, got %v", result) } return } if result == nil { t.Errorf("expected non-nil result, got nil") return } if len(result) != len(tt.expected) { t.Errorf("expected %d fields, got %d", len(tt.expected), len(result)) } for key, expectedValue := range tt.expected { actualValue, ok := result[key] if !ok { t.Errorf("expected field %q not found in result", key) continue } if actualValue != expectedValue { t.Errorf("field %q: expected %v, got %v", key, expectedValue, actualValue) } } }) } } ================================================ FILE: netbox/data_source_netbox_asn.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxAsn() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxAsnRead, Description: `:meta:subcategory:IP Address Management (IPAM):`, Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "asn": { Type: schema.TypeString, Optional: true, AtLeastOneOf: []string{"asn", "tag"}, }, "tag": { Type: schema.TypeString, Optional: true, AtLeastOneOf: []string{"asn", "tag"}, Description: "Tag to include in the data source filter (must match the tag's slug).", }, "tag__n": { Type: schema.TypeString, Optional: true, Description: `Tag to exclude from the data source filter (must match the tag's slug). Refer to [Netbox's documentation](https://demo.netbox.dev/static/docs/rest-api/filtering/#lookup-expressions) for more information on available lookup expressions.`, }, "description": { Type: schema.TypeString, Computed: true, }, "tags": tagsSchemaRead, }, } } func dataSourceNetboxAsnRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := ipam.NewIpamAsnsListParams() limit := int64(2) // Limit of 2 is enough params.Limit = &limit if asn, ok := d.Get("asn").(string); ok && asn != "" { params.Asn = &asn } if tag, ok := d.Get("tag").(string); ok && tag != "" { params.Tag = []string{tag} //TODO: switch schema to list? } if tagn, ok := d.Get("tag__n").(string); ok && tagn != "" { params.Tagn = &tagn } res, err := api.Ipam.IpamAsnsList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one asn returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no asn found matching filter") } result := res.GetPayload().Results[0] d.Set("id", result.ID) d.Set("asn", strconv.FormatInt(*result.Asn, 10)) d.Set("description", result.Description) d.Set("tags", getTagListFromNestedTagList(result.Tags)) d.SetId(strconv.FormatInt(result.ID, 10)) return nil } ================================================ FILE: netbox/data_source_netbox_asn_test.go ================================================ package netbox import ( "fmt" "regexp" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxAsnSetUp(testName string) string { return fmt.Sprintf(` resource "netbox_rir" "test" { name = "%[1]s" } resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_asn" "test" { asn = "456" rir_id = netbox_rir.test.id tags = [netbox_tag.test.slug] }`, testName) } const testAccNetboxAsnNoResult = ` data "netbox_asn" "test" { asn = "1337" }` func testAccNetboxAsnByAsn() string { return ` data "netbox_asn" "test" { asn = "456" }` } func testAccNetboxAsnByTag(testName string) string { return fmt.Sprintf(` data "netbox_asn" "test" { tag = "%[1]s" }`, testName) } func TestAccNetboxAsnDataSource_basic(t *testing.T) { testName := testAccGetTestName("asn_ds_basic") setUp := testAccNetboxAsnSetUp(testName) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: setUp, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_asn.test", "asn", "456"), ), }, { Config: setUp + testAccNetboxAsnNoResult, ExpectError: regexp.MustCompile("no asn found matching filter"), }, { Config: setUp + testAccNetboxAsnByAsn(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_asn.test", "id", "netbox_asn.test", "id"), ), }, { Config: setUp + testAccNetboxAsnByTag(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_asn.test", "id", "netbox_asn.test", "id"), resource.TestCheckResourceAttr("data.netbox_asn.test", "asn", "456"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_asns.go ================================================ package netbox import ( "errors" "fmt" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxAsns() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxAsnsRead, Description: `:meta:subcategory:IP Address Management (IPAM):`, Schema: map[string]*schema.Schema{ "filter": { Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "value": { Type: schema.TypeString, Required: true, }, }, }, }, "limit": { Type: schema.TypeInt, Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), Default: 0, }, "asns": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "asn": { Type: schema.TypeInt, Computed: true, }, "rir_id": { Type: schema.TypeInt, Computed: true, }, "tags": tagsSchemaRead, }, }, }, }, } } func dataSourceNetboxAsnsRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := ipam.NewIpamAsnsListParams() // Get user limit var userLimit int64 = 0 if limitValue, ok := d.GetOk("limit"); ok { userLimit = int64(limitValue.(int)) } if filter, ok := d.GetOk("filter"); ok { var filterParams = filter.(*schema.Set) for _, f := range filterParams.List() { k := f.(map[string]interface{})["name"] v := f.(map[string]interface{})["value"] vString := v.(string) switch k { case "asn": params.Asn = &vString case "asn__gte": params.AsnGte = &vString case "asn__lte": params.AsnLte = &vString case "asn__n": params.Asnn = &vString default: return fmt.Errorf("'%s' is not a supported filter parameter", k) } } } // Fetch all pages with pagination paginationHelper := NewPaginationHelper(userLimit) var allAsns []*models.ASN pageSize := paginationHelper.GetPageSize() for { currentOffset := paginationHelper.CurrentOffset() params.Limit = &pageSize params.Offset = ¤tOffset res, err := api.Ipam.IpamAsnsList(params, nil) if err != nil { return fmt.Errorf("failed to fetch ASNs at offset %d: %w", currentOffset, err) } payload := res.GetPayload() allAsns = append(allAsns, payload.Results...) if len(payload.Results) == 0 { break } if !paginationHelper.ShouldContinuePaging(int64(len(allAsns)), payload.Next) { break } paginationHelper.Advance(int64(len(payload.Results))) } // Trim to user limit if specified trimmedCount := paginationHelper.TrimToLimit(len(allAsns)) filteredAsns := allAsns[:trimmedCount] if len(filteredAsns) == 0 { return errors.New("no result") } var s []map[string]interface{} for _, v := range filteredAsns { var mapping = make(map[string]interface{}) mapping["id"] = v.ID mapping["asn"] = v.Asn mapping["rir_id"] = v.Rir.ID mapping["tags"] = getTagListFromNestedTagList(v.Tags) s = append(s, mapping) } d.SetId(id.UniqueId()) return d.Set("asns", s) } ================================================ FILE: netbox/data_source_netbox_asns_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxAsnsSetUp(testName string) string { return fmt.Sprintf(` resource "netbox_rir" "test" { name = "%[1]s" } resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_asn" "test_1" { asn = "123" rir_id = netbox_rir.test.id tags = [netbox_tag.test.slug] } resource "netbox_asn" "test_2" { asn = "1234" rir_id = netbox_rir.test.id tags = [netbox_tag.test.slug] }`, testName) } func testAccNetboxAsnsByAsn() string { return ` data "netbox_asns" "test" { filter { name = "asn" value = "123" } }` } func testAccNetboxAsnsByAsnN() string { return ` data "netbox_asns" "test" { filter { name = "asn__n" value = "123" } }` } func testAccNetboxAsnsByRange(testName string) string { return ` data "netbox_asns" "test" { filter { name = "asn__gte" value = "100" } filter { name = "asn__lte" value = "2000" } }` } func TestAccNetboxAsnsDataSource_basic(t *testing.T) { testName := testAccGetTestName("asns_ds_basic") setUp := testAccNetboxAsnsSetUp(testName) // This test cannot be run in parallel with other tests, because other tests create also ASNs // These ASNs then interfere with the __n filter test resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: setUp, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_asn.test_1", "asn", "123"), ), }, { Config: setUp + testAccNetboxAsnsByAsn(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_asns.test", "asns.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_asns.test", "asns.0.id", "netbox_asn.test_1", "id"), ), }, { Config: setUp + testAccNetboxAsnsByAsnN(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_asns.test", "asns.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_asns.test", "asns.0.id", "netbox_asn.test_2", "id"), ), }, { Config: setUp + testAccNetboxAsnsByRange(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_asns.test", "asns.#", "2"), resource.TestCheckResourceAttrPair("data.netbox_asns.test", "asns.0.id", "netbox_asn.test_1", "id"), resource.TestCheckResourceAttrPair("data.netbox_asns.test", "asns.1.id", "netbox_asn.test_2", "id"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_available_prefix.go ================================================ package netbox import ( "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxAvailablePrefix() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxAvailablePrefixRead, Description: `:meta:subcategory:IP Address Management (IPAM):`, Schema: map[string]*schema.Schema{ "prefix_id": { Type: schema.TypeInt, Required: true, }, "prefixes_available": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "family": { Type: schema.TypeInt, Computed: true, }, "prefix": { Type: schema.TypeString, Computed: true, }, "vrf_id": { Type: schema.TypeInt, Computed: true, }, }, }, }, }, } } func dataSourceNetboxAvailablePrefixRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := ipam.NewIpamPrefixesAvailablePrefixesListParams() if prefixID, ok := d.Get("prefix_id").(int); ok && prefixID != 0 { params.ID = int64(prefixID) } res, err := api.Ipam.IpamPrefixesAvailablePrefixesList(params, nil) if err != nil { return err } result := res.GetPayload() var s []map[string]interface{} for _, v := range result { var mapping = make(map[string]interface{}) mapping["prefix"] = v.Prefix mapping["family"] = v.Family if v.Vrf != nil { mapping["vrf_id"] = v.Vrf.ID } s = append(s, mapping) } d.SetId(id.UniqueId()) return d.Set("prefixes_available", s) } ================================================ FILE: netbox/data_source_netbox_available_prefix_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxAvailablePrefixesDataSource_basic(t *testing.T) { testPrefix := "10.10.10.0/24" testSlug := "available_prefixes_ds_basic" testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_prefix" "test_with_vrf" { prefix = "%[2]s" status = "active" vrf_id = netbox_vrf.test_vrf.id } resource "netbox_vrf" "test_vrf" { name = "%[1]s_test_vrf" } resource "netbox_available_prefix" "test_create_available_prefix" { parent_prefix_id = netbox_prefix.test_with_vrf.id prefix_length = 27 vrf_id = netbox_vrf.test_vrf.id status = "active" } data "netbox_available_prefix" "test_available_prefix" { prefix_id = netbox_available_prefix.test_create_available_prefix.parent_prefix_id } `, testName, testPrefix), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_available_prefix.test_available_prefix", "prefixes_available.#", "3"), resource.TestCheckResourceAttr("data.netbox_available_prefix.test_available_prefix", "prefixes_available.0.prefix", "10.10.10.32/27"), resource.TestCheckResourceAttr("data.netbox_available_prefix.test_available_prefix", "prefixes_available.1.prefix", "10.10.10.64/26"), resource.TestCheckResourceAttr("data.netbox_available_prefix.test_available_prefix", "prefixes_available.2.prefix", "10.10.10.128/25"), resource.TestCheckResourceAttrPair("data.netbox_available_prefix.test_available_prefix", "prefixes_available.0.vrf_id", "netbox_vrf.test_vrf", "id"), resource.TestCheckResourceAttrPair("data.netbox_available_prefix.test_available_prefix", "prefix_id", "netbox_prefix.test_with_vrf", "id"), ), }, }, }) } func TestAccNetboxAvailablePrefixesDataSource_without_vrf(t *testing.T) { testPrefix := "10.10.10.0/24" testSlug := "available_prefixes_ds_without_vrf" testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_prefix" "test_with_vrf" { prefix = "%[2]s" status = "active" } resource "netbox_available_prefix" "test_create_available_prefix" { parent_prefix_id = netbox_prefix.test_with_vrf.id prefix_length = 27 status = "active" } data "netbox_available_prefix" "test_available_prefix" { prefix_id = netbox_available_prefix.test_create_available_prefix.parent_prefix_id } `, testName, testPrefix), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_available_prefix.test_available_prefix", "prefixes_available.#", "3"), resource.TestCheckResourceAttr("data.netbox_available_prefix.test_available_prefix", "prefixes_available.0.prefix", "10.10.10.32/27"), resource.TestCheckResourceAttr("data.netbox_available_prefix.test_available_prefix", "prefixes_available.1.prefix", "10.10.10.64/26"), resource.TestCheckResourceAttr("data.netbox_available_prefix.test_available_prefix", "prefixes_available.2.prefix", "10.10.10.128/25"), resource.TestCheckResourceAttr("data.netbox_available_prefix.test_available_prefix", "prefixes_available.2.vrf_id", "0"), resource.TestCheckResourceAttrPair("data.netbox_available_prefix.test_available_prefix", "prefix_id", "netbox_prefix.test_with_vrf", "id"), ), }, }, }) } func TestAccNetboxAvailablePrefixesDataSource_none_available(t *testing.T) { testPrefix := "10.10.10.0/24" testSlug := "available_prefixes_ds_none_available" testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_prefix" "test_with_vrf" { prefix = "%[2]s" status = "active" vrf_id = netbox_vrf.test_vrf.id } resource "netbox_vrf" "test_vrf" { name = "%[1]s_test_vrf" } resource "netbox_available_prefix" "test_create_available_prefix" { count = 2 parent_prefix_id = netbox_prefix.test_with_vrf.id prefix_length = 25 vrf_id = netbox_vrf.test_vrf.id status = "active" } data "netbox_available_prefix" "test_available_prefix" { prefix_id = netbox_available_prefix.test_create_available_prefix[1].parent_prefix_id } `, testName, testPrefix), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_available_prefix.test_available_prefix", "prefixes_available.#", "0"), resource.TestCheckResourceAttrPair("data.netbox_available_prefix.test_available_prefix", "prefix_id", "netbox_prefix.test_with_vrf", "id"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_cluster.go ================================================ package netbox import ( "errors" "fmt" "strconv" "github.com/fbreckle/go-netbox/netbox/client/virtualization" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxCluster() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxClusterRead, Description: `:meta:subcategory:Virtualization:`, Schema: map[string]*schema.Schema{ "cluster_id": { Type: schema.TypeInt, Computed: true, }, "id": { Type: schema.TypeString, Optional: true, Computed: true, AtLeastOneOf: []string{"name", "site_id", "id"}, }, "comments": { Type: schema.TypeString, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, "site_id": { Type: schema.TypeInt, Computed: true, Optional: true, AtLeastOneOf: []string{"name", "site_id", "id"}, }, "site_group_id": { Type: schema.TypeInt, Computed: true, }, "location_id": { Type: schema.TypeInt, Computed: true, }, "region_id": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Optional: true, AtLeastOneOf: []string{"name", "site_id", "id"}, }, "cluster_type_id": { Type: schema.TypeInt, Computed: true, }, "cluster_group_id": { Type: schema.TypeInt, Computed: true, Optional: true, }, "scope_type": { Type: schema.TypeString, Computed: true, }, "scope_id": { Type: schema.TypeInt, Computed: true, }, "custom_fields": { Type: schema.TypeMap, Computed: true, }, tagsKey: tagsSchemaRead, }, } } func dataSourceNetboxClusterRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := virtualization.NewVirtualizationClustersListParams() if name, ok := d.Get("name").(string); ok && name != "" { params.Name = &name } if siteID, ok := d.Get("site_id").(int); ok && siteID != 0 { params.SiteID = strToPtr(strconv.FormatInt(int64(siteID), 10)) } if id, ok := d.Get("id").(string); ok && id != "0" { params.SetID(&id) } if clustergroupID, ok := d.Get("cluster_group_id").(int); ok && clustergroupID != 0 { clustGroupStr := fmt.Sprintf("%d", clustergroupID) params.GroupID = &clustGroupStr } limit := int64(2) // Limit of 2 is enough params.Limit = &limit res, err := api.Virtualization.VirtualizationClustersList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one result, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no result") } result := res.GetPayload().Results[0] d.Set("cluster_id", result.ID) d.SetId(strconv.FormatInt(result.ID, 10)) d.Set("name", result.Name) d.Set("cluster_type_id", result.Type.ID) if result.Group != nil { d.Set("cluster_group_id", result.Group.ID) } else { d.Set("cluster_group_id", nil) } d.Set("comments", result.Comments) d.Set("description", result.Description) if result.ScopeType != nil && result.ScopeID != nil { d.Set("scope_type", result.ScopeType) d.Set("scope_id", result.ScopeID) scopeID := result.ScopeID switch scopeType := result.ScopeType; *scopeType { case "dcim.site": d.Set("site_id", scopeID) case "dcim.sitegroup": d.Set("site_group_id", scopeID) case "dcim.location": d.Set("location_id", scopeID) case "dcim.region": d.Set("region_id", scopeID) } } if result.CustomFields != nil { d.Set("custom_fields", flattenCustomFields(result.CustomFields)) } d.Set(tagsKey, getTagListFromNestedTagList(result.Tags)) return nil } ================================================ FILE: netbox/data_source_netbox_cluster_group.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/virtualization" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxClusterGroup() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxClusterGroupRead, Description: `:meta:subcategory:Virtualization:`, Schema: map[string]*schema.Schema{ "cluster_group_id": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Required: true, }, }, } } func dataSourceNetboxClusterGroupRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) params := virtualization.NewVirtualizationClusterGroupsListParams() params.Name = &name limit := int64(2) // Limit of 2 is enough params.Limit = &limit res, err := api.Virtualization.VirtualizationClusterGroupsList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one cluster group returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no cluster group found matching filter") } result := res.GetPayload().Results[0] d.Set("cluster_group_id", result.ID) d.SetId(strconv.FormatInt(result.ID, 10)) d.Set("name", result.Name) return nil } ================================================ FILE: netbox/data_source_netbox_cluster_group_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxClusterGroupDataSource_basic(t *testing.T) { testSlug := "clstrgrp_ds_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_cluster_group" "test" { name = "%[1]s" } data "netbox_cluster_group" "test" { depends_on = [netbox_cluster_group.test] name = "%[1]s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_cluster_group.test", "cluster_group_id", "netbox_cluster_group.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_cluster_group.test", "id", "netbox_cluster_group.test", "id"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_cluster_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxClusterDataSource_basic(t *testing.T) { testSlug := "clstr_ds_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_cluster_type" "test" { name = "%[1]s" } resource "netbox_cluster_group" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_cluster" "test" { name = "%[1]s" cluster_type_id = netbox_cluster_type.test.id cluster_group_id = netbox_cluster_group.test.id site_id = netbox_site.test.id comments = "%[1]scomments" description = "%[1]sdescription" tags = [netbox_tag.test.name] } resource "netbox_region" "test" { name = "%[1]s" } resource "netbox_cluster" "test_with_region" { name = "%[1]s_with_region" cluster_type_id = netbox_cluster_type.test.id cluster_group_id = netbox_cluster_group.test.id region_id = netbox_region.test.id comments = "%[1]scomments" description = "%[1]sdescription" tags = [netbox_tag.test.name] } data "netbox_cluster" "by_name" { name = netbox_cluster.test.name } data "netbox_cluster" "by_site_id" { site_id = netbox_cluster.test.site_id } data "netbox_cluster" "by_id" { id = netbox_cluster.test.id } data "netbox_cluster" "by_id_with_region" { id = netbox_cluster.test_with_region.id } data "netbox_cluster" "by_site_id_and_group_id" { site_id = netbox_cluster.test.site_id cluster_group_id = netbox_cluster.test.cluster_group_id } `, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_cluster.by_name", "id", "netbox_cluster.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_cluster.by_site_id", "id", "netbox_cluster.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_cluster.by_id", "id", "netbox_cluster.test", "id"), resource.TestCheckResourceAttr("data.netbox_cluster.by_name", "name", testName), resource.TestCheckResourceAttrPair("data.netbox_cluster.by_name", "cluster_type_id", "netbox_cluster_type.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_cluster.by_name", "cluster_group_id", "netbox_cluster_group.test", "id"), resource.TestCheckResourceAttr("data.netbox_cluster.by_name", "comments", testName+"comments"), resource.TestCheckResourceAttr("data.netbox_cluster.by_name", "description", testName+"description"), resource.TestCheckResourceAttr("data.netbox_cluster.by_name", "scope_type", "dcim.site"), resource.TestCheckResourceAttrPair("data.netbox_cluster.by_name", "scope_id", "netbox_site.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_cluster.by_name", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_cluster.by_id_with_region", "region_id", "netbox_region.test", "id"), resource.TestCheckResourceAttr("data.netbox_cluster.by_name", "tags.#", "1"), resource.TestCheckResourceAttr("data.netbox_cluster.by_name", "tags.0", testName), resource.TestCheckResourceAttrPair("data.netbox_cluster.by_site_id_and_group_id", "id", "netbox_cluster.test", "id"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_cluster_type.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/virtualization" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxClusterType() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxClusterTypeRead, Description: `:meta:subcategory:Virtualization:`, Schema: map[string]*schema.Schema{ "cluster_type_id": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Required: true, }, }, } } func dataSourceNetboxClusterTypeRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) params := virtualization.NewVirtualizationClusterTypesListParams() params.Name = &name limit := int64(2) // Limit of 2 is enough params.Limit = &limit res, err := api.Virtualization.VirtualizationClusterTypesList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one cluster type returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no cluster type found matching filter") } result := res.GetPayload().Results[0] d.Set("cluster_type_id", result.ID) d.SetId(strconv.FormatInt(result.ID, 10)) d.Set("name", result.Name) return nil } ================================================ FILE: netbox/data_source_netbox_cluster_type_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxClusterTypeDataSource_basic(t *testing.T) { testSlug := "clstrtyp_ds_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_cluster_type" "test" { name = "%[1]s" } data "netbox_cluster_type" "test" { depends_on = [netbox_cluster_type.test] name = "%[1]s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_cluster_type.test", "cluster_type_id", "netbox_cluster_type.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_cluster_type.test", "id", "netbox_cluster_type.test", "id"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_clusters.go ================================================ package netbox import ( "fmt" "regexp" "github.com/fbreckle/go-netbox/netbox/client/virtualization" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxClusters() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxClustersRead, Description: `:meta:subcategory:Virtualization:`, Schema: map[string]*schema.Schema{ "filter": { Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "value": { Type: schema.TypeString, Required: true, }, }, }, }, "name_regex": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringIsValidRegExp, }, "limit": { Type: schema.TypeInt, Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), Default: 0, }, "clusters": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "cluster_id": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, "cluster_type_id": { Type: schema.TypeInt, Computed: true, }, "cluster_group_id": { Type: schema.TypeInt, Computed: true, }, "tenant_id": { Type: schema.TypeInt, Computed: true, }, "site_id": { Type: schema.TypeInt, Computed: true, }, "site_group_id": { Type: schema.TypeInt, Computed: true, }, "location_id": { Type: schema.TypeInt, Computed: true, }, "region_id": { Type: schema.TypeInt, Computed: true, }, "scope_type": { Type: schema.TypeString, Computed: true, }, "scope_id": { Type: schema.TypeInt, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, "comments": { Type: schema.TypeString, Computed: true, }, "custom_fields": { Type: schema.TypeMap, Computed: true, }, tagsKey: tagsSchemaRead, }, }, }, }, } } func dataSourceNetboxClustersRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := virtualization.NewVirtualizationClustersListParams() // Get user limit (0 = fetch all) var userLimit int64 = 0 if limitValue, ok := d.GetOk("limit"); ok { userLimit = int64(limitValue.(int)) } if filter, ok := d.GetOk("filter"); ok { var filterParams = filter.(*schema.Set) var tags []string for _, f := range filterParams.List() { k := f.(map[string]interface{})["name"] v := f.(map[string]interface{})["value"] vString := v.(string) switch k { case "name": params.Name = &vString case "cluster_type_id": params.TypeID = &vString case "cluster_group_id": params.GroupID = &vString case "site_id": params.SiteID = &vString case "tag": tags = append(tags, vString) params.Tag = tags default: return fmt.Errorf("'%s' is not a supported filter parameter", k) } } } // Fetch all pages with pagination (fetch all when name_regex is used) paginationHelper := NewPaginationHelper(FetchAll) var allClusters []*models.Cluster pageSize := paginationHelper.GetPageSize() for { currentOffset := paginationHelper.CurrentOffset() params.Limit = &pageSize params.Offset = ¤tOffset res, err := api.Virtualization.VirtualizationClustersList(params, nil) if err != nil { return fmt.Errorf("failed to fetch clusters at offset %d: %w", currentOffset, err) } payload := res.GetPayload() allClusters = append(allClusters, payload.Results...) if len(payload.Results) == 0 { break } if !paginationHelper.ShouldContinuePaging(int64(len(allClusters)), payload.Next) { break } paginationHelper.Advance(int64(len(payload.Results))) } // Apply name_regex filter var filteredClusters []*models.Cluster if nameRegex, ok := d.GetOk("name_regex"); ok { r := regexp.MustCompile(nameRegex.(string)) for _, cluster := range allClusters { if r.MatchString(*cluster.Name) { filteredClusters = append(filteredClusters, cluster) } } } else { filteredClusters = allClusters } // Apply user limit to filtered results if userLimit > 0 && int64(len(filteredClusters)) > userLimit { filteredClusters = filteredClusters[:userLimit] } var s []map[string]interface{} for _, cluster := range filteredClusters { var mapping = make(map[string]interface{}) mapping["cluster_id"] = cluster.ID if cluster.Name != nil { mapping["name"] = *cluster.Name } if cluster.Type != nil { mapping["cluster_type_id"] = cluster.Type.ID } if cluster.Group != nil { mapping["cluster_group_id"] = cluster.Group.ID } if cluster.Tenant != nil { mapping["tenant_id"] = cluster.Tenant.ID } if cluster.Description != "" { mapping["description"] = cluster.Description } if cluster.Comments != "" { mapping["comments"] = cluster.Comments } if cluster.ScopeType != nil && cluster.ScopeID != nil { mapping["scope_type"] = *cluster.ScopeType mapping["scope_id"] = *cluster.ScopeID switch *cluster.ScopeType { case "dcim.site": mapping["site_id"] = *cluster.ScopeID case "dcim.sitegroup": mapping["site_group_id"] = *cluster.ScopeID case "dcim.location": mapping["location_id"] = *cluster.ScopeID case "dcim.region": mapping["region_id"] = *cluster.ScopeID } } if cluster.CustomFields != nil { mapping["custom_fields"] = cluster.CustomFields } if cluster.Tags != nil { mapping[tagsKey] = getTagListFromNestedTagList(cluster.Tags) } s = append(s, mapping) } d.SetId(id.UniqueId()) return d.Set("clusters", s) } ================================================ FILE: netbox/data_source_netbox_clusters_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxClustersDataSource_basic(t *testing.T) { testSlug := "clusters_ds_basic" testName := testAccGetTestName(testSlug) dependencies := testAccNetboxClustersDataSourceDependencies(testName) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: dependencies, }, { Config: dependencies + testAccNetboxClustersDataSourceFilterName(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_clusters.test", "clusters.#", "1"), resource.TestCheckResourceAttr("data.netbox_clusters.test", "clusters.0.name", testName+"_0"), resource.TestCheckResourceAttr("data.netbox_clusters.test", "clusters.0.comments", "thisisacomment"), resource.TestCheckResourceAttr("data.netbox_clusters.test", "clusters.0.description", "thisisadescription"), resource.TestCheckResourceAttrPair("data.netbox_clusters.test", "clusters.0.cluster_type_id", "netbox_cluster_type.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_clusters.test", "clusters.0.cluster_group_id", "netbox_cluster_group.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_clusters.test", "clusters.0.tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_clusters.test", "clusters.0.site_id", "netbox_site.test", "id"), ), }, { Config: dependencies + testAccNetboxClustersDataSourceFilterClusterTypeID, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_clusters.test", "clusters.#", "4"), resource.TestCheckResourceAttrPair("data.netbox_clusters.test", "clusters.0.cluster_type_id", "netbox_cluster_type.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_clusters.test", "clusters.0.name", "netbox_cluster.test0", "name"), resource.TestCheckResourceAttrPair("data.netbox_clusters.test", "clusters.1.name", "netbox_cluster.test1", "name"), resource.TestCheckResourceAttrPair("data.netbox_clusters.test", "clusters.2.name", "netbox_cluster.test2", "name"), resource.TestCheckResourceAttrPair("data.netbox_clusters.test", "clusters.3.name", "netbox_cluster.test3", "name"), ), }, { Config: dependencies + testAccNetboxClustersDataSourceFilterClusterGroupID, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_clusters.test", "clusters.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_clusters.test", "clusters.0.cluster_group_id", "netbox_cluster_group.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_clusters.test", "clusters.0.name", "netbox_cluster.test0", "name"), ), }, { Config: dependencies + testAccNetboxClustersDataSourceFilterSiteID, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_clusters.test", "clusters.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_clusters.test", "clusters.0.site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_clusters.test", "clusters.0.name", "netbox_cluster.test0", "name"), ), }, { Config: dependencies + testAccNetboxClustersDataSourceNameRegex, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_clusters.test", "clusters.#", "2"), resource.TestCheckResourceAttrPair("data.netbox_clusters.test", "clusters.0.name", "netbox_cluster.test2", "name"), resource.TestCheckResourceAttrPair("data.netbox_clusters.test", "clusters.1.name", "netbox_cluster.test3", "name"), ), }, { Config: dependencies + testAccNetboxClustersDataSourceLimit, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_clusters.test", "clusters.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_clusters.test", "clusters.0.cluster_type_id", "netbox_cluster_type.test", "id"), ), }, }, }) } func TestAccNetboxClustersDataSource_tags(t *testing.T) { testSlug := "clusters_ds_tags" testName := testAccGetTestName(testSlug) dependencies := testAccNetboxClustersDataSourceDependenciesWithTags(testName) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: dependencies, }, { Config: dependencies + testAccNetboxClustersDataSourceTagA(testName) + testAccNetboxClustersDataSourceTagB(testName) + testAccNetboxClustersDataSourceTagAB(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_clusters.tag-a", "clusters.#", "2"), resource.TestCheckResourceAttr("data.netbox_clusters.tag-b", "clusters.#", "2"), resource.TestCheckResourceAttr("data.netbox_clusters.tag-ab", "clusters.#", "1"), ), }, }, }) } func testAccNetboxClustersDataSourceDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_cluster_type" "test" { name = "%[1]s" } resource "netbox_cluster_group" "test" { name = "%[1]s" } resource "netbox_cluster" "test0" { name = "%[1]s_0" cluster_type_id = netbox_cluster_type.test.id cluster_group_id = netbox_cluster_group.test.id site_id = netbox_site.test.id tenant_id = netbox_tenant.test.id comments = "thisisacomment" description = "thisisadescription" } resource "netbox_cluster" "test1" { name = "%[1]s_1" cluster_type_id = netbox_cluster_type.test.id } resource "netbox_cluster" "test2" { name = "%[1]s_2_regex" cluster_type_id = netbox_cluster_type.test.id } resource "netbox_cluster" "test3" { name = "%[1]s_3_regex" cluster_type_id = netbox_cluster_type.test.id } `, testName) } func testAccNetboxClustersDataSourceDependenciesWithTags(testName string) string { return fmt.Sprintf(` resource "netbox_cluster_type" "test" { name = "%[1]s" } resource "netbox_tag" "servicea" { name = "%[1]s_service-a" } resource "netbox_tag" "serviceb" { name = "%[1]s_service-b" } resource "netbox_cluster" "test0" { name = "%[1]s_0" cluster_type_id = netbox_cluster_type.test.id tags = [ netbox_tag.servicea.name, netbox_tag.serviceb.name, ] } resource "netbox_cluster" "test1" { name = "%[1]s_1" cluster_type_id = netbox_cluster_type.test.id tags = [ netbox_tag.servicea.name, ] } resource "netbox_cluster" "test2" { name = "%[1]s_2" cluster_type_id = netbox_cluster_type.test.id tags = [ netbox_tag.serviceb.name, ] } `, testName) } func testAccNetboxClustersDataSourceFilterName(testName string) string { return fmt.Sprintf(` data "netbox_clusters" "test" { filter { name = "name" value = "%[1]s_0" } }`, testName) } const testAccNetboxClustersDataSourceFilterClusterTypeID = ` data "netbox_clusters" "test" { filter { name = "cluster_type_id" value = netbox_cluster_type.test.id } }` const testAccNetboxClustersDataSourceFilterClusterGroupID = ` data "netbox_clusters" "test" { filter { name = "cluster_group_id" value = netbox_cluster_group.test.id } }` const testAccNetboxClustersDataSourceFilterSiteID = ` data "netbox_clusters" "test" { filter { name = "site_id" value = netbox_site.test.id } }` const testAccNetboxClustersDataSourceNameRegex = ` data "netbox_clusters" "test" { name_regex = "test.*_regex" }` const testAccNetboxClustersDataSourceLimit = ` data "netbox_clusters" "test" { limit = 1 filter { name = "cluster_type_id" value = netbox_cluster_type.test.id } }` func testAccNetboxClustersDataSourceTagA(testName string) string { return fmt.Sprintf(` data "netbox_clusters" "tag-a" { filter { name = "tag" value = "%[1]s_service-a" } }`, testName) } func testAccNetboxClustersDataSourceTagB(testName string) string { return fmt.Sprintf(` data "netbox_clusters" "tag-b" { filter { name = "tag" value = "%[1]s_service-b" } }`, testName) } func testAccNetboxClustersDataSourceTagAB(testName string) string { return fmt.Sprintf(` data "netbox_clusters" "tag-ab" { filter { name = "tag" value = "%[1]s_service-a" } filter { name = "tag" value = "%[1]s_service-b" } }`, testName) } ================================================ FILE: netbox/data_source_netbox_config_context.go ================================================ package netbox import ( "encoding/json" "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/extras" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxConfigContext() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxConfigContextRead, Description: `:meta:subcategory:Extras:`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "description": { Type: schema.TypeString, Computed: true, }, "weight": { Type: schema.TypeInt, Computed: true, }, "data": { Type: schema.TypeString, Computed: true, }, "cluster_groups": { Type: schema.TypeList, Computed: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "cluster_types": { Type: schema.TypeList, Computed: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "clusters": { Type: schema.TypeList, Computed: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "device_types": { Type: schema.TypeList, Computed: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "locations": { Type: schema.TypeList, Computed: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "platforms": { Type: schema.TypeList, Computed: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "regions": { Type: schema.TypeList, Computed: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "roles": { Type: schema.TypeList, Computed: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "site_groups": { Type: schema.TypeList, Computed: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "sites": { Type: schema.TypeList, Computed: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "tenant_groups": { Type: schema.TypeList, Computed: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "tenants": { Type: schema.TypeList, Computed: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "tags": { Type: schema.TypeList, Computed: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeString, }, }, }, } } func dataSourceNetboxConfigContextRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) params := extras.NewExtrasConfigContextsListParams() params.Name = &name limit := int64(2) // Limit of 2 is enough params.Limit = &limit res, err := api.Extras.ExtrasConfigContextsList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one result. Specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no result") } result := res.GetPayload().Results[0] d.SetId(strconv.FormatInt(result.ID, 10)) d.Set("name", result.Name) d.Set("weight", result.Weight) if result.Data != nil { if jsonArr, err := json.Marshal(result.Data); err == nil { d.Set("data", string(jsonArr)) } } else { d.Set("data", nil) } clusterGroups := make([]int64, len(result.ClusterGroups)) for i, v := range result.ClusterGroups { clusterGroups[i] = int64(v.ID) } d.Set("cluster_groups", clusterGroups) clusterTypes := make([]int64, len(result.ClusterTypes)) for i, v := range result.ClusterTypes { clusterTypes[i] = int64(v.ID) } d.Set("cluster_types", clusterTypes) clusters := make([]int64, len(result.Clusters)) for i, v := range result.Clusters { clusters[i] = int64(v.ID) } d.Set("clusters", clusters) deviceTypes := make([]int64, len(result.DeviceTypes)) for i, v := range result.DeviceTypes { deviceTypes[i] = int64(v.ID) } d.Set("device_types", deviceTypes) locations := make([]int64, len(result.Locations)) for i, v := range result.Locations { locations[i] = int64(v.ID) } d.Set("locations", locations) platforms := make([]int64, len(result.Platforms)) for i, v := range result.Platforms { platforms[i] = int64(v.ID) } d.Set("platforms", platforms) regions := make([]int64, len(result.Regions)) for i, v := range result.Regions { regions[i] = int64(v.ID) } d.Set("regions", regions) roles := make([]int64, len(result.Roles)) for i, v := range result.Roles { roles[i] = int64(v.ID) } d.Set("roles", roles) siteGroups := make([]int64, len(result.SiteGroups)) for i, v := range result.SiteGroups { siteGroups[i] = int64(v.ID) } d.Set("site_groups", siteGroups) sites := make([]int64, len(result.Sites)) for i, v := range result.Sites { sites[i] = int64(v.ID) } d.Set("sites", sites) tenantGroups := make([]int64, len(result.TenantGroups)) for i, v := range result.TenantGroups { tenantGroups[i] = int64(v.ID) } d.Set("tenant_groups", tenantGroups) tenants := make([]int64, len(result.Tenants)) for i, v := range result.Tenants { tenants[i] = int64(v.ID) } d.Set("tenants", tenants) tags := make([]string, len(result.Tags)) for i, v := range result.Tags { tags[i] = string(v) } d.Set("tags", tags) return nil } ================================================ FILE: netbox/data_source_netbox_config_context_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxConfigContextDataSource_basic(t *testing.T) { testSlug := "cfct_ds_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_config_context" "test" { name = "%[1]s" weight = 1000 data = jsonencode({"testkey" = "testval"}) } data "netbox_config_context" "test" { depends_on = [netbox_config_context.test] name = "%[1]s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_config_context.test", "id", "netbox_config_context.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_config_context.test", "weight", "netbox_config_context.test", "weight"), resource.TestCheckResourceAttrPair("data.netbox_config_context.test", "data", "netbox_config_context.test", "data"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_contact.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/tenancy" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxContact() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxContactRead, Description: `:meta:subcategory:Tenancy:`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Computed: true, Optional: true, AtLeastOneOf: []string{"name", "slug"}, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, AtLeastOneOf: []string{"name", "slug"}, }, "group_id": { Type: schema.TypeInt, Computed: true, }, "description": { Type: schema.TypeString, Optional: true, }, }, } } func dataSourceNetboxContactRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := tenancy.NewTenancyContactsListParams() if name, ok := d.Get("name").(string); ok && name != "" { params.Name = &name } limit := int64(2) // Limit of 2 is enough params.Limit = &limit res, err := api.Tenancy.TenancyContactsList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one contact returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no contact found matching filter") } result := res.GetPayload().Results[0] d.SetId(strconv.FormatInt(result.ID, 10)) d.Set("name", result.Name) if result.Group != nil { d.Set("group_id", result.Group.ID) } return nil } ================================================ FILE: netbox/data_source_netbox_contact_group.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/tenancy" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxContactGroup() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxContactGroupRead, Description: `:meta:subcategory:Tenancy:`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Computed: true, }, "parent_id": { Type: schema.TypeInt, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, }, } } func dataSourceNetboxContactGroupRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) params := tenancy.NewTenancyContactGroupsListParams() params.Name = &name limit := int64(2) // Limit of 2 is enough params.Limit = &limit res, err := api.Tenancy.TenancyContactGroupsList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one contact group returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no contact group found matching filter") } result := res.GetPayload().Results[0] d.SetId(strconv.FormatInt(result.ID, 10)) d.Set("name", result.Name) d.Set("slug", result.Slug) d.Set("description", result.Description) if result.Parent != nil { d.Set("parent_id", result.Parent.ID) } return nil } ================================================ FILE: netbox/data_source_netbox_contact_group_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxContactGroupDataSource_basic(t *testing.T) { testSlug := "cntctgrp_ds_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_contact_group" "test" { name = "%[1]s" } data "netbox_contact_group" "test" { name = netbox_contact_group.test.name }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_contact_group.test", "id", "netbox_contact_group.test", "id"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_contact_role.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/tenancy" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxContactRole() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxContactRoleRead, Description: `:meta:subcategory:Tenancy:`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Computed: true, Optional: true, AtLeastOneOf: []string{"name", "slug"}, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, AtLeastOneOf: []string{"name", "slug"}, }, }, } } func dataSourceNetboxContactRoleRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := tenancy.NewTenancyContactRolesListParams() if name, ok := d.Get("name").(string); ok && name != "" { params.Name = &name } limit := int64(2) // Limit of 2 is enough params.Limit = &limit res, err := api.Tenancy.TenancyContactRolesList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one contact role returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no contact role found matching filter") } result := res.GetPayload().Results[0] d.SetId(strconv.FormatInt(result.ID, 10)) d.Set("name", result.Name) d.Set("slug", result.Slug) return nil } ================================================ FILE: netbox/data_source_netbox_contact_role_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxContactRoleDataSource_basic(t *testing.T) { testSlug := "cntctrl_ds_basic" testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_contact_role" "test" { name = "%[1]s" } data "netbox_contact_role" "by_name" { name = netbox_contact_role.test.name } `, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_contact_role.by_name", "id", "netbox_contact_role.test", "id"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_contact_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxContactDataSource_basic(t *testing.T) { testSlug := "cntct_ds_basic" testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_contact" "test" { name = "%[1]s" } data "netbox_contact" "by_name" { depends_on = [netbox_contact.test] name = "%[1]s" } data "netbox_contact" "by_slug" { depends_on = [netbox_contact.test] slug = "%[1]s" } data "netbox_contact" "by_description" { depends_on = [netbox_contact.test] name = "%[1]s" description = "%[1]s" } data "netbox_contact" "by_both" { depends_on = [netbox_contact.test] name = "%[1]s" slug = "%[1]s" description = "%[1]s" } `, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_contact.by_name", "id", "netbox_contact.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_contact.by_slug", "id", "netbox_contact.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_contact.by_description", "id", "netbox_contact.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_contact.by_both", "id", "netbox_contact.test", "id"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_device_interfaces.go ================================================ package netbox import ( "errors" "fmt" "regexp" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxDeviceInterfaces() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxDeviceInterfaceRead, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):`, Schema: map[string]*schema.Schema{ "filter": { Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "value": { Type: schema.TypeString, Required: true, }, }, }, }, "name_regex": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringIsValidRegExp, }, "limit": { Type: schema.TypeInt, Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), Default: 0, }, "interfaces": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, "enabled": { Type: schema.TypeBool, Computed: true, }, "mac_address": { Type: schema.TypeString, Computed: true, Description: "The MAC address as string from the first MAC address assigned to this interface, if any.", }, "mac_addresses": { Type: schema.TypeSet, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "mac_address": { Type: schema.TypeString, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, }, }, }, "mode": { Type: schema.TypeMap, Computed: true, Elem: &schema.Schema{ Type: schema.TypeString, Computed: true, }, }, "mtu": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, "tag_ids": { Type: schema.TypeList, Computed: true, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "tagged_vlans": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "vid": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, }, }, }, // Do as a TypeList due to limitation of TypeMap "untagged_vlan": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "vid": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, }, }, }, "device_id": { Type: schema.TypeInt, Computed: true, }, "type": { Type: schema.TypeString, Computed: true, }, }, }, }, }, } } func dataSourceNetboxDeviceInterfaceRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := dcim.NewDcimInterfacesListParams() // Get user limit (0 = fetch all) var userLimit int64 = 0 if limitValue, ok := d.GetOk("limit"); ok { userLimit = int64(limitValue.(int)) } if filter, ok := d.GetOk("filter"); ok { filterParams := filter.(*schema.Set) for _, f := range filterParams.List() { k := f.(map[string]interface{})["name"] v := f.(map[string]interface{})["value"] vString := v.(string) switch k { case "mac_address": params.MacAddress = &vString case "name": params.Name = &vString case "tag": params.Tag = []string{vString} // TODO: switch schema to list? case "device_id": params.DeviceID = &vString default: return fmt.Errorf("'%s' is not a supported filter parameter", k) } } } // Fetch all pages with pagination (fetch all when name_regex is used) paginationHelper := NewPaginationHelper(FetchAll) var allInterfaces []*models.Interface pageSize := paginationHelper.GetPageSize() for { currentOffset := paginationHelper.CurrentOffset() params.Limit = &pageSize params.Offset = ¤tOffset res, err := api.Dcim.DcimInterfacesList(params, nil) if err != nil { return fmt.Errorf("failed to fetch interfaces at offset %d: %w", currentOffset, err) } payload := res.GetPayload() allInterfaces = append(allInterfaces, payload.Results...) if len(payload.Results) == 0 { break } if !paginationHelper.ShouldContinuePaging(int64(len(allInterfaces)), payload.Next) { break } paginationHelper.Advance(int64(len(payload.Results))) } // Apply name_regex filter var filteredInterfaces []*models.Interface if nameRegex, ok := d.GetOk("name_regex"); ok { r := regexp.MustCompile(nameRegex.(string)) for _, dcimInterface := range allInterfaces { if r.MatchString(*dcimInterface.Name) { filteredInterfaces = append(filteredInterfaces, dcimInterface) } } } else { filteredInterfaces = allInterfaces } // Apply user limit to filtered results if userLimit > 0 && int64(len(filteredInterfaces)) > userLimit { filteredInterfaces = filteredInterfaces[:userLimit] } if len(filteredInterfaces) == 0 { return errors.New("no result") } var s []map[string]interface{} for _, v := range filteredInterfaces { mapping := make(map[string]interface{}) mapping["id"] = v.ID if v.Description != "" { mapping["description"] = v.Description } mapping["enabled"] = v.Enabled if v.MacAddresses != nil { var mac_addresses []map[string]interface{} for i, mac := range v.MacAddresses { mac_address := make(map[string]interface{}) // We just set the first mac address in the `mac_address` attribute if i == 0 { mapping["mac_address"] = v.MacAddresses[i].MacAddress } mac_address["id"] = mac.ID mac_address["description"] = mac.Description mac_address["mac_address"] = mac.MacAddress mac_addresses = append(mac_addresses, mac_address) } mapping["mac_addresses"] = mac_addresses } if v.Mode != nil { mapping["mode"] = map[string]string{ "label": *v.Mode.Label, "value": *v.Mode.Value, } } if v.Mtu != nil { mapping["mtu"] = *v.Mtu } if v.Name != nil { mapping["name"] = *v.Name } if v.TaggedVlans != nil { mapping["tagged_vlans"] = flattenVlanAttributes(v.TaggedVlans) } if v.Tags != nil { var tags []int64 for _, t := range v.Tags { tags = append(tags, t.ID) } mapping["tag_ids"] = tags } if v.UntaggedVlan != nil { vlanSlice := []*models.NestedVLAN{v.UntaggedVlan} mapping["untagged_vlan"] = flattenVlanAttributes(vlanSlice) } if v.Type != nil && v.Type.Value != nil { mapping["type"] = *v.Type.Value } mapping["device_id"] = v.Device.ID s = append(s, mapping) } d.SetId(id.UniqueId()) return d.Set("interfaces", s) } /* TODO: maybe separate this function from both VM Interfaces and DCIM Interfaces? func flattenVlanAttributes(vlans []*models.NestedVLAN) []map[string]interface{} { var mappedVlans []map[string]interface{} for _, vlan := range vlans { v := *vlan mappedVlan := map[string]interface{}{ "id": v.ID, "vid": *v.Vid, "name": *v.Name, } mappedVlans = append(mappedVlans, mappedVlan) } return mappedVlans } */ ================================================ FILE: netbox/data_source_netbox_device_interfaces_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxDeviceInterfacesDataSource_basic(t *testing.T) { testSlug := "dev_ifaces_ds_basic" testName := testAccGetTestName(testSlug) dependencies := testAccNetboxDeviceInterfacesDataSourceDependencies(testName) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: dependencies, }, { Config: dependencies + ` data "netbox_device_interfaces" "by_name" { filter { name = "name" value = netbox_device_interface.test.name } } data "netbox_device_interfaces" "by_device_id" { filter { name = "device_id" value = netbox_device.test.id } } data "netbox_device_interfaces" "by_mac_address" { filter { name = "mac_address" value = netbox_mac_address.test.mac_address } } data "netbox_device_interfaces" "by_tag" { filter { name = "tag" value = netbox_tag.test.name } } `, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_device_interfaces.by_name", "interfaces.#", "1"), resource.TestCheckResourceAttr("data.netbox_device_interfaces.by_name", "interfaces.0.type", "1000base-t"), resource.TestCheckResourceAttr("data.netbox_device_interfaces.by_name", "interfaces.0.name", testName), resource.TestCheckResourceAttr("data.netbox_device_interfaces.by_name", "interfaces.0.enabled", "true"), resource.TestCheckResourceAttrPair("data.netbox_device_interfaces.by_name", "interfaces.0.device_id", "netbox_device.test", "id"), resource.TestCheckResourceAttr("data.netbox_device_interfaces.by_device_id", "interfaces.#", "2"), resource.TestCheckResourceAttr("data.netbox_device_interfaces.by_mac_address", "interfaces.#", "1"), resource.TestCheckResourceAttrSet("data.netbox_device_interfaces.by_mac_address", "interfaces.0.mac_addresses.0.id"), resource.TestCheckResourceAttrSet("data.netbox_device_interfaces.by_mac_address", "interfaces.0.mac_addresses.0.mac_address"), resource.TestCheckResourceAttrSet("data.netbox_device_interfaces.by_mac_address", "interfaces.0.mac_addresses.0.description"), resource.TestCheckResourceAttr("data.netbox_device_interfaces.by_tag", "interfaces.#", "2"), ), }, }, }) } func testAccNetboxDeviceInterfacesDataSourceDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_interface" "test" { name = "%[1]s" device_id = netbox_device.test.id tags = [netbox_tag.test.name] type = "1000base-t" } resource "netbox_device_interface" "test2" { name = "%[1]s_two" device_id = netbox_device.test.id tags = [netbox_tag.test.name] type = "1000base-t" } resource "netbox_mac_address" "test" { mac_address = "F4:02:BA:7F:FD:F8" device_interface_id = netbox_device_interface.test.id description = "%[1]s" } `, testName) } ================================================ FILE: netbox/data_source_netbox_device_power_ports.go ================================================ package netbox import ( "errors" "fmt" "regexp" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxDevicePowerPorts() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxDevicePowerPortRead, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):`, Schema: map[string]*schema.Schema{ "filter": { Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "value": { Type: schema.TypeString, Required: true, }, }, }, }, "name_regex": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringIsValidRegExp, }, "limit": { Type: schema.TypeInt, Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), Default: 0, }, "power_ports": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, "tag_ids": { Type: schema.TypeList, Computed: true, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "device_id": { Type: schema.TypeInt, Computed: true, }, "module_id": { Type: schema.TypeInt, Optional: true, Computed: true, }, "type": { Type: schema.TypeString, Optional: true, Computed: true, }, "maximum_draw": { Type: schema.TypeInt, Optional: true, Computed: true, }, "allocated_draw": { Type: schema.TypeInt, Optional: true, Computed: true, }, }, }, }, }, } } func dataSourceNetboxDevicePowerPortRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := dcim.NewDcimPowerPortsListParams() // Get user limit (0 = fetch all) var userLimit int64 = 0 if limitValue, ok := d.GetOk("limit"); ok { userLimit = int64(limitValue.(int)) } if filter, ok := d.GetOk("filter"); ok { var filterParams = filter.(*schema.Set) for _, f := range filterParams.List() { k := f.(map[string]interface{})["name"] v := f.(map[string]interface{})["value"] vString := v.(string) switch k { case "name": params.Name = &vString case "tag": params.Tag = []string{vString} //TODO: switch schema to list? case "device_id": params.DeviceID = &vString default: return fmt.Errorf("'%s' is not a supported filter parameter", k) } } } // Fetch all pages with pagination (fetch all when name_regex is used) paginationHelper := NewPaginationHelper(FetchAll) var allPowerPorts []*models.PowerPort pageSize := paginationHelper.GetPageSize() for { currentOffset := paginationHelper.CurrentOffset() params.Limit = &pageSize params.Offset = ¤tOffset res, err := api.Dcim.DcimPowerPortsList(params, nil) if err != nil { return fmt.Errorf("failed to fetch power ports at offset %d: %w", currentOffset, err) } payload := res.GetPayload() allPowerPorts = append(allPowerPorts, payload.Results...) if len(payload.Results) == 0 { break } if !paginationHelper.ShouldContinuePaging(int64(len(allPowerPorts)), payload.Next) { break } paginationHelper.Advance(int64(len(payload.Results))) } // Apply name_regex filter var filteredPowerPorts []*models.PowerPort if nameRegex, ok := d.GetOk("name_regex"); ok { r, err := regexp.Compile(nameRegex.(string)) if err != nil { return fmt.Errorf("failed to compile name regex: %w", err) } for _, port := range allPowerPorts { if r.MatchString(*port.Name) { filteredPowerPorts = append(filteredPowerPorts, port) } } } else { filteredPowerPorts = allPowerPorts } // Apply user limit to filtered results if userLimit > 0 && int64(len(filteredPowerPorts)) > userLimit { filteredPowerPorts = filteredPowerPorts[:userLimit] } if len(filteredPowerPorts) == 0 { return errors.New("no result") } var s []map[string]interface{} for _, v := range filteredPowerPorts { var mapping = make(map[string]interface{}) mapping["id"] = v.ID if v.Description != "" { mapping["description"] = v.Description } if v.Name != nil { mapping["name"] = *v.Name } if v.Tags != nil { var tags []int64 for _, t := range v.Tags { tags = append(tags, t.ID) } mapping["tag_ids"] = tags } mapping["device_id"] = v.Device.ID if v.Module != nil { mapping["module_id"] = v.Module.ID } if v.Type != nil { mapping["type"] = v.Type.Value } if v.MaximumDraw != nil { mapping["maximum_draw"] = *v.MaximumDraw } if v.AllocatedDraw != nil { mapping["allocated_draw"] = *v.AllocatedDraw } s = append(s, mapping) } d.SetId(id.UniqueId()) return d.Set("power_ports", s) } ================================================ FILE: netbox/data_source_netbox_device_power_ports_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxDevicePowerportsDataSource_basic(t *testing.T) { testSlug := "dev_powerports_ds_basic" testName := testAccGetTestName(testSlug) dependencies := testAccNetboxDevicePowerportsDataSourceDependencies(testName) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: dependencies, }, { Config: dependencies + fmt.Sprintf(` data "netbox_device_power_ports" "by_name" { depends_on = [ netbox_device_power_port.test, netbox_device_power_port.test2, ] filter { name = "name" value = "%[1]s" } } data "netbox_device_power_ports" "by_device_id" { depends_on = [ netbox_device_power_port.test, netbox_device_power_port.test2, ] filter { name = "device_id" value = netbox_device.test.id } } data "netbox_device_power_ports" "by_tag" { depends_on = [ netbox_device_power_port.test, netbox_device_power_port.test2, ] filter { name = "tag" value = netbox_tag.test.name } } `, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_device_power_ports.by_name", "power_ports.#", "1"), resource.TestCheckResourceAttr("data.netbox_device_power_ports.by_name", "power_ports.0.name", testName), resource.TestCheckResourceAttr("data.netbox_device_power_ports.by_name", "power_ports.0.type", "iec-60309-3p-n-e-9h"), resource.TestCheckResourceAttrPair("data.netbox_device_power_ports.by_name", "power_ports.0.device_id", "netbox_device.test", "id"), resource.TestCheckResourceAttr("data.netbox_device_power_ports.by_device_id", "power_ports.#", "2"), resource.TestCheckResourceAttr("data.netbox_device_power_ports.by_tag", "power_ports.#", "2"), ), }, }, }) } func testAccNetboxDevicePowerportsDataSourceDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_power_port" "test" { name = "%[1]s" device_id = netbox_device.test.id type = "iec-60309-3p-n-e-9h" tags = [netbox_tag.test.name] } resource "netbox_device_power_port" "test2" { name = "%[1]s_two" device_id = netbox_device.test.id tags = [netbox_tag.test.name] } `, testName) } ================================================ FILE: netbox/data_source_netbox_device_render_config.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxDeviceRenderConfig() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxDeviceRenderConfigRead, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):Render the configuration template assigned to a device using the device's config context.`, Schema: map[string]*schema.Schema{ "device_id": { Type: schema.TypeInt, Required: true, Description: "The ID of the device to render configuration for.", }, "content": { Type: schema.TypeString, Computed: true, Description: "The rendered configuration content.", }, "config_template_id": { Type: schema.TypeInt, Computed: true, Description: "The ID of the config template that was used for rendering.", }, "config_template_name": { Type: schema.TypeString, Computed: true, Description: "The name of the config template that was used for rendering.", }, }, } } func dataSourceNetboxDeviceRenderConfigRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) deviceID := int64(d.Get("device_id").(int)) params := dcim.NewDcimDevicesRenderConfigParams().WithID(deviceID) res, err := api.Dcim.DcimDevicesRenderConfig(params, nil) if err != nil { return err } result := res.GetPayload() d.SetId(strconv.FormatInt(deviceID, 10)) d.Set("content", result.Content) if result.Configtemplate != nil { d.Set("config_template_id", result.Configtemplate.ID) d.Set("config_template_name", result.Configtemplate.Name) } return nil } ================================================ FILE: netbox/data_source_netbox_device_render_config_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxDeviceRenderConfigDataSource_basic(t *testing.T) { testSlug := "device_render_cfg" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_config_template" "test" { name = "%[1]s" template_code = "hostname={{ device.name }}" } resource "netbox_device" "test" { name = "%[1]s" site_id = netbox_site.test.id device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id config_template_id = netbox_config_template.test.id } data "netbox_device_render_config" "test" { depends_on = [netbox_device.test] device_id = netbox_device.test.id }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_device_render_config.test", "id", "netbox_device.test", "id"), resource.TestCheckResourceAttr("data.netbox_device_render_config.test", "content", fmt.Sprintf("hostname=%s", testName)), resource.TestCheckResourceAttrPair("data.netbox_device_render_config.test", "config_template_id", "netbox_config_template.test", "id"), resource.TestCheckResourceAttr("data.netbox_device_render_config.test", "config_template_name", testName), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_device_role.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxDeviceRole() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxDeviceRoleRead, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Computed: true, }, "color_hex": { Type: schema.TypeString, Computed: true, }, tagsKey: tagsSchemaRead, }, } } func dataSourceNetboxDeviceRoleRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) params := dcim.NewDcimDeviceRolesListParams() params.Name = &name limit := int64(2) // Limit of 2 is enough params.Limit = &limit res, err := api.Dcim.DcimDeviceRolesList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one device role returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no device role found matching filter") } result := res.GetPayload().Results[0] d.SetId(strconv.FormatInt(result.ID, 10)) d.Set("name", result.Name) d.Set("slug", result.Slug) d.Set("color_hex", result.Color) d.Set(tagsKey, getTagListFromNestedTagList(result.Tags)) return nil } ================================================ FILE: netbox/data_source_netbox_device_role_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxDeviceRoleDataSource_basic(t *testing.T) { testSlug := "dvrl_ds_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" tags = [netbox_tag.test.name] } data "netbox_device_role" "test" { depends_on = [netbox_device_role.test] name = "%[1]s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_device_role.test", "id", "netbox_device_role.test", "id"), resource.TestCheckResourceAttr("data.netbox_device_role.test", "tags.#", "1"), resource.TestCheckResourceAttr("data.netbox_device_role.test", "tags.0", testName), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_device_type.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxDeviceType() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxDeviceTypeRead, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):`, Schema: map[string]*schema.Schema{ "is_full_depth": { Type: schema.TypeBool, Computed: true, }, "manufacturer": { Type: schema.TypeString, Optional: true, }, "manufacturer_id": { Type: schema.TypeInt, Computed: true, }, "model": { Type: schema.TypeString, Optional: true, }, "part_number": { Type: schema.TypeString, Optional: true, }, "slug": { Type: schema.TypeString, Optional: true, }, "subdevice_role": { Type: schema.TypeString, Optional: true, }, "u_height": { Type: schema.TypeFloat, Computed: true, }, }, } } func dataSourceNetboxDeviceTypeRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := dcim.NewDcimDeviceTypesListParams() params.Limit = int64ToPtr(2) if manufacturer, ok := d.Get("manufacturer").(string); ok && manufacturer != "" { params.Manufacturer = &manufacturer } if model, ok := d.Get("model").(string); ok && model != "" { params.Model = &model } if part, ok := d.Get("part_number").(string); ok && part != "" { params.PartNumber = &part } if slug, ok := d.Get("slug").(string); ok && slug != "" { params.Slug = &slug } res, err := api.Dcim.DcimDeviceTypesList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one device type returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no device type found matching filter") } result := res.GetPayload().Results[0] d.SetId(strconv.FormatInt(result.ID, 10)) d.Set("is_full_depth", result.IsFullDepth) d.Set("manufacturer_id", result.Manufacturer.ID) d.Set("model", result.Model) d.Set("part_number", result.PartNumber) if result.SubdeviceRole != nil && result.SubdeviceRole.Value != nil { d.Set("subdevice_role", *result.SubdeviceRole.Value) } else { d.Set("subdevice_role", "") } d.Set("slug", result.Slug) d.Set("u_height", result.UHeight) return nil } ================================================ FILE: netbox/data_source_netbox_device_type_test.go ================================================ package netbox import ( "fmt" "regexp" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxDeviceTypeDataSource_basic(t *testing.T) { testName := testAccGetTestName("ds_dv_tp") setUp := testAccNetboxDeviceTypeSetUp(testName) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: setUp, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_type.test", "slug", testName), ), }, { Config: setUp + testAccNetboxDeviceTypeDataNoResult, ExpectError: regexp.MustCompile("no device type found matching filter"), }, { Config: setUp + testAccNetboxDeviceTypeDataByModel(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_device_type.test", "slug", testName), ), }, { Config: setUp + testAccNetboxDeviceTypeDataCombo(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_device_type.test", "slug", testName), ), }, { Config: setUp + testAccNetboxDeviceTypeDataBySlug(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_device_type.test", "model", testName), resource.TestCheckResourceAttr("data.netbox_device_type.test", "part_number", testName), resource.TestCheckResourceAttrPair("data.netbox_device_type.test", "manufacturer_id", "netbox_manufacturer.test", "id"), resource.TestCheckResourceAttrSet("data.netbox_device_type.test", "subdevice_role"), resource.TestCheckResourceAttrSet("data.netbox_device_type.test", "is_full_depth"), resource.TestCheckResourceAttrSet("data.netbox_device_type.test", "u_height"), ), }, }, }) } func testAccNetboxDeviceTypeSetUp(testName string) string { return fmt.Sprintf(` resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { manufacturer_id = netbox_manufacturer.test.id model = "%[1]s" part_number = "%[1]s" subdevice_role = "parent" }`, testName) } const testAccNetboxDeviceTypeDataNoResult = ` data "netbox_device_type" "no_result" { model = "_no_result_" }` func testAccNetboxDeviceTypeDataByModel(testName string) string { return fmt.Sprintf(` data "netbox_device_type" "test" { model = "%[1]s" }`, testName) } func testAccNetboxDeviceTypeDataCombo(testName string) string { return fmt.Sprintf(` data "netbox_device_type" "test" { manufacturer = "%[1]s" part_number = "%[1]s" }`, testName) } func testAccNetboxDeviceTypeDataBySlug(testName string) string { return fmt.Sprintf(` data "netbox_device_type" "test" { slug = "%[1]s" }`, testName) } ================================================ FILE: netbox/data_source_netbox_devices.go ================================================ // Copyright (c) 2022 Cisco Systems, Inc. and its affiliates // All rights reserved. package netbox import ( "encoding/json" "fmt" "net" "regexp" "strings" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxDevices() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxDevicesRead, Description: ":meta:subcategory:Data Center Inventory Management (DCIM):", Schema: map[string]*schema.Schema{ "filter": { Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "value": { Type: schema.TypeString, Required: true, }, }, }, }, "name_regex": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringIsValidRegExp, }, "limit": { Type: schema.TypeInt, Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), Default: 0, }, "devices": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "asset_tag": { Type: schema.TypeString, Computed: true, }, "cluster_id": { Type: schema.TypeInt, Computed: true, }, "comments": { Type: schema.TypeString, Computed: true, }, "config_context": { Type: schema.TypeString, Computed: true, }, "local_context_data": { Type: schema.TypeString, Computed: true, }, "custom_fields": { Type: schema.TypeMap, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, "device_id": { Type: schema.TypeInt, Computed: true, }, "device_type_id": { Type: schema.TypeInt, Computed: true, }, "location_id": { Type: schema.TypeInt, Computed: true, }, "manufacturer_id": { Type: schema.TypeInt, Computed: true, }, "model": { Type: schema.TypeString, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, "platform_id": { Type: schema.TypeInt, Computed: true, }, "site_id": { Type: schema.TypeInt, Computed: true, }, "tenant_id": { Type: schema.TypeInt, Computed: true, }, "role_id": { Type: schema.TypeInt, Computed: true, }, "serial": { Type: schema.TypeString, Computed: true, }, "status": { Type: schema.TypeString, Computed: true, }, "rack_id": { Type: schema.TypeInt, Computed: true, }, "rack_face": { Type: schema.TypeString, Computed: true, }, "rack_position": { Type: schema.TypeFloat, Computed: true, }, "primary_ipv4": { Type: schema.TypeString, Computed: true, }, "primary_ipv6": { Type: schema.TypeString, Computed: true, }, "oob_ip": { Type: schema.TypeString, Computed: true, }, "tags": tagsSchemaRead, }, }, }, }, } } func dataSourceNetboxDevicesRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := dcim.NewDcimDevicesListParams() // Get user limit var userLimit int64 = 0 if limit, ok := d.GetOk("limit"); ok { userLimit = int64(limit.(int)) } if filter, ok := d.GetOk("filter"); ok { var filterParams = filter.(*schema.Set) for _, f := range filterParams.List() { k := f.(map[string]interface{})["name"] v := f.(map[string]interface{})["value"] switch k { case "asset_tag": var assetTagString = v.(string) params.AssetTag = &assetTagString case "cluster_id": var clusterString = v.(string) params.ClusterID = &clusterString case "device_type_id": var deviceTypeIDString = v.(string) params.DeviceTypeID = &deviceTypeIDString case "name": var nameString = v.(string) params.Name = &nameString case "region": var regionString = v.(string) params.Region = ®ionString case "role_id": var roleIDString = v.(string) params.RoleID = &roleIDString case "site_id": var siteIDString = v.(string) params.SiteID = &siteIDString case "location_id": var locationIDString = v.(string) params.LocationID = &locationIDString case "rack_id": var rackIDString = v.(string) params.RackID = &rackIDString case "tenant_id": var tenantIDString = v.(string) params.TenantID = &tenantIDString case "tags": var tagsString = v.(string) params.Tag = strings.Split(tagsString, ",") case "status": var statusString = v.(string) params.Status = &statusString default: return fmt.Errorf("'%s' is not a supported filter parameter", k) } } } // Fetch all pages with pagination (fetch all when name_regex is used) paginationHelper := NewPaginationHelper(FetchAll) var allDevices []*models.DeviceWithConfigContext pageSize := paginationHelper.GetPageSize() for { currentOffset := paginationHelper.CurrentOffset() params.Limit = &pageSize params.Offset = ¤tOffset res, err := api.Dcim.DcimDevicesList(params, nil) if err != nil { return fmt.Errorf("failed to fetch devices at offset %d: %w", currentOffset, err) } payload := res.GetPayload() allDevices = append(allDevices, payload.Results...) if len(payload.Results) == 0 { break } if !paginationHelper.ShouldContinuePaging(int64(len(allDevices)), payload.Next) { break } paginationHelper.Advance(int64(len(payload.Results))) } // Apply name_regex filter to all fetched devices var filteredDevices []*models.DeviceWithConfigContext if nameRegex, ok := d.GetOk("name_regex"); ok { r := regexp.MustCompile(nameRegex.(string)) for _, device := range allDevices { if r.MatchString(*device.Name) { filteredDevices = append(filteredDevices, device) } } } else { filteredDevices = allDevices } // Apply user limit to filtered results if userLimit > 0 && int64(len(filteredDevices)) > userLimit { filteredDevices = filteredDevices[:userLimit] } var s []map[string]interface{} for _, device := range filteredDevices { var mapping = make(map[string]interface{}) if device.AssetTag != nil { mapping["asset_tag"] = *device.AssetTag } if device.Cluster != nil { mapping["cluster_id"] = device.Cluster.ID } if device.Comments != "" { mapping["comments"] = device.Comments } if device.Description != "" { mapping["description"] = device.Description } if device.ConfigContext != nil { if configContext, err := json.Marshal(device.ConfigContext); err == nil { mapping["config_context"] = string(configContext) } } if device.LocalContextData != nil { if localContextData, err := json.Marshal(device.LocalContextData); err == nil { mapping["local_context_data"] = string(localContextData) } } mapping["device_id"] = device.ID if device.DeviceType != nil { mapping["device_type_id"] = device.DeviceType.ID } if device.DeviceType.Manufacturer != nil { mapping["manufacturer_id"] = device.DeviceType.Manufacturer.ID } if device.DeviceType.Model != nil { mapping["model"] = *device.DeviceType.Model } if device.Name != nil { mapping["name"] = *device.Name } if device.Location != nil { mapping["location_id"] = device.Location.ID } if device.Platform != nil { mapping["platform_id"] = device.Platform.ID } if device.Site != nil { mapping["site_id"] = device.Site.ID } if device.Tenant != nil { mapping["tenant_id"] = device.Tenant.ID } if device.Role != nil { mapping["role_id"] = device.Role.ID } if device.Serial != "" { mapping["serial"] = device.Serial } if device.Status != nil { mapping["status"] = *device.Status.Value } if device.CustomFields != nil { mapping["custom_fields"] = flattenCustomFields(device.CustomFields) } if device.Rack != nil { mapping["rack_id"] = device.Rack.ID } if device.Position != nil { mapping["rack_position"] = device.Position } if device.Face != nil { mapping["rack_face"] = device.Face.Value } if device.Tags != nil { mapping["tags"] = getTagListFromNestedTagList(device.Tags) } if device.PrimaryIp4 != nil { ip, _, err := net.ParseCIDR(*device.PrimaryIp4.Address) if err == nil { primaryIPv4 := ip.String() mapping["primary_ipv4"] = &primaryIPv4 } } if device.PrimaryIp6 != nil { ip, _, err := net.ParseCIDR(*device.PrimaryIp6.Address) if err == nil { primaryIPv6 := ip.String() mapping["primary_ipv6"] = &primaryIPv6 } } if device.OobIP != nil { ip, _, err := net.ParseCIDR(*device.OobIP.Address) if err == nil { OobIP := ip.String() mapping["oob_ip"] = &OobIP } } s = append(s, mapping) } d.SetId(id.UniqueId()) return d.Set("devices", s) } ================================================ FILE: netbox/data_source_netbox_devices_pagination_test.go ================================================ package netbox import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "strconv" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) // TestDataSourceNetboxDevicesPagination verifies that the devices data source fetches // all pages when total results exceed the page size, using a mock HTTP server. // // Without pagination the data source would return at most DefaultPageSize items; // this test uses 130 items (100 + 30, not divisible by page size) so the last page // is always partial, exercising the boundary condition. func TestDataSourceNetboxDevicesPagination(t *testing.T) { const totalDevices = 130 requestCount := 0 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/dcim/devices/" { http.NotFound(w, r) return } requestCount++ offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) limit := int(DefaultPageSize) if s := r.URL.Query().Get("limit"); s != "" { limit, _ = strconv.Atoi(s) } end := offset + limit if end > totalDevices { end = totalDevices } var nextURL interface{} if end < totalDevices { nextURL = fmt.Sprintf("http://%s/api/dcim/devices/?limit=%d&offset=%d", r.Host, limit, end) } results := make([]map[string]interface{}, 0, end-offset) for i := offset; i < end; i++ { results = append(results, minimalDeviceJSON(i+1, fmt.Sprintf("device-%d", i+1))) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "count": totalDevices, "next": nextURL, "previous": nil, "results": results, }) })) defer ts.Close() cfg := Config{APIToken: "test-token", ServerURL: ts.URL} client, err := cfg.Client() if err != nil { t.Fatalf("failed to create client: %v", err) } state := &providerState{NetBoxAPI: client} d := schema.TestResourceDataRaw(t, dataSourceNetboxDevices().Schema, map[string]interface{}{}) if err := dataSourceNetboxDevicesRead(d, state); err != nil { t.Fatalf("dataSourceNetboxDevicesRead returned error: %v", err) } devices := d.Get("devices").([]interface{}) if len(devices) != totalDevices { t.Errorf("got %d devices, want %d (pagination likely broken)", len(devices), totalDevices) } if requestCount < 2 { t.Errorf("expected multiple API requests (pagination), got %d", requestCount) } } // minimalDeviceJSON returns the minimum fields required by the go-netbox swagger // validator for a DeviceWithConfigContext response item. func minimalDeviceJSON(id int, name string) map[string]interface{} { return map[string]interface{}{ "id": id, "name": name, "device_type": map[string]interface{}{ "id": 1, "model": "test-model", "slug": "test-model", "manufacturer": map[string]interface{}{ "id": 1, "name": "test-manufacturer", "slug": "test-manufacturer", }, }, "role": map[string]interface{}{ "id": 1, "name": "test-role", "slug": "test-role", }, "site": map[string]interface{}{ "id": 1, "name": "test-site", "slug": "test-site", }, "tags": []interface{}{}, } } ================================================ FILE: netbox/data_source_netbox_devices_test.go ================================================ // Copyright (c) 2022 Cisco Systems, Inc. and its affiliates // All rights reserved. package netbox import ( "encoding/json" "fmt" "strings" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxDevicesDataSource_basic(t *testing.T) { testSlug := "devices_ds_basic" testName := testAccGetTestName(testSlug) testLocalContextData, _ := json.Marshal(map[string]string{"testkey0": "testvalue0"}) dependencies := testAccNetboxDeviceDataSourceDependencies(testName) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: dependencies, }, { Config: dependencies + testAccNetboxDeviceDataSourceFilterName(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_devices.test", "devices.#", "1"), resource.TestCheckResourceAttr("data.netbox_devices.test", "devices.0.name", testName+"_0"), resource.TestCheckResourceAttr("data.netbox_devices.test", "devices.0.comments", "this is also a comment"), resource.TestCheckResourceAttr("data.netbox_devices.test", "devices.0.description", "this is also a description"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.0.tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.0.role_id", "netbox_device_role.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.0.device_type_id", "netbox_device_type.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.0.site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.0.platform_id", "netbox_platform.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.0.location_id", "netbox_location.test", "id"), resource.TestCheckResourceAttr("data.netbox_devices.test", "devices.0.serial", "ABCDEF0"), resource.TestCheckResourceAttr("data.netbox_devices.test", "devices.0.status", "staged"), resource.TestCheckResourceAttr("data.netbox_devices.test", "devices.0.tags.#", "1"), resource.TestCheckResourceAttr("data.netbox_devices.test", "devices.0.local_context_data", string(testLocalContextData)), ), }, { Config: dependencies + testAccNetboxDeviceDataSourceFilterTenant, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_devices.test", "devices.#", "4"), resource.TestCheckResourceAttr("data.netbox_devices.test", "devices.0.primary_ipv4", "10.0.0.60"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.0.tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.0.name", "netbox_device.test0", "name"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.1.name", "netbox_device.test1", "name"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.2.name", "netbox_device.test2", "name"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.3.name", "netbox_device.test3", "name"), ), }, { Config: dependencies + testAccNetboxDeviceDataSourceFilterRole, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_devices.test", "devices.#", "4"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.1.role_id", "netbox_device_role.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.0.name", "netbox_device.test0", "name"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.1.name", "netbox_device.test1", "name"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.2.name", "netbox_device.test2", "name"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.3.name", "netbox_device.test3", "name"), ), }, { Config: dependencies + testAccNetboxDeviceDataSourceNameRegex(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_devices.test", "devices.#", "2"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.0.name", "netbox_device.test2", "name"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.1.name", "netbox_device.test3", "name"), ), }, { Config: dependencies + testAccNetboxDeviceDataSourceLimit, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_devices.test", "devices.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.0.tenant_id", "netbox_tenant.test", "id"), ), }, { Config: dependencies + testAccNetBoxDeviceDataSourceFilterTagsAndStatus, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_devices.tag_devices", "devices.#", "1"), resource.TestCheckResourceAttr("data.netbox_devices.tag_devices", "devices.0.tags.#", "1"), resource.TestCheckResourceAttr("data.netbox_devices.tag_devices", "devices.0.status", "staged"), ), }, { Config: dependencies + testAccNetBoxDeviceDataSourceMultipleTagsFilter, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_devices.multiple_filter_devices", "devices.#", "1"), resource.TestCheckResourceAttr("data.netbox_devices.multiple_filter_devices", "devices.0.tags.#", "2"), ), }, }, }) } func testAccNetboxDeviceDataSourceDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_platform" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_cluster_type" "test" { name = "%[1]s" } resource "netbox_cluster" "test" { name = "%[1]s" cluster_type_id = netbox_cluster_type.test.id site_id = netbox_site.test.id } resource "netbox_location" "test" { name = "%[1]s" site_id =netbox_site.test.id } resource "netbox_rack_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_rack" "test" { name = "%[1]s" site_id = netbox_site.test.id status = "reserved" width = 19 u_height = 48 tenant_id = netbox_tenant.test.id location_id = netbox_location.test.id } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_tag" "test_a" { name = "%[1]sa" } resource "netbox_tag" "test_b" { name = "%[1]sb" } resource "netbox_tag" "test_c" { name = "%[1]sc" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device_interface" "test" { name = "eth0" device_id = netbox_device.test0.id type = "1000base-t" } resource "netbox_ip_address" "test" { ip_address = "10.0.0.60/24" status = "active" interface_id = netbox_device_interface.test.id object_type = "dcim.interface" } resource "netbox_device_primary_ip" "test_v4" { device_id = netbox_device.test0.id ip_address_id = netbox_ip_address.test.id } resource "netbox_device" "test0" { name = "%[1]s_0" comments = "this is also a comment" description = "this is also a description" tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id device_type_id = netbox_device_type.test.id site_id = netbox_site.test.id platform_id = netbox_platform.test.id location_id = netbox_location.test.id serial = "ABCDEF0" status = "staged" tags = [netbox_tag.test_a.name] local_context_data = jsonencode({"testkey0"="testvalue0"}) } resource "netbox_device" "test1" { name = "%[1]s_1" comments = "this is also first comment" tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id device_type_id = netbox_device_type.test.id site_id = netbox_site.test.id platform_id = netbox_platform.test.id location_id = netbox_location.test.id serial = "ABCDEF1" local_context_data = jsonencode({"testkey1"="testvalue1"}) } resource "netbox_device" "test2" { name = "%[1]s_2_regex" comments = "this is also second comment" tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id device_type_id = netbox_device_type.test.id site_id = netbox_site.test.id platform_id = netbox_platform.test.id location_id = netbox_location.test.id serial = "ABCDEF2" tags = [netbox_tag.test_b.name, netbox_tag.test_c.name] local_context_data = jsonencode({"testkey2"="testvalue2"}) } resource "netbox_device" "test3" { name = "%[1]s_3_regex" comments = "this is also third comment" tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id device_type_id = netbox_device_type.test.id site_id = netbox_site.test.id platform_id = netbox_platform.test.id location_id = netbox_location.test.id serial = "ABCDEF3" local_context_data = jsonencode({"testkey3"="testvalue3"}) } `, testName) } func testAccNetboxDeviceDataSourceFilterName(testName string) string { return fmt.Sprintf(` data "netbox_devices" "test" { filter { name = "name" value = "%[1]s_0" } }`, testName) } const testAccNetboxDeviceDataSourceFilterTenant = ` data "netbox_devices" "test" { filter { name = "tenant_id" value = netbox_tenant.test.id } }` const testAccNetBoxDeviceDataSourceFilterTagsAndStatus = ` data "netbox_devices" "tag_devices" { filter { name = "tags" value = netbox_tag.test_a.name } filter { name = "status" value = "staged" } }` const testAccNetBoxDeviceDataSourceMultipleTagsFilter = ` data "netbox_devices" "multiple_filter_devices" { filter { name = "tags" value = join(",", [netbox_tag.test_b.name, netbox_tag.test_c.name]) } }` const testAccNetboxDeviceDataSourceFilterRole = ` data "netbox_devices" "test" { filter { name = "role_id" value = netbox_device_role.test.id } }` func testAccNetboxDeviceDataSourceNameRegex(testName string) string { return fmt.Sprintf(` data "netbox_devices" "test" { name_regex = "%[1]s.*_regex" }`, testName) } const testAccNetboxDeviceDataSourceLimit = ` data "netbox_devices" "test" { limit = 1 filter { name = "tenant_id" value = netbox_tenant.test.id } }` func TestAccNetboxDevicesDataSource_CustomFields(t *testing.T) { testSlug := "device_ds_customfields" testName := testAccGetTestName(testSlug) testField := strings.ReplaceAll(testAccGetTestName(testSlug), "-", "_") resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxDeviceFullDependencies(testName) + fmt.Sprintf(` data "netbox_devices" "test" { depends_on = [ netbox_device.test, netbox_custom_field.test, ] filter { name = "name" value = "%[2]s" } } resource "netbox_custom_field" "test" { name = "%[1]s" type = "text" content_types = ["dcim.device"] } resource "netbox_device" "test" { name = "%[2]s" comments = "thisisacomment" description = "thisisadescription" tenant_id = netbox_tenant.test.id platform_id = netbox_platform.test.id role_id = netbox_device_role.test.id device_type_id = netbox_device_type.test.id tags = [netbox_tag.test_a.name] site_id = netbox_site.test.id cluster_id = netbox_cluster.test.id location_id = netbox_location.test.id status = "staged" serial = "ABCDEF" custom_fields = {"${netbox_custom_field.test.name}" = "81"} } `, testField, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_devices.test", "devices.#", "1"), resource.TestCheckResourceAttr("data.netbox_devices.test", "devices.0.name", testName), resource.TestCheckResourceAttr("data.netbox_devices.test", "devices.0.comments", "thisisacomment"), resource.TestCheckResourceAttr("data.netbox_devices.test", "devices.0.description", "thisisadescription"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.0.tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.0.role_id", "netbox_device_role.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.0.device_type_id", "netbox_device_type.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.0.site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.0.platform_id", "netbox_platform.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_devices.test", "devices.0.location_id", "netbox_location.test", "id"), resource.TestCheckResourceAttr("data.netbox_devices.test", "devices.0.serial", "ABCDEF"), resource.TestCheckResourceAttr("data.netbox_devices.test", "devices.0.status", "staged"), resource.TestCheckResourceAttr("data.netbox_devices.test", "devices.0.custom_fields."+testField, "81"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_interfaces.go ================================================ package netbox import ( "errors" "fmt" "regexp" "github.com/fbreckle/go-netbox/netbox/client/virtualization" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxInterfaces() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxInterfaceRead, Description: `:meta:subcategory:Virtualization:`, Schema: map[string]*schema.Schema{ "filter": { Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "value": { Type: schema.TypeString, Required: true, }, }, }, }, "limit": { Type: schema.TypeInt, Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), Default: 0, Description: "The limit of objects to return from the API lookup.", }, "name_regex": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringIsValidRegExp, }, "interfaces": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, "enabled": { Type: schema.TypeBool, Computed: true, }, "mac_address": { Type: schema.TypeString, Computed: true, }, "mode": { Type: schema.TypeMap, Computed: true, Elem: &schema.Schema{ Type: schema.TypeString, Computed: true, }, }, "mtu": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, "tag_ids": { Type: schema.TypeList, Computed: true, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "tagged_vlans": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "vid": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, }, }, }, // Do as a TypeList due to limitation of TypeMap "untagged_vlan": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "vid": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, }, }, }, "vm_id": { Type: schema.TypeInt, Computed: true, }, }, }, }, }, } } func dataSourceNetboxInterfaceRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := virtualization.NewVirtualizationInterfacesListParams() // Get user limit (0 = fetch all) var userLimit int64 = 0 if limitValue, ok := d.GetOk("limit"); ok { userLimit = int64(limitValue.(int)) } if filter, ok := d.GetOk("filter"); ok { var filterParams = filter.(*schema.Set) for _, f := range filterParams.List() { k := f.(map[string]interface{})["name"] v := f.(map[string]interface{})["value"] vString := v.(string) switch k { case "cluster_id": params.ClusterID = &vString case "mac_address": params.MacAddress = &vString case "name": params.Name = &vString case "tag": params.Tag = []string{vString} //TODO: switch schema to list? case "vm_id": params.VirtualMachineID = &vString default: return fmt.Errorf("'%s' is not a supported filter parameter", k) } } } // Fetch all pages with pagination (fetch all when name_regex is used) paginationHelper := NewPaginationHelper(FetchAll) var allInterfaces []*models.VMInterface pageSize := paginationHelper.GetPageSize() for { currentOffset := paginationHelper.CurrentOffset() params.Limit = &pageSize params.Offset = ¤tOffset res, err := api.Virtualization.VirtualizationInterfacesList(params, nil) if err != nil { return fmt.Errorf("failed to fetch interfaces at offset %d: %w", currentOffset, err) } payload := res.GetPayload() allInterfaces = append(allInterfaces, payload.Results...) if len(payload.Results) == 0 { break } if !paginationHelper.ShouldContinuePaging(int64(len(allInterfaces)), payload.Next) { break } paginationHelper.Advance(int64(len(payload.Results))) } // Apply name_regex filter var filteredInterfaces []*models.VMInterface if nameRegex, ok := d.GetOk("name_regex"); ok { r := regexp.MustCompile(nameRegex.(string)) for _, vmInterface := range allInterfaces { if r.MatchString(*vmInterface.Name) { filteredInterfaces = append(filteredInterfaces, vmInterface) } } } else { filteredInterfaces = allInterfaces } // Apply user limit to filtered results if userLimit > 0 && int64(len(filteredInterfaces)) > userLimit { filteredInterfaces = filteredInterfaces[:userLimit] } if len(filteredInterfaces) == 0 { return errors.New("no result") } var s []map[string]interface{} for _, v := range filteredInterfaces { var mapping = make(map[string]interface{}) mapping["id"] = v.ID if v.Description != "" { mapping["description"] = v.Description } mapping["enabled"] = v.Enabled if v.MacAddress != nil { mapping["mac_address"] = *v.MacAddress } if v.Mode != nil { mapping["mode"] = map[string]string{ "label": *v.Mode.Label, "value": *v.Mode.Value, } } if v.Mtu != nil { mapping["mtu"] = *v.Mtu } if v.Name != nil { mapping["name"] = *v.Name } if v.TaggedVlans != nil { mapping["tagged_vlans"] = flattenVlanAttributes(v.TaggedVlans) } if v.Tags != nil { var tags []int64 for _, t := range v.Tags { tags = append(tags, t.ID) } mapping["tag_ids"] = tags } if v.UntaggedVlan != nil { vlanSlice := []*models.NestedVLAN{v.UntaggedVlan} mapping["untagged_vlan"] = flattenVlanAttributes(vlanSlice) } mapping["vm_id"] = v.VirtualMachine.ID s = append(s, mapping) } d.SetId(id.UniqueId()) return d.Set("interfaces", s) } func flattenVlanAttributes(vlans []*models.NestedVLAN) []map[string]interface{} { var mappedVlans []map[string]interface{} for _, vlan := range vlans { v := *vlan mappedVlan := map[string]interface{}{ "id": v.ID, "vid": *v.Vid, "name": *v.Name, } mappedVlans = append(mappedVlans, mappedVlan) } return mappedVlans } ================================================ FILE: netbox/data_source_netbox_interfaces_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxInterfacesDataSource_basic(t *testing.T) { testSlug := "interface_ds_basic" testResource := "data.netbox_interfaces.test" testName := testAccGetTestName(testSlug) dependencies := testAccNetboxInterfacesDataSourceDependencies(testName) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: dependencies, }, { Config: dependencies + testAccNetboxInterfacesDataSourceFilterName(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(testResource, "interfaces.#", "1"), resource.TestCheckResourceAttr(testResource, "interfaces.0.name", testName+"_0"), resource.TestCheckResourceAttr(testResource, "interfaces.0.enabled", "true"), resource.TestCheckResourceAttrPair(testResource, "interfaces.0.vm_id", "netbox_virtual_machine.test0", "id"), resource.TestCheckResourceAttrPair(testResource, "interfaces.0.id", "netbox_interface.vm0_1", "id"), ), }, { Config: dependencies + testAccNetboxInterfacesDataSourceFilterVM, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(testResource, "interfaces.#", "2"), resource.TestCheckResourceAttrPair(testResource, "interfaces.0.vm_id", "netbox_virtual_machine.test1", "id"), resource.TestCheckResourceAttrPair(testResource, "interfaces.1.vm_id", "netbox_virtual_machine.test1", "id"), ), }, { Config: dependencies + testAccNetboxInterfacesDataSourceFilterVMWithLimit, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(testResource, "interfaces.#", "1"), ), }, { Config: dependencies + testAccNetboxInterfacesDataSourceNameRegex, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(testResource, "interfaces.#", "1"), resource.TestCheckResourceAttr(testResource, "interfaces.0.name", testName+"_2_regex"), ), }, }, }) } func testAccNetboxInterfacesDataSourceDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_cluster_type" "test" { name = "%[1]s" } resource "netbox_cluster" "test" { name = "%[1]s" cluster_type_id = netbox_cluster_type.test.id } resource "netbox_virtual_machine" "test0" { name = "%[1]s_0" cluster_id = netbox_cluster.test.id } resource "netbox_virtual_machine" "test1" { name = "%[1]s_1" cluster_id = netbox_cluster.test.id } resource "netbox_interface" "vm0_1" { name = "%[1]s_0" virtual_machine_id = netbox_virtual_machine.test0.id } resource "netbox_interface" "vm1_1" { name = "%[1]s_1" virtual_machine_id = netbox_virtual_machine.test1.id } resource "netbox_interface" "vm1_2" { name = "%[1]s_2_regex" virtual_machine_id = netbox_virtual_machine.test1.id }`, testName) } const testAccNetboxInterfacesDataSourceFilterVM = ` data "netbox_interfaces" "test" { filter { name = "vm_id" value = netbox_virtual_machine.test1.id } }` const testAccNetboxInterfacesDataSourceFilterVMWithLimit = ` data "netbox_interfaces" "test" { limit = 1 filter { name = "vm_id" value = netbox_virtual_machine.test1.id } }` func testAccNetboxInterfacesDataSourceFilterName(testName string) string { return fmt.Sprintf(` data "netbox_interfaces" "test" { filter { name = "name" value = "%[1]s_0" } }`, testName) } const testAccNetboxInterfacesDataSourceNameRegex = ` data "netbox_interfaces" "test" { name_regex = "test.*_regex" }` ================================================ FILE: netbox/data_source_netbox_ip_address.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxIPAddress() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxIPAddressRead, Description: `:meta:subcategory:IP Address Management (IPAM):`, Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Required: true, ValidateFunc: validation.IntAtLeast(1), }, "ip_address": { Type: schema.TypeString, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, "created": { Type: schema.TypeString, Computed: true, }, "last_updated": { Type: schema.TypeString, Computed: true, }, "custom_fields": { Type: schema.TypeMap, Computed: true, }, "address_family": { Type: schema.TypeString, Computed: true, }, "status": { Type: schema.TypeString, Computed: true, }, "dns_name": { Type: schema.TypeString, Computed: true, }, "role": { Type: schema.TypeString, Computed: true, }, "tenant": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, "slug": { Type: schema.TypeString, Computed: true, }, }, }, }, "tags": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, "display": { Type: schema.TypeString, Computed: true, }, "slug": { Type: schema.TypeString, Computed: true, }, }, }, }, }, } } func dataSourceNetboxIPAddressRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id := d.Get("id").(int) params := ipam.NewIpamIPAddressesReadParams() params.SetID(int64(id)) res, err := api.Ipam.IpamIPAddressesRead(params, nil) if err != nil { return err } result := res.GetPayload() d.SetId(strconv.FormatInt(result.ID, 10)) d.Set("id", result.ID) d.Set("ip_address", result.Address) d.Set("description", result.Description) d.Set("created", result.Created.String()) d.Set("last_updated", result.LastUpdated.String()) d.Set("custom_fields", flattenCustomFields(result.CustomFields)) d.Set("address_family", result.Family.Label) d.Set("status", result.Status.Value) d.Set("dns_name", result.DNSName) if result.Role != nil { d.Set("role", result.Role.Value) } var tenant []map[string]interface{} if result.Tenant != nil { var mapping = make(map[string]interface{}) mapping["id"] = result.Tenant.ID mapping["name"] = result.Tenant.Name mapping["slug"] = result.Tenant.Slug tenant = append(tenant, mapping) } d.Set("tenant", tenant) var tags []map[string]interface{} for _, t := range result.Tags { var tagmapping = make(map[string]interface{}) tagmapping["name"] = t.Name tagmapping["display"] = t.Display tagmapping["slug"] = t.Slug tagmapping["id"] = t.ID tags = append(tags, tagmapping) } d.Set("tags", tags) return nil } ================================================ FILE: netbox/data_source_netbox_ip_address_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxIpAddressDataSource_basic(t *testing.T) { ipAddress := "10.0.0.107/24" status := "active" resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_ip_address" "test" { ip_address = "%[1]s" status = "%[2]s" } data "netbox_ip_address" "test" { depends_on = [netbox_ip_address.test] id = netbox_ip_address.test.id }`, ipAddress, status), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_ip_address.test", "ip_address", "netbox_ip_address.test", "ip_address"), ), ExpectNonEmptyPlan: false, }, }, }) } ================================================ FILE: netbox/data_source_netbox_ip_addresses.go ================================================ package netbox import ( "errors" "fmt" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxIPAddresses() *schema.Resource { customFieldsFilterSchema := *customFieldsSchema return &schema.Resource{ Read: dataSourceNetboxIPAddressesRead, Description: `:meta:subcategory:IP Address Management (IPAM):`, Schema: map[string]*schema.Schema{ "filter": { Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "value": { Type: schema.TypeString, Required: true, }, }, }, }, "limit": { Type: schema.TypeInt, Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), Default: 0, }, customFieldsKey: &customFieldsFilterSchema, "ip_addresses": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, "created": { Type: schema.TypeString, Computed: true, }, "last_updated": { Type: schema.TypeString, Computed: true, }, "custom_fields": { Type: schema.TypeMap, Computed: true, }, "ip_address": { Type: schema.TypeString, Computed: true, }, "address_family": { Type: schema.TypeString, Computed: true, }, "status": { Type: schema.TypeString, Computed: true, }, "dns_name": { Type: schema.TypeString, Computed: true, }, "role": { Type: schema.TypeString, Computed: true, }, "tenant": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, "slug": { Type: schema.TypeString, Computed: true, }, }, }, }, "tags": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, "display": { Type: schema.TypeString, Computed: true, }, "slug": { Type: schema.TypeString, Computed: true, }, }, }, }, }, }, }, }, } } func dataSourceNetboxIPAddressesRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := ipam.NewIpamIPAddressesListParams() var opts []ipam.ClientOption // Get user limit var userLimit int64 = 0 if limitValue, ok := d.GetOk("limit"); ok { userLimit = int64(limitValue.(int)) } if filter, ok := d.GetOk("filter"); ok { var filterParams = filter.(*schema.Set) var tags []string for _, f := range filterParams.List() { k := f.(map[string]interface{})["name"] v := f.(map[string]interface{})["value"] vString := v.(string) switch k { case "dns_name": params.DNSName = &vString case "interface_id": params.InterfaceID = &vString case "device_id": params.DeviceID = &vString case "ip_address": params.Address = &vString case "vm_interface_id": params.VminterfaceID = &vString case "role": params.Role = &vString case "status": params.Status = &vString case "vrf": params.Vrf = &vString case "tenant": params.Tenant = &vString case "parent_prefix": params.Parent = &vString case "tag": tags = append(tags, vString) params.Tag = tags case "description": params.Description = &vString default: return fmt.Errorf("'%s' is not a supported filter parameter", k) } } } // Fetch all pages with pagination paginationHelper := NewPaginationHelper(userLimit) var allIPAddresses []*models.IPAddress if cfm, ok := d.Get(customFieldsKey).(map[string]interface{}); ok { opts = append(opts, WithCustomFieldParamsOption(cfm)) } pageSize := paginationHelper.GetPageSize() for { currentOffset := paginationHelper.CurrentOffset() params.Limit = &pageSize params.Offset = ¤tOffset res, err := api.Ipam.IpamIPAddressesList(params, nil, opts...) if err != nil { return fmt.Errorf("failed to fetch IP addresses at offset %d: %w", currentOffset, err) } payload := res.GetPayload() allIPAddresses = append(allIPAddresses, payload.Results...) if len(payload.Results) == 0 { break } if !paginationHelper.ShouldContinuePaging(int64(len(allIPAddresses)), payload.Next) { break } paginationHelper.Advance(int64(len(payload.Results))) } // Trim to user limit if specified trimmedCount := paginationHelper.TrimToLimit(len(allIPAddresses)) filteredIPAddresses := allIPAddresses[:trimmedCount] if len(filteredIPAddresses) == 0 { return errors.New("no result") } var s []map[string]interface{} for _, v := range filteredIPAddresses { var mapping = make(map[string]interface{}) mapping["id"] = v.ID mapping["description"] = v.Description mapping["created"] = v.Created.String() mapping["last_updated"] = v.LastUpdated.String() mapping["custom_fields"] = flattenCustomFields(v.CustomFields) mapping["ip_address"] = v.Address mapping["address_family"] = v.Family.Label mapping["status"] = v.Status.Value mapping["dns_name"] = v.DNSName mapping["tenant"] = flattenTenant(v.Tenant) var stags []map[string]interface{} for _, t := range v.Tags { var tagmapping = make(map[string]interface{}) tagmapping["name"] = t.Name tagmapping["display"] = t.Display tagmapping["slug"] = t.Slug tagmapping["id"] = t.ID stags = append(stags, tagmapping) } mapping["tags"] = stags if v.Role != nil { mapping["role"] = v.Role.Value } s = append(s, mapping) } d.SetId(id.UniqueId()) return d.Set("ip_addresses", s) } func flattenTenant(tenant *models.NestedTenant) []map[string]interface{} { var s []map[string]interface{} if tenant != nil { var mapping = make(map[string]interface{}) mapping["id"] = tenant.ID mapping["name"] = tenant.Name mapping["slug"] = tenant.Slug s = append(s, mapping) } return s } ================================================ FILE: netbox/data_source_netbox_ip_addresses_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxIpAddressesDataSource_basic(t *testing.T) { testSlug := "ipam_ipaddrs_ds_basic" testName := testAccGetTestName(testSlug) testIP := "203.0.113.1/24" resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_address" "test" { ip_address = "%s" virtual_machine_interface_id = netbox_interface.test.id status = "active" tags = [netbox_tag.test.name] role = "anycast" } data "netbox_ip_addresses" "test" { depends_on = [netbox_ip_address.test] }`, testIP), // This snippet sometimes returns things from other tests, even if resource.Test is used instead of resource.ParallelTest // This happens especially in CI testing (where test execution is presumably slow) // The check functions are now removed so this does no longer happen // Check: resource.ComposeTestCheckFunc( // resource.TestCheckResourceAttrPair("data.netbox_ip_addresses.test", "ip_addresses.0.ip_address", "netbox_ip_address.test", "ip_address"), // resource.TestCheckResourceAttr("data.netbox_ip_addresses.test", "ip_addresses.0.role", "anycast"), // resource.TestCheckResourceAttrPair("data.netbox_ip_addresses.test", "ip_addresses.0.tags.0.name", "netbox_tag.test", "name"), // ), }, }, }) } func TestAccNetboxIpAddressesDataSource_filter(t *testing.T) { testSlug := "ipam_ipaddrs_ds_filter" testName := testAccGetTestName(testSlug) testIP0 := "203.0.113.1/24" testIP1 := "203.0.113.2/24" resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_address" "test_list_0" { ip_address = "%s" virtual_machine_interface_id = netbox_interface.test.id status = "active" tags = [netbox_tag.test.name] } resource "netbox_ip_address" "test_list_1" { ip_address = "%s" virtual_machine_interface_id = netbox_interface.test.id status = "active" tags = [netbox_tag.test.name] } data "netbox_ip_addresses" "test_list" { depends_on = [netbox_ip_address.test_list_0, netbox_ip_address.test_list_1] filter { name = "ip_address" value = "%s" } }`, testIP0, testIP1, testIP0), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_ip_addresses.test_list", "ip_addresses.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_ip_addresses.test_list", "ip_addresses.0.ip_address", "netbox_ip_address.test_list_0", "ip_address"), ), }, }, }) } func TestAccNetboxIpAddressesDataSource_filter2(t *testing.T) { testSlug := "ipam_ipaddrs_ds_filter_role" testName := testAccGetTestName(testSlug) testIP0 := "203.0.113.1/24" testIP1 := "203.0.113.2/24" resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_address" "test_list_0" { ip_address = "%s" virtual_machine_interface_id = netbox_interface.test.id status = "active" role = "vip" tags = [netbox_tag.test.name] } resource "netbox_ip_address" "test_list_1" { ip_address = "%s" virtual_machine_interface_id = netbox_interface.test.id status = "active" role = "vrrp" tags = [netbox_tag.test.name] } data "netbox_ip_addresses" "test_list" { depends_on = [netbox_ip_address.test_list_0, netbox_ip_address.test_list_1] filter { name = "role" value = "vip" } }`, testIP0, testIP1), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_ip_addresses.test_list", "ip_addresses.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_ip_addresses.test_list", "ip_addresses.0.ip_address", "netbox_ip_address.test_list_0", "ip_address"), ), }, }, }) } func TestAccNetboxIpAddressesDataSource_filter_parent_prefix(t *testing.T) { testSlug := "ipam_ipaddrs_ds_filter_prefix" testName := testAccGetTestName(testSlug) testPrefix1 := "203.0.113.0/24" testIP0 := "203.0.113.1/24" testIP1 := "203.0.200.1/24" resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_prefix" "testv4" { prefix = "%s" status = "active" } resource "netbox_ip_address" "test_list_0" { ip_address = "%s" virtual_machine_interface_id = netbox_interface.test.id status = "active" tags = [netbox_tag.test.name] } resource "netbox_ip_address" "test_list_1" { ip_address = "%s" virtual_machine_interface_id = netbox_interface.test.id status = "active" tags = [netbox_tag.test.name] } data "netbox_ip_addresses" "test_list" { depends_on = [netbox_ip_address.test_list_0, netbox_ip_address.test_list_1] filter { name = "parent_prefix" value = "%s" } }`, testPrefix1, testIP0, testIP1, testPrefix1), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_ip_addresses.test_list", "ip_addresses.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_ip_addresses.test_list", "ip_addresses.0.ip_address", "netbox_ip_address.test_list_0", "ip_address"), ), }, }, }) } func TestAccNetboxIpAddressesDataSource_multiple(t *testing.T) { testSlug := "ipam_ipaddrs_ds_multiple" testIP0 := "203.0.113.1/24" testIP1 := "203.0.113.2/24" testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_address" "test_list_0" { ip_address = "%s" virtual_machine_interface_id = netbox_interface.test.id status = "active" tags = [netbox_tag.test.name] } resource "netbox_ip_address" "test_list_1" { ip_address = "%s" virtual_machine_interface_id = netbox_interface.test.id status = "active" tags = [netbox_tag.test.name] } data "netbox_ip_addresses" "test_list" { depends_on = [netbox_ip_address.test_list_0, netbox_ip_address.test_list_1] filter { name = "vm_interface_id" value = netbox_interface.test.id } }`, testIP0, testIP1), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_ip_addresses.test_list", "ip_addresses.#", "2"), resource.TestCheckResourceAttrPair("data.netbox_ip_addresses.test_list", "ip_addresses.0.ip_address", "netbox_ip_address.test_list_0", "ip_address"), resource.TestCheckResourceAttrPair("data.netbox_ip_addresses.test_list", "ip_addresses.1.ip_address", "netbox_ip_address.test_list_1", "ip_address"), ), }, }, }) } func TestAccNetboxIpAddressesDataSource_flattenTenant(t *testing.T) { testSlug := "ipam_ipaddrs_ds_flattenTenant" testIP0 := "203.0.113.10/24" testIP1 := "203.0.113.20/24" testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_address" "test_list_0" { ip_address = "%s" virtual_machine_interface_id = netbox_interface.test.id status = "active" tags = [netbox_tag.test.name] tenant_id = netbox_tenant.test.id } resource "netbox_ip_address" "test_list_1" { ip_address = "%s" virtual_machine_interface_id = netbox_interface.test.id status = "active" tags = [netbox_tag.test.name] tenant_id = netbox_tenant.test.id } data "netbox_ip_addresses" "test_list" { depends_on = [netbox_ip_address.test_list_0, netbox_ip_address.test_list_1] filter { name = "vm_interface_id" value = netbox_interface.test.id } }`, testIP0, testIP1), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_ip_addresses.test_list", "ip_addresses.#", "2"), resource.TestCheckResourceAttr("data.netbox_ip_addresses.test_list", "ip_addresses.0.tenant.#", "1"), resource.TestCheckResourceAttr("data.netbox_ip_addresses.test_list", "ip_addresses.1.tenant.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_ip_addresses.test_list", "ip_addresses.0.tenant.0.name", "netbox_tenant.test", "name"), resource.TestCheckResourceAttrPair("data.netbox_ip_addresses.test_list", "ip_addresses.1.tenant.0.name", "netbox_tenant.test", "name"), ), }, }, }) } func testAccNetboxIPAddressesDataSourceDependenciesMany(testName string) string { return testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test" { name = "%s" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id }`, testName) + fmt.Sprintf(` resource "netbox_interface" "test" { name = "test" virtual_machine_id = netbox_virtual_machine.test.id } resource "netbox_ip_address" "test" { count = 51 ip_address = "10.11.12.${count.index}/32" status = "active" virtual_machine_interface_id = netbox_interface.test.id dns_name = "%s" } `, testName) } func TestAccNetboxIpAddressessDataSource_many(t *testing.T) { testSlug := "ip_adrs_ds_many" testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxIPAddressesDataSourceDependenciesMany(testName) + fmt.Sprintf(` data "netbox_ip_addresses" "test" { depends_on = [netbox_ip_address.test] filter { name = "dns_name" value = "%s" } }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_ip_addresses.test", "ip_addresses.#", "51"), ), }, { Config: testAccNetboxIPAddressesDataSourceDependenciesMany(testName) + `data "netbox_ip_addresses" "test" { depends_on = [netbox_ip_address.test] limit = 2 }`, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_ip_addresses.test", "ip_addresses.#", "2"), ), }, }, }) } func TestAccNetboxIpAddressesDataSource_filter_tags(t *testing.T) { testSlug := "ipam_ipaddrs_ds_filter_tags" testTag := "default-gw" testName := testAccGetTestName(testSlug) testIP0 := "203.0.113.1/24" testIP1 := "203.0.113.2/24" testIP2 := "203.0.113.3/24" resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_tag" "gw_tag" { name = "%s" } resource "netbox_ip_address" "test_list_0" { ip_address = "%s" virtual_machine_interface_id = netbox_interface.test.id status = "active" tags = [netbox_tag.test.name] } resource "netbox_ip_address" "test_list_1" { ip_address = "%s" virtual_machine_interface_id = netbox_interface.test.id status = "active" tags = [netbox_tag.test.name, netbox_tag.gw_tag.name] } resource "netbox_ip_address" "test_list_2" { ip_address = "%s" virtual_machine_interface_id = netbox_interface.test.id status = "active" tags = [netbox_tag.test.name] } data "netbox_ip_addresses" "test_list" { depends_on = [netbox_ip_address.test_list_0, netbox_ip_address.test_list_1, netbox_ip_address.test_list_2] filter { name = "tag" value = "%s" } }`, testTag, testIP0, testIP1, testIP2, testTag), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_ip_addresses.test_list", "ip_addresses.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_ip_addresses.test_list", "ip_addresses.0.ip_address", "netbox_ip_address.test_list_1", "ip_address"), ), }, }, }) } func TestAccNetboxIpAddressesDataSource_filter_description(t *testing.T) { testSlug := "ipam_ipaddrs_ds_filter_description" testName := testAccGetTestName(testSlug) testIP0 := "203.0.113.1/24" testIP1 := "203.0.113.2/24" resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_address" "test_list_0" { ip_address = "%s" virtual_machine_interface_id = netbox_interface.test.id status = "active" role = "vip" description = "test 1" } resource "netbox_ip_address" "test_list_1" { ip_address = "%s" virtual_machine_interface_id = netbox_interface.test.id status = "active" role = "vrrp" description = "test 2" } data "netbox_ip_addresses" "test_list" { depends_on = [netbox_ip_address.test_list_0, netbox_ip_address.test_list_1] filter { name = "description" value = "test 1" } }`, testIP0, testIP1), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_ip_addresses.test_list", "ip_addresses.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_ip_addresses.test_list", "ip_addresses.0.ip_address", "netbox_ip_address.test_list_0", "ip_address"), ), }, }, }) } func TestAccNetboxIpAddressesDataSource_filter_customFields(t *testing.T) { testSlug := "ipam_ipaddrs_ds_filter_cf" testName := testAccGetTestName(testSlug) testIP0 := "203.0.113.1/24" testIP1 := "203.0.113.2/24" resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_custom_field" "test" { name = "%s" type = "text" weight = 100 content_types = ["ipam.ipaddress"] } resource "netbox_ip_address" "test_list_0" { ip_address = "%s" virtual_machine_interface_id = netbox_interface.test.id status = "active" custom_fields = { "${netbox_custom_field.test.name}" = "match" } } resource "netbox_ip_address" "test_list_1" { ip_address = "%s" virtual_machine_interface_id = netbox_interface.test.id status = "active" custom_fields = { "${netbox_custom_field.test.name}" = "skip" } } data "netbox_ip_addresses" "test_list" { depends_on = [netbox_ip_address.test_list_0, netbox_ip_address.test_list_1] custom_fields = { "${netbox_custom_field.test.name}" = "match" } }`, testSlug, testIP0, testIP1), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_ip_addresses.test_list", "ip_addresses.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_ip_addresses.test_list", "ip_addresses.0.ip_address", "netbox_ip_address.test_list_0", "ip_address"), resource.TestCheckResourceAttr("data.netbox_ip_addresses.test_list", fmt.Sprintf("ip_addresses.0.custom_fields.%s", testSlug), "match"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_ip_range.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxIPRange() *schema.Resource { filterAtLeastOneOf := []string{ "contains", "family", "vrf_id", "tenant_id", "status", "role_id", "description", "tag", } return &schema.Resource{ Read: dataSourceNetboxIPRangeRead, Description: `:meta:subcategory:IP Address Management (IPAM):`, Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "start_address": { Type: schema.TypeString, Computed: true, }, "end_address": { Type: schema.TypeString, Computed: true, }, "contains": { Type: schema.TypeString, Optional: true, AtLeastOneOf: filterAtLeastOneOf, ValidateFunc: validation.IsCIDR, }, "family": { Type: schema.TypeInt, Optional: true, Computed: true, AtLeastOneOf: filterAtLeastOneOf, ValidateFunc: validation.IntInSlice([]int{4, 6}), Description: "The IP family of the IP range. One of 4 or 6", }, "vrf_id": { Type: schema.TypeInt, Optional: true, AtLeastOneOf: filterAtLeastOneOf, }, "tenant_id": { Type: schema.TypeInt, Optional: true, AtLeastOneOf: filterAtLeastOneOf, }, "status": { Type: schema.TypeString, Optional: true, AtLeastOneOf: filterAtLeastOneOf, }, "role_id": { Type: schema.TypeInt, Optional: true, Computed: true, AtLeastOneOf: filterAtLeastOneOf, }, "description": { Type: schema.TypeString, Optional: true, Computed: true, AtLeastOneOf: filterAtLeastOneOf, Description: "Description to include in the data source filter.", }, "tag": { Type: schema.TypeString, Optional: true, AtLeastOneOf: filterAtLeastOneOf, Description: "Tag to include in the data source filter (must match the tag's slug).", }, "tag__n": { Type: schema.TypeString, Optional: true, Description: `Tag to exclude from the data source filter (must match the tag's slug). Refer to [Netbox's documentation](https://netboxlabs.com/docs/netbox/reference/filtering/#lookup-expressions) for more information on available lookup expressions.`, }, "tags": tagsSchemaRead, }, } } func dataSourceNetboxIPRangeRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := ipam.NewIpamIPRangesListParams() limit := int64(2) // Limit of 2 is enough params.Limit = &limit if contains, ok := d.Get("contains").(string); ok && contains != "" { params.Contains = &contains } if family, ok := d.Get("family").(int); ok && family != 0 { familyFloat := float64(family) params.Family = &familyFloat } if vrfID, ok := d.Get("vrf_id").(int); ok && vrfID != 0 { // Note that vrf_id is a string pointer in the netbox filter, but we use a number in the provider params.VrfID = strToPtr(strconv.Itoa(vrfID)) } if tenantID, ok := d.Get("tenant_id").(int); ok && tenantID != 0 { // Note that tenant_id is a string pointer in the netbox filter, but we use a number in the provider params.TenantID = strToPtr(strconv.Itoa(tenantID)) } if status, ok := d.Get("status").(string); ok && status != "" { params.Status = &status } if roleID, ok := d.Get("role_id").(int); ok && roleID != 0 { params.RoleID = strToPtr(strconv.Itoa(roleID)) } if description, ok := d.Get("description").(string); ok && description != "" { params.Description = &description } if tag, ok := d.Get("tag").(string); ok && tag != "" { params.Tag = []string{tag} //TODO: switch schema to list } if tagn, ok := d.Get("tag__n").(string); ok && tagn != "" { params.Tagn = &tagn } res, err := api.Ipam.IpamIPRangesList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than IP range returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no IP range found matching filter") } result := res.GetPayload().Results[0] d.Set("id", result.ID) d.Set("start_address", result.StartAddress) d.Set("end_address", result.EndAddress) d.Set("status", result.Status.Value) d.Set("description", result.Description) d.Set("family", int(*result.Family.Value)) d.Set("tags", getTagListFromNestedTagList(result.Tags)) if result.Role != nil { d.Set("role_id", result.Role.ID) } if result.Vrf != nil { d.Set("vrf_id", result.Vrf.ID) } if result.Tenant != nil { d.Set("tenant_id", result.Tenant.ID) } d.SetId(strconv.FormatInt(result.ID, 10)) return nil } ================================================ FILE: netbox/data_source_netbox_ip_range_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxIpRangeDataSource_basic(t *testing.T) { testv4StartIP := "10.0.0.101/24" testv4EndIP := "10.0.0.150/24" testv6StartIP := "2001:db8:1:1::/112" testv6EndIP := "2001:db8:1:1::ffff/112" testSlug := "ip_range_ds_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_vrf" "test" { name = "%[1]s_vrf" } resource "netbox_tenant" "test" { name = "%[1]s_tenant" } resource "netbox_ipam_role" "test" { name = "%[1]s_role" } resource "netbox_tag" "test" { name = "%[1]s_role" } resource "netbox_ip_range" "testv4" { start_address = "%[2]s" end_address = "%[3]s" vrf_id = netbox_vrf.test.id tenant_id = netbox_tenant.test.id role_id = netbox_ipam_role.test.id description = "%[1]s_description_testv4" status = "active" tags = [netbox_tag.test.name] } resource "netbox_ip_range" "testv6" { start_address = "%[4]s" end_address = "%[5]s" description = "%[1]s_description_testv6" status = "reserved" } data "netbox_ip_range" "by_contains" { depends_on = [netbox_ip_range.testv4] contains = "%[2]s" family = 4 } data "netbox_ip_range" "by_family" { depends_on = [netbox_ip_range.testv6] family = 6 } data "netbox_ip_range" "by_vrf_id" { depends_on = [netbox_ip_range.testv4] vrf_id = netbox_vrf.test.id family = 4 } data "netbox_ip_range" "by_tenant_id" { depends_on = [netbox_ip_range.testv4] tenant_id = netbox_tenant.test.id family = 4 } data "netbox_ip_range" "by_status" { depends_on = [netbox_ip_range.testv6] status = "reserved" } data "netbox_ip_range" "by_role_id" { depends_on = [netbox_ip_range.testv4] role_id = netbox_ipam_role.test.id family = 4 } data "netbox_ip_range" "by_tag" { depends_on = [netbox_ip_range.testv4] tag = netbox_tag.test.name } data "netbox_ip_range" "by_description" { depends_on = [netbox_ip_range.testv4] description = "%[1]s_description_testv4" }`, testName, testv4StartIP, testv4EndIP, testv6StartIP, testv6EndIP), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_ip_range.by_contains", "id", "netbox_ip_range.testv4", "id"), resource.TestCheckResourceAttrPair("data.netbox_ip_range.by_family", "id", "netbox_ip_range.testv6", "id"), resource.TestCheckResourceAttrPair("data.netbox_ip_range.by_vrf_id", "id", "netbox_ip_range.testv4", "id"), resource.TestCheckResourceAttrPair("data.netbox_ip_range.by_tenant_id", "id", "netbox_ip_range.testv4", "id"), resource.TestCheckResourceAttrPair("data.netbox_ip_range.by_status", "id", "netbox_ip_range.testv6", "id"), resource.TestCheckResourceAttrPair("data.netbox_ip_range.by_role_id", "id", "netbox_ip_range.testv4", "id"), resource.TestCheckResourceAttrPair("data.netbox_ip_range.by_description", "id", "netbox_ip_range.testv4", "id"), resource.TestCheckResourceAttrPair("data.netbox_ip_range.by_tag", "id", "netbox_ip_range.testv4", "id"), ), ExpectNonEmptyPlan: false, }, }, }) } ================================================ FILE: netbox/data_source_netbox_ip_ranges.go ================================================ package netbox import ( "errors" "fmt" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxIPRanges() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxIPRangesRead, Description: `:meta:subcategory:IP Address Management (IPAM):`, Schema: map[string]*schema.Schema{ "filter": { Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "value": { Type: schema.TypeString, Required: true, }, }, }, }, "limit": { Type: schema.TypeInt, Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), Default: 0, }, "ip_ranges": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, "created": { Type: schema.TypeString, Computed: true, }, "last_updated": { Type: schema.TypeString, Computed: true, }, "custom_fields": { Type: schema.TypeMap, Computed: true, }, "start_address": { Type: schema.TypeString, Computed: true, }, "end_address": { Type: schema.TypeString, Computed: true, }, "address_family": { Type: schema.TypeString, Computed: true, }, "status": { Type: schema.TypeString, Computed: true, }, "role": { Type: schema.TypeString, Computed: true, }, "tenant": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, "slug": { Type: schema.TypeString, Computed: true, }, }, }, }, "tags": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, "display": { Type: schema.TypeString, Computed: true, }, "slug": { Type: schema.TypeString, Computed: true, }, }, }, }, }, }, }, }, } } func dataSourceNetboxIPRangesRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := ipam.NewIpamIPRangesListParams() // Get user limit var userLimit int64 = 0 if limitValue, ok := d.GetOk("limit"); ok { userLimit = int64(limitValue.(int)) } if filter, ok := d.GetOk("filter"); ok { var filterParams = filter.(*schema.Set) var tags []string for _, f := range filterParams.List() { k := f.(map[string]interface{})["name"] v := f.(map[string]interface{})["value"] vString := v.(string) switch k { case "contains": params.Contains = &vString case "start_address": params.StartAddress = &vString case "end_address": params.EndAddress = &vString case "role": params.Role = &vString case "status": params.Status = &vString case "vrf": params.Vrf = &vString case "tenant": params.Tenant = &vString case "tag": tags = append(tags, vString) params.Tag = tags default: return fmt.Errorf("'%s' is not a supported filter parameter", k) } } } // Fetch all pages with pagination paginationHelper := NewPaginationHelper(userLimit) var allIPRanges []*models.IPRange pageSize := paginationHelper.GetPageSize() for { currentOffset := paginationHelper.CurrentOffset() params.Limit = &pageSize params.Offset = ¤tOffset res, err := api.Ipam.IpamIPRangesList(params, nil) if err != nil { return fmt.Errorf("failed to fetch IP ranges at offset %d: %w", currentOffset, err) } payload := res.GetPayload() allIPRanges = append(allIPRanges, payload.Results...) if len(payload.Results) == 0 { break } if !paginationHelper.ShouldContinuePaging(int64(len(allIPRanges)), payload.Next) { break } paginationHelper.Advance(int64(len(payload.Results))) } // Trim to user limit if specified trimmedCount := paginationHelper.TrimToLimit(len(allIPRanges)) filteredIPRanges := allIPRanges[:trimmedCount] if len(filteredIPRanges) == 0 { return errors.New("no result") } var s []map[string]interface{} for _, v := range filteredIPRanges { var mapping = make(map[string]interface{}) mapping["id"] = v.ID mapping["description"] = v.Description mapping["created"] = v.Created.String() mapping["last_updated"] = v.LastUpdated.String() mapping["custom_fields"] = flattenCustomFields(v.CustomFields) mapping["start_address"] = v.StartAddress mapping["end_address"] = v.EndAddress mapping["address_family"] = v.Family.Label mapping["status"] = v.Status.Value mapping["tenant"] = flattenTenant(v.Tenant) if v.Vrf != nil { mapping["vrf_id"] = v.Vrf.ID } if v.Role != nil { mapping["role_id"] = v.Role.ID } mapping["description"] = v.Description var stags []map[string]interface{} for _, t := range v.Tags { var tagmapping = make(map[string]interface{}) tagmapping["name"] = t.Name tagmapping["display"] = t.Display tagmapping["slug"] = t.Slug tagmapping["id"] = t.ID stags = append(stags, tagmapping) } mapping["tags"] = stags s = append(s, mapping) } d.SetId(id.UniqueId()) return d.Set("ip_ranges", s) } ================================================ FILE: netbox/data_source_netbox_ip_ranges_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxIpRangesDataSource_basic(t *testing.T) { testStartIP := "11.0.0.101/24" testEndIP := "11.0.0.150/24" resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_ip_range" "test" { start_address = "%[1]s" end_address = "%[2]s" } data "netbox_ip_ranges" "test" { depends_on = [netbox_ip_range.test] }`, testStartIP, testEndIP), // This snippet sometimes returns things from other tests, yielding a different number than the expected 1 // The check functions are now removed so this does no longer happen // Check: resource.ComposeTestCheckFunc( // resource.TestCheckResourceAttr("data.netbox_ip_ranges.test", "ip_ranges.#", "1"), // ), }, }, }) } func TestAccNetboxIpRangesDataSource_filter(t *testing.T) { testSlug := "ipam_ipranges_ds_filter" testName := testAccGetTestName(testSlug) testStartIP0 := "12.0.0.101/24" testEndIP0 := "12.0.0.150/24" testStartIP1 := "13.0.0.101/24" testEndIP1 := "13.0.0.150/24" resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_range" "test_range_0" { start_address = "%[1]s" end_address = "%[2]s" } resource "netbox_ip_range" "test_range_1" { start_address = "%[3]s" end_address = "%[4]s" } data "netbox_ip_ranges" "test_list" { depends_on = [netbox_ip_range.test_range_0, netbox_ip_range.test_range_1] filter { name = "start_address" value = "%[1]s" } }`, testStartIP0, testEndIP0, testStartIP1, testEndIP1), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_ip_ranges.test_list", "ip_ranges.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_ip_ranges.test_list", "ip_ranges.0.start_address", "netbox_ip_range.test_range_0", "start_address"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_ipam_role.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxIPAMRole() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxIPAMRoleRead, Description: `:meta:subcategory:IP Address Management (IPAM):`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Computed: true, }, "weight": { Type: schema.TypeInt, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, }, } } func dataSourceNetboxIPAMRoleRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) params := ipam.NewIpamRolesListParams() params.Name = &name limit := int64(2) // Limit of 2 is enough params.Limit = &limit res, err := api.Ipam.IpamRolesList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one ipam role returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no ipam role found matching filter") } result := res.GetPayload().Results[0] d.SetId(strconv.FormatInt(result.ID, 10)) d.Set("slug", result.Slug) d.Set("name", result.Name) if result.Weight != nil { d.Set("weight", result.Weight) } if result.Description != "" { d.Set("description", result.Description) } return nil } ================================================ FILE: netbox/data_source_netbox_ipam_role_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxIPAMRoleDataSource_basic(t *testing.T) { testSlug := "ipamrole_ds_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_ipam_role" "test" { name = "%[1]s" } data "netbox_ipam_role" "test" { depends_on = [netbox_ipam_role.test] name = "%[1]s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_ipam_role.test", "id", "netbox_ipam_role.test", "id"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_location.go ================================================ package netbox import ( "errors" "fmt" "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxLocation() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxLocationRead, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Optional: true, }, "slug": { Type: schema.TypeString, Optional: true, }, "id": { Type: schema.TypeString, Optional: true, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, "tenant_id": { Type: schema.TypeInt, Computed: true, }, "status": { Type: schema.TypeString, Computed: true, }, "site_id": { Type: schema.TypeInt, Optional: true, Computed: true, }, "parent_id": { Type: schema.TypeInt, Optional: true, Computed: true, }, }, } } func dataSourceNetboxLocationRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := dcim.NewDcimLocationsListParams() params.Limit = int64ToPtr(2) if name, ok := d.Get("name").(string); ok && name != "" { params.SetName(&name) } if slug, ok := d.Get("slug").(string); ok && slug != "" { params.SetSlug(&slug) } if id, ok := d.Get("id").(string); ok && id != "0" { params.SetID(&id) } if site, ok := d.Get("site_id").(int); ok && site != 0 { siteID := fmt.Sprintf("%v", site) params.SetSiteID(&siteID) } if parent, ok := d.Get("parent_id").(int); ok && parent != 0 { parentID := fmt.Sprintf("%v", parent) params.SetParentID(&parentID) } res, err := api.Dcim.DcimLocationsList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one location returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no location found matching filter") } location := res.GetPayload().Results[0] d.SetId(strconv.FormatInt(location.ID, 10)) d.Set("description", location.Description) d.Set("name", location.Name) d.Set("site_id", location.Site.ID) d.Set("slug", location.Slug) if location.Parent != nil { d.Set("parent_id", location.Parent.ID) } if location.Status != nil { d.Set("status", location.Status.Value) } if location.Tenant != nil { d.Set("tenant_id", location.Tenant.ID) } return nil } ================================================ FILE: netbox/data_source_netbox_location_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxLocationDataSource_basic(t *testing.T) { testSlug := "location_ds_basic" testName := testAccGetTestName(testSlug) testNameSub := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_site" "test" { name = "%[1]s" } resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_location" "test" { name = "%[1]s" description = "my-description" site_id = netbox_site.test.id tenant_id = netbox_tenant.test.id } resource "netbox_location" "test_sub" { name = "%[2]s" description = "my-description" site_id = netbox_site.test.id tenant_id = netbox_tenant.test.id parent_id = netbox_location.test.id } data "netbox_location" "by_name" { name = netbox_location.test.name } data "netbox_location" "by_name_and_site" { name = netbox_location.test.name site_id = netbox_site.test.id } data "netbox_location" "sub_by_name" { name = netbox_location.test_sub.name } data "netbox_location" "by_id" { id = netbox_location.test.id } data "netbox_location" "by_slug" { slug = netbox_location.test.slug }`, testName, testNameSub), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_location.by_name", "id", "netbox_location.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_location.by_id", "id", "netbox_location.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_location.by_slug", "id", "netbox_location.test", "id"), resource.TestCheckResourceAttr("data.netbox_location.by_name", "name", testName), resource.TestCheckResourceAttrPair("data.netbox_location.by_name", "id", "netbox_location.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_location.by_name", "description", "netbox_location.test", "description"), resource.TestCheckResourceAttrPair("data.netbox_location.by_name", "site_id", "netbox_location.test", "site_id"), resource.TestCheckResourceAttrPair("data.netbox_location.by_name_and_site", "site_id", "netbox_location.test", "site_id"), resource.TestCheckResourceAttrPair("data.netbox_location.by_name", "tenant_id", "netbox_location.test", "tenant_id"), resource.TestCheckResourceAttrPair("data.netbox_location.sub_by_name", "parent_id", "netbox_location.test", "id"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_locations.go ================================================ package netbox import ( "fmt" "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxLocations() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxLocationsRead, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):`, Schema: map[string]*schema.Schema{ "filter": { Type: schema.TypeSet, Optional: true, Description: "A list of filter to apply to the API query when requesting locations.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, Description: "The name of the field to filter on. Supported fields are: .", }, "value": { Type: schema.TypeString, Required: true, Description: "The value to pass to the specified filter.", }, }, }, }, "tags": { Type: schema.TypeSet, Elem: &schema.Schema{ Type: schema.TypeString, }, Optional: true, Description: "A list of tags to filter on.", }, "limit": { Type: schema.TypeInt, Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), Default: 0, Description: "The limit of objects to return from the API lookup.", }, "locations": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeString, Computed: true, }, "name": { Type: schema.TypeString, Optional: true, }, "slug": { Type: schema.TypeString, Optional: true, }, "description": { Type: schema.TypeString, Computed: true, }, "facility": { Type: schema.TypeString, Computed: true, }, "tenant_id": { Type: schema.TypeInt, Computed: true, }, "status": { Type: schema.TypeString, Computed: true, }, "site_id": { Type: schema.TypeInt, Computed: true, }, "parent_id": { Type: schema.TypeInt, Computed: true, }, }, }, }, }, } } func dataSourceNetboxLocationsRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := dcim.NewDcimLocationsListParams() // Get user limit var userLimit int64 = 0 if limitValue, ok := d.GetOk("limit"); ok { userLimit = int64(limitValue.(int)) } if filter, ok := d.GetOk("filter"); ok { var filterParams = filter.(*schema.Set) for _, f := range filterParams.List() { k := f.(map[string]interface{})["name"] v := f.(map[string]interface{})["value"] vString := v.(string) switch k { case "name": params.Name = &vString case "slug": params.Slug = &vString case "site": params.Site = &vString case "site_id": params.SiteID = &vString case "parent_id": params.ParentID = &vString case "tenant": params.Tenant = &vString case "tenant_id": params.TenantID = &vString case "status": params.Status = &vString default: return fmt.Errorf("'%s' is not a supported filter parameter", k) } } } if tags, ok := d.GetOk("tags"); ok { tagSet := tags.(*schema.Set) for _, tag := range tagSet.List() { tagV := tag.(string) params.Tag = append(params.Tag, tagV) } } // Fetch all pages with pagination paginationHelper := NewPaginationHelper(userLimit) var allLocations []*models.Location pageSize := paginationHelper.GetPageSize() for { currentOffset := paginationHelper.CurrentOffset() params.Limit = &pageSize params.Offset = ¤tOffset res, err := api.Dcim.DcimLocationsList(params, nil) if err != nil { return fmt.Errorf("failed to fetch locations at offset %d: %w", currentOffset, err) } payload := res.GetPayload() allLocations = append(allLocations, payload.Results...) if len(payload.Results) == 0 { break } if !paginationHelper.ShouldContinuePaging(int64(len(allLocations)), payload.Next) { break } paginationHelper.Advance(int64(len(payload.Results))) } // Trim to user limit if specified trimmedCount := paginationHelper.TrimToLimit(len(allLocations)) filteredLocations := allLocations[:trimmedCount] var s []map[string]any for _, v := range filteredLocations { var mapping = make(map[string]any) mapping["id"] = strconv.FormatInt(v.ID, 10) mapping["name"] = v.Name mapping["slug"] = v.Slug mapping["site_id"] = v.Site.ID mapping["description"] = v.Description mapping["facility"] = v.Facility if v.Parent != nil { mapping["parent_id"] = v.Parent.ID } if v.Status != nil { mapping["status"] = v.Status.Value } if v.Tenant != nil { mapping["tenant_id"] = v.Tenant.ID } s = append(s, mapping) } d.SetId(id.UniqueId()) return d.Set("locations", s) } ================================================ FILE: netbox/data_source_netbox_locations_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxLocationsDataSource_basic(t *testing.T) { testSlug := "location_ds_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_site" "test" { name = "%[1]s" } resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_location" "test" { name = "%[1]s" description = "my-description" facility = "Building B" site_id = netbox_site.test.id tenant_id = netbox_tenant.test.id tags = [netbox_tag.test.slug] } data "netbox_locations" "by_name" { filter { name = "name" value = netbox_location.test.name } } data "netbox_locations" "no_match" { filter { name = "name" value = "non-existent" } } data "netbox_locations" "by_site_slug" { filter { name = "site" value = netbox_site.test.slug } depends_on = [netbox_location.test] } data "netbox_locations" "by_site_id" { filter { name = "site_id" value = netbox_site.test.id } depends_on = [netbox_location.test] } data "netbox_locations" "by_tenant_slug" { filter { name = "tenant" value = netbox_tenant.test.slug } depends_on = [netbox_location.test] } data "netbox_locations" "by_tenant_id" { filter { name = "tenant_id" value = netbox_tenant.test.id } depends_on = [netbox_location.test] } data "netbox_locations" "by_tags" { tags = [netbox_tag.test.slug] depends_on = [netbox_location.test] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_locations.by_name", "locations.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_locations.by_name", "locations.0.name", "netbox_location.test", "name"), resource.TestCheckResourceAttrPair("data.netbox_locations.by_name", "locations.0.site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_locations.by_name", "locations.0.tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttr("data.netbox_locations.by_name", "locations.0.parent_id", "0"), resource.TestCheckResourceAttr("data.netbox_locations.by_name", "locations.0.description", "my-description"), resource.TestCheckResourceAttr("data.netbox_locations.by_name", "locations.0.facility", "Building B"), resource.TestCheckResourceAttr("data.netbox_locations.no_match", "locations.#", "0"), resource.TestCheckResourceAttr("data.netbox_locations.by_site_slug", "locations.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_locations.by_site_slug", "locations.0.name", "netbox_location.test", "name"), resource.TestCheckResourceAttr("data.netbox_locations.by_site_id", "locations.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_locations.by_site_id", "locations.0.name", "netbox_location.test", "name"), resource.TestCheckResourceAttr("data.netbox_locations.by_tenant_slug", "locations.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_locations.by_tenant_slug", "locations.0.name", "netbox_location.test", "name"), resource.TestCheckResourceAttr("data.netbox_locations.by_tenant_id", "locations.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_locations.by_tenant_id", "locations.0.name", "netbox_location.test", "name"), resource.TestCheckResourceAttr("data.netbox_locations.by_tags", "locations.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_locations.by_tags", "locations.0.name", "netbox_location.test", "name"), ), }, }, }) } func TestAccNetboxLocationsDataSource_multiple(t *testing.T) { testSlug := "location_ds_multiple" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_site" "test" { name = "%[1]s" } resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_tag" "test1" { name = "%[1]s_1" } resource "netbox_tag" "test2" { name = "%[1]s_2" } resource "netbox_location" "test1" { name = "%[1]s_1" site_id = netbox_site.test.id tenant_id = netbox_tenant.test.id tags = [netbox_tag.test1.slug] } resource "netbox_location" "test2" { name = "%[1]s_2" site_id = netbox_site.test.id tenant_id = netbox_tenant.test.id tags = [netbox_tag.test2.slug] } data "netbox_locations" "by_site" { filter { name = "site" value = netbox_site.test.name } depends_on = [netbox_location.test1, netbox_location.test2] } data "netbox_locations" "by_tag" { tags = [netbox_tag.test1.slug] depends_on = [netbox_location.test1, netbox_location.test2] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_locations.by_site", "locations.#", "2"), resource.TestCheckResourceAttr("data.netbox_locations.by_tag", "locations.#", "1"), ), }, }, }) } func TestAccNetboxLocationsDataSource_sublocations(t *testing.T) { testSlug := "sublocations_ds_multiple" testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_site" "test" { name = "%[1]s" } resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_location" "parent" { name = "%[1]s_p" site_id = netbox_site.test.id tenant_id = netbox_tenant.test.id } resource "netbox_location" "test1" { name = "%[1]s_1" parent_id = netbox_location.parent.id site_id = netbox_site.test.id tenant_id = netbox_tenant.test.id } resource "netbox_location" "test2" { name = "%[1]s_2" parent_id = netbox_location.parent.id site_id = netbox_site.test.id tenant_id = netbox_tenant.test.id } data "netbox_locations" "by_parent" { filter { name = "parent_id" value = netbox_location.parent.id } depends_on = [netbox_location.test1, netbox_location.test2] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_locations.by_parent", "locations.#", "2"), resource.TestCheckResourceAttrPair("data.netbox_locations.by_parent", "locations.0.parent_id", "netbox_location.parent", "id"), resource.TestCheckResourceAttrPair("data.netbox_locations.by_parent", "locations.1.parent_id", "netbox_location.parent", "id"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_platform.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxPlatform() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxPlatformRead, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Computed: true, }, "manufacturer_id": { Type: schema.TypeInt, Optional: true, }, }, } } func dataSourceNetboxPlatformRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) params := dcim.NewDcimPlatformsListParams() params.Name = &name limit := int64(2) // Limit of 2 is enough params.Limit = &limit res, err := api.Dcim.DcimPlatformsList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one platform returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no platform found matching filter") } result := res.GetPayload().Results[0] d.SetId(strconv.FormatInt(result.ID, 10)) d.Set("name", result.Name) d.Set("slug", result.Slug) if result.Manufacturer != nil { d.Set("manufacturer_id", result.Manufacturer.ID) } return nil } ================================================ FILE: netbox/data_source_netbox_platform_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxPlatformDataSource_basic(t *testing.T) { testSlug := "pltf_ds_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_platform" "test" { name = "%[1]s" } data "netbox_platform" "test" { depends_on = [netbox_platform.test] name = "%[1]s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_platform.test", "id", "netbox_platform.test", "id"), ), }, }, }) } func TestAccNetboxPlatformDataSource_manufacturer(t *testing.T) { testSlug := "pltf_ds_manufacturer" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_platform" "test" { name = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } data "netbox_platform" "test" { depends_on = [netbox_platform.test] name = "%[1]s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_platform.test", "id", "netbox_platform.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_platform.test", "manufacturer_id", "netbox_manufacturer.test", "id"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_prefix.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxPrefix() *schema.Resource { filterAtLeastOneOf := []string{ "description", "family", "prefix", "vlan_vid", "vrf_id", "vlan_id", "tenant_id", "site_id", "role_id", "cidr", "custom_fields", "tag", "status", } customFieldsFilterSchema := *customFieldsSchema customFieldsFilterSchema.AtLeastOneOf = filterAtLeastOneOf return &schema.Resource{ Read: dataSourceNetboxPrefixRead, Description: `:meta:subcategory:IP Address Management (IPAM):`, Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "cidr": { Type: schema.TypeString, Optional: true, Deprecated: "The `cidr` parameter is deprecated in favor of the canonical `prefix` attribute.", ConflictsWith: []string{"prefix"}, ValidateFunc: validation.IsCIDR, AtLeastOneOf: filterAtLeastOneOf, }, customFieldsKey: &customFieldsFilterSchema, "description": { Type: schema.TypeString, Optional: true, Computed: true, AtLeastOneOf: filterAtLeastOneOf, Description: "Description to include in the data source filter.", }, "family": { Type: schema.TypeInt, Optional: true, Computed: true, AtLeastOneOf: filterAtLeastOneOf, ValidateFunc: validation.IntInSlice([]int{4, 6}), Description: "The IP family of the prefix. One of 4 or 6", }, "role_id": { Type: schema.TypeInt, Optional: true, Computed: true, AtLeastOneOf: filterAtLeastOneOf, }, "prefix": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.IsCIDR, ConflictsWith: []string{"cidr"}, AtLeastOneOf: filterAtLeastOneOf, }, "vlan_vid": { Type: schema.TypeFloat, Optional: true, AtLeastOneOf: filterAtLeastOneOf, ValidateFunc: validation.FloatBetween(1, 4094), }, "vrf_id": { Type: schema.TypeInt, Optional: true, AtLeastOneOf: filterAtLeastOneOf, }, "vlan_id": { Type: schema.TypeInt, Optional: true, AtLeastOneOf: filterAtLeastOneOf, }, "tenant_id": { Type: schema.TypeInt, Optional: true, AtLeastOneOf: filterAtLeastOneOf, }, "site_id": { Type: schema.TypeInt, Optional: true, AtLeastOneOf: filterAtLeastOneOf, }, "site_group_id": { Type: schema.TypeInt, Computed: true, }, "location_id": { Type: schema.TypeInt, Computed: true, }, "region_id": { Type: schema.TypeInt, Computed: true, }, "tag": { Type: schema.TypeString, Optional: true, AtLeastOneOf: filterAtLeastOneOf, Description: "Tag to include in the data source filter (must match the tag's slug).", }, "tag__n": { Type: schema.TypeString, Optional: true, Description: `Tag to exclude from the data source filter (must match the tag's slug). Refer to [Netbox's documentation](https://demo.netbox.dev/static/docs/rest-api/filtering/#lookup-expressions) for more information on available lookup expressions.`, }, "status": { Type: schema.TypeString, Optional: true, AtLeastOneOf: filterAtLeastOneOf, }, "tags": tagsSchemaRead, }, } } func dataSourceNetboxPrefixRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := ipam.NewIpamPrefixesListParams() var opts []ipam.ClientOption limit := int64(2) // Limit of 2 is enough params.Limit = &limit // note: cidr is deprecated in favor of prefix if cidr, ok := d.Get("cidr").(string); ok && cidr != "" { params.Prefix = &cidr } if description, ok := d.Get("description").(string); ok && description != "" { params.Description = &description } if family, ok := d.Get("family").(int); ok && family != 0 { familyFloat := float64(family) params.Family = &familyFloat } if roleID, ok := d.Get("role_id").(int); ok && roleID != 0 { params.RoleID = strToPtr(strconv.Itoa(roleID)) } if prefix, ok := d.Get("prefix").(string); ok && prefix != "" { params.Prefix = &prefix } if vrfID, ok := d.Get("vrf_id").(int); ok && vrfID != 0 { // Note that vrf_id is a string pointer in the netbox filter, but we use a number in the provider params.VrfID = strToPtr(strconv.Itoa(vrfID)) } if vlanID, ok := d.Get("vlan_id").(int); ok && vlanID != 0 { // Note that vlan_id is a string pointer in the netbox filter, but we use a number in the provider params.VlanID = strToPtr(strconv.Itoa(vlanID)) } if vlanVid, ok := d.Get("vlan_vid").(float64); ok && vlanVid != 0 { params.VlanVid = &vlanVid } if tenantID, ok := d.Get("tenant_id").(int); ok && tenantID != 0 { // Note that tenant_id is a string pointer in the netbox filter, but we use a number in the provider params.TenantID = strToPtr(strconv.Itoa(tenantID)) } if siteID, ok := d.Get("site_id").(int); ok && siteID != 0 { // Note that site_id is a string pointer in the netbox filter, but we use a number in the provider params.SiteID = strToPtr(strconv.Itoa(siteID)) } if tag, ok := d.Get("tag").(string); ok && tag != "" { params.Tag = []string{tag} //TODO: switch schema to list } if tagn, ok := d.Get("tag__n").(string); ok && tagn != "" { params.Tagn = &tagn } if status, ok := d.Get("status").(string); ok && status != "" { params.Status = &status } if cfm, ok := d.Get(customFieldsKey).(map[string]interface{}); ok { opts = append(opts, WithCustomFieldParamsOption(cfm)) } res, err := api.Ipam.IpamPrefixesList(params, nil, opts...) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than prefix returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no prefix found matching filter") } result := res.GetPayload().Results[0] d.Set("id", result.ID) d.Set("cidr", result.Prefix) d.Set("prefix", result.Prefix) d.Set("status", result.Status.Value) d.Set("description", result.Description) d.Set("family", int(*result.Family.Value)) d.Set("tags", getTagListFromNestedTagList(result.Tags)) cf := getCustomFields(result.CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } if result.Role != nil { d.Set("role_id", result.Role.ID) } if result.Vrf != nil { d.Set("vrf_id", result.Vrf.ID) } if result.Vlan != nil { d.Set("vlan_vid", result.Vlan.Vid) d.Set("vlan_id", result.Vlan.ID) } if result.Tenant != nil { d.Set("tenant_id", result.Tenant.ID) } d.Set("site_id", nil) d.Set("site_group_id", nil) d.Set("location_id", nil) d.Set("region_id", nil) if result.ScopeType != nil && result.ScopeID != nil { scopeID := result.ScopeID switch scopeType := result.ScopeType; *scopeType { case "dcim.site": d.Set("site_id", scopeID) case "dcim.sitegroup": d.Set("site_group_id", scopeID) case "dcim.location": d.Set("location_id", scopeID) case "dcim.region": d.Set("region_id", scopeID) } } d.SetId(strconv.FormatInt(result.ID, 10)) return nil } ================================================ FILE: netbox/data_source_netbox_prefix_test.go ================================================ package netbox import ( "fmt" "strings" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxPrefixDataSource_basic(t *testing.T) { testv4Prefix := "10.0.0.0/24" testv6Prefix := "2000::/64" testSlug := "prefix_ds_basic" testVlanVid := 4090 testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_vrf" "test" { name = "%[1]s_vrf" } resource "netbox_vlan" "test" { name = "%[1]s_vlan_test_id" vid = %[4]d } resource "netbox_tenant" "test" { name = "%[1]s_tenant" } resource "netbox_site" "test" { name = "%[1]s_site" } resource "netbox_ipam_role" "test" { name = "%[1]s_role" } resource "netbox_prefix" "testv4" { prefix = "%[2]s" status = "active" vrf_id = netbox_vrf.test.id vlan_id = netbox_vlan.test.id tenant_id = netbox_tenant.test.id site_id = netbox_site.test.id role_id = netbox_ipam_role.test.id description = "%[1]s_description_test_idv4" } resource "netbox_prefix" "testv6" { prefix = "%[3]s" status = "container" vrf_id = netbox_vrf.test.id vlan_id = netbox_vlan.test.id tenant_id = netbox_tenant.test.id site_id = netbox_site.test.id description = "%[1]s_description_test_idv6" } data "netbox_prefix" "by_description" { description = netbox_prefix.testv4.description } data "netbox_prefix" "by_cidr" { depends_on = [netbox_prefix.testv4] cidr = "%[2]s" } data "netbox_prefix" "by_vrf_id" { depends_on = [netbox_prefix.testv4] vrf_id = netbox_vrf.test.id family = 4 } data "netbox_prefix" "by_vlan_id" { depends_on = [netbox_prefix.testv4] vlan_id = netbox_vlan.test.id family = 4 } data "netbox_prefix" "by_vlan_vid" { depends_on = [netbox_prefix.testv4] vlan_vid = %[4]d family = 4 } data "netbox_prefix" "by_prefix" { depends_on = [netbox_prefix.testv4] prefix = "%[2]s" } data "netbox_prefix" "by_tenant_id" { depends_on = [netbox_prefix.testv4] tenant_id = netbox_tenant.test.id family = 4 } data "netbox_prefix" "by_site_id" { depends_on = [netbox_prefix.testv4] site_id = netbox_site.test.id family = 4 } data "netbox_prefix" "by_role_id" { depends_on = [netbox_prefix.testv4] role_id = netbox_ipam_role.test.id } data "netbox_prefix" "by_status" { depends_on = [netbox_prefix.testv4] status = "active" } data "netbox_prefix" "by_family" { depends_on = [netbox_prefix.testv6] family = 6 }`, testName, testv4Prefix, testv6Prefix, testVlanVid), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_prefix.by_prefix", "id", "netbox_prefix.testv4", "id"), resource.TestCheckResourceAttrPair("data.netbox_prefix.by_description", "id", "netbox_prefix.testv4", "id"), resource.TestCheckResourceAttrPair("data.netbox_prefix.by_cidr", "id", "netbox_prefix.testv4", "id"), resource.TestCheckResourceAttrPair("data.netbox_prefix.by_vrf_id", "id", "netbox_prefix.testv4", "id"), resource.TestCheckResourceAttrPair("data.netbox_prefix.by_vlan_id", "id", "netbox_prefix.testv4", "id"), resource.TestCheckResourceAttrPair("data.netbox_prefix.by_vlan_vid", "id", "netbox_prefix.testv4", "id"), resource.TestCheckResourceAttrPair("data.netbox_prefix.by_tenant_id", "id", "netbox_prefix.testv4", "id"), resource.TestCheckResourceAttrPair("data.netbox_prefix.by_site_id", "id", "netbox_prefix.testv4", "id"), resource.TestCheckResourceAttrPair("data.netbox_prefix.by_role_id", "id", "netbox_prefix.testv4", "id"), resource.TestCheckResourceAttrPair("data.netbox_prefix.by_status", "id", "netbox_prefix.testv4", "id"), resource.TestCheckResourceAttrPair("data.netbox_prefix.by_family", "id", "netbox_prefix.testv6", "id"), ), }, }, }) } func TestAccNetboxPrefixDataSource_customFields(t *testing.T) { testSlug := "prefix_customfields" testPrefix := "10.0.0.0/24" testField := strings.ReplaceAll(testAccGetTestName(testSlug), "-", "_") resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_custom_field" "test" { name = "%[1]s" type = "text" content_types = ["ipam.prefix"] weight = 100 } resource "netbox_prefix" "test" { prefix = "%[2]s" status = "active" custom_fields = { "${netbox_custom_field.test.name}" = "test value" } } data "netbox_prefix" "test_output" { depends_on = [netbox_prefix.test] prefix = "%[2]s" } data "netbox_prefix" "by_custom_fields" { depends_on = [netbox_prefix.test] custom_fields = { "${netbox_custom_field.test.name}" = "test value" } } `, testField, testPrefix), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_prefix.test_output", "status", "active"), resource.TestCheckResourceAttr("data.netbox_prefix.test_output", "prefix", testPrefix), resource.TestCheckResourceAttr("data.netbox_prefix.test_output", "custom_fields."+testField, "test value"), resource.TestCheckResourceAttrPair("data.netbox_prefix.by_custom_fields", "id", "netbox_prefix.test", "id"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_prefixes.go ================================================ package netbox import ( "fmt" "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxPrefixes() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxPrefixesRead, Description: `:meta:subcategory:IP Address Management (IPAM):`, Schema: map[string]*schema.Schema{ "filter": { Type: schema.TypeSet, Optional: true, Description: "A list of filters to apply to the API query when requesting prefixes.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, Description: "The name of the field to filter on. Supported fields are: `prefix`, `contains`, `vlan_vid`, `vrf_id`, `vlan_id`, `status`, `tenant_id`, `site_id`, `description` & `tag`.", }, "value": { Type: schema.TypeString, Required: true, Description: "The value to pass to the specified filter.", }, }, }, }, "limit": { Type: schema.TypeInt, Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), Default: 0, Description: "The limit of objects to return from the API lookup.", }, "prefixes": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "prefix": { Type: schema.TypeString, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, "tenant_id": { Type: schema.TypeInt, Computed: true, }, "site_id": { Type: schema.TypeInt, Computed: true, }, "site_group_id": { Type: schema.TypeInt, Computed: true, }, "location_id": { Type: schema.TypeInt, Computed: true, }, "region_id": { Type: schema.TypeInt, Computed: true, }, "vlan_vid": { Type: schema.TypeFloat, Computed: true, }, "vrf_id": { Type: schema.TypeInt, Computed: true, }, "vlan_id": { Type: schema.TypeInt, Computed: true, }, "status": { Type: schema.TypeString, Computed: true, }, "tags": tagsSchemaRead, customFieldsKey: { Type: schema.TypeMap, Computed: true, Elem: &schema.Schema{ Type: schema.TypeString, }, }, }, }, }, }, } } func dataSourceNetboxPrefixesRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := ipam.NewIpamPrefixesListParams() // Get user limit var userLimit int64 = 0 if limitValue, ok := d.GetOk("limit"); ok { userLimit = int64(limitValue.(int)) } if filter, ok := d.GetOk("filter"); ok { var filterParams = filter.(*schema.Set) for _, f := range filterParams.List() { k := f.(map[string]interface{})["name"] v := f.(map[string]interface{})["value"] vString := v.(string) switch k { case "prefix": params.Prefix = &vString case "vlan_vid": float, err := strconv.ParseFloat(vString, 64) if err != nil { return err } params.VlanVid = &float case "contains": params.Contains = &vString case "vrf_id": params.VrfID = &vString case "vlan_id": params.VlanID = &vString case "status": params.Status = &vString case "tenant_id": params.TenantID = &vString case "site_id": params.SiteID = &vString case "description": params.Description = &vString case "tag": params.Tag = []string{vString} default: return fmt.Errorf("'%s' is not a supported filter parameter", k) } } } // Fetch all pages with pagination paginationHelper := NewPaginationHelper(userLimit) var allPrefixes []*models.Prefix pageSize := paginationHelper.GetPageSize() for { currentOffset := paginationHelper.CurrentOffset() params.Limit = &pageSize params.Offset = ¤tOffset res, err := api.Ipam.IpamPrefixesList(params, nil) if err != nil { return fmt.Errorf("failed to fetch prefixes at offset %d: %w", currentOffset, err) } payload := res.GetPayload() allPrefixes = append(allPrefixes, payload.Results...) if len(payload.Results) == 0 { break } if !paginationHelper.ShouldContinuePaging(int64(len(allPrefixes)), payload.Next) { break } paginationHelper.Advance(int64(len(payload.Results))) } // Trim to user limit if specified trimmedCount := paginationHelper.TrimToLimit(len(allPrefixes)) filteredPrefixes := allPrefixes[:trimmedCount] var s []map[string]interface{} for _, v := range filteredPrefixes { var mapping = make(map[string]interface{}) mapping["id"] = v.ID mapping["prefix"] = v.Prefix mapping["description"] = v.Description if v.Vlan != nil { mapping["vlan_vid"] = v.Vlan.Vid mapping["vlan_id"] = v.Vlan.ID } if v.Vrf != nil { mapping["vrf_id"] = v.Vrf.ID } if v.Tenant != nil { mapping["tenant_id"] = v.Tenant.ID } if v.ScopeType != nil && v.ScopeID != nil { scopeID := v.ScopeID switch scopeType := v.ScopeType; *scopeType { case "dcim.site": mapping["site_id"] = scopeID case "dcim.sitegroup": mapping["site_group_id"] = scopeID case "dcim.location": mapping["location_id"] = scopeID case "dcim.region": mapping["region_id"] = scopeID } } mapping["status"] = v.Status.Value mapping["tags"] = getTagListFromNestedTagList(v.Tags) cf := flattenCustomFields(v.CustomFields) if cf != nil { mapping[customFieldsKey] = cf } s = append(s, mapping) } d.SetId(id.UniqueId()) return d.Set("prefixes", s) } ================================================ FILE: netbox/data_source_netbox_prefixes_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxPrefixesDataSource_basic(t *testing.T) { testPrefixes := []string{"10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24", "10.0.7.0/24", "10.0.8.0/24", "10.0.9.0/24"} testSlug := "prefixes_ds_basic" testVlanVids := []int{4093, 4094} testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_prefix" "test_prefix1" { prefix = "%[2]s" status = "active" description = "my-description" vrf_id = netbox_vrf.test_vrf.id vlan_id = netbox_vlan.test_vlan1.id tags = [netbox_tag.test_tag1.slug] } resource "netbox_prefix" "test_prefix2" { prefix = "%[3]s" status = "container" vrf_id = netbox_vrf.test_vrf.id vlan_id = netbox_vlan.test_vlan2.id } resource "netbox_prefix" "without_vrf_and_vlan" { prefix = "%[4]s" status = "container" } resource "netbox_tenant" "test" { name = "%[1]s_tenant" } resource "netbox_prefix" "with_tenant_id" { prefix = "%[5]s" status = "container" tenant_id = netbox_tenant.test.id } resource "netbox_site" "test" { name = "site-%[1]s" timezone = "Europe/Berlin" } resource "netbox_prefix" "with_site_id" { prefix = "%[6]s" status = "container" site_id = netbox_site.test.id } resource "netbox_site" "test2" { name = "site2-%[1]s" timezone = "Europe/Berlin" } resource "netbox_prefix" "with_container" { prefix = "%[9]s" status = "container" site_id = netbox_site.test2.id } resource "netbox_vrf" "test_vrf" { name = "%[1]s_test_vrf" } resource "netbox_vlan" "test_vlan1" { name = "%[1]s_vlan1" vid = %[7]d } resource "netbox_vlan" "test_vlan2" { name = "%[1]s_vlan2" vid = %[8]d } resource "netbox_tag" "test_tag1" { name = "%[1]s" } resource "netbox_tag" "test_tag2" { name = "tag-with-no-associations" } data "netbox_prefixes" "by_vrf" { depends_on = [netbox_prefix.test_prefix1, netbox_prefix.test_prefix2] filter { name = "vrf_id" value = netbox_vrf.test_vrf.id } } data "netbox_prefixes" "by_vid" { depends_on = [netbox_prefix.test_prefix1, netbox_prefix.test_prefix2] filter { name = "vlan_vid" value = "%[7]d" } } data "netbox_prefixes" "by_tag" { depends_on = [netbox_prefix.test_prefix1] filter { name = "tag" value = "%[1]s" } } data "netbox_prefixes" "by_status" { depends_on = [netbox_prefix.test_prefix1] filter { name = "status" value = "active" } } data "netbox_prefixes" "no_results" { depends_on = [netbox_prefix.test_prefix1] filter { name = "tag" value = netbox_tag.test_tag2.name } } data "netbox_prefixes" "find_prefix_without_vrf_and_vlan" { depends_on = [netbox_prefix.without_vrf_and_vlan] filter { name = "prefix" value = netbox_prefix.without_vrf_and_vlan.prefix } } data "netbox_prefixes" "find_prefix_with_tenant_id" { depends_on = [netbox_prefix.with_tenant_id] filter { name = "tenant_id" value = netbox_tenant.test.id } } data "netbox_prefixes" "find_prefix_with_site_id" { depends_on = [netbox_prefix.with_site_id] filter { name = "site_id" value = netbox_site.test.id } } data "netbox_prefixes" "find_prefix_with_contains" { depends_on = [netbox_prefix.with_container] filter { name = "contains" value = "10.0.9.50" } } data "netbox_prefixes" "find_prefix_with_description" { depends_on = [netbox_prefix.test_prefix1] filter { name = "description" value = netbox_prefix.test_prefix1.description } } `, testName, testPrefixes[0], testPrefixes[1], testPrefixes[2], testPrefixes[3], testPrefixes[4], testVlanVids[0], testVlanVids[1], testPrefixes[5]), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_prefixes.by_vrf", "prefixes.#", "2"), resource.TestCheckResourceAttrPair("data.netbox_prefixes.by_vrf", "prefixes.1.vlan_vid", "netbox_vlan.test_vlan2", "vid"), resource.TestCheckResourceAttrPair("data.netbox_prefixes.by_vid", "prefixes.0.vlan_vid", "netbox_vlan.test_vlan1", "vid"), resource.TestCheckResourceAttr("data.netbox_prefixes.by_tag", "prefixes.#", "1"), resource.TestCheckResourceAttr("data.netbox_prefixes.by_tag", "prefixes.0.description", "my-description"), resource.TestCheckResourceAttr("data.netbox_prefixes.by_status", "prefixes.#", "1"), resource.TestCheckResourceAttr("data.netbox_prefixes.by_status", "prefixes.0.description", "my-description"), resource.TestCheckResourceAttr("data.netbox_prefixes.no_results", "prefixes.#", "0"), resource.TestCheckResourceAttr("data.netbox_prefixes.find_prefix_with_tenant_id", "prefixes.#", "1"), resource.TestCheckResourceAttr("data.netbox_prefixes.find_prefix_with_tenant_id", "prefixes.0.prefix", "10.0.7.0/24"), resource.TestCheckResourceAttr("data.netbox_prefixes.find_prefix_with_site_id", "prefixes.#", "1"), resource.TestCheckResourceAttr("data.netbox_prefixes.find_prefix_with_site_id", "prefixes.0.prefix", "10.0.8.0/24"), resource.TestCheckResourceAttr("data.netbox_prefixes.find_prefix_with_contains", "prefixes.#", "1"), resource.TestCheckResourceAttr("data.netbox_prefixes.find_prefix_with_contains", "prefixes.0.prefix", "10.0.9.0/24"), resource.TestCheckResourceAttrSet("data.netbox_prefixes.find_prefix_with_contains", "prefixes.0.site_id"), resource.TestCheckResourceAttr("data.netbox_prefixes.find_prefix_with_description", "prefixes.0.description", "my-description"), ), }, }, }) } func TestAccNetboxPrefixesDataSource_customFields(t *testing.T) { testSlug := "prefixes_ds_custom_fields" testName := testAccGetTestName(testSlug) // Custom field names can only contain alphanumeric characters and underscores testFieldPrefix := "tf_test_cf" testPrefix := "10.100.0.0/24" testGatewayIP := "10.100.0.1/24" resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` # Create a text custom field (simple type) resource "netbox_custom_field" "test_text" { name = "%[4]s_text" type = "text" content_types = ["ipam.prefix"] } # Create a JSON custom field (can store complex objects like IP addresses) # Note: Object type custom fields are available in NetBox v4.0+, but this provider # version is tested against v4.2.2 which doesn't fully support them yet. # JSON fields can be used as a workaround for complex data structures. resource "netbox_custom_field" "test_json" { name = "%[4]s_json" type = "json" content_types = ["ipam.prefix"] } # Create a prefix with custom fields resource "netbox_prefix" "test_with_custom_fields" { prefix = "%[2]s" status = "active" custom_fields = { (netbox_custom_field.test_text.name) = "test-value" (netbox_custom_field.test_json.name) = jsonencode({ gateway = { address = "%[3]s" family = "IPv4" } }) } depends_on = [ netbox_custom_field.test_text, netbox_custom_field.test_json, ] } # Data source to fetch the prefix with custom fields data "netbox_prefixes" "with_custom_fields" { depends_on = [netbox_prefix.test_with_custom_fields] filter { name = "prefix" value = netbox_prefix.test_with_custom_fields.prefix } } # Output to test that custom_fields can be accessed output "custom_fields_output" { value = length(data.netbox_prefixes.with_custom_fields.prefixes) > 0 ? data.netbox_prefixes.with_custom_fields.prefixes[0].custom_fields : {} } # Output to test contains() function with keys() for simple field output "has_text_field" { value = ( length(data.netbox_prefixes.with_custom_fields.prefixes) > 0 && contains( keys(data.netbox_prefixes.with_custom_fields.prefixes[0].custom_fields), "%[4]s_text" ) ) } # Output to test contains() function with keys() for complex JSON field output "has_json_field" { value = ( length(data.netbox_prefixes.with_custom_fields.prefixes) > 0 && contains( keys(data.netbox_prefixes.with_custom_fields.prefixes[0].custom_fields), "%[4]s_json" ) ) } # Test that JSON field can be parsed (it will be a JSON string containing JSON) output "json_field_is_valid" { value = ( length(data.netbox_prefixes.with_custom_fields.prefixes) > 0 && can(jsondecode(data.netbox_prefixes.with_custom_fields.prefixes[0].custom_fields["%[4]s_json"])) ) } `, testName, testPrefix, testGatewayIP, testFieldPrefix), Check: resource.ComposeTestCheckFunc( // Verify we got one prefix resource.TestCheckResourceAttr("data.netbox_prefixes.with_custom_fields", "prefixes.#", "1"), resource.TestCheckResourceAttr("data.netbox_prefixes.with_custom_fields", "prefixes.0.prefix", testPrefix), // Verify custom_fields attribute exists and is set resource.TestCheckResourceAttrSet("data.netbox_prefixes.with_custom_fields", "prefixes.0.custom_fields.%"), // Verify simple custom field values resource.TestCheckResourceAttr("data.netbox_prefixes.with_custom_fields", fmt.Sprintf("prefixes.0.custom_fields.%s_text", testFieldPrefix), "test-value"), // Verify complex JSON field exists and is a JSON string resource.TestCheckResourceAttrSet("data.netbox_prefixes.with_custom_fields", fmt.Sprintf("prefixes.0.custom_fields.%s_json", testFieldPrefix)), // Verify outputs work for simple fields resource.TestCheckOutput("has_text_field", "true"), // Verify outputs work for complex JSON field - this tests flattenCustomFields works! resource.TestCheckOutput("has_json_field", "true"), resource.TestCheckOutput("json_field_is_valid", "true"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_rack_role.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxRackRole() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxRackRoleRead, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, "color_hex": { Type: schema.TypeString, Computed: true, }, tagsKey: tagsSchemaRead, }, } } func dataSourceNetboxRackRoleRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) params := dcim.NewDcimRackRolesListParams() params.Name = &name limit := int64(2) // Limit of 2 is enough params.Limit = &limit res, err := api.Dcim.DcimRackRolesList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one rack role returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no rack role found matching filter") } result := res.GetPayload().Results[0] d.SetId(strconv.FormatInt(result.ID, 10)) d.Set("name", result.Name) d.Set("slug", result.Slug) d.Set("description", result.Description) d.Set("color_hex", result.Color) d.Set(tagsKey, getTagListFromNestedTagList(result.Tags)) return nil } ================================================ FILE: netbox/data_source_netbox_rack_role_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxRackRoleDataSource_basic(t *testing.T) { testSlug := "rack_role_ds_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_rack_role" "test" { name = "%[1]s" color_hex = "123456" description = "%[1]sdescription" tags = [netbox_tag.test.name] } data "netbox_rack_role" "test" { depends_on = [netbox_rack_role.test] name = "%[1]s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_rack_role.test", "id", "netbox_rack_role.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_rack_role.test", "name", "netbox_rack_role.test", "name"), resource.TestCheckResourceAttrPair("data.netbox_rack_role.test", "color_hex", "netbox_rack_role.test", "color_hex"), resource.TestCheckResourceAttrPair("data.netbox_rack_role.test", "description", "netbox_rack_role.test", "description"), resource.TestCheckResourceAttr("data.netbox_rack_role.test", "tags.#", "1"), resource.TestCheckResourceAttr("data.netbox_rack_role.test", "tags.0", testName), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_racks.go ================================================ package netbox import ( "errors" "fmt" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxRacks() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxRacksRead, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):`, Schema: map[string]*schema.Schema{ "filter": { Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "value": { Type: schema.TypeString, Required: true, }, }, }, }, "limit": { Type: schema.TypeInt, Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), Default: 0, }, "racks": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, "site_id": { Type: schema.TypeInt, Computed: true, }, "status": { Type: schema.TypeString, Computed: true, }, "width": { Type: schema.TypeInt, Computed: true, }, "u_height": { Type: schema.TypeInt, Computed: true, }, tagsKey: tagsSchemaRead, "tenant_id": { Type: schema.TypeInt, Computed: true, }, "facility_id": { Type: schema.TypeString, Computed: true, }, "location_id": { Type: schema.TypeInt, Computed: true, }, "role_id": { Type: schema.TypeInt, Computed: true, }, "serial": { Type: schema.TypeString, Computed: true, }, "asset_tag": { Type: schema.TypeString, Computed: true, }, "type_id": { Type: schema.TypeInt, Computed: true, }, "weight": { Type: schema.TypeFloat, Computed: true, }, "max_weight": { Type: schema.TypeInt, Computed: true, }, "weight_unit": { Type: schema.TypeString, Computed: true, }, "desc_units": { Type: schema.TypeBool, Computed: true, }, "outer_width": { Type: schema.TypeInt, Computed: true, }, "outer_depth": { Type: schema.TypeInt, Computed: true, }, "outer_unit": { Type: schema.TypeString, Computed: true, }, "mounting_depth": { Type: schema.TypeInt, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, "comments": { Type: schema.TypeString, Computed: true, }, "custom_fields": { Type: schema.TypeMap, Computed: true, }, }, }, }, }, } } func dataSourceNetboxRacksRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := dcim.NewDcimRacksListParams() // Get user limit var userLimit int64 = 0 if limitValue, ok := d.GetOk("limit"); ok { userLimit = int64(limitValue.(int)) } if filter, ok := d.GetOk("filter"); ok { var filterParams = filter.(*schema.Set) for _, f := range filterParams.List() { k := f.(map[string]interface{})["name"] v := f.(map[string]interface{})["value"] vString := v.(string) switch k { case "asset_tag": params.AssetTag = &vString case "contact": params.Contact = &vString case "contact_group": params.ContactGroup = &vString case "contact_role": params.ContactRole = &vString case "desc_units": params.DescUnits = &vString case "facility_id": params.FacilityID = &vString case "id": params.ID = &vString case "location_id": params.LocationID = &vString case "max_weight": params.MaxWeight = &vString case "mounting_depth": params.MountingDepth = &vString case "name": params.Name = &vString case "outer_depth": params.OuterDepth = &vString case "outer_unit": params.OuterUnit = &vString case "outer_width": params.OuterWidth = &vString case "region_id": params.RegionID = &vString case "role_id": params.RoleID = &vString case "serial": params.Serial = &vString case "site_id": params.SiteID = &vString case "status": params.Status = &vString case "tenant_id": params.TenantID = &vString case "type_id": params.Type = &vString case "u_height": params.UHeight = &vString case "weight": params.Weight = &vString case "weight_unit": params.WeightUnit = &vString case "width": params.Width = &vString default: return fmt.Errorf("'%s' is not a supported filter parameter", k) } } } // Fetch all pages with pagination paginationHelper := NewPaginationHelper(userLimit) var allRacks []*models.Rack pageSize := paginationHelper.GetPageSize() for { currentOffset := paginationHelper.CurrentOffset() params.Limit = &pageSize params.Offset = ¤tOffset res, err := api.Dcim.DcimRacksList(params, nil) if err != nil { return fmt.Errorf("failed to fetch racks at offset %d: %w", currentOffset, err) } payload := res.GetPayload() allRacks = append(allRacks, payload.Results...) if len(payload.Results) == 0 { break } if !paginationHelper.ShouldContinuePaging(int64(len(allRacks)), payload.Next) { break } paginationHelper.Advance(int64(len(payload.Results))) } // Trim to user limit if specified trimmedCount := paginationHelper.TrimToLimit(len(allRacks)) filteredRacks := allRacks[:trimmedCount] if len(filteredRacks) == 0 { return errors.New("no result") } var s []map[string]interface{} for _, v := range filteredRacks { var mapping = make(map[string]interface{}) mapping["id"] = v.ID mapping["name"] = v.Name if v.Site != nil { mapping["site_id"] = v.Site.ID } if v.Status != nil { mapping["status"] = v.Status.Value } if v.Width != nil { mapping["width"] = v.Width.Value } mapping["u_height"] = v.UHeight mapping["tags"] = getTagListFromNestedTagList(v.Tags) if v.Tenant != nil { mapping["tenant_id"] = v.Tenant.ID } mapping["facility_id"] = v.FacilityID if v.Location != nil { mapping["location_id"] = v.Location.ID } if v.Role != nil { mapping["role_id"] = v.Role.ID } mapping["serial"] = v.Serial mapping["asset_tag"] = v.AssetTag mapping["type_id"] = v.Type mapping["weight"] = v.Weight mapping["max_weight"] = v.MaxWeight mapping["desc_units"] = v.DescUnits mapping["outer_width"] = v.OuterWidth mapping["outer_depth"] = v.OuterDepth if v.OuterUnit != nil { mapping["outer_unit"] = v.OuterUnit.Value } mapping["mounting_depth"] = v.MountingDepth mapping["description"] = v.Description mapping["comments"] = v.Comments mapping["custom_fields"] = getCustomFields(v.CustomFields) s = append(s, mapping) } d.SetId(id.UniqueId()) return d.Set("racks", s) } ================================================ FILE: netbox/data_source_netbox_racks_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxRacksDataSource_basic(t *testing.T) { testRacks := []string{"rack1", "rack2", "rack3"} testSlug := "racks_ds_basic" testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_rack" "test_rack1" { name = "%[2]s" site_id = netbox_site.test.id status = "active" width = 10 u_height = 40 } resource "netbox_rack" "test_rack2" { name = "%[3]s" site_id = netbox_site.test.id status = "active" width = 19 u_height = 41 } resource "netbox_rack" "test_rack3" { name = "%[4]s" site_id = netbox_site.test.id status = "reserved" width = 21 u_height = 42 } data "netbox_racks" "by_name" { depends_on = [netbox_rack.test_rack1, netbox_rack.test_rack2, netbox_rack.test_rack3] filter { name = "name" value = netbox_rack.test_rack3.name } filter { name = "site_id" value = netbox_site.test.id } } data "netbox_racks" "by_status" { depends_on = [netbox_rack.test_rack1, netbox_rack.test_rack2, netbox_rack.test_rack3] filter { name = "status" value = "active" } filter { name = "site_id" value = netbox_site.test.id } } `, testName, testRacks[0], testRacks[1], testRacks[2]), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_racks.by_name", "racks.#", "1"), resource.TestCheckResourceAttr("data.netbox_racks.by_status", "racks.#", "2"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_region.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxRegion() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxRegionRead, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):`, Schema: map[string]*schema.Schema{ "filter": { Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Optional: true, }, "name": { Type: schema.TypeString, Optional: true, }, "slug": { Type: schema.TypeString, Optional: true, }, }, }, }, "id": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, "slug": { Type: schema.TypeString, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, "parent_region_id": { Type: schema.TypeInt, Computed: true, }, }, } } func dataSourceNetboxRegionRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := dcim.NewDcimRegionsListParams() if filter, ok := d.GetOk("filter"); ok { var filterParams = filter.(*schema.Set) for _, f := range filterParams.List() { id := f.(map[string]interface{})["id"] if id != nil { vID := id.(int) if vID != 0 { vIDString := strconv.Itoa(vID) params.ID = &vIDString } } name := f.(map[string]interface{})["name"] if name != nil { vName := name.(string) params.Name = &vName } slug := f.(map[string]interface{})["slug"] if slug != nil { vSlug := slug.(string) params.Slug = &vSlug } } } res, err := api.Dcim.DcimRegionsList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one region returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no region found matching filter") } result := res.GetPayload().Results[0] d.SetId(strconv.FormatInt(result.ID, 10)) d.Set("name", result.Name) d.Set("slug", result.Slug) d.Set("description", result.Description) if result.Parent != nil { d.Set("parent_region_id", result.Parent.ID) } return nil } ================================================ FILE: netbox/data_source_netbox_region_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxRegionDataSource_basic(t *testing.T) { testSlug := "region_ds_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_region" "test" { name = "%[1]s" } data "netbox_region" "test" { depends_on = [netbox_region.test] filter { name = "%[1]s" } }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_region.test", "id", "netbox_region.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_region.test", "slug", "netbox_region.test", "slug"), ), }, }, }) } func TestAccNetboxRegionDataSource_parent(t *testing.T) { testSlug := "region_ds_parent" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_region" "test" { name = "%[1]s" } resource "netbox_region" "test-child" { name = "%[1]s-child" parent_region_id = netbox_region.test.id } data "netbox_region" "test-child" { depends_on = [netbox_region.test-child] filter { name = "%[1]s-child" } }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_region.test-child", "id", "netbox_region.test-child", "id"), resource.TestCheckResourceAttrPair("data.netbox_region.test-child", "parent_region_id", "netbox_region.test", "id"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_rir.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxRir() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxRirRead, Description: `:meta:subcategory:IP Address Management (IPAM):From the [official documentation](https://docs.netbox.dev/en/stable/features/ipam/#regional-internet-registries-rirs): > Regional Internet registries are responsible for the allocation of globally-routable address space. The five RIRs are ARIN, RIPE, APNIC, LACNIC, and AFRINIC. However, some address space has been set aside for internal use, such as defined in RFCs 1918 and 6598. NetBox considers these RFCs as a sort of RIR as well; that is, an authority which "owns" certain address space.`, Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Optional: true, AtLeastOneOf: []string{"name", "slug"}, }, "slug": { Type: schema.TypeString, Optional: true, AtLeastOneOf: []string{"name", "slug"}, }, "description": { Type: schema.TypeString, Computed: true, }, "is_private": { Type: schema.TypeBool, Computed: true, }, }, } } func dataSourceNetboxRirRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := ipam.NewIpamRirsListParams() limit := int64(2) // Limit of 2 is enough params.Limit = &limit if name, ok := d.Get("name").(string); ok && name != "" { params.Name = &name } if slug, ok := d.Get("slug").(string); ok && slug != "" { params.Slug = &slug } res, err := api.Ipam.IpamRirsList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one rir returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no rir found matching filter") } result := res.GetPayload().Results[0] d.Set("id", result.ID) d.Set("name", result.Name) d.Set("slug", result.Slug) d.Set("description", result.Description) d.Set("is_private", result.IsPrivate) d.SetId(strconv.FormatInt(result.ID, 10)) return nil } ================================================ FILE: netbox/data_source_netbox_rir_test.go ================================================ package netbox import ( "fmt" "regexp" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxRirSetUp(testName string) string { return fmt.Sprintf(` resource "netbox_rir" "test" { name = "%[1]s" }`, testName) } const testAccNetboxRirNoResult = ` data "netbox_rir" "test" { name = "nonexistent" }` func testAccNetboxRirByName(testName string) string { return fmt.Sprintf(` data "netbox_rir" "test" { name = "%s" }`, testName) } func testAccNetboxRirBySlug(testName string) string { return fmt.Sprintf(` data "netbox_rir" "test" { slug = "%[1]s" }`, testName) } func TestAccNetboxRirDataSource_basic(t *testing.T) { testName := testAccGetTestName("rir_ds_basic") setUp := testAccNetboxRirSetUp(testName) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: setUp, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_rir.test", "name", testName), ), }, { Config: setUp + testAccNetboxRirNoResult, ExpectError: regexp.MustCompile("no rir found matching filter"), }, { Config: setUp + testAccNetboxRirByName(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_rir.test", "id", "netbox_rir.test", "id"), ), }, { Config: setUp + testAccNetboxRirBySlug(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_rir.test", "id", "netbox_rir.test", "id"), resource.TestCheckResourceAttr("data.netbox_rir.test", "name", testName), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_route_target.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxRouteTarget() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxRouteTargetRead, Description: `:meta:subcategory:IP Address Management (IPAM):`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, ValidateFunc: validation.StringLenBetween(1, 21), Required: true, }, "tenant_id": { Type: schema.TypeInt, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, tagsKey: tagsSchema, }, } } func dataSourceNetboxRouteTargetRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) params := ipam.NewIpamRouteTargetsListParams() params.Name = &name limit := int64(2) params.Limit = &limit res, err := api.Ipam.IpamRouteTargetsList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one route target returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no route target found matching filter") } result := res.GetPayload().Results[0] d.SetId(strconv.FormatInt(result.ID, 10)) d.Set("name", result.Name) if result.Tenant != nil { d.Set("tenant_id", result.Tenant.ID) } if result.Description != "" { d.Set("description", result.Description) } if result.Tags != nil { d.Set(tagsKey, result.Tags) } return nil } ================================================ FILE: netbox/data_source_netbox_route_target_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func getNetboxDataSourceRouteTargetConfig(testName string) string { return fmt.Sprintf(` resource "netbox_tenant" "acctest_ds_rt" { name = "%[1]s" } resource "netbox_route_target" "acctest_ds_rt" { name = "%[1]s" tenant_id = netbox_tenant.acctest_ds_rt.id } data "netbox_route_target" "acctest_ds_rt" { name = "%[1]s" depends_on = [netbox_route_target.acctest_ds_rt] }`, testName) } func TestAccNetboxRouteTarget_basic(t *testing.T) { testSlug := "rtds" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: getNetboxDataSourceRouteTargetConfig(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_route_target.acctest_ds_rt", "id", "netbox_route_target.acctest_ds_rt", "id"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_site.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxSite() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxSiteRead, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Optional: true, Computed: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, }, "id": { Type: schema.TypeString, Optional: true, Computed: true, }, "facility": { Type: schema.TypeString, Optional: true, Computed: true, }, "asn_ids": { Type: schema.TypeSet, Computed: true, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "comments": { Type: schema.TypeString, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, "group_id": { Type: schema.TypeInt, Computed: true, }, "status": { Type: schema.TypeString, Computed: true, }, "region_id": { Type: schema.TypeInt, Computed: true, }, "site_id": { Type: schema.TypeInt, Computed: true, }, "tenant_id": { Type: schema.TypeInt, Computed: true, }, "time_zone": { Type: schema.TypeString, Computed: true, }, "physical_address": { Type: schema.TypeString, Computed: true, }, }, } } func dataSourceNetboxSiteRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := dcim.NewDcimSitesListParams() params.Limit = int64ToPtr(2) if name, ok := d.Get("name").(string); ok && name != "" { params.SetName(&name) } if slug, ok := d.Get("slug").(string); ok && slug != "" { params.SetSlug(&slug) } if id, ok := d.Get("id").(string); ok && id != "0" { params.SetID(&id) } if facility, ok := d.Get("facility").(string); ok && facility != "" { params.SetFacility(&facility) } res, err := api.Dcim.DcimSitesList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one site returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no site found matching filter") } site := res.GetPayload().Results[0] d.SetId(strconv.FormatInt(site.ID, 10)) d.Set("asn_ids", getIDsFromNestedASNList(site.Asns)) d.Set("comments", site.Comments) d.Set("description", site.Description) d.Set("name", site.Name) d.Set("site_id", site.ID) d.Set("slug", site.Slug) d.Set("time_zone", site.TimeZone) d.Set("facility", site.Facility) d.Set("physical_address", site.PhysicalAddress) if site.Group != nil { d.Set("group_id", site.Group.ID) } if site.Region != nil { d.Set("region_id", site.Region.ID) } if site.Status != nil { d.Set("status", site.Status.Value) } if site.Tenant != nil { d.Set("tenant_id", site.Tenant.ID) } return nil } ================================================ FILE: netbox/data_source_netbox_site_group.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxSiteGroup() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxSiteGroupRead, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Computed: true, Optional: true, AtLeastOneOf: []string{"name", "slug"}, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, AtLeastOneOf: []string{"name", "slug"}, }, "description": { Type: schema.TypeString, Computed: true, }, }, } } func dataSourceNetboxSiteGroupRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := dcim.NewDcimSiteGroupsListParams() if name, ok := d.Get("name").(string); ok && name != "" { params.Name = &name } if slug, ok := d.Get("slug").(string); ok && slug != "" { params.Slug = &slug } limit := int64(2) // Limit of 2 is enough params.Limit = &limit res, err := api.Dcim.DcimSiteGroupsList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one site group returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no site group found matching filter") } result := res.GetPayload().Results[0] d.SetId(strconv.FormatInt(result.ID, 10)) d.Set("name", result.Name) d.Set("slug", result.Slug) d.Set("description", result.Description) return nil } ================================================ FILE: netbox/data_source_netbox_site_group_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxSiteGroupDataSource_basic(t *testing.T) { testName := testAccGetTestName("sitegrp_ds_basic") resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_site_group" "test" { name = "%[1]s" description = "foo" } data "netbox_site_group" "by_name" { depends_on = [netbox_site_group.test] name = "%[1]s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_site_group.by_name", "id", "netbox_site_group.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_site_group.by_name", "name", "netbox_site_group.test", "name"), resource.TestCheckResourceAttr("data.netbox_site_group.by_name", "description", "foo"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_site_test.go ================================================ package netbox import ( "fmt" "regexp" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxSiteSetUp(testName string) string { return fmt.Sprintf(` resource "netbox_rir" "test" { name = "%[1]s" } resource "netbox_asn" "test" { asn = 234 rir_id = netbox_rir.test.id } resource "netbox_region" "test" { name = "%[1]s" } resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" asn_ids = [netbox_asn.test.id] description = "Test" region_id = netbox_region.test.id tenant_id = netbox_tenant.test.id timezone = "Europe/Berlin" facility = "Facility" physical_address = "Platz d. Republik 1, 10557 Berlin, Germany" }`, testName) } const testAccNetboxSiteNoResult = ` data "netbox_site" "test" { name = "_does_not_exist_" }` func testAccNetboxSiteByName(testName string) string { return fmt.Sprintf(` data "netbox_site" "test" { name = "%[1]s" }`, testName) } func testAccNetboxSiteBySlug(testName string) string { return fmt.Sprintf(` data "netbox_site" "test" { slug = "%[1]s" }`, testName) } func testAccNetboxSiteByID() string { return ` data "netbox_site" "test" { id = netbox_site.test.id }` } func testAccNetboxSiteByFacility() string { return ` data "netbox_site" "test" { facility = netbox_site.test.facility }` } func TestAccNetboxSiteDataSource_basic(t *testing.T) { testName := testAccGetTestName("site_ds_basic") setUp := testAccNetboxSiteSetUp(testName) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: setUp, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_site.test", "slug", testName), ), }, { Config: setUp + testAccNetboxSiteNoResult, ExpectError: regexp.MustCompile("no site found matching filter"), }, { Config: setUp + testAccNetboxSiteByName(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_site.test", "id", "netbox_site.test", "id"), ), }, { Config: setUp + testAccNetboxSiteBySlug(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_site.test", "id", "netbox_site.test", "id"), resource.TestCheckResourceAttr("netbox_site.test", "asn_ids.#", "1"), resource.TestCheckResourceAttrPair("netbox_site.test", "asn_ids.0", "netbox_asn.test", "id"), resource.TestCheckResourceAttr("data.netbox_site.test", "description", "Test"), resource.TestCheckResourceAttr("data.netbox_site.test", "time_zone", "Europe/Berlin"), resource.TestCheckResourceAttr("data.netbox_site.test", "status", "active"), resource.TestCheckResourceAttr("data.netbox_site.test", "physical_address", "Platz d. Republik 1, 10557 Berlin, Germany"), resource.TestCheckResourceAttrPair("data.netbox_site.test", "region_id", "netbox_region.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_site.test", "tenant_id", "netbox_tenant.test", "id"), ), }, { Config: setUp + testAccNetboxSiteByID(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_site.test", "id", "netbox_site.test", "id"), ), }, { Config: setUp + testAccNetboxSiteByFacility(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_site.test", "id", "netbox_site.test", "id"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_tag.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/extras" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxTag() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxTagRead, Description: `:meta:subcategory:Extras:`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Computed: true, }, "description": { Type: schema.TypeString, Optional: true, }, }, } } func dataSourceNetboxTagRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) params := extras.NewExtrasTagsListParams() params.Name = &name limit := int64(2) // Limit of 2 is enough params.Limit = &limit res, err := api.Extras.ExtrasTagsList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one tag returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no tag found matching filter") } result := res.GetPayload().Results[0] d.SetId(strconv.FormatInt(result.ID, 10)) d.Set("name", result.Name) d.Set("slug", result.Slug) d.Set("description", result.Description) return nil } ================================================ FILE: netbox/data_source_netbox_tags.go ================================================ package netbox import ( "errors" "fmt" "github.com/fbreckle/go-netbox/netbox/client/extras" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxTags() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxTagsRead, Description: `:meta:subcategory:Extras:`, Schema: map[string]*schema.Schema{ "filter": { Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "value": { Type: schema.TypeString, Required: true, }, }, }, }, "limit": { Type: schema.TypeInt, Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), Default: 0, }, "tags": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "tag_id": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, "slug": { Type: schema.TypeString, Computed: true, }, "description": { Type: schema.TypeString, Optional: true, }, "color": { Type: schema.TypeString, Optional: true, }, }, }, }, }, } } func dataSourceNetboxTagsRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := extras.NewExtrasTagsListParams() // Get user limit var userLimit int64 = 0 if limitValue, ok := d.GetOk("limit"); ok { userLimit = int64(limitValue.(int)) } if filter, ok := d.GetOk("filter"); ok { filterParams := filter.(*schema.Set) for _, f := range filterParams.List() { k := f.(map[string]interface{})["name"] v := f.(map[string]interface{})["value"] vString := v.(string) switch k { case "id": params.ID = &vString case "id__gt": params.IDGt = &vString case "id__gte": params.IDGte = &vString case "id__lt": params.IDLt = &vString case "id__lte": params.IDLte = &vString case "name": params.Name = &vString case "name__ic": params.NameIc = &vString case "name__niew": params.NameNiew = &vString case "name__iew": params.NameIew = &vString case "name__nisw": params.NameNisw = &vString case "name__isw": params.NameIsw = &vString case "slug": params.Slug = &vString case "slug__ic": params.SlugIc = &vString case "slug__niew": params.SlugNiew = &vString case "slug__iew": params.SlugIew = &vString case "slug__nisw": params.SlugNisw = &vString case "slug__isw": params.SlugIsw = &vString default: return fmt.Errorf("'%s' is not a supported filter parameter", k) } } } // Fetch all pages with pagination paginationHelper := NewPaginationHelper(userLimit) var allTags []*models.Tag pageSize := paginationHelper.GetPageSize() for { currentOffset := paginationHelper.CurrentOffset() params.Limit = &pageSize params.Offset = ¤tOffset res, err := api.Extras.ExtrasTagsList(params, nil) if err != nil { return fmt.Errorf("failed to fetch tags at offset %d: %w", currentOffset, err) } payload := res.GetPayload() allTags = append(allTags, payload.Results...) if len(payload.Results) == 0 { break } if !paginationHelper.ShouldContinuePaging(int64(len(allTags)), payload.Next) { break } paginationHelper.Advance(int64(len(payload.Results))) } // Trim to user limit if specified trimmedCount := paginationHelper.TrimToLimit(len(allTags)) results := allTags[:trimmedCount] if len(results) == 0 { return errors.New("no result") } var s []map[string]interface{} for _, v := range results { mapping := make(map[string]interface{}) mapping["tag_id"] = v.ID mapping["name"] = v.Name mapping["slug"] = v.Slug mapping["description"] = v.Description mapping["color"] = v.Color s = append(s, mapping) } d.SetId(id.UniqueId()) return d.Set("tags", s) } ================================================ FILE: netbox/data_source_netbox_tags_test.go ================================================ package netbox import ( "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxTagsSetUp() string { return ` resource "netbox_tag" "test_1" { name = "Tag1234" slug = "tag1234" } resource "netbox_tag" "test_2" { name = "Tag1235" slug = "tag1235" } resource "netbox_tag" "test_3" { name = "Tag2345" slug = "weird" }` } func testAccNetboxTagsByName() string { return ` data "netbox_tags" "test" { filter { name = "name" value = "Tag1234" } }` } func testAccNetboxTagsBySlug() string { return ` data "netbox_tags" "test" { filter { name = "slug" value = "weird" } }` } // func testAccNetboxTagsAll() string { // return ` // data "netbox_tags" "test" { // }` // } func TestAccNetboxTagsDataSource_basic(t *testing.T) { setUp := testAccNetboxTagsSetUp() resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: setUp, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_tag.test_1", "slug", "tag1234"), ), }, { Config: setUp + testAccNetboxTagsByName(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_tags.test", "tags.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_tags.test", "tags.0.tag_id", "netbox_tag.test_1", "id"), ), }, { Config: setUp + testAccNetboxTagsBySlug(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_tags.test", "tags.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_tags.test", "tags.0.tag_id", "netbox_tag.test_3", "id"), ), }, // { // Config: setUp + testAccNetboxTagsAll(), // Check: resource.ComposeTestCheckFunc( // resource.TestCheckResourceAttr("data.netbox_tags.test", "tags.#", "3"), // resource.TestCheckResourceAttrPair("data.netbox_tags.test", "tags.0.tag_id", "netbox_tag.test_1", "id"), // resource.TestCheckResourceAttrPair("data.netbox_tags.test", "tags.1.tag_id", "netbox_tag.test_2", "id"), // resource.TestCheckResourceAttrPair("data.netbox_tags.test", "tags.2.tag_id", "netbox_tag.test_3", "id"), // ), // }, }, }) } ================================================ FILE: netbox/data_source_netbox_tenant.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/tenancy" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxTenant() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxTenantRead, Description: `:meta:subcategory:Tenancy:`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Computed: true, Optional: true, AtLeastOneOf: []string{"name", "slug"}, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, AtLeastOneOf: []string{"name", "slug"}, }, "group_id": { Type: schema.TypeInt, Computed: true, }, "description": { Type: schema.TypeString, Optional: true, }, }, } } func dataSourceNetboxTenantRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := tenancy.NewTenancyTenantsListParams() if name, ok := d.Get("name").(string); ok && name != "" { params.Name = &name } if slug, ok := d.Get("slug").(string); ok && slug != "" { params.Slug = &slug } limit := int64(2) // Limit of 2 is enough params.Limit = &limit res, err := api.Tenancy.TenancyTenantsList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one tenant returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no tenant found matching filter") } result := res.GetPayload().Results[0] d.SetId(strconv.FormatInt(result.ID, 10)) d.Set("name", result.Name) d.Set("slug", result.Slug) d.Set("description", result.Description) if result.Group != nil { d.Set("group_id", result.Group.ID) } return nil } ================================================ FILE: netbox/data_source_netbox_tenant_group.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/tenancy" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxTenantGroup() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxTenantGroupRead, Description: `:meta:subcategory:Tenancy:`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Computed: true, }, "parent_id": { Type: schema.TypeInt, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, }, } } func dataSourceNetboxTenantGroupRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) params := tenancy.NewTenancyTenantGroupsListParams() params.Name = &name limit := int64(2) // Limit of 2 is enough params.Limit = &limit res, err := api.Tenancy.TenancyTenantGroupsList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one tenant group returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no tenant group found matching filter") } result := res.GetPayload().Results[0] d.SetId(strconv.FormatInt(result.ID, 10)) d.Set("name", result.Name) d.Set("slug", result.Slug) d.Set("description", result.Description) if result.Parent != nil { d.Set("parent_id", result.Parent.ID) } return nil } ================================================ FILE: netbox/data_source_netbox_tenant_group_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxTenantDataGroupSource_basic(t *testing.T) { testSlug := "tntgrp_ds_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tenant_group" "test" { name = "%[1]s" } data "netbox_tenant_group" "test" { depends_on = [netbox_tenant_group.test] name = "%[1]s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_tenant_group.test", "id", "netbox_tenant_group.test", "id"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_tenant_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxTenantDataSource_basic(t *testing.T) { testSlug := "tnt_ds_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tenant" "test" { name = "%[1]s" } data "netbox_tenant" "by_name" { depends_on = [netbox_tenant.test] name = "%[1]s" } data "netbox_tenant" "by_slug" { depends_on = [netbox_tenant.test] slug = "%[1]s" } data "netbox_tenant" "by_description" { depends_on = [netbox_tenant.test] name = "%[1]s" description = "%[1]s" } data "netbox_tenant" "by_both" { depends_on = [netbox_tenant.test] name = "%[1]s" slug = "%[1]s" description = "%[1]s" } `, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_tenant.by_name", "id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_tenant.by_slug", "id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_tenant.by_description", "id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_tenant.by_both", "id", "netbox_tenant.test", "id"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_tenants.go ================================================ package netbox import ( "errors" "fmt" "github.com/fbreckle/go-netbox/netbox/client/tenancy" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxTenants() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxTenantsRead, Description: `:meta:subcategory:Tenancy:`, Schema: map[string]*schema.Schema{ "filter": { Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "value": { Type: schema.TypeString, Required: true, }, }, }, }, "limit": { Type: schema.TypeInt, Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), Default: 0, }, "tenants": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, "slug": { Type: schema.TypeString, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, "created": { Type: schema.TypeString, Computed: true, }, "last_updated": { Type: schema.TypeString, Computed: true, }, "comments": { Type: schema.TypeString, Computed: true, }, "custom_fields": { Type: schema.TypeMap, Computed: true, }, "site_count": { Type: schema.TypeInt, Computed: true, }, "rack_count": { Type: schema.TypeInt, Computed: true, }, "device_count": { Type: schema.TypeInt, Computed: true, }, "vrf_count": { Type: schema.TypeInt, Computed: true, }, "prefix_count": { Type: schema.TypeInt, Computed: true, }, "ip_address_count": { Type: schema.TypeInt, Computed: true, }, "vlan_count": { Type: schema.TypeInt, Computed: true, }, "vm_count": { Type: schema.TypeInt, Computed: true, }, "circuit_count": { Type: schema.TypeInt, Computed: true, }, "cluster_count": { Type: schema.TypeInt, Computed: true, }, "tenant_group": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, "slug": { Type: schema.TypeString, Computed: true, }, "tenant_count": { Type: schema.TypeInt, Computed: true, }, }, }, }, }, }, }, }, } } func dataSourceNetboxTenantsRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := tenancy.NewTenancyTenantsListParams() // Get user limit var userLimit int64 = 0 if limitValue, ok := d.GetOk("limit"); ok { userLimit = int64(limitValue.(int)) } if filter, ok := d.GetOk("filter"); ok { var filterParams = filter.(*schema.Set) for _, f := range filterParams.List() { k := f.(map[string]interface{})["name"] v := f.(map[string]interface{})["value"] vString := v.(string) switch k { case "name": params.Name = &vString case "slug": params.Slug = &vString default: return fmt.Errorf("'%s' is not a supported filter parameter", k) } } } // Fetch all pages with pagination paginationHelper := NewPaginationHelper(userLimit) var allTenants []*models.Tenant pageSize := paginationHelper.GetPageSize() for { currentOffset := paginationHelper.CurrentOffset() params.Limit = &pageSize params.Offset = ¤tOffset res, err := api.Tenancy.TenancyTenantsList(params, nil) if err != nil { return fmt.Errorf("failed to fetch tenants at offset %d: %w", currentOffset, err) } payload := res.GetPayload() allTenants = append(allTenants, payload.Results...) if len(payload.Results) == 0 { break } if !paginationHelper.ShouldContinuePaging(int64(len(allTenants)), payload.Next) { break } paginationHelper.Advance(int64(len(payload.Results))) } // Trim to user limit if specified trimmedCount := paginationHelper.TrimToLimit(len(allTenants)) filteredTenants := allTenants[:trimmedCount] if len(filteredTenants) == 0 { return errors.New("no result") } var s []map[string]interface{} for _, v := range filteredTenants { var mapping = make(map[string]interface{}) mapping["id"] = v.ID mapping["name"] = v.Name mapping["slug"] = v.Slug mapping["description"] = v.Description mapping["created"] = v.Created.String() mapping["last_updated"] = v.LastUpdated.String() mapping["comments"] = v.Comments mapping["custom_fields"] = flattenCustomFields(v.CustomFields) mapping["site_count"] = v.SiteCount mapping["rack_count"] = v.RackCount mapping["device_count"] = v.DeviceCount mapping["vrf_count"] = v.VrfCount mapping["prefix_count"] = v.PrefixCount mapping["ip_address_count"] = v.IpaddressCount mapping["vlan_count"] = v.VlanCount mapping["vm_count"] = v.VirtualmachineCount mapping["circuit_count"] = v.CircuitCount mapping["cluster_count"] = v.ClusterCount mapping["tenant_group"] = flattenTenantGroup(v.Group) s = append(s, mapping) } d.SetId(id.UniqueId()) return d.Set("tenants", s) } func flattenTenantGroup(group *models.NestedTenantGroup) []map[string]interface{} { var s []map[string]interface{} if group != nil { var mapping = make(map[string]interface{}) mapping["id"] = group.ID mapping["name"] = group.Name mapping["slug"] = group.Slug mapping["tenant_count"] = group.TenantCount s = append(s, mapping) } return s } ================================================ FILE: netbox/data_source_netbox_tenants_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxTenantsDataSource_basic(t *testing.T) { testSlug := "tnts_ds_basic" testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tenant" "test_list_0" { name = "%[1]s_0" } resource "netbox_tenant" "test_list_1" { name = "%[1]s_1" } data "netbox_tenants" "test" { depends_on = [netbox_tenant.test_list_0, netbox_tenant.test_list_1] }`, testName), // This snippet sometimes returns things from other tests, even if resource.Test is used instead of resource.ParallelTest // This happens especially in CI testing (where test execution is presumably slow) // The check functions are now removed so this does no longer happen // Check: resource.ComposeTestCheckFunc( // resource.TestCheckResourceAttrPair("data.netbox_tenants.test", "tenants.0.name", "netbox_tenant.test_list_0", "name"), // resource.TestCheckResourceAttrPair("data.netbox_tenants.test", "tenants.1.name", "netbox_tenant.test_list_1", "name"), // ), }, }, }) } func TestAccNetboxTenantsDataSource_filter(t *testing.T) { testSlug := "tnts_ds_filter" testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tenant" "test_list_0" { name = "%[1]s_0" } resource "netbox_tenant" "test_list_1" { name = "%[1]s_1" } data "netbox_tenants" "test" { depends_on = [netbox_tenant.test_list_0, netbox_tenant.test_list_1] filter { name = "name" value = "%[1]s_0" } }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_tenants.test", "tenants.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_tenants.test", "tenants.0.name", "netbox_tenant.test_list_0", "name"), ), }, }, }) } func TestAccNetboxTenantsDataSource_tenantgroups(t *testing.T) { testSlug := "tnts_ds_tenant_group_filter" testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tenant_group" "group_0" { name = "group_%[1]s_1" } resource "netbox_tenant" "tenant_0" { name = "tenant_%[1]s_0" group_id = netbox_tenant_group.group_0.id } data "netbox_tenants" "test" { depends_on = [netbox_tenant.tenant_0, netbox_tenant_group.group_0] filter { name = "name" value = "tenant_%[1]s_0" } }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_tenants.test", "tenants.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_tenants.test", "tenants.0.tenant_group.0.name", "netbox_tenant_group.group_0", "name"), resource.TestCheckResourceAttrPair("data.netbox_tenants.test", "tenants.0.tenant_group.0.slug", "netbox_tenant_group.group_0", "slug"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_virtual_disk.go ================================================ package netbox import ( "fmt" "regexp" "github.com/fbreckle/go-netbox/netbox/client/virtualization" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxVirtualDisk() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxVirtualDiskRead, Description: `:meta:subcategory:Virtualization:`, Schema: map[string]*schema.Schema{ "filter": { Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "value": { Type: schema.TypeString, Required: true, }, }, }, }, "name_regex": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringIsValidRegExp, }, "limit": { Type: schema.TypeInt, Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), Default: 0, }, "virtual_disks": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, "size_mb": { Type: schema.TypeInt, Computed: true, }, "virtual_machine_id": { Type: schema.TypeInt, Computed: true, }, tagsKey: { Type: schema.TypeList, Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, }, customFieldsKey: { Type: schema.TypeMap, Computed: true, }, }, }, }, }, } } func dataSourceNetboxVirtualDiskRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := virtualization.NewVirtualizationVirtualDisksListParams() // Get user limit (0 = fetch all) var userLimit int64 = 0 if limitValue, ok := d.GetOk("limit"); ok { userLimit = int64(limitValue.(int)) } if filter, ok := d.GetOk("filter"); ok { var filterParams = filter.([]interface{}) var tags []string for _, f := range filterParams { k := f.(map[string]interface{})["name"] v := f.(map[string]interface{})["value"] vString := v.(string) switch k { case "name": params.NameIc = &vString case "tag": tags = append(tags, vString) default: return fmt.Errorf("'%s' is not a supported filter parameter", k) } } if len(tags) > 0 { params.Tag = tags } } // Fetch all pages with pagination (fetch all when name_regex is used) paginationHelper := NewPaginationHelper(FetchAll) var allDisks []*models.VirtualDisk pageSize := paginationHelper.GetPageSize() for { currentOffset := paginationHelper.CurrentOffset() params.Limit = &pageSize params.Offset = ¤tOffset res, err := api.Virtualization.VirtualizationVirtualDisksList(params, nil) if err != nil { return fmt.Errorf("failed to fetch virtual disks at offset %d: %w", currentOffset, err) } payload := res.GetPayload() allDisks = append(allDisks, payload.Results...) if len(payload.Results) == 0 { break } if !paginationHelper.ShouldContinuePaging(int64(len(allDisks)), payload.Next) { break } paginationHelper.Advance(int64(len(payload.Results))) } // Apply name_regex filter var filteredDisks []*models.VirtualDisk if nameRegex, ok := d.GetOk("name_regex"); ok { r := regexp.MustCompile(nameRegex.(string)) for _, disk := range allDisks { if disk.Name != nil && r.MatchString(*disk.Name) { filteredDisks = append(filteredDisks, disk) } } } else { filteredDisks = allDisks } // Apply user limit to filtered results if userLimit > 0 && int64(len(filteredDisks)) > userLimit { filteredDisks = filteredDisks[:userLimit] } var s []map[string]interface{} for _, v := range filteredDisks { var mapping = make(map[string]interface{}) if v.ID != 0 { mapping["id"] = v.ID } if v.Name != nil { mapping["name"] = *v.Name } if v.Description != "" { mapping["description"] = v.Description } if v.Size != nil { mapping["size_mb"] = *v.Size } if v.VirtualMachine != nil { mapping["virtual_machine_id"] = v.VirtualMachine.ID } if v.CustomFields != nil { mapping["custom_fields"] = flattenCustomFields(v.CustomFields) } if v.Tags != nil { tags := []string{} for _, t := range v.Tags { if t.Name != nil { tags = append(tags, *t.Name) } } mapping["tags"] = tags } s = append(s, mapping) } d.SetId(id.UniqueId()) return d.Set("virtual_disks", s) } ================================================ FILE: netbox/data_source_netbox_virtual_disk_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxVirtualDiskDataSource_basic(t *testing.T) { testSlug := "virtual_disk_ds_basic" testName := testAccGetTestName(testSlug) resourceName := "netbox_virtual_disk.test" dataSourceName := "data.netbox_virtual_disk.test" resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tag" "tag_a" { name = "%[1]s_a" color_hex = "123456" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_virtual_machine" "test" { name = "%[1]s" site_id = netbox_site.test.id } resource "netbox_virtual_disk" "test" { name = "%[1]s" description = "description" size_mb = 30 virtual_machine_id = netbox_virtual_machine.test.id tags = [netbox_tag.tag_a.name] } data "netbox_virtual_disk" "test" { filter { name = "name" value = netbox_virtual_disk.test.name } } `, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(dataSourceName, "virtual_disks.#", "1"), resource.TestCheckResourceAttrPair(dataSourceName, "virtual_disks.0.id", resourceName, "id"), resource.TestCheckResourceAttrPair(dataSourceName, "virtual_disks.0.name", resourceName, "name"), resource.TestCheckResourceAttrPair(dataSourceName, "virtual_disks.0.description", resourceName, "description"), resource.TestCheckResourceAttrPair(dataSourceName, "virtual_disks.0.size_mb", resourceName, "size_mb"), resource.TestCheckResourceAttrPair(dataSourceName, "virtual_disks.0.virtual_machine_id", resourceName, "virtual_machine_id"), ), }, }, }) } func TestAccNetboxVirtualDiskDataSource_filter_and_list(t *testing.T) { testSlug := "virtual_disk_ds_filter" testName := testAccGetTestName(testSlug) dataSourceName := "data.netbox_virtual_disk.filtered" resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tag" "tag_a" { name = "%[1]s_a" color_hex = "123456" } resource "netbox_tag" "tag_b" { name = "%[1]s_b" color_hex = "654321" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_virtual_machine" "test" { name = "%[1]s" site_id = netbox_site.test.id } resource "netbox_virtual_disk" "disk_a" { name = "%[1]s_disk_a" description = "disk a desc" size_mb = 10 virtual_machine_id = netbox_virtual_machine.test.id tags = [netbox_tag.tag_a.name] } resource "netbox_virtual_disk" "disk_b" { name = "%[1]s_disk_b" description = "disk b desc" size_mb = 20 virtual_machine_id = netbox_virtual_machine.test.id tags = [netbox_tag.tag_b.name] } data "netbox_virtual_disk" "filtered" { filter { name = "name" value = "%[1]s_disk_a" } filter { name = "tag" value = netbox_tag.tag_a.name } depends_on = [netbox_virtual_disk.disk_a, netbox_virtual_disk.disk_b] } `, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(dataSourceName, "virtual_disks.#", "1"), resource.TestCheckResourceAttr(dataSourceName, "virtual_disks.0.name", testName+"_disk_a"), resource.TestCheckResourceAttr(dataSourceName, "virtual_disks.0.description", "disk a desc"), resource.TestCheckResourceAttr(dataSourceName, "virtual_disks.0.size_mb", "10"), ), }, { Config: fmt.Sprintf(` resource "netbox_tag" "tag_a" { name = "%[1]s_a" color_hex = "123456" } resource "netbox_tag" "tag_b" { name = "%[1]s_b" color_hex = "654321" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_virtual_machine" "test" { name = "%[1]s" site_id = netbox_site.test.id } resource "netbox_virtual_disk" "disk_a" { name = "%[1]s_disk_a" description = "disk a desc" size_mb = 10 virtual_machine_id = netbox_virtual_machine.test.id tags = [netbox_tag.tag_a.name] } resource "netbox_virtual_disk" "disk_b" { name = "%[1]s_disk_b" description = "disk b desc" size_mb = 20 virtual_machine_id = netbox_virtual_machine.test.id tags = [netbox_tag.tag_b.name] } data "netbox_virtual_disk" "filtered" { filter { name = "tag" value = netbox_tag.tag_b.name } depends_on = [netbox_virtual_disk.disk_a, netbox_virtual_disk.disk_b] } `, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(dataSourceName, "virtual_disks.#", "1"), resource.TestCheckResourceAttr(dataSourceName, "virtual_disks.0.name", testName+"_disk_b"), resource.TestCheckResourceAttr(dataSourceName, "virtual_disks.0.description", "disk b desc"), resource.TestCheckResourceAttr(dataSourceName, "virtual_disks.0.size_mb", "20"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_virtual_machines.go ================================================ package netbox import ( "encoding/json" "errors" "fmt" "regexp" "github.com/fbreckle/go-netbox/netbox/client/virtualization" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxVirtualMachine() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxVirtualMachineRead, Description: `:meta:subcategory:Virtualization:`, Schema: map[string]*schema.Schema{ "filter": { Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "value": { Type: schema.TypeString, Required: true, }, }, }, }, "name_regex": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringIsValidRegExp, }, "limit": { Type: schema.TypeInt, Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), Default: 0, }, "vms": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "cluster_id": { Type: schema.TypeInt, Computed: true, }, "comments": { Type: schema.TypeString, Computed: true, }, "config_context": { Type: schema.TypeString, Computed: true, }, "custom_fields": { Type: schema.TypeMap, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, "device_id": { Type: schema.TypeInt, Computed: true, }, "device_name": { Type: schema.TypeString, Computed: true, }, "disk_size_mb": { Type: schema.TypeInt, Computed: true, }, "local_context_data": { Type: schema.TypeString, Computed: true, }, "memory_mb": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, "platform_id": { Type: schema.TypeInt, Computed: true, }, "platform_slug": { Type: schema.TypeString, Computed: true, }, "platform_name": { Type: schema.TypeString, Computed: true, }, "primary_ip": { Type: schema.TypeString, Computed: true, }, "primary_ip4": { Type: schema.TypeString, Computed: true, }, "primary_ip6": { Type: schema.TypeString, Computed: true, }, "role_id": { Type: schema.TypeInt, Computed: true, }, "site_id": { Type: schema.TypeInt, Computed: true, }, "status": { Type: schema.TypeString, Computed: true, }, "tag_ids": { Type: schema.TypeList, Computed: true, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "tenant_id": { Type: schema.TypeInt, Computed: true, }, "vcpus": { Type: schema.TypeFloat, Computed: true, }, "vm_id": { Type: schema.TypeInt, Computed: true, }, }, }, }, }, } } func dataSourceNetboxVirtualMachineRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := virtualization.NewVirtualizationVirtualMachinesListParams() // Get user limit (0 = fetch all) var userLimit int64 = 0 if limitValue, ok := d.GetOk("limit"); ok { userLimit = int64(limitValue.(int)) } if filter, ok := d.GetOk("filter"); ok { var filterParams = filter.(*schema.Set) var tags []string for _, f := range filterParams.List() { k := f.(map[string]interface{})["name"] v := f.(map[string]interface{})["value"] vString := v.(string) switch k { case "cluster_id": params.ClusterID = &vString case "cluster_group": params.ClusterGroup = &vString case "device_id": params.Name = &vString case "device": params.Name = &vString case "name": params.Name = &vString case "region": params.Region = &vString case "role": params.Role = &vString case "site": params.Site = &vString case "tenant_id": params.TenantID = &vString case "tag": tags = append(tags, vString) params.Tag = tags case "status": params.Status = &vString default: return fmt.Errorf("'%s' is not a supported filter parameter", k) } } } // Fetch all pages with pagination (fetch all when name_regex is used) paginationHelper := NewPaginationHelper(FetchAll) var allVms []*models.VirtualMachineWithConfigContext pageSize := paginationHelper.GetPageSize() for { currentOffset := paginationHelper.CurrentOffset() params.Limit = &pageSize params.Offset = ¤tOffset res, err := api.Virtualization.VirtualizationVirtualMachinesList(params, nil) if err != nil { return fmt.Errorf("failed to fetch virtual machines at offset %d: %w", currentOffset, err) } payload := res.GetPayload() allVms = append(allVms, payload.Results...) if len(payload.Results) == 0 { break } if !paginationHelper.ShouldContinuePaging(int64(len(allVms)), payload.Next) { break } paginationHelper.Advance(int64(len(payload.Results))) } // Apply name_regex filter var filteredVms []*models.VirtualMachineWithConfigContext if nameRegex, ok := d.GetOk("name_regex"); ok { r := regexp.MustCompile(nameRegex.(string)) for _, vm := range allVms { if r.MatchString(*vm.Name) { filteredVms = append(filteredVms, vm) } } } else { filteredVms = allVms } // Apply user limit to filtered results if userLimit > 0 && int64(len(filteredVms)) > userLimit { filteredVms = filteredVms[:userLimit] } if len(filteredVms) == 0 { return errors.New("no result") } var s []map[string]interface{} for _, v := range filteredVms { var mapping = make(map[string]interface{}) if v.Cluster != nil { mapping["cluster_id"] = v.Cluster.ID } if v.Comments != "" { mapping["comments"] = v.Comments } if v.Description != "" { mapping["description"] = v.Description } if v.Device != nil { mapping["device_id"] = v.Device.ID mapping["device_name"] = v.Device.Name } if v.ConfigContext != nil { if configContext, err := json.Marshal(v.ConfigContext); err == nil { mapping["config_context"] = string(configContext) } } if v.CustomFields != nil { mapping["custom_fields"] = flattenCustomFields(v.CustomFields) } if v.Disk != nil { mapping["disk_size_mb"] = *v.Disk } if v.LocalContextData != nil { if localContextData, err := json.Marshal(v.LocalContextData); err == nil { mapping["local_context_data"] = string(localContextData) } } if v.Memory != nil { mapping["memory_mb"] = *v.Memory } if v.Name != nil { mapping["name"] = *v.Name } if v.Platform != nil { mapping["platform_id"] = v.Platform.ID mapping["platform_slug"] = v.Platform.Slug mapping["platform_name"] = v.Platform.Name } if v.PrimaryIP != nil { mapping["primary_ip"] = v.PrimaryIP.Address } if v.PrimaryIp4 != nil { mapping["primary_ip4"] = v.PrimaryIp4.Address } if v.PrimaryIp6 != nil { mapping["primary_ip6"] = v.PrimaryIp6.Address } if v.Role != nil { mapping["role_id"] = v.Role.ID } if v.Site != nil { mapping["site_id"] = v.Site.ID } if v.Status != nil { mapping["status"] = v.Status.Value } if v.Tags != nil { var tags []int64 for _, t := range v.Tags { tags = append(tags, t.ID) } mapping["tag_ids"] = tags } if v.Tenant != nil { mapping["tenant_id"] = v.Tenant.ID } if v.Vcpus != nil { mapping["vcpus"] = *v.Vcpus } mapping["vm_id"] = v.ID s = append(s, mapping) } d.SetId(id.UniqueId()) return d.Set("vms", s) } ================================================ FILE: netbox/data_source_netbox_virtual_machines_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxVirtualMachinesDataSource_basic(t *testing.T) { testSlug := "vm_ds_basic" testName := testAccGetTestName(testSlug) dependencies := testAccNetboxVirtualMachineDataSourceDependencies(testName) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: dependencies, }, { Config: dependencies + testAccNetboxVirtualMachineDataSourceFilterName(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_virtual_machines.test", "vms.#", "1"), resource.TestCheckResourceAttr("data.netbox_virtual_machines.test", "vms.0.name", testName+"_0"), resource.TestCheckResourceAttr("data.netbox_virtual_machines.test", "vms.0.vcpus", "4"), resource.TestCheckResourceAttr("data.netbox_virtual_machines.test", "vms.0.memory_mb", "1024"), resource.TestCheckResourceAttr("data.netbox_virtual_machines.test", "vms.0.disk_size_mb", "256"), resource.TestCheckResourceAttr("data.netbox_virtual_machines.test", "vms.0.comments", "thisisacomment"), resource.TestCheckResourceAttrPair("data.netbox_virtual_machines.test", "vms.0.tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_virtual_machines.test", "vms.0.role_id", "netbox_device_role.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_virtual_machines.test", "vms.0.platform_id", "netbox_platform.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_virtual_machines.test", "vms.0.platform_slug", "netbox_platform.test", "slug"), resource.TestCheckResourceAttrPair("data.netbox_virtual_machines.test", "vms.0.platform_name", "netbox_platform.test", "name"), resource.TestCheckResourceAttrPair("data.netbox_virtual_machines.test", "vms.0.device_id", "netbox_device.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_virtual_machines.test", "vms.0.device_name", "netbox_device.test", "name"), ), }, { Config: dependencies + testAccNetboxVirtualMachineDataSourceFilterCluster, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_virtual_machines.test", "vms.#", "4"), resource.TestCheckResourceAttrPair("data.netbox_virtual_machines.test", "vms.0.cluster_id", "netbox_cluster.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_virtual_machines.test", "vms.0.name", "netbox_virtual_machine.test0", "name"), resource.TestCheckResourceAttrPair("data.netbox_virtual_machines.test", "vms.1.name", "netbox_virtual_machine.test1", "name"), resource.TestCheckResourceAttrPair("data.netbox_virtual_machines.test", "vms.2.name", "netbox_virtual_machine.test2", "name"), resource.TestCheckResourceAttrPair("data.netbox_virtual_machines.test", "vms.3.name", "netbox_virtual_machine.test3", "name"), ), }, { Config: dependencies + testAccNetboxVirtualMachineDataSourceFilterTenantID, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_virtual_machines.test", "vms.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_virtual_machines.test", "vms.0.cluster_id", "netbox_cluster.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_virtual_machines.test", "vms.0.name", "netbox_virtual_machine.test0", "name"), ), }, { Config: dependencies + testAccNetboxVirtualMachineDataSourceNameRegex, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_virtual_machines.test", "vms.#", "2"), resource.TestCheckResourceAttrPair("data.netbox_virtual_machines.test", "vms.0.name", "netbox_virtual_machine.test2", "name"), resource.TestCheckResourceAttrPair("data.netbox_virtual_machines.test", "vms.1.name", "netbox_virtual_machine.test3", "name"), ), }, { Config: dependencies + testAccNetboxVirtualMachineDataSourceLimit, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_virtual_machines.test", "vms.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_virtual_machines.test", "vms.0.cluster_id", "netbox_cluster.test", "id"), ), }, }, }) } func TestAccNetboxVirtualMachinesDataSource_tags(t *testing.T) { testSlug := "vm_ds_tags" testName := testAccGetTestName(testSlug) dependencies := testAccNetboxVirtualMachineDataSourceDependenciesWithTags(testName) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: dependencies, }, { Config: dependencies + testAccNetboxVirtualMachineDataSourceTagA(testName) + testAccNetboxVirtualMachineDataSourceTagB(testName) + testAccNetboxVirtualMachineDataSourceTagAB(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_virtual_machines.tag-a", "vms.#", "2"), resource.TestCheckResourceAttr("data.netbox_virtual_machines.tag-b", "vms.#", "2"), resource.TestCheckResourceAttr("data.netbox_virtual_machines.tag-ab", "vms.#", "1"), ), }, }, }) } func TestAccNetboxVirtualMachinesDataSource_status(t *testing.T) { testSlug := "vm_ds_status" testName := testAccGetTestName(testSlug) dependencies := testAccNetboxVirtualMachineDataSourceDependenciesWithStatus(testName) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: dependencies, }, { Config: dependencies + testAccNetboxVirtualMachineDataSourceStatusActive + testAccNetboxVirtualMachineDataSourceStatusDecommissioning, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_virtual_machines.test_active", "vms.#", "1"), resource.TestCheckResourceAttr("data.netbox_virtual_machines.test_decommissioning", "vms.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_virtual_machines.test_active", "vms.0.status", "netbox_virtual_machine.test0", "status"), resource.TestCheckResourceAttrPair("data.netbox_virtual_machines.test_decommissioning", "vms.0.status", "netbox_virtual_machine.test1", "status"), ), }, }, }) } func testAccNetboxVirtualMachineDataSourceDependencies(testName string) string { return testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test0" { name = "%[1]s_0" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id device_id = netbox_device.test.id comments = "thisisacomment" memory_mb = 1024 disk_size_mb = 256 tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id platform_id = netbox_platform.test.id vcpus = 4 } resource "netbox_virtual_machine" "test1" { name = "%[1]s_1" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id } resource "netbox_virtual_machine" "test2" { name = "%[1]s_2_regex" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id } resource "netbox_virtual_machine" "test3" { name = "%[1]s_3_regex" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id } `, testName) } func testAccNetboxVirtualMachineDataSourceDependenciesWithTags(testName string) string { return testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_tag" "servicea" { name = "%[1]s_service-a" } resource "netbox_tag" "serviceb" { name = "%[1]s_service-b" } resource "netbox_virtual_machine" "test0" { name = "%[1]s_0" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id comments = "thisisacomment" memory_mb = 1024 disk_size_mb = 256 tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id platform_id = netbox_platform.test.id vcpus = 4 tags = [ netbox_tag.servicea.name, netbox_tag.serviceb.name, ] } resource "netbox_virtual_machine" "test1" { name = "%[1]s_1" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id tags = [ netbox_tag.servicea.name, ] } resource "netbox_virtual_machine" "test2" { name = "%[1]s_2_regex" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id tags = [ netbox_tag.serviceb.name, ] } `, testName) } const testAccNetboxVirtualMachineDataSourceFilterCluster = ` data "netbox_virtual_machines" "test" { filter { name = "cluster_id" value = netbox_cluster.test.id } }` const testAccNetboxVirtualMachineDataSourceFilterTenantID = ` data "netbox_virtual_machines" "test" { filter { name = "tenant_id" value = netbox_tenant.test.id } }` func testAccNetboxVirtualMachineDataSourceFilterName(testName string) string { return fmt.Sprintf(` data "netbox_virtual_machines" "test" { filter { name = "name" value = "%[1]s_0" } }`, testName) } const testAccNetboxVirtualMachineDataSourceNameRegex = ` data "netbox_virtual_machines" "test" { name_regex = "test.*_regex" }` const testAccNetboxVirtualMachineDataSourceLimit = ` data "netbox_virtual_machines" "test" { limit = 1 filter { name = "cluster_id" value = netbox_cluster.test.id } }` func testAccNetboxVirtualMachineDataSourceTagA(testName string) string { return fmt.Sprintf(` data "netbox_virtual_machines" "tag-a" { filter { name = "tag" value = "%[1]s_service-a" } }`, testName) } func testAccNetboxVirtualMachineDataSourceTagB(testName string) string { return fmt.Sprintf(` data "netbox_virtual_machines" "tag-b" { filter { name = "tag" value = "%[1]s_service-b" } }`, testName) } func testAccNetboxVirtualMachineDataSourceTagAB(testName string) string { return fmt.Sprintf(` data "netbox_virtual_machines" "tag-ab" { filter { name = "tag" value = "%[1]s_service-a" } filter { name = "tag" value = "%[1]s_service-b" } }`, testName) } const testAccNetboxVirtualMachineDataSourceStatusActive = ` data "netbox_virtual_machines" "test_active" { filter { name = "status" value = "active" } }` const testAccNetboxVirtualMachineDataSourceStatusDecommissioning = ` data "netbox_virtual_machines" "test_decommissioning" { filter { name = "status" value = "decommissioning" } }` func testAccNetboxVirtualMachineDataSourceDependenciesWithStatus(testName string) string { return testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_tag" "servicea" { name = "%[1]s_service-a" } resource "netbox_virtual_machine" "test0" { name = "%[1]s_0" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id comments = "thisisacomment" memory_mb = 1024 disk_size_mb = 256 tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id platform_id = netbox_platform.test.id vcpus = 4 status = "active" tags = [ netbox_tag.servicea.name, ] } resource "netbox_virtual_machine" "test1" { name = "%[1]s_1" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id comments = "thisisacomment" memory_mb = 1024 disk_size_mb = 256 tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id platform_id = netbox_platform.test.id vcpus = 4 status = "decommissioning" tags = [ netbox_tag.servicea.name, ] }`, testName) } ================================================ FILE: netbox/data_source_netbox_vlan.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxVlan() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxVlanRead, Description: `:meta:subcategory:IP Address Management (IPAM):`, Schema: map[string]*schema.Schema{ "vid": { Type: schema.TypeInt, Optional: true, ValidateFunc: validation.IntBetween(1, 4094), }, "name": { Type: schema.TypeString, Optional: true, }, "description": { Type: schema.TypeString, Computed: true, }, "group_id": { Type: schema.TypeInt, Computed: true, Optional: true, }, "role": { Type: schema.TypeInt, Computed: true, Optional: true, }, "site": { Type: schema.TypeInt, Computed: true, }, "status": { Type: schema.TypeString, Computed: true, }, "tenant": { Type: schema.TypeInt, Computed: true, Optional: true, }, }, } } func dataSourceNetboxVlanRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := ipam.NewIpamVlansListParams() params.Limit = int64ToPtr(2) if name, ok := d.Get("name").(string); ok && name != "" { params.Name = &name } if vid, ok := d.Get("vid").(int); ok && vid != 0 { params.Vid = strToPtr(strconv.Itoa(vid)) } if groupID, ok := d.Get("group_id").(int); ok && groupID != 0 { params.GroupID = strToPtr(strconv.Itoa(groupID)) } if roleID, ok := d.Get("role").(int); ok && roleID != 0 { params.RoleID = strToPtr(strconv.Itoa(roleID)) } if tenantID, ok := d.Get("tenant").(int); ok && tenantID != 0 { params.TenantID = strToPtr(strconv.Itoa(tenantID)) } res, err := api.Ipam.IpamVlansList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one vlan returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no vlan found matching filter") } vlan := res.GetPayload().Results[0] d.SetId(strconv.FormatInt(vlan.ID, 10)) d.Set("vid", vlan.Vid) d.Set("name", vlan.Name) d.Set("status", vlan.Status.Value) d.Set("description", vlan.Description) if vlan.Group != nil { d.Set("group_id", vlan.Group.ID) } if vlan.Role != nil { d.Set("role", vlan.Role.ID) } if vlan.Site != nil { d.Set("site", vlan.Site.ID) } if vlan.Tenant != nil { d.Set("tenant", vlan.Tenant.ID) } return nil } ================================================ FILE: netbox/data_source_netbox_vlan_group.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxVlanGroup() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxVlanGroupRead, Description: `:meta:subcategory:IP Address Management (IPAM):`, Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeString, Computed: true, Optional: true, AtLeastOneOf: []string{"id", "name", "slug", "scope_type"}, }, "name": { Type: schema.TypeString, Computed: true, Optional: true, AtLeastOneOf: []string{"id", "name", "slug", "scope_type"}, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, AtLeastOneOf: []string{"id", "name", "slug", "scope_type"}, }, "scope_type": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringInSlice(resourceNetboxVlanGroupScopeTypeOptions, false), Description: buildValidValueDescription(resourceNetboxVlanGroupScopeTypeOptions), AtLeastOneOf: []string{"id", "name", "slug", "scope_type"}, }, "scope_id": { Type: schema.TypeString, Optional: true, RequiredWith: []string{"scope_type"}, }, "vlan_count": { Type: schema.TypeInt, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, }, } } func dataSourceNetboxVlanGroupRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := ipam.NewIpamVlanGroupsListParams() params.Limit = int64ToPtr(2) if id, ok := d.Get("id").(string); ok && id != "" { params.ID = strToPtr(id) } if name, ok := d.Get("name").(string); ok && name != "" { params.Name = strToPtr(name) } if slug, ok := d.Get("slug").(string); ok && slug != "" { params.Slug = strToPtr(slug) } if scopeType, ok := d.Get("scope_type").(string); ok && scopeType != "" { params.ScopeType = strToPtr(scopeType) } if scopeID, ok := d.Get("scope_id").(string); ok && scopeID != "" { params.ScopeID = strToPtr(scopeID) } res, err := api.Ipam.IpamVlanGroupsList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one vlan group returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no vlan group found matching filter") } result := res.GetPayload().Results[0] d.SetId(strconv.FormatInt(result.ID, 10)) d.Set("id", strconv.FormatInt(result.ID, 10)) d.Set("name", result.Name) d.Set("slug", result.Slug) d.Set("vlan_count", result.VlanCount) d.Set("description", result.Description) if result.ScopeID != nil { d.Set("scope_id", strconv.FormatInt(*result.ScopeID, 10)) } if result.ScopeType != nil { d.Set("scope_type", *result.ScopeType) } return nil } ================================================ FILE: netbox/data_source_netbox_vlan_group_test.go ================================================ package netbox import ( "fmt" "regexp" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxVlanGroupDataSource_basic(t *testing.T) { testSlug := "test_group" anotherSlug := "not_test_group" testName := testAccGetTestName(testSlug) setUp := testAccNetboxVlanGroupSetUp(testSlug, testName) extendedSetUp := testAccNetboxVlanGroupSetUpMore(testSlug, anotherSlug, testName) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: setUp, }, { Config: setUp + testAccNetboxVlanGroupDataNoResult, ExpectError: regexp.MustCompile("no vlan group found matching filter"), }, { Config: setUp + testAccNetboxVlanGroupDataByID("netbox_vlan_group.test", "id"), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_vlan_group.test", "id", "netbox_vlan_group.test", "id"), ), }, { Config: setUp + testAccNetboxVlanGroupDataByName(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_vlan_group.test", "id", "netbox_vlan_group.test", "id"), ), }, { Config: setUp + testAccNetboxVlanGroupDataBySlug(testSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_vlan_group.test", "id", "netbox_vlan_group.test", "id"), ), }, { Config: setUp + extendedSetUp + testAccNetboxVlanGroupDataByNameAndScope(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_vlan_group.test", "id", "netbox_vlan_group.test", "id"), resource.TestCheckResourceAttr("data.netbox_vlan_group.test", "name", testName), resource.TestCheckResourceAttr("data.netbox_vlan_group.test", "slug", testSlug), resource.TestCheckResourceAttr("data.netbox_vlan_group.test", "description", "Test"), resource.TestCheckResourceAttr("data.netbox_vlan_group.test", "scope_type", "dcim.site"), resource.TestCheckResourceAttrPair("data.netbox_vlan_group.test", "scope_id", "netbox_site.test", "id"), ), }, { Config: setUp + extendedSetUp + testAccNetboxVlanGroupDataBySlugAndScope(testSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_vlan_group.test", "id", "netbox_vlan_group.test", "id"), resource.TestCheckResourceAttr("data.netbox_vlan_group.test", "name", testName), resource.TestCheckResourceAttr("data.netbox_vlan_group.test", "slug", testSlug), resource.TestCheckResourceAttr("data.netbox_vlan_group.test", "description", "Test"), resource.TestCheckResourceAttr("data.netbox_vlan_group.test", "scope_type", "dcim.site"), resource.TestCheckResourceAttrPair("data.netbox_vlan_group.test", "scope_id", "netbox_site.test", "id"), ), }, { Config: setUp + extendedSetUp + testAccNetboxVlanGroupDataByName(testName), ExpectError: regexp.MustCompile("more than one vlan group returned, specify a more narrow filter"), }, { Config: setUp + extendedSetUp + testAccNetboxVlanGroupDataBySlug(testSlug), ExpectError: regexp.MustCompile("more than one vlan group returned, specify a more narrow filter"), }, }, }) } func testAccNetboxVlanGroupSetUp(testSlug, testName string) string { return fmt.Sprintf(` resource "netbox_site" "test" { name = "%[2]s" } resource "netbox_vlan_group" "test" { slug = "%[1]s" name = "%[2]s" description = "Test" scope_type = "dcim.site" scope_id = netbox_site.test.id vid_ranges = [[1, 4094]] tags = [] } `, testSlug, testName) } func testAccNetboxVlanGroupSetUpMore(testSlug, anotherSlug, testName string) string { return fmt.Sprintf(` resource "netbox_vlan_group" "same_name" { slug = "%[1]s" name = "%[3]s" vid_ranges = [[1, 4094]] } resource "netbox_vlan_group" "not_same" { slug = "%[2]s" name = "%[3]s_unique" vid_ranges = [[1, 4094]] } `, testSlug, anotherSlug, testName) } const testAccNetboxVlanGroupDataNoResult = ` data "netbox_vlan_group" "no_result" { name = "_no_result_" }` func testAccNetboxVlanGroupDataByID(resourceRef, field string) string { return fmt.Sprintf(` data "netbox_vlan_group" "test" { id = %[1]s.%[2]s }`, resourceRef, field) } func testAccNetboxVlanGroupDataByName(testName string) string { return fmt.Sprintf(` data "netbox_vlan_group" "test" { name = "%[1]s" }`, testName) } func testAccNetboxVlanGroupDataBySlug(testSlug string) string { return fmt.Sprintf(` data "netbox_vlan_group" "test" { slug = "%[1]s" }`, testSlug) } func testAccNetboxVlanGroupDataByNameAndScope(testName string) string { return fmt.Sprintf(` data "netbox_vlan_group" "test" { name = "%[1]s" scope_type = "dcim.site" scope_id = netbox_site.test.id }`, testName) } func testAccNetboxVlanGroupDataBySlugAndScope(testSlug string) string { return fmt.Sprintf(` data "netbox_vlan_group" "test" { slug = "%[1]s" scope_type = "dcim.site" scope_id = netbox_site.test.id }`, testSlug) } ================================================ FILE: netbox/data_source_netbox_vlan_groups.go ================================================ package netbox import ( "errors" "fmt" "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxVlanGroups() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxVlanGroupsRead, Description: `:meta:subcategory:IP Address Management (IPAM):`, Schema: map[string]*schema.Schema{ "filter": { Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "value": { Type: schema.TypeString, Required: true, }, }, }, }, "limit": { Type: schema.TypeInt, Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), Default: 0, }, "vlan_groups": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, "slug": { Type: schema.TypeString, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, "ranges": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "start": { Type: schema.TypeInt, Computed: true, }, "end": { Type: schema.TypeInt, Computed: true, }, }, }, }, "used": { Type: schema.TypeInt, Computed: true, }, "tag_ids": { Type: schema.TypeList, Computed: true, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, }, }, }, }, } } func dataSourceNetboxVlanGroupsRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := ipam.NewIpamVlanGroupsListParams() // Get user limit (0 = fetch all) var userLimit int64 = 0 if limitValue, ok := d.GetOk("limit"); ok { userLimit = int64(limitValue.(int)) } if filter, ok := d.GetOk("filter"); ok { var filterParams = filter.(*schema.Set) var tags []string for _, f := range filterParams.List() { k := f.(map[string]interface{})["name"] v := f.(map[string]interface{})["value"] vString := v.(string) switch k { case "name": params.Name = strToPtr(vString) case "name__empty": params.NameEmpty = strToPtr(vString) case "name__ic": params.NameIc = strToPtr(vString) case "name__ie": params.NameIe = strToPtr(vString) case "name__iew": params.NameIew = strToPtr(vString) case "name__isw": params.NameIsw = strToPtr(vString) case "name__n": params.Namen = strToPtr(vString) case "name__nic": params.NameNic = strToPtr(vString) case "name__nie": params.NameNie = strToPtr(vString) case "name__niew": params.NameNiew = strToPtr(vString) case "name__nisw": params.NameNisw = strToPtr(vString) case "slug": params.Slug = strToPtr(vString) case "slug__empty": params.SlugEmpty = strToPtr(vString) case "slug__ic": params.SlugIc = strToPtr(vString) case "slug__ie": params.SlugIe = strToPtr(vString) case "slug__iew": params.SlugIew = strToPtr(vString) case "slug__isw": params.SlugIsw = strToPtr(vString) case "slug__n": params.Slugn = strToPtr(vString) case "slug__nic": params.SlugNic = strToPtr(vString) case "slug__nie": params.SlugNie = strToPtr(vString) case "slug__niew": params.SlugNiew = strToPtr(vString) case "slug__nisw": params.SlugNisw = strToPtr(vString) case "description": params.Description = strToPtr(vString) case "description__empty": params.DescriptionEmpty = strToPtr(vString) case "description__ic": params.DescriptionIc = strToPtr(vString) case "description__ie": params.DescriptionIe = strToPtr(vString) case "description__iew": params.DescriptionIew = strToPtr(vString) case "description__isw": params.DescriptionIsw = strToPtr(vString) case "description__n": params.Descriptionn = strToPtr(vString) case "description__nic": params.DescriptionNic = strToPtr(vString) case "description__nie": params.DescriptionNie = strToPtr(vString) case "description__niew": params.DescriptionNiew = strToPtr(vString) case "description__nisw": params.DescriptionNisw = strToPtr(vString) case "id": params.ID = strToPtr(vString) case "id__gt": params.IDGt = strToPtr(vString) case "id__gte": params.IDGte = strToPtr(vString) case "id__lt": params.IDLt = strToPtr(vString) case "id__lte": params.IDLte = strToPtr(vString) case "id__n": params.IDn = strToPtr(vString) case "scope_type": params.ScopeType = strToPtr(vString) case "scope_type__n": params.ScopeTypen = strToPtr(vString) case "scope_id": params.ScopeID = strToPtr(vString) case "scope_id__gt": params.ScopeIDGt = strToPtr(vString) case "scope_id__gte": params.ScopeIDGte = strToPtr(vString) case "scope_id__lt": params.ScopeIDLt = strToPtr(vString) case "scope_id__lte": params.ScopeIDLte = strToPtr(vString) case "scope_id__n": params.ScopeIDn = strToPtr(vString) case "site_id", "site": if f, err := strconv.ParseFloat(vString, 64); err == nil { params.Site = float64ToPtr(f) } case "location_id", "location": if f, err := strconv.ParseFloat(vString, 64); err == nil { params.Location = float64ToPtr(f) } case "rack_id", "rack": if f, err := strconv.ParseFloat(vString, 64); err == nil { params.Rack = float64ToPtr(f) } case "region_id", "region": if f, err := strconv.ParseFloat(vString, 64); err == nil { params.Region = float64ToPtr(f) } case "sitegroup_id", "sitegroup": if f, err := strconv.ParseFloat(vString, 64); err == nil { params.Sitegroup = float64ToPtr(f) } case "cluster_id", "cluster": if f, err := strconv.ParseFloat(vString, 64); err == nil { params.Cluster = float64ToPtr(f) } case "clustergroup_id", "clustergroup": if f, err := strconv.ParseFloat(vString, 64); err == nil { params.Clustergroup = float64ToPtr(f) } case "minvid": params.MinVid = strToPtr(vString) case "minvid__gt": params.MinVidGt = strToPtr(vString) case "minvid__gte": params.MinVidGte = strToPtr(vString) case "minvid__lt": params.MinVidLt = strToPtr(vString) case "minvid__lte": params.MinVidLte = strToPtr(vString) case "minvid__n": params.MinVidn = strToPtr(vString) case "maxvid": params.MaxVid = strToPtr(vString) case "maxvid__gt": params.MaxVidGt = strToPtr(vString) case "maxvid__gte": params.MaxVidGte = strToPtr(vString) case "maxvid__lt": params.MaxVidLt = strToPtr(vString) case "maxvid__lte": params.MaxVidLte = strToPtr(vString) case "maxvid__n": params.MaxVidn = strToPtr(vString) case "tag": tags = append(tags, vString) params.Tag = tags default: return fmt.Errorf("'%s' is not a supported filter parameter", k) } } } // Fetch all pages with pagination paginationHelper := NewPaginationHelper(userLimit) var allVlanGroups []*models.VLANGroup pageSize := paginationHelper.GetPageSize() for { currentOffset := paginationHelper.CurrentOffset() params.Limit = &pageSize params.Offset = ¤tOffset res, err := api.Ipam.IpamVlanGroupsList(params, nil) if err != nil { return fmt.Errorf("failed to fetch VLAN groups at offset %d: %w", currentOffset, err) } payload := res.GetPayload() allVlanGroups = append(allVlanGroups, payload.Results...) if len(payload.Results) == 0 { break } if !paginationHelper.ShouldContinuePaging(int64(len(allVlanGroups)), payload.Next) { break } paginationHelper.Advance(int64(len(payload.Results))) } // Trim to user limit if specified trimmedCount := paginationHelper.TrimToLimit(len(allVlanGroups)) filteredVlanGroups := allVlanGroups[:trimmedCount] if len(filteredVlanGroups) == 0 { return errors.New("no result") } var s []map[string]interface{} for _, vg := range filteredVlanGroups { var mapping = make(map[string]interface{}) mapping["id"] = vg.ID mapping["name"] = vg.Name mapping["slug"] = vg.Slug mapping["description"] = vg.Description mapping["ranges"] = []map[string]int64{} for _, v := range vg.VidRanges { mapping["ranges"] = append(mapping["ranges"].([]map[string]int64), map[string]int64{ "start": v[0], "end": v[1], }) } mapping["used"] = vg.VlanCount if vg.Tags != nil { var tagIDs []int64 for _, t := range vg.Tags { tagIDs = append(tagIDs, t.ID) } mapping["tag_ids"] = tagIDs } s = append(s, mapping) } d.SetId(id.UniqueId()) return d.Set("vlan_groups", s) } ================================================ FILE: netbox/data_source_netbox_vlan_groups_test.go ================================================ package netbox import ( "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxVlanGroupsSetUp() string { return ` resource "netbox_vlan_group" "test_1" { name = "VLANGroup1" slug = "vlangroup1" vid_ranges = [[100, 200]] } resource "netbox_vlan_group" "test_2" { name = "VLANGroup2" slug = "vlangroup2" vid_ranges = [[300, 400]] } resource "netbox_vlan_group" "test_3" { name = "VLANGroup3" slug = "vlangroup3" vid_ranges = [[500, 600]] }` } func testAccNetboxVlanGroupsByName() string { return ` data "netbox_vlan_groups" "test" { filter { name = "name" value = "VLANGroup1" } }` } func testAccNetboxVlanGroupsBySlug() string { return ` data "netbox_vlan_groups" "test" { filter { name = "slug" value = "vlangroup2" } }` } func testAccNetboxVlanGroupsWithNameIsw() string { return ` data "netbox_vlan_groups" "test" { filter { name = "name__isw" value = "VLANGroup" } }` } func testAccNetboxVlanGroupsByID() string { return ` data "netbox_vlan_groups" "test" { filter { name = "id" value = netbox_vlan_group.test_1.id } }` } func testAccNetboxVlanGroupsWithLimit() string { return ` data "netbox_vlan_groups" "test" { filter { name = "name__isw" value = "VLANGroup" } limit = 2 }` } func TestAccNetboxVlanGroupsDataSource_basic(t *testing.T) { setUp := testAccNetboxVlanGroupsSetUp() resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: setUp, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_vlan_group.test_1", "name", "VLANGroup1"), resource.TestCheckResourceAttr("netbox_vlan_group.test_1", "slug", "vlangroup1"), ), }, { Config: setUp + testAccNetboxVlanGroupsByName(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_vlan_groups.test", "vlan_groups.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_vlan_groups.test", "vlan_groups.0.name", "netbox_vlan_group.test_1", "name"), resource.TestCheckResourceAttrPair("data.netbox_vlan_groups.test", "vlan_groups.0.slug", "netbox_vlan_group.test_1", "slug"), ), }, { Config: setUp + testAccNetboxVlanGroupsBySlug(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_vlan_groups.test", "vlan_groups.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_vlan_groups.test", "vlan_groups.0.slug", "netbox_vlan_group.test_2", "slug"), ), }, }, }) } func TestAccNetboxVlanGroupsDataSource_search(t *testing.T) { setUp := testAccNetboxVlanGroupsSetUp() resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: setUp, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_vlan_group.test_1", "name", "VLANGroup1"), ), }, { Config: setUp + testAccNetboxVlanGroupsWithNameIsw(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_vlan_groups.test", "vlan_groups.#", "3"), resource.TestCheckResourceAttrPair("data.netbox_vlan_groups.test", "vlan_groups.0.slug", "netbox_vlan_group.test_1", "slug"), resource.TestCheckResourceAttrPair("data.netbox_vlan_groups.test", "vlan_groups.1.slug", "netbox_vlan_group.test_2", "slug"), resource.TestCheckResourceAttrPair("data.netbox_vlan_groups.test", "vlan_groups.2.slug", "netbox_vlan_group.test_3", "slug"), ), }, }, }) } func TestAccNetboxVlanGroupsDataSource_byID(t *testing.T) { setUp := testAccNetboxVlanGroupsSetUp() resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: setUp, }, { Config: setUp + testAccNetboxVlanGroupsByID(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_vlan_groups.test", "vlan_groups.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_vlan_groups.test", "vlan_groups.0.name", "netbox_vlan_group.test_1", "name"), ), }, }, }) } func TestAccNetboxVlanGroupsDataSource_limit(t *testing.T) { setUp := testAccNetboxVlanGroupsSetUp() resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: setUp, }, { Config: setUp + testAccNetboxVlanGroupsWithLimit(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_vlan_groups.test", "vlan_groups.#", "2"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_vlan_test.go ================================================ package netbox import ( "fmt" "regexp" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxVlanDataSource_basic(t *testing.T) { testVid := 4092 testName := testAccGetTestName("vlan") setUp := testAccNetboxVlanSetUp(testVid, testName) extendedSetUp := testAccNetboxVlanSetUpMore(testVid, testVid-1, testName) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: setUp, }, { Config: setUp + testAccNetboxVlanDataNoResult, ExpectError: regexp.MustCompile("no vlan found matching filter"), }, { Config: setUp + testAccNetboxVlanDataByName(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_vlan.test", "id", "netbox_vlan.test", "id"), ), }, { Config: setUp + testAccNetboxVlanDataByVid(testVid), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_vlan.test", "id", "netbox_vlan.test", "id"), ), }, { Config: setUp + extendedSetUp + testAccNetboxVlanDataByNameAndRole(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_vlan.test", "id", "netbox_vlan.test", "id"), ), }, { Config: setUp + extendedSetUp + testAccNetboxVlanDataByVidAndTenant(testVid), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_vlan.test", "id", "netbox_vlan.test", "id"), resource.TestCheckResourceAttr("data.netbox_vlan.test", "name", testName), resource.TestCheckResourceAttr("data.netbox_vlan.test", "status", "active"), resource.TestCheckResourceAttr("data.netbox_vlan.test", "description", "Test"), resource.TestCheckResourceAttrPair("data.netbox_vlan.test", "role", "netbox_ipam_role.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_vlan.test", "site", "netbox_site.test", "id"), resource.TestCheckResourceAttrPair("data.netbox_vlan.test", "tenant", "netbox_tenant.test", "id"), ), }, { Config: setUp + extendedSetUp + testAccNetboxVlanDataByName(testName), ExpectError: regexp.MustCompile("more than one vlan returned, specify a more narrow filter"), }, { Config: setUp + extendedSetUp + testAccNetboxVlanDataByVid(testVid), ExpectError: regexp.MustCompile("more than one vlan returned, specify a more narrow filter"), }, }, }) } func testAccNetboxVlanSetUp(testVid int, testName string) string { return fmt.Sprintf(` resource "netbox_ipam_role" "test" { name = "%[2]s" } resource "netbox_site" "test" { name = "%[2]s" } resource "netbox_tenant" "test" { name = "%[2]s" } resource "netbox_vlan" "test" { vid = %[1]d name = "%[2]s" description = "Test" role_id = netbox_ipam_role.test.id site_id = netbox_site.test.id status = "active" tags = [] tenant_id = netbox_tenant.test.id } `, testVid, testName) } func testAccNetboxVlanSetUpMore(testVid int, anotherVid int, testName string) string { return fmt.Sprintf(` resource "netbox_vlan" "same_name" { vid = %[1]d name = "%[3]s" } resource "netbox_vlan" "not_same" { vid = %[2]d name = "%[3]s_unique" } `, testVid, anotherVid, testName) } const testAccNetboxVlanDataNoResult = ` data "netbox_vlan" "no_result" { name = "_no_result_" }` func testAccNetboxVlanDataByName(testName string) string { return fmt.Sprintf(` data "netbox_vlan" "test" { name = "%[1]s" }`, testName) } func testAccNetboxVlanDataByVid(testVid int) string { return fmt.Sprintf(` data "netbox_vlan" "test" { vid = "%[1]d" }`, testVid) } func testAccNetboxVlanDataByNameAndRole(testName string) string { return fmt.Sprintf(` data "netbox_vlan" "test" { name = "%[1]s" role = netbox_ipam_role.test.id }`, testName) } func testAccNetboxVlanDataByVidAndTenant(testVid int) string { return fmt.Sprintf(` data "netbox_vlan" "test" { vid = "%[1]d" tenant = netbox_tenant.test.id }`, testVid) } ================================================ FILE: netbox/data_source_netbox_vlans.go ================================================ package netbox import ( "errors" "fmt" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxVlans() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxVlansRead, Description: `:meta:subcategory:IP Address Management (IPAM):`, Schema: map[string]*schema.Schema{ "filter": { Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "value": { Type: schema.TypeString, Required: true, }, }, }, }, "limit": { Type: schema.TypeInt, Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), Default: 0, }, "vlans": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "vid": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, "group_id": { Type: schema.TypeInt, Computed: true, }, "role": { Type: schema.TypeInt, Computed: true, }, "site": { Type: schema.TypeInt, Computed: true, }, "status": { Type: schema.TypeString, Computed: true, }, "tag_ids": { Type: schema.TypeList, Computed: true, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "tenant": { Type: schema.TypeInt, Computed: true, }, }, }, }, }, } } func dataSourceNetboxVlansRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := ipam.NewIpamVlansListParams() // Get user limit var userLimit int64 = 0 if limitValue, ok := d.GetOk("limit"); ok { userLimit = int64(limitValue.(int)) } if filter, ok := d.GetOk("filter"); ok { var filterParams = filter.(*schema.Set) var tags []string for _, f := range filterParams.List() { k := f.(map[string]interface{})["name"] v := f.(map[string]interface{})["value"] vString := v.(string) switch k { case "vid": params.Vid = &vString case "vid__gt": params.VidGt = &vString case "vid__gte": params.VidGte = &vString case "vid__lt": params.VidLt = &vString case "vid__lte": params.VidLte = &vString case "vid__n": params.Vidn = &vString case "group": params.Group = &vString case "group__n": params.Groupn = &vString case "group_id": params.GroupID = &vString case "group_id__n": params.GroupIDn = &vString case "tag": tags = append(tags, vString) params.Tag = tags case "tenant": params.Tenant = &vString case "tenant__n": params.Tenantn = &vString case "tenant_group": params.TenantGroup = &vString case "tenant_group__n": params.TenantGroupn = &vString case "tenant_group_id": params.TenantGroupID = &vString case "tenant_group_id__n": params.TenantGroupIDn = &vString case "tenant_id": params.TenantID = &vString case "tenant_id__n": params.TenantIDn = &vString case "site_id": params.SiteID = &vString case "site_id__n": params.SiteIDn = &vString case "status": params.Status = &vString default: return fmt.Errorf("'%s' is not a supported filter parameter", k) } } } // Fetch all pages with pagination paginationHelper := NewPaginationHelper(userLimit) var allVlans []*models.VLAN pageSize := paginationHelper.GetPageSize() for { currentOffset := paginationHelper.CurrentOffset() params.Limit = &pageSize params.Offset = ¤tOffset res, err := api.Ipam.IpamVlansList(params, nil) if err != nil { return fmt.Errorf("failed to fetch VLANs at offset %d: %w", currentOffset, err) } payload := res.GetPayload() allVlans = append(allVlans, payload.Results...) if len(payload.Results) == 0 { break } if !paginationHelper.ShouldContinuePaging(int64(len(allVlans)), payload.Next) { break } paginationHelper.Advance(int64(len(payload.Results))) } // Trim to user limit if specified trimmedCount := paginationHelper.TrimToLimit(len(allVlans)) filteredVlans := allVlans[:trimmedCount] if len(filteredVlans) == 0 { return errors.New("no result") } var s []map[string]interface{} for _, v := range filteredVlans { var mapping = make(map[string]interface{}) mapping["id"] = v.ID mapping["vid"] = v.Vid mapping["name"] = v.Name mapping["description"] = v.Description if v.Group != nil { mapping["group_id"] = v.Group.ID } mapping["vid"] = v.Vid if v.Role != nil { mapping["role"] = v.Role.ID } if v.Site != nil { mapping["site"] = v.Site.ID } mapping["status"] = v.Status.Value if v.Tenant != nil { mapping["tenant"] = v.Tenant.ID } if v.Tags != nil { var tagIDs []int64 for _, t := range v.Tags { tagIDs = append(tagIDs, t.ID) } mapping["tag_ids"] = tagIDs } s = append(s, mapping) } d.SetId(id.UniqueId()) return d.Set("vlans", s) } ================================================ FILE: netbox/data_source_netbox_vlans_test.go ================================================ package netbox import ( "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxVlansSetUp() string { return ` resource "netbox_vlan" "test_1" { name = "VLAN1234" vid = 1234 } resource "netbox_vlan" "test_2" { name = "VLAN1235" vid = 1235 } resource "netbox_vlan" "test_3" { name = "VLAN1236" vid = 1236 }` } func testAccNetboxVlansByVid() string { return ` data "netbox_vlans" "test" { filter { name = "vid" value = "1234" } }` } func testAccNetboxVlansByVidN() string { return ` data "netbox_vlans" "test" { filter { name = "vid__n" value = "1234" } }` } func testAccNetboxVlansByVidRange() string { return ` data "netbox_vlans" "test" { filter { name = "vid__gte" value = "1235" } filter { name = "vid__lte" value = "1236" } }` } func TestAccNetboxVlansDataSource_basic(t *testing.T) { setUp := testAccNetboxVlansSetUp() // This test cannot be run in parallel with other tests, because other tests create also Vlans // These Vlans then interfere with the __n filter test resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: setUp, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_vlan.test_1", "vid", "1234"), ), }, { Config: setUp + testAccNetboxVlansByVid(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_vlans.test", "vlans.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_vlans.test", "vlans.0.id", "netbox_vlan.test_1", "id"), ), }, { Config: setUp + testAccNetboxVlansByVid(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_vlans.test", "vlans.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_vlans.test", "vlans.0.vid", "netbox_vlan.test_1", "vid"), ), }, { Config: setUp + testAccNetboxVlansByVidN(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_vlans.test", "vlans.#", "2"), resource.TestCheckResourceAttrPair("data.netbox_vlans.test", "vlans.0.vid", "netbox_vlan.test_2", "vid"), ), }, { Config: setUp + testAccNetboxVlansByVidRange(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_vlans.test", "vlans.#", "2"), resource.TestCheckResourceAttrPair("data.netbox_vlans.test", "vlans.0.vid", "netbox_vlan.test_2", "vid"), resource.TestCheckResourceAttrPair("data.netbox_vlans.test", "vlans.1.vid", "netbox_vlan.test_3", "vid"), ), }, }, }) } ================================================ FILE: netbox/data_source_netbox_vrf.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceNetboxVrf() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxVrfRead, Description: `:meta:subcategory:IP Address Management (IPAM):`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, }, } } func dataSourceNetboxVrfRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) params := ipam.NewIpamVrfsListParams() params.Name = &name limit := int64(2) // Limit of 2 is enough params.Limit = &limit if tenantID, ok := d.Get("tenant_id").(int); ok && tenantID != 0 { // Note that tenant_id is a string pointer in the netbox filter, but we use a number in the provider params.TenantID = strToPtr(strconv.Itoa(tenantID)) } res, err := api.Ipam.IpamVrfsList(params, nil) if err != nil { return err } if *res.GetPayload().Count > int64(1) { return errors.New("more than one vrf returned, specify a more narrow filter") } if *res.GetPayload().Count == int64(0) { return errors.New("no vrf found matching filter") } result := res.GetPayload().Results[0] d.SetId(strconv.FormatInt(result.ID, 10)) d.Set("name", result.Name) if result.Tenant != nil { d.Set("tenant_id", result.Tenant.ID) } else { d.Set("tenant_id", nil) } return nil } ================================================ FILE: netbox/data_source_netbox_vrf_test.go ================================================ package netbox import ( "fmt" "regexp" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxVrfDataSource_basic(t *testing.T) { testSlug := "vrf_ds_basic" testName := testAccGetTestName(testSlug) simpleSetup := testAccNetboxVrfSetUp(testName) advancedSetup := testAccNetboxVrfAdvancedSetUp(testName) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: simpleSetup + testAccNetboxVrfData(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_vrf.test", "id", "netbox_vrf.test", "id"), ), }, { Config: simpleSetup + advancedSetup + testAccNetboxVrfData(testName), ExpectError: regexp.MustCompile("more than one vrf returned, specify a more narrow filter"), }, { Config: advancedSetup + testAccNetboxVrfDataWithoutTenantID(testName), ExpectError: regexp.MustCompile("more than one vrf returned, specify a more narrow filter"), }, { Config: simpleSetup + advancedSetup + testAccNetboxVrfDataWithTenantID(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("data.netbox_vrf.vrf_a", "id", "netbox_vrf.vrf_a", "id"), resource.TestCheckResourceAttrPair("data.netbox_vrf.vrf_b", "id", "netbox_vrf.vrf_b", "id"), ), }, }, }) } func testAccNetboxVrfSetUp(testName string) string { return fmt.Sprintf(` resource"netbox_vrf" "test" { name = "%[1]s" }`, testName) } func testAccNetboxVrfData(testName string) string { return fmt.Sprintf(` data "netbox_vrf" "test" { depends_on = [netbox_vrf.test] name = "%[1]s" }`, testName) } func testAccNetboxVrfAdvancedSetUp(testName string) string { return fmt.Sprintf(` resource "netbox_tenant" "tenant_a" { name = "%[1]s-a" } resource "netbox_tenant" "tenant_b" { name = "%[1]s-b" } resource "netbox_vrf" "vrf_a" { name = "%[1]s" tenant_id = netbox_tenant.tenant_a.id } resource "netbox_vrf" "vrf_b" { name = "%[1]s" tenant_id = netbox_tenant.tenant_b.id } `, testName) } func testAccNetboxVrfDataWithoutTenantID(testName string) string { return fmt.Sprintf(` data "netbox_vrf" "vrf_a" { name = "%[1]s" } `, testName) } func testAccNetboxVrfDataWithTenantID(testName string) string { return fmt.Sprintf(` data "netbox_vrf" "vrf_a" { name = "%[1]s" tenant_id = netbox_tenant.tenant_a.id } data "netbox_vrf" "vrf_b" { name = "%[1]s" tenant_id = netbox_tenant.tenant_b.id } `, testName) } ================================================ FILE: netbox/data_source_netbox_vrfs.go ================================================ package netbox import ( "errors" "fmt" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceNetboxVrfs() *schema.Resource { return &schema.Resource{ Read: dataSourceNetboxVrfsRead, Description: `:meta:subcategory:IP Address Management (IPAM):`, Schema: map[string]*schema.Schema{ "filter": { Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "value": { Type: schema.TypeString, Required: true, }, }, }, }, "limit": { Type: schema.TypeInt, Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), Default: 0, }, "vrfs": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "name": { Type: schema.TypeString, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, "rd": { Type: schema.TypeString, Computed: true, }, "tenant": { Type: schema.TypeInt, Computed: true, }, }, }, }, }, } } func dataSourceNetboxVrfsRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) params := ipam.NewIpamVrfsListParams() // Get user limit var userLimit int64 = 0 if limitValue, ok := d.GetOk("limit"); ok { userLimit = int64(limitValue.(int)) } if filter, ok := d.GetOk("filter"); ok { var filterParams = filter.(*schema.Set) var tags []string for _, f := range filterParams.List() { k := f.(map[string]interface{})["name"] v := f.(map[string]interface{})["value"] vString := v.(string) switch k { case "id": params.ID = &vString case "name": params.Name = &vString case "description": params.Description = &vString case "rd": params.Rd = &vString case "tenant": params.Tenant = &vString case "tenant__n": params.Tenantn = &vString case "tenant_group": params.TenantGroup = &vString case "tenant_group__n": params.TenantGroupn = &vString case "tenant_group_id": params.TenantGroupID = &vString case "tenant_group_id__n": params.TenantGroupIDn = &vString case "tenant_id": params.TenantID = &vString case "tenant_id__n": params.TenantIDn = &vString case "tag": tags = append(tags, vString) params.Tag = tags default: return fmt.Errorf("'%s' is not a supported filter parameter", k) } } } // Fetch all pages with pagination paginationHelper := NewPaginationHelper(userLimit) var allVrfs []*models.VRF pageSize := paginationHelper.GetPageSize() for { currentOffset := paginationHelper.CurrentOffset() params.Limit = &pageSize params.Offset = ¤tOffset res, err := api.Ipam.IpamVrfsList(params, nil) if err != nil { return fmt.Errorf("failed to fetch VRFs at offset %d: %w", currentOffset, err) } payload := res.GetPayload() allVrfs = append(allVrfs, payload.Results...) if len(payload.Results) == 0 { break } if !paginationHelper.ShouldContinuePaging(int64(len(allVrfs)), payload.Next) { break } paginationHelper.Advance(int64(len(payload.Results))) } // Trim to user limit if specified trimmedCount := paginationHelper.TrimToLimit(len(allVrfs)) filteredVrfs := allVrfs[:trimmedCount] if len(filteredVrfs) == 0 { return errors.New("no result") } var s []map[string]interface{} for _, v := range filteredVrfs { var mapping = make(map[string]interface{}) mapping["id"] = v.ID mapping["name"] = v.Name mapping["description"] = v.Description if v.Rd != nil { mapping["rd"] = v.Rd } if v.Tenant != nil { mapping["tenant"] = v.Tenant.ID } s = append(s, mapping) } d.SetId(id.UniqueId()) return d.Set("vrfs", s) } ================================================ FILE: netbox/data_source_netbox_vrfs_test.go ================================================ package netbox import ( "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxVrfsSetUp() string { return ` resource "netbox_tag" "test" { name = "test_tag" } resource "netbox_vrf" "test_1" { name = "VRF1" } resource "netbox_vrf" "test_2" { name = "VRF2" tags = [netbox_tag.test.name] } resource "netbox_vrf" "test_3" { name = "VRF3" }` } func testAccNetboxVrfsByName() string { return ` data "netbox_vrfs" "test" { filter { name = "name" value = "VRF1" } }` } func testAccNetboxVrfsByTag() string { return ` data "netbox_vrfs" "test" { filter { name = "tag" value = netbox_tag.test.name } }` } func TestAccNetboxVrfsDataSource_basic(t *testing.T) { setUp := testAccNetboxVrfsSetUp() resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: setUp, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_vrf.test_1", "name", "VRF1"), resource.TestCheckResourceAttr("netbox_vrf.test_2", "name", "VRF2"), resource.TestCheckResourceAttr("netbox_vrf.test_3", "name", "VRF3"), ), }, { Config: setUp + testAccNetboxVrfsByName(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_vrfs.test", "vrfs.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_vrfs.test", "vrfs.0.name", "netbox_vrf.test_1", "name"), ), }, { Config: setUp + testAccNetboxVrfsByTag(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.netbox_vrfs.test", "vrfs.#", "1"), resource.TestCheckResourceAttrPair("data.netbox_vrfs.test", "vrfs.0.name", "netbox_vrf.test_2", "name"), ), }, }, }) } ================================================ FILE: netbox/generic_objects.go ================================================ package netbox import ( "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var genericObjectSchema = &schema.Resource{ Schema: map[string]*schema.Schema{ "object_type": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringInSlice([]string{ // known supported generic objects (wherever the schema is used) // can be made into a parameter when importing this schema if desired "dcim.powerport", "dcim.poweroutlet", "dcim.powerfeed", "dcim.frontport", "dcim.rearport", "dcim.consoleserverport", "dcim.consoleport", "dcim.interface", "circuits.circuittermination", }, false), }, "object_id": { Type: schema.TypeInt, Required: true, }, }, } func getGenericObjectsFromSchemaSet(schemaSet *schema.Set) []*models.GenericObject { retArr := make([]*models.GenericObject, 0, schemaSet.Len()) for _, i := range schemaSet.List() { retArr = append(retArr, &models.GenericObject{ ObjectID: int64ToPtr(int64(i.(map[string]interface{})["object_id"].(int))), ObjectType: strToPtr(i.(map[string]interface{})["object_type"].(string)), }) } return retArr } func getSchemaSetFromGenericObjects(objects []*models.GenericObject) []map[string]interface{} { retArr := make([]map[string]interface{}, 0, len(objects)) for _, obj := range objects { mapping := make(map[string]interface{}) mapping["object_type"] = obj.ObjectType mapping["object_id"] = obj.ObjectID retArr = append(retArr, mapping) } return retArr } ================================================ FILE: netbox/netbox_sweeper_test.go ================================================ package netbox import ( "os" "testing" "github.com/fbreckle/go-netbox/netbox/client" httptransport "github.com/go-openapi/runtime/client" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) // sweeperNetboxClients is a shared cache of netbox clients // This prevents client re-initialization for every resource with no benefit var sweeperNetboxClients map[string]interface{} func TestMain(m *testing.M) { resource.TestMain(m) } // sharedClientForRegion returns a common provider client configured for the specified region func sharedClientForRegion(region string) (interface{}, error) { if client, ok := sweeperNetboxClients[region]; ok { return client, nil } server := os.Getenv("NETBOX_SERVER") apiToken := os.Getenv("NETBOX_API_TOKEN") transport := httptransport.New(server, client.DefaultBasePath, []string{"http"}) transport.DefaultAuthentication = httptransport.APIKeyAuth("Authorization", "header", "Token "+apiToken) c := client.New(transport, nil) return c, nil } ================================================ FILE: netbox/pagination.go ================================================ package netbox import "github.com/go-openapi/strfmt" const ( // DefaultPageSize balances API response time, request count, and memory usage DefaultPageSize = 100 // FetchAll instructs the pagination helper to fetch all results (no user-imposed limit) FetchAll int64 = 0 ) // PaginatedListHelper manages automatic pagination for NetBox API list endpoints type PaginatedListHelper struct { userLimit int64 // Maximum results to return (0 = fetch all) pageSize int64 // Items per API request offset int64 // Current offset, advanced by Advance() } // NewPaginationHelper creates a helper with the given user limit func NewPaginationHelper(userLimit int64) *PaginatedListHelper { return &PaginatedListHelper{ userLimit: userLimit, pageSize: DefaultPageSize, } } // CurrentOffset returns the offset to use for the next API request func (h *PaginatedListHelper) CurrentOffset() int64 { return h.offset } // Advance moves the offset forward by the number of items returned in the last page. // Must be called with the actual returned count, not the requested page size, to correctly // handle servers that cap responses below the requested limit (NetBox MAX_PAGE_SIZE). func (h *PaginatedListHelper) Advance(returned int64) { h.offset += returned } // ShouldContinuePaging determines if another page should be fetched func (h *PaginatedListHelper) ShouldContinuePaging(currentCount int64, next *strfmt.URI) bool { if h.userLimit > 0 && currentCount >= h.userLimit { return false } return next != nil && *next != "" } // TrimToLimit trims results to user's limit if specified func (h *PaginatedListHelper) TrimToLimit(count int) int { if h.userLimit > 0 && int64(count) > h.userLimit { return int(h.userLimit) } return count } // GetPageSize returns the configured page size func (h *PaginatedListHelper) GetPageSize() int64 { return h.pageSize } ================================================ FILE: netbox/pagination_test.go ================================================ package netbox import ( "strconv" "testing" "github.com/go-openapi/strfmt" ) func TestPaginationHelper_ShouldContinuePaging(t *testing.T) { tests := []struct { name string userLimit int64 currentCount int64 next *strfmt.URI want bool }{ { name: "no limit, has next", userLimit: 0, currentCount: 50, next: uriPtr("http://example.com/api?offset=50"), want: true, }, { name: "no limit, no next", userLimit: 0, currentCount: 50, next: nil, want: false, }, { name: "no limit, empty next", userLimit: 0, currentCount: 50, next: uriPtr(""), want: false, }, { name: "at limit, has next", userLimit: 100, currentCount: 100, next: uriPtr("http://example.com/api?offset=100"), want: false, }, { name: "under limit, has next", userLimit: 100, currentCount: 50, next: uriPtr("http://example.com/api?offset=50"), want: true, }, { name: "over limit, has next", userLimit: 100, currentCount: 150, next: uriPtr("http://example.com/api?offset=150"), want: false, }, { name: "under limit, no next", userLimit: 100, currentCount: 50, next: nil, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { helper := NewPaginationHelper(tt.userLimit) got := helper.ShouldContinuePaging(tt.currentCount, tt.next) if got != tt.want { t.Errorf("ShouldContinuePaging() = %v, want %v", got, tt.want) } }) } } func TestPaginationHelper_TrimToLimit(t *testing.T) { tests := []struct { name string userLimit int64 count int want int }{ { name: "no limit, no trim", userLimit: 0, count: 150, want: 150, }, { name: "under limit, no trim", userLimit: 100, count: 50, want: 50, }, { name: "at limit, no trim", userLimit: 100, count: 100, want: 100, }, { name: "over limit, trim to limit", userLimit: 100, count: 150, want: 100, }, { name: "way over limit, trim to limit", userLimit: 50, count: 500, want: 50, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { helper := NewPaginationHelper(tt.userLimit) got := helper.TrimToLimit(tt.count) if got != tt.want { t.Errorf("TrimToLimit() = %d, want %d", got, tt.want) } }) } } func TestPaginationHelper_GetPageSize(t *testing.T) { helper := NewPaginationHelper(0) if got := helper.GetPageSize(); got != DefaultPageSize { t.Errorf("GetPageSize() = %d, want %d", got, DefaultPageSize) } } // TestPaginationHelper_ServerCapOffset verifies correct offset advancement when a // NetBox server has MAX_PAGE_SIZE set below DefaultPageSize (100). In that case the // server returns fewer items per page than requested, so the offset must advance by // len(results) — not by the requested page size — to avoid skipping items. // // Example: MAX_PAGE_SIZE=50, DefaultPageSize=100, total=150 items // // Correct (offset += len(results)=50): // // req offset=0 → server returns 50 items (0–49), offset becomes 50 // req offset=50 → server returns 50 items (50–99), offset becomes 100 // req offset=100 → server returns 50 items (100–149), done // collected: 150 ✓ // // Incorrect (offset += pageSize=100): // // req offset=0 → server returns 50 items (0–49), offset becomes 100 // req offset=100 → server returns 50 items (100–149), done // collected: 100 — items 50–99 silently skipped func TestPaginationHelper_ServerCapOffset(t *testing.T) { // simulatePage mimics NetBox's OptionalLimitOffsetPagination: // returns min(serverCap, requestedLimit, remaining) items and whether a next page exists. simulatePage := func(totalItems, serverCap, offset, requestedLimit int) (returned int, next *strfmt.URI) { actualLimit := requestedLimit if serverCap < actualLimit { actualLimit = serverCap } remaining := totalItems - offset if remaining <= 0 { return 0, nil } returned = actualLimit if remaining < returned { returned = remaining } if offset+returned < totalItems { u := strfmt.URI("http://netbox/api/?offset=" + strconv.Itoa(offset+returned)) return returned, &u } return returned, nil } tests := []struct { name string totalItems int serverCap int // simulates NetBox MAX_PAGE_SIZE wantCollected int }{ { // 50+50+30 — last page is partial name: "server cap below page size", totalItems: 130, serverCap: 50, wantCollected: 130, }, { // 100+100+10 — last page is partial name: "server cap equal to page size", totalItems: 210, serverCap: 100, wantCollected: 210, }, { // 100+30 — last page is partial name: "server cap above page size", totalItems: 130, serverCap: 1000, wantCollected: 130, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pageSize := int(DefaultPageSize) helper := NewPaginationHelper(FetchAll) collected := 0 for { returned, next := simulatePage(tt.totalItems, tt.serverCap, int(helper.CurrentOffset()), pageSize) collected += returned if returned == 0 { break } if !helper.ShouldContinuePaging(int64(collected), next) { break } helper.Advance(int64(returned)) } if collected != tt.wantCollected { t.Errorf("collected %d, want %d", collected, tt.wantCollected) } }) } } // Helper function to create URI pointers for tests func uriPtr(s string) *strfmt.URI { uri := strfmt.URI(s) return &uri } ================================================ FILE: netbox/provider.go ================================================ package netbox import ( "bytes" "context" "fmt" "strings" "github.com/fbreckle/go-netbox/netbox/client" "github.com/fbreckle/go-netbox/netbox/client/status" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "golang.org/x/exp/slices" ) type providerState struct { *client.NetBoxAPI defaultTags *schema.Set // concurrent access ok, only populated on provider start tagCache map[string]*models.NestedTag } // This makes the description contain the default value, particularly useful for the docs // From https://github.com/hashicorp/terraform-plugin-docs/issues/65#issuecomment-1152842370 func init() { schema.DescriptionKind = schema.StringMarkdown schema.SchemaDescriptionBuilder = func(s *schema.Schema) string { desc := s.Description desc = strings.TrimSpace(desc) if !bytes.HasSuffix([]byte(s.Description), []byte(".")) && s.Description != "" { desc += "." } if len(s.AtLeastOneOf) > 0 { atLeastOneOf := make([]string, len(s.AtLeastOneOf)) for i, l := range s.AtLeastOneOf { atLeastOneOf[i] = fmt.Sprintf("`%s`", l) } desc += fmt.Sprintf(" At least one of %s must be given.", joinStringWithFinalConjunction(atLeastOneOf, ", ", "or")) } if len(s.ExactlyOneOf) > 0 { exactlyOneOf := make([]string, len(s.ExactlyOneOf)) for i, l := range s.ExactlyOneOf { exactlyOneOf[i] = fmt.Sprintf("`%s`", l) } desc += fmt.Sprintf(" Exactly one of %s must be given.", joinStringWithFinalConjunction(exactlyOneOf, ", ", "or")) } if len(s.RequiredWith) > 0 { requires := make([]string, len(s.RequiredWith)) for i, c := range s.RequiredWith { requires[i] = fmt.Sprintf("`%s`", c) } desc += fmt.Sprintf(" Required when %s is set.", joinStringWithFinalConjunction(requires, ", ", "and")) } if len(s.ConflictsWith) > 0 { conflicts := make([]string, len(s.ConflictsWith)) for i, c := range s.ConflictsWith { conflicts[i] = fmt.Sprintf("`%s`", c) } desc += fmt.Sprintf(" Conflicts with %s.", joinStringWithFinalConjunction(conflicts, ", ", "and")) } if s.Default != nil { if s.Default == "" { desc += " Defaults to `\"\"`." } else { desc += fmt.Sprintf(" Defaults to `%v`.", s.Default) } } return strings.TrimSpace(desc) } } // Provider returns a schema.Provider for Netbox. func Provider() *schema.Provider { provider := &schema.Provider{ ResourcesMap: map[string]*schema.Resource{ "netbox_available_ip_address": resourceNetboxAvailableIPAddress(), "netbox_virtual_machine": resourceNetboxVirtualMachine(), "netbox_cluster_type": resourceNetboxClusterType(), "netbox_cluster": resourceNetboxCluster(), "netbox_contact": resourceNetboxContact(), "netbox_contact_group": resourceNetboxContactGroup(), "netbox_contact_assignment": resourceNetboxContactAssignment(), "netbox_contact_role": resourceNetboxContactRole(), "netbox_device": resourceNetboxDevice(), "netbox_device_interface": resourceNetboxDeviceInterface(), "netbox_device_type": resourceNetboxDeviceType(), "netbox_manufacturer": resourceNetboxManufacturer(), "netbox_tenant": resourceNetboxTenant(), "netbox_tenant_group": resourceNetboxTenantGroup(), "netbox_vrf": resourceNetboxVrf(), "netbox_ip_address": resourceNetboxIPAddress(), "netbox_interface_template": resourceNetboxInterfaceTemplate(), "netbox_interface": resourceNetboxInterface(), "netbox_service": resourceNetboxService(), "netbox_platform": resourceNetboxPlatform(), "netbox_prefix": resourceNetboxPrefix(), "netbox_available_prefix": resourceNetboxAvailablePrefix(), "netbox_primary_ip": resourceNetboxPrimaryIP(), "netbox_device_primary_ip": resourceNetboxDevicePrimaryIP(), "netbox_device_role": resourceNetboxDeviceRole(), "netbox_tag": resourceNetboxTag(), "netbox_cluster_group": resourceNetboxClusterGroup(), "netbox_site": resourceNetboxSite(), "netbox_vlan": resourceNetboxVlan(), "netbox_vlan_group": resourceNetboxVlanGroup(), "netbox_available_vlan": resourceNetboxAvailableVLAN(), "netbox_ipam_role": resourceNetboxIpamRole(), "netbox_ip_range": resourceNetboxIPRange(), "netbox_region": resourceNetboxRegion(), "netbox_aggregate": resourceNetboxAggregate(), "netbox_rir": resourceNetboxRir(), "netbox_route_target": resourceNetboxRouteTarget(), "netbox_circuit": resourceNetboxCircuit(), "netbox_circuit_type": resourceNetboxCircuitType(), "netbox_circuit_provider": resourceNetboxCircuitProvider(), "netbox_circuit_termination": resourceNetboxCircuitTermination(), "netbox_user": resourceNetboxUser(), "netbox_group": resourceNetboxGroup(), "netbox_permission": resourceNetboxPermission(), "netbox_token": resourceNetboxToken(), "netbox_custom_field": resourceCustomField(), "netbox_asn": resourceNetboxAsn(), "netbox_location": resourceNetboxLocation(), "netbox_site_group": resourceNetboxSiteGroup(), "netbox_rack": resourceNetboxRack(), "netbox_rack_type": resourceNetboxRackType(), "netbox_rack_role": resourceNetboxRackRole(), "netbox_rack_reservation": resourceNetboxRackReservation(), "netbox_cable": resourceNetboxCable(), "netbox_device_console_port": resourceNetboxDeviceConsolePort(), "netbox_device_console_server_port": resourceNetboxDeviceConsoleServerPort(), "netbox_device_power_port": resourceNetboxDevicePowerPort(), "netbox_device_power_outlet": resourceNetboxDevicePowerOutlet(), "netbox_device_front_port": resourceNetboxDeviceFrontPort(), "netbox_device_rear_port": resourceNetboxDeviceRearPort(), "netbox_device_module_bay": resourceNetboxDeviceModuleBay(), "netbox_device_bay": resourceNetboxDeviceBay(), "netbox_device_bay_template": resourceNetboxDeviceBayTemplate(), "netbox_module": resourceNetboxModule(), "netbox_module_type": resourceNetboxModuleType(), "netbox_power_feed": resourceNetboxPowerFeed(), "netbox_power_panel": resourceNetboxPowerPanel(), "netbox_inventory_item_role": resourceNetboxInventoryItemRole(), "netbox_inventory_item": resourceNetboxInventoryItem(), "netbox_webhook": resourceNetboxWebhook(), "netbox_custom_field_choice_set": resourceNetboxCustomFieldChoiceSet(), "netbox_virtual_chassis": resourceNetboxVirtualChassis(), "netbox_virtual_disk": resourceNetboxVirtualDisks(), "netbox_config_template": resourceNetboxConfigTemplate(), "netbox_event_rule": resourceNetboxEventRule(), "netbox_vpn_tunnel_group": resourceNetboxVpnTunnelGroup(), "netbox_vpn_tunnel": resourceNetboxVpnTunnel(), "netbox_vpn_tunnel_termination": resourceNetboxVpnTunnelTermination(), "netbox_config_context": resourceNetboxConfigContext(), "netbox_mac_address": resourceNetboxMACAddress(), "netbox_wireless_lan_group": resourceNetboxWirelessLANGroup(), "netbox_wireless_lan": resourceNetboxWirelessLAN(), }, DataSourcesMap: map[string]*schema.Resource{ "netbox_asn": dataSourceNetboxAsn(), "netbox_asns": dataSourceNetboxAsns(), "netbox_available_prefix": dataSourceNetboxAvailablePrefix(), "netbox_cluster": dataSourceNetboxCluster(), "netbox_clusters": dataSourceNetboxClusters(), "netbox_cluster_group": dataSourceNetboxClusterGroup(), "netbox_cluster_type": dataSourceNetboxClusterType(), "netbox_contact": dataSourceNetboxContact(), "netbox_contact_role": dataSourceNetboxContactRole(), "netbox_contact_group": dataSourceNetboxContactGroup(), "netbox_tenant": dataSourceNetboxTenant(), "netbox_tenants": dataSourceNetboxTenants(), "netbox_tenant_group": dataSourceNetboxTenantGroup(), "netbox_vrf": dataSourceNetboxVrf(), "netbox_vrfs": dataSourceNetboxVrfs(), "netbox_platform": dataSourceNetboxPlatform(), "netbox_prefix": dataSourceNetboxPrefix(), "netbox_prefixes": dataSourceNetboxPrefixes(), "netbox_devices": dataSourceNetboxDevices(), "netbox_device_role": dataSourceNetboxDeviceRole(), "netbox_device_type": dataSourceNetboxDeviceType(), "netbox_site": dataSourceNetboxSite(), "netbox_location": dataSourceNetboxLocation(), "netbox_locations": dataSourceNetboxLocations(), "netbox_tag": dataSourceNetboxTag(), "netbox_tags": dataSourceNetboxTags(), "netbox_virtual_machines": dataSourceNetboxVirtualMachine(), "netbox_interfaces": dataSourceNetboxInterfaces(), "netbox_device_interfaces": dataSourceNetboxDeviceInterfaces(), "netbox_device_power_ports": dataSourceNetboxDevicePowerPorts(), "netbox_ipam_role": dataSourceNetboxIPAMRole(), "netbox_route_target": dataSourceNetboxRouteTarget(), "netbox_ip_address": dataSourceNetboxIPAddress(), "netbox_ip_addresses": dataSourceNetboxIPAddresses(), "netbox_ip_range": dataSourceNetboxIPRange(), "netbox_ip_ranges": dataSourceNetboxIPRanges(), "netbox_region": dataSourceNetboxRegion(), "netbox_rir": dataSourceNetboxRir(), "netbox_vlan": dataSourceNetboxVlan(), "netbox_vlans": dataSourceNetboxVlans(), "netbox_vlan_group": dataSourceNetboxVlanGroup(), "netbox_vlan_groups": dataSourceNetboxVlanGroups(), "netbox_site_group": dataSourceNetboxSiteGroup(), "netbox_racks": dataSourceNetboxRacks(), "netbox_rack_role": dataSourceNetboxRackRole(), "netbox_config_context": dataSourceNetboxConfigContext(), "netbox_virtual_disk": dataSourceNetboxVirtualDisk(), "netbox_device_render_config": dataSourceNetboxDeviceRenderConfig(), }, Schema: map[string]*schema.Schema{ "server_url": { Type: schema.TypeString, Required: true, DefaultFunc: schema.EnvDefaultFunc("NETBOX_SERVER_URL", nil), Description: "Location of Netbox server including scheme (http or https) and optional port. Can be set via the `NETBOX_SERVER_URL` environment variable.", }, "api_token": { Type: schema.TypeString, Required: true, DefaultFunc: schema.EnvDefaultFunc("NETBOX_API_TOKEN", nil), Description: "Netbox API authentication token. Supports both v1 tokens (`Authorization: Token `) and v2 tokens (`Authorization: Bearer nbt_.`). V2 tokens are auto-detected by their `nbt_` prefix. Can be set via the `NETBOX_API_TOKEN` environment variable.", }, "allow_insecure_https": { Type: schema.TypeBool, Optional: true, DefaultFunc: schema.EnvDefaultFunc("NETBOX_ALLOW_INSECURE_HTTPS", false), Description: "Flag to set whether to allow https with invalid certificates. Can be set via the `NETBOX_ALLOW_INSECURE_HTTPS` environment variable. Defaults to `false`.", }, "headers": { Type: schema.TypeMap, Optional: true, DefaultFunc: schema.EnvDefaultFunc("NETBOX_HEADERS", map[string]interface{}{}), Description: "Set these header on all requests to Netbox. Can be set via the `NETBOX_HEADERS` environment variable.", }, "strip_trailing_slashes_from_url": { Type: schema.TypeBool, Optional: true, DefaultFunc: schema.EnvDefaultFunc("NETBOX_STRIP_TRAILING_SLASHES_FROM_URL", true), Description: "If true, strip trailing slashes from the `server_url` parameter and print a warning when doing so. Note that using trailing slashes in the `server_url` parameter will usually lead to errors. Can be set via the `NETBOX_STRIP_TRAILING_SLASHES_FROM_URL` environment variable. Defaults to `true`.", }, "skip_version_check": { Type: schema.TypeBool, Optional: true, DefaultFunc: schema.EnvDefaultFunc("NETBOX_SKIP_VERSION_CHECK", false), Description: "If true, do not try to determine the running Netbox version at provider startup. Disables warnings about possibly unsupported Netbox version. Also useful for local testing on terraform plans. Can be set via the `NETBOX_SKIP_VERSION_CHECK` environment variable. Defaults to `false`.", }, "request_timeout": { Type: schema.TypeInt, Optional: true, DefaultFunc: schema.EnvDefaultFunc("NETBOX_REQUEST_TIMEOUT", 10), Description: "Netbox API HTTP request timeout in seconds. Can be set via the `NETBOX_REQUEST_TIMEOUT` environment variable.", }, "default_tags": { Type: schema.TypeSet, Elem: &schema.Schema{ Type: schema.TypeString, }, Optional: true, Description: "Tags to add to every resource managed by this provider", }, "ca_cert_file": { Type: schema.TypeString, Optional: true, DefaultFunc: schema.EnvDefaultFunc("NETBOX_CA_CERT_FILE", nil), Description: "Path to a PEM-encoded CA certificate for verifying the Netbox server certificate. Can be set via the `NETBOX_CA_CERT_FILE` environment variable.", }, }, ConfigureContextFunc: providerConfigure, } // all resources that have tags get a custom diff function for _, def := range provider.ResourcesMap { if _, ok := def.Schema[tagsKey]; ok { def.Schema[tagsAllKey] = tagsAllSchema // add computed key for all tags if existingDiff := def.CustomizeDiff; existingDiff != nil { def.CustomizeDiff = customdiff.Sequence(existingDiff, tagsCustomDiff) } else { def.CustomizeDiff = tagsCustomDiff } } } return provider } func providerConfigure(ctx context.Context, data *schema.ResourceData) (interface{}, diag.Diagnostics) { var diags diag.Diagnostics config := Config{ APIToken: data.Get("api_token").(string), AllowInsecureHTTPS: data.Get("allow_insecure_https").(bool), Headers: data.Get("headers").(map[string]interface{}), RequestTimeout: data.Get("request_timeout").(int), StripTrailingSlashesFromURL: data.Get("strip_trailing_slashes_from_url").(bool), CACertFile: data.Get("ca_cert_file").(string), } serverURL := data.Get("server_url").(string) // Unless explicitly switched off, strip trailing slashes from the server url // Trailing slashes cause errors as seen in https://github.com/e-breuninger/terraform-provider-netbox/issues/198 // and https://github.com/e-breuninger/terraform-provider-netbox/issues/300 stripTrailingSlashesFromURL := data.Get("strip_trailing_slashes_from_url").(bool) if stripTrailingSlashesFromURL { trimmed := false // This is Go's poor man's while loop for strings.HasSuffix(serverURL, "/") { serverURL = strings.TrimRight(serverURL, "/") trimmed = true } if trimmed { diags = append(diags, diag.Diagnostic{ Severity: diag.Warning, Summary: "Stripped trailing slashes from the `server_url` parameter", Detail: "Trailing slashes in the `server_url` parameter lead to problems in most setups, so all trailing slashes were stripped. Use the `strip_trailing_slashes_from_url` parameter to disable this feature or remove all trailing slashes in the `server_url` to disable this warning.", }) } } config.ServerURL = serverURL netboxClient, clientError := config.Client() if clientError != nil { return nil, diag.FromErr(clientError) } // Unless explicitly switched off, use the client to retrieve the Netbox version // so we can determine compatibility of the provider with the used Netbox skipVersionCheck := data.Get("skip_version_check").(bool) if !skipVersionCheck { req := status.NewStatusListParams() res, err := netboxClient.Status.StatusList(req, nil) if err != nil { return nil, diag.FromErr(err) } netboxVersionStringFromAPI := res.GetPayload().(map[string]interface{})["netbox-version"].(string) netboxVersion, err := extractSemanticVersionFromString(netboxVersionStringFromAPI) if err != nil { return nil, diag.FromErr(fmt.Errorf("error extracting netbox version. try using the `skip_version_check` provider parameter to bypass this error. original error: %w", err)) } supportedVersions := []string{"4.3.0", "4.3.2", "4.3.3", "4.3.4", "4.3.5", "4.3.6", "4.3.7", "4.4.0", "4.4.1", "4.4.2", "4.4.4", "4.4.5", "4.4.6", "4.4.7", "4.4.8", "4.4.9", "4.4.10"} if !slices.Contains(supportedVersions, netboxVersion) { // Currently, there is no way to test these warnings. There is an issue to track this: https://github.com/hashicorp/terraform-plugin-sdk/issues/864 diags = append(diags, diag.Diagnostic{ Severity: diag.Warning, Summary: "Possibly unsupported Netbox version", Detail: fmt.Sprintf("Your Netbox reports version %v. From that, the provider extracted Netbox version %v.\nThe provider was successfully tested against the following versions:\n\n %v\n\nUnexpected errors may occur.", netboxVersionStringFromAPI, netboxVersion, strings.Join(supportedVersions, ", ")), }) } } tags, ok := data.Get("default_tags").(*schema.Set) tagCache := make(map[string]*models.NestedTag, tags.Len()) if ok { for _, tag := range tags.List() { if tagName, ok := tag.(string); ok { nbTag, err := findTag(netboxClient, tagName) if err != nil { d := diag.FromErr(fmt.Errorf("default tag not found: %w", err)) d[0].Severity = diag.Warning diags = append(diags, d...) } else { tagCache[tagName] = nbTag } } else { diags = append(diags, diag.Errorf("invalid type for default tag: %T", tag)...) } } } state := &providerState{ NetBoxAPI: netboxClient, defaultTags: schema.CopySet(tags), tagCache: tagCache, } return state, diags } func tagsCustomDiff(ctx context.Context, diff *schema.ResourceDiff, m interface{}) error { state := m.(*providerState) tagSet := diff.Get(tagsKey).(*schema.Set) allTags := tagSet.Union(state.defaultTags) // check if tags are already up-to-date if diff.Get(tagsAllKey).(*schema.Set).Equal(allTags) { return nil // nothing to do, same set } return diff.SetNew(tagsAllKey, allTags.List()) } ================================================ FILE: netbox/provider_test.go ================================================ package netbox import ( "context" "fmt" "os" "regexp" "strings" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) var testAccProviders map[string]*schema.Provider var testAccProvider *schema.Provider var testPrefix = "test" func init() { testAccProvider = Provider() testAccProviders = map[string]*schema.Provider{ "netbox": testAccProvider, } } func testAccGetTestName(testSlug string) string { randomString := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) return strings.Join([]string{testPrefix, testSlug, randomString}, "-") } func testAccGetTestToken() string { randomToken := acctest.RandStringFromCharSet(40, "0123456789") return randomToken } func testAccPreCheck(t *testing.T) { if v := os.Getenv("NETBOX_SERVER_URL"); v == "" { t.Fatal("NETBOX_SERVER_URL must be set for acceptance tests.") } if v := os.Getenv("NETBOX_API_TOKEN"); v == "" { t.Fatal("NETBOX_API_TOKEN must be set for acceptance tests.") } } func testProviderConfig(platform string) string { return fmt.Sprintf(` resource "netbox_platform" "testplatform" { name = "%s" }`, platform) } func providerInvalidConfigure() schema.ConfigureContextFunc { return func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { var diags diag.Diagnostics config := &Config{} config.ServerURL = "https://fake.netbox.server" config.APIToken = "1234567890" netboxClient, clientError := config.Client() if clientError != nil { return nil, diag.FromErr(clientError) } return &providerState{NetBoxAPI: netboxClient}, diags } } func TestAccNetboxProviderConfigure_failure(t *testing.T) { var testAccInvalidProviders map[string]*schema.Provider testAccInvalidProvider := Provider() testAccInvalidProvider.ConfigureContextFunc = providerInvalidConfigure() testAccInvalidProviders = map[string]*schema.Provider{ "netbox": testAccInvalidProvider, } resource.Test(t, resource.TestCase{ Providers: testAccInvalidProviders, Steps: []resource.TestStep{ { Config: testProviderConfig(acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)), ExpectError: regexp.MustCompile("Post \"https://fake.netbox.server/api/dcim/platforms/\": dial tcp: lookup fake.netbox.server.*: no such host"), }, }, }) } func TestAccNetboxProviderDefaultTags(t *testing.T) { defaultTag := fmt.Sprintf("managed-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) resource.Test(t, resource.TestCase{ ProviderFactories: map[string]func() (*schema.Provider, error){ "netbox": func() (*schema.Provider, error) { p := Provider() p.ConfigureContextFunc = func(ctx context.Context, rd *schema.ResourceData) (interface{}, diag.Diagnostics) { rd.Set("default_tags", []string{defaultTag}) return providerConfigure(ctx, rd) } return p, nil }, }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tag" "managed" { name = "%s" } resource "netbox_site" "testsite" { name = "%s" depends_on = [ netbox_tag.managed ] } `, defaultTag, acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum), ), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_site.testsite", "tags_all.#", "1"), resource.TestCheckResourceAttr("netbox_site.testsite", "tags_all.0", defaultTag), ), }, }, }) } ================================================ FILE: netbox/resource_netbox_aggregate.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxAggregate() *schema.Resource { return &schema.Resource{ Create: resourceNetboxAggregateCreate, Read: resourceNetboxAggregateRead, Update: resourceNetboxAggregateUpdate, Delete: resourceNetboxAggregateDelete, Description: `:meta:subcategory:IP Address Management (IPAM):From the [official documentation](https://docs.netbox.dev/en/stable/features/ipam/#aggregates): > NetBox allows us to specify the portions of IP space that are interesting to us by defining aggregates. Typically, an aggregate will correspond to either an allocation of public (globally routable) IP space granted by a regional authority, or a private (internally-routable) designation.`, Schema: map[string]*schema.Schema{ "prefix": { Type: schema.TypeString, Required: true, ValidateFunc: validation.IsCIDR, }, "description": { Type: schema.TypeString, Optional: true, }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, "rir_id": { Type: schema.TypeInt, Optional: true, }, tagsKey: tagsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxAggregateCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableAggregate{} prefix := d.Get("prefix").(string) description := d.Get("description").(string) data.Prefix = &prefix data.Description = description if tenantID, ok := d.GetOk("tenant_id"); ok { data.Tenant = int64ToPtr(int64(tenantID.(int))) } if rirID, ok := d.GetOk("rir_id"); ok { data.Rir = int64ToPtr(int64(rirID.(int))) } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } params := ipam.NewIpamAggregatesCreateParams().WithData(&data) res, err := api.Ipam.IpamAggregatesCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxAggregateRead(d, m) } func resourceNetboxAggregateRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamAggregatesReadParams().WithID(id) res, err := api.Ipam.IpamAggregatesRead(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamAggregatesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } d.Set("description", res.GetPayload().Description) if res.GetPayload().Prefix != nil { d.Set("prefix", res.GetPayload().Prefix) } if res.GetPayload().Tenant != nil { d.Set("tenant_id", res.GetPayload().Tenant.ID) } else { d.Set("tenant_id", nil) } if res.GetPayload().Rir != nil { d.Set("rir_id", res.GetPayload().Rir.ID) } else { d.Set("rir_id", nil) } api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxAggregateUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableAggregate{} prefix := d.Get("prefix").(string) description := d.Get("description").(string) data.Prefix = &prefix data.Description = description if tenantID, ok := d.GetOk("tenant_id"); ok { data.Tenant = int64ToPtr(int64(tenantID.(int))) } if rirID, ok := d.GetOk("rir_id"); ok { data.Rir = int64ToPtr(int64(rirID.(int))) } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } params := ipam.NewIpamAggregatesUpdateParams().WithID(id).WithData(&data) _, err = api.Ipam.IpamAggregatesUpdate(params, nil) if err != nil { return err } return resourceNetboxAggregateRead(d, m) } func resourceNetboxAggregateDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamAggregatesDeleteParams().WithID(id) _, err := api.Ipam.IpamAggregatesDelete(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamAggregatesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } d.SetId("") return nil } ================================================ FILE: netbox/resource_netbox_aggregate_test.go ================================================ package netbox import ( "fmt" "log" "testing" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxAggregate_basic(t *testing.T) { testPrefix := "1.1.1.0/25" testSlug := "aggregate" testDesc := "test aggregate" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_rir" "test" { name = "%s" slug = "%s" } resource "netbox_aggregate" "test" { prefix = "%s" description = "%s" rir_id = netbox_rir.test.id }`, testName, randomSlug, testPrefix, testDesc), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_aggregate.test", "prefix", testPrefix), resource.TestCheckResourceAttr("netbox_aggregate.test", "description", testDesc), resource.TestCheckResourceAttrPair("netbox_aggregate.test", "rir_id", "netbox_rir.test", "id"), ), }, { ResourceName: "netbox_aggregate.test", ImportState: true, ImportStateVerify: true, }, }, }) } func init() { resource.AddTestSweepers("netbox_aggregate", &resource.Sweeper{ Name: "netbox_aggregate", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := ipam.NewIpamAggregatesListParams() res, err := api.Ipam.IpamAggregatesList(params, nil) if err != nil { return err } for _, prefix := range res.GetPayload().Results { if len(prefix.Tags) > 0 && (prefix.Tags[0] == &models.NestedTag{Name: strToPtr("acctest"), Slug: strToPtr("acctest")}) { deleteParams := ipam.NewIpamAggregatesDeleteParams().WithID(prefix.ID) _, err := api.Ipam.IpamAggregatesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a aggregate") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_asn.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceNetboxAsn() *schema.Resource { return &schema.Resource{ Create: resourceNetboxAsnCreate, Read: resourceNetboxAsnRead, Update: resourceNetboxAsnUpdate, Delete: resourceNetboxAsnDelete, Description: `:meta:subcategory:IP Address Management (IPAM):From the [official documentation](https://docs.netbox.dev/en/stable/features/ipam/#asn): > ASN is short for Autonomous System Number. This identifier is used in the BGP protocol to identify which "autonomous system" a particular prefix is originating and transiting through. > > The AS number model within NetBox allows you to model some of this real-world relationship.`, Schema: map[string]*schema.Schema{ "asn": { Type: schema.TypeInt, Required: true, Description: "Value for the AS Number record", }, "rir_id": { Type: schema.TypeInt, Required: true, Description: "ID for the RIR for the AS Number record", }, "description": { Type: schema.TypeString, Optional: true, Description: "Description field for the AS Number record", }, "comments": { Type: schema.TypeString, Optional: true, Description: "Comments field for the AS Number record", }, tagsKey: tagsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxAsnCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableASN{} asn := int64(d.Get("asn").(int)) data.Asn = &asn rir := int64(d.Get("rir_id").(int)) data.Rir = &rir data.Description = d.Get("description").(string) data.Comments = d.Get("comments").(string) var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } params := ipam.NewIpamAsnsCreateParams().WithData(&data) res, err := api.Ipam.IpamAsnsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxAsnRead(d, m) } func resourceNetboxAsnRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamAsnsReadParams().WithID(id) res, err := api.Ipam.IpamAsnsRead(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamAsnsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } asn := res.GetPayload() d.Set("asn", asn.Asn) d.Set("rir_id", asn.Rir.ID) d.Set("description", asn.Description) d.Set("comments", asn.Comments) api.readTags(d, asn.Tags) return nil } func resourceNetboxAsnUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableASN{} asn := int64(d.Get("asn").(int)) data.Asn = &asn rir := int64(d.Get("rir_id").(int)) data.Rir = &rir data.Description = d.Get("description").(string) data.Comments = d.Get("comments").(string) var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } params := ipam.NewIpamAsnsUpdateParams().WithID(id).WithData(&data) _, err = api.Ipam.IpamAsnsUpdate(params, nil) if err != nil { return err } return resourceNetboxAsnRead(d, m) } func resourceNetboxAsnDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamAsnsDeleteParams().WithID(id) _, err := api.Ipam.IpamAsnsDelete(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamAsnsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_asn_test.go ================================================ package netbox import ( "fmt" "log" "testing" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxAsn_basic(t *testing.T) { testSlug := "asn_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]sa" } resource "netbox_rir" "test" { name = "%[1]s" } resource "netbox_asn" "test" { asn = 1337 rir_id = netbox_rir.test.id description = "test" comments = "test" tags = [netbox_tag.test.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_asn.test", "asn", "1337"), resource.TestCheckResourceAttr("netbox_asn.test", "description", "test"), resource.TestCheckResourceAttr("netbox_asn.test", "comments", "test"), resource.TestCheckResourceAttr("netbox_asn.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_asn.test", "tags.0", testName+"a"), ), }, { ResourceName: "netbox_asn.test", ImportState: true, ImportStateVerify: true, }, }, }) } //func TestAccNetboxAsn_customFields(t *testing.T) { // testSlug := "asn_detail" // testName := testAccGetTestName(testSlug) // testField := strings.ReplaceAll(testAccGetTestName(testSlug), "-", "_") // resource.Test(t, resource.TestCase{ // PreCheck: func() { testAccPreCheck(t) }, // Providers: testAccProviders, // Steps: []resource.TestStep{ // { // Config: fmt.Sprintf(` //resource "netbox_custom_field" "test" { // name = "%[1]s" // type = "text" // content_types = ["ipam.asn"] //} //resource "netbox_asn" "test" { // name = "%[2]s" // status = "active" // latitude = "12.123456" // longitude = "-13.123456" // timezone = "Africa/Johannesburg" // custom_fields = {"${netbox_custom_field.test.name}" = "81"} //}`, testField, testName), // Check: resource.ComposeTestCheckFunc( // resource.TestCheckResourceAttr("netbox_asn.test", "custom_fields."+testField, "81"), // resource.TestCheckResourceAttr("netbox_asn.test", "timezone", "Africa/Johannesburg"), // resource.TestCheckResourceAttr("netbox_asn.test", "latitude", "12.123456"), // resource.TestCheckResourceAttr("netbox_asn.test", "longitude", "-13.123456"), // ), // }, // }, // }) //} func init() { resource.AddTestSweepers("netbox_asn", &resource.Sweeper{ Name: "netbox_asn", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := ipam.NewIpamAsnsListParams() res, err := api.Ipam.IpamAsnsList(params, nil) if err != nil { return err } for _, asn := range res.GetPayload().Results { deleteParams := ipam.NewIpamAsnsDeleteParams().WithID(asn.ID) _, err := api.Ipam.IpamAsnsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted an asn") } return nil }, }) } ================================================ FILE: netbox/resource_netbox_available_ip_address.go ================================================ package netbox import ( "fmt" "strconv" "strings" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxAvailableIPAddress() *schema.Resource { return &schema.Resource{ Create: resourceNetboxAvailableIPAddressCreate, Read: resourceNetboxAvailableIPAddressRead, Update: resourceNetboxAvailableIPAddressUpdate, Delete: resourceNetboxAvailableIPAddressDelete, Description: `:meta:subcategory:IP Address Management (IPAM):Per [the docs](https://netbox.readthedocs.io/en/stable/models/ipam/ipaddress/): > An IP address comprises a single host address (either IPv4 or IPv6) and its subnet mask. Its mask should match exactly how the IP address is configured on an interface in the real world. > Like a prefix, an IP address can optionally be assigned to a VRF (otherwise, it will appear in the "global" table). IP addresses are automatically arranged under parent prefixes within their respective VRFs according to the IP hierarchya. > > Each IP address can also be assigned an operational status and a functional role. Statuses are hard-coded in NetBox and include the following: > * Active > * Reserved > * Deprecated > * DHCP > * SLAAC (IPv6 Stateless Address Autoconfiguration) This resource will retrieve the next available IP address from a given prefix or IP range (specified by ID)`, Schema: map[string]*schema.Schema{ "prefix_id": { Type: schema.TypeInt, Optional: true, ForceNew: true, ExactlyOneOf: []string{"prefix_id", "ip_range_id"}, }, "ip_range_id": { Type: schema.TypeInt, Optional: true, ForceNew: true, ExactlyOneOf: []string{"prefix_id", "ip_range_id"}, }, "ip_address": { Type: schema.TypeString, Computed: true, }, "interface_id": { Type: schema.TypeInt, Optional: true, RequiredWith: []string{"object_type"}, }, "object_type": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringInSlice(resourceNetboxIPAddressObjectTypeOptions, false), Description: buildValidValueDescription(resourceNetboxIPAddressObjectTypeOptions), RequiredWith: []string{"interface_id"}, }, "virtual_machine_interface_id": { Type: schema.TypeInt, Optional: true, ConflictsWith: []string{"interface_id", "device_interface_id"}, }, "device_interface_id": { Type: schema.TypeInt, Optional: true, ConflictsWith: []string{"interface_id", "virtual_machine_interface_id"}, }, "vrf_id": { Type: schema.TypeInt, Optional: true, }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, "status": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringInSlice(resourceNetboxIPAddressStatusOptions, false), Description: buildValidValueDescription(resourceNetboxIPAddressStatusOptions), Default: "active", }, "dns_name": { Type: schema.TypeString, Optional: true, // NetBox always converts DNS names to lowercase DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { return strings.EqualFold(old, new) }, }, "description": { Type: schema.TypeString, Optional: true, }, tagsKey: tagsSchema, "role": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringInSlice(resourceNetboxIPAddressRoleOptions, false), Description: buildValidValueDescription(resourceNetboxIPAddressRoleOptions), }, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxAvailableIPAddressCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) prefixID := int64(d.Get("prefix_id").(int)) vrfID := int64(int64(d.Get("vrf_id").(int))) rangeID := int64(d.Get("ip_range_id").(int)) nestedvrf := models.NestedVRF{ ID: vrfID, } data := models.AvailableIP{ Vrf: &nestedvrf, } if prefixID != 0 { params := ipam.NewIpamPrefixesAvailableIpsCreateParams().WithID(prefixID).WithData([]*models.AvailableIP{&data}) res, err := api.Ipam.IpamPrefixesAvailableIpsCreate(params, nil) if err != nil { return err } if len(res.Payload) == 0 { return fmt.Errorf("no available IP addresses in prefix %d", prefixID) } // Since we generated the ip_address, set that now d.SetId(strconv.FormatInt(res.Payload[0].ID, 10)) d.Set("ip_address", *res.Payload[0].Address) } if rangeID != 0 { params := ipam.NewIpamIPRangesAvailableIpsCreateParams().WithID(rangeID).WithData([]*models.AvailableIP{&data}) res, err := api.Ipam.IpamIPRangesAvailableIpsCreate(params, nil) if err != nil { return err } if len(res.Payload) == 0 { return fmt.Errorf("no available IP addresses in IP range %d", rangeID) } // Since we generated the ip_address, set that now d.SetId(strconv.FormatInt(res.Payload[0].ID, 10)) d.Set("ip_address", *res.Payload[0].Address) } return resourceNetboxAvailableIPAddressUpdate(d, m) } func resourceNetboxAvailableIPAddressRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamIPAddressesReadParams().WithID(id) res, err := api.Ipam.IpamIPAddressesRead(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamIPAddressesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } ipAddress := res.GetPayload() if ipAddress.AssignedObjectID != nil { vmInterfaceID := getOptionalInt(d, "virtual_machine_interface_id") deviceInterfaceID := getOptionalInt(d, "device_interface_id") interfaceID := getOptionalInt(d, "interface_id") switch { case vmInterfaceID != nil: d.Set("virtual_machine_interface_id", ipAddress.AssignedObjectID) case deviceInterfaceID != nil: d.Set("device_interface_id", ipAddress.AssignedObjectID) // if interfaceID is given, object_type must be set as well case interfaceID != nil: d.Set("object_type", ipAddress.AssignedObjectType) d.Set("interface_id", ipAddress.AssignedObjectID) } } else { d.Set("interface_id", nil) d.Set("object_type", "") } if ipAddress.Vrf != nil { d.Set("vrf_id", ipAddress.Vrf.ID) } else { d.Set("vrf_id", nil) } if ipAddress.Tenant != nil { d.Set("tenant_id", ipAddress.Tenant.ID) } else { d.Set("tenant_id", nil) } if ipAddress.DNSName != "" { d.Set("dns_name", ipAddress.DNSName) } d.Set("ip_address", ipAddress.Address) d.Set("description", ipAddress.Description) d.Set("status", ipAddress.Status.Value) api.readTags(d, ipAddress.Tags) cf := getCustomFields(ipAddress.CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } return nil } func resourceNetboxAvailableIPAddressUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableIPAddress{} data.Address = strToPtr(d.Get("ip_address").(string)) data.Status = d.Get("status").(string) data.Description = getOptionalStr(d, "description", false) data.Role = getOptionalStr(d, "role", false) data.DNSName = getOptionalStr(d, "dns_name", false) data.Vrf = getOptionalInt(d, "vrf_id") data.Tenant = getOptionalInt(d, "tenant_id") if cf, ok := d.GetOk(customFieldsKey); ok { data.CustomFields = cf } if interfaceID, ok := d.GetOk("interface_id"); ok { // The other possible type is dcim.interface for devices data.AssignedObjectType = strToPtr("virtualization.vminterface") data.AssignedObjectID = int64ToPtr(int64(interfaceID.(int))) } vmInterfaceID := getOptionalInt(d, "virtual_machine_interface_id") deviceInterfaceID := getOptionalInt(d, "device_interface_id") interfaceID := getOptionalInt(d, "interface_id") switch { case vmInterfaceID != nil: data.AssignedObjectType = strToPtr("virtualization.vminterface") data.AssignedObjectID = vmInterfaceID case deviceInterfaceID != nil: data.AssignedObjectType = strToPtr("dcim.interface") data.AssignedObjectID = deviceInterfaceID // if interfaceID is given, object_type must be set as well case interfaceID != nil: data.AssignedObjectType = strToPtr(d.Get("object_type").(string)) data.AssignedObjectID = interfaceID // default = ip is not linked to anything default: data.AssignedObjectType = strToPtr("") data.AssignedObjectID = nil } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } params := ipam.NewIpamIPAddressesUpdateParams().WithID(id).WithData(&data) _, err = api.Ipam.IpamIPAddressesUpdate(params, nil) if err != nil { return err } return resourceNetboxAvailableIPAddressRead(d, m) } func resourceNetboxAvailableIPAddressDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamIPAddressesDeleteParams().WithID(id) _, err := api.Ipam.IpamIPAddressesDelete(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamIPAddressesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_available_ip_address_test.go ================================================ package netbox import ( "fmt" "log" "regexp" "testing" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxAvailableIPAddress_basic(t *testing.T) { testPrefix := "1.1.2.0/24" testIP := "1.1.2.1/24" resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_prefix" "test" { prefix = "%s" status = "active" is_pool = false } resource "netbox_available_ip_address" "test" { prefix_id = netbox_prefix.test.id status = "active" dns_name = "test.mydomain.local" role = "loopback" }`, testPrefix), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_available_ip_address.test", "ip_address", testIP), resource.TestCheckResourceAttr("netbox_available_ip_address.test", "status", "active"), resource.TestCheckResourceAttr("netbox_available_ip_address.test", "dns_name", "test.mydomain.local"), resource.TestCheckResourceAttr("netbox_available_ip_address.test", "role", "loopback"), ), }, }, }) } func TestAccNetboxAvailableIPAddress_basic_range(t *testing.T) { startAddress := "1.1.5.1/24" endAddress := "1.1.5.50/24" testIP := "1.1.5.1/24" resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_ip_range" "test" { start_address = "%s" end_address = "%s" } resource "netbox_available_ip_address" "test_range" { ip_range_id = netbox_ip_range.test.id status = "active" dns_name = "test_range.mydomain.local" }`, startAddress, endAddress), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_available_ip_address.test_range", "ip_address", testIP), resource.TestCheckResourceAttr("netbox_available_ip_address.test_range", "status", "active"), resource.TestCheckResourceAttr("netbox_available_ip_address.test_range", "dns_name", "test_range.mydomain.local"), ), }, }, }) } func TestAccNetboxAvailableIPAddress_multipleIpsParallel(t *testing.T) { testPrefix := "1.1.3.0/24" resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_prefix" "test" { prefix = "%s" status = "active" is_pool = false } resource "netbox_available_ip_address" "test1" { prefix_id = netbox_prefix.test.id status = "active" dns_name = "test.mydomain.local" } resource "netbox_available_ip_address" "test2" { prefix_id = netbox_prefix.test.id status = "active" dns_name = "test.mydomain.local" } resource "netbox_available_ip_address" "test3" { prefix_id = netbox_prefix.test.id status = "active" dns_name = "test.mydomain.local" }`, testPrefix), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrSet("netbox_available_ip_address.test1", "ip_address"), resource.TestCheckResourceAttrSet("netbox_available_ip_address.test2", "ip_address"), resource.TestCheckResourceAttrSet("netbox_available_ip_address.test3", "ip_address"), ), }, }, }) } func TestAccNetboxAvailableIPAddress_multipleIpsParallel_range(t *testing.T) { startAddress := "1.1.6.1/24" endAddress := "1.1.6.50/24" testIP := []string{"1.1.6.1/24", "1.1.6.2/24", "1.1.6.3/24"} resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_ip_range" "test_range" { start_address = "%s" end_address = "%s" } resource "netbox_available_ip_address" "test_range1" { ip_range_id = test_range.test_range.id status = "active" dns_name = "test_range.mydomain.local" } resource "netbox_available_ip_address" "test_range2" { ip_range_id = test_range.test_range.id status = "active" dns_name = "test_range.mydomain.local" } resource "netbox_available_ip_address" "test_range3" { ip_range_id = test_range.test_range.id status = "active" dns_name = "test_range.mydomain.local" }`, startAddress, endAddress), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_available_ip_address.test1", "ip_address", testIP[0]), resource.TestCheckResourceAttr("netbox_available_ip_address.test2", "ip_address", testIP[1]), resource.TestCheckResourceAttr("netbox_available_ip_address.test3", "ip_address", testIP[2]), ), ExpectError: regexp.MustCompile(".*"), }, }, }) } func TestAccNetboxAvailableIPAddress_multipleIpsSerial(t *testing.T) { testPrefix := "1.1.4.0/24" testIP := []string{"1.1.4.1/24", "1.1.4.2/24", "1.1.4.3/24"} resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_prefix" "test" { prefix = "%s" status = "active" is_pool = false } resource "netbox_available_ip_address" "test1" { prefix_id = netbox_prefix.test.id status = "active" dns_name = "test.mydomain.local" } resource "netbox_available_ip_address" "test2" { depends_on = [netbox_available_ip_address.test1] prefix_id = netbox_prefix.test.id status = "active" dns_name = "test.mydomain.local" } resource "netbox_available_ip_address" "test3" { depends_on = [netbox_available_ip_address.test2] prefix_id = netbox_prefix.test.id status = "active" dns_name = "test.mydomain.local" }`, testPrefix), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_available_ip_address.test1", "ip_address", testIP[0]), resource.TestCheckResourceAttr("netbox_available_ip_address.test2", "ip_address", testIP[1]), resource.TestCheckResourceAttr("netbox_available_ip_address.test3", "ip_address", testIP[2]), ), }, }, }) } func TestAccNetboxAvailableIPAddress_multipleIpsSerial_range(t *testing.T) { startAddress := "1.1.7.1/24" endAddress := "1.1.7.50/24" testIP := []string{"1.1.7.1/24", "1.1.7.2/24", "1.1.7.3/24"} resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_ip_range" "test_range" { start_address = "%s" end_address = "%s" } resource "netbox_available_ip_address" "test_range1" { ip_range_id = netbox_ip_range.test_range.id status = "active" dns_name = "test_range.mydomain.local" } resource "netbox_available_ip_address" "test_range2" { depends_on = [netbox_available_ip_address.test_range1] ip_range_id = netbox_ip_range.test_range.id status = "active" dns_name = "test_range.mydomain.local" } resource "netbox_available_ip_address" "test_range3" { depends_on = [netbox_available_ip_address.test_range2] ip_range_id = netbox_ip_range.test_range.id status = "active" dns_name = "test_range.mydomain.local" }`, startAddress, endAddress), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_available_ip_address.test_range1", "ip_address", testIP[0]), resource.TestCheckResourceAttr("netbox_available_ip_address.test_range2", "ip_address", testIP[1]), resource.TestCheckResourceAttr("netbox_available_ip_address.test_range3", "ip_address", testIP[2]), ), }, }, }) } func TestAccNetboxAvailableIPAddress_deviceByObjectType(t *testing.T) { startAddress := "1.2.7.1/24" endAddress := "1.2.7.50/24" testSlug := "av_ipa_dev_ot" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxIPAddressFullDeviceDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_range" "test_range" { start_address = "%s" end_address = "%s" } resource "netbox_available_ip_address" "test" { ip_range_id = netbox_ip_range.test_range.id status = "active" dns_name = "test_range.mydomain.local" object_type = "dcim.interface" interface_id = netbox_device_interface.test.id }`, startAddress, endAddress), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_available_ip_address.test", "status", "active"), resource.TestCheckResourceAttr("netbox_available_ip_address.test", "object_type", "dcim.interface"), resource.TestCheckResourceAttrPair("netbox_available_ip_address.test", "interface_id", "netbox_device_interface.test", "id"), ), }, }, }) } func TestAccNetboxAvailableIPAddress_deviceByFieldName(t *testing.T) { startAddress := "1.3.7.1/24" endAddress := "1.3.7.50/24" testSlug := "av_ipa_dev_fn" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxIPAddressFullDeviceDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_range" "test_range" { start_address = "%s" end_address = "%s" } resource "netbox_available_ip_address" "test" { ip_range_id = netbox_ip_range.test_range.id status = "active" dns_name = "test_range.mydomain.local" device_interface_id = netbox_device_interface.test.id }`, startAddress, endAddress), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_available_ip_address.test", "status", "active"), resource.TestCheckResourceAttrPair("netbox_available_ip_address.test", "device_interface_id", "netbox_device_interface.test", "id"), ), }, }, }) } func TestAccNetboxAvailableIPAddress_cf(t *testing.T) { testPrefix := "1.1.8.0/24" testIP := "1.1.8.1/24" testSlug := "av_ipa_cf" resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_custom_field" "test" { name = "%s" type = "text" weight = 100 content_types = ["ipam.ipaddress"] } resource "netbox_prefix" "test" { prefix = "%s" status = "active" is_pool = false } resource "netbox_available_ip_address" "test" { prefix_id = netbox_prefix.test.id status = "active" custom_fields = { "${netbox_custom_field.test.name}" = "test-field" } }`, testSlug, testPrefix), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_available_ip_address.test", "ip_address", testIP), resource.TestCheckResourceAttr("netbox_available_ip_address.test", fmt.Sprintf("custom_fields.%s", testSlug), "test-field"), ), }, { ResourceName: "netbox_available_ip_address.test", ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{ "prefix_id", }, }, }, }) } func init() { resource.AddTestSweepers("netbox_available_ip_address", &resource.Sweeper{ Name: "netbox_available_ip_address", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := ipam.NewIpamIPAddressesListParams() res, err := api.Ipam.IpamIPAddressesList(params, nil) if err != nil { return err } for _, ipAddress := range res.GetPayload().Results { if len(ipAddress.Tags) > 0 && (ipAddress.Tags[0] == &models.NestedTag{Name: strToPtr("acctest"), Slug: strToPtr("acctest")}) { deleteParams := ipam.NewIpamIPAddressesDeleteParams().WithID(ipAddress.ID) _, err := api.Ipam.IpamIPAddressesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted an ip address") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_available_prefix.go ================================================ package netbox import ( "context" "fmt" "strconv" "strings" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxAvailablePrefix() *schema.Resource { return &schema.Resource{ Create: resourceNetboxAvailablePrefixCreate, Read: resourceNetboxPrefixRead, Update: resourceNetboxPrefixUpdate, Delete: resourceNetboxPrefixDelete, Description: `:meta:subcategory:IP Address Management (IPAM):`, Schema: map[string]*schema.Schema{ "parent_prefix_id": { Type: schema.TypeInt, Required: true, ForceNew: true, }, "prefix_length": { Type: schema.TypeInt, Required: true, ForceNew: true, ValidateFunc: validation.IntBetween(0, 128), }, "prefix": { Type: schema.TypeString, Computed: true, }, "status": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringInSlice(resourceNetboxPrefixStatusOptions, false), Description: buildValidValueDescription(resourceNetboxPrefixStatusOptions), }, "description": { Type: schema.TypeString, Optional: true, }, "is_pool": { Type: schema.TypeBool, Optional: true, }, "mark_utilized": { Type: schema.TypeBool, Optional: true, }, "vrf_id": { Type: schema.TypeInt, Optional: true, }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, "location_id": { Type: schema.TypeInt, Optional: true, ConflictsWith: []string{"site_id", "site_group_id", "region_id"}, }, "site_id": { Type: schema.TypeInt, Optional: true, ConflictsWith: []string{"location_id", "site_group_id", "region_id"}, }, "site_group_id": { Type: schema.TypeInt, Optional: true, ConflictsWith: []string{"location_id", "site_id", "region_id"}, }, "region_id": { Type: schema.TypeInt, Optional: true, ConflictsWith: []string{"location_id", "site_id", "site_group_id"}, }, "vlan_id": { Type: schema.TypeInt, Optional: true, }, "role_id": { Type: schema.TypeInt, Optional: true, }, customFieldsKey: customFieldsSchema, tagsKey: tagsSchema, }, Importer: &schema.ResourceImporter{ StateContext: func(c context.Context, rd *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { parentPrefixID, prefixID, prefixLength, err := resourceNetboxAvailablePrefixParseImport(rd.Id()) if err != nil { return nil, err } rd.Set("parent_prefix_id", parentPrefixID) rd.Set("prefix_length", prefixLength) rd.SetId(prefixID) return []*schema.ResourceData{rd}, nil }, }, } } func resourceNetboxAvailablePrefixParseImport(importStr string) (int, string, int, error) { parts := strings.SplitN(importStr, " ", 3) if len(parts) != 3 || parts[0] == "" || parts[1] == "" || parts[2] == "" { return 0, "", 0, fmt.Errorf("unexpected format of (%s), expected 'parent_prefix_id prefix_id prefix_length'", importStr) } parentID, err := strconv.Atoi(parts[0]) if err != nil { return 0, "", 0, fmt.Errorf("parent_id (%s) is not an integer", parts[0]) } prefixLength, err := strconv.Atoi(parts[2]) if err != nil { return 0, "", 0, fmt.Errorf("prefix_length (%s) is not an integer", parts[1]) } return parentID, parts[1], prefixLength, nil } func resourceNetboxAvailablePrefixCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) parentPrefixID := int64(d.Get("parent_prefix_id").(int)) prefixLength := int64(d.Get("prefix_length").(int)) data := models.PrefixLength{ PrefixLength: &prefixLength, } if cf, ok := d.GetOk(customFieldsKey); ok { data.CustomFields = cf } params := ipam.NewIpamPrefixesAvailablePrefixesCreateParams().WithID(parentPrefixID).WithData(&data) res, err := api.Ipam.IpamPrefixesAvailablePrefixesCreate(params, nil) if err != nil { return err } payload := res.GetPayload() d.SetId(strconv.FormatInt(payload.ID, 10)) d.Set("prefix", payload.Prefix) return resourceNetboxPrefixUpdate(d, m) } ================================================ FILE: netbox/resource_netbox_available_prefix_test.go ================================================ package netbox import ( "fmt" "regexp" "testing" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" log "github.com/sirupsen/logrus" ) func testAccNetboxAvailablePrefixFullDependencies(testName string, parentPrefix string) string { return fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_prefix" "parent" { prefix = "%[2]s" description = "%[1]s" status = "container" tags = [netbox_tag.test.name] lifecycle { ignore_changes = [%[3]s] } } `, testName, parentPrefix, customFieldsKey) } func TestAccNetboxAvailablePrefix_basic(t *testing.T) { testParentPrefix := "1.1.0.0/24" testPrefixLength := 25 expectedPrefix := "1.1.0.0/25" testSlug := "prefix" testDesc := "test prefix" testName := testAccGetTestName(testSlug) parentResourceName := "netbox_prefix.parent" resourceName := "netbox_available_prefix.test" resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxAvailablePrefixFullDependencies(testName, testParentPrefix) + fmt.Sprintf(` resource "netbox_available_prefix" "test" { parent_prefix_id = netbox_prefix.parent.id prefix_length = %d description = "%s" status = "active" tags = [netbox_tag.test.name] mark_utilized = true is_pool = true }`, testPrefixLength, testDesc), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "prefix", expectedPrefix), resource.TestCheckResourceAttr(resourceName, "status", "active"), resource.TestCheckResourceAttr(resourceName, "description", testDesc), resource.TestCheckResourceAttr(resourceName, "tags.#", "1"), resource.TestCheckResourceAttr(resourceName, "tags.0", testName), resource.TestCheckResourceAttr(resourceName, "mark_utilized", "true"), resource.TestCheckResourceAttr(resourceName, "is_pool", "true"), ), }, { Config: testAccNetboxAvailablePrefixFullDependencies(testName, testParentPrefix) + fmt.Sprintf(` resource "netbox_available_prefix" "test" { parent_prefix_id = netbox_prefix.parent.id prefix_length = %d description = "%s" status = "active" tags = [netbox_tag.test.name] mark_utilized = false is_pool = false }`, testPrefixLength, testDesc), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "mark_utilized", "false"), resource.TestCheckResourceAttr(resourceName, "is_pool", "false"), ), }, { ResourceName: resourceName, ImportState: true, ImportStateVerify: true, ImportStateIdFunc: func(s *terraform.State) (string, error) { parent, ok := s.RootModule().Resources[parentResourceName] if !ok { return "", fmt.Errorf("Not found: %s", parentResourceName) } resource, ok := s.RootModule().Resources[resourceName] if !ok { return "", fmt.Errorf("Not found: %s", resourceName) } return fmt.Sprintf("%s %s %d", parent.Primary.ID, resource.Primary.ID, testPrefixLength), nil }, }, }, }) } func TestAccNetboxAvailablePrefix_cf(t *testing.T) { testParentPrefix := "1.1.0.0/24" testPrefixLength := 25 expectedPrefix := "1.1.0.0/25" testSlug := "prefix_cf" testName := testAccGetTestName(testSlug) parentResourceName := "netbox_prefix.parent" resourceName := "netbox_available_prefix.test" resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxAvailablePrefixFullDependencies(testName, testParentPrefix) + fmt.Sprintf(` resource "netbox_custom_field" "test" { name = "%s" type = "text" weight = 100 content_types = ["ipam.prefix"] } resource "netbox_available_prefix" "test" { parent_prefix_id = netbox_prefix.parent.id prefix_length = %d status = "active" custom_fields = { "${netbox_custom_field.test.name}" = "test-field" } }`, testSlug, testPrefixLength), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "prefix", expectedPrefix), resource.TestCheckResourceAttr(resourceName, "status", "active"), resource.TestCheckResourceAttr(resourceName, fmt.Sprintf("custom_fields.%s", testSlug), "test-field"), ), }, { ResourceName: resourceName, ImportState: true, ImportStateVerify: true, ImportStateIdFunc: func(s *terraform.State) (string, error) { parent, ok := s.RootModule().Resources[parentResourceName] if !ok { return "", fmt.Errorf("Not found: %s", parentResourceName) } resource, ok := s.RootModule().Resources[resourceName] if !ok { return "", fmt.Errorf("Not found: %s", resourceName) } return fmt.Sprintf("%s %s %d", parent.Primary.ID, resource.Primary.ID, testPrefixLength), nil }, }, }, }) } func TestAccNetboxAvailablePrefix_multiplePrefixesSerial(t *testing.T) { testParentPrefix := "1.1.0.0/24" testPrefixLength := 25 expectedPrefixes := []string{ "1.1.0.0/25", "1.1.0.128/25", } testSlug := "prefix" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxAvailablePrefixFullDependencies(testName, testParentPrefix) + fmt.Sprintf(` resource "netbox_available_prefix" "test1" { parent_prefix_id = netbox_prefix.parent.id prefix_length = %d status = "active" tags = [netbox_tag.test.name] } resource "netbox_available_prefix" "test2" { depends_on = [netbox_available_prefix.test1] parent_prefix_id = netbox_prefix.parent.id prefix_length = netbox_available_prefix.test1.prefix_length status = "active" tags = [netbox_tag.test.name] } `, testPrefixLength), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_available_prefix.test1", "prefix", expectedPrefixes[0]), resource.TestCheckResourceAttr("netbox_available_prefix.test2", "prefix", expectedPrefixes[1]), ), }, { Config: testAccNetboxAvailablePrefixFullDependencies(testName, testParentPrefix) + fmt.Sprintf(` resource "netbox_available_prefix" "test1" { parent_prefix_id = netbox_prefix.parent.id prefix_length = %d status = "active" tags = [netbox_tag.test.name] } resource "netbox_available_prefix" "test2" { depends_on = [netbox_available_prefix.test1] parent_prefix_id = netbox_prefix.parent.id prefix_length = netbox_available_prefix.test1.prefix_length status = "active" tags = [netbox_tag.test.name] } resource "netbox_available_prefix" "test3" { depends_on = [netbox_available_prefix.test2] parent_prefix_id = netbox_prefix.parent.id prefix_length = netbox_available_prefix.test1.prefix_length status = "active" tags = [netbox_tag.test.name] } `, testPrefixLength), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_available_prefix.test1", "prefix", expectedPrefixes[0]), resource.TestCheckResourceAttr("netbox_available_prefix.test2", "prefix", expectedPrefixes[1]), ), ExpectError: regexp.MustCompile(".*Insufficient resources are available to satisfy the request.*"), }, }, }) } func testAccNetboxAvailablePrefixScopeDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_site" "test" { name = "%[1]s" } resource "netbox_location" "test" { name = "%[1]s" site_id = netbox_site.test.id } resource "netbox_region" "test" { name = "%[1]s" } resource "netbox_site_group" "test" { name = "%[1]s" } `, testName) } func TestAccNetboxAvailablePrefix_scopes(t *testing.T) { testParentPrefix := "16.1.0.0/24" testPrefixLength := 25 testSlug := "prefix-scopes" testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxAvailablePrefixFullDependencies(testName, testParentPrefix) + testAccNetboxAvailablePrefixScopeDependencies(testName) + fmt.Sprintf(` resource "netbox_available_prefix" "with_site_id" { parent_prefix_id = netbox_prefix.parent.id prefix_length = %[2]d status = "active" site_id = netbox_site.test.id }`, testSlug, testPrefixLength), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_available_prefix.with_site_id", "site_id", "netbox_site.test", "id"), ), }, { Config: testAccNetboxAvailablePrefixFullDependencies(testName, testParentPrefix) + testAccNetboxAvailablePrefixScopeDependencies(testName) + fmt.Sprintf(` resource "netbox_available_prefix" "with_location_id" { parent_prefix_id = netbox_prefix.parent.id prefix_length = %[2]d status = "active" location_id = netbox_location.test.id }`, testSlug, testPrefixLength), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_available_prefix.with_location_id", "location_id", "netbox_location.test", "id"), ), }, { Config: testAccNetboxAvailablePrefixFullDependencies(testName, testParentPrefix) + testAccNetboxAvailablePrefixScopeDependencies(testName) + fmt.Sprintf(` resource "netbox_available_prefix" "with_region_id" { parent_prefix_id = netbox_prefix.parent.id prefix_length = %[2]d status = "active" region_id = netbox_region.test.id }`, testSlug, testPrefixLength), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_available_prefix.with_region_id", "region_id", "netbox_region.test", "id"), ), }, { Config: testAccNetboxAvailablePrefixFullDependencies(testName, testParentPrefix) + testAccNetboxAvailablePrefixScopeDependencies(testName) + fmt.Sprintf(` resource "netbox_available_prefix" "with_site_group_id" { parent_prefix_id = netbox_prefix.parent.id prefix_length = %[2]d status = "active" site_group_id = netbox_site_group.test.id }`, testSlug, testPrefixLength), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_available_prefix.with_site_group_id", "site_group_id", "netbox_site_group.test", "id"), ), }, }, }) } func init() { resource.AddTestSweepers("netbox_available_prefix", &resource.Sweeper{ Name: "netbox_available_prefix", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := ipam.NewIpamPrefixesListParams() res, err := api.Ipam.IpamPrefixesList(params, nil) if err != nil { return err } for _, prefix := range res.GetPayload().Results { if len(prefix.Tags) > 0 && (prefix.Tags[0] == &models.NestedTag{Name: strToPtr("acctest"), Slug: strToPtr("acctest")}) { deleteParams := ipam.NewIpamPrefixesDeleteParams().WithID(prefix.ID) _, err := api.Ipam.IpamPrefixesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a prefix") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_available_vlan.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceNetboxAvailableVLAN() *schema.Resource { return &schema.Resource{ Create: resourceNetboxAvailableVLANCreate, Read: resourceNetboxAvailableVLANRead, Update: resourceNetboxAvailableVLANUpdate, Delete: resourceNetboxAvailableVLANDelete, Description: `:meta:subcategory:IP Address Management (IPAM):Per [the docs](https://netbox.readthedocs.io/en/stable/models/ipam/vlan/): > A VLAN represents an isolated Layer 2 domain identified by a numeric ID (1–4094). VLANs may be assigned to specific sites or marked as global. > Optionally, they can be organized within VLAN groups to define scope and enforce uniqueness. > > Each VLAN can also be assigned an operational status and a functional role. Statuses are hard-coded in NetBox and include the following: > * Active > * Reserved > * Deprecated This resource will retrieve the next available VLAN ID from a given VLAN group (specified by ID).`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "description": { Type: schema.TypeString, Optional: true, }, "group_id": { Type: schema.TypeInt, Optional: true, }, "role_id": { Type: schema.TypeInt, Optional: true, }, "site_id": { Type: schema.TypeInt, Optional: true, }, "status": { Type: schema.TypeString, Optional: true, }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, "vid": { Type: schema.TypeInt, Computed: true, // it's auto-assigned by NetBox, not user-supplied }, "comments": { Type: schema.TypeString, Computed: true, }, tagsKey: tagsSchema, }, } } func resourceNetboxAvailableVLANCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) groupID := int64(d.Get("group_id").(int)) tags, err := getNestedTagListFromResourceDataSet(api, d.Get(tagsKey)) if err != nil { return err } data := &models.WritableCreateAvailableVLAN{ Name: strToPtr(d.Get("name").(string)), Description: getOptionalStr(d, "description", false), Tenant: getOptionalInt(d, "tenant_id"), Site: getOptionalInt(d, "site_id"), Role: getOptionalInt(d, "role_id"), Status: d.Get("status").(string), Tags: tags, } params := ipam.NewIpamVlanGroupsAvailableVlansCreateParams().WithID(groupID).WithData(data) resp, err := api.Ipam.IpamVlanGroupsAvailableVlansCreate(params, nil) if err != nil { return err } vlan := resp.Payload d.SetId(strconv.FormatInt(vlan.ID, 10)) d.Set("vid", vlan.Vid) d.Set("name", vlan.Name) d.Set("group_id", vlan.Group.ID) return resourceNetboxAvailableVLANRead(d, m) } func resourceNetboxAvailableVLANRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamVlansReadParams().WithID(id) res, err := api.Ipam.IpamVlansRead(params, nil) if err != nil { if erresp, ok := err.(*ipam.IpamVlansReadDefault); ok { errorcode := erresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } vlan := res.GetPayload() // Required fields d.Set("vid", vlan.Vid) d.Set("name", vlan.Name) // Optional fields d.Set("description", vlan.Description) d.Set("comments", vlan.Comments) if vlan.Status != nil && vlan.Status.Value != nil { d.Set("status", *vlan.Status.Value) } else { d.Set("status", "") } if vlan.Tenant != nil { d.Set("tenant_id", vlan.Tenant.ID) } else { d.Set("tenant_id", nil) } if vlan.Site != nil { d.Set("site_id", vlan.Site.ID) } else { d.Set("site_id", nil) } if vlan.Group != nil { d.Set("group_id", vlan.Group.ID) } else { d.Set("group_id", nil) } if vlan.Role != nil { d.Set("role_id", vlan.Role.ID) } else { d.Set("role_id", nil) } api.readTags(d, vlan.Tags) return nil } func resourceNetboxAvailableVLANUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := &models.WritableVLAN{ Name: strToPtr(d.Get("name").(string)), Description: getOptionalStr(d, "description", false), Tenant: getOptionalInt(d, "tenant_id"), Site: getOptionalInt(d, "site_id"), Group: getOptionalInt(d, "group_id"), Role: getOptionalInt(d, "role_id"), Status: d.Get("status").(string), Vid: int64ToPtr(int64(d.Get("vid").(int))), } var err_tags error data.Tags, err_tags = getNestedTagListFromResourceDataSet(api, d.Get(tagsKey)) if err_tags != nil { return err_tags } params := ipam.NewIpamVlansUpdateParams(). WithID(id). WithData(data) _, err := api.Ipam.IpamVlansUpdate(params, nil) if err != nil { return err } return resourceNetboxAvailableVLANRead(d, m) } func resourceNetboxAvailableVLANDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamVlansDeleteParams().WithID(id) _, err := api.Ipam.IpamVlansDelete(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamVlansDeleteDefault); ok && errresp.Code() == 404 { d.SetId("") return nil } return err } return nil } ================================================ FILE: netbox/resource_netbox_available_vlan_test.go ================================================ package netbox import ( "fmt" "regexp" "strconv" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) // TestAccNetboxAvailableVLAN_basic verifies that a basic available VLAN can be // created in NetBox using a VLAN group and a site. It ensures required attributes // like name, status, and vid are correctly set and persisted. func TestAccNetboxAvailableVLAN_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: ` resource "netbox_site" "test_site" { name = "testSite" slug = "ts" } resource "netbox_vlan_group" "groupTest" { name = "Group Test" slug = "group-test" scope_id = netbox_site.test_site.id scope_type = "dcim.site" description = "First VLAN group" vid_ranges = [[1,20]] } resource "netbox_available_vlan" "vlanTest" { name = "test-vlan" status = "active" description = "Virtual network for testing purposes" group_id = netbox_vlan_group.groupTest.id site_id = netbox_vlan_group.groupTest.scope_id } `, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_available_vlan.vlanTest", "name", "test-vlan"), resource.TestCheckResourceAttr("netbox_available_vlan.vlanTest", "status", "active"), resource.TestCheckResourceAttrSet("netbox_available_vlan.vlanTest", "vid"), resource.TestCheckResourceAttrSet("netbox_available_vlan.vlanTest", "id"), resource.TestCheckResourceAttrSet("netbox_available_vlan.vlanTest", "group_id"), ), }, }, }) } // TestAccNetboxAvailableVLAN_basic_range ensures that the VLAN created by the // provider has a VID that falls within the specified vid_ranges of the group. func TestAccNetboxAvailableVLAN_basic_range(t *testing.T) { const ( minVID = 10 maxVID = 20 ) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_site" "test_site" { name = "Test Site" slug = "test-site" } resource "netbox_vlan_group" "group1" { name = "Group One" slug = "group-one" scope_id = netbox_site.test_site.id scope_type = "dcim.site" description = "First VLAN group" vid_ranges = [[%d, %d]] } resource "netbox_available_vlan" "vlan_test" { name = "test-vlan" status = "active" group_id = netbox_vlan_group.group1.id site_id = netbox_vlan_group.group1.scope_id description = "Autogenerated VLAN" } `, minVID, maxVID), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrSet("netbox_available_vlan.vlan_test", "vid"), func(s *terraform.State) error { rs, ok := s.RootModule().Resources["netbox_available_vlan.vlan_test"] if !ok { return fmt.Errorf("not found: netbox_available_vlan.vlan_test") } vidStr := rs.Primary.Attributes["vid"] vid, err := strconv.Atoi(vidStr) if err != nil { return fmt.Errorf("invalid vid: %s", vidStr) } if vid < minVID || vid > maxVID { return fmt.Errorf("vid %d is out of expected range [%d-%d]", vid, minVID, maxVID) } return nil }, ), }, }, }) } // TestAccNetboxAvailableVLAN_multipleSerial checks that multiple available VLANs // can be allocated serially from the same VLAN group. func TestAccNetboxAvailableVLAN_multipleSerial(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: ` resource "netbox_site" "test_site" { name = "Serial Test Site" slug = "serial-test-site" } resource "netbox_vlan_group" "group2" { name = "Group Two" slug = "group-two" scope_id = netbox_site.test_site.id scope_type = "dcim.site" vid_ranges = [[30, 32]] } resource "netbox_available_vlan" "vlan1" { name = "vlan-1" status = "active" group_id = netbox_vlan_group.group2.id site_id = netbox_vlan_group.group2.scope_id } resource "netbox_available_vlan" "vlan2" { depends_on = [netbox_available_vlan.vlan1] name = "vlan-2" status = "active" group_id = netbox_vlan_group.group2.id site_id = netbox_vlan_group.group2.scope_id } `, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrSet("netbox_available_vlan.vlan1", "vid"), resource.TestCheckResourceAttrSet("netbox_available_vlan.vlan2", "vid"), ), }, }, }) } // TestAccNetboxAvailableVLAN_multipleSerial_range validates that sequential allocation // works correctly when a VLAN group has multiple vid_ranges. Here it confirms it by // verifying that third vlan falls in the second range of available ids. func TestAccNetboxAvailableVLAN_multipleSerial_range(t *testing.T) { const ( minVID1 = 1 maxVID1 = 2 minVID2 = 7 maxVID2 = 17 ) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_site" "test_site" { name = "Serial Test range Site" slug = "serial-test-range-site" } resource "netbox_vlan_group" "group2" { name = "Group Two" slug = "group-two" scope_id = netbox_site.test_site.id scope_type = "dcim.site" vid_ranges = [[%d, %d], [%d,%d]] } resource "netbox_available_vlan" "vlan1" { name = "vlan-1" status = "active" group_id = netbox_vlan_group.group2.id site_id = netbox_vlan_group.group2.scope_id } resource "netbox_available_vlan" "vlan2" { depends_on = [netbox_available_vlan.vlan1] name = "vlan-2" status = "active" group_id = netbox_vlan_group.group2.id site_id = netbox_vlan_group.group2.scope_id } resource "netbox_available_vlan" "vlan3" { depends_on = [netbox_available_vlan.vlan2] name = "vlan-3" status = "active" group_id = netbox_vlan_group.group2.id site_id = netbox_vlan_group.group2.scope_id } `, minVID1, maxVID1, minVID2, maxVID2), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrSet("netbox_available_vlan.vlan1", "vid"), resource.TestCheckResourceAttrSet("netbox_available_vlan.vlan2", "vid"), resource.TestCheckResourceAttrSet("netbox_available_vlan.vlan3", "vid"), func(s *terraform.State) error { rs, ok := s.RootModule().Resources["netbox_available_vlan.vlan3"] if !ok { return fmt.Errorf("not found: netbox_available_vlan.vlan3") } vidStr := rs.Primary.Attributes["vid"] vid, err := strconv.Atoi(vidStr) if err != nil { return fmt.Errorf("invalid vid: %s", vidStr) } if vid < minVID2 || vid > maxVID2 { return fmt.Errorf("vid %d is out of expected range [%d-%d]", vid, minVID2, maxVID2) } return nil }, ), }, }, }) } // TestAccNetboxAvailableVLAN_multipleParallel tests that multiple available VLANs // can be allocated in parallel from the same VLAN group. func TestAccNetboxAvailableVLAN_multipleParallel(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: ` resource "netbox_site" "test_site" { name = "Parallel Test Site" slug = "Parallel-test-site" } resource "netbox_vlan_group" "group2" { name = "Group Two" slug = "group-two" scope_id = netbox_site.test_site.id scope_type = "dcim.site" vid_ranges = [[30, 32]] } resource "netbox_available_vlan" "vlan1" { name = "vlan-1" status = "active" group_id = netbox_vlan_group.group2.id site_id = netbox_vlan_group.group2.scope_id } resource "netbox_available_vlan" "vlan2" { name = "vlan-2" status = "active" group_id = netbox_vlan_group.group2.id site_id = netbox_vlan_group.group2.scope_id } `, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrSet("netbox_available_vlan.vlan1", "vid"), resource.TestCheckResourceAttrSet("netbox_available_vlan.vlan2", "vid"), ), }, }, }) } // TestAccNetboxAvailableVLAN_vidExhaustion validates that the provider returns // an appropriate error when trying to allocate more VLANs than the defined // vid_ranges allow (i.e., resource exhaustion). func TestAccNetboxAvailableVLAN_vidExhaustion(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: ` resource "netbox_vlan_group" "group" { name = "Exhausted VLAN Group" slug = "exhausted" vid_ranges = [[500, 500]] } resource "netbox_available_vlan" "vlan1" { name = "exhausted-vlan-1" group_id = netbox_vlan_group.group.id status = "active" } resource "netbox_available_vlan" "vlan2" { name = "exhausted-vlan-2" group_id = netbox_vlan_group.group.id status = "active" } `, ExpectError: regexp.MustCompile(`(?i)(409|400|no available vlans|must be greater than or equal to)`), }, }, }) } // TestAccNetboxAvailableVLAN_withTenant verifies that a VLAN can be created // with a specific tenant. It ensures the tenant_id is correctly // assigned and persisted by the provider. // NOTE: This test could also be run with tenant AND role but the vlan-role // is not yet implemented in the netbox API (as of 05.05.2025 at least) func TestAccNetboxAvailableVLAN_withTenant(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: ` resource "netbox_tenant" "tenant" { name = "TestTenant" slug = "testtenant" } resource "netbox_vlan_group" "group" { name = "VLAN Role Test" slug = "vlan-role-test" vid_ranges = [[400, 405]] } resource "netbox_available_vlan" "vlan" { name = "vlan-role-tenant" group_id = netbox_vlan_group.group.id tenant_id = netbox_tenant.tenant.id status = "active" } `, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrSet("netbox_available_vlan.vlan", "tenant_id"), ), }, }, }) } ================================================ FILE: netbox/resource_netbox_cable.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxCable() *schema.Resource { return &schema.Resource{ Create: resourceNetboxCableCreate, Read: resourceNetboxCableRead, Update: resourceNetboxCableUpdate, Delete: resourceNetboxCableDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/cable/): > All connections between device components in NetBox are represented using cables. A cable represents a direct physical connection between two sets of endpoints (A and B), such as a console port and a patch panel port, or between two network interfaces.`, Schema: map[string]*schema.Schema{ "a_termination": { Type: schema.TypeSet, Required: true, Elem: genericObjectSchema, }, "b_termination": { Type: schema.TypeSet, Required: true, Elem: genericObjectSchema, }, "status": { Type: schema.TypeString, Required: true, Description: "One of [connected, planned, decommissioning]", ValidateFunc: validation.StringInSlice([]string{"connected", "planned", "decommissioning"}, false), }, "type": { Type: schema.TypeString, Optional: true, Description: "One of [cat3, cat5, cat5e, cat6, cat6a, cat7, cat7a, cat8, dac-active, dac-passive, mrj21-trunk, coaxial, mmf, mmf-om1, mmf-om2, mmf-om3, mmf-om4, mmf-om5, smf, smf-os1, smf-os2, aoc, power, usb]", ValidateFunc: validation.StringInSlice([]string{ "cat3", "cat5", "cat5e", "cat6", "cat6a", "cat7", "cat7a", "cat8", "dac-active", "dac-passive", "mrj21-trunk", "coaxial", "mmf", "mmf-om1", "mmf-om2", "mmf-om3", "mmf-om4", "mmf-om5", "smf", "smf-os1", "smf-os2", "aoc", "power", "usb", }, false), }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, "label": { Type: schema.TypeString, Optional: true, }, "color_hex": { Type: schema.TypeString, Optional: true, }, "length": { Type: schema.TypeFloat, Optional: true, }, "length_unit": { Type: schema.TypeString, Optional: true, RequiredWith: []string{"length"}, Description: "One of [km, m, cm, mi, ft, in]", ValidateFunc: validation.StringInSlice([]string{"km", "m", "cm", "mi", "ft", "in"}, false), }, "description": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringLenBetween(0, 200), }, "comments": { Type: schema.TypeString, Optional: true, }, tagsKey: tagsSchema, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxCableCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableCable{ Status: d.Get("status").(string), Type: getOptionalStr(d, "type", false), Tenant: getOptionalInt(d, "tenant_id"), Label: getOptionalStr(d, "label", false), Color: getOptionalStr(d, "color_hex", false), Length: getOptionalFloat(d, "length"), LengthUnit: getOptionalStr(d, "length_unit", false), Description: getOptionalStr(d, "description", false), Comments: getOptionalStr(d, "comments", false), } aTerminations := d.Get("a_termination").(*schema.Set) data.ATerminations = getGenericObjectsFromSchemaSet(aTerminations) bTerminations := d.Get("b_termination").(*schema.Set) data.BTerminations = getGenericObjectsFromSchemaSet(bTerminations) var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimCablesCreateParams().WithData(&data) res, err := api.Dcim.DcimCablesCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxCableRead(d, m) } func resourceNetboxCableRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimCablesReadParams().WithID(id) res, err := api.Dcim.DcimCablesRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimCablesReadDefault); ok { if errresp.Code() == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } cable := res.GetPayload() d.Set("a_termination", getSchemaSetFromGenericObjects(cable.ATerminations)) d.Set("b_termination", getSchemaSetFromGenericObjects(cable.BTerminations)) if cable.Status != nil { d.Set("status", cable.Status.Value) } else { d.Set("status", nil) } d.Set("type", cable.Type) if cable.Tenant != nil { d.Set("tenant_id", cable.Tenant.ID) } else { d.Set("tenant_id", nil) } d.Set("label", cable.Label) d.Set("color_hex", cable.Color) d.Set("length", cable.Length) if cable.LengthUnit != nil { d.Set("length_unit", cable.LengthUnit.Value) } else { d.Set("length_unit", nil) } d.Set("description", cable.Description) d.Set("comments", cable.Comments) cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxCableUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableCable{ Status: d.Get("status").(string), Type: getOptionalStr(d, "type", false), Tenant: getOptionalInt(d, "tenant_id"), Label: getOptionalStr(d, "label", true), Color: getOptionalStr(d, "color_hex", false), Length: getOptionalFloat(d, "length"), LengthUnit: getOptionalStr(d, "length_unit", false), Description: getOptionalStr(d, "description", true), Comments: getOptionalStr(d, "comments", true), } aTerminations := d.Get("a_termination").(*schema.Set) data.ATerminations = getGenericObjectsFromSchemaSet(aTerminations) bTerminations := d.Get("b_termination").(*schema.Set) data.BTerminations = getGenericObjectsFromSchemaSet(bTerminations) var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimCablesPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimCablesPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxCableRead(d, m) } func resourceNetboxCableDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimCablesDeleteParams().WithID(id) _, err := api.Dcim.DcimCablesDelete(params, nil) if err != nil { return err } return nil } ================================================ FILE: netbox/resource_netbox_cable_test.go ================================================ package netbox import ( "fmt" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" log "github.com/sirupsen/logrus" ) func testAccNetboxCableFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_tag" "test" { name = "%[1]sa" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_module_bay" "test" { device_id = netbox_device.test.id name = "%[1]s" } resource "netbox_module_type" "test" { manufacturer_id = netbox_manufacturer.test.id model = "%[1]s" } resource "netbox_module" "test" { device_id = netbox_device.test.id module_bay_id = netbox_device_module_bay.test.id module_type_id = netbox_module_type.test.id status = "active" } resource "netbox_device_console_port" "test1" { device_id = netbox_device.test.id name = "%[1]s1" } resource "netbox_device_console_port" "test2" { device_id = netbox_device.test.id name = "%[1]s2" } resource "netbox_device_console_server_port" "test1" { device_id = netbox_device.test.id name = "%[1]s1" } resource "netbox_device_console_server_port" "test2" { device_id = netbox_device.test.id name = "%[1]s2" } `, testName) } func TestAccNetboxCable_basic(t *testing.T) { testSlug := "cable_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckCableDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxCableFullDependencies(testName) + fmt.Sprintf(` resource "netbox_cable" "test" { a_termination { object_type = "dcim.consoleserverport" object_id = netbox_device_console_server_port.test1.id } a_termination { object_type = "dcim.consoleserverport" object_id = netbox_device_console_server_port.test2.id } b_termination { object_type = "dcim.consoleport" object_id = netbox_device_console_port.test1.id } b_termination { object_type = "dcim.consoleport" object_id = netbox_device_console_port.test2.id } status = "connected" label = "%[1]s_label" type = "cat8" tenant_id = netbox_tenant.test.id color_hex = "123456" length = 10 length_unit = "m" description = "%[1]s_description" comments = "%[1]s_comments" tags = [netbox_tag.test.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_cable.test", "status", "connected"), resource.TestCheckResourceAttr("netbox_cable.test", "label", testName+"_label"), resource.TestCheckResourceAttr("netbox_cable.test", "type", "cat8"), resource.TestCheckResourceAttr("netbox_cable.test", "color_hex", "123456"), resource.TestCheckResourceAttr("netbox_cable.test", "length", "10"), resource.TestCheckResourceAttr("netbox_cable.test", "length_unit", "m"), resource.TestCheckResourceAttr("netbox_cable.test", "description", testName+"_description"), resource.TestCheckResourceAttr("netbox_cable.test", "comments", testName+"_comments"), resource.TestCheckResourceAttr("netbox_cable.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_cable.test", "tags.0", testName+"a"), resource.TestCheckResourceAttr("netbox_cable.test", "a_termination.#", "2"), resource.TestCheckResourceAttr("netbox_cable.test", "a_termination.0.object_type", "dcim.consoleserverport"), resource.TestCheckResourceAttr("netbox_cable.test", "a_termination.1.object_type", "dcim.consoleserverport"), resource.TestCheckTypeSetElemAttrPair("netbox_cable.test", "a_termination.*.object_id", "netbox_device_console_server_port.test1", "id"), resource.TestCheckTypeSetElemAttrPair("netbox_cable.test", "a_termination.*.object_id", "netbox_device_console_server_port.test2", "id"), resource.TestCheckResourceAttr("netbox_cable.test", "b_termination.#", "2"), resource.TestCheckResourceAttr("netbox_cable.test", "b_termination.0.object_type", "dcim.consoleport"), resource.TestCheckResourceAttr("netbox_cable.test", "b_termination.1.object_type", "dcim.consoleport"), resource.TestCheckTypeSetElemAttrPair("netbox_cable.test", "b_termination.*.object_id", "netbox_device_console_port.test1", "id"), resource.TestCheckTypeSetElemAttrPair("netbox_cable.test", "b_termination.*.object_id", "netbox_device_console_port.test2", "id"), resource.TestCheckResourceAttrPair("netbox_cable.test", "tenant_id", "netbox_tenant.test", "id"), ), }, { Config: testAccNetboxCableFullDependencies(testName) + fmt.Sprintf(` resource "netbox_cable" "test" { a_termination { object_type = "dcim.consoleserverport" object_id = netbox_device_console_server_port.test1.id } b_termination { object_type = "dcim.consoleport" object_id = netbox_device_console_port.test1.id } status = "connected" label = "%[1]s_label" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_cable.test", "status", "connected"), resource.TestCheckResourceAttr("netbox_cable.test", "label", testName+"_label"), resource.TestCheckResourceAttr("netbox_cable.test", "type", ""), resource.TestCheckResourceAttr("netbox_cable.test", "color_hex", ""), resource.TestCheckResourceAttr("netbox_cable.test", "length", "0"), resource.TestCheckResourceAttr("netbox_cable.test", "length_unit", ""), resource.TestCheckResourceAttr("netbox_cable.test", "description", ""), resource.TestCheckResourceAttr("netbox_cable.test", "comments", ""), resource.TestCheckResourceAttr("netbox_cable.test", "tags.#", "0"), resource.TestCheckResourceAttr("netbox_cable.test", "tenant_id", "0"), resource.TestCheckResourceAttr("netbox_cable.test", "a_termination.#", "1"), resource.TestCheckResourceAttr("netbox_cable.test", "a_termination.0.object_type", "dcim.consoleserverport"), resource.TestCheckResourceAttrPair("netbox_cable.test", "a_termination.0.object_id", "netbox_device_console_server_port.test1", "id"), resource.TestCheckResourceAttr("netbox_cable.test", "b_termination.#", "1"), resource.TestCheckResourceAttr("netbox_cable.test", "b_termination.0.object_type", "dcim.consoleport"), resource.TestCheckResourceAttrPair("netbox_cable.test", "b_termination.0.object_id", "netbox_device_console_port.test1", "id"), ), }, { ResourceName: "netbox_cable.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckCableDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each cable // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_cable" { continue } // Retrieve our device by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := dcim.NewDcimCablesReadParams().WithID(stateID) _, err := conn.Dcim.DcimCablesRead(params, nil) if err == nil { return fmt.Errorf("cable (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*dcim.DcimCablesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func init() { resource.AddTestSweepers("netbox_cable", &resource.Sweeper{ Name: "netbox_cable", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimCablesListParams() res, err := api.Dcim.DcimCablesList(params, nil) if err != nil { return err } for _, cable := range res.GetPayload().Results { if strings.HasPrefix(cable.Label, testPrefix) { deleteParams := dcim.NewDcimCablesDeleteParams().WithID(cable.ID) _, err := api.Dcim.DcimCablesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a cable") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_circuit.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/circuits" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var resourceNetboxCircuitStatusOptions = []string{"planned", "provisioning", "active", "offline", "deprovisioning", "decommissioning"} func resourceNetboxCircuit() *schema.Resource { return &schema.Resource{ Create: resourceNetboxCircuitCreate, Read: resourceNetboxCircuitRead, Update: resourceNetboxCircuitUpdate, Delete: resourceNetboxCircuitDelete, Description: `:meta:subcategory:Circuits:From the [official documentation](https://docs.netbox.dev/en/stable/features/circuits/#circuits_1): > A communications circuit represents a single physical link connecting exactly two endpoints, commonly referred to as its A and Z terminations. A circuit in NetBox may have zero, one, or two terminations defined. It is common to have only one termination defined when you don't necessarily care about the details of the provider side of the circuit, e.g. for Internet access circuits. Both terminations would likely be modeled for circuits which connect one customer site to another. > > Each circuit is associated with a provider and a user-defined type. For example, you might have Internet access circuits delivered to each site by one provider, and private MPLS circuits delivered by another. Each circuit must be assigned a circuit ID, each of which must be unique per provider.`, Schema: map[string]*schema.Schema{ "provider_id": { Type: schema.TypeInt, Required: true, }, "cid": { Type: schema.TypeString, Required: true, }, "type_id": { Type: schema.TypeInt, Required: true, }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, "status": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringInSlice(resourceNetboxCircuitStatusOptions, false), Description: buildValidValueDescription(resourceNetboxCircuitStatusOptions), }, "description": { Type: schema.TypeString, Optional: true, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxCircuitCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableCircuit{} cid := d.Get("cid").(string) data.Cid = &cid data.Status = d.Get("status").(string) data.Description = d.Get("description").(string) providerIDValue, ok := d.GetOk("provider_id") if ok { data.Provider = int64ToPtr(int64(providerIDValue.(int))) } typeIDValue, ok := d.GetOk("type_id") if ok { data.Type = int64ToPtr(int64(typeIDValue.(int))) } tenantIDValue, ok := d.GetOk("tenant_id") if ok { data.Tenant = int64ToPtr(int64(tenantIDValue.(int))) } data.Tags = []*models.NestedTag{} params := circuits.NewCircuitsCircuitsCreateParams().WithData(&data) res, err := api.Circuits.CircuitsCircuitsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxCircuitRead(d, m) } func resourceNetboxCircuitRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := circuits.NewCircuitsCircuitsReadParams().WithID(id) res, err := api.Circuits.CircuitsCircuitsRead(params, nil) if err != nil { if errresp, ok := err.(*circuits.CircuitsCircuitsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-.html d.SetId("") return nil } } return err } d.Set("cid", res.GetPayload().Cid) d.Set("status", res.GetPayload().Status.Value) d.Set("description", res.GetPayload().Description) if res.GetPayload().Provider != nil { d.Set("provider_id", res.GetPayload().Provider.ID) } else { d.Set("provider_id", nil) } if res.GetPayload().Type != nil { d.Set("type_id", res.GetPayload().Type.ID) } else { d.Set("type_id", nil) } if res.GetPayload().Tenant != nil { d.Set("tenant_id", res.GetPayload().Tenant.ID) } else { d.Set("tenant_id", nil) } return nil } func resourceNetboxCircuitUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableCircuit{} cid := d.Get("cid").(string) data.Cid = &cid data.Status = d.Get("status").(string) data.Description = d.Get("description").(string) providerIDValue, ok := d.GetOk("provider_id") if ok { data.Provider = int64ToPtr(int64(providerIDValue.(int))) } typeIDValue, ok := d.GetOk("type_id") if ok { data.Type = int64ToPtr(int64(typeIDValue.(int))) } tenantIDValue, ok := d.GetOk("tenant_id") if ok { data.Tenant = int64ToPtr(int64(tenantIDValue.(int))) } data.Tags = []*models.NestedTag{} params := circuits.NewCircuitsCircuitsPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Circuits.CircuitsCircuitsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxCircuitRead(d, m) } func resourceNetboxCircuitDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := circuits.NewCircuitsCircuitsDeleteParams().WithID(id) _, err := api.Circuits.CircuitsCircuitsDelete(params, nil) if err != nil { if errresp, ok := err.(*circuits.CircuitsCircuitsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_circuit_provider.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/circuits" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxCircuitProvider() *schema.Resource { return &schema.Resource{ Create: resourceNetboxCircuitProviderCreate, Read: resourceNetboxCircuitProviderRead, Update: resourceNetboxCircuitProviderUpdate, Delete: resourceNetboxCircuitProviderDelete, Description: `:meta:subcategory:Circuits:From the [official documentation](https://docs.netbox.dev/en/stable/features/circuits/#providers): > A circuit provider is any entity which provides some form of connectivity of among sites or organizations within a site. While this obviously includes carriers which offer Internet and private transit service, it might also include Internet exchange (IX) points and even organizations with whom you peer directly. Each circuit within NetBox must be assigned a provider and a circuit ID which is unique to that provider. > > Each provider may be assigned an autonomous system number (ASN), an account number, and contact information.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "description": { Type: schema.TypeString, Optional: true, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxCircuitProviderCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableProvider{} name := d.Get("name").(string) data.Name = &name data.Description = d.Get("description").(string) slugValue, slugOk := d.GetOk("slug") // Default slug to generated slug if not given if !slugOk { data.Slug = strToPtr(getSlug(name)) } else { data.Slug = strToPtr(slugValue.(string)) } data.Tags = []*models.NestedTag{} data.Asns = []int64{} params := circuits.NewCircuitsProvidersCreateParams().WithData(&data) res, err := api.Circuits.CircuitsProvidersCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxCircuitProviderRead(d, m) } func resourceNetboxCircuitProviderRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := circuits.NewCircuitsProvidersReadParams().WithID(id) res, err := api.Circuits.CircuitsProvidersRead(params, nil) if err != nil { if errresp, ok := err.(*circuits.CircuitsProvidersReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } d.Set("name", res.GetPayload().Name) d.Set("slug", res.GetPayload().Slug) d.Set("description", res.GetPayload().Description) return nil } func resourceNetboxCircuitProviderUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableProvider{} name := d.Get("name").(string) data.Name = &name data.Description = d.Get("description").(string) slugValue, slugOk := d.GetOk("slug") // Default slug to generated slug if not given if !slugOk { data.Slug = strToPtr(getSlug(name)) } else { data.Slug = strToPtr(slugValue.(string)) } data.Tags = []*models.NestedTag{} data.Asns = []int64{} params := circuits.NewCircuitsProvidersPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Circuits.CircuitsProvidersPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxCircuitProviderRead(d, m) } func resourceNetboxCircuitProviderDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := circuits.NewCircuitsProvidersDeleteParams().WithID(id) _, err := api.Circuits.CircuitsProvidersDelete(params, nil) if err != nil { return err } return nil } ================================================ FILE: netbox/resource_netbox_circuit_provider_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/circuits" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxCircuitProvider_basic(t *testing.T) { testSlug := "circuit_prov" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_circuit_provider" "test" { name = "%[1]s" slug = "%[2]s" description = "This is my circuit provider!" }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_circuit_provider.test", "name", testName), resource.TestCheckResourceAttr("netbox_circuit_provider.test", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_circuit_provider.test", "description", "This is my circuit provider!"), ), }, { Config: fmt.Sprintf(` resource "netbox_circuit_provider" "test" { name = "%[1]s" slug = "%[2]s" description = "This is my circuit provider!" }`, testName+"2", randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_circuit_provider.test", "name", testName+"2"), resource.TestCheckResourceAttr("netbox_circuit_provider.test", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_circuit_provider.test", "description", "This is my circuit provider!"), ), }, { ResourceName: "netbox_circuit_provider.test", ImportState: true, ImportStateVerify: true, }, }, }) } func init() { resource.AddTestSweepers("netbox_circuit_provider", &resource.Sweeper{ Name: "netbox_circuit_provider", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := circuits.NewCircuitsProvidersListParams() res, err := api.Circuits.CircuitsProvidersList(params, nil) if err != nil { return err } for _, CircuitProvider := range res.GetPayload().Results { if strings.HasPrefix(*CircuitProvider.Name, testPrefix) { deleteParams := circuits.NewCircuitsProvidersDeleteParams().WithID(CircuitProvider.ID) _, err := api.Circuits.CircuitsProvidersDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a circuit provider") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_circuit_termination.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/circuits" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var resourceNetboxCircuitTerminationTermSideOptions = []string{"A", "Z"} func resourceNetboxCircuitTermination() *schema.Resource { return &schema.Resource{ Create: resourceNetboxCircuitTerminationCreate, Read: resourceNetboxCircuitTerminationRead, Update: resourceNetboxCircuitTerminationUpdate, Delete: resourceNetboxCircuitTerminationDelete, Description: `:meta:subcategory:Circuits:From the [official documentation](https://docs.netbox.dev/en/stable/features/circuits/#circuit-terminations): > The association of a circuit with a particular site and/or device is modeled separately as a circuit termination. A circuit may have up to two terminations, labeled A and Z. A single-termination circuit can be used when you don't know (or care) about the far end of a circuit (for example, an Internet access circuit which connects to a transit provider). A dual-termination circuit is useful for tracking circuits which connect two sites. > > Each circuit termination is attached to either a site or to a provider network. Site terminations may optionally be connected via a cable to a specific device interface or port within that site. Each termination must be assigned a port speed, and can optionally be assigned an upstream speed if it differs from the downstream speed (a common scenario with e.g. DOCSIS cable modems). Fields are also available to track cross-connect and patch panel details.`, Schema: map[string]*schema.Schema{ "circuit_id": { Type: schema.TypeInt, Required: true, }, "location_id": { Type: schema.TypeInt, Optional: true, ExactlyOneOf: []string{"site_id", "site_group_id", "region_id", "provider_network_id"}, }, "site_id": { Type: schema.TypeInt, Optional: true, ExactlyOneOf: []string{"location_id", "site_group_id", "region_id", "provider_network_id"}, }, "site_group_id": { Type: schema.TypeInt, Optional: true, ExactlyOneOf: []string{"location_id", "site_id", "region_id", "provider_network_id"}, }, "region_id": { Type: schema.TypeInt, Optional: true, ExactlyOneOf: []string{"location_id", "site_id", "site_group_id", "provider_network_id"}, }, "provider_network_id": { Type: schema.TypeInt, Optional: true, ExactlyOneOf: []string{"location_id", "site_id", "site_group_id", "region_id"}, }, "port_speed": { Type: schema.TypeInt, Optional: true, }, "upstream_speed": { Type: schema.TypeInt, Optional: true, }, "term_side": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringInSlice(resourceNetboxCircuitTerminationTermSideOptions, false), Description: buildValidValueDescription(resourceNetboxCircuitTerminationTermSideOptions), }, "description": { Type: schema.TypeString, Optional: true, }, tagsKey: tagsSchema, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxCircuitTerminationCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableCircuitTermination{} termside := d.Get("term_side").(string) data.TermSide = &termside data.Description = d.Get("description").(string) circuitIDValue, ok := d.GetOk("circuit_id") if ok { data.Circuit = int64ToPtr(int64(circuitIDValue.(int))) } siteID := getOptionalInt(d, "site_id") siteGroupID := getOptionalInt(d, "site_group_id") locationID := getOptionalInt(d, "location_id") regionID := getOptionalInt(d, "region_id") providerNetworkID := getOptionalInt(d, "provider_network_id") switch { case siteID != nil: data.TerminationType = strToPtr("dcim.site") data.TerminationID = siteID case siteGroupID != nil: data.TerminationType = strToPtr("dcim.sitegroup") data.TerminationID = siteGroupID case locationID != nil: data.TerminationType = strToPtr("dcim.location") data.TerminationID = locationID case regionID != nil: data.TerminationType = strToPtr("dcim.region") data.TerminationID = regionID case providerNetworkID != nil: data.TerminationType = strToPtr("circuits.providernetwork") data.TerminationID = providerNetworkID default: data.TerminationType = nil data.TerminationID = nil } portspeedValue, ok := d.GetOk("port_speed") if ok { data.PortSpeed = int64ToPtr(int64(portspeedValue.(int))) } upstreamspeedValue, ok := d.GetOk("upstream_speed") if ok { data.UpstreamSpeed = int64ToPtr(int64(upstreamspeedValue.(int))) } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := circuits.NewCircuitsCircuitTerminationsCreateParams().WithData(&data) res, err := api.Circuits.CircuitsCircuitTerminationsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxCircuitTerminationRead(d, m) } func resourceNetboxCircuitTerminationRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := circuits.NewCircuitsCircuitTerminationsReadParams().WithID(id) res, err := api.Circuits.CircuitsCircuitTerminationsRead(params, nil) if err != nil { if errresp, ok := err.(*circuits.CircuitsCircuitTerminationsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-.html d.SetId("") return nil } } return err } term := res.GetPayload() d.Set("term_side", term.TermSide) if term.Circuit != nil { d.Set("circuit_id", term.Circuit.ID) } else { d.Set("circuit_id", nil) } d.Set("site_id", nil) d.Set("site_group_id", nil) d.Set("location_id", nil) d.Set("region_id", nil) d.Set("provider_network_id", nil) d.Set("description", res.GetPayload().Description) if term.TerminationType != nil && term.TerminationID != nil { scopeID := term.TerminationID switch scopeType := term.TerminationType; *scopeType { case "dcim.site": d.Set("site_id", scopeID) case "dcim.sitegroup": d.Set("site_group_id", scopeID) case "dcim.location": d.Set("location_id", scopeID) case "dcim.region": d.Set("region_id", scopeID) case "circuits.providernetwork": d.Set("provider_network_id", scopeID) } } if term.PortSpeed != nil { d.Set("port_speed", term.PortSpeed) } else { d.Set("port_speed", nil) } if term.UpstreamSpeed != nil { d.Set("upstream_speed", term.UpstreamSpeed) } else { d.Set("upstream_speed", nil) } api.readTags(d, term.Tags) cf := getCustomFields(term.CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } return nil } func resourceNetboxCircuitTerminationUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableCircuitTermination{} termside := d.Get("term_side").(string) data.TermSide = &termside data.Description = d.Get("description").(string) circuitIDValue, ok := d.GetOk("circuit_id") if ok { data.Circuit = int64ToPtr(int64(circuitIDValue.(int))) } siteID := getOptionalInt(d, "site_id") siteGroupID := getOptionalInt(d, "site_group_id") locationID := getOptionalInt(d, "location_id") regionID := getOptionalInt(d, "region_id") providerNetworkID := getOptionalInt(d, "provider_network_id") switch { case siteID != nil: data.TerminationType = strToPtr("dcim.site") data.TerminationID = siteID case siteGroupID != nil: data.TerminationType = strToPtr("dcim.sitegroup") data.TerminationID = siteGroupID case locationID != nil: data.TerminationType = strToPtr("dcim.location") data.TerminationID = locationID case regionID != nil: data.TerminationType = strToPtr("dcim.region") data.TerminationID = regionID case providerNetworkID != nil: data.TerminationType = strToPtr("circuits.providernetwork") data.TerminationID = providerNetworkID default: data.TerminationType = nil data.TerminationID = nil } portspeedValue, ok := d.GetOk("port_speed") if ok { data.PortSpeed = int64ToPtr(int64(portspeedValue.(int))) } upstreamspeedValue, ok := d.GetOk("upstream_speed") if ok { data.UpstreamSpeed = int64ToPtr(int64(upstreamspeedValue.(int))) } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } cf, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = cf } params := circuits.NewCircuitsCircuitTerminationsPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Circuits.CircuitsCircuitTerminationsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxCircuitTerminationRead(d, m) } func resourceNetboxCircuitTerminationDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := circuits.NewCircuitsCircuitTerminationsDeleteParams().WithID(id) _, err := api.Circuits.CircuitsCircuitTerminationsDelete(params, nil) if err != nil { if errresp, ok := err.(*circuits.CircuitsCircuitTerminationsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_circuit_termination_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/circuits" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxCircuitTermination_basic(t *testing.T) { testSlug := "circuit_term" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_site" "test" { name = "%[1]s" slug = "%[2]s" status = "active" } resource "netbox_circuit_provider" "test" { name = "%[1]s" slug = "%[2]s" } resource "netbox_circuit_type" "test" { name = "%[1]s" slug = "%[2]s" } resource "netbox_circuit" "test" { cid = "%[1]s" status = "active" provider_id = netbox_circuit_provider.test.id type_id = netbox_circuit_type.test.id } resource "netbox_circuit_termination" "test" { circuit_id = netbox_circuit.test.id term_side = "A" site_id = netbox_site.test.id port_speed = 100000 upstream_speed = 50000 description = "This is my circuit termination!" }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_circuit_termination.test", "circuit_id", "netbox_circuit.test", "id"), resource.TestCheckResourceAttrPair("netbox_circuit_termination.test", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttr("netbox_circuit_termination.test", "port_speed", "100000"), resource.TestCheckResourceAttr("netbox_circuit_termination.test", "upstream_speed", "50000"), resource.TestCheckResourceAttr("netbox_circuit_termination.test", "description", "This is my circuit termination!"), ), }, { ResourceName: "netbox_circuit_termination.test", ImportState: true, ImportStateVerify: true, }, }, }) } func init() { resource.AddTestSweepers("netbox_circuit_termination", &resource.Sweeper{ Name: "netbox_circuit_termination", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := circuits.NewCircuitsCircuitsListParams() res, err := api.Circuits.CircuitsCircuitsList(params, nil) if err != nil { return err } for _, Circuit := range res.GetPayload().Results { if strings.HasPrefix(*Circuit.Cid, testPrefix) { deleteParams := circuits.NewCircuitsCircuitsDeleteParams().WithID(Circuit.ID) _, err := api.Circuits.CircuitsCircuitsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a circuit termination") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_circuit_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/circuits" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxCircuitDependencies(testName string, testSlug string) string { return fmt.Sprintf(` resource "netbox_tenant" "test" { name = "%[1]s" slug = "%[2]s" } resource "netbox_circuit_provider" "test" { name = "%[1]s" slug = "%[2]s" } resource "netbox_circuit_type" "test" { name = "%[1]s" slug = "%[2]s" } `, testName, testSlug) } func TestAccNetboxCircuit_basic(t *testing.T) { testSlug := "circuit_prov" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testAccNetboxCircuitDependencies(testName, randomSlug) + fmt.Sprintf(` resource "netbox_circuit" "test" { cid = "%[1]s" status = "active" description = "This is my circuit!" provider_id = netbox_circuit_provider.test.id type_id = netbox_circuit_type.test.id }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_circuit.test", "cid", testName), resource.TestCheckResourceAttrPair("netbox_circuit.test", "provider_id", "netbox_circuit_provider.test", "id"), resource.TestCheckResourceAttrPair("netbox_circuit.test", "type_id", "netbox_circuit_type.test", "id"), resource.TestCheckResourceAttr("netbox_circuit.test", "description", "This is my circuit!"), ), }, { Config: testAccNetboxCircuitDependencies(testName, randomSlug) + fmt.Sprintf(` resource "netbox_circuit" "test" { cid = "%[1]s" status = "active" description = "This is my circuit!" provider_id = netbox_circuit_provider.test.id type_id = netbox_circuit_type.test.id tenant_id = netbox_tenant.test.id }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_circuit.test", "cid", testName), resource.TestCheckResourceAttrPair("netbox_circuit.test", "provider_id", "netbox_circuit_provider.test", "id"), resource.TestCheckResourceAttrPair("netbox_circuit.test", "type_id", "netbox_circuit_type.test", "id"), resource.TestCheckResourceAttrPair("netbox_circuit.test", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttr("netbox_circuit.test", "description", "This is my circuit!"), ), }, { ResourceName: "netbox_circuit.test", ImportState: true, ImportStateVerify: true, }, }, }) } func init() { resource.AddTestSweepers("netbox_circuit", &resource.Sweeper{ Name: "netbox_circuit", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := circuits.NewCircuitsCircuitsListParams() res, err := api.Circuits.CircuitsCircuitsList(params, nil) if err != nil { return err } for _, Circuit := range res.GetPayload().Results { if strings.HasPrefix(*Circuit.Cid, testPrefix) { deleteParams := circuits.NewCircuitsCircuitsDeleteParams().WithID(Circuit.ID) _, err := api.Circuits.CircuitsCircuitsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a circuit") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_circuit_type.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/circuits" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxCircuitType() *schema.Resource { return &schema.Resource{ Create: resourceNetboxCircuitTypeCreate, Read: resourceNetboxCircuitTypeRead, Update: resourceNetboxCircuitTypeUpdate, Delete: resourceNetboxCircuitTypeDelete, Description: `:meta:subcategory:Circuits:From the [official documentation](https://docs.netbox.dev/en/stable/features/circuits/#circuit-types): > Circuits are classified by functional type. These types are completely customizable, and are typically used to convey the type of service being delivered over a circuit.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "description": { Type: schema.TypeString, Optional: true, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxCircuitTypeCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.CircuitType{} name := d.Get("name").(string) data.Name = &name data.Description = d.Get("description").(string) slugValue, slugOk := d.GetOk("slug") // Default slug to generated slug if not given if !slugOk { data.Slug = strToPtr(getSlug(name)) } else { data.Slug = strToPtr(slugValue.(string)) } data.Tags = []*models.NestedTag{} params := circuits.NewCircuitsCircuitTypesCreateParams().WithData(&data) res, err := api.Circuits.CircuitsCircuitTypesCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxCircuitTypeRead(d, m) } func resourceNetboxCircuitTypeRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := circuits.NewCircuitsCircuitTypesReadParams().WithID(id) res, err := api.Circuits.CircuitsCircuitTypesRead(params, nil) if err != nil { if errresp, ok := err.(*circuits.CircuitsCircuitTypesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } d.Set("name", res.GetPayload().Name) d.Set("slug", res.GetPayload().Slug) d.Set("description", res.GetPayload().Description) return nil } func resourceNetboxCircuitTypeUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.CircuitType{} name := d.Get("name").(string) data.Name = &name data.Description = d.Get("description").(string) slugValue, slugOk := d.GetOk("slug") // Default slug to generated slug if not given if !slugOk { data.Slug = strToPtr(getSlug(name)) } else { data.Slug = strToPtr(slugValue.(string)) } data.Tags = []*models.NestedTag{} params := circuits.NewCircuitsCircuitTypesPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Circuits.CircuitsCircuitTypesPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxCircuitTypeRead(d, m) } func resourceNetboxCircuitTypeDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := circuits.NewCircuitsCircuitTypesDeleteParams().WithID(id) _, err := api.Circuits.CircuitsCircuitTypesDelete(params, nil) if err != nil { if errresp, ok := err.(*circuits.CircuitsCircuitTypesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_circuit_type_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/circuits" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxCircuitType_basic(t *testing.T) { testSlug := "circuit_type" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_circuit_type" "test" { name = "%[1]s" slug = "%[2]s" description = "This is my circuit type!" }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_circuit_type.test", "name", testName), resource.TestCheckResourceAttr("netbox_circuit_type.test", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_circuit_type.test", "description", "This is my circuit type!"), ), }, { ResourceName: "netbox_circuit_type.test", ImportState: true, ImportStateVerify: true, }, }, }) } func init() { resource.AddTestSweepers("netbox_circuit_type", &resource.Sweeper{ Name: "netbox_circuit_type", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := circuits.NewCircuitsCircuitTypesListParams() res, err := api.Circuits.CircuitsCircuitTypesList(params, nil) if err != nil { return err } for _, CircuitType := range res.GetPayload().Results { if strings.HasPrefix(*CircuitType.Name, testPrefix) { deleteParams := circuits.NewCircuitsCircuitTypesDeleteParams().WithID(CircuitType.ID) _, err := api.Circuits.CircuitsCircuitTypesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a circuit type") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_cluster.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/virtualization" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceNetboxCluster() *schema.Resource { return &schema.Resource{ Create: resourceNetboxClusterCreate, Read: resourceNetboxClusterRead, Update: resourceNetboxClusterUpdate, Delete: resourceNetboxClusterDelete, Description: `:meta:subcategory:Virtualization:From the [official documentation](https://netboxlabs.com/docs/netbox/models/virtualization/cluster/): > A cluster is a logical grouping of physical resources within which virtual machines run. Physical devices may be associated with clusters as hosts. This allows users to track on which host(s) a particular virtual machine may reside.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "cluster_type_id": { Type: schema.TypeInt, Required: true, }, "cluster_group_id": { Type: schema.TypeInt, Optional: true, }, "comments": { Type: schema.TypeString, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, "location_id": { Type: schema.TypeInt, Optional: true, ConflictsWith: []string{"site_id", "site_group_id", "region_id"}, }, "site_id": { Type: schema.TypeInt, Optional: true, ConflictsWith: []string{"location_id", "site_group_id", "region_id"}, }, "site_group_id": { Type: schema.TypeInt, Optional: true, ConflictsWith: []string{"location_id", "site_id", "region_id"}, }, "region_id": { Type: schema.TypeInt, Optional: true, ConflictsWith: []string{"location_id", "site_id", "site_group_id"}, }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, tagsKey: tagsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxClusterCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableCluster{} name := d.Get("name").(string) data.Name = &name clusterTypeID := int64(d.Get("cluster_type_id").(int)) data.Type = &clusterTypeID siteID := getOptionalInt(d, "site_id") siteGroupID := getOptionalInt(d, "site_group_id") locationID := getOptionalInt(d, "location_id") regionID := getOptionalInt(d, "region_id") switch { case siteID != nil: data.ScopeType = strToPtr("dcim.site") data.ScopeID = siteID case siteGroupID != nil: data.ScopeType = strToPtr("dcim.sitegroup") data.ScopeID = siteGroupID case locationID != nil: data.ScopeType = strToPtr("dcim.location") data.ScopeID = locationID case regionID != nil: data.ScopeType = strToPtr("dcim.region") data.ScopeID = regionID default: data.ScopeType = nil data.ScopeID = nil } if clusterGroupIDValue, ok := d.GetOk("cluster_group_id"); ok { clusterGroupID := int64(clusterGroupIDValue.(int)) data.Group = &clusterGroupID } data.Comments = getOptionalStr(d, "comments", false) data.Description = getOptionalStr(d, "description", false) if tenantIDValue, ok := d.GetOk("tenant_id"); ok { tenantID := int64(tenantIDValue.(int)) data.Tenant = &tenantID } tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) data.Tags = tags params := virtualization.NewVirtualizationClustersCreateParams().WithData(&data) res, err := api.Virtualization.VirtualizationClustersCreate(params, nil) if err != nil { //return errors.New(getTextFromError(err)) return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxClusterRead(d, m) } func resourceNetboxClusterRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := virtualization.NewVirtualizationClustersReadParams().WithID(id) res, err := api.Virtualization.VirtualizationClustersRead(params, nil) if err != nil { if errresp, ok := err.(*virtualization.VirtualizationClustersReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } cluster := res.GetPayload() d.Set("name", cluster.Name) d.Set("cluster_type_id", cluster.Type.ID) if cluster.Group != nil { d.Set("cluster_group_id", cluster.Group.ID) } else { d.Set("cluster_group_id", nil) } d.Set("comments", cluster.Comments) d.Set("description", cluster.Description) if cluster.Tenant != nil { d.Set("tenant_id", cluster.Tenant.ID) } else { d.Set("tenant_id", nil) } d.Set("site_id", nil) d.Set("site_group_id", nil) d.Set("location_id", nil) d.Set("region_id", nil) if cluster.ScopeType != nil && cluster.ScopeID != nil { scopeID := cluster.ScopeID switch scopeType := cluster.ScopeType; *scopeType { case "dcim.site": d.Set("site_id", scopeID) case "dcim.sitegroup": d.Set("site_group_id", scopeID) case "dcim.location": d.Set("location_id", scopeID) case "dcim.region": d.Set("region_id", scopeID) } } api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxClusterUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableCluster{} name := d.Get("name").(string) data.Name = &name clusterTypeID := int64(d.Get("cluster_type_id").(int)) data.Type = &clusterTypeID if clusterGroupIDValue, ok := d.GetOk("cluster_group_id"); ok { clusterGroupID := int64(clusterGroupIDValue.(int)) data.Group = &clusterGroupID } data.Comments = getOptionalStr(d, "comments", true) data.Description = getOptionalStr(d, "description", true) if tenantIDValue, ok := d.GetOk("tenant_id"); ok { tenantID := int64(tenantIDValue.(int)) data.Tenant = &tenantID } siteID := getOptionalInt(d, "site_id") siteGroupID := getOptionalInt(d, "site_group_id") locationID := getOptionalInt(d, "location_id") regionID := getOptionalInt(d, "region_id") switch { case siteID != nil: data.ScopeType = strToPtr("dcim.site") data.ScopeID = siteID case siteGroupID != nil: data.ScopeType = strToPtr("dcim.sitegroup") data.ScopeID = siteGroupID case locationID != nil: data.ScopeType = strToPtr("dcim.location") data.ScopeID = locationID case regionID != nil: data.ScopeType = strToPtr("dcim.region") data.ScopeID = regionID default: data.ScopeType = nil data.ScopeID = nil } tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) data.Tags = tags params := virtualization.NewVirtualizationClustersPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Virtualization.VirtualizationClustersPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxClusterRead(d, m) } func resourceNetboxClusterDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := virtualization.NewVirtualizationClustersDeleteParams().WithID(id) _, err := api.Virtualization.VirtualizationClustersDelete(params, nil) if err != nil { if errresp, ok := err.(*virtualization.VirtualizationClustersDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_cluster_group.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/virtualization" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxClusterGroup() *schema.Resource { return &schema.Resource{ Create: resourceNetboxClusterGroupCreate, Read: resourceNetboxClusterGroupRead, Update: resourceNetboxClusterGroupUpdate, Delete: resourceNetboxClusterGroupDelete, Description: `:meta:subcategory:Virtualization:From the [official documentation](https://docs.netbox.dev/en/stable/features/virtualization/#cluster-groups): > Cluster groups may be created for the purpose of organizing clusters. The arrangement of clusters into groups is optional.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "description": { Type: schema.TypeString, Optional: true, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxClusterGroupCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.ClusterGroup{} name := d.Get("name").(string) data.Name = &name slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } data.Slug = &slug if description, ok := d.GetOk("description"); ok { data.Description = description.(string) } data.Tags = []*models.NestedTag{} params := virtualization.NewVirtualizationClusterGroupsCreateParams().WithData(&data) res, err := api.Virtualization.VirtualizationClusterGroupsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxClusterGroupRead(d, m) } func resourceNetboxClusterGroupRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := virtualization.NewVirtualizationClusterGroupsReadParams().WithID(id) res, err := api.Virtualization.VirtualizationClusterGroupsRead(params, nil) if err != nil { if errresp, ok := err.(*virtualization.VirtualizationClusterGroupsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } d.Set("name", res.GetPayload().Name) d.Set("slug", res.GetPayload().Slug) d.Set("description", res.GetPayload().Description) return nil } func resourceNetboxClusterGroupUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.ClusterGroup{} name := d.Get("name").(string) data.Name = &name slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } data.Slug = &slug if d.HasChange("description") { // description omits empty values so set to ' ' if description := d.Get("description"); description.(string) == "" { data.Description = " " } else { data.Description = description.(string) } } data.Tags = []*models.NestedTag{} params := virtualization.NewVirtualizationClusterGroupsPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Virtualization.VirtualizationClusterGroupsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxClusterGroupRead(d, m) } func resourceNetboxClusterGroupDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := virtualization.NewVirtualizationClusterGroupsDeleteParams().WithID(id) _, err := api.Virtualization.VirtualizationClusterGroupsDelete(params, nil) if err != nil { if errresp, ok := err.(*virtualization.VirtualizationClusterGroupsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_cluster_group_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/virtualization" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxClusterGroup_basic(t *testing.T) { testSlug := "clstrgrp_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_cluster_group" "test" { name = "%[1]s" slug = "%[1]s" description = "%[1]s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_cluster_group.test", "name", testName), resource.TestCheckResourceAttr("netbox_cluster_group.test", "slug", testName), resource.TestCheckResourceAttr("netbox_cluster_group.test", "description", testName), ), }, { ResourceName: "netbox_cluster_group.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxClusterGroup_defaultSlug(t *testing.T) { testSlug := "clstrgrp_defSlug" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_cluster_group" "test" { name = "%[1]s" description = "%[1]s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_cluster_group.test", "name", testName), resource.TestCheckResourceAttr("netbox_cluster_group.test", "slug", getSlug(testName)), resource.TestCheckResourceAttr("netbox_cluster_group.test", "description", testName), ), }, }, }) } func init() { resource.AddTestSweepers("netbox_cluster_group", &resource.Sweeper{ Name: "netbox_cluster_group", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := virtualization.NewVirtualizationClusterGroupsListParams() res, err := api.Virtualization.VirtualizationClusterGroupsList(params, nil) if err != nil { return err } for _, clusterGroup := range res.GetPayload().Results { if strings.HasPrefix(*clusterGroup.Name, testPrefix) { deleteParams := virtualization.NewVirtualizationClusterGroupsDeleteParams().WithID(clusterGroup.ID) _, err := api.Virtualization.VirtualizationClusterGroupsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a cluster_group") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_cluster_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/virtualization" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxCluster_basic(t *testing.T) { testSlug := "clstr_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_cluster_type" "test" { name = "%[1]s" } resource "netbox_cluster_group" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_cluster" "test" { name = "%[1]s" cluster_type_id = netbox_cluster_type.test.id cluster_group_id = netbox_cluster_group.test.id comments = "%[1]scomments" description = "%[1]sdescription" site_id = netbox_site.test.id tags = [netbox_tag.test.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_cluster.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_cluster.test", "cluster_type_id", "netbox_cluster_type.test", "id"), resource.TestCheckResourceAttrPair("netbox_cluster.test", "cluster_group_id", "netbox_cluster_group.test", "id"), resource.TestCheckResourceAttr("netbox_cluster.test", "comments", testName+"comments"), resource.TestCheckResourceAttr("netbox_cluster.test", "description", testName+"description"), resource.TestCheckResourceAttrPair("netbox_cluster.test", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttr("netbox_cluster.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_cluster.test", "tags.0", testName), ), }, { Config: fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_tag" "test_updatetag" { name = "%[1]s-a" } resource "netbox_cluster_type" "test" { name = "%[1]s" } resource "netbox_cluster_group" "test" { name = "%[1]s" } resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_cluster" "test" { name = "%[1]s" cluster_type_id = netbox_cluster_type.test.id cluster_group_id = netbox_cluster_group.test.id tenant_id = netbox_tenant.test.id tags = [netbox_tag.test.name, netbox_tag.test_updatetag.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_cluster.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_cluster.test", "cluster_type_id", "netbox_cluster_type.test", "id"), resource.TestCheckResourceAttrPair("netbox_cluster.test", "cluster_group_id", "netbox_cluster_group.test", "id"), resource.TestCheckResourceAttrPair("netbox_cluster.test", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttr("netbox_cluster.test", "tags.#", "2"), resource.TestCheckResourceAttr("netbox_cluster.test", "tags.0", testName), resource.TestCheckResourceAttr("netbox_cluster.test", "tags.1", fmt.Sprintf("%[1]s-a", testName)), ), }, { ResourceName: "netbox_cluster.test", ImportState: true, ImportStateVerify: true, }, }, }) } func init() { resource.AddTestSweepers("netbox_cluster", &resource.Sweeper{ Name: "netbox_cluster", Dependencies: []string{"netbox_virtual_machine", "netbox_site"}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := virtualization.NewVirtualizationClustersListParams() res, err := api.Virtualization.VirtualizationClustersList(params, nil) if err != nil { return err } for _, cluster := range res.GetPayload().Results { if strings.HasPrefix(*cluster.Name, testPrefix) { deleteParams := virtualization.NewVirtualizationClustersDeleteParams().WithID(cluster.ID) _, err := api.Virtualization.VirtualizationClustersDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a cluster") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_cluster_type.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/virtualization" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxClusterType() *schema.Resource { return &schema.Resource{ Create: resourceNetboxClusterTypeCreate, Read: resourceNetboxClusterTypeRead, Update: resourceNetboxClusterTypeUpdate, Delete: resourceNetboxClusterTypeDelete, Description: `:meta:subcategory:Virtualization:From the [official documentation](https://docs.netbox.dev/en/stable/features/virtualization/#cluster-types): > A cluster type represents a technology or mechanism by which a cluster is formed. For example, you might create a cluster type named "VMware vSphere" for a locally hosted cluster or "DigitalOcean NYC3" for one hosted by a cloud provider.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxClusterTypeCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } params := virtualization.NewVirtualizationClusterTypesCreateParams().WithData( &models.ClusterType{ Name: &name, Slug: &slug, Tags: []*models.NestedTag{}, }, ) res, err := api.Virtualization.VirtualizationClusterTypesCreate(params, nil) if err != nil { //return errors.New(getTextFromError(err)) return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxClusterTypeRead(d, m) } func resourceNetboxClusterTypeRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := virtualization.NewVirtualizationClusterTypesReadParams().WithID(id) res, err := api.Virtualization.VirtualizationClusterTypesRead(params, nil) if err != nil { if errresp, ok := err.(*virtualization.VirtualizationClusterTypesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } d.Set("name", res.GetPayload().Name) d.Set("slug", res.GetPayload().Slug) return nil } func resourceNetboxClusterTypeUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.ClusterType{} name := d.Get("name").(string) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } data.Slug = &slug data.Name = &name data.Tags = []*models.NestedTag{} params := virtualization.NewVirtualizationClusterTypesPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Virtualization.VirtualizationClusterTypesPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxClusterTypeRead(d, m) } func resourceNetboxClusterTypeDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := virtualization.NewVirtualizationClusterTypesDeleteParams().WithID(id) _, err := api.Virtualization.VirtualizationClusterTypesDelete(params, nil) if err != nil { if errresp, ok := err.(*virtualization.VirtualizationClusterTypesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_cluster_type_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/virtualization" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxClusterType_basic(t *testing.T) { testSlug := "clstr_type_data_basic" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_cluster_type" "test" { name = "%s" slug = "%s" }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_cluster_type.test", "name", testName), resource.TestCheckResourceAttr("netbox_cluster_type.test", "slug", randomSlug), ), }, { ResourceName: "netbox_cluster_type.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxClusterType_defaultSlug(t *testing.T) { testSlug := "clstr_type_data_default_slug" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_cluster_type" "test" { name = "%s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_cluster_type.test", "name", testName), resource.TestCheckResourceAttr("netbox_cluster_type.test", "slug", testName), ), }, }, }) } func init() { resource.AddTestSweepers("netbox_cluster_type", &resource.Sweeper{ Name: "netbox_cluster_type", Dependencies: []string{"netbox_cluster"}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := virtualization.NewVirtualizationClusterTypesListParams() res, err := api.Virtualization.VirtualizationClusterTypesList(params, nil) if err != nil { return err } for _, clusterType := range res.GetPayload().Results { if strings.HasPrefix(*clusterType.Name, testPrefix) { deleteParams := virtualization.NewVirtualizationClusterTypesDeleteParams().WithID(clusterType.ID) _, err := api.Virtualization.VirtualizationClusterTypesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a cluster type") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_config_context.go ================================================ package netbox import ( "encoding/json" "strconv" "github.com/fbreckle/go-netbox/netbox/client/extras" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxConfigContext() *schema.Resource { return &schema.Resource{ Create: resourceNetboxConfigContextCreate, Read: resourceNetboxConfigContextRead, Update: resourceNetboxConfigContextUpdate, Delete: resourceNetboxConfigContextDelete, Description: `:meta:subcategory:Extras:From the [official documentation](https://docs.netbox.dev/en/stable/models/extras/configcontext/): > Context data is made available to devices and/or virtual machines based on their relationships to other objects in NetBox. For example, context data can be associated only with devices assigned to a particular site, or only to virtual machines in a certain cluster.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "description": { Type: schema.TypeString, Optional: true, }, "weight": { Type: schema.TypeInt, Optional: true, Default: 1000, }, "data": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringIsJSON, }, "cluster_groups": { Type: schema.TypeSet, Optional: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "cluster_types": { Type: schema.TypeSet, Optional: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "clusters": { Type: schema.TypeSet, Optional: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "device_types": { Type: schema.TypeSet, Optional: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "locations": { Type: schema.TypeSet, Optional: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "platforms": { Type: schema.TypeSet, Optional: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "regions": { Type: schema.TypeSet, Optional: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "roles": { Type: schema.TypeSet, Optional: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "site_groups": { Type: schema.TypeSet, Optional: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "sites": { Type: schema.TypeSet, Optional: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "tenant_groups": { Type: schema.TypeSet, Optional: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "tenants": { Type: schema.TypeSet, Optional: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "tags": { Type: schema.TypeSet, Optional: true, Default: nil, Elem: &schema.Schema{ Type: schema.TypeString, }, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxConfigContextCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableConfigContext{} data.Name = strToPtr(d.Get("name").(string)) dataJSON, ok := d.GetOk("data") if ok { var jsonObj any localContextBA := []byte(dataJSON.(string)) if err := json.Unmarshal(localContextBA, &jsonObj); err == nil { data.Data = jsonObj } } data.Description = d.Get("description").(string) data.ClusterGroups = toInt64List(d.Get("cluster_groups")) data.ClusterTypes = toInt64List(d.Get("cluster_types")) data.Clusters = toInt64List(d.Get("clusters")) data.DeviceTypes = toInt64List(d.Get("device_types")) data.Locations = toInt64List(d.Get("locations")) data.Platforms = toInt64List(d.Get("platforms")) data.Regions = toInt64List(d.Get("regions")) data.Roles = toInt64List(d.Get("roles")) data.SiteGroups = toInt64List(d.Get("site_groups")) data.Sites = toInt64List(d.Get("sites")) data.TenantGroups = toInt64List(d.Get("tenant_groups")) data.Tenants = toInt64List(d.Get("tenants")) data.Tags = toStringList(d.Get(tagsAllKey)) data.Weight = int64ToPtr(int64(d.Get("weight").(int))) params := extras.NewExtrasConfigContextsCreateParams().WithData(&data) res, err := api.Extras.ExtrasConfigContextsCreate(params, nil) if err != nil { //return errors.New(getTextFromError(err)) return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxConfigContextRead(d, m) } func resourceNetboxConfigContextRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := extras.NewExtrasConfigContextsReadParams().WithID(id) res, err := api.Extras.ExtrasConfigContextsRead(params, nil) if err != nil { if errresp, ok := err.(*extras.ExtrasConfigContextsReadDefault); ok { if errresp.Code() == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } d.Set("name", res.GetPayload().Name) d.Set("description", res.GetPayload().Description) d.Set("weight", res.GetPayload().Weight) if res.GetPayload().Data != nil { if jsonArr, err := json.Marshal(res.GetPayload().Data); err == nil { d.Set("data", string(jsonArr)) } } else { d.Set("data", nil) } clusterGroups := res.GetPayload().ClusterGroups clusterGroupsSlice := make([]int64, len(clusterGroups)) for i, v := range clusterGroups { clusterGroupsSlice[i] = int64(v.ID) } d.Set("cluster_groups", clusterGroupsSlice) clusterTypes := res.GetPayload().ClusterTypes clusterTypesSlice := make([]int64, len(clusterTypes)) for i, v := range clusterTypes { clusterTypesSlice[i] = int64(v.ID) } d.Set("cluster_types", clusterTypesSlice) clusters := res.GetPayload().Clusters clustersSlice := make([]int64, len(clusters)) for i, v := range clusters { clustersSlice[i] = int64(v.ID) } d.Set("clusters", clustersSlice) deviceTypes := res.GetPayload().DeviceTypes deviceTypesSlice := make([]int64, len(deviceTypes)) for i, v := range deviceTypes { deviceTypesSlice[i] = int64(v.ID) } d.Set("device_types", deviceTypesSlice) locations := res.GetPayload().Locations locationsSlice := make([]int64, len(locations)) for i, v := range locations { locationsSlice[i] = int64(v.ID) } d.Set("locations", locationsSlice) platforms := res.GetPayload().Platforms platformsSlice := make([]int64, len(platforms)) for i, v := range platforms { platformsSlice[i] = int64(v.ID) } d.Set("platforms", platformsSlice) regions := res.GetPayload().Regions regionsSlice := make([]int64, len(regions)) for i, v := range regions { regionsSlice[i] = int64(v.ID) } d.Set("regions", regionsSlice) roles := res.GetPayload().Roles rolesSlice := make([]int64, len(roles)) for i, v := range roles { rolesSlice[i] = int64(v.ID) } d.Set("roles", rolesSlice) siteGroups := res.GetPayload().SiteGroups siteGroupsSlice := make([]int64, len(siteGroups)) for i, v := range siteGroups { siteGroupsSlice[i] = int64(v.ID) } d.Set("site_groups", siteGroupsSlice) sites := res.GetPayload().Sites sitesSlice := make([]int64, len(sites)) for i, v := range sites { sitesSlice[i] = int64(v.ID) } d.Set("sites", sitesSlice) // hack since `readTags` mostly deals with nested tags tags := make([]*models.NestedTag, 0, len(res.GetPayload().Tags)) for _, tagName := range res.GetPayload().Tags { tags = append(tags, &models.NestedTag{ Name: &tagName, }) } api.readTags(d, tags) tenantGroups := res.GetPayload().TenantGroups tenantGroupsSlice := make([]int64, len(tenantGroups)) for i, v := range tenantGroups { tenantGroupsSlice[i] = int64(v.ID) } d.Set("tenant_groups", tenantGroupsSlice) tenants := res.GetPayload().Tenants tenantsSlice := make([]int64, len(tenants)) for i, v := range tenants { tenantsSlice[i] = int64(v.ID) } d.Set("tenants", tenantsSlice) return nil } func resourceNetboxConfigContextUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableConfigContext{} name := d.Get("name").(string) data.Name = &name dataValue, ok := d.GetOk("data") if ok { var jsonObj any localContextBA := []byte(dataValue.(string)) if err := json.Unmarshal(localContextBA, &jsonObj); err == nil { data.Data = jsonObj } } data.Description = d.Get("description").(string) data.ClusterGroups = toInt64List(d.Get("cluster_groups")) data.ClusterTypes = toInt64List(d.Get("cluster_types")) data.Clusters = toInt64List(d.Get("clusters")) data.DeviceTypes = toInt64List(d.Get("device_types")) data.Locations = toInt64List(d.Get("locations")) data.Platforms = toInt64List(d.Get("platforms")) data.Regions = toInt64List(d.Get("regions")) data.Roles = toInt64List(d.Get("roles")) data.SiteGroups = toInt64List(d.Get("site_groups")) data.Sites = toInt64List(d.Get("sites")) data.TenantGroups = toInt64List(d.Get("tenant_groups")) data.Tenants = toInt64List(d.Get("tenants")) data.Tags = toStringList(d.Get(tagsAllKey)) data.Weight = int64ToPtr(int64(d.Get("weight").(int))) params := extras.NewExtrasConfigContextsPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Extras.ExtrasConfigContextsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxConfigContextRead(d, m) } func resourceNetboxConfigContextDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := extras.NewExtrasConfigContextsDeleteParams().WithID(id) _, err := api.Extras.ExtrasConfigContextsDelete(params, nil) if err != nil { return err } return nil } ================================================ FILE: netbox/resource_netbox_config_context_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/extras" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxConfigContext_basic(t *testing.T) { testSlug := "config_context_assignments" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_config_context" "test" { name = "%s" description = "test description" data = jsonencode({"testkey" = "testval"}) }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_config_context.test", "name", testName), resource.TestCheckResourceAttr("netbox_config_context.test", "data", "{\"testkey\":\"testval\"}"), ), }, { ResourceName: "netbox_config_context.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxConfigContext_defaultWeight(t *testing.T) { testSlug := "config_context_assignments" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_config_context" "test" { name = "%s" data = jsonencode({"testkey" = "testval"}) }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_config_context.test", "name", testName), resource.TestCheckResourceAttr("netbox_config_context.test", "weight", "1000"), ), }, }, }) } func TestAccNetboxConfigContext_assignments(t *testing.T) { testSlug := "config_context_assignments" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tenant_group" "test" { name = "%[1]s" } resource "netbox_tenant" "test" { name = "%[1]s" group_id = netbox_tenant_group.test.id } resource "netbox_platform" "test" { name = "%[1]s" } resource "netbox_site_group" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" group_id = netbox_site_group.test.id } resource "netbox_region" "test" { name = "%[1]s" } resource "netbox_cluster_type" "test" { name = "%[1]s" } resource "netbox_cluster_group" "test" { name = "%[1]s" } resource "netbox_cluster" "test" { name = "%[1]s" cluster_type_id = netbox_cluster_type.test.id site_id = netbox_site.test.id cluster_group_id = netbox_cluster_group.test.id } resource "netbox_location" "test" { name = "%[1]s" site_id =netbox_site.test.id } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } # Untested: cluster_groups, regions, site_groups, tenant_groups resource "netbox_config_context" "test" { name = "%[1]s" data = jsonencode({"testkey" = "testval"}) regions = [netbox_region.test.id] tenant_groups = [netbox_tenant_group.test.id] tenants = [netbox_tenant.test.id] platforms = [netbox_platform.test.id] sites = [netbox_site.test.id] site_groups = [netbox_site_group.test.id] cluster_types = [netbox_cluster_type.test.id] cluster_groups = [netbox_cluster_group.test.id] clusters = [netbox_cluster.test.id] locations = [netbox_location.test.id] roles = [netbox_device_role.test.id] tags = [netbox_tag.test.name] device_types = [netbox_device_type.test.id] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_config_context.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_config_context.test", "regions.0", "netbox_region.test", "id"), resource.TestCheckResourceAttrPair("netbox_config_context.test", "tenant_groups.0", "netbox_tenant_group.test", "id"), resource.TestCheckResourceAttrPair("netbox_config_context.test", "tenants.0", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("netbox_config_context.test", "platforms.0", "netbox_platform.test", "id"), resource.TestCheckResourceAttrPair("netbox_config_context.test", "sites.0", "netbox_site.test", "id"), resource.TestCheckResourceAttrPair("netbox_config_context.test", "site_groups.0", "netbox_site_group.test", "id"), resource.TestCheckResourceAttrPair("netbox_config_context.test", "cluster_types.0", "netbox_cluster_type.test", "id"), resource.TestCheckResourceAttrPair("netbox_config_context.test", "cluster_groups.0", "netbox_cluster_group.test", "id"), resource.TestCheckResourceAttrPair("netbox_config_context.test", "clusters.0", "netbox_cluster.test", "id"), resource.TestCheckResourceAttrPair("netbox_config_context.test", "locations.0", "netbox_location.test", "id"), resource.TestCheckResourceAttrPair("netbox_config_context.test", "roles.0", "netbox_device_role.test", "id"), resource.TestCheckResourceAttrPair("netbox_config_context.test", "tags.0", "netbox_tag.test", "name"), resource.TestCheckResourceAttrPair("netbox_config_context.test", "device_types.0", "netbox_device_type.test", "id"), ), }, }, }) } func init() { resource.AddTestSweepers("netbox_config_context", &resource.Sweeper{ Name: "netbox_config_context", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := extras.NewExtrasConfigContextsListParams() res, err := api.Extras.ExtrasConfigContextsList(params, nil) if err != nil { return err } for _, configContext := range res.GetPayload().Results { if strings.HasPrefix(*configContext.Name, testPrefix) { deleteParams := extras.NewExtrasConfigContextsDeleteParams().WithID(configContext.ID) _, err := api.Extras.ExtrasConfigContextsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a config context") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_config_template.go ================================================ package netbox import ( "context" "encoding/json" "strconv" "github.com/fbreckle/go-netbox/netbox/client/extras" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxConfigTemplate() *schema.Resource { return &schema.Resource{ CreateContext: resourceNetboxConfigTemplateCreate, ReadContext: resourceNetboxConfigTemplateRead, UpdateContext: resourceNetboxConfigTemplateUpdate, DeleteContext: resourceNetboxConfigTemplateDelete, Description: `:meta:subcategory:Extras:From the [official documentation](https://docs.netbox.dev/en/stable/models/extras/configtemplate/): > Configuration templates can be used to render device configurations from context data. Templates are written in the Jinja2 language and can be associated with devices roles, platforms, and/or individual devices. > Context data is made available to devices and/or virtual machines based on their relationships to other objects in NetBox. For example, context data can be associated only with devices assigned to a particular site, or only to virtual machines in a certain cluster.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "description": { Type: schema.TypeString, Optional: true, }, "template_code": { Type: schema.TypeString, Required: true, }, "environment_params": { Type: schema.TypeString, Optional: true, Default: "{}", ValidateFunc: validation.StringIsJSON, }, tagsKey: tagsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxConfigTemplateCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) var diags diag.Diagnostics name := d.Get("name").(string) description := d.Get("description").(string) templateCode := d.Get("template_code").(string) tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) data := models.WritableConfigTemplate{ Name: &name, Description: description, TemplateCode: &templateCode, Tags: tags, } // Unmarshal environment_params and add it to data if valid environmentParamsJSON, ok := d.GetOk("environment_params") if ok { var environmentParams any err := json.Unmarshal([]byte(environmentParamsJSON.(string)), &environmentParams) if err != nil { return diag.FromErr(err) } data.EnvironmentParams = environmentParams } params := extras.NewExtrasConfigTemplatesCreateParams().WithData(&data) res, err := api.Extras.ExtrasConfigTemplatesCreate(params, nil) if err != nil { return diag.FromErr(err) } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return diags } func resourceNetboxConfigTemplateRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) var diags diag.Diagnostics params := extras.NewExtrasConfigTemplatesReadParams().WithID(id) res, err := api.Extras.ExtrasConfigTemplatesRead(params, nil) if err != nil { if errresp, ok := err.(*extras.ExtrasConfigTemplatesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return diag.FromErr(err) } tmpl := res.GetPayload() d.Set("name", tmpl.Name) d.Set("description", tmpl.Description) d.Set("template_code", tmpl.TemplateCode) if tmpl.EnvironmentParams != nil { environmentParamsJSON, err := json.Marshal(tmpl.EnvironmentParams) if err != nil { return diag.FromErr(err) } d.Set("environment_params", string(environmentParamsJSON)) } else { d.Set("environment_params", "{}") } return diags } func resourceNetboxConfigTemplateUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) var diags diag.Diagnostics id, _ := strconv.ParseInt(d.Id(), 10, 64) name := d.Get("name").(string) description := d.Get("description").(string) templateCode := d.Get("template_code").(string) tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) data := models.WritableConfigTemplate{ Name: &name, Description: description, TemplateCode: &templateCode, Tags: tags, } // Unmarshal environment_params and add it to data if valid environmentParamsJSON, ok := d.GetOk("environment_params") if ok { var environmentParams any err := json.Unmarshal([]byte(environmentParamsJSON.(string)), &environmentParams) if err != nil { return diag.FromErr(err) } data.EnvironmentParams = environmentParams } params := extras.NewExtrasConfigTemplatesUpdateParams().WithID(id).WithData(&data) _, err := api.Extras.ExtrasConfigTemplatesUpdate(params, nil) if err != nil { return diag.FromErr(err) } return diags } func resourceNetboxConfigTemplateDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := extras.NewExtrasConfigTemplatesDeleteParams().WithID(id) _, err := api.Extras.ExtrasConfigTemplatesDelete(params, nil) if err != nil { if errresp, ok := err.(*extras.ExtrasConfigTemplatesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return diag.FromErr(err) } return nil } ================================================ FILE: netbox/resource_netbox_config_template_test.go ================================================ package netbox import ( "fmt" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/extras" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" log "github.com/sirupsen/logrus" ) func TestAccNetboxConfigTemplate_basic(t *testing.T) { testName := testAccGetTestName("config_template") resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_config_template" "test" { name = "%[1]s" description = "%[1]s description" template_code = "hostname {{ name }}" environment_params = jsonencode({"name" = "my-hostname"}) }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_config_template.test", "name", testName), resource.TestCheckResourceAttr("netbox_config_template.test", "description", fmt.Sprintf("%s description", testName)), resource.TestCheckResourceAttr("netbox_config_template.test", "template_code", "hostname {{ name }}"), resource.TestCheckResourceAttr("netbox_config_template.test", "environment_params", "{\"name\":\"my-hostname\"}"), ), }, { Config: fmt.Sprintf(` resource "netbox_config_template" "test" { name = "%[1]s" description = "%[1]s description" template_code = "hostname {{ new_var }}" environment_params = jsonencode({"new_var" = "my-hostname-2"}) }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_config_template.test", "name", testName), resource.TestCheckResourceAttr("netbox_config_template.test", "description", fmt.Sprintf("%s description", testName)), resource.TestCheckResourceAttr("netbox_config_template.test", "template_code", "hostname {{ new_var }}"), resource.TestCheckResourceAttr("netbox_config_template.test", "environment_params", "{\"new_var\":\"my-hostname-2\"}"), ), }, { ResourceName: "netbox_config_template.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxConfigTemplate_tags(t *testing.T) { testName := testAccGetTestName("config_template_tags") resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testAccNetboxVrfTagDependencies(testName) + fmt.Sprintf(` resource "netbox_config_template" "test_tags" { name = "%[1]s" template_code = "hostname test" tags = [netbox_tag.test_a.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_config_template.test_tags", "name", testName), resource.TestCheckResourceAttr("netbox_config_template.test_tags", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_config_template.test_tags", "tags.0", testName+"a"), ), }, { Config: testAccNetboxVrfTagDependencies(testName) + fmt.Sprintf(` resource "netbox_config_template" "test_tags" { name = "%[1]s" template_code = "hostname test" tags = [netbox_tag.test_a.name, netbox_tag.test_b.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_config_template.test_tags", "tags.#", "2"), resource.TestCheckResourceAttr("netbox_config_template.test_tags", "tags.0", testName+"a"), resource.TestCheckResourceAttr("netbox_config_template.test_tags", "tags.1", testName+"b"), ), }, { Config: testAccNetboxVrfTagDependencies(testName) + fmt.Sprintf(` resource "netbox_config_template" "test_tags" { name = "%s" template_code = "hostname test" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_config_template.test_tags", "tags.#", "0"), ), }, }, }) } func init() { resource.AddTestSweepers("netbox_config_template", &resource.Sweeper{ Name: "netbox_config_template", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := extras.NewExtrasConfigTemplatesListParams() res, err := api.Extras.ExtrasConfigTemplatesList(params, nil) if err != nil { return err } for _, tmpl := range res.GetPayload().Results { if strings.HasPrefix(*tmpl.Name, testPrefix) { deleteParams := extras.NewExtrasConfigTemplatesDeleteParams().WithID(tmpl.ID) _, err := api.Extras.ExtrasConfigTemplatesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a config template") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_contact.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/tenancy" "github.com/fbreckle/go-netbox/netbox/models" "github.com/go-openapi/strfmt" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceNetboxContact() *schema.Resource { return &schema.Resource{ Create: resourceNetboxContactCreate, Read: resourceNetboxContactRead, Update: resourceNetboxContactUpdate, Delete: resourceNetboxContactDelete, Description: `:meta:subcategory:Tenancy:From the [official documentation](https://docs.netbox.dev/en/stable/features/contacts/#contacts_1): > A contact should represent an individual or permanent point of contact. Each contact must define a name, and may optionally include a title, phone number, email address, and related details. > > Contacts are reused for assignments, so each unique contact must be created only once and can be assigned to any number of NetBox objects, and there is no limit to the number of assigned contacts an object may have. Most core objects in NetBox can have contacts assigned to them.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, tagsKey: tagsSchema, "group_id": { Type: schema.TypeInt, Optional: true, }, "email": { Type: schema.TypeString, Optional: true, }, "phone": { Type: schema.TypeString, Optional: true, }, "link": { Type: schema.TypeString, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxContactCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) phone := d.Get("phone").(string) email := d.Get("email").(string) link := d.Get("link").(string) description := d.Get("description").(string) groupID := int64(d.Get("group_id").(int)) tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) data := &models.WritableContact{} data.Name = &name data.Tags = tags data.Phone = phone data.Email = strfmt.Email(email) data.Link = strfmt.URI(link) data.Description = description if groupID != 0 { data.Group = &groupID } params := tenancy.NewTenancyContactsCreateParams().WithData(data) res, err := api.Tenancy.TenancyContactsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxContactRead(d, m) } func resourceNetboxContactRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := tenancy.NewTenancyContactsReadParams().WithID(id) res, err := api.Tenancy.TenancyContactsRead(params, nil) if err != nil { if errresp, ok := err.(*tenancy.TenancyContactsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } d.Set("name", res.GetPayload().Name) d.Set("phone", res.GetPayload().Phone) d.Set("email", res.GetPayload().Email) d.Set("link", res.GetPayload().Link) d.Set("description", res.GetPayload().Description) if res.GetPayload().Group != nil { d.Set("group_id", res.GetPayload().Group.ID) } return nil } func resourceNetboxContactUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableContact{} name := d.Get("name").(string) phone := d.Get("phone").(string) email := d.Get("email").(string) link := d.Get("link").(string) description := d.Get("description").(string) groupID := int64(d.Get("group_id").(int)) tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) data.Name = &name data.Tags = tags data.Phone = phone data.Email = strfmt.Email(email) data.Link = strfmt.URI(link) data.Description = description if groupID != 0 { data.Group = &groupID } params := tenancy.NewTenancyContactsPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Tenancy.TenancyContactsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxContactRead(d, m) } func resourceNetboxContactDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := tenancy.NewTenancyContactsDeleteParams().WithID(id) _, err := api.Tenancy.TenancyContactsDelete(params, nil) if err != nil { if errresp, ok := err.(*tenancy.TenancyContactsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_contact_assignment.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/tenancy" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var resourceNetboxContactAssignmentPriorityOptions = []string{"primary", "secondary", "tertiary", "inactive"} func resourceNetboxContactAssignment() *schema.Resource { return &schema.Resource{ Create: resourceNetboxContactAssignmentCreate, Read: resourceNetboxContactAssignmentRead, Update: resourceNetboxContactAssignmentUpdate, Delete: resourceNetboxContactAssignmentDelete, Description: `:meta:subcategory:Tenancy:From the [official documentation](https://docs.netbox.dev/en/stable/features/contacts#contactassignments_1): > Much like tenancy, contact assignment enables you to track ownership of resources modeled in NetBox.`, Schema: map[string]*schema.Schema{ "content_type": { Type: schema.TypeString, Required: true, }, "object_id": { Type: schema.TypeInt, Required: true, }, "contact_id": { Type: schema.TypeInt, Required: true, }, "role_id": { Type: schema.TypeInt, Required: true, }, "priority": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringInSlice(resourceNetboxContactAssignmentPriorityOptions, false), Description: buildValidValueDescription(resourceNetboxContactAssignmentPriorityOptions), }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxContactAssignmentCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) contentType := d.Get("content_type").(string) objectID := int64(d.Get("object_id").(int)) contactID := int64(d.Get("contact_id").(int)) roleID := int64(d.Get("role_id").(int)) priority := d.Get("priority").(string) data := &models.WritableContactAssignment{} data.ObjectType = contentType data.ObjectID = &objectID data.Contact = &contactID data.Role = &roleID data.Priority = priority params := tenancy.NewTenancyContactAssignmentsCreateParams().WithData(data) res, err := api.Tenancy.TenancyContactAssignmentsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxContactAssignmentRead(d, m) } func resourceNetboxContactAssignmentRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := tenancy.NewTenancyContactAssignmentsReadParams().WithID(id) res, err := api.Tenancy.TenancyContactAssignmentsRead(params, nil) if err != nil { if errresp, ok := err.(*tenancy.TenancyContactAssignmentsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } d.Set("content_type", res.GetPayload().ObjectType) if res.GetPayload().ObjectID != nil { d.Set("object_id", res.GetPayload().ObjectID) } if res.GetPayload().Contact != nil { d.Set("contact_id", res.GetPayload().Contact.ID) } if res.GetPayload().Role != nil { d.Set("role_id", res.GetPayload().Role.ID) } if res.GetPayload().Priority != nil { d.Set("priority", res.GetPayload().Priority.Value) } return nil } func resourceNetboxContactAssignmentUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableContactAssignment{} contentType := d.Get("content_type").(string) objectID := int64(d.Get("object_id").(int)) contactID := int64(d.Get("contact_id").(int)) roleID := int64(d.Get("role_id").(int)) priority := d.Get("priority").(string) data.ObjectType = contentType if objectID != 0 { data.ObjectID = &objectID } if contactID != 0 { data.Contact = &contactID } if roleID != 0 { data.Role = &roleID } data.Priority = priority params := tenancy.NewTenancyContactAssignmentsPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Tenancy.TenancyContactAssignmentsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxContactAssignmentRead(d, m) } func resourceNetboxContactAssignmentDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := tenancy.NewTenancyContactAssignmentsDeleteParams().WithID(id) _, err := api.Tenancy.TenancyContactAssignmentsDelete(params, nil) if err != nil { if errresp, ok := err.(*tenancy.TenancyContactAssignmentsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_contact_assignment_test.go ================================================ package netbox import ( "fmt" "log" "testing" "github.com/fbreckle/go-netbox/netbox/client/tenancy" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxContactAssignment_basic(t *testing.T) { testSlug := "contactassign" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_site" "test" { name = "%[1]s" slug = "%[2]s" } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device" "test" { name = "%[1]s" site_id = netbox_site.test.id role_id = netbox_device_role.test.id device_type_id = netbox_device_type.test.id } resource "netbox_contact" "test" { name = "%[1]s" } resource "netbox_contact_role" "test" { name = "%[1]s" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_contact_assignment" "test" { content_type = "dcim.device" object_id = netbox_device.test.id contact_id = netbox_contact.test.id role_id = netbox_contact_role.test.id priority = "primary" }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_contact_assignment.test", "content_type", "dcim.device"), resource.TestCheckResourceAttrPair("netbox_contact_assignment.test", "object_id", "netbox_device.test", "id"), resource.TestCheckResourceAttrPair("netbox_contact_assignment.test", "contact_id", "netbox_contact.test", "id"), resource.TestCheckResourceAttrPair("netbox_contact_assignment.test", "role_id", "netbox_contact_role.test", "id"), resource.TestCheckResourceAttr("netbox_contact_assignment.test", "priority", "primary"), ), }, { ResourceName: "netbox_contact_assignment.test", ImportState: true, ImportStateVerify: true, }, }, }) } func init() { resource.AddTestSweepers("netbox_contact_assignment", &resource.Sweeper{ Name: "netbox_contact_assignment", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := tenancy.NewTenancyContactAssignmentsListParams() res, err := api.Tenancy.TenancyContactAssignmentsList(params, nil) if err != nil { return err } for _, contactassignment := range res.GetPayload().Results { deleteParams := tenancy.NewTenancyContactAssignmentsDeleteParams().WithID(contactassignment.ID) _, err := api.Tenancy.TenancyContactAssignmentsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a contact assignment") } return nil }, }) } ================================================ FILE: netbox/resource_netbox_contact_group.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/tenancy" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxContactGroup() *schema.Resource { return &schema.Resource{ Create: resourceNetboxContactGroupCreate, Read: resourceNetboxContactGroupRead, Update: resourceNetboxContactGroupUpdate, Delete: resourceNetboxContactGroupDelete, Description: `:meta:subcategory:Tenancy:From the [official documentation](https://docs.netbox.dev/en/stable/features/contacts/#contact-groups): > Contacts can be grouped arbitrarily into a recursive hierarchy, and a contact can be assigned to a group at any level within the hierarchy.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "parent_id": { Type: schema.TypeInt, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxContactGroupCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) parentID := int64(d.Get("parent_id").(int)) description := d.Get("description").(string) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } data := &models.WritableContactGroup{} data.Name = &name data.Slug = &slug data.Description = description data.Tags = []*models.NestedTag{} if parentID != 0 { data.Parent = &parentID } params := tenancy.NewTenancyContactGroupsCreateParams().WithData(data) res, err := api.Tenancy.TenancyContactGroupsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxContactGroupRead(d, m) } func resourceNetboxContactGroupRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := tenancy.NewTenancyContactGroupsReadParams().WithID(id) res, err := api.Tenancy.TenancyContactGroupsRead(params, nil) if err != nil { if errresp, ok := err.(*tenancy.TenancyContactGroupsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } d.Set("name", res.GetPayload().Name) d.Set("slug", res.GetPayload().Slug) d.Set("description", res.GetPayload().Description) if res.GetPayload().Parent != nil { d.Set("parent", res.GetPayload().Parent.ID) } return nil } func resourceNetboxContactGroupUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableContactGroup{} name := d.Get("name").(string) description := d.Get("description").(string) parentID := int64(d.Get("parent_id").(int)) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } data.Slug = &slug data.Name = &name data.Description = description data.Tags = []*models.NestedTag{} if parentID != 0 { data.Parent = &parentID } params := tenancy.NewTenancyContactGroupsPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Tenancy.TenancyContactGroupsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxContactGroupRead(d, m) } func resourceNetboxContactGroupDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := tenancy.NewTenancyContactGroupsDeleteParams().WithID(id) _, err := api.Tenancy.TenancyContactGroupsDelete(params, nil) if err != nil { return err } return nil } ================================================ FILE: netbox/resource_netbox_contact_group_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/tenancy" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxContactGroup_basic(t *testing.T) { testSlug := "t_grp_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_contact_group" "test" { name = "%s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_contact_group.test", "name", testName), ), }, { ResourceName: "netbox_contact_group.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxContactGroup_defaultSlug(t *testing.T) { testSlug := "contactgrp_defSlug" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_contact_group" "test" { name = "%s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_contact_group.test", "name", testName), ), }, }, }) } func init() { resource.AddTestSweepers("netbox_contact_group", &resource.Sweeper{ Name: "netbox_contact_group", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := tenancy.NewTenancyContactGroupsListParams() res, err := api.Tenancy.TenancyContactGroupsList(params, nil) if err != nil { return err } for _, contactGroup := range res.GetPayload().Results { if strings.HasPrefix(*contactGroup.Name, testPrefix) { deleteParams := tenancy.NewTenancyContactGroupsDeleteParams().WithID(contactGroup.ID) _, err := api.Tenancy.TenancyContactGroupsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a contact group") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_contact_role.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/tenancy" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxContactRole() *schema.Resource { return &schema.Resource{ Create: resourceNetboxContactRoleCreate, Read: resourceNetboxContactRoleRead, Update: resourceNetboxContactRoleUpdate, Delete: resourceNetboxContactRoleDelete, Description: `:meta:subcategory:Tenancy:From the [official documentation](https://docs.netbox.dev/en/stable/features/contacts/#contactroles): > A contact role defines the relationship of a contact to an assigned object. For example, you might define roles for administrative, operational, and emergency contacts`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxContactRoleCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) data := &models.ContactRole{} slugValue, slugOk := d.GetOk("slug") // Default slug to generated slug if not given if !slugOk { data.Slug = strToPtr(getSlug(name)) } else { data.Slug = strToPtr(slugValue.(string)) } data.Name = &name data.Tags = []*models.NestedTag{} params := tenancy.NewTenancyContactRolesCreateParams().WithData(data) res, err := api.Tenancy.TenancyContactRolesCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxContactRoleRead(d, m) } func resourceNetboxContactRoleRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := tenancy.NewTenancyContactRolesReadParams().WithID(id) res, err := api.Tenancy.TenancyContactRolesRead(params, nil) if err != nil { if errresp, ok := err.(*tenancy.TenancyContactRolesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } contactrole := res.GetPayload() d.Set("name", contactrole.Name) d.Set("slug", contactrole.Slug) return nil } func resourceNetboxContactRoleUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.ContactRole{} name := d.Get("name").(string) slugValue, slugOk := d.GetOk("slug") // Default slug to generated slug if not given if !slugOk { data.Slug = strToPtr(getSlug(name)) } else { data.Slug = strToPtr(slugValue.(string)) } data.Name = &name data.Tags = []*models.NestedTag{} params := tenancy.NewTenancyContactRolesPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Tenancy.TenancyContactRolesPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxContactRoleRead(d, m) } func resourceNetboxContactRoleDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := tenancy.NewTenancyContactRolesDeleteParams().WithID(id) _, err := api.Tenancy.TenancyContactRolesDelete(params, nil) if err != nil { if errresp, ok := err.(*tenancy.TenancyContactRolesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_contact_role_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/tenancy" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxContactRole_basic(t *testing.T) { testSlug := "contactrole" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_contact_role" "test" { name = "%s" slug = "%s" }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_contact_role.test", "name", testName), ), }, { ResourceName: "netbox_contact_role.test", ImportState: true, ImportStateVerify: true, }, }, }) } func init() { resource.AddTestSweepers("netbox_contact_role", &resource.Sweeper{ Name: "netbox_contact_role", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := tenancy.NewTenancyContactRolesListParams() res, err := api.Tenancy.TenancyContactRolesList(params, nil) if err != nil { return err } for _, contactrole := range res.GetPayload().Results { if strings.HasPrefix(*contactrole.Name, testPrefix) { deleteParams := tenancy.NewTenancyContactRolesDeleteParams().WithID(contactrole.ID) _, err := api.Tenancy.TenancyContactRolesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a contact role") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_contact_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/tenancy" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxContactTagDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tag" "test_a" { name = "%[1]sa" } resource "netbox_tag" "test_b" { name = "%[1]sb" } `, testName) } func TestAccNetboxContact_basic(t *testing.T) { testSlug := "contact_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_contact" "test" { name = "%s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_contact.test", "name", testName), ), }, { Config: fmt.Sprintf(` resource "netbox_contact" "test" { name = "%s" email = "test@test.com" phone = "123-123123" link = "https://example.com" description = "desc" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_contact.test", "name", testName), resource.TestCheckResourceAttr("netbox_contact.test", "email", "test@test.com"), resource.TestCheckResourceAttr("netbox_contact.test", "phone", "123-123123"), resource.TestCheckResourceAttr("netbox_contact.test", "link", "https://example.com"), resource.TestCheckResourceAttr("netbox_contact.test", "description", "desc"), ), }, { ResourceName: "netbox_contact.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxContact_tags(t *testing.T) { testSlug := "contact_tags" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testAccNetboxContactTagDependencies(testName) + fmt.Sprintf(` resource "netbox_contact" "test_tags" { name = "%[1]s" tags = [netbox_tag.test_a.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_contact.test_tags", "name", testName), resource.TestCheckResourceAttr("netbox_contact.test_tags", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_contact.test_tags", "tags.0", testName+"a"), ), }, { Config: testAccNetboxContactTagDependencies(testName) + fmt.Sprintf(` resource "netbox_contact" "test_tags" { name = "%[1]s" tags = [netbox_tag.test_a.name, netbox_tag.test_b.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_contact.test_tags", "tags.#", "2"), resource.TestCheckResourceAttr("netbox_contact.test_tags", "tags.0", testName+"a"), resource.TestCheckResourceAttr("netbox_contact.test_tags", "tags.1", testName+"b"), ), }, { Config: testAccNetboxContactTagDependencies(testName) + fmt.Sprintf(` resource "netbox_contact" "test_tags" { name = "%s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_contact.test_tags", "tags.#", "0"), ), }, }, }) } func init() { resource.AddTestSweepers("netbox_contact", &resource.Sweeper{ Name: "netbox_contact", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := tenancy.NewTenancyContactsListParams() res, err := api.Tenancy.TenancyContactsList(params, nil) if err != nil { return err } for _, contact := range res.GetPayload().Results { if strings.HasPrefix(*contact.Name, testPrefix) { deleteParams := tenancy.NewTenancyContactsDeleteParams().WithID(contact.ID) _, err := api.Tenancy.TenancyContactsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a contact") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_custom_field.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/extras" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceCustomField() *schema.Resource { return &schema.Resource{ Create: resourceNetboxCustomFieldCreate, Read: resourceNetboxCustomFieldRead, Update: resourceNetboxCustomFieldUpdate, Delete: resourceNetboxCustomFieldDelete, Description: `:meta:subcategory:Extras:From the [official documentation](https://docs.netbox.dev/en/stable/customization/custom-fields/#custom-fields): > Each model in NetBox is represented in the database as a discrete table, and each attribute of a model exists as a column within its table. For example, sites are stored in the dcim_site table, which has columns named name, facility, physical_address, and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows. > > However, some users might want to store additional object attributes that are somewhat esoteric in nature, and that would not make sense to include in the core NetBox database schema. For instance, suppose your organization needs to associate each device with a ticket number correlating it with an internal support system record. This is certainly a legitimate use for NetBox, but it's not a common enough need to warrant including a field for every NetBox installation. Instead, you can create a custom field to hold this data.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "type": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringInSlice([]string{ models.CustomFieldTypeValueText, models.CustomFieldTypeValueInteger, models.CustomFieldTypeValueBoolean, models.CustomFieldTypeValueDate, models.CustomFieldTypeValueURL, models.CustomFieldTypeValueSelect, models.CustomFieldTypeValueMultiselect, models.CustomFieldTypeValueJSON, }, false), }, "content_types": { Type: schema.TypeSet, Required: true, Elem: &schema.Schema{ Type: schema.TypeString, }, Set: schema.HashString, }, "weight": { Type: schema.TypeInt, Required: true, DefaultFunc: func() (interface{}, error) { return 100, nil }, }, "default": { Type: schema.TypeString, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, "group_name": { Type: schema.TypeString, Optional: true, }, "label": { Type: schema.TypeString, Optional: true, }, "required": { Type: schema.TypeBool, Optional: true, }, "validation_maximum": { Type: schema.TypeInt, Optional: true, }, "validation_minimum": { Type: schema.TypeInt, Optional: true, }, "validation_regex": { Type: schema.TypeString, Optional: true, }, "choice_set_id": { Type: schema.TypeInt, Optional: true, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxCustomFieldUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := &models.WritableCustomField{ Name: strToPtr(d.Get("name").(string)), Type: d.Get("type").(string), Default: d.Get("default").(string), Description: d.Get("description").(string), GroupName: d.Get("group_name").(string), Label: d.Get("label").(string), Required: d.Get("required").(bool), ValidationRegex: d.Get("validation_regex").(string), Weight: int64ToPtr(int64(d.Get("weight").(int))), } choiceSet, ok := d.GetOk("choice_set_id") if ok { data.ChoiceSet = int64ToPtr(int64(choiceSet.(int))) } ctypes, ok := d.GetOk("content_types") if ok { ctypes := ctypes.(*schema.Set).List() objectTypes := make([]string, 0, len(ctypes)) for _, t := range ctypes { objectTypes = append(objectTypes, t.(string)) } data.ObjectTypes = objectTypes } vmax, ok := d.GetOk("validation_maximum") if ok { data.ValidationMaximum = int64ToPtr(int64(vmax.(int))) } vmin, ok := d.GetOk("validation_minimum") if ok { data.ValidationMinimum = int64ToPtr(int64(vmin.(int))) } params := extras.NewExtrasCustomFieldsUpdateParams().WithID(id).WithData(data) res, err := api.Extras.ExtrasCustomFieldsUpdate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxCustomFieldRead(d, m) } func resourceNetboxCustomFieldCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := &models.WritableCustomField{ Name: strToPtr(d.Get("name").(string)), Type: d.Get("type").(string), Default: d.Get("default").(string), Description: d.Get("description").(string), GroupName: d.Get("group_name").(string), Label: d.Get("label").(string), Required: d.Get("required").(bool), ValidationRegex: d.Get("validation_regex").(string), Weight: int64ToPtr(int64(d.Get("weight").(int))), } choiceSet, ok := d.GetOk("choice_set_id") if ok { data.ChoiceSet = int64ToPtr(int64(choiceSet.(int))) } ctypes, ok := d.GetOk("content_types") if ok { ctypes := ctypes.(*schema.Set).List() objectTypes := make([]string, 0, len(ctypes)) for _, t := range ctypes { objectTypes = append(objectTypes, t.(string)) } data.ObjectTypes = objectTypes } vmax, ok := d.GetOk("validation_maximum") if ok { data.ValidationMaximum = int64ToPtr(int64(vmax.(int))) } vmin, ok := d.GetOk("validation_minimum") if ok { data.ValidationMinimum = int64ToPtr(int64(vmin.(int))) } params := extras.NewExtrasCustomFieldsCreateParams().WithData(data) res, err := api.Extras.ExtrasCustomFieldsCreate(params, nil) if err != nil { //return errors.New(getTextFromError(err)) return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxCustomFieldRead(d, m) } func resourceNetboxCustomFieldRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := extras.NewExtrasCustomFieldsReadParams().WithID(id) res, err := api.Extras.ExtrasCustomFieldsRead(params, nil) if err != nil { errapi, ok := err.(*extras.ExtrasCustomFieldsReadDefault) if !ok { return err } errorcode := errapi.Code() if errorcode == 404 { d.SetId("") return nil } return err } customField := res.GetPayload() d.Set("name", customField.Name) d.Set("type", *customField.Type.Value) d.Set("content_types", customField.ObjectTypes) choiceSet := customField.ChoiceSet if choiceSet != nil { d.Set("choice_set_id", customField.ChoiceSet.ID) } d.Set("weight", customField.Weight) if customField.Default != nil { d.Set("default", customField.Default) } d.Set("description", customField.Description) d.Set("group_name", customField.GroupName) d.Set("label", customField.Label) d.Set("required", customField.Required) d.Set("validation_maximum", customField.ValidationMaximum) d.Set("validation_minimum", customField.ValidationMinimum) d.Set("validation_regex", customField.ValidationRegex) return nil } func resourceNetboxCustomFieldDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := extras.NewExtrasCustomFieldsDeleteParams().WithID(id) _, err := api.Extras.ExtrasCustomFieldsDelete(params, nil) if err != nil { if errresp, ok := err.(*extras.ExtrasCustomFieldsDeleteDefault); ok { errorcode := errresp.Code() if errorcode == 404 { d.SetId("") } } return err } return nil } ================================================ FILE: netbox/resource_netbox_custom_field_choice_set.go ================================================ package netbox import ( "errors" "strconv" "github.com/fbreckle/go-netbox/netbox/client/extras" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var resourceNetboxCustomFieldChoiceSetBaseChoicesOptions = []string{"IATA", "ISO_3166", "UN_LOCODE"} func resourceNetboxCustomFieldChoiceSet() *schema.Resource { return &schema.Resource{ Create: resourceNetboxCustomFieldChoiceSetCreate, Read: resourceNetboxCustomFieldChoiceSetRead, Update: resourceNetboxCustomFieldChoiceSetUpdate, Delete: resourceNetboxCustomFieldChoiceSetDelete, Description: `:meta:subcategory:Extras:From the [official documentation](https://docs.netbox.dev/en/stable/models/extras/customfieldchoiceset/): Single- and multi-selection custom fields must define a set of valid choices from which the user may choose when defining the field value. These choices are defined as sets that may be reused among multiple custom fields. A choice set must define a base choice set and/or a set of arbitrary extra choices.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "description": { Type: schema.TypeString, Optional: true, }, "base_choices": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringInSlice(resourceNetboxCustomFieldChoiceSetBaseChoicesOptions, false), Description: buildValidValueDescription(resourceNetboxCustomFieldChoiceSetBaseChoicesOptions), AtLeastOneOf: []string{"base_choices", "extra_choices"}, }, "extra_choices": { Type: schema.TypeList, Description: "This length of the inner lists must be exactly two, where the first value is the value of a choice and the second value is the label of the choice.", Elem: &schema.Schema{ Type: schema.TypeList, Elem: &schema.Schema{ Type: schema.TypeString, }, }, Optional: true, AtLeastOneOf: []string{"base_choices", "extra_choices"}, // ValidateFunc: func (i interface{}, k string) (warnings []string, _errors []error) { // // Outer list length must be > 0 // extraChoiceListList := i.([][]string) // if len(extraChoiceListList) == 0 { // return nil, []error{errors.New("length of list must be > 0")} // } // // // Inner list length must be exactly 2 // for _, innerList := range extraChoiceListList { // if len(innerList) != 2 { // return nil, []error{errors.New("length of list must be > 0")} // } // } // // return warnings, _errors //}, }, "order_alphabetically": { Type: schema.TypeBool, Optional: true, Description: "experimental", Default: false, }, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxCustomFieldChoiceSetCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) data := models.CustomFieldChoiceSet{ Name: &name, } data.Description = getOptionalStr(d, "description", false) var extraChoiceListList [][]string extraChoices, ok := d.GetOk("extra_choices") if ok { for _, innerList := range extraChoices.([]interface{}) { tmp := innerList.([]interface{}) if len(tmp) != 2 { return errors.New("length of inner lists must be exactly two for custom field choice sets") } extraChoiceListList = append(extraChoiceListList, []string{tmp[0].(string), tmp[1].(string)}) } data.ExtraChoices = extraChoiceListList } params := extras.NewExtrasCustomFieldChoiceSetsCreateParams().WithData(&data) res, err := api.Extras.ExtrasCustomFieldChoiceSetsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxCustomFieldChoiceSetRead(d, m) } func resourceNetboxCustomFieldChoiceSetRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := extras.NewExtrasCustomFieldChoiceSetsReadParams().WithID(id) res, err := api.Extras.ExtrasCustomFieldChoiceSetsRead(params, nil) if err != nil { if errresp, ok := err.(*extras.ExtrasCustomFieldChoiceSetsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } choiceSet := res.GetPayload() d.Set("name", choiceSet.Name) if choiceSet.Description != "" { d.Set("description", choiceSet.Description) } else { d.Set("description", nil) } return nil } func resourceNetboxCustomFieldChoiceSetUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) name := d.Get("name").(string) data := models.CustomFieldChoiceSet{ Name: &name, } data.Description = getOptionalStr(d, "description", true) var extraChoiceListList [][]string extraChoices, ok := d.GetOk("extra_choices") if ok { for _, innerList := range extraChoices.([]interface{}) { tmp := innerList.([]interface{}) if len(tmp) != 2 { return errors.New("length of inner lists must be exactly two for custom field choice sets") } extraChoiceListList = append(extraChoiceListList, []string{tmp[0].(string), tmp[1].(string)}) } data.ExtraChoices = extraChoiceListList } params := extras.NewExtrasCustomFieldChoiceSetsPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Extras.ExtrasCustomFieldChoiceSetsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxCustomFieldChoiceSetRead(d, m) } func resourceNetboxCustomFieldChoiceSetDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := extras.NewExtrasCustomFieldChoiceSetsDeleteParams().WithID(id) _, err := api.Extras.ExtrasCustomFieldChoiceSetsDelete(params, nil) if err != nil { if errresp, ok := err.(*extras.ExtrasCustomFieldChoiceSetsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_custom_field_choice_set_test.go ================================================ package netbox import ( "fmt" "regexp" "strings" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxCustomFieldChoiceSet_basic(t *testing.T) { testSlug := "cfields_choiceset" testName := strings.ReplaceAll(testAccGetTestName(testSlug), "-", "_") resource.Test(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_custom_field_choice_set" "test" { name = "%s" description = "foo" extra_choices = [ ["choice1", "label1"], # label and choice are different ["choice2", "choice2"] # label and choice are the same ] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_custom_field_choice_set.test", "name", testName), resource.TestCheckResourceAttr("netbox_custom_field_choice_set.test", "description", "foo"), resource.TestCheckResourceAttr("netbox_custom_field_choice_set.test", "extra_choices.0.0", "choice1"), resource.TestCheckResourceAttr("netbox_custom_field_choice_set.test", "extra_choices.0.1", "label1"), resource.TestCheckResourceAttr("netbox_custom_field_choice_set.test", "extra_choices.1.0", "choice2"), resource.TestCheckResourceAttr("netbox_custom_field_choice_set.test", "extra_choices.1.1", "choice2"), ), }, }, }) } func TestAccNetboxCustomFieldChoiceSet_listlength(t *testing.T) { testSlug := "cfields_choiceset_length" testName := strings.ReplaceAll(testAccGetTestName(testSlug), "-", "_") resource.Test(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_custom_field_choice_set" "test" { name = "%s" description = "foo" extra_choices = [ ["choice1", "label1", "toolong"] ] }`, testName), ExpectError: regexp.MustCompile("length of inner lists must be exactly two for custom field choice sets"), }, { Config: fmt.Sprintf(` resource "netbox_custom_field_choice_set" "test" { name = "%s" description = "foo" extra_choices = [ ["choice1"] ] }`, testName), ExpectError: regexp.MustCompile("length of inner lists must be exactly two for custom field choice sets"), }, }, }) } ================================================ FILE: netbox/resource_netbox_custom_field_test.go ================================================ package netbox import ( "fmt" "strings" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxCustomField_basic(t *testing.T) { testSlug := "custom_fields_basic" testName := strings.ReplaceAll(testAccGetTestName(testSlug), "-", "_") resource.Test(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_custom_field" "test" { name = "%s" type = "text" content_types = ["virtualization.vminterface"] weight = 100 default = "red" validation_regex = "^.*$" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_custom_field.test", "name", testName), resource.TestCheckResourceAttr("netbox_custom_field.test", "type", "text"), resource.TestCheckTypeSetElemAttr("netbox_custom_field.test", "content_types.*", "virtualization.vminterface"), resource.TestCheckResourceAttr("netbox_custom_field.test", "weight", "100"), resource.TestCheckResourceAttr("netbox_custom_field.test", "default", "red"), resource.TestCheckResourceAttr("netbox_custom_field.test", "validation_regex", "^.*$"), ), }, }, }) } func TestAccNetboxCustomField_json(t *testing.T) { testSlug := "custom_fields_json" testName := strings.ReplaceAll(testAccGetTestName(testSlug), "-", "_") resource.Test(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_custom_field" "test" { name = "%s" type = "json" content_types = ["virtualization.vminterface"] group_name = "mygroup" weight = 100 }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_custom_field.test", "name", testName), resource.TestCheckResourceAttr("netbox_custom_field.test", "type", "json"), resource.TestCheckTypeSetElemAttr("netbox_custom_field.test", "content_types.*", "virtualization.vminterface"), resource.TestCheckResourceAttr("netbox_custom_field.test", "group_name", "mygroup"), resource.TestCheckResourceAttr("netbox_custom_field.test", "weight", "100"), ), }, }, }) } func TestAccNetboxCustomField_integer(t *testing.T) { testSlug := "custom_fields_integer" testName := strings.ReplaceAll(testAccGetTestName(testSlug), "-", "_") resource.Test(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_custom_field" "test" { name = "%s" type = "integer" content_types = ["virtualization.vminterface"] group_name = "mygroup" weight = 100 validation_maximum = 1000 validation_minimum = 10 }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_custom_field.test", "name", testName), resource.TestCheckResourceAttr("netbox_custom_field.test", "type", "integer"), resource.TestCheckTypeSetElemAttr("netbox_custom_field.test", "content_types.*", "virtualization.vminterface"), resource.TestCheckResourceAttr("netbox_custom_field.test", "group_name", "mygroup"), resource.TestCheckResourceAttr("netbox_custom_field.test", "weight", "100"), resource.TestCheckResourceAttr("netbox_custom_field.test", "validation_maximum", "1000"), resource.TestCheckResourceAttr("netbox_custom_field.test", "validation_minimum", "10"), ), }, }, }) } func TestAccNetboxCustomField_select(t *testing.T) { testSlug := "custom_fields_select" testName := strings.ReplaceAll(testAccGetTestName(testSlug), "-", "_") resource.Test(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_custom_field_choice_set" "test" { name = "%[1]s" extra_choices = [ ["red", "red"], ["blue", "blue"] ] } resource "netbox_custom_field" "test" { name = "%[1]s" type = "select" content_types = ["virtualization.vminterface"] weight = 101 default = "red" choice_set_id = netbox_custom_field_choice_set.test.id description = "select field" label = "external" required = false }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_custom_field.test", "name", testName), resource.TestCheckResourceAttr("netbox_custom_field.test", "type", "select"), resource.TestCheckResourceAttr("netbox_custom_field.test", "default", "red"), resource.TestCheckTypeSetElemAttr("netbox_custom_field.test", "content_types.*", "virtualization.vminterface"), resource.TestCheckResourceAttrPair("netbox_custom_field.test", "choice_set_id", "netbox_custom_field_choice_set.test", "id"), resource.TestCheckResourceAttr("netbox_custom_field.test", "weight", "101"), resource.TestCheckResourceAttr("netbox_custom_field.test", "default", "red"), resource.TestCheckResourceAttr("netbox_custom_field.test", "description", "select field"), resource.TestCheckResourceAttr("netbox_custom_field.test", "label", "external"), resource.TestCheckResourceAttr("netbox_custom_field.test", "required", "false"), ), }, { Config: fmt.Sprintf(` resource "netbox_custom_field_choice_set" "test" { name = "%[1]s" extra_choices = [ ["red", "red"], ["blue", "blue"] ] } resource "netbox_custom_field" "test" { name = "%[1]s" type = "select" content_types = ["virtualization.vminterface"] weight = 102 default = "red" choice_set_id = netbox_custom_field_choice_set.test.id description = "select field" label = "external" required = true }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_custom_field.test", "name", testName), resource.TestCheckResourceAttr("netbox_custom_field.test", "type", "select"), resource.TestCheckResourceAttr("netbox_custom_field.test", "default", "red"), resource.TestCheckTypeSetElemAttr("netbox_custom_field.test", "content_types.*", "virtualization.vminterface"), resource.TestCheckResourceAttrPair("netbox_custom_field.test", "choice_set_id", "netbox_custom_field_choice_set.test", "id"), resource.TestCheckResourceAttr("netbox_custom_field.test", "weight", "102"), resource.TestCheckResourceAttr("netbox_custom_field.test", "default", "red"), resource.TestCheckResourceAttr("netbox_custom_field.test", "description", "select field"), resource.TestCheckResourceAttr("netbox_custom_field.test", "label", "external"), resource.TestCheckResourceAttr("netbox_custom_field.test", "required", "true"), ), }, { Config: fmt.Sprintf(` resource "netbox_custom_field_choice_set" "test" { name = "%[1]s" extra_choices = [ ["red", "red"], ["blue", "blue"] ] } resource "netbox_custom_field" "test" { name = "%[1]s" type = "select" content_types = ["virtualization.vminterface"] weight = 102 default = "red" choice_set_id = netbox_custom_field_choice_set.test.id description = "select field" label = "external" required = false }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_custom_field.test", "required", "false"), ), }, }, }) } ================================================ FILE: netbox/resource_netbox_device.go ================================================ package netbox import ( "context" "encoding/json" "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var resourceNetboxDeviceStatusOptions = []string{"offline", "active", "planned", "staged", "failed", "inventory", "decommissioning"} var resourceNetboxDeviceRackFaceOptions = []string{"front", "rear"} func resourceNetboxDevice() *schema.Resource { return &schema.Resource{ CreateContext: resourceNetboxDeviceCreate, ReadContext: resourceNetboxDeviceRead, UpdateContext: resourceNetboxDeviceUpdate, DeleteContext: resourceNetboxDeviceDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/features/devices/#devices): > Every piece of hardware which is installed within a site or rack exists in NetBox as a device. Devices are measured in rack units (U) and can be half depth or full depth. A device may have a height of 0U: These devices do not consume vertical rack space and cannot be assigned to a particular rack unit. A common example of a 0U device is a vertically-mounted PDU.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "device_type_id": { Type: schema.TypeInt, Required: true, }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, "cluster_id": { Type: schema.TypeInt, Optional: true, }, "platform_id": { Type: schema.TypeInt, Optional: true, }, "location_id": { Type: schema.TypeInt, Optional: true, }, "role_id": { Type: schema.TypeInt, Required: true, }, "serial": { Type: schema.TypeString, Optional: true, }, "site_id": { Type: schema.TypeInt, Required: true, }, "config_template_id": { Type: schema.TypeInt, Optional: true, }, "comments": { Type: schema.TypeString, Optional: true, }, "asset_tag": { Type: schema.TypeString, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, tagsKey: tagsSchema, "primary_ipv4": { Type: schema.TypeInt, Computed: true, }, "primary_ipv6": { Type: schema.TypeInt, Computed: true, }, "status": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringInSlice(resourceNetboxDeviceStatusOptions, false), Description: buildValidValueDescription(resourceNetboxDeviceStatusOptions), Default: "active", }, "rack_id": { Type: schema.TypeInt, Optional: true, }, "rack_face": { Type: schema.TypeString, Optional: true, RequiredWith: []string{"rack_position"}, ValidateFunc: validation.StringInSlice(resourceNetboxDeviceRackFaceOptions, false), Description: buildValidValueDescription(resourceNetboxDeviceRackFaceOptions), }, "rack_position": { Type: schema.TypeFloat, Optional: true, }, "virtual_chassis_id": { Type: schema.TypeInt, Optional: true, RequiredWith: []string{"virtual_chassis_master", "virtual_chassis_id"}, }, "virtual_chassis_position": { Type: schema.TypeInt, Optional: true, }, "virtual_chassis_priority": { Type: schema.TypeInt, Optional: true, }, "virtual_chassis_master": { Type: schema.TypeBool, Optional: true, RequiredWith: []string{"virtual_chassis_master", "virtual_chassis_id"}, }, "local_context_data": { Type: schema.TypeString, Optional: true, Description: "This is best managed through the use of `jsonencode` and a map of settings.", }, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxDeviceCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) name := d.Get("name").(string) data := models.WritableDeviceWithConfigContext{ Name: &name, } typeIDValue, ok := d.GetOk("device_type_id") if ok { typeID := int64(typeIDValue.(int)) data.DeviceType = &typeID } if assetTagValue, ok := d.GetOk("asset_tag"); ok { assetTag := string(assetTagValue.(string)) data.AssetTag = &assetTag } data.Comments = d.Get("comments").(string) data.Description = d.Get("description").(string) data.Serial = d.Get("serial").(string) data.Status = d.Get("status").(string) tenantIDValue, ok := d.GetOk("tenant_id") if ok { tenantID := int64(tenantIDValue.(int)) data.Tenant = &tenantID } platformIDValue, ok := d.GetOk("platform_id") if ok { platformID := int64(platformIDValue.(int)) data.Platform = &platformID } locationIDValue, ok := d.GetOk("location_id") if ok { locationID := int64(locationIDValue.(int)) data.Location = &locationID } clusterIDValue, ok := d.GetOk("cluster_id") if ok { clusterID := int64(clusterIDValue.(int)) data.Cluster = &clusterID } roleIDValue, ok := d.GetOk("role_id") if ok { roleID := int64(roleIDValue.(int)) data.Role = &roleID } siteIDValue, ok := d.GetOk("site_id") if ok { siteID := int64(siteIDValue.(int)) data.Site = &siteID } configTemplateIDValue, ok := d.GetOk("config_template_id") if ok { configTemplateID := int64(configTemplateIDValue.(int)) data.ConfigTemplate = &configTemplateID } data.Rack = getOptionalInt(d, "rack_id") data.Face = getOptionalStr(d, "rack_face", false) rackPosition, ok := d.GetOk("rack_position") if ok && rackPosition.(float64) > 0 { data.Position = float64ToPtr(rackPosition.(float64)) } else { data.Position = nil } data.VirtualChassis = getOptionalInt(d, "virtual_chassis_id") data.VcPosition = getOptionalInt(d, "virtual_chassis_position") data.VcPriority = getOptionalInt(d, "virtual_chassis_priority") localContextValue, ok := d.GetOk("local_context_data") if ok { var jsonObj any localContextBA := []byte(localContextValue.(string)) if err := json.Unmarshal(localContextBA, &jsonObj); err == nil { data.LocalContextData = jsonObj } } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return diag.FromErr(err) } params := dcim.NewDcimDevicesCreateParams().WithData(&data) res, err := api.Dcim.DcimDevicesCreate(params, nil) if err != nil { return diag.FromErr(err) } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) if vcMaster, ok := d.GetOk("virtual_chassis_master"); ok { var err error if vcMaster.(bool) { err = virtualChassisUpdateMaster(api, *data.VirtualChassis, &(res.GetPayload().ID)) } else { err = virtualChassisUpdateMaster(api, *data.VirtualChassis, nil) } if err != nil { return diag.FromErr(err) } } return resourceNetboxDeviceRead(ctx, d, m) } func resourceNetboxDeviceRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) var diags diag.Diagnostics id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimDevicesReadParams().WithID(id) res, err := api.Dcim.DcimDevicesRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimDevicesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return diag.FromErr(err) } device := res.GetPayload() d.Set("name", device.Name) if device.DeviceType != nil { d.Set("device_type_id", device.DeviceType.ID) } if device.PrimaryIp4 != nil { d.Set("primary_ipv4", device.PrimaryIp4.ID) } else { d.Set("primary_ipv4", nil) } if device.PrimaryIp6 != nil { d.Set("primary_ipv6", device.PrimaryIp6.ID) } else { d.Set("primary_ipv6", nil) } if device.Tenant != nil { d.Set("tenant_id", device.Tenant.ID) } else { d.Set("tenant_id", nil) } if device.Platform != nil { d.Set("platform_id", device.Platform.ID) } else { d.Set("platform_id", nil) } if device.Location != nil { d.Set("location_id", device.Location.ID) } else { d.Set("location_id", nil) } if device.Cluster != nil { d.Set("cluster_id", device.Cluster.ID) } else { d.Set("cluster_id", nil) } if device.Role != nil { d.Set("role_id", device.Role.ID) } else { d.Set("role_id", nil) } if device.Site != nil { d.Set("site_id", device.Site.ID) } else { d.Set("site_id", nil) } if device.ConfigTemplate != nil { d.Set("config_template_id", device.ConfigTemplate.ID) } else { d.Set("config_template_id", nil) } cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } d.Set("asset_tag", device.AssetTag) d.Set("comments", device.Comments) d.Set("description", device.Description) d.Set("serial", device.Serial) d.Set("status", device.Status.Value) if device.Rack != nil { d.Set("rack_id", device.Rack.ID) } else { d.Set("rack_id", nil) } if device.Face != nil { d.Set("rack_face", device.Face.Value) } else { d.Set("rack_face", nil) } d.Set("rack_position", device.Position) if device.VirtualChassis != nil { d.Set("virtual_chassis_id", device.VirtualChassis.ID) d.Set( "virtual_chassis_master", device.VirtualChassis.Master != nil && device.VirtualChassis.Master.ID == device.ID, ) } else { d.Set("virtual_chassis_id", 0) d.Set("virtual_chassis_master", false) } d.Set("virtual_chassis_position", device.VcPosition) d.Set("virtual_chassis_priority", device.VcPriority) if device.LocalContextData != nil { if jsonArr, err := json.Marshal(device.LocalContextData); err == nil { d.Set("local_context_data", string(jsonArr)) } } else { d.Set("local_context_data", nil) } api.readTags(d, device.Tags) return diags } func resourceNetboxDeviceUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableDeviceWithConfigContext{} name := d.Get("name").(string) data.Name = &name status := d.Get("status").(string) data.Status = status typeIDValue, ok := d.GetOk("device_type_id") if ok { typeID := int64(typeIDValue.(int)) data.DeviceType = &typeID } tenantIDValue, ok := d.GetOk("tenant_id") if ok { tenantID := int64(tenantIDValue.(int)) data.Tenant = &tenantID } platformIDValue, ok := d.GetOk("platform_id") if ok { platformID := int64(platformIDValue.(int)) data.Platform = &platformID } locationIDValue, ok := d.GetOk("location_id") if ok { locationID := int64(locationIDValue.(int)) data.Location = &locationID } clusterIDValue, ok := d.GetOk("cluster_id") if ok { clusterID := int64(clusterIDValue.(int)) data.Cluster = &clusterID } roleIDValue, ok := d.GetOk("role_id") if ok { roleID := int64(roleIDValue.(int)) data.Role = &roleID } siteIDValue, ok := d.GetOk("site_id") if ok { siteID := int64(siteIDValue.(int)) data.Site = &siteID } configTemplateIDValue, ok := d.GetOk("config_template_id") if ok { configTemplateID := int64(configTemplateIDValue.(int)) data.ConfigTemplate = &configTemplateID } data.Rack = getOptionalInt(d, "rack_id") data.Face = getOptionalStr(d, "rack_face", false) data.Position = getOptionalFloat(d, "rack_position") data.VirtualChassis = getOptionalInt(d, "virtual_chassis_id") data.VcPosition = getOptionalInt(d, "virtual_chassis_position") data.VcPriority = getOptionalInt(d, "virtual_chassis_priority") localContextValue, ok := d.GetOk("local_context_data") if ok { var jsonObj any localContextBA := []byte(localContextValue.(string)) if err := json.Unmarshal(localContextBA, &jsonObj); err == nil { data.LocalContextData = jsonObj } } cf, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = cf } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return diag.FromErr(err) } if d.HasChanges("asset_tag") { if assetTagValue, ok := d.GetOk("asset_tag"); ok { assetTag := assetTagValue.(string) data.AssetTag = &assetTag } else { assetTag := " " data.AssetTag = &assetTag } } if d.HasChanges("comments") { // check if comment is set if commentsValue, ok := d.GetOk("comments"); ok { data.Comments = commentsValue.(string) } else { data.Comments = " " } } if d.HasChanges("description") { // check if description is set if descriptionValue, ok := d.GetOk("description"); ok { data.Description = descriptionValue.(string) } else { data.Description = " " } } if d.HasChanges("serial") { // check if serial is set serialValue, ok := d.GetOk("serial") serial := "" if !ok { // Setting an space string deletes the serial serial = " " } else { serial = serialValue.(string) } data.Serial = serial } params := dcim.NewDcimDevicesUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimDevicesUpdate(params, nil) if err != nil { return diag.FromErr(err) } if d.HasChange("virtual_chassis_master") && data.VirtualChassis != nil { var err error if vcMaster, ok := d.GetOk("virtual_chassis_master"); ok { if vcMaster.(bool) { err = virtualChassisUpdateMaster(api, *data.VirtualChassis, &id) } else { err = virtualChassisUpdateMaster(api, *data.VirtualChassis, nil) } } else { // It was set before, but no longer set, remove it as master err = virtualChassisUpdateMaster(api, *data.VirtualChassis, nil) } if err != nil { return diag.FromErr(err) } } return resourceNetboxDeviceRead(ctx, d, m) } func resourceNetboxDeviceDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) var diags diag.Diagnostics id, _ := strconv.ParseInt(d.Id(), 10, 64) // If the device is member of a virtual chassis and it's the master, we cannot // delete it directly. We first need to update it to not be the master. if virtualChassisIDValue, ok := d.GetOk("virtual_chassis_id"); ok { if d.Get("virtual_chassis_master").(bool) { virtualChassisID := int64(virtualChassisIDValue.(int)) err := virtualChassisUpdateMaster(api, virtualChassisID, nil) if err != nil { return diag.FromErr(err) } } } params := dcim.NewDcimDevicesDeleteParams().WithID(id) _, err := api.Dcim.DcimDevicesDelete(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimDevicesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return diag.FromErr(err) } return diags } ================================================ FILE: netbox/resource_netbox_device_bay.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceNetboxDeviceBay() *schema.Resource { return &schema.Resource{ Create: resourceNetboxDeviceBayCreate, Read: resourceNetboxDeviceBayRead, Update: resourceNetboxDeviceBayUpdate, Delete: resourceNetboxDeviceBayDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/devicebay/): > Device bays represent a space or slot within a device in which a field-replaceable device may be installed. A common example is that of a chassis-based server that holds a number of blades which may contain device components that don't fit the module pattern. Devices in turn hold additional components that become available to the parent device.`, Schema: map[string]*schema.Schema{ "device_id": { Type: schema.TypeInt, Required: true, }, "name": { Type: schema.TypeString, Required: true, }, "label": { Type: schema.TypeString, Optional: true, }, "installed_device_id": { Type: schema.TypeInt, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, tagsKey: tagsSchema, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxDeviceBayCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableDeviceBay{ Device: int64ToPtr(int64(d.Get("device_id").(int))), Name: strToPtr(d.Get("name").(string)), Label: getOptionalStr(d, "label", false), InstalledDevice: getOptionalInt(d, "installed_device_id"), Description: getOptionalStr(d, "description", false), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimDeviceBaysCreateParams().WithData(&data) res, err := api.Dcim.DcimDeviceBaysCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxDeviceBayRead(d, m) } func resourceNetboxDeviceBayRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimDeviceBaysReadParams().WithID(id) res, err := api.Dcim.DcimDeviceBaysRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimDeviceBaysReadDefault); ok { if errresp.Code() == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } deviceBay := res.GetPayload() if deviceBay.Device != nil { d.Set("device_id", deviceBay.Device.ID) } else { d.Set("device_id", nil) } d.Set("name", deviceBay.Name) d.Set("label", deviceBay.Label) if deviceBay.InstalledDevice != nil { d.Set("installed_device_id", deviceBay.InstalledDevice.ID) } d.Set("description", deviceBay.Description) cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxDeviceBayUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableDeviceBay{ Device: int64ToPtr(int64(d.Get("device_id").(int))), Name: strToPtr(d.Get("name").(string)), Label: getOptionalStr(d, "label", true), InstalledDevice: getOptionalInt(d, "installed_device_id"), Description: getOptionalStr(d, "description", true), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimDeviceBaysPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimDeviceBaysPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxDeviceBayRead(d, m) } func resourceNetboxDeviceBayDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimDeviceBaysDeleteParams().WithID(id) _, err := api.Dcim.DcimDeviceBaysDelete(params, nil) if err != nil { return err } return nil } ================================================ FILE: netbox/resource_netbox_device_bay_template.go ================================================ package netbox import ( "context" "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxDeviceBayTemplate() *schema.Resource { return &schema.Resource{ CreateContext: resourceNetboxDeviceBayTemplateCreate, ReadContext: resourceNetboxDeviceBayTemplateRead, UpdateContext: resourceNetboxDeviceBayTemplateUpdate, DeleteContext: resourceNetboxDeviceBayTemplateDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/devicebaytemplate/): > A template for a device bay that will be created on all instantiations of the parent device type.`, Schema: map[string]*schema.Schema{ "device_type_id": { Type: schema.TypeInt, Required: true, }, "name": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringLenBetween(1, 64), }, "description": { Type: schema.TypeString, Optional: true, }, "label": { Type: schema.TypeString, Optional: true, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxDeviceBayTemplateCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) var diags diag.Diagnostics name := d.Get("name").(string) description := d.Get("description").(string) label := d.Get("label").(string) data := models.WritableDeviceBayTemplate{ Name: &name, Description: description, Label: label, } if deviceTypeID, ok := d.Get("device_type_id").(int); ok && deviceTypeID != 0 { data.DeviceType = int64ToPtr(int64(deviceTypeID)) } params := dcim.NewDcimDeviceBayTemplatesCreateParams().WithData(&data) res, err := api.Dcim.DcimDeviceBayTemplatesCreate(params, nil) if err != nil { return diag.FromErr(err) } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return diags } func resourceNetboxDeviceBayTemplateRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) var diags diag.Diagnostics params := dcim.NewDcimDeviceBayTemplatesReadParams().WithID(id) res, err := api.Dcim.DcimDeviceBayTemplatesRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimDeviceBayTemplatesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return diag.FromErr(err) } tmpl := res.GetPayload() d.Set("device_type_id", tmpl.DeviceType.ID) d.Set("name", tmpl.Name) d.Set("description", tmpl.Description) d.Set("label", tmpl.Label) return diags } func resourceNetboxDeviceBayTemplateUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) var diags diag.Diagnostics id, _ := strconv.ParseInt(d.Id(), 10, 64) name := d.Get("name").(string) description := d.Get("description").(string) label := d.Get("label").(string) data := models.WritableDeviceBayTemplate{ Name: &name, Description: description, Label: label, } if d.HasChange("device_type_id") { deviceTypeID := int64(d.Get("device_type_id").(int)) data.DeviceType = &deviceTypeID } params := dcim.NewDcimDeviceBayTemplatesPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Dcim.DcimDeviceBayTemplatesPartialUpdate(params, nil) if err != nil { return diag.FromErr(err) } return diags } func resourceNetboxDeviceBayTemplateDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimDeviceBayTemplatesDeleteParams().WithID(id) _, err := api.Dcim.DcimDeviceBayTemplatesDelete(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimDeviceBayTemplatesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return diag.FromErr(err) } return nil } ================================================ FILE: netbox/resource_netbox_device_bay_template_test.go ================================================ package netbox import ( "fmt" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" log "github.com/sirupsen/logrus" ) func TestAccNetboxDeviceBayTemplate_basic(t *testing.T) { testSlug := "device_bay_template" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" slug = "%[2]s" part_number = "%[2]s" manufacturer_id = netbox_manufacturer.test.id subdevice_role = "parent" } resource "netbox_device_bay_template" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_bay_template.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_device_bay_template.test", "device_type_id", "netbox_device_type.test", "id"), ), }, { ResourceName: "netbox_device_bay_template.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxDeviceBayTemplate_opts(t *testing.T) { testSlug := "device_bay_template" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" slug = "%[2]s" part_number = "%[2]s" subdevice_role = "parent" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device_bay_template" "test" { name = "%[1]s" description = "%[1]s description" label = "%[1]s label" device_type_id = netbox_device_type.test.id }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_bay_template.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_bay_template.test", "description", fmt.Sprintf("%s description", testName)), resource.TestCheckResourceAttr("netbox_device_bay_template.test", "label", fmt.Sprintf("%s label", testName)), resource.TestCheckResourceAttrPair("netbox_device_bay_template.test", "device_type_id", "netbox_device_type.test", "id"), ), }, { Config: fmt.Sprintf(` resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" slug = "%[2]s" part_number = "%[2]s" subdevice_role = "parent" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device_bay_template" "test" { name = "%[1]s" description = "%[1]s description" label = "%[1]s label" device_type_id = netbox_device_type.test.id }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_bay_template.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_bay_template.test", "description", fmt.Sprintf("%s description", testName)), resource.TestCheckResourceAttr("netbox_device_bay_template.test", "label", fmt.Sprintf("%s label", testName)), resource.TestCheckResourceAttrPair("netbox_device_bay_template.test", "device_type_id", "netbox_device_type.test", "id"), ), }, }, }) } func init() { resource.AddTestSweepers("netbox_device_bay_template", &resource.Sweeper{ Name: "netbox_device_bay_template", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimDeviceBayTemplatesListParams() res, err := api.Dcim.DcimDeviceBayTemplatesList(params, nil) if err != nil { return err } for _, tmpl := range res.GetPayload().Results { if strings.HasPrefix(*tmpl.Name, testPrefix) { deleteParams := dcim.NewDcimDeviceBayTemplatesDeleteParams().WithID(tmpl.ID) _, err := api.Dcim.DcimDeviceBayTemplatesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a device bay template") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_device_bay_test.go ================================================ package netbox import ( "fmt" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" log "github.com/sirupsen/logrus" ) func testAccNetboxDeviceBayFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_tag" "test" { name = "%[1]sa" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id subdevice_role = "parent" } resource "netbox_device_type" "test_installed" { model = "%[1]s_installed" manufacturer_id = netbox_manufacturer.test.id u_height = 0 subdevice_role = "child" } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device" "test_installed" { name = "%[1]s_installed" device_type_id = netbox_device_type.test_installed.id tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } `, testName) } func TestAccNetboxDeviceBay_basic(t *testing.T) { testSlug := "device_bay_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckDeviceBayDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxDeviceBayFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device_bay" "test" { device_id = netbox_device.test.id name = "%[1]s" label = "%[1]s_label" description = "%[1]s_description" installed_device_id = netbox_device.test_installed.id tags = [netbox_tag.test.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_bay.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_bay.test", "label", testName+"_label"), resource.TestCheckResourceAttr("netbox_device_bay.test", "description", testName+"_description"), resource.TestCheckResourceAttr("netbox_device_bay.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_device_bay.test", "tags.0", testName+"a"), resource.TestCheckResourceAttrPair("netbox_device_bay.test", "installed_device_id", "netbox_device.test_installed", "id"), resource.TestCheckResourceAttrPair("netbox_device_bay.test", "device_id", "netbox_device.test", "id"), ), }, { Config: testAccNetboxDeviceBayFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device_bay" "test" { device_id = netbox_device.test.id name = "%[1]s" installed_device_id = netbox_device.test_installed.id }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_bay.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_bay.test", "label", ""), resource.TestCheckResourceAttr("netbox_device_bay.test", "description", ""), resource.TestCheckResourceAttr("netbox_device_bay.test", "tags.#", "0"), resource.TestCheckResourceAttrPair("netbox_device_bay.test", "installed_device_id", "netbox_device.test_installed", "id"), resource.TestCheckResourceAttrPair("netbox_device_bay.test", "device_id", "netbox_device.test", "id"), ), }, { ResourceName: "netbox_device_bay.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckDeviceBayDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each module bay // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_device_bay" { continue } // Retrieve our device by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := dcim.NewDcimDeviceBaysReadParams().WithID(stateID) _, err := conn.Dcim.DcimDeviceBaysRead(params, nil) if err == nil { return fmt.Errorf("device_bay (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*dcim.DcimDeviceBaysReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func init() { resource.AddTestSweepers("netbox_device_bay", &resource.Sweeper{ Name: "netbox_device_bay", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimDeviceBaysListParams() res, err := api.Dcim.DcimDeviceBaysList(params, nil) if err != nil { return err } for _, deviceBay := range res.GetPayload().Results { if strings.HasPrefix(*deviceBay.Name, testPrefix) { deleteParams := dcim.NewDcimDeviceBaysDeleteParams().WithID(deviceBay.ID) _, err := api.Dcim.DcimDeviceBaysDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a device_bay") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_device_console_port.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceNetboxDeviceConsolePort() *schema.Resource { return &schema.Resource{ Create: resourceNetboxDeviceConsolePortCreate, Read: resourceNetboxDeviceConsolePortRead, Update: resourceNetboxDeviceConsolePortUpdate, Delete: resourceNetboxDeviceConsolePortDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/consoleport/): > A console port provides connectivity to the physical console of a device. These are typically used for temporary access by someone who is physically near the device, or for remote out-of-band access provided via a networked console server.`, Schema: map[string]*schema.Schema{ "device_id": { Type: schema.TypeInt, Required: true, }, "name": { Type: schema.TypeString, Required: true, }, "module_id": { Type: schema.TypeInt, Optional: true, }, "label": { Type: schema.TypeString, Optional: true, }, "type": { Type: schema.TypeString, Optional: true, Description: "One of [de-9, db-25, rj-11, rj-12, rj-45, mini-din-8, usb-a, usb-b, usb-c, usb-mini-a, usb-mini-b, usb-micro-a, usb-micro-b, usb-micro-ab, other]", }, "speed": { Type: schema.TypeInt, Optional: true, Description: "One of [1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200]", }, "description": { Type: schema.TypeString, Optional: true, }, "mark_connected": { Type: schema.TypeBool, Default: false, Optional: true, }, tagsKey: tagsSchema, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxDeviceConsolePortCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableConsolePort{ Device: int64ToPtr(int64(d.Get("device_id").(int))), Module: getOptionalInt(d, "module_id"), Name: strToPtr(d.Get("name").(string)), Label: getOptionalStr(d, "label", false), Type: getOptionalStr(d, "type", false), Speed: getOptionalInt(d, "speed"), Description: getOptionalStr(d, "description", false), MarkConnected: d.Get("mark_connected").(bool), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimConsolePortsCreateParams().WithData(&data) res, err := api.Dcim.DcimConsolePortsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxDeviceConsolePortRead(d, m) } func resourceNetboxDeviceConsolePortRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimConsolePortsReadParams().WithID(id) res, err := api.Dcim.DcimConsolePortsRead(params, nil) if err != nil { if errIntf, ok := err.(*dcim.DcimConsolePortsReadDefault); ok && errIntf.Code() == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } return err } consolePort := res.GetPayload() if consolePort.Device != nil { d.Set("device_id", consolePort.Device.ID) } else { d.Set("device_id", nil) } d.Set("name", consolePort.Name) if consolePort.Module != nil { d.Set("module_id", consolePort.Module.ID) } else { d.Set("module_id", nil) } d.Set("label", consolePort.Label) if consolePort.Type != nil { d.Set("type", consolePort.Type.Value) } else { d.Set("type", nil) } if consolePort.Speed != nil { d.Set("speed", consolePort.Speed.Value) } else { d.Set("speed", nil) } d.Set("description", consolePort.Description) d.Set("mark_connected", consolePort.MarkConnected) cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxDeviceConsolePortUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableConsolePort{ Device: int64ToPtr(int64(d.Get("device_id").(int))), Module: getOptionalInt(d, "module_id"), Name: strToPtr(d.Get("name").(string)), Label: getOptionalStr(d, "label", true), Type: getOptionalStr(d, "type", false), Speed: getOptionalInt(d, "speed"), Description: getOptionalStr(d, "description", true), MarkConnected: d.Get("mark_connected").(bool), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimConsolePortsPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimConsolePortsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxDeviceConsolePortRead(d, m) } func resourceNetboxDeviceConsolePortDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimConsolePortsDeleteParams().WithID(id) _, err := api.Dcim.DcimConsolePortsDelete(params, nil) if err != nil { return err } return nil } ================================================ FILE: netbox/resource_netbox_device_console_port_test.go ================================================ package netbox import ( "fmt" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" log "github.com/sirupsen/logrus" ) func testAccNetboxDeviceConsolePortFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_tag" "test" { name = "%[1]sa" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_module_bay" "test" { device_id = netbox_device.test.id name = "%[1]s" } resource "netbox_module_type" "test" { manufacturer_id = netbox_manufacturer.test.id model = "%[1]s" } resource "netbox_module" "test" { device_id = netbox_device.test.id module_bay_id = netbox_device_module_bay.test.id module_type_id = netbox_module_type.test.id status = "active" } `, testName) } func TestAccNetboxDeviceConsolePort_basic(t *testing.T) { testSlug := "device_console_port_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckDeviceConsolePortDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxDeviceConsolePortFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device_console_port" "test" { device_id = netbox_device.test.id name = "%[1]s" module_id = netbox_module.test.id label = "%[1]s_label" type = "de-9" speed = 1200 mark_connected = true description = "%[1]s_description" tags = [netbox_tag.test.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_console_port.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_console_port.test", "label", testName+"_label"), resource.TestCheckResourceAttr("netbox_device_console_port.test", "type", "de-9"), resource.TestCheckResourceAttr("netbox_device_console_port.test", "speed", "1200"), resource.TestCheckResourceAttr("netbox_device_console_port.test", "mark_connected", "true"), resource.TestCheckResourceAttr("netbox_device_console_port.test", "description", testName+"_description"), resource.TestCheckResourceAttr("netbox_device_console_port.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_device_console_port.test", "tags.0", testName+"a"), resource.TestCheckResourceAttrPair("netbox_device_console_port.test", "device_id", "netbox_device.test", "id"), resource.TestCheckResourceAttrPair("netbox_device_console_port.test", "module_id", "netbox_module.test", "id"), ), }, { Config: testAccNetboxDeviceConsolePortFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device_console_port" "test" { device_id = netbox_device.test.id name = "%[1]s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_console_port.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_console_port.test", "label", ""), resource.TestCheckResourceAttr("netbox_device_console_port.test", "type", ""), resource.TestCheckResourceAttr("netbox_device_console_port.test", "speed", "0"), resource.TestCheckResourceAttr("netbox_device_console_port.test", "mark_connected", "false"), resource.TestCheckResourceAttr("netbox_device_console_port.test", "description", ""), resource.TestCheckResourceAttr("netbox_device_console_port.test", "tags.#", "0"), resource.TestCheckResourceAttr("netbox_device_console_port.test", "module_id", "0"), resource.TestCheckResourceAttrPair("netbox_device_console_port.test", "device_id", "netbox_device.test", "id"), ), }, { ResourceName: "netbox_device_console_port.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckDeviceConsolePortDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each console port // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_device_console_port" { continue } // Retrieve our device by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := dcim.NewDcimConsolePortsReadParams().WithID(stateID) _, err := conn.Dcim.DcimConsolePortsRead(params, nil) if err == nil { return fmt.Errorf("device_console_port (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*dcim.DcimConsolePortsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func init() { resource.AddTestSweepers("netbox_device_console_port", &resource.Sweeper{ Name: "netbox_device_console_port", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimConsolePortsListParams() res, err := api.Dcim.DcimConsolePortsList(params, nil) if err != nil { return err } for _, consolePort := range res.GetPayload().Results { if strings.HasPrefix(*consolePort.Name, testPrefix) { deleteParams := dcim.NewDcimConsolePortsDeleteParams().WithID(consolePort.ID) _, err := api.Dcim.DcimConsolePortsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a device_console_port") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_device_console_server_port.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceNetboxDeviceConsoleServerPort() *schema.Resource { return &schema.Resource{ Create: resourceNetboxDeviceConsoleServerPortCreate, Read: resourceNetboxDeviceConsoleServerPortRead, Update: resourceNetboxDeviceConsoleServerPortUpdate, Delete: resourceNetboxDeviceConsoleServerPortDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/consoleserverport/): > A console server is a device which provides remote access to the local consoles of connected devices. They are typically used to provide remote out-of-band access to network devices, and generally connect to console ports.`, Schema: map[string]*schema.Schema{ "device_id": { Type: schema.TypeInt, Required: true, }, "name": { Type: schema.TypeString, Required: true, }, "module_id": { Type: schema.TypeInt, Optional: true, }, "label": { Type: schema.TypeString, Optional: true, }, "type": { Type: schema.TypeString, Optional: true, Description: "One of [de-9, db-25, rj-11, rj-12, rj-45, mini-din-8, usb-a, usb-b, usb-c, usb-mini-a, usb-mini-b, usb-micro-a, usb-micro-b, usb-micro-ab, other]", }, "speed": { Type: schema.TypeInt, Optional: true, Description: "One of [1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200]", }, "description": { Type: schema.TypeString, Optional: true, }, "mark_connected": { Type: schema.TypeBool, Default: false, Optional: true, }, tagsKey: tagsSchema, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxDeviceConsoleServerPortCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableConsoleServerPort{ Device: int64ToPtr(int64(d.Get("device_id").(int))), Module: getOptionalInt(d, "module_id"), Name: strToPtr(d.Get("name").(string)), Label: getOptionalStr(d, "label", true), Type: getOptionalStr(d, "type", false), Speed: getOptionalInt(d, "speed"), Description: getOptionalStr(d, "description", false), MarkConnected: d.Get("mark_connected").(bool), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimConsoleServerPortsCreateParams().WithData(&data) res, err := api.Dcim.DcimConsoleServerPortsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxDeviceConsoleServerPortRead(d, m) } func resourceNetboxDeviceConsoleServerPortRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimConsoleServerPortsReadParams().WithID(id) res, err := api.Dcim.DcimConsoleServerPortsRead(params, nil) if err != nil { if errIntf, ok := err.(*dcim.DcimConsoleServerPortsReadDefault); ok && errIntf.Code() == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } return err } consoleServerPort := res.GetPayload() if consoleServerPort.Device != nil { d.Set("device_id", consoleServerPort.Device.ID) } else { d.Set("device_id", nil) } d.Set("name", consoleServerPort.Name) if consoleServerPort.Module != nil { d.Set("module_id", consoleServerPort.Module.ID) } else { d.Set("module_id", nil) } d.Set("label", consoleServerPort.Label) if consoleServerPort.Type != nil { d.Set("type", consoleServerPort.Type.Value) } else { d.Set("type", nil) } if consoleServerPort.Speed != nil { d.Set("speed", consoleServerPort.Speed.Value) } else { d.Set("speed", nil) } d.Set("description", consoleServerPort.Description) d.Set("mark_connected", consoleServerPort.MarkConnected) cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxDeviceConsoleServerPortUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableConsoleServerPort{ Device: int64ToPtr(int64(d.Get("device_id").(int))), Module: getOptionalInt(d, "module_id"), Name: strToPtr(d.Get("name").(string)), Label: getOptionalStr(d, "label", true), Type: getOptionalStr(d, "type", false), Speed: getOptionalInt(d, "speed"), Description: getOptionalStr(d, "description", true), MarkConnected: d.Get("mark_connected").(bool), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimConsoleServerPortsPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimConsoleServerPortsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxDeviceConsoleServerPortRead(d, m) } func resourceNetboxDeviceConsoleServerPortDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimConsoleServerPortsDeleteParams().WithID(id) _, err := api.Dcim.DcimConsoleServerPortsDelete(params, nil) if err != nil { return err } return nil } ================================================ FILE: netbox/resource_netbox_device_console_server_port_test.go ================================================ package netbox import ( "fmt" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" log "github.com/sirupsen/logrus" ) func testAccNetboxDeviceConsoleServerPortFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_tag" "test" { name = "%[1]sa" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_module_bay" "test" { device_id = netbox_device.test.id name = "%[1]s" } resource "netbox_module_type" "test" { manufacturer_id = netbox_manufacturer.test.id model = "%[1]s" } resource "netbox_module" "test" { device_id = netbox_device.test.id module_bay_id = netbox_device_module_bay.test.id module_type_id = netbox_module_type.test.id status = "active" } `, testName) } func TestAccNetboxDeviceConsoleServerPort_basic(t *testing.T) { testSlug := "device_console_server_port_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckDeviceConsoleServerPortDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxDeviceConsoleServerPortFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device_console_server_port" "test" { device_id = netbox_device.test.id name = "%[1]s" module_id = netbox_module.test.id label = "%[1]s_label" type = "de-9" speed = 1200 mark_connected = true description = "%[1]s_description" tags = [netbox_tag.test.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_console_server_port.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_console_server_port.test", "label", testName+"_label"), resource.TestCheckResourceAttr("netbox_device_console_server_port.test", "type", "de-9"), resource.TestCheckResourceAttr("netbox_device_console_server_port.test", "speed", "1200"), resource.TestCheckResourceAttr("netbox_device_console_server_port.test", "mark_connected", "true"), resource.TestCheckResourceAttr("netbox_device_console_server_port.test", "description", testName+"_description"), resource.TestCheckResourceAttr("netbox_device_console_server_port.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_device_console_server_port.test", "tags.0", testName+"a"), resource.TestCheckResourceAttrPair("netbox_device_console_server_port.test", "device_id", "netbox_device.test", "id"), resource.TestCheckResourceAttrPair("netbox_device_console_server_port.test", "module_id", "netbox_module.test", "id"), ), }, { Config: testAccNetboxDeviceConsoleServerPortFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device_console_server_port" "test" { device_id = netbox_device.test.id name = "%[1]s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_console_server_port.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_console_server_port.test", "label", ""), resource.TestCheckResourceAttr("netbox_device_console_server_port.test", "type", ""), resource.TestCheckResourceAttr("netbox_device_console_server_port.test", "speed", "0"), resource.TestCheckResourceAttr("netbox_device_console_server_port.test", "mark_connected", "false"), resource.TestCheckResourceAttr("netbox_device_console_server_port.test", "description", ""), resource.TestCheckResourceAttr("netbox_device_console_server_port.test", "tags.#", "0"), resource.TestCheckResourceAttr("netbox_device_console_server_port.test", "module_id", "0"), resource.TestCheckResourceAttrPair("netbox_device_console_server_port.test", "device_id", "netbox_device.test", "id"), ), }, { ResourceName: "netbox_device_console_server_port.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckDeviceConsoleServerPortDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each console server port // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_device_console_server_port" { continue } // Retrieve our device by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := dcim.NewDcimConsoleServerPortsReadParams().WithID(stateID) _, err := conn.Dcim.DcimConsoleServerPortsRead(params, nil) if err == nil { return fmt.Errorf("device_console_server_port (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*dcim.DcimConsoleServerPortsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func init() { resource.AddTestSweepers("netbox_device_console_server_port", &resource.Sweeper{ Name: "netbox_device_console_server_port", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimConsoleServerPortsListParams() res, err := api.Dcim.DcimConsoleServerPortsList(params, nil) if err != nil { return err } for _, csPort := range res.GetPayload().Results { if strings.HasPrefix(*csPort.Name, testPrefix) { deleteParams := dcim.NewDcimConsoleServerPortsDeleteParams().WithID(csPort.ID) _, err := api.Dcim.DcimConsoleServerPortsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a device_console_server_port") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_device_front_port.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceNetboxDeviceFrontPort() *schema.Resource { return &schema.Resource{ Create: resourceNetboxDeviceFrontPortCreate, Read: resourceNetboxDeviceFrontPortRead, Update: resourceNetboxDeviceFrontPortUpdate, Delete: resourceNetboxDeviceFrontPortDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/frontport/): > Front ports are pass-through ports which represent physical cable connections that comprise part of a longer path. For example, the ports on the front face of a UTP patch panel would be modeled in NetBox as front ports. Each port is assigned a physical type, and must be mapped to a specific rear port on the same device. A single rear port may be mapped to multiple front ports, using numeric positions to annotate the specific alignment of each.`, Schema: map[string]*schema.Schema{ "device_id": { Type: schema.TypeInt, Required: true, }, "name": { Type: schema.TypeString, Required: true, }, "type": { Type: schema.TypeString, Required: true, Description: "One of [8p8c, 8p6c, 8p4c, 8p2c, 6p6c, 6p4c, 6p2c, 4p4c, 4p2c, gg45, tera-4p, tera-2p, tera-1p, 110-punch, bnc, f, n, mrj21, fc, lc, lc-pc, lc-upc, lc-apc, lsh, lsh-pc, lsh-upc, lsh-apc, mpo, mtrj, sc, sc-pc, sc-upc, sc-apc, st, cs, sn, sma-905, sma-906, urm-p2, urm-p4, urm-p8, splice, other]", }, "rear_port_id": { Type: schema.TypeInt, Required: true, }, "rear_port_position": { Type: schema.TypeInt, Required: true, }, "module_id": { Type: schema.TypeInt, Optional: true, }, "label": { Type: schema.TypeString, Optional: true, }, "color_hex": { Type: schema.TypeString, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, "mark_connected": { Type: schema.TypeBool, Default: false, Optional: true, }, tagsKey: tagsSchema, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxDeviceFrontPortCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableFrontPort{ Device: int64ToPtr(int64(d.Get("device_id").(int))), Name: strToPtr(d.Get("name").(string)), Type: strToPtr(d.Get("type").(string)), RearPort: int64ToPtr(int64(d.Get("rear_port_id").(int))), RearPortPosition: int64(d.Get("rear_port_position").(int)), Module: getOptionalInt(d, "module_id"), Label: getOptionalStr(d, "label", false), Color: getOptionalStr(d, "color_hex", false), Description: getOptionalStr(d, "description", false), MarkConnected: d.Get("mark_connected").(bool), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimFrontPortsCreateParams().WithData(&data) res, err := api.Dcim.DcimFrontPortsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxDeviceFrontPortRead(d, m) } func resourceNetboxDeviceFrontPortRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimFrontPortsReadParams().WithID(id) res, err := api.Dcim.DcimFrontPortsRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimFrontPortsReadDefault); ok { if errresp.Code() == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } frontPort := res.GetPayload() if frontPort.Device != nil { d.Set("device_id", frontPort.Device.ID) } else { d.Set("device_id", nil) } d.Set("name", frontPort.Name) if frontPort.Type != nil { d.Set("type", frontPort.Type.Value) } else { d.Set("type", nil) } if frontPort.RearPort != nil { d.Set("rear_port_id", frontPort.RearPort.ID) } else { d.Set("rear_port_id", nil) } d.Set("rear_port_position", frontPort.RearPortPosition) if frontPort.Module != nil { d.Set("module_id", frontPort.Module.ID) } else { d.Set("module_id", nil) } d.Set("label", frontPort.Label) d.Set("color_hex", frontPort.Color) d.Set("description", frontPort.Description) d.Set("mark_connected", frontPort.MarkConnected) cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxDeviceFrontPortUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableFrontPort{ Device: int64ToPtr(int64(d.Get("device_id").(int))), Name: strToPtr(d.Get("name").(string)), Type: strToPtr(d.Get("type").(string)), RearPort: int64ToPtr(int64(d.Get("rear_port_id").(int))), RearPortPosition: int64(d.Get("rear_port_position").(int)), Module: getOptionalInt(d, "module_id"), Label: getOptionalStr(d, "label", true), Color: getOptionalStr(d, "color_hex", false), Description: getOptionalStr(d, "description", true), MarkConnected: d.Get("mark_connected").(bool), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimFrontPortsPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimFrontPortsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxDeviceFrontPortRead(d, m) } func resourceNetboxDeviceFrontPortDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimFrontPortsDeleteParams().WithID(id) _, err := api.Dcim.DcimFrontPortsDelete(params, nil) if err != nil { return err } return nil } ================================================ FILE: netbox/resource_netbox_device_front_port_test.go ================================================ package netbox import ( "fmt" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" log "github.com/sirupsen/logrus" ) func testAccNetboxDeviceFrontPortFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_tag" "test" { name = "%[1]sa" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_module_bay" "test" { device_id = netbox_device.test.id name = "%[1]s" } resource "netbox_module_type" "test" { manufacturer_id = netbox_manufacturer.test.id model = "%[1]s" } resource "netbox_module" "test" { device_id = netbox_device.test.id module_bay_id = netbox_device_module_bay.test.id module_type_id = netbox_module_type.test.id status = "active" } resource "netbox_device_rear_port" "test" { device_id = netbox_device.test.id name = "%[1]s" type = "8p8c" positions = 1 mark_connected = true } `, testName) } func TestAccNetboxDeviceFrontPort_basic(t *testing.T) { testSlug := "device_front_port_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckDeviceFrontPortDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxDeviceFrontPortFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device_front_port" "test" { device_id = netbox_device.test.id name = "%[1]s" type = "8p8c" rear_port_id = netbox_device_rear_port.test.id rear_port_position = 1 mark_connected = true module_id = netbox_module.test.id label = "%[1]s_label" color_hex = "123456" description = "%[1]s_description" tags = [netbox_tag.test.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_front_port.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_front_port.test", "type", "8p8c"), resource.TestCheckResourceAttr("netbox_device_front_port.test", "mark_connected", "true"), resource.TestCheckResourceAttr("netbox_device_front_port.test", "label", testName+"_label"), resource.TestCheckResourceAttr("netbox_device_front_port.test", "color_hex", "123456"), resource.TestCheckResourceAttr("netbox_device_front_port.test", "description", testName+"_description"), resource.TestCheckResourceAttr("netbox_device_front_port.test", "rear_port_position", "1"), resource.TestCheckResourceAttr("netbox_device_front_port.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_device_front_port.test", "tags.0", testName+"a"), resource.TestCheckResourceAttrPair("netbox_device_front_port.test", "device_id", "netbox_device.test", "id"), resource.TestCheckResourceAttrPair("netbox_device_front_port.test", "rear_port_id", "netbox_device_rear_port.test", "id"), resource.TestCheckResourceAttrPair("netbox_device_front_port.test", "module_id", "netbox_module.test", "id"), ), }, { Config: testAccNetboxDeviceFrontPortFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device_front_port" "test" { device_id = netbox_device.test.id name = "%[1]s" type = "8p8c" rear_port_id = netbox_device_rear_port.test.id rear_port_position = 1 }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_front_port.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_front_port.test", "type", "8p8c"), resource.TestCheckResourceAttr("netbox_device_front_port.test", "mark_connected", "false"), resource.TestCheckResourceAttr("netbox_device_front_port.test", "label", ""), resource.TestCheckResourceAttr("netbox_device_front_port.test", "color_hex", ""), resource.TestCheckResourceAttr("netbox_device_front_port.test", "description", ""), resource.TestCheckResourceAttr("netbox_device_front_port.test", "rear_port_position", "1"), resource.TestCheckResourceAttr("netbox_device_front_port.test", "tags.#", "0"), resource.TestCheckResourceAttr("netbox_device_front_port.test", "module_id", "0"), resource.TestCheckResourceAttrPair("netbox_device_front_port.test", "device_id", "netbox_device.test", "id"), resource.TestCheckResourceAttrPair("netbox_device_front_port.test", "rear_port_id", "netbox_device_rear_port.test", "id"), ), }, { ResourceName: "netbox_device_front_port.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckDeviceFrontPortDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each front port // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_device_front_port" { continue } // Retrieve our device by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := dcim.NewDcimFrontPortsReadParams().WithID(stateID) _, err := conn.Dcim.DcimFrontPortsRead(params, nil) if err == nil { return fmt.Errorf("device_front_port (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*dcim.DcimFrontPortsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func init() { resource.AddTestSweepers("netbox_device_front_port", &resource.Sweeper{ Name: "netbox_device_front_port", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimFrontPortsListParams() res, err := api.Dcim.DcimFrontPortsList(params, nil) if err != nil { return err } for _, frontPort := range res.GetPayload().Results { if strings.HasPrefix(*frontPort.Name, testPrefix) { deleteParams := dcim.NewDcimFrontPortsDeleteParams().WithID(frontPort.ID) _, err := api.Dcim.DcimFrontPortsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a device_front_port") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_device_interface.go ================================================ package netbox import ( "context" "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var resourceNetboxDeviceInterfaceModeOptions = []string{"access", "tagged", "tagged-all", "q-in-q"} func resourceNetboxDeviceInterface() *schema.Resource { return &schema.Resource{ CreateContext: resourceNetboxDeviceInterfaceCreate, ReadContext: resourceNetboxDeviceInterfaceRead, UpdateContext: resourceNetboxDeviceInterfaceUpdate, DeleteContext: resourceNetboxDeviceInterfaceDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/features/device/#interface): > Interfaces in NetBox represent network interfaces used to exchange data with connected devices. On modern networks, these are most commonly Ethernet, but other types are supported as well. IP addresses and VLANs can be assigned to interfaces.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "device_id": { Type: schema.TypeInt, Required: true, }, "description": { Type: schema.TypeString, Optional: true, }, "label": { Type: schema.TypeString, Optional: true, }, "enabled": { Type: schema.TypeBool, Optional: true, Default: true, }, "lag_device_interface_id": { Type: schema.TypeInt, Optional: true, Description: "If this device is a member of a LAG group, you can reference the LAG interface here.", }, "mac_address": { Type: schema.TypeString, Computed: true, Description: "The MAC address as string from the first MAC address assigned to this interface, if any.", }, "mac_addresses": { Type: schema.TypeSet, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "mac_address": { Type: schema.TypeString, Computed: true, }, "description": { Type: schema.TypeString, Computed: true, }, }, }, }, "mgmtonly": { Type: schema.TypeBool, Optional: true, }, "mode": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringInSlice(resourceNetboxDeviceInterfaceModeOptions, false), Description: buildValidValueDescription(resourceNetboxDeviceInterfaceModeOptions), }, "mtu": { Type: schema.TypeInt, Optional: true, ValidateFunc: validation.IntBetween(1, 65536), }, "parent_device_interface_id": { Type: schema.TypeInt, Optional: true, Description: "The netbox_device_interface id of the parent interface. Useful if this interface is a logical interface.", }, "speed": { Type: schema.TypeInt, Optional: true, }, "type": { Type: schema.TypeString, Required: true, }, tagsKey: tagsSchema, "tagged_vlans": { Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "untagged_vlan": { Type: schema.TypeInt, Optional: true, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxDeviceInterfaceCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) var diags diag.Diagnostics name := d.Get("name").(string) description := d.Get("description").(string) label := d.Get("label").(string) interfaceType := d.Get("type").(string) enabled := d.Get("enabled").(bool) mgmtonly := d.Get("mgmtonly").(bool) mode := d.Get("mode").(string) tags, err := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return diag.FromErr(err) } taggedVlans := toInt64List(d.Get("tagged_vlans")) deviceID := int64(d.Get("device_id").(int)) data := models.WritableInterface{ Name: &name, Description: description, Label: label, Type: &interfaceType, Enabled: enabled, MgmtOnly: mgmtonly, Mode: mode, Tags: tags, TaggedVlans: taggedVlans, Device: &deviceID, WirelessLans: []int64{}, Vdcs: []int64{}, } if lag, ok := d.Get("lag_device_interface_id").(int); ok && lag != 0 { data.Lag = int64ToPtr(int64(lag)) } if mtu, ok := d.Get("mtu").(int); ok && mtu != 0 { data.Mtu = int64ToPtr(int64(mtu)) } if parent, ok := d.Get("parent_device_interface_id").(int); ok && parent != 0 { data.Parent = int64ToPtr(int64(parent)) } if speed, ok := d.Get("speed").(int); ok && speed != 0 { data.Speed = int64ToPtr(int64(speed)) } if untaggedVlan, ok := d.Get("untagged_vlan").(int); ok && untaggedVlan != 0 { data.UntaggedVlan = int64ToPtr(int64(untaggedVlan)) } params := dcim.NewDcimInterfacesCreateParams().WithData(&data) res, err := api.Dcim.DcimInterfacesCreate(params, nil) if err != nil { return diag.FromErr(err) } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return diags } func resourceNetboxDeviceInterfaceRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) var diags diag.Diagnostics params := dcim.NewDcimInterfacesReadParams().WithID(id) res, err := api.Dcim.DcimInterfacesRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimInterfacesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return diag.FromErr(err) } iface := res.GetPayload() d.Set("name", iface.Name) d.Set("description", iface.Description) d.Set("label", iface.Label) d.Set("type", iface.Type.Value) d.Set("enabled", iface.Enabled) d.Set("mgmtonly", iface.MgmtOnly) d.Set("mtu", iface.Mtu) d.Set("speed", iface.Speed) api.readTags(d, iface.Tags) d.Set("tagged_vlans", getIDsFromNestedVLANDevice(iface.TaggedVlans)) d.Set("device_id", iface.Device.ID) if iface.Lag != nil { d.Set("lag_device_interface_id", iface.Lag.ID) } if iface.Mode != nil { d.Set("mode", iface.Mode.Value) } if iface.Parent != nil { d.Set("parent_device_interface_id", iface.Parent.ID) } if iface.UntaggedVlan != nil { d.Set("untagged_vlan", iface.UntaggedVlan.ID) } if iface.MacAddresses != nil { var mac_addresses []map[string]interface{} for i, mac := range iface.MacAddresses { var mac_address = make(map[string]interface{}) // We just set the first mac address in the `mac_address` attribute if i == 0 { d.Set("mac_address", iface.MacAddresses[i].MacAddress) } mac_address["id"] = mac.ID mac_address["description"] = mac.Description mac_address["mac_address"] = mac.MacAddress mac_addresses = append(mac_addresses, mac_address) } d.Set("mac_addresses", mac_addresses) } return diags } func resourceNetboxDeviceInterfaceUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) var diags diag.Diagnostics id, _ := strconv.ParseInt(d.Id(), 10, 64) name := d.Get("name").(string) description := d.Get("description").(string) label := d.Get("label").(string) interfaceType := d.Get("type").(string) enabled := d.Get("enabled").(bool) mgmtonly := d.Get("mgmtonly").(bool) mode := d.Get("mode").(string) tags, err := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return diag.FromErr(err) } taggedVlans := toInt64List(d.Get("tagged_vlans")) deviceID := int64(d.Get("device_id").(int)) data := models.WritableInterface{ Name: &name, Description: description, Label: label, Type: &interfaceType, Enabled: enabled, MgmtOnly: mgmtonly, Mode: mode, Tags: tags, TaggedVlans: taggedVlans, Device: &deviceID, WirelessLans: []int64{}, Vdcs: []int64{}, } if d.HasChange("lag_device_interface_id") { lag := int64(d.Get("lag_device_interface_id").(int)) data.Lag = &lag } if d.HasChange("mtu") { mtu := int64(d.Get("mtu").(int)) data.Mtu = &mtu } if d.HasChange("parent_device_interface_id") { parent := int64(d.Get("parent_device_interface_id").(int)) data.Parent = &parent } if d.HasChange("speed") { speed := int64(d.Get("speed").(int)) data.Speed = &speed } if d.HasChange("untagged_vlan") { untaggedvlan := int64(d.Get("untagged_vlan").(int)) data.UntaggedVlan = &untaggedvlan } params := dcim.NewDcimInterfacesPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimInterfacesPartialUpdate(params, nil) if err != nil { return diag.FromErr(err) } return diags } func resourceNetboxDeviceInterfaceDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimInterfacesDeleteParams().WithID(id) _, err := api.Dcim.DcimInterfacesDelete(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimInterfacesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return diag.FromErr(err) } return nil } func getIDsFromNestedVLANDevice(nestedvlans []*models.NestedVLAN) []int64 { var vlans []int64 for _, vlan := range nestedvlans { vlans = append(vlans, vlan.ID) } return vlans } ================================================ FILE: netbox/resource_netbox_device_interface_test.go ================================================ package netbox import ( "fmt" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" log "github.com/sirupsen/logrus" ) func testAccNetboxDeviceInterfaceFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_vlan" "test1" { name = "%[1]s_vlan1" vid = 1001 tags = [] } resource "netbox_vlan" "test2" { name = "%[1]s_vlan2" vid = 1002 tags = [] }`, testName) } func testAccNetboxDeviceInterfaceBasic(testName string) string { return fmt.Sprintf(` resource "netbox_device_interface" "test" { name = "%s" device_id = netbox_device.test.id tags = [netbox_tag.test.name] type = "1000base-t" }`, testName) } func testAccNetboxDeviceInterfaceOpts(testName string) string { return fmt.Sprintf(` resource "netbox_device_interface" "test" { name = "%[1]s" description = "%[1]s" label = "%[1]s" enabled = true mgmtonly = true mtu = 1440 device_id = netbox_device.test.id type = "1000base-t" }`, testName) } func testAccNetboxDeviceInterfaceParentAndLAG(testName string) string { return fmt.Sprintf(` resource "netbox_device_interface" "testLAG_parent" { name = "%[1]s_parentlag" device_id = netbox_device.test.id type = "lag" } resource "netbox_device_interface" "testLAG_member1" { name = "%[1]s_lagmember1" device_id = netbox_device.test.id lag_device_interface_id = "${netbox_device_interface.testLAG_parent.id}" type = "25gbase-x-sfp28" } resource "netbox_device_interface" "testLAG_member2" { name = "%[1]s_lagmember2" device_id = netbox_device.test.id lag_device_interface_id = "${netbox_device_interface.testLAG_parent.id}" type = "25gbase-x-sfp28" } resource "netbox_device_interface" "testparent" { name = "%[1]s_parent_parent" device_id = netbox_device.test.id type = "25gbase-x-sfp28" } resource "netbox_device_interface" "testparent_child1" { name = "%[1]s_parent_child" device_id = netbox_device.test.id parent_device_interface_id = "${netbox_device_interface.testparent.id}" type = "virtual" } `, testName) } func testAccNetboxDeviceInterfaceVlans(testName string) string { return fmt.Sprintf(` resource "netbox_device_interface" "test1" { name = "%[1]s_1" mode = "access" untagged_vlan = netbox_vlan.test1.id device_id = netbox_device.test.id type = "1000base-t" } resource "netbox_device_interface" "test2" { name = "%[1]s_2" mode = "tagged" tagged_vlans = [netbox_vlan.test2.id] untagged_vlan = netbox_vlan.test1.id device_id = netbox_device.test.id type = "1000base-t" } resource "netbox_device_interface" "test3" { name = "%[1]s_3" mode = "tagged-all" device_id = netbox_device.test.id type = "1000base-t" }`, testName) } func TestAccNetboxDeviceInterface_basic(t *testing.T) { testSlug := "iface_basic" testName := testAccGetTestName(testSlug) setUp := testAccNetboxDeviceInterfaceFullDependencies(testName) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckDeviceInterfaceDestroy, Steps: []resource.TestStep{ { Config: setUp + testAccNetboxDeviceInterfaceBasic(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_interface.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_interface.test", "type", "1000base-t"), resource.TestCheckResourceAttrPair("netbox_device_interface.test", "device_id", "netbox_device.test", "id"), resource.TestCheckResourceAttr("netbox_device_interface.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_device_interface.test", "tags.0", testName), ), }, { ResourceName: "netbox_device_interface.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxDeviceInterface_parentAndLAG(t *testing.T) { testSlug := "iface_mac" testName := testAccGetTestName(testSlug) setUp := testAccNetboxDeviceInterfaceFullDependencies(testName) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckDeviceInterfaceDestroy, Steps: []resource.TestStep{ { Config: setUp + testAccNetboxDeviceInterfaceParentAndLAG(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_interface.testLAG_parent", "name", testName+"_parentlag"), resource.TestCheckResourceAttr("netbox_device_interface.testLAG_parent", "type", "lag"), resource.TestCheckResourceAttr("netbox_device_interface.testLAG_member1", "type", "25gbase-x-sfp28"), resource.TestCheckResourceAttr("netbox_device_interface.testLAG_member2", "type", "25gbase-x-sfp28"), resource.TestCheckResourceAttrPair("netbox_device_interface.testLAG_member1", "lag_device_interface_id", "netbox_device_interface.testLAG_parent", "id"), resource.TestCheckResourceAttrPair("netbox_device_interface.testLAG_member2", "lag_device_interface_id", "netbox_device_interface.testLAG_parent", "id"), resource.TestCheckResourceAttr("netbox_device_interface.testparent_child1", "type", "virtual"), resource.TestCheckResourceAttrPair("netbox_device_interface.testparent_child1", "parent_device_interface_id", "netbox_device_interface.testparent", "id"), ), }, { ResourceName: "netbox_device_interface.testLAG_parent", ImportState: true, ImportStateVerify: true, }, { ResourceName: "netbox_device_interface.testLAG_member1", ImportState: true, ImportStateVerify: true, }, { ResourceName: "netbox_device_interface.testLAG_member2", ImportState: true, ImportStateVerify: true, }, { ResourceName: "netbox_device_interface.testparent", ImportState: true, ImportStateVerify: true, }, { ResourceName: "netbox_device_interface.testparent_child1", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxDeviceInterface_opts(t *testing.T) { testSlug := "iface_mac" testName := testAccGetTestName(testSlug) setUp := testAccNetboxDeviceInterfaceFullDependencies(testName) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckDeviceInterfaceDestroy, Steps: []resource.TestStep{ { Config: setUp + testAccNetboxDeviceInterfaceOpts(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_interface.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_interface.test", "type", "1000base-t"), resource.TestCheckResourceAttr("netbox_device_interface.test", "description", testName), resource.TestCheckResourceAttr("netbox_device_interface.test", "label", testName), resource.TestCheckResourceAttr("netbox_device_interface.test", "enabled", "true"), resource.TestCheckResourceAttr("netbox_device_interface.test", "mgmtonly", "true"), resource.TestCheckResourceAttr("netbox_device_interface.test", "mtu", "1440"), resource.TestCheckResourceAttrPair("netbox_device_interface.test", "device_id", "netbox_device.test", "id"), ), }, { ResourceName: "netbox_device_interface.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxDeviceInterface_vlans(t *testing.T) { testSlug := "iface_vlan" testName := testAccGetTestName(testSlug) setUp := testAccNetboxDeviceInterfaceFullDependencies(testName) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckDeviceInterfaceDestroy, Steps: []resource.TestStep{ { Config: setUp + testAccNetboxDeviceInterfaceVlans(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_interface.test1", "mode", "access"), resource.TestCheckResourceAttr("netbox_device_interface.test2", "mode", "tagged"), resource.TestCheckResourceAttr("netbox_device_interface.test3", "mode", "tagged-all"), resource.TestCheckResourceAttrPair("netbox_device_interface.test1", "untagged_vlan", "netbox_vlan.test1", "id"), resource.TestCheckResourceAttrPair("netbox_device_interface.test2", "untagged_vlan", "netbox_vlan.test1", "id"), resource.TestCheckResourceAttrPair("netbox_device_interface.test2", "tagged_vlans.0", "netbox_vlan.test2", "id"), ), }, { ResourceName: "netbox_device_interface.test1", ImportState: true, ImportStateVerify: true, }, { ResourceName: "netbox_device_interface.test2", ImportState: true, ImportStateVerify: true, }, { ResourceName: "netbox_device_interface.test3", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckDeviceInterfaceDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each interface // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_device_interface" { continue } // Retrieve our interface by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := dcim.NewDcimInterfacesReadParams().WithID(stateID) _, err := conn.Dcim.DcimInterfacesRead(params, nil) if err == nil { return fmt.Errorf("device interface (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*dcim.DcimInterfacesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func init() { resource.AddTestSweepers("netbox_device_interface", &resource.Sweeper{ Name: "netbox_device_interface", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimInterfacesListParams() res, err := api.Dcim.DcimInterfacesList(params, nil) if err != nil { return err } for _, intrface := range res.GetPayload().Results { if strings.HasPrefix(*intrface.Name, testPrefix) { deleteParams := dcim.NewDcimInterfacesDeleteParams().WithID(intrface.ID) _, err := api.Dcim.DcimInterfacesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a device interface") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_device_module_bay.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxDeviceModuleBay() *schema.Resource { return &schema.Resource{ Create: resourceNetboxDeviceModuleBayCreate, Read: resourceNetboxDeviceModuleBayRead, Update: resourceNetboxDeviceModuleBayUpdate, Delete: resourceNetboxDeviceModuleBayDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/modulebay/): > Module bays represent a space or slot within a device in which a field-replaceable module may be installed. A common example is that of a chassis-based switch such as the Cisco Nexus 9000 or Juniper EX9200. Modules in turn hold additional components that become available to the parent device.`, Schema: map[string]*schema.Schema{ "device_id": { Type: schema.TypeInt, Required: true, }, "name": { Type: schema.TypeString, Required: true, }, "label": { Type: schema.TypeString, Optional: true, }, "position": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringLenBetween(0, 30), }, "description": { Type: schema.TypeString, Optional: true, }, tagsKey: tagsSchema, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxDeviceModuleBayCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableModuleBay{ Device: int64ToPtr(int64(d.Get("device_id").(int))), Name: strToPtr(d.Get("name").(string)), Label: getOptionalStr(d, "label", false), Position: getOptionalStr(d, "position", false), Description: getOptionalStr(d, "description", false), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimModuleBaysCreateParams().WithData(&data) res, err := api.Dcim.DcimModuleBaysCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxDeviceModuleBayRead(d, m) } func resourceNetboxDeviceModuleBayRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimModuleBaysReadParams().WithID(id) res, err := api.Dcim.DcimModuleBaysRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimModuleBaysReadDefault); ok { if errresp.Code() == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } moduleBay := res.GetPayload() if moduleBay.Device != nil { d.Set("device_id", moduleBay.Device.ID) } else { d.Set("device_id", nil) } d.Set("name", moduleBay.Name) d.Set("label", moduleBay.Label) d.Set("position", moduleBay.Position) d.Set("description", moduleBay.Description) cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxDeviceModuleBayUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableModuleBay{ Device: int64ToPtr(int64(d.Get("device_id").(int))), Name: strToPtr(d.Get("name").(string)), Label: getOptionalStr(d, "label", true), Position: getOptionalStr(d, "position", true), Description: getOptionalStr(d, "description", true), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimModuleBaysPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimModuleBaysPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxDeviceModuleBayRead(d, m) } func resourceNetboxDeviceModuleBayDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimModuleBaysDeleteParams().WithID(id) _, err := api.Dcim.DcimModuleBaysDelete(params, nil) if err != nil { return err } return nil } ================================================ FILE: netbox/resource_netbox_device_module_bay_test.go ================================================ package netbox import ( "fmt" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" log "github.com/sirupsen/logrus" ) func testAccNetboxDeviceModuleBayFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_tag" "test" { name = "%[1]sa" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } `, testName) } func TestAccNetboxDeviceModuleBay_basic(t *testing.T) { testSlug := "device_module_bay_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckDeviceModuleBayDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxDeviceModuleBayFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device_module_bay" "test" { device_id = netbox_device.test.id name = "%[1]s" label = "%[1]s_label" position = "testposition" description = "%[1]s_description" tags = [netbox_tag.test.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_module_bay.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_module_bay.test", "label", testName+"_label"), resource.TestCheckResourceAttr("netbox_device_module_bay.test", "position", "testposition"), resource.TestCheckResourceAttr("netbox_device_module_bay.test", "description", testName+"_description"), resource.TestCheckResourceAttr("netbox_device_module_bay.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_device_module_bay.test", "tags.0", testName+"a"), resource.TestCheckResourceAttrPair("netbox_device_module_bay.test", "device_id", "netbox_device.test", "id"), ), }, { Config: testAccNetboxDeviceModuleBayFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device_module_bay" "test" { device_id = netbox_device.test.id name = "%[1]s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_module_bay.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_module_bay.test", "label", ""), resource.TestCheckResourceAttr("netbox_device_module_bay.test", "position", ""), resource.TestCheckResourceAttr("netbox_device_module_bay.test", "description", ""), resource.TestCheckResourceAttr("netbox_device_module_bay.test", "tags.#", "0"), resource.TestCheckResourceAttrPair("netbox_device_module_bay.test", "device_id", "netbox_device.test", "id"), ), }, { ResourceName: "netbox_device_module_bay.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckDeviceModuleBayDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each module bay // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_device_module_bay" { continue } // Retrieve our device by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := dcim.NewDcimModuleBaysReadParams().WithID(stateID) _, err := conn.Dcim.DcimModuleBaysRead(params, nil) if err == nil { return fmt.Errorf("device_module_bay (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*dcim.DcimModuleBaysReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func init() { resource.AddTestSweepers("netbox_device_module_bay", &resource.Sweeper{ Name: "netbox_device_module_bay", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimModuleBaysListParams() res, err := api.Dcim.DcimModuleBaysList(params, nil) if err != nil { return err } for _, moduleBay := range res.GetPayload().Results { if strings.HasPrefix(*moduleBay.Name, testPrefix) { deleteParams := dcim.NewDcimModuleBaysDeleteParams().WithID(moduleBay.ID) _, err := api.Dcim.DcimModuleBaysDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a device_module_bay") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_device_power_feed.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxPowerFeed() *schema.Resource { return &schema.Resource{ Create: resourceNetboxPowerFeedCreate, Read: resourceNetboxPowerFeedRead, Update: resourceNetboxPowerFeedUpdate, Delete: resourceNetboxPowerFeedDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/powerfeed/): > A power feed represents the distribution of power from a power panel to a particular device, typically a power distribution unit (PDU). The power port (inlet) on a device can be connected via a cable to a power feed. A power feed may optionally be assigned to a rack to allow more easily tracking the distribution of power among racks.`, Schema: map[string]*schema.Schema{ "power_panel_id": { Type: schema.TypeInt, Required: true, }, "name": { Type: schema.TypeString, Required: true, }, "status": { Type: schema.TypeString, Required: true, Description: "One of [offline, active, planned, failed]", ValidateFunc: validation.StringInSlice([]string{"offline", "active", "planned", "failed"}, false), }, "type": { Type: schema.TypeString, Required: true, Description: "One of [primary, redundant]", ValidateFunc: validation.StringInSlice([]string{"primary", "redundant"}, false), }, "supply": { Type: schema.TypeString, Required: true, Description: "One of [ac, dc]", ValidateFunc: validation.StringInSlice([]string{"ac", "dc"}, false), }, "phase": { Type: schema.TypeString, Required: true, Description: "One of [single-phase, three-phase]", ValidateFunc: validation.StringInSlice([]string{"single-phase", "three-phase"}, false), }, "voltage": { Type: schema.TypeInt, Required: true, }, "amperage": { Type: schema.TypeInt, Required: true, }, "max_percent_utilization": { Type: schema.TypeInt, Required: true, ValidateFunc: validation.IntBetween(1, 100), }, "rack_id": { Type: schema.TypeInt, Optional: true, }, "mark_connected": { Type: schema.TypeBool, Default: false, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, "comments": { Type: schema.TypeString, Optional: true, }, tagsKey: tagsSchema, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxPowerFeedCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritablePowerFeed{ PowerPanel: int64ToPtr(int64(d.Get("power_panel_id").(int))), Name: strToPtr(d.Get("name").(string)), Status: d.Get("status").(string), Type: d.Get("type").(string), Supply: d.Get("supply").(string), Phase: d.Get("phase").(string), Voltage: int64ToPtr(int64(d.Get("voltage").(int))), Amperage: int64(d.Get("amperage").(int)), MaxUtilization: int64(d.Get("max_percent_utilization").(int)), Rack: getOptionalInt(d, "rack_id"), MarkConnected: d.Get("mark_connected").(bool), Description: getOptionalStr(d, "description", false), Comments: getOptionalStr(d, "comments", false), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimPowerFeedsCreateParams().WithData(&data) res, err := api.Dcim.DcimPowerFeedsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxPowerFeedRead(d, m) } func resourceNetboxPowerFeedRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimPowerFeedsReadParams().WithID(id) res, err := api.Dcim.DcimPowerFeedsRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimPowerFeedsReadDefault); ok { if errresp.Code() == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } powerFeed := res.GetPayload() if powerFeed.PowerPanel != nil { d.Set("power_panel_id", powerFeed.PowerPanel.ID) } else { d.Set("power_panel_id", nil) } d.Set("name", powerFeed.Name) if powerFeed.Status != nil { d.Set("status", powerFeed.Status.Value) } else { d.Set("status", nil) } if powerFeed.Type != nil { d.Set("type", powerFeed.Type.Value) } else { d.Set("type", nil) } if powerFeed.Supply != nil { d.Set("supply", powerFeed.Supply.Value) } else { d.Set("supply", nil) } if powerFeed.Phase != nil { d.Set("phase", powerFeed.Phase.Value) } else { d.Set("phase", nil) } d.Set("voltage", powerFeed.Voltage) d.Set("amperage", powerFeed.Amperage) d.Set("max_percent_utilization", powerFeed.MaxUtilization) if powerFeed.Rack != nil { d.Set("rack_id", powerFeed.Rack.ID) } else { d.Set("rack_id", nil) } d.Set("mark_connected", powerFeed.MarkConnected) d.Set("description", powerFeed.Description) d.Set("comments", powerFeed.Comments) cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxPowerFeedUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritablePowerFeed{ PowerPanel: int64ToPtr(int64(d.Get("power_panel_id").(int))), Name: strToPtr(d.Get("name").(string)), Status: d.Get("status").(string), Type: d.Get("type").(string), Supply: d.Get("supply").(string), Phase: d.Get("phase").(string), Voltage: int64ToPtr(int64(d.Get("voltage").(int))), Amperage: int64(d.Get("amperage").(int)), MaxUtilization: int64(d.Get("max_percent_utilization").(int)), Rack: getOptionalInt(d, "rack_id"), MarkConnected: d.Get("mark_connected").(bool), Description: getOptionalStr(d, "description", true), Comments: getOptionalStr(d, "comments", true), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimPowerFeedsPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimPowerFeedsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxPowerFeedRead(d, m) } func resourceNetboxPowerFeedDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimPowerFeedsDeleteParams().WithID(id) _, err := api.Dcim.DcimPowerFeedsDelete(params, nil) if err != nil { return err } return nil } ================================================ FILE: netbox/resource_netbox_device_power_feed_test.go ================================================ package netbox import ( "fmt" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" log "github.com/sirupsen/logrus" ) func testAccNetboxDevicePowerFeedFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_tag" "test" { name = "%[1]sa" } resource "netbox_location" "test" { name = "%[1]s" site_id =netbox_site.test.id } resource "netbox_power_panel" "test" { name = "%[1]s" site_id = netbox_site.test.id location_id = netbox_location.test.id } resource "netbox_rack" "test" { name = "%[1]s" site_id = netbox_site.test.id status = "reserved" width = 19 u_height = 48 tenant_id = netbox_tenant.test.id location_id = netbox_location.test.id } `, testName) } func TestAccNetboxDevicePowerFeed_basic(t *testing.T) { testSlug := "device_power_feed_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckDevicePowerFeedDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxDevicePowerFeedFullDependencies(testName) + fmt.Sprintf(` resource "netbox_power_feed" "test" { power_panel_id = netbox_power_panel.test.id name = "%[1]s" status = "active" type = "primary" supply = "ac" phase = "single-phase" voltage = 250 amperage = 100 max_percent_utilization = 80 rack_id = netbox_rack.test.id mark_connected = true description = "%[1]s_description" comments = "%[1]s_comments" tags = [netbox_tag.test.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_power_feed.test", "name", testName), resource.TestCheckResourceAttr("netbox_power_feed.test", "status", "active"), resource.TestCheckResourceAttr("netbox_power_feed.test", "type", "primary"), resource.TestCheckResourceAttr("netbox_power_feed.test", "supply", "ac"), resource.TestCheckResourceAttr("netbox_power_feed.test", "phase", "single-phase"), resource.TestCheckResourceAttr("netbox_power_feed.test", "voltage", "250"), resource.TestCheckResourceAttr("netbox_power_feed.test", "amperage", "100"), resource.TestCheckResourceAttr("netbox_power_feed.test", "max_percent_utilization", "80"), resource.TestCheckResourceAttr("netbox_power_feed.test", "description", testName+"_description"), resource.TestCheckResourceAttr("netbox_power_feed.test", "comments", testName+"_comments"), resource.TestCheckResourceAttr("netbox_power_feed.test", "mark_connected", "true"), resource.TestCheckResourceAttr("netbox_power_feed.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_power_feed.test", "tags.0", testName+"a"), resource.TestCheckResourceAttrPair("netbox_power_feed.test", "power_panel_id", "netbox_power_panel.test", "id"), resource.TestCheckResourceAttrPair("netbox_power_feed.test", "rack_id", "netbox_rack.test", "id"), ), }, { Config: testAccNetboxDevicePowerFeedFullDependencies(testName) + fmt.Sprintf(` resource "netbox_power_feed" "test" { power_panel_id = netbox_power_panel.test.id name = "%[1]s" status = "active" type = "primary" supply = "ac" phase = "single-phase" voltage = 250 amperage = 100 max_percent_utilization = 80 }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_power_feed.test", "name", testName), resource.TestCheckResourceAttr("netbox_power_feed.test", "status", "active"), resource.TestCheckResourceAttr("netbox_power_feed.test", "type", "primary"), resource.TestCheckResourceAttr("netbox_power_feed.test", "supply", "ac"), resource.TestCheckResourceAttr("netbox_power_feed.test", "phase", "single-phase"), resource.TestCheckResourceAttr("netbox_power_feed.test", "voltage", "250"), resource.TestCheckResourceAttr("netbox_power_feed.test", "amperage", "100"), resource.TestCheckResourceAttr("netbox_power_feed.test", "max_percent_utilization", "80"), resource.TestCheckResourceAttr("netbox_power_feed.test", "description", ""), resource.TestCheckResourceAttr("netbox_power_feed.test", "comments", ""), resource.TestCheckResourceAttr("netbox_power_feed.test", "mark_connected", "false"), resource.TestCheckResourceAttr("netbox_power_feed.test", "tags.#", "0"), resource.TestCheckResourceAttr("netbox_power_feed.test", "rack_id", "0"), resource.TestCheckResourceAttrPair("netbox_power_feed.test", "power_panel_id", "netbox_power_panel.test", "id"), ), }, { ResourceName: "netbox_power_feed.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckDevicePowerFeedDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each power feed // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_power_feed" { continue } // Retrieve our device by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := dcim.NewDcimPowerFeedsReadParams().WithID(stateID) _, err := conn.Dcim.DcimPowerFeedsRead(params, nil) if err == nil { return fmt.Errorf("device_power_feed (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*dcim.DcimPowerFeedsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func init() { resource.AddTestSweepers("netbox_power_feed", &resource.Sweeper{ Name: "netbox_power_feed", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimPowerFeedsListParams() res, err := api.Dcim.DcimPowerFeedsList(params, nil) if err != nil { return err } for _, powerFeed := range res.GetPayload().Results { if strings.HasPrefix(*powerFeed.Name, testPrefix) { deleteParams := dcim.NewDcimPowerFeedsDeleteParams().WithID(powerFeed.ID) _, err := api.Dcim.DcimPowerFeedsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a power_feed") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_device_power_outlet.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxDevicePowerOutlet() *schema.Resource { return &schema.Resource{ Create: resourceNetboxDevicePowerOutletCreate, Read: resourceNetboxDevicePowerOutletRead, Update: resourceNetboxDevicePowerOutletUpdate, Delete: resourceNetboxDevicePowerOutletDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/poweroutlet/): > Power outlets represent the outlets on a power distribution unit (PDU) or other device that supplies power to dependent devices. Each power port may be assigned a physical type, and may be associated with a specific feed leg (where three-phase power is used) and/or a specific upstream power port. This association can be used to model the distribution of power within a device. For example, imagine a PDU with one power port which draws from a three-phase feed and 48 power outlets arranged into three banks of 16 outlets each. Outlets 1-16 would be associated with leg A on the port, and outlets 17-32 and 33-48 would be associated with legs B and C, respectively.`, Schema: map[string]*schema.Schema{ "device_id": { Type: schema.TypeInt, Required: true, }, "name": { Type: schema.TypeString, Required: true, }, "module_id": { Type: schema.TypeInt, Optional: true, }, "label": { Type: schema.TypeString, Optional: true, }, "type": { Type: schema.TypeString, Optional: true, Description: "One of [iec-60320-c5, iec-60320-c7, iec-60320-c13, iec-60320-c15, iec-60320-c19, iec-60320-c21, iec-60309-p-n-e-4h, iec-60309-p-n-e-6h, iec-60309-p-n-e-9h, iec-60309-2p-e-4h, iec-60309-2p-e-6h, iec-60309-2p-e-9h, iec-60309-3p-e-4h, iec-60309-3p-e-6h, iec-60309-3p-e-9h, iec-60309-3p-n-e-4h, iec-60309-3p-n-e-6h, iec-60309-3p-n-e-9h, nema-1-15r, nema-5-15r, nema-5-20r, nema-5-30r, nema-5-50r, nema-6-15r, nema-6-20r, nema-6-30r, nema-6-50r, nema-10-30r, nema-10-50r, nema-14-20r, nema-14-30r, nema-14-50r, nema-14-60r, nema-15-15r, nema-15-20r, nema-15-30r, nema-15-50r, nema-15-60r, nema-l1-15r, nema-l5-15r, nema-l5-20r, nema-l5-30r, nema-l5-50r, nema-l6-15r, nema-l6-20r, nema-l6-30r, nema-l6-50r, nema-l10-30r, nema-l14-20r, nema-l14-30r, nema-l14-50r, nema-l14-60r, nema-l15-20r, nema-l15-30r, nema-l15-50r, nema-l15-60r, nema-l21-20r, nema-l21-30r, nema-l22-30r, CS6360C, CS6364C, CS8164C, CS8264C, CS8364C, CS8464C, ita-e, ita-f, ita-g, ita-h, ita-i, ita-j, ita-k, ita-l, ita-m, ita-n, ita-o, ita-multistandard, usb-a, usb-micro-b, usb-c, dc-terminal, hdot-cx, saf-d-grid, neutrik-powercon-20a, neutrik-powercon-32a, neutrik-powercon-true1, neutrik-powercon-true1-top, ubiquiti-smartpower, hardwired, other]", }, "power_port_id": { Type: schema.TypeInt, Optional: true, }, "feed_leg": { Type: schema.TypeString, Optional: true, Description: "One of [A, B, C]", ValidateFunc: validation.StringInSlice([]string{"A", "B", "C"}, false), }, "description": { Type: schema.TypeString, Optional: true, }, "mark_connected": { Type: schema.TypeBool, Default: false, Optional: true, }, tagsKey: tagsSchema, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxDevicePowerOutletCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritablePowerOutlet{ Device: int64ToPtr(int64(d.Get("device_id").(int))), Module: getOptionalInt(d, "module_id"), Name: strToPtr(d.Get("name").(string)), Label: getOptionalStr(d, "label", false), Type: getOptionalStr(d, "type", false), PowerPort: getOptionalInt(d, "power_port_id"), FeedLeg: getOptionalStr(d, "feed_leg", false), Description: getOptionalStr(d, "description", false), MarkConnected: d.Get("mark_connected").(bool), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimPowerOutletsCreateParams().WithData(&data) res, err := api.Dcim.DcimPowerOutletsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxDevicePowerOutletRead(d, m) } func resourceNetboxDevicePowerOutletRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimPowerOutletsReadParams().WithID(id) res, err := api.Dcim.DcimPowerOutletsRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimPowerOutletsReadDefault); ok { if errresp.Code() == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } powerOutlet := res.GetPayload() if powerOutlet.Device != nil { d.Set("device_id", powerOutlet.Device.ID) } else { d.Set("device_id", nil) } d.Set("name", powerOutlet.Name) if powerOutlet.Module != nil { d.Set("module_id", powerOutlet.Module.ID) } else { d.Set("module_id", nil) } d.Set("label", powerOutlet.Label) if powerOutlet.Type != nil { d.Set("type", powerOutlet.Type.Value) } else { d.Set("type", nil) } if powerOutlet.PowerPort != nil { d.Set("power_port_id", powerOutlet.PowerPort.ID) } else { d.Set("power_port_id", nil) } if powerOutlet.FeedLeg != nil { d.Set("feed_leg", powerOutlet.FeedLeg.Value) } else { d.Set("feed_leg", nil) } d.Set("description", powerOutlet.Description) d.Set("mark_connected", powerOutlet.MarkConnected) cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxDevicePowerOutletUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritablePowerOutlet{ Device: int64ToPtr(int64(d.Get("device_id").(int))), Module: getOptionalInt(d, "module_id"), Name: strToPtr(d.Get("name").(string)), Label: getOptionalStr(d, "label", true), Type: getOptionalStr(d, "type", false), PowerPort: getOptionalInt(d, "power_port_id"), FeedLeg: getOptionalStr(d, "feed_leg", false), Description: getOptionalStr(d, "description", true), MarkConnected: d.Get("mark_connected").(bool), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimPowerOutletsPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimPowerOutletsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxDevicePowerOutletRead(d, m) } func resourceNetboxDevicePowerOutletDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimPowerOutletsDeleteParams().WithID(id) _, err := api.Dcim.DcimPowerOutletsDelete(params, nil) if err != nil { return err } return nil } ================================================ FILE: netbox/resource_netbox_device_power_outlet_test.go ================================================ package netbox import ( "fmt" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" log "github.com/sirupsen/logrus" ) func testAccNetboxDevicePowerOutletFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_tag" "test" { name = "%[1]sa" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_module_bay" "test" { device_id = netbox_device.test.id name = "%[1]s" } resource "netbox_module_type" "test" { manufacturer_id = netbox_manufacturer.test.id model = "%[1]s" } resource "netbox_module" "test" { device_id = netbox_device.test.id module_bay_id = netbox_device_module_bay.test.id module_type_id = netbox_module_type.test.id status = "active" } resource "netbox_device_power_port" "test" { device_id = netbox_device.test.id name = "%[1]s" } `, testName) } func TestAccNetboxDevicePowerOutlet_basic(t *testing.T) { testSlug := "device_power_outlet_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckDevicePowerOutletDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxDevicePowerOutletFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device_power_outlet" "test" { device_id = netbox_device.test.id name = "%[1]s" module_id = netbox_module.test.id label = "%[1]s_label" type = "iec-60320-c5" power_port_id = netbox_device_power_port.test.id feed_leg = "A" description = "%[1]s_description" mark_connected = true tags = [netbox_tag.test.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_power_outlet.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_power_outlet.test", "label", testName+"_label"), resource.TestCheckResourceAttr("netbox_device_power_outlet.test", "type", "iec-60320-c5"), resource.TestCheckResourceAttr("netbox_device_power_outlet.test", "feed_leg", "A"), resource.TestCheckResourceAttr("netbox_device_power_outlet.test", "description", testName+"_description"), resource.TestCheckResourceAttr("netbox_device_power_outlet.test", "mark_connected", "true"), resource.TestCheckResourceAttr("netbox_device_power_outlet.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_device_power_outlet.test", "tags.0", testName+"a"), resource.TestCheckResourceAttrPair("netbox_device_power_outlet.test", "device_id", "netbox_device.test", "id"), resource.TestCheckResourceAttrPair("netbox_device_power_outlet.test", "module_id", "netbox_module.test", "id"), resource.TestCheckResourceAttrPair("netbox_device_power_outlet.test", "power_port_id", "netbox_device_power_port.test", "id"), ), }, { Config: testAccNetboxDevicePowerOutletFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device_power_outlet" "test" { device_id = netbox_device.test.id name = "%[1]s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_power_outlet.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_power_outlet.test", "label", ""), resource.TestCheckResourceAttr("netbox_device_power_outlet.test", "type", ""), resource.TestCheckResourceAttr("netbox_device_power_outlet.test", "feed_leg", ""), resource.TestCheckResourceAttr("netbox_device_power_outlet.test", "description", ""), resource.TestCheckResourceAttr("netbox_device_power_outlet.test", "mark_connected", "false"), resource.TestCheckResourceAttr("netbox_device_power_outlet.test", "tags.#", "0"), resource.TestCheckResourceAttr("netbox_device_power_outlet.test", "module_id", "0"), resource.TestCheckResourceAttr("netbox_device_power_outlet.test", "power_port_id", "0"), resource.TestCheckResourceAttrPair("netbox_device_power_outlet.test", "device_id", "netbox_device.test", "id"), ), }, { ResourceName: "netbox_device_power_outlet.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckDevicePowerOutletDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each power outlet // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_device_power_outlet" { continue } // Retrieve our device by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := dcim.NewDcimPowerOutletsReadParams().WithID(stateID) _, err := conn.Dcim.DcimPowerOutletsRead(params, nil) if err == nil { return fmt.Errorf("device_power_outlet (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*dcim.DcimPowerOutletsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func init() { resource.AddTestSweepers("netbox_device_power_outlet", &resource.Sweeper{ Name: "netbox_device_power_outlet", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimPowerOutletsListParams() res, err := api.Dcim.DcimPowerOutletsList(params, nil) if err != nil { return err } for _, powerOutlet := range res.GetPayload().Results { if strings.HasPrefix(*powerOutlet.Name, testPrefix) { deleteParams := dcim.NewDcimPowerOutletsDeleteParams().WithID(powerOutlet.ID) _, err := api.Dcim.DcimPowerOutletsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a device_power_outlet") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_device_power_port.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceNetboxDevicePowerPort() *schema.Resource { return &schema.Resource{ Create: resourceNetboxDevicePowerPortCreate, Read: resourceNetboxDevicePowerPortRead, Update: resourceNetboxDevicePowerPortUpdate, Delete: resourceNetboxDevicePowerPortDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/powerport/): > A power port is a device component which draws power from some external source (e.g. an upstream power outlet), and generally represents a power supply internal to a device.`, Schema: map[string]*schema.Schema{ "device_id": { Type: schema.TypeInt, Required: true, }, "name": { Type: schema.TypeString, Required: true, }, "module_id": { Type: schema.TypeInt, Optional: true, }, "label": { Type: schema.TypeString, Optional: true, }, "type": { Type: schema.TypeString, Optional: true, Description: "One of [iec-60320-c6, iec-60320-c8, iec-60320-c14, iec-60320-c16, iec-60320-c20, iec-60320-c22, iec-60309-p-n-e-4h, iec-60309-p-n-e-6h, iec-60309-p-n-e-9h, iec-60309-2p-e-4h, iec-60309-2p-e-6h, iec-60309-2p-e-9h, iec-60309-3p-e-4h, iec-60309-3p-e-6h, iec-60309-3p-e-9h, iec-60309-3p-n-e-4h, iec-60309-3p-n-e-6h, iec-60309-3p-n-e-9h, nema-1-15p, nema-5-15p, nema-5-20p, nema-5-30p, nema-5-50p, nema-6-15p, nema-6-20p, nema-6-30p, nema-6-50p, nema-10-30p, nema-10-50p, nema-14-20p, nema-14-30p, nema-14-50p, nema-14-60p, nema-15-15p, nema-15-20p, nema-15-30p, nema-15-50p, nema-15-60p, nema-l1-15p, nema-l5-15p, nema-l5-20p, nema-l5-30p, nema-l5-50p, nema-l6-15p, nema-l6-20p, nema-l6-30p, nema-l6-50p, nema-l10-30p, nema-l14-20p, nema-l14-30p, nema-l14-50p, nema-l14-60p, nema-l15-20p, nema-l15-30p, nema-l15-50p, nema-l15-60p, nema-l21-20p, nema-l21-30p, nema-l22-30p, cs6361c, cs6365c, cs8165c, cs8265c, cs8365c, cs8465c, ita-c, ita-e, ita-f, ita-ef, ita-g, ita-h, ita-i, ita-j, ita-k, ita-l, ita-m, ita-n, ita-o, usb-a, usb-b, usb-c, usb-mini-a, usb-mini-b, usb-micro-a, usb-micro-b, usb-micro-ab, usb-3-b, usb-3-micro-b, dc-terminal, saf-d-grid, neutrik-powercon-20, neutrik-powercon-32, neutrik-powercon-true1, neutrik-powercon-true1-top, ubiquiti-smartpower, hardwired, other]", }, "maximum_draw": { Type: schema.TypeInt, Optional: true, }, "allocated_draw": { Type: schema.TypeInt, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, "mark_connected": { Type: schema.TypeBool, Default: false, Optional: true, }, tagsKey: tagsSchema, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxDevicePowerPortCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritablePowerPort{ Device: int64ToPtr(int64(d.Get("device_id").(int))), Module: getOptionalInt(d, "module_id"), Name: strToPtr(d.Get("name").(string)), Label: getOptionalStr(d, "label", false), Type: getOptionalStr(d, "type", false), MaximumDraw: getOptionalInt(d, "maximum_draw"), AllocatedDraw: getOptionalInt(d, "allocated_draw"), Description: getOptionalStr(d, "description", false), MarkConnected: d.Get("mark_connected").(bool), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimPowerPortsCreateParams().WithData(&data) res, err := api.Dcim.DcimPowerPortsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxDevicePowerPortRead(d, m) } func resourceNetboxDevicePowerPortRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimPowerPortsReadParams().WithID(id) res, err := api.Dcim.DcimPowerPortsRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimPowerPortsReadDefault); ok { if errresp.Code() == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } powerPort := res.GetPayload() if powerPort.Device != nil { d.Set("device_id", powerPort.Device.ID) } else { d.Set("device_id", nil) } d.Set("name", powerPort.Name) if powerPort.Module != nil { d.Set("module_id", powerPort.Module.ID) } else { d.Set("module_id", nil) } d.Set("label", powerPort.Label) if powerPort.Type != nil { d.Set("type", powerPort.Type.Value) } else { d.Set("type", nil) } d.Set("maximum_draw", powerPort.MaximumDraw) d.Set("allocated_draw", powerPort.AllocatedDraw) d.Set("description", powerPort.Description) d.Set("mark_connected", powerPort.MarkConnected) cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxDevicePowerPortUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritablePowerPort{ Device: int64ToPtr(int64(d.Get("device_id").(int))), Module: getOptionalInt(d, "module_id"), Name: strToPtr(d.Get("name").(string)), Label: getOptionalStr(d, "label", true), Type: getOptionalStr(d, "type", false), MaximumDraw: getOptionalInt(d, "maximum_draw"), AllocatedDraw: getOptionalInt(d, "allocated_draw"), Description: getOptionalStr(d, "description", true), MarkConnected: d.Get("mark_connected").(bool), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimPowerPortsPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimPowerPortsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxDevicePowerPortRead(d, m) } func resourceNetboxDevicePowerPortDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimPowerPortsDeleteParams().WithID(id) _, err := api.Dcim.DcimPowerPortsDelete(params, nil) if err != nil { return err } return nil } ================================================ FILE: netbox/resource_netbox_device_power_port_test.go ================================================ package netbox import ( "fmt" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" log "github.com/sirupsen/logrus" ) func testAccNetboxDevicePowerPortFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_tag" "test" { name = "%[1]sa" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_module_bay" "test" { device_id = netbox_device.test.id name = "%[1]s" } resource "netbox_module_type" "test" { manufacturer_id = netbox_manufacturer.test.id model = "%[1]s" } resource "netbox_module" "test" { device_id = netbox_device.test.id module_bay_id = netbox_device_module_bay.test.id module_type_id = netbox_module_type.test.id status = "active" } `, testName) } func TestAccNetboxDevicePowerPort_basic(t *testing.T) { testSlug := "device_power_port_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckDevicePowerPortDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxDevicePowerPortFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device_power_port" "test" { device_id = netbox_device.test.id name = "%[1]s" maximum_draw = 750 allocated_draw = 500 mark_connected = true type = "iec-60320-c6" module_id = netbox_module.test.id description = "%[1]s_description" tags = [netbox_tag.test.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_power_port.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_power_port.test", "maximum_draw", "750"), resource.TestCheckResourceAttr("netbox_device_power_port.test", "allocated_draw", "500"), resource.TestCheckResourceAttr("netbox_device_power_port.test", "type", "iec-60320-c6"), resource.TestCheckResourceAttr("netbox_device_power_port.test", "mark_connected", "true"), resource.TestCheckResourceAttr("netbox_device_power_port.test", "description", testName+"_description"), resource.TestCheckResourceAttr("netbox_device_power_port.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_device_power_port.test", "tags.0", testName+"a"), resource.TestCheckResourceAttrPair("netbox_device_power_port.test", "device_id", "netbox_device.test", "id"), resource.TestCheckResourceAttrPair("netbox_device_power_port.test", "module_id", "netbox_module.test", "id"), ), }, { Config: testAccNetboxDevicePowerPortFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device_power_port" "test" { device_id = netbox_device.test.id name = "%[1]s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_power_port.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_power_port.test", "maximum_draw", "0"), resource.TestCheckResourceAttr("netbox_device_power_port.test", "allocated_draw", "0"), resource.TestCheckResourceAttr("netbox_device_power_port.test", "type", ""), resource.TestCheckResourceAttr("netbox_device_power_port.test", "mark_connected", "false"), resource.TestCheckResourceAttr("netbox_device_power_port.test", "description", ""), resource.TestCheckResourceAttr("netbox_device_power_port.test", "tags.#", "0"), resource.TestCheckResourceAttr("netbox_device_power_port.test", "module_id", "0"), resource.TestCheckResourceAttrPair("netbox_device_power_port.test", "device_id", "netbox_device.test", "id"), ), }, { ResourceName: "netbox_device_power_port.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckDevicePowerPortDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each power port // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_device_power_port" { continue } // Retrieve our device by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := dcim.NewDcimPowerPortsReadParams().WithID(stateID) _, err := conn.Dcim.DcimPowerPortsRead(params, nil) if err == nil { return fmt.Errorf("device_power_port (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*dcim.DcimPowerPortsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func init() { resource.AddTestSweepers("netbox_device_power_port", &resource.Sweeper{ Name: "netbox_device_power_port", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimPowerPortsListParams() res, err := api.Dcim.DcimPowerPortsList(params, nil) if err != nil { return err } for _, powerPort := range res.GetPayload().Results { if strings.HasPrefix(*powerPort.Name, testPrefix) { deleteParams := dcim.NewDcimPowerPortsDeleteParams().WithID(powerPort.ID) _, err := api.Dcim.DcimPowerPortsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a device_power_port") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_device_primary_ip.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxDevicePrimaryIP() *schema.Resource { return &schema.Resource{ Create: resourceNetboxDevicePrimaryIPCreate, Read: resourceNetboxDevicePrimaryIPRead, Update: resourceNetboxDevicePrimaryIPUpdate, Delete: resourceNetboxDevicePrimaryIPDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):This resource is used to define the primary IP for a given device. The primary IP is reflected in the device Netbox UI, which identifies the Primary IPv4 and IPv6 addresses.`, Schema: map[string]*schema.Schema{ "device_id": { Type: schema.TypeInt, Required: true, }, "ip_address_id": { Type: schema.TypeInt, Required: true, }, "ip_address_version": { Type: schema.TypeInt, ValidateFunc: validation.IntInSlice([]int{4, 6}), Optional: true, Default: 4, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxDevicePrimaryIPCreate(d *schema.ResourceData, m interface{}) error { d.SetId(strconv.Itoa(d.Get("device_id").(int))) return resourceNetboxDevicePrimaryIPUpdate(d, m) } func resourceNetboxDevicePrimaryIPRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimDevicesReadParams().WithID(id) res, err := api.Dcim.DcimDevicesRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimDevicesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } IPAddressVersion := d.Get("ip_address_version") d.Set("ip_address_version", IPAddressVersion) if IPAddressVersion == 4 && res.GetPayload().PrimaryIp4 != nil { d.Set("ip_address_id", res.GetPayload().PrimaryIp4.ID) } else if IPAddressVersion == 6 && res.GetPayload().PrimaryIp6 != nil { d.Set("ip_address_id", res.GetPayload().PrimaryIp6.ID) } else { // if the device exists, but has no primary ip, consider this element deleted d.SetId("") return nil } d.Set("device_id", res.GetPayload().ID) return nil } func resourceNetboxDevicePrimaryIPUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) deviceID := int64(d.Get("device_id").(int)) IPAddressID := int64(d.Get("ip_address_id").(int)) IPAddressVersion := int64(d.Get("ip_address_version").(int)) // because the go-netbox library does not have patch support atm, we have to get the whole object and re-put it // first, get the device readParams := dcim.NewDcimDevicesReadParams().WithID(deviceID) res, err := api.Dcim.DcimDevicesRead(readParams, nil) if err != nil { return err } device := res.GetPayload() // then update the FULL device with ALL tracked attributes data := models.WritableDeviceWithConfigContext{} data.Name = device.Name data.Tags = device.Tags // the netbox API sends the URL property as part of NestedTag, but it does not accept the URL property when we send it back // so set it to empty // display too for _, tag := range data.Tags { tag.URL = "" tag.Display = "" } if device.DeviceType != nil { data.DeviceType = &device.DeviceType.ID } if device.Site != nil { data.Site = &device.Site.ID } if device.Role != nil { data.Role = &device.Role.ID } if device.PrimaryIp4 != nil { data.PrimaryIp4 = &device.PrimaryIp4.ID } if device.PrimaryIp6 != nil { data.PrimaryIp6 = &device.PrimaryIp6.ID } // unset primary ip address if -1 is passed as id if IPAddressID == -1 { if IPAddressVersion == 4 { data.PrimaryIp4 = nil } else { data.PrimaryIp6 = nil } } else { if IPAddressVersion == 4 { data.PrimaryIp4 = &IPAddressID } else { data.PrimaryIp6 = &IPAddressID } } updateParams := dcim.NewDcimDevicesPartialUpdateParams().WithID(deviceID).WithData(&data) _, err = api.Dcim.DcimDevicesPartialUpdate(updateParams, nil) if err != nil { return err } return resourceNetboxDevicePrimaryIPRead(d, m) } func resourceNetboxDevicePrimaryIPDelete(d *schema.ResourceData, m interface{}) error { // Set ip_address_id to minus one and go to update. Update will set nil d.Set("ip_address_id", -1) return resourceNetboxDevicePrimaryIPUpdate(d, m) } ================================================ FILE: netbox/resource_netbox_device_primary_ip_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxDevicePrimaryIPFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_cluster_type" "test" { name = "%[1]s" } resource "netbox_cluster" "test" { name = "%[1]s" cluster_type_id = netbox_cluster_type.test.id site_id = netbox_site.test.id } resource "netbox_platform" "test" { name = "%[1]s" } resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_location" "test" { name = "%[1]s" site_id =netbox_site.test.id } resource "netbox_rack" "test" { name = "%[1]s" site_id = netbox_site.test.id status = "reserved" width = 19 u_height = 48 tenant_id = netbox_tenant.test.id location_id = netbox_location.test.id } resource "netbox_device" "test" { name = "%[1]s" role_id = netbox_device_role.test.id site_id = netbox_site.test.id tenant_id = netbox_tenant.test.id device_type_id = netbox_device_type.test.id cluster_id = netbox_cluster.test.id platform_id = netbox_platform.test.id location_id = netbox_location.test.id comments = "thisisacomment" status = "planned" rack_id = netbox_rack.test.id rack_face = "front" rack_position = 10 tags = [netbox_tag.test.name] } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_device_interface" "test" { device_id = netbox_device.test.id name = "%[1]s" type = "1000base-t" } `, testName) } func TestAccNetboxDevicePrimaryIP4_basic(t *testing.T) { testSlug := "pr_ip_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxDevicePrimaryIPFullDependencies(testName) + ` resource "netbox_ip_address" "test_v4" { ip_address = "1.1.1.12/32" status = "active" interface_id = netbox_device_interface.test.id object_type = "dcim.interface" } resource "netbox_device_primary_ip" "test_v4" { device_id = netbox_device.test.id ip_address_id = netbox_ip_address.test_v4.id }`, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4", "device_id", "netbox_device.test", "id"), resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4", "ip_address_id", "netbox_ip_address.test_v4", "id"), resource.TestCheckResourceAttr("netbox_device.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_device.test", "cluster_id", "netbox_cluster.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "platform_id", "netbox_platform.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "location_id", "netbox_location.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "role_id", "netbox_device_role.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttr("netbox_device.test", "comments", "thisisacomment"), resource.TestCheckResourceAttr("netbox_device.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_device.test", "tags.0", testName), resource.TestCheckResourceAttr("netbox_device.test", "status", "planned"), ), }, }, }) } func TestAccNetboxDevicePrimaryIP6_basic(t *testing.T) { testSlug := "pr_ipv6_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxDevicePrimaryIPFullDependencies(testName) + ` resource "netbox_ip_address" "test_v6" { ip_address = "2001::1/128" status = "active" interface_id = netbox_device_interface.test.id object_type = "dcim.interface" } resource "netbox_device_primary_ip" "test_v6" { device_id = netbox_device.test.id ip_address_id = netbox_ip_address.test_v6.id ip_address_version = 6 }`, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v6", "device_id", "netbox_device.test", "id"), resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v6", "ip_address_id", "netbox_ip_address.test_v6", "id"), resource.TestCheckResourceAttr("netbox_device.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_device.test", "cluster_id", "netbox_cluster.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "platform_id", "netbox_platform.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "location_id", "netbox_location.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "role_id", "netbox_device_role.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttr("netbox_device.test", "comments", "thisisacomment"), resource.TestCheckResourceAttr("netbox_device.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_device.test", "tags.0", testName), resource.TestCheckResourceAttr("netbox_device.test", "status", "planned"), ), }, }, }) } func TestAccNetboxDevicePrimaryIP4_removePrimary(t *testing.T) { testSlug := "pr_ip_removePrimary" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxDevicePrimaryIPFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device" "test2" { name = "%[1]s" role_id = netbox_device_role.test.id site_id = netbox_site.test.id device_type_id = netbox_device_type.test.id cluster_id = netbox_cluster.test.id platform_id = netbox_platform.test.id location_id = netbox_location.test.id comments = "thisisacomment" status = "planned" rack_id = netbox_rack.test.id rack_face = "front" rack_position = 11 tags = [netbox_tag.test.name] } resource "netbox_device_interface" "test2" { device_id = netbox_device.test2.id name = "%[1]s" type = "1000base-t" } resource "netbox_ip_address" "test_v4_2" { ip_address = "1.1.1.16/32" status = "active" interface_id = netbox_device_interface.test2.id object_type = "dcim.interface" } resource "netbox_device_primary_ip" "test_v4_2" { device_id = netbox_device.test2.id ip_address_id = netbox_ip_address.test_v4_2.id }`, testName), }, // A repeated second step is required, so that the resource "netbox_device" "test2" goes through a resourceNetboxDeviceRead cycle // This is needed because adding a netbox_device_primary_ip updates the netbox_device { Config: testAccNetboxDevicePrimaryIPFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device" "test2" { name = "%[1]s" role_id = netbox_device_role.test.id site_id = netbox_site.test.id device_type_id = netbox_device_type.test.id cluster_id = netbox_cluster.test.id platform_id = netbox_platform.test.id location_id = netbox_location.test.id comments = "thisisacomment" status = "planned" rack_id = netbox_rack.test.id rack_face = "front" rack_position = 11 tags = [netbox_tag.test.name] } resource "netbox_device_interface" "test2" { device_id = netbox_device.test2.id name = "%[1]s" type = "1000base-t" } resource "netbox_ip_address" "test_v4_2" { ip_address = "1.1.1.16/32" status = "active" interface_id = netbox_device_interface.test2.id object_type = "dcim.interface" } resource "netbox_device_primary_ip" "test_v4_2" { device_id = netbox_device.test2.id ip_address_id = netbox_ip_address.test_v4_2.id }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_2", "device_id", "netbox_device.test2", "id"), resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_2", "ip_address_id", "netbox_ip_address.test_v4_2", "id"), ), }, // Now we do 2 things: modify netbox_device.test2 (changing the comment value), AND we remove the IP and primary IP // This fails with: // Error: [PUT /dcim/devices/{id}/][400] dcim_devices_update default {"primary_ip4":["Related object not found using the provided numeric ID: 14"]} // because (I think) that the device is doing 1) a read of the current state, 2) the deletion of the primary IP then modifies the device, 3) the device then tries to write its changes, but its now out of date { Config: testAccNetboxDevicePrimaryIPFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device" "test2" { name = "%[1]s" role_id = netbox_device_role.test.id site_id = netbox_site.test.id device_type_id = netbox_device_type.test.id cluster_id = netbox_cluster.test.id platform_id = netbox_platform.test.id location_id = netbox_location.test.id comments = "thisisacomment with changes" status = "planned" rack_id = netbox_rack.test.id rack_face = "front" rack_position = 11 tags = [netbox_tag.test.name] } resource "netbox_device_interface" "test2" { device_id = netbox_device.test2.id name = "%[1]s" type = "1000base-t" } `, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device.test2", "name", testName), resource.TestCheckResourceAttr("netbox_device.test2", "primary_ipv4", "0"), resource.TestCheckResourceAttr("netbox_device.test2", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_device.test2", "tags.0", testName), resource.TestCheckResourceAttr("netbox_device.test2", "status", "planned"), ), }, }, }) } func TestAccNetboxDevicePrimaryIP4_simpleUpdateDevice(t *testing.T) { testSlug := "pr_ip_updateDevice" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxDevicePrimaryIPFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device" "test3" { name = "%[1]s" role_id = netbox_device_role.test.id site_id = netbox_site.test.id device_type_id = netbox_device_type.test.id cluster_id = netbox_cluster.test.id platform_id = netbox_platform.test.id location_id = netbox_location.test.id comments = "comment1" status = "planned" rack_id = netbox_rack.test.id rack_face = "front" rack_position = 11 tags = [netbox_tag.test.name] } resource "netbox_device_interface" "test3" { device_id = netbox_device.test3.id name = "%[1]s" type = "1000base-t" } resource "netbox_ip_address" "test_v4_3" { ip_address = "1.1.1.101/32" status = "active" interface_id = netbox_device_interface.test3.id object_type = "dcim.interface" } resource "netbox_device_primary_ip" "test_v4_3" { device_id = netbox_device.test3.id ip_address_id = netbox_ip_address.test_v4_3.id }`, testName), }, // Update the device and check the primary ip value { Config: testAccNetboxDevicePrimaryIPFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device" "test3" { name = "%[1]s" role_id = netbox_device_role.test.id site_id = netbox_site.test.id device_type_id = netbox_device_type.test.id cluster_id = netbox_cluster.test.id platform_id = netbox_platform.test.id location_id = netbox_location.test.id comments = "comment2" status = "planned" rack_id = netbox_rack.test.id rack_face = "front" rack_position = 11 tags = [netbox_tag.test.name] } resource "netbox_device_interface" "test3" { device_id = netbox_device.test3.id name = "%[1]s" type = "virtual" } resource "netbox_ip_address" "test_v4_3" { ip_address = "1.1.1.101/32" status = "active" interface_id = netbox_device_interface.test3.id object_type = "dcim.interface" } resource "netbox_device_primary_ip" "test_v4_3" { device_id = netbox_device.test3.id ip_address_id = netbox_ip_address.test_v4_3.id }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_3", "device_id", "netbox_device.test3", "id"), resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_3", "ip_address_id", "netbox_ip_address.test_v4_3", "id"), resource.TestCheckResourceAttrPair("netbox_device.test3", "primary_ipv4", "netbox_ip_address.test_v4_3", "id"), ), }, // Update the device and the interface and check the primary ip value { Config: testAccNetboxDevicePrimaryIPFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device" "test3" { name = "%[1]s" role_id = netbox_device_role.test.id site_id = netbox_site.test.id device_type_id = netbox_device_type.test.id cluster_id = netbox_cluster.test.id platform_id = netbox_platform.test.id location_id = netbox_location.test.id comments = "comment3" status = "planned" rack_id = netbox_rack.test.id rack_face = "front" rack_position = 11 tags = [netbox_tag.test.name] } resource "netbox_device_interface" "test3" { device_id = netbox_device.test3.id name = "%[1]s" type = "1000base-t" } resource "netbox_ip_address" "test_v4_3" { ip_address = "1.1.1.101/32" status = "active" interface_id = netbox_device_interface.test3.id object_type = "dcim.interface" } resource "netbox_device_primary_ip" "test_v4_3" { device_id = netbox_device.test3.id ip_address_id = netbox_ip_address.test_v4_3.id }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_3", "device_id", "netbox_device.test3", "id"), resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_3", "ip_address_id", "netbox_ip_address.test_v4_3", "id"), resource.TestCheckResourceAttrPair("netbox_device.test3", "primary_ipv4", "netbox_ip_address.test_v4_3", "id"), ), }, // Update the device, the interface and the IP address, then check the primary ip { Config: testAccNetboxDevicePrimaryIPFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device" "test3" { name = "%[1]s" role_id = netbox_device_role.test.id site_id = netbox_site.test.id device_type_id = netbox_device_type.test.id cluster_id = netbox_cluster.test.id platform_id = netbox_platform.test.id location_id = netbox_location.test.id comments = "comment4" status = "planned" rack_id = netbox_rack.test.id rack_face = "front" rack_position = 11 tags = [netbox_tag.test.name] } resource "netbox_device_interface" "test3" { device_id = netbox_device.test3.id name = "%[1]s" type = "virtual" } resource "netbox_ip_address" "test_v4_3" { ip_address = "1.1.1.102/32" status = "active" interface_id = netbox_device_interface.test3.id object_type = "dcim.interface" } resource "netbox_device_primary_ip" "test_v4_3" { device_id = netbox_device.test3.id ip_address_id = netbox_ip_address.test_v4_3.id }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_3", "device_id", "netbox_device.test3", "id"), resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_3", "ip_address_id", "netbox_ip_address.test_v4_3", "id"), resource.TestCheckResourceAttrPair("netbox_device.test3", "primary_ipv4", "netbox_ip_address.test_v4_3", "id"), ), }, // Force a read at the end { Config: testAccNetboxDevicePrimaryIPFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device" "test3" { name = "%[1]s" role_id = netbox_device_role.test.id site_id = netbox_site.test.id device_type_id = netbox_device_type.test.id cluster_id = netbox_cluster.test.id platform_id = netbox_platform.test.id location_id = netbox_location.test.id comments = "commnent4" status = "planned" rack_id = netbox_rack.test.id rack_face = "front" rack_position = 11 tags = [netbox_tag.test.name] } resource "netbox_device_interface" "test3" { device_id = netbox_device.test3.id name = "%[1]s" type = "virtual" } resource "netbox_ip_address" "test_v4_3" { ip_address = "1.1.1.102/32" status = "active" interface_id = netbox_device_interface.test3.id object_type = "dcim.interface" } resource "netbox_device_primary_ip" "test_v4_3" { device_id = netbox_device.test3.id ip_address_id = netbox_ip_address.test_v4_3.id }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_3", "device_id", "netbox_device.test3", "id"), resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_3", "ip_address_id", "netbox_ip_address.test_v4_3", "id"), resource.TestCheckResourceAttrPair("netbox_device.test3", "primary_ipv4", "netbox_ip_address.test_v4_3", "id"), ), }, }, }) } func TestAccNetboxDevicePrimaryIP4_refsUpdateDevice(t *testing.T) { testSlug := "pr_ip_refUpdateDevice" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxDevicePrimaryIPFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device" "test4" { name = "%[1]s" role_id = netbox_device_role.test.id site_id = netbox_site.test.id device_type_id = netbox_device_type.test.id cluster_id = netbox_cluster.test.id platform_id = netbox_platform.test.id location_id = netbox_location.test.id comments = "comment1" status = "planned" rack_id = netbox_rack.test.id rack_face = "front" rack_position = 11 tags = [netbox_tag.test.name] } resource "netbox_device_interface" "test4" { device_id = netbox_device.test4.id name = "%[1]s" type = "1000base-t" lifecycle { create_before_destroy = true } } resource "netbox_ip_address" "test_v4_4" { ip_address = "1.1.2.101/32" status = "active" interface_id = netbox_device_interface.test4.id object_type = "dcim.interface" } resource "netbox_device_primary_ip" "test_v4_4" { device_id = netbox_device.test4.id ip_address_id = netbox_ip_address.test_v4_4.id }`, testName), }, // Switch the interface for a new one // the create_before_destroy lifecycle ensures the interface exists at all times { Config: testAccNetboxDevicePrimaryIPFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device" "test4" { name = "%[1]s" role_id = netbox_device_role.test.id site_id = netbox_site.test.id device_type_id = netbox_device_type.test.id cluster_id = netbox_cluster.test.id platform_id = netbox_platform.test.id location_id = netbox_location.test.id comments = "comment1" status = "planned" rack_id = netbox_rack.test.id rack_face = "front" rack_position = 11 tags = [netbox_tag.test.name] } resource "netbox_device_interface" "test4_2" { device_id = netbox_device.test4.id name = "%[1]s-2" type = "1000base-t" lifecycle { create_before_destroy = true } } resource "netbox_ip_address" "test_v4_4" { ip_address = "1.1.2.101/32" status = "active" interface_id = netbox_device_interface.test4_2.id object_type = "dcim.interface" } resource "netbox_device_primary_ip" "test_v4_4" { device_id = netbox_device.test4.id ip_address_id = netbox_ip_address.test_v4_4.id }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_4", "device_id", "netbox_device.test4", "id"), resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_4", "ip_address_id", "netbox_ip_address.test_v4_4", "id"), resource.TestCheckResourceAttrPair("netbox_device.test4", "primary_ipv4", "netbox_ip_address.test_v4_4", "id"), ), }, // switch the ip address for a new one { Config: testAccNetboxDevicePrimaryIPFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device" "test4" { name = "%[1]s" role_id = netbox_device_role.test.id site_id = netbox_site.test.id device_type_id = netbox_device_type.test.id cluster_id = netbox_cluster.test.id platform_id = netbox_platform.test.id location_id = netbox_location.test.id comments = "comment1" status = "planned" rack_id = netbox_rack.test.id rack_face = "front" rack_position = 11 tags = [netbox_tag.test.name] } resource "netbox_device_interface" "test4_2" { device_id = netbox_device.test4.id name = "%[1]s-2" type = "1000base-t" lifecycle { create_before_destroy = true } } resource "netbox_ip_address" "test_v4_4_2" { ip_address = "1.1.2.102/32" status = "active" interface_id = netbox_device_interface.test4_2.id object_type = "dcim.interface" } resource "netbox_device_primary_ip" "test_v4_4" { device_id = netbox_device.test4.id ip_address_id = netbox_ip_address.test_v4_4_2.id }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_4", "device_id", "netbox_device.test4", "id"), resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_4", "ip_address_id", "netbox_ip_address.test_v4_4_2", "id"), // Needs another read on the device for the field to be accurate //resource.TestCheckResourceAttrPair("netbox_device.test4", "primary_ipv4", "netbox_ip_address.test_v4_4_2", "id"), ), }, // Force another read to update device { Config: testAccNetboxDevicePrimaryIPFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device" "test4" { name = "%[1]s" role_id = netbox_device_role.test.id site_id = netbox_site.test.id device_type_id = netbox_device_type.test.id cluster_id = netbox_cluster.test.id platform_id = netbox_platform.test.id location_id = netbox_location.test.id comments = "comment1" status = "planned" rack_id = netbox_rack.test.id rack_face = "front" rack_position = 11 tags = [netbox_tag.test.name] } resource "netbox_device_interface" "test4_2" { device_id = netbox_device.test4.id name = "%[1]s-2" type = "1000base-t" lifecycle { create_before_destroy = true } } resource "netbox_ip_address" "test_v4_4_2" { ip_address = "1.1.2.102/32" status = "active" interface_id = netbox_device_interface.test4_2.id object_type = "dcim.interface" } resource "netbox_device_primary_ip" "test_v4_4" { device_id = netbox_device.test4.id ip_address_id = netbox_ip_address.test_v4_4_2.id }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_4", "device_id", "netbox_device.test4", "id"), resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_4", "ip_address_id", "netbox_ip_address.test_v4_4_2", "id"), resource.TestCheckResourceAttrPair("netbox_device.test4", "primary_ipv4", "netbox_ip_address.test_v4_4_2", "id"), ), }, // Switch both the interface and the ip address at the same time { Config: testAccNetboxDevicePrimaryIPFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device" "test4" { name = "%[1]s" role_id = netbox_device_role.test.id site_id = netbox_site.test.id device_type_id = netbox_device_type.test.id cluster_id = netbox_cluster.test.id platform_id = netbox_platform.test.id location_id = netbox_location.test.id comments = "comment1" status = "planned" rack_id = netbox_rack.test.id rack_face = "front" rack_position = 11 tags = [netbox_tag.test.name] } resource "netbox_device_interface" "test4_3" { device_id = netbox_device.test4.id name = "%[1]s-3" type = "virtual" lifecycle { create_before_destroy = true } } resource "netbox_ip_address" "test_v4_4_3" { ip_address = "1.1.2.103/32" status = "active" interface_id = netbox_device_interface.test4_3.id object_type = "dcim.interface" } resource "netbox_device_primary_ip" "test_v4_4" { device_id = netbox_device.test4.id ip_address_id = netbox_ip_address.test_v4_4_3.id }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_4", "device_id", "netbox_device.test4", "id"), resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_4", "ip_address_id", "netbox_ip_address.test_v4_4_3", "id"), // Require another read //resource.TestCheckResourceAttrPair("netbox_device.test4", "primary_ipv4", "netbox_ip_address.test_v4_4_3", "id"), ), }, // Force another read { Config: testAccNetboxDevicePrimaryIPFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device" "test4" { name = "%[1]s" role_id = netbox_device_role.test.id site_id = netbox_site.test.id device_type_id = netbox_device_type.test.id cluster_id = netbox_cluster.test.id platform_id = netbox_platform.test.id location_id = netbox_location.test.id comments = "comment1" status = "planned" rack_id = netbox_rack.test.id rack_face = "front" rack_position = 11 tags = [netbox_tag.test.name] } resource "netbox_device_interface" "test4_3" { device_id = netbox_device.test4.id name = "%[1]s-3" type = "virtual" lifecycle { create_before_destroy = true } } resource "netbox_ip_address" "test_v4_4_3" { ip_address = "1.1.2.103/32" status = "active" interface_id = netbox_device_interface.test4_3.id object_type = "dcim.interface" } resource "netbox_device_primary_ip" "test_v4_4" { device_id = netbox_device.test4.id ip_address_id = netbox_ip_address.test_v4_4_3.id }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_4", "device_id", "netbox_device.test4", "id"), resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_4", "ip_address_id", "netbox_ip_address.test_v4_4_3", "id"), resource.TestCheckResourceAttrPair("netbox_device.test4", "primary_ipv4", "netbox_ip_address.test_v4_4_3", "id"), ), }, // Make a new primary IP { Config: testAccNetboxDevicePrimaryIPFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device" "test4" { name = "%[1]s" role_id = netbox_device_role.test.id site_id = netbox_site.test.id device_type_id = netbox_device_type.test.id cluster_id = netbox_cluster.test.id platform_id = netbox_platform.test.id location_id = netbox_location.test.id comments = "comment1" status = "planned" rack_id = netbox_rack.test.id rack_face = "front" rack_position = 11 tags = [netbox_tag.test.name] } resource "netbox_device_interface" "test4_3" { device_id = netbox_device.test4.id name = "%[1]s-3" type = "virtual" lifecycle { create_before_destroy = true } } resource "netbox_ip_address" "test_v4_4_3" { ip_address = "1.1.2.103/32" status = "active" interface_id = netbox_device_interface.test4_3.id object_type = "dcim.interface" } resource "netbox_device_primary_ip" "test_v4_4_2" { device_id = netbox_device.test4.id ip_address_id = netbox_ip_address.test_v4_4_3.id ip_address_version = 4 }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_4_2", "device_id", "netbox_device.test4", "id"), resource.TestCheckResourceAttrPair("netbox_device_primary_ip.test_v4_4_2", "ip_address_id", "netbox_ip_address.test_v4_4_3", "id"), resource.TestCheckResourceAttrPair("netbox_device.test4", "primary_ipv4", "netbox_ip_address.test_v4_4_3", "id"), ), }, }, }) } ================================================ FILE: netbox/resource_netbox_device_rear_port.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceNetboxDeviceRearPort() *schema.Resource { return &schema.Resource{ Create: resourceNetboxDeviceRearPortCreate, Read: resourceNetboxDeviceRearPortRead, Update: resourceNetboxDeviceRearPortUpdate, Delete: resourceNetboxDeviceRearPortDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/rearport/): > Like front ports, rear ports are pass-through ports which represent the continuation of a path from one cable to the next. Each rear port is defined with its physical type and a number of positions: Rear ports with more than one position can be mapped to multiple front ports. This can be useful for modeling instances where multiple paths share a common cable (for example, six discrete two-strand fiber connections sharing a 12-strand MPO cable).`, Schema: map[string]*schema.Schema{ "device_id": { Type: schema.TypeInt, Required: true, }, "name": { Type: schema.TypeString, Required: true, }, "type": { Type: schema.TypeString, Required: true, Description: "One of [8p8c, 8p6c, 8p4c, 8p2c, 6p6c, 6p4c, 6p2c, 4p4c, 4p2c, gg45, tera-4p, tera-2p, tera-1p, 110-punch, bnc, f, n, mrj21, fc, lc, lc-pc, lc-upc, lc-apc, lsh, lsh-pc, lsh-upc, lsh-apc, mpo, mtrj, sc, sc-pc, sc-upc, sc-apc, st, cs, sn, sma-905, sma-906, urm-p2, urm-p4, urm-p8, splice, other]", }, "positions": { Type: schema.TypeInt, Required: true, }, "module_id": { Type: schema.TypeInt, Optional: true, }, "label": { Type: schema.TypeString, Optional: true, }, "color_hex": { Type: schema.TypeString, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, "mark_connected": { Type: schema.TypeBool, Default: false, Optional: true, }, tagsKey: tagsSchema, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxDeviceRearPortCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableRearPort{ Device: int64ToPtr(int64(d.Get("device_id").(int))), Name: strToPtr(d.Get("name").(string)), Type: strToPtr(d.Get("type").(string)), Positions: int64(d.Get("positions").(int)), Module: getOptionalInt(d, "module_id"), Label: getOptionalStr(d, "label", false), Color: getOptionalStr(d, "color_hex", false), Description: getOptionalStr(d, "description", false), MarkConnected: d.Get("mark_connected").(bool), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimRearPortsCreateParams().WithData(&data) res, err := api.Dcim.DcimRearPortsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxDeviceRearPortRead(d, m) } func resourceNetboxDeviceRearPortRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimRearPortsReadParams().WithID(id) res, err := api.Dcim.DcimRearPortsRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimRearPortsReadDefault); ok { if errresp.Code() == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } rearPort := res.GetPayload() if rearPort.Device != nil { d.Set("device_id", rearPort.Device.ID) } else { d.Set("device_id", nil) } d.Set("name", rearPort.Name) if rearPort.Type != nil { d.Set("type", rearPort.Type.Value) } else { d.Set("type", nil) } d.Set("positions", rearPort.Positions) if rearPort.Module != nil { d.Set("module_id", rearPort.Module.ID) } else { d.Set("module_id", nil) } d.Set("label", rearPort.Label) d.Set("color_hex", rearPort.Color) d.Set("description", rearPort.Description) d.Set("mark_connected", rearPort.MarkConnected) cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxDeviceRearPortUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableRearPort{ Device: int64ToPtr(int64(d.Get("device_id").(int))), Name: strToPtr(d.Get("name").(string)), Type: strToPtr(d.Get("type").(string)), Positions: int64(d.Get("positions").(int)), Module: getOptionalInt(d, "module_id"), Label: getOptionalStr(d, "label", true), Color: getOptionalStr(d, "color_hex", false), Description: getOptionalStr(d, "description", true), MarkConnected: d.Get("mark_connected").(bool), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimRearPortsPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimRearPortsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxDeviceRearPortRead(d, m) } func resourceNetboxDeviceRearPortDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimRearPortsDeleteParams().WithID(id) _, err := api.Dcim.DcimRearPortsDelete(params, nil) if err != nil { return err } return nil } ================================================ FILE: netbox/resource_netbox_device_rear_port_test.go ================================================ package netbox import ( "fmt" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" log "github.com/sirupsen/logrus" ) func testAccNetboxDeviceRearPortFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_tag" "test" { name = "%[1]sa" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_module_bay" "test" { device_id = netbox_device.test.id name = "%[1]s" } resource "netbox_module_type" "test" { manufacturer_id = netbox_manufacturer.test.id model = "%[1]s" } resource "netbox_module" "test" { device_id = netbox_device.test.id module_bay_id = netbox_device_module_bay.test.id module_type_id = netbox_module_type.test.id status = "active" } `, testName) } func TestAccNetboxDeviceRearPort_basic(t *testing.T) { testSlug := "device_rear_port_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckDeviceRearPortDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxDeviceRearPortFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device_rear_port" "test" { device_id = netbox_device.test.id name = "%[1]s" type = "8p8c" positions = 1 mark_connected = true module_id = netbox_module.test.id label = "%[1]s_label" color_hex = "123456" description = "%[1]s_description" tags = [netbox_tag.test.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_rear_port.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_rear_port.test", "type", "8p8c"), resource.TestCheckResourceAttr("netbox_device_rear_port.test", "positions", "1"), resource.TestCheckResourceAttr("netbox_device_rear_port.test", "mark_connected", "true"), resource.TestCheckResourceAttr("netbox_device_rear_port.test", "label", testName+"_label"), resource.TestCheckResourceAttr("netbox_device_rear_port.test", "color_hex", "123456"), resource.TestCheckResourceAttr("netbox_device_rear_port.test", "description", testName+"_description"), resource.TestCheckResourceAttr("netbox_device_rear_port.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_device_rear_port.test", "tags.0", testName+"a"), resource.TestCheckResourceAttrPair("netbox_device_rear_port.test", "device_id", "netbox_device.test", "id"), resource.TestCheckResourceAttrPair("netbox_device_rear_port.test", "module_id", "netbox_module.test", "id"), ), }, { Config: testAccNetboxDeviceRearPortFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device_rear_port" "test" { device_id = netbox_device.test.id name = "%[1]s" type = "8p6c" positions = 2 }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_rear_port.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_rear_port.test", "type", "8p6c"), resource.TestCheckResourceAttr("netbox_device_rear_port.test", "positions", "2"), resource.TestCheckResourceAttr("netbox_device_rear_port.test", "mark_connected", "false"), resource.TestCheckResourceAttr("netbox_device_rear_port.test", "label", ""), resource.TestCheckResourceAttr("netbox_device_rear_port.test", "color_hex", ""), resource.TestCheckResourceAttr("netbox_device_rear_port.test", "description", ""), resource.TestCheckResourceAttr("netbox_device_rear_port.test", "tags.#", "0"), resource.TestCheckResourceAttr("netbox_device_rear_port.test", "module_id", "0"), resource.TestCheckResourceAttrPair("netbox_device_rear_port.test", "device_id", "netbox_device.test", "id"), ), }, { ResourceName: "netbox_device_rear_port.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckDeviceRearPortDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each rear port // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_device_rear_port" { continue } // Retrieve our device by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := dcim.NewDcimRearPortsReadParams().WithID(stateID) _, err := conn.Dcim.DcimRearPortsRead(params, nil) if err == nil { return fmt.Errorf("device_rear_port (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*dcim.DcimRearPortsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func init() { resource.AddTestSweepers("netbox_device_rear_port", &resource.Sweeper{ Name: "netbox_device_rear_port", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimRearPortsListParams() res, err := api.Dcim.DcimRearPortsList(params, nil) if err != nil { return err } for _, rearPort := range res.GetPayload().Results { if strings.HasPrefix(*rearPort.Name, testPrefix) { deleteParams := dcim.NewDcimRearPortsDeleteParams().WithID(rearPort.ID) _, err := api.Dcim.DcimRearPortsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a device_rear_port") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_device_role.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxDeviceRole() *schema.Resource { return &schema.Resource{ Create: resourceNetboxDeviceRoleCreate, Read: resourceNetboxDeviceRoleRead, Update: resourceNetboxDeviceRoleUpdate, Delete: resourceNetboxDeviceRoleDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/features/devices/#device-roles): > Devices can be organized by functional roles, which are fully customizable by the user. For example, you might create roles for core switches, distribution switches, and access switches within your network.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "vm_role": { Type: schema.TypeBool, Optional: true, Default: true, }, "color_hex": { Type: schema.TypeString, Required: true, }, "description": { Type: schema.TypeString, Optional: true, }, tagsKey: tagsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxDeviceRoleCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } color := d.Get("color_hex").(string) vmRole := d.Get("vm_role").(bool) description := d.Get("description").(string) tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) params := dcim.NewDcimDeviceRolesCreateParams().WithData( &models.DeviceRole{ Name: &name, Slug: &slug, Color: color, Description: description, VMRole: vmRole, Tags: tags, }, ) res, err := api.Dcim.DcimDeviceRolesCreate(params, nil) if err != nil { //return errors.New(getTextFromError(err)) return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxDeviceRoleRead(d, m) } func resourceNetboxDeviceRoleRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimDeviceRolesReadParams().WithID(id) res, err := api.Dcim.DcimDeviceRolesRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimDeviceRolesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } d.Set("name", res.GetPayload().Name) d.Set("slug", res.GetPayload().Slug) d.Set("vm_role", res.GetPayload().VMRole) d.Set("color_hex", res.GetPayload().Color) d.Set("description", res.GetPayload().Description) api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxDeviceRoleUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.DeviceRole{} name := d.Get("name").(string) color := d.Get("color_hex").(string) vmRole := d.Get("vm_role").(bool) description := d.Get("description").(string) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } data.Slug = &slug data.Name = &name data.VMRole = vmRole data.Color = color data.Description = description tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) data.Tags = tags params := dcim.NewDcimDeviceRolesPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Dcim.DcimDeviceRolesPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxDeviceRoleRead(d, m) } func resourceNetboxDeviceRoleDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimDeviceRolesDeleteParams().WithID(id) _, err := api.Dcim.DcimDeviceRolesDelete(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimDeviceRolesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_device_role_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxDeviceRole_basic(t *testing.T) { testSlug := "dvcrl_basic" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_device_role" "test" { name = "%s" slug = "%s" color_hex = "111111" description = "Some fancy device role" }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_role.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_role.test", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_device_role.test", "color_hex", "111111"), resource.TestCheckResourceAttr("netbox_device_role.test", "description", "Some fancy device role"), ), }, { ResourceName: "netbox_device_role.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxDeviceRole_defaultSlug(t *testing.T) { testSlug := "device_role_defSlug" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_device_role" "test" { name = "%s" color_hex = "111111" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_role.test", "name", testName), resource.TestCheckResourceAttr("netbox_device_role.test", "slug", getSlug(testName)), ), }, }, }) } func init() { resource.AddTestSweepers("netbox_device_role", &resource.Sweeper{ Name: "netbox_device_role", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimDeviceRolesListParams() res, err := api.Dcim.DcimDeviceRolesList(params, nil) if err != nil { return err } for _, deviceRole := range res.GetPayload().Results { if strings.HasPrefix(*deviceRole.Name, testPrefix) { deleteParams := dcim.NewDcimDeviceRolesDeleteParams().WithID(deviceRole.ID) _, err := api.Dcim.DcimDeviceRolesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a device_role") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_device_test.go ================================================ package netbox import ( "fmt" "log" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func testAccNetboxDeviceFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_platform" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_cluster_type" "test" { name = "%[1]s" } resource "netbox_cluster" "test" { name = "%[1]s" cluster_type_id = netbox_cluster_type.test.id site_id = netbox_site.test.id } resource "netbox_location" "test" { name = "%[1]s" site_id =netbox_site.test.id } resource "netbox_rack_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_rack" "test" { name = "%[1]s" site_id = netbox_site.test.id status = "reserved" width = 19 u_height = 48 tenant_id = netbox_tenant.test.id location_id = netbox_location.test.id } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_tag" "test_a" { name = "%[1]sa" } resource "netbox_tag" "test_b" { name = "%[1]sb" } resource "netbox_tag" "test_c" { name = "%[1]sc" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_config_template" "test" { name = "%[1]s" template_code = "hostname {{ name }}" }`, testName) } func TestAccNetboxDevice_basic(t *testing.T) { testSlug := "device_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckDeviceDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxDeviceFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device" "test" { name = "%[1]s" asset_tag = "TAGGEDITAGGEDITAG" comments = "thisisacomment" description = "thisisadescription" tenant_id = netbox_tenant.test.id platform_id = netbox_platform.test.id role_id = netbox_device_role.test.id device_type_id = netbox_device_type.test.id tags = [netbox_tag.test_a.name] site_id = netbox_site.test.id cluster_id = netbox_cluster.test.id location_id = netbox_location.test.id config_template_id = netbox_config_template.test.id status = "staged" serial = "ABCDEF" rack_id = netbox_rack.test.id rack_face = "front" rack_position = 10 local_context_data = jsonencode({"context_string"="context_value"}) }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_device.test", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "platform_id", "netbox_platform.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "location_id", "netbox_location.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "role_id", "netbox_device_role.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "cluster_id", "netbox_cluster.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "rack_id", "netbox_rack.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "config_template_id", "netbox_config_template.test", "id"), resource.TestCheckResourceAttr("netbox_device.test", "asset_tag", "TAGGEDITAGGEDITAG"), resource.TestCheckResourceAttr("netbox_device.test", "comments", "thisisacomment"), resource.TestCheckResourceAttr("netbox_device.test", "description", "thisisadescription"), resource.TestCheckResourceAttr("netbox_device.test", "status", "staged"), resource.TestCheckResourceAttr("netbox_device.test", "serial", "ABCDEF"), resource.TestCheckResourceAttr("netbox_device.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_device.test", "tags.0", testName+"a"), resource.TestCheckResourceAttr("netbox_device.test", "rack_face", "front"), resource.TestCheckResourceAttr("netbox_device.test", "rack_position", "10"), resource.TestCheckResourceAttr("netbox_device.test", "local_context_data", "{\"context_string\":\"context_value\"}"), ), }, { Config: testAccNetboxDeviceFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device" "test" { name = "%[1]s" asset_tag = "TAGGEDITAGGEDITAG_TAGGEDITAGGEDITAGGEDITAG" comments = "thisisacomment" description = "thisisadescription" tenant_id = netbox_tenant.test.id platform_id = netbox_platform.test.id role_id = netbox_device_role.test.id device_type_id = netbox_device_type.test.id tags = [netbox_tag.test_a.name] site_id = netbox_site.test.id cluster_id = netbox_cluster.test.id location_id = netbox_location.test.id config_template_id = netbox_config_template.test.id rack_id = netbox_rack.test.id status = "staged" serial = "ABCDEF" local_context_data = jsonencode({"context_string"="context_value2"}) }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_device.test", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "platform_id", "netbox_platform.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "location_id", "netbox_location.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "role_id", "netbox_device_role.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "cluster_id", "netbox_cluster.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "rack_id", "netbox_rack.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "config_template_id", "netbox_config_template.test", "id"), resource.TestCheckResourceAttr("netbox_device.test", "asset_tag", "TAGGEDITAGGEDITAG_TAGGEDITAGGEDITAGGEDITAG"), resource.TestCheckResourceAttr("netbox_device.test", "comments", "thisisacomment"), resource.TestCheckResourceAttr("netbox_device.test", "description", "thisisadescription"), resource.TestCheckResourceAttr("netbox_device.test", "status", "staged"), resource.TestCheckResourceAttr("netbox_device.test", "serial", "ABCDEF"), resource.TestCheckResourceAttr("netbox_device.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_device.test", "tags.0", testName+"a"), resource.TestCheckResourceAttr("netbox_device.test", "rack_face", ""), resource.TestCheckResourceAttr("netbox_device.test", "rack_position", "0"), resource.TestCheckResourceAttr("netbox_device.test", "local_context_data", "{\"context_string\":\"context_value2\"}"), ), }, { Config: testAccNetboxDeviceFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device" "test" { name = "%[1]s" asset_tag = "TAGGEDITAGGEDITAG" comments = "thisisacomment" description = "thisisadescription" tenant_id = netbox_tenant.test.id platform_id = netbox_platform.test.id role_id = netbox_device_role.test.id device_type_id = netbox_device_type.test.id tags = [netbox_tag.test_a.name] site_id = netbox_site.test.id cluster_id = netbox_cluster.test.id location_id = netbox_location.test.id config_template_id = netbox_config_template.test.id status = "staged" serial = "ABCDEF" local_context_data = jsonencode({"context_string"="context_value"}) }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_device.test", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "platform_id", "netbox_platform.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "location_id", "netbox_location.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "role_id", "netbox_device_role.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "cluster_id", "netbox_cluster.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "config_template_id", "netbox_config_template.test", "id"), resource.TestCheckResourceAttr("netbox_device.test", "asset_tag", "TAGGEDITAGGEDITAG"), resource.TestCheckResourceAttr("netbox_device.test", "comments", "thisisacomment"), resource.TestCheckResourceAttr("netbox_device.test", "description", "thisisadescription"), resource.TestCheckResourceAttr("netbox_device.test", "status", "staged"), resource.TestCheckResourceAttr("netbox_device.test", "serial", "ABCDEF"), resource.TestCheckResourceAttr("netbox_device.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_device.test", "tags.0", testName+"a"), resource.TestCheckResourceAttr("netbox_device.test", "rack_id", "0"), resource.TestCheckResourceAttr("netbox_device.test", "rack_face", ""), resource.TestCheckResourceAttr("netbox_device.test", "rack_position", "0"), resource.TestCheckResourceAttr("netbox_device.test", "local_context_data", "{\"context_string\":\"context_value\"}"), ), }, { Config: testAccNetboxDeviceFullDependencies(testName) + fmt.Sprintf(` resource "netbox_device" "test" { name = "%[1]s" role_id = netbox_device_role.test.id device_type_id = netbox_device_type.test.id site_id = netbox_site.test.id cluster_id = netbox_cluster.test.id platform_id = netbox_platform.test.id local_context_data = jsonencode({"context_string"="context_value"}) }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_device.test", "role_id", "netbox_device_role.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "platform_id", "netbox_platform.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "cluster_id", "netbox_cluster.test", "id"), resource.TestCheckResourceAttrPair("netbox_device.test", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttr("netbox_device.test", "local_context_data", "{\"context_string\":\"context_value\"}"), ), }, { ResourceName: "netbox_device.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxDevice_virtual_chassis(t *testing.T) { testSlug := "device_virtual_chassis" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckDeviceDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxDeviceFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_chassis" "test" { name = "%[1]s" tags = [netbox_tag.test_a.name] } resource "netbox_device" "test" { name = "%[1]s" role_id = netbox_device_role.test.id device_type_id = netbox_device_type.test.id site_id = netbox_site.test.id platform_id = netbox_platform.test.id virtual_chassis_id = netbox_virtual_chassis.test.id virtual_chassis_position = 1 virtual_chassis_master = true }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_virtual_chassis.test", "id", "netbox_device.test", "virtual_chassis_id"), resource.TestCheckResourceAttr("netbox_device.test", "virtual_chassis_master", "true"), resource.TestCheckResourceAttr("netbox_device.test", "virtual_chassis_position", "1"), ), }, { Config: testAccNetboxDeviceFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_chassis" "test" { name = "%[1]s" tags = [netbox_tag.test_a.name] } resource "netbox_device" "test" { name = "%[1]s" role_id = netbox_device_role.test.id device_type_id = netbox_device_type.test.id site_id = netbox_site.test.id platform_id = netbox_platform.test.id virtual_chassis_id = netbox_virtual_chassis.test.id virtual_chassis_position = 1 virtual_chassis_master = false }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device.test", "virtual_chassis_master", "false"), ), }, { Config: testAccNetboxDeviceFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_chassis" "test" { name = "%[1]s" tags = [netbox_tag.test_a.name] } resource "netbox_device" "test" { name = "%[1]s" role_id = netbox_device_role.test.id device_type_id = netbox_device_type.test.id site_id = netbox_site.test.id platform_id = netbox_platform.test.id }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device.test", "virtual_chassis_id", "0"), ), }, { Config: testAccNetboxDeviceFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_chassis" "test" { name = "%[1]s" tags = [netbox_tag.test_a.name] } resource "netbox_device" "test" { name = "%[1]s" role_id = netbox_device_role.test.id device_type_id = netbox_device_type.test.id site_id = netbox_site.test.id platform_id = netbox_platform.test.id virtual_chassis_id = netbox_virtual_chassis.test.id virtual_chassis_position = 1 virtual_chassis_master = true }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_device.test", "virtual_chassis_id", "netbox_virtual_chassis.test", "id"), ), }, { ResourceName: "netbox_device.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckDeviceDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each device // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_device" { continue } // Retrieve our device by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := dcim.NewDcimDevicesReadParams().WithID(stateID) _, err := conn.Dcim.DcimDevicesRead(params, nil) if err == nil { return fmt.Errorf("device (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*dcim.DcimDevicesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func init() { resource.AddTestSweepers("netbox_device", &resource.Sweeper{ Name: "netbox_device", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimDevicesListParams() res, err := api.Dcim.DcimDevicesList(params, nil) if err != nil { return err } for _, Device := range res.GetPayload().Results { if strings.HasPrefix(*Device.Name, testPrefix) { deleteParams := dcim.NewDcimDevicesDeleteParams().WithID(Device.ID) _, err := api.Dcim.DcimDevicesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a device") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_device_type.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxDeviceType() *schema.Resource { return &schema.Resource{ Create: resourceNetboxDeviceTypeCreate, Read: resourceNetboxDeviceTypeRead, Update: resourceNetboxDeviceTypeUpdate, Delete: resourceNetboxDeviceTypeDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/features/device-types/#device-types_1): > A device type represents a particular make and model of hardware that exists in the real world. Device types define the physical attributes of a device (rack height and depth) and its individual components (console, power, network interfaces, and so on).`, Schema: map[string]*schema.Schema{ "model": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "manufacturer_id": { Type: schema.TypeInt, Required: true, }, "part_number": { Type: schema.TypeString, Optional: true, }, "u_height": { Type: schema.TypeFloat, Optional: true, Default: "1.0", }, "is_full_depth": { Type: schema.TypeBool, Optional: true, }, "subdevice_role": { Type: schema.TypeString, Optional: true, }, tagsKey: tagsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxDeviceTypeCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableDeviceType{} model := d.Get("model").(string) data.Model = &model slugValue, slugOk := d.GetOk("slug") // Default slug to generated slug if not given if !slugOk { data.Slug = strToPtr(getSlug(model)) } else { data.Slug = strToPtr(slugValue.(string)) } manufacturerIDValue, ok := d.GetOk("manufacturer_id") if ok { data.Manufacturer = int64ToPtr(int64(manufacturerIDValue.(int))) } if partNo, ok := d.GetOk("part_number"); ok { data.PartNumber = partNo.(string) } //Needed to account for 0 u_height values uHeightValue := d.Get("u_height") data.UHeight = float64ToPtr(uHeightValue.(float64)) if isFullDepthValue, ok := d.GetOk("is_full_depth"); ok { data.IsFullDepth = isFullDepthValue.(bool) } if subdeviceRoleValue, ok := d.GetOk("subdevice_role"); ok { data.SubdeviceRole = subdeviceRoleValue.(string) } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } params := dcim.NewDcimDeviceTypesCreateParams().WithData(&data) res, err := api.Dcim.DcimDeviceTypesCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxDeviceTypeRead(d, m) } func resourceNetboxDeviceTypeRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimDeviceTypesReadParams().WithID(id) res, err := api.Dcim.DcimDeviceTypesRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimDeviceTypesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } deviceType := res.GetPayload() d.Set("model", deviceType.Model) d.Set("slug", deviceType.Slug) d.Set("manufacturer_id", deviceType.Manufacturer.ID) d.Set("part_number", deviceType.PartNumber) d.Set("u_height", deviceType.UHeight) d.Set("is_full_depth", deviceType.IsFullDepth) if deviceType.SubdeviceRole != nil && deviceType.SubdeviceRole.Value != nil { d.Set("subdevice_role", *deviceType.SubdeviceRole.Value) } else { d.Set("subdevice_role", "") } api.readTags(d, deviceType.Tags) return nil } func resourceNetboxDeviceTypeUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableDeviceType{} model := d.Get("model").(string) data.Model = &model slugValue, slugOk := d.GetOk("slug") // Default slug to generated slug if not given if !slugOk { data.Slug = strToPtr(getSlug(model)) } else { data.Slug = strToPtr(slugValue.(string)) } manufacturerIDValue, ok := d.GetOk("manufacturer_id") if ok { data.Manufacturer = int64ToPtr(int64(manufacturerIDValue.(int))) } if partNo, ok := d.GetOk("part_number"); ok { data.PartNumber = partNo.(string) } uHeightValue := d.Get("u_height") data.UHeight = float64ToPtr(uHeightValue.(float64)) if isFullDepthValue, ok := d.GetOk("is_full_depth"); ok { data.IsFullDepth = isFullDepthValue.(bool) } if subdeviceRoleValue, ok := d.GetOk("subdevice_role"); ok { data.SubdeviceRole = subdeviceRoleValue.(string) } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } params := dcim.NewDcimDeviceTypesPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimDeviceTypesPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxDeviceTypeRead(d, m) } func resourceNetboxDeviceTypeDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimDeviceTypesDeleteParams().WithID(id) _, err := api.Dcim.DcimDeviceTypesDelete(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimDeviceTypesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_device_type_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxDeviceType_basic(t *testing.T) { testSlug := "device_type" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" slug = "%[2]s" part_number = "%[2]s" u_height = "0.5" manufacturer_id = netbox_manufacturer.test.id is_full_depth = true subdevice_role = "parent" }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_type.test", "model", testName), resource.TestCheckResourceAttr("netbox_device_type.test", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_device_type.test", "part_number", randomSlug), resource.TestCheckResourceAttr("netbox_device_type.test", "u_height", "0.5"), resource.TestCheckResourceAttrPair("netbox_device_type.test", "manufacturer_id", "netbox_manufacturer.test", "id"), resource.TestCheckResourceAttr("netbox_device_type.test", "is_full_depth", "true"), resource.TestCheckResourceAttr("netbox_device_type.test", "subdevice_role", "parent"), ), }, { Config: fmt.Sprintf(` resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" slug = "%[2]s" part_number = "%[2]s" u_height = "0" manufacturer_id = netbox_manufacturer.test.id is_full_depth = false subdevice_role = "child" }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_type.test", "model", testName), resource.TestCheckResourceAttr("netbox_device_type.test", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_device_type.test", "part_number", randomSlug), resource.TestCheckResourceAttr("netbox_device_type.test", "u_height", "0"), resource.TestCheckResourceAttrPair("netbox_device_type.test", "manufacturer_id", "netbox_manufacturer.test", "id"), resource.TestCheckResourceAttr("netbox_device_type.test", "is_full_depth", "false"), resource.TestCheckResourceAttr("netbox_device_type.test", "subdevice_role", "child"), ), }, { ResourceName: "netbox_device_type.test", ImportState: true, ImportStateVerify: true, }, }, }) } func init() { resource.AddTestSweepers("netbox_device_type", &resource.Sweeper{ Name: "netbox_device_type", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimDeviceTypesListParams() res, err := api.Dcim.DcimDeviceTypesList(params, nil) if err != nil { return err } for _, devicetype := range res.GetPayload().Results { if strings.HasPrefix(*devicetype.Model, testPrefix) { deleteParams := dcim.NewDcimDeviceTypesDeleteParams().WithID(devicetype.ID) _, err := api.Dcim.DcimDeviceTypesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a device type") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_event_rule.go ================================================ package netbox import ( "encoding/json" "strconv" "github.com/fbreckle/go-netbox/netbox/client/extras" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var resourceNetboxEventRuleActionTypeOptions = []string{"webhook", "script"} var resourceNetboxEventRuleActionTypeToObjectType = map[string]string{ "webhook": "extras.webhook", "script": "extras.script", } func resourceNetboxEventRule() *schema.Resource { return &schema.Resource{ Create: resourceNetboxEventRuleCreate, Read: resourceNetboxEventRuleRead, Update: resourceNetboxEventRuleUpdate, Delete: resourceNetboxEventRuleDelete, Description: `:meta:subcategory:Extras:From the [official documentation](https://docs.netbox.dev/en/stable/features/event-rules/): > NetBox can be configured via Event Rules to transmit outgoing webhooks to remote systems in response to internal object changes. The receiver can act on the data in these webhook messages to perform related tasks. Event rules can also execute custom scripts, enabling NetBox to run defined logic locally in response to object changes.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "description": { Type: schema.TypeString, Optional: true, }, "content_types": { Type: schema.TypeSet, Required: true, Elem: &schema.Schema{Type: schema.TypeString}, }, "event_types": { Type: schema.TypeSet, Required: true, Elem: &schema.Schema{ // We do not enforce event-type validation, because plugins might extend the list of valid events Type: schema.TypeString, }, Description: "The types of event which will trigger this rule. By default, valid values are `object_created`, `oject_updated`, `object_deleted`, `job_started`, `job_completed`, `job_failed` and `job_errored`", }, "enabled": { Type: schema.TypeBool, Optional: true, Default: true, }, "conditions": { Type: schema.TypeString, Optional: true, DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool { equal, _ := jsonSemanticCompare(oldValue, newValue) return equal }, DiffSuppressOnRefresh: true, }, "action_type": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringInSlice(resourceNetboxEventRuleActionTypeOptions, false), Description: buildValidValueDescription(resourceNetboxEventRuleActionTypeOptions), }, "action_object_id": { Type: schema.TypeInt, Required: true, }, tagsKey: tagsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxEventRuleCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := &models.WritableEventRule{} name := d.Get("name").(string) data.Name = &name actionType := d.Get("action_type").(string) data.ActionType = actionType data.Description = getOptionalStr(d, "description", false) if objectType, ok := resourceNetboxEventRuleActionTypeToObjectType[actionType]; ok { data.ActionObjectType = strToPtr(objectType) } eventTypes := make([]string, 0) for _, eventType := range d.Get("event_types").(*schema.Set).List() { eventTypes = append(eventTypes, eventType.(string)) } data.EventTypes = eventTypes enabled := d.Get("enabled").(bool) data.Enabled = enabled data.ActionObjectID = getOptionalInt(d, "action_object_id") tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) data.Tags = tags ctypes := d.Get("content_types").(*schema.Set).List() objectTypes := make([]string, 0, len(ctypes)) for _, contentType := range d.Get("content_types").(*schema.Set).List() { objectTypes = append(objectTypes, contentType.(string)) } data.ObjectTypes = objectTypes if conditionsData, ok := d.GetOk("conditions"); ok { var conditions any err := json.Unmarshal([]byte(conditionsData.(string)), &conditions) if err != nil { return err } data.Conditions = conditions } params := extras.NewExtrasEventRulesCreateParams().WithData(data) res, err := api.Extras.ExtrasEventRulesCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxEventRuleRead(d, m) } func resourceNetboxEventRuleRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := extras.NewExtrasEventRulesReadParams().WithID(id) res, err := api.Extras.ExtrasEventRulesRead(params, nil) if err != nil { if errresp, ok := err.(*extras.ExtrasEventRulesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { d.SetId("") return nil } } return err } eventRule := res.GetPayload() d.Set("name", eventRule.Name) d.Set("description", eventRule.Description) d.Set("action_type", eventRule.ActionType.Value) d.Set("content_types", eventRule.ObjectTypes) d.Set("event_types", eventRule.EventTypes) d.Set("enabled", eventRule.Enabled) d.Set("action_object_id", eventRule.ActionObjectID) if eventRule.Conditions != nil { conditions, err := json.Marshal(eventRule.Conditions) if err != nil { return err } d.Set("conditions", string(conditions)) } api.readTags(d, eventRule.Tags) return nil } func resourceNetboxEventRuleUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableEventRule{} name := d.Get("name").(string) data.Name = &name actionType := d.Get("action_type").(string) data.ActionType = actionType data.Description = getOptionalStr(d, "description", true) if objectType, ok := resourceNetboxEventRuleActionTypeToObjectType[actionType]; ok { data.ActionObjectType = strToPtr(objectType) } eventTypes := make([]string, 0) for _, eventType := range d.Get("event_types").(*schema.Set).List() { eventTypes = append(eventTypes, eventType.(string)) } data.EventTypes = eventTypes enabled := d.Get("enabled").(bool) data.Enabled = enabled data.ActionObjectID = getOptionalInt(d, "action_object_id") if conditionsData, ok := d.GetOk("conditions"); ok { var conditions any err := json.Unmarshal([]byte(conditionsData.(string)), &conditions) if err != nil { return err } data.Conditions = conditions } tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) data.Tags = tags ctypes := d.Get("content_types").(*schema.Set).List() objectTypes := make([]string, 0, len(ctypes)) for _, contentType := range d.Get("content_types").(*schema.Set).List() { objectTypes = append(objectTypes, contentType.(string)) } data.ObjectTypes = objectTypes params := extras.NewExtrasEventRulesUpdateParams().WithID(id).WithData(&data) _, err := api.Extras.ExtrasEventRulesUpdate(params, nil) if err != nil { return err } return resourceNetboxEventRuleRead(d, m) } func resourceNetboxEventRuleDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := extras.NewExtrasEventRulesDeleteParams().WithID(id) _, err := api.Extras.ExtrasEventRulesDelete(params, nil) if err != nil { if errresp, ok := err.(*extras.ExtrasEventRulesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_event_rule_test.go ================================================ package netbox import ( "fmt" "log" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/extras" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccNetboxEventRule_basic(t *testing.T) { testName := testAccGetTestName("evt_rule_basic") resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, CheckDestroy: testAccCheckNetBoxEventRuleDestroy, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_webhook" "test" { name = "%[1]s" payload_url = "https://example.com/webhook" } resource "netbox_event_rule" "test" { name = "%[1]s" description = "foo description" content_types = ["dcim.site"] action_type = "webhook" action_object_id = netbox_webhook.test.id event_types = ["object_created", "object_updated", "object_deleted", "job_started", "job_completed", "job_failed", "job_errored"] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_event_rule.test", "name", testName), resource.TestCheckResourceAttr("netbox_event_rule.test", "content_types.#", "1"), resource.TestCheckResourceAttr("netbox_event_rule.test", "content_types.0", "dcim.site"), resource.TestCheckResourceAttr("netbox_event_rule.test", "action_type", "webhook"), resource.TestCheckResourceAttr("netbox_event_rule.test", "description", "foo description"), resource.TestCheckResourceAttr("netbox_event_rule.test", "event_types.#", "7"), ), }, { Config: fmt.Sprintf(` resource "netbox_webhook" "test" { name = "%[1]s" payload_url = "https://example.com/webhook" } resource "netbox_event_rule" "test" { name = "%[1]s" content_types = ["dcim.site", "virtualization.cluster"] action_type = "webhook" action_object_id = netbox_webhook.test.id event_types = ["object_created"] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_event_rule.test", "name", testName), resource.TestCheckResourceAttr("netbox_event_rule.test", "content_types.#", "2"), resource.TestCheckResourceAttr("netbox_event_rule.test", "content_types.0", "dcim.site"), resource.TestCheckResourceAttr("netbox_event_rule.test", "content_types.1", "virtualization.cluster"), resource.TestCheckResourceAttr("netbox_event_rule.test", "action_type", "webhook"), resource.TestCheckResourceAttr("netbox_event_rule.test", "event_types.#", "1"), ), }, { ResourceName: "netbox_event_rule.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckNetBoxEventRuleDestroy(s *terraform.State) error { client := testAccProvider.Meta().(*providerState) for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_event_rule" { continue } // Fetch the eventRule by ID // Retrieve our interface by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) eventRule, err := client.Extras.ExtrasEventRulesRead(extras.NewExtrasEventRulesReadParams().WithID(stateID), nil) if err == nil && eventRule != nil { return fmt.Errorf("EventRule %s still exists", rs.Primary.ID) } } return nil } func init() { resource.AddTestSweepers("netbox_event_rule", &resource.Sweeper{ Name: "netbox_event_rule", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := extras.NewExtrasEventRulesListParams() res, err := api.Extras.ExtrasEventRulesList(params, nil) if err != nil { return err } for _, eventRule := range res.GetPayload().Results { if strings.HasPrefix(*eventRule.Name, testPrefix) { deleteParams := extras.NewExtrasEventRulesDeleteParams().WithID(eventRule.ID) _, err := api.Extras.ExtrasEventRulesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a eventRule") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_group.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/users" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceNetboxGroup() *schema.Resource { return &schema.Resource{ Create: resourceNetboxGroupCreate, Read: resourceNetboxGroupRead, Update: resourceNetboxGroupUpdate, Delete: resourceNetboxGroupDelete, Description: `:meta:subcategory:Authentication:This resource is used to manage groups.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "description": { Type: schema.TypeString, Optional: true, Default: "", }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxGroupCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.Group{} name := d.Get("name").(string) description := d.Get("description").(string) data.Name = &name data.Description = description params := users.NewUsersGroupsCreateParams().WithData(&data) res, err := api.Users.UsersGroupsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxGroupRead(d, m) } func resourceNetboxGroupRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := users.NewUsersGroupsReadParams().WithID(id) res, err := api.Users.UsersGroupsRead(params, nil) if err != nil { if errresp, ok := err.(*users.UsersGroupsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } if res.GetPayload().Name != nil { d.Set("name", res.GetPayload().Name) } d.Set("description", res.GetPayload().Description) return nil } func resourceNetboxGroupUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.Group{} name := d.Get("name").(string) description := d.Get("description").(string) data.Name = &name data.Description = description params := users.NewUsersGroupsUpdateParams().WithID(id).WithData(&data) _, err := api.Users.UsersGroupsUpdate(params, nil) if err != nil { return err } return resourceNetboxGroupRead(d, m) } func resourceNetboxGroupDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := users.NewUsersGroupsDeleteParams().WithID(id) _, err := api.Users.UsersGroupsDelete(params, nil) if err != nil { if errresp, ok := err.(*users.UsersGroupsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } d.SetId("") return nil } ================================================ FILE: netbox/resource_netbox_group_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/users" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxGroup_basic(t *testing.T) { testSlug := "groups" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_group" "test_basic" { name = "%s" description = "This is my example resource" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_group.test_basic", "name", testName), resource.TestCheckResourceAttr("netbox_group.test_basic", "description", "This is my example resource"), ), }, { ResourceName: "netbox_group.test_basic", ImportState: true, ImportStateVerify: false, }, }, }) } func init() { resource.AddTestSweepers("netbox_group", &resource.Sweeper{ Name: "netbox_group", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := users.NewUsersGroupsListParams() res, err := api.Users.UsersGroupsList(params, nil) if err != nil { return err } for _, group := range res.GetPayload().Results { if strings.HasPrefix(*group.Name, testPrefix) { deleteParams := users.NewUsersGroupsDeleteParams().WithID(group.ID) _, err := api.Users.UsersGroupsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a group") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_interface.go ================================================ package netbox import ( "context" "strconv" "strings" "github.com/fbreckle/go-netbox/netbox/client/virtualization" "github.com/fbreckle/go-netbox/netbox/models" "github.com/go-openapi/runtime" "github.com/go-openapi/strfmt" "github.com/go-viper/mapstructure/v2" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var resourceNetboxInterfaceModeOptions = []string{"access", "tagged", "tagged-all"} func resourceNetboxInterface() *schema.Resource { return &schema.Resource{ CreateContext: resourceNetboxInterfaceCreate, ReadContext: resourceNetboxInterfaceRead, UpdateContext: resourceNetboxInterfaceUpdate, DeleteContext: resourceNetboxInterfaceDelete, Description: `:meta:subcategory:Virtualization:From the [official documentation](https://docs.netbox.dev/en/stable/features/virtualization/#interfaces): > Virtual machine interfaces behave similarly to device interfaces, and can be assigned to VRFs, and may have IP addresses, VLANs, and services attached to them. However, given their virtual nature, they lack properties pertaining to physical attributes. For example, VM interfaces do not have a physical type and cannot have cables attached to them.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "virtual_machine_id": { Type: schema.TypeInt, Required: true, }, "description": { Type: schema.TypeString, Optional: true, }, "enabled": { Type: schema.TypeBool, Optional: true, Default: true, }, "mac_address": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.IsMACAddress, // Netbox converts MAC addresses always to uppercase DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { return strings.EqualFold(old, new) }, }, "mode": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringInSlice(resourceNetboxInterfaceModeOptions, false), Description: buildValidValueDescription(resourceNetboxInterfaceModeOptions), }, "mtu": { Type: schema.TypeInt, Optional: true, ValidateFunc: validation.IntBetween(1, 65536), }, "type": { Type: schema.TypeString, Optional: true, Deprecated: "This attribute is not supported by netbox any longer. It will be removed in future versions of this provider.", }, tagsKey: tagsSchema, "tagged_vlans": { Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "untagged_vlan": { Type: schema.TypeInt, Optional: true, }, "bridge_interface_id": { Type: schema.TypeInt, Optional: true, Description: "ID of the bridge interface this interface belongs to", }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxInterfaceCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) var diags diag.Diagnostics name := d.Get("name").(string) description := d.Get("description").(string) enabled := d.Get("enabled").(bool) mode := d.Get("mode").(string) tags, err := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return diag.FromErr(err) } taggedVlans := toInt64List(d.Get("tagged_vlans")) virtualMachineID := int64(d.Get("virtual_machine_id").(int)) data := models.WritableVMInterface{ Name: &name, Description: description, Enabled: enabled, Mode: mode, Tags: tags, TaggedVlans: taggedVlans, VirtualMachine: &virtualMachineID, } if macAddress := d.Get("mac_address").(string); macAddress != "" { data.MacAddress = &macAddress } if mtu, ok := d.Get("mtu").(int); ok && mtu != 0 { data.Mtu = int64ToPtr(int64(mtu)) } if untaggedVlan, ok := d.Get("untagged_vlan").(int); ok && untaggedVlan != 0 { data.UntaggedVlan = int64ToPtr(int64(untaggedVlan)) } if bridgeIF, ok := d.Get("bridge_interface_id").(int); ok && bridgeIF != 0 { data.Bridge = int64ToPtr(int64(bridgeIF)) } params := virtualization.NewVirtualizationInterfacesCreateParams().WithData(&data) res, err := api.Virtualization.VirtualizationInterfacesCreate(params, nil) if err != nil { return diag.FromErr(err) } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return diags } func resourceNetboxInterfaceRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) var diags diag.Diagnostics params := virtualization.NewVirtualizationInterfacesReadParams().WithID(id) res, err := api.Virtualization.VirtualizationInterfacesRead(params, nil) if err != nil { if errresp, ok := err.(*virtualization.VirtualizationInterfacesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return diag.FromErr(err) } iface := res.GetPayload() d.Set("name", iface.Name) d.Set("description", iface.Description) d.Set("enabled", iface.Enabled) d.Set("mac_address", iface.MacAddress) d.Set("mtu", iface.Mtu) api.readTags(d, iface.Tags) d.Set("tagged_vlans", getIDsFromNestedVLAN(iface.TaggedVlans)) d.Set("virtual_machine_id", iface.VirtualMachine.ID) if iface.Mode != nil { d.Set("mode", iface.Mode.Value) } if iface.UntaggedVlan != nil { d.Set("untagged_vlan", iface.UntaggedVlan.ID) } if iface.Bridge != nil { d.Set("bridge_interface_id", iface.Bridge.ID) } else { d.Set("bridge_interface_id", nil) } return diags } func resourceNetboxInterfaceUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) name := d.Get("name").(string) description := d.Get("description").(string) enabled := d.Get("enabled").(bool) mode := d.Get("mode").(string) tags, err := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return diag.FromErr(err) } taggedVlans := toInt64List(d.Get("tagged_vlans")) virtualMachineID := int64(d.Get("virtual_machine_id").(int)) data := models.WritableVMInterface{ Name: &name, Description: description, Enabled: enabled, Mode: mode, Tags: tags, TaggedVlans: taggedVlans, VirtualMachine: &virtualMachineID, } if d.HasChange("mac_address") { macAddress := d.Get("mac_address").(string) data.MacAddress = &macAddress } if d.HasChange("mtu") { mtu := int64(d.Get("mtu").(int)) data.Mtu = &mtu } if d.HasChange("untagged_vlan") { untaggedvlan := int64(d.Get("untagged_vlan").(int)) data.UntaggedVlan = &untaggedvlan } // About the `nullFields` hack // // Without this hack, the `bridge_interface_id` field can never be unset // in the Netbox API once it has been set. This likely also applies to other // fields, but for now, this is only solved for this field. It can be extended // though. // // Why is it needed? // // This method uses VirtualizationInterfacesPartialUpdate which takes similar // parameters as Create/Update, but will only apply changes to fields that are // supplied in the JSON payload. // // Example: // - `{ "bridge": 123 }` - will set the field // - `{ "bridge": null }` - will unset the field // - `{}` - will make no changes // // Unfortunately with how the Netbox API client is generated, there is no way // to produce the second example. If you set the field `Bridge` to `nil` it // will produce JSON without the field present. This is because of the // `omitempty` struct tag on the field. Which is something you need to make // partial updates work, but prevents you from unsetting the field. // There is simply no way to represent this case in Go without introducing // a special wrapper type that can represent all 3 states. // (Note that sending `"bridge": 0` is not accepted by the API.) // // The way this is solved is on the serialization level. // With `hackSerializeAsNull` I made a serialization middleware of sorts that // will serialize an explicit `null` value for field names given in the slice. var nullFields []string if d.HasChange("bridge_interface_id") { ifID := int64(d.Get("bridge_interface_id").(int)) data.Bridge = &ifID if ifID == 0 { nullFields = append(nullFields, "bridge") } } params := virtualization.NewVirtualizationInterfacesPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Virtualization.VirtualizationInterfacesPartialUpdate(params, nil, hackSerializeAsNull(nullFields...)) if err != nil { return diag.FromErr(err) } return resourceNetboxInterfaceRead(ctx, d, m) } func resourceNetboxInterfaceDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := virtualization.NewVirtualizationInterfacesDeleteParams().WithID(id) _, err := api.Virtualization.VirtualizationInterfacesDelete(params, nil) if err != nil { if errresp, ok := err.(*virtualization.VirtualizationInterfacesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return diag.FromErr(err) } return nil } func getIDsFromNestedVLAN(nestedvlans []*models.NestedVLAN) []int64 { var vlans []int64 for _, vlan := range nestedvlans { vlans = append(vlans, vlan.ID) } return vlans } type interceptWriter struct { runtime.ClientRequest fields []string } func (iw interceptWriter) SetBodyParam(p any) error { out := make(map[string]any) dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ TagName: "json", Result: &out, }) if err != nil { return err } if err := dec.Decode(p); err != nil { return err } for _, fieldName := range iw.fields { _, ok := out[fieldName] if ok { out[fieldName] = nil } } return iw.ClientRequest.SetBodyParam(out) } type interceptParams struct { inner runtime.ClientRequestWriter fields []string } // WriteToRequest implements [runtime.ClientRequestWriter]. func (ip interceptParams) WriteToRequest(req runtime.ClientRequest, reg strfmt.Registry) error { writer := interceptWriter{ClientRequest: req, fields: ip.fields} return ip.inner.WriteToRequest(writer, reg) } // hackSerializeAsNull is a serialization middleware of sorts that will // serialize an explicit `null` value for all field names given in the slice. // // It does this by first serializing the params from a struct to a map // and then sets the value for given fields to `nil`. // With this the field will definitely be serialized and not omitted when set // to `nil`. func hackSerializeAsNull(fields ...string) virtualization.ClientOption { return func(co *runtime.ClientOperation) { originalParams := co.Params co.Params = interceptParams{inner: originalParams, fields: fields} } } ================================================ FILE: netbox/resource_netbox_interface_template.go ================================================ package netbox import ( "context" "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxInterfaceTemplate() *schema.Resource { return &schema.Resource{ CreateContext: resourceNetboxInterfaceTemplateCreate, ReadContext: resourceNetboxInterfaceTemplateRead, UpdateContext: resourceNetboxInterfaceTemplateUpdate, DeleteContext: resourceNetboxInterfaceTemplateDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/interfacetemplate/): > A template for a network interface that will be created on all instantiations of the parent device type. See the interface documentation for more detail.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringLenBetween(1, 64), }, "description": { Type: schema.TypeString, Optional: true, }, "label": { Type: schema.TypeString, Optional: true, }, "type": { Type: schema.TypeString, Required: true, }, "mgmt_only": { Type: schema.TypeBool, Optional: true, }, "device_type_id": { Type: schema.TypeInt, Optional: true, ExactlyOneOf: []string{"device_type_id", "module_type_id"}, ForceNew: true, }, "module_type_id": { Type: schema.TypeInt, Optional: true, ExactlyOneOf: []string{"device_type_id", "module_type_id"}, ForceNew: true, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxInterfaceTemplateCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) var diags diag.Diagnostics name := d.Get("name").(string) description := d.Get("description").(string) label := d.Get("label").(string) interfaceType := d.Get("type").(string) mgmtOnly := d.Get("mgmt_only").(bool) data := models.WritableInterfaceTemplate{ Name: &name, Description: description, Label: label, Type: &interfaceType, MgmtOnly: mgmtOnly, } if deviceTypeID, ok := d.Get("device_type_id").(int); ok && deviceTypeID != 0 { data.DeviceType = int64ToPtr(int64(deviceTypeID)) } if moduleTypeID, ok := d.Get("module_type_id").(int); ok && moduleTypeID != 0 { data.ModuleType = int64ToPtr(int64(moduleTypeID)) } params := dcim.NewDcimInterfaceTemplatesCreateParams().WithData(&data) res, err := api.Dcim.DcimInterfaceTemplatesCreate(params, nil) if err != nil { return diag.FromErr(err) } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return diags } func resourceNetboxInterfaceTemplateRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) var diags diag.Diagnostics params := dcim.NewDcimInterfaceTemplatesReadParams().WithID(id) res, err := api.Dcim.DcimInterfaceTemplatesRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimInterfaceTemplatesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return diag.FromErr(err) } tmpl := res.GetPayload() d.Set("name", tmpl.Name) d.Set("description", tmpl.Description) d.Set("label", tmpl.Label) d.Set("type", tmpl.Type.Value) d.Set("mgmt_only", tmpl.MgmtOnly) if tmpl.DeviceType != nil { d.Set("device_type_id", tmpl.DeviceType.ID) } if tmpl.ModuleType != nil { d.Set("module_type_id", tmpl.ModuleType.ID) } return diags } func resourceNetboxInterfaceTemplateUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) var diags diag.Diagnostics id, _ := strconv.ParseInt(d.Id(), 10, 64) name := d.Get("name").(string) description := d.Get("description").(string) label := d.Get("label").(string) interfaceType := d.Get("type").(string) mgmtOnly := d.Get("mgmt_only").(bool) data := models.WritableInterfaceTemplate{ Name: &name, Description: description, Label: label, Type: &interfaceType, MgmtOnly: mgmtOnly, } if d.HasChange("device_type_id") { deviceTypeID := int64(d.Get("device_type_id").(int)) data.DeviceType = &deviceTypeID } if d.HasChange("module_type_id") { moduleTypeID := int64(d.Get("module_type_id").(int)) data.ModuleType = &moduleTypeID } params := dcim.NewDcimInterfaceTemplatesPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Dcim.DcimInterfaceTemplatesPartialUpdate(params, nil) if err != nil { return diag.FromErr(err) } return diags } func resourceNetboxInterfaceTemplateDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimInterfaceTemplatesDeleteParams().WithID(id) _, err := api.Dcim.DcimInterfaceTemplatesDelete(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimInterfaceTemplatesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return diag.FromErr(err) } return nil } ================================================ FILE: netbox/resource_netbox_interface_template_test.go ================================================ package netbox import ( "fmt" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" log "github.com/sirupsen/logrus" ) func TestAccNetboxInterfaceTemplate_basic(t *testing.T) { testSlug := "interface_template" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" slug = "%[2]s" part_number = "%[2]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_interface_template" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id type = "100base-tx" }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_interface_template.test", "name", testName), resource.TestCheckResourceAttr("netbox_interface_template.test", "type", "100base-tx"), resource.TestCheckResourceAttrPair("netbox_interface_template.test", "device_type_id", "netbox_device_type.test", "id"), ), }, { ResourceName: "netbox_interface_template.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxInterfaceTemplate_opts(t *testing.T) { testSlug := "interface_template" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" slug = "%[2]s" part_number = "%[2]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_interface_template" "test" { name = "%[1]s" description = "%[1]s description" label = "%[1]s label" device_type_id = netbox_device_type.test.id type = "100base-tx" mgmt_only = true }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_interface_template.test", "name", testName), resource.TestCheckResourceAttr("netbox_interface_template.test", "description", fmt.Sprintf("%s description", testName)), resource.TestCheckResourceAttr("netbox_interface_template.test", "label", fmt.Sprintf("%s label", testName)), resource.TestCheckResourceAttr("netbox_interface_template.test", "type", "100base-tx"), resource.TestCheckResourceAttr("netbox_interface_template.test", "mgmt_only", "true"), resource.TestCheckResourceAttrPair("netbox_interface_template.test", "device_type_id", "netbox_device_type.test", "id"), ), }, { Config: fmt.Sprintf(` resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_module_type" "test" { manufacturer_id = netbox_manufacturer.test.id model = "%[1]s" } resource "netbox_interface_template" "test" { name = "%[1]s" description = "%[1]s description" label = "%[1]s label" module_type_id = netbox_module_type.test.id type = "100base-tx" mgmt_only = false }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_interface_template.test", "name", testName), resource.TestCheckResourceAttr("netbox_interface_template.test", "description", fmt.Sprintf("%s description", testName)), resource.TestCheckResourceAttr("netbox_interface_template.test", "label", fmt.Sprintf("%s label", testName)), resource.TestCheckResourceAttr("netbox_interface_template.test", "type", "100base-tx"), resource.TestCheckResourceAttr("netbox_interface_template.test", "mgmt_only", "false"), resource.TestCheckResourceAttrPair("netbox_interface_template.test", "module_type_id", "netbox_module_type.test", "id"), ), }, }, }) } func init() { resource.AddTestSweepers("netbox_interface_template", &resource.Sweeper{ Name: "netbox_interface_template", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimInterfaceTemplatesListParams() res, err := api.Dcim.DcimInterfaceTemplatesList(params, nil) if err != nil { return err } for _, tmpl := range res.GetPayload().Results { if strings.HasPrefix(*tmpl.Name, testPrefix) { deleteParams := dcim.NewDcimInterfaceTemplatesDeleteParams().WithID(tmpl.ID) _, err := api.Dcim.DcimInterfaceTemplatesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted an interface template") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_interface_test.go ================================================ package netbox import ( "fmt" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/virtualization" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" log "github.com/sirupsen/logrus" ) func testAccNetboxInterfaceFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_cluster_type" "test" { name = "%[1]s" } resource "netbox_cluster" "test" { name = "%[1]s" cluster_type_id = netbox_cluster_type.test.id } resource "netbox_virtual_machine" "test" { name = "%[1]s" cluster_id = netbox_cluster.test.id } resource "netbox_vlan" "test1" { name = "%[1]s_vlan1" vid = 1001 tags = [] } resource "netbox_vlan" "test2" { name = "%[1]s_vlan2" vid = 1002 tags = [] }`, testName) } func testAccNetboxInterfaceBasic(testName string) string { return fmt.Sprintf(` resource "netbox_interface" "test" { name = "%s" virtual_machine_id = netbox_virtual_machine.test.id tags = [netbox_tag.test.name] }`, testName) } func testAccNetboxInterfaceOpts(testName string, enabled string) string { return fmt.Sprintf(` resource "netbox_interface" "test" { name = "%[1]s" description = "%[1]s" enabled = %[2]s mtu = 1440 virtual_machine_id = netbox_virtual_machine.test.id }`, testName, enabled) } func testAccNetboxInterfaceVlans(testName string) string { return fmt.Sprintf(` resource "netbox_interface" "test1" { name = "%[1]s_1" mode = "access" untagged_vlan = netbox_vlan.test1.id virtual_machine_id = netbox_virtual_machine.test.id } resource "netbox_interface" "test2" { name = "%[1]s_2" mode = "tagged" tagged_vlans = [netbox_vlan.test2.id] untagged_vlan = netbox_vlan.test1.id virtual_machine_id = netbox_virtual_machine.test.id } resource "netbox_interface" "test3" { name = "%[1]s_3" mode = "tagged-all" tagged_vlans = [netbox_vlan.test1.id, netbox_vlan.test2.id] virtual_machine_id = netbox_virtual_machine.test.id }`, testName) } func testAccNetboxInterfaceBridge(testName string) string { return fmt.Sprintf(` resource "netbox_interface" "bridge" { name = "%[1]s_1" description = "%[1]s" virtual_machine_id = netbox_virtual_machine.test.id } resource "netbox_interface" "test" { name = "%[1]s_2" virtual_machine_id = netbox_virtual_machine.test.id bridge_interface_id = netbox_interface.bridge.id }`, testName) } func TestAccNetboxInterface_basic(t *testing.T) { testSlug := "iface_basic" testName := testAccGetTestName(testSlug) setUp := testAccNetboxInterfaceFullDependencies(testName) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckInterfaceDestroy, Steps: []resource.TestStep{ { Config: setUp + testAccNetboxInterfaceBasic(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_interface.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_interface.test", "virtual_machine_id", "netbox_virtual_machine.test", "id"), resource.TestCheckResourceAttr("netbox_interface.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_interface.test", "tags.0", testName), ), }, { ResourceName: "netbox_interface.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxInterface_opts(t *testing.T) { testSlug := "iface" testName := testAccGetTestName(testSlug) setUp := testAccNetboxInterfaceFullDependencies(testName) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckInterfaceDestroy, Steps: []resource.TestStep{ { Config: setUp + testAccNetboxInterfaceOpts(testName, "true"), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_interface.test", "name", testName), resource.TestCheckResourceAttr("netbox_interface.test", "description", testName), resource.TestCheckResourceAttr("netbox_interface.test", "enabled", "true"), resource.TestCheckResourceAttr("netbox_interface.test", "mtu", "1440"), resource.TestCheckResourceAttrPair("netbox_interface.test", "virtual_machine_id", "netbox_virtual_machine.test", "id"), ), }, { Config: setUp + testAccNetboxInterfaceOpts(testName, "false"), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_interface.test", "name", testName), resource.TestCheckResourceAttr("netbox_interface.test", "description", testName), resource.TestCheckResourceAttr("netbox_interface.test", "enabled", "false"), resource.TestCheckResourceAttr("netbox_interface.test", "mtu", "1440"), resource.TestCheckResourceAttrPair("netbox_interface.test", "virtual_machine_id", "netbox_virtual_machine.test", "id"), ), }, { ResourceName: "netbox_interface.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxInterface_vlans(t *testing.T) { testSlug := "iface_vlan" testName := testAccGetTestName(testSlug) setUp := testAccNetboxInterfaceFullDependencies(testName) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckInterfaceDestroy, Steps: []resource.TestStep{ { Config: setUp + testAccNetboxInterfaceVlans(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_interface.test1", "mode", "access"), resource.TestCheckResourceAttr("netbox_interface.test2", "mode", "tagged"), resource.TestCheckResourceAttr("netbox_interface.test3", "mode", "tagged-all"), resource.TestCheckResourceAttrPair("netbox_interface.test1", "untagged_vlan", "netbox_vlan.test1", "id"), resource.TestCheckResourceAttrPair("netbox_interface.test2", "untagged_vlan", "netbox_vlan.test1", "id"), resource.TestCheckResourceAttrPair("netbox_interface.test2", "tagged_vlans.0", "netbox_vlan.test2", "id"), resource.TestCheckResourceAttr("netbox_interface.test3", "tagged_vlans.#", "2"), ), }, { ResourceName: "netbox_interface.test1", ImportState: true, ImportStateVerify: true, }, { ResourceName: "netbox_interface.test2", ImportState: true, ImportStateVerify: true, }, { ResourceName: "netbox_interface.test3", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxInterface_bridge(t *testing.T) { testSlug := "iface_bridge" testName := testAccGetTestName(testSlug) setUp := testAccNetboxInterfaceFullDependencies(testName) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckInterfaceDestroy, Steps: []resource.TestStep{ { Config: setUp + testAccNetboxInterfaceBridge(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_interface.test", "bridge_interface_id", "netbox_interface.bridge", "id"), ), }, { Config: setUp + testAccNetboxInterfaceBasic(testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_interface.test", "bridge_interface_id", "0"), ), }, { ResourceName: "netbox_interface.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckInterfaceDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each interface // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_interface" { continue } // Retrieve our interface by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := virtualization.NewVirtualizationInterfacesReadParams().WithID(stateID) _, err := conn.Virtualization.VirtualizationInterfacesRead(params, nil) if err == nil { return fmt.Errorf("interface (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*virtualization.VirtualizationInterfacesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func init() { resource.AddTestSweepers("netbox_interface", &resource.Sweeper{ Name: "netbox_interface", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := virtualization.NewVirtualizationInterfacesListParams() res, err := api.Virtualization.VirtualizationInterfacesList(params, nil) if err != nil { return err } for _, intrface := range res.GetPayload().Results { if strings.HasPrefix(*intrface.Name, testPrefix) { deleteParams := virtualization.NewVirtualizationInterfacesDeleteParams().WithID(intrface.ID) _, err := api.Virtualization.VirtualizationInterfacesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted an interface") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_inventory_item.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxInventoryItem() *schema.Resource { return &schema.Resource{ Create: resourceNetboxInventoryItemCreate, Read: resourceNetboxInventoryItemRead, Update: resourceNetboxInventoryItemUpdate, Delete: resourceNetboxInventoryItemDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/inventoryitem/): > Inventory items represent hardware components installed within a device, such as a power supply or CPU or line card. They are intended to be used primarily for inventory purposes.`, Schema: map[string]*schema.Schema{ "device_id": { Type: schema.TypeInt, Required: true, }, "name": { Type: schema.TypeString, Required: true, }, "parent_id": { Type: schema.TypeInt, Optional: true, }, "label": { Type: schema.TypeString, Optional: true, }, "role_id": { Type: schema.TypeInt, Optional: true, }, "manufacturer_id": { Type: schema.TypeInt, Optional: true, }, "part_id": { Type: schema.TypeString, Optional: true, }, "serial": { Type: schema.TypeString, Optional: true, }, "asset_tag": { Type: schema.TypeString, Optional: true, }, "discovered": { Type: schema.TypeBool, Optional: true, Default: false, }, "description": { Type: schema.TypeString, Optional: true, }, "component_type": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringInSlice([]string{ "dcim.powerport", "dcim.poweroutlet", "dcim.frontport", "dcim.rearport", "dcim.consoleserverport", "dcim.consoleport", "dcim.interface", }, false), }, "component_id": { Type: schema.TypeInt, Optional: true, RequiredWith: []string{"component_type"}, }, tagsKey: tagsSchema, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxInventoryItemCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableInventoryItem{ Device: int64ToPtr(int64(d.Get("device_id").(int))), Name: strToPtr(d.Get("name").(string)), Parent: getOptionalInt(d, "parent_id"), Label: getOptionalStr(d, "label", false), Role: getOptionalInt(d, "role_id"), Manufacturer: getOptionalInt(d, "manufacturer_id"), PartID: getOptionalStr(d, "part_id", false), Serial: getOptionalStr(d, "serial", false), Discovered: d.Get("discovered").(bool), Description: getOptionalStr(d, "description", false), } if assetTag := getOptionalStr(d, "asset_tag", false); assetTag != "" { data.AssetTag = &assetTag } if componentType := getOptionalStr(d, "component_type", false); componentType != "" { data.ComponentType = &componentType data.ComponentID = getOptionalInt(d, "component_id") } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimInventoryItemsCreateParams().WithData(&data) res, err := api.Dcim.DcimInventoryItemsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxInventoryItemRead(d, m) } func resourceNetboxInventoryItemRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimInventoryItemsReadParams().WithID(id) res, err := api.Dcim.DcimInventoryItemsRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimInventoryItemsReadDefault); ok { if errresp.Code() == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } item := res.GetPayload() if item.Device != nil { d.Set("device_id", item.Device.ID) } else { d.Set("device_id", nil) } d.Set("name", item.Name) d.Set("parent_id", item.Parent) d.Set("label", item.Label) if item.Role != nil { d.Set("role_id", item.Role.ID) } else { d.Set("role_id", nil) } if item.Manufacturer != nil { d.Set("manufacturer_id", item.Manufacturer.ID) } else { d.Set("manufacturer_id", nil) } d.Set("part_id", item.PartID) d.Set("serial", item.Serial) d.Set("asset_tag", item.AssetTag) d.Set("discovered", item.Discovered) d.Set("description", item.Description) d.Set("component_type", item.ComponentType) d.Set("component_id", item.ComponentID) cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxInventoryItemUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableInventoryItem{ Device: int64ToPtr(int64(d.Get("device_id").(int))), Name: strToPtr(d.Get("name").(string)), Parent: getOptionalInt(d, "parent_id"), Label: getOptionalStr(d, "label", true), Role: getOptionalInt(d, "role_id"), Manufacturer: getOptionalInt(d, "manufacturer_id"), PartID: getOptionalStr(d, "part_id", true), Serial: getOptionalStr(d, "serial", true), Discovered: d.Get("discovered").(bool), Description: getOptionalStr(d, "description", true), } if assetTag := getOptionalStr(d, "asset_tag", false); assetTag != "" { data.AssetTag = &assetTag } if componentType := getOptionalStr(d, "component_type", false); componentType != "" { data.ComponentType = &componentType data.ComponentID = getOptionalInt(d, "component_id") } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimInventoryItemsPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimInventoryItemsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxInventoryItemRead(d, m) } func resourceNetboxInventoryItemDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimInventoryItemsDeleteParams().WithID(id) _, err := api.Dcim.DcimInventoryItemsDelete(params, nil) if err != nil { return err } return nil } ================================================ FILE: netbox/resource_netbox_inventory_item_role.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxInventoryItemRole() *schema.Resource { return &schema.Resource{ Create: resourceNetboxInventoryItemRoleCreate, Read: resourceNetboxInventoryItemRoleRead, Update: resourceNetboxInventoryItemRoleUpdate, Delete: resourceNetboxInventoryItemRoleDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/inventoryitemrole/): > Inventory items can be organized by functional roles, which are fully customizable by the user. For example, you might create roles for power supplies, fans, interface optics, etc.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "color_hex": { Type: schema.TypeString, Required: true, }, "description": { Type: schema.TypeString, Optional: true, }, tagsKey: tagsSchema, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxInventoryItemRoleCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.InventoryItemRole{ Name: strToPtr(d.Get("name").(string)), Slug: strToPtr(d.Get("slug").(string)), Description: getOptionalStr(d, "description", false), Color: getOptionalStr(d, "color_hex", false), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimInventoryItemRolesCreateParams().WithData(&data) res, err := api.Dcim.DcimInventoryItemRolesCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxInventoryItemRoleRead(d, m) } func resourceNetboxInventoryItemRoleRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimInventoryItemRolesReadParams().WithID(id) res, err := api.Dcim.DcimInventoryItemRolesRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimInventoryItemRolesReadDefault); ok { if errresp.Code() == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } role := res.GetPayload() d.Set("name", role.Name) d.Set("slug", role.Slug) d.Set("color_hex", role.Color) d.Set("description", role.Description) cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxInventoryItemRoleUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.InventoryItemRole{ Name: strToPtr(d.Get("name").(string)), Slug: strToPtr(d.Get("slug").(string)), Description: getOptionalStr(d, "description", true), Color: getOptionalStr(d, "color_hex", false), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimInventoryItemRolesPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimInventoryItemRolesPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxInventoryItemRoleRead(d, m) } func resourceNetboxInventoryItemRoleDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimInventoryItemRolesDeleteParams().WithID(id) _, err := api.Dcim.DcimInventoryItemRolesDelete(params, nil) if err != nil { return err } return nil } ================================================ FILE: netbox/resource_netbox_inventory_item_role_test.go ================================================ package netbox import ( "fmt" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" log "github.com/sirupsen/logrus" ) func testAccNetboxInventoryItemRoleFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]sa" } `, testName) } func TestAccNetboxInventoryItemRole_basic(t *testing.T) { testSlug := "inventory_item_role_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckInventoryItemRoleDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxInventoryItemRoleFullDependencies(testName) + fmt.Sprintf(` resource "netbox_inventory_item_role" "test" { name = "%[1]s" slug = "%[1]s_slug" color_hex = "123456" description = "%[1]s_description" tags = [ netbox_tag.test.name ] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_inventory_item_role.test", "name", testName), resource.TestCheckResourceAttr("netbox_inventory_item_role.test", "slug", testName+"_slug"), resource.TestCheckResourceAttr("netbox_inventory_item_role.test", "description", testName+"_description"), resource.TestCheckResourceAttr("netbox_inventory_item_role.test", "color_hex", "123456"), resource.TestCheckResourceAttr("netbox_inventory_item_role.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_inventory_item_role.test", "tags.0", testName+"a"), ), }, { Config: testAccNetboxInventoryItemRoleFullDependencies(testName) + fmt.Sprintf(` resource "netbox_inventory_item_role" "test" { name = "%[1]s" slug = "%[1]s_slug" color_hex = "123456" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_inventory_item_role.test", "name", testName), resource.TestCheckResourceAttr("netbox_inventory_item_role.test", "slug", testName+"_slug"), resource.TestCheckResourceAttr("netbox_inventory_item_role.test", "description", ""), resource.TestCheckResourceAttr("netbox_inventory_item_role.test", "color_hex", "123456"), resource.TestCheckResourceAttr("netbox_inventory_item_role.test", "tags.#", "0"), ), }, { ResourceName: "netbox_inventory_item_role.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckInventoryItemRoleDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each inventory item role // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_inventory_item_role" { continue } // Retrieve our device by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := dcim.NewDcimInventoryItemRolesReadParams().WithID(stateID) _, err := conn.Dcim.DcimInventoryItemRolesRead(params, nil) if err == nil { return fmt.Errorf("inventory item role (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*dcim.DcimInventoryItemRolesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func init() { resource.AddTestSweepers("netbox_inventory_item_role", &resource.Sweeper{ Name: "netbox_inventory_item_role", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimInventoryItemRolesListParams() res, err := api.Dcim.DcimInventoryItemRolesList(params, nil) if err != nil { return err } for _, role := range res.GetPayload().Results { if strings.HasPrefix(*role.Name, testPrefix) { deleteParams := dcim.NewDcimInventoryItemRolesDeleteParams().WithID(role.ID) _, err := api.Dcim.DcimInventoryItemRolesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted an inventory item role") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_inventory_item_test.go ================================================ package netbox import ( "fmt" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" log "github.com/sirupsen/logrus" ) func testAccNetboxInventoryItemFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_tag" "test" { name = "%[1]sa" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_module_bay" "test" { device_id = netbox_device.test.id name = "%[1]s" } resource "netbox_module_type" "test" { manufacturer_id = netbox_manufacturer.test.id model = "%[1]s" } resource "netbox_module" "test" { device_id = netbox_device.test.id module_bay_id = netbox_device_module_bay.test.id module_type_id = netbox_module_type.test.id status = "active" } resource "netbox_device_rear_port" "test" { device_id = netbox_device.test.id name = "%[1]s" type = "8p8c" positions = 1 mark_connected = true } resource "netbox_inventory_item_role" "test" { name = "%[1]s" slug = "%[1]s_slug" color_hex = "123456" } `, testName) } func TestAccNetboxInventoryItem_basic(t *testing.T) { testSlug := "inventory_item_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckInventoryItemDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxInventoryItemFullDependencies(testName) + fmt.Sprintf(` resource "netbox_inventory_item" "parent" { device_id = netbox_device.test.id name = "%[1]s_parent" } resource "netbox_inventory_item" "test" { device_id = netbox_device.test.id name = "%[1]s" parent_id = netbox_inventory_item.parent.id label = "%[1]s_label" role_id = netbox_inventory_item_role.test.id manufacturer_id = netbox_manufacturer.test.id part_id = "%[1]s_part" serial = "%[1]s_serial" asset_tag = "%[1]s_asset" discovered = true description = "%[1]s_description" component_type = "dcim.rearport" component_id = netbox_device_rear_port.test.id tags = [netbox_tag.test.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_inventory_item.parent", "name", testName+"_parent"), resource.TestCheckResourceAttrPair("netbox_inventory_item.parent", "device_id", "netbox_device.test", "id"), resource.TestCheckResourceAttr("netbox_inventory_item.test", "name", testName), resource.TestCheckResourceAttr("netbox_inventory_item.test", "label", testName+"_label"), resource.TestCheckResourceAttr("netbox_inventory_item.test", "part_id", testName+"_part"), resource.TestCheckResourceAttr("netbox_inventory_item.test", "serial", testName+"_serial"), resource.TestCheckResourceAttr("netbox_inventory_item.test", "asset_tag", testName+"_asset"), resource.TestCheckResourceAttr("netbox_inventory_item.test", "discovered", "true"), resource.TestCheckResourceAttr("netbox_inventory_item.test", "description", testName+"_description"), resource.TestCheckResourceAttr("netbox_inventory_item.test", "component_type", "dcim.rearport"), resource.TestCheckResourceAttr("netbox_inventory_item.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_inventory_item.test", "tags.0", testName+"a"), resource.TestCheckResourceAttrPair("netbox_inventory_item.test", "device_id", "netbox_device.test", "id"), resource.TestCheckResourceAttrPair("netbox_inventory_item.test", "parent_id", "netbox_inventory_item.parent", "id"), resource.TestCheckResourceAttrPair("netbox_inventory_item.test", "role_id", "netbox_inventory_item_role.test", "id"), resource.TestCheckResourceAttrPair("netbox_inventory_item.test", "manufacturer_id", "netbox_manufacturer.test", "id"), resource.TestCheckResourceAttrPair("netbox_inventory_item.test", "component_id", "netbox_device_rear_port.test", "id"), ), }, { Config: testAccNetboxInventoryItemFullDependencies(testName) + fmt.Sprintf(` resource "netbox_inventory_item" "parent" { device_id = netbox_device.test.id name = "%[1]s_parent" } resource "netbox_inventory_item" "test" { device_id = netbox_device.test.id name = "%[1]s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_inventory_item.test", "name", testName), resource.TestCheckResourceAttr("netbox_inventory_item.test", "label", ""), resource.TestCheckResourceAttr("netbox_inventory_item.test", "part_id", ""), resource.TestCheckResourceAttr("netbox_inventory_item.test", "serial", ""), resource.TestCheckResourceAttr("netbox_inventory_item.test", "asset_tag", ""), resource.TestCheckResourceAttr("netbox_inventory_item.test", "discovered", "false"), resource.TestCheckResourceAttr("netbox_inventory_item.test", "description", ""), resource.TestCheckResourceAttr("netbox_inventory_item.test", "component_type", ""), resource.TestCheckResourceAttr("netbox_inventory_item.test", "tags.#", "0"), resource.TestCheckResourceAttr("netbox_inventory_item.test", "parent_id", "0"), resource.TestCheckResourceAttr("netbox_inventory_item.test", "role_id", "0"), resource.TestCheckResourceAttr("netbox_inventory_item.test", "manufacturer_id", "0"), resource.TestCheckResourceAttr("netbox_inventory_item.test", "component_id", "0"), resource.TestCheckResourceAttrPair("netbox_inventory_item.test", "device_id", "netbox_device.test", "id"), ), }, { ResourceName: "netbox_inventory_item.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckInventoryItemDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each inventory item // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_inventory_item" { continue } // Retrieve our device by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := dcim.NewDcimInventoryItemsReadParams().WithID(stateID) _, err := conn.Dcim.DcimInventoryItemsRead(params, nil) if err == nil { return fmt.Errorf("inventory_item (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*dcim.DcimInventoryItemsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func init() { resource.AddTestSweepers("netbox_inventory_item", &resource.Sweeper{ Name: "netbox_inventory_item", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimInventoryItemsListParams() res, err := api.Dcim.DcimInventoryItemsList(params, nil) if err != nil { return err } for _, rearPort := range res.GetPayload().Results { if strings.HasPrefix(*rearPort.Name, testPrefix) { deleteParams := dcim.NewDcimInventoryItemsDeleteParams().WithID(rearPort.ID) _, err := api.Dcim.DcimInventoryItemsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted an inventory_item") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_ip_address.go ================================================ package netbox import ( "strconv" "strings" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var resourceNetboxIPAddressObjectTypeOptions = []string{"virtualization.vminterface", "dcim.interface"} var resourceNetboxIPAddressStatusOptions = []string{"active", "reserved", "deprecated", "dhcp", "slaac"} var resourceNetboxIPAddressRoleOptions = []string{"loopback", "secondary", "anycast", "vip", "vrrp", "hsrp", "glbp", "carp"} func resourceNetboxIPAddress() *schema.Resource { return &schema.Resource{ Create: resourceNetboxIPAddressCreate, Read: resourceNetboxIPAddressRead, Update: resourceNetboxIPAddressUpdate, Delete: resourceNetboxIPAddressDelete, Description: `:meta:subcategory:IP Address Management (IPAM):From the [official documentation](https://docs.netbox.dev/en/stable/features/ipam/#ip-addresses): > An IP address comprises a single host address (either IPv4 or IPv6) and its subnet mask. Its mask should match exactly how the IP address is configured on an interface in the real world. > > Like a prefix, an IP address can optionally be assigned to a VRF (otherwise, it will appear in the "global" table). IP addresses are automatically arranged under parent prefixes within their respective VRFs according to the IP hierarchy.`, Schema: map[string]*schema.Schema{ "ip_address": { Type: schema.TypeString, Required: true, ValidateFunc: validation.IsCIDR, }, "interface_id": { Type: schema.TypeInt, Optional: true, RequiredWith: []string{"object_type"}, }, "object_type": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringInSlice(resourceNetboxIPAddressObjectTypeOptions, false), Description: buildValidValueDescription(resourceNetboxIPAddressObjectTypeOptions), RequiredWith: []string{"interface_id"}, }, "virtual_machine_interface_id": { Type: schema.TypeInt, Optional: true, ConflictsWith: []string{"interface_id", "device_interface_id"}, }, "device_interface_id": { Type: schema.TypeInt, Optional: true, ConflictsWith: []string{"interface_id", "virtual_machine_interface_id"}, }, "vrf_id": { Type: schema.TypeInt, Optional: true, }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, "status": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringInSlice(resourceNetboxIPAddressStatusOptions, false), Description: buildValidValueDescription(resourceNetboxIPAddressStatusOptions), }, "dns_name": { Type: schema.TypeString, Optional: true, // NetBox always converts DNS names to lowercase DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { return strings.EqualFold(old, new) }, }, tagsKey: tagsSchema, "description": { Type: schema.TypeString, Optional: true, }, "role": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringInSlice(resourceNetboxIPAddressRoleOptions, false), Description: buildValidValueDescription(resourceNetboxIPAddressRoleOptions), }, "nat_inside_address_id": { Type: schema.TypeInt, Optional: true, }, "nat_outside_addresses": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeInt, Computed: true, }, "ip_address": { Type: schema.TypeString, Computed: true, }, "address_family": { Type: schema.TypeInt, Computed: true, }, }, }, }, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxIPAddressCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableIPAddress{} data.Address = strToPtr(d.Get("ip_address").(string)) data.Status = d.Get("status").(string) data.Description = getOptionalStr(d, "description", false) data.Role = getOptionalStr(d, "role", false) data.DNSName = getOptionalStr(d, "dns_name", false) data.Vrf = getOptionalInt(d, "vrf_id") data.Tenant = getOptionalInt(d, "tenant_id") data.NatInside = getOptionalInt(d, "nat_inside_address_id") vmInterfaceID := getOptionalInt(d, "virtual_machine_interface_id") deviceInterfaceID := getOptionalInt(d, "device_interface_id") interfaceID := getOptionalInt(d, "interface_id") switch { case vmInterfaceID != nil: data.AssignedObjectType = strToPtr("virtualization.vminterface") data.AssignedObjectID = vmInterfaceID case deviceInterfaceID != nil: data.AssignedObjectType = strToPtr("dcim.interface") data.AssignedObjectID = deviceInterfaceID // if interfaceID is given, object_type must be set as well case interfaceID != nil: data.AssignedObjectType = strToPtr(d.Get("object_type").(string)) data.AssignedObjectID = interfaceID // default = ip is not linked to anything default: data.AssignedObjectType = strToPtr("") data.AssignedObjectID = nil } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } cf, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = cf } params := ipam.NewIpamIPAddressesCreateParams().WithData(&data) res, err := api.Ipam.IpamIPAddressesCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxIPAddressRead(d, m) } func resourceNetboxIPAddressRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamIPAddressesReadParams().WithID(id) res, err := api.Ipam.IpamIPAddressesRead(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamIPAddressesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } ipAddress := res.GetPayload() if ipAddress.AssignedObjectID != nil { vmInterfaceID := getOptionalInt(d, "virtual_machine_interface_id") deviceInterfaceID := getOptionalInt(d, "device_interface_id") interfaceID := getOptionalInt(d, "interface_id") switch { case vmInterfaceID != nil: d.Set("virtual_machine_interface_id", ipAddress.AssignedObjectID) d.Set("interface_id", nil) d.Set("object_type", "") case deviceInterfaceID != nil: d.Set("device_interface_id", ipAddress.AssignedObjectID) d.Set("interface_id", nil) d.Set("object_type", "") // if interfaceID is given, object_type must be set as well case interfaceID != nil: d.Set("interface_id", ipAddress.AssignedObjectID) d.Set("object_type", ipAddress.AssignedObjectType) default: // Set changes made to the ip address outside of Terraform and update the state accordingly. d.Set("interface_id", ipAddress.AssignedObjectID) d.Set("object_type", ipAddress.AssignedObjectType) } } else { d.Set("virtual_machine_interface_id", nil) d.Set("device_interface_id", nil) d.Set("interface_id", nil) d.Set("object_type", "") } if ipAddress.Vrf != nil { d.Set("vrf_id", ipAddress.Vrf.ID) } else { d.Set("vrf_id", nil) } if ipAddress.Tenant != nil { d.Set("tenant_id", ipAddress.Tenant.ID) } else { d.Set("tenant_id", nil) } if ipAddress.DNSName != "" { d.Set("dns_name", ipAddress.DNSName) } if ipAddress.Role != nil { d.Set("role", ipAddress.Role.Value) } else { d.Set("role", nil) } if ipAddress.NatInside != nil { d.Set("nat_inside_address_id", ipAddress.NatInside.ID) } else { d.Set("nat_inside_address_id", nil) } if ipAddress.NatOutside != nil { natOutsideIPAddresses := ipAddress.NatOutside var s []map[string]interface{} for _, v := range natOutsideIPAddresses { var mapping = make(map[string]interface{}) mapping["id"] = v.ID mapping["ip_address"] = v.Address mapping["address_family"] = v.Family s = append(s, mapping) } d.Set("nat_outside_addresses", s) } else { d.Set("nat_outside_addresses", nil) } d.Set("ip_address", ipAddress.Address) d.Set("description", ipAddress.Description) d.Set("status", ipAddress.Status.Value) api.readTags(d, ipAddress.Tags) cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } return nil } func resourceNetboxIPAddressUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableIPAddress{} data.Address = strToPtr(d.Get("ip_address").(string)) data.Status = d.Get("status").(string) data.Description = getOptionalStr(d, "description", true) data.Role = getOptionalStr(d, "role", false) data.DNSName = getOptionalStr(d, "dns_name", true) data.Vrf = getOptionalInt(d, "vrf_id") data.Tenant = getOptionalInt(d, "tenant_id") data.NatInside = getOptionalInt(d, "nat_inside_address_id") vmInterfaceID := getOptionalInt(d, "virtual_machine_interface_id") deviceInterfaceID := getOptionalInt(d, "device_interface_id") interfaceID := getOptionalInt(d, "interface_id") switch { case vmInterfaceID != nil: data.AssignedObjectType = strToPtr("virtualization.vminterface") data.AssignedObjectID = vmInterfaceID case deviceInterfaceID != nil: data.AssignedObjectType = strToPtr("dcim.interface") data.AssignedObjectID = deviceInterfaceID // if interfaceID is given, object_type must be set as well case interfaceID != nil: data.AssignedObjectType = strToPtr(d.Get("object_type").(string)) data.AssignedObjectID = interfaceID // default = ip is not linked to anything default: data.AssignedObjectType = strToPtr("") data.AssignedObjectID = nil } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } if cf, ok := d.GetOk(customFieldsKey); ok { data.CustomFields = cf } params := ipam.NewIpamIPAddressesUpdateParams().WithID(id).WithData(&data) _, err = api.Ipam.IpamIPAddressesUpdate(params, nil) if err != nil { return err } return resourceNetboxIPAddressRead(d, m) } func resourceNetboxIPAddressDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamIPAddressesDeleteParams().WithID(id) _, err := api.Ipam.IpamIPAddressesDelete(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamIPAddressesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_ip_address_test.go ================================================ package netbox import ( "fmt" "log" "regexp" "testing" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxIPAddressFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_vrf" "test" { name = "%[1]s" } resource "netbox_cluster_type" "test" { name = "%[1]s" } resource "netbox_cluster" "test" { name = "%[1]s" cluster_type_id = netbox_cluster_type.test.id } resource "netbox_virtual_machine" "test" { name = "%[1]s" cluster_id = netbox_cluster.test.id } resource "netbox_interface" "test" { name = "%[1]s" virtual_machine_id = netbox_virtual_machine.test.id } `, testName) } func testAccNetboxIPAddressFullDeviceDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device" "test" { name = "%[1]s" site_id = netbox_site.test.id device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id } resource "netbox_device_interface" "test" { name = "%[1]s" device_id = netbox_device.test.id type = "1000base-t" } `, testName) } func TestAccNetboxIPAddress_basic(t *testing.T) { testIP := "1.1.1.1/32" testSlug := "ipaddress" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_address" "test" { ip_address = "%s" interface_id = netbox_interface.test.id object_type = "virtualization.vminterface" status = "active" tags = [netbox_tag.test.name] }`, testIP), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_ip_address.test", "ip_address", testIP), resource.TestCheckResourceAttr("netbox_ip_address.test", "status", "active"), resource.TestCheckResourceAttr("netbox_ip_address.test", "object_type", "virtualization.vminterface"), resource.TestCheckResourceAttr("netbox_ip_address.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_ip_address.test", "tags.0", testName), resource.TestCheckResourceAttr("netbox_ip_address.test", "tenant_id", "0"), resource.TestCheckResourceAttr("netbox_ip_address.test", "vrf_id", "0"), resource.TestCheckResourceAttrPair("netbox_ip_address.test", "interface_id", "netbox_interface.test", "id"), ), }, { Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_address" "test" { ip_address = "%s" interface_id = netbox_interface.test.id object_type = "virtualization.vminterface" status = "reserved" tenant_id = netbox_tenant.test.id vrf_id = netbox_vrf.test.id tags = [netbox_tag.test.name] description = "description for %[1]s" role = "loopback" }`, testIP), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_ip_address.test", "ip_address", testIP), resource.TestCheckResourceAttr("netbox_ip_address.test", "status", "reserved"), resource.TestCheckResourceAttr("netbox_ip_address.test", "object_type", "virtualization.vminterface"), resource.TestCheckResourceAttrPair("netbox_ip_address.test", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("netbox_ip_address.test", "vrf_id", "netbox_vrf.test", "id"), resource.TestCheckResourceAttr("netbox_ip_address.test", "description", fmt.Sprintf("description for %[1]s", testIP)), resource.TestCheckResourceAttr("netbox_ip_address.test", "role", "loopback"), resource.TestCheckResourceAttrPair("netbox_ip_address.test", "interface_id", "netbox_interface.test", "id"), ), }, { Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_address" "test" { ip_address = "%s" interface_id = netbox_interface.test.id object_type = "virtualization.vminterface" status = "dhcp" tags = [netbox_tag.test.name] }`, testIP), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_ip_address.test", "ip_address", testIP), resource.TestCheckResourceAttr("netbox_ip_address.test", "status", "dhcp"), resource.TestCheckResourceAttr("netbox_ip_address.test", "object_type", "virtualization.vminterface"), resource.TestCheckResourceAttr("netbox_ip_address.test", "tenant_id", "0"), resource.TestCheckResourceAttr("netbox_ip_address.test", "vrf_id", "0"), resource.TestCheckResourceAttr("netbox_ip_address.test", "role", ""), resource.TestCheckResourceAttrPair("netbox_ip_address.test", "interface_id", "netbox_interface.test", "id"), ), }, { Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_address" "test" { ip_address = "%s" interface_id = netbox_interface.test.id object_type = "virtualization.vminterface" status = "provoke_error" tags = [netbox_tag.test.name] }`, testIP), ExpectError: regexp.MustCompile("expected status to be one of .*"), }, { Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_address" "test" { ip_address = "%s" interface_id = netbox_interface.test.id object_type = "virtualization.vminterface" status = "deprecated" tags = [netbox_tag.test.name] }`, testIP), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_ip_address.test", "ip_address", testIP), resource.TestCheckResourceAttr("netbox_ip_address.test", "status", "deprecated"), resource.TestCheckResourceAttr("netbox_ip_address.test", "object_type", "virtualization.vminterface"), resource.TestCheckResourceAttrPair("netbox_ip_address.test", "interface_id", "netbox_interface.test", "id"), ), }, { Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_address" "test" { ip_address = "%s" interface_id = netbox_interface.test.id object_type = "virtualization.vminterface" status = "active" dns_name = "mytest.example.com" tags = [netbox_tag.test.name] }`, testIP), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_ip_address.test", "ip_address", testIP), resource.TestCheckResourceAttr("netbox_ip_address.test", "status", "active"), resource.TestCheckResourceAttr("netbox_ip_address.test", "object_type", "virtualization.vminterface"), resource.TestCheckResourceAttr("netbox_ip_address.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_ip_address.test", "dns_name", "mytest.example.com"), resource.TestCheckResourceAttrPair("netbox_ip_address.test", "interface_id", "netbox_interface.test", "id"), ), }, { ResourceName: "netbox_ip_address.test", ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{"interface_id", "object_type"}, }, }, }) } func TestAccNetboxIPAddress_cf(t *testing.T) { testIP := "1.1.1.8/32" testSlug := "ipaddr_cf" testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_custom_field" "test" { name = "%s" type = "text" weight = 100 content_types = ["ipam.ipaddress"] } resource "netbox_ip_address" "test_customfield" { ip_address = "%s" status = "active" custom_fields = { "${netbox_custom_field.test.name}" = "test-field" } }`, testSlug, testIP), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_ip_address.test_customfield", fmt.Sprintf("custom_fields.%s", testSlug), "test-field"), ), }, { ResourceName: "netbox_ip_address.test_customfield", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxIPAddress_deviceByObjectType(t *testing.T) { testIP := "1.1.1.2/32" testSlug := "ipadr_dev_ot" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxIPAddressFullDeviceDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_address" "test" { ip_address = "%s" object_type = "dcim.interface" interface_id = netbox_device_interface.test.id status = "active" }`, testIP), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_ip_address.test", "ip_address", testIP), resource.TestCheckResourceAttr("netbox_ip_address.test", "status", "active"), resource.TestCheckResourceAttr("netbox_ip_address.test", "object_type", "dcim.interface"), resource.TestCheckResourceAttrPair("netbox_ip_address.test", "interface_id", "netbox_device_interface.test", "id"), ), }, { ResourceName: "netbox_ip_address.test", ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{"interface_id", "object_type"}, }, }, }) } func TestAccNetboxIPAddress_vmByObjectType(t *testing.T) { testIP := "1.1.1.3/32" testSlug := "ipadr_vm_ot" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_address" "test" { ip_address = "%s" object_type = "virtualization.vminterface" interface_id = netbox_interface.test.id status = "active" }`, testIP), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_ip_address.test", "ip_address", testIP), resource.TestCheckResourceAttr("netbox_ip_address.test", "status", "active"), resource.TestCheckResourceAttr("netbox_ip_address.test", "object_type", "virtualization.vminterface"), resource.TestCheckResourceAttrPair("netbox_ip_address.test", "interface_id", "netbox_interface.test", "id"), ), }, { ResourceName: "netbox_ip_address.test", ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{"interface_id", "object_type"}, }, }, }) } func TestAccNetboxIPAddress_vmSwitchStyle(t *testing.T) { testIP := "1.1.1.9/32" testSlug := "ipadr_vm_sw" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_address" "test" { ip_address = "%s" object_type = "virtualization.vminterface" interface_id = netbox_interface.test.id status = "active" }`, testIP), }, { Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_address" "test" { ip_address = "%s" virtual_machine_interface_id = netbox_interface.test.id status = "active" }`, testIP), }, { ResourceName: "netbox_ip_address.test", ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{"interface_id", "object_type", "virtual_machine_interface_id"}, }, }, }) } // TestAccNetboxIPAddress_deviceByFieldName tests if creating an ip address and linking it to a device via the `device_interface_id` field works func TestAccNetboxIPAddress_deviceByFieldName(t *testing.T) { testIP := "1.1.1.4/32" testSlug := "ipadr_dev_fn" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxIPAddressFullDeviceDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_address" "test" { ip_address = "%s" device_interface_id = netbox_device_interface.test.id status = "active" }`, testIP), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_ip_address.test", "ip_address", testIP), resource.TestCheckResourceAttr("netbox_ip_address.test", "status", "active"), resource.TestCheckResourceAttrPair("netbox_ip_address.test", "device_interface_id", "netbox_device_interface.test", "id"), ), }, { ResourceName: "netbox_ip_address.test", ImportState: true, ImportStateVerify: true, // import step doesn't have context about device_interface_id, thus we // get interface_id and object_type imported instead ImportStateVerifyIgnore: []string{"interface_id", "object_type", "device_interface_id"}, }, }, }) } func TestAccNetboxIPAddress_vmByFieldName(t *testing.T) { testIP := "1.1.1.5/32" testSlug := "ipadr_vm_fn" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_address" "test" { ip_address = "%s" virtual_machine_interface_id = netbox_interface.test.id status = "active" }`, testIP), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_ip_address.test", "ip_address", testIP), resource.TestCheckResourceAttr("netbox_ip_address.test", "status", "active"), resource.TestCheckResourceAttrPair("netbox_ip_address.test", "virtual_machine_interface_id", "netbox_interface.test", "id"), ), }, { ResourceName: "netbox_ip_address.test", ImportState: true, ImportStateVerify: true, // import step doesn't have context about virtual_machine_interface_id, thus we // get interface_id and object_type imported instead ImportStateVerifyIgnore: []string{"interface_id", "object_type", "virtual_machine_interface_id"}, }, }, }) } // TestAccNetboxIPAddress_standalone tests the case where an ip address is not linked to a vm or device func TestAccNetboxIPAddress_standalone(t *testing.T) { testIP := "1.1.1.6/32" resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_ip_address" "test" { ip_address = "%s" status = "active" }`, testIP), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_ip_address.test", "ip_address", testIP), resource.TestCheckResourceAttr("netbox_ip_address.test", "status", "active"), ), }, { ResourceName: "netbox_ip_address.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxIPAddress_nat(t *testing.T) { testIP := "1.1.1.10/32" testIPInside := "1.1.1.11/32" resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_ip_address" "test" { ip_address = "%s" status = "active" } resource "netbox_ip_address" "inside" { ip_address = "%s" status = "active" nat_inside_address_id = netbox_ip_address.test.id } `, testIP, testIPInside), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_ip_address.test", "ip_address", testIP), resource.TestCheckResourceAttr("netbox_ip_address.test", "status", "active"), resource.TestCheckResourceAttrPair("netbox_ip_address.inside", "nat_inside_address_id", "netbox_ip_address.test", "id"), ), }, // we have to make another step because netbox_ip_address.test.nat_outside_addresses needs a refresh { Config: fmt.Sprintf(` resource "netbox_ip_address" "test" { ip_address = "%s" status = "active" } resource "netbox_ip_address" "inside" { ip_address = "%s" status = "active" nat_inside_address_id = netbox_ip_address.test.id } `, testIP, testIPInside), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_ip_address.test", "nat_outside_addresses.#", "1"), resource.TestCheckResourceAttrPair("netbox_ip_address.test", "nat_outside_addresses.0.id", "netbox_ip_address.inside", "id"), ), }, { ResourceName: "netbox_ip_address.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxIPAddress_invalidConfig(t *testing.T) { testIP := "1.1.1.7/32" resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_ip_address" "test" { ip_address = "%s" object_type = "dcim.interface" status = "active" }`, testIP), ExpectError: regexp.MustCompile(".*all of `interface_id,object_type` must be specified.*"), }, { Config: fmt.Sprintf(` resource "netbox_ip_address" "test" { ip_address = "%s" interface_id = 1 status = "active" }`, testIP), ExpectError: regexp.MustCompile(".*all of `interface_id,object_type` must be specified.*"), }, { Config: fmt.Sprintf(` resource "netbox_ip_address" "test" { ip_address = "%s" virtual_machine_interface_id = 1 interface_id = 1 object_type = "dcim.interface" status = "active" }`, testIP), ExpectError: regexp.MustCompile(".*conflicts with interface_id.*"), }, }, }) } func init() { resource.AddTestSweepers("netbox_ip_address", &resource.Sweeper{ Name: "netbox_ip_address", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := ipam.NewIpamIPAddressesListParams() res, err := api.Ipam.IpamIPAddressesList(params, nil) if err != nil { return err } for _, ipAddress := range res.GetPayload().Results { if len(ipAddress.Tags) > 0 && (ipAddress.Tags[0] == &models.NestedTag{Name: strToPtr("acctest"), Slug: strToPtr("acctest")}) { deleteParams := ipam.NewIpamIPAddressesDeleteParams().WithID(ipAddress.ID) _, err := api.Ipam.IpamIPAddressesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted an ip address") } } return nil }, }) } func TestAccNetboxIPAddress_dnsNameCase(t *testing.T) { testIP := "1.1.1.253/32" testSlug := "ipaddr_dns_case" testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { // Mixed-case dns_name: NetBox stores it as lowercase. // Without DiffSuppressFunc the post-apply plan check detects // drift and fails with "the plan was not empty" (#828). Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` resource "netbox_ip_address" "test" { ip_address = "%s" status = "active" dns_name = "Mytest.Example.Com" }`, testIP), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_ip_address.test", "dns_name", "mytest.example.com"), ), }, }, }) } ================================================ FILE: netbox/resource_netbox_ip_range.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var resourceNetboxIPRangeStatusOptions = []string{"active", "reserved", "deprecated"} func resourceNetboxIPRange() *schema.Resource { return &schema.Resource{ Create: resourceNetboxIPRangeCreate, Read: resourceNetboxIPRangeRead, Update: resourceNetboxIPRangeUpdate, Delete: resourceNetboxIPRangeDelete, Description: `:meta:subcategory:IP Address Management (IPAM):From the [official documentation](https://docs.netbox.dev/en/stable/features/ipam/#ip-ranges): > This model represents an arbitrary range of individual IPv4 or IPv6 addresses, inclusive of its starting and ending addresses. For instance, the range 192.0.2.10 to 192.0.2.20 has eleven members. (The total member count is available as the size property on an IPRange instance.) Like prefixes and IP addresses, each IP range may optionally be assigned to a VRF and/or tenant.`, Schema: map[string]*schema.Schema{ "start_address": { Type: schema.TypeString, Required: true, }, "end_address": { Type: schema.TypeString, Required: true, }, "status": { Type: schema.TypeString, Optional: true, Default: "active", ValidateFunc: validation.StringInSlice(resourceNetboxIPRangeStatusOptions, false), Description: buildValidValueDescription(resourceNetboxIPRangeStatusOptions), }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, "role_id": { Type: schema.TypeInt, Optional: true, }, "vrf_id": { Type: schema.TypeInt, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, "size": { Type: schema.TypeInt, Computed: true, Description: "The total member count of the IP range", }, tagsKey: tagsSchema, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxIPRangeCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableIPRange{} startAddress := d.Get("start_address").(string) endAddress := d.Get("end_address").(string) status := d.Get("status").(string) description := d.Get("description").(string) data.StartAddress = &startAddress data.EndAddress = &endAddress data.Status = status data.Description = description var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } params := ipam.NewIpamIPRangesCreateParams().WithData(&data) res, err := api.Ipam.IpamIPRangesCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxIPRangeUpdate(d, m) } func resourceNetboxIPRangeRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamIPRangesReadParams().WithID(id) res, err := api.Ipam.IpamIPRangesRead(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamIPRangesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } if res.GetPayload().StartAddress != nil { d.Set("start_address", res.GetPayload().StartAddress) } if res.GetPayload().EndAddress != nil { d.Set("end_address", res.GetPayload().EndAddress) } if res.GetPayload().Status != nil { d.Set("status", res.GetPayload().Status.Value) } if res.GetPayload().Vrf != nil { d.Set("vrf_id", res.GetPayload().Vrf.ID) } if res.GetPayload().Description != "" { d.Set("description", res.GetPayload().Description) } if res.GetPayload().Tenant != nil { d.Set("tenant_id", res.GetPayload().Tenant.ID) } if res.GetPayload().Role != nil { d.Set("role_id", res.GetPayload().Role.ID) } if res.GetPayload().Size != 0 { d.Set("size", res.GetPayload().Size) } api.readTags(d, res.GetPayload().Tags) cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } return nil } func resourceNetboxIPRangeUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableIPRange{} startAddress := d.Get("start_address").(string) endAddress := d.Get("end_address").(string) status := d.Get("status").(string) description := d.Get("description").(string) data.StartAddress = &startAddress data.EndAddress = &endAddress data.Status = status data.Description = description if vrfID, ok := d.GetOk("vrf_id"); ok { data.Vrf = int64ToPtr(int64(vrfID.(int))) } if tenantID, ok := d.GetOk("tenant_id"); ok { data.Tenant = int64ToPtr(int64(tenantID.(int))) } if roleID, ok := d.GetOk("role_id"); ok { data.Role = int64ToPtr(int64(roleID.(int))) } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } if cf, ok := d.GetOk(customFieldsKey); ok { data.CustomFields = cf } params := ipam.NewIpamIPRangesUpdateParams().WithID(id).WithData(&data) _, err = api.Ipam.IpamIPRangesUpdate(params, nil) if err != nil { return err } return resourceNetboxIPRangeRead(d, m) } func resourceNetboxIPRangeDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamIPRangesDeleteParams().WithID(id) _, err := api.Ipam.IpamIPRangesDelete(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamIPRangesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_ip_range_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxIPRangeFullDependencies(testStartAddress string, testSlug string) string { return fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_vrf" "test" { name = "%[1]s" } resource "netbox_ipam_role" "test" { name = "%[1]s" slug = "%[2]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } `, testStartAddress, testSlug) } func TestAccNetboxIpRange_basic(t *testing.T) { testSlug := "range_basic" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) testStartAddress := "10.0.0.1/24" testEndAddress := "10.0.0.50/24" testDescription := "Test Description" resource.Test(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testAccNetboxIPRangeFullDependencies(testName, randomSlug) + fmt.Sprintf(` resource "netbox_ip_range" "test_basic" { start_address = "%s" end_address = "%s" status = "active" description = "%s" tags = [] }`, testStartAddress, testEndAddress, testDescription), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_ip_range.test_basic", "start_address", testStartAddress), resource.TestCheckResourceAttr("netbox_ip_range.test_basic", "end_address", testEndAddress), resource.TestCheckResourceAttr("netbox_ip_range.test_basic", "status", "active"), resource.TestCheckResourceAttr("netbox_ip_range.test_basic", "description", testDescription), resource.TestCheckResourceAttr("netbox_ip_range.test_basic", "tags.#", "0"), resource.TestCheckResourceAttr("netbox_ip_range.test_basic", "size", "50"), ), }, { Config: testAccNetboxIPRangeFullDependencies(testName, randomSlug) + fmt.Sprintf(` resource "netbox_ip_range" "test_basic" { start_address = "%s" end_address = "%s" vrf_id = netbox_vrf.test.id tenant_id = netbox_tenant.test.id status = "active" description = "%s" tags = [] }`, testStartAddress, testEndAddress, testDescription), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_ip_range.test_basic", "start_address", testStartAddress), resource.TestCheckResourceAttr("netbox_ip_range.test_basic", "end_address", testEndAddress), resource.TestCheckResourceAttr("netbox_ip_range.test_basic", "description", testDescription), resource.TestCheckResourceAttr("netbox_ip_range.test_basic", "status", "active"), resource.TestCheckResourceAttrPair("netbox_ip_range.test_basic", "vrf_id", "netbox_vrf.test", "id"), resource.TestCheckResourceAttrPair("netbox_ip_range.test_basic", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttr("netbox_ip_range.test_basic", "tags.#", "0"), ), }, { ResourceName: "netbox_ip_range.test_basic", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxIpRange_with_dependencies(t *testing.T) { testSlug := "range_with_dependencies" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) testStartAddress := "10.0.0.51/24" testEndAddress := "10.0.0.100/24" testDescription := "Test Description" resource.Test(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testAccNetboxIPRangeFullDependencies(testName, randomSlug) + fmt.Sprintf(` resource "netbox_ip_range" "test_with_dependencies" { start_address = "%s" end_address = "%s" description = "%s" vrf_id = netbox_vrf.test.id status = "active" tenant_id = netbox_tenant.test.id role_id = netbox_ipam_role.test.id tags = [netbox_tag.test.name] }`, testStartAddress, testEndAddress, testDescription), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_ip_range.test_with_dependencies", "start_address", testStartAddress), resource.TestCheckResourceAttr("netbox_ip_range.test_with_dependencies", "end_address", testEndAddress), resource.TestCheckResourceAttr("netbox_ip_range.test_with_dependencies", "description", testDescription), resource.TestCheckResourceAttr("netbox_ip_range.test_with_dependencies", "status", "active"), resource.TestCheckResourceAttrPair("netbox_ip_range.test_with_dependencies", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("netbox_ip_range.test_with_dependencies", "vrf_id", "netbox_vrf.test", "id"), resource.TestCheckResourceAttrPair("netbox_ip_range.test_with_dependencies", "role_id", "netbox_ipam_role.test", "id"), resource.TestCheckResourceAttr("netbox_ip_range.test_with_dependencies", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_ip_range.test_with_dependencies", "tags.0", testName), resource.TestCheckResourceAttr("netbox_ip_range.test_with_dependencies", "size", "50"), ), }, { ResourceName: "netbox_ip_range.test_with_dependencies", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxIpRange_cf(t *testing.T) { testSlug := "range_cf" testStartAddress := "10.0.1.1/24" testEndAddress := "10.0.1.50/24" resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_custom_field" "test" { name = "%s" type = "text" weight = 100 content_types = ["ipam.iprange"] } resource "netbox_ip_range" "test_cf" { start_address = "%s" end_address = "%s" status = "active" custom_fields = { "${netbox_custom_field.test.name}" = "test-field" } }`, testSlug, testStartAddress, testEndAddress), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_ip_range.test_cf", "start_address", testStartAddress), resource.TestCheckResourceAttr("netbox_ip_range.test_cf", "end_address", testEndAddress), resource.TestCheckResourceAttr("netbox_ip_range.test_cf", fmt.Sprintf("custom_fields.%s", testSlug), "test-field"), ), }, { ResourceName: "netbox_ip_range.test_cf", ImportState: true, ImportStateVerify: true, }, }, }) } func init() { resource.AddTestSweepers("netbox_ip_range", &resource.Sweeper{ Name: "netbox_ip_range", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := ipam.NewIpamIPRangesListParams() res, err := api.Ipam.IpamIPRangesList(params, nil) if err != nil { return err } for _, r := range res.GetPayload().Results { if strings.HasPrefix(*r.StartAddress, testPrefix) { deleteParams := ipam.NewIpamIPRangesDeleteParams().WithID(r.ID) _, err := api.Ipam.IpamIPRangesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a range") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_ipam_role.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxIpamRole() *schema.Resource { return &schema.Resource{ Create: resourceNetboxIpamRoleCreate, Read: resourceNetboxIpamRoleRead, Update: resourceNetboxIpamRoleUpdate, Delete: resourceNetboxIpamRoleDelete, Description: `:meta:subcategory:IP Address Management (IPAM):From the [official documentation](https://docs.netbox.dev/en/stable/features/ipam/#prefixvlan-roles): > A role indicates the function of a prefix or VLAN. For example, you might define Data, Voice, and Security roles. Generally, a prefix will be assigned the same functional role as the VLAN to which it is assigned (if any).`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "weight": { Type: schema.TypeInt, Optional: true, ValidateFunc: validation.IntBetween(0, 32767), }, "description": { Type: schema.TypeString, Optional: true, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxIpamRoleCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.Role{} name := d.Get("name").(string) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } weight := int64(d.Get("weight").(int)) description := d.Get("description").(string) data.Name = &name data.Slug = &slug data.Weight = &weight data.Description = description data.Tags = []*models.NestedTag{} params := ipam.NewIpamRolesCreateParams().WithData(&data) res, err := api.Ipam.IpamRolesCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxIpamRoleUpdate(d, m) } func resourceNetboxIpamRoleRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamRolesReadParams().WithID(id) res, err := api.Ipam.IpamRolesRead(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamRolesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } if res.GetPayload().Name != nil { d.Set("name", res.GetPayload().Name) } if res.GetPayload().Slug != nil { d.Set("slug", res.GetPayload().Slug) } if res.GetPayload().Weight != nil { d.Set("weight", res.GetPayload().Weight) } if res.GetPayload().Description != "" { d.Set("description", res.GetPayload().Description) } return nil } func resourceNetboxIpamRoleUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.Role{} name := d.Get("name").(string) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } weight := int64(d.Get("weight").(int)) description := d.Get("description").(string) data.Name = &name data.Slug = &slug data.Weight = &weight data.Description = description data.Tags = []*models.NestedTag{} params := ipam.NewIpamRolesUpdateParams().WithID(id).WithData(&data) _, err := api.Ipam.IpamRolesUpdate(params, nil) if err != nil { return err } return resourceNetboxIpamRoleRead(d, m) } func resourceNetboxIpamRoleDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamRolesDeleteParams().WithID(id) _, err := api.Ipam.IpamRolesDelete(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamRolesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } d.SetId("") return nil } ================================================ FILE: netbox/resource_netbox_ipam_role_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxRole_basic(t *testing.T) { testSlug := "role_basic" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_ipam_role" "test_basic" { name = "%s" slug = "%s" }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_ipam_role.test_basic", "name", testName), resource.TestCheckResourceAttr("netbox_ipam_role.test_basic", "slug", randomSlug), ), }, { ResourceName: "netbox_ipam_role.test_basic", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxRole_extended(t *testing.T) { testSlug := "role_extended" testName := testAccGetTestName(testSlug) testWeight := "55" testDescription := "Test description" randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_ipam_role" "role_extended" { name = "%[1]s" slug = "%[2]s" weight = "%[3]s" description = "%[4]s" }`, testName, randomSlug, testWeight, testDescription), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_ipam_role.role_extended", "name", testName), resource.TestCheckResourceAttr("netbox_ipam_role.role_extended", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_ipam_role.role_extended", "weight", testWeight), resource.TestCheckResourceAttr("netbox_ipam_role.role_extended", "description", testDescription), ), }, { ResourceName: "netbox_ipam_role.role_extended", ImportState: true, ImportStateVerify: true, }, }, }) } func init() { resource.AddTestSweepers("netbox_ipam_role", &resource.Sweeper{ Name: "netbox_ipam_role", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := ipam.NewIpamRolesListParams() res, err := api.Ipam.IpamRolesList(params, nil) if err != nil { return err } for _, role := range res.GetPayload().Results { if strings.HasPrefix(*role.Name, testPrefix) { deleteParams := ipam.NewIpamRolesDeleteParams().WithID(role.ID) _, err := api.Ipam.IpamRolesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a role") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_location.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxLocation() *schema.Resource { return &schema.Resource{ Create: resourceNetboxLocationCreate, Read: resourceNetboxLocationRead, Update: resourceNetboxLocationUpdate, Delete: resourceNetboxLocationDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/features/sites-and-racks/#locations): > Racks and devices can be grouped by location within a site. A location may represent a floor, room, cage, or similar organizational unit. Locations can be nested to form a hierarchy. For example, you may have floors within a site, and rooms within a floor. Each location must have a name that is unique within its parent site and location, if any.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "description": { Type: schema.TypeString, Optional: true, }, "facility": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringLenBetween(1, 50), }, "site_id": { Type: schema.TypeInt, Required: true, }, "parent_id": { Type: schema.TypeInt, Optional: true, }, tagsKey: tagsSchema, "tenant_id": { Type: schema.TypeInt, Optional: true, }, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxLocationCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableLocation{} name := d.Get("name").(string) data.Name = &name slugValue, slugOk := d.GetOk("slug") // Default slug to generated slug if not given if !slugOk { data.Slug = strToPtr(getSlug(name)) } else { data.Slug = strToPtr(slugValue.(string)) } data.Description = getOptionalStr(d, "description", true) data.Facility = getOptionalStr(d, "facility", true) siteIDValue, ok := d.GetOk("site_id") if ok { data.Site = int64ToPtr(int64(siteIDValue.(int))) } parentIDValue, ok := d.GetOk("parent_id") if ok { data.Parent = int64ToPtr(int64(parentIDValue.(int))) } tenantIDValue, ok := d.GetOk("tenant_id") if ok { data.Tenant = int64ToPtr(int64(tenantIDValue.(int))) } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimLocationsCreateParams().WithData(&data) res, err := api.Dcim.DcimLocationsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxLocationRead(d, m) } func resourceNetboxLocationRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimLocationsReadParams().WithID(id) res, err := api.Dcim.DcimLocationsRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimLocationsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } location := res.GetPayload() d.Set("name", location.Name) d.Set("slug", location.Slug) d.Set("description", location.Description) d.Set("facility", location.Facility) if res.GetPayload().Site != nil { d.Set("site_id", res.GetPayload().Site.ID) } else { d.Set("site_id", nil) } if res.GetPayload().Parent != nil { d.Set("parent_id", res.GetPayload().Parent.ID) } else { d.Set("parent_id", nil) } if res.GetPayload().Tenant != nil { d.Set("tenant_id", res.GetPayload().Tenant.ID) } else { d.Set("tenant_id", nil) } cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxLocationUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableLocation{} name := d.Get("name").(string) data.Name = &name slugValue, slugOk := d.GetOk("slug") // Default slug to generated slug if not given if !slugOk { data.Slug = strToPtr(getSlug(name)) } else { data.Slug = strToPtr(slugValue.(string)) } data.Description = getOptionalStr(d, "description", true) data.Facility = getOptionalStr(d, "facility", true) siteIDValue, ok := d.GetOk("site_id") if ok { data.Site = int64ToPtr(int64(siteIDValue.(int))) } parentIDValue := d.Get("parent_id") data.Parent = int64ToPtr(int64(parentIDValue.(int))) // remove parent (set null) if we got zero ID if parentIDValue == 0 { data.Parent = nil } tenantIDValue, ok := d.GetOk("tenant_id") if ok { data.Tenant = int64ToPtr(int64(tenantIDValue.(int))) } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } cf, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = cf } params := dcim.NewDcimLocationsPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimLocationsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxLocationRead(d, m) } func resourceNetboxLocationDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimLocationsDeleteParams().WithID(id) _, err := api.Dcim.DcimLocationsDelete(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimLocationsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_location_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxLocation_basic(t *testing.T) { testSlug := "location_basic" testName := testAccGetTestName(testSlug) testNameSub := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) randomSlugSub := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_site" "test" { name = "%[1]s" } resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_location" "test" { name = "%[1]s" slug = "%[2]s" description = "my-description" facility = "Building B" site_id = netbox_site.test.id tenant_id = netbox_tenant.test.id } resource "netbox_location" "test-sub" { name = "%[3]s" slug = "%[4]s" description = "my-description" parent_id = netbox_location.test.id site_id = netbox_site.test.id }`, testName, randomSlug, testNameSub, randomSlugSub), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_location.test", "name", testName), resource.TestCheckResourceAttr("netbox_location.test", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_location.test", "description", "my-description"), resource.TestCheckResourceAttr("netbox_location.test", "facility", "Building B"), resource.TestCheckResourceAttrPair("netbox_location.test", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttrPair("netbox_location.test", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("netbox_location.test", "id", "netbox_location.test-sub", "parent_id"), ), }, { Config: fmt.Sprintf(` resource "netbox_site" "test" { name = "%[1]s" } resource "netbox_site" "test_2" { name = "%[1]s_b" } resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_location" "test" { name = "%[1]s" slug = "%[2]s" site_id = netbox_site.test_2.id tenant_id = netbox_tenant.test.id }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_location.test", "name", testName), resource.TestCheckResourceAttr("netbox_location.test", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_location.test", "description", ""), resource.TestCheckResourceAttr("netbox_location.test", "facility", ""), ), }, { ResourceName: "netbox_location.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxLocation_updateParent(t *testing.T) { testSlug := "loc_upd_parent" testName := testAccGetTestName(testSlug) testNameSub := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) randomSlugSub := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testAccNetboxLocationUpdateParent1(testName, randomSlug, testNameSub, randomSlugSub), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_location.test", "name", testName), resource.TestCheckResourceAttr("netbox_location.test", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_location.test", "description", "my-description"), resource.TestCheckResourceAttrPair("netbox_location.test", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttrPair("netbox_location.test", "id", "netbox_location.test_sub", "parent_id"), ), }, { Config: testAccNetboxLocationUpdateParent2(testName, randomSlug, testNameSub, randomSlugSub), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_location.test", "name", testName), resource.TestCheckResourceAttr("netbox_location.test", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_location.test", "description", "my-description"), resource.TestCheckResourceAttr("netbox_location.test_sub", "parent_id", "0"), ), }, }, }) } func testAccNetboxLocationUpdateParent1(testName string, randomSlug string, testNameSub string, randomSlugSub string) string { return fmt.Sprintf(` resource "netbox_site" "test" { name = "%[1]s" } resource "netbox_location" "test" { name = "%[1]s" slug = "%[2]s" description = "my-description" site_id = netbox_site.test.id } resource "netbox_location" "test_sub" { name = "%[3]s" slug = "%[4]s" description = "my-description" parent_id = netbox_location.test.id site_id = netbox_site.test.id }`, testName, randomSlug, testNameSub, randomSlugSub) } func testAccNetboxLocationUpdateParent2(testName string, randomSlug string, testNameSub string, randomSlugSub string) string { return fmt.Sprintf(` resource "netbox_site" "test" { name = "%[1]s" } resource "netbox_location" "test" { name = "%[1]s" slug = "%[2]s" description = "my-description" site_id = netbox_site.test.id } resource "netbox_location" "test_sub" { name = "%[3]s" slug = "%[4]s" description = "my-description" parent_id = "0" site_id = netbox_site.test.id }`, testName, randomSlug, testNameSub, randomSlugSub) } func init() { resource.AddTestSweepers("netbox_location", &resource.Sweeper{ Name: "netbox_location", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimLocationsListParams() res, err := api.Dcim.DcimLocationsList(params, nil) if err != nil { return err } for _, Location := range res.GetPayload().Results { if strings.HasPrefix(*Location.Name, testPrefix) { deleteParams := dcim.NewDcimLocationsDeleteParams().WithID(Location.ID) _, err := api.Dcim.DcimLocationsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a location") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_mac_address.go ================================================ package netbox import ( "strconv" "strings" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var resourceNetboxMACAddressObjectTypeOptions = []string{"virtualization.vminterface", "dcim.interface"} func resourceNetboxMACAddress() *schema.Resource { return &schema.Resource{ Create: resourceNetboxMACAddressCreate, Read: resourceNetboxMACAddressRead, Update: resourceNetboxMACAddressUpdate, Delete: resourceNetboxMACAddressDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://netboxlabs.com/docs/netbox/models/dcim/macaddress/): > A MAC address object in NetBox comprises a single Ethernet link layer address, and represents a MAC address as reported by or assigned to a network interface. MAC addresses can be assigned to device and virtual machine interfaces. A MAC address can be specified as the primary MAC address for a given device or VM interface.`, Schema: map[string]*schema.Schema{ "mac_address": { Type: schema.TypeString, Required: true, // Netbox converts MAC addresses always to uppercase DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { return strings.EqualFold(old, new) }, }, "interface_id": { Type: schema.TypeInt, Optional: true, RequiredWith: []string{"object_type"}, }, "object_type": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringInSlice(resourceNetboxMACAddressObjectTypeOptions, false), Description: buildValidValueDescription(resourceNetboxMACAddressObjectTypeOptions), RequiredWith: []string{"interface_id"}, }, "virtual_machine_interface_id": { Type: schema.TypeInt, Optional: true, ConflictsWith: []string{"interface_id", "device_interface_id"}, }, "device_interface_id": { Type: schema.TypeInt, Optional: true, ConflictsWith: []string{"interface_id", "virtual_machine_interface_id"}, }, "description": { Type: schema.TypeString, Optional: true, }, "comments": { Type: schema.TypeString, Optional: true, }, tagsKey: tagsSchema, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxMACAddressCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.MACAddress{} data.MacAddress = strToPtr(d.Get("mac_address").(string)) data.Description = strToPtr(getOptionalStr(d, "description", false)) data.Comments = strToPtr(getOptionalStr(d, "comments", false)) vmInterfaceID := getOptionalInt(d, "virtual_machine_interface_id") deviceInterfaceID := getOptionalInt(d, "device_interface_id") interfaceID := getOptionalInt(d, "interface_id") switch { case vmInterfaceID != nil: data.AssignedObjectType = strToPtr("virtualization.vminterface") data.AssignedObjectID = vmInterfaceID case deviceInterfaceID != nil: data.AssignedObjectType = strToPtr("dcim.interface") data.AssignedObjectID = deviceInterfaceID // if interfaceID is given, object_type must be set as well case interfaceID != nil: data.AssignedObjectType = strToPtr(d.Get("object_type").(string)) data.AssignedObjectID = interfaceID // default = mac address is not linked to anything default: data.AssignedObjectType = strToPtr("") data.AssignedObjectID = nil } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } cf, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = cf } params := dcim.NewDcimMacAddressesCreateParams().WithData(&data) res, err := api.Dcim.DcimMacAddressesCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxMACAddressRead(d, m) } func resourceNetboxMACAddressRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimMacAddressesReadParams().WithID(id) res, err := api.Dcim.DcimMacAddressesRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimMacAddressesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } macAddress := res.GetPayload() if macAddress.AssignedObjectID != nil && macAddress.AssignedObjectType != nil { vmInterfaceID := getOptionalInt(d, "virtual_machine_interface_id") deviceInterfaceID := getOptionalInt(d, "device_interface_id") interfaceID := getOptionalInt(d, "interface_id") switch { case vmInterfaceID != nil && *macAddress.AssignedObjectType == "virtualization.vminterface": d.Set("virtual_machine_interface_id", macAddress.AssignedObjectID) case deviceInterfaceID != nil && *macAddress.AssignedObjectType == "dcim.interface": d.Set("device_interface_id", macAddress.AssignedObjectID) // if interfaceID is given, object_type must be set as well case interfaceID != nil: d.Set("object_type", macAddress.AssignedObjectType) d.Set("interface_id", macAddress.AssignedObjectID) } } else { d.Set("interface_id", nil) d.Set("object_type", "") } d.Set("mac_address", macAddress.MacAddress) d.Set("description", macAddress.Description) d.Set("comments", macAddress.Comments) api.readTags(d, macAddress.Tags) cf := getCustomFields(macAddress.CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } return nil } func resourceNetboxMACAddressUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.MACAddress{} data.MacAddress = strToPtr(d.Get("mac_address").(string)) data.Description = strToPtr(getOptionalStr(d, "description", false)) data.Comments = strToPtr(getOptionalStr(d, "comments", false)) vmInterfaceID := getOptionalInt(d, "virtual_machine_interface_id") deviceInterfaceID := getOptionalInt(d, "device_interface_id") interfaceID := getOptionalInt(d, "interface_id") switch { case vmInterfaceID != nil: data.AssignedObjectType = strToPtr("virtualization.vminterface") data.AssignedObjectID = vmInterfaceID case deviceInterfaceID != nil: data.AssignedObjectType = strToPtr("dcim.interface") data.AssignedObjectID = deviceInterfaceID // if interfaceID is given, object_type must be set as well case interfaceID != nil: data.AssignedObjectType = strToPtr(d.Get("object_type").(string)) data.AssignedObjectID = interfaceID // default = mac address is not linked to anything default: data.AssignedObjectType = strToPtr("") data.AssignedObjectID = nil } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } cf, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = cf } params := dcim.NewDcimMacAddressesPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimMacAddressesPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxMACAddressRead(d, m) } func resourceNetboxMACAddressDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimMacAddressesDeleteParams().WithID(id) _, err := api.Dcim.DcimMacAddressesDelete(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimMacAddressesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_mac_address_test.go ================================================ package netbox import ( "fmt" "log" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxMACAddressFullVmDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_vrf" "test" { name = "%[1]s" } resource "netbox_cluster_type" "test" { name = "%[1]s" } resource "netbox_cluster" "test" { name = "%[1]s" cluster_type_id = netbox_cluster_type.test.id } resource "netbox_virtual_machine" "test" { name = "%[1]s" cluster_id = netbox_cluster.test.id } resource "netbox_interface" "test" { name = "%[1]s" virtual_machine_id = netbox_virtual_machine.test.id }`, testName) } func testAccNetboxMACAddressFullDeviceDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device" "test" { name = "%[1]s" site_id = netbox_site.test.id device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id } resource "netbox_device_interface" "test" { name = "%[1]s" device_id = netbox_device.test.id type = "1000base-t" }`, testName) } func TestAccNetboxMACAddress_standalone(t *testing.T) { testSlug := "mac-addr" macAddress := "00:1A:2B:3C:4D:5E" resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_mac_address" "test" { mac_address = "%[1]s" description = "%[2]s" comments = "%[2]s" } `, macAddress, testSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_mac_address.test", "mac_address", macAddress), resource.TestCheckResourceAttr("netbox_mac_address.test", "description", testSlug), resource.TestCheckResourceAttr("netbox_mac_address.test", "comments", testSlug), ), }, { Config: fmt.Sprintf(` resource "netbox_mac_address" "test" { mac_address = "%s" }`, macAddress), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_mac_address.test", "mac_address", macAddress), ), }, { ResourceName: "netbox_mac_address.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxMACAddress_customFields(t *testing.T) { testSlug := "mac-addr_cf" macAddress := "00:1A:2B:3F:4D:5E" resource.Test(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_custom_field" "test" { name = "mac_custom_field" type = "text" content_types = ["dcim.macaddress"] } resource "netbox_mac_address" "test" { mac_address = "%[1]s" description = "%[2]s" comments = "%[2]s" custom_fields = {"${netbox_custom_field.test.name}" = "foomac"} } `, macAddress, testSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_mac_address.test", "custom_fields.mac_custom_field", "foomac"), ), }, { Config: fmt.Sprintf(` resource "netbox_mac_address" "test" { mac_address = "%s" }`, macAddress), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_mac_address.test", "mac_address", macAddress), ), }, { ResourceName: "netbox_mac_address.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxMACAddress_deviceByFieldName(t *testing.T) { testSlug := "mac-addr-dev-fn" macAddress := "01:1A:2B:3C:4D:5E" macAddress2 := "01:1A:2B:7C:4D:5E" resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testAccNetboxMACAddressFullDeviceDependencies(testSlug) + fmt.Sprintf(` resource "netbox_mac_address" "test" { mac_address = "%[1]s" device_interface_id = netbox_device_interface.test.id description = "%[3]s" } resource "netbox_mac_address" "test2" { mac_address = "%[2]s" device_interface_id = netbox_device_interface.test.id description = "%[3]s" comments = "%[3]s" }`, macAddress, macAddress2, testSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_mac_address.test", "mac_address", macAddress), resource.TestCheckResourceAttr("netbox_mac_address.test", "description", testSlug), resource.TestCheckResourceAttrPair("netbox_mac_address.test", "device_interface_id", "netbox_device_interface.test", "id"), ), }, { RefreshState: true, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_device_interface.test", "mac_address", macAddress), resource.TestCheckResourceAttr("netbox_device_interface.test", "mac_addresses.#", "2"), resource.TestCheckResourceAttrSet("netbox_device_interface.test", "mac_addresses.0.id"), resource.TestCheckResourceAttrSet("netbox_device_interface.test", "mac_addresses.0.mac_address"), resource.TestCheckResourceAttrSet("netbox_device_interface.test", "mac_addresses.0.description"), ), }, { ResourceName: "netbox_mac_address.test", ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{"device_interface_id"}, }, }, }) } func TestAccNetboxMACAddress_vmByFieldName(t *testing.T) { testSlug := "mac-addr-vm-fn" macAddress := "02:1A:2B:3C:4D:5E" resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testAccNetboxMACAddressFullVmDependencies(testSlug) + fmt.Sprintf(` resource "netbox_mac_address" "test" { mac_address = "%s" virtual_machine_interface_id = netbox_interface.test.id description = "%s" }`, macAddress, testSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_mac_address.test", "mac_address", macAddress), resource.TestCheckResourceAttr("netbox_mac_address.test", "description", testSlug), resource.TestCheckResourceAttrPair("netbox_mac_address.test", "virtual_machine_interface_id", "netbox_interface.test", "id"), ), }, { ResourceName: "netbox_mac_address.test", ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{"virtual_machine_interface_id"}, }, }, }) } func TestAccNetboxMACAddress_deviceByObjectType(t *testing.T) { testSlug := "mac-addr-dev-ot" macAddress := "03:1A:2B:3C:4D:5E" resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testAccNetboxMACAddressFullDeviceDependencies(testSlug) + fmt.Sprintf(` resource "netbox_mac_address" "test" { mac_address = "%s" object_type = "dcim.interface" interface_id = netbox_device_interface.test.id description = "%s" }`, macAddress, testSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_mac_address.test", "mac_address", macAddress), resource.TestCheckResourceAttr("netbox_mac_address.test", "description", testSlug), resource.TestCheckResourceAttr("netbox_mac_address.test", "object_type", "dcim.interface"), resource.TestCheckResourceAttrPair("netbox_mac_address.test", "interface_id", "netbox_device_interface.test", "id"), ), }, { ResourceName: "netbox_mac_address.test", ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{"interface_id", "object_type"}, }, }, }) } func TestAccNetboxMACAddress_vmByObjectType(t *testing.T) { testSlug := "mac-addr-vm-ot" macAddress := "04:1A:2B:3C:4D:5E" resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testAccNetboxMACAddressFullVmDependencies(testSlug) + fmt.Sprintf(` resource "netbox_mac_address" "test" { mac_address = "%s" object_type = "virtualization.vminterface" interface_id = netbox_interface.test.id description = "%s" }`, macAddress, testSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_mac_address.test", "mac_address", macAddress), resource.TestCheckResourceAttr("netbox_mac_address.test", "description", testSlug), resource.TestCheckResourceAttr("netbox_mac_address.test", "object_type", "virtualization.vminterface"), resource.TestCheckResourceAttrPair("netbox_mac_address.test", "interface_id", "netbox_interface.test", "id"), ), }, { ResourceName: "netbox_mac_address.test", ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{"interface_id", "object_type"}, }, }, }) } func init() { resource.AddTestSweepers("netbox_mac_address", &resource.Sweeper{ Name: "netbox_mac_address", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimMacAddressesListParams() res, err := api.Dcim.DcimMacAddressesList(params, nil) if err != nil { return err } for _, macAddress := range res.GetPayload().Results { if len(macAddress.Tags) > 0 && (macAddress.Tags[0] == &models.NestedTag{Name: strToPtr("acctest"), Slug: strToPtr("acctest")}) { deleteParams := dcim.NewDcimMacAddressesDeleteParams().WithID(macAddress.ID) _, err := api.Dcim.DcimMacAddressesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a mac address") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_manufacturer.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxManufacturer() *schema.Resource { return &schema.Resource{ Create: resourceNetboxManufacturerCreate, Read: resourceNetboxManufacturerRead, Update: resourceNetboxManufacturerUpdate, Delete: resourceNetboxManufacturerDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/features/device-types/#manufacturers): > A manufacturer represents the "make" of a device; e.g. Cisco or Dell. Each device type must be assigned to a manufacturer. (Inventory items and platforms may also be associated with manufacturers.) Each manufacturer must have a unique name and may have a description assigned to it.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxManufacturerCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.Manufacturer{} name := d.Get("name").(string) data.Name = &name slugValue, slugOk := d.GetOk("slug") // Default slug to generated slug if not given if !slugOk { data.Slug = strToPtr(getSlug(name)) } else { data.Slug = strToPtr(slugValue.(string)) } data.Tags = []*models.NestedTag{} params := dcim.NewDcimManufacturersCreateParams().WithData(&data) res, err := api.Dcim.DcimManufacturersCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxManufacturerRead(d, m) } func resourceNetboxManufacturerRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimManufacturersReadParams().WithID(id) res, err := api.Dcim.DcimManufacturersRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimManufacturersReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } d.Set("name", res.GetPayload().Name) d.Set("slug", res.GetPayload().Slug) return nil } func resourceNetboxManufacturerUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.Manufacturer{} name := d.Get("name").(string) data.Name = &name slugValue, slugOk := d.GetOk("slug") // Default slug to generated slug if not given if !slugOk { data.Slug = strToPtr(getSlug(name)) } else { data.Slug = strToPtr(slugValue.(string)) } data.Tags = []*models.NestedTag{} params := dcim.NewDcimManufacturersPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Dcim.DcimManufacturersPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxManufacturerRead(d, m) } func resourceNetboxManufacturerDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimManufacturersDeleteParams().WithID(id) _, err := api.Dcim.DcimManufacturersDelete(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimManufacturersDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_manufacturer_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxManufacturer_basic(t *testing.T) { testSlug := "manufacturer" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_manufacturer" "test" { name = "%s" slug = "%s" }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_manufacturer.test", "name", testName), resource.TestCheckResourceAttr("netbox_manufacturer.test", "slug", randomSlug), ), }, { ResourceName: "netbox_manufacturer.test", ImportState: true, ImportStateVerify: true, }, }, }) } func init() { resource.AddTestSweepers("netbox_manufacturer", &resource.Sweeper{ Name: "netbox_manufacturer", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimManufacturersListParams() res, err := api.Dcim.DcimManufacturersList(params, nil) if err != nil { return err } for _, manufacturer := range res.GetPayload().Results { if strings.HasPrefix(*manufacturer.Name, testPrefix) { deleteParams := dcim.NewDcimManufacturersDeleteParams().WithID(manufacturer.ID) _, err := api.Dcim.DcimManufacturersDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a device type") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_module.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxModule() *schema.Resource { return &schema.Resource{ Create: resourceNetboxModuleCreate, Read: resourceNetboxModuleRead, Update: resourceNetboxModuleUpdate, Delete: resourceNetboxModuleDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/module/): > A module is a field-replaceable hardware component installed within a device which houses its own child components. The most common example is a chassis-based router or switch. Similar to devices, modules are instantiated from module types, and any components associated with the module type are automatically instantiated on the new model. Each module must be installed within a module bay on a device, and each module bay may have only one module installed in it.`, Schema: map[string]*schema.Schema{ "device_id": { Type: schema.TypeInt, Required: true, }, "module_bay_id": { Type: schema.TypeInt, Required: true, }, "module_type_id": { Type: schema.TypeInt, Required: true, }, "status": { Type: schema.TypeString, Required: true, Description: "One of [offline, active, planned, staged, failed, decommissioning]", ValidateFunc: validation.StringInSlice([]string{"offline", "active", "planned", "staged", "failed", "decommissioning"}, false), }, "serial": { Type: schema.TypeString, Optional: true, }, "asset_tag": { Type: schema.TypeString, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, "comments": { Type: schema.TypeString, Optional: true, }, tagsKey: tagsSchema, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxModuleCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableModule{ Device: int64ToPtr(int64(d.Get("device_id").(int))), ModuleBay: int64ToPtr(int64(d.Get("module_bay_id").(int))), ModuleType: int64ToPtr(int64(d.Get("module_type_id").(int))), Status: d.Get("status").(string), Serial: getOptionalStr(d, "serial", false), Description: getOptionalStr(d, "description", false), Comments: getOptionalStr(d, "comments", false), } if assetTag := getOptionalStr(d, "asset_tag", false); assetTag != "" { data.AssetTag = &assetTag } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimModulesCreateParams().WithData(&data) res, err := api.Dcim.DcimModulesCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxModuleRead(d, m) } func resourceNetboxModuleRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimModulesReadParams().WithID(id) res, err := api.Dcim.DcimModulesRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimModulesReadDefault); ok { if errresp.Code() == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } module := res.GetPayload() if module.Device != nil { d.Set("device_id", module.Device.ID) } else { d.Set("device_id", nil) } if module.ModuleBay != nil { d.Set("module_bay_id", module.ModuleBay.ID) } else { d.Set("module_bay_id", nil) } if module.ModuleType != nil { d.Set("module_type_id", module.ModuleType.ID) } else { d.Set("module_type_id", nil) } if module.Status != nil { d.Set("status", module.Status.Value) } else { d.Set("status", nil) } d.Set("serial", module.Serial) d.Set("asset_tag", module.AssetTag) d.Set("description", module.Description) d.Set("comments", module.Comments) cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxModuleUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableModule{ Device: int64ToPtr(int64(d.Get("device_id").(int))), ModuleBay: int64ToPtr(int64(d.Get("module_bay_id").(int))), ModuleType: int64ToPtr(int64(d.Get("module_type_id").(int))), Status: d.Get("status").(string), Serial: getOptionalStr(d, "serial", true), Description: getOptionalStr(d, "description", true), Comments: getOptionalStr(d, "comments", true), } if assetTag := getOptionalStr(d, "asset_tag", false); assetTag != "" { data.AssetTag = &assetTag } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimModulesPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimModulesPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxModuleRead(d, m) } func resourceNetboxModuleDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimModulesDeleteParams().WithID(id) _, err := api.Dcim.DcimModulesDelete(params, nil) if err != nil { return err } return nil } ================================================ FILE: netbox/resource_netbox_module_test.go ================================================ package netbox import ( "fmt" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" log "github.com/sirupsen/logrus" ) func testAccNetboxModuleFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_tag" "test" { name = "%[1]sa" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_module_bay" "test" { device_id = netbox_device.test.id name = "%[1]s" } resource "netbox_module_type" "test" { manufacturer_id = netbox_manufacturer.test.id model = "%[1]s" }`, testName) } func TestAccNetboxModule_basic(t *testing.T) { testSlug := "module_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckModuleDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxModuleFullDependencies(testName) + fmt.Sprintf(` resource "netbox_module" "test" { device_id = netbox_device.test.id module_bay_id = netbox_device_module_bay.test.id module_type_id = netbox_module_type.test.id status = "active" serial = "%[1]s_serial" asset_tag = "%[1]s_asset" description = "%[1]s_description" comments = "%[1]s_comments" tags = [netbox_tag.test.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_module.test", "status", "active"), resource.TestCheckResourceAttr("netbox_module.test", "serial", testName+"_serial"), resource.TestCheckResourceAttr("netbox_module.test", "asset_tag", testName+"_asset"), resource.TestCheckResourceAttr("netbox_module.test", "description", testName+"_description"), resource.TestCheckResourceAttr("netbox_module.test", "comments", testName+"_comments"), resource.TestCheckResourceAttr("netbox_module.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_module.test", "tags.0", testName+"a"), resource.TestCheckResourceAttrPair("netbox_module.test", "device_id", "netbox_device.test", "id"), resource.TestCheckResourceAttrPair("netbox_module.test", "module_bay_id", "netbox_device_module_bay.test", "id"), resource.TestCheckResourceAttrPair("netbox_module.test", "module_type_id", "netbox_module_type.test", "id"), ), }, { Config: testAccNetboxModuleFullDependencies(testName) + ` resource "netbox_module" "test" { device_id = netbox_device.test.id module_bay_id = netbox_device_module_bay.test.id module_type_id = netbox_module_type.test.id status = "offline" }`, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_module.test", "status", "offline"), resource.TestCheckResourceAttr("netbox_module.test", "serial", ""), resource.TestCheckResourceAttr("netbox_module.test", "asset_tag", ""), resource.TestCheckResourceAttr("netbox_module.test", "description", ""), resource.TestCheckResourceAttr("netbox_module.test", "comments", ""), resource.TestCheckResourceAttr("netbox_module.test", "tags.#", "0"), resource.TestCheckResourceAttrPair("netbox_module.test", "device_id", "netbox_device.test", "id"), resource.TestCheckResourceAttrPair("netbox_module.test", "module_bay_id", "netbox_device_module_bay.test", "id"), resource.TestCheckResourceAttrPair("netbox_module.test", "module_type_id", "netbox_module_type.test", "id"), ), }, { ResourceName: "netbox_module.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckModuleDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each module // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_module" { continue } // Retrieve our device by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := dcim.NewDcimModulesReadParams().WithID(stateID) _, err := conn.Dcim.DcimModulesRead(params, nil) if err == nil { return fmt.Errorf("module (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*dcim.DcimModulesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func init() { resource.AddTestSweepers("netbox_module", &resource.Sweeper{ Name: "netbox_module", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimModulesListParams() res, err := api.Dcim.DcimModulesList(params, nil) if err != nil { return err } for _, module := range res.GetPayload().Results { if strings.HasPrefix(*module.ModuleType.Model, testPrefix) { deleteParams := dcim.NewDcimModulesDeleteParams().WithID(module.ID) _, err := api.Dcim.DcimModulesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a module") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_module_type.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxModuleType() *schema.Resource { return &schema.Resource{ Create: resourceNetboxModuleTypeCreate, Read: resourceNetboxModuleTypeRead, Update: resourceNetboxModuleTypeUpdate, Delete: resourceNetboxModuleTypeDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/moduletype/): > A module type represents a specific make and model of hardware component which is installable within a device's module bay and has its own child components. For example, consider a chassis-based switch or router with a number of field-replaceable line cards. Each line card has its own model number and includes a certain set of components such as interfaces. Each module type may have a manufacturer, model number, and part number assigned to it.`, Schema: map[string]*schema.Schema{ "manufacturer_id": { Type: schema.TypeInt, Required: true, }, "model": { Type: schema.TypeString, Required: true, }, "part_number": { Type: schema.TypeString, Optional: true, }, "weight": { Type: schema.TypeFloat, Optional: true, }, "weight_unit": { Type: schema.TypeString, Optional: true, RequiredWith: []string{"weight"}, Description: "One of [kg, g, lb, oz]", ValidateFunc: validation.StringInSlice([]string{"kg", "g", "lb", "oz"}, false), }, "description": { Type: schema.TypeString, Optional: true, }, "comments": { Type: schema.TypeString, Optional: true, }, tagsKey: tagsSchema, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxModuleTypeCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableModuleType{ Manufacturer: int64ToPtr(int64(d.Get("manufacturer_id").(int))), Model: strToPtr(d.Get("model").(string)), PartNumber: getOptionalStr(d, "part_number", false), Weight: getOptionalFloat(d, "weight"), WeightUnit: getOptionalStr(d, "weight_unit", false), Description: getOptionalStr(d, "description", false), Comments: getOptionalStr(d, "comments", false), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimModuleTypesCreateParams().WithData(&data) res, err := api.Dcim.DcimModuleTypesCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxModuleTypeRead(d, m) } func resourceNetboxModuleTypeRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimModuleTypesReadParams().WithID(id) res, err := api.Dcim.DcimModuleTypesRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimModuleTypesReadDefault); ok { if errresp.Code() == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } moduleType := res.GetPayload() if moduleType.Manufacturer != nil { d.Set("manufacturer_id", moduleType.Manufacturer.ID) } else { d.Set("manufacturer_id", nil) } d.Set("model", moduleType.Model) d.Set("part_number", moduleType.PartNumber) d.Set("weight", moduleType.Weight) if moduleType.WeightUnit != nil { d.Set("weight_unit", moduleType.WeightUnit.Value) } else { d.Set("weight_unit", nil) } d.Set("description", moduleType.Description) d.Set("comments", moduleType.Comments) cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxModuleTypeUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableModuleType{ Manufacturer: int64ToPtr(int64(d.Get("manufacturer_id").(int))), Model: strToPtr(d.Get("model").(string)), PartNumber: getOptionalStr(d, "part_number", true), Weight: getOptionalFloat(d, "weight"), WeightUnit: getOptionalStr(d, "weight_unit", false), Description: getOptionalStr(d, "description", true), Comments: getOptionalStr(d, "comments", true), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimModuleTypesPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimModuleTypesPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxModuleTypeRead(d, m) } func resourceNetboxModuleTypeDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimModuleTypesDeleteParams().WithID(id) _, err := api.Dcim.DcimModuleTypesDelete(params, nil) if err != nil { return err } return nil } ================================================ FILE: netbox/resource_netbox_module_type_test.go ================================================ package netbox import ( "fmt" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" log "github.com/sirupsen/logrus" ) func testAccNetboxModuleTypeFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_tag" "test" { name = "%[1]sa" } `, testName) } func TestAccNetboxModuleType_basic(t *testing.T) { testSlug := "module_type_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckModuleTypeDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxModuleTypeFullDependencies(testName) + fmt.Sprintf(` resource "netbox_module_type" "test" { manufacturer_id = netbox_manufacturer.test.id model = "%[1]s" part_number = "%[1]s_pn" description = "%[1]s_description" comments = "%[1]s_comments" weight = 1 weight_unit = "kg" tags = [netbox_tag.test.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_module_type.test", "model", testName), resource.TestCheckResourceAttr("netbox_module_type.test", "part_number", testName+"_pn"), resource.TestCheckResourceAttr("netbox_module_type.test", "description", testName+"_description"), resource.TestCheckResourceAttr("netbox_module_type.test", "comments", testName+"_comments"), resource.TestCheckResourceAttr("netbox_module_type.test", "weight", "1"), resource.TestCheckResourceAttr("netbox_module_type.test", "weight_unit", "kg"), resource.TestCheckResourceAttr("netbox_module_type.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_module_type.test", "tags.0", testName+"a"), resource.TestCheckResourceAttrPair("netbox_module_type.test", "manufacturer_id", "netbox_manufacturer.test", "id"), ), }, { Config: testAccNetboxModuleTypeFullDependencies(testName) + fmt.Sprintf(` resource "netbox_module_type" "test" { manufacturer_id = netbox_manufacturer.test.id model = "%[1]s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_module_type.test", "model", testName), resource.TestCheckResourceAttr("netbox_module_type.test", "part_number", ""), resource.TestCheckResourceAttr("netbox_module_type.test", "description", ""), resource.TestCheckResourceAttr("netbox_module_type.test", "comments", ""), resource.TestCheckResourceAttr("netbox_module_type.test", "weight", "0"), resource.TestCheckResourceAttr("netbox_module_type.test", "weight_unit", ""), resource.TestCheckResourceAttr("netbox_module_type.test", "tags.#", "0"), resource.TestCheckResourceAttrPair("netbox_module_type.test", "manufacturer_id", "netbox_manufacturer.test", "id"), ), }, { ResourceName: "netbox_module_type.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckModuleTypeDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each module type // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_module_type" { continue } // Retrieve our device by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := dcim.NewDcimModuleTypesReadParams().WithID(stateID) _, err := conn.Dcim.DcimModuleTypesRead(params, nil) if err == nil { return fmt.Errorf("module type (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*dcim.DcimModuleTypesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func init() { resource.AddTestSweepers("netbox_module_type", &resource.Sweeper{ Name: "netbox_module_type", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimModuleTypesListParams() res, err := api.Dcim.DcimModuleTypesList(params, nil) if err != nil { return err } for _, moduleType := range res.GetPayload().Results { if strings.HasPrefix(*moduleType.Model, testPrefix) { deleteParams := dcim.NewDcimModuleTypesDeleteParams().WithID(moduleType.ID) _, err := api.Dcim.DcimModuleTypesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a module_type") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_permission.go ================================================ package netbox import ( "encoding/json" "strconv" "github.com/fbreckle/go-netbox/netbox/client/users" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxPermission() *schema.Resource { return &schema.Resource{ Create: resourceNetboxPermissionCreate, Read: resourceNetboxPermissionRead, Update: resourceNetboxPermissionUpdate, Delete: resourceNetboxPermissionDelete, Description: `:meta:subcategory:Authentication:This resource manages the object-based permissions for Netbox users, built into the application. > Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type. > For more information, see the [Netbox Object-Based Permissions Docs.](https://docs.netbox.dev/en/stable/administration/permissions/)`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Description: "The name of the permission object.", Required: true, }, "description": { Type: schema.TypeString, Description: "The description of the permission object.", Optional: true, }, "enabled": { Type: schema.TypeBool, Description: "Whether the permission object is enabled or not.", Optional: true, Default: true, }, "object_types": { Type: schema.TypeSet, Description: "A list of object types that the permission object allows access to. Should be in a form " + "the API can accept. For example: `circuits.provider`, `dcim.inventoryitem`, etc.", Required: true, Elem: &schema.Schema{ Type: schema.TypeString, }, }, "groups": { Type: schema.TypeSet, Optional: true, Description: "A list of group IDs that have been assigned to this permission object.", Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "users": { Type: schema.TypeSet, Optional: true, Description: "A list of user IDs that have been assigned to this permission object.", Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "actions": { Type: schema.TypeSet, Required: true, Description: "A list actions that are allowed on the object types. Acceptable values are `view`, `add`, `change`, or `delete`.", Elem: &schema.Schema{ Type: schema.TypeString, }, }, "constraints": { Type: schema.TypeString, Description: "A JSON string of an arbitrary filter used to limit the granted action(s) to a specific subset of objects. " + "For more information on correct syntax, see https://docs.netbox.dev/en/stable/administration/permissions/#constraints ", Optional: true, ValidateFunc: validation.StringIsJSON, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxPermissionCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableObjectPermission{} name := d.Get("name").(string) data.Name = &name data.Description = d.Get("description").(string) data.Enabled = d.Get("enabled").(bool) data.ObjectTypes = toStringList(d.Get("object_types")) data.Groups = toInt64List(d.Get("groups")) data.Users = toInt64List(d.Get("users")) data.Actions = toStringList(d.Get("actions")) var constraints interface{} c := d.Get("constraints").(string) if c == "" { data.Constraints = nil } else { err := json.Unmarshal([]byte(c), &constraints) if err != nil { return err } switch v := constraints.(type) { case []interface{}: data.Constraints = v case map[string]interface{}: data.Constraints = v } } params := users.NewUsersPermissionsCreateParams().WithData(&data) res, err := api.Users.UsersPermissionsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxPermissionRead(d, m) } func resourceNetboxPermissionRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := users.NewUsersPermissionsReadParams().WithID(id) res, err := api.Users.UsersPermissionsRead(params, nil) if err != nil { if errresp, ok := err.(*users.UsersPermissionsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } d.Set("name", res.GetPayload().Name) d.Set("description", res.GetPayload().Description) d.Set("enabled", res.GetPayload().Enabled) d.Set("object_types", res.GetPayload().ObjectTypes) var groups []int for _, v := range res.GetPayload().Groups { groups = append(groups, int(v.ID)) } d.Set("groups", groups) var users []int for _, v := range res.GetPayload().Users { users = append(users, int(v.ID)) } d.Set("users", users) d.Set("actions", res.GetPayload().Actions) if res.GetPayload().Constraints == nil { d.Set("constraints", "") return nil } b, err := json.Marshal(res.GetPayload().Constraints) if err != nil { return err } d.Set("constraints", string(b)) return nil } func resourceNetboxPermissionUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableObjectPermission{} name := d.Get("name").(string) data.Name = &name data.Description = d.Get("description").(string) data.Enabled = d.Get("enabled").(bool) data.ObjectTypes = toStringList(d.Get("object_types")) data.Groups = toInt64List(d.Get("groups")) data.Users = toInt64List(d.Get("users")) data.Actions = toStringList(d.Get("actions")) var constraints interface{} c := d.Get("constraints").(string) if c == "" { data.Constraints = nil } else { err := json.Unmarshal([]byte(c), &constraints) if err != nil { return err } switch v := constraints.(type) { case []interface{}: data.Constraints = v case map[string]interface{}: data.Constraints = v } } params := users.NewUsersPermissionsUpdateParams().WithID(id).WithData(&data) _, err := api.Users.UsersPermissionsUpdate(params, nil) if err != nil { return err } return resourceNetboxPermissionRead(d, m) } func resourceNetboxPermissionDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := users.NewUsersPermissionsDeleteParams().WithID(id) _, err := api.Users.UsersPermissionsDelete(params, nil) if err != nil { if errresp, ok := err.(*users.UsersPermissionsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } d.SetId("") return nil } ================================================ FILE: netbox/resource_netbox_permission_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/users" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxPermission_basic(t *testing.T) { testSlug := "user_permissions" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_permission" "test_basic" { name = "%s" description = "This is a terraform test." enabled = true object_types = ["ipam.prefix"] actions = ["add", "change"] users = [1] constraints = jsonencode([{ "status" = "active" }]) }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_permission.test_basic", "name", testName), resource.TestCheckResourceAttr("netbox_permission.test_basic", "description", "This is a terraform test."), resource.TestCheckResourceAttr("netbox_permission.test_basic", "enabled", "true"), resource.TestCheckResourceAttr("netbox_permission.test_basic", "object_types.#", "1"), resource.TestCheckResourceAttr("netbox_permission.test_basic", "object_types.0", "ipam.prefix"), resource.TestCheckResourceAttr("netbox_permission.test_basic", "actions.#", "2"), resource.TestCheckResourceAttr("netbox_permission.test_basic", "actions.0", "add"), resource.TestCheckResourceAttr("netbox_permission.test_basic", "actions.1", "change"), resource.TestCheckResourceAttr("netbox_permission.test_basic", "users.#", "1"), resource.TestCheckResourceAttr("netbox_permission.test_basic", "users.0", "1"), resource.TestCheckResourceAttr("netbox_permission.test_basic", "constraints", "[{\"status\":\"active\"}]"), ), }, { ResourceName: "netbox_permission.test_basic", ImportState: true, ImportStateVerify: false, }, }, }) } func TestAccNetboxPermission_noConstraint(t *testing.T) { testSlug := "user_perms_nocnstrnt" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_permission" "test_basic" { name = "%s" description = "This is a terraform test." enabled = true object_types = ["ipam.prefix"] actions = ["add", "change"] users = [1] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_permission.test_basic", "name", testName), ), }, { ResourceName: "netbox_permission.test_basic", ImportState: true, ImportStateVerify: false, }, }, }) } func init() { resource.AddTestSweepers("netbox_permission", &resource.Sweeper{ Name: "netbox_permission", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := users.NewUsersPermissionsListParams() res, err := api.Users.UsersPermissionsList(params, nil) if err != nil { return err } for _, perm := range res.GetPayload().Results { if strings.HasPrefix(*perm.Name, testPrefix) { deleteParams := users.NewUsersPermissionsDeleteParams().WithID(perm.ID) _, err := api.Users.UsersPermissionsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a user") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_platform.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxPlatform() *schema.Resource { return &schema.Resource{ Create: resourceNetboxPlatformCreate, Read: resourceNetboxPlatformRead, Update: resourceNetboxPlatformUpdate, Delete: resourceNetboxPlatformDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/features/devices/#platforms): > A platform defines the type of software running on a device or virtual machine. This can be helpful to model when it is necessary to distinguish between different versions or feature sets. Note that two devices of the same type may be assigned different platforms: For example, one Juniper MX240 might run Junos 14 while another runs Junos 15.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "manufacturer_id": { Type: schema.TypeInt, Optional: true, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxPlatformCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } data := models.WritablePlatform{ Name: &name, Slug: &slug, Tags: []*models.NestedTag{}, } manufacturerIDValue, ok := d.GetOk("manufacturer_id") if ok { data.Manufacturer = int64ToPtr(int64(manufacturerIDValue.(int))) } params := dcim.NewDcimPlatformsCreateParams().WithData(&data) res, err := api.Dcim.DcimPlatformsCreate(params, nil) if err != nil { //return errors.New(getTextFromError(err)) return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxPlatformRead(d, m) } func resourceNetboxPlatformRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimPlatformsReadParams().WithID(id) res, err := api.Dcim.DcimPlatformsRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimPlatformsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } result := res.GetPayload() d.Set("name", result.Name) d.Set("slug", result.Slug) if result.Manufacturer != nil { d.Set("manufacturer_id", result.Manufacturer.ID) } return nil } func resourceNetboxPlatformUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritablePlatform{} name := d.Get("name").(string) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } data.Slug = &slug data.Name = &name data.Tags = []*models.NestedTag{} manufacturerIDValue, ok := d.GetOk("manufacturer_id") if ok { data.Manufacturer = int64ToPtr(int64(manufacturerIDValue.(int))) } params := dcim.NewDcimPlatformsPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Dcim.DcimPlatformsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxPlatformRead(d, m) } func resourceNetboxPlatformDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimPlatformsDeleteParams().WithID(id) _, err := api.Dcim.DcimPlatformsDelete(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimPlatformsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_platform_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxPlatform_basic(t *testing.T) { testSlug := "platform_basic" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_platform" "test" { name = "%s" slug = "%s" }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_platform.test", "name", testName), resource.TestCheckResourceAttr("netbox_platform.test", "slug", randomSlug), ), }, { ResourceName: "netbox_platform.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxPlatform_manufacturer(t *testing.T) { testSlug := "platform_manufacturer" testName := testAccGetTestName(testSlug) testManufacturer := "manu_test" randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_manufacturer" "test" { name = "%[3]s" } resource "netbox_platform" "test" { name = "%[1]s" slug = "%[2]s" manufacturer_id = netbox_manufacturer.test.id }`, testName, randomSlug, testManufacturer), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_platform.test", "name", testName), resource.TestCheckResourceAttr("netbox_platform.test", "slug", randomSlug), resource.TestCheckResourceAttrPair("netbox_platform.test", "manufacturer_id", "netbox_manufacturer.test", "id"), ), }, { ResourceName: "netbox_platform.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxPlatform_defaultSlug(t *testing.T) { testSlug := "platform_defSlug" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_platform" "test" { name = "%s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_platform.test", "name", testName), resource.TestCheckResourceAttr("netbox_platform.test", "slug", getSlug(testName)), ), }, }, }) } func init() { resource.AddTestSweepers("netbox_platform", &resource.Sweeper{ Name: "netbox_platform", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimPlatformsListParams() res, err := api.Dcim.DcimPlatformsList(params, nil) if err != nil { return err } for _, platform := range res.GetPayload().Results { if strings.HasPrefix(*platform.Name, testPrefix) { deleteParams := dcim.NewDcimPlatformsDeleteParams().WithID(platform.ID) _, err := api.Dcim.DcimPlatformsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a platform") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_power_panel.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceNetboxPowerPanel() *schema.Resource { return &schema.Resource{ Create: resourceNetboxPowerPanelCreate, Read: resourceNetboxPowerPanelRead, Update: resourceNetboxPowerPanelUpdate, Delete: resourceNetboxPowerPanelDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/powerpanel/): > A power panel represents the origin point in NetBox for electrical power being disseminated by one or more power feeds. In a data center environment, one power panel often serves a group of racks, with an individual power feed extending to each rack, though this is not always the case. It is common to have two sets of panels and feeds arranged in parallel to provide redundant power to each rack.`, Schema: map[string]*schema.Schema{ "site_id": { Type: schema.TypeInt, Required: true, }, "name": { Type: schema.TypeString, Required: true, }, "location_id": { Type: schema.TypeInt, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, "comments": { Type: schema.TypeString, Optional: true, }, tagsKey: tagsSchema, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxPowerPanelCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritablePowerPanel{ Site: int64ToPtr(int64(d.Get("site_id").(int))), Name: strToPtr(d.Get("name").(string)), Location: getOptionalInt(d, "location_id"), Description: getOptionalStr(d, "description", false), Comments: getOptionalStr(d, "comments", false), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimPowerPanelsCreateParams().WithData(&data) res, err := api.Dcim.DcimPowerPanelsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxPowerPanelRead(d, m) } func resourceNetboxPowerPanelRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimPowerPanelsReadParams().WithID(id) res, err := api.Dcim.DcimPowerPanelsRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimPowerPanelsReadDefault); ok { if errresp.Code() == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } powerPanel := res.GetPayload() if powerPanel.Site != nil { d.Set("site_id", powerPanel.Site.ID) } else { d.Set("site_id", nil) } d.Set("name", powerPanel.Name) if powerPanel.Location != nil { d.Set("location_id", powerPanel.Location.ID) } else { d.Set("location_id", nil) } d.Set("description", powerPanel.Description) d.Set("comments", powerPanel.Comments) cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxPowerPanelUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritablePowerPanel{ Site: int64ToPtr(int64(d.Get("site_id").(int))), Name: strToPtr(d.Get("name").(string)), Location: getOptionalInt(d, "location_id"), Description: getOptionalStr(d, "description", true), Comments: getOptionalStr(d, "comments", true), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimPowerPanelsPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimPowerPanelsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxPowerPanelRead(d, m) } func resourceNetboxPowerPanelDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimPowerPanelsDeleteParams().WithID(id) _, err := api.Dcim.DcimPowerPanelsDelete(params, nil) if err != nil { return err } return nil } ================================================ FILE: netbox/resource_netbox_power_panel_test.go ================================================ package netbox import ( "fmt" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" log "github.com/sirupsen/logrus" ) func testAccNetboxPowerPanelFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_location" "test" { name = "%[1]s" site_id =netbox_site.test.id } resource "netbox_tag" "test_a" { name = "%[1]sa" }`, testName) } func TestAccNetboxPowerPanel_basic(t *testing.T) { testSlug := "power_panel_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckPowerPanelDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxPowerPanelFullDependencies(testName) + fmt.Sprintf(` resource "netbox_power_panel" "test" { name = "%[1]s" description = "%[1]sdescription" comments = "%[1]scomments" site_id = netbox_site.test.id location_id = netbox_location.test.id tags = [netbox_tag.test_a.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_power_panel.test", "name", testName), resource.TestCheckResourceAttr("netbox_power_panel.test", "description", testName+"description"), resource.TestCheckResourceAttr("netbox_power_panel.test", "comments", testName+"comments"), resource.TestCheckResourceAttr("netbox_power_panel.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_power_panel.test", "tags.0", testName+"a"), resource.TestCheckResourceAttrPair("netbox_power_panel.test", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttrPair("netbox_power_panel.test", "location_id", "netbox_location.test", "id"), ), }, { Config: testAccNetboxPowerPanelFullDependencies(testName) + fmt.Sprintf(` resource "netbox_power_panel" "test" { name = "%[1]s" site_id = netbox_site.test.id }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_power_panel.test", "name", testName), resource.TestCheckResourceAttr("netbox_power_panel.test", "description", ""), resource.TestCheckResourceAttr("netbox_power_panel.test", "comments", ""), resource.TestCheckResourceAttr("netbox_power_panel.test", "tags.#", "0"), resource.TestCheckResourceAttr("netbox_power_panel.test", "location_id", "0"), resource.TestCheckResourceAttrPair("netbox_power_panel.test", "site_id", "netbox_site.test", "id"), ), }, { ResourceName: "netbox_power_panel.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckPowerPanelDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each power panel // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_power_panel" { continue } // Retrieve our device by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := dcim.NewDcimPowerPanelsReadParams().WithID(stateID) _, err := conn.Dcim.DcimPowerPanelsRead(params, nil) if err == nil { return fmt.Errorf("power panel (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*dcim.DcimPowerPanelsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func init() { resource.AddTestSweepers("netbox_power_panel", &resource.Sweeper{ Name: "netbox_power_panel", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimPowerPanelsListParams() res, err := api.Dcim.DcimPowerPanelsList(params, nil) if err != nil { return err } for _, powerPanel := range res.GetPayload().Results { if strings.HasPrefix(*powerPanel.Name, testPrefix) { deleteParams := dcim.NewDcimPowerPanelsDeleteParams().WithID(powerPanel.ID) _, err := api.Dcim.DcimPowerPanelsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a power_panel") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_prefix.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var resourceNetboxPrefixStatusOptions = []string{"active", "container", "reserved", "deprecated"} func resourceNetboxPrefix() *schema.Resource { return &schema.Resource{ Create: resourceNetboxPrefixCreate, Read: resourceNetboxPrefixRead, Update: resourceNetboxPrefixUpdate, Delete: resourceNetboxPrefixDelete, Description: `:meta:subcategory:IP Address Management (IPAM):From the [official documentation](https://docs.netbox.dev/en/stable/features/ipam/#prefixes): > A prefix is an IPv4 or IPv6 network and mask expressed in CIDR notation (e.g. 192.0.2.0/24). A prefix entails only the "network portion" of an IP address: All bits in the address not covered by the mask must be zero. (In other words, a prefix cannot be a specific IP address.) > > Prefixes are automatically organized by their parent aggregates. Additionally, each prefix can be assigned to a particular site and virtual routing and forwarding instance (VRF). Each VRF represents a separate IP space or routing table. All prefixes not assigned to a VRF are considered to be in the "global" table.`, Schema: map[string]*schema.Schema{ "prefix": { Type: schema.TypeString, Required: true, ValidateFunc: validation.IsCIDR, }, "status": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringInSlice(resourceNetboxPrefixStatusOptions, false), Description: buildValidValueDescription(resourceNetboxPrefixStatusOptions), }, "description": { Type: schema.TypeString, Optional: true, }, "is_pool": { Type: schema.TypeBool, Optional: true, }, "mark_utilized": { Type: schema.TypeBool, Optional: true, }, "vrf_id": { Type: schema.TypeInt, Optional: true, }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, "location_id": { Type: schema.TypeInt, Optional: true, ConflictsWith: []string{"site_id", "site_group_id", "region_id"}, }, "site_id": { Type: schema.TypeInt, Optional: true, ConflictsWith: []string{"location_id", "site_group_id", "region_id"}, }, "site_group_id": { Type: schema.TypeInt, Optional: true, ConflictsWith: []string{"location_id", "site_id", "region_id"}, }, "region_id": { Type: schema.TypeInt, Optional: true, ConflictsWith: []string{"location_id", "site_id", "site_group_id"}, }, "vlan_id": { Type: schema.TypeInt, Optional: true, }, "role_id": { Type: schema.TypeInt, Optional: true, }, customFieldsKey: customFieldsSchema, tagsKey: tagsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxPrefixCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritablePrefix{} prefix := d.Get("prefix").(string) status := d.Get("status").(string) description := d.Get("description").(string) isPool := d.Get("is_pool").(bool) markUtilized := d.Get("mark_utilized").(bool) data.Prefix = &prefix data.Status = status data.Description = description data.IsPool = isPool data.MarkUtilized = markUtilized if vrfID, ok := d.GetOk("vrf_id"); ok { data.Vrf = int64ToPtr(int64(vrfID.(int))) } if tenantID, ok := d.GetOk("tenant_id"); ok { data.Tenant = int64ToPtr(int64(tenantID.(int))) } if vlanID, ok := d.GetOk("vlan_id"); ok { data.Vlan = int64ToPtr(int64(vlanID.(int))) } if roleID, ok := d.GetOk("role_id"); ok { data.Role = int64ToPtr(int64(roleID.(int))) } siteID := getOptionalInt(d, "site_id") siteGroupID := getOptionalInt(d, "site_group_id") locationID := getOptionalInt(d, "location_id") regionID := getOptionalInt(d, "region_id") switch { case siteID != nil: data.ScopeType = strToPtr("dcim.site") data.ScopeID = siteID case siteGroupID != nil: data.ScopeType = strToPtr("dcim.sitegroup") data.ScopeID = siteGroupID case locationID != nil: data.ScopeType = strToPtr("dcim.location") data.ScopeID = locationID case regionID != nil: data.ScopeType = strToPtr("dcim.region") data.ScopeID = regionID default: data.ScopeType = nil data.ScopeID = nil } cf, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = cf } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } params := ipam.NewIpamPrefixesCreateParams().WithData(&data) res, err := api.Ipam.IpamPrefixesCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxPrefixRead(d, m) } func resourceNetboxPrefixRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamPrefixesReadParams().WithID(id) res, err := api.Ipam.IpamPrefixesRead(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamPrefixesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } prefix := res.GetPayload() d.Set("description", prefix.Description) d.Set("is_pool", prefix.IsPool) d.Set("mark_utilized", prefix.MarkUtilized) if prefix.Status != nil { d.Set("status", prefix.Status.Value) } if prefix.Prefix != nil { d.Set("prefix", prefix.Prefix) } if prefix.Vrf != nil { d.Set("vrf_id", prefix.Vrf.ID) } else { d.Set("vrf_id", nil) } if prefix.Tenant != nil { d.Set("tenant_id", prefix.Tenant.ID) } else { d.Set("tenant_id", nil) } if prefix.Vlan != nil { d.Set("vlan_id", prefix.Vlan.ID) } else { d.Set("vlan_id", nil) } if prefix.Role != nil { d.Set("role_id", prefix.Role.ID) } else { d.Set("role_id", nil) } d.Set("site_id", nil) d.Set("site_group_id", nil) d.Set("location_id", nil) d.Set("region_id", nil) if prefix.ScopeType != nil && prefix.ScopeID != nil { scopeID := prefix.ScopeID switch scopeType := prefix.ScopeType; *scopeType { case "dcim.site": d.Set("site_id", scopeID) case "dcim.sitegroup": d.Set("site_group_id", scopeID) case "dcim.location": d.Set("location_id", scopeID) case "dcim.region": d.Set("region_id", scopeID) } } cf := getCustomFields(prefix.CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, prefix.Tags) // FIGURE OUT NESTED VRF AND NESTED VLAN (from maybe interfaces?) return nil } func resourceNetboxPrefixUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritablePrefix{} prefix := d.Get("prefix").(string) status := d.Get("status").(string) isPool := d.Get("is_pool").(bool) markUtilized := d.Get("mark_utilized").(bool) data.Prefix = &prefix data.Status = status data.IsPool = isPool data.MarkUtilized = markUtilized if description, ok := d.GetOk("description"); ok { data.Description = description.(string) } else { data.Description = " " } if vrfID, ok := d.GetOk("vrf_id"); ok { data.Vrf = int64ToPtr(int64(vrfID.(int))) } if tenantID, ok := d.GetOk("tenant_id"); ok { data.Tenant = int64ToPtr(int64(tenantID.(int))) } if vlanID, ok := d.GetOk("vlan_id"); ok { data.Vlan = int64ToPtr(int64(vlanID.(int))) } if roleID, ok := d.GetOk("role_id"); ok { data.Role = int64ToPtr(int64(roleID.(int))) } if cf, ok := d.GetOk(customFieldsKey); ok { data.CustomFields = cf } siteID := getOptionalInt(d, "site_id") siteGroupID := getOptionalInt(d, "site_group_id") locationID := getOptionalInt(d, "location_id") regionID := getOptionalInt(d, "region_id") switch { case siteID != nil: data.ScopeType = strToPtr("dcim.site") data.ScopeID = siteID case siteGroupID != nil: data.ScopeType = strToPtr("dcim.sitegroup") data.ScopeID = siteGroupID case locationID != nil: data.ScopeType = strToPtr("dcim.location") data.ScopeID = locationID case regionID != nil: data.ScopeType = strToPtr("dcim.region") data.ScopeID = regionID default: data.ScopeType = nil data.ScopeID = nil } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } params := ipam.NewIpamPrefixesUpdateParams().WithID(id).WithData(&data) _, err = api.Ipam.IpamPrefixesUpdate(params, nil) if err != nil { return err } return resourceNetboxPrefixRead(d, m) } func resourceNetboxPrefixDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamPrefixesDeleteParams().WithID(id) _, err := api.Ipam.IpamPrefixesDelete(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamPrefixesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } d.SetId("") return nil } ================================================ FILE: netbox/resource_netbox_prefix_test.go ================================================ package netbox import ( "fmt" "log" "regexp" "testing" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxPrefixFullDependencies(testName string, testSlug string, testVid string) string { return fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_vrf" "test" { name = "%[1]s" } resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_ipam_role" "test" { name = "%[1]s" slug = "%[2]s" } resource "netbox_vlan" "test" { name = "%[1]s" vid = "%[3]s" status = "active" description = "Test" tags = [] } `, testName, testSlug, testVid) } func TestAccNetboxPrefix_basic(t *testing.T) { testPrefix := "1.1.1.128/25" testSlug := "prefix" testVid := "123" randomSlug := testAccGetTestName(testSlug) testDesc := "test prefix" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxPrefixFullDependencies(testName, randomSlug, testVid) + fmt.Sprintf(` resource "netbox_prefix" "test" { prefix = "%s" description = "%s" status = "active" tags = [netbox_tag.test.name] mark_utilized = true is_pool = true }`, testPrefix, testDesc), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_prefix.test", "prefix", testPrefix), resource.TestCheckResourceAttr("netbox_prefix.test", "status", "active"), resource.TestCheckResourceAttr("netbox_prefix.test", "description", testDesc), resource.TestCheckResourceAttr("netbox_prefix.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_prefix.test", "tags.0", testName), resource.TestCheckResourceAttr("netbox_prefix.test", "mark_utilized", "true"), resource.TestCheckResourceAttr("netbox_prefix.test", "is_pool", "true"), ), }, { Config: testAccNetboxPrefixFullDependencies(testName, randomSlug, testVid) + fmt.Sprintf(` resource "netbox_prefix" "test" { prefix = "%s" description = "%s" status = "provoke_error" tags = [netbox_tag.test.name] mark_utilized = true }`, testPrefix, testDesc), ExpectError: regexp.MustCompile("expected status to be one of .*"), }, { Config: testAccNetboxPrefixFullDependencies(testName, randomSlug, testVid) + fmt.Sprintf(` resource "netbox_prefix" "test" { prefix = "%s" description = "%s" status = "active" tags = [netbox_tag.test.name] mark_utilized = false is_pool = false }`, testPrefix, testDesc), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_prefix.test", "mark_utilized", "false"), resource.TestCheckResourceAttr("netbox_prefix.test", "is_pool", "false"), ), }, { Config: testAccNetboxPrefixFullDependencies(testName, randomSlug, testVid) + fmt.Sprintf(` resource "netbox_prefix" "test" { prefix = "%s" description = "%s" status = "deprecated" tags = [netbox_tag.test.name] mark_utilized = true }`, testPrefix, testDesc), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_prefix.test", "prefix", testPrefix), resource.TestCheckResourceAttr("netbox_prefix.test", "status", "deprecated"), ), }, { Config: testAccNetboxPrefixFullDependencies(testName, randomSlug, testVid) + fmt.Sprintf(` resource "netbox_prefix" "test" { prefix = "%s" description = "%s" status = "container" tags = [netbox_tag.test.name] mark_utilized = true }`, testPrefix, testDesc), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_prefix.test", "prefix", testPrefix), resource.TestCheckResourceAttr("netbox_prefix.test", "status", "container"), ), }, { Config: testAccNetboxPrefixFullDependencies(testName, randomSlug, testVid) + fmt.Sprintf(` resource "netbox_prefix" "test" { prefix = "%s" description = "%s 2" status = "active" tags = [netbox_tag.test.name] mark_utilized = true }`, testPrefix, testDesc), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_prefix.test", "prefix", testPrefix), resource.TestCheckResourceAttr("netbox_prefix.test", "status", "active"), resource.TestCheckResourceAttr("netbox_prefix.test", "description", fmt.Sprintf("%s 2", testDesc)), resource.TestCheckResourceAttr("netbox_prefix.test", "vrf_id", "0"), resource.TestCheckResourceAttr("netbox_prefix.test", "tenant_id", "0"), resource.TestCheckResourceAttr("netbox_prefix.test", "site_id", "0"), resource.TestCheckResourceAttr("netbox_prefix.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_prefix.test", "tags.0", testName), resource.TestCheckResourceAttr("netbox_prefix.test", "mark_utilized", "true"), ), }, { Config: testAccNetboxPrefixFullDependencies(testName, randomSlug, testVid) + fmt.Sprintf(` resource "netbox_prefix" "test" { prefix = "%s" description = "%s 2" status = "active" vrf_id = netbox_vrf.test.id tenant_id = netbox_tenant.test.id tags = [netbox_tag.test.name] mark_utilized = true }`, testPrefix, testDesc), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_prefix.test", "prefix", testPrefix), resource.TestCheckResourceAttr("netbox_prefix.test", "status", "active"), resource.TestCheckResourceAttr("netbox_prefix.test", "description", fmt.Sprintf("%s 2", testDesc)), resource.TestCheckResourceAttrPair("netbox_prefix.test", "vrf_id", "netbox_vrf.test", "id"), resource.TestCheckResourceAttrPair("netbox_prefix.test", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttr("netbox_prefix.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_prefix.test", "tags.0", testName), resource.TestCheckResourceAttr("netbox_prefix.test", "mark_utilized", "true"), ), }, { Config: testAccNetboxPrefixFullDependencies(testName, randomSlug, testVid) + fmt.Sprintf(` resource "netbox_prefix" "test" { prefix = "%s" description = "%s 2" status = "active" vrf_id = netbox_vrf.test.id tenant_id = netbox_tenant.test.id site_id = netbox_site.test.id vlan_id = netbox_vlan.test.id role_id = netbox_ipam_role.test.id tags = [netbox_tag.test.name] mark_utilized = true }`, testPrefix, testDesc), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_prefix.test", "prefix", testPrefix), resource.TestCheckResourceAttr("netbox_prefix.test", "status", "active"), resource.TestCheckResourceAttr("netbox_prefix.test", "description", fmt.Sprintf("%s 2", testDesc)), resource.TestCheckResourceAttrPair("netbox_prefix.test", "vrf_id", "netbox_vrf.test", "id"), resource.TestCheckResourceAttrPair("netbox_prefix.test", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("netbox_prefix.test", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttrPair("netbox_prefix.test", "vlan_id", "netbox_vlan.test", "id"), resource.TestCheckResourceAttrPair("netbox_prefix.test", "role_id", "netbox_ipam_role.test", "id"), resource.TestCheckResourceAttr("netbox_prefix.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_prefix.test", "tags.0", testName), resource.TestCheckResourceAttr("netbox_prefix.test", "mark_utilized", "true"), ), }, { Config: testAccNetboxPrefixFullDependencies(testName, randomSlug, testVid) + fmt.Sprintf(` resource "netbox_prefix" "test" { prefix = "%s" description = "%s 2" status = "active" }`, testPrefix, testDesc), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_prefix.test", "prefix", testPrefix), resource.TestCheckResourceAttr("netbox_prefix.test", "status", "active"), resource.TestCheckResourceAttr("netbox_prefix.test", "description", fmt.Sprintf("%s 2", testDesc)), ), }, { Config: testAccNetboxPrefixFullDependencies(testName, randomSlug, testVid) + fmt.Sprintf(` resource "netbox_prefix" "test" { prefix = "%s" status = "active" }`, testPrefix), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_prefix.test", "prefix", testPrefix), resource.TestCheckResourceAttr("netbox_prefix.test", "status", "active"), ), }, { ResourceName: "netbox_prefix.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxPrefix_cf(t *testing.T) { testPrefix := "1.1.2.128/25" testSlug := "prefix_cf" testVid := "124" randomSlug := testAccGetTestName(testSlug) testDesc := "test cf prefix" testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxPrefixFullDependencies(testName, randomSlug, testVid) + fmt.Sprintf(` resource "netbox_custom_field" "test" { name = "%s" type = "text" weight = 100 content_types = ["ipam.prefix"] } resource "netbox_prefix" "test" { prefix = "%s" description = "%s 2" status = "active" mark_utilized = true custom_fields = { "${netbox_custom_field.test.name}" = "test-field" } }`, testSlug, testPrefix, testDesc), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_prefix.test", fmt.Sprintf("custom_fields.%s", testSlug), "test-field"), ), }, { ResourceName: "netbox_prefix.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxPrefix_scopes(t *testing.T) { testPrefix := "1.8.1.128/25" testPrefix2 := "1.8.2.128/25" testPrefix3 := "1.8.3.128/25" testSlug := "prefix-scopes" testVid := "333" randomSlug := testAccGetTestName(testSlug) testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxPrefixFullDependencies(testName, randomSlug, testVid) + fmt.Sprintf(` resource "netbox_prefix" "test" { prefix = "%s" status = "active" }`, testPrefix), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_prefix.test", "site_id", "0"), // resource.TestCheckNoResourceAttr("netbox_prefix.test", "site_id"), resource.TestCheckResourceAttr("netbox_prefix.test", "site_group_id", "0"), resource.TestCheckResourceAttr("netbox_prefix.test", "region_id", "0"), resource.TestCheckResourceAttr("netbox_prefix.test", "location_id", "0"), ), }, { Config: testAccNetboxPrefixFullDependencies(testName, randomSlug, testVid) + fmt.Sprintf(` resource "netbox_prefix" "test" { prefix = "%s" status = "active" site_id = netbox_site.test.id }`, testPrefix), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_prefix.test", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttr("netbox_prefix.test", "site_group_id", "0"), resource.TestCheckResourceAttr("netbox_prefix.test", "region_id", "0"), resource.TestCheckResourceAttr("netbox_prefix.test", "location_id", "0"), ), }, { Config: testAccNetboxPrefixFullDependencies(testName, randomSlug, testVid) + fmt.Sprintf(` resource "netbox_location" "test" { name = "%s" site_id = netbox_site.test.id } resource "netbox_prefix" "test" { prefix = "%s" status = "active" location_id = netbox_location.test.id }`, testSlug, testPrefix), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_prefix.test", "site_id", "0"), resource.TestCheckResourceAttr("netbox_prefix.test", "site_group_id", "0"), resource.TestCheckResourceAttr("netbox_prefix.test", "region_id", "0"), resource.TestCheckResourceAttrPair("netbox_prefix.test", "location_id", "netbox_location.test", "id"), ), }, { Config: testAccNetboxPrefixFullDependencies(testName, randomSlug, testVid) + fmt.Sprintf(` resource "netbox_region" "test" { name = "%s" } resource "netbox_prefix" "test2" { prefix = "%s" status = "active" region_id = netbox_region.test.id }`, testSlug, testPrefix2), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_prefix.test2", "site_id", "0"), resource.TestCheckResourceAttr("netbox_prefix.test2", "site_group_id", "0"), resource.TestCheckResourceAttrPair("netbox_prefix.test2", "region_id", "netbox_region.test", "id"), resource.TestCheckResourceAttr("netbox_prefix.test2", "location_id", "0"), ), }, { Config: testAccNetboxPrefixFullDependencies(testName, randomSlug, testVid) + fmt.Sprintf(` resource "netbox_site_group" "test" { name = "%s" } resource "netbox_prefix" "test3" { prefix = "%s" status = "active" site_group_id = netbox_site_group.test.id }`, testSlug, testPrefix3), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_prefix.test3", "site_id", "0"), resource.TestCheckResourceAttrPair("netbox_prefix.test3", "site_group_id", "netbox_site_group.test", "id"), resource.TestCheckResourceAttr("netbox_prefix.test3", "region_id", "0"), resource.TestCheckResourceAttr("netbox_prefix.test3", "location_id", "0"), ), }, { ResourceName: "netbox_prefix.test3", ImportState: true, ImportStateVerify: true, }, }, }) } func init() { resource.AddTestSweepers("netbox_prefix", &resource.Sweeper{ Name: "netbox_prefix", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := ipam.NewIpamPrefixesListParams() res, err := api.Ipam.IpamPrefixesList(params, nil) if err != nil { return err } for _, prefix := range res.GetPayload().Results { if len(prefix.Tags) > 0 && (prefix.Tags[0] == &models.NestedTag{Name: strToPtr("acctest"), Slug: strToPtr("acctest")}) { deleteParams := ipam.NewIpamPrefixesDeleteParams().WithID(prefix.ID) _, err := api.Ipam.IpamPrefixesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a prefix") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_primary_ip.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/virtualization" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxPrimaryIP() *schema.Resource { return &schema.Resource{ Create: resourceNetboxPrimaryIPCreate, Read: resourceNetboxPrimaryIPRead, Update: resourceNetboxPrimaryIPUpdate, Delete: resourceNetboxPrimaryIPDelete, Description: `:meta:subcategory:Virtualization:This resource is used to define the primary IP for a given virtual machine. The primary IP is reflected in the Virtual machine Netbox UI, which identifies the Primary IPv4 and IPv6 addresses.`, Schema: map[string]*schema.Schema{ "virtual_machine_id": { Type: schema.TypeInt, Required: true, }, "ip_address_id": { Type: schema.TypeInt, Required: true, }, "ip_address_version": { Type: schema.TypeInt, ValidateFunc: validation.IntInSlice([]int{4, 6}), Optional: true, Default: 4, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxPrimaryIPCreate(d *schema.ResourceData, m interface{}) error { d.SetId(strconv.Itoa(d.Get("virtual_machine_id").(int))) return resourceNetboxPrimaryIPUpdate(d, m) } func resourceNetboxPrimaryIPRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := virtualization.NewVirtualizationVirtualMachinesReadParams().WithID(id) res, err := api.Virtualization.VirtualizationVirtualMachinesRead(params, nil) if err != nil { if errresp, ok := err.(*virtualization.VirtualizationVirtualMachinesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } IPAddressVersion := d.Get("ip_address_version") d.Set("ip_address_version", IPAddressVersion) if IPAddressVersion == 4 && res.GetPayload().PrimaryIp4 != nil { d.Set("ip_address_id", res.GetPayload().PrimaryIp4.ID) } else if IPAddressVersion == 6 && res.GetPayload().PrimaryIp6 != nil { d.Set("ip_address_id", res.GetPayload().PrimaryIp6.ID) } else { // if the vm exists, but has no primary ip, consider this element deleted d.SetId("") return nil } d.Set("virtual_machine_id", res.GetPayload().ID) return nil } func resourceNetboxPrimaryIPUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) virtualMachineID := int64(d.Get("virtual_machine_id").(int)) IPAddressID := int64(d.Get("ip_address_id").(int)) IPAddressVersion := int64(d.Get("ip_address_version").(int)) // because the go-netbox library does not have patch support atm, we have to get the whole object and re-put it // first, get the vm readParams := virtualization.NewVirtualizationVirtualMachinesReadParams().WithID(virtualMachineID) res, err := api.Virtualization.VirtualizationVirtualMachinesRead(readParams, nil) if err != nil { return err } vm := res.GetPayload() // then update the FULL vm with ALL tracked attributes data := models.WritableVirtualMachineWithConfigContext{} data.Name = vm.Name data.Tags = vm.Tags // the netbox API sends the URL property as part of NestedTag, but it does not accept the URL property when we send it back // so set it to empty // display too for _, tag := range data.Tags { tag.URL = "" tag.Display = "" } data.Comments = vm.Comments data.Description = vm.Description data.Memory = vm.Memory data.Vcpus = vm.Vcpus data.Disk = vm.Disk if vm.Cluster != nil { data.Cluster = &vm.Cluster.ID } if vm.Site != nil { data.Site = &vm.Site.ID } if vm.PrimaryIp4 != nil { data.PrimaryIp4 = &vm.PrimaryIp4.ID } if vm.PrimaryIp6 != nil { data.PrimaryIp6 = &vm.PrimaryIp6.ID } if vm.Platform != nil { data.Platform = &vm.Platform.ID } if vm.Tenant != nil { data.Tenant = &vm.Tenant.ID } if vm.Role != nil { data.Role = &vm.Role.ID } if vm.Device != nil { data.Device = &vm.Device.ID } if vm.LocalContextData != nil { data.LocalContextData = vm.LocalContextData } // unset primary ip address if -1 is passed as id if IPAddressID == -1 { if IPAddressVersion == 4 { data.PrimaryIp4 = nil } else { data.PrimaryIp6 = nil } } else { if IPAddressVersion == 4 { data.PrimaryIp4 = &IPAddressID } else { data.PrimaryIp6 = &IPAddressID } } updateParams := virtualization.NewVirtualizationVirtualMachinesUpdateParams().WithID(virtualMachineID).WithData(&data) _, err = api.Virtualization.VirtualizationVirtualMachinesUpdate(updateParams, nil) if err != nil { return err } return resourceNetboxPrimaryIPRead(d, m) } func resourceNetboxPrimaryIPDelete(d *schema.ResourceData, m interface{}) error { // Set ip_address_id to minus one and go to update. Update will set nil d.Set("ip_address_id", -1) return resourceNetboxPrimaryIPUpdate(d, m) } ================================================ FILE: netbox/resource_netbox_primary_ip_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxPrimaryIPFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_cluster_type" "test" { name = "%[1]s" } resource "netbox_cluster" "test" { name = "%[1]s" cluster_type_id = netbox_cluster_type.test.id site_id = netbox_site.test.id } resource "netbox_platform" "test" { name = "%[1]s" } resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device" "test" { name = "%[1]s" role_id = netbox_device_role.test.id site_id = netbox_site.test.id device_type_id = netbox_device_type.test.id cluster_id = netbox_cluster.test.id } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_virtual_machine" "test" { name = "%[1]s" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id comments = "thisisacomment" memory_mb = 1024 disk_size_mb = 256 tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id platform_id = netbox_platform.test.id vcpus = "4" status = "planned" device_id = netbox_device.test.id local_context_data = jsonencode({"context_string"="context_value"}) tags = [netbox_tag.test.name] } resource "netbox_interface" "test" { virtual_machine_id = netbox_virtual_machine.test.id name = "%[1]s" } `, testName) } func TestAccNetboxPrimaryIP4_basic(t *testing.T) { testSlug := "pr_ip_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxPrimaryIPFullDependencies(testName) + ` resource "netbox_ip_address" "test_v4" { ip_address = "1.1.1.13/32" status = "active" virtual_machine_interface_id = netbox_interface.test.id } resource "netbox_primary_ip" "test_v4" { virtual_machine_id = netbox_virtual_machine.test.id ip_address_id = netbox_ip_address.test_v4.id }`, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_primary_ip.test_v4", "virtual_machine_id", "netbox_virtual_machine.test", "id"), resource.TestCheckResourceAttrPair("netbox_primary_ip.test_v4", "ip_address_id", "netbox_ip_address.test_v4", "id"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "cluster_id", "netbox_cluster.test", "id"), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "platform_id", "netbox_platform.test", "id"), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "role_id", "netbox_device_role.test", "id"), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "comments", "thisisacomment"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "memory_mb", "1024"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "vcpus", "4"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "disk_size_mb", "256"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "tags.0", testName), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "status", "planned"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "local_context_data", "{\"context_string\":\"context_value\"}"), ), }, }, }) } func TestAccNetboxPrimaryIP6_basic(t *testing.T) { testSlug := "pr_ipv6_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxPrimaryIPFullDependencies(testName) + ` resource "netbox_ip_address" "test_v6" { ip_address = "2000::1/128" status = "active" virtual_machine_interface_id = netbox_interface.test.id } resource "netbox_primary_ip" "test_v6" { virtual_machine_id = netbox_virtual_machine.test.id ip_address_id = netbox_ip_address.test_v6.id ip_address_version = 6 }`, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_primary_ip.test_v6", "virtual_machine_id", "netbox_virtual_machine.test", "id"), resource.TestCheckResourceAttrPair("netbox_primary_ip.test_v6", "ip_address_id", "netbox_ip_address.test_v6", "id"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "cluster_id", "netbox_cluster.test", "id"), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "platform_id", "netbox_platform.test", "id"), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "role_id", "netbox_device_role.test", "id"), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "comments", "thisisacomment"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "memory_mb", "1024"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "vcpus", "4"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "disk_size_mb", "256"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "tags.0", testName), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "status", "planned"), ), }, }, }) } ================================================ FILE: netbox/resource_netbox_rack.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var resourceNetboxRackStatusOptions = []string{"reserved", "available", "planned", "active", "deprecated"} var resourceNetboxRackWeightUnitOptions = []string{"kg", "g", "lb", "oz"} var resourceNetboxRackOuterUnitOptions = []string{"mm", "in"} var resourceNetboxRackWidthOptions = []int{10, 19, 21, 23} var resourceNetboxRackFormFactorOptions = []string{"2-post-frame", "4-post-frame", "4-post-cabinet", "wall-frame", "wall-frame-vertical", "wall-cabinet", "wall-cabinet-vertical"} func resourceNetboxRack() *schema.Resource { return &schema.Resource{ Create: resourceNetboxRackCreate, Read: resourceNetboxRackRead, Update: resourceNetboxRackUpdate, Delete: resourceNetboxRackDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/rack/): > The rack model represents a physical two- or four-post equipment rack in which devices can be installed. Each rack must be assigned to a site, and may optionally be assigned to a location within that site. Racks can also be organized by user-defined functional roles. The name and facility ID of each rack within a location must be unique. Rack height is measured in rack units (U); racks are commonly between 42U and 48U tall, but NetBox allows you to define racks of arbitrary height. A toggle is provided to indicate whether rack units are in ascending (from the ground up) or descending order. Each rack is assigned a name and (optionally) a separate facility ID. This is helpful when leasing space in a data center your organization does not own: The facility will often assign a seemingly arbitrary ID to a rack (for example, "M204.313") whereas internally you refer to is simply as "R113." A unique serial number and asset tag may also be associated with each rack.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "site_id": { Type: schema.TypeInt, Required: true, }, "status": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringInSlice(resourceNetboxRackStatusOptions, false), Description: buildValidValueDescription(resourceNetboxRackStatusOptions), }, "width": { Type: schema.TypeInt, //Required: true, Optional: true, ValidateFunc: validation.IntInSlice(resourceNetboxRackWidthOptions), Description: "Valid values are `10`, `19`, `21` and `23`", }, "u_height": { Type: schema.TypeInt, //Required: true, Optional: true, ValidateFunc: validation.IntBetween(1, 100), }, tagsKey: tagsSchema, "tenant_id": { Type: schema.TypeInt, Optional: true, }, "facility_id": { Type: schema.TypeString, Optional: true, }, "location_id": { Type: schema.TypeInt, Optional: true, }, "role_id": { Type: schema.TypeInt, Optional: true, }, "serial": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringLenBetween(0, 50), }, "asset_tag": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringLenBetween(0, 50), }, "weight": { Type: schema.TypeFloat, Optional: true, }, "max_weight": { Type: schema.TypeInt, Optional: true, ValidateFunc: validatePositiveInt32, }, "weight_unit": { Type: schema.TypeString, Optional: true, RequiredWith: []string{"weight", "max_weight"}, ValidateFunc: validation.StringInSlice(resourceNetboxRackWeightUnitOptions, false), Description: buildValidValueDescription(resourceNetboxRackWeightUnitOptions), }, "desc_units": { Type: schema.TypeBool, Optional: true, Description: "If rack units are descending", Default: false, }, "outer_width": { Type: schema.TypeInt, Optional: true, ValidateFunc: validatePositiveInt16, }, "outer_depth": { Type: schema.TypeInt, Optional: true, ValidateFunc: validatePositiveInt16, }, "outer_unit": { Type: schema.TypeString, Optional: true, RequiredWith: []string{"outer_width", "outer_depth"}, ValidateFunc: validation.StringInSlice(resourceNetboxRackOuterUnitOptions, false), Description: buildValidValueDescription(resourceNetboxRackOuterUnitOptions), }, "mounting_depth": { Type: schema.TypeInt, Optional: true, ValidateFunc: validatePositiveInt16, }, "description": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringLenBetween(0, 200), }, "comments": { Type: schema.TypeString, Optional: true, }, customFieldsKey: customFieldsSchema, "form_factor": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringInSlice(resourceNetboxRackFormFactorOptions, false), Description: buildValidValueDescription(resourceNetboxRackFormFactorOptions), }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxRackCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) siteID := int64(d.Get("site_id").(int)) status := d.Get("status").(string) width := int64(d.Get("width").(int)) uHeight := int64(d.Get("u_height").(int)) data := models.WritableRack{ Name: &name, Site: &siteID, Status: status, Width: width, UHeight: uHeight, } data.Tenant = getOptionalInt(d, "tenant_id") if facilityID := getOptionalStr(d, "facility_id", false); facilityID != "" { data.FacilityID = strToPtr(facilityID) } data.Location = getOptionalInt(d, "location_id") data.Role = getOptionalInt(d, "role_id") data.Serial = getOptionalStr(d, "serial", false) if assetTag := getOptionalStr(d, "asset_tag", false); assetTag != "" { data.AssetTag = &assetTag } data.Weight = getOptionalFloat(d, "weight") data.MaxWeight = getOptionalInt(d, "max_weight") data.WeightUnit = getOptionalStr(d, "weight_unit", false) if descUnits, ok := d.GetOk("desc_units"); ok { data.DescUnits = descUnits.(bool) } data.OuterWidth = getOptionalInt(d, "outer_width") data.OuterDepth = getOptionalInt(d, "outer_depth") data.OuterUnit = getOptionalStr(d, "outer_unit", false) data.MountingDepth = getOptionalInt(d, "mounting_depth") data.Description = getOptionalStr(d, "description", false) data.Comments = getOptionalStr(d, "comments", false) data.FormFactor = getOptionalStr(d, "form_factor", false) var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimRacksCreateParams().WithData(&data) res, err := api.Dcim.DcimRacksCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxRackRead(d, m) } func resourceNetboxRackRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimRacksReadParams().WithID(id) res, err := api.Dcim.DcimRacksRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimRacksReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } rack := res.GetPayload() d.Set("name", rack.Name) if rack.Site != nil { d.Set("site_id", rack.Site.ID) } else { d.Set("site_id", nil) } if rack.Status != nil { d.Set("status", rack.Status.Value) } else { d.Set("status", nil) } if rack.Width != nil { d.Set("width", rack.Width.Value) } else { d.Set("width", nil) } d.Set("u_height", rack.UHeight) if rack.Tenant != nil { d.Set("tenant_id", rack.Tenant.ID) } else { d.Set("tenant_id", nil) } d.Set("facility_id", rack.FacilityID) if rack.Location != nil { d.Set("location_id", rack.Location.ID) } else { d.Set("location_id", nil) } if rack.Role != nil { d.Set("role_id", rack.Role.ID) } else { d.Set("role_id", nil) } d.Set("serial", rack.Serial) d.Set("asset_tag", rack.AssetTag) d.Set("weight", rack.Weight) d.Set("max_weight", rack.MaxWeight) if rack.WeightUnit != nil { d.Set("weight_unit", rack.WeightUnit.Value) } else { d.Set("weight_unit", nil) } d.Set("desc_units", rack.DescUnits) d.Set("outer_width", rack.OuterWidth) d.Set("outer_depth", rack.OuterDepth) if rack.OuterUnit != nil { d.Set("outer_unit", rack.OuterUnit.Value) } else { d.Set("outer_unit", nil) } d.Set("mounting_depth", rack.MountingDepth) d.Set("description", rack.Description) d.Set("comments", rack.Comments) if rack.FormFactor != nil { d.Set("form_factor", rack.FormFactor.Value) } else { d.Set("form_factor", nil) } cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxRackUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) name := d.Get("name").(string) siteID := int64(d.Get("site_id").(int)) status := d.Get("status").(string) width := int64(d.Get("width").(int)) uHeight := int64(d.Get("u_height").(int)) data := models.WritableRack{ Name: &name, Site: &siteID, Status: status, Width: width, UHeight: uHeight, } data.Tenant = getOptionalInt(d, "tenant_id") if facilityID := getOptionalStr(d, "facility_id", false); facilityID != "" { data.FacilityID = strToPtr(facilityID) } data.Location = getOptionalInt(d, "location_id") data.Role = getOptionalInt(d, "role_id") data.Serial = getOptionalStr(d, "serial", true) if assetTag := getOptionalStr(d, "asset_tag", false); assetTag != "" { data.AssetTag = &assetTag } data.Weight = getOptionalFloat(d, "weight") data.MaxWeight = getOptionalInt(d, "max_weight") data.WeightUnit = getOptionalStr(d, "weight_unit", false) if descUnits, ok := d.GetOk("desc_units"); ok { data.DescUnits = descUnits.(bool) } data.OuterWidth = getOptionalInt(d, "outer_width") data.OuterDepth = getOptionalInt(d, "outer_depth") data.OuterUnit = getOptionalStr(d, "outer_unit", false) data.MountingDepth = getOptionalInt(d, "mounting_depth") data.Description = getOptionalStr(d, "description", true) data.Comments = getOptionalStr(d, "comments", true) data.FormFactor = getOptionalStr(d, "form_factor", false) var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } cf, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = cf } params := dcim.NewDcimRacksPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimRacksPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxRackRead(d, m) } func resourceNetboxRackDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimRacksDeleteParams().WithID(id) _, err := api.Dcim.DcimRacksDelete(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimRacksDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_rack_reservation.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceNetboxRackReservation() *schema.Resource { return &schema.Resource{ Create: resourceNetboxRackReservationCreate, Read: resourceNetboxRackReservationRead, Update: resourceNetboxRackReservationUpdate, Delete: resourceNetboxRackReservationDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/rackreservation/): > Users can reserve specific units within a rack for future use. An arbitrary set of units within a rack can be associated with a single reservation, but reservations cannot span multiple racks. A description is required for each reservation, reservations may optionally be associated with a specific tenant.`, Schema: map[string]*schema.Schema{ "rack_id": { Type: schema.TypeInt, Required: true, }, "units": { Type: schema.TypeSet, Elem: &schema.Schema{ Type: schema.TypeInt, }, Required: true, Set: schema.HashInt, }, "user_id": { Type: schema.TypeInt, Required: true, }, "description": { Type: schema.TypeString, Required: true, }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, "comments": { Type: schema.TypeString, Optional: true, }, tagsKey: tagsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxRackReservationCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) params := dcim.NewDcimRackReservationsCreateParams().WithData( &models.WritableRackReservation{ Rack: getOptionalInt(d, "rack_id"), Units: toInt64PtrList(d.Get("units")), User: getOptionalInt(d, "user_id"), Description: strToPtr(getOptionalStr(d, "description", false)), Tenant: getOptionalInt(d, "tenant_id"), Comments: getOptionalStr(d, "comments", false), Tags: tags, }, ) res, err := api.Dcim.DcimRackReservationsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxRackReservationRead(d, m) } func resourceNetboxRackReservationRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimRackReservationsReadParams().WithID(id) res, err := api.Dcim.DcimRackReservationsRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimRackReservationsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } rackRes := res.GetPayload() if rackRes.Rack != nil { d.Set("rack_id", rackRes.Rack.ID) } units := []int{} for _, unit := range rackRes.Units { units = append(units, int(*unit)) } d.Set("units", units) if rackRes.User != nil { d.Set("user_id", rackRes.User.ID) } d.Set("description", rackRes.Description) if rackRes.Tenant != nil { d.Set("tenant_id", rackRes.Tenant.ID) } d.Set("comments", rackRes.Comments) api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxRackReservationUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) data := models.WritableRackReservation{ Rack: getOptionalInt(d, "rack_id"), Units: toInt64PtrList(d.Get("units")), User: getOptionalInt(d, "user_id"), Description: strToPtr(getOptionalStr(d, "description", false)), Tenant: getOptionalInt(d, "tenant_id"), Comments: getOptionalStr(d, "comments", false), Tags: tags, } params := dcim.NewDcimRackReservationsPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Dcim.DcimRackReservationsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxRackReservationRead(d, m) } func resourceNetboxRackReservationDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimRackReservationsDeleteParams().WithID(id) _, err := api.Dcim.DcimRackReservationsDelete(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimRackReservationsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_rack_reservation_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxRackReservationFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_rack" "test" { name = "%[1]s" site_id = netbox_site.test.id status = "active" width = 10 u_height = 40 }`, testName) } func TestAccNetboxRackReservation_basic(t *testing.T) { testSlug := "rack_reservation_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testAccNetboxRackReservationFullDependencies(testName) + fmt.Sprintf(` resource "netbox_rack_reservation" "test" { rack_id = netbox_rack.test.id units = [1,2,3,4,5] user_id = 1 description = "%[1]sdescription" tenant_id = netbox_tenant.test.id }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_rack_reservation.test", "rack_id", "netbox_rack.test", "id"), resource.TestCheckResourceAttrPair("netbox_rack_reservation.test", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttr("netbox_rack_reservation.test", "units.#", "5"), resource.TestCheckResourceAttr("netbox_rack_reservation.test", "user_id", "1"), resource.TestCheckResourceAttr("netbox_rack_reservation.test", "description", testName+"description"), ), }, { ResourceName: "netbox_rack_reservation.test", ImportState: true, ImportStateVerify: true, }, }, }) } func init() { resource.AddTestSweepers("netbox_rack_reservation", &resource.Sweeper{ Name: "netbox_rack_reservation", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimRackReservationsListParams() res, err := api.Dcim.DcimRackReservationsList(params, nil) if err != nil { return err } for _, rackRes := range res.GetPayload().Results { if strings.HasPrefix(*rackRes.Description, testPrefix) { deleteParams := dcim.NewDcimRackReservationsDeleteParams().WithID(rackRes.ID) _, err := api.Dcim.DcimRackReservationsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a rack_reservation") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_rack_role.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxRackRole() *schema.Resource { return &schema.Resource{ Create: resourceNetboxRackRoleCreate, Read: resourceNetboxRackRoleRead, Update: resourceNetboxRackRoleUpdate, Delete: resourceNetboxRackRoleDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/models/dcim/rackrole/): > Each rack can optionally be assigned a user-defined functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "description": { Type: schema.TypeString, Optional: true, }, "color_hex": { Type: schema.TypeString, Required: true, }, tagsKey: tagsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxRackRoleCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } color := d.Get("color_hex").(string) description := getOptionalStr(d, "description", false) tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) params := dcim.NewDcimRackRolesCreateParams().WithData( &models.RackRole{ Name: &name, Slug: &slug, Color: color, Description: description, Tags: tags, }, ) res, err := api.Dcim.DcimRackRolesCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxRackRoleRead(d, m) } func resourceNetboxRackRoleRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimRackRolesReadParams().WithID(id) res, err := api.Dcim.DcimRackRolesRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimRackRolesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } rackRole := res.GetPayload() d.Set("name", rackRole.Name) d.Set("slug", rackRole.Slug) d.Set("description", rackRole.Description) d.Set("color_hex", rackRole.Color) api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxRackRoleUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.RackRole{} name := d.Get("name").(string) color := d.Get("color_hex").(string) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } data.Slug = &slug data.Name = &name data.Description = getOptionalStr(d, "description", true) data.Color = color tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) data.Tags = tags params := dcim.NewDcimRackRolesPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Dcim.DcimRackRolesPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxRackRoleRead(d, m) } func resourceNetboxRackRoleDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimRackRolesDeleteParams().WithID(id) _, err := api.Dcim.DcimRackRolesDelete(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimRackRolesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_rack_role_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxRackRole_basic(t *testing.T) { testSlug := "rack_role_basic" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_rack_role" "test" { name = "%s" slug = "%s" color_hex = "111111" }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_rack_role.test", "name", testName), resource.TestCheckResourceAttr("netbox_rack_role.test", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_rack_role.test", "color_hex", "111111"), ), }, { ResourceName: "netbox_rack_role.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxRackRole_defaultSlug(t *testing.T) { testSlug := "rack_role_defSlug" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_rack_role" "test" { name = "%s" color_hex = "111111" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_rack_role.test", "name", testName), resource.TestCheckResourceAttr("netbox_rack_role.test", "slug", getSlug(testName)), ), }, }, }) } func init() { resource.AddTestSweepers("netbox_rack_role", &resource.Sweeper{ Name: "netbox_rack_role", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimRackRolesListParams() res, err := api.Dcim.DcimRackRolesList(params, nil) if err != nil { return err } for _, rackRole := range res.GetPayload().Results { if strings.HasPrefix(*rackRole.Name, testPrefix) { deleteParams := dcim.NewDcimRackRolesDeleteParams().WithID(rackRole.ID) _, err := api.Dcim.DcimRackRolesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a rack_role") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_rack_test.go ================================================ package netbox import ( "fmt" "log" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func testAccNetboxRackFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_location" "test" { name = "%[1]s" site_id =netbox_site.test.id } resource "netbox_rack_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_tag" "test_a" { name = "%[1]sa" }`, testName) } func TestAccNetboxRack_basic(t *testing.T) { testSlug := "rack_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckRackDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxRackFullDependencies(testName) + fmt.Sprintf(` resource "netbox_rack" "test" { name = "%[1]s" site_id = netbox_site.test.id status = "reserved" width = 19 u_height = 48 tags = [netbox_tag.test_a.name] tenant_id = netbox_tenant.test.id facility_id = "%[1]sfacility" location_id = netbox_location.test.id role_id = netbox_rack_role.test.id serial = "%[1]sserial" asset_tag = "%[1]sasset_tag" desc_units = true outer_width = 10 outer_depth = 15 outer_unit = "mm" comments = "%[1]scomments" form_factor = "2-post-frame" } resource "netbox_rack" "test2" { name = "%[1]s2" site_id = netbox_site.test.id location_id = netbox_location.test.id status = "reserved" width = 19 u_height = 48 } `, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_rack.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_rack.test", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttr("netbox_rack.test", "status", "reserved"), resource.TestCheckResourceAttr("netbox_rack.test", "width", "19"), resource.TestCheckResourceAttr("netbox_rack.test", "u_height", "48"), resource.TestCheckResourceAttr("netbox_rack.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_rack.test", "tags.0", testName+"a"), resource.TestCheckResourceAttrPair("netbox_rack.test", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttr("netbox_rack.test", "facility_id", testName+"facility"), resource.TestCheckResourceAttrPair("netbox_rack.test", "location_id", "netbox_location.test", "id"), resource.TestCheckResourceAttrPair("netbox_rack.test", "role_id", "netbox_rack_role.test", "id"), resource.TestCheckResourceAttr("netbox_rack.test", "serial", testName+"serial"), resource.TestCheckResourceAttr("netbox_rack.test", "asset_tag", testName+"asset_tag"), resource.TestCheckResourceAttr("netbox_rack.test", "desc_units", "true"), resource.TestCheckResourceAttr("netbox_rack.test", "outer_width", "10"), resource.TestCheckResourceAttr("netbox_rack.test", "outer_depth", "15"), resource.TestCheckResourceAttr("netbox_rack.test", "outer_unit", "mm"), resource.TestCheckResourceAttr("netbox_rack.test", "comments", testName+"comments"), resource.TestCheckResourceAttr("netbox_rack.test", "form_factor", "2-post-frame"), resource.TestCheckResourceAttr("netbox_rack.test2", "name", testName+"2"), resource.TestCheckResourceAttrPair("netbox_rack.test2", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttrPair("netbox_rack.test2", "location_id", "netbox_location.test", "id"), resource.TestCheckResourceAttr("netbox_rack.test2", "status", "reserved"), resource.TestCheckResourceAttr("netbox_rack.test2", "width", "19"), resource.TestCheckResourceAttr("netbox_rack.test2", "u_height", "48"), ), }, { Config: testAccNetboxRackFullDependencies(testName) + fmt.Sprintf(` resource "netbox_rack" "test" { name = "%[1]s" site_id = netbox_site.test.id status = "reserved" width = 19 u_height = 48 }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_rack.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_rack.test", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttr("netbox_rack.test", "status", "reserved"), resource.TestCheckResourceAttr("netbox_rack.test", "width", "19"), resource.TestCheckResourceAttr("netbox_rack.test", "u_height", "48"), resource.TestCheckResourceAttr("netbox_rack.test", "tags.#", "0"), resource.TestCheckResourceAttr("netbox_rack.test", "tenant_id", "0"), resource.TestCheckResourceAttr("netbox_rack.test", "facility_id", ""), resource.TestCheckResourceAttr("netbox_rack.test", "location_id", "0"), resource.TestCheckResourceAttr("netbox_rack.test", "role_id", "0"), resource.TestCheckResourceAttr("netbox_rack.test", "serial", ""), resource.TestCheckResourceAttr("netbox_rack.test", "asset_tag", ""), resource.TestCheckResourceAttr("netbox_rack.test", "weight", "0"), resource.TestCheckResourceAttr("netbox_rack.test", "max_weight", "0"), resource.TestCheckResourceAttr("netbox_rack.test", "weight_unit", ""), resource.TestCheckResourceAttr("netbox_rack.test", "desc_units", "false"), resource.TestCheckResourceAttr("netbox_rack.test", "outer_width", "0"), resource.TestCheckResourceAttr("netbox_rack.test", "outer_depth", "0"), resource.TestCheckResourceAttr("netbox_rack.test", "outer_unit", ""), resource.TestCheckResourceAttr("netbox_rack.test", "mounting_depth", "0"), resource.TestCheckResourceAttr("netbox_rack.test", "description", ""), resource.TestCheckResourceAttr("netbox_rack.test", "comments", ""), resource.TestCheckResourceAttr("netbox_rack.test", "form_factor", ""), ), }, { ResourceName: "netbox_rack.test", ImportState: true, ImportStateVerify: true, }, }, }) } /* Not sure if creating a rack from rack type is a sustainable process when using terraform func TestAccNetboxRack_fromRackType(t *testing.T) { testSlug := "rack_fromType" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckRackDestroy, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_rack_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id width = 19 u_height = 48 starting_unit = 1 form_factor = "2-post-frame" } resource "netbox_rack" "test" { name = "%[1]s" site_id = netbox_site.test.id status = "active" comments = "%[1]scomments" } `, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_rack.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_rack.test", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttr("netbox_rack.test", "status", "active"), resource.TestCheckResourceAttr("netbox_rack.test", "width", "19"), resource.TestCheckResourceAttr("netbox_rack.test", "u_height", "48"), resource.TestCheckResourceAttr("netbox_rack.test", "tags.#", "1"), ), }, { ResourceName: "netbox_rack.test", ImportState: true, ImportStateVerify: true, }, }, }) } */ func testAccCheckRackDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each rack // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_rack" { continue } // Retrieve our rack by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := dcim.NewDcimRacksReadParams().WithID(stateID) _, err := conn.Dcim.DcimRacksRead(params, nil) if err == nil { return fmt.Errorf("rack (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*dcim.DcimRacksReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func init() { resource.AddTestSweepers("netbox_rack", &resource.Sweeper{ Name: "netbox_rack", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimRacksListParams() res, err := api.Dcim.DcimRacksList(params, nil) if err != nil { return err } for _, Rack := range res.GetPayload().Results { if strings.HasPrefix(*Rack.Name, testPrefix) { deleteParams := dcim.NewDcimRacksDeleteParams().WithID(Rack.ID) _, err := api.Dcim.DcimRacksDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a rack") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_rack_type.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var resourceNetboxRackTypeFormFactorOptions = []string{"2-post-frame", "4-post-frame", "4-post-cabinet", "wall-frame", "wall-frame-vertical", "wall-cabinet", "wall-cabinet-vertical"} var resourceNetboxRackTypeWeightUnitOptions = []string{"kg", "g", "lb", "oz"} var resourceNetboxRackTypeOuterUnitOptions = []string{"mm", "in"} var resourceNetboxRackTypeWidthOptions = []int{10, 19, 21, 23} func resourceNetboxRackType() *schema.Resource { return &schema.Resource{ Create: resourceNetboxRackTypeCreate, Read: resourceNetboxRackTypeRead, Update: resourceNetboxRackTypeUpdate, Delete: resourceNetboxRackTypeDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://netboxlabs.com/docs/netbox/en/stable/models/dcim/racktype/): > A rack type defines the physical characteristics of a particular model of rack.`, Schema: map[string]*schema.Schema{ "model": { Type: schema.TypeString, Required: true, }, "manufacturer_id": { Type: schema.TypeInt, Optional: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "form_factor": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringInSlice(resourceNetboxRackTypeFormFactorOptions, false), Description: buildValidValueDescription(resourceNetboxRackTypeFormFactorOptions), }, "width": { Type: schema.TypeInt, Required: true, ValidateFunc: validation.IntInSlice(resourceNetboxRackTypeWidthOptions), Description: "Valid values are `10`, `19`, `21` and `23`", }, "u_height": { Type: schema.TypeInt, Required: true, ValidateFunc: validation.IntBetween(1, 100), }, "starting_unit": { Type: schema.TypeInt, Required: true, }, tagsKey: tagsSchema, "description": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringLenBetween(0, 200), }, "outer_width": { Type: schema.TypeInt, Optional: true, ValidateFunc: validatePositiveInt16, }, "outer_depth": { Type: schema.TypeInt, Optional: true, ValidateFunc: validatePositiveInt16, }, "outer_unit": { Type: schema.TypeString, Optional: true, RequiredWith: []string{"outer_width", "outer_depth"}, ValidateFunc: validation.StringInSlice(resourceNetboxRackTypeOuterUnitOptions, false), Description: buildValidValueDescription(resourceNetboxRackTypeOuterUnitOptions), }, "comments": { Type: schema.TypeString, Optional: true, }, "weight": { Type: schema.TypeFloat, Optional: true, }, "max_weight": { Type: schema.TypeInt, Optional: true, ValidateFunc: validatePositiveInt32, }, "weight_unit": { Type: schema.TypeString, Optional: true, RequiredWith: []string{"weight", "max_weight"}, ValidateFunc: validation.StringInSlice(resourceNetboxRackTypeWeightUnitOptions, false), Description: buildValidValueDescription(resourceNetboxRackTypeWeightUnitOptions), }, "mounting_depth_mm": { Type: schema.TypeInt, Optional: true, ValidateFunc: validatePositiveInt16, }, // "tenant_id": { // Type: schema.TypeInt, // Optional: true, // }, // "facility_id": { // Type: schema.TypeString, // Optional: true, // }, // "location_id": { // Type: schema.TypeInt, // Optional: true, // }, // "role_id": { // Type: schema.TypeInt, // Optional: true, // }, // "serial": { // Type: schema.TypeString, // Optional: true, // ValidateFunc: validation.StringLenBetween(0, 50), // }, // "asset_tag": { // Type: schema.TypeString, // Optional: true, // ValidateFunc: validation.StringLenBetween(0, 50), // }, // "type": { // Type: schema.TypeString, // Optional: true, // ValidateFunc: validation.StringInSlice(resourceNetboxRackTypeTypeOptions, false), // Description: buildValidValueDescription(resourceNetboxRackTypeTypeOptions), // }, // "desc_units": { // Type: schema.TypeBool, // Optional: true, // Description: "If rack units are descending", // Default: false, // }, // "outer_width": { // Type: schema.TypeInt, // Optional: true, // ValidateFunc: validatePositiveInt16, // }, // "outer_depth": { // Type: schema.TypeInt, // Optional: true, // ValidateFunc: validatePositiveInt16, // }, // "outer_unit": { // Type: schema.TypeString, // Optional: true, // RequiredWith: []string{"outer_width", "outer_depth"}, // ValidateFunc: validation.StringInSlice(resourceNetboxRackTypeOuterUnitOptions, false), // Description: buildValidValueDescription(resourceNetboxRackTypeOuterUnitOptions), // }, // "mounting_depth": { // Type: schema.TypeInt, // Optional: true, // ValidateFunc: validatePositiveInt16, // }, // "description": { // Type: schema.TypeString, // Optional: true, // ValidateFunc: validation.StringLenBetween(0, 200), // }, // "comments": { // Type: schema.TypeString, // Optional: true, // }, // customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxRackTypeCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) model := d.Get("model").(string) formFactor := d.Get("form_factor").(string) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(model) } else { slug = slugValue.(string) } manufacturerID := int64(d.Get("manufacturer_id").(int)) width := int64(d.Get("width").(int)) uHeight := int64(d.Get("u_height").(int)) data := models.WritableRackTypeRequest{ Model: &model, FormFactor: &formFactor, Slug: &slug, Manufacturer: &manufacturerID, Width: &width, UHeight: uHeight, Description: getOptionalStr(d, "description", false), OuterWidth: getOptionalInt(d, "outer_width"), OuterDepth: getOptionalInt(d, "outer_depth"), OuterUnit: getOptionalStr(d, "outer_unit", false), Comments: getOptionalStr(d, "comments", false), Weight: getOptionalFloat(d, "weight"), MaxWeight: getOptionalInt(d, "max_weight"), WeightUnit: getOptionalStr(d, "weight_unit", false), MountingDepth: getOptionalInt(d, "mounting_depth_mm"), } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } params := dcim.NewDcimRackTypesCreateParams().WithData(&data) res, err := api.Dcim.DcimRackTypesCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxRackTypeRead(d, m) } func resourceNetboxRackTypeRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimRackTypesReadParams().WithID(id) res, err := api.Dcim.DcimRackTypesRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimRackTypesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } rackType := res.GetPayload() d.Set("model", rackType.Model) d.Set("form_factor", rackType.FormFactor.Value) d.Set("starting_unit", rackType.StartingUnit) d.Set("manufacturer_id", rackType.Manufacturer.ID) if rackType.Width != nil { d.Set("width", rackType.Width.Value) } else { d.Set("width", nil) } d.Set("u_height", rackType.UHeight) api.readTags(d, res.GetPayload().Tags) d.Set("description", rackType.Description) d.Set("comments", rackType.Comments) d.Set("outer_width", rackType.OuterWidth) d.Set("outer_depth", rackType.OuterDepth) if rackType.OuterUnit != nil { d.Set("outer_unit", rackType.OuterUnit.Value) } else { d.Set("outer_unit", nil) } d.Set("weight", rackType.Weight) d.Set("max_weight", rackType.MaxWeight) if rackType.WeightUnit != nil { d.Set("weight_unit", rackType.WeightUnit.Value) } else { d.Set("weight_unit", nil) } d.Set("mounting_depth_mm", rackType.MountingDepth) return nil } func resourceNetboxRackTypeUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) model := d.Get("model").(string) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(model) } else { slug = slugValue.(string) } manufacturerID := int64(d.Get("manufacturer_id").(int)) width := int64(d.Get("width").(int)) uHeight := int64(d.Get("u_height").(int)) data := models.WritableRackTypeRequest{ Model: &model, Slug: &slug, Manufacturer: &manufacturerID, Width: &width, UHeight: uHeight, Description: getOptionalStr(d, "description", true), OuterWidth: getOptionalInt(d, "outer_width"), OuterDepth: getOptionalInt(d, "outer_depth"), OuterUnit: getOptionalStr(d, "outer_unit", true), Comments: getOptionalStr(d, "comments", true), Weight: getOptionalFloat(d, "weight"), MaxWeight: getOptionalInt(d, "max_weight"), WeightUnit: getOptionalStr(d, "weight_unit", true), MountingDepth: getOptionalInt(d, "mounting_depth_mm"), } params := dcim.NewDcimRackTypesUpdateParams().WithID(id).WithData(&data) _, err := api.Dcim.DcimRackTypesUpdate(params, nil) if err != nil { return err } return resourceNetboxRackTypeRead(d, m) } func resourceNetboxRackTypeDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimRackTypesDeleteParams().WithID(id) _, err := api.Dcim.DcimRackTypesDelete(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimRackTypesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_rack_type_test.go ================================================ package netbox import ( "fmt" "log" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func testAccNetboxRackTypeFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_tag" "test_a" { name = "%[1]sa" }`, testName) } func TestAccNetboxRackType_basic(t *testing.T) { testSlug := "racktype_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckRackTypeDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxRackTypeFullDependencies(testName) + fmt.Sprintf(` resource "netbox_rack_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id width = 19 u_height = 48 starting_unit = 1 form_factor = "2-post-frame" tags = [netbox_tag.test_a.name] description = "%[1]s" outer_width = 10 outer_depth = 15 outer_unit = "mm" weight = 15 max_weight = 20 weight_unit = "kg" mounting_depth_mm = 21 comments = "%[1]scomments" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_rack_type.test", "model", testName), resource.TestCheckResourceAttr("netbox_rack_type.test", "description", testName), resource.TestCheckResourceAttrPair("netbox_rack_type.test", "manufacturer_id", "netbox_manufacturer.test", "id"), resource.TestCheckResourceAttr("netbox_rack_type.test", "width", "19"), resource.TestCheckResourceAttr("netbox_rack_type.test", "u_height", "48"), resource.TestCheckResourceAttr("netbox_rack_type.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_rack_type.test", "tags.0", testName+"a"), resource.TestCheckResourceAttr("netbox_rack_type.test", "outer_width", "10"), resource.TestCheckResourceAttr("netbox_rack_type.test", "outer_depth", "15"), resource.TestCheckResourceAttr("netbox_rack_type.test", "outer_unit", "mm"), resource.TestCheckResourceAttr("netbox_rack_type.test", "weight", "15"), resource.TestCheckResourceAttr("netbox_rack_type.test", "max_weight", "20"), resource.TestCheckResourceAttr("netbox_rack_type.test", "weight_unit", "kg"), resource.TestCheckResourceAttr("netbox_rack_type.test", "comments", testName+"comments"), resource.TestCheckResourceAttr("netbox_rack_type.test", "mounting_depth_mm", "21"), ), }, { ResourceName: "netbox_rack_type.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckRackTypeDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each rack // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_rack_type" { continue } // Retrieve our rack by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := dcim.NewDcimRackTypesReadParams().WithID(stateID) _, err := conn.Dcim.DcimRackTypesRead(params, nil) if err == nil { return fmt.Errorf("rack type (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*dcim.DcimRackTypesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func init() { resource.AddTestSweepers("netbox_rack_type", &resource.Sweeper{ Name: "netbox_rack_type", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimRackTypesListParams() res, err := api.Dcim.DcimRackTypesList(params, nil) if err != nil { return err } for _, RackType := range res.GetPayload().Results { if strings.HasPrefix(*RackType.Model, testPrefix) { deleteParams := dcim.NewDcimRackTypesDeleteParams().WithID(RackType.ID) _, err := api.Dcim.DcimRackTypesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a rack type") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_region.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxRegion() *schema.Resource { return &schema.Resource{ Create: resourceNetboxRegionCreate, Read: resourceNetboxRegionRead, Update: resourceNetboxRegionUpdate, Delete: resourceNetboxRegionDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/features/sites-and-racks/#regions): > Sites can be arranged geographically using regions. A region might represent a continent, country, city, campus, or other area depending on your use case. Regions can be nested recursively to construct a hierarchy. For example, you might define several country regions, and within each of those several state or city regions to which sites are assigned. > > Each region must have a name that is unique within its parent region, if any.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "parent_region_id": { Type: schema.TypeInt, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringLenBetween(0, 200), }, "id": { Type: schema.TypeString, Computed: true, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxRegionCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableRegion{} name := d.Get("name").(string) data.Name = &name slugValue, slugOk := d.GetOk("slug") // Default slug to generated slug if not given if !slugOk { data.Slug = strToPtr(getSlug(name)) } else { data.Slug = strToPtr(slugValue.(string)) } if description, ok := d.GetOk("description"); ok { data.Description = description.(string) } parentRegionIDValue, ok := d.GetOk("parent_region_id") if ok { data.Parent = int64ToPtr(int64(parentRegionIDValue.(int))) } data.Tags = []*models.NestedTag{} params := dcim.NewDcimRegionsCreateParams().WithData(&data) res, err := api.Dcim.DcimRegionsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxRegionRead(d, m) } func resourceNetboxRegionRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimRegionsReadParams().WithID(id) res, err := api.Dcim.DcimRegionsRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimRegionsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } d.Set("name", res.GetPayload().Name) d.Set("slug", res.GetPayload().Slug) if res.GetPayload().Parent != nil { d.Set("parent_region_id", res.GetPayload().Parent.ID) } else { d.Set("parent_region_id", nil) } d.Set("description", res.GetPayload().Description) return nil } func resourceNetboxRegionUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableRegion{} name := d.Get("name").(string) data.Name = &name slugValue, slugOk := d.GetOk("slug") // Default slug to generated slug if not given if !slugOk { data.Slug = strToPtr(getSlug(name)) } else { data.Slug = strToPtr(slugValue.(string)) } if description, ok := d.GetOk("description"); ok { data.Description = description.(string) } parentRegionIDValue, ok := d.GetOk("parent_region_id") if ok { data.Parent = int64ToPtr(int64(parentRegionIDValue.(int))) } data.Tags = []*models.NestedTag{} params := dcim.NewDcimRegionsPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Dcim.DcimRegionsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxRegionRead(d, m) } func resourceNetboxRegionDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimRegionsDeleteParams().WithID(id) _, err := api.Dcim.DcimRegionsDelete(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimRegionsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_region_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxRegion_basic(t *testing.T) { testSlug := "region_basic" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_region" "test" { name = "%s" slug = "%s" description = "%[1]s" }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_region.test", "name", testName), resource.TestCheckResourceAttr("netbox_region.test", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_region.test", "parent_region_id", "0"), resource.TestCheckResourceAttr("netbox_region.test", "description", testName), ), }, { ResourceName: "netbox_region.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxRegion_defaultSlug(t *testing.T) { testSlug := "region_defSlug" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_region" "test" { name = "%s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_region.test", "name", testName), resource.TestCheckResourceAttr("netbox_region.test", "slug", getSlug(testName)), ), }, }, }) } func init() { resource.AddTestSweepers("netbox_region", &resource.Sweeper{ Name: "netbox_region", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimRegionsListParams() res, err := api.Dcim.DcimRegionsList(params, nil) if err != nil { return err } for _, region := range res.GetPayload().Results { if strings.HasPrefix(*region.Name, testPrefix) { deleteParams := dcim.NewDcimRegionsDeleteParams().WithID(region.ID) _, err := api.Dcim.DcimRegionsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a Region") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_rir.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxRir() *schema.Resource { return &schema.Resource{ Create: resourceNetboxRirCreate, Read: resourceNetboxRirRead, Update: resourceNetboxRirUpdate, Delete: resourceNetboxRirDelete, Description: `:meta:subcategory:IP Address Management (IPAM):From the [official documentation](https://docs.netbox.dev/en/stable/features/ipam/#regional-internet-registries-rirs): > Regional Internet registries are responsible for the allocation of globally-routable address space. The five RIRs are ARIN, RIPE, APNIC, LACNIC, and AFRINIC. However, some address space has been set aside for internal use, such as defined in RFCs 1918 and 6598. NetBox considers these RFCs as a sort of RIR as well; that is, an authority which "owns" certain address space. There also exist lower-tier registries which serve particular geographic areas.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "description": { Type: schema.TypeString, Optional: true, }, "is_private": { Type: schema.TypeBool, Optional: true, Default: false, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxRirCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.RIR{} name := d.Get("name").(string) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } data.Name = &name data.Slug = &slug data.Description = getOptionalStr(d, "description", true) data.Tags = []*models.NestedTag{} data.IsPrivate = d.Get("is_private").(bool) params := ipam.NewIpamRirsCreateParams().WithData(&data) res, err := api.Ipam.IpamRirsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxRirUpdate(d, m) } func resourceNetboxRirRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamRirsReadParams().WithID(id) res, err := api.Ipam.IpamRirsRead(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamRirsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } rir := res.GetPayload() d.Set("name", rir.Name) d.Set("slug", rir.Slug) d.Set("description", rir.Description) d.Set("is_private", rir.IsPrivate) return nil } func resourceNetboxRirUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.RIR{} name := d.Get("name").(string) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } data.Name = &name data.Slug = &slug data.Description = getOptionalStr(d, "description", true) data.Tags = []*models.NestedTag{} data.IsPrivate = d.Get("is_private").(bool) params := ipam.NewIpamRirsUpdateParams().WithID(id).WithData(&data) _, err := api.Ipam.IpamRirsUpdate(params, nil) if err != nil { return err } return resourceNetboxRirRead(d, m) } func resourceNetboxRirDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamRirsDeleteParams().WithID(id) _, err := api.Ipam.IpamRirsDelete(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamRirsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } d.SetId("") return nil } ================================================ FILE: netbox/resource_netbox_rir_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxRir_basic(t *testing.T) { testSlug := "rir" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_rir" "test_basic" { name = "%s" slug = "%s" description = "my-description" }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_rir.test_basic", "name", testName), resource.TestCheckResourceAttr("netbox_rir.test_basic", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_rir.test_basic", "description", "my-description"), ), }, { ResourceName: "netbox_rir.test_basic", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxRir_privacy(t *testing.T) { testSlug := "rir_privacy" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_rir" "test_privacy" { name = "%s" slug = "%s" is_private = false }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_rir.test_privacy", "name", testName), resource.TestCheckResourceAttr("netbox_rir.test_privacy", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_rir.test_privacy", "is_private", "false"), ), }, { Config: fmt.Sprintf(` resource "netbox_rir" "test_privacy" { name = "%s" slug = "%s" is_private = true }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_rir.test_privacy", "name", testName), resource.TestCheckResourceAttr("netbox_rir.test_privacy", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_rir.test_privacy", "is_private", "true"), ), }, { Config: fmt.Sprintf(` resource "netbox_rir" "test_privacy" { name = "%s" slug = "%s" is_private = false }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_rir.test_privacy", "name", testName), resource.TestCheckResourceAttr("netbox_rir.test_privacy", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_rir.test_privacy", "is_private", "false"), ), }, }, }) } func init() { resource.AddTestSweepers("netbox_rir", &resource.Sweeper{ Name: "netbox_rir", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := ipam.NewIpamRirsListParams() res, err := api.Ipam.IpamRirsList(params, nil) if err != nil { return err } for _, role := range res.GetPayload().Results { if strings.HasPrefix(*role.Name, testPrefix) { deleteParams := ipam.NewIpamRirsDeleteParams().WithID(role.ID) _, err := api.Ipam.IpamRirsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a rir") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_route_target.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxRouteTarget() *schema.Resource { return &schema.Resource{ Create: resourceNetboxRouteTargetCreate, Read: resourceNetboxRouteTargetRead, Update: resourceNetboxRouteTargetUpdate, Delete: resourceNetboxRouteTargetDelete, Description: `:meta:subcategory:IP Address Management (IPAM):From the [official documentation](https://docs.netbox.dev/en/stable/models/ipam/routetarget/): > A route target is a particular type of extended BGP community used to control the redistribution of routes among VRF tables in a network. Route targets can be assigned to individual VRFs in NetBox as import or export targets (or both) to model this exchange in an L3VPN. Each route target must be given a unique name, which should be in a format prescribed by RFC 4364, similar to a VR route distinguisher.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringLenBetween(1, 21), }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringLenBetween(0, 200), }, tagsKey: tagsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxRouteTargetCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableRouteTarget{} name := d.Get("name").(string) data.Name = &name data.Tags = []*models.NestedTag{} params := ipam.NewIpamRouteTargetsCreateParams().WithData(&data) res, err := api.Ipam.IpamRouteTargetsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxRouteTargetUpdate(d, m) } func resourceNetboxRouteTargetRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamRouteTargetsReadParams().WithID(id) res, err := api.Ipam.IpamRouteTargetsRead(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamRouteTargetsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } if res.GetPayload().Name != nil { d.Set("name", res.GetPayload().Name) } if res.GetPayload().Tenant != nil { d.Set("tenant_id", res.GetPayload().Tenant.ID) } if res.GetPayload().Description != "" { d.Set("description", res.GetPayload().Description) } if res.GetPayload().Tags != nil { api.readTags(d, res.GetPayload().Tags) } return nil } func resourceNetboxRouteTargetUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableRouteTarget{} name := d.Get("name").(string) tenantID := int64(d.Get("tenant_id").(int)) description := d.Get("description").(string) data.Name = &name data.Description = description data.Tenant = &tenantID data.Tags = []*models.NestedTag{} params := ipam.NewIpamRouteTargetsUpdateParams().WithID(id).WithData(&data) _, err := api.Ipam.IpamRouteTargetsUpdate(params, nil) if err != nil { return err } return resourceNetboxRouteTargetRead(d, m) } func resourceNetboxRouteTargetDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamRouteTargetsDeleteParams().WithID(id) _, err := api.Ipam.IpamRouteTargetsDelete(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamRouteTargetsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } d.SetId("") return nil } ================================================ FILE: netbox/resource_netbox_route_target_test.go ================================================ package netbox import ( "fmt" "regexp" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" log "github.com/sirupsen/logrus" ) func getNetboxRouteTargetResource(rtName, tenantName string) string { return fmt.Sprintf(` resource "netbox_tenant" "rt_acctest_basic" { name = "%[2]s" } resource "netbox_route_target" "rt_acctest_basic" { name = "%[1]s" description = "rt for acctest" tenant_id = netbox_tenant.rt_acctest_basic.id }`, rtName, tenantName) } func Test_resourceNetboxRouteTarget(t *testing.T) { testSlug := "rt" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: getNetboxRouteTargetResource(testName, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_route_target.rt_acctest_basic", "name", testName), resource.TestCheckResourceAttr("netbox_route_target.rt_acctest_basic", "description", "rt for acctest"), resource.TestCheckResourceAttrPair("netbox_route_target.rt_acctest_basic", "tenant_id", "netbox_tenant.rt_acctest_basic", "id"), ), }, { ResourceName: "netbox_route_target.rt_acctest_basic", ImportState: true, ImportStateVerify: true, }, { Config: fmt.Sprintf(` resource "netbox_tenant" "rt_acctest_basic" { name = "%[2]s" } resource "netbox_route_target" "rt_acctest_basic" { name = "%[1]s" description = "change description" tenant_id = netbox_tenant.rt_acctest_basic.id }`, testName, fmt.Sprintf("new%s", testName)), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("netbox_route_target.rt_acctest_basic", "name", testName), resource.TestCheckResourceAttr("netbox_route_target.rt_acctest_basic", "description", "change description"), resource.TestCheckResourceAttrPair("netbox_route_target.rt_acctest_basic", "tenant_id", "netbox_tenant.rt_acctest_basic", "id"), ), }, { ResourceName: "netbox_route_target.rt_acctest_basic", ImportState: true, ImportStateVerify: true, }, { Config: fmt.Sprintf(` resource "netbox_route_target" "rt_acctest_basic2" { name = "%[1]s" description = "change description" tenant_id = "10001" }`, fmt.Sprintf("2%s", testName)), ExpectError: regexp.MustCompile(`.*Related object not found using the provided numeric ID.*`), }, }, }) } func init() { resource.AddTestSweepers("netbox_route_target", &resource.Sweeper{ Name: "netbox_route_target", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := ipam.NewIpamRouteTargetsListParams() res, err := api.Ipam.IpamRouteTargetsList(params, nil) if err != nil { return err } for _, role := range res.GetPayload().Results { if strings.HasPrefix(*role.Name, testPrefix) { deleteParams := ipam.NewIpamRouteTargetsDeleteParams().WithID(role.ID) _, err := api.Ipam.IpamRouteTargetsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a rir") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_service.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var resourceNetboxServiceProtocolOptions = []string{"tcp", "udp", "sctp"} func resourceNetboxService() *schema.Resource { return &schema.Resource{ Create: resourceNetboxServiceCreate, Read: resourceNetboxServiceRead, Update: resourceNetboxServiceUpdate, Delete: resourceNetboxServiceDelete, Description: `:meta:subcategory:IP Address Management (IPAM):From the [official documentation](https://docs.netbox.dev/en/stable/features/services/#services): > A service represents a layer four TCP or UDP service available on a device or virtual machine. For example, you might want to document that an HTTP service is running on a device. Each service includes a name, protocol, and port number; for example, "SSH (TCP/22)" or "DNS (UDP/53)." > > A service may optionally be bound to one or more specific IP addresses belonging to its parent device or VM. (If no IP addresses are bound, the service is assumed to be reachable via any assigned IP address.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "virtual_machine_id": { Type: schema.TypeInt, Optional: true, ExactlyOneOf: []string{"virtual_machine_id", "device_id"}, }, "protocol": { Type: schema.TypeString, Required: true, ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice(resourceNetboxServiceProtocolOptions, false)), Description: buildValidValueDescription(resourceNetboxServiceProtocolOptions), }, "port": { Type: schema.TypeInt, Optional: true, ExactlyOneOf: []string{"port", "ports"}, Deprecated: "This field is deprecated. Please use the new \"ports\" attribute instead.", }, "ports": { Type: schema.TypeSet, Optional: true, ExactlyOneOf: []string{"port", "ports"}, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, "description": { Type: schema.TypeString, Optional: true, }, "tags": { Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{ Type: schema.TypeString, }, }, "device_id": { Type: schema.TypeInt, Optional: true, ExactlyOneOf: []string{"virtual_machine_id", "device_id"}, }, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxServiceCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableService{} dataName := d.Get("name").(string) data.Name = &dataName dataProtocol := d.Get("protocol").(string) data.Protocol = &dataProtocol // for backwards compatibility, we allow either port or ports // the API only supports ports. We give precedence to port, if it exists. dataPort, dataPortOk := d.GetOk("port") if dataPortOk { data.Ports = []int64{int64(dataPort.(int))} } else { // if port is not set, ports has to be set var dataPorts []int64 if v := d.Get("ports").(*schema.Set); v.Len() > 0 { for _, v := range v.List() { dataPorts = append(dataPorts, int64(v.(int))) } data.Ports = dataPorts } } virtualMachineID := getOptionalInt(d, "virtual_machine_id") deviceID := getOptionalInt(d, "device_id") switch { case virtualMachineID != nil: data.ParentObjectType = strToPtr("virtualization.virtualmachine") data.ParentObjectID = virtualMachineID case deviceID != nil: data.ParentObjectType = strToPtr("dcim.device") data.ParentObjectID = deviceID } tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) data.Tags = tags if v, ok := d.GetOk("description"); ok { data.Description = v.(string) } data.Ipaddresses = []int64{} ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := ipam.NewIpamServicesCreateParams().WithData(&data) res, err := api.Ipam.IpamServicesCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxServiceUpdate(d, m) } func resourceNetboxServiceRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamServicesReadParams().WithID(id) res, err := api.Ipam.IpamServicesRead(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamServicesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } service := res.GetPayload() d.Set("name", service.Name) d.Set("protocol", service.Protocol.Value) d.Set("ports", service.Ports) d.Set("description", service.Description) parentObjectType := service.ParentObjectType switch parentObjectType { case "virtualization.virtualmachine": d.Set("virtual_machine_id", service.ParentObjectID) case "dcim.device": d.Set("device_id", service.ParentObjectID) } if tags := service.Tags; tags != nil { api.readTags(d, tags) } cf := getCustomFields(service.CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } return nil } func resourceNetboxServiceUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableService{} dataName := d.Get("name").(string) data.Name = &dataName dataProtocol := d.Get("protocol").(string) data.Protocol = &dataProtocol dataPort, dataPortOk := d.GetOk("port") if dataPortOk { data.Ports = []int64{int64(dataPort.(int))} } else { // if port is not set, ports has to be set var dataPorts []int64 if v := d.Get("ports").(*schema.Set); v.Len() > 0 { for _, v := range v.List() { dataPorts = append(dataPorts, int64(v.(int))) } data.Ports = dataPorts } } data.Ipaddresses = []int64{} tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) data.Tags = tags if v, ok := d.GetOk("description"); ok { data.Description = v.(string) } virtualMachineID := getOptionalInt(d, "virtual_machine_id") deviceID := getOptionalInt(d, "device_id") switch { case virtualMachineID != nil: data.ParentObjectType = strToPtr("virtualization.virtualmachine") data.ParentObjectID = virtualMachineID case deviceID != nil: data.ParentObjectType = strToPtr("dcim.device") data.ParentObjectID = deviceID } cf, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = cf } params := ipam.NewIpamServicesUpdateParams().WithID(id).WithData(&data) _, err := api.Ipam.IpamServicesUpdate(params, nil) if err != nil { return err } return resourceNetboxServiceRead(d, m) } func resourceNetboxServiceDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamServicesDeleteParams().WithID(id) _, err := api.Ipam.IpamServicesDelete(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamServicesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_service_test.go ================================================ package netbox import ( "fmt" "log" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func testAccNetboxServiceFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_cluster_type" "test" { name = "%[1]s" } resource "netbox_cluster" "test" { name = "%[1]s" cluster_type_id = netbox_cluster_type.test.id } resource "netbox_virtual_machine" "test" { name = "%[1]s" cluster_id = netbox_cluster.test.id } `, testName) } func TestAccNetboxService_basic(t *testing.T) { testSlug := "svc_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckServiceDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxServiceFullDependencies(testName) + fmt.Sprintf(` resource "netbox_service" "test" { name = "%s" virtual_machine_id = netbox_virtual_machine.test.id ports = [666] protocol = "tcp" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_service.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_service.test", "virtual_machine_id", "netbox_virtual_machine.test", "id"), resource.TestCheckResourceAttr("netbox_service.test", "ports.#", "1"), resource.TestCheckResourceAttr("netbox_service.test", "ports.0", "666"), resource.TestCheckResourceAttr("netbox_service.test", "protocol", "tcp"), ), }, { ResourceName: "netbox_service.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxService_customFields(t *testing.T) { testSlug := "svc_custom_fields" testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckServiceDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxServiceFullDependencies(testName) + fmt.Sprintf(` resource "netbox_custom_field" "test" { name = "custom_field" type = "text" content_types = ["ipam.service"] } resource "netbox_service" "test_customfield" { name = "%s" virtual_machine_id = netbox_virtual_machine.test.id ports = [333] protocol = "tcp" custom_fields = {"${netbox_custom_field.test.name}" = "testtext"} }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_service.test_customfield", "name", testName), resource.TestCheckResourceAttrPair("netbox_service.test_customfield", "virtual_machine_id", "netbox_virtual_machine.test", "id"), resource.TestCheckResourceAttr("netbox_service.test_customfield", "ports.#", "1"), resource.TestCheckResourceAttr("netbox_service.test_customfield", "ports.0", "333"), resource.TestCheckResourceAttr("netbox_service.test_customfield", "protocol", "tcp"), resource.TestCheckResourceAttr("netbox_service.test_customfield", "custom_fields.custom_field", "testtext"), ), }, { ResourceName: "netbox_service.test_customfield", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckServiceDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each service // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_service" { continue } // Retrieve our service by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := ipam.NewIpamServicesReadParams().WithID(stateID) _, err := conn.Ipam.IpamServicesRead(params, nil) if err == nil { return fmt.Errorf("service (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*ipam.IpamServicesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func TestAccNetboxService_withDescriptionDeviceID(t *testing.T) { testSlug := "svc_with_desc_tags_device" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckServiceDestroy, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_service" "test" { name = "%s" device_id = netbox_device.test_device.id ports = [666] protocol = "tcp" description = "Test service description" } resource "netbox_site" "test_site" { name = "%[1]s_site" slug = "%[1]s_site" } resource "netbox_device_role" "test_role" { name = "%[1]s_role" slug = "%[1]s_role" color_hex = "123456" } resource "netbox_manufacturer" "test_manufacturer" { name = "%[1]s_manufacturer" } resource "netbox_device_type" "test_type" { model = "%[1]s_type" manufacturer_id = netbox_manufacturer.test_manufacturer.id } resource "netbox_device" "test_device" { name = "%[1]s_device" role_id = netbox_device_role.test_role.id device_type_id = netbox_device_type.test_type.id site_id = netbox_site.test_site.id } `, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_service.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_service.test", "device_id", "netbox_device.test_device", "id"), resource.TestCheckResourceAttr("netbox_service.test", "ports.#", "1"), resource.TestCheckResourceAttr("netbox_service.test", "ports.0", "666"), resource.TestCheckResourceAttr("netbox_service.test", "protocol", "tcp"), resource.TestCheckResourceAttr("netbox_service.test", "description", "Test service description"), ), }, { ResourceName: "netbox_service.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxService_withDescriptionTagsVirtualMachine(t *testing.T) { testSlug := "svc_with_desc_tags_device" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckServiceDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxServiceFullDependencies(testName) + fmt.Sprintf( ` resource "netbox_tag" "tag1" { name = "tag1" slug = "tag1" } resource "netbox_tag" "tag2" { name = "tag2" slug = "tag2" } resource "netbox_service" "test" { name = "%s" virtual_machine_id = netbox_virtual_machine.test.id ports = [666] protocol = "tcp" description = "Test service description" tags = [netbox_tag.tag1.name, netbox_tag.tag2.name] } `, testName, ), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_service.test", "name", testName), resource.TestCheckResourceAttr("netbox_service.test", "tags.#", "2"), resource.TestCheckResourceAttr("netbox_service.test", "tags.0", "tag1"), resource.TestCheckResourceAttr("netbox_service.test", "tags.1", "tag2"), resource.TestCheckResourceAttrPair("netbox_service.test", "virtual_machine_id", "netbox_virtual_machine.test", "id"), resource.TestCheckResourceAttr("netbox_service.test", "ports.#", "1"), resource.TestCheckResourceAttr("netbox_service.test", "ports.0", "666"), resource.TestCheckResourceAttr("netbox_service.test", "protocol", "tcp"), resource.TestCheckResourceAttr("netbox_service.test", "description", "Test service description"), ), }, { ResourceName: "netbox_service.test", ImportState: true, ImportStateVerify: true, }, }, }) } func init() { resource.AddTestSweepers("netbox_service", &resource.Sweeper{ Name: "netbox_service", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := ipam.NewIpamServicesListParams() res, err := api.Ipam.IpamServicesList(params, nil) if err != nil { return err } for _, intrface := range res.GetPayload().Results { if strings.HasPrefix(*intrface.Name, testPrefix) { deleteParams := ipam.NewIpamServicesDeleteParams().WithID(intrface.ID) _, err := api.Ipam.IpamServicesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted an interface") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_site.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var resourceNetboxSiteStatusOptions = []string{"planned", "staging", "active", "decommissioning", "retired"} func resourceNetboxSite() *schema.Resource { return &schema.Resource{ Create: resourceNetboxSiteCreate, Read: resourceNetboxSiteRead, Update: resourceNetboxSiteUpdate, Delete: resourceNetboxSiteDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/features/sites-and-racks/#sites): > How you choose to employ sites when modeling your network may vary depending on the nature of your organization, but generally a site will equate to a building or campus. For example, a chain of banks might create a site to represent each of its branches, a site for its corporate headquarters, and two additional sites for its presence in two colocation facilities. > > Each site must be assigned a unique name and may optionally be assigned to a region and/or tenant.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "status": { Type: schema.TypeString, Optional: true, Default: "active", ValidateFunc: validation.StringInSlice(resourceNetboxSiteStatusOptions, false), Description: buildValidValueDescription(resourceNetboxSiteStatusOptions), }, "description": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringLenBetween(0, 200), }, "comments": { Type: schema.TypeString, Optional: true, }, "facility": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringLenBetween(0, 50), }, "longitude": { Type: schema.TypeFloat, Optional: true, }, "latitude": { Type: schema.TypeFloat, Optional: true, }, "physical_address": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringLenBetween(0, 200), }, "shipping_address": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringLenBetween(0, 200), }, "region_id": { Type: schema.TypeInt, Optional: true, }, "group_id": { Type: schema.TypeInt, Optional: true, }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, tagsKey: tagsSchema, "timezone": { Type: schema.TypeString, Optional: true, }, "asn_ids": { Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxSiteCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableSite{} name := d.Get("name").(string) data.Name = &name slugValue, slugOk := d.GetOk("slug") // Default slug to generated slug if not given if !slugOk { data.Slug = strToPtr(getSlug(name)) } else { data.Slug = strToPtr(slugValue.(string)) } data.Status = d.Get("status").(string) if description, ok := d.GetOk("description"); ok { data.Description = description.(string) } if comments, ok := d.GetOk("comments"); ok { data.Comments = comments.(string) } if facility, ok := d.GetOk("facility"); ok { data.Facility = facility.(string) } latitudeValue, ok := d.GetOk("latitude") if ok { data.Latitude = float64ToPtr(float64(latitudeValue.(float64))) } longitudeValue, ok := d.GetOk("longitude") if ok { data.Longitude = float64ToPtr(float64(longitudeValue.(float64))) } physicalAddressValue, ok := d.GetOk("physical_address") if ok { data.PhysicalAddress = physicalAddressValue.(string) } shippingAddressValue, ok := d.GetOk("shipping_address") if ok { data.ShippingAddress = shippingAddressValue.(string) } regionIDValue, ok := d.GetOk("region_id") if ok { data.Region = int64ToPtr(int64(regionIDValue.(int))) } groupIDValue, ok := d.GetOk("group_id") if ok { data.Group = int64ToPtr(int64(groupIDValue.(int))) } tenantIDValue, ok := d.GetOk("tenant_id") if ok { data.Tenant = int64ToPtr(int64(tenantIDValue.(int))) } if timezone, ok := d.GetOk("timezone"); ok { data.TimeZone = strToPtr(timezone.(string)) } data.Asns = []int64{} if asnsValue, ok := d.GetOk("asn_ids"); ok { data.Asns = toInt64List(asnsValue) } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := dcim.NewDcimSitesCreateParams().WithData(&data) res, err := api.Dcim.DcimSitesCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxSiteRead(d, m) } func resourceNetboxSiteRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimSitesReadParams().WithID(id) res, err := api.Dcim.DcimSitesRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimSitesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } site := res.GetPayload() d.Set("name", site.Name) d.Set("slug", site.Slug) d.Set("status", site.Status.Value) d.Set("description", site.Description) d.Set("comments", site.Comments) d.Set("facility", site.Facility) d.Set("longitude", site.Longitude) d.Set("latitude", site.Latitude) d.Set("physical_address", site.PhysicalAddress) d.Set("shipping_address", site.ShippingAddress) d.Set("timezone", site.TimeZone) d.Set("asn_ids", getIDsFromNestedASNList(site.Asns)) if res.GetPayload().Region != nil { d.Set("region_id", res.GetPayload().Region.ID) } else { d.Set("region_id", nil) } if res.GetPayload().Group != nil { d.Set("group_id", res.GetPayload().Group.ID) } else { d.Set("group_id", nil) } if res.GetPayload().Tenant != nil { d.Set("tenant_id", res.GetPayload().Tenant.ID) } else { d.Set("tenant_id", nil) } cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxSiteUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableSite{} name := d.Get("name").(string) data.Name = &name slugValue, slugOk := d.GetOk("slug") // Default slug to generated slug if not given if !slugOk { data.Slug = strToPtr(getSlug(name)) } else { data.Slug = strToPtr(slugValue.(string)) } data.Status = d.Get("status").(string) if description, ok := d.GetOk("description"); ok { data.Description = description.(string) } else if d.HasChange("description") { // If GetOK returned unset description and its value changed, set it as a space string to delete it ... data.Description = " " } if comments, ok := d.GetOk("comments"); ok { data.Comments = comments.(string) } else if d.HasChange("comments") { // If GetOK returned unset description and its value changed, set it as a space string to delete it ... data.Comments = " " } if facility, ok := d.GetOk("facility"); ok { data.Facility = facility.(string) } latitudeValue, ok := d.GetOk("latitude") if ok { data.Latitude = float64ToPtr(float64(latitudeValue.(float64))) } longitudeValue, ok := d.GetOk("longitude") if ok { data.Longitude = float64ToPtr(float64(longitudeValue.(float64))) } physicalAddressValue, ok := d.GetOk("physical_address") if ok { data.PhysicalAddress = physicalAddressValue.(string) } else if d.HasChange("physical_address") { // If GetOK returned unset description and its value changed, set it as a space string to delete it ... data.PhysicalAddress = " " } shippingAddressValue, ok := d.GetOk("shipping_address") if ok { data.ShippingAddress = shippingAddressValue.(string) } else if d.HasChange("shipping_address") { // If GetOK returned unset description and its value changed, set it as a space string to delete it ... data.ShippingAddress = " " } regionIDValue, ok := d.GetOk("region_id") if ok { data.Region = int64ToPtr(int64(regionIDValue.(int))) } groupIDValue, ok := d.GetOk("group_id") if ok { data.Group = int64ToPtr(int64(groupIDValue.(int))) } tenantIDValue, ok := d.GetOk("tenant_id") if ok { data.Tenant = int64ToPtr(int64(tenantIDValue.(int))) } if timezone, ok := d.GetOk("timezone"); ok { data.TimeZone = strToPtr(timezone.(string)) } data.Asns = []int64{} if asnsValue, ok := d.GetOk("asn_ids"); ok { data.Asns = toInt64List(asnsValue) } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } cf, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = cf } params := dcim.NewDcimSitesPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimSitesPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxSiteRead(d, m) } func resourceNetboxSiteDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimSitesDeleteParams().WithID(id) _, err := api.Dcim.DcimSitesDelete(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimSitesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } func getIDsFromNestedASNList(nestedASNs []*models.NestedASN) []int64 { var asns []int64 for _, asn := range nestedASNs { asns = append(asns, asn.ID) } return asns } ================================================ FILE: netbox/resource_netbox_site_group.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxSiteGroup() *schema.Resource { return &schema.Resource{ Create: resourceNetboxSiteGroupCreate, Read: resourceNetboxSiteGroupRead, Update: resourceNetboxSiteGroupUpdate, Delete: resourceNetboxSiteGroupDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/features/facilities/#site-groups): > Like regions, site groups can be arranged in a recursive hierarchy for grouping sites. However, whereas regions are intended for geographic organization, site groups may be used for functional grouping. For example, you might classify sites as corporate, branch, or customer sites in addition to where they are physically located. > > The use of both regions and site groups affords to independent but complementary dimensions across which sites can be organized.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "parent_id": { Type: schema.TypeInt, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxSiteGroupCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) parentID := int64(d.Get("parent_id").(int)) description := d.Get("description").(string) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } data := &models.WritableSiteGroup{} data.Name = &name data.Slug = &slug data.Description = description data.Tags = []*models.NestedTag{} if parentID != 0 { data.Parent = &parentID } params := dcim.NewDcimSiteGroupsCreateParams().WithData(data) res, err := api.Dcim.DcimSiteGroupsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxSiteGroupRead(d, m) } func resourceNetboxSiteGroupRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimSiteGroupsReadParams().WithID(id) res, err := api.Dcim.DcimSiteGroupsRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimSiteGroupsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } siteGroup := res.GetPayload() d.Set("name", siteGroup.Name) d.Set("slug", siteGroup.Slug) d.Set("description", siteGroup.Description) if siteGroup.Parent != nil { d.Set("parent_id", siteGroup.Parent.ID) } return nil } func resourceNetboxSiteGroupUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableSiteGroup{} name := d.Get("name").(string) description := d.Get("description").(string) parentID := int64(d.Get("parent_id").(int)) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } data.Slug = &slug data.Name = &name data.Description = description data.Tags = []*models.NestedTag{} if parentID != 0 { data.Parent = &parentID } params := dcim.NewDcimSiteGroupsPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Dcim.DcimSiteGroupsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxSiteGroupRead(d, m) } func resourceNetboxSiteGroupDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimSiteGroupsDeleteParams().WithID(id) _, err := api.Dcim.DcimSiteGroupsDelete(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimSiteGroupsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_site_group_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxSiteGroup_basic(t *testing.T) { testSlug := "s_grp_basic" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_site_group" "parent" { name = "%[1]s" slug = "%[2]s" description = "foo bar." } resource "netbox_site_group" "child" { name = "%[1]s-child" slug = "%[2]s-c" parent_id = netbox_site_group.parent.id }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_site_group.parent", "name", testName), resource.TestCheckResourceAttr("netbox_site_group.parent", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_site_group.parent", "description", "foo bar."), resource.TestCheckResourceAttr("netbox_site_group.child", "name", fmt.Sprintf("%s-child", testName)), resource.TestCheckResourceAttr("netbox_site_group.child", "slug", fmt.Sprintf("%s-c", randomSlug)), resource.TestCheckResourceAttrPair("netbox_site_group.child", "parent_id", "netbox_site_group.parent", "id"), ), }, { ResourceName: "netbox_site_group.parent", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxSiteGroup_defaultSlug(t *testing.T) { testSlug := "sitegrp_defSlug" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_site_group" "test" { name = "%s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_site_group.test", "name", testName), resource.TestCheckResourceAttr("netbox_site_group.test", "slug", getSlug(testName)), ), }, }, }) } func init() { resource.AddTestSweepers("netbox_site_group", &resource.Sweeper{ Name: "netbox_site_group", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimSiteGroupsListParams() res, err := api.Dcim.DcimSiteGroupsList(params, nil) if err != nil { return err } for _, siteGroup := range res.GetPayload().Results { if strings.HasPrefix(*siteGroup.Name, testPrefix) { deleteParams := dcim.NewDcimSiteGroupsDeleteParams().WithID(siteGroup.ID) _, err := api.Dcim.DcimSiteGroupsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a site group") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_site_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxSite_basic(t *testing.T) { testSlug := "site_basic" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_site_group" "test" { name = "%[1]s" } resource "netbox_rir" "test" { name = "%[1]s" } resource "netbox_asn" "test" { asn = 1338 rir_id = netbox_rir.test.id } resource "netbox_site" "test" { name = "%[1]s" slug = "%[2]s" status = "planned" description = "%[1]s" comments = "%[1]s" facility = "%[1]s" physical_address = "%[1]s" shipping_address = "%[1]s" asn_ids = [netbox_asn.test.id] group_id = netbox_site_group.test.id }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_site.test", "name", testName), resource.TestCheckResourceAttr("netbox_site.test", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_site.test", "status", "planned"), resource.TestCheckResourceAttr("netbox_site.test", "description", testName), resource.TestCheckResourceAttr("netbox_site.test", "comments", testName), resource.TestCheckResourceAttr("netbox_site.test", "facility", testName), resource.TestCheckResourceAttr("netbox_site.test", "physical_address", testName), resource.TestCheckResourceAttr("netbox_site.test", "shipping_address", testName), resource.TestCheckResourceAttr("netbox_site.test", "asn_ids.#", "1"), resource.TestCheckResourceAttrPair("netbox_site.test", "asn_ids.0", "netbox_asn.test", "id"), resource.TestCheckResourceAttrPair("netbox_site.test", "group_id", "netbox_site_group.test", "id"), ), }, { ResourceName: "netbox_site.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxSite_customFields(t *testing.T) { testSlug := "site_detail" testName := testAccGetTestName(testSlug) testField := strings.ReplaceAll(testAccGetTestName(testSlug), "-", "_") resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_custom_field" "test" { name = "%[1]s" type = "text" content_types = ["dcim.site"] } resource "netbox_site" "test" { name = "%[2]s" status = "decommissioning" latitude = "12.123456" longitude = "-13.123456" timezone = "Africa/Johannesburg" custom_fields = {"${netbox_custom_field.test.name}" = "81"} }`, testField, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_site.test", "status", "decommissioning"), resource.TestCheckResourceAttr("netbox_site.test", "custom_fields."+testField, "81"), resource.TestCheckResourceAttr("netbox_site.test", "timezone", "Africa/Johannesburg"), resource.TestCheckResourceAttr("netbox_site.test", "latitude", "12.123456"), resource.TestCheckResourceAttr("netbox_site.test", "longitude", "-13.123456"), ), }, }, }) } func TestAccNetboxSite_fieldUpdate(t *testing.T) { testSlug := "site_field_update" testName := testAccGetTestName(testSlug) testField := strings.ReplaceAll(testAccGetTestName(testSlug), "-", "_") resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_site" "test" { name = "%[2]s" description = "Test site description" comments = "Test comment" physical_address = "Physical address" shipping_address = "Shipping address" latitude = "12.123456" longitude = "-13.123456" }`, testField, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_site.test", "description", "Test site description"), resource.TestCheckResourceAttr("netbox_site.test", "comments", "Test comment"), resource.TestCheckResourceAttr("netbox_site.test", "physical_address", "Physical address"), resource.TestCheckResourceAttr("netbox_site.test", "shipping_address", "Shipping address"), resource.TestCheckResourceAttr("netbox_site.test", "latitude", "12.123456"), resource.TestCheckResourceAttr("netbox_site.test", "longitude", "-13.123456"), )}, { Config: fmt.Sprintf(` resource "netbox_site" "test" { name = "%[2]s" }`, testField, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_site.test", "description", ""), resource.TestCheckResourceAttr("netbox_site.test", "comments", ""), resource.TestCheckResourceAttr("netbox_site.test", "physical_address", ""), resource.TestCheckResourceAttr("netbox_site.test", "shipping_address", ""), resource.TestCheckResourceAttr("netbox_site.test", "latitude", "0"), resource.TestCheckResourceAttr("netbox_site.test", "longitude", "0"), ), }, }, }) } func init() { resource.AddTestSweepers("netbox_site", &resource.Sweeper{ Name: "netbox_site", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := dcim.NewDcimSitesListParams() res, err := api.Dcim.DcimSitesList(params, nil) if err != nil { return err } for _, site := range res.GetPayload().Results { if strings.HasPrefix(*site.Name, testPrefix) { deleteParams := dcim.NewDcimSitesDeleteParams().WithID(site.ID) _, err := api.Dcim.DcimSitesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a site") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_tag.go ================================================ package netbox import ( "regexp" "strconv" "github.com/fbreckle/go-netbox/netbox/client/extras" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxTag() *schema.Resource { return &schema.Resource{ Create: resourceNetboxTagCreate, Read: resourceNetboxTagRead, Update: resourceNetboxTagUpdate, Delete: resourceNetboxTagDelete, Description: `:meta:subcategory:Extras:From the [official documentation](https://docs.netbox.dev/en/stable/models/extras/tag/): > Tags are user-defined labels which can be applied to a variety of objects within NetBox. They can be used to establish dimensions of organization beyond the relationships built into NetBox. For example, you might create a tag to identify a particular ownership or condition across several types of objects. > > Each tag has a label, color, and a URL-friendly slug. For example, the slug for a tag named "Dunder Mifflin, Inc." would be dunder-mifflin-inc. The slug is generated automatically and makes tags easier to work with as URL parameters. Each tag can also be assigned a description indicating its purpose.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "color_hex": { Type: schema.TypeString, Optional: true, Default: "9e9e9e", ValidateFunc: validation.StringMatch(regexp.MustCompile("^[0-9a-f]{6}$"), "Must be hex color string"), }, "description": { Type: schema.TypeString, Optional: true, }, tagsKey: tagsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxTagCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } color := d.Get("color_hex").(string) description := d.Get("description").(string) params := extras.NewExtrasTagsCreateParams().WithData( &models.Tag{ Name: &name, Slug: &slug, Color: color, Description: description, }, ) res, err := api.Extras.ExtrasTagsCreate(params, nil) if err != nil { //return errors.New(getTextFromError(err)) return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxTagRead(d, m) } func resourceNetboxTagRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := extras.NewExtrasTagsReadParams().WithID(id) res, err := api.Extras.ExtrasTagsRead(params, nil) if err != nil { if errresp, ok := err.(*extras.ExtrasTagsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } d.Set("name", res.GetPayload().Name) d.Set("slug", res.GetPayload().Slug) d.Set("color_hex", res.GetPayload().Color) d.Set("description", res.GetPayload().Description) return nil } func resourceNetboxTagUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.Tag{} name := d.Get("name").(string) color := d.Get("color_hex").(string) description := d.Get("description").(string) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } data.Slug = &slug data.Name = &name data.Color = color data.Description = description params := extras.NewExtrasTagsUpdateParams().WithID(id).WithData(&data) _, err := api.Extras.ExtrasTagsUpdate(params, nil) if err != nil { return err } return resourceNetboxTagRead(d, m) } func resourceNetboxTagDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := extras.NewExtrasTagsDeleteParams().WithID(id) _, err := api.Extras.ExtrasTagsDelete(params, nil) if err != nil { if errresp, ok := err.(*extras.ExtrasTagsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_tag_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/extras" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxTag_basic(t *testing.T) { testSlug := "tag_basic" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tag" "test" { name = "%s" slug = "%s" color_hex = "112233" description = "This is a test" }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_tag.test", "name", testName), resource.TestCheckResourceAttr("netbox_tag.test", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_tag.test", "color_hex", "112233"), resource.TestCheckResourceAttr("netbox_tag.test", "description", "This is a test"), ), }, { ResourceName: "netbox_tag.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxTag_defaultSlug(t *testing.T) { testSlug := "tag_defSlug" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tag" "test" { name = "%s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_tag.test", "name", testName), resource.TestCheckResourceAttr("netbox_tag.test", "slug", getSlug(testName)), ), }, }, }) } func init() { resource.AddTestSweepers("netbox_tag", &resource.Sweeper{ Name: "netbox_tag", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := extras.NewExtrasTagsListParams() res, err := api.Extras.ExtrasTagsList(params, nil) if err != nil { return err } for _, tag := range res.GetPayload().Results { if strings.HasPrefix(*tag.Name, testPrefix) { deleteParams := extras.NewExtrasTagsDeleteParams().WithID(tag.ID) _, err := api.Extras.ExtrasTagsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a tag") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_tenant.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/tenancy" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxTenant() *schema.Resource { return &schema.Resource{ Create: resourceNetboxTenantCreate, Read: resourceNetboxTenantRead, Update: resourceNetboxTenantUpdate, Delete: resourceNetboxTenantDelete, Description: `:meta:subcategory:Tenancy:From the [official documentation](https://docs.netbox.dev/en/stable/features/tenancy/#tenants): > A tenant represents a discrete grouping of resources used for administrative purposes. Typically, tenants are used to represent individual customers or internal departments within an organization. > > Tenant assignment is used to signify the ownership of an object in NetBox. As such, each object may only be owned by a single tenant. For example, if you have a firewall dedicated to a particular customer, you would assign it to the tenant which represents that customer. However, if the firewall serves multiple customers, it doesn't belong to any particular customer, so tenant assignment would not be appropriate.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, tagsKey: tagsSchema, "group_id": { Type: schema.TypeInt, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxTenantCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) groupID := int64(d.Get("group_id").(int)) description := d.Get("description").(string) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) data := &models.WritableTenant{} data.Name = &name data.Slug = &slug data.Description = description data.Tags = tags if groupID != 0 { data.Group = &groupID } params := tenancy.NewTenancyTenantsCreateParams().WithData(data) res, err := api.Tenancy.TenancyTenantsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxTenantRead(d, m) } func resourceNetboxTenantRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := tenancy.NewTenancyTenantsReadParams().WithID(id) res, err := api.Tenancy.TenancyTenantsRead(params, nil) if err != nil { if errresp, ok := err.(*tenancy.TenancyTenantsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } d.Set("name", res.GetPayload().Name) d.Set("slug", res.GetPayload().Slug) d.Set("description", res.GetPayload().Description) if res.GetPayload().Group != nil { d.Set("group_id", res.GetPayload().Group.ID) } return nil } func resourceNetboxTenantUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableTenant{} name := d.Get("name").(string) description := d.Get("description").(string) groupID := int64(d.Get("group_id").(int)) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) data.Slug = &slug data.Name = &name data.Description = description data.Tags = tags if groupID != 0 { data.Group = &groupID } params := tenancy.NewTenancyTenantsPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Tenancy.TenancyTenantsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxTenantRead(d, m) } func resourceNetboxTenantDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := tenancy.NewTenancyTenantsDeleteParams().WithID(id) _, err := api.Tenancy.TenancyTenantsDelete(params, nil) if err != nil { if errresp, ok := err.(*tenancy.TenancyTenantsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_tenant_group.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/tenancy" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxTenantGroup() *schema.Resource { return &schema.Resource{ Create: resourceNetboxTenantGroupCreate, Read: resourceNetboxTenantGroupRead, Update: resourceNetboxTenantGroupUpdate, Delete: resourceNetboxTenantGroupDelete, Description: `:meta:subcategory:Tenancy:From the [official documentation](https://docs.netbox.dev/en/stable/features/tenancy/#tenant-groups): > Tenants can be organized by custom groups. For instance, you might create one group called "Customers" and one called "Departments." The assignment of a tenant to a group is optional. > > Tenant groups may be nested recursively to achieve a multi-level hierarchy. For example, you might have a group called "Customers" containing subgroups of individual tenants grouped by product or account team.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "parent_id": { Type: schema.TypeInt, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxTenantGroupCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) parentID := int64(d.Get("parent_id").(int)) description := d.Get("description").(string) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } data := &models.WritableTenantGroup{} data.Name = &name data.Slug = &slug data.Description = description data.Tags = []*models.NestedTag{} if parentID != 0 { data.Parent = &parentID } params := tenancy.NewTenancyTenantGroupsCreateParams().WithData(data) res, err := api.Tenancy.TenancyTenantGroupsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxTenantGroupRead(d, m) } func resourceNetboxTenantGroupRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := tenancy.NewTenancyTenantGroupsReadParams().WithID(id) res, err := api.Tenancy.TenancyTenantGroupsRead(params, nil) if err != nil { if errresp, ok := err.(*tenancy.TenancyTenantGroupsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } d.Set("name", res.GetPayload().Name) d.Set("slug", res.GetPayload().Slug) d.Set("description", res.GetPayload().Description) if res.GetPayload().Parent != nil { d.Set("parent", res.GetPayload().Parent.ID) } return nil } func resourceNetboxTenantGroupUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableTenantGroup{} name := d.Get("name").(string) description := d.Get("description").(string) parentID := int64(d.Get("parent_id").(int)) slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } data.Slug = &slug data.Name = &name data.Description = description data.Tags = []*models.NestedTag{} if parentID != 0 { data.Parent = &parentID } params := tenancy.NewTenancyTenantGroupsPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Tenancy.TenancyTenantGroupsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxTenantGroupRead(d, m) } func resourceNetboxTenantGroupDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := tenancy.NewTenancyTenantGroupsDeleteParams().WithID(id) _, err := api.Tenancy.TenancyTenantGroupsDelete(params, nil) if err != nil { if errresp, ok := err.(*tenancy.TenancyTenantGroupsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_tenant_group_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/tenancy" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxTenantGroup_basic(t *testing.T) { testSlug := "t_grp_basic" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tenant_group" "test" { name = "%s" slug = "%s" }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_tenant_group.test", "name", testName), resource.TestCheckResourceAttr("netbox_tenant_group.test", "slug", randomSlug), ), }, { ResourceName: "netbox_tenant_group.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxTenantGroup_defaultSlug(t *testing.T) { testSlug := "tenant_defSlug" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tenant_group" "test" { name = "%s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_tenant_group.test", "name", testName), resource.TestCheckResourceAttr("netbox_tenant_group.test", "slug", getSlug(testName)), ), }, }, }) } func init() { resource.AddTestSweepers("netbox_tenant_group", &resource.Sweeper{ Name: "netbox_tenant_group", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := tenancy.NewTenancyTenantGroupsListParams() res, err := api.Tenancy.TenancyTenantGroupsList(params, nil) if err != nil { return err } for _, tenant := range res.GetPayload().Results { if strings.HasPrefix(*tenant.Name, testPrefix) { deleteParams := tenancy.NewTenancyTenantGroupsDeleteParams().WithID(tenant.ID) _, err := api.Tenancy.TenancyTenantGroupsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a tenant group") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_tenant_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/tenancy" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxTenantTagDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tag" "test_a" { name = "%[1]sa" } resource "netbox_tag" "test_b" { name = "%[1]sb" } `, testName) } func TestAccNetboxTenant_basic(t *testing.T) { testSlug := "tenant_basic" testName := testAccGetTestName(testSlug) testDescription := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tenant" "test" { name = "%s" slug = "%s" description = "%s" }`, testName, randomSlug, testDescription), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_tenant.test", "name", testName), resource.TestCheckResourceAttr("netbox_tenant.test", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_tenant.test", "description", testDescription), ), }, { ResourceName: "netbox_tenant.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxTenant_defaultSlug(t *testing.T) { testSlug := "tenant_defSlug" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tenant" "test" { name = "%s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_tenant.test", "name", testName), resource.TestCheckResourceAttr("netbox_tenant.test", "slug", getSlug(testName)), ), }, }, }) } func TestAccNetboxTenant_tags(t *testing.T) { testSlug := "tenant_tags" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testAccNetboxTenantTagDependencies(testName) + fmt.Sprintf(` resource "netbox_tenant" "test_tags" { name = "%[1]s" tags = [netbox_tag.test_a.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_tenant.test_tags", "name", testName), resource.TestCheckResourceAttr("netbox_tenant.test_tags", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_tenant.test_tags", "tags.0", testName+"a"), ), }, { Config: testAccNetboxTenantTagDependencies(testName) + fmt.Sprintf(` resource "netbox_tenant" "test_tags" { name = "%[1]s" tags = [netbox_tag.test_a.name, netbox_tag.test_b.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_tenant.test_tags", "tags.#", "2"), resource.TestCheckResourceAttr("netbox_tenant.test_tags", "tags.0", testName+"a"), resource.TestCheckResourceAttr("netbox_tenant.test_tags", "tags.1", testName+"b"), ), }, { Config: testAccNetboxTenantTagDependencies(testName) + fmt.Sprintf(` resource "netbox_tenant" "test_tags" { name = "%s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_tenant.test_tags", "tags.#", "0"), ), }, }, }) } func init() { resource.AddTestSweepers("netbox_tenant", &resource.Sweeper{ Name: "netbox_tenant", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := tenancy.NewTenancyTenantsListParams() res, err := api.Tenancy.TenancyTenantsList(params, nil) if err != nil { return err } for _, tenant := range res.GetPayload().Results { if strings.HasPrefix(*tenant.Name, testPrefix) { deleteParams := tenancy.NewTenancyTenantsDeleteParams().WithID(tenant.ID) _, err := api.Tenancy.TenancyTenantsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a tenant") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_token.go ================================================ package netbox import ( "context" "strconv" "github.com/fbreckle/go-netbox/netbox/client/users" "github.com/fbreckle/go-netbox/netbox/models" "github.com/go-openapi/strfmt" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxToken() *schema.Resource { return &schema.Resource{ CreateContext: resourceNetboxTokenCreate, ReadContext: resourceNetboxTokenRead, UpdateContext: resourceNetboxTokenUpdate, DeleteContext: resourceNetboxTokenDelete, Description: `:meta:subcategory:Authentication:From the [official documentation](https://docs.netbox.dev/en/stable/rest-api/authentication/#tokens): > A token is a unique identifier mapped to a NetBox user account. Each user may have one or more tokens which he or she can use for authentication when making REST API requests. To create a token, navigate to the API tokens page under your user profile.`, Schema: map[string]*schema.Schema{ "user_id": { Type: schema.TypeInt, Required: true, }, "key": { Type: schema.TypeString, Sensitive: true, Optional: true, ValidateFunc: validation.StringLenBetween(40, 256), }, "allowed_ips": { Type: schema.TypeList, Optional: true, Elem: &schema.Schema{ Type: schema.TypeString, ValidateFunc: validation.IsCIDR, }, }, "write_enabled": { Type: schema.TypeBool, Optional: true, }, "last_used": { Type: schema.TypeString, Computed: true, }, "expires": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.IsRFC3339Time, }, "description": { Type: schema.TypeString, Optional: true, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxTokenCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) data := models.WritableToken{} userid := int64(d.Get("user_id").(int)) key := d.Get("key").(string) allowedIps := d.Get("allowed_ips").([]interface{}) data.User = &userid data.Key = key data.AllowedIps = make([]models.IPNetwork, len(allowedIps)) for i, v := range allowedIps { data.AllowedIps[i] = v } data.WriteEnabled = d.Get("write_enabled").(bool) data.Description = d.Get("description").(string) if expiresRaw, ok := d.GetOk("expires"); ok { expires, err := strfmt.ParseDateTime(expiresRaw.(string)) if err != nil { return diag.FromErr(err) } data.Expires = &expires } params := users.NewUsersTokensCreateParams().WithData(&data) res, err := api.Users.UsersTokensCreate(params, nil) if err != nil { return diag.FromErr(err) } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxTokenUpdate(ctx, d, m) } func resourceNetboxTokenRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := users.NewUsersTokensReadParams().WithID(id) res, err := api.Users.UsersTokensRead(params, nil) if err != nil { if errresp, ok := err.(*users.UsersTokensReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return diag.FromErr(err) } token := res.GetPayload() if token.User != nil { d.Set("user_id", token.User.ID) } // Since NetBox 4.3.0, ALLOW_TOKEN_RETRIEVAL is disabled by default // This means we will usually not get a Key value from the API if token.Key != "" { d.Set("key", token.Key) } d.Set("last_used", token.LastUsed) if token.Expires != nil { d.Set("expires", token.Expires.String()) } d.Set("allowed_ips", token.AllowedIps) d.Set("write_enabled", token.WriteEnabled) d.Set("description", token.Description) return nil } func resourceNetboxTokenUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableToken{} userid := int64(d.Get("user_id").(int)) key := d.Get("key").(string) allowedIps := d.Get("allowed_ips").([]interface{}) data.User = &userid data.Key = key data.AllowedIps = make([]models.IPNetwork, len(allowedIps)) for i, v := range allowedIps { data.AllowedIps[i] = v } data.WriteEnabled = d.Get("write_enabled").(bool) data.Description = d.Get("description").(string) if expiresRaw, ok := d.GetOk("expires"); ok { expires, err := strfmt.ParseDateTime(expiresRaw.(string)) if err != nil { return diag.FromErr(err) } data.Expires = &expires } params := users.NewUsersTokensUpdateParams().WithID(id).WithData(&data) _, err := api.Users.UsersTokensUpdate(params, nil) if err != nil { return diag.FromErr(err) } return resourceNetboxTokenRead(ctx, d, m) } func resourceNetboxTokenDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := users.NewUsersTokensDeleteParams().WithID(id) _, err := api.Users.UsersTokensDelete(params, nil) if err != nil { if errresp, ok := err.(*users.UsersTokensDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return diag.FromErr(err) } d.SetId("") return nil } ================================================ FILE: netbox/resource_netbox_token_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/users" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxToken_basic(t *testing.T) { testSlug := "users" testName := testAccGetTestName(testSlug) testToken := testAccGetTestToken() resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_user" "test" { username = "%s" password = "Abcdefghijkl1" } resource "netbox_token" "test_basic" { user_id = netbox_user.test.id key = "%s" allowed_ips = ["2.4.8.16/32"] write_enabled = false description = "Netbox Test Basic Token" expires = "2036-01-02T15:04:05.000Z" }`, testName, testToken), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_token.test_basic", "key", testToken), resource.TestCheckResourceAttr("netbox_token.test_basic", "allowed_ips.#", "1"), resource.TestCheckResourceAttr("netbox_token.test_basic", "allowed_ips.0", "2.4.8.16/32"), resource.TestCheckResourceAttr("netbox_token.test_basic", "write_enabled", "false"), resource.TestCheckResourceAttr("netbox_token.test_basic", "description", "Netbox Test Basic Token"), resource.TestCheckResourceAttr("netbox_token.test_basic", "expires", "2036-01-02T15:04:05.000Z"), ), }, { ResourceName: "netbox_token.test_basic", ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{"key"}, }, }, }) } func TestAccNetboxToken_withoutExpires(t *testing.T) { testSlug := "users" testName := testAccGetTestName(testSlug) testToken := testAccGetTestToken() resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_user" "test" { username = "%s" password = "Abcdefghijkl1" } resource "netbox_token" "test_without_expires" { user_id = netbox_user.test.id key = "%s" allowed_ips = ["2.4.8.16/32"] write_enabled = false description = "Netbox Token Without Expires" }`, testName, testToken), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_token.test_without_expires", "key", testToken), resource.TestCheckResourceAttr("netbox_token.test_without_expires", "allowed_ips.#", "1"), resource.TestCheckResourceAttr("netbox_token.test_without_expires", "allowed_ips.0", "2.4.8.16/32"), resource.TestCheckResourceAttr("netbox_token.test_without_expires", "write_enabled", "false"), resource.TestCheckResourceAttr("netbox_token.test_without_expires", "description", "Netbox Token Without Expires"), resource.TestCheckNoResourceAttr("netbox_token.test_without_expires", "expires"), ), }, { ResourceName: "netbox_token.test_without_expires", ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{"key"}, }, }, }) } func init() { resource.AddTestSweepers("netbox_token", &resource.Sweeper{ Name: "netbox_token", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := users.NewUsersUsersListParams() res, err := api.Users.UsersUsersList(params, nil) if err != nil { return err } for _, user := range res.GetPayload().Results { if strings.HasPrefix(*user.Username, testPrefix) { deleteParams := users.NewUsersUsersDeleteParams().WithID(user.ID) _, err := api.Users.UsersUsersDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a token") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_user.go ================================================ package netbox import ( "strconv" "time" "github.com/fbreckle/go-netbox/netbox/client/users" "github.com/fbreckle/go-netbox/netbox/models" "github.com/go-openapi/strfmt" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceNetboxUser() *schema.Resource { return &schema.Resource{ Create: resourceNetboxUserCreate, Read: resourceNetboxUserRead, Update: resourceNetboxUserUpdate, Delete: resourceNetboxUserDelete, Description: `:meta:subcategory:Authentication:This resource is used to manage users.`, Schema: map[string]*schema.Schema{ "username": { Type: schema.TypeString, Required: true, }, "password": { Type: schema.TypeString, Required: true, Sensitive: true, }, "email": { Type: schema.TypeString, Optional: true, Default: "", }, "first_name": { Type: schema.TypeString, Optional: true, Default: "", }, "last_name": { Type: schema.TypeString, Optional: true, Default: "", }, "active": { Type: schema.TypeBool, Optional: true, Default: true, }, "staff": { Type: schema.TypeBool, Optional: true, Default: false, }, "group_ids": { Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxUserCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableUser{} username := d.Get("username").(string) password := d.Get("password").(string) email := d.Get("email").(string) firstName := d.Get("first_name").(string) lastName := d.Get("last_name").(string) active := d.Get("active").(bool) staff := d.Get("staff").(bool) groupIDs := toInt64List(d.Get("group_ids")) data.Username = &username data.Password = &password data.Email = strfmt.Email(email) data.FirstName = firstName data.LastName = lastName data.IsActive = active data.IsStaff = staff data.Groups = groupIDs data.DateJoined = strfmt.DateTime(time.Now()) params := users.NewUsersUsersCreateParams().WithData(&data) res, err := api.Users.UsersUsersCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxUserRead(d, m) } func resourceNetboxUserRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := users.NewUsersUsersReadParams().WithID(id) res, err := api.Users.UsersUsersRead(params, nil) if err != nil { if errresp, ok := err.(*users.UsersUsersReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } if res.GetPayload().Username != nil { d.Set("username", res.GetPayload().Username) } d.Set("email", res.GetPayload().Email) d.Set("first_name", res.GetPayload().FirstName) d.Set("last_name", res.GetPayload().LastName) d.Set("staff", res.GetPayload().IsStaff) d.Set("active", res.GetPayload().IsActive) d.Set("group_ids", getIDsFromNestedGroup(res.GetPayload().Groups)) // Passwords cannot be set and not read return nil } func resourceNetboxUserUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableUser{} username := d.Get("username").(string) password := d.Get("password").(string) email := d.Get("email").(string) firstName := d.Get("first_name").(string) lastName := d.Get("last_name").(string) active := d.Get("active").(bool) staff := d.Get("staff").(bool) groupIDs := toInt64List(d.Get("group_ids")) data.Username = &username data.Password = &password data.Email = strfmt.Email(email) data.FirstName = firstName data.LastName = lastName data.IsActive = active data.IsStaff = staff data.Groups = groupIDs data.DateJoined = strfmt.DateTime(time.Now()) params := users.NewUsersUsersUpdateParams().WithID(id).WithData(&data) _, err := api.Users.UsersUsersUpdate(params, nil) if err != nil { return err } return resourceNetboxUserRead(d, m) } func resourceNetboxUserDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := users.NewUsersUsersDeleteParams().WithID(id) _, err := api.Users.UsersUsersDelete(params, nil) if err != nil { if errresp, ok := err.(*users.UsersUsersDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } d.SetId("") return nil } func getIDsFromNestedGroup(nestedGroups []*models.NestedGroup) []int64 { var groupIDs []int64 for _, group := range nestedGroups { groupIDs = append(groupIDs, group.ID) } return groupIDs } ================================================ FILE: netbox/resource_netbox_user_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/users" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxUser_basic(t *testing.T) { testSlug := "users" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_user" "test_basic" { username = "%s" password = "Abcdefghijkl1" active = true staff = true }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_user.test_basic", "username", testName), resource.TestCheckResourceAttr("netbox_user.test_basic", "active", "true"), resource.TestCheckResourceAttr("netbox_user.test_basic", "staff", "true"), ), }, { Config: fmt.Sprintf(` resource "netbox_user" "test_basic" { username = "%s" password = "Abcdefghijkl1" email = "foo@bar.com" first_name = "Hannah" last_name = "Acker" active = true staff = true }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_user.test_basic", "username", testName), resource.TestCheckResourceAttr("netbox_user.test_basic", "email", "foo@bar.com"), resource.TestCheckResourceAttr("netbox_user.test_basic", "first_name", "Hannah"), resource.TestCheckResourceAttr("netbox_user.test_basic", "last_name", "Acker"), resource.TestCheckResourceAttr("netbox_user.test_basic", "active", "true"), resource.TestCheckResourceAttr("netbox_user.test_basic", "staff", "true"), ), }, { ResourceName: "netbox_user.test_basic", ImportState: true, ImportStateVerify: false, }, }, }) } func TestAccNetboxUser_group(t *testing.T) { testSlug := "users" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_group" "test_group" { name = "%[1]s-group" } resource "netbox_user" "test_group" { username = "%[1]s" password = "Abcdefghijkl1" active = true staff = true group_ids = [netbox_group.test_group.id] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_user.test_group", "username", testName), resource.TestCheckResourceAttr("netbox_user.test_group", "active", "true"), resource.TestCheckResourceAttr("netbox_user.test_group", "staff", "true"), resource.TestCheckResourceAttr("netbox_user.test_group", "group_ids.#", "1"), ), }, { ResourceName: "netbox_user.test_group", ImportState: true, ImportStateVerify: false, }, }, }) } func init() { resource.AddTestSweepers("netbox_user", &resource.Sweeper{ Name: "netbox_user", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := users.NewUsersUsersListParams() res, err := api.Users.UsersUsersList(params, nil) if err != nil { return err } for _, user := range res.GetPayload().Results { if strings.HasPrefix(*user.Username, testPrefix) { deleteParams := users.NewUsersUsersDeleteParams().WithID(user.ID) _, err := api.Users.UsersUsersDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a user") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_virtual_chassis.go ================================================ package netbox import ( "context" "strconv" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceNetboxVirtualChassis() *schema.Resource { return &schema.Resource{ CreateContext: resourceNetboxVirtualChassisCreate, ReadContext: resourceNetboxVirtualChassisRead, UpdateContext: resourceNetboxVirtualChassisUpdate, DeleteContext: resourceNetboxVirtualChassisDelete, Description: `:meta:subcategory:Data Center Inventory Management (DCIM):From the [official documentation](https://docs.netbox.dev/en/stable/features/devices-cabling/#virtual-chassis): > Sometimes it is necessary to model a set of physical devices as sharing a single management plane. Perhaps the most common example of such a scenario is stackable switches. These can be modeled as virtual chassis in NetBox, with one device acting as the chassis master and the rest as members. All components of member devices will appear on the master.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "domain": { Type: schema.TypeString, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, "comments": { Type: schema.TypeString, Optional: true, }, tagsKey: tagsSchema, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxVirtualChassisCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) name := d.Get("name").(string) data := models.WritableVirtualChassis{ Name: &name, } domainValue, ok := d.GetOk("domain") if ok { domain := domainValue.(string) data.Domain = domain } descriptionValue, ok := d.GetOk("description") if ok { description := descriptionValue.(string) data.Description = description } commentsValue, ok := d.GetOk("comments") if ok { comments := commentsValue.(string) data.Comments = comments } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return diag.FromErr(err) } params := dcim.NewDcimVirtualChassisCreateParams().WithData(&data) res, err := api.Dcim.DcimVirtualChassisCreate(params, nil) if err != nil { return diag.FromErr(err) } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxVirtualChassisRead(ctx, d, m) } func resourceNetboxVirtualChassisRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimVirtualChassisReadParams().WithID(id) res, err := api.Dcim.DcimVirtualChassisRead(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimVirtualChassisReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { d.SetId("") return nil } } return diag.FromErr(err) } virtualChassis := res.GetPayload() d.Set("name", virtualChassis.Name) d.Set("domain", virtualChassis.Domain) d.Set("description", virtualChassis.Description) d.Set("comments", virtualChassis.Comments) cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, virtualChassis.Tags) return nil } func resourceNetboxVirtualChassisUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableVirtualChassis{} name := d.Get("name").(string) data.Name = &name domainValue, ok := d.GetOk("domain") if ok { domain := domainValue.(string) data.Domain = domain } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return diag.FromErr(err) } if d.HasChanges("comments") { // check if comment is set if commentsValue, ok := d.GetOk("comments"); ok { data.Comments = commentsValue.(string) } else { data.Comments = " " } } if d.HasChanges("description") { // check if description is set if descriptionValue, ok := d.GetOk("description"); ok { data.Description = descriptionValue.(string) } else { data.Description = " " } } params := dcim.NewDcimVirtualChassisUpdateParams().WithID(id).WithData(&data) _, err = api.Dcim.DcimVirtualChassisUpdate(params, nil) if err != nil { return diag.FromErr(err) } return resourceNetboxVirtualChassisRead(ctx, d, m) } func resourceNetboxVirtualChassisDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := dcim.NewDcimVirtualChassisDeleteParams().WithID(id) _, err := api.Dcim.DcimVirtualChassisDelete(params, nil) if err != nil { if errresp, ok := err.(*dcim.DcimVirtualChassisDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return diag.FromErr(err) } return nil } func virtualChassisUpdateMaster(api *providerState, id int64, master *int64) error { // Need to read the virtual chassis because we cannot do a partial update // because setting `master` to nil would omit it entirely, so we need to // do a PUT request instead of PATCH vcRes, err := api.Dcim.DcimVirtualChassisRead(dcim.NewDcimVirtualChassisReadParams().WithID(id), nil) if err != nil { return err } vcData := vcRes.GetPayload() vcData.Master = nil newTags := make([]*models.NestedTag, 0) for _, Tag := range vcData.Tags { Tag.Display = "" Tag.ID = 0 Tag.URL = "" newTags = append(newTags, Tag) } // Need to manually copy data because there is no automatic method to convert // from VirtualChassis to WritableVirtualChassis vcUpdateData := models.WritableVirtualChassis{ ID: vcData.ID, Description: vcData.Description, Domain: vcData.Domain, Name: vcData.Name, Comments: vcData.Comments, Tags: newTags, Master: master, } _, err = api.Dcim.DcimVirtualChassisUpdate(dcim.NewDcimVirtualChassisUpdateParams().WithID(id).WithData(&vcUpdateData), nil) return err } ================================================ FILE: netbox/resource_netbox_virtual_chassis_test.go ================================================ package netbox import ( "fmt" "strconv" "testing" "github.com/fbreckle/go-netbox/netbox/client/dcim" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccNetboxVirtualChassis_basic(t *testing.T) { testSlug := "virtual_chassis" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckVirtualChassisDestroy, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tag" "tag_a" { name = "[%[1]s_a]" color_hex = "123456" } resource "netbox_virtual_chassis" "test" { name = "%[1]s" domain = "domain" description = "description" comments = "comment" tags = [netbox_tag.tag_a.name] } `, testName), }, { Config: fmt.Sprintf(` resource "netbox_tag" "tag_a" { name = "[%[1]s_a]" color_hex = "123456" } resource "netbox_virtual_chassis" "test" { name = "%[1]s_updated" domain = "domain_updated" description = "description updated" comments = "comment updated" tags = [netbox_tag.tag_a.name] } `, testName), }, { ResourceName: "netbox_virtual_chassis.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckVirtualChassisDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each virtual machine // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_virtual_chassis" { continue } stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := dcim.NewDcimVirtualChassisReadParams().WithID(stateID) _, err := conn.Dcim.DcimVirtualChassisRead(params, nil) if err == nil { return fmt.Errorf("virtual chassis (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*dcim.DcimVirtualChassisReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } ================================================ FILE: netbox/resource_netbox_virtual_disk.go ================================================ package netbox import ( "context" "strconv" "github.com/fbreckle/go-netbox/netbox/client/virtualization" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceNetboxVirtualDisks() *schema.Resource { return &schema.Resource{ CreateContext: resourceNetboxVirtualDisksCreate, ReadContext: resourceNetboxVirtualDisksRead, UpdateContext: resourceNetboxVirtualDisksUpdate, DeleteContext: resourceNetboxVirtualDisksDelete, Description: `:meta:subcategory:Virtualization:From the [official documentation](https://docs.netbox.dev/en/stable/models/virtualization/virtualdisk/): > A virtual disk is used to model discrete virtual hard disks assigned to virtual machines.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "description": { Type: schema.TypeString, Optional: true, }, "size_mb": { Type: schema.TypeInt, Required: true, }, "virtual_machine_id": { Type: schema.TypeInt, Required: true, }, tagsKey: tagsSchema, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, SchemaVersion: 1, StateUpgraders: []schema.StateUpgrader{ { Type: resourceNetboxVirtualDiskResourceV0().CoreConfigSchema().ImpliedType(), Upgrade: resourceNetboxVirtualDiskStateUpgradeV0, Version: 0, }, }, } } func resourceNetboxVirtualDisksCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) name := d.Get("name").(string) size := d.Get("size_mb").(int) virtualMachineID := d.Get("virtual_machine_id").(int) data := models.WritableVirtualDisk{ Name: &name, Size: int64ToPtr(int64(size)), VirtualMachine: int64ToPtr(int64(virtualMachineID)), } descriptionValue, ok := d.GetOk("description") if ok { description := descriptionValue.(string) data.Description = description } ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return diag.FromErr(err) } params := virtualization.NewVirtualizationVirtualDisksCreateParams().WithData(&data) res, err := api.Virtualization.VirtualizationVirtualDisksCreate(params, nil) if err != nil { return diag.FromErr(err) } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxVirtualDisksRead(ctx, d, m) } func resourceNetboxVirtualDisksRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := virtualization.NewVirtualizationVirtualDisksReadParams().WithID(id) res, err := api.Virtualization.VirtualizationVirtualDisksRead(params, nil) if err != nil { if errresp, ok := err.(*virtualization.VirtualizationVirtualDisksReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { d.SetId("") return nil } } return diag.FromErr(err) } VirtualDisks := res.GetPayload() d.Set("name", VirtualDisks.Name) d.Set("description", VirtualDisks.Description) if VirtualDisks.Size != nil { d.Set("size_mb", *VirtualDisks.Size) } if VirtualDisks.VirtualMachine != nil { d.Set("virtual_machine_id", VirtualDisks.VirtualMachine.ID) } cf := getCustomFields(res.GetPayload().CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, VirtualDisks.Tags) return nil } func resourceNetboxVirtualDisksUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableVirtualDisk{} name := d.Get("name").(string) size := int64(d.Get("size_mb").(int)) virtualMachineID := int64(d.Get("virtual_machine_id").(int)) data.Name = &name data.Size = &size data.VirtualMachine = &virtualMachineID ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return diag.FromErr(err) } if d.HasChanges("description") { // check if description is set if descriptionValue, ok := d.GetOk("description"); ok { data.Description = descriptionValue.(string) } else { data.Description = " " } } params := virtualization.NewVirtualizationVirtualDisksUpdateParams().WithID(id).WithData(&data) _, err = api.Virtualization.VirtualizationVirtualDisksUpdate(params, nil) if err != nil { return diag.FromErr(err) } return resourceNetboxVirtualDisksRead(ctx, d, m) } func resourceNetboxVirtualDisksDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := virtualization.NewVirtualizationVirtualDisksDeleteParams().WithID(id) _, err := api.Virtualization.VirtualizationVirtualDisksDelete(params, nil) if err != nil { if errresp, ok := err.(*virtualization.VirtualizationVirtualDisksDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return diag.FromErr(err) } return nil } ================================================ FILE: netbox/resource_netbox_virtual_disk_migrate_v0.go ================================================ package netbox import ( "context" "log" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceNetboxVirtualDiskResourceV0() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "description": { Type: schema.TypeString, Optional: true, }, "size_mb": { Type: schema.TypeInt, Required: true, }, "virtual_machine_id": { Type: schema.TypeInt, Required: true, }, tagsKey: tagsSchema, customFieldsKey: customFieldsSchema, }, } } func resourceNetboxVirtualDiskStateUpgradeV0(_ context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { v, ok := rawState["size_gb"].(float64) if !ok { log.Printf("[DEBUG] disk size before migration isnt float64: %#v\n but a %T", rawState["size_gb"], rawState["size_gb"]) rawState["size_mb"] = float64(0) delete(rawState, "size_gb") return rawState, nil } log.Printf("[DEBUG] disk size in GB before migration: %#v\n", rawState["size_gb"]) // set new disk size rawState["size_mb"] = v * 1000 log.Printf("[DEBUG] disk size in MB after migration: %#v\n", rawState["size_mb"]) delete(rawState, "size_gb") return rawState, nil } ================================================ FILE: netbox/resource_netbox_virtual_disk_test.go ================================================ package netbox import ( "fmt" "strconv" "testing" "github.com/fbreckle/go-netbox/netbox/client/virtualization" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccNetboxVirtualDisk_basic(t *testing.T) { testSlug := "virtual_disk" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckVirtualDiskDestroy, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tag" "tag_a" { name = "[%[1]s_a]" color_hex = "123456" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_virtual_machine" "test" { name = "%[1]s" site_id = netbox_site.test.id } resource "netbox_virtual_disk" "test" { name = "%[1]s" description = "description" size_mb = 30 virtual_machine_id = netbox_virtual_machine.test.id tags = [netbox_tag.tag_a.name] } `, testName), }, { Config: fmt.Sprintf(` resource "netbox_tag" "tag_a" { name = "[%[1]s_a]" color_hex = "123456" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_virtual_machine" "test" { name = "%[1]s" site_id = netbox_site.test.id } resource "netbox_virtual_disk" "test" { name = "%[1]s_updated" description = "description updated" size_mb = 60 virtual_machine_id = netbox_virtual_machine.test.id tags = [netbox_tag.tag_a.name] } `, testName), }, { ResourceName: "netbox_virtual_disk.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckVirtualDiskDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each virtual machine // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_virtual_disk" { continue } stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := virtualization.NewVirtualizationVirtualDisksReadParams().WithID(stateID) _, err := conn.Virtualization.VirtualizationVirtualDisksRead(params, nil) if err == nil { return fmt.Errorf("virtual disk (%s) still exists", rs.Primary.ID) } if errresp, ok := err.(*virtualization.VirtualizationVirtualDisksReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_virtual_machine.go ================================================ package netbox import ( "context" "encoding/json" "strconv" "github.com/fbreckle/go-netbox/netbox/client/virtualization" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var resourceNetboxVirtualMachineStatusOptions = []string{"offline", "active", "planned", "staged", "failed", "decommissioning"} func resourceNetboxVirtualMachine() *schema.Resource { return &schema.Resource{ CreateContext: resourceNetboxVirtualMachineCreate, ReadContext: resourceNetboxVirtualMachineRead, UpdateContext: resourceNetboxVirtualMachineUpdate, DeleteContext: resourceNetboxVirtualMachineDelete, Description: `:meta:subcategory:Virtualization:From the [official documentation](https://docs.netbox.dev/en/stable/features/virtualization/#virtual-machines): > A virtual machine is a virtualized compute instance. These behave in NetBox very similarly to device objects, but without any physical attributes. For example, a VM may have interfaces assigned to it with IP addresses and VLANs, however its interfaces cannot be connected via cables (because they are virtual). Each VM may also define its compute, memory, and storage resources as well.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "cluster_id": { Type: schema.TypeInt, Optional: true, AtLeastOneOf: []string{"site_id", "cluster_id"}, }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, "device_id": { Type: schema.TypeInt, Optional: true, }, "platform_id": { Type: schema.TypeInt, Optional: true, }, "role_id": { Type: schema.TypeInt, Optional: true, }, "site_id": { Type: schema.TypeInt, Optional: true, AtLeastOneOf: []string{"site_id", "cluster_id"}, }, "comments": { Type: schema.TypeString, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, "memory_mb": { Type: schema.TypeInt, Optional: true, }, "vcpus": { Type: schema.TypeFloat, Optional: true, }, "disk_size_mb": { Type: schema.TypeInt, Optional: true, Computed: true, Description: "The disk size in MB. When virtual disks are attached to this VM, NetBox automatically computes this as the aggregate of those disks and rejects manual values. In that case, omit this field from the configuration and let it be computed.", }, "status": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringInSlice(resourceNetboxVirtualMachineStatusOptions, false), Default: "active", Description: buildValidValueDescription(resourceNetboxVirtualMachineStatusOptions), }, tagsKey: tagsSchema, "primary_ipv4": { Type: schema.TypeInt, Computed: true, }, "primary_ipv6": { Type: schema.TypeInt, Computed: true, }, "local_context_data": { Type: schema.TypeString, Optional: true, Description: "This is best managed through the use of `jsonencode` and a map of settings.", }, customFieldsKey: customFieldsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, SchemaVersion: 2, StateUpgraders: []schema.StateUpgrader{ { Type: resourceNetboxVirtualMachineResourceV0().CoreConfigSchema().ImpliedType(), Upgrade: resourceNetboxVirtualMachineStateUpgradeV0, Version: 0, }, { Type: resourceNetboxVirtualMachineResourceV1().CoreConfigSchema().ImpliedType(), Upgrade: resourceNetboxVirtualMachineStateUpgradeV1, Version: 1, }, }, } } func resourceNetboxVirtualMachineCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) name := d.Get("name").(string) data := models.WritableVirtualMachineWithConfigContext{ Name: &name, } clusterIDValue, ok := d.GetOk("cluster_id") if ok { clusterID := int64(clusterIDValue.(int)) data.Cluster = &clusterID } siteIDValue, ok := d.GetOk("site_id") if ok { siteID := int64(siteIDValue.(int)) data.Site = &siteID } data.Comments = d.Get("comments").(string) data.Description = d.Get("description").(string) vcpusValue, ok := d.GetOk("vcpus") if ok { vcpus := vcpusValue.(float64) data.Vcpus = &vcpus } memoryMbValue, ok := d.GetOk("memory_mb") if ok { memoryMb := int64(memoryMbValue.(int)) data.Memory = &memoryMb } diskSizeValue, ok := d.GetOk("disk_size_mb") if ok { diskSize := int64(diskSizeValue.(int)) data.Disk = &diskSize } tenantIDValue, ok := d.GetOk("tenant_id") if ok { tenantID := int64(tenantIDValue.(int)) data.Tenant = &tenantID } deviceIDValue, ok := d.GetOk("device_id") if ok { deviceID := int64(deviceIDValue.(int)) data.Device = &deviceID } platformIDValue, ok := d.GetOk("platform_id") if ok { platformID := int64(platformIDValue.(int)) data.Platform = &platformID } roleIDValue, ok := d.GetOk("role_id") if ok { roleID := int64(roleIDValue.(int)) data.Role = &roleID } localContextValue, ok := d.GetOk("local_context_data") if ok { var jsonObj any localContextBA := []byte(localContextValue.(string)) if err := json.Unmarshal(localContextBA, &jsonObj); err == nil { data.LocalContextData = jsonObj } } data.Status = d.Get("status").(string) tags, err := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return diag.FromErr(err) } data.Tags = tags ct, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = ct } params := virtualization.NewVirtualizationVirtualMachinesCreateParams().WithData(&data) res, err := api.Virtualization.VirtualizationVirtualMachinesCreate(params, nil) if err != nil { return diag.FromErr(err) } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxVirtualMachineRead(ctx, d, m) } func resourceNetboxVirtualMachineRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) var diags diag.Diagnostics id, _ := strconv.ParseInt(d.Id(), 10, 64) params := virtualization.NewVirtualizationVirtualMachinesReadParams().WithID(id) res, err := api.Virtualization.VirtualizationVirtualMachinesRead(params, nil) if err != nil { if errresp, ok := err.(*virtualization.VirtualizationVirtualMachinesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return diag.FromErr(err) } vm := res.GetPayload() d.Set("name", vm.Name) if vm.Cluster != nil { d.Set("cluster_id", vm.Cluster.ID) } else { d.Set("cluster_id", nil) } if vm.PrimaryIp4 != nil { d.Set("primary_ipv4", vm.PrimaryIp4.ID) } else { d.Set("primary_ipv4", nil) } if vm.PrimaryIp6 != nil { d.Set("primary_ipv6", vm.PrimaryIp6.ID) } else { d.Set("primary_ipv6", nil) } if vm.Tenant != nil { d.Set("tenant_id", vm.Tenant.ID) } else { d.Set("tenant_id", nil) } if vm.Device != nil { d.Set("device_id", vm.Device.ID) } else { d.Set("device_id", nil) } if vm.Role != nil { d.Set("role_id", vm.Role.ID) } else { d.Set("role_id", nil) } if vm.Platform != nil { d.Set("platform_id", vm.Platform.ID) } else { d.Set("platform_id", nil) } if vm.Role != nil { d.Set("role_id", vm.Role.ID) } else { d.Set("role_id", nil) } if vm.Site != nil { d.Set("site_id", vm.Site.ID) } else { d.Set("site_id", nil) } if vm.LocalContextData != nil { if jsonArr, err := json.Marshal(vm.LocalContextData); err == nil { d.Set("local_context_data", string(jsonArr)) } } else { d.Set("local_context_data", nil) } d.Set("comments", vm.Comments) d.Set("description", vm.Description) vcpus := vm.Vcpus if vcpus != nil { d.Set("vcpus", vm.Vcpus) } else { d.Set("vcpus", nil) } d.Set("memory_mb", vm.Memory) d.Set("disk_size_mb", vm.Disk) if vm.Status != nil { d.Set("status", vm.Status.Value) } else { d.Set("status", nil) } api.readTags(d, vm.Tags) cf := getCustomFields(vm.CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } return diags } func resourceNetboxVirtualMachineUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableVirtualMachineWithConfigContext{} name := d.Get("name").(string) data.Name = &name clusterIDValue, ok := d.GetOk("cluster_id") if ok { clusterID := int64(clusterIDValue.(int)) data.Cluster = &clusterID } siteIDValue, ok := d.GetOk("site_id") if ok { siteID := int64(siteIDValue.(int)) data.Site = &siteID } tenantIDValue, ok := d.GetOk("tenant_id") if ok { tenantID := int64(tenantIDValue.(int)) data.Tenant = &tenantID } deviceIDValue, ok := d.GetOk("device_id") if ok { deviceID := int64(deviceIDValue.(int)) data.Device = &deviceID } platformIDValue, ok := d.GetOk("platform_id") if ok { platformID := int64(platformIDValue.(int)) data.Platform = &platformID } roleIDValue, ok := d.GetOk("role_id") if ok { roleID := int64(roleIDValue.(int)) data.Role = &roleID } memoryMbValue, ok := d.GetOk("memory_mb") if ok { memoryMb := int64(memoryMbValue.(int)) data.Memory = &memoryMb } vcpusValue, ok := d.GetOk("vcpus") if ok { vcpus := vcpusValue.(float64) data.Vcpus = &vcpus } // Only send disk_size_mb when explicitly set in config. When virtual disks // are attached, NetBox computes this as their aggregate and rejects any // value that does not match — including stale prior-state values. if !d.GetRawConfig().GetAttr("disk_size_mb").IsNull() { diskSize := int64(d.Get("disk_size_mb").(int)) data.Disk = &diskSize } primaryIP4Value, ok := d.GetOk("primary_ipv4") if ok { primaryIP4 := int64(primaryIP4Value.(int)) data.PrimaryIp4 = &primaryIP4 } primaryIP6Value, ok := d.GetOk("primary_ipv6") if ok { primaryIP6 := int64(primaryIP6Value.(int)) data.PrimaryIp6 = &primaryIP6 } localContextValue, ok := d.GetOk("local_context_data") if ok { var jsonObj any localContextBA := []byte(localContextValue.(string)) if err := json.Unmarshal(localContextBA, &jsonObj); err == nil { data.LocalContextData = jsonObj } } tags, err := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return diag.FromErr(err) } data.Tags = tags cf, ok := d.GetOk(customFieldsKey) if ok { data.CustomFields = cf } if d.HasChanges("comments") { // check if comment is set if commentsValue, ok := d.GetOk("comments"); ok { data.Comments = commentsValue.(string) } else { data.Comments = " " } } if d.HasChanges("description") { // check if description is set if descriptionValue, ok := d.GetOk("description"); ok { data.Description = descriptionValue.(string) } else { data.Description = " " } } // if d.HasChanges("status") { if status, ok := d.GetOk("status"); ok { data.Status = status.(string) } //} params := virtualization.NewVirtualizationVirtualMachinesUpdateParams().WithID(id).WithData(&data) _, err = api.Virtualization.VirtualizationVirtualMachinesUpdate(params, nil) if err != nil { return diag.FromErr(err) } return resourceNetboxVirtualMachineRead(ctx, d, m) } func resourceNetboxVirtualMachineDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { api := m.(*providerState) var diags diag.Diagnostics id, _ := strconv.ParseInt(d.Id(), 10, 64) params := virtualization.NewVirtualizationVirtualMachinesDeleteParams().WithID(id) _, err := api.Virtualization.VirtualizationVirtualMachinesDelete(params, nil) if err != nil { if errresp, ok := err.(*virtualization.VirtualizationVirtualMachinesDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return diag.FromErr(err) } return diags } ================================================ FILE: netbox/resource_netbox_virtual_machine_migrate_v0.go ================================================ package netbox import ( "context" "log" "strconv" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceNetboxVirtualMachineResourceV0() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "cluster_id": { Type: schema.TypeInt, Required: true, }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, "platform_id": { Type: schema.TypeInt, Optional: true, }, "role_id": { Type: schema.TypeInt, Optional: true, }, "site_id": { Type: schema.TypeInt, Computed: true, }, "comments": { Type: schema.TypeString, Optional: true, }, "memory_mb": { Type: schema.TypeInt, Optional: true, }, "vcpus": { Type: schema.TypeString, Optional: true, }, "disk_size_gb": { Type: schema.TypeInt, Optional: true, }, tagsKey: tagsSchema, "primary_ipv4": { Type: schema.TypeInt, Computed: true, }, }, } } func resourceNetboxVirtualMachineStateUpgradeV0(_ context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { v, ok := rawState["vcpus"] if !ok { return rawState, nil } s, ok := v.(string) if !ok { // since the provider was already released without this state migration, we have to accept that this field already contains non-string content return rawState, nil } log.Printf("[DEBUG] vcpus before migration: %#v", rawState["vcpus"]) f, err := strconv.ParseFloat(s, 64) if err == nil { rawState["vcpus"] = f } else { rawState["vcpus"] = float64(0) log.Printf("[DEBUG] Schema upgrade: vcpus has been migrated to %g", f) } log.Printf("[DEBUG] vcpus after migration: %#v", rawState["vcpus"]) return rawState, nil } ================================================ FILE: netbox/resource_netbox_virtual_machine_migrate_v0_test.go ================================================ package netbox import ( "context" "reflect" "testing" ) func TestResourceNetboxVirtualMachineStateUpgradeV0(t *testing.T) { for _, tt := range []struct { name string state map[string]interface{} expected map[string]interface{} }{ { name: "Empty", state: map[string]interface{}{"vcpus": ""}, expected: map[string]interface{}{"vcpus": float64(0)}, }, { name: "Zero", state: map[string]interface{}{"vcpus": "0"}, expected: map[string]interface{}{"vcpus": float64(0)}, }, { name: "NonZero", state: map[string]interface{}{"vcpus": "123"}, expected: map[string]interface{}{"vcpus": float64(123)}, }, { name: "Float", state: map[string]interface{}{"vcpus": "4.5"}, expected: map[string]interface{}{"vcpus": 4.5}, }, { name: "Invalid", state: map[string]interface{}{"vcpus": "foo"}, expected: map[string]interface{}{"vcpus": float64(0)}, }, { name: "FloatAsFloat", state: map[string]interface{}{"vcpus": 4.5}, expected: map[string]interface{}{"vcpus": 4.5}, }, } { t.Run(tt.name, func(t *testing.T) { actual, err := resourceNetboxVirtualMachineStateUpgradeV0(context.Background(), tt.state, nil) if err != nil { t.Fatalf("error migrating state: %s", err) } if !reflect.DeepEqual(tt.expected, actual) { t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", tt.expected, actual) } }) } } ================================================ FILE: netbox/resource_netbox_virtual_machine_migrate_v1.go ================================================ package netbox import ( "context" "log" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceNetboxVirtualMachineResourceV1() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "cluster_id": { Type: schema.TypeInt, Optional: true, AtLeastOneOf: []string{"site_id", "cluster_id"}, }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, "device_id": { Type: schema.TypeInt, Optional: true, }, "platform_id": { Type: schema.TypeInt, Optional: true, }, "role_id": { Type: schema.TypeInt, Optional: true, }, "site_id": { Type: schema.TypeInt, Optional: true, AtLeastOneOf: []string{"site_id", "cluster_id"}, }, "comments": { Type: schema.TypeString, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, "memory_mb": { Type: schema.TypeInt, Optional: true, }, "vcpus": { Type: schema.TypeFloat, Optional: true, }, "disk_size_gb": { Type: schema.TypeInt, Optional: true, Computed: true, }, "status": { Type: schema.TypeString, Optional: true, Default: "active", }, tagsKey: tagsSchema, "primary_ipv4": { Type: schema.TypeInt, Computed: true, }, "primary_ipv6": { Type: schema.TypeInt, Computed: true, }, "local_context_data": { Type: schema.TypeString, Optional: true, Description: "This is best managed through the use of `jsonencode` and a map of settings.", }, customFieldsKey: customFieldsSchema, }, } } func resourceNetboxVirtualMachineStateUpgradeV1(_ context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { v, ok := rawState["disk_size_gb"].(float64) if !ok { log.Printf("[DEBUG] disk size before migration isnt float64: %#v\n but a %T", rawState["disk_size_gb"], rawState["disk_size_gb"]) rawState["disk_size_mb"] = float64(0) delete(rawState, "disk_size_gb") return rawState, nil } log.Printf("[DEBUG] disk size in GB before migration: %#v\n", rawState["disk_size_gb"]) // set new disk size rawState["disk_size_mb"] = v * 1000 log.Printf("[DEBUG] disk size in MB after migration: %#v\n", rawState["disk_size_mb"]) delete(rawState, "disk_size_gb") return rawState, nil } ================================================ FILE: netbox/resource_netbox_virtual_machine_migrate_v1_test.go ================================================ package netbox import ( "context" "reflect" "testing" ) func TestResourceNetboxVirtualMachineStateUpgradeV1(t *testing.T) { for _, tt := range []struct { name string state map[string]interface{} expected map[string]interface{} }{ { name: "Zero", state: map[string]interface{}{"disk_size_gb": float64(0)}, expected: map[string]interface{}{"disk_size_mb": float64(0)}, }, { name: "NonZero", state: map[string]interface{}{"disk_size_gb": float64(123)}, expected: map[string]interface{}{"disk_size_mb": float64(123000)}, }, { name: "Invalid", state: map[string]interface{}{"disk_size_gb": "foo"}, expected: map[string]interface{}{"disk_size_mb": float64(0)}, }, } { t.Run(tt.name, func(t *testing.T) { actual, err := resourceNetboxVirtualMachineStateUpgradeV1(context.Background(), tt.state, nil) if err != nil { t.Fatalf("error migrating state: %s", err) } if !reflect.DeepEqual(tt.expected, actual) { t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", tt.expected, actual) } }) } } ================================================ FILE: netbox/resource_netbox_virtual_machine_test.go ================================================ package netbox import ( "fmt" "log" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/virtualization" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func testAccNetboxVirtualMachineFullDependencies(testName string) string { testSlug := "device_type" randomSlug := testAccGetTestName(testSlug) return fmt.Sprintf(` resource "netbox_cluster_type" "test" { name = "%[1]s" } resource "netbox_cluster" "test" { name = "%[1]s" cluster_type_id = netbox_cluster_type.test.id site_id = netbox_site.test.id } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "123456" } resource "netbox_platform" "test" { name = "%[1]s" } resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_tag" "test_a" { name = "%[1]sa" } resource "netbox_tag" "test_b" { name = "%[1]sb" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" slug = "%[2]s" part_number = "%[2]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device" "test" { name = "%[1]s" role_id = netbox_device_role.test.id device_type_id = netbox_device_type.test.id site_id = netbox_site.test.id cluster_id = netbox_cluster.test.id } `, testName, randomSlug) } func testAccNetboxVirtualMachineSiteClusterDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_cluster_type" "test" { name = "%[1]s" } resource "netbox_cluster" "test" { name = "%[1]s" cluster_type_id = netbox_cluster_type.test.id site_id = netbox_site.test.id } `, testName) } func TestAccNetboxVirtualMachine_SiteOnly(t *testing.T) { testSlug := "vm_site" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckVirtualMachineDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxVirtualMachineSiteClusterDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "only_site" { name = "%s" site_id = netbox_site.test.id } `, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.only_site", "name", testName), resource.TestCheckResourceAttrPair("netbox_virtual_machine.only_site", "site_id", "netbox_site.test", "id"), ), }, { ResourceName: "netbox_virtual_machine.only_site", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxVirtualMachine_ClusterWithoutSite(t *testing.T) { testSlug := "vm_clstrnosite" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckVirtualMachineDestroy, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_cluster_type" "test" { name = "%[1]s" } resource "netbox_cluster" "test" { name = "%[1]s" cluster_type_id = netbox_cluster_type.test.id } resource "netbox_virtual_machine" "cluster_without_site" { name = "%[1]s" cluster_id = netbox_cluster.test.id } `, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.cluster_without_site", "name", testName), resource.TestCheckResourceAttrPair("netbox_virtual_machine.cluster_without_site", "cluster_id", "netbox_cluster.test", "id"), ), }, { ResourceName: "netbox_virtual_machine.cluster_without_site", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxVirtualMachine_basic(t *testing.T) { testSlug := "vm_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckVirtualMachineDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test" { name = "%s" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "cluster_id", "netbox_cluster.test", "id"), ), }, { Config: testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test" { name = "%[1]s" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id comments = "thisisacomment" description = "thisisadescription" memory_mb = 1024 disk_size_mb = 256 tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id platform_id = netbox_platform.test.id device_id = netbox_device.test.id vcpus = 4 status = "active" tags = [netbox_tag.test_a.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "cluster_id", "netbox_cluster.test", "id"), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "platform_id", "netbox_platform.test", "id"), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "role_id", "netbox_device_role.test", "id"), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "device_id", "netbox_device.test", "id"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "comments", "thisisacomment"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "description", "thisisadescription"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "memory_mb", "1024"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "vcpus", "4"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "disk_size_mb", "256"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "status", "active"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "tags.0", testName+"a"), ), }, { Config: testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test" { name = "%[1]s" site_id = netbox_site.test.id comments = "thisisacomment" description = "thisisadescription" memory_mb = 1024 disk_size_mb = 256 tenant_id = netbox_tenant.test.id role_id = netbox_device_role.test.id platform_id = netbox_platform.test.id vcpus = 4 status = "active" tags = [netbox_tag.test_a.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "platform_id", "netbox_platform.test", "id"), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "role_id", "netbox_device_role.test", "id"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "comments", "thisisacomment"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "description", "thisisadescription"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "memory_mb", "1024"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "vcpus", "4"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "disk_size_mb", "256"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "status", "active"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "tags.0", testName+"a"), ), }, { Config: testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test" { name = "%s" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id tenant_id = netbox_tenant.test.id platform_id = netbox_platform.test.id }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "cluster_id", "netbox_cluster.test", "id"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "vcpus", "0"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "memory_mb", "0"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "comments", ""), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "description", ""), ), }, { ResourceName: "netbox_virtual_machine.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxVirtualMachine_fractionalVcpu(t *testing.T) { testSlug := "vm_fracVcpu" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckVirtualMachineDestroy, Steps: []resource.TestStep{ { Config: testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test" { name = "%s" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id vcpus = 2.50 }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "cluster_id", "netbox_cluster.test", "id"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "vcpus", "2.5"), ), }, { Config: testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test" { name = "%s" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id vcpus = 4 }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "cluster_id", "netbox_cluster.test", "id"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "vcpus", "4"), ), }, { ResourceName: "netbox_virtual_machine.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckVirtualMachineDestroy(s *terraform.State) error { // retrieve the connection established in Provider configuration conn := testAccProvider.Meta().(*providerState) // loop through the resources in state, verifying each virtual machine // is destroyed for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_virtual_machine" { continue } // Retrieve our virtual machine by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) params := virtualization.NewVirtualizationVirtualMachinesReadParams().WithID(stateID) _, err := conn.Virtualization.VirtualizationVirtualMachinesRead(params, nil) if err == nil { return fmt.Errorf("virtual machine (%s) still exists", rs.Primary.ID) } if err != nil { if errresp, ok := err.(*virtualization.VirtualizationVirtualMachinesReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { return nil } } return err } } return nil } func TestAccNetboxVirtualMachine_tags(t *testing.T) { testSlug := "vm_tags" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test" { name = "%[1]s" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id tags = [netbox_tag.test_a.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.test", "name", testName), resource.TestCheckResourceAttrPair("netbox_virtual_machine.test", "cluster_id", "netbox_cluster.test", "id"), resource.TestCheckResourceAttr("netbox_virtual_machine.test", "tags.#", "1"), ), }, { Config: testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test" { name = "%s" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id tags = [netbox_tag.test_a.name, netbox_tag.test_b.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.test", "tags.#", "2"), ), }, { Config: testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test" { name = "%s" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.test", "tags.#", "0"), ), }, }, }) } func TestAccNetboxVirtualMachine_customFields(t *testing.T) { testSlug := "vm_cf" testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_custom_field" "test" { name = "custom_field" type = "text" content_types = ["virtualization.virtualmachine"] } resource "netbox_virtual_machine" "test" { name = "%[1]s" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id custom_fields = {"${netbox_custom_field.test.name}" = "76"} }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.test", "custom_fields.custom_field", "76"), ), }, }, }) } func TestAccNetboxVirtualMachine_localContext(t *testing.T) { testSlug := "vm_lc" testName := testAccGetTestName(testSlug) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test" { name = "%[1]s" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id local_context_data = jsonencode({"context_string"="context_value"}) }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.test", "local_context_data", "{\"context_string\":\"context_value\"}"), ), }, { Config: testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test" { name = "%[1]s_foo" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id local_context_data = jsonencode({"context_string"="context_value"}) }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.test", "local_context_data", "{\"context_string\":\"context_value\"}"), ), }, { Config: testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test" { name = "%[1]s" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id local_context_data = jsonencode({"context_string"="context_value"}) }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.test", "local_context_data", "{\"context_string\":\"context_value\"}"), ), }, { Config: testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test" { name = "%[1]s" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id local_context_data = jsonencode({"context_string"="context_value_2"}) }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.test", "local_context_data", "{\"context_string\":\"context_value_2\"}"), ), }, { Config: testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test" { name = "%[1]s" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id local_context_data = jsonencode({}) }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.test", "local_context_data", "{}"), ), }, { Config: testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test" { name = "%[1]s" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id local_context_data = jsonencode({"context_string"="context_value_2"}) }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.test", "local_context_data", "{\"context_string\":\"context_value_2\"}"), ), }, { Config: testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test" { name = "%[1]s" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id local_context_data = jsonencode({"context_string"="context_value_2","test_string"="broken"}) }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.test", "local_context_data", "{\"context_string\":\"context_value_2\",\"test_string\":\"broken\"}"), ), }, { Config: testAccNetboxVirtualMachineFullDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test" { name = "%[1]s" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id local_context_data = jsonencode({"context_string"="context_value_2"}) }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.test", "local_context_data", "{\"context_string\":\"context_value_2\"}"), ), }, }, }) } // TestAccNetboxVirtualMachine_diskSizeMb covers two scenarios: // // 1. No virtual disks: disk_size_mb is set explicitly in config, and the // provider sends that value to NetBox without interference from state. // // 2. Virtual disks attached: disk_size_mb is omitted from the VM config so // the provider never sends it in PUT requests. NetBox does not automatically // compute the aggregate when disks are attached to an existing VM; the field // remains 0. The field is still readable (Computed) from state after refresh. func TestAccNetboxVirtualMachine_diskSizeMb(t *testing.T) { testSlug := "vm_disk" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckVirtualMachineDestroy, Steps: []resource.TestStep{ // Step 1 – scenario 1: no virtual disks, disk_size_mb set explicitly. { Config: testAccNetboxVirtualMachineSiteClusterDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test_scenario1" { name = "%[1]s_scenario1" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id disk_size_mb = 256 } `, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.test_scenario1", "disk_size_mb", "256"), ), }, // Step 2 – scenario 1 update: change disk_size_mb without virtual disks. { Config: testAccNetboxVirtualMachineSiteClusterDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test_scenario1" { name = "%[1]s_scenario1" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id disk_size_mb = 512 } `, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_virtual_machine.test_scenario1", "disk_size_mb", "512"), ), }, // Step 3 – scenario 2: virtual disks attached, disk_size_mb omitted from VM // config so NetBox owns the aggregate; no 400 error from disk mismatch. { Config: testAccNetboxVirtualMachineSiteClusterDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test" { name = "%[1]s" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id # disk_size_mb intentionally omitted – managed by attached virtual disks } resource "netbox_virtual_disk" "a" { name = "%[1]s_a" size_mb = 100 virtual_machine_id = netbox_virtual_machine.test.id } resource "netbox_virtual_disk" "b" { name = "%[1]s_b" size_mb = 200 virtual_machine_id = netbox_virtual_machine.test.id } `, testName), Check: resource.ComposeTestCheckFunc( // Check that disk_size_mb is computed but not verified in state ), }, { ResourceName: "netbox_virtual_machine.test", ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{"disk_size_mb"}, }, // Step 4 – scenario 2 update: resize a disk; NetBox computes disk_size_mb // to the aggregate of the attached disks at the time of VM update (300). { Config: testAccNetboxVirtualMachineSiteClusterDependencies(testName) + fmt.Sprintf(` resource "netbox_virtual_machine" "test" { name = "%[1]s" cluster_id = netbox_cluster.test.id site_id = netbox_site.test.id } resource "netbox_virtual_disk" "a" { name = "%[1]s_a" size_mb = 150 virtual_machine_id = netbox_virtual_machine.test.id } resource "netbox_virtual_disk" "b" { name = "%[1]s_b" size_mb = 200 virtual_machine_id = netbox_virtual_machine.test.id } `, testName), Check: resource.ComposeTestCheckFunc( // Check that disk_size_mb is computed but not verified in state ), }, { ResourceName: "netbox_virtual_machine.test", ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{"disk_size_mb"}, }, }, }) } func init() { resource.AddTestSweepers("netbox_virtual_machine", &resource.Sweeper{ Name: "netbox_virtual_machine", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := virtualization.NewVirtualizationVirtualMachinesListParams() res, err := api.Virtualization.VirtualizationVirtualMachinesList(params, nil) if err != nil { return err } for _, virtualMachine := range res.GetPayload().Results { if strings.HasPrefix(*virtualMachine.Name, testPrefix) { deleteParams := virtualization.NewVirtualizationVirtualMachinesDeleteParams().WithID(virtualMachine.ID) _, err := api.Virtualization.VirtualizationVirtualMachinesDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a virtual machine") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_vlan.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var resourceNetboxVlanStatusOptions = []string{"active", "reserved", "deprecated"} func resourceNetboxVlan() *schema.Resource { return &schema.Resource{ Create: resourceNetboxVlanCreate, Read: resourceNetboxVlanRead, Update: resourceNetboxVlanUpdate, Delete: resourceNetboxVlanDelete, Description: `:meta:subcategory:IP Address Management (IPAM):From the [official documentation](https://docs.netbox.dev/en/stable/features/vlans/#vlans): > A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in IEEE 802.1Q. VLANs are arranged into VLAN groups to define scope and to enforce uniqueness.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "vid": { Type: schema.TypeInt, Required: true, }, "status": { Type: schema.TypeString, Optional: true, Default: "active", ValidateFunc: validation.StringInSlice(resourceNetboxVlanStatusOptions, false), Description: buildValidValueDescription(resourceNetboxVlanStatusOptions), }, "group_id": { Type: schema.TypeInt, Optional: true, }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, "role_id": { Type: schema.TypeInt, Optional: true, }, "site_id": { Type: schema.TypeInt, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, Default: "", }, tagsKey: tagsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxVlanCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableVLAN{} name := d.Get("name").(string) vid := int64(d.Get("vid").(int)) status := d.Get("status").(string) description := d.Get("description").(string) data.Name = &name data.Vid = &vid data.Status = status data.Description = description if groupID, ok := d.GetOk("group_id"); ok { data.Group = int64ToPtr(int64(groupID.(int))) } if siteID, ok := d.GetOk("site_id"); ok { data.Site = int64ToPtr(int64(siteID.(int))) } if tenantID, ok := d.GetOk("tenant_id"); ok { data.Tenant = int64ToPtr(int64(tenantID.(int))) } if roleID, ok := d.GetOk("role_id"); ok { data.Role = int64ToPtr(int64(roleID.(int))) } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } params := ipam.NewIpamVlansCreateParams().WithData(&data) res, err := api.Ipam.IpamVlansCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxVlanRead(d, m) } func resourceNetboxVlanRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamVlansReadParams().WithID(id) res, err := api.Ipam.IpamVlansRead(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamVlansReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } vlan := res.GetPayload() d.Set("name", vlan.Name) d.Set("vid", vlan.Vid) d.Set("description", vlan.Description) api.readTags(d, vlan.Tags) if vlan.Status != nil { d.Set("status", vlan.Status.Value) } if vlan.Group != nil { d.Set("group_id", vlan.Group.ID) } if vlan.Site != nil { d.Set("site_id", vlan.Site.ID) } if vlan.Tenant != nil { d.Set("tenant_id", vlan.Tenant.ID) } if vlan.Role != nil { d.Set("role_id", vlan.Role.ID) } return nil } func resourceNetboxVlanUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableVLAN{} name := d.Get("name").(string) vid := int64(d.Get("vid").(int)) status := d.Get("status").(string) description := d.Get("description").(string) data.Name = &name data.Vid = &vid data.Status = status data.Description = description if groupID, ok := d.GetOk("group_id"); ok { data.Group = int64ToPtr(int64(groupID.(int))) } if siteID, ok := d.GetOk("site_id"); ok { data.Site = int64ToPtr(int64(siteID.(int))) } if tenantID, ok := d.GetOk("tenant_id"); ok { data.Tenant = int64ToPtr(int64(tenantID.(int))) } if roleID, ok := d.GetOk("role_id"); ok { data.Role = int64ToPtr(int64(roleID.(int))) } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } params := ipam.NewIpamVlansUpdateParams().WithID(id).WithData(&data) _, err = api.Ipam.IpamVlansUpdate(params, nil) if err != nil { return err } return resourceNetboxVlanRead(d, m) } func resourceNetboxVlanDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamVlansDeleteParams().WithID(id) _, err := api.Ipam.IpamVlansDelete(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamVlansDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_vlan_group.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var resourceNetboxVlanGroupScopeTypeOptions = []string{"dcim.location", "dcim.site", "dcim.sitegroup", "dcim.region", "dcim.rack", "virtualization.cluster", "virtualization.clustergroup"} func resourceNetboxVlanGroup() *schema.Resource { return &schema.Resource{ Create: resourceNetboxVlanGroupCreate, Read: resourceNetboxVlanGroupRead, Update: resourceNetboxVlanGroupUpdate, Delete: resourceNetboxVlanGroupDelete, Description: `:meta:subcategory:IP Address Management (IPAM): > A VLAN Group represents a collection of VLANs. Generally, these are limited by one of a number of scopes such as "Site" or "Virtualization Cluster".`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "scope_type": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringInSlice(resourceNetboxVlanGroupScopeTypeOptions, false), Description: buildValidValueDescription(resourceNetboxVlanGroupScopeTypeOptions), }, "scope_id": { Type: schema.TypeInt, Optional: true, RequiredWith: []string{"scope_type"}, }, "description": { Type: schema.TypeString, Optional: true, Default: "", }, "vid_ranges": { Type: schema.TypeList, Elem: &schema.Schema{ Type: schema.TypeList, Elem: &schema.Schema{ Type: schema.TypeInt, }, }, Required: true, }, tagsKey: tagsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxVlanGroupCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.VLANGroup{} name := d.Get("name").(string) slug := d.Get("slug").(string) description := d.Get("description").(string) vidRanges := d.Get("vid_ranges").([]interface{}) var result = make([][]int64, 0) for _, v := range vidRanges { inner := v.([]interface{}) pair := make([]int64, 2) pair[0] = int64(inner[0].(int)) pair[1] = int64(inner[1].(int)) result = append(result, pair) } data.VidRanges = result data.Name = &name data.Slug = &slug data.Description = description if scopeType, ok := d.GetOk("scope_type"); ok { data.ScopeType = strToPtr(scopeType.(string)) } if scopeID, ok := d.GetOk("scope_id"); ok { data.ScopeID = int64ToPtr(int64(scopeID.(int))) } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } params := ipam.NewIpamVlanGroupsCreateParams().WithData(&data) res, err := api.Ipam.IpamVlanGroupsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxVlanGroupRead(d, m) } func resourceNetboxVlanGroupRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamVlanGroupsReadParams().WithID(id) res, err := api.Ipam.IpamVlanGroupsRead(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamVlanGroupsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } vlanGroup := res.GetPayload() d.Set("name", vlanGroup.Name) d.Set("slug", vlanGroup.Slug) d.Set("description", vlanGroup.Description) d.Set("vid_ranges", vlanGroup.VidRanges) api.readTags(d, vlanGroup.Tags) if vlanGroup.ScopeType != nil { d.Set("scope_type", vlanGroup.ScopeType) } if vlanGroup.ScopeID != nil { d.Set("scope_id", vlanGroup.ScopeID) } return nil } func resourceNetboxVlanGroupUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.VLANGroup{} name := d.Get("name").(string) slug := d.Get("slug").(string) description := d.Get("description").(string) vidRanges := d.Get("vid_ranges").([]interface{}) var result = make([][]int64, 0) for _, v := range vidRanges { inner := v.([]interface{}) pair := make([]int64, 2) pair[0] = int64(inner[0].(int)) pair[1] = int64(inner[1].(int)) result = append(result, pair) } data.VidRanges = result data.Name = &name data.Slug = &slug data.Description = description if scopeType, ok := d.GetOk("scope_type"); ok { data.ScopeType = strToPtr(scopeType.(string)) } if scopeID, ok := d.GetOk("scope_id"); ok { data.ScopeID = int64ToPtr(int64(scopeID.(int))) } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } params := ipam.NewIpamVlanGroupsUpdateParams().WithID(id).WithData(&data) _, err = api.Ipam.IpamVlanGroupsUpdate(params, nil) if err != nil { return err } return resourceNetboxVlanGroupRead(d, m) } func resourceNetboxVlanGroupDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamVlanGroupsDeleteParams().WithID(id) _, err := api.Ipam.IpamVlanGroupsDelete(params, nil) if err != nil { return err } return nil } ================================================ FILE: netbox/resource_netbox_vlan_group_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxVlanGroupFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } `, testName) } func TestAccNetboxVlanGroup_basic(t *testing.T) { testSlug := "vlan_group_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testAccNetboxVlanGroupFullDependencies(testName) + fmt.Sprintf(` resource "netbox_vlan_group" "test_basic" { name = "%s" slug = "%s" vid_ranges = [[1, 2], [3, 4]] tags = [] }`, testName, testSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_vlan_group.test_basic", "name", testName), resource.TestCheckResourceAttr("netbox_vlan_group.test_basic", "slug", testSlug), resource.TestCheckResourceAttr("netbox_vlan_group.test_basic", "description", ""), resource.TestCheckResourceAttr("netbox_vlan_group.test_basic", "tags.#", "0"), resource.TestCheckResourceAttr("netbox_vlan_group.test_basic", "vid_ranges.0.0", "1"), resource.TestCheckResourceAttr("netbox_vlan_group.test_basic", "vid_ranges.0.1", "2"), resource.TestCheckResourceAttr("netbox_vlan_group.test_basic", "vid_ranges.1.0", "3"), resource.TestCheckResourceAttr("netbox_vlan_group.test_basic", "vid_ranges.1.1", "4"), ), }, { ResourceName: "netbox_vlan_group.test_basic", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxVlanGroup_with_dependencies(t *testing.T) { testSlug := "vlan_group_with_dependencies" testName := testAccGetTestName(testSlug) testDescription := "Test Description" resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testAccNetboxVlanGroupFullDependencies(testName) + fmt.Sprintf(` resource "netbox_vlan_group" "test_with_dependencies" { name = "%s" slug = "%s" description = "%s" scope_type = "dcim.site" scope_id = netbox_site.test.id vid_ranges = [[1, 4094]] tags = [netbox_tag.test.name] }`, testName, testSlug, testDescription), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_vlan_group.test_with_dependencies", "name", testName), resource.TestCheckResourceAttr("netbox_vlan_group.test_with_dependencies", "slug", testSlug), resource.TestCheckResourceAttr("netbox_vlan_group.test_with_dependencies", "description", testDescription), resource.TestCheckResourceAttr("netbox_vlan_group.test_with_dependencies", "scope_type", "dcim.site"), resource.TestCheckResourceAttrPair("netbox_vlan_group.test_with_dependencies", "scope_id", "netbox_site.test", "id"), resource.TestCheckResourceAttr("netbox_vlan_group.test_with_dependencies", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_vlan_group.test_with_dependencies", "tags.0", testName), ), }, { ResourceName: "netbox_vlan_group.test_with_dependencies", ImportState: true, ImportStateVerify: true, }, }, }) } func init() { resource.AddTestSweepers("netbox_vlan_group", &resource.Sweeper{ Name: "netbox_vlan_group", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := ipam.NewIpamVlanGroupsListParams() res, err := api.Ipam.IpamVlanGroupsList(params, nil) if err != nil { return err } for _, vlan := range res.GetPayload().Results { if strings.HasPrefix(*vlan.Name, testPrefix) { deleteParams := ipam.NewIpamVlanGroupsDeleteParams().WithID(vlan.ID) _, err := api.Ipam.IpamVlanGroupsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a vlan group") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_vlan_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxVlanFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_vlan_group" "test_group" { name = "%[1]s" slug = "%[1]s" scope_type = "dcim.site" scope_id = netbox_site.test.id vid_ranges = [[1, 4094]] } `, testName) } func TestAccNetboxVlan_basic(t *testing.T) { testSlug := "vlan_basic" testName := testAccGetTestName(testSlug) testVid := "777" resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testAccNetboxVlanFullDependencies(testName) + fmt.Sprintf(` resource "netbox_vlan" "test_basic" { name = "%s" vid = "%s" tags = [] }`, testName, testVid), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_vlan.test_basic", "name", testName), resource.TestCheckResourceAttr("netbox_vlan.test_basic", "vid", testVid), resource.TestCheckResourceAttr("netbox_vlan.test_basic", "status", "active"), resource.TestCheckResourceAttr("netbox_vlan.test_basic", "description", ""), resource.TestCheckResourceAttr("netbox_vlan.test_basic", "tags.#", "0"), ), }, { ResourceName: "netbox_vlan.test_basic", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxVlan_with_dependencies(t *testing.T) { testSlug := "vlan_with_dependencies" testName := testAccGetTestName(testSlug) testVid := "666" testDescription := "Test Description" resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testAccNetboxVlanFullDependencies(testName) + fmt.Sprintf(` resource "netbox_vlan" "test_with_dependencies" { name = "%s" vid = "%s" description = "%s" status = "active" tenant_id = netbox_tenant.test.id site_id = netbox_site.test.id group_id = netbox_vlan_group.test_group.id tags = [netbox_tag.test.name] }`, testName, testVid, testDescription), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_vlan.test_with_dependencies", "name", testName), resource.TestCheckResourceAttr("netbox_vlan.test_with_dependencies", "vid", testVid), resource.TestCheckResourceAttr("netbox_vlan.test_with_dependencies", "description", testDescription), resource.TestCheckResourceAttr("netbox_vlan.test_with_dependencies", "status", "active"), resource.TestCheckResourceAttrPair("netbox_vlan.test_with_dependencies", "group_id", "netbox_vlan_group.test_group", "id"), resource.TestCheckResourceAttrPair("netbox_vlan.test_with_dependencies", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("netbox_vlan.test_with_dependencies", "site_id", "netbox_site.test", "id"), resource.TestCheckResourceAttr("netbox_vlan.test_with_dependencies", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_vlan.test_with_dependencies", "tags.0", testName), ), }, { ResourceName: "netbox_vlan.test_with_dependencies", ImportState: true, ImportStateVerify: true, }, }, }) } func init() { resource.AddTestSweepers("netbox_vlan", &resource.Sweeper{ Name: "netbox_vlan", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := ipam.NewIpamVlansListParams() res, err := api.Ipam.IpamVlansList(params, nil) if err != nil { return err } for _, vlan := range res.GetPayload().Results { if strings.HasPrefix(*vlan.Name, testPrefix) { deleteParams := ipam.NewIpamVlansDeleteParams().WithID(vlan.ID) _, err := api.Ipam.IpamVlansDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a vlan") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_vpn_tunnel.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/vpn" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) var resourceNetboxVpnTunnelEncapsulationOptions = []string{"ipsec-transport", "ipsec-tunnel", "ip-ip", "gre"} var resourceNetboxVpnTunnelStatusOptions = []string{"planned", "active", "disabled"} func resourceNetboxVpnTunnel() *schema.Resource { return &schema.Resource{ Create: resourceNetboxVpnTunnelCreate, Read: resourceNetboxVpnTunnelRead, Update: resourceNetboxVpnTunnelUpdate, Delete: resourceNetboxVpnTunnelDelete, Description: `:meta:subcategory:VPN Tunnels:From the [official documentation](https://docs.netbox.dev/en/stable/features/vpn-tunnels/): > NetBox can model private tunnels formed among virtual termination points across your network. Typical tunnel implementations include GRE, IP-in-IP, and IPSec. A tunnel may be terminated to two or more device or virtual machine interfaces. For convenient organization, tunnels may be assigned to user-defined groups.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "encapsulation": { Type: schema.TypeString, Required: true, Description: buildValidValueDescription(resourceNetboxVpnTunnelEncapsulationOptions), }, "status": { Type: schema.TypeString, Required: true, Description: buildValidValueDescription(resourceNetboxVpnTunnelStatusOptions), }, "tunnel_group_id": { Type: schema.TypeInt, Required: true, }, "description": { Type: schema.TypeString, Optional: true, }, "tunnel_id": { Type: schema.TypeInt, Optional: true, }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, tagsKey: tagsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxVpnTunnelCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableTunnel{} data.Name = strToPtr(d.Get("name").(string)) data.Encapsulation = strToPtr(d.Get("encapsulation").(string)) data.Status = strToPtr(d.Get("status").(string)) data.Group = int64ToPtr(int64(d.Get("tunnel_group_id").(int))) data.Description = getOptionalStr(d, "description", false) data.Tenant = getOptionalInt(d, "tenant_id") data.TunnelID = getOptionalInt(d, "tunnel_id") tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) data.Tags = tags params := vpn.NewVpnTunnelsCreateParams().WithData(&data) res, err := api.Vpn.VpnTunnelsCreate(params, nil) if err != nil { //return errors.New(getTextFromError(err)) return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxVpnTunnelRead(d, m) } func resourceNetboxVpnTunnelRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := vpn.NewVpnTunnelsReadParams().WithID(id) res, err := api.Vpn.VpnTunnelsRead(params, nil) if err != nil { if errresp, ok := err.(*vpn.VpnTunnelsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } tunnel := res.GetPayload() d.Set("name", tunnel.Name) d.Set("encapsulation", tunnel.Encapsulation.Value) d.Set("status", tunnel.Status.Value) if tunnel.Group != nil { d.Set("tunnel_group_id", tunnel.Group.ID) } else { d.Set("tunnel_group_id", nil) } if tunnel.Tenant != nil { d.Set("tenant_id", tunnel.Tenant.ID) } else { d.Set("tenant_id", nil) } d.Set("tunnel_id", tunnel.TunnelID) d.Set("description", tunnel.Description) api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxVpnTunnelUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableTunnel{} data.Name = strToPtr(d.Get("name").(string)) data.Encapsulation = strToPtr(d.Get("encapsulation").(string)) data.Status = strToPtr(d.Get("status").(string)) data.Group = int64ToPtr(int64(d.Get("tunnel_group_id").(int))) data.Description = getOptionalStr(d, "description", false) data.Tenant = getOptionalInt(d, "tenant_id") data.TunnelID = getOptionalInt(d, "tunnel_id") tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) data.Tags = tags params := vpn.NewVpnTunnelsUpdateParams().WithID(id).WithData(&data) _, err := api.Vpn.VpnTunnelsUpdate(params, nil) if err != nil { return err } return resourceNetboxVpnTunnelRead(d, m) } func resourceNetboxVpnTunnelDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := vpn.NewVpnTunnelsDeleteParams().WithID(id) _, err := api.Vpn.VpnTunnelsDelete(params, nil) if err != nil { if errresp, ok := err.(*vpn.VpnTunnelsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_vpn_tunnel_group.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/vpn" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxVpnTunnelGroup() *schema.Resource { return &schema.Resource{ Create: resourceNetboxVpnTunnelGroupCreate, Read: resourceNetboxVpnTunnelGroupRead, Update: resourceNetboxVpnTunnelGroupUpdate, Delete: resourceNetboxVpnTunnelGroupDelete, Description: `:meta:subcategory:VPN Tunnels:From the [official documentation](https://docs.netbox.dev/en/stable/features/vpn-tunnels/): > NetBox can model private tunnels formed among virtual termination points across your network. Typical tunnel implementations include GRE, IP-in-IP, and IPSec. A tunnel may be terminated to two or more device or virtual machine interfaces. For convenient organization, tunnels may be assigned to user-defined groups.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "description": { Type: schema.TypeString, Optional: true, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxVpnTunnelGroupCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.TunnelGroup{} name := d.Get("name").(string) data.Name = &name slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } data.Slug = &slug if description, ok := d.GetOk("description"); ok { data.Description = description.(string) } data.Tags = []*models.NestedTag{} params := vpn.NewVpnTunnelGroupsCreateParams().WithData(&data) res, err := api.Vpn.VpnTunnelGroupsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxVpnTunnelGroupRead(d, m) } func resourceNetboxVpnTunnelGroupRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := vpn.NewVpnTunnelGroupsReadParams().WithID(id) res, err := api.Vpn.VpnTunnelGroupsRead(params, nil) if err != nil { if errresp, ok := err.(*vpn.VpnTunnelGroupsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } d.Set("name", res.GetPayload().Name) d.Set("slug", res.GetPayload().Slug) d.Set("description", res.GetPayload().Description) return nil } func resourceNetboxVpnTunnelGroupUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.TunnelGroup{} name := d.Get("name").(string) data.Name = &name slugValue, slugOk := d.GetOk("slug") var slug string // Default slug to generated slug if not given if !slugOk { slug = getSlug(name) } else { slug = slugValue.(string) } data.Slug = &slug if d.HasChange("description") { // description omits empty values so set to ' ' if description := d.Get("description"); description.(string) == "" { data.Description = " " } else { data.Description = description.(string) } } data.Tags = []*models.NestedTag{} params := vpn.NewVpnTunnelGroupsUpdateParams().WithID(id).WithData(&data) _, err := api.Vpn.VpnTunnelGroupsUpdate(params, nil) if err != nil { return err } return resourceNetboxVpnTunnelGroupRead(d, m) } func resourceNetboxVpnTunnelGroupDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := vpn.NewVpnTunnelGroupsDeleteParams().WithID(id) _, err := api.Vpn.VpnTunnelGroupsDelete(params, nil) if err != nil { if errresp, ok := err.(*vpn.VpnTunnelGroupsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_vpn_tunnel_group_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/vpn" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxVpnTunnelGroup_basic(t *testing.T) { testSlug := "vpntnlgrp_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_vpn_tunnel_group" "test" { name = "%[1]s" slug = "%[1]s" description = "%[1]s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_vpn_tunnel_group.test", "name", testName), resource.TestCheckResourceAttr("netbox_vpn_tunnel_group.test", "slug", testName), resource.TestCheckResourceAttr("netbox_vpn_tunnel_group.test", "description", testName), ), }, { ResourceName: "netbox_vpn_tunnel_group.test", ImportState: true, ImportStateVerify: true, }, }, }) } func init() { resource.AddTestSweepers("netbox_vpn_tunnel_group", &resource.Sweeper{ Name: "netbox_vpn_tunnel_group", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := vpn.NewVpnTunnelGroupsListParams() res, err := api.Vpn.VpnTunnelGroupsList(params, nil) if err != nil { return err } for _, clusterGroup := range res.GetPayload().Results { if strings.HasPrefix(*clusterGroup.Name, testPrefix) { deleteParams := vpn.NewVpnTunnelGroupsDeleteParams().WithID(clusterGroup.ID) _, err := api.Vpn.VpnTunnelGroupsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a vpn_tunnel_group") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_vpn_tunnel_termination.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/vpn" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) var resourceNetboxVpnTunnelTerminationRoleOptions = []string{"peer", "hub", "spoke"} func resourceNetboxVpnTunnelTermination() *schema.Resource { return &schema.Resource{ Create: resourceNetboxVpnTunnelTerminationCreate, Read: resourceNetboxVpnTunnelTerminationRead, Update: resourceNetboxVpnTunnelTerminationUpdate, Delete: resourceNetboxVpnTunnelTerminationDelete, Description: `:meta:subcategory:VPN Tunnels:From the [official documentation](https://docs.netbox.dev/en/stable/features/vpn-tunnels/): > NetBox can model private tunnels formed among virtual termination points across your network. Typical tunnel implementations include GRE, IP-in-IP, and IPSec. A tunnel may be terminated to two or more device or virtual machine interfaces. For convenient organization, tunnels may be assigned to user-defined groups.`, Schema: map[string]*schema.Schema{ "tunnel_id": { Type: schema.TypeInt, Required: true, }, "role": { Type: schema.TypeString, Required: true, Description: buildValidValueDescription(resourceNetboxVpnTunnelTerminationRoleOptions), }, "virtual_machine_interface_id": { Type: schema.TypeInt, Optional: true, ExactlyOneOf: []string{"virtual_machine_interface_id", "device_interface_id"}, }, "device_interface_id": { Type: schema.TypeInt, Optional: true, ExactlyOneOf: []string{"virtual_machine_interface_id", "device_interface_id"}, }, "outside_ip_address_id": { Type: schema.TypeInt, Optional: true, }, tagsKey: tagsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxVpnTunnelTerminationCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableTunnelTermination{} tunnelID := int64(d.Get("tunnel_id").(int)) data.Tunnel = &tunnelID data.Role = d.Get("role").(string) vmInterfaceID := getOptionalInt(d, "virtual_machine_interface_id") deviceInterfaceID := getOptionalInt(d, "device_interface_id") switch { case vmInterfaceID != nil: data.TerminationType = strToPtr("virtualization.vminterface") data.TerminationID = *vmInterfaceID case deviceInterfaceID != nil: data.TerminationType = strToPtr("dcim.interface") data.TerminationID = *deviceInterfaceID } data.OutsideIP = getOptionalInt(d, "outside_ip_address_id") tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) data.Tags = tags params := vpn.NewVpnTunnelTerminationsCreateParams().WithData(&data) res, err := api.Vpn.VpnTunnelTerminationsCreate(params, nil) if err != nil { //return errors.New(getTextFromError(err)) return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxVpnTunnelTerminationRead(d, m) } func resourceNetboxVpnTunnelTerminationRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := vpn.NewVpnTunnelTerminationsReadParams().WithID(id) res, err := api.Vpn.VpnTunnelTerminationsRead(params, nil) if err != nil { if errresp, ok := err.(*vpn.VpnTunnelTerminationsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } tunnelTermination := res.GetPayload() d.Set("tunnel_id", tunnelTermination.Tunnel.ID) d.Set("role", tunnelTermination.Role.Value) vmInterfaceID := getOptionalInt(d, "virtual_machine_interface_id") deviceInterfaceID := getOptionalInt(d, "device_interface_id") switch { case vmInterfaceID != nil: d.Set("virtual_machine_interface_id", tunnelTermination.TerminationID) case deviceInterfaceID != nil: d.Set("device_interface_id", tunnelTermination.TerminationID) } if tunnelTermination.OutsideIP != nil { d.Set("outside_ip_address_id", tunnelTermination.OutsideIP.ID) } api.readTags(d, res.GetPayload().Tags) return nil } func resourceNetboxVpnTunnelTerminationUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableTunnelTermination{} tunnelID := int64(d.Get("tunnel_id").(int)) data.Tunnel = &tunnelID data.Role = d.Get("role").(string) vmInterfaceID := getOptionalInt(d, "virtual_machine_interface_id") deviceInterfaceID := getOptionalInt(d, "device_interface_id") switch { case vmInterfaceID != nil: data.TerminationType = strToPtr("virtualization.vminterface") data.TerminationID = *vmInterfaceID case deviceInterfaceID != nil: data.TerminationType = strToPtr("dcim.interface") data.TerminationID = *deviceInterfaceID } data.OutsideIP = getOptionalInt(d, "outside_ip_address_id") tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) data.Tags = tags params := vpn.NewVpnTunnelTerminationsUpdateParams().WithID(id).WithData(&data) _, err := api.Vpn.VpnTunnelTerminationsUpdate(params, nil) if err != nil { return err } return resourceNetboxVpnTunnelTerminationRead(d, m) } func resourceNetboxVpnTunnelTerminationDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := vpn.NewVpnTunnelTerminationsDeleteParams().WithID(id) _, err := api.Vpn.VpnTunnelTerminationsDelete(params, nil) if err != nil { if errresp, ok := err.(*vpn.VpnTunnelTerminationsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_vpn_tunnel_termination_test.go ================================================ package netbox import ( "fmt" "log" "testing" "github.com/fbreckle/go-netbox/netbox/client/vpn" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxVpnTunnelTerminationFullDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_site" "test" { name = "%[1]s" } resource "netbox_device_role" "test" { name = "%[1]s" color_hex = "ff00ff" } resource "netbox_manufacturer" "test" { name = "%[1]s" } resource "netbox_device_type" "test" { model = "%[1]s" manufacturer_id = netbox_manufacturer.test.id } resource "netbox_device" "test" { name = "%[1]s" device_type_id = netbox_device_type.test.id role_id = netbox_device_role.test.id site_id = netbox_site.test.id } resource "netbox_device_interface" "test" { name = "eth0" device_id = netbox_device.test.id type = "virtual" } resource "netbox_ip_address" "device_1" { ip_address = "2.2.2.0/32" status = "active" device_interface_id = netbox_device_interface.test.id } resource "netbox_ip_address" "device_2" { ip_address = "2.2.2.1/32" status = "active" device_interface_id = netbox_device_interface.test.id } resource "netbox_virtual_machine" "test" { name = "%[1]s" site_id = netbox_site.test.id } resource "netbox_interface" "test" { name = "eth0" virtual_machine_id = netbox_virtual_machine.test.id } resource "netbox_ip_address" "vm_1" { ip_address = "2.2.2.2/32" status = "active" virtual_machine_interface_id = netbox_interface.test.id } resource "netbox_ip_address" "vm_2" { ip_address = "2.2.2.3/32" status = "active" virtual_machine_interface_id = netbox_interface.test.id } resource "netbox_vpn_tunnel_group" "test" { name = "%[1]s" description = "%[1]s" } resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_vpn_tunnel" "test" { name = "%[1]s" encapsulation = "ipsec-transport" status = "active" tunnel_group_id = netbox_vpn_tunnel_group.test.id } `, testName) } func TestAccNetboxVpnTunnelTermination_basic(t *testing.T) { testSlug := "vpnterm_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testAccNetboxVpnTunnelTerminationFullDependencies(testName) + ` resource "netbox_vpn_tunnel_termination" "device" { role = "peer" tunnel_id = netbox_vpn_tunnel.test.id device_interface_id = netbox_device_interface.test.id tags = [netbox_tag.test.name] } resource "netbox_vpn_tunnel_termination" "vm" { role = "peer" tunnel_id = netbox_vpn_tunnel.test.id virtual_machine_interface_id = netbox_interface.test.id outside_ip_address_id = netbox_ip_address.vm_1.id }`, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_vpn_tunnel_termination.device", "tunnel_id", "netbox_vpn_tunnel.test", "id"), resource.TestCheckResourceAttrPair("netbox_vpn_tunnel_termination.vm", "tunnel_id", "netbox_vpn_tunnel.test", "id"), resource.TestCheckResourceAttrPair("netbox_vpn_tunnel_termination.vm", "outside_ip_address_id", "netbox_ip_address.vm_1", "id"), resource.TestCheckResourceAttr("netbox_vpn_tunnel_termination.device", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_vpn_tunnel_termination.device", "tags.0", testName), ), }, { Config: testAccNetboxVpnTunnelTerminationFullDependencies(testName) + ` resource "netbox_ip_address" "outside_ip" { ip_address = "203.0.113.2/24" status = "active" } resource "netbox_vpn_tunnel_termination" "device" { role = "peer" tunnel_id = netbox_vpn_tunnel.test.id device_interface_id = netbox_device_interface.test.id outside_ip_address_id = netbox_ip_address.outside_ip.id tags = [netbox_tag.test.name] } resource "netbox_vpn_tunnel_termination" "vm" { role = "peer" tunnel_id = netbox_vpn_tunnel.test.id virtual_machine_interface_id = netbox_interface.test.id outside_ip_address_id = netbox_ip_address.vm_1.id }`, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair("netbox_vpn_tunnel_termination.device", "tunnel_id", "netbox_vpn_tunnel.test", "id"), resource.TestCheckResourceAttrPair("netbox_vpn_tunnel_termination.device", "outside_ip_address_id", "netbox_ip_address.outside_ip", "id"), ), }, { ResourceName: "netbox_vpn_tunnel.test", ImportState: true, ImportStateVerify: true, }, }, }) } func init() { resource.AddTestSweepers("netbox_vpn_tunnel_termination", &resource.Sweeper{ Name: "netbox_vpn_tunnel_termination", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := vpn.NewVpnTunnelTerminationsListParams() res, err := api.Vpn.VpnTunnelTerminationsList(params, nil) if err != nil { return err } for _, vpnTunnelTermination := range res.GetPayload().Results { deleteParams := vpn.NewVpnTunnelTerminationsDeleteParams().WithID(vpnTunnelTermination.ID) _, err := api.Vpn.VpnTunnelTerminationsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a vpn tunnel termination") } return nil }, }) } ================================================ FILE: netbox/resource_netbox_vpn_tunnel_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/vpn" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxVpnTunnel_basic(t *testing.T) { testSlug := "vpntun_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_vpn_tunnel_group" "test" { name = "%[1]s" description = "%[1]s" } resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_vpn_tunnel" "test" { name = "%[1]s" encapsulation = "ipsec-transport" status = "active" tunnel_group_id = netbox_vpn_tunnel_group.test.id description = "%[1]s" tenant_id = netbox_tenant.test.id tunnel_id = 123 tags = [netbox_tag.test.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_vpn_tunnel.test", "name", testName), resource.TestCheckResourceAttr("netbox_vpn_tunnel.test", "encapsulation", "ipsec-transport"), resource.TestCheckResourceAttr("netbox_vpn_tunnel.test", "description", testName), resource.TestCheckResourceAttr("netbox_vpn_tunnel.test", "tunnel_id", "123"), resource.TestCheckResourceAttrPair("netbox_vpn_tunnel.test", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("netbox_vpn_tunnel.test", "tunnel_group_id", "netbox_vpn_tunnel_group.test", "id"), resource.TestCheckResourceAttr("netbox_vpn_tunnel.test", "tags.0", testName), ), }, { ResourceName: "netbox_vpn_tunnel.test", ImportState: true, ImportStateVerify: true, }, }, }) } func init() { resource.AddTestSweepers("netbox_vpn_tunnel", &resource.Sweeper{ Name: "netbox_vpn_tunnel", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := vpn.NewVpnTunnelsListParams() res, err := api.Vpn.VpnTunnelsList(params, nil) if err != nil { return err } for _, vpnTunnel := range res.GetPayload().Results { if strings.HasPrefix(*vpnTunnel.Name, testPrefix) { deleteParams := vpn.NewVpnTunnelsDeleteParams().WithID(vpnTunnel.ID) _, err := api.Vpn.VpnTunnelsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a vpnTunnel") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_vrf.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxVrf() *schema.Resource { return &schema.Resource{ Create: resourceNetboxVrfCreate, Read: resourceNetboxVrfRead, Update: resourceNetboxVrfUpdate, Delete: resourceNetboxVrfDelete, Description: `:meta:subcategory:IP Address Management (IPAM):From the [official documentation](https://docs.netbox.dev/en/stable/features/ipam/#virtual-routing-and-forwarding-vrf): > A VRF object in NetBox represents a virtual routing and forwarding (VRF) domain. Each VRF is essentially a separate routing table. VRFs are commonly used to isolate customers or organizations from one another within a network, or to route overlapping address space (e.g. multiple instances of the 10.0.0.0/8 space). Each VRF may be assigned to a specific tenant to aid in organizing the available IP space by customer or internal user.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "description": { Type: schema.TypeString, Optional: true, }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, "enforce_unique": { Type: schema.TypeBool, Optional: true, Default: true, }, "rd": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringLenBetween(1, 21), }, tagsKey: tagsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxVrfCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := models.WritableVRF{} name := d.Get("name").(string) tenantID := int64(d.Get("tenant_id").(int)) enforceUnique := d.Get("enforce_unique").(bool) rd := d.Get("rd").(string) data.Name = &name if tenantID != 0 { data.Tenant = &tenantID } data.Description = getOptionalStr(d, "description", true) data.EnforceUnique = enforceUnique if rd != "" { data.Rd = &rd } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } data.ExportTargets = []int64{} data.ImportTargets = []int64{} params := ipam.NewIpamVrfsCreateParams().WithData(&data) res, err := api.Ipam.IpamVrfsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxVrfRead(d, m) } func resourceNetboxVrfRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamVrfsReadParams().WithID(id) res, err := api.Ipam.IpamVrfsRead(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamVrfsReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html d.SetId("") return nil } } return err } vrf := res.GetPayload() d.Set("name", vrf.Name) d.Set("description", vrf.Description) d.Set("enforce_unique", vrf.EnforceUnique) if vrf.Rd != nil { d.Set("rd", *vrf.Rd) } else { d.Set("rd", nil) } if vrf.Tenant != nil { d.Set("tenant_id", vrf.Tenant.ID) } else { d.Set("tenant_id", nil) } return nil } func resourceNetboxVrfUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.WritableVRF{} name := d.Get("name").(string) enforceUnique := d.Get("enforce_unique").(bool) tags, _ := getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) data.Name = &name data.Tags = tags data.ExportTargets = []int64{} data.ImportTargets = []int64{} data.Description = getOptionalStr(d, "description", true) data.EnforceUnique = enforceUnique if rd, ok := d.GetOk("rd"); ok { data.Rd = strToPtr(rd.(string)) } if tenantID, ok := d.GetOk("tenant_id"); ok { data.Tenant = int64ToPtr(int64(tenantID.(int))) } params := ipam.NewIpamVrfsPartialUpdateParams().WithID(id).WithData(&data) _, err := api.Ipam.IpamVrfsPartialUpdate(params, nil) if err != nil { return err } return resourceNetboxVrfRead(d, m) } func resourceNetboxVrfDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := ipam.NewIpamVrfsDeleteParams().WithID(id) _, err := api.Ipam.IpamVrfsDelete(params, nil) if err != nil { if errresp, ok := err.(*ipam.IpamVrfsDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_vrf_test.go ================================================ package netbox import ( "fmt" "log" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/ipam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxVrfTagDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tag" "test_a" { name = "%[1]sa" } resource "netbox_tag" "test_b" { name = "%[1]sb" } `, testName) } func testAccNetboxVrfTenantDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tenant" "test_tenant_a" { name = "%[1]sa" } resource "netbox_tenant" "test_tenant_b" { name = "%[1]sb" } `, testName) } func TestAccNetboxVrf_basic(t *testing.T) { testSlug := "vrf_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_vrf" "test" { name = "%s" description = "my-description" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_vrf.test", "name", testName), resource.TestCheckResourceAttr("netbox_vrf.test", "description", "my-description"), ), }, { ResourceName: "netbox_vrf.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxVrf_tags(t *testing.T) { testSlug := "vrf_tag" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testAccNetboxVrfTagDependencies(testName) + fmt.Sprintf(` resource "netbox_vrf" "test_tags" { name = "%[1]s" tags = [netbox_tag.test_a.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_vrf.test_tags", "name", testName), resource.TestCheckResourceAttr("netbox_vrf.test_tags", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_vrf.test_tags", "tags.0", testName+"a"), ), }, { Config: testAccNetboxVrfTagDependencies(testName) + fmt.Sprintf(` resource "netbox_vrf" "test_tags" { name = "%[1]s" tags = [netbox_tag.test_a.name, netbox_tag.test_b.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_vrf.test_tags", "tags.#", "2"), resource.TestCheckResourceAttr("netbox_vrf.test_tags", "tags.0", testName+"a"), resource.TestCheckResourceAttr("netbox_vrf.test_tags", "tags.1", testName+"b"), ), }, { Config: testAccNetboxVrfTagDependencies(testName) + fmt.Sprintf(` resource "netbox_vrf" "test_tags" { name = "%s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_vrf.test_tags", "tags.#", "0"), ), }, }, }) } func TestAccNetboxVrf_tenant(t *testing.T) { testSlug := "vrf_tenant" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testAccNetboxVrfTenantDependencies(testName) + fmt.Sprintf(` resource "netbox_vrf" "test_tenant" { name = "%[1]s" tenant_id = netbox_tenant.test_tenant_a.id }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_vrf.test_tenant", "name", testName), resource.TestCheckResourceAttrPair("netbox_vrf.test_tenant", "tenant_id", "netbox_tenant.test_tenant_a", "id"), ), }, { Config: testAccNetboxVrfTenantDependencies(testName) + fmt.Sprintf(` resource "netbox_vrf" "test_tenant" { name = "%[1]s" tenant_id = netbox_tenant.test_tenant_b.id }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_vrf.test_tenant", "name", testName), resource.TestCheckResourceAttrPair("netbox_vrf.test_tenant", "tenant_id", "netbox_tenant.test_tenant_b", "id"), ), }, }, }) } func TestAccNetboxVrf_rd(t *testing.T) { testSlug := "vrf_rd" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_vrf" "test_rd" { name = "%s" rd = "123:456" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_vrf.test_rd", "name", testName), resource.TestCheckResourceAttr("netbox_vrf.test_rd", "rd", "123:456"), ), }, { ResourceName: "netbox_vrf.test_rd", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxVrf_enforceUnique(t *testing.T) { testSlug := "vrf_enforce_unique" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_vrf" "test_enforce_unique" { name = "%s-true" enforce_unique = true }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_vrf.test_enforce_unique", "name", testName+"-true"), resource.TestCheckResourceAttr("netbox_vrf.test_enforce_unique", "enforce_unique", "true"), ), }, { Config: fmt.Sprintf(` resource "netbox_vrf" "test_enforce_unique_false" { name = "%s-false" enforce_unique = false }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_vrf.test_enforce_unique_false", "name", testName+"-false"), resource.TestCheckResourceAttr("netbox_vrf.test_enforce_unique_false", "enforce_unique", "false"), ), }, }, }) } func init() { resource.AddTestSweepers("netbox_vrf", &resource.Sweeper{ Name: "netbox_vrf", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := ipam.NewIpamVrfsListParams() res, err := api.Ipam.IpamVrfsList(params, nil) if err != nil { return err } for _, vrf := range res.GetPayload().Results { if strings.HasPrefix(*vrf.Name, testPrefix) { deleteParams := ipam.NewIpamVrfsDeleteParams().WithID(vrf.ID) _, err := api.Ipam.IpamVrfsDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a vrf") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_webhook.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/extras" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var resourceNetboxWebhookHTTPMethodOptions = []string{"GET", "POST", "PUT", "PATCH", "DELETE"} func resourceNetboxWebhook() *schema.Resource { return &schema.Resource{ Create: resourceNetboxWebhookCreate, Read: resourceNetboxWebhookRead, Update: resourceNetboxWebhookUpdate, Delete: resourceNetboxWebhookDelete, Description: `:meta:subcategory:Extras:From the [official documentation](https://docs.netbox.dev/en/stable/integrations/webhooks/): > A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "payload_url": { Type: schema.TypeString, Required: true, }, "body_template": { Type: schema.TypeString, Optional: true, DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool { equal, _ := jsonSemanticCompare(oldValue, newValue) return equal }, DiffSuppressOnRefresh: true, }, "http_method": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringInSlice(resourceNetboxWebhookHTTPMethodOptions, false), Description: buildValidValueDescription(resourceNetboxWebhookHTTPMethodOptions), Default: "POST", }, "http_content_type": { Type: schema.TypeString, Optional: true, Description: "The complete list of official content types is available [here](https://www.iana.org/assignments/media-types/media-types.xhtml).", Default: "application/json", }, "additional_headers": { Type: schema.TypeString, Optional: true, }, "ca_file_path": { Type: schema.TypeString, Optional: true, }, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxWebhookCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) data := &models.Webhook{} name := d.Get("name").(string) data.Name = &name payloadURL := d.Get("payload_url").(string) data.PayloadURL = &payloadURL bodyTemplate := d.Get("body_template").(string) data.BodyTemplate = bodyTemplate data.HTTPMethod = getOptionalStr(d, "http_method", false) data.HTTPContentType = getOptionalStr(d, "http_content_type", false) data.AdditionalHeaders = getOptionalStr(d, "additional_headers", false) data.CaFilePath = strToPtr(getOptionalStr(d, "ca_file_path", false)) params := extras.NewExtrasWebhooksCreateParams().WithData(data) res, err := api.Extras.ExtrasWebhooksCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxWebhookRead(d, m) } func resourceNetboxWebhookRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := extras.NewExtrasWebhooksReadParams().WithID(id) res, err := api.Extras.ExtrasWebhooksRead(params, nil) if err != nil { if errresp, ok := err.(*extras.ExtrasWebhooksReadDefault); ok { errorcode := errresp.Code() if errorcode == 404 { d.SetId("") return nil } } return err } webhook := res.GetPayload() d.Set("name", webhook.Name) d.Set("payload_url", webhook.PayloadURL) d.Set("body_template", webhook.BodyTemplate) d.Set("http_method", webhook.HTTPMethod) d.Set("http_content_type", webhook.HTTPContentType) d.Set("additional_headers", webhook.AdditionalHeaders) d.Set("ca_file_path", webhook.CaFilePath) return nil } func resourceNetboxWebhookUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) data := models.Webhook{} name := d.Get("name").(string) payloadURL := d.Get("payload_url").(string) bodyTemplate := d.Get("body_template").(string) data.Name = &name data.PayloadURL = &payloadURL data.BodyTemplate = bodyTemplate data.HTTPMethod = getOptionalStr(d, "http_method", false) data.HTTPContentType = getOptionalStr(d, "http_content_type", false) data.AdditionalHeaders = getOptionalStr(d, "additional_headers", false) data.CaFilePath = strToPtr(getOptionalStr(d, "ca_file_path", false)) params := extras.NewExtrasWebhooksUpdateParams().WithID(id).WithData(&data) _, err := api.Extras.ExtrasWebhooksUpdate(params, nil) if err != nil { return err } return resourceNetboxWebhookRead(d, m) } func resourceNetboxWebhookDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := extras.NewExtrasWebhooksDeleteParams().WithID(id) _, err := api.Extras.ExtrasWebhooksDelete(params, nil) if err != nil { if errresp, ok := err.(*extras.ExtrasWebhooksDeleteDefault); ok { if errresp.Code() == 404 { d.SetId("") return nil } } return err } return nil } ================================================ FILE: netbox/resource_netbox_webhook_test.go ================================================ package netbox import ( "fmt" "log" "strconv" "strings" "testing" "github.com/fbreckle/go-netbox/netbox/client/extras" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccNetboxWebhook_basic(t *testing.T) { testName := testAccGetTestName("webhook_basic") testPayloadURL := "https://example.com/webhook" testBodyTemplate := "Sample Body" testAdditionalHeaders := "Authentication: Bearer abcdef123456" testCaFilePath := "/etc/ssl/certs" resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, CheckDestroy: testAccCheckNetBoxWebhookDestroy, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_webhook" "test" { name = "%s" payload_url = "%s" body_template = "%s" additional_headers = "%s" ca_file_path = "%s" }`, testName, testPayloadURL, testBodyTemplate, testAdditionalHeaders, testCaFilePath), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_webhook.test", "name", testName), resource.TestCheckResourceAttr("netbox_webhook.test", "payload_url", testPayloadURL), resource.TestCheckResourceAttr("netbox_webhook.test", "body_template", testBodyTemplate), resource.TestCheckResourceAttr("netbox_webhook.test", "additional_headers", testAdditionalHeaders), resource.TestCheckResourceAttr("netbox_webhook.test", "ca_file_path", testCaFilePath), ), }, { ResourceName: "netbox_webhook.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxWebhook_update(t *testing.T) { testName := testAccGetTestName("webhook_update") testPayloadURL := "https://example.com/webhookupdate" testBodyTemplate := `{"text": "This is a sample json"}` testHTTPMethod := "PUT" testHTTPContentType := "application/xml" resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_webhook" "test" { name = "%s" payload_url = "%s" body_template = <<-EOT {"text": "This is a sample json"} EOT http_method = "%s" http_content_type = "%s" }`, testName, testPayloadURL, testHTTPMethod, testHTTPContentType), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_webhook.test", "name", testName), resource.TestCheckResourceAttr("netbox_webhook.test", "payload_url", testPayloadURL), resource.TestCheckResourceAttr("netbox_webhook.test", "body_template", testBodyTemplate), resource.TestCheckResourceAttr("netbox_webhook.test", "http_method", testHTTPMethod), resource.TestCheckResourceAttr("netbox_webhook.test", "http_content_type", testHTTPContentType), ), }, { Config: fmt.Sprintf(` resource "netbox_webhook" "test" { name = "%s_updated" payload_url = "%s" body_template = <<-EOT {"text": "This is a sample json"} EOT }`, testName, testPayloadURL), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_webhook.test", "name", testName+"_updated"), resource.TestCheckResourceAttr("netbox_webhook.test", "payload_url", testPayloadURL), resource.TestCheckResourceAttr("netbox_webhook.test", "body_template", testBodyTemplate), ), }, }, }) } func TestAccNetboxWebhook_import(t *testing.T) { testName := testAccGetTestName("webhook_import") testPayloadURL := "https://test2.com/webhook" resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_webhook" "test" { name = "%s" payload_url = "%s" }`, testName, testPayloadURL), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_webhook.test", "name", testName), resource.TestCheckResourceAttr("netbox_webhook.test", "payload_url", testPayloadURL), ), }, { ResourceName: "netbox_webhook.test", ImportState: true, ImportStateVerify: true, }, }, }) } func testAccCheckNetBoxWebhookDestroy(s *terraform.State) error { client := testAccProvider.Meta().(*providerState) for _, rs := range s.RootModule().Resources { if rs.Type != "netbox_webhook" { continue } // Fetch the webhook by ID // Retrieve our interface by referencing it's state ID for API lookup stateID, _ := strconv.ParseInt(rs.Primary.ID, 10, 64) webhook, err := client.Extras.ExtrasWebhooksRead(extras.NewExtrasWebhooksReadParams().WithID(stateID), nil) if err == nil && webhook != nil { return fmt.Errorf("Webhook %s still exists", rs.Primary.ID) } } return nil } func init() { resource.AddTestSweepers("netbox_webhook", &resource.Sweeper{ Name: "netbox_webhook", Dependencies: []string{}, F: func(region string) error { m, err := sharedClientForRegion(region) if err != nil { return fmt.Errorf("Error getting client: %s", err) } api := m.(*providerState) params := extras.NewExtrasWebhooksListParams() res, err := api.Extras.ExtrasWebhooksList(params, nil) if err != nil { return err } for _, webhook := range res.GetPayload().Results { if strings.HasPrefix(*webhook.Name, testPrefix) { deleteParams := extras.NewExtrasWebhooksDeleteParams().WithID(webhook.ID) _, err := api.Extras.ExtrasWebhooksDelete(deleteParams, nil) if err != nil { return err } log.Print("[DEBUG] Deleted a webhook") } } return nil }, }) } ================================================ FILE: netbox/resource_netbox_wireless_helpers.go ================================================ package netbox import ( "github.com/fbreckle/go-netbox/netbox/client/wireless" "github.com/go-openapi/runtime" "github.com/go-openapi/strfmt" "github.com/go-viper/mapstructure/v2" ) type wirelessInterceptWriter struct { runtime.ClientRequest fields map[string]any } func (iw wirelessInterceptWriter) SetBodyParam(p any) error { out := make(map[string]any) dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ TagName: "json", Result: &out, }) if err != nil { return err } if err := dec.Decode(p); err != nil { return err } for fieldName, value := range iw.fields { out[fieldName] = value } return iw.ClientRequest.SetBodyParam(out) } type wirelessInterceptParams struct { inner runtime.ClientRequestWriter fields map[string]any } func (ip wirelessInterceptParams) WriteToRequest(req runtime.ClientRequest, reg strfmt.Registry) error { writer := wirelessInterceptWriter{ClientRequest: req, fields: ip.fields} return ip.inner.WriteToRequest(writer, reg) } func hackSerializeWirelessWithValues(fields map[string]any) wireless.ClientOption { overrideFields := make(map[string]any, len(fields)) for fieldName, value := range fields { overrideFields[fieldName] = value } return func(co *runtime.ClientOperation) { originalParams := co.Params co.Params = wirelessInterceptParams{inner: originalParams, fields: overrideFields} } } func hackSerializeWirelessAsNull(fields ...string) wireless.ClientOption { overrideFields := make(map[string]any, len(fields)) for _, field := range fields { overrideFields[field] = nil } return hackSerializeWirelessWithValues(overrideFields) } ================================================ FILE: netbox/resource_netbox_wireless_lan.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/wireless" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) var resourceNetboxWirelessLANStatusOptions = []string{"active", "reserved", "disabled", "deprecated"} var resourceNetboxWirelessLANAuthTypeOptions = []string{"open", "wep", "wpa-personal", "wpa-enterprise"} var resourceNetboxWirelessLANAuthCipherOptions = []string{"auto", "tkip", "aes"} func resourceNetboxWirelessLAN() *schema.Resource { return &schema.Resource{ Create: resourceNetboxWirelessLANCreate, Read: resourceNetboxWirelessLANRead, Update: resourceNetboxWirelessLANUpdate, Delete: resourceNetboxWirelessLANDelete, Description: `:meta:subcategory:Wireless: > A Wireless LAN represents a broadcast wireless network, identified by its SSID and optional authentication settings.`, Schema: map[string]*schema.Schema{ "ssid": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringLenBetween(1, 32), }, "status": { Type: schema.TypeString, Optional: true, Default: "active", ValidateFunc: validation.StringInSlice(resourceNetboxWirelessLANStatusOptions, false), Description: buildValidValueDescription(resourceNetboxWirelessLANStatusOptions), }, "group_id": { Type: schema.TypeInt, Optional: true, }, "tenant_id": { Type: schema.TypeInt, Optional: true, }, "vlan_id": { Type: schema.TypeInt, Optional: true, }, "auth_type": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringInSlice(resourceNetboxWirelessLANAuthTypeOptions, false), Description: buildValidValueDescription(resourceNetboxWirelessLANAuthTypeOptions), }, "auth_cipher": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringInSlice(resourceNetboxWirelessLANAuthCipherOptions, false), Description: buildValidValueDescription(resourceNetboxWirelessLANAuthCipherOptions), }, "auth_psk": { Type: schema.TypeString, Optional: true, Sensitive: true, ValidateFunc: validation.StringLenBetween(0, 64), }, "description": { Type: schema.TypeString, Optional: true, }, "comments": { Type: schema.TypeString, Optional: true, }, customFieldsKey: customFieldsSchema, tagsKey: tagsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxWirelessLANCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) ssid := d.Get("ssid").(string) status := d.Get("status").(string) data := &models.WritableWirelessLAN{ Ssid: &ssid, Status: status, AuthType: d.Get("auth_type").(string), AuthCipher: d.Get("auth_cipher").(string), AuthPsk: d.Get("auth_psk").(string), Description: d.Get("description").(string), Comments: d.Get("comments").(string), } if groupID, ok := d.GetOk("group_id"); ok { data.Group = int64ToPtr(int64(groupID.(int))) } if tenantID, ok := d.GetOk("tenant_id"); ok { data.Tenant = int64ToPtr(int64(tenantID.(int))) } if vlanID, ok := d.GetOk("vlan_id"); ok { data.Vlan = int64ToPtr(int64(vlanID.(int))) } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } if cf, ok := d.GetOk(customFieldsKey); ok { data.CustomFields = cf } params := wireless.NewWirelessWirelessLansCreateParams().WithData(data) res, err := api.Wireless.WirelessWirelessLansCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxWirelessLANRead(d, m) } func resourceNetboxWirelessLANRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := wireless.NewWirelessWirelessLansReadParams().WithID(id) res, err := api.Wireless.WirelessWirelessLansRead(params, nil) if err != nil { if errresp, ok := err.(*wireless.WirelessWirelessLansReadDefault); ok && errresp.Code() == 404 { d.SetId("") return nil } return err } wlan := res.GetPayload() d.Set("ssid", wlan.Ssid) d.Set("description", wlan.Description) d.Set("comments", wlan.Comments) d.Set("auth_psk", wlan.AuthPsk) if wlan.Status != nil { d.Set("status", wlan.Status.Value) } else { d.Set("status", nil) } if wlan.AuthType != nil { d.Set("auth_type", wlan.AuthType.Value) } else { d.Set("auth_type", nil) } if wlan.AuthCipher != nil { d.Set("auth_cipher", wlan.AuthCipher.Value) } else { d.Set("auth_cipher", nil) } if wlan.Group != nil { d.Set("group_id", wlan.Group.ID) } else { d.Set("group_id", nil) } if wlan.Tenant != nil { d.Set("tenant_id", wlan.Tenant.ID) } else { d.Set("tenant_id", nil) } if wlan.Vlan != nil { d.Set("vlan_id", wlan.Vlan.ID) } else { d.Set("vlan_id", nil) } cf := getCustomFields(wlan.CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, wlan.Tags) return nil } func resourceNetboxWirelessLANUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) ssid := d.Get("ssid").(string) data := models.WritableWirelessLAN{ Ssid: &ssid, Status: d.Get("status").(string), Description: getOptionalStr(d, "description", true), Comments: getOptionalStr(d, "comments", true), } overrideFields := make(map[string]any) groupID := d.Get("group_id").(int) if groupID != 0 { data.Group = int64ToPtr(int64(groupID)) } else { overrideFields["group"] = nil } tenantID := d.Get("tenant_id").(int) if tenantID != 0 { data.Tenant = int64ToPtr(int64(tenantID)) } else { overrideFields["tenant"] = nil } vlanID := d.Get("vlan_id").(int) if vlanID != 0 { data.Vlan = int64ToPtr(int64(vlanID)) } else { overrideFields["vlan"] = nil } if authType, ok := d.GetOk("auth_type"); ok { data.AuthType = authType.(string) } else { overrideFields["auth_type"] = nil } if authCipher, ok := d.GetOk("auth_cipher"); ok { data.AuthCipher = authCipher.(string) } else { overrideFields["auth_cipher"] = nil } if authPSK, ok := d.GetOk("auth_psk"); ok { data.AuthPsk = authPSK.(string) } else { overrideFields["auth_psk"] = "" } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } if cf, ok := d.GetOk(customFieldsKey); ok { data.CustomFields = cf } params := wireless.NewWirelessWirelessLansPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Wireless.WirelessWirelessLansPartialUpdate(params, nil, hackSerializeWirelessWithValues(overrideFields)) if err != nil { return err } return resourceNetboxWirelessLANRead(d, m) } func resourceNetboxWirelessLANDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := wireless.NewWirelessWirelessLansDeleteParams().WithID(id) _, err := api.Wireless.WirelessWirelessLansDelete(params, nil) if err != nil { if errresp, ok := err.(*wireless.WirelessWirelessLansDeleteDefault); ok && errresp.Code() == 404 { d.SetId("") return nil } return err } return nil } ================================================ FILE: netbox/resource_netbox_wireless_lan_group.go ================================================ package netbox import ( "strconv" "github.com/fbreckle/go-netbox/netbox/client/wireless" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceNetboxWirelessLANGroup() *schema.Resource { return &schema.Resource{ Create: resourceNetboxWirelessLANGroupCreate, Read: resourceNetboxWirelessLANGroupRead, Update: resourceNetboxWirelessLANGroupUpdate, Delete: resourceNetboxWirelessLANGroupDelete, Description: `:meta:subcategory:Wireless: > A Wireless LAN Group is used to organize wireless LANs into a recursive hierarchy.`, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "slug": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringLenBetween(1, 100), }, "parent_id": { Type: schema.TypeInt, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, customFieldsKey: customFieldsSchema, tagsKey: tagsSchema, }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } func resourceNetboxWirelessLANGroupCreate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) name := d.Get("name").(string) description := d.Get("description").(string) slugValue, slugOk := d.GetOk("slug") slug := getSlug(name) if slugOk { slug = slugValue.(string) } data := &models.WritableWirelessLANGroup{ Name: &name, Slug: &slug, Description: description, } if parentID, ok := d.GetOk("parent_id"); ok { data.Parent = int64ToPtr(int64(parentID.(int))) } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } if cf, ok := d.GetOk(customFieldsKey); ok { data.CustomFields = cf } params := wireless.NewWirelessWirelessLanGroupsCreateParams().WithData(data) res, err := api.Wireless.WirelessWirelessLanGroupsCreate(params, nil) if err != nil { return err } d.SetId(strconv.FormatInt(res.GetPayload().ID, 10)) return resourceNetboxWirelessLANGroupRead(d, m) } func resourceNetboxWirelessLANGroupRead(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := wireless.NewWirelessWirelessLanGroupsReadParams().WithID(id) res, err := api.Wireless.WirelessWirelessLanGroupsRead(params, nil) if err != nil { if errresp, ok := err.(*wireless.WirelessWirelessLanGroupsReadDefault); ok && errresp.Code() == 404 { d.SetId("") return nil } return err } group := res.GetPayload() d.Set("name", group.Name) d.Set("slug", group.Slug) d.Set("description", group.Description) if group.Parent != nil { d.Set("parent_id", group.Parent.ID) } else { d.Set("parent_id", nil) } cf := getCustomFields(group.CustomFields) if cf != nil { d.Set(customFieldsKey, cf) } api.readTags(d, group.Tags) return nil } func resourceNetboxWirelessLANGroupUpdate(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) name := d.Get("name").(string) slugValue, slugOk := d.GetOk("slug") slug := getSlug(name) if slugOk { slug = slugValue.(string) } data := models.WritableWirelessLANGroup{ Name: &name, Slug: &slug, Description: getOptionalStr(d, "description", true), } var nullFields []string parentID := d.Get("parent_id").(int) if parentID != 0 { data.Parent = int64ToPtr(int64(parentID)) } else { nullFields = append(nullFields, "parent") } var err error data.Tags, err = getNestedTagListFromResourceDataSet(api, d.Get(tagsAllKey)) if err != nil { return err } if cf, ok := d.GetOk(customFieldsKey); ok { data.CustomFields = cf } params := wireless.NewWirelessWirelessLanGroupsPartialUpdateParams().WithID(id).WithData(&data) _, err = api.Wireless.WirelessWirelessLanGroupsPartialUpdate(params, nil, hackSerializeWirelessAsNull(nullFields...)) if err != nil { return err } return resourceNetboxWirelessLANGroupRead(d, m) } func resourceNetboxWirelessLANGroupDelete(d *schema.ResourceData, m interface{}) error { api := m.(*providerState) id, _ := strconv.ParseInt(d.Id(), 10, 64) params := wireless.NewWirelessWirelessLanGroupsDeleteParams().WithID(id) _, err := api.Wireless.WirelessWirelessLanGroupsDelete(params, nil) if err != nil { if errresp, ok := err.(*wireless.WirelessWirelessLanGroupsDeleteDefault); ok && errresp.Code() == 404 { d.SetId("") return nil } return err } return nil } ================================================ FILE: netbox/resource_netbox_wireless_lan_group_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccNetboxWirelessLANGroup_basic(t *testing.T) { testSlug := "wlangrp_basic" testName := testAccGetTestName(testSlug) randomSlug := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_wireless_lan_group" "parent" { name = "%[1]s" slug = "%[2]s" description = "foo bar" tags = [netbox_tag.test.name] } resource "netbox_wireless_lan_group" "child" { name = "%[1]s-child" slug = "%[2]s-c" parent_id = netbox_wireless_lan_group.parent.id }`, testName, randomSlug), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_wireless_lan_group.parent", "name", testName), resource.TestCheckResourceAttr("netbox_wireless_lan_group.parent", "slug", randomSlug), resource.TestCheckResourceAttr("netbox_wireless_lan_group.parent", "description", "foo bar"), resource.TestCheckResourceAttr("netbox_wireless_lan_group.parent", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_wireless_lan_group.parent", "tags.0", testName), resource.TestCheckResourceAttr("netbox_wireless_lan_group.child", "name", fmt.Sprintf("%s-child", testName)), resource.TestCheckResourceAttr("netbox_wireless_lan_group.child", "slug", fmt.Sprintf("%s-c", randomSlug)), resource.TestCheckResourceAttrPair("netbox_wireless_lan_group.child", "parent_id", "netbox_wireless_lan_group.parent", "id"), ), }, { ResourceName: "netbox_wireless_lan_group.parent", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxWirelessLANGroup_defaultSlug(t *testing.T) { testSlug := "wlangrp_defslug" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_wireless_lan_group" "test" { name = "%s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_wireless_lan_group.test", "name", testName), resource.TestCheckResourceAttr("netbox_wireless_lan_group.test", "slug", getSlug(testName)), ), }, }, }) } func TestAccNetboxWirelessLANGroup_updateParentAndDescription(t *testing.T) { testSlug := "wlangrp_update" testName := testAccGetTestName(testSlug) parentName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_wireless_lan_group" "parent" { name = "%[1]s" } resource "netbox_wireless_lan_group" "test" { name = "%[2]s" description = "foo bar" parent_id = netbox_wireless_lan_group.parent.id }`, parentName, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_wireless_lan_group.test", "name", testName), resource.TestCheckResourceAttr("netbox_wireless_lan_group.test", "description", "foo bar"), resource.TestCheckResourceAttrPair("netbox_wireless_lan_group.test", "parent_id", "netbox_wireless_lan_group.parent", "id"), ), }, { Config: fmt.Sprintf(` resource "netbox_wireless_lan_group" "parent" { name = "%[1]s" } resource "netbox_wireless_lan_group" "test" { name = "%[2]s" }`, parentName, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_wireless_lan_group.test", "name", testName), resource.TestCheckResourceAttr("netbox_wireless_lan_group.test", "description", ""), resource.TestCheckResourceAttr("netbox_wireless_lan_group.test", "parent_id", "0"), ), }, }, }) } ================================================ FILE: netbox/resource_netbox_wireless_lan_test.go ================================================ package netbox import ( "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccNetboxWirelessLANDependencies(testName string) string { return fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]s" } resource "netbox_tenant" "test" { name = "%[1]s" } resource "netbox_site" "test" { name = "%[1]s" status = "active" } resource "netbox_vlan_group" "test_group" { name = "%[1]s" slug = "%[1]s" scope_type = "dcim.site" scope_id = netbox_site.test.id vid_ranges = [[1, 4094]] } resource "netbox_vlan" "test" { name = "%[1]s" vid = 777 status = "active" tenant_id = netbox_tenant.test.id site_id = netbox_site.test.id group_id = netbox_vlan_group.test_group.id } resource "netbox_wireless_lan_group" "test" { name = "%[1]s" }`, testName) } func TestAccNetboxWirelessLAN_basic(t *testing.T) { testSlug := "wlan_basic" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` resource "netbox_wireless_lan" "test" { ssid = "%s" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_wireless_lan.test", "ssid", testName), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "status", "active"), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "description", ""), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "comments", ""), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "tags.#", "0"), ), }, { ResourceName: "netbox_wireless_lan.test", ImportState: true, ImportStateVerify: true, }, }, }) } func TestAccNetboxWirelessLAN_withDependencies(t *testing.T) { testSlug := "wlan_with_deps" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testAccNetboxWirelessLANDependencies(testName) + fmt.Sprintf(` resource "netbox_wireless_lan" "test" { ssid = "%[1]s" status = "reserved" group_id = netbox_wireless_lan_group.test.id tenant_id = netbox_tenant.test.id vlan_id = netbox_vlan.test.id auth_type = "wpa-personal" auth_cipher = "aes" auth_psk = "supersecret123" description = "test description" comments = "test comments" tags = [netbox_tag.test.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_wireless_lan.test", "ssid", testName), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "status", "reserved"), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "auth_type", "wpa-personal"), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "auth_cipher", "aes"), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "description", "test description"), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "comments", "test comments"), resource.TestCheckResourceAttrPair("netbox_wireless_lan.test", "group_id", "netbox_wireless_lan_group.test", "id"), resource.TestCheckResourceAttrPair("netbox_wireless_lan.test", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("netbox_wireless_lan.test", "vlan_id", "netbox_vlan.test", "id"), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "tags.#", "1"), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "tags.0", testName), ), }, { ResourceName: "netbox_wireless_lan.test", ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{"auth_psk"}, }, }, }) } func TestAccNetboxWirelessLAN_clearOptionalFields(t *testing.T) { testSlug := "wlan_clear_opts" testName := testAccGetTestName(testSlug) resource.ParallelTest(t, resource.TestCase{ Providers: testAccProviders, PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testAccNetboxWirelessLANDependencies(testName) + fmt.Sprintf(` resource "netbox_wireless_lan" "test" { ssid = "%[1]s" status = "reserved" group_id = netbox_wireless_lan_group.test.id tenant_id = netbox_tenant.test.id vlan_id = netbox_vlan.test.id auth_type = "wpa-personal" auth_cipher = "aes" auth_psk = "supersecret123" description = "test description" comments = "test comments" tags = [netbox_tag.test.name] }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_wireless_lan.test", "status", "reserved"), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "auth_type", "wpa-personal"), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "auth_cipher", "aes"), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "description", "test description"), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "comments", "test comments"), resource.TestCheckResourceAttrPair("netbox_wireless_lan.test", "group_id", "netbox_wireless_lan_group.test", "id"), resource.TestCheckResourceAttrPair("netbox_wireless_lan.test", "tenant_id", "netbox_tenant.test", "id"), resource.TestCheckResourceAttrPair("netbox_wireless_lan.test", "vlan_id", "netbox_vlan.test", "id"), ), }, { Config: testAccNetboxWirelessLANDependencies(testName) + fmt.Sprintf(` resource "netbox_wireless_lan" "test" { ssid = "%[1]s" status = "active" }`, testName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("netbox_wireless_lan.test", "status", "active"), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "auth_type", ""), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "auth_cipher", ""), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "auth_psk", ""), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "description", ""), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "comments", ""), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "group_id", "0"), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "tenant_id", "0"), resource.TestCheckResourceAttr("netbox_wireless_lan.test", "vlan_id", "0"), ), }, }, }) } ================================================ FILE: netbox/slug_test.go ================================================ package netbox import ( "testing" ) func TestSlugGeneration(t *testing.T) { for _, tt := range []struct { name, input, expected string }{ { name: "LowerCase", input: "FOO", expected: "foo", }, { name: "SpecialChars", input: `f^o!o"§$%&/-_()=?b`, expected: "foo-_b", }, { name: "Multidash", input: "--d-a---s---h------", expected: "d-a-s-h", }, { name: "Trailing", input: "foo& $", expected: "foo", }, { name: "Full", input: "Foo & 33 bar -- yes-", expected: "foo-33-bar-yes", }, } { t.Run(tt.name, func(t *testing.T) { actual := getSlug(tt.input) if actual != tt.expected { t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", tt.expected, actual) } }) } } ================================================ FILE: netbox/slugs.go ================================================ package netbox import ( "regexp" "strings" ) func getSlug(name string) string { var result string // \w = word characters (== [0-9A-Za-z_]) // \s = whitespace (== [\t\n\f\r ]) matchSpecial, _ := regexp.Compile(`[^\w\s-]`) matchMultiWhitespacesAndDashes, _ := regexp.Compile(`[\s-]+`) // Special chars are stripped result = matchSpecial.ReplaceAllString(name, "") // Blocks of multiple whitespaces and dashes will be replaced by a single dash result = matchMultiWhitespacesAndDashes.ReplaceAllString(result, "-") result = strings.Trim(result, "-") return strings.ToLower(result) } ================================================ FILE: netbox/tags.go ================================================ package netbox import ( "fmt" "slices" "github.com/fbreckle/go-netbox/netbox/client" "github.com/fbreckle/go-netbox/netbox/client/extras" "github.com/fbreckle/go-netbox/netbox/models" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) const ( tagsKey = "tags" tagsAllKey = "tags_all" ) var tagsSchema = &schema.Schema{ Type: schema.TypeSet, Elem: &schema.Schema{ Type: schema.TypeString, }, Optional: true, Set: schema.HashString, } var tagsAllSchema = &schema.Schema{ Type: schema.TypeSet, Elem: &schema.Schema{ Type: schema.TypeString, }, Computed: true, Set: schema.HashString, } var tagsSchemaRead = &schema.Schema{ Type: schema.TypeSet, Elem: &schema.Schema{ Type: schema.TypeString, }, Computed: true, Set: schema.HashString, } func getNestedTagListFromResourceDataSet(state *providerState, d interface{}) ([]*models.NestedTag, error) { tagList := d.(*schema.Set).List() tags := []*models.NestedTag{} for _, tag := range tagList { nbTag, ok := state.tagCache[tag.(string)] if !ok { var err error nbTag, err = findTag(state.NetBoxAPI, tag.(string)) if err != nil { return tags, err } } tags = append(tags, nbTag) } return tags, nil } func findTag(client *client.NetBoxAPI, name string) (*models.NestedTag, error) { params := extras.NewExtrasTagsListParams() params.Name = &name limit := int64(2) // We search for a unique tag. Having two hits suffices to know its not unique. params.Limit = &limit res, err := client.Extras.ExtrasTagsList(params, nil) if err != nil { return nil, fmt.Errorf("API Error trying to retrieve tag %q from netbox: %w", name, err) } payload := res.GetPayload() switch *payload.Count { case int64(0): return nil, fmt.Errorf("could not locate referenced tag %q in netbox, no results", name) case int64(1): return &models.NestedTag{ Name: payload.Results[0].Name, Slug: payload.Results[0].Slug, }, nil default: return nil, fmt.Errorf("could not map tag %q to unique tag in netbox, %d results", name, *payload.Count) } } func getTagListFromNestedTagList(nestedTags []*models.NestedTag) []string { tags := []string{} for _, nestedTag := range nestedTags { tags = append(tags, *nestedTag.Name) } return tags } func (s *providerState) readTags(d *schema.ResourceData, apiTags []*models.NestedTag) { allTags := schema.NewSet(schema.HashString, nil) for _, t := range apiTags { allTags.Add(*t.Name) } d.Set(tagsAllKey, allTags.List()) configTags := make([]string, len(apiTags)) cf := d.GetRawConfig() if cf.IsNull() || !cf.IsKnown() { cf = d.GetRawState() // config is missing during refresh } if !cf.IsNull() && cf.IsKnown() { // there is some config c := cf.GetAttr(tagsKey) if !c.IsNull() && c.IsKnown() { // tags are configured for _, t := range c.AsValueSet().Values() { configTags = append(configTags, t.AsString()) } } } resourceTags := schema.NewSet(schema.HashString, nil) // remove default tags (except when configured on the resource) for _, tag := range apiTags { if !s.defaultTags.Contains(*tag.Name) || slices.Contains(configTags, *tag.Name) { resourceTags.Add(*tag.Name) } } d.Set(tagsKey, resourceTags.List()) } ================================================ FILE: netbox/tags_test.go ================================================ package netbox import ( "testing" "github.com/fbreckle/go-netbox/netbox/models" "github.com/stretchr/testify/assert" ) func TestGetTagListFromNestedTagList(t *testing.T) { tags := []*models.NestedTag{ { Name: strToPtr("Foo"), Slug: strToPtr("foo"), }, { Name: strToPtr("Bar"), Slug: strToPtr("bar"), }, } flat := getTagListFromNestedTagList(tags) expected := []string{ "Foo", "Bar", } assert.Equal(t, flat, expected) } ================================================ FILE: netbox/util.go ================================================ package netbox import ( "encoding/json" "fmt" "reflect" "regexp" "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func strToPtr(str string) *string { return &str } func int64ToPtr(i int64) *int64 { return &i } func float64ToPtr(i float64) *float64 { return &i } func toStringList(a interface{}) []string { strList := []string{} for _, str := range a.(*schema.Set).List() { strList = append(strList, str.(string)) } return strList } func toInt64List(a interface{}) []int64 { intList := []int64{} for _, number := range a.(*schema.Set).List() { if n, ok := number.(int); ok { intList = append(intList, int64(n)) } else if n, ok := number.(int64); ok { intList = append(intList, n) } } return intList } func toInt64PtrList(a interface{}) []*int64 { intList := []*int64{} if set, ok := a.(*schema.Set); ok { for _, number := range set.List() { if n, ok := number.(int); ok { intList = append(intList, int64ToPtr(int64(n))) } else if n, ok := number.(int64); ok { intList = append(intList, int64ToPtr(n)) } } } return intList } func joinStringWithFinalConjunction(elems []string, sep, con string) string { switch len(elems) { case 0: return "" case 1: return elems[0] } var b strings.Builder b.WriteString(strings.Join(elems[0:len(elems)-1], sep)) fmt.Fprintf(&b, " %s %s", con, elems[len(elems)-1]) return b.String() } func buildValidValueDescription(options []string) string { var quoted []string for _, option := range options { quoted = append(quoted, fmt.Sprintf("`%s`", option)) } return "Valid values are " + joinStringWithFinalConjunction(quoted, ", ", "and") } func getOptionalStr(d *schema.ResourceData, key string, useSpace bool) string { strVal := "" // check if key is set strValInterface, ok := d.GetOk(key) if ok || d.HasChange(key) { if !ok && useSpace { // Setting an space string deletes the value // Ideally we would have the ability to determine if a struct value was set to the zero-value or not // The API often supports setting null to clear a value, but this is a Go/Swagger limitation strVal = " " } else if ok { strVal = strValInterface.(string) } } return strVal } func getOptionalVal[SchemaT int | float64, ApiT int64 | float64](d *schema.ResourceData, key string) *ApiT { var apiPtr *ApiT schemaValIf, ok := d.GetOk(key) if ok { schemaVal, _ := schemaValIf.(SchemaT) apiVal := ApiT(schemaVal) apiPtr = &apiVal } return apiPtr } func getOptionalInt(d *schema.ResourceData, key string) *int64 { return getOptionalVal[int, int64](d, key) } func getOptionalFloat(d *schema.ResourceData, key string) *float64 { return getOptionalVal[float64, float64](d, key) } // jsonSemanticCompare returns true when 2 json strings encode the same // structure, regardless of whitespace differences. This can be used in // DiffSuppressFunc implementations to prevent terraform showing whitespace // changes as differences on refresh. func jsonSemanticCompare(a, b string) (equal bool, err error) { var aDecoded, bDecoded any err = json.Unmarshal([]byte(a), &aDecoded) if err != nil { return false, fmt.Errorf("could not decode a: %w", err) } err = json.Unmarshal([]byte(b), &bDecoded) if err != nil { return false, fmt.Errorf("could not decode b: %w", err) } return reflect.DeepEqual(aDecoded, bDecoded), nil } // extractNetboxVersionFromString extracts the first semantic versioning string // from a given string. The string must be preceded with "v". // This is needed since netbox-docker recently started displaying its version string // as, for example, v4.2.8-Docker-3.2.1 and we want to ignore the Docker part // ref: https://github.com/e-breuninger/terraform-provider-netbox/issues/729 func extractSemanticVersionFromString(s string) (version string, err error) { re := regexp.MustCompile(`^v?(\d+\.\d+\.\d+)`) matches := re.FindStringSubmatch(s) if len(matches) < 2 { return "", fmt.Errorf("no semantic version found in version string") } return matches[1], nil } ================================================ FILE: netbox/util_test.go ================================================ package netbox import ( "testing" ) func TestJoinStringWithFinalConjunction(t *testing.T) { for _, tt := range []struct { name string list []string sep string con string expected string }{ { name: "Full", list: []string{"foo", "bar", "baz"}, sep: ", ", con: "and", expected: "foo, bar and baz", }, { name: "OnlyTwoItems", list: []string{"foo", "bar"}, sep: ", ", con: "and", expected: "foo and bar", }, } { t.Run(tt.name, func(t *testing.T) { actual := joinStringWithFinalConjunction(tt.list, tt.sep, tt.con) if actual != tt.expected { t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", tt.expected, actual) } }) } } func TestBuildValidValueDescription(t *testing.T) { for _, tt := range []struct { name string list []string expected string }{ { name: "Full", list: []string{"foo", "bar", "baz"}, expected: "Valid values are `foo`, `bar` and `baz`", }, } { t.Run(tt.name, func(t *testing.T) { actual := buildValidValueDescription(tt.list) if actual != tt.expected { t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", tt.expected, actual) } }) } } func TestJsonSemanticCompareEqual(t *testing.T) { a := `{"a": [{ "b": [1, 2, 3]}]}` b := `{"a":[{"b":[1,2,3]}]}` equal, err := jsonSemanticCompare(a, b) if err != nil { t.Fatalf("unexpected error: %s", err) } if !equal { t.Errorf("expected 'a' and 'b' to be semantically equal\n\na: %s\nb: %s\n", a, b) } } func TestJsonSemanticCompareUnequal(t *testing.T) { a := `{"a": [{ "b": [1, 2, 3]}]}` b := `{"a": [{ "b": [1, 2, 4]}]}` equal, err := jsonSemanticCompare(a, b) if err != nil { t.Fatalf("unexpected error: %s", err) } if equal { t.Errorf("expected 'a' and 'b' to be semantically unequal\n\na: %s\nb: %s\n", a, b) } } func TestExtractSemanticVersionFromString(t *testing.T) { for _, tt := range []struct { name string input string expected string }{ { name: "Incomplete", input: "v1.3", expected: "", }, { name: "SimpleWithV", input: "v1.2.3", expected: "1.2.3", }, { name: "SimpleWithoutV", input: "1.2.3", expected: "1.2.3", }, { name: "Docker", input: "v4.5.6-Docker-3.2", expected: "4.5.6", }, } { t.Run(tt.name, func(t *testing.T) { actual, _ := extractSemanticVersionFromString(tt.input) if actual != tt.expected { t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", tt.expected, actual) } }) } } ================================================ FILE: netbox/validation.go ================================================ package netbox import "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" const ( maxUint16 = ^uint16(0) maxInt16 = int(maxUint16 >> 1) maxUint32 = ^uint32(0) maxInt32 = int(maxUint32 >> 1) ) var ( validatePositiveInt16 = validation.IntBetween(0, maxInt16) validatePositiveInt32 = validation.IntBetween(0, maxInt32) ) ================================================ FILE: scripts/allowed_subcategories.sh ================================================ #!/bin/bash # This script makes sure that we do not accidentally introduce a new subcategory in the docs # Each subcategory is rendered as a separate list in the docs page at https://registry.terraform.io/providers/e-breuninger/netbox/latest/docs # The list in the docs should be concise, not cluttered with typos or other inadvertent subcategories # # The list of allowed subcategories is maintained in a file (see below) readonly allowfilepath=".github/allowed-subcategories.txt" while read -r line; do if ! grep --quiet "$line" "$allowfilepath"; then echo "error: subcategory \"$line\" is not in $allowfilepath" exit 1 fi done <<<"$(grep --no-filename --recursive subcategory docs | sort --unique | cut --delimiter='"' --fields=2)" ================================================ FILE: scripts/ensure_docs_examples.sh ================================================ #!/bin/bash exitcode=0 resources="$(grep "resource.*()," netbox/provider.go | sed 's/.*"\(.*\)":.*/\1/')" for resource in ${resources}; do ls -1 examples/resources/"$resource"/*.tf >/dev/null 2>&1 # ls has exitcode 2 if no files are found if [ "$?" = "2" ]; then echo "Resource $resource has no example" exitcode=1 fi done # Code for data source examples. Currently not used. #datasources="$(grep "dataSource.*()," netbox/provider.go | sed 's/.*"\(.*\)":.*/\1/')" # #for datasource in ${datasources}; do # ls -1 examples/data-sources/"$datasource"/*.tf >/dev/null 2>&1 # if [ "$?" = "2" ]; then # echo "Data source $datasource has no example" # exitcode=1 # fi #done exit $exitcode ================================================ FILE: templates/index.md.tmpl ================================================ --- layout: "" page_title: "Provider: Netbox" description: |- The netbox provider provides resources and data sources to interact with Netbox. --- # Netbox Provider The Terraform Netbox provider is a plugin for Terraform that allows for the full lifecycle management of [Netbox](https://netboxlabs.com/docs/netbox/) resources. Use the navigation to the left to read about the available resources. ## Supported Netbox versions Netbox often makes breaking API changes even in non-major releases. Check the table below to see which version this provider is compatible with your Netbox version. It is generally recommended to use the provider version matching your Netbox version. | Netbox version | Provider version | | --------------- | ---------------- | | v4.3.0 - 4.4.10 | v5.0.0 and up | | v4.2.2 - 4.2.9 | v4.0.0 - 4.3.1 | | v4.1.0 - 4.1.11 | v3.10.0 - 3.11.1 | | v4.0.0 - 4.0.11 | v3.9.0 - 3.9.2 | | v3.7.0 - 3.7.8 | v3.8.0 - 3.8.9 | | v3.6.0 - 3.6.9 | v3.7.0 - 3.7.7 | | v3.5.1 - 3.5.9 | v3.6.x | | v3.4.3 - 3.4.10 | v3.5.x | | v3.3.0 - 3.4.2 | v3.0.x - 3.5.1 | | v3.2.0 - 3.2.9 | v2.0.x | | v3.1.9 | v1.6.0 - 1.6.7 | | v3.1.3 | v1.1.x - 1.5.2 | | v3.0.9 | v1.0.x | | v2.11.12 | v0.3.x | | v2.10.10 | v0.2.x | | v2.9 | v0.1.x | Additionally, since version [1.6.6](https://github.com/e-breuninger/terraform-provider-netbox/commit/0b0b2fffa54d4ab2e5f1677e948b01e56ba211c8), each version of the provider has a built-in list of all Netbox versions it supports at release time. Upon initialization, the provider will probe your Netbox version and include a (non-blocking) warning if the used Netbox version is not supported. ## Configuration You must configure the provider with proper credentials before you can use it. You can configure the provider via attributes in the provider block or via environment variables. See [Schema](#schema) for all configuration options ## Example Usage {{tffile "examples/provider/provider.tf"}} {{ .SchemaMarkdown | trimspace }} ================================================ FILE: templates/resources/available_ip_address.md.tmpl ================================================ --- page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" subcategory: "IP Address Management (IPAM)" description: |- {{ .Description | plainmarkdown | trimspace | prefixlines " " }} --- # {{.Name}} ({{.Type}}) {{ .Description | trimspace }} ## Example Usage ### Creating an IP in a prefix {{ tffile "examples/resources/netbox_available_ip_address/prefix.tf" }} ### Creating an IP in an IP range {{ tffile "examples/resources/netbox_available_ip_address/range.tf" }} ### Marking an IP active and assigning to interface {{ tffile "examples/resources/netbox_available_ip_address/assign_to_interface.tf" }} {{ .SchemaMarkdown | trimspace }} {{ if .HasImport -}} ## Import Import is supported using the following syntax: {{ printf "{{codefile \"shell\" %q}}" .ImportFile }} {{- end }} ================================================ FILE: templates/resources/ip_address.md.tmpl ================================================ --- page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" subcategory: "IP Address Management (IPAM)" description: |- {{ .Description | plainmarkdown | trimspace | prefixlines " " }} --- # {{.Name}} ({{.Type}}) {{ .Description | trimspace }} ## Example Usage ### Creating an IP address that is assigned to a virtual machine interface Starting with provider version 3.5.0, you can use the `virtual_machine_interface_id` attribute to assign an IP address to a virtual machine interface. You can also use the `interface_id` and `object_type` attributes instead. With `virtual_machine_interface_id`: {{ tffile "examples/resources/netbox_ip_address/virtual_machine_interface_id.tf" }} With `object_type` and `interface_id`: {{ tffile "examples/resources/netbox_ip_address/object_type_virtual_machine.tf" }} ### Creating an IP address that is assigned to a device interface Starting with provider version 3.5.0, you can use the `device_interface_id` attribute to assign an IP address to a device interface. You can also use the `interface_id` and `object_type` attributes instead. With `device_interface_id`: {{ tffile "examples/resources/netbox_ip_address/device_interface_id.tf" }} With `object_type` and `interface_id`: {{ tffile "examples/resources/netbox_ip_address/object_type_device.tf" }} ### Creating an IP address that is not assigned to anything You can create an IP address that is not assigned to anything by omitting the attributes mentioned above. {{ tffile "examples/resources/netbox_ip_address/standalone.tf" }} {{ .SchemaMarkdown | trimspace }} {{ if .HasImport -}} ## Import Import is supported using the following syntax: {{ printf "{{codefile \"shell\" %q}}" .ImportFile }} {{- end }} ================================================ FILE: templates/resources/mac_address.md.tmpl ================================================ --- page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" subcategory: "Data Center Inventory Management (DCIM)" description: |- {{ .Description | plainmarkdown | trimspace | prefixlines " " }} --- # {{.Name}} ({{.Type}}) {{ .Description | trimspace }} ## Example Usage ### Creating a MAC address that is assigned to a virtual machine interface With `virtual_machine_interface_id`: {{ tffile "examples/resources/netbox_mac_address/virtual_machine_interface_id.tf" }} With `object_type` and `interface_id`: {{ tffile "examples/resources/netbox_mac_address/object_type_virtual_machine.tf" }} ### Creating a MAC address that is assigned to a device interface With `device_interface_id`: {{ tffile "examples/resources/netbox_mac_address/device_interface_id.tf" }} With `object_type` and `interface_id`: {{ tffile "examples/resources/netbox_mac_address/object_type_device.tf" }} ### Creating a MAC address that is not assigned to anything You can create a MAC address that is not assigned to anything by omitting the attributes mentioned above. {{ tffile "examples/resources/netbox_mac_address/standalone.tf" }} {{ .SchemaMarkdown | trimspace }} {{ if .HasImport -}} ## Import Import is supported using the following syntax: {{ printf "{{codefile \"shell\" %q}}" .ImportFile }} {{- end }} ================================================ FILE: tools/tools.go ================================================ // +build tools package tools import ( // document generation _ "github.com/fbreckle/terraform-plugin-docs/cmd/tfplugindocs" )