luci-mod-network: don't implicitly move bridge opts into device sections
[project/luci.git] / modules / luci-mod-network / htdocs / luci-static / resources / tools / network.js
1 'use strict';
2 'require ui';
3 'require uci';
4 'require form';
5 'require network';
6 'require baseclass';
7 'require validation';
8 'require tools.widgets as widgets';
9
10 function validateAddr(section_id, value) {
11 if (value == '')
12 return true;
13
14 var ipv6 = /6$/.test(this.section.formvalue(section_id, 'mode')),
15 addr = ipv6 ? validation.parseIPv6(value) : validation.parseIPv4(value);
16
17 return addr ? true : (ipv6 ? _('Expecting a valid IPv6 address') : _('Expecting a valid IPv4 address'));
18 }
19
20 function setIfActive(section_id, value) {
21 if (this.isActive(section_id)) {
22 uci.set('network', section_id, this.ucioption, value);
23
24 /* Requires http://lists.openwrt.org/pipermail/openwrt-devel/2020-July/030397.html */
25 if (false && this.option == 'ifname_multi') {
26 var devname = this.section.formvalue(section_id, 'name_complex'),
27 m = devname ? devname.match(/^br-([A-Za-z0-9_]+)$/) : null;
28
29 if (m && uci.get('network', m[1], 'type') == 'bridge') {
30 uci.set('network', m[1], 'ifname', devname);
31 uci.unset('network', m[1], 'type');
32 }
33 }
34 }
35 }
36
37 function validateQoSMap(section_id, value) {
38 if (value == '')
39 return true;
40
41 var m = value.match(/^(\d+):(\d+)$/);
42
43 if (!m || +m[1] > 0xFFFFFFFF || +m[2] > 0xFFFFFFFF)
44 return _('Expecting two priority values separated by a colon');
45
46 return true;
47 }
48
49 function deviceSectionExists(section_id, devname) {
50 var exists = false;
51
52 uci.sections('network', 'device', function(ss) {
53 exists = exists || (ss['.name'] != section_id && ss.name == devname /* && !ss.type*/);
54 });
55
56 return exists;
57 }
58
59 function isBridgePort(dev) {
60 if (!dev)
61 return false;
62
63 if (dev.isBridgePort())
64 return true;
65
66 var isPort = false;
67
68 uci.sections('network', null, function(s) {
69 if (s['.type'] != 'interface' && s['.type'] != 'device')
70 return;
71
72 if (s.type == 'bridge' && L.toArray(s.ifname).indexOf(dev.getName()) > -1)
73 isPort = true;
74 });
75
76 return isPort;
77 }
78
79 function renderDevBadge(dev) {
80 var type = dev.getType(), up = dev.isUp();
81
82 return E('span', { 'class': 'ifacebadge', 'style': 'font-weight:normal' }, [
83 E('img', {
84 'class': 'middle',
85 'src': L.resource('icons/%s%s.png').format(type, up ? '' : '_disabled')
86 }),
87 '\x0a', dev.getName()
88 ]);
89 }
90
91 function lookupDevName(s, section_id) {
92 var typeui = s.getUIElement(section_id, 'type'),
93 typeval = typeui ? typeui.getValue() : s.cfgvalue(section_id, 'type'),
94 ifnameui = s.getUIElement(section_id, 'ifname_single'),
95 ifnameval = ifnameui ? ifnameui.getValue() : s.cfgvalue(section_id, 'ifname_single');
96
97 return (typeval == 'bridge') ? 'br-%s'.format(section_id) : ifnameval;
98 }
99
100 function lookupDevSection(s, section_id, autocreate) {
101 var devname = lookupDevName(s, section_id),
102 devsection = null;
103
104 uci.sections('network', 'device', function(ds) {
105 if (ds.name == devname)
106 devsection = ds['.name'];
107 });
108
109 if (autocreate && !devsection) {
110 devsection = uci.add('network', 'device');
111 uci.set('network', devsection, 'name', devname);
112 }
113
114 return devsection;
115 }
116
117 function getDeviceValue(dev, method) {
118 if (dev && dev.getL3Device)
119 dev = dev.getL3Device();
120
121 if (dev && typeof(dev[method]) == 'function')
122 return dev[method].apply(dev);
123
124 return '';
125 }
126
127 function deviceCfgValue(section_id) {
128 if (arguments.length == 2)
129 return;
130
131 var ds = lookupDevSection(this.section, section_id, false);
132
133 return (ds ? uci.get('network', ds, this.option) : null) ||
134 uci.get('network', section_id, this.option) ||
135 this.default;
136 }
137
138 function deviceWrite(section_id, formvalue) {
139 var ds = lookupDevSection(this.section, section_id, true);
140
141 uci.set('network', ds, this.option, formvalue);
142 uci.unset('network', section_id, this.option);
143 }
144
145 function deviceRemove(section_id) {
146 var ds = lookupDevSection(this.section, section_id, false),
147 sv = ds ? uci.get('network', ds) : null;
148
149 if (sv) {
150 var empty = true;
151
152 for (var opt in sv) {
153 if (opt.charAt(0) == '.' || opt == 'name' || opt == this.option)
154 continue;
155
156 empty = false;
157 }
158
159 if (empty)
160 uci.remove('network', ds);
161 }
162
163 uci.unset('network', section_id, this.option);
164 }
165
166 function deviceRefresh(section_id) {
167 var dev = network.instantiateDevice(lookupDevName(this.section, section_id)),
168 uielem = this.getUIElement(section_id);
169
170 if (uielem) {
171 switch (this.option) {
172 case 'mtu':
173 case 'mtu6':
174 uielem.setPlaceholder(dev.getMTU());
175 break;
176
177 case 'macaddr':
178 uielem.setPlaceholder(dev.getMAC());
179 break;
180 }
181
182 uielem.setValue(this.cfgvalue(section_id));
183 }
184 }
185
186
187 var cbiTagValue = form.Value.extend({
188 renderWidget: function(section_id, option_index, cfgvalue) {
189 var widget = new ui.Dropdown(cfgvalue || ['-'], {
190 '-': E([], [
191 E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ '—' ]),
192 E('span', { 'class': 'hide-close' }, [ _('Do not participate', 'VLAN port state') ])
193 ]),
194 'u': E([], [
195 E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ 'u' ]),
196 E('span', { 'class': 'hide-close' }, [ _('Egress untagged', 'VLAN port state') ])
197 ]),
198 't': E([], [
199 E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ 't' ]),
200 E('span', { 'class': 'hide-close' }, [ _('Egress tagged', 'VLAN port state') ])
201 ]),
202 '*': E([], [
203 E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ '*' ]),
204 E('span', { 'class': 'hide-close' }, [ _('Primary VLAN ID', 'VLAN port state') ])
205 ])
206 }, {
207 id: this.cbid(section_id),
208 sort: [ '-', 'u', 't', '*' ],
209 optional: false,
210 multiple: true
211 });
212
213 var field = this;
214
215 widget.toggleItem = function(sb, li, force_state) {
216 var lis = li.parentNode.querySelectorAll('li'),
217 toggle = ui.Dropdown.prototype.toggleItem;
218
219 toggle.apply(this, [sb, li, force_state]);
220
221 if (force_state != null)
222 return;
223
224 switch (li.getAttribute('data-value'))
225 {
226 case '-':
227 if (li.hasAttribute('selected')) {
228 for (var i = 0; i < lis.length; i++) {
229 switch (lis[i].getAttribute('data-value')) {
230 case '-':
231 break;
232
233 case '*':
234 toggle.apply(this, [sb, lis[i], false]);
235 lis[i].setAttribute('unselectable', '');
236 break;
237
238 default:
239 toggle.apply(this, [sb, lis[i], false]);
240 }
241 }
242 }
243 break;
244
245 case 't':
246 case 'u':
247 if (li.hasAttribute('selected')) {
248 for (var i = 0; i < lis.length; i++) {
249 switch (lis[i].getAttribute('data-value')) {
250 case li.getAttribute('data-value'):
251 break;
252
253 case '*':
254 lis[i].removeAttribute('unselectable');
255 break;
256
257 default:
258 toggle.apply(this, [sb, lis[i], false]);
259 }
260 }
261 }
262 else {
263 toggle.apply(this, [sb, li, true]);
264 }
265 break;
266
267 case '*':
268 if (li.hasAttribute('selected')) {
269 var section_ids = field.section.cfgsections();
270
271 for (var i = 0; i < section_ids.length; i++) {
272 var other_widget = field.getUIElement(section_ids[i]),
273 other_value = L.toArray(other_widget.getValue());
274
275 if (other_widget === this)
276 continue;
277
278 var new_value = other_value.filter(function(v) { return v != '*' });
279
280 if (new_value.length == other_value.length)
281 continue;
282
283 other_widget.setValue(new_value);
284 break;
285 }
286 }
287 }
288 };
289
290 var node = widget.render();
291
292 node.style.minWidth = '4em';
293
294 if (cfgvalue == '-')
295 node.querySelector('li[data-value="*"]').setAttribute('unselectable', '');
296
297 return E('div', { 'style': 'display:inline-block' }, node);
298 },
299
300 cfgvalue: function(section_id) {
301 var pname = this.port,
302 spec = L.toArray(uci.get('network', section_id, 'ports')).filter(function(p) { return p.replace(/:[ut*]+$/, '') == pname })[0];
303
304 if (spec && spec.match(/t/))
305 return spec.match(/\*/) ? ['t', '*'] : ['t'];
306 else if (spec)
307 return spec.match(/\*/) ? ['u', '*'] : ['u'];
308 else
309 return ['-'];
310 },
311
312 write: function(section_id, value) {
313 var ports = [];
314
315 for (var i = 0; i < this.section.children.length; i++) {
316 var opt = this.section.children[i];
317
318 if (opt.port) {
319 var val = L.toArray(opt.formvalue(section_id)).join('');
320
321 switch (val) {
322 case '-':
323 break;
324
325 case 'u':
326 ports.push(opt.port);
327 break;
328
329 default:
330 ports.push('%s:%s'.format(opt.port, val));
331 break;
332 }
333 }
334 }
335
336 uci.set('network', section_id, 'ports', ports);
337 },
338
339 remove: function() {}
340 });
341
342 return baseclass.extend({
343 replaceOption: function(s, tabName, optionClass, optionName, optionTitle, optionDescription) {
344 var o = s.getOption(optionName);
345
346 if (o) {
347 if (o.tab) {
348 s.tabs[o.tab].children = s.tabs[o.tab].children.filter(function(opt) {
349 return opt.option != optionName;
350 });
351 }
352
353 s.children = s.children.filter(function(opt) {
354 return opt.option != optionName;
355 });
356 }
357
358 return s.taboption(tabName, optionClass, optionName, optionTitle, optionDescription);
359 },
360
361 addOption: function(s, tabName, optionClass, optionName, optionTitle, optionDescription) {
362 var o = this.replaceOption(s, tabName, optionClass, optionName, optionTitle, optionDescription);
363
364 if (s.sectiontype == 'interface' && optionName != 'type' && optionName != 'vlan_filtering') {
365 o.cfgvalue = deviceCfgValue;
366 o.write = deviceWrite;
367 o.remove = deviceRemove;
368 o.refresh = deviceRefresh;
369 }
370
371 return o;
372 },
373
374 addDeviceOptions: function(s, dev, isNew) {
375 var isIface = (s.sectiontype == 'interface'),
376 ifc = isIface ? network.instantiateNetwork(s.section) : null,
377 gensection = ifc ? 'physical' : 'devgeneral',
378 advsection = ifc ? 'physical' : 'devadvanced',
379 simpledep = ifc ? { type: '', ifname_single: /^[^@]/ } : { type: '' },
380 disableLegacyBridging = isIface && deviceSectionExists(null, 'br-%s'.format(ifc.getName())),
381 o, ss;
382
383 /* If an externally configured br-xxx interface already exists,
384 * then disable legacy bridge configuration */
385 if (disableLegacyBridging) {
386 o = this.addOption(s, gensection, form.HiddenValue, 'type');
387 o.cfgvalue = function() { return '' };
388 }
389 else if (isIface) {
390 var type;
391
392 type = this.addOption(s, gensection, form.Flag, 'type', _('Bridge interfaces'), _('Creates a bridge over specified interface(s)'));
393 type.modalonly = true;
394 type.disabled = '';
395 type.enabled = 'bridge';
396 type.write = type.remove = function(section_id, value) {
397 var protoname = this.section.formvalue(section_id, 'proto'),
398 protocol = network.getProtocol(protoname),
399 new_ifnames = this.isActive(section_id) ? L.toArray(this.section.formvalue(section_id, value ? 'ifname_multi' : 'ifname_single')) : [];
400
401 if (!protocol.isVirtual() && !this.isActive(section_id))
402 return;
403
404 var old_ifnames = [],
405 devs = ifc.getDevices() || L.toArray(ifc.getDevice());
406
407 for (var i = 0; i < devs.length; i++)
408 old_ifnames.push(devs[i].getName());
409
410 if (!value)
411 new_ifnames.length = Math.max(new_ifnames.length, 1);
412
413 old_ifnames.sort();
414 new_ifnames.sort();
415
416 for (var i = 0; i < Math.max(old_ifnames.length, new_ifnames.length); i++) {
417 if (old_ifnames[i] != new_ifnames[i]) {
418 // backup_ifnames()
419 for (var j = 0; j < old_ifnames.length; j++)
420 ifc.deleteDevice(old_ifnames[j]);
421
422 for (var j = 0; j < new_ifnames.length; j++)
423 ifc.addDevice(new_ifnames[j]);
424
425 break;
426 }
427 }
428
429 if (value)
430 uci.set('network', section_id, 'type', 'bridge');
431 else
432 uci.unset('network', section_id, 'type');
433 };
434 }
435 else {
436 s.tab('devgeneral', _('General device options'));
437 s.tab('devadvanced', _('Advanced device options'));
438 s.tab('brport', _('Bridge port specific options'));
439 s.tab('bridgevlan', _('Bridge VLAN filtering'));
440
441 o = this.addOption(s, gensection, form.ListValue, 'type', _('Device type'));
442 o.readonly = !isNew;
443 o.value('', _('Network device'));
444 o.value('bridge', _('Bridge device'));
445 o.value('8021q', _('VLAN (802.1q)'));
446 o.value('8021ad', _('VLAN (802.1ad)'));
447 o.value('macvlan', _('MAC VLAN'));
448 o.value('veth', _('Virtual Ethernet'));
449
450 o = this.addOption(s, gensection, widgets.DeviceSelect, 'name_simple', _('Existing device'));
451 o.readonly = !isNew;
452 o.rmempty = false;
453 o.noaliases = true;
454 o.default = (dev ? dev.getName() : '');
455 o.ucioption = 'name';
456 o.write = o.remove = setIfActive;
457 o.filter = function(section_id, value) {
458 return !deviceSectionExists(section_id, value);
459 };
460 o.validate = function(section_id, value) {
461 return deviceSectionExists(section_id, value) ? _('A configuration for the device "%s" already exists').format(value) : true;
462 };
463 o.depends('type', '');
464 }
465
466 o = this.addOption(s, gensection, widgets.DeviceSelect, 'ifname_single', isIface ? _('Interface') : _('Base device'));
467 o.readonly = !isNew;
468 o.rmempty = false;
469 o.noaliases = !isIface;
470 o.default = (dev ? dev.getName() : '').match(/^.+\.\d+$/) ? dev.getName().replace(/\.\d+$/, '') : '';
471 o.ucioption = 'ifname';
472 o.validate = function(section_id, value) {
473 var type = this.section.formvalue(section_id, 'type'),
474 name = this.section.getUIElement(section_id, 'name_complex');
475
476 if (type == 'macvlan' && value && name && !name.isChanged()) {
477 var i = 0;
478
479 while (deviceSectionExists(section_id, '%smac%d'.format(value, i)))
480 i++;
481
482 name.setValue('%smac%d'.format(value, i));
483 name.triggerValidation();
484 }
485
486 return true;
487 };
488 if (isIface) {
489 o.write = o.remove = function() {};
490 o.cfgvalue = function(section_id) {
491 return (ifc.getDevices() || L.toArray(ifc.getDevice())).map(function(dev) {
492 return dev.getName();
493 });
494 };
495 o.onchange = function(ev, section_id, values) {
496 for (var i = 0, co; (co = this.section.children[i]) != null; i++)
497 if (co !== this && co.refresh)
498 co.refresh(section_id);
499
500 };
501 o.depends('type', '');
502 }
503 else {
504 o.write = o.remove = setIfActive;
505 o.depends('type', '8021q');
506 o.depends('type', '8021ad');
507 o.depends('type', 'macvlan');
508 }
509
510 o = this.addOption(s, gensection, form.Value, 'vid', _('VLAN ID'));
511 o.readonly = !isNew;
512 o.datatype = 'range(1, 4094)';
513 o.rmempty = false;
514 o.default = (dev ? dev.getName() : '').match(/^.+\.\d+$/) ? dev.getName().replace(/^.+\./, '') : '';
515 o.validate = function(section_id, value) {
516 var base = this.section.formvalue(section_id, 'ifname_single'),
517 vid = this.section.formvalue(section_id, 'vid'),
518 name = this.section.getUIElement(section_id, 'name_complex');
519
520 if (base && vid && name && !name.isChanged()) {
521 name.setValue('%s.%d'.format(base, vid));
522 name.triggerValidation();
523 }
524
525 return true;
526 };
527 o.depends('type', '8021q');
528 o.depends('type', '8021ad');
529
530 o = this.addOption(s, gensection, form.ListValue, 'mode', _('Mode'));
531 o.value('vepa', _('VEPA (Virtual Ethernet Port Aggregator)', 'MACVLAN mode'));
532 o.value('private', _('Private (Prevent communication between MAC VLANs)', 'MACVLAN mode'));
533 o.value('bridge', _('Bridge (Support direct communication between MAC VLANs)', 'MACVLAN mode'));
534 o.value('passthru', _('Pass-through (Mirror physical device to single MAC VLAN)', 'MACVLAN mode'));
535 o.depends('type', 'macvlan');
536
537 if (!isIface) {
538 o = this.addOption(s, gensection, form.Value, 'name_complex', _('Device name'));
539 o.rmempty = false;
540 o.datatype = 'maxlength(15)';
541 o.readonly = !isNew;
542 o.ucioption = 'name';
543 o.write = o.remove = setIfActive;
544 o.validate = function(section_id, value) {
545 return deviceSectionExists(section_id, value) ? _('The device name "%s" is already taken').format(value) : true;
546 };
547 o.depends({ type: '', '!reverse': true });
548 }
549
550 o = this.addOption(s, advsection, form.DynamicList, 'ingress_qos_mapping', _('Ingress QoS mapping'), _('Defines a mapping of VLAN header priority to the Linux internal packet priority on incoming frames'));
551 o.rmempty = true;
552 o.validate = validateQoSMap;
553 o.depends('type', '8021q');
554 o.depends('type', '8021ad');
555
556 o = this.addOption(s, advsection, form.DynamicList, 'egress_qos_mapping', _('Egress QoS mapping'), _('Defines a mapping of Linux internal packet priority to VLAN header priority but for outgoing frames'));
557 o.rmempty = true;
558 o.validate = validateQoSMap;
559 o.depends('type', '8021q');
560 o.depends('type', '8021ad');
561
562 o = this.addOption(s, gensection, widgets.DeviceSelect, 'ifname_multi', _('Bridge ports'));
563 o.size = 10;
564 o.rmempty = true;
565 o.multiple = true;
566 o.noaliases = true;
567 o.nobridges = true;
568 o.ucioption = 'ifname';
569 if (isIface) {
570 o.write = o.remove = function() {};
571 o.cfgvalue = function(section_id) {
572 return (ifc.getDevices() || L.toArray(ifc.getDevice())).map(function(dev) { return dev.getName() });
573 };
574 }
575 else {
576 o.write = o.remove = setIfActive;
577 o.default = L.toArray(dev ? dev.getPorts() : null).filter(function(p) { return p.getType() != 'wifi' }).map(function(p) { return p.getName() });
578 o.filter = function(section_id, device_name) {
579 var bridge_name = uci.get('network', section_id, 'name'),
580 choice_dev = network.instantiateDevice(device_name),
581 parent_dev = choice_dev.getParent();
582
583 /* only show wifi networks which are already present in "option ifname" */
584 if (choice_dev.getType() == 'wifi') {
585 var ifnames = L.toArray(uci.get('network', section_id, 'ifname'));
586
587 for (var i = 0; i < ifnames.length; i++)
588 if (ifnames[i] == device_name)
589 return true;
590
591 return false;
592 }
593
594 return (!parent_dev || parent_dev.getName() != bridge_name);
595 };
596 o.description = _('Specifies the wired ports to attach to this bridge. In order to attach wireless networks, choose the associated interface as network in the wireless settings.')
597 }
598 o.onchange = function(ev, section_id, values) {
599 ss.updatePorts(values);
600
601 return ss.parse().then(function() {
602 ss.redraw();
603 });
604 };
605 o.depends('type', 'bridge');
606
607 o = this.replaceOption(s, gensection, form.Flag, 'bridge_empty', _('Bring up empty bridge'), _('Bring up the bridge interface even if no ports are attached'));
608 o.default = o.disabled;
609 o.depends('type', 'bridge');
610
611 o = this.replaceOption(s, advsection, form.Value, 'priority', _('Priority'));
612 o.placeholder = '32767';
613 o.datatype = 'range(0, 65535)';
614 o.depends('type', 'bridge');
615
616 o = this.replaceOption(s, advsection, form.Value, 'ageing_time', _('Ageing time'), _('Timeout in seconds for learned MAC addresses in the forwarding database'));
617 o.placeholder = '30';
618 o.datatype = 'uinteger';
619 o.depends('type', 'bridge');
620
621 o = this.replaceOption(s, advsection, form.Flag, 'stp', _('Enable <abbr title="Spanning Tree Protocol">STP</abbr>'), _('Enables the Spanning Tree Protocol on this bridge'));
622 o.default = o.disabled;
623 o.depends('type', 'bridge');
624
625 o = this.replaceOption(s, advsection, form.Value, 'hello_time', _('Hello interval'), _('Interval in seconds for STP hello packets'));
626 o.placeholder = '2';
627 o.datatype = 'range(1, 10)';
628 o.depends({ type: 'bridge', stp: '1' });
629
630 o = this.replaceOption(s, advsection, form.Value, 'forward_delay', _('Forward delay'), _('Time in seconds to spend in listening and learning states'));
631 o.placeholder = '15';
632 o.datatype = 'range(2, 30)';
633 o.depends({ type: 'bridge', stp: '1' });
634
635 o = this.replaceOption(s, advsection, form.Value, 'max_age', _('Maximum age'), _('Timeout in seconds until topology updates on link loss'));
636 o.placeholder = '20';
637 o.datatype = 'range(6, 40)';
638 o.depends({ type: 'bridge', stp: '1' });
639
640
641 o = this.replaceOption(s, advsection, form.Flag, 'igmp_snooping', _('Enable <abbr title="Internet Group Management Protocol">IGMP</abbr> snooping'), _('Enables IGMP snooping on this bridge'));
642 o.default = o.disabled;
643 o.depends('type', 'bridge');
644
645 o = this.replaceOption(s, advsection, form.Value, 'hash_max', _('Maximum snooping table size'));
646 o.placeholder = '512';
647 o.datatype = 'uinteger';
648 o.depends({ type: 'bridge', igmp_snooping: '1' });
649
650 o = this.replaceOption(s, advsection, form.Flag, 'multicast_querier', _('Enable multicast querier'));
651 o.defaults = { '1': [{'igmp_snooping': '1'}], '0': [{'igmp_snooping': '0'}] };
652 o.depends('type', 'bridge');
653
654 o = this.replaceOption(s, advsection, form.Value, 'robustness', _('Robustness'), _('The robustness value allows tuning for the expected packet loss on the network. If a network is expected to be lossy, the robustness value may be increased. IGMP is robust to (Robustness-1) packet losses'));
655 o.placeholder = '2';
656 o.datatype = 'min(1)';
657 o.depends({ type: 'bridge', multicast_querier: '1' });
658
659 o = this.replaceOption(s, advsection, form.Value, 'query_interval', _('Query interval'), _('Interval in centiseconds between multicast general queries. By varying the value, an administrator may tune the number of IGMP messages on the subnet; larger values cause IGMP Queries to be sent less often'));
660 o.placeholder = '12500';
661 o.datatype = 'uinteger';
662 o.depends({ type: 'bridge', multicast_querier: '1' });
663
664 o = this.replaceOption(s, advsection, form.Value, 'query_response_interval', _('Query response interval'), _('The max response time in centiseconds inserted into the periodic general queries. By varying the value, an administrator may tune the burstiness of IGMP messages on the subnet; larger values make the traffic less bursty, as host responses are spread out over a larger interval'));
665 o.placeholder = '1000';
666 o.datatype = 'uinteger';
667 o.validate = function(section_id, value) {
668 var qiopt = L.toArray(this.map.lookupOption('query_interval', section_id))[0],
669 qival = qiopt ? (qiopt.formvalue(section_id) || qiopt.placeholder) : '';
670
671 if (value != '' && qival != '' && +value >= +qival)
672 return _('The query response interval must be lower than the query interval value');
673
674 return true;
675 };
676 o.depends({ type: 'bridge', multicast_querier: '1' });
677
678 o = this.replaceOption(s, advsection, form.Value, 'last_member_interval', _('Last member interval'), _('The max response time in centiseconds inserted into group-specific queries sent in response to leave group messages. It is also the amount of time between group-specific query messages. This value may be tuned to modify the "leave latency" of the network. A reduced value results in reduced time to detect the loss of the last member of a group'));
679 o.placeholder = '100';
680 o.datatype = 'uinteger';
681 o.depends({ type: 'bridge', multicast_querier: '1' });
682
683 o = this.addOption(s, gensection, form.Value, 'mtu', _('MTU'));
684 o.placeholder = getDeviceValue(ifc || dev, 'getMTU');
685 o.datatype = 'max(9200)';
686 o.depends(simpledep);
687
688 o = this.addOption(s, gensection, form.Value, 'macaddr', _('MAC address'));
689 o.placeholder = getDeviceValue(ifc || dev, 'getMAC');
690 o.datatype = 'macaddr';
691 o.depends(simpledep);
692 o.depends('type', 'macvlan');
693 o.depends('type', 'veth');
694
695 o = this.addOption(s, gensection, form.Value, 'peer_name', _('Peer device name'));
696 o.rmempty = true;
697 o.datatype = 'maxlength(15)';
698 o.depends('type', 'veth');
699 o.load = function(section_id) {
700 var sections = uci.sections('network', 'device'),
701 idx = 0;
702
703 for (var i = 0; i < sections.length; i++)
704 if (sections[i]['.name'] == section_id)
705 break;
706 else if (sections[i].type == 'veth')
707 idx++;
708
709 this.placeholder = 'veth%d'.format(idx);
710
711 return form.Value.prototype.load.apply(this, arguments);
712 };
713
714 o = this.addOption(s, gensection, form.Value, 'peer_macaddr', _('Peer MAC address'));
715 o.rmempty = true;
716 o.datatype = 'macaddr';
717 o.depends('type', 'veth');
718
719 o = this.addOption(s, gensection, form.Value, 'txqueuelen', _('TX queue length'));
720 o.placeholder = dev ? dev._devstate('qlen') : '';
721 o.datatype = 'uinteger';
722 o.depends(simpledep);
723
724 o = this.addOption(s, advsection, form.Flag, 'promisc', _('Enable promiscious mode'));
725 o.default = o.disabled;
726 o.depends(simpledep);
727
728 o = this.addOption(s, advsection, form.ListValue, 'rpfilter', _('Reverse path filter'));
729 o.default = '';
730 o.value('', _('disabled'));
731 o.value('loose', _('Loose filtering'));
732 o.value('strict', _('Strict filtering'));
733 o.cfgvalue = function(section_id) {
734 var val = form.ListValue.prototype.cfgvalue.apply(this, [section_id]);
735
736 switch (val || '') {
737 case 'loose':
738 case '1':
739 return 'loose';
740
741 case 'strict':
742 case '2':
743 return 'strict';
744
745 default:
746 return '';
747 }
748 };
749 o.depends(simpledep);
750
751 o = this.addOption(s, advsection, form.Flag, 'acceptlocal', _('Accept local'), _('Accept packets with local source addresses'));
752 o.default = o.disabled;
753 o.depends(simpledep);
754
755 o = this.addOption(s, advsection, form.Flag, 'sendredirects', _('Send ICMP redirects'));
756 o.default = o.enabled;
757 o.depends(simpledep);
758
759 o = this.addOption(s, advsection, form.Value, 'neighreachabletime', _('Neighbour cache validity'), _('Time in milliseconds'));
760 o.placeholder = '30000';
761 o.datatype = 'uinteger';
762 o.depends(simpledep);
763
764 o = this.addOption(s, advsection, form.Value, 'neighgcstaletime', _('Stale neighbour cache timeout'), _('Timeout in seconds'));
765 o.placeholder = '60';
766 o.datatype = 'uinteger';
767 o.depends(simpledep);
768
769 o = this.addOption(s, advsection, form.Value, 'neighlocktime', _('Minimum ARP validity time'), _('Minimum required time in seconds before an ARP entry may be replaced. Prevents ARP cache thrashing.'));
770 o.placeholder = '0';
771 o.datatype = 'uinteger';
772 o.depends(simpledep);
773
774 o = this.addOption(s, gensection, form.Flag, 'ipv6', _('Enable IPv6'));
775 o.default = o.enabled;
776 o.depends(simpledep);
777
778 o = this.addOption(s, gensection, form.Value, 'mtu6', _('IPv6 MTU'));
779 o.placeholder = getDeviceValue(ifc || dev, 'getMTU');
780 o.datatype = 'max(9200)';
781 o.depends(Object.assign({ ipv6: '1' }, simpledep));
782
783 o = this.addOption(s, gensection, form.Value, 'dadtransmits', _('DAD transmits'), _('Amount of Duplicate Address Detection probes to send'));
784 o.placeholder = '1';
785 o.datatype = 'uinteger';
786 o.depends(Object.assign({ ipv6: '1' }, simpledep));
787
788
789 o = this.addOption(s, advsection, form.Flag, 'multicast', _('Enable multicast support'));
790 o.default = o.enabled;
791 o.depends(simpledep);
792
793 o = this.addOption(s, advsection, form.ListValue, 'igmpversion', _('Force IGMP version'));
794 o.value('', _('No enforcement'));
795 o.value('1', _('Enforce IGMPv1'));
796 o.value('2', _('Enforce IGMPv2'));
797 o.value('3', _('Enforce IGMPv3'));
798 o.depends(Object.assign({ multicast: '1' }, simpledep));
799
800 o = this.addOption(s, advsection, form.ListValue, 'mldversion', _('Force MLD version'));
801 o.value('', _('No enforcement'));
802 o.value('1', _('Enforce MLD version 1'));
803 o.value('2', _('Enforce MLD version 2'));
804 o.depends(Object.assign({ multicast: '1' }, simpledep));
805
806 if (isBridgePort(dev)) {
807 o = this.addOption(s, 'brport', form.Flag, 'learning', _('Enable MAC address learning'));
808 o.default = o.enabled;
809 o.depends(simpledep);
810
811 o = this.addOption(s, 'brport', form.Flag, 'unicast_flood', _('Enable unicast flooding'));
812 o.default = o.enabled;
813 o.depends(simpledep);
814
815 o = this.addOption(s, 'brport', form.Flag, 'isolated', _('Port isolation'), _('Only allow communication with non-isolated bridge ports when enabled'));
816 o.default = o.disabled;
817 o.depends(simpledep);
818
819 o = this.addOption(s, 'brport', form.ListValue, 'multicast_router', _('Multicast routing'));
820 o.value('', _('Never'));
821 o.value('1', _('Learn'));
822 o.value('2', _('Always'));
823 o.depends(Object.assign({ multicast: '1' }, simpledep));
824
825 o = this.addOption(s, 'brport', form.Flag, 'multicast_to_unicast', _('Multicast to unicast'), _('Forward multicast packets as unicast packets on this device.'));
826 o.default = o.disabled;
827 o.depends(Object.assign({ multicast: '1' }, simpledep));
828
829 o = this.addOption(s, 'brport', form.Flag, 'multicast_fast_leave', _('Enable multicast fast leave'));
830 o.default = o.disabled;
831 o.depends(Object.assign({ multicast: '1' }, simpledep));
832 }
833
834 o = this.addOption(s, 'bridgevlan', form.Flag, 'vlan_filtering', _('Enable VLAN filterering'));
835 o.depends('type', 'bridge');
836 o.updateDefaultValue = function(section_id) {
837 var device = isIface ? 'br-%s'.format(s.section) : uci.get('network', s.section, 'name'),
838 uielem = this.getUIElement(section_id),
839 has_vlans = false;
840
841 uci.sections('network', 'bridge-vlan', function(bvs) {
842 has_vlans = has_vlans || (bvs.device == device);
843 });
844
845 this.default = has_vlans ? this.enabled : this.disabled;
846
847 if (uielem && !uielem.isChanged())
848 uielem.setValue(this.default);
849 };
850
851 o = this.addOption(s, 'bridgevlan', form.SectionValue, 'bridge-vlan', form.TableSection, 'bridge-vlan');
852 o.depends('type', 'bridge');
853 o.renderWidget = function(/* ... */) {
854 return form.SectionValue.prototype.renderWidget.apply(this, arguments).then(L.bind(function(node) {
855 node.style.overflowX = 'auto';
856 node.style.overflowY = 'visible';
857 node.style.paddingBottom = '100px';
858 node.style.marginBottom = '-100px';
859
860 return node;
861 }, this));
862 };
863
864 ss = o.subsection;
865 ss.addremove = true;
866 ss.anonymous = true;
867
868 ss.renderHeaderRows = function(/* ... */) {
869 var node = form.TableSection.prototype.renderHeaderRows.apply(this, arguments);
870
871 node.querySelectorAll('.th').forEach(function(th) {
872 th.classList.add('middle');
873 });
874
875 return node;
876 };
877
878 ss.filter = function(section_id) {
879 var devname = isIface ? 'br-%s'.format(s.section) : uci.get('network', s.section, 'name');
880 return (uci.get('network', section_id, 'device') == devname);
881 };
882
883 ss.render = function(/* ... */) {
884 return form.TableSection.prototype.render.apply(this, arguments).then(L.bind(function(node) {
885 if (this.node)
886 this.node.parentNode.replaceChild(node, this.node);
887
888 this.node = node;
889
890 return node;
891 }, this));
892 };
893
894 ss.redraw = function() {
895 return this.load().then(L.bind(this.render, this));
896 };
897
898 ss.updatePorts = function(ports) {
899 var devices = ports.map(function(port) {
900 return network.instantiateDevice(port)
901 }).filter(function(dev) {
902 return dev.getType() != 'wifi' || dev.isUp();
903 });
904
905 this.children = this.children.filter(function(opt) { return !opt.option.match(/^port_/) });
906
907 for (var i = 0; i < devices.length; i++) {
908 o = ss.option(cbiTagValue, 'port_%s'.format(sfh(devices[i].getName())), renderDevBadge(devices[i]));
909 o.port = devices[i].getName();
910 }
911
912 var section_ids = this.cfgsections(),
913 device_names = devices.reduce(function(names, dev) { names[dev.getName()] = true; return names }, {});
914
915 for (var i = 0; i < section_ids.length; i++) {
916 var old_spec = L.toArray(uci.get('network', section_ids[i], 'ports')),
917 new_spec = old_spec.filter(function(spec) { return device_names[spec.replace(/:[ut*]+$/, '')] });
918
919 if (old_spec.length != new_spec.length)
920 uci.set('network', section_ids[i], 'ports', new_spec.length ? new_spec : null);
921 }
922 };
923
924 ss.handleAdd = function(ev) {
925 return s.parse().then(L.bind(function() {
926 var device = isIface ? 'br-%s'.format(s.section) : uci.get('network', s.section, 'name'),
927 section_ids = this.cfgsections(),
928 section_id = null,
929 max_vlan_id = 0;
930
931 if (!device)
932 return;
933
934 for (var i = 0; i < section_ids.length; i++) {
935 var vid = +uci.get('network', section_ids[i], 'vlan');
936
937 if (vid > max_vlan_id)
938 max_vlan_id = vid;
939 }
940
941 section_id = uci.add('network', 'bridge-vlan');
942 uci.set('network', section_id, 'device', device);
943 uci.set('network', section_id, 'vlan', max_vlan_id + 1);
944
945 s.children.forEach(function(opt) {
946 switch (opt.option) {
947 case 'type':
948 case 'name_complex':
949 var input = opt.map.findElement('id', 'widget.%s'.format(opt.cbid(s.section)));
950 if (input)
951 input.disabled = true;
952 break;
953 }
954 });
955
956 s.getOption('vlan_filtering').updateDefaultValue(s.section);
957
958 return this.redraw();
959 }, this));
960 };
961
962 o = ss.option(form.Value, 'vlan', _('VLAN ID'));
963 o.datatype = 'range(1, 4094)';
964
965 o.renderWidget = function(/* ... */) {
966 var node = form.Value.prototype.renderWidget.apply(this, arguments);
967
968 node.style.width = '5em';
969
970 return node;
971 };
972
973 o.validate = function(section_id, value) {
974 var section_ids = this.section.cfgsections();
975
976 for (var i = 0; i < section_ids.length; i++) {
977 if (section_ids[i] == section_id)
978 continue;
979
980 if (uci.get('network', section_ids[i], 'vlan') == value)
981 return _('The VLAN ID must be unique');
982 }
983
984 return true;
985 };
986
987 o = ss.option(form.Flag, 'local', _('Local'));
988 o.default = o.enabled;
989
990 /* Do not touch bridge port state from interface config if legacy
991 * bridge config is disabled due to explicitely declared br-xxx
992 * device section... */
993 if (disableLegacyBridging)
994 return;
995
996 var ports = [];
997
998 if (isIface) {
999 Array.prototype.push.apply(ports, L.toArray(ifc.getDevices() || ifc.getDevice()).map(function(dev) {
1000 return dev.getName();
1001 }));
1002 }
1003 else {
1004 var seen_ports = {};
1005
1006 L.toArray(uci.get('network', s.section, 'ifname')).forEach(function(ifname) {
1007 seen_ports[ifname] = true;
1008 });
1009
1010 uci.sections('network', 'bridge-vlan', function(bvs) {
1011 L.toArray(bvs.ports).forEach(function(portspec) {
1012 var m = portspec.match(/^([^:]+)(?::[ut*]+)?$/);
1013
1014 if (m)
1015 seen_ports[m[1]] = true;
1016 });
1017 });
1018
1019 for (var port_name in seen_ports)
1020 ports.push(port_name);
1021 }
1022
1023 ports.sort();
1024
1025 ss.updatePorts(ports);
1026 }
1027 });