Skip to content

Conversation

l0crian1
Copy link
Contributor

@l0crian1 l0crian1 commented Oct 1, 2025

Change summary

There are a large number of error generating checks in the verify() function within config scripts that checks whether an interface exists. There is however, very few checks in the reverse when deleting the dependent interface.

Example:

There are checks when configuring an interface in OSPF:

vyos@vyos# set protocols ospf interface dum5 area 0
vyos@vyos# commit
Interface "dum5" does not exist!

[[protocols ospf]] failed
Commit failed

But if we delete the interface after OSPF has been configured, it won't fail:

vyos@vyos# set interfaces dummy dum5
vyos@vyos# set protocols ospf interface dum5 area 0
vyos@vyos# commit

vyos@vyos# delete interfaces dummy dum5
vyos@vyos# commit

This will lead to the entire OSPF section failing to load upon a reboot, potentially causing an outage for a customer.

This check adds a global check that can be called in conf_mode scripts that checks for dependencies of an interface before it is deleted.

This check was added to a single PR (interfaces_dummy.py) as an example. The check would need to be added to the other scripts in subsequent PR/tasks.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Code style update (formatting, renaming)
  • Refactoring (no functional changes)
  • Migration from an old Vyatta component to vyos-1x, please link to related PR inside obsoleted component
  • Other (please describe):

Related Task(s)

https://vyos.dev/T7886

Related PR(s)

How to test / Smoketest result

Embed dependencies into a config dict within the get_config() function: Example - intefaces_dummy.py

def get_config(config=None):
    dummy['int_dependencies'] = verify_interface_dependencies(conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True),
                                                           dummy['ifname'],
                                                           ignore=f"interfaces dummy {dummy['ifname']}")
    return dummy

Add checks within the verify() function

def verify(dummy):
    if 'deleted' in dummy:
        # Check for interface dependencies
        dependency_errors = dict_search('int_dependencies.errors', dummy)
        dependency_warnings = dict_search('int_dependencies.warnings', dummy)
        if dependency_errors:
            raise ConfigError(dummy['int_dependencies']['errors_msg'])
        if dependency_warnings:
            Warning(dummy['int_dependencies']['warnings_msg'])

Configure an interface and a dependent config section: Example - dum5 and OSPF:

vyos@vyos# set interfaces dummy dum5
vyos@vyos# set protocols ospf interface dum5 area 0
vyos@vyos# commit

Delete, commit and observe error:

vyos@vyos# delete interfaces dummy dum5
vyos@vyos# commit
[ interfaces dummy dum5 ]
dum5 can't be deleted while configured in the following configuration
paths:
- protocols ospf interface dum5
delete [ interfaces dummy dum5 ] failed
Commit failed

Warnings are also shown if there are no Errors. These are sections that will not fail to load if the interface is missing upon boot: Example - dum5 and BGP:

vyos@vyos# set interfaces dummy dum5
vyos@vyos# set protocols bgp system-as 1
vyos@vyos# set protocols bgp interface dum5
vyos@vyos# commit

vyos@vyos# delete interfaces dummy dum5
vyos@vyos# commit
[ interfaces dummy dum5 ]

WARNING: dum5 is configured in the following configuration paths:
- protocols bgp interface dum5

interface_dummy.py smoketest results:

test_add_multiple_ip_addresses (__main__.DummyInterfaceTest.test_add_multiple_ip_addresses) ... ok
test_add_single_ip_address (__main__.DummyInterfaceTest.test_add_single_ip_address) ... ok
test_add_to_invalid_vrf (__main__.DummyInterfaceTest.test_add_to_invalid_vrf) ... ok
test_dhcp_client_options (__main__.DummyInterfaceTest.test_dhcp_client_options) ... skipped 'unsupported on interface family'
test_dhcp_disable_interface (__main__.DummyInterfaceTest.test_dhcp_disable_interface) ... skipped 'unsupported on interface family'
test_dhcp_vrf (__main__.DummyInterfaceTest.test_dhcp_vrf) ... skipped 'unsupported on interface family'
test_dhcpv6_client_options (__main__.DummyInterfaceTest.test_dhcpv6_client_options) ... skipped 'unsupported on interface family'
test_dhcpv6_vrf (__main__.DummyInterfaceTest.test_dhcpv6_vrf) ... skipped 'unsupported on interface family'
test_dhcpv6pd_auto_sla_id (__main__.DummyInterfaceTest.test_dhcpv6pd_auto_sla_id) ... skipped 'unsupported on interface family'
test_dhcpv6pd_manual_sla_id (__main__.DummyInterfaceTest.test_dhcpv6pd_manual_sla_id) ... skipped 'unsupported on interface family'
test_eapol (__main__.DummyInterfaceTest.test_eapol) ... skipped 'unsupported on interface family'
test_interface_description (__main__.DummyInterfaceTest.test_interface_description) ... ok
test_interface_disable (__main__.DummyInterfaceTest.test_interface_disable) ... ok
test_interface_ip_options (__main__.DummyInterfaceTest.test_interface_ip_options) ... ok
test_interface_ipv6_options (__main__.DummyInterfaceTest.test_interface_ipv6_options) ... ok
test_interface_mtu (__main__.DummyInterfaceTest.test_interface_mtu) ... ok
test_ipv6_link_local_address (__main__.DummyInterfaceTest.test_ipv6_link_local_address) ... ok
test_move_interface_between_vrf_instances (__main__.DummyInterfaceTest.test_move_interface_between_vrf_instances) ... ok
test_mtu_1200_no_ipv6_interface (__main__.DummyInterfaceTest.test_mtu_1200_no_ipv6_interface) ... ok
test_span_mirror (__main__.DummyInterfaceTest.test_span_mirror) ... skipped 'unsupported on interface family'
test_vif_8021q_interfaces (__main__.DummyInterfaceTest.test_vif_8021q_interfaces) ... skipped 'unsupported on interface family'
test_vif_8021q_lower_up_down (__main__.DummyInterfaceTest.test_vif_8021q_lower_up_down) ... skipped 'unsupported on interface family'
test_vif_8021q_mtu_limits (__main__.DummyInterfaceTest.test_vif_8021q_mtu_limits) ... skipped 'unsupported on interface family'
test_vif_8021q_qos_change (__main__.DummyInterfaceTest.test_vif_8021q_qos_change) ... skipped 'unsupported on interface family'
test_vif_s_8021ad_vlan_interfaces (__main__.DummyInterfaceTest.test_vif_s_8021ad_vlan_interfaces) ... skipped 'unsupported on interface family'
test_vif_s_protocol_change (__main__.DummyInterfaceTest.test_vif_s_protocol_change) ... skipped 'unsupported on interface family'

