]> git.openstreetmap.org Git - chef.git/commitdiff
Add support for using systemd-networkd directly instead of netplan
authorTom Hughes <tom@compton.nu>
Sun, 19 Mar 2023 17:48:43 +0000 (17:48 +0000)
committerTom Hughes <tom@compton.nu>
Mon, 20 Mar 2023 18:19:32 +0000 (18:19 +0000)
cookbooks/networking/attributes/default.rb
cookbooks/networking/recipes/default.rb
cookbooks/networking/templates/default/bond.netdev.erb [new file with mode: 0644]
cookbooks/networking/templates/default/network.erb [new file with mode: 0644]
cookbooks/networking/templates/default/slave.network.erb [new file with mode: 0644]
cookbooks/networking/templates/default/vlan.netdev.erb [new file with mode: 0644]

index 9832ce8f3f8c7527e92760fe76792642258043f8..f143d59f9cffb042cfd476fab514cde9b0fe5a46 100644 (file)
@@ -1,3 +1,4 @@
+default[:networking][:engine] = "netplan"
 default[:networking][:firewall][:enabled] = true
 default[:networking][:firewall][:sets] = []
 default[:networking][:firewall][:helpers] = []
 default[:networking][:firewall][:enabled] = true
 default[:networking][:firewall][:sets] = []
 default[:networking][:firewall][:helpers] = []
index 540d858fcfe0eafdb70c7d8361333528d13328b2..7731511c0f7d19d077d8537a6d8b8b2cd0b4f5b1 100644 (file)
@@ -25,189 +25,306 @@ require "yaml"
 
 keys = data_bag_item("networking", "keys")
 
 
 keys = data_bag_item("networking", "keys")
 
