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