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