Skip to content

Commit 42349d5

Browse files
HiranAdikariclaude
andcommitted
Support multiple VLAN networks per tenant space
- vlan_id is now list(number); each entry creates a harvester_network via for_each so networks can be added/removed independently - Auto-route path (no vyos_endpoint) supports multiple VLANs; VyOS path still requires exactly one VLAN (precondition enforced) - network_namespace_name override lets callers import brownfield namespaces whose names differ from the <project>-net default - vlan_network_names map override lets callers import brownfield harvester_network resources with non-default names - network_name output replaced by network_names map keyed by VLAN ID Closes #67 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 71d351d commit 42349d5

3 files changed

Lines changed: 43 additions & 23 deletions

File tree

modules/management/tenant-space/main.tf

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@ locals {
44
namespace_storage_limit = var.namespace_storage_limit != null ? var.namespace_storage_limit : var.storage_limit
55
namespaces = var.namespaces != null ? (var.create_default_namespace ? distinct(concat([var.project_name], var.namespaces)) : var.namespaces) : (var.create_default_namespace ? [var.project_name] : [])
66

7-
# create_net_ns is true when explicitly requested OR when a vlan_id is set.
7+
# create_net_ns is true when explicitly requested OR when vlan_id has entries.
88
# Keeping backward compat: callers already using vlan_id still get the namespace.
9-
create_net_ns = var.create_network_namespace || var.vlan_id != null
10-
network_namespace = local.create_net_ns ? "${var.project_name}-net" : null
9+
create_net_ns = var.create_network_namespace || (var.vlan_id != null && length(var.vlan_id) > 0)
10+
network_namespace = local.create_net_ns ? coalesce(var.network_namespace_name, "${var.project_name}-net") : null
1111

1212
# VyOS path: compute a deterministic /23 subnet from 10.0.0.0/8 using the VLAN
13-
# index. Only relevant when vyos_endpoint is set; auto-routed environments
14-
# (physical switch / DigiOps-issued VLANs) do not need explicit subnets.
15-
use_vyos = var.vlan_id != null && var.vyos_endpoint != null
16-
tenant_subnet = local.use_vyos ? cidrsubnet("10.0.0.0/8", 15, var.vlan_id - 1000) : null
13+
# index. Only relevant when vyos_endpoint is set and exactly one VLAN is given.
14+
# Auto-routed environments (physical switch / DigiOps-issued VLANs) use multiple
15+
# VLANs with route_mode=auto; no explicit subnets needed.
16+
use_vyos = var.vlan_id != null && length(var.vlan_id) > 0 && var.vyos_endpoint != null
17+
tenant_subnet = local.use_vyos ? cidrsubnet("10.0.0.0/8", 15, var.vlan_id[0] - 1000) : null
1718
tenant_gateway = local.use_vyos ? cidrhost(local.tenant_subnet, 1) : null
1819
}
1920

@@ -54,6 +55,10 @@ resource "rancher2_project" "this" {
5455
])
5556
error_message = "Quota variables (memory_limit, storage_limit, namespace_*_limit) are only applied when cpu_limit is set. Either set cpu_limit or remove the other quota variables."
5657
}
58+
precondition {
59+
condition = !local.use_vyos || length(var.vlan_id) == 1
60+
error_message = "VyOS path requires exactly one VLAN ID. Set vyos_endpoint = null for multi-VLAN auto-route configurations."
61+
}
5762
}
5863
}
5964