-package "netplan.io"
-
-netplan = {
-  "network" => {
-    "version" => 2,
-    "renderer" => "networkd",
-    "ethernets" => {},
-    "bonds" => {},
-    "vlans" => {}
+if node[:networking][:engine] == "netplan"
+  package "netplan.io"
+
+  netplan = {
+    "network" => {
+      "version" => 2,
+      "renderer" => "networkd",
+      "ethernets" => {},
+      "bonds" => {},
+      "vlans" => {}
+    }
   }
   }
-}
-
-node[:networking][:interfaces].each do |name, interface|
-  if interface[:interface]
-    if interface[:role] && (role = node[:networking][:roles][interface[:role]])
-      if interface[:inet] && role[:inet]
-        node.default[:networking][:interfaces][name][:inet][:prefix] = role[:inet][:prefix]
-        node.default[:networking][:interfaces][name][:inet][:gateway] = role[:inet][:gateway]
-        node.default[:networking][:interfaces][name][:inet][:routes] = role[:inet][:routes]
-      end
 
 
-      if interface[:inet6] && role[:inet6]
-        node.default[:networking][:interfaces][name][:inet6][:prefix] = role[:inet6][:prefix]
-        node.default[:networking][:interfaces][name][:inet6][:gateway] = role[:inet6][:gateway]
-        node.default[:networking][:interfaces][name][:inet6][:routes] = role[:inet6][:routes]
-      end
+  node[:networking][:interfaces].each do |name, interface|
+    if interface[:interface]
+      if interface[:role] && (role = node[:networking][:roles][interface[:role]])
+        if interface[:inet] && role[:inet]
+          node.default[:networking][:interfaces][name][:inet][:prefix] = role[:inet][:prefix]
+          node.default[:networking][:interfaces][name][:inet][:gateway] = role[:inet][:gateway]
+          node.default[:networking][:interfaces][name][:inet][:routes] = role[:inet][:routes]
+        end
 
 
-      node.default[:networking][:interfaces][name][:metric] = role[:metric]
-      node.default[:networking][:interfaces][name][:zone] = role[:zone]
-    end
+        if interface[:inet6] && role[:inet6]
+          node.default[:networking][:interfaces][name][:inet6][:prefix] = role[:inet6][:prefix]
+          node.default[:networking][:interfaces][name][:inet6][:gateway] = role[:inet6][:gateway]
+          node.default[:networking][:interfaces][name][:inet6][:routes] = role[:inet6][:routes]
+        end
 
 
-    interface = node[:networking][:interfaces][name]
-
-    deviceplan = if interface[:interface] =~ /^(.*)\.(\d+)$/
-                   netplan["network"]["vlans"][interface[:interface]] ||= {
-                     "id" => Regexp.last_match(2).to_i,
-                     "link" => Regexp.last_match(1),
-                     "accept-ra" => false,
-                     "addresses" => [],
-                     "routes" => []
-                   }
-                 elsif interface[:interface] =~ /^bond\d+$/
-                   netplan["network"]["bonds"][interface[:interface]] ||= {
-                     "accept-ra" => false,
-                     "addresses" => [],
-                     "routes" => []
-                   }
-                 else
-                   netplan["network"]["ethernets"][interface[:interface]] ||= {
-                     "accept-ra" => false,
-                     "addresses" => [],
-                     "routes" => []
-                   }
-                 end
-
-    if interface[:inet]
-      deviceplan["addresses"].push("#{interface[:inet][:address]}/#{interface[:inet][:prefix]}")
-    end
+        node.default[:networking][:interfaces][name][:metric] = role[:metric]
+        node.default[:networking][:interfaces][name][:zone] = role[:zone]
+      end
 
 
-    if interface[:inet6]
-      deviceplan["addresses"].push("#{interface[:inet6][:address]}/#{interface[:inet6][:prefix]}")
-    end
+      interface = node[:networking][:interfaces][name]
+
+      deviceplan = if interface[:interface] =~ /^(.*)\.(\d+)$/
+                     netplan["network"]["vlans"][interface[:interface]] ||= {
+                       "id" => Regexp.last_match(2).to_i,
+                       "link" => Regexp.last_match(1),
+                       "accept-ra" => false,
+                       "addresses" => [],
+                       "routes" => []
+                     }
+                   elsif interface[:interface] =~ /^bond\d+$/
+                     netplan["network"]["bonds"][interface[:interface]] ||= {
+                       "accept-ra" => false,
+                       "addresses" => [],
+                       "routes" => []
+                     }
+                   else
+                     netplan["network"]["ethernets"][interface[:interface]] ||= {
+                       "accept-ra" => false,
+                       "addresses" => [],
+                       "routes" => []
+                     }
+                   end
+
+      if interface[:inet]
+        deviceplan["addresses"].push("#{interface[:inet][:address]}/#{interface[:inet][:prefix]}")
+      end
 
 
-    if interface[:mtu]
-      deviceplan["mtu"] = interface[:mtu]
-    end
+      if interface[:inet6]
+        deviceplan["addresses"].push("#{interface[:inet6][:address]}/#{interface[:inet6][:prefix]}")
+      end
 
 
-    if interface[:bond]
-      deviceplan["interfaces"] = interface[:bond][:slaves].to_a
+      if interface[:mtu]
+        deviceplan["mtu"] = interface[:mtu]
+      end
 
 
-      deviceplan["parameters"] = {
-        "mode" => interface[:bond][:mode] || "active-backup",
-        "mii-monitor-interval" => interface[:bond][:miimon] || 100,
-        "down-delay" => interface[:bond][:downdelay] || 200,
-        "up-delay" => interface[:bond][:updelay] || 200
-      }
+      if interface[:bond]
+        deviceplan["interfaces"] = interface[:bond][:slaves].to_a
 
 
-      deviceplan["parameters"]["primary"] = interface[:bond][:slaves].first if deviceplan["parameters"]["mode"] == "active-backup"
-      deviceplan["parameters"]["transmit-hash-policy"] = interface[:bond][:xmithashpolicy] if interface[:bond][:xmithashpolicy]
-      deviceplan["parameters"]["lacp-rate"] = interface[:bond][:lacprate] if interface[:bond][:lacprate]
-    end
+        deviceplan["parameters"] = {
+          "mode" => interface[:bond][:mode] || "active-backup",
+          "mii-monitor-interval" => interface[:bond][:miimon] || 100,
+          "down-delay" => interface[:bond][:downdelay] || 200,
+          "up-delay" => interface[:bond][:updelay] || 200
+        }
 
 
-    if interface[:inet]
-      if interface[:inet][:gateway] && interface[:inet][:gateway] != interface[:inet][:address]
-        deviceplan["routes"].push(
-          "to" => "0.0.0.0/0",
-          "via" => interface[:inet][:gateway],
-          "metric" => interface[:metric],
-          "on-link" => true
-        )
+        deviceplan["parameters"]["primary"] = interface[:bond][:slaves].first if deviceplan["parameters"]["mode"] == "active-backup"
+        deviceplan["parameters"]["transmit-hash-policy"] = interface[:bond][:xmithashpolicy] if interface[:bond][:xmithashpolicy]
+        deviceplan["parameters"]["lacp-rate"] = interface[:bond][:lacprate] if interface[:bond][:lacprate]
       end
 
       end
 
