0fa2696e8a0f7352afa868aa8acdb67b5ab29134
[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                 local mx_vid = has_vlan4k and 4094 or (num_vlans - 1) 
206
207                 vid.rmempty = false
208                 vid.forcewrite = true
209                 vid.vlan_used = { }
210                 vid.datatype = "and(uinteger,range("..min_vid..","..mx_vid.."))"
211
212                 -- Validate user provided VLAN ID, make sure its within the bounds
213                 -- allowed by the switch.
214                 vid.validate = function(self, value, section)
215                         local v = tonumber(value)
216                         local m = has_vlan4k and 4094 or (num_vlans - 1)
217                         if v ~= nil and v >= min_vid and v <= m then
218                                 if not self.vlan_used[v] then
219                                         self.vlan_used[v] = true
220                                         return value
221                                 else
222                                         return nil,
223                                                 translatef("Invalid VLAN ID given! Only unique IDs are allowed")
224                                 end
225                         else
226                                 return nil,
227                                         translatef("Invalid VLAN ID given! Only IDs between %d and %d are allowed.", min_vid, m)
228                         end
229                 end
230
231                 -- When writing the "vid" or "vlan" option, serialize the port states
232                 -- as well and write them as "ports" option to uci.
233                 vid.write = function(self, section, value)
234                         local o
235                         local p = { }
236
237                         for _, o in ipairs(port_opts) do
238                                 local v = o:formvalue(section)
239                                 if v == "t" then
240                                         p[#p+1] = o.option .. v
241                                 elseif v == "u" then
242                                         p[#p+1] = o.option
243                                 end
244                         end
245
246                         if enable_vlan4k then
247                                 m:set(sid, "enable_vlan4k", "1")
248                         end
249
250                         m:set(section, "ports", table.concat(p, " "))
251                         return Value.write(self, section, value)
252                 end
253
254                 -- Fallback to "vlan" option if "vid" option is supported but unset.
255                 vid.cfgvalue = function(self, section)
256                         return m:get(section, has_vlan4k or "vlan")
257                                 or m:get(section, "vlan")
258                 end
259
260                 -- Build per-port off/untagged/tagged choice lists.
261                 local pt
262                 for pt = 0, num_ports - 1 do
263                         local title
264                         if pt == cpu_port then
265                                 title = translate("CPU")
266                         else
267                                 title = translatef("Port %d", pt)
268                         end
269
270                         local po = s:option(ListValue, tostring(pt), title)
271
272                         po:value("",  translate("off"))
273                         po:value("u", translate("untagged"))
274                         po:value("t", translate("tagged"))
275
276                         po.cfgvalue = portvalue
277                         po.validate = portvalidate
278                         po.write    = function() end
279
280                         port_opts[#port_opts+1] = po
281                 end
282
283                 switches[#switches+1] = switch_name
284         end
285 )
286
287 -- Switch status template
288 s = m:section(SimpleSection)
289 s.template = "admin_network/switch_status"
290 s.switches = switches
291
292 return m