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