-      if interface[:inet][:routes]
-        interface[:inet][:routes].each do |to, parameters|
-          next if parameters[:via] == interface[:inet][:address]
+      if interface[:inet]
+        if interface[:inet][:gateway] && interface[:inet][:gateway] != interface[:inet][:address]
+          deviceplan["routes"].push(
+            "to" => "0.0.0.0/0",
+            "via" => interface[:inet][:gateway],
+            "metric" => interface[:metric],
+            "on-link" => true
+          )
+        end
 
 
-          route = {
-            "to" => to
-          }
+        if interface[:inet][:routes]
+          interface[:inet][:routes].each do |to, parameters|
+            next if parameters[:via] == interface[:inet][:address]
 
 
-          route["type"] = parameters[:type] if parameters[:type]
-          route["via"] = parameters[:via] if parameters[:via]
-          route["metric"] = parameters[:metric] if parameters[:metric]
+            route = {
+              "to" => to
+            }
 
 
-          deviceplan["routes"].push(route)
+            route["type"] = parameters[:type] if parameters[:type]
+            route["via"] = parameters[:via] if parameters[:via]
+            route["metric"] = parameters[:metric] if parameters[:metric]
+
+            deviceplan["routes"].push(route)
+          end
         end
       end
         end
       end
-    end
 
 
-    if interface[:inet6]
-      if interface[:inet6][:gateway] && interface[:inet6][:gateway] != interface[:inet6][:address]
-        deviceplan["routes"].push(
-          "to" => "::/0",
-          "via" => interface[:inet6][:gateway],
-          "metric" => interface[:metric],
-          "on-link" => true
-        )
-
-        # This ordering relies on systemd-networkd adding routes
-        # in reverse order and will need moving before the previous
-        # route once that is fixed:
-        #
-        # https://github.com/systemd/systemd/issues/5430
-        # https://github.com/systemd/systemd/pull/10938
-        if !IPAddr.new(interface[:inet6][:address]).mask(interface[:inet6][:prefix]).include?(interface[:inet6][:gateway]) &&
-           !IPAddr.new("fe80::/64").include?(interface[:inet6][:gateway])
+      if interface[:inet6]
+        if interface[:inet6][:gateway] && interface[:inet6][:gateway] != interface[:inet6][:address]
           deviceplan["routes"].push(
           deviceplan["routes"].push(
-            "to" => interface[:inet6][:gateway],
-            "scope" => "link"
+            "to" => "::/0",
+            "via" => interface[:inet6][:gateway],
+            "metric" => interface[:metric],
+            "on-link" => true
           )
           )
+
+          # This ordering relies on systemd-networkd adding routes
+          # in reverse order and will need moving before the previous
+          # route once that is fixed:
+          #
+          # https://github.com/systemd/systemd/issues/5430
+          # https://github.com/systemd/systemd/pull/10938
+          if !IPAddr.new(interface[:inet6][:address]).mask(interface[:inet6][:prefix]).include?(interface[:inet6][:gateway]) &&
+             !IPAddr.new("fe80::/64").include?(interface[:inet6][:gateway])
+            deviceplan["routes"].push(
+              "to" => interface[:inet6][:gateway],
+              "scope" => "link"
+            )
+          end
         end
         end
-      end
 
 
-      if interface[:inet6][:routes]
-        interface[:inet6][:routes].each do |to, parameters|
-          next if parameters[:via] == interface[:inet6][:address]
+        if interface[:inet6][:routes]
+          interface[:inet6][:routes].each do |to, parameters|
+            next if parameters[:via] == interface[:inet6][:address]
 
 
-          route = {
-            "to" => to
-          }
+            route = {
+              "to" => to
+            }
 
 
-          route["type"] = parameters[:type] if parameters[:type]
-          route["via"] = parameters[:via] if parameters[:via]
-          route["metric"] = parameters[:metric] if parameters[:metric]
+            route["type"] = parameters[:type] if parameters[:type]
+            route["via"] = parameters[:via] if parameters[:via]
+            route["metric"] = parameters[:metric] if parameters[:metric]
 
 
-          deviceplan["routes"].push(route)
+            deviceplan["routes"].push(route)
+          end
         end
       end
         end
       end
