diff --git a/dockers/docker-fpm-frr/frr/bgpd/bgpd.aggregate.conf.j2 b/dockers/docker-fpm-frr/frr/bgpd/bgpd.aggregate.conf.j2 new file mode 100644 index 00000000000..68e7f3d0529 --- /dev/null +++ b/dockers/docker-fpm-frr/frr/bgpd/bgpd.aggregate.conf.j2 @@ -0,0 +1,83 @@ +! +! template: bgpd/bgpd.aggregate.conf.j2 +! +! Render BGP_AGGREGATE_ADDRESS from CONFIG_DB directly into the bootstrap +! bgpd.conf so the aggregate-address and the contributing/aggregate prefix-list +! entries are present in FRR *before* bgpd forms any session. This closes the +! transient leak window where bgpcfgd's AggregateAddressMgr replays the +! aggregate config AFTER the neighbor managers have already brought sessions up +! (contributing routes would otherwise escape to T2 until the manager runs). +! +! This template is the SOLE gate at boot (template-only design: the manager does +! NOT withdraw a bootstrap-rendered entry). It must therefore reproduce every +! safety check the manager applies before pushing an aggregate to FRR: +! * prefix must parse as a v4/v6 network (else it would poison bgpd.conf parse) +! * prefix must be strict (no host bits) -- parity with validate_prefix(strict=True) +! * bbr-required entries only when BBR is *certainly* on (see BBR safety gate) +! Anything we skip stays owned by AggregateAddressMgr, which gates on the +! fully-resolved live BBR state. +! +! Idempotency: +! * command strings MUST mirror managers_aggregate_address.py byte-for-byte: +! generate_aggregate_address_commands() and generate_prefix_list_commands(). +! A later manager reconcile is then a FRR no-op (duplicate prefix-list value +! + identical aggregate-address are both dropped by FRR). If you edit one +! side, edit the other. +! * prefix-list entries use explicit seq >= 10 to avoid colliding with any +! existing lower-numbered prefix-list entries. The manager emits NO seq (FRR +! auto-assigns), and FRR dedups by value, so the differing seq does not matter. +! +! BBR safety gate (STRICT -- do not loosen): +! bbr-required=true aggregates must only be rendered when bounce-back (BBR) is +! active, otherwise suppressing the contributing routes to T2 risks a +! blackhole. Because there is NO manager withdrawal to undo a wrong guess in +! this template-only design, we render a bbr-required aggregate at boot ONLY +! when BGP_BBR|all status == "enabled" is EXPLICITLY present. We deliberately +! do NOT consult constants.yml default_state: BBRMgr's effective state also +! depends on BBR peer-groups existing, which this template cannot verify, so +! the default_state path could render unsafe suppression with nothing to +! reverse it. Conservative gate = correct when you are the only gate. +! +{% set bbr_on = (BGP_BBR is defined and 'all' in BGP_BBR and BGP_BBR['all'].get('status', 'disabled') == 'enabled') %} +{% if BGP_AGGREGATE_ADDRESS is defined and (DEVICE_METADATA is defined) and ('localhost' in DEVICE_METADATA) and ('bgp_asn' in DEVICE_METADATA['localhost']) and (DEVICE_METADATA['localhost']['bgp_asn'].lower() not in ['none', 'null']) %} +{% set bgp_asn = DEVICE_METADATA['localhost']['bgp_asn'] %} +{# Precompute whether any entry qualifies so we never emit empty stanzas. #} +{% set ns = namespace(any=false) %} +{% for prefix, attr in BGP_AGGREGATE_ADDRESS.items() %} +{% if ('/' in prefix) and ((prefix | ipv4) or (prefix | ipv6)) and ((prefix | ip | string | lower) == (prefix | ip_network | string | lower)) and (attr.get('bbr-required', 'false') != 'true' or bbr_on) %} +{% set ns.any = true %} +{% endif %} +{% endfor %} +{% if ns.any %} +! +! ---- aggregate / contributing prefix-lists (global scope) ---- +{% for prefix, attr in BGP_AGGREGATE_ADDRESS.items() %} +{% if ('/' in prefix) and ((prefix | ipv4) or (prefix | ipv6)) and ((prefix | ip | string | lower) == (prefix | ip_network | string | lower)) and (attr.get('bbr-required', 'false') != 'true' or bbr_on) %} +{% set is_v4 = prefix | ipv4 %} +{% set ipcmd = 'ip' if is_v4 else 'ipv6' %} +{% set seq = 10 + loop.index0 * 5 %} +{% if attr.get('aggregate-address-prefix-list', '') %} +{{ ipcmd }} prefix-list {{ attr['aggregate-address-prefix-list'] }} seq {{ seq }} permit {{ prefix }} +{% endif %} +{% if attr.get('contributing-address-prefix-list', '') %} +{{ ipcmd }} prefix-list {{ attr['contributing-address-prefix-list'] }} seq {{ seq }} permit {{ prefix }} le {{ 32 if is_v4 else 128 }} +{% endif %} +{% endif %} +{% endfor %} +! +! ---- aggregate-address (router bgp scope) ---- +router bgp {{ bgp_asn }} +{% for prefix, attr in BGP_AGGREGATE_ADDRESS.items() %} +{% if ('/' in prefix) and ((prefix | ipv4) or (prefix | ipv6)) and ((prefix | ip | string | lower) == (prefix | ip_network | string | lower)) and (attr.get('bbr-required', 'false') != 'true' or bbr_on) %} +{% set is_v4 = prefix | ipv4 %} + address-family {{ 'ipv4' if is_v4 else 'ipv6' }} + aggregate-address {{ prefix }}{{ ' summary-only' if attr.get('summary-only', 'false') == 'true' else '' }}{{ ' as-set' if attr.get('as-set', 'false') == 'true' else '' }} + exit-address-family +{% endif %} +{% endfor %} +exit +{% endif %} +{% endif %} +! +! end of template: bgpd/bgpd.aggregate.conf.j2 +! diff --git a/dockers/docker-fpm-frr/frr/bgpd/bgpd.conf.j2 b/dockers/docker-fpm-frr/frr/bgpd/bgpd.conf.j2 index 9b294111ba2..166b429f30c 100644 --- a/dockers/docker-fpm-frr/frr/bgpd/bgpd.conf.j2 +++ b/dockers/docker-fpm-frr/frr/bgpd/bgpd.conf.j2 @@ -20,6 +20,10 @@ agentx {% include "bgpd.spine_chassis_frontend_router.conf.j2" %} {% endif %} ! +{% if BGP_AGGREGATE_ADDRESS is defined %} +{% include "bgpd.aggregate.conf.j2" %} +! +{% endif %} {% include "bgpd.main.conf.j2" %} ! ! end of template: bgpd/bgpd.conf.j2 diff --git a/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/bbr_required_disabled.conf b/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/bbr_required_disabled.conf new file mode 100644 index 00000000000..2d4297eecb8 --- /dev/null +++ b/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/bbr_required_disabled.conf @@ -0,0 +1,43 @@ +! +! template: bgpd/bgpd.aggregate.conf.j2 +! +! Render BGP_AGGREGATE_ADDRESS from CONFIG_DB directly into the bootstrap +! bgpd.conf so the aggregate-address and the contributing/aggregate prefix-list +! entries are present in FRR *before* bgpd forms any session. This closes the +! transient leak window where bgpcfgd's AggregateAddressMgr replays the +! aggregate config AFTER the neighbor managers have already brought sessions up +! (contributing routes would otherwise escape to T2 until the manager runs). +! +! This template is the SOLE gate at boot (template-only design: the manager does +! NOT withdraw a bootstrap-rendered entry). It must therefore reproduce every +! safety check the manager applies before pushing an aggregate to FRR: +! * prefix must parse as a v4/v6 network (else it would poison bgpd.conf parse) +! * prefix must be strict (no host bits) -- parity with validate_prefix(strict=True) +! * bbr-required entries only when BBR is *certainly* on (see BBR safety gate) +! Anything we skip stays owned by AggregateAddressMgr, which gates on the +! fully-resolved live BBR state. +! +! Idempotency: +! * command strings MUST mirror managers_aggregate_address.py byte-for-byte: +! generate_aggregate_address_commands() and generate_prefix_list_commands(). +! A later manager reconcile is then a FRR no-op (duplicate prefix-list value +! + identical aggregate-address are both dropped by FRR). If you edit one +! side, edit the other. +! * prefix-list entries use explicit seq >= 10 to avoid colliding with any +! existing lower-numbered prefix-list entries. The manager emits NO seq (FRR +! auto-assigns), and FRR dedups by value, so the differing seq does not matter. +! +! BBR safety gate (STRICT -- do not loosen): +! bbr-required=true aggregates must only be rendered when bounce-back (BBR) is +! active, otherwise suppressing the contributing routes to T2 risks a +! blackhole. Because there is NO manager withdrawal to undo a wrong guess in +! this template-only design, we render a bbr-required aggregate at boot ONLY +! when BGP_BBR|all status == "enabled" is EXPLICITLY present. We deliberately +! do NOT consult constants.yml default_state: BBRMgr's effective state also +! depends on BBR peer-groups existing, which this template cannot verify, so +! the default_state path could render unsafe suppression with nothing to +! reverse it. Conservative gate = correct when you are the only gate. +! +! +! end of template: bgpd/bgpd.aggregate.conf.j2 +! \ No newline at end of file diff --git a/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/bbr_required_disabled.json b/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/bbr_required_disabled.json new file mode 100644 index 00000000000..979a87ed4f5 --- /dev/null +++ b/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/bbr_required_disabled.json @@ -0,0 +1,18 @@ +{ + "DEVICE_METADATA": { + "localhost": { + "bgp_asn": "65001" + } + }, + "BGP_BBR": { + "all": { + "status": "disabled" + } + }, + "BGP_AGGREGATE_ADDRESS": { + "192.168.0.0/24": { + "bbr-required": "true", + "summary-only": "true" + } + } +} \ No newline at end of file diff --git a/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/bbr_required_enabled.conf b/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/bbr_required_enabled.conf new file mode 100644 index 00000000000..a36528b067b --- /dev/null +++ b/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/bbr_required_enabled.conf @@ -0,0 +1,52 @@ +! +! template: bgpd/bgpd.aggregate.conf.j2 +! +! Render BGP_AGGREGATE_ADDRESS from CONFIG_DB directly into the bootstrap +! bgpd.conf so the aggregate-address and the contributing/aggregate prefix-list +! entries are present in FRR *before* bgpd forms any session. This closes the +! transient leak window where bgpcfgd's AggregateAddressMgr replays the +! aggregate config AFTER the neighbor managers have already brought sessions up +! (contributing routes would otherwise escape to T2 until the manager runs). +! +! This template is the SOLE gate at boot (template-only design: the manager does +! NOT withdraw a bootstrap-rendered entry). It must therefore reproduce every +! safety check the manager applies before pushing an aggregate to FRR: +! * prefix must parse as a v4/v6 network (else it would poison bgpd.conf parse) +! * prefix must be strict (no host bits) -- parity with validate_prefix(strict=True) +! * bbr-required entries only when BBR is *certainly* on (see BBR safety gate) +! Anything we skip stays owned by AggregateAddressMgr, which gates on the +! fully-resolved live BBR state. +! +! Idempotency: +! * command strings MUST mirror managers_aggregate_address.py byte-for-byte: +! generate_aggregate_address_commands() and generate_prefix_list_commands(). +! A later manager reconcile is then a FRR no-op (duplicate prefix-list value +! + identical aggregate-address are both dropped by FRR). If you edit one +! side, edit the other. +! * prefix-list entries use explicit seq >= 10 to avoid colliding with any +! existing lower-numbered prefix-list entries. The manager emits NO seq (FRR +! auto-assigns), and FRR dedups by value, so the differing seq does not matter. +! +! BBR safety gate (STRICT -- do not loosen): +! bbr-required=true aggregates must only be rendered when bounce-back (BBR) is +! active, otherwise suppressing the contributing routes to T2 risks a +! blackhole. Because there is NO manager withdrawal to undo a wrong guess in +! this template-only design, we render a bbr-required aggregate at boot ONLY +! when BGP_BBR|all status == "enabled" is EXPLICITLY present. We deliberately +! do NOT consult constants.yml default_state: BBRMgr's effective state also +! depends on BBR peer-groups existing, which this template cannot verify, so +! the default_state path could render unsafe suppression with nothing to +! reverse it. Conservative gate = correct when you are the only gate. +! +! +! ---- aggregate / contributing prefix-lists (global scope) ---- +! +! ---- aggregate-address (router bgp scope) ---- +router bgp 65001 + address-family ipv4 + aggregate-address 192.168.0.0/24 summary-only + exit-address-family +exit +! +! end of template: bgpd/bgpd.aggregate.conf.j2 +! \ No newline at end of file diff --git a/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/bbr_required_enabled.json b/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/bbr_required_enabled.json new file mode 100644 index 00000000000..7fd42e477ac --- /dev/null +++ b/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/bbr_required_enabled.json @@ -0,0 +1,18 @@ +{ + "DEVICE_METADATA": { + "localhost": { + "bgp_asn": "65001" + } + }, + "BGP_BBR": { + "all": { + "status": "enabled" + } + }, + "BGP_AGGREGATE_ADDRESS": { + "192.168.0.0/24": { + "bbr-required": "true", + "summary-only": "true" + } + } +} \ No newline at end of file diff --git a/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/ipv4_basic.conf b/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/ipv4_basic.conf new file mode 100644 index 00000000000..a36528b067b --- /dev/null +++ b/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/ipv4_basic.conf @@ -0,0 +1,52 @@ +! +! template: bgpd/bgpd.aggregate.conf.j2 +! +! Render BGP_AGGREGATE_ADDRESS from CONFIG_DB directly into the bootstrap +! bgpd.conf so the aggregate-address and the contributing/aggregate prefix-list +! entries are present in FRR *before* bgpd forms any session. This closes the +! transient leak window where bgpcfgd's AggregateAddressMgr replays the +! aggregate config AFTER the neighbor managers have already brought sessions up +! (contributing routes would otherwise escape to T2 until the manager runs). +! +! This template is the SOLE gate at boot (template-only design: the manager does +! NOT withdraw a bootstrap-rendered entry). It must therefore reproduce every +! safety check the manager applies before pushing an aggregate to FRR: +! * prefix must parse as a v4/v6 network (else it would poison bgpd.conf parse) +! * prefix must be strict (no host bits) -- parity with validate_prefix(strict=True) +! * bbr-required entries only when BBR is *certainly* on (see BBR safety gate) +! Anything we skip stays owned by AggregateAddressMgr, which gates on the +! fully-resolved live BBR state. +! +! Idempotency: +! * command strings MUST mirror managers_aggregate_address.py byte-for-byte: +! generate_aggregate_address_commands() and generate_prefix_list_commands(). +! A later manager reconcile is then a FRR no-op (duplicate prefix-list value +! + identical aggregate-address are both dropped by FRR). If you edit one +! side, edit the other. +! * prefix-list entries use explicit seq >= 10 to avoid colliding with any +! existing lower-numbered prefix-list entries. The manager emits NO seq (FRR +! auto-assigns), and FRR dedups by value, so the differing seq does not matter. +! +! BBR safety gate (STRICT -- do not loosen): +! bbr-required=true aggregates must only be rendered when bounce-back (BBR) is +! active, otherwise suppressing the contributing routes to T2 risks a +! blackhole. Because there is NO manager withdrawal to undo a wrong guess in +! this template-only design, we render a bbr-required aggregate at boot ONLY +! when BGP_BBR|all status == "enabled" is EXPLICITLY present. We deliberately +! do NOT consult constants.yml default_state: BBRMgr's effective state also +! depends on BBR peer-groups existing, which this template cannot verify, so +! the default_state path could render unsafe suppression with nothing to +! reverse it. Conservative gate = correct when you are the only gate. +! +! +! ---- aggregate / contributing prefix-lists (global scope) ---- +! +! ---- aggregate-address (router bgp scope) ---- +router bgp 65001 + address-family ipv4 + aggregate-address 192.168.0.0/24 summary-only + exit-address-family +exit +! +! end of template: bgpd/bgpd.aggregate.conf.j2 +! \ No newline at end of file diff --git a/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/ipv4_basic.json b/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/ipv4_basic.json new file mode 100644 index 00000000000..58a9264b252 --- /dev/null +++ b/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/ipv4_basic.json @@ -0,0 +1,12 @@ +{ + "DEVICE_METADATA": { + "localhost": { + "bgp_asn": "65001" + } + }, + "BGP_AGGREGATE_ADDRESS": { + "192.168.0.0/24": { + "summary-only": "true" + } + } +} \ No newline at end of file diff --git a/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/ipv4_with_prefix_lists.conf b/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/ipv4_with_prefix_lists.conf new file mode 100644 index 00000000000..1e49200f081 --- /dev/null +++ b/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/ipv4_with_prefix_lists.conf @@ -0,0 +1,54 @@ +! +! template: bgpd/bgpd.aggregate.conf.j2 +! +! Render BGP_AGGREGATE_ADDRESS from CONFIG_DB directly into the bootstrap +! bgpd.conf so the aggregate-address and the contributing/aggregate prefix-list +! entries are present in FRR *before* bgpd forms any session. This closes the +! transient leak window where bgpcfgd's AggregateAddressMgr replays the +! aggregate config AFTER the neighbor managers have already brought sessions up +! (contributing routes would otherwise escape to T2 until the manager runs). +! +! This template is the SOLE gate at boot (template-only design: the manager does +! NOT withdraw a bootstrap-rendered entry). It must therefore reproduce every +! safety check the manager applies before pushing an aggregate to FRR: +! * prefix must parse as a v4/v6 network (else it would poison bgpd.conf parse) +! * prefix must be strict (no host bits) -- parity with validate_prefix(strict=True) +! * bbr-required entries only when BBR is *certainly* on (see BBR safety gate) +! Anything we skip stays owned by AggregateAddressMgr, which gates on the +! fully-resolved live BBR state. +! +! Idempotency: +! * command strings MUST mirror managers_aggregate_address.py byte-for-byte: +! generate_aggregate_address_commands() and generate_prefix_list_commands(). +! A later manager reconcile is then a FRR no-op (duplicate prefix-list value +! + identical aggregate-address are both dropped by FRR). If you edit one +! side, edit the other. +! * prefix-list entries use explicit seq >= 10 to avoid colliding with any +! existing lower-numbered prefix-list entries. The manager emits NO seq (FRR +! auto-assigns), and FRR dedups by value, so the differing seq does not matter. +! +! BBR safety gate (STRICT -- do not loosen): +! bbr-required=true aggregates must only be rendered when bounce-back (BBR) is +! active, otherwise suppressing the contributing routes to T2 risks a +! blackhole. Because there is NO manager withdrawal to undo a wrong guess in +! this template-only design, we render a bbr-required aggregate at boot ONLY +! when BGP_BBR|all status == "enabled" is EXPLICITLY present. We deliberately +! do NOT consult constants.yml default_state: BBRMgr's effective state also +! depends on BBR peer-groups existing, which this template cannot verify, so +! the default_state path could render unsafe suppression with nothing to +! reverse it. Conservative gate = correct when you are the only gate. +! +! +! ---- aggregate / contributing prefix-lists (global scope) ---- +ip prefix-list AGG_PL seq 10 permit 192.168.0.0/24 +ip prefix-list CON_PL seq 10 permit 192.168.0.0/24 le 32 +! +! ---- aggregate-address (router bgp scope) ---- +router bgp 65001 + address-family ipv4 + aggregate-address 192.168.0.0/24 summary-only + exit-address-family +exit +! +! end of template: bgpd/bgpd.aggregate.conf.j2 +! \ No newline at end of file diff --git a/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/ipv4_with_prefix_lists.json b/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/ipv4_with_prefix_lists.json new file mode 100644 index 00000000000..d4168cba54d --- /dev/null +++ b/src/sonic-bgpcfgd/tests/data/sonic-cfggen/bgpd.aggregate.conf.j2/ipv4_with_prefix_lists.json @@ -0,0 +1,14 @@ +{ + "DEVICE_METADATA": { + "localhost": { + "bgp_asn": "65001" + } + }, + "BGP_AGGREGATE_ADDRESS": { + "192.168.0.0/24": { + "summary-only": "true", + "aggregate-address-prefix-list": "AGG_PL", + "contributing-address-prefix-list": "CON_PL" + } + } +} \ No newline at end of file diff --git a/src/sonic-bgpcfgd/tests/test_sonic-cfggen.py b/src/sonic-bgpcfgd/tests/test_sonic-cfggen.py index ddf58a1e24e..fb3a4672a60 100644 --- a/src/sonic-bgpcfgd/tests/test_sonic-cfggen.py +++ b/src/sonic-bgpcfgd/tests/test_sonic-cfggen.py @@ -269,6 +269,16 @@ def _render_bgpd_main(json_path): assert p.returncode == 0, "sonic-cfggen returned %d. stderr=%r" % (p.returncode, stderr) return stdout.decode("ascii") + +def _render_aggregate_conf(json_path): + template_path = os.path.join(TEMPLATE_PATH, "bgpd/bgpd.aggregate.conf.j2") + json_full_path = os.path.join(DATA_PATH, json_path) + command = ['sonic-cfggen', "-T", TEMPLATE_PATH, "-t", template_path, "-y", json_full_path] + p = subprocess.Popen(command, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = p.communicate() + assert p.returncode == 0, "sonic-cfggen returned %d. stderr=%r" % (p.returncode, stderr) + return stdout.decode("ascii") + def test_bgpd_main_llgr_helper_emitted_on_urh(): """LLGR helper-only block must be emitted for UpperRegionalHub.""" rendered = _render_bgpd_main("bgpd.main.conf.j2/single_asic_urh.json") @@ -296,3 +306,49 @@ def test_bgpd_main_llgr_helper_absent_on_non_urh(): "%s must not contain 'bgp graceful-restart-disable'" % fixture assert "long-lived-graceful-restart" not in rendered, \ "%s must not contain 'long-lived-graceful-restart'" % fixture + + +def test_aggregate_conf_ipv4_basic(): + """IPv4 aggregate-address with summary-only must be rendered into bgpd config.""" + run_test("bgpd.aggregate.conf.j2 IPv4 basic", + "bgpd/bgpd.aggregate.conf.j2", + "bgpd.aggregate.conf.j2/ipv4_basic.json", + "bgpd.aggregate.conf.j2/ipv4_basic.conf") + + +def test_aggregate_conf_ipv4_with_prefix_lists(): + """IPv4 aggregate with aggregate/contributing prefix-list names must generate prefix-list stanzas.""" + run_test("bgpd.aggregate.conf.j2 IPv4 with prefix-lists", + "bgpd/bgpd.aggregate.conf.j2", + "bgpd.aggregate.conf.j2/ipv4_with_prefix_lists.json", + "bgpd.aggregate.conf.j2/ipv4_with_prefix_lists.conf") + + +def test_aggregate_conf_bbr_required_enabled(): + """A bbr-required aggregate must be rendered when BGP_BBR|all status == 'enabled'.""" + run_test("bgpd.aggregate.conf.j2 BBR required + enabled", + "bgpd/bgpd.aggregate.conf.j2", + "bgpd.aggregate.conf.j2/bbr_required_enabled.json", + "bgpd.aggregate.conf.j2/bbr_required_enabled.conf") + + +def test_aggregate_conf_bbr_required_disabled(): + """A bbr-required aggregate must NOT be rendered when BGP_BBR|all status == 'disabled'.""" + run_test("bgpd.aggregate.conf.j2 BBR required + disabled", + "bgpd/bgpd.aggregate.conf.j2", + "bgpd.aggregate.conf.j2/bbr_required_disabled.json", + "bgpd.aggregate.conf.j2/bbr_required_disabled.conf") + + +def test_aggregate_conf_no_bbr_required_renders_regardless(): + """An aggregate without bbr-required must be rendered regardless of BGP_BBR state.""" + rendered = _render_aggregate_conf("bgpd.aggregate.conf.j2/ipv4_basic.json") + assert "aggregate-address 192.168.0.0/24" in rendered, \ + "Expected aggregate-address in output, got:\n%s" % rendered + + +def test_aggregate_conf_bbr_gate_blocks_bbr_required_when_disabled(): + """BBR gate: bbr-required=true aggregate is suppressed when BBR is disabled.""" + rendered = _render_aggregate_conf("bgpd.aggregate.conf.j2/bbr_required_disabled.json") + assert "aggregate-address" not in rendered, \ + "bbr-required aggregate must not appear when BBR is disabled, got:\n%s" % rendered