| 1 | --[[ |
|---|
| 2 | LuCI - Lua Configuration Interface |
|---|
| 3 | |
|---|
| 4 | Copyright 2008 Steven Barth <steven@midlink.org> |
|---|
| 5 | Copyright 2010-2011 Jo-Philipp Wich <xm@subsignal.org> |
|---|
| 6 | |
|---|
| 7 | Licensed under the Apache License, Version 2.0 (the "License"); |
|---|
| 8 | you may not use this file except in compliance with the License. |
|---|
| 9 | You may obtain a copy of the License at |
|---|
| 10 | |
|---|
| 11 | http://www.apache.org/licenses/LICENSE-2.0 |
|---|
| 12 | |
|---|
| 13 | $Id$ |
|---|
| 14 | ]]-- |
|---|
| 15 | |
|---|
| 16 | m = Map("network", translate("Switch"), translate("The network ports on your router can be combined to several <abbr title=\"Virtual Local Area Network\">VLAN</abbr>s in which computers can communicate directly with each other. <abbr title=\"Virtual Local Area Network\">VLAN</abbr>s are often used to separate different network segments. Often there is by default one Uplink port for a connection to the next greater network like the internet and other ports for a local network.")) |
|---|
| 17 | |
|---|
| 18 | m.uci:foreach("network", "switch", |
|---|
| 19 | function(x) |
|---|
| 20 | local sid = x['.name'] |
|---|
| 21 | local switch_name = x.name or sid |
|---|
| 22 | local has_vlan = nil |
|---|
| 23 | local has_learn = nil |
|---|
| 24 | local has_vlan4k = nil |
|---|
| 25 | local has_jumbo3 = nil |
|---|
| 26 | local min_vid = 0 |
|---|
| 27 | local max_vid = 16 |
|---|
| 28 | local num_vlans = 16 |
|---|
| 29 | local num_ports = 6 |
|---|
| 30 | local cpu_port = 5 |
|---|
| 31 | |
|---|
| 32 | local switch_title |
|---|
| 33 | local enable_vlan4k = false |
|---|
| 34 | |
|---|
| 35 | -- Parse some common switch properties from swconfig help output. |
|---|
| 36 | local swc = io.popen("swconfig dev %q help 2>/dev/null" % switch_name) |
|---|
| 37 | if swc then |
|---|
| 38 | |
|---|
| 39 | local is_port_attr = false |
|---|
| 40 | local is_vlan_attr = false |
|---|
| 41 | |
|---|
| 42 | while true do |
|---|
| 43 | local line = swc:read("*l") |
|---|
| 44 | if not line then break end |
|---|
| 45 | |
|---|
| 46 | if line:match("^%s+%-%-vlan") then |
|---|
| 47 | is_vlan_attr = true |
|---|
| 48 | |
|---|
| 49 | elseif line:match("^%s+%-%-port") then |
|---|
| 50 | is_vlan_attr = false |
|---|
| 51 | is_port_attr = true |
|---|
| 52 | |
|---|
| 53 | elseif line:match("cpu @") then |
|---|
| 54 | switch_title = line:match("^switch%d: %w+%((.-)%)") |
|---|
| 55 | num_ports, cpu_port, num_vlans = |
|---|
| 56 | line:match("ports: (%d+) %(cpu @ (%d+)%), vlans: (%d+)") |
|---|
| 57 | |
|---|
| 58 | num_ports = tonumber(num_ports) or 6 |
|---|
| 59 | num_vlans = tonumber(num_vlans) or 16 |
|---|
| 60 | cpu_port = tonumber(cpu_port) or 5 |
|---|
| 61 | min_vid = 1 |
|---|
| 62 | |
|---|
| 63 | elseif line:match(": pvid") or line:match(": tag") or line:match(": vid") then |
|---|
| 64 | if is_vlan_attr then has_vlan4k = line:match(": (%w+)") end |
|---|
| 65 | |
|---|
| 66 | elseif line:match(": enable_vlan4k") then |
|---|
| 67 | enable_vlan4k = true |
|---|
| 68 | |
|---|
| 69 | elseif line:match(": enable_vlan") then |
|---|
| 70 | has_vlan = "enable_vlan" |
|---|
| 71 | |
|---|
| 72 | elseif line:match(": enable_learning") then |
|---|
| 73 | has_learn = "enable_learning" |
|---|
| 74 | |
|---|
| 75 | elseif line:match(": max_length") then |
|---|
| 76 | has_jumbo3 = "max_length" |
|---|
| 77 | end |
|---|
| 78 | end |
|---|
| 79 | |
|---|
| 80 | swc:close() |
|---|
| 81 | end |
|---|
| 82 | |
|---|
| 83 | |
|---|
| 84 | -- Switch properties |
|---|
| 85 | s = m:section(NamedSection, x['.name'], "switch", |
|---|
| 86 | switch_title and translatef("Switch %q (%s)", switch_name, switch_title) |
|---|
| 87 | or translatef("Switch %q", switch_name)) |
|---|
| 88 | |
|---|
| 89 | s.addremove = false |
|---|
| 90 | |
|---|
| 91 | if has_vlan then |
|---|
| 92 | s:option(Flag, has_vlan, translate("Enable VLAN functionality")) |
|---|
| 93 | end |
|---|
| 94 | |
|---|
| 95 | if enable_vlan4k then |
|---|
| 96 | s:option(Flag, "enable_vlan4k", translate("Enable 4K VLANs")) |
|---|
| 97 | end |
|---|
| 98 | |
|---|
| 99 | if has_learn then |
|---|
| 100 | x = s:option(Flag, has_learn, translate("Enable learning and aging")) |
|---|
| 101 | x.default = x.enabled |
|---|
| 102 | end |
|---|
| 103 | |
|---|
| 104 | if has_jumbo3 then |
|---|
| 105 | x = s:option(Flag, has_jumbo3, translate("Enable Jumbo Frame passthrough")) |
|---|
| 106 | x.enabled = "3" |
|---|
| 107 | x.rmempty = true |
|---|
| 108 | end |
|---|
| 109 | |
|---|
| 110 | |
|---|
| 111 | -- VLAN table |
|---|
| 112 | s = m:section(TypedSection, "switch_vlan", translatef("VLANs on %q", switch_name)) |
|---|
| 113 | s.template = "cbi/tblsection" |
|---|
| 114 | s.addremove = true |
|---|
| 115 | s.anonymous = true |
|---|
| 116 | |
|---|
| 117 | -- Filter by switch |
|---|
| 118 | s.filter = function(self, section) |
|---|
| 119 | local device = m:get(section, "device") |
|---|
| 120 | return (device and device == switch_name) |
|---|
| 121 | end |
|---|
| 122 | |
|---|
| 123 | -- Override cfgsections callback to enforce row ordering by vlan id. |
|---|
| 124 | s.cfgsections = function(self) |
|---|
| 125 | local osections = TypedSection.cfgsections(self) |
|---|
| 126 | local sections = { } |
|---|
| 127 | local section |
|---|
| 128 | |
|---|
| 129 | for _, section in luci.util.spairs( |
|---|
| 130 | osections, |
|---|
| 131 | function(a, b) |
|---|
| 132 | return (tonumber(m:get(osections[a], has_vlan4k or "vlan")) or 9999) |
|---|
| 133 | < (tonumber(m:get(osections[b], has_vlan4k or "vlan")) or 9999) |
|---|
| 134 | end |
|---|
| 135 | ) do |
|---|
| 136 | sections[#sections+1] = section |
|---|
| 137 | end |
|---|
| 138 | |
|---|
| 139 | return sections |
|---|
| 140 | end |
|---|
| 141 | |
|---|
| 142 | -- When creating a new vlan, preset it with the highest found vid + 1. |
|---|
| 143 | s.create = function(self, section, origin) |
|---|
| 144 | -- Filter by switch |
|---|
| 145 | if m:get(origin, "device") ~= switch_name then |
|---|
| 146 | return |
|---|
| 147 | end |
|---|
| 148 | |
|---|
| 149 | local sid = TypedSection.create(self, section) |
|---|
| 150 | |
|---|
| 151 | local max_nr = 0 |
|---|
| 152 | local max_id = 0 |
|---|
| 153 | |
|---|
| 154 | m.uci:foreach("network", "switch_vlan", |
|---|
| 155 | function(s) |
|---|
| 156 | if s.device == switch_name then |
|---|
| 157 | local nr = tonumber(s.vlan) |
|---|
| 158 | local id = has_vlan4k and tonumber(s[has_vlan4k]) |
|---|
| 159 | if nr ~= nil and nr > max_nr then max_nr = nr end |
|---|
| 160 | if id ~= nil and id > max_id then max_id = id end |
|---|
| 161 | end |
|---|
| 162 | end) |
|---|
| 163 | |
|---|
| 164 | m.uci:set("network", sid, "device", switch_name) |
|---|
| 165 | m.uci:set("network", sid, "vlan", max_nr + 1) |
|---|
| 166 | |
|---|
| 167 | if has_vlan4k then |
|---|
| 168 | m.uci:set("network", sid, has_vlan4k, max_id + 1) |
|---|
| 169 | end |
|---|
| 170 | |
|---|
| 171 | return sid |
|---|
| 172 | end |
|---|
| 173 | |
|---|
| 174 | |
|---|
| 175 | local port_opts = { } |
|---|
| 176 | local untagged = { } |
|---|
| 177 | |
|---|
| 178 | -- Parse current tagging state from the "ports" option. |
|---|
| 179 | local portvalue = function(self, section) |
|---|
| 180 | local pt |
|---|
| 181 | for pt in (m:get(section, "ports") or ""):gmatch("%w+") do |
|---|
| 182 | local pc, tu = pt:match("^(%d+)([tu]*)") |
|---|
| 183 | if pc == self.option then return (#tu > 0) and tu or "u" end |
|---|
| 184 | end |
|---|
| 185 | return "" |
|---|
| 186 | end |
|---|
| 187 | |
|---|
| 188 | -- Validate port tagging. Ensure that a port is only untagged once, |
|---|
| 189 | -- bail out if not. |
|---|
| 190 | local portvalidate = function(self, value, section) |
|---|
| 191 | -- ensure that the ports appears untagged only once |
|---|
| 192 | if value == "u" then |
|---|
| 193 | if not untagged[self.option] then |
|---|
| 194 | untagged[self.option] = true |
|---|
| 195 | elseif min_vid > 0 or tonumber(self.option) ~= cpu_port then -- enable multiple untagged cpu ports due to weird broadcom default setup |
|---|
| 196 | return nil, |
|---|
| 197 | translatef("Port %d is untagged in multiple VLANs!", tonumber(self.option) + 1) |
|---|
| 198 | end |
|---|
| 199 | end |
|---|
| 200 | return value |
|---|
| 201 | end |
|---|
| 202 | |
|---|
| 203 | |
|---|
| 204 | local vid = s:option(Value, has_vlan4k or "vlan", "VLAN ID") |
|---|
| 205 | |
|---|
| 206 | vid.rmempty = false |
|---|
| 207 | vid.forcewrite = true |
|---|
| 208 | |
|---|
| 209 | -- Validate user provided VLAN ID, make sure its within the bounds |
|---|
| 210 | -- allowed by the switch. |
|---|
| 211 | vid.validate = function(self, value, section) |
|---|
| 212 | local v = tonumber(value) |
|---|
| 213 | local m = has_vlan4k and 4094 or (num_vlans - 1) |
|---|
| 214 | if v ~= nil and v >= min_vid and v <= m then |
|---|
| 215 | return value |
|---|
| 216 | else |
|---|
| 217 | return nil, |
|---|
| 218 | translatef("Invalid VLAN ID given! Only IDs between %d and %d are allowed.", min_vid, m) |
|---|
| 219 | end |
|---|
| 220 | end |
|---|
| 221 | |
|---|
| 222 | -- When writing the "vid" or "vlan" option, serialize the port states |
|---|
| 223 | -- as well and write them as "ports" option to uci. |
|---|
| 224 | vid.write = function(self, section, value) |
|---|
| 225 | local o |
|---|
| 226 | local p = { } |
|---|
| 227 | |
|---|
| 228 | for _, o in ipairs(port_opts) do |
|---|
| 229 | local v = o:formvalue(section) |
|---|
| 230 | if v == "t" then |
|---|
| 231 | p[#p+1] = o.option .. v |
|---|
| 232 | elseif v == "u" then |
|---|
| 233 | p[#p+1] = o.option |
|---|
| 234 | end |
|---|
| 235 | end |
|---|
| 236 | |
|---|
| 237 | m.uci:set("network", section, "ports", table.concat(p, " ")) |
|---|
| 238 | return Value.write(self, section, value) |
|---|
| 239 | end |
|---|
| 240 | |
|---|
| 241 | -- Fallback to "vlan" option if "vid" option is supported but unset. |
|---|
| 242 | vid.cfgvalue = function(self, section) |
|---|
| 243 | return m:get(section, has_vlan4k or "vlan") |
|---|
| 244 | or m:get(section, "vlan") |
|---|
| 245 | end |
|---|
| 246 | |
|---|
| 247 | -- Build per-port off/untagged/tagged choice lists. |
|---|
| 248 | local pt |
|---|
| 249 | for pt = 0, num_ports - 1 do |
|---|
| 250 | local po = s:option(ListValue, tostring(pt), |
|---|
| 251 | (pt == cpu_port) and translate("CPU") or translatef("Port %d", (pt + 1))) |
|---|
| 252 | |
|---|
| 253 | po:value("", translate("off")) |
|---|
| 254 | po:value("u" % pt, translate("untagged")) |
|---|
| 255 | po:value("t" % pt, translate("tagged")) |
|---|
| 256 | |
|---|
| 257 | po.cfgvalue = portvalue |
|---|
| 258 | po.validate = portvalidate |
|---|
| 259 | po.write = function() end |
|---|
| 260 | |
|---|
| 261 | port_opts[#port_opts+1] = po |
|---|
| 262 | end |
|---|
| 263 | end |
|---|
| 264 | ) |
|---|
| 265 | |
|---|
| 266 | return m |
|---|