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