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