+    else
+      node.rm(:networking, :interfaces, name)
     end
     end
-  else
-    node.rm(:networking, :interfaces, name)
   end
   end
-end
 
 
-netplan["network"]["bonds"].each_value do |bond|
-  bond["interfaces"].each do |interface|
-    netplan["network"]["ethernets"][interface] ||= { "accept-ra" => false, "optional" => true }
+  netplan["network"]["bonds"].each_value do |bond|
+    bond["interfaces"].each do |interface|
+      netplan["network"]["ethernets"][interface] ||= { "accept-ra" => false, "optional" => true }
+    end
   end
   end
-end
 
 
-netplan["network"]["vlans"].each_value do |vlan|
-  unless vlan["link"] =~ /^bond\d+$/
-    netplan["network"]["ethernets"][vlan["link"]] ||= { "accept-ra" => false }
+  netplan["network"]["vlans"].each_value do |vlan|
+    unless vlan["link"] =~ /^bond\d+$/
+      netplan["network"]["ethernets"][vlan["link"]] ||= { "accept-ra" => false }
+    end
   end
   end
-end
 
 
-file "/etc/netplan/00-installer-config.yaml" do
-  action :delete
-end
+  file "/etc/netplan/00-installer-config.yaml" do
+    action :delete
+  end
 
 
-file "/etc/netplan/01-netcfg.yaml" do
-  action :delete
-end
+  file "/etc/netplan/01-netcfg.yaml" do
+    action :delete
+  end
 
 
-file "/etc/netplan/50-cloud-init.yaml" do
-  action :delete
-end
+  file "/etc/netplan/50-cloud-init.yaml" do
+    action :delete
+  end
 
 
-file "/etc/netplan/99-chef.yaml" do
-  owner "root"
-  group "root"
-  mode "644"
-  content YAML.dump(netplan)
+  file "/etc/netplan/99-chef.yaml" do
+    owner "root"
+    group "root"
+    mode "644"
+    content YAML.dump(netplan)
+  end
+elsif node[:networking][:engine] == "systemd-networkd"
+  file "/etc/netplan/99-chef.yaml" do
+    action :delete
+  end
+
+  package "netplan.io" do
+    action :purge
+  end
+
+  interfaces = node[:networking][:interfaces].collect do |name, interface|
+    [interface[:interface], name]
+  end.to_h
+
+  node[:networking][:interfaces].each do |name, interface|
+    if interface[:interface] =~ /^(.*)\.(\d+)$/
+      vlan_interface = Regexp.last_match(1)
+      vlan_id = Regexp.last_match(2)
+
+      parent = interfaces[vlan_interface] || "vlans_#{vlan_interface}"
+
+      node.default_unless[:networking][:interfaces][parent][:interface] = vlan_interface,
+      node.default_unless[:networking][:interfaces][parent][:vlans] = []
+
+      node.default[:networking][:interfaces][parent][:vlans] << vlan_id
+    end
+
+    next unless interface[:role] && (role = node[:networking][:roles][interface[:role]])
+
+    if interface[:inet] && role[:inet]
+      node.default[:networking][:interfaces][name][:inet][:prefix] = role[:inet][:prefix]
+      node.default[:networking][:interfaces][name][:inet][:gateway] = role[:inet][:gateway]
+      node.default[:networking][:interfaces][name][:inet][:routes] = role[:inet][:routes]
+    end
+
+    if interface[:inet6] && role[:inet6]
+      node.default[:networking][:interfaces][name][:inet6][:prefix] = role[:inet6][:prefix]
+      node.default[:networking][:interfaces][name][:inet6][:gateway] = role[:inet6][:gateway]
+      node.default[:networking][:interfaces][name][:inet6][:routes] = role[:inet6][:routes]
+    end
+
+    node.default[:networking][:interfaces][name][:metric] = role[:metric]
+    node.default[:networking][:interfaces][name][:zone] = role[:zone]
+  end
+
+  node[:networking][:interfaces].each do |_, interface|
+    file "/run/systemd/network/10-netplan-#{interface[:interface]}.netdev" do
+      action :delete
+    end
+
+    if interface[:interface] =~ /^.*\.(\d+)$/
+      template "/etc/systemd/network/10-#{interface[:interface]}.netdev" do
+        source "vlan.netdev.erb"
+        owner "root"
+        group "root"
+        mode "644"
+        variables :interface => interface, :vlan => Regexp.last_match(1)
+        notifies :run, "execute[networkctl-delete-#{interface[:interface]}]"
+        notifies :run, "notify_group[networkctl-reload]"
+      end
+
+      execute "networkctl-delete-#{interface[:interface]}" do
+        action :nothing
+        command "networkctl delete #{interface[:interface]}"
+        only_if { ::File.exist?("/sys/class/net/#{interface[:interface]}") }
+      end
+    elsif interface[:interface] =~ /^bond\d+$/
+      template "/etc/systemd/network/10-#{interface[:interface]}.netdev" do
+        source "bond.netdev.erb"
+        owner "root"
+        group "root"
+        mode "644"
+        variables :interface => interface
+        notifies :run, "execute[networkctl-delete-#{interface[:interface]}]"
+        notifies :run, "notify_group[networkctl-reload]"
+      end
+
+      execute "networkctl-delete-#{interface[:interface]}" do
+        action :nothing
+        command "networkctl delete #{interface[:interface]}"
+        only_if { ::File.exist?("/sys/class/net/#{interface[:interface]}") }
+      end
+
+      interface[:bond][:slaves].each do |slave|
+        file "/run/systemd/network/10-netplan-#{slave}.network" do
+          action :delete
+        end
+
+        template "/etc/systemd/network/10-#{slave}.network" do
+          source "slave.network.erb"
+          owner "root"
+          group "root"
+          mode "644"
+          variables :master => interface, :slave => slave
+          notifies :run, "notify_group[networkctl-reload]"
+        end
+      end
+    end
+
+    file "/run/systemd/network/10-netplan-#{interface[:interface]}.network" do
+      action :delete
+    end
+
+    template "/etc/systemd/network/10-#{interface[:interface]}.network" do
+      source "network.erb"
+      owner "root"
+      group "root"
+      mode "644"
+      variables :interface => interface
+      notifies :run, "notify_group[networkctl-reload]"
+    end
+  end
+
+  service "systemd-networkd" do
+    action [:enable, :start]
+  end
 end
 
 package "cloud-init" do
 end
 
 package "cloud-init" do
