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