modules/admin-full: make interface configuration modular
[project/luci.git] / modules / admin-full / luasrc / model / cbi / admin_network / ifaces.lua
1 --[[
2 LuCI - Lua Configuration Interface
3
4 Copyright 2008 Steven Barth <steven@midlink.org>
5 Copyright 2008-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 $Id$
14 ]]--
15
16 local fs = require "nixio.fs"
17 local ut = require "luci.util"
18 local nw = require "luci.model.network"
19 local fw = require "luci.model.firewall"
20
21 arg[1] = arg[1] or ""
22
23 local has_dnsmasq  = fs.access("/etc/config/dhcp")
24 local has_firewall = fs.access("/etc/config/firewall")
25
26 m = Map("network", translate("Interfaces") .. " - " .. arg[1]:upper(), translate("On this page you can configure the network interfaces. You can bridge several interfaces by ticking the \"bridge interfaces\" field and enter the names of several network interfaces separated by spaces. You can also use <abbr title=\"Virtual Local Area Network\">VLAN</abbr> notation <samp>INTERFACE.VLANNR</samp> (<abbr title=\"for example\">e.g.</abbr>: <samp>eth0.1</samp>)."))
27 m:chain("wireless")
28
29 if has_firewall then
30         m:chain("firewall")
31 end
32
33 nw.init(m.uci)
34 fw.init(m.uci)
35
36
37 local net = nw:get_network(arg[1])
38
39 local function backup_ifnames(is_bridge)
40         if not net:is_floating() and not m:get(net:name(), "_orig_ifname") then
41                 local ifcs = net:get_interfaces() or { net:get_interface() }
42                 if ifcs then
43                         local _, ifn
44                         local ifns = { }
45                         for _, ifn in ipairs(ifcs) do
46                                 ifns[#ifns+1] = ifn:name()
47                         end
48                         if #ifns > 0 then
49                                 m:set(net:name(), "_orig_ifname", table.concat(ifns, " "))
50                                 m:set(net:name(), "_orig_bridge", tostring(net:is_bridge()))
51                         end
52                 end
53         end
54 end
55
56
57 -- redirect to overview page if network does not exist anymore (e.g. after a revert)
58 if not net then
59         luci.http.redirect(luci.dispatcher.build_url("admin/network/network"))
60         return
61 end
62
63 -- protocol switch was requested, rebuild interface config and reload page
64 if m:formvalue("cbid.network.%s._switch" % net:name()) then
65         -- get new protocol
66         local ptype = m:formvalue("cbid.network.%s.proto" % net:name()) or "-"
67         local proto = nw:get_protocol(ptype, net:name())
68         if proto then
69                 -- backup default
70                 backup_ifnames()
71
72                 -- if current proto is not floating and target proto is not floating,
73                 -- then attempt to retain the ifnames
74                 --error(net:proto() .. " > " .. proto:proto())
75                 if not net:is_floating() and not proto:is_floating() then
76                         -- if old proto is a bridge and new proto not, then clip the
77                         -- interface list to the first ifname only
78                         if net:is_bridge() and proto:is_virtual() then
79                                 local _, ifn
80                                 local first = true
81                                 for _, ifn in ipairs(net:get_interfaces() or { net:get_interface() }) do
82                                         if first then
83                                                 first = false
84                                         else
85                                                 net:del_interface(ifn)
86                                         end
87                                 end
88                                 m:del(net:name(), "type")
89                         end
90
91                 -- if the current proto is floating, the target proto not floating,
92                 -- then attempt to restore ifnames from backup
93                 elseif net:is_floating() and not proto:is_floating() then
94                         -- if we have backup data, then re-add all orphaned interfaces
95                         -- from it and restore the bridge choice
96                         local br = (m:get(net:name(), "_orig_bridge") == "true")
97                         local ifn
98                         local ifns = { }
99                         for ifn in ut.imatch(m:get(net:name(), "_orig_ifname")) do
100                                 ifn = nw:get_interface(ifn)
101                                 if ifn and not ifn:get_network() then
102                                         proto:add_interface(ifn)
103                                         if not br then
104                                                 break
105                                         end
106                                 end
107                         end
108                         if br then
109                                 m:set(net:name(), "type", "bridge")
110                         end
111
112                 -- in all other cases clear the ifnames
113                 else
114                         local _, ifc
115                         for _, ifc in ipairs(net:get_interfaces() or { net:get_interface() }) do
116                                 net:del_interface(ifc)
117                         end
118                         m:del(net:name(), "type")
119                 end
120
121                 -- clear options
122                 local k, v
123                 for k, v in pairs(m:get(net:name())) do
124                         if k:sub(1,1) ~= "." and
125                            k ~= "type" and
126                            k ~= "ifname" and
127                            k ~= "_orig_ifname" and
128                            k ~= "_orig_bridge"
129                         then
130                                 m:del(net:name(), k)
131                         end
132                 end
133
134                 -- set proto
135                 m:set(net:name(), "proto", proto:proto())
136                 m.uci:save("network")
137                 m.uci:save("wireless")
138
139                 -- reload page
140                 luci.http.redirect(luci.dispatcher.build_url("admin/network/network", arg[1]))
141                 return
142         end
143 end
144
145 -- dhcp setup was requested, create section and reload page
146 if m:formvalue("cbid.dhcp._enable._enable") then
147         m.uci:section("dhcp", "dhcp", nil, {
148                 interface = arg[1],
149                 start     = "100",
150                 limit     = "150",
151                 leasetime = "12h"
152         })
153
154         m.uci:save("dhcp")
155         luci.http.redirect(luci.dispatcher.build_url("admin/network/network", arg[1]))
156         return
157 end
158
159 local ifc = net:get_interface()
160
161 s = m:section(NamedSection, arg[1], "interface", translate("Common Configuration"))
162 s.addremove = false
163
164 s:tab("general",  translate("General Setup"))
165 s:tab("advanced", translate("Advanced Settings"))
166 s:tab("physical", translate("Physical Settings"))
167
168 if has_firewall then
169         s:tab("firewall", translate("Firewall Settings"))
170 end
171
172
173 -- if current network is empty, print a warning
174 if not net:is_floating() and net:is_empty() then
175         st = s:taboption("general", DummyValue, "__status", translate("Status"))
176         st.value = translate("There is no device assigned yet, please attach a network device in the \"Physical Settings\" tab")
177 else
178         st = s:taboption("general", DummyValue, "__status", translate("Status"))
179         st.template = "admin_network/iface_status"
180         st.network  = arg[1]
181 end
182
183
184 p = s:taboption("general", ListValue, "proto", translate("Protocol"))
185 p.default = net:proto()
186
187
188 if not net:is_installed() then
189         p_install = s:taboption("general", Button, "_install")
190         p_install.title      = translate("Protocol support is not installed")
191         p_install.inputtitle = translate("Install package %q" % net:opkg_package())
192         p_install.inputstyle = "apply"
193         p_install:depends("proto", net:proto())
194
195         function p_install.write()
196                 return luci.http.redirect(
197                         luci.dispatcher.build_url("admin/system/packages") ..
198                         "?submit=1&install=%s" % net:opkg_package()
199                 )
200         end
201 end
202
203
204 p_switch = s:taboption("general", Button, "_switch")
205 p_switch.title      = translate("Really switch protocol?")
206 p_switch.inputtitle = translate("Switch protocol")
207 p_switch.inputstyle = "apply"
208
209 local _, pr
210 for _, pr in ipairs(nw:get_protocols()) do
211         p:value(pr:proto(), pr:get_i18n())
212         if pr:proto() ~= net:proto() then
213                 p_switch:depends("proto", pr:proto())
214         end
215 end
216
217
218 auto = s:taboption("advanced", Flag, "auto", translate("Bring up on boot"))
219 auto.default = (net:proto() == "none") and auto.disabled or auto.enabled
220
221
222 if not net:is_virtual() then
223         br = s:taboption("physical", Flag, "type", translate("Bridge interfaces"), translate("creates a bridge over specified interface(s)"))
224         br.enabled = "bridge"
225         br.rmempty = true
226         br:depends("proto", "static")
227         br:depends("proto", "dhcp")
228         br:depends("proto", "none")
229
230         stp = s:taboption("physical", Flag, "stp", translate("Enable <abbr title=\"Spanning Tree Protocol\">STP</abbr>"),
231                 translate("Enables the Spanning Tree Protocol on this bridge"))
232         stp:depends("type", "bridge")
233         stp.rmempty = true
234 end
235
236
237 if not net:is_floating() then
238         ifname_single = s:taboption("physical", Value, "ifname_single", translate("Interface"))
239         ifname_single.template = "cbi/network_ifacelist"
240         ifname_single.widget = "radio"
241         ifname_single.nobridges = true
242         ifname_single.rmempty = false
243         ifname_single.network = arg[1]
244         ifname_single:depends("type", "")
245
246         function ifname_single.cfgvalue(self, s)
247                 -- let the template figure out the related ifaces through the network model
248                 return nil
249         end
250
251         function ifname_single.write(self, s, val)
252                 local i
253                 local new_ifs = { }
254                 local old_ifs = { }
255
256                 for _, i in ipairs(net:get_interfaces() or { net:get_interface() }) do
257                         old_ifs[#old_ifs+1] = i:name()
258                 end
259
260                 for i in ut.imatch(val) do
261                         new_ifs[#new_ifs+1] = i
262
263                         -- if this is not a bridge, only assign first interface
264                         if self.option == "ifname_single" then
265                                 break
266                         end
267                 end
268
269                 table.sort(old_ifs)
270                 table.sort(new_ifs)
271
272                 for i = 1, math.max(#old_ifs, #new_ifs) do
273                         if old_ifs[i] ~= new_ifs[i] then
274                                 backup_ifnames()
275                                 for i = 1, #old_ifs do
276                                         net:del_interface(old_ifs[i])
277                                 end
278                                 for i = 1, #new_ifs do
279                                         net:add_interface(new_ifs[i])
280                                 end
281                                 break
282                         end
283                 end
284         end
285 end
286
287
288 if not net:is_virtual() then
289         ifname_multi = s:taboption("physical", Value, "ifname_multi", translate("Interface"))
290         ifname_multi.template = "cbi/network_ifacelist"
291         ifname_multi.nobridges = true
292         ifname_multi.rmempty = false
293         ifname_multi.network = arg[1]
294         ifname_multi.widget = "checkbox"
295         ifname_multi:depends("type", "bridge")
296         ifname_multi.cfgvalue = ifname_single.cfgvalue
297         ifname_multi.write = ifname_single.write
298 end
299
300
301 if has_firewall then
302         fwzone = s:taboption("firewall", Value, "_fwzone",
303                 translate("Create / Assign firewall-zone"),
304                 translate("Choose the firewall zone you want to assign to this interface. Select <em>unspecified</em> to remove the interface from the associated zone or fill out the <em>create</em> field to define a new zone and attach the interface to it."))
305
306         fwzone.template = "cbi/firewall_zonelist"
307         fwzone.network = arg[1]
308         fwzone.rmempty = false
309
310         function fwzone.cfgvalue(self, section)
311                 self.iface = section
312                 local z = fw:get_zone_by_network(section)
313                 return z and z:name()
314         end
315
316         function fwzone.write(self, section, value)
317                 local zone = fw:get_zone(value)
318
319                 if not zone and value == '-' then
320                         value = m:formvalue(self:cbid(section) .. ".newzone")
321                         if value and #value > 0 then
322                                 zone = fw:add_zone(value)
323                         else
324                                 fw:del_network(section)
325                         end
326                 end
327
328                 if zone then
329                         fw:del_network(section)
330                         zone:add_network(section)
331                 end
332         end
333 end
334
335
336 function p.write() end
337 function p.remove() end
338 function p.validate(self, value, section)
339         if value == net:proto() then
340                 if not net:is_floating() and net:is_empty() then
341                         local ifn = ((br and (br:formvalue(section) == "bridge"))
342                                 and ifname_multi:formvalue(section)
343                              or ifname_single:formvalue(section))
344
345                         for ifn in ut.imatch(ifn) do
346                                 return value
347                         end
348                         return nil, translate("The selected protocol needs a device assigned")
349                 end
350         end
351         return value
352 end
353
354
355 local form, ferr = loadfile(
356         ut.libpath() .. "/model/cbi/admin_network/proto_%s.lua" % net:proto()
357 )
358
359 if not form then
360         s:taboption("general", DummyValue, "_error",
361                 translate("Missing protocol extension for proto %q" % net:proto())
362         ).value = ferr
363 else
364         setfenv(form, getfenv(1))(m, s, net)
365 end
366
367
368 local _, field
369 for _, field in ipairs(s.children) do
370         if field ~= st and field ~= p and field ~= p_install and field ~= p_switch then
371                 if next(field.deps) then
372                         local _, dep
373                         for _, dep in ipairs(field.deps) do
374                                 dep.deps.proto = net:proto()
375                         end
376                 else
377                         field:depends("proto", net:proto())
378                 end
379         end
380 end
381
382
383 --
384 -- Display IP Aliases
385 --
386
387 if not net:is_floating() then
388         s2 = m:section(TypedSection, "alias", translate("IP-Aliases"))
389         s2.addremove = true
390
391         s2:depends("interface", arg[1])
392         s2.defaults.interface = arg[1]
393
394         s2:tab("general", translate("General Setup"))
395         s2.defaults.proto = "static"
396
397         ip = s2:taboption("general", Value, "ipaddr", translate("<abbr title=\"Internet Protocol Version 4\">IPv4</abbr>-Address"))
398         ip.optional = true
399         ip.datatype = "ip4addr"
400
401         nm = s2:taboption("general", Value, "netmask", translate("<abbr title=\"Internet Protocol Version 4\">IPv4</abbr>-Netmask"))
402         nm.optional = true
403         nm.datatype = "ip4addr"
404         nm:value("255.255.255.0")
405         nm:value("255.255.0.0")
406         nm:value("255.0.0.0")
407
408         gw = s2:taboption("general", Value, "gateway", translate("<abbr title=\"Internet Protocol Version 4\">IPv4</abbr>-Gateway"))
409         gw.optional = true
410         gw.datatype = "ip4addr"
411
412         if has_ipv6 then
413                 s2:tab("ipv6", translate("IPv6 Setup"))
414
415                 ip6 = s2:taboption("ipv6", Value, "ip6addr", translate("<abbr title=\"Internet Protocol Version 6\">IPv6</abbr>-Address"), translate("<abbr title=\"Classless Inter-Domain Routing\">CIDR</abbr>-Notation: address/prefix"))
416                 ip6.optional = true
417                 ip6.datatype = "ip6addr"
418
419                 gw6 = s2:taboption("ipv6", Value, "ip6gw", translate("<abbr title=\"Internet Protocol Version 6\">IPv6</abbr>-Gateway"))
420                 gw6.optional = true
421                 gw6.datatype = "ip6addr"
422         end
423
424         s2:tab("advanced", translate("Advanced Settings"))
425
426         bcast = s2:taboption("advanced", Value, "bcast", translate("<abbr title=\"Internet Protocol Version 4\">IPv4</abbr>-Broadcast"))
427         bcast.optional = true
428         bcast.datatype = "ip4addr"
429
430         dns = s2:taboption("advanced", Value, "dns", translate("<abbr title=\"Domain Name System\">DNS</abbr>-Server"))
431         dns.optional = true
432         dns.datatype = "ip4addr"
433 end
434
435
436 --
437 -- Display DNS settings if dnsmasq is available
438 --
439
440 if has_dnsmasq and net:proto() == "static" then
441         m2 = Map("dhcp", "", "")
442
443         local has_section = false
444
445         m2.uci:foreach("dhcp", "dhcp", function(s)
446                 if s.interface == arg[1] then
447                         has_section = true
448                         return false
449                 end
450         end)
451
452         if not has_section then
453
454                 s = m2:section(TypedSection, "dhcp", translate("DHCP Server"))
455                 s.anonymous   = true
456                 s.cfgsections = function() return { "_enable" } end
457
458                 x = s:option(Button, "_enable")
459                 x.title      = translate("No DHCP Server configured for this interface")
460                 x.inputtitle = translate("Setup DHCP Server")
461                 x.inputstyle = "apply"
462
463         else
464
465                 s = m2:section(TypedSection, "dhcp", translate("DHCP Server"))
466                 s.addremove = false
467                 s.anonymous = true
468                 s:tab("general",  translate("General Setup"))
469                 s:tab("advanced", translate("Advanced Settings"))
470
471                 function s.filter(self, section)
472                         return m2.uci:get("dhcp", section, "interface") == arg[1]
473                 end
474
475                 local ignore = s:taboption("general", Flag, "ignore",
476                         translate("Ignore interface"),
477                         translate("Disable <abbr title=\"Dynamic Host Configuration Protocol\">DHCP</abbr> for " ..
478                                 "this interface."))
479
480                 local start = s:taboption("general", Value, "start", translate("Start"),
481                         translate("Lowest leased address as offset from the network address."))
482                 start.optional = true
483                 start.datatype = "uinteger"
484                 start.default = "100"
485
486                 local limit = s:taboption("general", Value, "limit", translate("Limit"),
487                         translate("Maximum number of leased addresses."))
488                 limit.optional = true
489                 limit.datatype = "uinteger"
490                 limit.default = "150"
491
492                 local ltime = s:taboption("general", Value, "leasetime", translate("Leasetime"),
493                         translate("Expiry time of leased addresses, minimum is 2 Minutes (<code>2m</code>)."))
494                 ltime.rmempty = true
495                 ltime.default = "12h"
496
497                 local dd = s:taboption("advanced", Flag, "dynamicdhcp",
498                         translate("Dynamic <abbr title=\"Dynamic Host Configuration Protocol\">DHCP</abbr>"),
499                         translate("Dynamically allocate DHCP addresses for clients. If disabled, only " ..
500                                 "clients having static leases will be served."))
501                 dd.default = dd.enabled
502
503                 s:taboption("advanced", Flag, "force", translate("Force"),
504                         translate("Force DHCP on this network even if another server is detected."))
505
506                 -- XXX: is this actually useful?
507                 --s:taboption("advanced", Value, "name", translate("Name"),
508                 --      translate("Define a name for this network."))
509
510                 mask = s:taboption("advanced", Value, "netmask",
511                         translate("<abbr title=\"Internet Protocol Version 4\">IPv4</abbr>-Netmask"),
512                         translate("Override the netmask sent to clients. Normally it is calculated " ..
513                                 "from the subnet that is served."))
514
515                 mask.optional = true
516                 mask.datatype = "ip4addr"
517
518                 s:taboption("advanced", DynamicList, "dhcp_option", translate("DHCP-Options"),
519                         translate("Define additional DHCP options, for example \"<code>6,192.168.2.1," ..
520                                 "192.168.2.2</code>\" which advertises different DNS servers to clients."))
521
522                 for i, n in ipairs(s.children) do
523                         if n ~= ignore then
524                                 n:depends("ignore", "")
525                         end
526                 end
527
528         end
529 end
530
531
532 return m, m2