summaryrefslogtreecommitdiff
path: root/src/conf_mode/vpp_interfaces_bonding.py
blob: 932287a039cc7d73cb4e488ba9dd3baf20701bf6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
#!/usr/bin/env python3
#
# Copyright (C) VyOS Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configdict import is_node_changed
from vyos.configdep import set_dependents, call_dependents
from vyos.configverify import verify_mtu_ipv6
from vyos import ConfigError
from vyos.utils.assertion import assert_mac
from vyos.utils.process import is_systemd_service_active

from vyos.ifconfig import Interface
from vyos.ifconfig.vpp import VPPBondInterface
from vyos.vpp.config_deps import deps_bond_dict
from vyos.vpp.config_deps import deps_bridge_dict
from vyos.vpp.config_deps import deps_xconnect_dict
from vyos.vpp.config_verify import verify_member_conflicts
from vyos.vpp.config_verify import verify_vpp_remove_bridge_interface
from vyos.vpp.config_verify import verify_vpp_remove_xconnect_interface
from vyos.vpp.config_verify import verify_vpp_remove_interface
from vyos.vpp.config_verify import verify_vpp_interface_not_in_feature
from vyos.vpp.utils import cli_ifaces_list


def _get_bond_mode(mode_name: str) -> int:
    """Convert VyOS CLI name bonding mode to VPP compatible"""
    mode_mapping = {
        'round-robin': 1,
        'active-backup': 2,
        'xor-hash': 3,
        'broadcast': 4,
        '802.3ad': 5,
    }

    return mode_mapping.get(mode_name, 5)


def _get_bond_lb(lb_name: str) -> int:
    """Convert VyOS CLI name bonding load balance to VPP compatible"""
    lb_mapping = {
        'layer2': 0,
        'layer2+3': 2,
        'layer3+4': 1,
    }

    return lb_mapping.get(lb_name, 0)


def get_config(config=None) -> dict:
    """Get Bonding interface configuration

    Args:
        config (vyos.config.Config, optional): The VyOS configuration dictionary
    Returns:
        dict: Bonding interface configuration
    """
    if config:
        conf = config
    else:
        conf = Config()

    base = ['interfaces', 'vpp', 'bonding']

    ifname, config = get_interface_dict(conf, base)

    # Get pppoe-server interfaces
    config['pppoe_ifaces'] = conf.list_nodes(['service', 'pppoe-server', 'interface'])

    if not conf.exists(['vpp']) and not conf.exists(base):
        config['remove_vpp'] = True
        return config

    config['vpp_ifaces'] = cli_ifaces_list(conf, 'candidate')

    # convert values to VPP compatible
    if 'mode' in config:
        config['mode'] = _get_bond_mode(config['mode'])
    if 'hash_policy' in config:
        config['hash_policy'] = _get_bond_lb(config['hash_policy'])

    # Get 'vpp settings' config with default values
    config['vpp_settings'] = conf.get_config_dict(
        ['vpp', 'settings'],
        key_mangling=('-', '_'),
        get_first_key=True,
        no_tag_node_value_mangle=True,
    )
    # VPP config for member-in-feature checks
    config['vpp'] = conf.get_config_dict(
        ['vpp'],
        key_mangling=('-', '_'),
        get_first_key=True,
        no_tag_node_value_mangle=True,
    )

    iface_path = ['interfaces', 'vpp', 'bonding', ifname]
    rebuild_required_nodes = ['mode', 'hash-policy', 'mac']
    for node in rebuild_required_nodes:
        if is_node_changed(conf, iface_path + [node]):
            config.update({'rebuild_required': {}})

    config['bond_members'] = deps_bond_dict(conf)

    # Dependency
    config['xconn_members'] = deps_xconnect_dict(conf)
    if ifname in config['xconn_members']:
        for xconn_iface in config['xconn_members'][ifname]:
            set_dependents('vpp_interfaces_xconnect', conf, xconn_iface)

    config['bridge_members'] = deps_bridge_dict(conf)
    if ifname in config['bridge_members']:
        for bridge_iface in config['bridge_members'][ifname]:
            set_dependents('vpp_interfaces_bridge', conf, bridge_iface)

    # PPPoE dependency
    if any(i == ifname or i.startswith(f'{ifname}.') for i in config['pppoe_ifaces']):
        set_dependents('pppoe_server', conf)

    # NAT dependency
    if conf.exists(['vpp', 'nat', 'nat44']):
        set_dependents('vpp_nat_nat44', conf)
    if conf.exists(['vpp', 'nat', 'cgnat']):
        set_dependents('vpp_nat_cgnat', conf)

    # ACL dependency
    if conf.exists(['vpp', 'acl']):
        set_dependents('vpp_acl', conf)

    # IPFIX dependency
    if conf.exists(['vpp', 'ipfix']):
        set_dependents('vpp_ipfix', conf)

    return config


