luci-mod-admin-full: do not override iface section in wireless cbi map
[project/luci.git] / modules / luci-mod-admin-full / 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 = m.uci:get("network", section[".name"], "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", "<div id='portstatus-%s'></div>" % switch_name)
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, '<div id="portstatus-%s-%d"></div>' %{ switch_name, pt.num })
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