diff options
author | Christian Poessinger <christian@poessinger.com> | 2020-09-19 21:08:40 +0200 |
---|---|---|
committer | Christian Poessinger <christian@poessinger.com> | 2020-09-19 21:08:40 +0200 |
commit | d1c9ee33f25e45cea0d01f9685f99c960ed4d7f8 (patch) | |
tree | 72fdd97c9050f0e77a7a5bc744de3266e4058cac /python/vyos/ifconfig/interface.py | |
parent | 14c754f8bd6c96165d8ad3745c19c80a562910e1 (diff) | |
download | vyos-1x-d1c9ee33f25e45cea0d01f9685f99c960ed4d7f8.tar.gz vyos-1x-d1c9ee33f25e45cea0d01f9685f99c960ed4d7f8.zip |
ifconfig: T2653: convert VLAN interfaces do discrete class
Instead of using an Adapter pattern to make interfaces VLAN-aware, create a
derived class named VLANIf to represent a VLAN. This change was necessary to
eliminate mixed code in Interfaces class which was VLAN - free, but recently
gained some VLAN specific code for set_admin_state().
In addition this "autoresolves" the issue in T2894 as a bond vlan interface
will no longer change the lower interface.
Diffstat (limited to 'python/vyos/ifconfig/interface.py')
-rw-r--r-- | python/vyos/ifconfig/interface.py | 190 |
1 files changed, 146 insertions, 44 deletions
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index ffe69f61b..0774235b6 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -86,10 +86,6 @@ class Interface(Control): 'shellcmd': 'ip -json link show dev {ifname}', 'format': lambda j: 'up' if 'UP' in jmespath.search('[*].flags | [0]', json.loads(j)) else 'down', }, - 'vlan_protocol': { - 'shellcmd': 'ip -json -details link show dev {ifname}', - 'format': lambda j: jmespath.search('[*].linkinfo.info_data.protocol | [0]', json.loads(j)), - }, } _command_set = { @@ -562,17 +558,6 @@ class Interface(Control): """ self.set_interface('alias', ifalias) - def get_vlan_protocol(self): - """ - Retrieve VLAN protocol in use, this can be 802.1Q, 802.1ad or None - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0.10').get_vlan_protocol() - '802.1Q' - """ - return self.get_interface('vlan_protocol') - def get_admin_state(self): """ Get interface administrative state. Function will return 'up' or 'down' @@ -594,17 +579,6 @@ class Interface(Control): >>> Interface('eth0').get_admin_state() 'down' """ - # A VLAN interface can only be placed in admin up state when - # the lower interface is up, too - if self.get_vlan_protocol(): - lower_interface = glob(f'/sys/class/net/{self.ifname}/lower*/flags')[0] - with open(lower_interface, 'r') as f: - flags = f.read() - # If parent is not up - bail out as we can not bring up the VLAN. - # Flags are defined in kernel source include/uapi/linux/if.h - if not int(flags, 16) & 1: - return None - if state == 'up': self._admin_state_down_cnt -= 1 if self._admin_state_down_cnt < 1: @@ -1031,33 +1005,161 @@ class Interface(Control): self.add_to_bridge(bridge) # remove no longer required 802.1ad (Q-in-Q VLANs) + ifname = config['ifname'] for vif_s_id in config.get('vif_s_remove', {}): - self.del_vlan(vif_s_id) + vif_s_ifname = f'{ifname}.{vif_s_id}' + VLANIf(vif_s_ifname).remove() # create/update 802.1ad (Q-in-Q VLANs) - ifname = config['ifname'] - for vif_s_id, vif_s in config.get('vif_s', {}).items(): - tmp=get_ethertype(vif_s.get('ethertype', '0x88A8')) - s_vlan = self.add_vlan(vif_s_id, ethertype=tmp) - vif_s['ifname'] = f'{ifname}.{vif_s_id}' - s_vlan.update(vif_s) + for vif_s_id, vif_s_config in config.get('vif_s', {}).items(): + tmp = deepcopy(VLANIf.get_config()) + tmp['ethertype'] = get_ethertype(vif_s_config.get('ethertype', '0x88A8')) + tmp['source_interface'] = ifname + tmp['vlan_id'] = vif_s_id + + vif_s_ifname = f'{ifname}.{vif_s_id}' + vif_s_config['ifname'] = vif_s_ifname + s_vlan = VLANIf(vif_s_ifname, **tmp) + s_vlan.update(vif_s_config) # remove no longer required client VLAN (vif-c) - for vif_c_id in vif_s.get('vif_c_remove', {}): - s_vlan.del_vlan(vif_c_id) + for vif_c_id in vif_s_config.get('vif_c_remove', {}): + vif_c_ifname = f'{vif_s_ifname}.{vif_c_id}' + VLANIf(vif_c_ifname).remove() # create/update client VLAN (vif-c) interface - for vif_c_id, vif_c in vif_s.get('vif_c', {}).items(): - c_vlan = s_vlan.add_vlan(vif_c_id) - vif_c['ifname'] = f'{ifname}.{vif_s_id}.{vif_c_id}' - c_vlan.update(vif_c) + for vif_c_id, vif_c_config in vif_s_config.get('vif_c', {}).items(): + tmp = deepcopy(VLANIf.get_config()) + tmp['source_interface'] = vif_s_ifname + tmp['vlan_id'] = vif_c_id + + vif_c_ifname = f'{vif_s_ifname}.{vif_c_id}' + vif_c_config['ifname'] = vif_c_ifname + c_vlan = VLANIf(vif_c_ifname, **tmp) + c_vlan.update(vif_c_config) # remove no longer required 802.1q VLAN interfaces for vif_id in config.get('vif_remove', {}): - self.del_vlan(vif_id) + vif_ifname = f'{ifname}.{vif_id}' + VLANIf(vif_ifname).remove() # create/update 802.1q VLAN interfaces - for vif_id, vif in config.get('vif', {}).items(): - vlan = self.add_vlan(vif_id) - vif['ifname'] = f'{ifname}.{vif_id}' - vlan.update(vif) + for vif_id, vif_config in config.get('vif', {}).items(): + tmp = deepcopy(VLANIf.get_config()) + tmp['source_interface'] = ifname + tmp['vlan_id'] = vif_id + + vif_ifname = f'{ifname}.{vif_id}' + vif_config['ifname'] = vif_ifname + vlan = VLANIf(vif_ifname, **tmp) + vlan.update(vif_config) + + +class VLANIf(Interface): + """ Specific class which abstracts 802.1q and 802.1ad (Q-in-Q) VLAN interfaces """ + default = { + 'type': 'vlan', + 'source_interface': '', + 'vlan_id': '', + 'ethertype': '', + 'ingress_qos': '', + 'egress_qos': '', + } + + options = Interface.options + \ + ['source_interface', 'vlan_id', 'ethertype', 'ingress_qos', 'egress_qos'] + + def remove(self): + """ + Remove interface from operating system. Removing the interface + deconfigures all assigned IP addresses and clear possible DHCP(v6) + client processes. + + Example: + >>> from vyos.ifconfig import Interface + >>> i = Interface('eth0') + >>> i.remove() + """ + # Do we have sub interfaces (VLANs)? As interfaces need to be deleted + # "in order" starting from Q-in-Q we delete them first. + for upper in glob(f'/sys/class/net/{self.ifname}/upper*'): + # an upper interface could be named: upper_bond0.1000.1100, thus + # we need top drop the upper_ prefix + vif_c = os.path.basename(upper) + vif_c = vif_c.replace('upper_', '') + VLANIf(vif_c).remove() + + super().remove() + + def _create(self): + # bail out early if interface already exists + if os.path.exists(f'/sys/class/net/{self.ifname}'): + return + + cmd = 'ip link add link {source_interface} name {ifname} type vlan id {vlan_id}' + if self.config['ethertype']: + cmd += ' proto {ethertype}' + if self.config['ingress_qos']: + cmd += ' ingress-qos-map {ingress_qos}' + if self.config['egress_qos']: + cmd += ' egress-qos-map {egress_qos}' + + self._cmd(cmd.format(**self.config)) + + # interface is always A/D down. It needs to be enabled explicitly + self.set_admin_state('down') + + @staticmethod + def get_config(): + """ + MACsec interfaces require a configuration when they are added using + iproute2. This static method will provide the configuration dictionary + used by this class. + + Example: + >> dict = MACsecIf().get_config() + """ + config = deepcopy(__class__.default) + del config['type'] + return config + + def set_admin_state(self, state): + """ + Set interface administrative state to be 'up' or 'down' + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_admin_state('down') + >>> Interface('eth0').get_admin_state() + 'down' + """ + # A VLAN interface can only be placed in admin up state when + # the lower interface is up, too + lower_interface = glob(f'/sys/class/net/{self.ifname}/lower*/flags')[0] + with open(lower_interface, 'r') as f: + flags = f.read() + # If parent is not up - bail out as we can not bring up the VLAN. + # Flags are defined in kernel source include/uapi/linux/if.h + if not int(flags, 16) & 1: + return None + + return super().set_admin_state(state) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) |