@@ -319,7 +436,7 @@ if node[:networking][:wireguard][:enabled]
     group "systemd-network"
     mode "640"
     notifies :run, "execute[networkctl-delete-wg0]"
     group "systemd-network"
     mode "640"
     notifies :run, "execute[networkctl-delete-wg0]"
-    notifies :run, "execute[networkctl-reload]"
+    notifies :run, "notify_group[networkctl-reload]"
   end
 
   file "/etc/systemd/network/wireguard.network" do
   end
 
   file "/etc/systemd/network/wireguard.network" do
@@ -339,12 +456,15 @@ if node[:networking][:wireguard][:enabled]
     command "networkctl delete wg0"
     only_if { ::File.exist?("/sys/class/net/wg0") }
   end
     command "networkctl delete wg0"
     only_if { ::File.exist?("/sys/class/net/wg0") }
   end
+end
 
 
-  execute "networkctl-reload" do
-    action :nothing
-    command "networkctl reload"
-    not_if { kitchen? }
-  end
+notify_group "networkctl-reload"
+
+execute "networkctl-reload" do
+  action :nothing
+  command "networkctl reload"
+  subscribes :run, "notify_group[networkctl-reload]"
+  not_if { kitchen? && node[:networking][:engine] == "netplan" }
 end
 
 ohai "reload-hostname" do
 end
 
 ohai "reload-hostname" do
