mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-07-04 21:14:05 +00:00
feat(sdn)!: add SDN support for zones, vnets, subnets with validation and tests
BREAKING CHANGE: introduces sdn support. Signed-off-by: MacherelR <64424331+MacherelR@users.noreply.github.com>
This commit is contained in:
parent
221faafc8c
commit
58ff2ff240
41
docs/data-sources/virtual_environment_sdn_subnet.md
Normal file
41
docs/data-sources/virtual_environment_sdn_subnet.md
Normal file
@ -0,0 +1,41 @@
|
||||
---
|
||||
layout: page
|
||||
title: proxmox_virtual_environment_sdn_subnet
|
||||
parent: Data Sources
|
||||
subcategory: Virtual Environment
|
||||
description: |-
|
||||
Retrieve details about a specific SDN Subnet in Proxmox VE.
|
||||
---
|
||||
|
||||
# Data Source: proxmox_virtual_environment_sdn_subnet
|
||||
|
||||
Retrieve details about a specific SDN Subnet in Proxmox VE.
|
||||
|
||||
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `subnet` (String)
|
||||
- `vnet` (String) The VNet this subnet belongs to.
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `canonical_name` (String)
|
||||
- `dhcp_dns_server` (String) The DNS server used for DHCP.
|
||||
- `dhcp_range` (Attributes List) List of DHCP ranges (start and end IPs). (see [below for nested schema](#nestedatt--dhcp_range))
|
||||
- `dnszoneprefix` (String) Prefix used for DNS zone delegation.
|
||||
- `gateway` (String) The gateway address for the subnet.
|
||||
- `id` (String) The full ID in the format 'vnet-id/subnet-id'.
|
||||
- `snat` (Boolean) Whether SNAT is enabled for the subnet.
|
||||
- `type` (String)
|
||||
|
||||
<a id="nestedatt--dhcp_range"></a>
|
||||
### Nested Schema for `dhcp_range`
|
||||
|
||||
Read-Only:
|
||||
|
||||
- `end_address` (String) End of the DHCP range.
|
||||
- `start_address` (String) Start of the DHCP range.
|
32
docs/data-sources/virtual_environment_sdn_vnet.md
Normal file
32
docs/data-sources/virtual_environment_sdn_vnet.md
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
layout: page
|
||||
title: proxmox_virtual_environment_sdn_vnet
|
||||
parent: Data Sources
|
||||
subcategory: Virtual Environment
|
||||
description: |-
|
||||
Retrieves information about an existing SDN Vnet in Proxmox VE.
|
||||
---
|
||||
|
||||
# Data Source: proxmox_virtual_environment_sdn_vnet
|
||||
|
||||
Retrieves information about an existing SDN Vnet in Proxmox VE.
|
||||
|
||||
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `name` (String) The name of the vnet.
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `alias` (String) - An alias for this vnet.
|
||||
- `id` (String) - The ID of the vnet (usually the name).
|
||||
- `isolate_ports` (Boolean) - Whether ports are isolated.
|
||||
- `tag` (Number) - VLAN/VXLAN tag.
|
||||
- `type` (String) - Type of the vnet.
|
||||
- `vlanaware` (Boolean) - Whether this vnet is VLAN aware.
|
||||
- `zone` (String) - The zone associated with the vnet.
|
||||
- `zonetype` (String) - The type of the zone associated with this vnet.
|
45
docs/data-sources/virtual_environment_sdn_zone.md
Normal file
45
docs/data-sources/virtual_environment_sdn_zone.md
Normal file
@ -0,0 +1,45 @@
|
||||
---
|
||||
layout: page
|
||||
title: proxmox_virtual_environment_sdn_zone
|
||||
parent: Data Sources
|
||||
subcategory: Virtual Environment
|
||||
description: |-
|
||||
Fetch a Proxmox SDN Zone by name.
|
||||
---
|
||||
|
||||
# Data Source: proxmox_virtual_environment_sdn_zone
|
||||
|
||||
|
||||
This data source allows you to fetch information about an existing SDN zone in a Proxmox Virtual Environment (PVE) cluster by its name.
|
||||
|
||||
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `name` (String) Name (ID) of the SDN zone.
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `advertise_subnets` (Boolean) - Whether to advertise subnets to the zone.
|
||||
- `bridge` (String) – Linux bridge device used (if applicable).
|
||||
- `controller` (String) – Controller for EVPN zones.
|
||||
- `disable_arp_nd_suppression` (Boolean) – Whether ARP/ND suppression is disabled.
|
||||
- `dns` (String) – DNS server configured for the zone.
|
||||
- `dns_zone` (String) – The DNS zone name used by this SDN zone.
|
||||
- `exit_nodes` (String) – Nodes designated as exit points.
|
||||
- `exit_nodes_local_routing` (Boolean) – Whether local routing is enabled for exit nodes.
|
||||
- `id` (String) - The ID of the SDN zone.
|
||||
- `ipam` (String) – The IP Address Management (IPAM) method used in the zone.
|
||||
- `mtu` (Number) – Maximum Transmission Unit for this zone.
|
||||
- `nodes` (String) – Comma-separated list of node names associated with the zone.
|
||||
- `peers` (String) – Peers used for some zone types only.
|
||||
- `primary_exit_node` (String) – The main exit node.
|
||||
- `reversedns` (String) – Reverse DNS server for the zone.
|
||||
- `rt_import` (String) – Route targets to import.
|
||||
- `tag` (Number) – VLAN tag or other numeric identifier.
|
||||
- `type` (String) – The SDN zone type (e.g., `simple`, `vlan`, `vxlan`, `evpn`).
|
||||
- `vlan_protocol` (String) – VLAN protocol used.
|
||||
- `vrf_vxlan` (Number) – VXLAN ID associated with VRF zones.
|
44
docs/resources/virtual_environment_sdn_subnet.md
Normal file
44
docs/resources/virtual_environment_sdn_subnet.md
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
layout: page
|
||||
title: proxmox_virtual_environment_sdn_subnet
|
||||
parent: Resources
|
||||
subcategory: Virtual Environment
|
||||
description: |-
|
||||
Manages SDN Subnets in Proxmox VE.
|
||||
---
|
||||
|
||||
# Resource: proxmox_virtual_environment_sdn_subnet
|
||||
|
||||
Manages SDN Subnets in Proxmox VE.
|
||||
|
||||
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `subnet` (String) The name/ID of the subnet.
|
||||
- `vnet` (String) The VNet to which this subnet belongs.
|
||||
|
||||
### Optional
|
||||
|
||||
- `dhcp_dns_server` (String) The DNS server used for DHCP.
|
||||
- `dhcp_range` (Attributes List) List of DHCP ranges (start and end IPs). (see [below for nested schema](#nestedatt--dhcp_range))
|
||||
- `dnszoneprefix` (String) Prefix used for DNS zone delegation.
|
||||
- `gateway` (String) The gateway address for the subnet.
|
||||
- `snat` (Boolean) Whether SNAT is enabled for the subnet.
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `canonical_name` (String) Canonical name of the subnet (e.g. zoneM-10.10.0.0-24).
|
||||
- `id` (String) The unique identifier of this resource.
|
||||
- `type` (String) Subnet type (set default at 'subnet')
|
||||
|
||||
<a id="nestedatt--dhcp_range"></a>
|
||||
### Nested Schema for `dhcp_range`
|
||||
|
||||
Required:
|
||||
|
||||
- `end_address` (String) End of the DHCP range.
|
||||
- `start_address` (String) Start of the DHCP range.
|
35
docs/resources/virtual_environment_sdn_vnet.md
Normal file
35
docs/resources/virtual_environment_sdn_vnet.md
Normal file
@ -0,0 +1,35 @@
|
||||
---
|
||||
layout: page
|
||||
title: proxmox_virtual_environment_sdn_vnet
|
||||
parent: Resources
|
||||
subcategory: Virtual Environment
|
||||
description: |-
|
||||
Manages Proxmox VE SDN vnet.
|
||||
---
|
||||
|
||||
# Resource: proxmox_virtual_environment_sdn_vnet
|
||||
|
||||
Manages Proxmox VE SDN vnet.
|
||||
|
||||
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `name` (String) Unique identifier for the vnet.
|
||||
- `zone` (String) The zone to which this vnet belongs.
|
||||
- `zonetype` (String) Parent's zone type. MUST be specified.
|
||||
|
||||
### Optional
|
||||
|
||||
- `alias` (String) An optional alias for this vnet.
|
||||
- `isolate_ports` (Boolean) Whether to isolate ports within this vnet.
|
||||
- `tag` (Number) Tag value for VLAN/VXLAN (depends on zone type).
|
||||
- `vlanaware` (Boolean) Whether this vnet is VLAN aware.
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `id` (String) The unique identifier of this resource.
|
||||
- `type` (String) Type of vnet (e.g. 'vnet').
|
60
docs/resources/virtual_environment_sdn_zone.md
Normal file
60
docs/resources/virtual_environment_sdn_zone.md
Normal file
@ -0,0 +1,60 @@
|
||||
---
|
||||
layout: page
|
||||
title: proxmox_virtual_environment_sdn_zone
|
||||
parent: Resources
|
||||
subcategory: Virtual Environment
|
||||
description: |-
|
||||
Manages SDN Zones in Proxmox VE.
|
||||
---
|
||||
|
||||
# Resource: proxmox_virtual_environment_sdn_zone
|
||||
|
||||
Manages SDN Zones in Proxmox VE.
|
||||
Some attributes in the `proxmox_virtual_environment_sdn_zone` resource or data source are only applicable to certain zone types. For example:
|
||||
|
||||
`bridge` is relevant only for `vlan` zones.
|
||||
|
||||
`peers`, `controller`, `vrf_vxlan`, and related attributes are specific to `vxlan` and `evpn` zone types.
|
||||
|
||||
`service_vlan` and `vlan_protocol` apply to `qinq` zones.
|
||||
|
||||
While the Proxmox API does not explicitly document these constraints, they are enforced by the Proxmox backend and have been validated manually through API experimentation.
|
||||
|
||||
The Terraform provider implements field-level validation to ensure that only compatible attributes are used with each zone type. If incompatible attributes are set, Terraform will raise a configuration error during plan or apply to prevent invalid requests to the Proxmox API.
|
||||
|
||||
This design helps ensure correctness and avoids unexpected API failures when managing SDN zones across different zone types.
|
||||
|
||||
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `name` (String) The unique ID of the SDN zone.
|
||||
- `type` (String) Zone type (e.g. simple, vlan, qinq, vxlan, evpn).
|
||||
|
||||
### Optional
|
||||
|
||||
- `advertise_subnets` (Boolean) Enable subnet advertisement for EVPN.
|
||||
- `bridge` (String) Bridge interface for VLAN/QinQ.
|
||||
- `controller` (String) EVPN controller address.
|
||||
- `disable_arp_nd_suppression` (Boolean) Disable ARP/ND suppression for EVPN.
|
||||
- `dns` (String) DNS server address.
|
||||
- `dns_zone` (String) DNS zone name.
|
||||
- `exit_nodes` (String) Comma-separated list of exit nodes for EVPN.
|
||||
- `exit_nodes_local_routing` (Boolean) Enable local routing for EVPN exit nodes.
|
||||
- `ipam` (String) IP Address Management system.
|
||||
- `mtu` (Number) MTU value for the zone.
|
||||
- `nodes` (String) Comma-separated list of Proxmox node names.
|
||||
- `peers` (String) Peers list for VXLAN.
|
||||
- `primary_exit_node` (String) Primary exit node for EVPN.
|
||||
- `reversedns` (String) Reverse DNS settings.
|
||||
- `rt_import` (String) Route target import for EVPN.
|
||||
- `tag` (Number) Service VLAN tag for QinQ.
|
||||
- `vlan_protocol` (String) Service VLAN protocol for QinQ.
|
||||
- `vrf_vxlan` (Number) EVPN VRF VXLAN ID.
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `id` (String) The unique identifier of this resource.
|
@ -4,13 +4,13 @@ resource "proxmox_virtual_environment_container" "example_template" {
|
||||
start_on_boot = "true"
|
||||
|
||||
disk {
|
||||
datastore_id = "local-lvm"
|
||||
datastore_id = var.virtual_environment_storage
|
||||
size = 4
|
||||
}
|
||||
|
||||
mount_point {
|
||||
// volume mount
|
||||
volume = "local-lvm"
|
||||
volume = var.virtual_environment_storage
|
||||
size = "4G"
|
||||
path = "mnt/local"
|
||||
}
|
||||
@ -66,7 +66,7 @@ resource "proxmox_virtual_environment_container" "example_template" {
|
||||
|
||||
resource "proxmox_virtual_environment_container" "example" {
|
||||
disk {
|
||||
datastore_id = "local-lvm"
|
||||
datastore_id = var.virtual_environment_storage
|
||||
}
|
||||
|
||||
clone {
|
||||
|
@ -3,7 +3,7 @@
|
||||
resource "proxmox_virtual_environment_download_file" "release_20240725_ubuntu_24_noble_lxc_img" {
|
||||
content_type = "vztmpl"
|
||||
datastore_id = "local"
|
||||
node_name = "pve"
|
||||
node_name = var.virtual_environment_node_name
|
||||
url = var.release_20240725_ubuntu_24_noble_lxc_img_url
|
||||
checksum = var.release_20240725_ubuntu_24_noble_lxc_img_checksum
|
||||
checksum_algorithm = "sha256"
|
||||
@ -15,7 +15,7 @@ resource "proxmox_virtual_environment_download_file" "latest_debian_12_bookworm_
|
||||
content_type = "iso"
|
||||
datastore_id = "local"
|
||||
file_name = "debian-12-generic-amd64.img"
|
||||
node_name = "pve"
|
||||
node_name = var.virtual_environment_node_name
|
||||
url = var.latest_debian_12_bookworm_qcow2_img_url
|
||||
overwrite = true
|
||||
overwrite_unmanaged = true
|
||||
|
108
example/resource_virtual_environment_sdn.tf
Normal file
108
example/resource_virtual_environment_sdn.tf
Normal file
@ -0,0 +1,108 @@
|
||||
# --- SDN Zones ---
|
||||
|
||||
resource "proxmox_virtual_environment_sdn_zone" "zone_simple" {
|
||||
name = "zoneS"
|
||||
type = "simple"
|
||||
nodes = var.virtual_environment_node_name
|
||||
mtu = 1496
|
||||
}
|
||||
|
||||
resource "proxmox_virtual_environment_sdn_zone" "zone_vlan" {
|
||||
name = "zoneVLAN"
|
||||
type = "vlan"
|
||||
nodes = var.virtual_environment_node_name
|
||||
mtu = 1500
|
||||
bridge = "vmbr0"
|
||||
}
|
||||
|
||||
# --- SDN Vnets ---
|
||||
|
||||
resource "proxmox_virtual_environment_sdn_vnet" "vnet_simple" {
|
||||
name = "vnetM"
|
||||
zone = proxmox_virtual_environment_sdn_zone.zone_simple.name
|
||||
alias = "vnet in zoneM"
|
||||
isolate_ports = "0"
|
||||
vlanaware = "0"
|
||||
zonetype = proxmox_virtual_environment_sdn_zone.zone_simple.type
|
||||
}
|
||||
|
||||
resource "proxmox_virtual_environment_sdn_vnet" "vnet_vlan" {
|
||||
name = "vnetVLAN"
|
||||
zone = proxmox_virtual_environment_sdn_zone.zone_vlan.name
|
||||
alias = "vnet in zoneVLAN"
|
||||
tag = 1000
|
||||
zonetype = proxmox_virtual_environment_sdn_zone.zone_vlan.type
|
||||
}
|
||||
|
||||
# --- SDN Subnets ---
|
||||
|
||||
resource "proxmox_virtual_environment_sdn_subnet" "subnet_simple" {
|
||||
subnet = "10.10.0.0/24"
|
||||
vnet = proxmox_virtual_environment_sdn_vnet.vnet_simple.name
|
||||
dhcp_dns_server = "10.10.0.53"
|
||||
dhcp_range = [
|
||||
{
|
||||
start_address = "10.10.0.10"
|
||||
end_address = "10.10.0.100"
|
||||
}
|
||||
]
|
||||
gateway = "10.10.0.1"
|
||||
snat = true
|
||||
}
|
||||
|
||||
resource "proxmox_virtual_environment_sdn_subnet" "subnet_simple2" {
|
||||
subnet = "10.40.0.0/24"
|
||||
vnet = proxmox_virtual_environment_sdn_vnet.vnet_simple.name
|
||||
dhcp_dns_server = "10.40.0.53"
|
||||
dhcp_range = [
|
||||
{
|
||||
start_address = "10.40.0.10"
|
||||
end_address = "10.40.0.100"
|
||||
}
|
||||
]
|
||||
gateway = "10.40.0.1"
|
||||
snat = true
|
||||
}
|
||||
|
||||
resource "proxmox_virtual_environment_sdn_subnet" "subnet_vlan" {
|
||||
subnet = "10.20.0.0/24"
|
||||
vnet = proxmox_virtual_environment_sdn_vnet.vnet_vlan.name
|
||||
dhcp_dns_server = "10.20.0.53"
|
||||
dhcp_range = [
|
||||
{
|
||||
start_address = "10.20.0.10"
|
||||
end_address = "10.20.0.100"
|
||||
}
|
||||
]
|
||||
gateway = "10.20.0.100"
|
||||
snat = false
|
||||
}
|
||||
|
||||
# --- Data Sources ---
|
||||
|
||||
data "proxmox_virtual_environment_sdn_zone" "zone_ex" {
|
||||
name = "ZoneEx"
|
||||
}
|
||||
|
||||
data "proxmox_virtual_environment_sdn_vnet" "vnet_ex" {
|
||||
name = "VnetEx"
|
||||
}
|
||||
|
||||
data "proxmox_virtual_environment_sdn_subnet" "subnet_ex" {
|
||||
subnet = "ZoneEx-100.100.0.0-24"
|
||||
vnet = data.proxmox_virtual_environment_sdn_vnet.vnet_ex.id
|
||||
}
|
||||
|
||||
# --- Outputs ---
|
||||
|
||||
output "sdn_zone" {
|
||||
value = data.proxmox_virtual_environment_sdn_zone.zone_ex
|
||||
}
|
||||
|
||||
output "sdn_vnet" {
|
||||
value = data.proxmox_virtual_environment_sdn_vnet.vnet_ex
|
||||
}
|
||||
|
||||
output "sdn_subnet" {
|
||||
value = data.proxmox_virtual_environment_sdn_subnet.subnet_ex
|
||||
}
|
@ -13,6 +13,18 @@ variable "virtual_environment_ssh_username" {
|
||||
description = "The username for the Proxmox Virtual Environment API"
|
||||
}
|
||||
|
||||
variable "virtual_environment_node_name" {
|
||||
description = "Name of the Proxmox node"
|
||||
type = string
|
||||
default = "pve"
|
||||
}
|
||||
|
||||
variable "virtual_environment_storage" {
|
||||
description = "Name of the Proxmox storage"
|
||||
type = string
|
||||
default = "local-lvm"
|
||||
}
|
||||
|
||||
variable "latest_debian_12_bookworm_qcow2_img_url" {
|
||||
type = string
|
||||
description = "The URL for the latest Debian 12 Bookworm qcow2 image"
|
||||
|
@ -1,6 +1,6 @@
|
||||
resource "proxmox_virtual_environment_vm" "ubuntu_clone" {
|
||||
name = "ubuntu-clone"
|
||||
node_name = "pve"
|
||||
node_name = var.virtual_environment_node_name
|
||||
|
||||
clone {
|
||||
vm_id = proxmox_virtual_environment_vm.ubuntu_template.id
|
||||
|
137
fwprovider/cluster/sdn/datasource_sdn_subnets.go
Normal file
137
fwprovider/cluster/sdn/datasource_sdn_subnets.go
Normal file
@ -0,0 +1,137 @@
|
||||
package sdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/config"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/subnets"
|
||||
)
|
||||
|
||||
var (
|
||||
_ datasource.DataSource = &sdnSubnetDataSource{}
|
||||
_ datasource.DataSourceWithConfigure = &sdnSubnetDataSource{}
|
||||
)
|
||||
|
||||
type sdnSubnetDataSource struct {
|
||||
client *subnets.Client
|
||||
}
|
||||
|
||||
func NewSDNSubnetDataSource() datasource.DataSource {
|
||||
return &sdnSubnetDataSource{}
|
||||
}
|
||||
|
||||
func (d *sdnSubnetDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_sdn_subnet"
|
||||
}
|
||||
|
||||
func (d *sdnSubnetDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cfg, ok := req.ProviderData.(config.DataSource)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Provider Configuration",
|
||||
fmt.Sprintf("Expected config.DataSource, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
d.client = cfg.Client.Cluster().SDNSubnets()
|
||||
}
|
||||
|
||||
func (d *sdnSubnetDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Retrieve details about a specific SDN Subnet in Proxmox VE.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Computed: true,
|
||||
Description: "The full ID in the format 'vnet-id/subnet-id'.",
|
||||
},
|
||||
"subnet": schema.StringAttribute{
|
||||
Required: true,
|
||||
},
|
||||
"canonical_name": schema.StringAttribute{
|
||||
Computed: true,
|
||||
},
|
||||
"type": schema.StringAttribute{
|
||||
Computed: true,
|
||||
},
|
||||
"vnet": schema.StringAttribute{
|
||||
Required: true,
|
||||
Description: "The VNet this subnet belongs to.",
|
||||
},
|
||||
"dhcp_dns_server": schema.StringAttribute{
|
||||
Computed: true,
|
||||
Description: "The DNS server used for DHCP.",
|
||||
},
|
||||
"dhcp_range": schema.ListNestedAttribute{
|
||||
Optional: false,
|
||||
Computed: true,
|
||||
Description: "List of DHCP ranges (start and end IPs).",
|
||||
NestedObject: schema.NestedAttributeObject{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"start_address": schema.StringAttribute{
|
||||
Computed: true,
|
||||
Description: "Start of the DHCP range.",
|
||||
},
|
||||
"end_address": schema.StringAttribute{
|
||||
Computed: true,
|
||||
Description: "End of the DHCP range.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"dnszoneprefix": schema.StringAttribute{
|
||||
Computed: true,
|
||||
Description: "Prefix used for DNS zone delegation.",
|
||||
},
|
||||
"gateway": schema.StringAttribute{
|
||||
Computed: true,
|
||||
Description: "The gateway address for the subnet.",
|
||||
},
|
||||
"snat": schema.BoolAttribute{
|
||||
Computed: true,
|
||||
Description: "Whether SNAT is enabled for the subnet.",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *sdnSubnetDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||
var config sdnSubnetModel
|
||||
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
subnet, err := d.client.GetSubnet(ctx, config.Vnet.ValueString(), config.Subnet.ValueString())
|
||||
if err != nil {
|
||||
if errors.Is(err, api.ErrResourceDoesNotExist) {
|
||||
resp.Diagnostics.AddError("Subnet not found", err.Error())
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddError("Failed to retrieve subnet", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set the state
|
||||
state := &sdnSubnetModel{}
|
||||
state.Subnet = config.Subnet
|
||||
state.Vnet = config.Vnet
|
||||
state.importFromAPI(config.Subnet.ValueString(), subnet)
|
||||
|
||||
// Set canonical name and ID (both = user-supplied subnet)
|
||||
state.ID = config.Subnet
|
||||
state.CanonicalName = config.Subnet
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
|
||||
}
|
119
fwprovider/cluster/sdn/datasource_sdn_vnets.go
Normal file
119
fwprovider/cluster/sdn/datasource_sdn_vnets.go
Normal file
@ -0,0 +1,119 @@
|
||||
package sdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/config"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/vnets"
|
||||
)
|
||||
|
||||
var (
|
||||
_ datasource.DataSource = &sdnVnetDataSource{}
|
||||
_ datasource.DataSourceWithConfigure = &sdnVnetDataSource{}
|
||||
)
|
||||
|
||||
type sdnVnetDataSource struct {
|
||||
client *vnets.Client
|
||||
}
|
||||
|
||||
func NewSDNVnetDataSource() datasource.DataSource {
|
||||
return &sdnVnetDataSource{}
|
||||
}
|
||||
|
||||
func (d *sdnVnetDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_sdn_vnet"
|
||||
}
|
||||
|
||||
func (d *sdnVnetDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cfg, ok := req.ProviderData.(config.DataSource)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Provider Data",
|
||||
fmt.Sprintf("Expected config.DataSource, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
d.client = cfg.Client.Cluster().SDNVnets()
|
||||
}
|
||||
|
||||
func (d *sdnVnetDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Retrieves information about an existing SDN Vnet in Proxmox VE.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "The ID of the vnet (usually the name).",
|
||||
Computed: true,
|
||||
},
|
||||
"name": schema.StringAttribute{
|
||||
Required: true,
|
||||
Description: "The name of the vnet.",
|
||||
},
|
||||
"zone": schema.StringAttribute{
|
||||
Computed: true,
|
||||
Description: "The zone associated with the vnet.",
|
||||
},
|
||||
"zonetype": schema.StringAttribute{
|
||||
Computed: true,
|
||||
Description: "The type of the zone associated with this vnet.",
|
||||
},
|
||||
"alias": schema.StringAttribute{
|
||||
Computed: true,
|
||||
Description: "An alias for this vnet.",
|
||||
},
|
||||
"isolate_ports": schema.BoolAttribute{
|
||||
Computed: true,
|
||||
Description: "Whether ports are isolated.",
|
||||
},
|
||||
"tag": schema.Int64Attribute{
|
||||
Computed: true,
|
||||
Description: "VLAN/VXLAN tag.",
|
||||
},
|
||||
"type": schema.StringAttribute{
|
||||
Computed: true,
|
||||
Description: "Type of the vnet.",
|
||||
},
|
||||
"vlanaware": schema.BoolAttribute{
|
||||
Computed: true,
|
||||
Description: "Whether this vnet is VLAN aware.",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *sdnVnetDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||
var config sdnVnetModel
|
||||
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
vnetID := config.Name.ValueString()
|
||||
vnet, err := d.client.GetVnet(ctx, vnetID)
|
||||
if err != nil {
|
||||
if errors.Is(err, api.ErrResourceDoesNotExist) {
|
||||
resp.Diagnostics.AddError("Vnet not found", fmt.Sprintf("No vnet with ID %q exists", vnetID))
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddError("Error retrieving vnet", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
state := sdnVnetModel{}
|
||||
state.importFromAPI(vnetID, vnet)
|
||||
state.ID = types.StringValue(vnetID)
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
98
fwprovider/cluster/sdn/datasource_sdn_zones.go
Normal file
98
fwprovider/cluster/sdn/datasource_sdn_zones.go
Normal file
@ -0,0 +1,98 @@
|
||||
package sdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/config"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones"
|
||||
)
|
||||
|
||||
var _ datasource.DataSource = &sdnZoneDataSource{}
|
||||
var _ datasource.DataSourceWithConfigure = &sdnZoneDataSource{}
|
||||
|
||||
type sdnZoneDataSource struct {
|
||||
client *zones.Client
|
||||
}
|
||||
|
||||
func NewSDNZoneDataSource() datasource.DataSource {
|
||||
return &sdnZoneDataSource{}
|
||||
}
|
||||
|
||||
func (d *sdnZoneDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_sdn_zone"
|
||||
}
|
||||
|
||||
func (d *sdnZoneDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cfg, ok := req.ProviderData.(config.DataSource)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Provider Configuration",
|
||||
fmt.Sprintf("Expected config.DataSource but got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
d.client = cfg.Client.Cluster().SDNZones()
|
||||
}
|
||||
|
||||
func (d *sdnZoneDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Fetch a Proxmox SDN Zone by name.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Computed: true,
|
||||
Description: "The ID of the SDN zone.",
|
||||
},
|
||||
"name": schema.StringAttribute{
|
||||
Required: true,
|
||||
Description: "Name (ID) of the SDN zone.",
|
||||
},
|
||||
"type": schema.StringAttribute{Computed: true},
|
||||
"ipam": schema.StringAttribute{Computed: true},
|
||||
"dns": schema.StringAttribute{Computed: true},
|
||||
"reversedns": schema.StringAttribute{Computed: true},
|
||||
"dns_zone": schema.StringAttribute{Computed: true},
|
||||
"nodes": schema.StringAttribute{Computed: true},
|
||||
"mtu": schema.Int64Attribute{Computed: true},
|
||||
"bridge": schema.StringAttribute{Computed: true},
|
||||
"tag": schema.Int64Attribute{Computed: true},
|
||||
"vlan_protocol": schema.StringAttribute{Computed: true},
|
||||
"peers": schema.StringAttribute{Computed: true},
|
||||
"controller": schema.StringAttribute{Computed: true},
|
||||
"vrf_vxlan": schema.Int64Attribute{Computed: true},
|
||||
"exit_nodes": schema.StringAttribute{Computed: true},
|
||||
"primary_exit_node": schema.StringAttribute{Computed: true},
|
||||
"exit_nodes_local_routing": schema.BoolAttribute{Computed: true},
|
||||
"advertise_subnets": schema.BoolAttribute{Computed: true},
|
||||
"disable_arp_nd_suppression": schema.BoolAttribute{Computed: true},
|
||||
"rt_import": schema.StringAttribute{Computed: true},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *sdnZoneDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||
var data sdnZoneModel
|
||||
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
zone, err := d.client.GetZone(ctx, data.Name.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Unable to fetch SDN Zone", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
readModel := &sdnZoneModel{}
|
||||
readModel.importFromAPI(zone.ID, zone)
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...)
|
||||
}
|
340
fwprovider/cluster/sdn/resource_sdn_subnets.go
Normal file
340
fwprovider/cluster/sdn/resource_sdn_subnets.go
Normal file
@ -0,0 +1,340 @@
|
||||
package sdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/attribute"
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/config"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/subnets"
|
||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = &sdnSubnetResource{}
|
||||
_ resource.ResourceWithConfigure = &sdnSubnetResource{}
|
||||
_ resource.ResourceWithImportState = &sdnSubnetResource{}
|
||||
)
|
||||
|
||||
type sdnSubnetResource struct {
|
||||
client *subnets.Client
|
||||
}
|
||||
|
||||
func NewSDNSubnetResource() resource.Resource {
|
||||
return &sdnSubnetResource{}
|
||||
}
|
||||
|
||||
func (r *sdnSubnetResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_sdn_subnet"
|
||||
}
|
||||
|
||||
func (r *sdnSubnetResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cfg, ok := req.ProviderData.(config.Resource)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected config.Resource, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
r.client = cfg.Client.Cluster().SDNSubnets()
|
||||
}
|
||||
|
||||
func (r *sdnSubnetResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Manages SDN Subnets in Proxmox VE.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": attribute.ResourceID(),
|
||||
"subnet": schema.StringAttribute{
|
||||
Required: true,
|
||||
Description: "The name/ID of the subnet.",
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"canonical_name": schema.StringAttribute{
|
||||
Computed: true,
|
||||
Description: "Canonical name of the subnet (e.g. zoneM-10.10.0.0-24).",
|
||||
},
|
||||
"type": schema.StringAttribute{
|
||||
Computed: true,
|
||||
Description: "Subnet type (set default at 'subnet')",
|
||||
Default: stringdefault.StaticString("subnet"),
|
||||
},
|
||||
"vnet": schema.StringAttribute{
|
||||
Required: true,
|
||||
Description: "The VNet to which this subnet belongs.",
|
||||
},
|
||||
"dhcp_dns_server": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Description: "The DNS server used for DHCP.",
|
||||
},
|
||||
"dhcp_range": schema.ListNestedAttribute{
|
||||
Optional: true,
|
||||
Description: "List of DHCP ranges (start and end IPs).",
|
||||
NestedObject: schema.NestedAttributeObject{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"start_address": schema.StringAttribute{
|
||||
Required: true,
|
||||
Description: "Start of the DHCP range.",
|
||||
},
|
||||
"end_address": schema.StringAttribute{
|
||||
Required: true,
|
||||
Description: "End of the DHCP range.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"dnszoneprefix": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Description: "Prefix used for DNS zone delegation.",
|
||||
},
|
||||
"gateway": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Description: "The gateway address for the subnet.",
|
||||
},
|
||||
"snat": schema.BoolAttribute{
|
||||
Optional: true,
|
||||
Description: "Whether SNAT is enabled for the subnet.",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *sdnSubnetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan sdnSubnetModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
if plan.Vnet.IsNull() || plan.Vnet.IsUnknown() {
|
||||
resp.Diagnostics.AddAttributeError(
|
||||
path.Root("vnet"),
|
||||
"missing required field",
|
||||
"Missing the parent vnet's ID attribute, which is required to define a subnet")
|
||||
return
|
||||
}
|
||||
err := r.client.CreateSubnet(ctx, plan.Vnet.ValueString(), plan.toAPIRequestBody())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error creating subnet", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
tflog.Debug(ctx, "Created object's ID", map[string]any{"plan name:": plan.Subnet})
|
||||
plan.ID = plan.Subnet
|
||||
|
||||
// Because proxmox API doesn't return the created object's properties and the subnet's name gets modified by proxmox internally
|
||||
// Read it back to get the canonical-ID from proxmox
|
||||
canonicalID, err := resolveCanonicalSubnetID(ctx, r.client, plan.Vnet.ValueString(), plan.Subnet.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error resolving canonical subnet ID", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
plan.ID = types.StringValue(canonicalID)
|
||||
plan.CanonicalName = types.StringValue(canonicalID)
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *sdnSubnetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state sdnSubnetModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
subnet, err := r.client.GetSubnet(ctx, state.Vnet.ValueString(), state.ID.ValueString())
|
||||
if err != nil {
|
||||
if errors.Is(err, api.ErrResourceDoesNotExist) {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.AddError("Error reading subnet", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
readModel := &sdnSubnetModel{}
|
||||
readModel.Subnet = state.Subnet
|
||||
readModel.importFromAPI(state.ID.ValueString(), subnet)
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...)
|
||||
}
|
||||
|
||||
func (r *sdnSubnetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan sdnSubnetModel
|
||||
// var state sdnSubnetModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
// resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
reqData := plan.toAPIRequestBody()
|
||||
// reqData.Delete = toDelete
|
||||
|
||||
if plan.Vnet.IsNull() || plan.Vnet.IsUnknown() {
|
||||
resp.Diagnostics.AddAttributeError(
|
||||
path.Root("vnet"),
|
||||
"missing required field",
|
||||
"Missing the parent vnet's ID attribute, which is required to define a subnet")
|
||||
return
|
||||
}
|
||||
err := r.client.UpdateSubnet(ctx, plan.Vnet.ValueString(), reqData)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error updating subnet", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *sdnSubnetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state sdnSubnetModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
err := r.client.DeleteSubnet(ctx, state.Vnet.ValueString(), state.ID.ValueString())
|
||||
if err != nil && !errors.Is(err, api.ErrResourceDoesNotExist) {
|
||||
resp.Diagnostics.AddError("Error deleting subnet", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (r *sdnSubnetResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
// Expect ID format: "vnet/subnet"
|
||||
parts := strings.Split(req.ID, "/")
|
||||
if len(parts) != 2 {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Import Identifier",
|
||||
"Expected import identifier in format 'vnet-id/subnet-id'.",
|
||||
)
|
||||
return
|
||||
}
|
||||
vnetID := parts[0]
|
||||
subnetID := parts[1]
|
||||
subnet, err := r.client.GetSubnet(ctx, vnetID, subnetID)
|
||||
if err != nil {
|
||||
if errors.Is(err, api.ErrResourceDoesNotExist) {
|
||||
resp.Diagnostics.AddError("Subnet does not exist", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.AddError("Unable to import subnet", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
readModel := &sdnSubnetModel{}
|
||||
readModel.importFromAPI(req.ID, subnet)
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...)
|
||||
}
|
||||
|
||||
func resolveCanonicalSubnetID(ctx context.Context, client *subnets.Client, vnet string, originalID string) (string, error) {
|
||||
subnets, err := client.GetSubnets(ctx, vnet)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to list subnets for canonical name resolution: %w", err)
|
||||
}
|
||||
|
||||
for _, subnet := range subnets {
|
||||
if subnet.ID == originalID {
|
||||
return subnet.ID, nil // Already canonical
|
||||
}
|
||||
|
||||
// Proxmox canonical format is usually zone-prefixed:
|
||||
// e.g., zoneM-10-10-0-0-24 instead of 10.10.0.0/24
|
||||
if strings.HasSuffix(subnet.ID, strings.ReplaceAll(originalID, "/", "-")) {
|
||||
return subnet.ID, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("could not resolve canonical subnet ID for %s", originalID)
|
||||
}
|
||||
|
||||
// ValidateConfig checks that the subnet's field are correctly set. Particularly that gateway, dhcp and dns are within CIDR
|
||||
func (r *sdnSubnetResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
|
||||
var config sdnSubnetModel
|
||||
diags := req.Config.Get(ctx, &config)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
_, ipnet, err := net.ParseCIDR(config.Subnet.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddAttributeError(
|
||||
path.Root("subnet"),
|
||||
"Invalid Subnet",
|
||||
fmt.Sprintf("Could not parse subnet: %s", err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
checkIPInCIDR := func(attrName string, ipVal types.String) {
|
||||
if !ipVal.IsNull() {
|
||||
ip := net.ParseIP(ipVal.ValueString())
|
||||
if ip == nil {
|
||||
resp.Diagnostics.AddAttributeError(
|
||||
path.Root(attrName),
|
||||
"Invalid IP Address",
|
||||
fmt.Sprintf("Could not parse IP address: %s", ipVal.ValueString()),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if !ipnet.Contains(ip) {
|
||||
resp.Diagnostics.AddAttributeError(
|
||||
path.Root(attrName),
|
||||
"Invalid IP for Subnet",
|
||||
fmt.Sprintf("%s must be within the subnet %s", ipVal.ValueString(), config.Subnet.ValueString()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkIPInCIDR("gateway", config.Gateway)
|
||||
checkIPInCIDR("dhcp_dns_server", config.DhcpDnsServer)
|
||||
|
||||
for i, r := range config.DhcpRange {
|
||||
if !r.StartAddress.IsNull() {
|
||||
ip := net.ParseIP(r.StartAddress.ValueString())
|
||||
if !ipnet.Contains(ip) {
|
||||
resp.Diagnostics.AddAttributeError(
|
||||
path.Root("dhcp_range").AtListIndex(i).AtMapKey("start_address"),
|
||||
"Invalid DHCP Range Start Address",
|
||||
fmt.Sprintf("Start address %s must be within the subnet %s", ip, config.Subnet.ValueString()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if !r.EndAddress.IsNull() {
|
||||
ip := net.ParseIP(r.EndAddress.ValueString())
|
||||
if !ipnet.Contains(ip) {
|
||||
resp.Diagnostics.AddAttributeError(
|
||||
path.Root("dhcp_range").AtListIndex(i).AtMapKey("end_address"),
|
||||
"Invalid DHCP Range End Address",
|
||||
fmt.Sprintf("End address %s must be within the subnet %s", ip, config.Subnet.ValueString()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
313
fwprovider/cluster/sdn/resource_sdn_vnets.go
Normal file
313
fwprovider/cluster/sdn/resource_sdn_vnets.go
Normal file
@ -0,0 +1,313 @@
|
||||
package sdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/attribute"
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/config"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/vnets"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = &sdnVnetResource{}
|
||||
_ resource.ResourceWithConfigure = &sdnVnetResource{}
|
||||
_ resource.ResourceWithImportState = &sdnVnetResource{}
|
||||
)
|
||||
|
||||
type sdnVnetResource struct {
|
||||
client *vnets.Client
|
||||
}
|
||||
|
||||
func NewSDNVnetResource() resource.Resource {
|
||||
return &sdnVnetResource{}
|
||||
}
|
||||
|
||||
func (r *sdnVnetResource) Metadata(
|
||||
_ context.Context,
|
||||
req resource.MetadataRequest,
|
||||
resp *resource.MetadataResponse,
|
||||
) {
|
||||
resp.TypeName = req.ProviderTypeName + "_sdn_vnet"
|
||||
}
|
||||
|
||||
func (r *sdnVnetResource) Configure(
|
||||
_ context.Context,
|
||||
req resource.ConfigureRequest,
|
||||
resp *resource.ConfigureResponse,
|
||||
) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cfg, ok := req.ProviderData.(config.Resource)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected config.Resource, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
r.client = cfg.Client.Cluster().SDNVnets()
|
||||
}
|
||||
|
||||
func (r *sdnVnetResource) Schema(
|
||||
_ context.Context,
|
||||
_ resource.SchemaRequest,
|
||||
resp *resource.SchemaResponse,
|
||||
) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Manages Proxmox VE SDN vnet.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": attribute.ResourceID(),
|
||||
"name": schema.StringAttribute{
|
||||
Description: "Unique identifier for the vnet.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"zonetype": schema.StringAttribute{
|
||||
Required: true,
|
||||
Description: "Parent's zone type. MUST be specified.",
|
||||
},
|
||||
"zone": schema.StringAttribute{
|
||||
Description: "The zone to which this vnet belongs.",
|
||||
Required: true,
|
||||
},
|
||||
"alias": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Description: "An optional alias for this vnet.",
|
||||
},
|
||||
"isolate_ports": schema.BoolAttribute{
|
||||
Optional: true,
|
||||
Description: "Whether to isolate ports within this vnet.",
|
||||
},
|
||||
"tag": schema.Int64Attribute{
|
||||
Optional: true,
|
||||
Description: "Tag value for VLAN/VXLAN (depends on zone type).",
|
||||
},
|
||||
"type": schema.StringAttribute{
|
||||
Computed: true,
|
||||
Description: "Type of vnet (e.g. 'vnet').",
|
||||
Default: stringdefault.StaticString("vnet"),
|
||||
},
|
||||
"vlanaware": schema.BoolAttribute{
|
||||
Optional: true,
|
||||
Description: "Whether this vnet is VLAN aware.",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *sdnVnetResource) Create(
|
||||
ctx context.Context,
|
||||
req resource.CreateRequest,
|
||||
resp *resource.CreateResponse,
|
||||
) {
|
||||
var plan sdnVnetModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
err := r.client.CreateVnet(ctx, plan.toAPIRequestBody())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error creating vnet", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
plan.ID = plan.Name
|
||||
tflog.Info(ctx, "ZONETYPE value", map[string]any{"zonetype": plan.ZoneType.ValueString()})
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *sdnVnetResource) Read(
|
||||
ctx context.Context,
|
||||
req resource.ReadRequest,
|
||||
resp *resource.ReadResponse,
|
||||
) {
|
||||
var state sdnVnetModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := r.client.GetVnet(ctx, state.ID.ValueString())
|
||||
if err != nil {
|
||||
if errors.Is(err, api.ErrResourceDoesNotExist) {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.AddError("Error reading vnet", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
readModel := &sdnVnetModel{}
|
||||
readModel.importFromAPI(state.ID.ValueString(), data)
|
||||
// Preserve provider-only field
|
||||
readModel.ZoneType = state.ZoneType
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...)
|
||||
}
|
||||
|
||||
func (r *sdnVnetResource) Update(
|
||||
ctx context.Context,
|
||||
req resource.UpdateRequest,
|
||||
resp *resource.UpdateResponse,
|
||||
) {
|
||||
var plan sdnVnetModel
|
||||
var state sdnVnetModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
var toDelete []string
|
||||
checkDelete(plan.Alias, state.Alias, &toDelete, "alias")
|
||||
checkDelete(plan.IsolatePorts, state.IsolatePorts, &toDelete, "isolate-ports")
|
||||
checkDelete(plan.Tag, state.Tag, &toDelete, "tag")
|
||||
checkDelete(plan.Type, state.Type, &toDelete, "type")
|
||||
checkDelete(plan.VlanAware, state.VlanAware, &toDelete, "vlanaware")
|
||||
|
||||
reqData := plan.toAPIRequestBody()
|
||||
reqData.Delete = toDelete
|
||||
|
||||
err := r.client.UpdateVnet(ctx, reqData)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error updating vnet", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *sdnVnetResource) Delete(
|
||||
ctx context.Context,
|
||||
req resource.DeleteRequest,
|
||||
resp *resource.DeleteResponse,
|
||||
) {
|
||||
var state sdnVnetModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
err := r.client.DeleteVnet(ctx, state.ID.ValueString())
|
||||
if err != nil && !errors.Is(err, api.ErrResourceDoesNotExist) {
|
||||
resp.Diagnostics.AddError("Error deleting vnet", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (r *sdnVnetResource) ImportState(
|
||||
ctx context.Context,
|
||||
req resource.ImportStateRequest,
|
||||
resp *resource.ImportStateResponse,
|
||||
) {
|
||||
data, err := r.client.GetVnet(ctx, req.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, api.ErrResourceDoesNotExist) {
|
||||
resp.Diagnostics.AddError("Resource does not exist", err.Error())
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddError("Failed to import resource", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
readModel := &sdnVnetModel{}
|
||||
readModel.importFromAPI(req.ID, data)
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...)
|
||||
}
|
||||
|
||||
func checkDelete(planField, stateField attr.Value, toDelete *[]string, apiName string) {
|
||||
if planField.IsNull() && !stateField.IsNull() {
|
||||
*toDelete = append(*toDelete, apiName)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *sdnVnetResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
|
||||
var data sdnVnetModel
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
if data.Zone.IsNull() || data.Zone.IsUnknown() {
|
||||
return
|
||||
}
|
||||
|
||||
if data.ZoneType.IsNull() || data.ZoneType.IsUnknown() {
|
||||
resp.Diagnostics.AddAttributeError(
|
||||
path.Root("zonetype"),
|
||||
"Missing Required Field",
|
||||
"No Zone linked to this Vnet, please set the 'zonetype' property. \nEither from a created zone or a datasource import.")
|
||||
return
|
||||
}
|
||||
|
||||
zoneType := data.ZoneType.ValueString()
|
||||
|
||||
required := map[string][]string{
|
||||
"simple": {"name", "zone"},
|
||||
"vlan": {"name", "zone", "tag"},
|
||||
"qinq": {"name", "zone"},
|
||||
"vxlan": {"name", "zone", "tag"},
|
||||
"evpn": {"name", "zone", "tag"},
|
||||
}
|
||||
|
||||
authorized := map[string]map[string]bool{
|
||||
"simple": {"name": true, "alias": true, "zone": true, "isolate_ports": true, "vlanaware": true},
|
||||
"vlan": {"name": true, "alias": true, "zone": true, "tag": true, "isolate_ports": true, "vlanaware": true},
|
||||
"qinq": {"name": true, "alias": true, "zone": true, "tag": true, "isolate_ports": true, "vlanaware": true},
|
||||
"vxlan": {"name": true, "alias": true, "zone": true, "tag": true, "isolate_ports": true, "vlanaware": true},
|
||||
"evpn": {"name": true, "alias": true, "zone": true, "tag": true, "isolate_ports": true},
|
||||
}
|
||||
|
||||
fieldMap := map[string]attr.Value{
|
||||
"name": data.Name,
|
||||
"zone": data.Zone,
|
||||
"alias": data.Alias,
|
||||
"tag": data.Tag,
|
||||
"isolate_ports": data.IsolatePorts,
|
||||
"vlanaware": data.VlanAware,
|
||||
"type": data.Type,
|
||||
}
|
||||
|
||||
// Check required fields
|
||||
for _, field := range required[zoneType] {
|
||||
if val, ok := fieldMap[field]; ok {
|
||||
if val.IsNull() || val.IsUnknown() {
|
||||
resp.Diagnostics.AddAttributeError(
|
||||
path.Root(field),
|
||||
"Missing Required Attribute",
|
||||
fmt.Sprintf("The attribute %q is required for SDN VNETs in a %q zone.", field, zoneType),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for fieldName, val := range fieldMap {
|
||||
if !authorized[zoneType][fieldName] && !val.IsNull() && !val.IsUnknown() {
|
||||
resp.Diagnostics.AddAttributeError(
|
||||
path.Root(fieldName),
|
||||
"Unauthorized Attribute for Zone Type",
|
||||
fmt.Sprintf("The attribute %q is not allowed in VNETs under a %q zone.", fieldName, zoneType),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
315
fwprovider/cluster/sdn/resource_sdn_zones.go
Normal file
315
fwprovider/cluster/sdn/resource_sdn_zones.go
Normal file
@ -0,0 +1,315 @@
|
||||
package sdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/attribute"
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/config"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones"
|
||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = &sdnZoneResource{}
|
||||
_ resource.ResourceWithConfigure = &sdnZoneResource{}
|
||||
_ resource.ResourceWithImportState = &sdnZoneResource{}
|
||||
)
|
||||
|
||||
type sdnZoneResource struct {
|
||||
client *zones.Client
|
||||
}
|
||||
|
||||
func NewSDNZoneResource() resource.Resource {
|
||||
return &sdnZoneResource{}
|
||||
}
|
||||
|
||||
func (r *sdnZoneResource) Metadata(
|
||||
_ context.Context,
|
||||
req resource.MetadataRequest,
|
||||
resp *resource.MetadataResponse,
|
||||
) {
|
||||
resp.TypeName = req.ProviderTypeName + "_sdn_zone"
|
||||
}
|
||||
|
||||
func (r *sdnZoneResource) Configure(
|
||||
_ context.Context,
|
||||
req resource.ConfigureRequest,
|
||||
resp *resource.ConfigureResponse,
|
||||
) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cfg, ok := req.ProviderData.(config.Resource)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected config.Resource, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
r.client = cfg.Client.Cluster().SDNZones()
|
||||
}
|
||||
|
||||
func (r *sdnZoneResource) Schema(
|
||||
_ context.Context,
|
||||
_ resource.SchemaRequest,
|
||||
resp *resource.SchemaResponse,
|
||||
) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Manages SDN Zones in Proxmox VE.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": attribute.ResourceID(),
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The unique ID of the SDN zone.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"type": schema.StringAttribute{
|
||||
Description: "Zone type (e.g. simple, vlan, qinq, vxlan, evpn).",
|
||||
Required: true,
|
||||
},
|
||||
"ipam": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Description: "IP Address Management system.",
|
||||
},
|
||||
"dns": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Description: "DNS server address.",
|
||||
},
|
||||
"reversedns": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Description: "Reverse DNS settings.",
|
||||
},
|
||||
"dns_zone": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Description: "DNS zone name.",
|
||||
},
|
||||
"nodes": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Description: "Comma-separated list of Proxmox node names.",
|
||||
},
|
||||
"mtu": schema.Int64Attribute{
|
||||
Optional: true,
|
||||
Description: "MTU value for the zone.",
|
||||
},
|
||||
"bridge": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Description: "Bridge interface for VLAN/QinQ.",
|
||||
},
|
||||
"tag": schema.Int64Attribute{
|
||||
Optional: true,
|
||||
Description: "Service VLAN tag for QinQ.",
|
||||
},
|
||||
"vlan_protocol": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Description: "Service VLAN protocol for QinQ.",
|
||||
},
|
||||
"peers": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Description: "Peers list for VXLAN.",
|
||||
},
|
||||
"controller": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Description: "EVPN controller address.",
|
||||
},
|
||||
"vrf_vxlan": schema.Int64Attribute{
|
||||
Optional: true,
|
||||
Description: "EVPN VRF VXLAN ID.",
|
||||
},
|
||||
"exit_nodes": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Description: "Comma-separated list of exit nodes for EVPN.",
|
||||
},
|
||||
"primary_exit_node": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Description: "Primary exit node for EVPN.",
|
||||
},
|
||||
"exit_nodes_local_routing": schema.BoolAttribute{
|
||||
Optional: true,
|
||||
Description: "Enable local routing for EVPN exit nodes.",
|
||||
},
|
||||
"advertise_subnets": schema.BoolAttribute{
|
||||
Optional: true,
|
||||
Description: "Enable subnet advertisement for EVPN.",
|
||||
},
|
||||
"disable_arp_nd_suppression": schema.BoolAttribute{
|
||||
Optional: true,
|
||||
Description: "Disable ARP/ND suppression for EVPN.",
|
||||
},
|
||||
"rt_import": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Description: "Route target import for EVPN.",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *sdnZoneResource) Create(
|
||||
ctx context.Context,
|
||||
req resource.CreateRequest,
|
||||
resp *resource.CreateResponse,
|
||||
) {
|
||||
var plan sdnZoneModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
reqData := plan.toAPIRequestBody()
|
||||
err := r.client.CreateZone(ctx, reqData)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Unable to Create SDN Zone", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
plan.ID = plan.Name
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *sdnZoneResource) Read(
|
||||
ctx context.Context,
|
||||
req resource.ReadRequest,
|
||||
resp *resource.ReadResponse,
|
||||
) {
|
||||
var state sdnZoneModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
zone, err := r.client.GetZone(ctx, state.ID.ValueString())
|
||||
if err != nil {
|
||||
if errors.Is(err, api.ErrResourceDoesNotExist) {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.AddError("Unable to Read SDN Zone", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
readModel := &sdnZoneModel{}
|
||||
readModel.importFromAPI(zone.ID, zone)
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...)
|
||||
}
|
||||
|
||||
func (r *sdnZoneResource) Update(
|
||||
ctx context.Context,
|
||||
req resource.UpdateRequest,
|
||||
resp *resource.UpdateResponse,
|
||||
) {
|
||||
var plan sdnZoneModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
reqData := plan.toAPIRequestBody()
|
||||
err := r.client.UpdateZone(ctx, reqData)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Unable to Update SDN Zone", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *sdnZoneResource) Delete(
|
||||
ctx context.Context,
|
||||
req resource.DeleteRequest,
|
||||
resp *resource.DeleteResponse,
|
||||
) {
|
||||
var state sdnZoneModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
err := r.client.DeleteZone(ctx, state.ID.ValueString())
|
||||
if err != nil && !errors.Is(err, api.ErrResourceDoesNotExist) {
|
||||
resp.Diagnostics.AddError("Unable to Delete SDN Zone", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (r *sdnZoneResource) ImportState(
|
||||
ctx context.Context,
|
||||
req resource.ImportStateRequest,
|
||||
resp *resource.ImportStateResponse,
|
||||
) {
|
||||
zone, err := r.client.GetZone(ctx, req.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, api.ErrResourceDoesNotExist) {
|
||||
resp.Diagnostics.AddError("Zone does not exist", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.AddError("Unable to Import SDN Zone", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
readModel := &sdnZoneModel{}
|
||||
readModel.importFromAPI(zone.ID, zone)
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...)
|
||||
}
|
||||
|
||||
func (r *sdnZoneResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
|
||||
var data sdnZoneModel
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
// Check the type field
|
||||
if data.Type.IsNull() || data.Type.IsUnknown() {
|
||||
return
|
||||
}
|
||||
|
||||
required := map[string][]string{
|
||||
"vlan": {"bridge"},
|
||||
"qinq": {"bridge", "service_vlan"},
|
||||
"vxlan": {"peers"},
|
||||
"evpn": {"controller", "vrf_vxlan"},
|
||||
}
|
||||
|
||||
zoneType := data.Type.ValueString()
|
||||
|
||||
// Extracts required fields and at the same time checks zone type validity
|
||||
fields, ok := required[zoneType]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Map of field names to their values from data
|
||||
fieldMap := map[string]attr.Value{
|
||||
"bridge": data.Bridge,
|
||||
"service_vlan": data.ServiceVLAN,
|
||||
"peers": data.Peers,
|
||||
"controller": data.Controller,
|
||||
"vrf_vxlan": data.VRFVXLANID,
|
||||
}
|
||||
|
||||
for _, field := range fields {
|
||||
val, exists := fieldMap[field]
|
||||
if !exists || val.IsNull() || val.IsUnknown() {
|
||||
resp.Diagnostics.AddAttributeError(
|
||||
path.Root(field),
|
||||
"Missing Required Field",
|
||||
fmt.Sprintf("Attribute %q is required when type is %q.", field, zoneType),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
89
fwprovider/cluster/sdn/sdn_subnet_model.go
Normal file
89
fwprovider/cluster/sdn/sdn_subnet_model.go
Normal file
@ -0,0 +1,89 @@
|
||||
package sdn
|
||||
|
||||
/*
|
||||
--------------------------------- Subnet Model Terraform ---------------------------------
|
||||
|
||||
Note: Currently in the API there are Delete and Digest options which are not available
|
||||
in the UI so the choice was made to remove them temporary, waiting for a fix.
|
||||
Also, it is not really in the way of working with terraform to use such parameters.
|
||||
----------------------------------------------------------------------------------------
|
||||
*/
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/helpers/ptrConversion"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/subnets"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||
)
|
||||
|
||||
type sdnSubnetModel struct {
|
||||
ID types.String `tfsdk:"id"`
|
||||
Subnet types.String `tfsdk:"subnet"`
|
||||
CanonicalName types.String `tfsdk:"canonical_name"`
|
||||
Type types.String `tfsdk:"type"`
|
||||
Vnet types.String `tfsdk:"vnet"`
|
||||
DhcpDnsServer types.String `tfsdk:"dhcp_dns_server"`
|
||||
DhcpRange []dhcpRangeModel `tfsdk:"dhcp_range"`
|
||||
DnsZonePrefix types.String `tfsdk:"dnszoneprefix"`
|
||||
Gateway types.String `tfsdk:"gateway"`
|
||||
Snat types.Bool `tfsdk:"snat"`
|
||||
}
|
||||
|
||||
type dhcpRangeModel struct {
|
||||
StartAddress types.String `tfsdk:"start_address"`
|
||||
EndAddress types.String `tfsdk:"end_address"`
|
||||
}
|
||||
|
||||
func (m *sdnSubnetModel) importFromAPI(name string, data *subnets.SubnetData) {
|
||||
m.ID = types.StringValue(name)
|
||||
m.CanonicalName = types.StringValue(name)
|
||||
|
||||
m.Type = types.StringPointerValue(data.Type)
|
||||
m.Vnet = types.StringPointerValue(data.Vnet)
|
||||
m.DhcpDnsServer = types.StringPointerValue(data.DHCPDNSServer)
|
||||
if data.DHCPRange != nil {
|
||||
var ranges []dhcpRangeModel
|
||||
for _, r := range data.DHCPRange {
|
||||
ranges = append(ranges, dhcpRangeModel{
|
||||
StartAddress: types.StringValue(r.StartAddress),
|
||||
EndAddress: types.StringValue(r.EndAddress),
|
||||
})
|
||||
}
|
||||
m.DhcpRange = ranges
|
||||
}
|
||||
|
||||
m.DnsZonePrefix = types.StringPointerValue(data.DNSZonePrefix)
|
||||
m.Gateway = types.StringPointerValue(data.Gateway)
|
||||
m.Snat = types.BoolPointerValue(ptrConversion.Int64ToBoolPtr(data.SNAT))
|
||||
}
|
||||
|
||||
func (m *sdnSubnetModel) toAPIRequestBody() *subnets.SubnetRequestData {
|
||||
data := &subnets.SubnetRequestData{}
|
||||
|
||||
// When creating the subnet it is ok to pass subnet cidr, but when updating need to pass canonical name
|
||||
if m.CanonicalName.ValueString() == "" {
|
||||
data.ID = m.Subnet.ValueString()
|
||||
} else {
|
||||
data.ID = m.CanonicalName.ValueString()
|
||||
}
|
||||
tflog.Warn(context.Background(), "TO API", map[string]any{
|
||||
"canonical name": m.CanonicalName.ValueString(),
|
||||
"ID": m.ID.ValueString(),
|
||||
})
|
||||
data.Type = m.Type.ValueStringPointer()
|
||||
data.Vnet = m.Vnet.ValueStringPointer()
|
||||
data.DHCPDNSServer = m.DhcpDnsServer.ValueStringPointer()
|
||||
if m.DhcpRange != nil {
|
||||
var dhcpRanges []string
|
||||
for _, r := range m.DhcpRange {
|
||||
dhcpRanges = append(dhcpRanges, fmt.Sprintf("start-address=%s,end-address=%s", r.StartAddress.ValueString(), r.EndAddress.ValueString()))
|
||||
}
|
||||
data.DHCPRange = dhcpRanges
|
||||
}
|
||||
data.DNSZonePrefix = m.DnsZonePrefix.ValueStringPointer()
|
||||
data.Gateway = m.Gateway.ValueStringPointer()
|
||||
data.SNAT = ptrConversion.BoolToInt64Ptr(m.Snat.ValueBoolPointer())
|
||||
return data
|
||||
}
|
53
fwprovider/cluster/sdn/sdn_vnet_model.go
Normal file
53
fwprovider/cluster/sdn/sdn_vnet_model.go
Normal file
@ -0,0 +1,53 @@
|
||||
package sdn
|
||||
|
||||
/*
|
||||
--------------------------------- VNET Model Terraform ---------------------------------
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import (
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/helpers/ptrConversion"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/vnets"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
type sdnVnetModel struct {
|
||||
ID types.String `tfsdk:"id"`
|
||||
Name types.String `tfsdk:"name"`
|
||||
Zone types.String `tfsdk:"zone"`
|
||||
Alias types.String `tfsdk:"alias"`
|
||||
IsolatePorts types.Bool `tfsdk:"isolate_ports"`
|
||||
Tag types.Int64 `tfsdk:"tag"`
|
||||
Type types.String `tfsdk:"type"`
|
||||
VlanAware types.Bool `tfsdk:"vlanaware"`
|
||||
ZoneType types.String `tfsdk:"zonetype"`
|
||||
}
|
||||
|
||||
func (m *sdnVnetModel) importFromAPI(name string, data *vnets.VnetData) {
|
||||
m.ID = types.StringValue(name)
|
||||
m.Name = types.StringValue(name)
|
||||
|
||||
m.Zone = types.StringPointerValue(data.Zone)
|
||||
m.Alias = types.StringPointerValue(data.Alias)
|
||||
m.IsolatePorts = types.BoolPointerValue(ptrConversion.Int64ToBoolPtr(data.IsolatePorts))
|
||||
m.Tag = types.Int64PointerValue(data.Tag)
|
||||
m.Type = types.StringPointerValue(data.Type)
|
||||
m.VlanAware = types.BoolPointerValue(ptrConversion.Int64ToBoolPtr(data.VlanAware))
|
||||
}
|
||||
|
||||
func (m *sdnVnetModel) toAPIRequestBody() *vnets.VnetRequestData {
|
||||
data := &vnets.VnetRequestData{}
|
||||
|
||||
data.ID = m.Name.ValueString()
|
||||
|
||||
data.Zone = m.Zone.ValueStringPointer()
|
||||
data.Alias = m.Alias.ValueStringPointer()
|
||||
data.IsolatePorts = ptrConversion.BoolToInt64Ptr(m.IsolatePorts.ValueBoolPointer())
|
||||
data.Tag = m.Tag.ValueInt64Pointer()
|
||||
data.Type = m.Type.ValueStringPointer()
|
||||
data.VlanAware = ptrConversion.BoolToInt64Ptr(m.VlanAware.ValueBoolPointer())
|
||||
|
||||
return data
|
||||
}
|
89
fwprovider/cluster/sdn/sdn_zone_model.go
Normal file
89
fwprovider/cluster/sdn/sdn_zone_model.go
Normal file
@ -0,0 +1,89 @@
|
||||
package sdn
|
||||
|
||||
import (
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/helpers/ptrConversion"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
type sdnZoneModel struct {
|
||||
ID types.String `tfsdk:"id"`
|
||||
Name types.String `tfsdk:"name"`
|
||||
Type types.String `tfsdk:"type"`
|
||||
IPAM types.String `tfsdk:"ipam"`
|
||||
DNS types.String `tfsdk:"dns"`
|
||||
ReverseDNS types.String `tfsdk:"reversedns"`
|
||||
DNSZone types.String `tfsdk:"dns_zone"`
|
||||
Nodes types.String `tfsdk:"nodes"`
|
||||
MTU types.Int64 `tfsdk:"mtu"`
|
||||
// VLAN
|
||||
Bridge types.String `tfsdk:"bridge"`
|
||||
// QinQ
|
||||
ServiceVLAN types.Int64 `tfsdk:"tag"`
|
||||
ServiceVLANProtocol types.String `tfsdk:"vlan_protocol"`
|
||||
// VXLAN
|
||||
Peers types.String `tfsdk:"peers"`
|
||||
// EVPN
|
||||
Controller types.String `tfsdk:"controller"`
|
||||
ExitNodes types.String `tfsdk:"exit_nodes"`
|
||||
PrimaryExitNode types.String `tfsdk:"primary_exit_node"`
|
||||
RouteTargetImport types.String `tfsdk:"rt_import"`
|
||||
VRFVXLANID types.Int64 `tfsdk:"vrf_vxlan"`
|
||||
ExitNodesLocalRouting types.Bool `tfsdk:"exit_nodes_local_routing"`
|
||||
AdvertiseSubnets types.Bool `tfsdk:"advertise_subnets"`
|
||||
DisableARPNDSuppression types.Bool `tfsdk:"disable_arp_nd_suppression"`
|
||||
}
|
||||
|
||||
func (m *sdnZoneModel) importFromAPI(name string, data *zones.ZoneData) {
|
||||
m.ID = types.StringValue(name)
|
||||
m.Name = types.StringValue(name)
|
||||
|
||||
m.Type = types.StringPointerValue(data.Type)
|
||||
m.IPAM = types.StringPointerValue(data.IPAM)
|
||||
m.DNS = types.StringPointerValue(data.DNS)
|
||||
m.ReverseDNS = types.StringPointerValue(data.ReverseDNS)
|
||||
m.DNSZone = types.StringPointerValue(data.DNSZone)
|
||||
m.Nodes = types.StringPointerValue(data.Nodes)
|
||||
m.MTU = types.Int64PointerValue(data.MTU)
|
||||
m.Bridge = types.StringPointerValue(data.Bridge)
|
||||
m.ServiceVLAN = types.Int64PointerValue(data.ServiceVLAN)
|
||||
m.ServiceVLANProtocol = types.StringPointerValue(data.ServiceVLANProtocol)
|
||||
m.Peers = types.StringPointerValue(data.Peers)
|
||||
m.Controller = types.StringPointerValue(data.Controller)
|
||||
m.ExitNodes = types.StringPointerValue(data.ExitNodes)
|
||||
m.PrimaryExitNode = types.StringPointerValue(data.PrimaryExitNode)
|
||||
m.RouteTargetImport = types.StringPointerValue(data.RouteTargetImport)
|
||||
m.VRFVXLANID = types.Int64PointerValue(data.VRFVXLANID)
|
||||
m.ExitNodesLocalRouting = types.BoolPointerValue(ptrConversion.Int64ToBoolPtr(data.ExitNodesLocalRouting))
|
||||
m.AdvertiseSubnets = types.BoolPointerValue(ptrConversion.Int64ToBoolPtr(data.AdvertiseSubnets))
|
||||
m.DisableARPNDSuppression = types.BoolPointerValue(ptrConversion.Int64ToBoolPtr(data.DisableARPNDSuppression))
|
||||
|
||||
}
|
||||
|
||||
func (m *sdnZoneModel) toAPIRequestBody() *zones.ZoneRequestData {
|
||||
data := &zones.ZoneRequestData{}
|
||||
|
||||
data.ID = m.Name.ValueString()
|
||||
|
||||
data.Type = m.Type.ValueStringPointer()
|
||||
data.IPAM = m.IPAM.ValueStringPointer()
|
||||
data.DNS = m.DNS.ValueStringPointer()
|
||||
data.ReverseDNS = m.ReverseDNS.ValueStringPointer()
|
||||
data.DNSZone = m.DNSZone.ValueStringPointer()
|
||||
data.Nodes = m.Nodes.ValueStringPointer()
|
||||
data.MTU = m.MTU.ValueInt64Pointer()
|
||||
data.Bridge = m.Bridge.ValueStringPointer()
|
||||
data.ServiceVLAN = m.ServiceVLAN.ValueInt64Pointer()
|
||||
data.ServiceVLANProtocol = m.ServiceVLANProtocol.ValueStringPointer()
|
||||
data.Peers = m.Peers.ValueStringPointer()
|
||||
data.Controller = m.Controller.ValueStringPointer()
|
||||
data.ExitNodes = m.ExitNodes.ValueStringPointer()
|
||||
data.PrimaryExitNode = m.PrimaryExitNode.ValueStringPointer()
|
||||
data.RouteTargetImport = m.RouteTargetImport.ValueStringPointer()
|
||||
data.VRFVXLANID = m.VRFVXLANID.ValueInt64Pointer()
|
||||
data.ExitNodesLocalRouting = ptrConversion.BoolToInt64Ptr(m.ExitNodesLocalRouting.ValueBoolPointer())
|
||||
data.AdvertiseSubnets = ptrConversion.BoolToInt64Ptr(m.AdvertiseSubnets.ValueBoolPointer())
|
||||
data.DisableARPNDSuppression = ptrConversion.BoolToInt64Ptr(m.DisableARPNDSuppression.ValueBoolPointer())
|
||||
|
||||
return data
|
||||
}
|
33
fwprovider/helpers/ptrConversion/ptr_conversion.go
Normal file
33
fwprovider/helpers/ptrConversion/ptr_conversion.go
Normal file
@ -0,0 +1,33 @@
|
||||
package ptrConversion
|
||||
|
||||
func BoolToInt64Ptr(boolPtr *bool) *int64 {
|
||||
if boolPtr != nil {
|
||||
var result int64
|
||||
|
||||
if *boolPtr {
|
||||
result = int64(1)
|
||||
} else {
|
||||
result = int64(0)
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Int64ToBoolPtr(int64ptr *int64) *bool {
|
||||
if int64ptr != nil {
|
||||
var result bool
|
||||
|
||||
if *int64ptr == 0 {
|
||||
result = false
|
||||
} else {
|
||||
result = true
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -30,6 +30,7 @@ import (
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/cluster/hardwaremapping"
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/cluster/metrics"
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/cluster/options"
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/cluster/sdn"
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/config"
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/nodes"
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/nodes/apt"
|
||||
@ -515,6 +516,9 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc
|
||||
nodes.NewDownloadFileResource,
|
||||
options.NewClusterOptionsResource,
|
||||
vm.NewResource,
|
||||
sdn.NewSDNZoneResource,
|
||||
sdn.NewSDNVnetResource,
|
||||
sdn.NewSDNSubnetResource,
|
||||
}
|
||||
}
|
||||
|
||||
@ -538,6 +542,9 @@ func (p *proxmoxProvider) DataSources(_ context.Context) []func() datasource.Dat
|
||||
hardwaremapping.NewUSBDataSource,
|
||||
metrics.NewMetricsServerDatasource,
|
||||
vm.NewDataSource,
|
||||
sdn.NewSDNZoneDataSource,
|
||||
sdn.NewSDNVnetDataSource,
|
||||
sdn.NewSDNSubnetDataSource,
|
||||
}
|
||||
}
|
||||
|
||||
|
64
fwprovider/test/datasource_sdn_subnet_test.go
Normal file
64
fwprovider/test/datasource_sdn_subnet_test.go
Normal file
@ -0,0 +1,64 @@
|
||||
//go:build acceptance || all
|
||||
|
||||
/*
|
||||
* 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 https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||
)
|
||||
|
||||
func TestAccDatasourceSDNSubnet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
te := InitEnvironment(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
steps []resource.TestStep
|
||||
}{
|
||||
{
|
||||
"read sdn subnet attributes",
|
||||
[]resource.TestStep{{
|
||||
Config: te.RenderConfig(`
|
||||
data "proxmox_virtual_environment_sdn_vnet" "vnet_ex" {
|
||||
name = "{{ .VNetName }}"
|
||||
}
|
||||
|
||||
data "proxmox_virtual_environment_sdn_subnet" "subnet_ex" {
|
||||
subnet = "{{ .SubnetName }}"
|
||||
vnet = data.proxmox_virtual_environment_sdn_vnet.vnet_ex.id
|
||||
}
|
||||
`),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
ResourceAttributesSet("data.proxmox_virtual_environment_sdn_subnet.subnet_ex", []string{
|
||||
"id",
|
||||
"subnet",
|
||||
"canonical_name",
|
||||
"type",
|
||||
"vnet",
|
||||
"dhcp_dns_server",
|
||||
"dhcp_range.#",
|
||||
"gateway",
|
||||
"snat",
|
||||
}),
|
||||
),
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resource.ParallelTest(t, resource.TestCase{
|
||||
ProtoV6ProviderFactories: te.AccProviders,
|
||||
Steps: tt.steps,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
54
fwprovider/test/datasource_sdn_vnet_test.go
Normal file
54
fwprovider/test/datasource_sdn_vnet_test.go
Normal file
@ -0,0 +1,54 @@
|
||||
//go:build acceptance || all
|
||||
|
||||
/*
|
||||
* 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 https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||
)
|
||||
|
||||
func TestAccDatasourceSDNVNet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
te := InitEnvironment(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
steps []resource.TestStep
|
||||
}{
|
||||
{
|
||||
"read sdn vnet attributes",
|
||||
[]resource.TestStep{{
|
||||
Config: te.RenderConfig(`
|
||||
data "proxmox_virtual_environment_sdn_vnet" "vnet_ex" {
|
||||
name = "{{ .VnetName }}"
|
||||
}
|
||||
`),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
ResourceAttributesSet("data.proxmox_virtual_environment_sdn_vnet.vnet_ex", []string{
|
||||
"id",
|
||||
"name",
|
||||
"zone",
|
||||
"type",
|
||||
}),
|
||||
),
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resource.ParallelTest(t, resource.TestCase{
|
||||
ProtoV6ProviderFactories: te.AccProviders,
|
||||
Steps: tt.steps,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
54
fwprovider/test/datasource_sdn_zone_test.go
Normal file
54
fwprovider/test/datasource_sdn_zone_test.go
Normal file
@ -0,0 +1,54 @@
|
||||
//go:build acceptance || all
|
||||
|
||||
/*
|
||||
* 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 https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||
)
|
||||
|
||||
func TestAccDatasourceSDNZone(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
te := InitEnvironment(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
steps []resource.TestStep
|
||||
}{
|
||||
{
|
||||
"read sdn zone attributes",
|
||||
[]resource.TestStep{{
|
||||
Config: te.RenderConfig(`
|
||||
data "proxmox_virtual_environment_sdn_zone" "zone_ex" {
|
||||
name = "{{ .ZoneName }}"
|
||||
}
|
||||
`),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
ResourceAttributesSet("data.proxmox_virtual_environment_sdn_zone.zone_ex", []string{
|
||||
"id",
|
||||
"name",
|
||||
"type",
|
||||
"ipam",
|
||||
}),
|
||||
),
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resource.ParallelTest(t, resource.TestCase{
|
||||
ProtoV6ProviderFactories: te.AccProviders,
|
||||
Steps: tt.steps,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
157
fwprovider/test/resource_sdn_test.go
Normal file
157
fwprovider/test/resource_sdn_test.go
Normal file
@ -0,0 +1,157 @@
|
||||
//go:build acceptance || all
|
||||
|
||||
/*
|
||||
* 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 https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||
)
|
||||
|
||||
func TestAccResourceSDN(t *testing.T) {
|
||||
te := InitEnvironment(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
steps []resource.TestStep
|
||||
}{
|
||||
{"create zones, vnets and subnets", []resource.TestStep{{
|
||||
Config: te.RenderConfig(`
|
||||
resource "proxmox_virtual_environment_sdn_zone" "zone_simple" {
|
||||
name = "zoneS"
|
||||
type = "simple"
|
||||
nodes = "weisshorn-proxmox"
|
||||
mtu = 1496
|
||||
}
|
||||
|
||||
resource "proxmox_virtual_environment_sdn_zone" "zone_vlan" {
|
||||
name = "zoneVLAN"
|
||||
type = "vlan"
|
||||
nodes = "weisshorn-proxmox"
|
||||
mtu = 1500
|
||||
bridge = "vmbr0"
|
||||
}
|
||||
|
||||
resource "proxmox_virtual_environment_sdn_vnet" "vnet_simple" {
|
||||
name = "vnetM"
|
||||
zone = proxmox_virtual_environment_sdn_zone.zone_simple.name
|
||||
alias = "vnet in zoneM"
|
||||
isolate_ports = "0"
|
||||
vlanaware = "0"
|
||||
zonetype = proxmox_virtual_environment_sdn_zone.zone_simple.type
|
||||
depends_on = [proxmox_virtual_environment_sdn_zone.zone_simple]
|
||||
}
|
||||
|
||||
resource "proxmox_virtual_environment_sdn_vnet" "vnet_vlan" {
|
||||
name = "vnetVLAN"
|
||||
zone = proxmox_virtual_environment_sdn_zone.zone_vlan.name
|
||||
alias = "vnet in zoneVLAN"
|
||||
tag = 1000
|
||||
zonetype = proxmox_virtual_environment_sdn_zone.zone_vlan.type
|
||||
depends_on = [proxmox_virtual_environment_sdn_zone.zone_vlan]
|
||||
}
|
||||
|
||||
resource "proxmox_virtual_environment_sdn_subnet" "subnet_simple" {
|
||||
subnet = "10.10.0.0/24"
|
||||
vnet = proxmox_virtual_environment_sdn_vnet.vnet_simple.name
|
||||
dhcp_dns_server = "10.10.0.53"
|
||||
dhcp_range = [
|
||||
{
|
||||
start_address = "10.10.0.10"
|
||||
end_address = "10.10.0.100"
|
||||
}
|
||||
]
|
||||
gateway = "10.10.0.1"
|
||||
snat = true
|
||||
depends_on = [proxmox_virtual_environment_sdn_vnet.vnet_simple]
|
||||
}
|
||||
|
||||
resource "proxmox_virtual_environment_sdn_subnet" "subnet_simple2" {
|
||||
subnet = "10.40.0.0/24"
|
||||
vnet = proxmox_virtual_environment_sdn_vnet.vnet_simple.name
|
||||
dhcp_dns_server = "10.40.0.53"
|
||||
dhcp_range = [
|
||||
{
|
||||
start_address = "10.40.0.10"
|
||||
end_address = "10.40.0.100"
|
||||
}
|
||||
]
|
||||
gateway = "10.40.0.1"
|
||||
snat = true
|
||||
depends_on = [proxmox_virtual_environment_sdn_vnet.vnet_simple]
|
||||
}
|
||||
|
||||
resource "proxmox_virtual_environment_sdn_subnet" "subnet_vlan" {
|
||||
subnet = "10.20.0.0/24"
|
||||
vnet = proxmox_virtual_environment_sdn_vnet.vnet_vlan.name
|
||||
dhcp_dns_server = "10.20.0.53"
|
||||
dhcp_range = [
|
||||
{
|
||||
start_address = "10.20.0.10"
|
||||
end_address = "10.20.0.100"
|
||||
}
|
||||
]
|
||||
gateway = "10.20.0.100"
|
||||
snat = false
|
||||
depends_on = [proxmox_virtual_environment_sdn_vnet.vnet_vlan]
|
||||
}
|
||||
`),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
// Zones
|
||||
ResourceAttributes("proxmox_virtual_environment_sdn_zone.zone_simple", map[string]string{
|
||||
"name": "zoneS",
|
||||
"type": "simple",
|
||||
"mtu": "1496",
|
||||
"nodes": "weisshorn-proxmox",
|
||||
}),
|
||||
ResourceAttributes("proxmox_virtual_environment_sdn_zone.zone_vlan", map[string]string{
|
||||
"name": "zoneVLAN",
|
||||
"type": "vlan",
|
||||
"mtu": "1500",
|
||||
"bridge": "vmbr0",
|
||||
}),
|
||||
|
||||
// VNets
|
||||
ResourceAttributes("proxmox_virtual_environment_sdn_vnet.vnet_simple", map[string]string{
|
||||
"name": "vnetM",
|
||||
"alias": "vnet in zoneM",
|
||||
"zone": "zoneS",
|
||||
"isolate_ports": "false",
|
||||
"vlanaware": "false",
|
||||
"zonetype": "simple",
|
||||
}),
|
||||
ResourceAttributes("proxmox_virtual_environment_sdn_vnet.vnet_vlan", map[string]string{
|
||||
"name": "vnetVLAN",
|
||||
"alias": "vnet in zoneVLAN",
|
||||
"zone": "zoneVLAN",
|
||||
"tag": "1000",
|
||||
"zonetype": "vlan",
|
||||
}),
|
||||
|
||||
// Subnet (only check one in detail to avoid too many long checks)
|
||||
ResourceAttributes("proxmox_virtual_environment_sdn_subnet.subnet_simple", map[string]string{
|
||||
"subnet": "10.10.0.0/24",
|
||||
"vnet": "vnetM",
|
||||
"gateway": "10.10.0.1",
|
||||
"dhcp_dns_server": "10.10.0.53",
|
||||
"snat": "true",
|
||||
}),
|
||||
),
|
||||
}}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resource.ParallelTest(t, resource.TestCase{
|
||||
ProtoV6ProviderFactories: te.AccProviders,
|
||||
Steps: tt.steps,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
@ -141,6 +141,16 @@ func InitEnvironment(t *testing.T) *Environment {
|
||||
nodeName = "pve"
|
||||
}
|
||||
|
||||
zoneName := utils.GetAnyStringEnv("PROXMOX_VE_ACC_ZONE_NAME")
|
||||
if zoneName == "" {
|
||||
zoneName = "ZoneEx"
|
||||
}
|
||||
|
||||
vnetName := utils.GetAnyStringEnv("PROXMOX_VE_ACC_VNET_NAME")
|
||||
if vnetName == "" {
|
||||
vnetName = "VnetEx"
|
||||
}
|
||||
|
||||
const datastoreID = "local"
|
||||
|
||||
cloudImagesServer := utils.GetAnyStringEnv("PROXMOX_VE_ACC_CLOUD_IMAGES_SERVER")
|
||||
@ -160,6 +170,8 @@ func InitEnvironment(t *testing.T) *Environment {
|
||||
"DatastoreID": datastoreID,
|
||||
"CloudImagesServer": cloudImagesServer,
|
||||
"ContainerImagesServer": containerImagesServer,
|
||||
"ZoneName": zoneName,
|
||||
"VnetName": vnetName,
|
||||
},
|
||||
NodeName: nodeName,
|
||||
DatastoreID: datastoreID,
|
||||
|
@ -15,6 +15,9 @@ import (
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/ha"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/mapping"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/metrics"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/subnets"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/vnets"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/firewall"
|
||||
)
|
||||
|
||||
@ -54,3 +57,18 @@ func (c *Client) ACME() *acme.Client {
|
||||
func (c *Client) Metrics() *metrics.Client {
|
||||
return &metrics.Client{Client: c}
|
||||
}
|
||||
|
||||
// SDNZones returns a client for managing the cluster's SDN zones
|
||||
func (c *Client) SDNZones() *zones.Client {
|
||||
return &zones.Client{Client: c}
|
||||
}
|
||||
|
||||
// SDNVnets returns a client for managing the cluster's SDN Vnets
|
||||
func (c *Client) SDNVnets() *vnets.Client {
|
||||
return &vnets.Client{Client: c}
|
||||
}
|
||||
|
||||
// SDNSubnets returns a client for managing the cluster's SDN Subnets
|
||||
func (c *Client) SDNSubnets() *subnets.Client {
|
||||
return &subnets.Client{Client: c}
|
||||
}
|
||||
|
196
proxmox/cluster/sdn/sdn_test.go
Normal file
196
proxmox/cluster/sdn/sdn_test.go
Normal file
@ -0,0 +1,196 @@
|
||||
package sdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/subnets"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/vnets"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/helpers/ptr"
|
||||
)
|
||||
|
||||
const (
|
||||
testZoneID = "testzone"
|
||||
testVnetID = "testvnet"
|
||||
testSubnetCIDR = "10.10.0.0/24"
|
||||
testSubnetCanonical = "testzone-10.10.0.0-24"
|
||||
testGateway = "10.10.0.1"
|
||||
testDNS = "10.10.0.53"
|
||||
testDHCPStart = "10.10.0.10"
|
||||
testDHCPEnd = "10.10.0.100"
|
||||
)
|
||||
|
||||
type testClients struct {
|
||||
zone *zones.Client
|
||||
vnet *vnets.Client
|
||||
subnet *subnets.Client
|
||||
}
|
||||
|
||||
func getTestClients(t *testing.T) *testClients {
|
||||
apiToken := os.Getenv("PVE_TOKEN")
|
||||
url := os.Getenv("PVE_URL")
|
||||
if apiToken == "" || url == "" {
|
||||
t.Skip("PVE_TOKEN and PVE_URL must be set")
|
||||
}
|
||||
conn, err := api.NewConnection(url, true, "")
|
||||
if err != nil {
|
||||
t.Fatalf("connection error: %v", err)
|
||||
}
|
||||
creds := api.Credentials{TokenCredentials: &api.TokenCredentials{APIToken: apiToken}}
|
||||
client, err := api.NewClient(creds, conn)
|
||||
if err != nil {
|
||||
t.Fatalf("client error: %v", err)
|
||||
}
|
||||
|
||||
return &testClients{
|
||||
zone: &zones.Client{Client: client},
|
||||
vnet: &vnets.Client{Client: client},
|
||||
subnet: &subnets.Client{Client: client},
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDNLifecycle(t *testing.T) {
|
||||
clients := getTestClients(t)
|
||||
|
||||
t.Run("Create Zone", func(t *testing.T) {
|
||||
err := clients.zone.CreateZone(context.Background(), &zones.ZoneRequestData{
|
||||
ZoneData: zones.ZoneData{
|
||||
ID: testZoneID,
|
||||
Type: ptr.Ptr("vlan"),
|
||||
IPAM: ptr.Ptr("pve"),
|
||||
Bridge: ptr.Ptr("vmbr0"),
|
||||
MTU: ptr.Ptr(int64(1500)),
|
||||
Nodes: ptr.Ptr("pvenode1"),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateZone failed: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Get Zone", func(t *testing.T) {
|
||||
zone, err := clients.zone.GetZone(context.Background(), testZoneID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetZone failed: %v", err)
|
||||
}
|
||||
t.Logf("Zone: %+v", zone)
|
||||
})
|
||||
|
||||
t.Run("Update Zone", func(t *testing.T) {
|
||||
err := clients.zone.UpdateZone(context.Background(), &zones.ZoneRequestData{
|
||||
ZoneData: zones.ZoneData{
|
||||
ID: testZoneID,
|
||||
Nodes: ptr.Ptr("updatednode"),
|
||||
Bridge: ptr.Ptr("vmbr1"), // simulate a VLAN-related update
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateZone failed: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Create VNet", func(t *testing.T) {
|
||||
err := clients.vnet.CreateVnet(context.Background(), &vnets.VnetRequestData{
|
||||
VnetData: vnets.VnetData{
|
||||
ID: testVnetID,
|
||||
Zone: ptr.Ptr(testZoneID),
|
||||
Alias: ptr.Ptr("TestVNet"),
|
||||
IsolatePorts: ptr.Ptr(int64(0)),
|
||||
Type: ptr.Ptr("vnet"),
|
||||
Tag: ptr.Ptr(int64(100)),
|
||||
VlanAware: ptr.Ptr(int64(0)),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateVnet failed: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Get VNet", func(t *testing.T) {
|
||||
vnet, err := clients.vnet.GetVnet(context.Background(), testVnetID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetVnet failed: %v", err)
|
||||
}
|
||||
t.Logf("VNet: %+v", vnet)
|
||||
})
|
||||
|
||||
t.Run("Update VNet", func(t *testing.T) {
|
||||
err := clients.vnet.UpdateVnet(context.Background(), &vnets.VnetRequestData{
|
||||
VnetData: vnets.VnetData{
|
||||
ID: testVnetID,
|
||||
Alias: ptr.Ptr("UpdatedAlias"),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateVnet failed: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Create Subnet", func(t *testing.T) {
|
||||
ptr := &subnets.SubnetData{
|
||||
ID: testSubnetCIDR,
|
||||
Vnet: ptr.Ptr(testVnetID),
|
||||
Type: ptr.Ptr("subnet"),
|
||||
Gateway: ptr.Ptr(testGateway),
|
||||
DHCPDNSServer: ptr.Ptr(testDNS),
|
||||
DHCPRange: subnets.DHCPRangeList{
|
||||
{StartAddress: testDHCPStart, EndAddress: testDHCPEnd},
|
||||
},
|
||||
SNAT: ptr.Ptr(int64(1)),
|
||||
}
|
||||
req := &subnets.SubnetRequestData{
|
||||
EncodedSubnetData: *ptr.ToEncoded(),
|
||||
}
|
||||
err := clients.subnet.CreateSubnet(context.Background(), testVnetID, req)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateSubnet failed: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Get Subnet", func(t *testing.T) {
|
||||
subnet, err := clients.subnet.GetSubnet(context.Background(), testVnetID, testSubnetCanonical)
|
||||
if err != nil {
|
||||
t.Fatalf("GetSubnet failed: %v", err)
|
||||
}
|
||||
t.Logf("Subnet: %+v", subnet)
|
||||
})
|
||||
|
||||
t.Run("Update Subnet", func(t *testing.T) {
|
||||
ptr := &subnets.SubnetData{
|
||||
ID: testSubnetCanonical,
|
||||
Vnet: ptr.Ptr(testVnetID),
|
||||
Gateway: ptr.Ptr("10.10.0.254"),
|
||||
}
|
||||
req := &subnets.SubnetRequestData{
|
||||
EncodedSubnetData: *ptr.ToEncoded(),
|
||||
}
|
||||
err := clients.subnet.UpdateSubnet(context.Background(), testVnetID, req)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateSubnet failed: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete Subnet", func(t *testing.T) {
|
||||
err := clients.subnet.DeleteSubnet(context.Background(), testVnetID, testSubnetCanonical)
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteSubnet failed: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete VNet", func(t *testing.T) {
|
||||
err := clients.vnet.DeleteVnet(context.Background(), testVnetID)
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteVnet failed: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete Zone", func(t *testing.T) {
|
||||
err := clients.zone.DeleteZone(context.Background(), testZoneID)
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteZone failed: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
13
proxmox/cluster/sdn/subnets/api.go
Normal file
13
proxmox/cluster/sdn/subnets/api.go
Normal file
@ -0,0 +1,13 @@
|
||||
package subnets
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type API interface {
|
||||
GetSubnets(ctx context.Context, vnetID string) ([]SubnetData, error)
|
||||
GetSubnet(ctx context.Context, vnetID string, id string) (*SubnetData, error)
|
||||
CreateSubnet(ctx context.Context, vnetID string, data *SubnetRequestData) error
|
||||
UpdateSubnet(ctx context.Context, vnetID string, data *SubnetRequestData) error
|
||||
DeleteSubnet(ctx context.Context, vnetID string, id string) error
|
||||
}
|
17
proxmox/cluster/sdn/subnets/client.go
Normal file
17
proxmox/cluster/sdn/subnets/client.go
Normal file
@ -0,0 +1,17 @@
|
||||
package subnets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
|
||||
)
|
||||
|
||||
// Client is a client for accessing the Proxmox SDN VNETs API.
|
||||
type Client struct {
|
||||
api.Client
|
||||
}
|
||||
|
||||
// ExpandPath returns the API path for SDN VNETS.
|
||||
func (c *Client) ExpandPath(vnet_id string, path string) string {
|
||||
return fmt.Sprintf("cluster/sdn/vnets/%s/subnets/%s", vnet_id, path)
|
||||
}
|
71
proxmox/cluster/sdn/subnets/subnets.go
Normal file
71
proxmox/cluster/sdn/subnets/subnets.go
Normal file
@ -0,0 +1,71 @@
|
||||
package subnets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
|
||||
)
|
||||
|
||||
// GetSubnet retrieves a single Subnet by ID and containing Vnet's ID
|
||||
func (c *Client) GetSubnet(ctx context.Context, vnetID string, id string) (*SubnetData, error) {
|
||||
resBody := &SubnetResponseBody{}
|
||||
|
||||
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath(vnetID, id), nil, resBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error reading SDN subnet %s for Vnet %s: %w", id, vnetID, err)
|
||||
}
|
||||
|
||||
if resBody.Data == nil {
|
||||
return nil, api.ErrNoDataObjectInResponse
|
||||
}
|
||||
|
||||
return resBody.Data, nil
|
||||
}
|
||||
|
||||
// GetSubnets lists all Subnets related to a Vnet
|
||||
func (c *Client) GetSubnets(ctx context.Context, vnetID string) ([]SubnetData, error) {
|
||||
resBody := &SubnetsResponseBody{}
|
||||
|
||||
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath(vnetID, ""), nil, resBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error listing Subnets for Vnet %s: %w", vnetID, err)
|
||||
}
|
||||
|
||||
if resBody.Data == nil {
|
||||
return nil, api.ErrNoDataObjectInResponse
|
||||
}
|
||||
|
||||
return *resBody.Data, nil
|
||||
}
|
||||
|
||||
// CreateSubnet creates a new Subnet in the defined Vnet
|
||||
func (c *Client) CreateSubnet(ctx context.Context, vnetID string, data *SubnetRequestData) error {
|
||||
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath(vnetID, ""), data, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating subnet %s on VNet %s: %w", data.ID, vnetID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateSubnet updates an existing subnet inside a defined vnet
|
||||
func (c *Client) UpdateSubnet(ctx context.Context, vnetID string, data *SubnetRequestData) error {
|
||||
err := c.DoRequest(ctx, http.MethodPut, c.ExpandPath(vnetID, data.ID), data, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error updating subnet %s on VNet %s: %w", data.ID, vnetID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSubnet deletes an existing subnet inside a defined vnet
|
||||
func (c *Client) DeleteSubnet(ctx context.Context, vnetID string, id string) error {
|
||||
err := c.DoRequest(ctx, http.MethodDelete, c.ExpandPath(vnetID, id), nil, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error deleting subnet %s on VNet %s: %s", id, vnetID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
87
proxmox/cluster/sdn/subnets/subnets_types.go
Normal file
87
proxmox/cluster/sdn/subnets/subnets_types.go
Normal file
@ -0,0 +1,87 @@
|
||||
package subnets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
/*
|
||||
--------------------------------- SUBNETS -----------------------------------------------
|
||||
|
||||
This part is related to the SDN component : SubNets
|
||||
Based on docs :
|
||||
https://pve.proxmox.com/pve-docs/chapter-pvesdn.html#pvesdn_config_subnet
|
||||
https://pve.proxmox.com/pve-docs/api-viewer/index.html#/cluster/sdn/vnets/{vnet}/subnets
|
||||
|
||||
Notes:
|
||||
1. The Type is once again defined as an enum type in the API docs but isn't referenced
|
||||
anywhere. Therefore no way to check what are allowed types. 'subnet' works
|
||||
2. Currently in the API there are Delete and Digest options which are not available
|
||||
in the UI so the choice was made to remove them temporary, waiting for a fix.
|
||||
3. It is also not really in the terraform spirit to update elements like this.
|
||||
|
||||
-----------------------------------------------------------------------------------------
|
||||
*/
|
||||
type SubnetData struct {
|
||||
ID string `json:"subnet,omitempty" url:"subnet,omitempty"`
|
||||
Type *string `json:"type,omitempty" url:"type,omitempty"`
|
||||
Vnet *string `json:"vnet,omitempty" url:"vnet,omitempty"`
|
||||
DHCPDNSServer *string `json:"dhcp-dns-server,omitempty" url:"dhcp-dns-server,omitempty"`
|
||||
DHCPRange DHCPRangeList `json:"dhcp-range,omitempty" url:"dhcp-range,omitempty"`
|
||||
DNSZonePrefix *string `json:"dnszoneprefix,omitempty" url:"dnszoneprefix,omitempty"`
|
||||
Gateway *string `json:"gateway,omitempty" url:"gateway,omitempty"`
|
||||
SNAT *int64 `json:"snat,omitempty" url:"snat,omitempty"`
|
||||
}
|
||||
|
||||
type SubnetRequestData struct {
|
||||
EncodedSubnetData
|
||||
Delete []string `url:"delete,omitempty"`
|
||||
}
|
||||
|
||||
type SubnetResponseBody struct {
|
||||
Data *SubnetData `json:"data"`
|
||||
}
|
||||
|
||||
type SubnetsResponseBody struct {
|
||||
Data *[]SubnetData `json:"data"`
|
||||
}
|
||||
|
||||
type DHCPRangeList []DHCPRangeEntry
|
||||
|
||||
type DHCPRangeEntry struct {
|
||||
StartAddress string `json:"start-address"`
|
||||
EndAddress string `json:"end-address"`
|
||||
}
|
||||
|
||||
/*
|
||||
This structure had to be defined and added after realizing a weird behavior in Proxmox's API.
|
||||
When creating or updating Subnets, the dhcpRange needs to be passed as string array.
|
||||
But when reading (GET), it arrives as an array of JSON structures.
|
||||
*/
|
||||
type EncodedSubnetData struct {
|
||||
ID string `url:"subnet,omitempty"`
|
||||
Type *string `url:"type,omitempty"`
|
||||
Vnet *string `url:"vnet,omitempty"`
|
||||
DHCPDNSServer *string `url:"dhcp-dns-server,omitempty"`
|
||||
DHCPRange []string `url:"dhcp-range,omitempty"` // manually formatted
|
||||
DNSZonePrefix *string `url:"dnszoneprefix,omitempty"`
|
||||
Gateway *string `url:"gateway,omitempty"`
|
||||
SNAT *int64 `url:"snat,omitempty"`
|
||||
}
|
||||
|
||||
func (s *SubnetData) ToEncoded() *EncodedSubnetData {
|
||||
var encodedRanges []string
|
||||
for _, r := range s.DHCPRange {
|
||||
encodedRanges = append(encodedRanges, fmt.Sprintf("start-address=%s,end-address=%s", r.StartAddress, r.EndAddress))
|
||||
}
|
||||
|
||||
return &EncodedSubnetData{
|
||||
ID: s.ID,
|
||||
Type: s.Type,
|
||||
Vnet: s.Vnet,
|
||||
DHCPDNSServer: s.DHCPDNSServer,
|
||||
DHCPRange: encodedRanges,
|
||||
DNSZonePrefix: s.DNSZonePrefix,
|
||||
Gateway: s.Gateway,
|
||||
SNAT: s.SNAT,
|
||||
}
|
||||
}
|
16
proxmox/cluster/sdn/vnets/api.go
Normal file
16
proxmox/cluster/sdn/vnets/api.go
Normal file
@ -0,0 +1,16 @@
|
||||
package vnets
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones"
|
||||
)
|
||||
|
||||
type API interface {
|
||||
GetVnets(ctx context.Context) ([]VnetData, error)
|
||||
GetVnet(ctx context.Context, id string) (*VnetData, error)
|
||||
CreateVnet(ctx context.Context, req *VnetRequestData) error
|
||||
UpdateVnet(ctx context.Context, req *VnetRequestData) error
|
||||
DeleteVnet(ctx context.Context, id string) error
|
||||
GetParentZone(ctx context.Context, zoneId string) (*zones.ZoneData, error)
|
||||
}
|
21
proxmox/cluster/sdn/vnets/client.go
Normal file
21
proxmox/cluster/sdn/vnets/client.go
Normal file
@ -0,0 +1,21 @@
|
||||
package vnets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
|
||||
)
|
||||
|
||||
// Client is a client for accessing the Proxmox SDN VNETs API.
|
||||
type Client struct {
|
||||
api.Client
|
||||
}
|
||||
|
||||
// ExpandPath returns the API path for SDN VNETS.
|
||||
func (c *Client) ExpandPath(path string) string {
|
||||
return fmt.Sprintf("cluster/sdn/vnets/%s", path)
|
||||
}
|
||||
|
||||
func (c *Client) ParentPath(parentId string) string {
|
||||
return fmt.Sprintf("cluster/sdn/zones/%s", parentId)
|
||||
}
|
82
proxmox/cluster/sdn/vnets/vnets.go
Normal file
82
proxmox/cluster/sdn/vnets/vnets.go
Normal file
@ -0,0 +1,82 @@
|
||||
package vnets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones"
|
||||
)
|
||||
|
||||
// GetVnet retrieves a single SDN Vnet by ID
|
||||
func (c *Client) GetVnet(ctx context.Context, id string) (*VnetData, error) {
|
||||
resBody := &VnetResponseBody{}
|
||||
|
||||
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath(id), nil, resBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error reading SDN Vnet %s: %w", id, err)
|
||||
}
|
||||
|
||||
if resBody.Data == nil {
|
||||
return nil, api.ErrNoDataObjectInResponse
|
||||
}
|
||||
|
||||
return resBody.Data, nil
|
||||
}
|
||||
|
||||
// GetVnets lists all SDN Vnets
|
||||
func (c *Client) GetVnets(ctx context.Context) ([]VnetData, error) {
|
||||
resBody := &VnetsResponseBody{}
|
||||
|
||||
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath(""), nil, resBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error listing SDN Vnets: %w", err)
|
||||
}
|
||||
|
||||
if resBody.Data == nil {
|
||||
return nil, api.ErrNoDataObjectInResponse
|
||||
}
|
||||
|
||||
return *resBody.Data, nil
|
||||
}
|
||||
|
||||
// CreateVnet creates a new SDN VNET
|
||||
func (c *Client) CreateVnet(ctx context.Context, data *VnetRequestData) error {
|
||||
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath(""), data, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating SDN VNET: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateVnet Updates an existing VNet
|
||||
func (c *Client) UpdateVnet(ctx context.Context, data *VnetRequestData) error {
|
||||
err := c.DoRequest(ctx, http.MethodPut, c.ExpandPath(data.ID), data, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error updating SDN VNET: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteVnet deletes an SDN VNET by ID
|
||||
func (c *Client) DeleteVnet(ctx context.Context, id string) error {
|
||||
err := c.DoRequest(ctx, http.MethodDelete, c.ExpandPath(id), nil, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error deleting SDN VNET: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) GetParentZone(ctx context.Context, zoneId string) (*zones.ZoneData, error) {
|
||||
parentZone := zones.ZoneResponseBody{}
|
||||
err := c.DoRequest(ctx, http.MethodGet, c.ParentPath(zoneId), nil, parentZone)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error fetching vnet's parent zone %s: %w", zoneId, err)
|
||||
}
|
||||
|
||||
return parentZone.Data, nil
|
||||
}
|
49
proxmox/cluster/sdn/vnets/vnets_types.go
Normal file
49
proxmox/cluster/sdn/vnets/vnets_types.go
Normal file
@ -0,0 +1,49 @@
|
||||
package vnets
|
||||
|
||||
/*
|
||||
--------------------------------- VNETS ---------------------------------
|
||||
|
||||
This part is related to the SDN component : VNETS
|
||||
Based on docs :
|
||||
https://pve.proxmox.com/pve-docs/chapter-pvesdn.html#pvesdn_config_vnet
|
||||
https://pve.proxmox.com/pve-docs/api-viewer/index.html#/cluster/sdn/vnets
|
||||
|
||||
Notes:
|
||||
|
||||
1. IsolatePorts is a boolean in the docs but needs to be passed as 0 or 1
|
||||
and is therefore defined as int.
|
||||
|
||||
2. Type field can be 'vnet' but other values are unknown
|
||||
|
||||
3. Tag cannot be set on Vnets created in simple Zones, might actually be
|
||||
only usable on vlan or vxlan zones as it sets the vlan or vxlan id.
|
||||
|
||||
4. Currently in the API there are Delete and Digest options which are not available
|
||||
in the UI so the choice was made to remove them temporary, waiting for a fix.
|
||||
|
||||
-------------------------------------------------------------------------
|
||||
*/
|
||||
type VnetData struct {
|
||||
ID string `json:"vnet,omitempty" url:"vnet,omitempty"`
|
||||
Zone *string `json:"zone,omitempty" url:"zone,omitempty"`
|
||||
Alias *string `json:"alias,omitempty" url:"alias,omitempty"`
|
||||
IsolatePorts *int64 `json:"isolate-ports,omitempty" url:"isolate-ports,omitempty"`
|
||||
Tag *int64 `json:"tag,omitempty" url:"tag,omitempty"`
|
||||
Type *string `json:"type,omitempty" url:"type,omitempty"`
|
||||
VlanAware *int64 `json:"vlanaware,omitempty" url:"vlanaware,omitempty"`
|
||||
// DeleteSettings *string `json:"delete,omitempty" url:"delete,omitempty"`
|
||||
// Digest *string `json:"digest,omitempty" url:"digest,omitempty"`
|
||||
}
|
||||
|
||||
type VnetRequestData struct {
|
||||
VnetData
|
||||
Delete []string `url:"delete,omitempty"`
|
||||
}
|
||||
|
||||
type VnetResponseBody struct {
|
||||
Data *VnetData `json:"data"`
|
||||
}
|
||||
|
||||
type VnetsResponseBody struct {
|
||||
Data *[]VnetData `json:"data"`
|
||||
}
|
13
proxmox/cluster/sdn/zones/api.go
Normal file
13
proxmox/cluster/sdn/zones/api.go
Normal file
@ -0,0 +1,13 @@
|
||||
package zones
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type API interface {
|
||||
GetZones(ctx context.Context) ([]ZoneData, error)
|
||||
GetZone(ctx context.Context, id string) (*ZoneData, error)
|
||||
CreateZone(ctx context.Context, req *ZoneRequestData) error
|
||||
UpdateZone(ctx context.Context, req *ZoneRequestData) error
|
||||
DeleteZone(ctx context.Context, id string) error
|
||||
}
|
17
proxmox/cluster/sdn/zones/client.go
Normal file
17
proxmox/cluster/sdn/zones/client.go
Normal file
@ -0,0 +1,17 @@
|
||||
package zones
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
|
||||
)
|
||||
|
||||
// Client is a client for accessing the Proxmox SDN Zones API.
|
||||
type Client struct {
|
||||
api.Client
|
||||
}
|
||||
|
||||
// ExpandPath returns the API path for SDN zones.
|
||||
func (c *Client) ExpandPath(path string) string {
|
||||
return fmt.Sprintf("cluster/sdn/zones/%s", path)
|
||||
}
|
75
proxmox/cluster/sdn/zones/zones.go
Normal file
75
proxmox/cluster/sdn/zones/zones.go
Normal file
@ -0,0 +1,75 @@
|
||||
package zones
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
|
||||
)
|
||||
|
||||
// GetZone retrieves a single SDN zone by ID.
|
||||
func (c *Client) GetZone(ctx context.Context, id string) (*ZoneData, error) {
|
||||
resBody := &ZoneResponseBody{}
|
||||
|
||||
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath(id), nil, resBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading SDN zone %s: %w", id, err)
|
||||
}
|
||||
|
||||
if resBody.Data == nil {
|
||||
return nil, api.ErrNoDataObjectInResponse
|
||||
}
|
||||
|
||||
return resBody.Data, nil
|
||||
}
|
||||
|
||||
// GetZones lists all SDN zones.
|
||||
func (c *Client) GetZones(ctx context.Context) ([]ZoneData, error) {
|
||||
resBody := &ZonesResponseBody{}
|
||||
|
||||
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath(""), nil, resBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error listing SDN zones: %w", err)
|
||||
}
|
||||
|
||||
if resBody.Data == nil {
|
||||
return nil, api.ErrNoDataObjectInResponse
|
||||
}
|
||||
|
||||
return *resBody.Data, nil
|
||||
}
|
||||
|
||||
// CreateZone creates a new SDN zone.
|
||||
func (c *Client) CreateZone(ctx context.Context, data *ZoneRequestData) error {
|
||||
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath(""), data, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating SDN zone: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateZone updates an existing SDN zone.
|
||||
func (c *Client) UpdateZone(ctx context.Context, data *ZoneRequestData) error {
|
||||
// PVE API does not allow to pass "type" in PUT requests, this doesn't makes any sense
|
||||
// since other required params like port, server must still be there
|
||||
// while we could spawn another struct, let's just fix it silently
|
||||
data.Type = nil
|
||||
err := c.DoRequest(ctx, http.MethodPut, c.ExpandPath(data.ID), data, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating SDN zone: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteZone deletes an SDN zone by ID.
|
||||
func (c *Client) DeleteZone(ctx context.Context, id string) error {
|
||||
err := c.DoRequest(ctx, http.MethodDelete, c.ExpandPath(id), nil, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting SDN zone: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
57
proxmox/cluster/sdn/zones/zones_types.go
Normal file
57
proxmox/cluster/sdn/zones/zones_types.go
Normal file
@ -0,0 +1,57 @@
|
||||
package zones
|
||||
|
||||
/*
|
||||
--------------------------------- ZONES ---------------------------------
|
||||
|
||||
This part is related to the first SDN component : Zones
|
||||
Based on docs :
|
||||
https://pve.proxmox.com/pve-docs/chapter-pvesdn.html#pvesdn_config_zone
|
||||
https://pve.proxmox.com/pve-docs/api-viewer/index.html#/cluster/sdn/zones
|
||||
-------------------------------------------------------------------------
|
||||
*/
|
||||
type ZoneData struct {
|
||||
ID string `json:"zone,omitempty" url:"zone,omitempty"`
|
||||
Type *string `json:"type,omitempty" url:"type,omitempty"`
|
||||
IPAM *string `json:"ipam,omitempty" url:"ipam,omitempty"`
|
||||
DNS *string `json:"dns,omitempty" url:"dns,omitempty"`
|
||||
ReverseDNS *string `json:"reversedns,omitempty" url:"reversedns,omitempty"`
|
||||
DNSZone *string `json:"dnszone,omitempty" url:"dnszone,omitempty"`
|
||||
Nodes *string `json:"nodes,omitempty" url:"nodes,omitempty"`
|
||||
MTU *int64 `json:"mtu,omitempty" url:"mtu,omitempty"`
|
||||
|
||||
// VLAN
|
||||
Bridge *string `json:"bridge,omitempty" url:"bridge,omitempty"`
|
||||
|
||||
// QinQ
|
||||
ServiceVLAN *int64 `json:"tag,omitempty" url:"tag,omitempty"`
|
||||
ServiceVLANProtocol *string `json:"vlan-protocol,omitempty" url:"vlan-protocol,omitempty"`
|
||||
|
||||
// VXLAN
|
||||
Peers *string `json:"peers,omitempty" url:"peers,omitempty"`
|
||||
|
||||
// EVPN
|
||||
Controller *string `json:"controller,omitempty" url:"controller,omitempty"`
|
||||
VRFVXLANID *int64 `json:"vrf-vxlan,omitempty" url:"vrf-vxlan,omitempty"`
|
||||
ExitNodes *string `json:"exitnodes,omitempty" url:"exitnodes,omitempty"`
|
||||
PrimaryExitNode *string `json:"exitnodes-primary,omitempty" url:"exitnodes-primary,omitempty"`
|
||||
ExitNodesLocalRouting *int64 `json:"exitnodes-local-routing,omitempty" url:"exitnodes-local-routing,omitempty"`
|
||||
AdvertiseSubnets *int64 `json:"advertise-subnets,omitempty" url:"advertise-subnets,omitempty"`
|
||||
DisableARPNDSuppression *int64 `json:"disable-arp-nd-suppression,omitempty" url:"disable-arp-nd-suppression,omitempty"`
|
||||
RouteTargetImport *string `json:"rt-import,omitempty" url:"rt-import,omitempty"`
|
||||
}
|
||||
|
||||
// ZoneRequestData wraps a ZoneData struct with optional delete instructions.
|
||||
type ZoneRequestData struct {
|
||||
ZoneData
|
||||
Delete []string `url:"delete,omitempty"`
|
||||
}
|
||||
|
||||
// ZoneResponseBody represents the response for a single zone.
|
||||
type ZoneResponseBody struct {
|
||||
Data *ZoneData `json:"data"`
|
||||
}
|
||||
|
||||
// ZonesResponseBody represents the response for a list of zones.
|
||||
type ZonesResponseBody struct {
|
||||
Data *[]ZoneData `json:"data"`
|
||||
}
|
@ -6,6 +6,12 @@
|
||||
|
||||
package ptr
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
||||
)
|
||||
|
||||
// Ptr creates a ptr from a value to use it inline.
|
||||
func Ptr[T any](val T) *T {
|
||||
return &val
|
||||
@ -43,3 +49,19 @@ func UpdateIfChanged[T comparable](dst **T, src *T) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// PtrOrNil safely gets a value of any type from schema.ResourceData.
|
||||
// If the key is missing, returns nil. For strings, also returns nil if empty or whitespace.
|
||||
func PtrOrNil[T any](d *schema.ResourceData, key string) *T {
|
||||
if v, ok := d.GetOk(key); ok {
|
||||
val := v.(T)
|
||||
|
||||
// Special case: skip empty/whitespace-only strings
|
||||
if s, ok := any(val).(string); ok && strings.TrimSpace(s) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &val
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
34
proxmoxtf/resource/cluster/sdn/subnets.go
Normal file
34
proxmoxtf/resource/cluster/sdn/subnets.go
Normal file
@ -0,0 +1,34 @@
|
||||
package sdn
|
||||
|
||||
import "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
||||
|
||||
const (
|
||||
mkSubnetID = "subnet"
|
||||
mkSubnetType = "type"
|
||||
mkSubnetVnet = "vnet"
|
||||
mkSubnetDhcpDnsServer = "DhcpDnsServer"
|
||||
mkSubnetDhcpRange = "DhcpRange"
|
||||
mkSubnetDnsZonePrefix = "DnsZonePrefix"
|
||||
mkSubnetGateway = "gateway"
|
||||
mkSubnetSnat = "snat"
|
||||
mkSubnetDeleteSettings = "deleteSettings"
|
||||
mkSubnetDigest = "digest"
|
||||
)
|
||||
|
||||
func Subnet() *schema.Resource {
|
||||
return &schema.Resource{
|
||||
Schema: map[string]*schema.Schema{
|
||||
mkSubnetID: {
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
ForceNew: true,
|
||||
Description: "Subnet value",
|
||||
},
|
||||
mkSubnetType: {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Description: "Subnet type",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user