modules/admin-full: add support for configuring mirror port
[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