diff --git a/cookbooks/networking/templates/default/bond.netdev.erb b/cookbooks/networking/templates/default/bond.netdev.erb
new file mode 100644 (file)
index 0000000..971d127
--- /dev/null
@@ -0,0 +1,15 @@
+[NetDev]
+Name=<%= @interface[:interface] %>
+Kind=bond
+
+[Bond]
+Mode=<%= @interface[:bond][:mode] || "active-backup" %>
+<% if @interface[:bond][:xmithashpolicy] -%>
+TransmitHashPolicy=<%= @interface[:bond][:xmithashpolicy] %>
+<% end -%>
+<% if @interface[:bond][:lacprate] -%>
+LACPTransmitRate=<%= @interface[:bond][:lacprate] %>
+<% end -%>
+MIIMonitorSec=<%= @interface[:bond][:miimon] || "100ms" %>
+UpDelaySec=<%= @interface[:bond][:updelay] || "200ms" %>
+DownDelaySec=<%= @interface[:bond][:downdelay] || "200ms" %>
diff --git a/cookbooks/networking/templates/default/network.erb b/cookbooks/networking/templates/default/network.erb
new file mode 100644 (file)
index 0000000..e937964
--- /dev/null
@@ -0,0 +1,60 @@
+[Match]
+Name=<%= @interface[:interface] %>
+
+[Network]
+<% if @interface[:inet] -%>
+Address=<%= @interface[:inet][:address] %>/<%== @interface[:inet][:prefix] %>
+<% end -%>
+<% if @interface[:inet6] -%>
+Address=<%= @interface[:inet6][:address] %>/<%== @interface[:inet6][:prefix] %>
+<% end -%>
+IPv6AcceptRA=no
+<% Array(@interface[:vlans]).sort.uniq.each do |vlan| -%>
+VLAN=<%= @interface[:interface] %>.<%= vlan %>
+<% end -%>
+<% if @interface.dig(:inet, :gateway) && @interface[:inet][:gateway] != @interface[:inet][:address] -%>
+
+[Route]
+Gateway=<%= @interface[:inet][:gateway] %>
+GatewayOnLink=true
+<% if @interface[:metric] -%>
+Metric=<%= @interface[:metric] %>
+<% end -%>
+<% end -%>
+<% if @interface.dig(:inet6, :gateway) && @interface[:inet6][:gateway] != @interface[:inet6][:address] -%>
+
+[Route]
+Gateway=<%= @interface[:inet6][:gateway] %>
+GatewayOnLink=true
+<% if @interface[:metric] -%>
+Metric=<%= @interface[:metric] %>
+<% end -%>
+<% end -%>
+<% Hash(@interface.dig(:inet, :routes)).sort.each do |destination, details| -%>
+
+[Route]
+<% if details[:gateway] -%>
+Gateway=<%= details[:gateway] %>
+<% end -%>
+Destination=<%= destination %>
+<% if details[:metric] -%>
+Metric=<%= details[:metric] %>
+<% end -%>
+<% if details[:type] -%>
+Type=<%= details[:type] %>
+<% end -%>
+<% end -%>
+<% Hash(@interface.dig(:inet6, :routes)).sort.each do |destination, details| -%>
+
+[Route]
+<% if details[:gateway] -%>
+Gateway=<%= details[:gateway] %>
+<% end -%>
+Destination=<%= destination %>
+<% if details[:metric] -%>
+Metric=<%= details[:metric] %>
+<% end -%>
+<% if details[:type] -%>
+Type=<%= details[:type] %>
+<% end -%>
+<% end -%>
diff --git a/cookbooks/networking/templates/default/slave.network.erb b/cookbooks/networking/templates/default/slave.network.erb
new file mode 100644 (file)
index 0000000..609f38f
--- /dev/null
@@ -0,0 +1,15 @@
+[Match]
+Name=<%= @slave %>
+
+[Link]
+RequiredForOnline=no
+
+[Network]
+LinkLocalAddressing=no
+IPv6AcceptRA=no
+Bond=<%= @master[:interface] %>
+<% if @master[:bond][:mode].nil? || @master[:bond][:mode] == "active-backup" -%>
+<% if @master[:bond][:slaves].first == @slave -%>
+PrimarySlave=true
+<% end -%>
+<% end -%>
diff --git a/cookbooks/networking/templates/default/vlan.netdev.erb b/cookbooks/networking/templates/default/vlan.netdev.erb
new file mode 100644 (file)
index 0000000..8446f35
--- /dev/null
@@ -0,0 +1,6 @@
+[NetDev]
+Name=<%= @interface[:interface] %>
+Kind=vlan
+
+[VLAN]
+Id=<%= @vlan %>