modules/admin-full: only allow unique vlan ids, fix port labelling if CPU is at 0
[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 $Id$
14 ]]--
15
16 m = Map("network", translate("Switch"), translate("The network ports on your router 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."))
17
18 m.uci:foreach("network", "switch",
19         function(x)
20                 local sid         = x['.name']
21                 local switch_name = x.name or sid
22                 local has_vlan    = nil
23                 local has_learn   = nil
24                 local has_vlan4k  = nil
25                 local has_jumbo3  = nil
26                 local min_vid     = 0
27                 local max_vid     = 16
28                 local num_vlans   = 16
29                 local num_ports   = 6
30                 local cpu_port    = 5
31
32                 local switch_title
33                 local enable_vlan4k = false
34
35                 -- Parse some common switch properties from swconfig help output.
36                 local swc = io.popen("swconfig dev %q help 2>/dev/null" % switch_name)
37                 if swc then
38
39                         local is_port_attr = false
40                         local is_vlan_attr = false
41
42                         while true do
43                                 local line = swc:read("*l")
44                                 if not line then break end
45
46                                 if line:match("^%s+%-%-vlan") then
47                                         is_vlan_attr = true
48
49                                 elseif line:match("^%s+%-%-port") then
50                                         is_vlan_attr = false
51                                         is_port_attr = true
52
53                                 elseif line:match("cpu @") then
54                                         switch_title = line:match("^switch%d: %w+%((.-)%)")
55                                         num_ports, cpu_port, num_vlans =
56                                                 line:match("ports: (%d+) %(cpu @ (%d+)%), vlans: (%d+)")
57
58                                         num_ports  = tonumber(num_ports) or  6
59                                         num_vlans  = tonumber(num_vlans) or 16
60                                         cpu_port   = tonumber(cpu_port)  or  5
61                                         min_vid    = 1
62
63                                 elseif line:match(": pvid") or line:match(": tag") or line:match(": vid") then
64                                         if is_vlan_attr then has_vlan4k = line:match(": (%w+)") end
65
66                                 elseif line:match(": enable_vlan4k") then
67                                         enable_vlan4k = true
68
69                                 elseif line:match(": enable_vlan") then
70                                         has_vlan = "enable_vlan"
71
72                                 elseif line:match(": enable_learning") then
73                                         has_learn = "enable_learning"
74
75                                 elseif line:match(": max_length") then
76                                         has_jumbo3 = "max_length"
77                                 end
78                         end
79
80                         swc:close()
81                 end
82
83
84                 -- Switch properties
85                 s = m:section(NamedSection, x['.name'], "switch",
86                         switch_title and translatef("Switch %q (%s)", switch_name, switch_title)
87                                               or translatef("Switch %q", switch_name))
88
89                 s.addremove = false
90
91                 if has_vlan then
92                         s:option(Flag, has_vlan, translate("Enable VLAN functionality"))
93                 end
94
95                 if enable_vlan4k then
96                         s:option(Flag, "enable_vlan4k", translate("Enable 4K VLANs"))
97                 end
98
99                 if has_learn then
100                         x = s:option(Flag, has_learn, translate("Enable learning and aging"))
101                         x.default = x.enabled
102                 end
103
104                 if has_jumbo3 then
105                         x = s:option(Flag, has_jumbo3, translate("Enable Jumbo Frame passthrough"))
106                         x.enabled = "3"
107                         x.rmempty = true
108                 end
109
110
111                 -- VLAN table
112                 s = m:section(TypedSection, "switch_vlan", translatef("VLANs on %q", switch_name))
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.uci:set("network", sid, "device", switch_name)
165                         m.uci:set("network", sid, "vlan", max_nr + 1)
166
167                         if has_vlan4k then
168                                 m.uci:set("network", 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")
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                         m.uci:set("network", section, "ports", table.concat(p, " "))
245                         return Value.write(self, section, value)
246                 end
247
248                 -- Fallback to "vlan" option if "vid" option is supported but unset.
249                 vid.cfgvalue = function(self, section)
250                         return m:get(section, has_vlan4k or "vlan")
251                                 or m:get(section, "vlan")
252                 end
253
254                 -- Build per-port off/untagged/tagged choice lists.
255                 local pt
256                 local off = 1
257                 for pt = 0, num_ports - 1 do
258                         local title
259                         if pt == cpu_port then
260                                 off   = 0
261                                 title = translate("CPU")
262                         else
263                                 title = translatef("Port %d", pt + off)
264                         end
265
266                         local po = s:option(ListValue, tostring(pt), title)
267
268                         po:value("",  translate("off"))
269                         po:value("u", translate("untagged"))
270                         po:value("t", translate("tagged"))
271
272                         po.cfgvalue = portvalue
273                         po.validate = portvalidate
274                         po.write    = function() end
275
276                         port_opts[#port_opts+1] = po
277                 end
278         end
279 )
280
281 return m