----------------------------------------------------------------------
Ran 26 tests in 320.807s

OK (skipped=15)

Checklist:

  • I have read the CONTRIBUTING document
  • I have linked this PR to one or more Phabricator Task(s)
  • I have run the components SMOKETESTS if applicable
  • My commit headlines contain a valid Task id
  • My change requires a change to the documentation
  • I have updated the documentation accordingly

Copy link

github-actions bot commented Oct 1, 2025

👍
No issues in PR Title / Commit Title

Copy link

github-actions bot commented Oct 2, 2025

CI integration 👍 passed!

Details

CI logs

  • CLI Smoketests (no interfaces) 👍 passed
  • CLI Smoketests VPP 👍 passed
  • CLI Smoketests (interfaces only) 👍 passed
  • Config tests 👍 passed
  • Config tests VPP 👍 passed
  • RAID1 tests 👍 passed
  • TPM tests 👍 passed

Copy link
Member

@dmbaturin dmbaturin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea to start verifying those dependencies. I'm not sure what the ultimate design for that should be, though.

I think in the long run, components should be able to register their dependencies, so that the config backend is aware of the full dependency graph.

Whether this solution is good to merge as a stop-gap measure — I'm not sure. It seems to do that job. Maybe one suggestion: I'd put dependency lists and types (warning/error) in a dict so that it's easier to maintain and use a guide for converting it to a general mechanism later.

@l0crian1
Copy link
Contributor Author

l0crian1 commented Oct 9, 2025

I think in the long run, components should be able to register their dependencies, so that the config backend is aware of the full dependency graph.

@dmbaturin

That was my original idea, but there doesn't appear to be a general programmatic mechanism for inserting data into the config object. I thought about storing them in JSON, then the dependencies could be in a simple key of the interface name, though it wouldn't be scalable since every conf_mode script has a different way and section of the config dict for dealing with interfaces.

What I made is the most scalable mechanism I could execute on, though it's also probably the most brute force way to handle this. It can also be fragile, which is why 'values' default to warnings, because of the possibility of someone doing something like set protocols bgp neighbor 10.0.0.1 description br0. This would be seen as a dependency (incorrectly) for the br0 interface in my solution which could be annoying ultimately, but not as bad as an outage caused by OSPF, MPLS, etc... failing to load upon booting if there's an actual dependency.

I think probably the best way to handle this is to store what is a dependency in the XML tree, kind of like defaultValue is currently. Then you can register dependency types in the backend like interface, policy-object, etc.... It can also simplify some of the existing checks like the vrf, bridge/bond members, etc... checks. Something like this:

# Ex. interface-definitions/include/ospf/protocol-common-config.xml.i
<tagNode name="interface">
  <dependency>
    <type>interface</type>
    <level>error</level>
  </dependency>
</tagNode>

Or

<tagNode name="interface">
  <dependency type="interface" level="error"/>
</tagNode>

And in the config dict, it would return something like:

{
    "dependencies": {
        "interfaces": {
            "br0": {
                "errors": [
                    "protocols ospf interface br0",
                    "interfaces tunnel tun0 source-interface br0"
                ],
                "warnings": [
                    "firewall ipv4 forward filter rule 10 inbound-interface name br0"
                ]
            }
        },
    }
}

I'm okay if this doesn't get merged. If all I accomplished with this PR is to generate the discussion, then I think it was time well spent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Development

Successfully merging this pull request may close these issues.

3 participants