summaryrefslogtreecommitdiffstats
path: root/modules/admin-full/luasrc/model/cbi/admin_network/vlan.lua
blob: 0fa2696e8a0f7352afa868aa8acdb67b5ab29134 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
--[[
LuCI - Lua Configuration Interface

Copyright 2008 Steven Barth <steven@midlink.org>
Copyright 2010-2011 Jo-Philipp Wich <xm@subsignal.org>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

	http://www.apache.org/licenses/LICENSE-2.0

]]--

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."))

local switches = { }

m.uci:foreach("network", "switch",
	function(x)
		local sid         = x['.name']
		local switch_name = x.name or sid
		local has_vlan    = nil
		local has_learn   = nil
		local has_vlan4k  = nil
		local has_jumbo3  = nil
		local min_vid     = 0
		local max_vid     = 16
		local num_vlans   = 16
		local num_ports   = 6
		local cpu_port    = 5

		local switch_title
		local enable_vlan4k = false

		-- Parse some common switch properties from swconfig help output.
		local swc = io.popen("swconfig dev %q help 2>/dev/null" % switch_name)
		if swc then

			local is_port_attr = false
			local is_vlan_attr = false

			while true do
				local line = swc:read("*l")
				if not line then break end

				if line:match("^%s+%-%-vlan") then
					is_vlan_attr = true

				elseif line:match("^%s+%-%-port") then
					is_vlan_attr = false
					is_port_attr = true

				elseif line:match("cpu @") then
					switch_title = line:match("^switch%d: %w+%((.-)%)")
					num_ports, cpu_port, num_vlans =
						line:match("ports: (%d+) %(cpu @ (%d+)%), vlans: (%d+)")

					num_ports  = tonumber(num_ports) or  6
					num_vlans  = tonumber(num_vlans) or 16
					cpu_port   = tonumber(cpu_port)  or  5
					min_vid    = 1

				elseif line:match(": pvid") or line:match(": tag") or line:match(": vid") then
					if is_vlan_attr then has_vlan4k = line:match(": (%w+)") end

				elseif line:match(": enable_vlan4k") then
					enable_vlan4k = true

				elseif line:match(": enable_vlan") then
					has_vlan = "enable_vlan"

				elseif line:match(": enable_learning") then
					has_learn = "enable_learning"

				elseif line:match(": max_length") then
					has_jumbo3 = "max_length"
				end
			end

			swc:close()
		end


		-- Switch properties
		s = m:section(NamedSection, x['.name'], "switch",
			switch_title and translatef("Switch %q (%s)", switch_name, switch_title)
					      or translatef("Switch %q", switch_name))

		s.addremove = false

		if has_vlan then
			s:option(Flag, has_vlan, translate("Enable VLAN functionality"))
		end

		if has_learn then
			x = s:option(Flag, has_learn, translate("Enable learning and aging"))
			x.default = x.enabled
		end

		if has_jumbo3 then
			x = s:option(Flag, has_jumbo3, translate("Enable Jumbo Frame passthrough"))
			x.enabled = "3"
			x.rmempty = true
		end


		-- VLAN table
		s = m:section(TypedSection, "switch_vlan",
			switch_title and translatef("VLANs on %q (%s)", switch_name, switch_title)
						  or translatef("VLANs on %q", switch_name))

		s.template = "cbi/tblsection"
		s.addremove = true
		s.anonymous = true

		-- Filter by switch
		s.filter = function(self, section)
			local device = m:get(section, "device")
			return (device and device == switch_name)
		end

		-- Override cfgsections callback to enforce row ordering by vlan id.
		s.cfgsections = function(self)
			local osections = TypedSection.cfgsections(self)
			local sections = { }
			local section

			for _, section in luci.util.spairs(
				osections,
				function(a, b)
					return (tonumber(m:get(osections[a], has_vlan4k or "vlan")) or 9999)
						<  (tonumber(m:get(osections[b], has_vlan4k or "vlan")) or 9999)
				end
			) do
				sections[#sections+1] = section
			end

			return sections
		end

		-- When creating a new vlan, preset it with the highest found vid + 1.
		s.create = function(self, section, origin)
			-- Filter by switch
			if m:get(origin, "device") ~= switch_name then
				return
			end

			local sid = TypedSection.create(self, section)

			local max_nr = 0
			local max_id = 0

			m.uci:foreach("network", "switch_vlan",
				function(s)
					if s.device == switch_name then
						local nr = tonumber(s.vlan)
						local id = has_vlan4k and tonumber(s[has_vlan4k])
						if nr ~= nil and nr > max_nr then max_nr = nr end
						if id ~= nil and id > max_id then max_id = id end
					end
				end)

			m:set(sid, "device", switch_name)
			m:set(sid, "vlan", max_nr + 1)

			if has_vlan4k then
				m:set(sid, has_vlan4k, max_id + 1)
			end

			return sid
		end


		local port_opts = { }
		local untagged  = { }

		-- Parse current tagging state from the "ports" option.
		local portvalue = function(self, section)
			local pt
			for pt in (m:get(section, "ports") or ""):gmatch("%w+") do
				local pc, tu = pt:match("^(%d+)([tu]*)")
				if pc == self.option then return (#tu > 0) and tu or "u" end
			end
			return ""
		end

		-- Validate port tagging. Ensure that a port is only untagged once,
		-- bail out if not.
		local portvalidate = function(self, value, section)
			-- ensure that the ports appears untagged only once
			if value == "u" then
				if not untagged[self.option] then
					untagged[self.option] = true
				elseif min_vid > 0 or tonumber(self.option) ~= cpu_port then -- enable multiple untagged cpu ports due to weird broadcom default setup
					return nil,
						translatef("Port %d is untagged in multiple VLANs!", tonumber(self.option) + 1)
				end
			end
			return value
		end


		local vid = s:option(Value, has_vlan4k or "vlan", "VLAN ID", "<div id='portstatus-%s'></div>" % switch_name)
		local mx_vid = has_vlan4k and 4094 or (num_vlans - 1) 

		vid.rmempty = false
		vid.forcewrite = true
		vid.vlan_used = { }
		vid.datatype = "and(uinteger,range("..min_vid..","..mx_vid.."))"

		-- Validate user provided VLAN ID, make sure its within the bounds
		-- allowed by the switch.
		vid.validate = function(self, value, section)
			local v = tonumber(value)
			local m = has_vlan4k and 4094 or (num_vlans - 1)
			if v ~= nil and v >= min_vid and v <= m then
				if not self.vlan_used[v] then
					self.vlan_used[v] = true
					return value
				else
					return nil,
						translatef("Invalid VLAN ID given! Only unique IDs are allowed")
				end
			else
				return nil,
					translatef("Invalid VLAN ID given! Only IDs between %d and %d are allowed.", min_vid, m)
			end
		end

		-- When writing the "vid" or "vlan" option, serialize the port states
		-- as well and write them as "ports" option to uci.
		vid.write = function(self, section, value)
			local o
			local p = { }

			for _, o in ipairs(port_opts) do
				local v = o:formvalue(section)
				if v == "t" then
					p[#p+1] = o.option .. v
				elseif v == "u" then
					p[#p+1] = o.option
				end
			end

			if enable_vlan4k then
				m:set(sid, "enable_vlan4k", "1")
			end

			m:set(section, "ports", table.concat(p, " "))
			return Value.write(self, section, value)
		end

		-- Fallback to "vlan" option if "vid" option is supported but unset.
		vid.cfgvalue = function(self, section)
			return m:get(section, has_vlan4k or "vlan")
				or m:get(section, "vlan")
		end

		-- Build per-port off/untagged/tagged choice lists.
		local pt
		for pt = 0, num_ports - 1 do
			local title
			if pt == cpu_port then
				title = translate("CPU")
			else
				title = translatef("Port %d", pt)
			end

			local po = s:option(ListValue, tostring(pt), title)

			po:value("",  translate("off"))
			po:value("u", translate("untagged"))
			po:value("t", translate("tagged"))

			po.cfgvalue = portvalue
			po.validate = portvalidate
			po.write    = function() end

			port_opts[#port_opts+1] = po
		end

		switches[#switches+1] = switch_name
	end
)

-- Switch status template
s = m:section(SimpleSection)
s.template = "admin_network/switch_status"
s.switches = switches

return m