def verify(config):
    ifname = config['ifname']
    if 'deleted' in config and any(
        i == ifname or i.startswith(f'{ifname}.')
        for i in config.get('pppoe_ifaces', [])
    ):
        raise ConfigError(
            'Cannot remove interface: it is still in use by the PPPoE server'
        )

    if 'remove_vpp' in config:
        return None

    verify_vpp_remove_xconnect_interface(config)
    verify_vpp_remove_bridge_interface(config)

    if 'deleted' in config:
        verify_vpp_remove_interface(ifname, config.get('vpp'), match_vlans=True)
        return None

    if not is_systemd_service_active('vpp.service'):
        raise ConfigError(
            'Cannot configure VPP bonding interface: vpp.service is not running'
        )

    # Member must belong to VPP
    for iface in config.get('member', {}).get('interface', []):
        if iface not in config['vpp_ifaces']:
            raise ConfigError(f'{iface} must be a VPP interface for bonding')

        # Each interface can belong only to one bond
        bond_members = config['bond_members'][iface]
        if len(bond_members) > 1:
            raise ConfigError(
                f'Interface {iface} cannot be a member of multiple bonding interfaces: {", ".join(bond_members)}'
            )

        verify_member_conflicts(iface, config, 'bond')
        verify_vpp_interface_not_in_feature(iface, config.get('vpp'))

        if mtu := config.get('mtu'):
            mtu = int(mtu)
            max_mtu = Interface(iface).get_max_mtu()
            min_mtu = Interface(iface).get_min_mtu()
            if mtu > max_mtu:
                raise ConfigError(
                    f'Configured MTU is greater than member interface "{iface}" maximum of {max_mtu}!'
                )
            if mtu < min_mtu:
                raise ConfigError(
                    f'Configured MTU is less than member interface "{iface}" minimum of {min_mtu}!'
                )

    if 'mac' in config:
        mac = config['mac']
        try:
            assert_mac(mac, test_all_zero=False)
        except Exception:
            raise ConfigError(
                f'Cannot use {mac}: it is a multicast MAC address. Please provide a unicast MAC address.'
            )

    for vif_remove in config.get('vif_remove', []):
        vif_iface = f'{ifname}.{vif_remove}'
        if vif_iface in config.get('pppoe_ifaces', []):
            raise ConfigError(
                f'Cannot remove interface {vif_iface}: it is still in use by the PPPoE server'
            )
        verify_vpp_remove_interface(vif_iface, config.get('vpp'))

    verify_mtu_ipv6(config)


def generate(config):
    pass


def apply(config):
    if 'remove_vpp' in config:
        return None

    ifname = config.get('ifname')
    bond = VPPBondInterface(ifname, config)

    if 'deleted' in config or 'rebuild_required' in config:
        bond.remove()

    if 'deleted' in config:
        return None

    bond.update(config)

    call_dependents()

    return None


if __name__ == '__main__':
    try:
        c = get_config()
        verify(c)
        generate(c)
        apply(c)
    except ConfigError as e:
        print(e)
        exit(1)