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