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