bd4ce5790184c4159b7d3abfbbfae7f4477ca241
[oweals/luci.git] / modules / luci-mod-network / htdocs / luci-static / resources / view / network / interfaces.js
1 'use strict';
2 'require view';
3 'require dom';
4 'require poll';
5 'require fs';
6 'require ui';
7 'require uci';
8 'require form';
9 'require network';
10 'require firewall';
11 'require tools.widgets as widgets';
12
13 function count_changes(section_id) {
14         var changes = ui.changes.changes, n = 0;
15
16         if (!L.isObject(changes))
17                 return n;
18
19         if (Array.isArray(changes.network))
20                 for (var i = 0; i < changes.network.length; i++)
21                         n += (changes.network[i][1] == section_id);
22
23         if (Array.isArray(changes.dhcp))
24                 for (var i = 0; i < changes.dhcp.length; i++)
25                         n += (changes.dhcp[i][1] == section_id);
26
27         return n;
28 }
29
30 function render_iface(dev, alias) {
31         var type = dev ? dev.getType() : 'ethernet',
32             up   = dev ? dev.isUp() : false;
33
34         return E('span', { class: 'cbi-tooltip-container' }, [
35                 E('img', { 'class' : 'middle', 'src': L.resource('icons/%s%s.png').format(
36                         alias ? 'alias' : type,
37                         up ? '' : '_disabled') }),
38                 E('span', { 'class': 'cbi-tooltip ifacebadge large' }, [
39                         E('img', { 'src': L.resource('icons/%s%s.png').format(
40                                 type, up ? '' : '_disabled') }),
41                         L.itemlist(E('span', { 'class': 'left' }), [
42                                 _('Type'),      dev ? dev.getTypeI18n() : null,
43                                 _('Device'),    dev ? dev.getName() : _('Not present'),
44                                 _('Connected'), up ? _('yes') : _('no'),
45                                 _('MAC'),       dev ? dev.getMAC() : null,
46                                 _('RX'),        dev ? '%.2mB (%d %s)'.format(dev.getRXBytes(), dev.getRXPackets(), _('Pkts.')) : null,
47                                 _('TX'),        dev ? '%.2mB (%d %s)'.format(dev.getTXBytes(), dev.getTXPackets(), _('Pkts.')) : null
48                         ])
49                 ])
50         ]);
51 }
52
53 function render_status(node, ifc, with_device) {
54         var desc = null, c = [];
55
56         if (ifc.isDynamic())
57                 desc = _('Virtual dynamic interface');
58         else if (ifc.isAlias())
59                 desc = _('Alias Interface');
60         else if (!uci.get('network', ifc.getName()))
61                 return L.itemlist(node, [
62                         null, E('em', _('Interface is marked for deletion'))
63                 ]);
64
65         var i18n = ifc.getI18n();
66         if (i18n)
67                 desc = desc ? '%s (%s)'.format(desc, i18n) : i18n;
68
69         var changecount = with_device ? 0 : count_changes(ifc.getName()),
70             ipaddrs = changecount ? [] : ifc.getIPAddrs(),
71             ip6addrs = changecount ? [] : ifc.getIP6Addrs(),
72             errors = ifc.getErrors(),
73             maindev = ifc.getL3Device() || ifc.getDevice(),
74             macaddr = maindev ? maindev.getMAC() : null;
75
76         return L.itemlist(node, [
77                 _('Protocol'), with_device ? null : (desc || '?'),
78                 _('Device'),   with_device ? (maindev ? maindev.getShortName() : E('em', _('Not present'))) : null,
79                 _('Uptime'),   (!changecount && ifc.isUp()) ? '%t'.format(ifc.getUptime()) : null,
80                 _('MAC'),      (!changecount && !ifc.isDynamic() && !ifc.isAlias() && macaddr) ? macaddr : null,
81                 _('RX'),       (!changecount && !ifc.isDynamic() && !ifc.isAlias() && maindev) ? '%.2mB (%d %s)'.format(maindev.getRXBytes(), maindev.getRXPackets(), _('Pkts.')) : null,
82                 _('TX'),       (!changecount && !ifc.isDynamic() && !ifc.isAlias() && maindev) ? '%.2mB (%d %s)'.format(maindev.getTXBytes(), maindev.getTXPackets(), _('Pkts.')) : null,
83                 _('IPv4'),     ipaddrs[0],
84                 _('IPv4'),     ipaddrs[1],
85                 _('IPv4'),     ipaddrs[2],
86                 _('IPv4'),     ipaddrs[3],
87                 _('IPv4'),     ipaddrs[4],
88                 _('IPv6'),     ip6addrs[0],
89                 _('IPv6'),     ip6addrs[1],
90                 _('IPv6'),     ip6addrs[2],
91                 _('IPv6'),     ip6addrs[3],
92                 _('IPv6'),     ip6addrs[4],
93                 _('IPv6'),     ip6addrs[5],
94                 _('IPv6'),     ip6addrs[6],
95                 _('IPv6'),     ip6addrs[7],
96                 _('IPv6'),     ip6addrs[8],
97                 _('IPv6'),     ip6addrs[9],
98                 _('IPv6-PD'),  changecount ? null : ifc.getIP6Prefix(),
99                 _('Information'), with_device ? null : (ifc.get('auto') != '0' ? null : _('Not started on boot')),
100                 _('Error'),    errors ? errors[0] : null,
101                 _('Error'),    errors ? errors[1] : null,
102                 _('Error'),    errors ? errors[2] : null,
103                 _('Error'),    errors ? errors[3] : null,
104                 _('Error'),    errors ? errors[4] : null,
105                 null, changecount ? E('a', {
106                         href: '#',
107                         click: L.bind(ui.changes.displayChanges, ui.changes)
108                 }, _('Interface has %d pending changes').format(changecount)) : null
109         ]);
110 }
111
112 function render_modal_status(node, ifc) {
113         var dev = ifc ? (ifc.getDevice() || ifc.getL3Device() || ifc.getL3Device()) : null;
114
115         dom.content(node, [
116                 E('img', {
117                         'src': L.resource('icons/%s%s.png').format(dev ? dev.getType() : 'ethernet', (dev && dev.isUp()) ? '' : '_disabled'),
118                         'title': dev ? dev.getTypeI18n() : _('Not present')
119                 }),
120                 ifc ? render_status(E('span'), ifc, true) : E('em', _('Interface not present or not connected yet.'))
121         ]);
122
123         return node;
124 }
125
126 function render_ifacebox_status(node, ifc) {
127         var dev = ifc.getL3Device() || ifc.getDevice(),
128             subdevs = ifc.getDevices(),
129             c = [ render_iface(dev, ifc.isAlias()) ];
130
131         if (subdevs && subdevs.length) {
132                 var sifs = [ ' (' ];
133
134                 for (var j = 0; j < subdevs.length; j++)
135                         sifs.push(render_iface(subdevs[j]));
136
137                 sifs.push(')');
138
139                 c.push(E('span', {}, sifs));
140         }
141
142         c.push(E('br'));
143         c.push(E('small', {}, ifc.isAlias() ? _('Alias of "%s"').format(ifc.isAlias())
144                                             : (dev ? dev.getName() : E('em', _('Not present')))));
145
146         dom.content(node, c);
147
148         return firewall.getZoneByNetwork(ifc.getName()).then(L.bind(function(zone) {
149                 this.style.backgroundColor = zone ? zone.getColor() : '#EEEEEE';
150                 this.title = zone ? _('Part of zone %q').format(zone.getName()) : _('No zone assigned');
151         }, node.previousElementSibling));
152 }
153
154 function iface_updown(up, id, ev, force) {
155         var row = document.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(id)),
156             dsc = row.querySelector('[data-name="_ifacestat"] > div'),
157             btns = row.querySelectorAll('.cbi-section-actions .reconnect, .cbi-section-actions .down');
158
159         btns[+!up].blur();
160         btns[+!up].classList.add('spinning');
161
162         btns[0].disabled = true;
163         btns[1].disabled = true;
164
165         if (!up) {
166                 L.resolveDefault(fs.exec_direct('/usr/libexec/luci-peeraddr')).then(function(res) {
167                         var info = null; try { info = JSON.parse(res); } catch(e) {}
168
169                         if (L.isObject(info) &&
170                             Array.isArray(info.inbound_interfaces) &&
171                             info.inbound_interfaces.filter(function(i) { return i == id })[0]) {
172
173                                 ui.showModal(_('Confirm disconnect'), [
174                                         E('p', _('You appear to be currently connected to the device via the "%h" interface. Do you really want to shut down the interface?').format(id)),
175                                         E('div', { 'class': 'right' }, [
176                                                 E('button', {
177                                                         'class': 'cbi-button cbi-button-neutral',
178                                                         'click': function(ev) {
179                                                                 btns[1].classList.remove('spinning');
180                                                                 btns[1].disabled = false;
181                                                                 btns[0].disabled = false;
182
183                                                                 ui.hideModal();
184                                                         }
185                                                 }, _('Cancel')),
186                                                 ' ',
187                                                 E('button', {
188                                                         'class': 'cbi-button cbi-button-negative important',
189                                                         'click': function(ev) {
190                                                                 dsc.setAttribute('disconnect', '');
191                                                                 dom.content(dsc, E('em', _('Interface is shutting down...')));
192
193                                                                 ui.hideModal();
194                                                         }
195                                                 }, _('Disconnect'))
196                                         ])
197                                 ]);
198                         }
199                         else {
200                                 dsc.setAttribute('disconnect', '');
201                                 dom.content(dsc, E('em', _('Interface is shutting down...')));
202                         }
203                 });
204         }
205         else {
206                 dsc.setAttribute(up ? 'reconnect' : 'disconnect', force ? 'force' : '');
207                 dom.content(dsc, E('em', up ? _('Interface is reconnecting...') : _('Interface is shutting down...')));
208         }
209 }
210
211 function get_netmask(s, use_cfgvalue) {
212         var readfn = use_cfgvalue ? 'cfgvalue' : 'formvalue',
213             addropt = s.children.filter(function(o) { return o.option == 'ipaddr'})[0],
214             addrvals = addropt ? L.toArray(addropt[readfn](s.section)) : [],
215             maskopt = s.children.filter(function(o) { return o.option == 'netmask'})[0],
216             maskval = maskopt ? maskopt[readfn](s.section) : null,
217             firstsubnet = maskval ? addrvals[0] + '/' + maskval : addrvals.filter(function(a) { return a.indexOf('/') > 0 })[0];
218
219         if (firstsubnet == null)
220                 return null;
221
222         var mask = firstsubnet.split('/')[1];
223
224         if (!isNaN(mask))
225                 mask = network.prefixToMask(+mask);
226
227         return mask;
228 }
229
230 return view.extend({
231         poll_status: function(map, networks) {
232                 var resolveZone = null;
233
234                 for (var i = 0; i < networks.length; i++) {
235                         var ifc = networks[i],
236                             row = map.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(ifc.getName()));
237
238                         if (row == null)
239                                 continue;
240
241                         var dsc = row.querySelector('[data-name="_ifacestat"] > div'),
242                             box = row.querySelector('[data-name="_ifacebox"] .ifacebox-body'),
243                             btn1 = row.querySelector('.cbi-section-actions .reconnect'),
244                             btn2 = row.querySelector('.cbi-section-actions .down'),
245                             stat = document.querySelector('[id="%s-ifc-status"]'.format(ifc.getName())),
246                             resolveZone = render_ifacebox_status(box, ifc),
247                             disabled = ifc ? !ifc.isUp() : true,
248                             dynamic = ifc ? ifc.isDynamic() : false;
249
250                         if (dsc.hasAttribute('reconnect')) {
251                                 dom.content(dsc, E('em', _('Interface is starting...')));
252                         }
253                         else if (dsc.hasAttribute('disconnect')) {
254                                 dom.content(dsc, E('em', _('Interface is stopping...')));
255                         }
256                         else if (ifc.getProtocol() || uci.get('network', ifc.getName()) == null) {
257                                 render_status(dsc, ifc, false);
258                         }
259                         else if (!ifc.getProtocol()) {
260                                 var e = map.querySelector('[id="cbi-network-%s"] .cbi-button-edit'.format(ifc.getName()));
261                                 if (e) e.disabled = true;
262
263                                 var link = L.url('admin/system/opkg') + '?query=luci-proto';
264                                 dom.content(dsc, [
265                                         E('em', _('Unsupported protocol type.')), E('br'),
266                                         E('a', { href: link }, _('Install protocol extensions...'))
267                                 ]);
268                         }
269                         else {
270                                 dom.content(dsc, E('em', _('Interface not present or not connected yet.')));
271                         }
272
273                         if (stat) {
274                                 var dev = ifc.getDevice();
275                                 dom.content(stat, [
276                                         E('img', {
277                                                 'src': L.resource('icons/%s%s.png').format(dev ? dev.getType() : 'ethernet', (dev && dev.isUp()) ? '' : '_disabled'),
278                                                 'title': dev ? dev.getTypeI18n() : _('Not present')
279                                         }),
280                                         render_status(E('span'), ifc, true)
281                                 ]);
282                         }
283
284                         btn1.disabled = btn1.classList.contains('spinning') || btn2.classList.contains('spinning') || dynamic;
285                         btn2.disabled = btn1.classList.contains('spinning') || btn2.classList.contains('spinning') || dynamic || disabled;
286                 }
287
288                 return Promise.all([ resolveZone, network.flushCache() ]);
289         },
290
291         load: function() {
292                 return Promise.all([
293                         network.getDSLModemType(),
294                         uci.changes()
295                 ]);
296         },
297
298         render: function(data) {
299                 var dslModemType = data[0],
300                     m, s, o;
301
302                 m = new form.Map('network');
303                 m.tabbed = true;
304                 m.chain('dhcp');
305
306                 s = m.section(form.GridSection, 'interface', _('Interfaces'));
307                 s.anonymous = true;
308                 s.addremove = true;
309                 s.addbtntitle = _('Add new interface...');
310
311                 s.load = function() {
312                         return Promise.all([
313                                 network.getNetworks(),
314                                 firewall.getZones()
315                         ]).then(L.bind(function(data) {
316                                 this.networks = data[0];
317                                 this.zones = data[1];
318                         }, this));
319                 };
320
321                 s.tab('general', _('General Settings'));
322                 s.tab('advanced', _('Advanced Settings'));
323                 s.tab('physical', _('Physical Settings'));
324                 s.tab('firewall', _('Firewall Settings'));
325                 s.tab('dhcp', _('DHCP Server'));
326
327                 s.cfgsections = function() {
328                         return this.networks.map(function(n) { return n.getName() })
329                                 .filter(function(n) { return n != 'loopback' });
330                 };
331
332                 s.modaltitle = function(section_id) {
333                         return _('Interfaces') + ' Â» ' + section_id.toUpperCase();
334                 };
335
336                 s.renderRowActions = function(section_id) {
337                         var tdEl = this.super('renderRowActions', [ section_id, _('Edit') ]),
338                             net = this.networks.filter(function(n) { return n.getName() == section_id })[0],
339                             disabled = net ? !net.isUp() : true,
340                             dynamic = net ? net.isDynamic() : false;
341
342                         dom.content(tdEl.lastChild, [
343                                 E('button', {
344                                         'class': 'cbi-button cbi-button-neutral reconnect',
345                                         'click': iface_updown.bind(this, true, section_id),
346                                         'title': _('Reconnect this interface'),
347                                         'disabled': dynamic ? 'disabled' : null
348                                 }, _('Restart')),
349                                 E('button', {
350                                         'class': 'cbi-button cbi-button-neutral down',
351                                         'click': iface_updown.bind(this, false, section_id),
352                                         'title': _('Shutdown this interface'),
353                                         'disabled': (dynamic || disabled) ? 'disabled' : null
354                                 }, _('Stop')),
355                                 tdEl.lastChild.firstChild,
356                                 tdEl.lastChild.lastChild
357                         ]);
358
359                         if (!dynamic && net && !uci.get('network', net.getName())) {
360                                 tdEl.lastChild.childNodes[0].disabled = true;
361                                 tdEl.lastChild.childNodes[2].disabled = true;
362                                 tdEl.lastChild.childNodes[3].disabled = true;
363                         }
364
365                         return tdEl;
366                 };
367
368                 s.addModalOptions = function(s) {
369                         var protoval = uci.get('network', s.section, 'proto'),
370                             protoclass = protoval ? network.getProtocol(protoval) : null,
371                             o, ifname_single, ifname_multi, proto_select, proto_switch, type, stp, igmp, ss, so;
372
373                         if (!protoval)
374                                 return;
375
376                         return network.getNetwork(s.section).then(L.bind(function(ifc) {
377                                 var protocols = network.getProtocols();
378
379                                 protocols.sort(function(a, b) {
380                                         return a.getProtocol() > b.getProtocol();
381                                 });
382
383                                 o = s.taboption('general', form.DummyValue, '_ifacestat_modal', _('Status'));
384                                 o.modalonly = true;
385                                 o.cfgvalue = L.bind(function(section_id) {
386                                         var net = this.networks.filter(function(n) { return n.getName() == section_id })[0];
387
388                                         return render_modal_status(E('div', {
389                                                 'id': '%s-ifc-status'.format(section_id),
390                                                 'class': 'ifacebadge large'
391                                         }), net);
392                                 }, this);
393                                 o.write = function() {};
394
395                                 proto_select = s.taboption('general', form.ListValue, 'proto', _('Protocol'));
396                                 proto_select.modalonly = true;
397
398                                 proto_switch = s.taboption('general', form.Button, '_switch_proto');
399                                 proto_switch.modalonly  = true;
400                                 proto_switch.title      = _('Really switch protocol?');
401                                 proto_switch.inputtitle = _('Switch protocol');
402                                 proto_switch.inputstyle = 'apply';
403                                 proto_switch.onclick = L.bind(function(ev) {
404                                         s.map.save()
405                                                 .then(L.bind(m.load, m))
406                                                 .then(L.bind(m.render, m))
407                                                 .then(L.bind(this.renderMoreOptionsModal, this, s.section));
408                                 }, this);
409
410                                 o = s.taboption('general', form.Flag, 'auto', _('Bring up on boot'));
411                                 o.modalonly = true;
412                                 o.default = o.enabled;
413
414                                 type = s.taboption('physical', form.Flag, 'type', _('Bridge interfaces'), _('Creates a bridge over specified interface(s)'));
415                                 type.modalonly = true;
416                                 type.disabled = '';
417                                 type.enabled = 'bridge';
418                                 type.write = type.remove = function(section_id, value) {
419                                         var protocol = network.getProtocol(proto_select.formvalue(section_id)),
420                                             ifnameopt = this.section.children.filter(function(o) { return o.option == (value ? 'ifname_multi' : 'ifname_single') })[0];
421
422                                         if (!protocol.isVirtual() && !this.isActive(section_id))
423                                                 return;
424
425                                         var old_ifnames = [],
426                                             devs = ifc.getDevices() || L.toArray(ifc.getDevice());
427
428                                         for (var i = 0; i < devs.length; i++)
429                                                 old_ifnames.push(devs[i].getName());
430
431                                         var new_ifnames = L.toArray(ifnameopt.formvalue(section_id));
432
433                                         if (!value)
434                                                 new_ifnames.length = Math.max(new_ifnames.length, 1);
435
436                                         old_ifnames.sort();
437                                         new_ifnames.sort();
438
439                                         for (var i = 0; i < Math.max(old_ifnames.length, new_ifnames.length); i++) {
440                                                 if (old_ifnames[i] != new_ifnames[i]) {
441                                                         // backup_ifnames()
442                                                         for (var j = 0; j < old_ifnames.length; j++)
443                                                                 ifc.deleteDevice(old_ifnames[j]);
444
445                                                         for (var j = 0; j < new_ifnames.length; j++)
446                                                                 ifc.addDevice(new_ifnames[j]);
447
448                                                         break;
449                                                 }
450                                         }
451
452                                         if (value)
453                                                 uci.set('network', section_id, 'type', 'bridge');
454                                         else
455                                                 uci.unset('network', section_id, 'type');
456                                 };
457
458                                 stp = s.taboption('physical', form.Flag, 'stp', _('Enable <abbr title="Spanning Tree Protocol">STP</abbr>'), _('Enables the Spanning Tree Protocol on this bridge'));
459
460                                 igmp = s.taboption('physical', form.Flag, 'igmp_snooping', _('Enable <abbr title="Internet Group Management Protocol">IGMP</abbr> snooping'), _('Enables IGMP snooping on this bridge'));
461
462                                 ifname_single = s.taboption('physical', widgets.DeviceSelect, 'ifname_single', _('Interface'));
463                                 ifname_single.nobridges = ifc.isBridge();
464                                 ifname_single.noaliases = false;
465                                 ifname_single.optional = false;
466                                 ifname_single.network = ifc.getName();
467                                 ifname_single.write = ifname_single.remove = function() {};
468
469                                 ifname_multi = s.taboption('physical', widgets.DeviceSelect, 'ifname_multi', _('Interface'));
470                                 ifname_multi.nobridges = ifc.isBridge();
471                                 ifname_multi.noaliases = true;
472                                 ifname_multi.multiple = true;
473                                 ifname_multi.optional = true;
474                                 ifname_multi.network = ifc.getName();
475                                 ifname_multi.display_size = 6;
476                                 ifname_multi.write = ifname_multi.remove = function() {};
477
478                                 ifname_single.cfgvalue = ifname_multi.cfgvalue = function(section_id) {
479                                         var devs = ifc.getDevices() || L.toArray(ifc.getDevice()),
480                                             ifnames = [];
481
482                                         for (var i = 0; i < devs.length; i++)
483                                                 ifnames.push(devs[i].getName());
484
485                                         return ifnames;
486                                 };
487
488                                 if (L.hasSystemFeature('firewall')) {
489                                         o = s.taboption('firewall', widgets.ZoneSelect, '_zone', _('Create / Assign firewall-zone'), _('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>custom</em> field to define a new zone and attach the interface to it.'));
490                                         o.network = ifc.getName();
491                                         o.optional = true;
492
493                                         o.cfgvalue = function(section_id) {
494                                                 return firewall.getZoneByNetwork(ifc.getName()).then(function(zone) {
495                                                         return (zone != null ? zone.getName() : null);
496                                                 });
497                                         };
498
499                                         o.write = o.remove = function(section_id, value) {
500                                                 return Promise.all([
501                                                         firewall.getZoneByNetwork(ifc.getName()),
502                                                         (value != null) ? firewall.getZone(value) : null
503                                                 ]).then(function(data) {
504                                                         var old_zone = data[0],
505                                                             new_zone = data[1];
506
507                                                         if (old_zone == null && new_zone == null && (value == null || value == ''))
508                                                                 return;
509
510                                                         if (old_zone != null && new_zone != null && old_zone.getName() == new_zone.getName())
511                                                                 return;
512
513                                                         if (old_zone != null)
514                                                                 old_zone.deleteNetwork(ifc.getName());
515
516                                                         if (new_zone != null)
517                                                                 new_zone.addNetwork(ifc.getName());
518                                                         else if (value != null)
519                                                                 return firewall.addZone(value).then(function(new_zone) {
520                                                                         new_zone.addNetwork(ifc.getName());
521                                                                 });
522                                                 });
523                                         };
524                                 }
525
526                                 for (var i = 0; i < protocols.length; i++) {
527                                         proto_select.value(protocols[i].getProtocol(), protocols[i].getI18n());
528
529                                         if (protocols[i].getProtocol() != uci.get('network', s.section, 'proto'))
530                                                 proto_switch.depends('proto', protocols[i].getProtocol());
531
532                                         if (!protocols[i].isVirtual()) {
533                                                 type.depends('proto', protocols[i].getProtocol());
534                                                 stp.depends({ type: 'bridge', proto: protocols[i].getProtocol() });
535                                                 igmp.depends({ type: 'bridge', proto: protocols[i].getProtocol() });
536                                                 ifname_single.depends({ type: '', proto: protocols[i].getProtocol() });
537                                                 ifname_multi.depends({ type: 'bridge', proto: protocols[i].getProtocol() });
538                                         }
539                                 }
540
541                                 if (L.hasSystemFeature('dnsmasq') || L.hasSystemFeature('odhcpd')) {
542                                         o = s.taboption('dhcp', form.SectionValue, '_dhcp', form.TypedSection, 'dhcp');
543                                         o.depends('proto', 'static');
544
545                                         ss = o.subsection;
546                                         ss.uciconfig = 'dhcp';
547                                         ss.addremove = false;
548                                         ss.anonymous = true;
549
550                                         ss.tab('general',  _('General Setup'));
551                                         ss.tab('advanced', _('Advanced Settings'));
552                                         ss.tab('ipv6', _('IPv6 Settings'));
553
554                                         ss.filter = function(section_id) {
555                                                 return (uci.get('dhcp', section_id, 'interface') == ifc.getName());
556                                         };
557
558                                         ss.renderSectionPlaceholder = function() {
559                                                 return E('div', { 'class': 'cbi-section-create' }, [
560                                                         E('p', _('No DHCP Server configured for this interface') + ' &#160; '),
561                                                         E('button', {
562                                                                 'class': 'cbi-button cbi-button-add',
563                                                                 'title': _('Setup DHCP Server'),
564                                                                 'click': ui.createHandlerFn(this, function(section_id, ev) {
565                                                                         this.map.save(function() {
566                                                                                 uci.add('dhcp', 'dhcp', section_id);
567                                                                                 uci.set('dhcp', section_id, 'interface', section_id);
568                                                                                 uci.set('dhcp', section_id, 'start', 100);
569                                                                                 uci.set('dhcp', section_id, 'limit', 150);
570                                                                                 uci.set('dhcp', section_id, 'leasetime', '12h');
571                                                                         });
572                                                                 }, ifc.getName())
573                                                         }, _('Setup DHCP Server'))
574                                                 ]);
575                                         };
576
577                                         ss.taboption('general', form.Flag, 'ignore', _('Ignore interface'), _('Disable <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> for this interface.'));
578
579                                         so = ss.taboption('general', form.Value, 'start', _('Start'), _('Lowest leased address as offset from the network address.'));
580                                         so.optional = true;
581                                         so.datatype = 'or(uinteger,ip4addr("nomask"))';
582                                         so.default = '100';
583
584                                         so = ss.taboption('general', form.Value, 'limit', _('Limit'), _('Maximum number of leased addresses.'));
585                                         so.optional = true;
586                                         so.datatype = 'uinteger';
587                                         so.default = '150';
588
589                                         so = ss.taboption('general', form.Value, 'leasetime', _('Lease time'), _('Expiry time of leased addresses, minimum is 2 minutes (<code>2m</code>).'));
590                                         so.optional = true;
591                                         so.default = '12h';
592
593                                         so = ss.taboption('advanced', form.Flag, 'dynamicdhcp', _('Dynamic <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>'), _('Dynamically allocate DHCP addresses for clients. If disabled, only clients having static leases will be served.'));
594                                         so.default = so.enabled;
595
596                                         ss.taboption('advanced', form.Flag, 'force', _('Force'), _('Force DHCP on this network even if another server is detected.'));
597
598                                         // XXX: is this actually useful?
599                                         //ss.taboption('advanced', form.Value, 'name', _('Name'), _('Define a name for this network.'));
600
601                                         so = ss.taboption('advanced', form.Value, 'netmask', _('<abbr title="Internet Protocol Version 4">IPv4</abbr>-Netmask'), _('Override the netmask sent to clients. Normally it is calculated from the subnet that is served.'));
602                                         so.optional = true;
603                                         so.datatype = 'ip4addr';
604
605                                         so.render = function(option_index, section_id, in_table) {
606                                                 this.placeholder = get_netmask(s, true);
607                                                 return form.Value.prototype.render.apply(this, [ option_index, section_id, in_table ]);
608                                         };
609
610                                         so.validate = function(section_id, value) {
611                                                 var node = this.map.findElement('id', this.cbid(section_id));
612                                                 if (node)
613                                                         node.querySelector('input').setAttribute('placeholder', get_netmask(s, false));
614                                                 return form.Value.prototype.validate.apply(this, [ section_id, value ]);
615                                         };
616
617                                         ss.taboption('advanced', form.DynamicList, 'dhcp_option', _('DHCP-Options'), _('Define additional DHCP options, for example "<code>6,192.168.2.1,192.168.2.2</code>" which advertises different DNS servers to clients.'));
618
619                                         for (var i = 0; i < ss.children.length; i++)
620                                                 if (ss.children[i].option != 'ignore')
621                                                         ss.children[i].depends('ignore', '0');
622
623                                         so = ss.taboption('ipv6', form.ListValue, 'ra', _('Router Advertisement-Service'));
624                                         so.value('', _('disabled'));
625                                         so.value('server', _('server mode'));
626                                         so.value('relay', _('relay mode'));
627                                         so.value('hybrid', _('hybrid mode'));
628
629                                         so = ss.taboption('ipv6', form.ListValue, 'dhcpv6', _('DHCPv6-Service'));
630                                         so.value('', _('disabled'));
631                                         so.value('server', _('server mode'));
632                                         so.value('relay', _('relay mode'));
633                                         so.value('hybrid', _('hybrid mode'));
634
635                                         so = ss.taboption('ipv6', form.ListValue, 'ndp', _('NDP-Proxy'));
636                                         so.value('', _('disabled'));
637                                         so.value('relay', _('relay mode'));
638                                         so.value('hybrid', _('hybrid mode'));
639
640                                         so = ss.taboption('ipv6', form.Flag , 'master', _('Master'), _('Set this interface as master for the dhcpv6 relay.'));
641                                         so.depends('dhcpv6', 'relay');
642                                         so.depends('dhcpv6', 'hybrid');
643
644                                         so = ss.taboption('ipv6', form.ListValue, 'ra_management', _('DHCPv6-Mode'), _('Default is stateless + stateful'));
645                                         so.value('0', _('stateless'));
646                                         so.value('1', _('stateless + stateful'));
647                                         so.value('2', _('stateful-only'));
648                                         so.depends('dhcpv6', 'server');
649                                         so.depends('dhcpv6', 'hybrid');
650                                         so.default = '1';
651
652                                         so = ss.taboption('ipv6', form.Flag, 'ra_default', _('Always announce default router'), _('Announce as default router even if no public prefix is available.'));
653                                         so.depends('ra', 'server');
654                                         so.depends('ra', 'hybrid');
655
656                                         ss.taboption('ipv6', form.DynamicList, 'dns', _('Announced DNS servers'));
657                                         ss.taboption('ipv6', form.DynamicList, 'domain', _('Announced DNS domains'));
658                                 }
659
660                                 ifc.renderFormOptions(s);
661
662                                 for (var i = 0; i < s.children.length; i++) {
663                                         o = s.children[i];
664
665                                         switch (o.option) {
666                                         case 'proto':
667                                         case 'delegate':
668                                         case 'auto':
669                                         case 'type':
670                                         case 'stp':
671                                         case 'igmp_snooping':
672                                         case 'ifname_single':
673                                         case 'ifname_multi':
674                                         case '_dhcp':
675                                         case '_zone':
676                                         case '_switch_proto':
677                                         case '_ifacestat_modal':
678                                                 continue;
679
680                                         default:
681                                                 if (o.deps.length)
682                                                         for (var j = 0; j < o.deps.length; j++)
683                                                                 o.deps[j].proto = protoval;
684                                                 else
685                                                         o.depends('proto', protoval);
686                                         }
687                                 }
688                         }, this));
689                 };
690
691                 s.handleAdd = function(ev) {
692                         var m2 = new form.Map('network'),
693                             s2 = m2.section(form.NamedSection, '_new_'),
694                             protocols = network.getProtocols(),
695                             proto, name, bridge, ifname_single, ifname_multi;
696
697                         protocols.sort(function(a, b) {
698                                 return a.getProtocol() > b.getProtocol();
699                         });
700
701                         s2.render = function() {
702                                 return Promise.all([
703                                         {},
704                                         this.renderUCISection('_new_')
705                                 ]).then(this.renderContents.bind(this));
706                         };
707
708                         name = s2.option(form.Value, 'name', _('Name'));
709                         name.rmempty = false;
710                         name.datatype = 'uciname';
711                         name.placeholder = _('New interface name…');
712                         name.validate = function(section_id, value) {
713                                 if (uci.get('network', value) != null)
714                                         return _('The interface name is already used');
715
716                                 var pr = network.getProtocol(proto.formvalue(section_id), value),
717                                     ifname = pr.isVirtual() ? '%s-%s'.format(pr.getProtocol(), value) : 'br-%s'.format(value);
718
719                                 if (value.length > 15)
720                                         return _('The interface name is too long');
721
722                                 return true;
723                         };
724
725                         proto = s2.option(form.ListValue, 'proto', _('Protocol'));
726                         proto.validate = name.validate;
727
728                         bridge = s2.option(form.Flag, 'type', _('Bridge interfaces'), _('Creates a bridge over specified interface(s)'));
729                         bridge.modalonly = true;
730                         bridge.disabled = '';
731                         bridge.enabled = 'bridge';
732
733                         ifname_single = s2.option(widgets.DeviceSelect, 'ifname_single', _('Interface'));
734                         ifname_single.noaliases = false;
735                         ifname_single.optional = false;
736
737                         ifname_multi = s2.option(widgets.DeviceSelect, 'ifname_multi', _('Interface'));
738                         ifname_multi.nobridges = true;
739                         ifname_multi.noaliases = true;
740                         ifname_multi.multiple = true;
741                         ifname_multi.optional = true;
742                         ifname_multi.display_size = 6;
743
744                         for (var i = 0; i < protocols.length; i++) {
745                                 proto.value(protocols[i].getProtocol(), protocols[i].getI18n());
746
747                                 if (!protocols[i].isVirtual()) {
748                                         bridge.depends({ proto: protocols[i].getProtocol() });
749                                         ifname_single.depends({ type: '', proto: protocols[i].getProtocol() });
750                                         ifname_multi.depends({ type: 'bridge', proto: protocols[i].getProtocol() });
751                                 }
752                         }
753
754                         m2.render().then(L.bind(function(nodes) {
755                                 ui.showModal(_('Add new interface...'), [
756                                         nodes,
757                                         E('div', { 'class': 'right' }, [
758                                                 E('button', {
759                                                         'class': 'btn',
760                                                         'click': ui.hideModal
761                                                 }, _('Cancel')), ' ',
762                                                 E('button', {
763                                                         'class': 'cbi-button cbi-button-positive important',
764                                                         'click': ui.createHandlerFn(this, function(ev) {
765                                                                 var nameval = name.isValid('_new_') ? name.formvalue('_new_') : null,
766                                                                     protoval = proto.isValid('_new_') ? proto.formvalue('_new_') : null,
767                                                                     protoclass = protoval ? network.getProtocol(protoval) : null;
768
769                                                                 if (nameval == null || protoval == null || nameval == '' || protoval == '')
770                                                                         return;
771
772                                                                 return protoclass.isCreateable(nameval).then(function(checkval) {
773                                                                         if (checkval != null) {
774                                                                                 ui.addNotification(null,
775                                                                                                 E('p', _('New interface for "%s" can not be created: %s').format(protoclass.getI18n(), checkval)));
776                                                                                 ui.hideModal();
777                                                                                 return;
778                                                                         }
779
780                                                                         return m.save(function() {
781                                                                                 var section_id = uci.add('network', 'interface', nameval);
782
783                                                                                 uci.set('network', section_id, 'proto', protoval);
784
785                                                                                 if (ifname_single.isActive('_new_')) {
786                                                                                         uci.set('network', section_id, 'ifname', ifname_single.formvalue('_new_'));
787                                                                                 }
788                                                                                 else if (ifname_multi.isActive('_new_')) {
789                                                                                         uci.set('network', section_id, 'type', 'bridge');
790                                                                                         uci.set('network', section_id, 'ifname', L.toArray(ifname_multi.formvalue('_new_')).join(' '));
791                                                                                 }
792                                                                         }).then(L.bind(m.children[0].renderMoreOptionsModal, m.children[0], nameval));
793
794                                                                 });
795                                                         })
796                                                 }, _('Create interface'))
797                                         ])
798                                 ], 'cbi-modal');
799
800                                 nodes.querySelector('[id="%s"] input[type="text"]'.format(name.cbid('_new_'))).focus();
801                         }, this));
802                 };
803
804                 s.handleRemove = function(section_id, ev) {
805                         return network.deleteNetwork(section_id).then(L.bind(function(section_id, ev) {
806                                 return form.GridSection.prototype.handleRemove.apply(this, [section_id, ev]);
807                         }, this, section_id, ev));
808                 };
809
810                 o = s.option(form.DummyValue, '_ifacebox');
811                 o.modalonly = false;
812                 o.textvalue = function(section_id) {
813                         var net = this.section.networks.filter(function(n) { return n.getName() == section_id })[0],
814                             zone = net ? this.section.zones.filter(function(z) { return !!z.getNetworks().filter(function(n) { return n == section_id })[0] })[0] : null;
815
816                         if (!net)
817                                 return;
818
819                         var node = E('div', { 'class': 'ifacebox' }, [
820                                 E('div', {
821                                         'class': 'ifacebox-head',
822                                         'style': 'background-color:%s'.format(zone ? zone.getColor() : '#EEEEEE'),
823                                         'title': zone ? _('Part of zone %q').format(zone.getName()) : _('No zone assigned')
824                                 }, E('strong', net.getName().toUpperCase())),
825                                 E('div', {
826                                         'class': 'ifacebox-body',
827                                         'id': '%s-ifc-devices'.format(section_id),
828                                         'data-network': section_id
829                                 }, [
830                                         E('img', {
831                                                 'src': L.resource('icons/ethernet_disabled.png'),
832                                                 'style': 'width:16px; height:16px'
833                                         }),
834                                         E('br'), E('small', '?')
835                                 ])
836                         ]);
837
838                         render_ifacebox_status(node.childNodes[1], net);
839
840                         return node;
841                 };
842
843                 o = s.option(form.DummyValue, '_ifacestat');
844                 o.modalonly = false;
845                 o.textvalue = function(section_id) {
846                         var net = this.section.networks.filter(function(n) { return n.getName() == section_id })[0];
847
848                         if (!net)
849                                 return;
850
851                         var node = E('div', { 'id': '%s-ifc-description'.format(section_id) });
852
853                         render_status(node, net, false);
854
855                         return node;
856                 };
857
858                 o = s.taboption('advanced', form.Flag, 'delegate', _('Use builtin IPv6-management'));
859                 o.modalonly = true;
860                 o.default = o.enabled;
861
862                 o = s.taboption('advanced', form.Flag, 'force_link', _('Force link'), _('Set interface properties regardless of the link carrier (If set, carrier sense events do not invoke hotplug handlers).'));
863                 o.modalonly = true;
864                 o.render = function(option_index, section_id, in_table) {
865                         var protoopt = this.section.children.filter(function(o) { return o.option == 'proto' })[0],
866                             protoval = protoopt ? protoopt.cfgvalue(section_id) : null;
867
868                         this.default = (protoval == 'static') ? this.enabled : this.disabled;
869                         return this.super('render', [ option_index, section_id, in_table ]);
870                 };
871
872
873                 s = m.section(form.TypedSection, 'globals', _('Global network options'));
874                 s.addremove = false;
875                 s.anonymous = true;
876
877                 o = s.option(form.Value, 'ula_prefix', _('IPv6 ULA-Prefix'));
878                 o.datatype = 'cidr6';
879
880                 o = s.option(form.Flag, 'packet_steering', _('Packet Steering'), _('Enable packet steering across all CPUs. May help or hinder network speed.'));
881                 o.optional = true;
882
883
884                 if (dslModemType != null) {
885                         s = m.section(form.TypedSection, 'dsl', _('DSL'));
886                         s.anonymous = true;
887
888                         o = s.option(form.ListValue, 'annex', _('Annex'));
889                         o.value('a', _('Annex A + L + M (all)'));
890                         o.value('b', _('Annex B (all)'));
891                         o.value('j', _('Annex J (all)'));
892                         o.value('m', _('Annex M (all)'));
893                         o.value('bdmt', _('Annex B G.992.1'));
894                         o.value('b2', _('Annex B G.992.3'));
895                         o.value('b2p', _('Annex B G.992.5'));
896                         o.value('at1', _('ANSI T1.413'));
897                         o.value('admt', _('Annex A G.992.1'));
898                         o.value('alite', _('Annex A G.992.2'));
899                         o.value('a2', _('Annex A G.992.3'));
900                         o.value('a2p', _('Annex A G.992.5'));
901                         o.value('l', _('Annex L G.992.3 POTS 1'));
902                         o.value('m2', _('Annex M G.992.3'));
903                         o.value('m2p', _('Annex M G.992.5'));
904
905                         o = s.option(form.ListValue, 'tone', _('Tone'));
906                         o.value('', _('auto'));
907                         o.value('a', _('A43C + J43 + A43'));
908                         o.value('av', _('A43C + J43 + A43 + V43'));
909                         o.value('b', _('B43 + B43C'));
910                         o.value('bv', _('B43 + B43C + V43'));
911
912                         if (dslModemType == 'vdsl') {
913                                 o = s.option(form.ListValue, 'xfer_mode', _('Encapsulation mode'));
914                                 o.value('', _('auto'));
915                                 o.value('atm', _('ATM (Asynchronous Transfer Mode)'));
916                                 o.value('ptm', _('PTM/EFM (Packet Transfer Mode)'));
917
918                                 o = s.option(form.ListValue, 'line_mode', _('DSL line mode'));
919                                 o.value('', _('auto'));
920                                 o.value('adsl', _('ADSL'));
921                                 o.value('vdsl', _('VDSL'));
922
923                                 o = s.option(form.ListValue, 'ds_snr_offset', _('Downstream SNR offset'));
924                                 o.default = '0';
925
926                                 for (var i = -100; i <= 100; i += 5)
927                                         o.value(i, _('%.1f dB').format(i / 10));
928                         }
929
930                         s.option(form.Value, 'firmware', _('Firmware File'));
931                 }
932
933
934                 // Show ATM bridge section if we have the capabilities
935                 if (L.hasSystemFeature('br2684ctl')) {
936                         s = m.section(form.TypedSection, 'atm-bridge', _('ATM Bridges'), _('ATM bridges expose encapsulated ethernet in AAL5 connections as virtual Linux network interfaces which can be used in conjunction with DHCP or PPP to dial into the provider network.'));
937
938                         s.addremove = true;
939                         s.anonymous = true;
940                         s.addbtntitle = _('Add ATM Bridge');
941
942                         s.handleAdd = function(ev) {
943                                 var sections = uci.sections('network', 'atm-bridge'),
944                                     max_unit = -1;
945
946                                 for (var i = 0; i < sections.length; i++) {
947                                         var unit = +sections[i].unit;
948
949                                         if (!isNaN(unit) && unit > max_unit)
950                                                 max_unit = unit;
951                                 }
952
953                                 return this.map.save(function() {
954                                         var sid = uci.add('network', 'atm-bridge');
955
956                                         uci.set('network', sid, 'unit', max_unit + 1);
957                                         uci.set('network', sid, 'atmdev', 0);
958                                         uci.set('network', sid, 'encaps', 'llc');
959                                         uci.set('network', sid, 'payload', 'bridged');
960                                         uci.set('network', sid, 'vci', 35);
961                                         uci.set('network', sid, 'vpi', 8);
962                                 });
963                         };
964
965                         s.tab('general', _('General Setup'));
966                         s.tab('advanced', _('Advanced Settings'));
967
968                         o = s.taboption('general', form.Value, 'vci', _('ATM Virtual Channel Identifier (VCI)'));
969                         s.taboption('general', form.Value, 'vpi', _('ATM Virtual Path Identifier (VPI)'));
970
971                         o = s.taboption('general', form.ListValue, 'encaps', _('Encapsulation mode'));
972                         o.value('llc', _('LLC'));
973                         o.value('vc', _('VC-Mux'));
974
975                         s.taboption('advanced', form.Value, 'atmdev', _('ATM device number'));
976                         s.taboption('advanced', form.Value, 'unit', _('Bridge unit number'));
977
978                         o = s.taboption('advanced', form.ListValue, 'payload', _('Forwarding mode'));
979                         o.value('bridged', _('bridged'));
980                         o.value('routed', _('routed'));
981                 }
982
983
984                 return m.render().then(L.bind(function(m, nodes) {
985                         poll.add(L.bind(function() {
986                                 var section_ids = m.children[0].cfgsections(),
987                                     tasks = [];
988
989                                 for (var i = 0; i < section_ids.length; i++) {
990                                         var row = nodes.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(section_ids[i])),
991                                             dsc = row.querySelector('[data-name="_ifacestat"] > div'),
992                                             btn1 = row.querySelector('.cbi-section-actions .reconnect'),
993                                             btn2 = row.querySelector('.cbi-section-actions .down');
994
995                                         if (dsc.getAttribute('reconnect') == '') {
996                                                 dsc.setAttribute('reconnect', '1');
997                                                 tasks.push(fs.exec('/sbin/ifup', [section_ids[i]]).catch(function(e) {
998                                                         ui.addNotification(null, E('p', e.message));
999                                                 }));
1000                                         }
1001                                         else if (dsc.getAttribute('disconnect') == '') {
1002                                                 dsc.setAttribute('disconnect', '1');
1003                                                 tasks.push(fs.exec('/sbin/ifdown', [section_ids[i]]).catch(function(e) {
1004                                                         ui.addNotification(null, E('p', e.message));
1005                                                 }));
1006                                         }
1007                                         else if (dsc.getAttribute('reconnect') == '1') {
1008                                                 dsc.removeAttribute('reconnect');
1009                                                 btn1.classList.remove('spinning');
1010                                                 btn1.disabled = false;
1011                                         }
1012                                         else if (dsc.getAttribute('disconnect') == '1') {
1013                                                 dsc.removeAttribute('disconnect');
1014                                                 btn2.classList.remove('spinning');
1015                                                 btn2.disabled = false;
1016                                         }
1017                                 }
1018
1019                                 return Promise.all(tasks)
1020                                         .then(L.bind(network.getNetworks, network))
1021                                         .then(L.bind(this.poll_status, this, nodes));
1022                         }, this), 5);
1023
1024                         return nodes;
1025                 }, this, m));
1026         }
1027 });