luci-mod-network: add support for bridge vlan filtering
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
This commit is contained in:
parent
faad7464a8
commit
eeef38d534
2 changed files with 359 additions and 1 deletions
|
@ -1,4 +1,5 @@
|
|||
'use strict';
|
||||
'require ui';
|
||||
'require uci';
|
||||
'require form';
|
||||
'require network';
|
||||
|
@ -85,6 +86,18 @@ function isBridgePort(dev) {
|
|||
return isPort;
|
||||
}
|
||||
|
||||
function renderDevBadge(dev) {
|
||||
var type = dev.getType(), up = dev.isUp();
|
||||
|
||||
return E('span', { 'class': 'ifacebadge', 'style': 'font-weight:normal' }, [
|
||||
E('img', {
|
||||
'class': 'middle',
|
||||
'src': L.resource('icons/%s%s.png').format(type, up ? '' : '_disabled')
|
||||
}),
|
||||
' ', dev.getName()
|
||||
]);
|
||||
}
|
||||
|
||||
function lookupDevName(s, section_id) {
|
||||
var typeui = s.getUIElement(section_id, 'type'),
|
||||
typeval = typeui ? typeui.getValue() : s.cfgvalue(section_id, 'type'),
|
||||
|
@ -180,6 +193,162 @@ function deviceRefresh(section_id) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
var cbiTagValue = form.Value.extend({
|
||||
renderWidget: function(section_id, option_index, cfgvalue) {
|
||||
var widget = new ui.Dropdown(cfgvalue || ['-'], {
|
||||
'-': E([], [
|
||||
E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ '—' ]),
|
||||
E('span', { 'class': 'hide-close' }, [ _('Do not participate', 'VLAN port state') ])
|
||||
]),
|
||||
'u': E([], [
|
||||
E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ 'u' ]),
|
||||
E('span', { 'class': 'hide-close' }, [ _('Egress untagged', 'VLAN port state') ])
|
||||
]),
|
||||
't': E([], [
|
||||
E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ 't' ]),
|
||||
E('span', { 'class': 'hide-close' }, [ _('Egress tagged', 'VLAN port state') ])
|
||||
]),
|
||||
'*': E([], [
|
||||
E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ '*' ]),
|
||||
E('span', { 'class': 'hide-close' }, [ _('Primary VLAN ID', 'VLAN port state') ])
|
||||
])
|
||||
}, {
|
||||
id: this.cbid(section_id),
|
||||
sort: [ '-', 'u', 't', '*' ],
|
||||
optional: false,
|
||||
multiple: true
|
||||
});
|
||||
|
||||
var field = this;
|
||||
|
||||
widget.toggleItem = function(sb, li, force_state) {
|
||||
var lis = li.parentNode.querySelectorAll('li'),
|
||||
toggle = ui.Dropdown.prototype.toggleItem;
|
||||
|
||||
toggle.apply(this, [sb, li, force_state]);
|
||||
|
||||
if (force_state != null)
|
||||
return;
|
||||
|
||||
switch (li.getAttribute('data-value'))
|
||||
{
|
||||
case '-':
|
||||
if (li.hasAttribute('selected')) {
|
||||
for (var i = 0; i < lis.length; i++) {
|
||||
switch (lis[i].getAttribute('data-value')) {
|
||||
case '-':
|
||||
break;
|
||||
|
||||
case '*':
|
||||
toggle.apply(this, [sb, lis[i], false]);
|
||||
lis[i].setAttribute('unselectable', '');
|
||||
break;
|
||||
|
||||
default:
|
||||
toggle.apply(this, [sb, lis[i], false]);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 't':
|
||||
case 'u':
|
||||
if (li.hasAttribute('selected')) {
|
||||
for (var i = 0; i < lis.length; i++) {
|
||||
switch (lis[i].getAttribute('data-value')) {
|
||||
case li.getAttribute('data-value'):
|
||||
break;
|
||||
|
||||
case '*':
|
||||
lis[i].removeAttribute('unselectable');
|
||||
break;
|
||||
|
||||
default:
|
||||
toggle.apply(this, [sb, lis[i], false]);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
toggle.apply(this, [sb, li, true]);
|
||||
}
|
||||
break;
|
||||
|
||||
case '*':
|
||||
if (li.hasAttribute('selected')) {
|
||||
var section_ids = field.section.cfgsections();
|
||||
|
||||
for (var i = 0; i < section_ids.length; i++) {
|
||||
var other_widget = field.getUIElement(section_ids[i]),
|
||||
other_value = L.toArray(other_widget.getValue());
|
||||
|
||||
if (other_widget === this)
|
||||
continue;
|
||||
|
||||
var new_value = other_value.filter(function(v) { return v != '*' });
|
||||
|
||||
if (new_value.length == other_value.length)
|
||||
continue;
|
||||
|
||||
other_widget.setValue(new_value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var node = widget.render();
|
||||
|
||||
node.style.minWidth = '4em';
|
||||
|
||||
if (cfgvalue == '-')
|
||||
node.querySelector('li[data-value="*"]').setAttribute('unselectable', '');
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
cfgvalue: function(section_id) {
|
||||
var pname = this.port,
|
||||
spec = L.toArray(uci.get('network', section_id, 'ports')).filter(function(p) { return p.replace(/:[ut*]+$/, '') == pname })[0];
|
||||
|
||||
if (spec && spec.match(/t/))
|
||||
return spec.match(/\*/) ? ['t', '*'] : ['t'];
|
||||
else if (spec)
|
||||
return spec.match(/\*/) ? ['u', '*'] : ['u'];
|
||||
else
|
||||
return ['-'];
|
||||
},
|
||||
|
||||
write: function(section_id, value) {
|
||||
var ports = [];
|
||||
|
||||
for (var i = 0; i < this.section.children.length; i++) {
|
||||
var opt = this.section.children[i];
|
||||
|
||||
if (opt.port) {
|
||||
var val = L.toArray(opt.formvalue(section_id)).join('');
|
||||
|
||||
switch (val) {
|
||||
case '-':
|
||||
break;
|
||||
|
||||
case 'u':
|
||||
ports.push(opt.port);
|
||||
break;
|
||||
|
||||
default:
|
||||
ports.push('%s:%s'.format(opt.port, val));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uci.set('network', section_id, 'ports', ports);
|
||||
},
|
||||
|
||||
remove: function() {}
|
||||
});
|
||||
|
||||
return baseclass.extend({
|
||||
replaceOption: function(s, tabName, optionClass, optionName, optionTitle, optionDescription) {
|
||||
var o = s.getOption(optionName);
|
||||
|
@ -649,5 +818,167 @@ return baseclass.extend({
|
|||
o.default = o.disabled;
|
||||
o.depends(Object.assign({ multicast: '1' }, simpledep));
|
||||
}
|
||||
|
||||
o = this.addOption(s, 'bridgevlan', form.Flag, 'vlan_filtering', _('Enable VLAN filterering'));
|
||||
o.depends('type', 'bridge');
|
||||
o.updateDefaultValue = function(section_id) {
|
||||
var device = isIface ? 'br-%s'.format(s.section) : uci.get('network', s.section, 'name'),
|
||||
uielem = this.getUIElement(section_id),
|
||||
has_vlans = false;
|
||||
|
||||
uci.sections('network', 'bridge-vlan', function(bvs) {
|
||||
has_vlans = has_vlans || (bvs.device == device);
|
||||
});
|
||||
|
||||
this.default = has_vlans ? this.enabled : this.disabled;
|
||||
|
||||
if (uielem && !uielem.isChanged())
|
||||
uielem.setValue(this.default);
|
||||
};
|
||||
|
||||
o = this.addOption(s, 'bridgevlan', form.SectionValue, 'bridge-vlan', form.TableSection, 'bridge-vlan');
|
||||
o.depends('type', 'bridge');
|
||||
o.renderWidget = function(/* ... */) {
|
||||
return form.SectionValue.prototype.renderWidget.apply(this, arguments).then(L.bind(function(node) {
|
||||
node.style.overflowX = 'auto';
|
||||
node.style.overflowY = 'visible';
|
||||
node.style.paddingBottom = '100px';
|
||||
node.style.marginBottom = '-100px';
|
||||
|
||||
return node;
|
||||
}, this));
|
||||
};
|
||||
|
||||
ss = o.subsection;
|
||||
ss.addremove = true;
|
||||
ss.anonymous = true;
|
||||
|
||||
ss.renderHeaderRows = function(/* ... */) {
|
||||
var node = form.TableSection.prototype.renderHeaderRows.apply(this, arguments);
|
||||
|
||||
node.querySelectorAll('.th').forEach(function(th) {
|
||||
th.classList.add('middle');
|
||||
});
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
ss.filter = function(section_id) {
|
||||
var devname = isIface ? 'br-%s'.format(s.section) : uci.get('network', s.section, 'name');
|
||||
return (uci.get('network', section_id, 'device') == devname);
|
||||
};
|
||||
|
||||
ss.render = function(/* ... */) {
|
||||
return form.TableSection.prototype.render.apply(this, arguments).then(L.bind(function(node) {
|
||||
if (this.node)
|
||||
this.node.parentNode.replaceChild(node, this.node);
|
||||
|
||||
this.node = node;
|
||||
|
||||
return node;
|
||||
}, this));
|
||||
};
|
||||
|
||||
ss.redraw = function() {
|
||||
return this.load().then(L.bind(this.render, this));
|
||||
};
|
||||
|
||||
ss.updatePorts = function(ports) {
|
||||
var devices = ports.map(function(port) {
|
||||
return network.instantiateDevice(port)
|
||||
}).filter(function(dev) {
|
||||
return dev.getType() != 'wifi' || dev.isUp();
|
||||
});
|
||||
|
||||
this.children = this.children.filter(function(opt) { return !opt.option.match(/^port_/) });
|
||||
|
||||
for (var i = 0; i < devices.length; i++) {
|
||||
o = ss.option(cbiTagValue, 'port_%s'.format(sfh(devices[i].getName())), renderDevBadge(devices[i]));
|
||||
o.port = devices[i].getName();
|
||||
}
|
||||
|
||||
var section_ids = this.cfgsections(),
|
||||
device_names = devices.reduce(function(names, dev) { names[dev.getName()] = true; return names }, {});
|
||||
|
||||
for (var i = 0; i < section_ids.length; i++) {
|
||||
var old_spec = L.toArray(uci.get('network', section_ids[i], 'ports')),
|
||||
new_spec = old_spec.filter(function(spec) { return device_names[spec.replace(/:[ut*]+$/, '')] });
|
||||
|
||||
if (old_spec.length != new_spec.length)
|
||||
uci.set('network', section_ids[i], 'ports', new_spec.length ? new_spec : null);
|
||||
}
|
||||
};
|
||||
|
||||
ss.handleAdd = function(ev) {
|
||||
return s.parse().then(L.bind(function() {
|
||||
var device = isIface ? 'br-%s'.format(s.section) : uci.get('network', s.section, 'name'),
|
||||
section_ids = this.cfgsections(),
|
||||
section_id = null,
|
||||
max_vlan_id = 0;
|
||||
|
||||
if (!device)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < section_ids.length; i++) {
|
||||
var vid = +uci.get('network', section_ids[i], 'vlan');
|
||||
|
||||
if (vid > max_vlan_id)
|
||||
max_vlan_id = vid;
|
||||
}
|
||||
|
||||
section_id = uci.add('network', 'bridge-vlan');
|
||||
uci.set('network', section_id, 'device', device);
|
||||
uci.set('network', section_id, 'vlan', max_vlan_id + 1);
|
||||
|
||||
s.children.forEach(function(opt) {
|
||||
switch (opt.option) {
|
||||
case 'type':
|
||||
case 'name_complex':
|
||||
var input = opt.map.findElement('id', 'widget.%s'.format(opt.cbid(s.section)));
|
||||
if (input)
|
||||
input.disabled = true;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
s.getOption('vlan_filtering').updateDefaultValue(s.section);
|
||||
|
||||
return this.redraw();
|
||||
}, this));
|
||||
};
|
||||
|
||||
o = ss.option(form.Value, 'vlan', _('VLAN ID'));
|
||||
o.datatype = 'range(1, 4094)';
|
||||
|
||||
o.renderWidget = function(/* ... */) {
|
||||
var node = form.Value.prototype.renderWidget.apply(this, arguments);
|
||||
|
||||
node.style.width = '5em';
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
o.validate = function(section_id, value) {
|
||||
var section_ids = this.section.cfgsections();
|
||||
|
||||
for (var i = 0; i < section_ids.length; i++) {
|
||||
if (section_ids[i] == section_id)
|
||||
continue;
|
||||
|
||||
if (uci.get('network', section_ids[i], 'vlan') == value)
|
||||
return _('The VLAN ID must be unique');
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
o = ss.option(form.Flag, 'local', _('Local'));
|
||||
o.default = o.enabled;
|
||||
|
||||
var ports = isIface
|
||||
? (ifc.getDevices() || L.toArray(ifc.getDevice())).map(function(dev) { return dev.getName() })
|
||||
: L.toArray(uci.get('network', s.section, 'ifname'));
|
||||
|
||||
ss.updatePorts(ports);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -333,6 +333,7 @@ return view.extend({
|
|||
s.tab('advanced', _('Advanced Settings'));
|
||||
s.tab('physical', _('Physical Settings'));
|
||||
s.tab('brport', _('Bridge port specific options'));
|
||||
s.tab('bridgevlan', _('Bridge VLAN filtering'));
|
||||
s.tab('firewall', _('Firewall Settings'));
|
||||
s.tab('dhcp', _('DHCP Server'));
|
||||
|
||||
|
@ -717,9 +718,23 @@ return view.extend({
|
|||
o.depends('proto', protoval);
|
||||
}
|
||||
}
|
||||
|
||||
this.activeSection = s.section;
|
||||
}, this));
|
||||
};
|
||||
|
||||
s.handleModalCancel = function(/* ... */) {
|
||||
var type = uci.get('network', this.activeSection || this.addedSection, 'type'),
|
||||
ifname = (type == 'bridge') ? 'br-%s'.format(this.activeSection || this.addedSection) : null;
|
||||
|
||||
uci.sections('network', 'bridge-vlan', function(bvs) {
|
||||
if (ifname != null && bvs.device == ifname)
|
||||
uci.remove('network', bvs['.name']);
|
||||
});
|
||||
|
||||
return form.GridSection.prototype.handleModalCancel.apply(this, arguments);
|
||||
};
|
||||
|
||||
s.handleAdd = function(ev) {
|
||||
var m2 = new form.Map('network'),
|
||||
s2 = m2.section(form.NamedSection, '_new_'),
|
||||
|
@ -823,8 +838,9 @@ return view.extend({
|
|||
protoclass.addDevice(dev);
|
||||
});
|
||||
}
|
||||
}).then(L.bind(m.children[0].renderMoreOptionsModal, m.children[0], nameval));
|
||||
|
||||
m.children[0].addedSection = section_id;
|
||||
}).then(L.bind(m.children[0].renderMoreOptionsModal, m.children[0], nameval));
|
||||
});
|
||||
})
|
||||
}, _('Create interface'))
|
||||
|
@ -974,6 +990,17 @@ return view.extend({
|
|||
nettools.addDeviceOptions(s, dev, isNew);
|
||||
};
|
||||
|
||||
s.handleModalCancel = function(/* ... */) {
|
||||
var name = uci.get('network', this.addedSection, 'name')
|
||||
|
||||
uci.sections('network', 'bridge-vlan', function(bvs) {
|
||||
if (name != null && bvs.device == name)
|
||||
uci.remove('network', bvs['.name']);
|
||||
});
|
||||
|
||||
return form.GridSection.prototype.handleModalCancel.apply(this, arguments);
|
||||
};
|
||||
|
||||
function getDevice(section_id) {
|
||||
var m = section_id.match(/^dev:(.+)$/),
|
||||
name = m ? m[1] : uci.get('network', section_id, 'name');
|
||||
|
|
Loading…
Reference in a new issue