@@ -100,21 +105,21 @@ resource "rancher2_namespace" "network" {
100105
# the harvester_network resource to attach VMs to the correct VLAN.
101106

102107
resource "harvester_network" "tenant" {
103-
count = var.vlan_id != null ? 1 : 0
104-
name = "${var.project_name}-vlan${var.vlan_id}"
108+
for_each = var.vlan_id != null ? toset([for id in var.vlan_id : tostring(id)]) : toset([])
109+
name = lookup(var.vlan_network_names, each.value, "${var.project_name}-vlan${each.value}")
105110
namespace = rancher2_namespace.network[0].name
106-
vlan_id = var.vlan_id
111+
vlan_id = tonumber(each.value)
107112
cluster_network_name = var.cluster_network_name
108113

109-
# VyOS path: manual routing with a deterministic /23 from 10.0.0.0/8.
114+
# VyOS path: manual routing with a deterministic /23 from 10.0.0.0/8 (single VLAN only).
110115
# DigiOps / physical-switch path: auto routing — the upstream router
111116
# advertises the gateway; no explicit CIDR or gateway needed here.
112117
route_mode = local.use_vyos ? "manual" : "auto"
113-
route_cidr = local.tenant_subnet
114-
route_gateway = local.tenant_gateway
118+
route_cidr = local.use_vyos ? local.tenant_subnet : null
119+
route_gateway = local.use_vyos ? local.tenant_gateway : null
115120

116121
# When VyOS is configured, wait for the vif/DHCP to be provisioned before
117-
# the network is visible to tenant VMs. count=0 module depends_on is a no-op.
122+
# the network is visible to tenant VMs. for_each with empty set is a no-op.
118123
depends_on = [rancher2_namespace.network, module.vyos_tenant]
119124
}
120125

@@ -124,11 +129,11 @@ resource "harvester_network" "tenant" {
124129
# vif sub-interface, DHCP server, and NAT rule in addition.
125130

126131
module "vyos_tenant" {
127-
count = var.vlan_id != null && var.vyos_endpoint != null ? 1 : 0
132+
count = local.use_vyos ? 1 : 0
128133
source = "../../network/vyos-tenant"
129134

130135
tenant_name = var.project_name
131-
vlan_id = var.vlan_id
136+
vlan_id = var.vlan_id[0]
132137
vyos_endpoint = var.vyos_endpoint
133138
vyos_api_key = var.vyos_api_key
134139
}

modules/management/tenant-space/outputs.tf

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ output "network_namespace_id" {
2323
description = "Rancher namespace ID of the network namespace. Non-null when create_network_namespace = true or vlan_id is set."
2424
}
2525

26-
output "network_name" {
27-
value = var.vlan_id != null ? "${harvester_network.tenant[0].namespace}/${harvester_network.tenant[0].name}" : null
28-
description = "Full harvester_network reference (<namespace>/<name>) for attaching tenant VMs. Null when vlan_id is not set."
26+
output "network_names" {
27+
value = { for id, r in harvester_network.tenant : id => "${r.namespace}/${r.name}" }
28+
description = "Map of VLAN ID (string) → full harvester_network reference (<namespace>/<name>) for attaching tenant VMs. Empty map when vlan_id is null or empty."
2929
}
3030

3131
output "subnet_cidr" {

modules/management/tenant-space/variables.tf

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,15 +110,30 @@ variable "create_network_namespace" {
110110
}
111111

112112
variable "vlan_id" {
113-
type = number
114-
description = "VLAN ID for this tenant's network (>= 1000). When set, always creates the network namespace and a harvester_network. Routing mode depends on vyos_endpoint: if set, route_mode=manual with a deterministic /23 from 10.0.0.0/8 plus full VyOS vif/DHCP/NAT config; if null, route_mode=auto (upstream router / DigiOps-issued VLAN handles routing). When vlan_id is null, no network resources are created."
113+
type = list(number)
114+
description = "List of VLAN IDs for this tenant's networks. Each entry creates a harvester_network in the network namespace. When non-empty, the network namespace is always created. VyOS path (vyos_endpoint set) requires exactly one VLAN ID — a deterministic /23 from 10.0.0.0/8 is computed and full VyOS vif/DHCP/NAT config is provisioned. Auto-route path (vyos_endpoint null) supports multiple VLANs — the upstream router handles routing. When null or empty, no network resources are created."
115115
default = null
116116
validation {
117-
condition = var.vlan_id == null || (var.vlan_id >= 1 && var.vlan_id <= 4094)
118-
error_message = "vlan_id must be a valid 802.1Q VLAN ID (1–4094)."
117+
condition = var.vlan_id == null || (
118+
length(var.vlan_id) > 0 &&
119+
alltrue([for id in var.vlan_id : id >= 1 && id <= 4094])
120+
)
121+
error_message = "vlan_id must be null or a non-empty list of valid 802.1Q VLAN IDs (1–4094)."
119122
}
120123
}
121124

125+
variable "network_namespace_name" {
126+
type = string
127+
description = "Override the name of the network namespace. Defaults to <project_name>-net. Use this when importing a brownfield namespace whose name differs from the default."
128+
default = null
129+
}
130+
131+
variable "vlan_network_names" {
132+
type = map(string)
133+
description = "Map of VLAN ID (as string) to harvester_network resource name override. Use when importing brownfield networks whose names differ from the default <project_name>-vlan<id> pattern. Example: { \"608\" = \"vm-subnet-008\" }."
134+
default = {}
135+
}
136+
122137
variable "cluster_network_name" {
123138
type = string
124139
description = "Harvester cluster network carrying tenant VLANs. Defaults to 'vm-network' — override only if your datacenter uses a different cluster network name."

0 commit comments

Comments
 (0)