diff options
| author | Kyrylo Yatsenko <hedrok@gmail.com> | 2025-09-26 22:59:43 +0300 |
|---|---|---|
| committer | Kyrylo Yatsenko <hedrok@gmail.com> | 2025-10-14 15:05:07 +0300 |
| commit | 47d5f21b7770b0b8dcd29272e6c93bea9028adfe (patch) | |
| tree | fac2466429dfdccf6d68c99f91aadd244c62cefd /src | |
| parent | 1c181499a3af4b6c170d3fea2b96af58bb0a34a7 (diff) | |
| download | vyos-1x-47d5f21b7770b0b8dcd29272e6c93bea9028adfe.tar.gz vyos-1x-47d5f21b7770b0b8dcd29272e6c93bea9028adfe.zip | |
T5942: Make failover support dhcp-interface
* Refactor XML a little: move common dhcp-interface properties to
separate include file
* Add failover support for dhcp-interface
* Add test for DHCP in failover protocol
Diffstat (limited to 'src')
| -rwxr-xr-x | src/conf_mode/protocols_failover.py | 84 | ||||
| -rwxr-xr-x | src/helpers/vyos-failover.py | 92 |
2 files changed, 138 insertions, 38 deletions
diff --git a/src/conf_mode/protocols_failover.py b/src/conf_mode/protocols_failover.py index 956c060f4..752bd6011 100755 --- a/src/conf_mode/protocols_failover.py +++ b/src/conf_mode/protocols_failover.py @@ -85,38 +85,60 @@ def verify(failover): if 'route' not in failover: raise ConfigError(f'Failover "route" is mandatory!') + def _verify_route_item(item_config, item_description, interface_mandatory): + if interface_mandatory and 'interface' not in item_config: + raise ConfigError( + f'Interface for route "{route}" {item_description} is mandatory!' + ) + + if not item_config.get('check'): + raise ConfigError(f'Check target for {item_description} is mandatory!') + + if 'target' not in item_config['check']: + raise ConfigError(f'Check target for {item_description} is mandatory!') + + check_type = item_config['check']['type'] + if check_type == 'tcp' and 'port' not in item_config['check']: + raise ConfigError( + f'Check port for {item_description} and type TCP is mandatory!' + ) + + errors = { + 'icmp': {}, + 'tcp': { + 'interface': 'Check target "interface" option does nothing for type TCP. Use "vrf" if needed', + }, + 'arp': { + 'vrf': 'Check target "vrf" option is incompatible with type ARP, use "interface" option if needed', + }, + } + + for target, target_config in item_config['check']['target'].items(): + for key, msg in errors[check_type].items(): + if key in target_config: + raise ConfigError(msg) + for route, route_config in failover['route'].items(): - if not route_config.get('next_hop'): - raise ConfigError(f'Next-hop for "{route}" is mandatory!') - - for next_hop, next_hop_config in route_config.get('next_hop').items(): - if 'interface' not in next_hop_config: - raise ConfigError(f'Interface for route "{route}" next-hop "{next_hop}" is mandatory!') - - if not next_hop_config.get('check'): - raise ConfigError(f'Check target for next-hop "{next_hop}" is mandatory!') - - if 'target' not in next_hop_config['check']: - raise ConfigError(f'Check target for next-hop "{next_hop}" is mandatory!') - - check_type = next_hop_config['check']['type'] - if check_type == 'tcp' and 'port' not in next_hop_config['check']: - raise ConfigError(f'Check port for next-hop "{next_hop}" and type TCP is mandatory!') - - errors = { - 'icmp': {}, - 'tcp': { - 'interface': 'Check target "interface" option does nothing for type TCP. Use "vrf" if needed', - }, - 'arp': { - 'vrf': 'Check target "vrf" option is incompatible with type ARP, use "interface" option if needed', - }, - } - - for target, target_config in next_hop_config['check']['target'].items(): - for key, msg in errors[check_type].items(): - if key in target_config: - raise ConfigError(msg) + if not route_config.get('next_hop') and not route_config.get('dhcp_interface'): + raise ConfigError( + f'Either next-hop or dhcp-interface for "{route}" is mandatory!' + ) + + if route_config.get('next_hop'): + for next_hop, next_hop_config in route_config.get('next_hop').items(): + _verify_route_item( + next_hop_config, f'next-hop "{next_hop}"', interface_mandatory=True + ) + + if route_config.get('dhcp_interface'): + for dhcp_interface, dhcp_interface_config in route_config.get( + 'dhcp_interface' + ).items(): + _verify_route_item( + dhcp_interface_config, + f'dhcp-interface "{dhcp_interface}"', + interface_mandatory=False, + ) return None diff --git a/src/helpers/vyos-failover.py b/src/helpers/vyos-failover.py index d255f2303..de89f20d0 100755 --- a/src/helpers/vyos-failover.py +++ b/src/helpers/vyos-failover.py @@ -21,6 +21,7 @@ import signal import time from collections import namedtuple +from vyos.template import get_dhcp_router from vyos.utils.process import rc_cmd from vyos.utils.process import run from pathlib import Path @@ -170,6 +171,7 @@ NextHopNamedTuple = namedtuple( 'NextHopConfig', [ 'route', + 'dhcp_interface', 'next_hop', 'vrf', 'vrf_opt', @@ -187,7 +189,9 @@ NextHopNamedTuple = namedtuple( ) -def get_nexthop_config_vars(destination, vrf, vrf_opt, nexthop_config, next_hop): +def get_nexthop_config_vars( + destination, vrf, vrf_opt, nexthop_config, next_hop, dhcp_interface +): port = nexthop_config.get('check').get('port') targets = tuple( @@ -215,10 +219,13 @@ def get_nexthop_config_vars(destination, vrf, vrf_opt, nexthop_config, next_hop) return NextHopNamedTuple( route=destination, + dhcp_interface=dhcp_interface, next_hop=next_hop, vrf=vrf, vrf_opt=vrf_opt, - conf_iface=nexthop_config.get('interface'), + # For next-hop interface is mandatory + # For dhcp-interface it may be not given, then dhcp-interface is used + conf_iface=nexthop_config.get('interface', dhcp_interface), conf_metric=int(nexthop_config.get('metric')), port=port, port_opt=f'port {port}' if port else '', @@ -245,10 +252,22 @@ RouteNamedTuple = namedtuple( def get_route_config(route, route_config, config_path, vrf): vrf_opt = f'vrf {vrf}' if vrf else '' - nexthops = tuple( - get_nexthop_config_vars(route, vrf, vrf_opt, nexthop_config, next_hop) - for next_hop, nexthop_config in route_config.get('next_hop').items() - ) + nexthops = [] + if route_config.get('next_hop'): + nexthops.extend( + get_nexthop_config_vars(route, vrf, vrf_opt, nexthop_config, next_hop, None) + for next_hop, nexthop_config in route_config.get('next_hop').items() + ) + if route_config.get('dhcp_interface'): + nexthops.extend( + get_nexthop_config_vars( + route, vrf, vrf_opt, dhcp_nexthop_config, None, interface + ) + for interface, dhcp_nexthop_config in route_config.get( + 'dhcp_interface' + ).items() + ) + nexthops = tuple(nexthops) return RouteNamedTuple( destination=route, vrf=vrf, @@ -399,6 +418,55 @@ def update_configuration(last_modification_times, all_routes, config_dir): print_debug(f"All routes: {all_routes}") +def process_dhcp_interface(nhc, nexthop_by_dhcp_nexthop): + """ + Processes NextHopNamedTuple with dhcp_interface != None + Return NextHopNamedTuple with next_hop equal to DHCP gateway of nhc. + If there is no gateway for nhc, return None + + Args: + nhc(NextHopNamedTuple): configuration with dhcp_interface + nexthop_by_dhcp_nexthop(dict): dict with previous returned values + """ + cur_dhcpgw = get_dhcp_router(nhc.dhcp_interface) + if not cur_dhcpgw: + cur_dhcpgw = False + + if nhc in nexthop_by_dhcp_nexthop: + prev_dhcpgw = nexthop_by_dhcp_nexthop[nhc].next_hop + else: + prev_dhcpgw = False + + # Equal - do nothing, just return previous value + if prev_dhcpgw == cur_dhcpgw: + if not cur_dhcpgw: + return None + return nexthop_by_dhcp_nexthop[nhc] + + print_debug( + f"DHCP Gateway changed for interface {nhc.dhcp_interface} from '{prev_dhcpgw}' to '{cur_dhcpgw}'" + ) + + # dhcpgw differ and there was previous dhcpgw + if prev_dhcpgw: + prevnhc = nexthop_by_dhcp_nexthop.pop(nhc) + print_debug( + f"Deleting previous nexthop {prevnhc} because of DHCP interface change" + ) + ip_args = get_ip_command_args(prevnhc) + if is_route_exists(ip_args): + delete_route(ip_args) + + newnhc = None + # dhcpgw differ and there is new dhcpgw + if cur_dhcpgw: + newnhc = nhc._replace(next_hop=cur_dhcpgw, dhcp_interface=None) + print_debug(f"Saving new nexthop {newnhc} because of DHCP interface change") + nexthop_by_dhcp_nexthop[nhc] = newnhc + + return newnhc + + if __name__ == '__main__': print_debug(f"{my_name} started") @@ -426,6 +494,11 @@ if __name__ == '__main__': signal.signal(signal.SIGINT, kill_handler) signal.signal(signal.SIGTERM, kill_handler) + # keys: NextHopNamedTuple with dhcp_interface != None + # values: NextHopNamedTuple with next_hop != None + # Translates nexthop with dhcp_interface to ususal nexthop + nexthop_by_dhcp_nexthop = {} + had_sleeps = True while not kill_called: # Check in case daemon was launched without routes @@ -443,6 +516,11 @@ if __name__ == '__main__': vrf_opt = route_config.vrf_opt for nhc in route_config.nexthops: + if nhc.dhcp_interface: + nhc = process_dhcp_interface(nhc, nexthop_by_dhcp_nexthop) + if not nhc: + continue + next_hop = nhc.next_hop ip_args = get_ip_command_args(nhc) @@ -478,7 +556,7 @@ if __name__ == '__main__': f' [ TARGET_FAIL ] target checks fails for [{nhc.pretty_targets}], do nothing' ) journal.send( - f'Check fail for route {route} target {nhc.pretty_targets} proto {nhc.proto} ' + f'Check fail for route {route} interface "{nhc.conf_iface}" target {nhc.pretty_targets} proto {nhc.proto} ' f'{nhc.port_opt}', SYSLOG_IDENTIFIER=my_name, ) |
