fbea03af98eabfdee1149ed3354bcb66c73c645e
[project/luci.git] / modules / admin-full / luasrc / model / cbi / admin_network / vlan.lua
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 ]]--
14
15 m = Map("network", translate("Switch"), translate("The network ports on this device 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."))
16
17 local fs = require "nixio.fs"
18 local switches = { }
19
20 m.uci:foreach("network", "switch",
21 function(x)
22 local sid = x['.name']
23 local switch_name = x.name or sid
24 local has_vlan = nil
25 local has_learn = nil
26 local has_vlan4k = nil
27 local has_jumbo3 = nil
28 local has_mirror = nil
29 local min_vid = 0
30 local max_vid = 16
31 local num_vlans = 16
32 local cpu_port = tonumber(fs.readfile("/proc/switch/eth0/cpuport") or 5)
33 local num_ports = cpu_port + 1
34
35 local switch_title
36 local enable_vlan4k = false
37
38 -- Parse some common switch properties from swconfig help output.
39 local swc = io.popen("swconfig dev %q help 2>/dev/null" % switch_name)
40 if swc then
41
42 local is_port_attr = false
43 local is_vlan_attr = false
44
45 while true do
46 local line = swc:read("*l")
47 if not line then break end
48
49 if line:match("^%s+%-%-vlan") then
50 is_vlan_attr = true
51
52 elseif line:match("^%s+%-%-port") then
53 is_vlan_attr = false
54 is_port_attr = true
55
56 elseif line:match("cpu @") then
57 switch_title = line:match("^switch%d: %w+%((.-)%)")
58 num_ports, cpu_port, num_vlans =
59 line:match("ports: (%d+) %(cpu @ (%d+)%), vlans: (%d+)")
60
61 num_ports = tonumber(num_ports) or 6
62 num_vlans = tonumber(num_vlans) or 16
63 cpu_port = tonumber(cpu_port) or 5
64 min_vid = 1
65
66 elseif line:match(": pvid") or line:match(": tag") or line:match(": vid") then
67 if is_vlan_attr then has_vlan4k = line:match(": (%w+)") end
68
69 elseif line:match(": enable_vlan4k") then
70 enable_vlan4k = true
71
72 elseif line:match(": enable_vlan") then
73 has_vlan = "enable_vlan"
74
75 elseif line:match(": enable_learning") then
76 has_learn = "enable_learning"
77
78 elseif line:match(": enable_mirror_rx") then
79 has_mirror = "enable_mirror_rx"
80
81 elseif line:match(": max_length") then
82 has_jumbo3 = "max_length"
83 end
84 end
85
86 swc:close()
87 end
88
89
90 -- Switch properties
91 s = m:section(NamedSection, x['.name'], "switch",
92 switch_title and translatef("Switch %q (%s)", switch_name, switch_title)
93 or translatef("Switch %q", switch_name))
94
95 s.addremove = false
96
97 if has_vlan then
98 s:option(Flag, has_vlan, translate("Enable VLAN functionality"))
99 end
100
101 if has_learn then
102 x = s:option(Flag, has_learn, translate("Enable learning and aging"))
103 x.default = x.enabled
104 end
105
106 if has_jumbo3 then
107 x = s:option(Flag, has_jumbo3, translate("Enable Jumbo Frame passthrough"))
108 x.enabled = "3"
109 x.rmempty = true
110 end
111
112 -- Does this switch support port mirroring?
113 if has_mirror then
114 s:option(Flag, "enable_mirror_rx", translate("Enable mirroring of incoming packets"))
115 s:option(Flag, "enable_mirror_tx", translate("Enable mirroring of outgoing packets"))
116
117 local sp = s:option(ListValue, "mirror_source_port", translate("Mirror source port"))
118 local mp = s:option(ListValue, "mirror_monitor_port", translate("Mirror monitor port"))
119
120 local pt
121 for pt = 0, num_ports - 1 do
122 local name
123
124 name = (pt == cpu_port) and translate("CPU") or translatef("Port %d", pt)
125
126 sp:value(pt, name)
127 mp:value(pt, name)
128 end
129 end
130
131 -- VLAN table
132 s = m:section(TypedSection, "switch_vlan",
133 switch_title and translatef("VLANs on %q (%s)", switch_name, switch_title)
134 or translatef("VLANs on %q", switch_name))
135
136 s.template = "cbi/tblsection"
137 s.addremove = true
138 s.anonymous = true
139
140 -- Filter by switch
141 s.filter = function(self, section)
142 local device = m:get(section, "device")
143 return (device and device == switch_name)
144 end
145
146 -- Override cfgsections callback to enforce row ordering by vlan id.
147 s.cfgsections = function(self)
148 local osections = TypedSection.cfgsections(self)
149 local sections = { }
150 local section
151
152 for _, section in luci.util.spairs(
153 osections,
154 function(a, b)
155 return (tonumber(m:get(osections[a], has_vlan4k or "vlan")) or 9999)
156 < (tonumber(m:get(osections[b], has_vlan4k or "vlan")) or 9999)
157 end
158 ) do
159 sections[#sections+1] = section
160 end
161
162 return sections
163 end
164
165 -- When creating a new vlan, preset it with the highest found vid + 1.
166 s.create = function(self, section, origin)
167 -- Filter by switch
168 if m:get(origin, "device") ~= switch_name then
169 return
170 end
171
172 local sid = TypedSection.create(self, section)
173
174 local max_nr = 0
175 local max_id = 0
176
177 m.uci:foreach("network", "switch_vlan",
178 function(s)
179 if s.device == switch_name then
180 local nr = tonumber(s.vlan)
181 local id = has_vlan4k and tonumber(s[has_vlan4k])
182 if nr ~= nil and nr > max_nr then max_nr = nr end
183 if id ~= nil and id > max_id then max_id = id end
184 end
185 end)
186
187 m:set(sid, "device", switch_name)
188 m:set(sid, "vlan", max_nr + 1)
189
190 if has_vlan4k then
191 m:set(sid, has_vlan4k, max_id + 1)
192 end
193
194 return sid
195 end
196
197
198 local port_opts = { }
199 local untagged = { }
200
201 -- Parse current tagging state from the "ports" option.
202 local portvalue = function(self, section)
203 local pt
204 for pt in (m:get(section, "ports") or ""):gmatch("%w+") do
205 local pc, tu = pt:match("^(%d+)([tu]*)")
206 if pc == self.option then return (#tu > 0) and tu or "u" end
207 end
208 return ""
209 end
210
211 -- Validate port tagging. Ensure that a port is only untagged once,
212 -- bail out if not.
213 local portvalidate = function(self, value, section)
214 -- ensure that the ports appears untagged only once
215 if value == "u" then
216 if not untagged[self.option] then
217 untagged[self.option] = true
218 elseif min_vid > 0 or tonumber(self.option) ~= cpu_port then -- enable multiple untagged cpu ports due to weird broadcom default setup
219 return nil,
220 translatef("Port %d is untagged in multiple VLANs!", tonumber(self.option) + 1)
221 end
222 end
223 return value
224 end
225
226
227 local vid = s:option(Value, has_vlan4k or "vlan", "VLAN ID", "<div id='portstatus-%s'></div>" % switch_name)
228 local mx_vid = has_vlan4k and 4094 or (num_vlans - 1)
229
230 vid.rmempty = false
231 vid.forcewrite = true
232 vid.vlan_used = { }
233 vid.datatype = "and(uinteger,range("..min_vid..","..mx_vid.."))"
234
235 -- Validate user provided VLAN ID, make sure its within the bounds
236 -- allowed by the switch.
237 vid.validate = function(self, value, section)
238 local v = tonumber(value)
239 local m = has_vlan4k and 4094 or (num_vlans - 1)
240 if v ~= nil and v >= min_vid and v <= m then
241 if not self.vlan_used[v] then
242 self.vlan_used[v] = true
243 return value
244 else
245 return nil,
246 translatef("Invalid VLAN ID given! Only unique IDs are allowed")
247 end
248 else
249 return nil,
250 translatef("Invalid VLAN ID given! Only IDs between %d and %d are allowed.", min_vid, m)
251 end
252 end
253
254 -- When writing the "vid" or "vlan" option, serialize the port states
255 -- as well and write them as "ports" option to uci.
256 vid.write = function(self, section, value)
257 local o
258 local p = { }
259
260 for _, o in ipairs(port_opts) do
261 local v = o:formvalue(section)
262 if v == "t" then
263 p[#p+1] = o.option .. v
264 elseif v == "u" then
265 p[#p+1] = o.option
266 end
267 end
268
269 if enable_vlan4k then
270 m:set(sid, "enable_vlan4k", "1")
271 end
272
273 m:set(section, "ports", table.concat(p, " "))
274 return Value.write(self, section, value)
275 end
276
277 -- Fallback to "vlan" option if "vid" option is supported but unset.
278 vid.cfgvalue = function(self, section)
279 return m:get(section, has_vlan4k or "vlan")
280 or m:get(section, "vlan")
281 end
282
283 -- Build per-port off/untagged/tagged choice lists.
284 local pt
285 for pt = 0, num_ports - 1 do
286 local title
287 if pt == cpu_port then
288 title = translate("CPU")
289 else
290 title = translatef("Port %d", pt)
291 end
292
293 local po = s:option(ListValue, tostring(pt), title)
294
295 po:value("", translate("off"))
296 po:value("u", translate("untagged"))
297 po:value("t", translate("tagged"))
298
299 po.cfgvalue = portvalue
300 po.validate = portvalidate
301 po.write = function() end
302
303 port_opts[#port_opts+1] = po
304 end
305
306 switches[#switches+1] = switch_name
307 end
308 )
309
310 -- Switch status template
311 s = m:section(SimpleSection)
312 s.template = "admin_network/switch_status"
313 s.switches = switches
314
315 return m