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