diff --git a/doc/developer/index.rst b/doc/developer/index.rst index c06a0f8011d5..a2bdd5076935 100644 --- a/doc/developer/index.rst +++ b/doc/developer/index.rst @@ -18,6 +18,7 @@ FRRouting Developer's Guide fpm grpc ospf + ospf-yang-northbound-notes zebra vtysh path diff --git a/doc/developer/ospf-yang-northbound-notes.rst b/doc/developer/ospf-yang-northbound-notes.rst new file mode 100644 index 000000000000..1ba8d6d114b0 --- /dev/null +++ b/doc/developer/ospf-yang-northbound-notes.rst @@ -0,0 +1,784 @@ +.. SPDX-License-Identifier: GPL-2.0-or-later +.. Copyright (C) 2026 Eric Parsonage + +OSPF YANG Northbound Notes +========================== + +OSPF northbound work should converge on the standard RFC 9129 ``ietf-ospf`` +model for behavior covered by the RFC. FRR-native OSPF modules should be used +as augments for behavior outside the RFC model, not as parallel replacements +for standard OSPF state or configuration. + +The tree contains ``yang/frr-ospfd.yang`` for future FRR-specific OSPFv2 +work, but ``ospfd`` should not advertise that module until there are concrete +callbacks or augments behind it. The OSPFv3 daemon has route-map YANG support +but no native ``frr-ospf6d`` module. A native OSPFv3 module should be added +only when there is concrete FRR-specific state or configuration to expose. + +IETF Module Sources +------------------- + +The added IETF modules, ``ietf-ospf.yang``, ``ietf-routing.yang``, +``ietf-bfd-types.yang``, ``iana-routing-types.yang``, and +``iana-bfd-types.yang``, are imported from their respective RFCs and keep the +IETF Trust BSD license text unchanged. This follows the existing +``yang/ietf/`` treatment of ``ietf-interfaces.yang``, +``ietf-key-chain.yang``, and ``ietf-routing-types.yang``. + +Related Work +------------ + +Earlier OSPF northbound work is useful context for future development: + +* FRR PR #18401, ``pr-18401-ospf-yang`` + + * Adds ``yang/frr-ospf-common-lite.yang`` and + ``yang/frr-ospfd-lite.yang``. + * Adds real operational callbacks in ``ospfd/ospf_nb_state.c``. + * Good source material for instance, area, interface, and neighbor state. + * Targets a lite model, so paths do not map directly to current + ``frr-ospfd.yang``. + +* FRR PR #19066, ``pr-19066-ospf-nb`` + + * Adds a broad generated callback skeleton for current ``frr-ospfd.yang``. + * Useful for path coverage and generated callback names. + * Most callbacks are TODO/no-op stubs, so it should not be transplanted + wholesale. + +FRR also has an experimental YANG module translator for mapping non-native +models onto native FRR models with deviation modules and XPath translation +tables. This branch does not use that mechanism because OSPF does not yet have +a complete callback-backed native OSPF YANG model to serve as the source of +truth. Instead, RFC 9129 is implemented directly as the canonical northbound +surface for the OSPF behavior it covers. + +Current Implementation +---------------------- + +This branch implements the RFC 9129 ``ietf-ospf`` tree directly for OSPFv2 and +OSPFv3 rather than adding an FRR-native OSPF model with parallel semantics. +``yang/frr-ospfd.yang`` remains on disk for future FRR-specific OSPFv2 work, +but ``ospfd`` does not link its generated schema into the daemon binary or +advertise it with no callbacks behind it. + +Both daemons load ``ietf-ospf`` and map FRR behavior toward the RFC 9129 +``/ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf`` +tree. OSPFv2 and OSPFv3 operational callbacks expose the RFC +``control-plane-protocol`` list, router-id, instance LSA counters, area +SPF/ABR/ASBR/LSA counters, interface list, and neighbor list with neighbor +address and state. The default instance name is ``default`` for normal +``ospfd`` and ``ospf6d`` instances. In ``ospfd --instance N`` daemon-instance +mode, the RFC 9129 ``control-plane-protocol`` name is the decimal instance ID +(``N``), matching the legacy ``router ospf N`` CLI. The OSPFv2 interface list +exposes one entry per interface key because RFC 9129 keys the list by interface +name while FRR can hold multiple OSPFv2 interface objects for different +addresses on the same interface. + +``ospfd`` and ``ospf6d`` register as mgmtd backend clients for typed +``control-plane-protocol`` entries so OSPFv2 and OSPFv3 can share the standard +``ietf-routing`` list without one daemon claiming the other daemon's instance. +Instanced ``ospfd`` backends further constrain their registration with the +``name`` key so ``ospfd-1`` and ``ospfd-2`` receive only edits for their own +RFC 9129 protocol instance. +``mgmtd`` also loads the RFC OSPF modules so it can parse OSPF backend replies +in the merged operational datastore. + +The instanced ``ospfd`` backend name is externally visible in mgmtd state: +an instanced process registers as ``ospfd-N`` rather than plain ``ospfd`` so +operators can distinguish backend ownership by OSPF instance. This is an +intentional release-note item, along with the new RFC 9129 OSPF +configuration, RPC, operational state and notification surface. + +Notifications emitted by this branch are available to mgmtd native frontend +subscribers today. They are also the producer side expected by the companion +gRPC Subscribe work; the two branches remain independently useful. + +The mgmtd backend matcher treats predicates in backend registrations as +ownership constraints, not as a reason to hide unfiltered list data. A query +with ``type='ietf-ospf:ospfv2'`` must dispatch only to ``ospfd`` and a query +with ``type='ietf-ospf:ospfv3'`` must dispatch only to ``ospf6d``. A query that +omits the ``type`` predicate, such as a request for the whole +``control-plane-protocol`` list or one of its unkeyed descendants, still +dispatches to both daemons so mgmtd can merge the OSPFv2 and OSPFv3 entries. +This keeps shared IETF lists usable for current OSPF and future protocol +families without special-casing OSPF in mgmtd. + +Configuration write support is intentionally limited to CLI-equivalent RFC 9129 +leaves. The converted leaves are router-id, preference, spf-control paths, +auto-cost, OSPFv2 mpls/ldp/igp-sync, OSPFv2 mpls/te-rid, graceful-restart +enabled, restart-interval, helper-enabled and helper-strict-lsa-checking, OSPFv2 stub-router unconditional, area +lifecycle, area-type, area summary, OSPFv2 default-cost, area ranges, +per-interface area attachment, interface cost, hello-interval, dead-interval, +retransmit-interval, priority, mtu-ignore, transmit-delay, interface-type, +passive, OSPFv2 prefix-suppression, per-interface BFD +(enabled, local-multiplier, desired-min-tx-interval, +required-min-rx-interval), OSPFv2 per-interface static +neighbours (poll-interval, priority), and per-interface +authentication key-chain (OSPFv2 ospfv2-key-chain, OSPFv3 +ospfv3-key-chain). Existing CLI commands for those leaves +set the same YANG nodes as mgmtd writes. + +``ietf-ospf`` is a mixed-mode module in this branch. Nodes listed in the +mapping table below have config callbacks and mutate daemon state. Other RFC +9129 config nodes may still be parsed and schema-validated by libyang, but +they are intentionally outside the supported write surface unless they are +deviated as not-supported or listed below. Future work should prefer adding a +real callback or a deviation over allowing a writable node to appear supported +without mutating daemon state. + +The daemons advertise only the RFC 9129 features used by the converted +surface. ``ospfd`` enables ``auto-cost``, ``bfd``, ``explicit-router-id``, +``graceful-restart``, ``key-chain``, ``ldp-igp-sync``, ``max-ecmp``, +``mtu-ignore``, ``prefix-suppression``, ``stub-router`` and ``te-rid``. +``ospf6d`` enables ``auto-cost``, ``bfd``, ``explicit-router-id``, +``graceful-restart``, ``key-chain``, ``max-ecmp``, ``mtu-ignore``, +and ``ospfv3-authentication-trailer``. ``mgmtd`` loads the union so it can +validate writes for either backend, while deviations still remove unsupported +leaves from the advertised schema and reject OSPFv2-only writes on OSPFv3 +instances. + +Configuration Mapping Model +--------------------------- + +The config-write implementation should be maintained as a mapping from RFC +9129 schema nodes to existing FRR daemon objects, not as a collection of +independent leaf fixes. Every supported config node should have an explicit +answer for these questions: + +* Which FRR object owns the value? +* Which candidate-state constraints must be rejected during ``NB_EV_VALIDATE``? +* What daemon mutation and protocol side effects happen during ``NB_EV_APPLY``? +* What FRR default is restored when the YANG node is destroyed? +* Which legacy CLI commands enqueue the same YANG edit? +* Which topotest asserts mgmtd writes, CLI writes, deletion, and any negative + validation path? + +The common resolution chain is: + +:: + + control-plane-protocol[type,name] + -> ospfd / ospf6d instance + -> area + -> interface, range, or per-instance attribute + +Missing daemon objects should be rejected in ``NB_EV_VALIDATE`` when accepting +the commit would leave the intended daemon mutation as a silent no-op. The +``NB_EV_APPLY`` phase should still tolerate races, such as an instance, area, or +interface disappearing after validation, and return ``NB_OK`` where no useful +recovery exists. + +Interface APPLY paths also need to materialise daemon-private interface state +when the RFC 9129 area/interface entry is valid but the daemon's native +``if_add`` hook has not populated ``ifp->info`` yet. This can happen during +batched startup loads. ``ospfd`` creates ``ospf_if_info`` before using +``IF_DEF_PARAMS(ifp)``; ``ospf6d`` creates ``struct ospf6_interface`` before +mutating per-interface state. + +Aggregate subtrees use container or list-entry ``apply_finish`` callbacks when +the daemon-side mutation depends on more than one leaf, creates or refreshes a +daemon aggregate, or would otherwise depend on libyang callback ordering. For +BFD, the per-leaf callbacks validate only; the ``/bfd`` container reads the +settled subtree once per transaction and materialises FRR's per-interface BFD +state. For ``static-neighbors/neighbor``, the list-entry callback creates or +refreshes the FRR NBMA neighbour and then applies the settled +``poll-interval`` and ``priority`` values. + +.. warning:: + + Only the key-chain branch of the RFC authentication choice is implemented + in this branch. The explicit-key, IPsec SA and auth-trailer leaves are + marked ``not-supported`` in ``frr-deviations-ietf-routing-ospf.yang`` so + mgmtd rejects unsupported YANG writes at validation time. Those deviation + paths include the RFC ``choice`` / ``case`` schema nodes; direct data-leaf + paths do not resolve through ``pyang`` or ``libyang`` for this part of the + schema. + +The current config-write mapping is: + ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| RFC 9129 config node | OSPFv2 owner / default | OSPFv3 owner / default | Notes | ++===============================+=============================+=============================+=============================+ +| ``control-plane-protocol`` | ``struct ospf`` instance; | ``struct ospf6`` instance; | Parent list entry must stay | +| | destroy calls | destroy calls | in the candidate so child | +| | ``ospf_finish()`` | ``ospf6_delete()`` | edits have a real parent. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``ospf/explicit-router-id`` | ``router_id_static``; | ``router_id_static``; | Apply updates the active | +| | destroy clears to automatic | destroy clears to automatic | router ID and resets OSPFv3 | +| | selection | selection | when needed. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``ospf/preference/*`` | ``distance_*`` fields; | ``distance_*`` fields; | ``internal`` is a coarse | +| | destroy restores OSPFv2 | destroy restores OSPFv3 | RFC leaf mapped onto intra | +| | admin-distance defaults | admin-distance defaults | and inter area distances. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``areas/area`` | ``struct ospf_area``; | ``struct ospf6_area``; | Destroy must first restore | +| | destroy resets area attrs | destroy resets area attrs | child defaults and remove | +| | and ranges before free | and ranges before free | ranges so area-free checks | +| | checks | checks | can pass. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``areas/area/area-type`` | ``external_routing`` via | area stub/NSSA state via | OSPFv2 validates virtual | +| | stub/NSSA helpers; default | ospf6 helpers; default is | links before stub/NSSA | +| | is normal area; destroy | normal area; destroy | conversion. | +| | restores normal area | restores normal area | | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``areas/area/summary`` | ``no_summary`` inverted | ``no_summary`` inverted | RFC ``summary=true`` means | +| | from the RFC leaf; destroy | from the RFC leaf; destroy | summary LSAs are allowed. | +| | allows summaries | allows summaries | | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``areas/area/default-cost`` | ``area->default_cost``; | Not implemented: ospf6d has | The RFC ``when`` constraint | +| | destroy restores ``1`` | no matching FRR CLI/daemon | handles atomic area-type + | +| | | knob | default-cost commits. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``areas/area/ranges/range`` | ``area->ranges`` route | ``area->range_table`` route | Destroy removes the range | +| | table; advertise defaults | table; advertise defaults | entry, including cost and | +| | to true and cost unset | to true and cost unset | advertise state. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``range/advertise`` | range advertise flag; | range flag; destroy | ``false`` maps to | +| | destroy restores advertise | restores advertise | ``not-advertise``. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``range/cost`` | range configured cost; | ``cost_config``; destroy | Destroy restores automatic | +| | destroy unsets cost | unsets configured cost | range cost. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``interfaces/interface`` | ``ospf_if_params`` default | ``struct ospf6_interface`` | The YANG shape is | +| | params plus interface-area | area attachment | area-centric; validation | +| | attachment | | enforces one area per | +| | | | interface. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``interface/cost`` | ``params->output_cost_cmd`` | ``oi->cost`` plus | Destroy returns to auto | +| | and cost recalculation | ``NOAUTOCOST`` flag | cost. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``interface/hello-interval`` | ``params->v_hello``; | ``oi->hello_interval`` | OSPFv2 mirrors the legacy | +| | destroy restores default | | implicit dead-interval | +| | hello behavior | | behavior when dead is unset | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``interface/dead-interval`` | ``params->v_wait`` plus | ``oi->dead_interval`` | Destroy restores daemon | +| | ``is_v_wait_set`` | | defaults. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``interface/retransmit-`` | ``params->retransmit_`` | ``oi->rxmt_interval`` | Destroy restores daemon | +| ``interval`` | ``interval`` | | defaults. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``interface/priority`` | ``params->priority`` and | ``oi->priority`` | OSPFv2 schedules neighbor | +| | neighbor-change side effect | | change when priority moves. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``interface/mtu-ignore`` | ``params->mtu_ignore`` | ``oi->mtu_ignore`` | Destroy restores false. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``interface/transmit-delay`` | ``params->transmit_delay``; | ``oi->transdelay``; destroy | Passive flood-time scalar; | +| | destroy restores ``1`` | restores ``1`` | no protocol side effects. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``interface/interface-type`` | ``params->type`` / OSPF | ``oi->type`` with | Loopback and unsupported | +| | interface type side effects | ``type_cfg`` marker | enum values are rejected at | +| | | | validate. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``interface/passive`` | ``params->passive_`` | ``OSPF6_INTERFACE_PASSIVE`` | Destroy restores active. | +| | ``interface`` and passive | flag | | +| | update helper | | | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``interface/prefix-`` | ``params->prefix_`` | Not implemented: ospf6d | RFC 6860; toggling | +| ``suppression`` | ``suppression``; destroy | has no equivalent | reoriginates Router-LSA on | +| | restores | per-interface flag | every adjacency and the | +| | ``OSPF_PREFIX_SUPPRESSION_``| | Network-LSA on any iface | +| | ``DEFAULT`` | | where this router is DR. | +| | | | Per-address overrides stay | +| | | | on the legacy direct path. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``interface/bfd/enabled`` | ``params->bfd_config`` | ``oi->bfd_config.enabled`` | Presence-style toggle: | +| | as active runtime mirror | as active runtime mirror | modify stores the requested | +| | | | state in YANG; parent | +| | | | ``apply_finish`` | +| | | | materialises daemon BFD | +| | | | state from stored parameter | +| | | | leaves; disable or destroy | +| | | | tears down every session. | +| | | | ``[quick]`` (v2) and | +| | | | ``[profile X]`` (v3) have | +| | | | no YANG counterpart and | +| | | | stay on the legacy path. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``interface/bfd/`` | ``bfd_config->`` | ``oi->bfd_config.`` | Type ``multiplier`` (uint8 | +| ``local-multiplier`` | ``detection_multiplier``; | ``detection_multiplier``; | 1..255). Modify stores the | +| | delete restores the | delete restores the | parameter but does not | +| | defaulted value through | defaulted value through | activate BFD; when enabled, | +| | parent ``apply_finish`` | parent ``apply_finish`` | the session is refreshed. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``interface/bfd/`` | ``bfd_config->min_tx`` / | ``oi->bfd_config.min_tx`` / | RFC unit is microseconds; | +| ``desired-min-tx-interval`` | ``->min_rx``; delete | ``.min_rx``; delete | FRR stores milliseconds. | +| ``interface/bfd/`` | restores defaults through | restores defaults through | NB_EV_VALIDATE rejects | +| ``required-min-rx-interval`` | ``BFD_DEF_MIN_TX`` / | ``BFD_DEF_MIN_TX`` / | non-multiple-of-1000 or | +| | ``BFD_DEF_MIN_RX`` | ``BFD_DEF_MIN_RX`` | out-of-range (50..60000 ms) | +| | | | values; the deviations file | +| | | | pins the RFC default to FRR | +| | | | 300000 us. The single- | +| | | | interval case is marked | +| | | | not-supported. Parameter | +| | | | leaves follow the same | +| | | | activation rule as | +| | | | ``local-multiplier``. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``interface/static-`` | ``struct ospf_nbr_nbma`` | Not implemented: ospf6d | RFC keys the list per- | +| ``neighbors/neighbor`` | via ``ospf_nbr_nbma_set`` / | has no NBMA neighbour | (area, interface, | +| | ``_unset``; list-entry | surface | identifier); FRR's NBMA | +| | ``apply_finish`` applies | | table is per-(instance, | +| | settled ``poll-interval`` | | addr). Area/interface | +| | and ``priority`` values | | labels are stored in the | +| | after create / modify | | candidate but ignored on | +| | callbacks validate | | the FRR side: FRR auto- | +| | | | binds the entry to the OI | +| | | | whose subnet matches the | +| | | | neighbour address. | +| | | | Duplicate identifiers in | +| | | | one OSPF instance are | +| | | | rejected at validate. | +| | | | ``poll-interval`` defaults | +| | | | to 60 and ``priority`` to 0 | +| | | | to match FRR's NBMA | +| | | | neighbour defaults. The | +| | | | RFC ``cost`` leaf is | +| | | | marked not-supported in the | +| | | | deviations file (FRR has | +| | | | no NBMA cost knob). Legacy | +| | | | ``neighbor A.B.C.D`` CLI | +| | | | stays on the direct path: | +| | | | it is instance-level and | +| | | | cannot synthesise a YANG | +| | | | area/interface key. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``interface/`` | ``params->keychain_name`` | ``oi->at_data.keychain`` | Only the key-chain case of | +| ``authentication/`` | + ``auth_type = | + ``OSPF6_AUTH_TRAILER_`` | the RFC's authentication | +| ``ospfv2-key-chain`` | OSPF_AUTH_CRYPTOGRAPHIC``; | ``KEYCHAIN`` flag; destroy | choice is implemented in | +| (v2) | destroy restores | clears flag + frees | this branch. v3 rejects | +| ``interface/`` | NOTSET | keychain | the modify at | +| ``authentication/`` | | | NB_EV_VALIDATE if a manual | +| ``ospfv3-key-chain`` | | | key is already set (mirrors | +| (v3) | | | the legacy CLI's lock). | +| | | | The RFC type is | +| | | | ``key-chain:key-chain-ref`` | +| | | | (leafref), so the named | +| | | | keychain must exist at | +| | | | commit time; this diverges | +| | | | from the legacy | +| | | | CLI which accepts forward | +| | | | references. Other | +| | | | authentication leaves | +| | | | (explicit-key, IPsec SA, | +| | | | auth-trailer-rfc) are | +| | | | deferred and marked | +| | | | ``not-supported`` via | +| | | | deviations that include the | +| | | | RFC ``choice`` / ``case`` | +| | | | schema nodes. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``ospf/spf-control/paths`` | ``ospf->max_multipath``; | ``ospf6->max_multipath``; | RFC types ``paths`` as | +| | destroy restores | destroy restores | uint16 (1..65535) and FRR's | +| | ``MULTIPATH_NUM`` | ``MULTIPATH_NUM`` | ``MULTIPATH_NUM`` cap stays | +| | | | enforced in the CLI body. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``ospf/auto-cost/enabled`` | no-op on modify=true; | no-op on modify=true; | FRR has no off-switch for | +| | NB_EV_VALIDATE rejects | NB_EV_VALIDATE rejects | auto-cost. Deviations file | +| | modify=false; destroy | modify=false; destroy | pins default to ``true`` so | +| | no-ops | no-ops | | +| | | | the ``when`` clause on | +| | | | ``reference-bandwidth`` is | +| | | | always satisfied. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``ospf/auto-cost/reference-`` | ``ospf->ref_bandwidth``; | ``ospf6->ref_bandwidth``; | RFC units are Mbits. | +| ``bandwidth`` | destroy restores | destroy restores | Modify walks every VRF | +| | ``OSPF_DEFAULT_REF_`` | ``OSPF6_REFERENCE_`` | interface (v2) or area | +| | ``BANDWIDTH`` | ``BANDWIDTH`` | interface (v3) to recompute | +| | | | output cost. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``ospf/mpls/ldp/igp-sync`` | ``ospf->ldp_sync_cmd`` | Not implemented: ospf6d | Enable registers opaque | +| | flags; modify=true enables | has no LDP/IGP sync | LDP zclient handlers and | +| | and walks all PtoP ifaces, | implementation | walks all interfaces; | +| | modify=false / destroy call | | ``ospf_ldp_sync_gbl_exit`` | +| | ``ospf_ldp_sync_gbl_exit`` | | tears the state back down. | +| | | | Validate rejects non- | +| | | | default VRF instances. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``ospf/mpls/te-rid/`` | ``OspfMplsTE.router_addr`` | Not implemented: ospf6d | MPLS-TE state is per- | +| ``ipv4-router-id`` | (process-wide global); | has no MPLS-TE module | process global; validate | +| | destroy zeros the TLV | | rejects non-default VRF. | +| | header so the running | | Modify refreshes the | +| | config gates the line off | | Opaque Router-Address LSAs | +| | | | when MPLS-TE is enabled, | +| | | | otherwise just stores the | +| | | | value for later use. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``ospf/graceful-restart/`` | ``ospf->gr_info.restart_`` | ``ospf6->gr_info.restart_`` | NB_EV_VALIDATE rejects | +| ``enabled`` | ``support`` plus zebra GR | ``support`` plus zebra GR | disable when a GR prepare | +| | enable / NVM bookkeeping | enable / NVM bookkeeping | is in flight (mirrors the | +| | | | legacy CLI rejection). | +| | | | Sibling restart-interval is | +| | | | a separate northbound knob. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``ospf/graceful-restart/`` | ``ospf->gr_info.grace_`` | ``ospf6->gr_info.grace_`` | RFC default is 120s, which | +| ``restart-interval`` | ``period``; destroy | ``period``; destroy | matches FRR's compile-time | +| | restores | restores | default; no deviation | +| | ``OSPF_DFLT_GRACE_`` | ``OSPF6_DFLT_GRACE_`` | needed. Modify refreshes | +| | ``INTERVAL`` | ``INTERVAL`` | the zebra stale-route timer | +| | | | when GR is enabled. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``ospf/graceful-restart/`` | ``ospf->is_helper_`` | ``ospf6->ospf6_helper_cfg`` | RFC has no enable-list; the | +| ``helper-enabled`` | ``supported`` via | ``.is_helper_supported`` | legacy `graceful-restart | +| | ``ospf_gr_helper_support_`` | via | helper enable A.B.C.D` per- | +| | ``set`` | ``ospf6_gr_helper_support_``| router-id form stays on the | +| | | ``set`` | legacy direct mutation path.| +| | | | Disable evicts every active | +| | | | helper not pinned by the | +| | | | per-router-id list. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``ospf/graceful-restart/`` | ``ospf->strict_lsa_check`` | ``ospf6->ospf6_helper_cfg`` | FRR default is true on both | +| ``helper-strict-lsa-`` | via | ``.strict_lsa_check`` via | daemons; destroy restores | +| ``checking`` | ``ospf_gr_helper_lsa_`` | ``ospf6_gr_helper_`` | true. v3's legacy CLI uses | +| | ``check_set`` | ``lsacheck_set`` | the inverted form | +| | | | ``lsa-check-disable``; the | +| | | | DEFPY_YANG shim flips the | +| | | | meaning before enqueueing. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``ospf/stub-router/always`` | ``OSPF_AREA_ADMIN_STUB_`` | Not implemented: ospf6d | Presence container | +| | ``ROUTED`` per area + | has no stub-router | (create / destroy | +| | ``ospf->stub_router_admin`` | implementation | callbacks). RFC 6987 | +| | ``_set``; destroy preserves | | unconditional stub router; | +| | in-flight startup-timer | | per-area LSA reorigination | +| | stub state | | triggered by the flag flip. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ + +When adding another RFC 9129 config node, add its row here before or alongside +the callback implementation. If the row cannot name a daemon owner, default +restore behavior, and CLI-equivalent command, the node is probably outside this +branch's current config-write scope. + +Direct daemon config-file loads in ``ospfd`` and ``ospf6d`` opt in to batching +for the process lifetime, so cross-leaf validation can evaluate any direct +daemon config-file load as one northbound transaction. This is not a +startup-only temporary flag; a later ``config_from_file()`` call in these +daemons has the same cross-leaf validation requirements. Other daemons keep the +legacy per-line config-file behavior. + +RPC Support +----------- + +Both RFC 9129 RPCs are implemented on ospfd and ospf6d: + ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| RFC 9129 RPC | OSPFv2 mapping | OSPFv3 mapping | Notes | ++===============================+=============================+=============================+=============================+ +| ``clear-neighbor`` | ``ospf_neighbor_reset`` for | ``ospf6_interface_clear`` | Both daemons register the | +| | the instance, or | iterated over every | xpath; mgmtd fans the RPC | +| | neighbour-on-OI loop for | OSPFv3-bound interface, or | out to every backend. | +| | the per-interface case | a single ``ifp`` for the | Each handler looks up the | +| | | per-interface case | named instance and returns | +| | | | ``NB_OK`` silently when | +| | | | not local. Unknown | +| | | | interface returns | +| | | | ``NB_ERR_RESOURCE`` with | +| | | | ``ospf-interface-not- | +| | | | found`` per the RFC. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``clear-database`` | ``ospf_process_reset`` | ``ospf6_process_reset`` | Flushes self-originated | +| | | | LSAs and drops every | +| | | | adjacency; the RFC | +| | | | semantics line up exactly | +| | | | with the existing process- | +| | | | reset helpers the legacy | +| | | | ``clear ip ospf process`` | +| | | | / ``clear ipv6 ospf6 | +| | | | process`` commands invoke. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ + +The RPC xpaths are registered via the ``rpc_xpaths`` array on each daemon's +``mgmt_be_client_cbs`` (the per-frr-yang-module-info ``.cbs.rpc`` entry alone +is not enough -- mgmtd routes RPC dispatch through the BE-adapter subscription +map, not the schema callback registry). + +The libyang RPC parser expects the input wrapped in the RPC name: + +:: + + mgmt rpc /ietf-ospf:clear-neighbor json + {"ietf-ospf:clear-neighbor":{"routing-protocol-name":"default"}} + +Two deviations from strict RFC 9129 wording are intentional: + +* RFC 9129 says an RPC against an unknown ``routing-protocol-name`` SHALL + fail with ``error-tag=data-missing`` and ``error-app-tag=routing-protocol- + instance-not-found``. Both daemons receive every dispatched RPC and look + up the named instance locally; a non-owner can't distinguish "instance + doesn't exist on me but might exist on the sibling" from "instance + doesn't exist anywhere", so non-owners return ``NB_OK`` silently rather + than racing each other to surface a misleading "not found" reply. If no + daemon owns the name, the client sees combined success. Lifting this + would require mgmtd-side coordination across backend replies (collect + every backend's verdict before responding to the frontend), which is + outside the scope of this slice. + + The same RFC input shape means an OSPFv2 instance and an OSPFv3 instance + with the same ``routing-protocol-name`` both match the RPC. In that case, + ``clear-neighbor`` and ``clear-database`` intentionally act on both local + instances. The RFC RPC input has no protocol ``type`` key to disambiguate + the two split FRR daemons, and treating both matching local instances as + owners keeps the behaviour aligned with the model the client invoked. A + client that needs protocol-specific clearing should use distinct instance + names until mgmtd grows a coordinated multi-backend RPC result model. + +* RFC 9129 prescribes structured ``error-app-tag`` strings + (``routing-protocol-instance-not-found``, ``ospf-interface-not-found``). + FRR's ``nb_cb_rpc_args`` carries only an unstructured ``errmsg`` buffer, + so the app-tag string is embedded in the message text rather than + surfaced via a NETCONF / RESTCONF ```` element. The + ``ospf-interface-not-found`` case returns ``NB_ERR_NOT_FOUND`` (mgmtd + maps to ``MGMTD_INVALID_PARAM``) so the error code at least reflects + "client supplied a bad reference" rather than a daemon-internal failure. + +Frontend coverage +~~~~~~~~~~~~~~~~~ + +The vtysh frontend (``mgmt rpc XPATH json DATA``) is the supported +invocation path for these RPCs. It exercises the full mgmtd commit and +backend-dispatch flow; the four topotests above cover it. + +FRR's per-daemon gRPC frontend (``lib/northbound_grpc.cpp``, enabled with +``--enable-grpc`` and loaded per-daemon via ``-M grpc:``) cannot +dispatch these RPCs in standalone mode. ``HandleUnaryExecute`` calls +``lyd_validate_op(..., LYD_TYPE_RPC_YANG, ...)`` before invoking the +backend handler, and that validator strictly enforces the +``routing-protocol-name`` leafref against the daemon's local libyang +context. ``ospfd`` and ``ospf6d`` have no ``/ietf-routing:routing/...`` +state populated in their own contexts (that data lives in mgmtd's +candidate datastore), so every Execute call with a valid +``routing-protocol-name`` is rejected with ``grpc::StatusCode::INVALID_ +ARGUMENT`` ``"Invalid input data"``. This is a frontend-side limitation, +not specific to these RPCs; it affects every RFC 9129 RPC whose inputs +include a leafref into the routing tree. Fixing it would require either +relaxing leafref validation for RPC inputs in +``lib/northbound_grpc.cpp``, or having backend daemons mirror the +routing-protocol list locally. Use the mgmtd-fronted vtysh path until +either change lands upstream. + +Notification Support +-------------------- + +RFC 9129 notifications are emitted by hooking the existing state-change +hooks each daemon already exposes: + ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| RFC 9129 notification | OSPFv2 hook | OSPFv3 hook | Notes | ++===============================+=============================+=============================+=============================+ +| ``nbr-state-change`` | ``ospf_nsm_change`` | ``ospf6_neighbor_change`` | OSPFv2 NSM state values | +| | | | translate via a small table | +| | | | (FRR reserves 0/1 for | +| | | | DependUpon/Deleted); OSPFv3 | +| | | | NSM values match RFC 1:1. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``if-state-change`` | ``ospf_ism_change`` | ``ospf6_interface_change`` | Both daemons need a state | +| | | | translation table: numeric | +| | | | values agree for the first | +| | | | four states but FRR orders | +| | | | the DR-election trio as | +| | | | DROther/Backup/DR while RFC | +| | | | uses dr/bdr/dr-other. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``restart-status-change`` | ``ospf_gr_restart_enter`` / | ``ospf6_gr_restart_enter`` /| Direct emit calls from the | +| | ``ospf_gr_restart_exit`` | ``ospf6_gr_restart_exit`` | GR enter/exit sites (no | +| | | | dedicated hook). All FRR- | +| | | | known restart reasons are | +| | | | software-initiated, so they | +| | | | map to RFC | +| | | | ``planned-restart`` (value | +| | | | 2); ``unplanned-restart`` | +| | | | (3) would correspond to a | +| | | | crash recovery FRR does | +| | | | not currently signal. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``nbr-restart-helper-status-``| ``ospf_gr_helper`` enter + | ``ospf6_gr_helper`` enter + | Direct emit calls, one when | +| ``change`` | ``ospf_gr_helper_exit`` | ``ospf6_gr_helper_exit`` | the router accepts a grace- | +| | | | LSA and becomes helper, and | +| | | | one when helper status ends | +| | | | (completion, timeout, or | +| | | | topology change). FRR's | +| | | | ``enum ospf_helper_exit_`` | +| | | | ``reason`` is offset by 1 | +| | | | from the RFC | +| | | | ``restart-exit-reason-type``| +| | | | values but otherwise | +| | | | matches name-for-name. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``if-rx-bad-packet`` | ``ospf_read_helper`` post- | ``ospf6_receive`` post- | Emitted once per packet | +| | ``ospf_verify_header`` | header-validation failure | that fails the post-header | +| | failure path | path | sanity check. ``packet- | +| | | | type`` leaf is omitted when | +| | | | the header didn't parse far | +| | | | enough to extract it. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``if-config-error`` | hello-interval and dead- | hello-interval and dead- | Wired at the two most | +| | interval mismatch checks | interval mismatch checks | commonly-hit per-packet | +| | in ``ospf_hello`` | in ``ospf6_receive`` | mismatches; ``error`` leaf | +| | | | passed as RFC enum name | +| | | | string. Other reject paths | +| | | | (auth-failure, mtu- | +| | | | mismatch, area-mismatch, | +| | | | option-mismatch, etc.) wire | +| | | | as future incremental work; | +| | | | the emit helper is generic | +| | | | enough that additions are | +| | | | one-line call insertions. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ +| ``nssa-translator-status-`` | ``ospf_abr_nssa_check_`` | n/a; ospf6d has no NSSA | Wired at the | +| ``change`` (v2 only) | ``status`` transition site | translator surface | transition site. FRR keeps | +| | | | configured translator role | +| | | | and operational translator | +| | | | state separately; the | +| | | | emitter combines both to | +| | | | produce the RFC states | +| | | | enabled, elected or | +| | | | disabled. | ++-------------------------------+-----------------------------+-----------------------------+-----------------------------+ + +Each daemon registers its hook subscriber from +``ospf{,6}d_ietf_notif_init()``, called once from +``ospf{,6}_master_init()``. The handlers build the YANG notification +data tree (instance header, interface identity, neighbour leaves, the +RFC ``nbr-state-type`` enum) and dispatch through +``nb_notification_send()``. ``DEBUGD(&nb_dbg_notif, ...)`` logs every +emit so operators can verify wiring with +``debug northbound notifications``. + +Out of scope for now: ``lsdb-approaching-overflow`` and +``lsdb-overflow`` (FRR has no max-LSA threshold, as documented under +Remaining Scope). All other RFC 9129 notifications now have emit wiring on +at least one daemon. + +Live tests cover ``nbr-state-change``, ``if-state-change`` and +``if-config-error`` by driving the protocol paths that emit them. The +graceful-restart suites for ``ospfd`` and ``ospf6d`` also cover +``restart-status-change`` and ``nbr-restart-helper-status-change`` by +triggering a real graceful-restart prepare, helper observation and daemon +restart in ``ospf_gr_topo1`` and ``ospf6_gr_topo1``. + +Live test coverage for ``if-rx-bad-packet`` and +``nssa-translator-status-change`` is deferred. The former needs a topotest +packet-injection helper that can send malformed OSPF packets safely inside a +router namespace and drive the daemon receive path. The latter needs an NSSA +topology that drives a real translator state transition, rather than merely +calling the notification helper. Both emit sites are documented in the table +above and can be checked manually with ``debug northbound notifications`` until +those focused tests are added. + +Remaining Scope +--------------- + +``ietf-ospf`` remains the canonical configuration tree for everything RFC 9129 +models. ``frr-ospfd`` and future ``frr-ospf6d`` should augment only +FRR-specific behavior that the RFC model does not cover. + +The current config-write scope deliberately does not include redistribution, +default-information-originate, virtual links, per-address OSPFv2 interface +overrides, OSPFv2 NSSA translator/suppress-fa knobs, or other FRR-specific +extensions outside RFC 9129. A native OSPFv3 module should be added only when +there is concrete FRR-specific state or configuration to expose. + +The following RFC 9129 nodes are also out of scope because neither ``ospfd`` +nor ``ospf6d`` has any matching FRR surface to map onto: + +* ``ospf/nsr`` -- FRR has no OSPF Non-Stop Routing. +* ``ospf/database-control/max-lsa`` -- FRR's overload protection is + ``max-metric router-lsa on-shutdown/on-startup``, structurally different + from RFC 9129's max-LSA accept threshold. +* ``ospf/spf-control/ietf-spf-delay`` -- FRR's ``timers throttle spf`` + implements a different back-off algorithm. +* ``ospf/node-tag-config`` -- FRR has no node-tag CLI, struct field, or LSA + encoding; ``router-info`` only enables the Opaque Router Information LSA + without exposing administrative tags. +* ``ospf/enabled`` and per-interface ``interface/enabled`` -- FRR has no + separate OSPF on/off toggle. The protocol runs whenever an ``ospfd`` / + ``ospf6d`` instance is configured (control-plane-protocol create) and on + every interface that is bound into an area. Writing ``enabled=false`` + has no corresponding FRR mutation. +* ``interface/multi-areas`` (``{multi-area-adj}`` feature) -- FRR has no + multi-area-adjacency surface on either daemon. Each interface belongs + to exactly one area. +* ``interface/ttl-security`` (``{ttl-security}`` feature) -- neither + ``ospfd`` nor ``ospf6d`` exposes a per-interface TTL-security check; + GTSM is a generic socket option used elsewhere in FRR but not wired + into the OSPF interface params. +* ``ospf/fast-reroute/lfa`` and ``interface/fast-reroute/lfa/*`` + (``{fast-reroute}`` / ``{lfa}`` / ``{remote-lfa}`` features) -- FRR's + only OSPF fast-reroute surface is the instance-level + ``fast-reroute ti-lfa [node-protection]`` command on ``ospfd`` (writes + ``ospf->ti_lfa_enabled`` + ``ti_lfa_protection_type``). RFC 9129 models + LFA as a per-interface enable plus an empty instance-level container + (``Container creation has no effect on LFA activation.``); the + semantics don't line up. ``ospf6d`` has no TI-LFA implementation at + all. The legacy ``fast-reroute ti-lfa`` CLI stays on the direct + mutation path. +* ``ospf/address-family`` -- the RFC leaf is only present for OSPFv3 + and is retained because notification headers leafref it. FRR constrains + the leaf to ``ipv6`` in the deviation module; other values are rejected + by schema validation because this branch has no separate address-family + knob. +* ``ospf/mpls/te-rid/ipv6-router-id`` -- the converted OSPFv2 TE callback maps + the IPv4 Router Address TLV. The RFC 9129 IPv6 TE router-id leaf is not + wired in this branch and is marked ``not-supported``. +* ``area/virtual-links`` -- FRR has OSPFv2 virtual-link CLI support, but the + RFC 9129 virtual-link subtree is not wired through northbound callbacks in + this branch. It is marked ``not-supported`` until the timers and + authentication leaves below it can be implemented as a coherent unit. +* ``interface/instance-id`` -- the OSPFv3 instance ID has legacy CLI support, + but is not exposed through RFC 9129 callbacks in this branch. +* ``interface/authentication/(auth-key-explicit|ospfv2-auth-trailer-rfc)`` + and the OSPFv3 ``ospfv3-sa`` / ``ospfv3-key`` / ``ospfv3-sa-id`` + branches -- the legacy CLI supports them, but this branch implements + only the key-chain case through YANG. The unsupported leaves are + marked ``not-supported`` by deviation so mgmtd rejects them at + validation time. The matching legacy CLI commands stay on the direct + mutation path. + +These should be revisited only if FRR grows the underlying surface; until +then, leaving the YANG nodes unimplemented is preferable to silent +no-op callbacks. + +Test Coverage +------------- + +``tests/topotests/ospf_topo1/test_ospf_topo1.py`` includes an RFC 9129 +operational-data check for OSPFv2 and OSPFv3 router-id, area, interface, and +neighbor state. It also checks that ``ospfd`` and ``ospf6d`` register with mgmtd +and that mgmtd's operational xpath registry includes the RFC 9129 control-plane +protocol subtree. + +The operational tests also include a targeted mgmtd dispatch check. Predicate +queries for ``ietf-ospf:ospfv2`` and ``ietf-ospf:ospfv3`` must return exactly +one protocol entry from the correct backend, and the backend subscription check +must show that the other OSPF daemon was not selected. Unfiltered parent/list +queries must still select both OSPF daemons. A predicate query against +``ietf-interfaces`` verifies that untyped backend registrations still match. + +The same test file includes config-write checks for the supported OSPFv2 and +OSPFv3 leaves. The tests exercise mgmtd writes, legacy CLI writes routed through +YANG, negative validation paths, and cleanup after deleting and recreating an +area. + +Deviation coverage follows the same mapping rule. A deviation that supplies an +FRR default must have a positive test for the defaulted behaviour, such as bare +``auto-cost/reference-bandwidth``, bare BFD enable, or partial NBMA static +neighbour writes. A deviation that marks an RFC leaf ``not-supported`` must +have a rejection test, such as ``ospf/enabled``, ``interface/enabled``, BFD +``min-interval``, or NBMA static-neighbour ``cost``. + +``tests/topotests/ospf_yang_startup_config/test_ospf_yang_startup_config.py`` +checks startup config-file batching in an isolated one-router topology. Its +``r1/ospfd.conf`` places an OSPFv2 ``default-cost`` line before the stub-area +line that makes it valid; startup succeeds only when the whole daemon config +file is committed as one northbound transaction. ``r1/ospf6d.conf`` keeps a +matching OSPFv3 stub area in the startup path. + +The test queries the merged mgmtd operational datastore rather than daemon-local +``show yang operational-data`` output. Zebra supplies the narrow +``/ietf-interfaces:interfaces/interface`` operational state needed to satisfy RFC +9129's interface-name leafref, so the OSPF model is satisfied without deviating +from RFC 9129. In netns-backed VRF mode, OSPF and zebra both emit +VRF-qualified interface names, matching FRR's existing +``frr-interface:lib/interface`` convention for netns interface keys. + +The test should run without ``Invalid leafref``, ``Invalid identityref``, or +missing-module errors in the router daemon logs. diff --git a/doc/user/grpc.rst b/doc/user/grpc.rst index ff5d192c442e..22e359b91702 100644 --- a/doc/user/grpc.rst +++ b/doc/user/grpc.rst @@ -28,6 +28,15 @@ Northbound gRPC Features There is currently no support for YANG notifications. +.. note:: + + Per-daemon gRPC ``Execute`` validates RPC input in the daemon-local + libyang context before dispatching to the northbound callback. RPCs whose + input contains a leafref into ``/ietf-routing:routing`` can therefore fail + with ``INVALID_ARGUMENT`` even when the referenced routing protocol exists + in mgmtd. The mgmtd-fronted ``vtysh`` RPC path does not have this + limitation. + .. note:: diff --git a/doc/user/ospf6d.rst b/doc/user/ospf6d.rst index e94dbd778f4d..1d656a4c3588 100644 --- a/doc/user/ospf6d.rst +++ b/doc/user/ospf6d.rst @@ -933,6 +933,65 @@ Showing OSPF6 information This command shows the graceful-restart helper details including helper configuration parameters. +YANG / NETCONF Support +---------------------- + +OSPFv3 operational state and a subset of OSPFv3 configuration are exposed +through the standard :rfc:`9129` ``ietf-ospf`` YANG model. The OSPFv3 +instance itself remains owned by the legacy ``router ospf6`` CLI, but +per-area, per-interface and per-instance configuration leaves are routed +through the mgmtd northbound and can be read, set, and committed through +NETCONF / RESTCONF / ``vtysh``'s ``mgmt`` subcommands as well as the legacy +CLI. + +The supported set mirrors the OSPFv2 side documented in :ref:`ospfv2`, with +two v3-specific constraints: + +* ``areas/area/default-cost``: ospf6d has no per-area stub default-cost + knob, so this OSPFv2-only leaf is rejected by the deviation module's + schema constraints on OSPFv3 instances. The same OSPFv2-only protection + applies to ``mpls/ldp/igp-sync``, ``mpls/te-rid/ipv4-router-id``, + ``interface/prefix-suppression``, ``static-neighbors/neighbor`` and + ``virtual-links``. ``ospf/address-family`` is retained for RFC 9129 + notification leafrefs and constrained to ``ipv6``. +* ``interface-type``: RFC 9129 declares ``broadcast``, ``non-broadcast``, + ``point-to-multipoint``, ``point-to-point`` and ``hybrid``. ospf6d only + accepts ``broadcast``, ``point-to-point`` and ``point-to-multipoint``; + the NB callback rejects ``non-broadcast`` and ``hybrid`` at VALIDATE + with a clear error. + +Otherwise the supported set matches the shared OSPFv2 RFC 9129 surface: +router-id, preference, spf-control paths, auto-cost, graceful-restart +enabled, restart-interval, helper-enabled and helper-strict-lsa-checking, +area lifecycle, area-type, area summary, ranges, per-interface attachment, +cost, hello-interval, dead-interval, retransmit-interval, priority, +mtu-ignore, transmit-delay, interface-type, passive, per-interface BFD +``enabled``, ``local-multiplier``, ``desired-min-tx-interval`` and +``required-min-rx-interval``, and per-interface +``authentication/ospfv3-key-chain``. + +For per-interface BFD, ``bfd/enabled`` controls activation. The multiplier +and interval leaves can be configured while BFD is disabled, but they do not +create or register BFD sessions until ``bfd/enabled=true`` is committed. The +legacy parameterised BFD CLI enqueues that enable leaf before it writes the +parameter leaves. + +Examples +^^^^^^^^ + +Retrieve the OSPFv3 instance from the operational datastore: + +.. code-block:: shell + + vtysh -c 'show mgmt get-data /ietf-routing:routing/control-plane-protocols/control-plane-protocol[type="ietf-ospf:ospfv3"][name="default"] datastore operational' + +Retrieve the merged operational datastore, including the OSPFv3 protocol +entry and the ``ietf-interfaces`` data used by OSPF interface leafrefs: + +.. code-block:: shell + + vtysh -c 'show mgmt get-data /* datastore operational' + .. clicmd:: show debugging ospf6 Show debugging status for OSPFv3. diff --git a/doc/user/ospfd.rst b/doc/user/ospfd.rst index 04d0c85d58ae..7a0add46c92a 100644 --- a/doc/user/ospfd.rst +++ b/doc/user/ospfd.rst @@ -1083,6 +1083,116 @@ Showing Information Displays the Graceful Restart Helper details including helper config changes. +YANG / NETCONF Support +---------------------- + +OSPF operational state and a subset of OSPF configuration are exposed through +the standard :rfc:`9129` ``ietf-ospf`` YANG model. The OSPF instance itself +remains owned by the legacy ``router ospf`` CLI, but per-area, per-interface +and per-instance configuration leaves are routed through the mgmtd northbound +so they can be read, set and committed via NETCONF / RESTCONF / ``vtysh``'s +``mgmt`` subcommands as well as the legacy CLI. + +For the default or VRF-based daemon, the RFC 9129 +``control-plane-protocol`` name is the OSPF VRF name, normally ``default``. +When ``ospfd`` is started in daemon-instance mode, the RFC 9129 name is the +decimal instance ID used by the legacy CLI; for example, ``router ospf 5`` is +addressed as ``control-plane-protocol[type='ietf-ospf:ospfv2'][name='5']``. +This keeps separate ``ospfd`` backend processes distinct in mgmtd. + +Supported configuration leaves +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Under ``/ietf-routing:routing/control-plane-protocols/control-plane-protocol[type='ietf-ospf:ospfv2']/ietf-ospf:ospf``: + +* ``explicit-router-id`` +* ``preference/{all,intra-area,inter-area,internal,external}`` (admin distance) +* ``spf-control/paths`` +* ``auto-cost/{enabled,reference-bandwidth}`` +* ``mpls/ldp/igp-sync`` and ``mpls/te-rid/ipv4-router-id`` +* ``graceful-restart/{enabled,restart-interval,helper-enabled,helper-strict-lsa-checking}`` +* ``stub-router/always`` +* ``areas/area`` (list create / destroy keyed by ``area-id``) +* ``areas/area/area-type`` (``normal-area``, ``stub-area``, ``nssa-area``) +* ``areas/area/summary`` (totally-stubby toggle; RFC 9129 inverts FRR's + ``no-summary`` sense) +* ``areas/area/default-cost`` (stub / NSSA only) +* ``areas/area/ranges/range`` (list create / destroy), with ``advertise`` + and ``cost`` leaves +* ``areas/area/interfaces/interface`` (list create / destroy: assigns an + interface to the area), plus the per-interface leaves + ``cost``, ``hello-interval``, ``dead-interval``, ``retransmit-interval``, + ``priority``, ``mtu-ignore``, ``transmit-delay``, ``interface-type``, + ``passive`` and ``prefix-suppression`` +* ``areas/area/interfaces/interface/bfd`` leaves: ``enabled``, + ``local-multiplier``, ``desired-min-tx-interval`` and + ``required-min-rx-interval`` +* ``areas/area/interfaces/interface/static-neighbors/neighbor`` (list create + / destroy), with ``poll-interval`` and ``priority`` leaves +* ``areas/area/interfaces/interface/authentication/ospfv2-key-chain`` + +For per-interface BFD, ``bfd/enabled`` controls activation. The multiplier +and interval leaves can be configured while BFD is disabled, but they do not +create or register BFD sessions until ``bfd/enabled=true`` is committed. The +legacy parameterised BFD CLI enqueues that enable leaf before it writes the +parameter leaves. + +Out of scope for this slice +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* ``redistribute`` and ``default-information-originate`` are FRR-specific + concepts that RFC 9129 / :rfc:`8349` leave to a separate import / export + mechanism. They stay reachable through the legacy CLI on the + direct-mutation path. +* Per-address overrides (e.g. ``ip ospf cost N A.B.C.D``) have no RFC 9129 + counterpart; the YANG model is strictly per-interface. The legacy CLI + with an explicit address argument continues to use direct mutation. +* FRR-specific area NSSA augments (translator-role, + default-information-originate, suppress-fa) are not in the RFC 9129 area + grouping; they remain legacy-CLI-only. +* The ``router ospf [{(1-65535)|vrf NAME}]`` instance creation step is still + CLI-only; YANG operations that target a non-existent instance are + rejected at VALIDATE with a clear error pointing at ``router ospf``. + +Examples +^^^^^^^^ + +Retrieve the OSPFv2 instance from the operational datastore: + +.. code-block:: shell + + vtysh -c 'show mgmt get-data /ietf-routing:routing/control-plane-protocols/control-plane-protocol[type="ietf-ospf:ospfv2"][name="default"] datastore operational' + +Retrieve the merged operational datastore, including the OSPF protocol +entry and the ``ietf-interfaces`` data used by OSPF interface leafrefs: + +.. code-block:: shell + + vtysh -c 'show mgmt get-data /* datastore operational' + +Set the router-id through mgmtd, then commit: + +.. code-block:: shell + + vtysh -c 'configure terminal file-lock' \ + -c 'mgmt set-config /ietf-routing:routing/control-plane-protocols/control-plane-protocol[type="ietf-ospf:ospfv2"][name="default"]/ietf-ospf:ospf/explicit-router-id "10.0.0.1"' \ + -c 'mgmt commit apply' + +The corresponding ``ospf router-id 10.0.0.1`` legacy CLI command takes the +same path through the northbound; both surfaces converge on the same +committed running configuration. + +Interface leafref relaxation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +RFC 9129's per-interface entry keys on a leafref into +``/ietf-interfaces:interfaces/interface/name``. The +``frr-deviations-ietf-routing-ospf`` deviation relaxes this so OSPF config +can be staged ahead of interface plumbing (useful when emitting config from +an external orchestrator). The relaxation removes libyang's referential +check; the NB callbacks restore it themselves, rejecting unknown interface +names at VALIDATE with a clear error. + .. _opaque-lsa: Opaque LSA diff --git a/lib/bfd.c b/lib/bfd.c index 5c0004224b36..cf962f4bf9a7 100644 --- a/lib/bfd.c +++ b/lib/bfd.c @@ -1347,3 +1347,24 @@ bool bfd_session_is_admin_down(const struct bfd_session_params *session) { return session->bss.state == BSS_ADMIN_DOWN; } + +/* + * Shared ietf-bfd-types loader. See the header comment in lib/bfd.h + * for why backend clients that inherit from `bfd-types:client-cfg- + * parms` need this in their `frr_yang_module_info` table. Wildcard + * features enabled so every if-feature in the module (including + * `client-base-cfg-parms` which gates the multiplier and tx/rx + * intervals) becomes visible in the compiled schema. + */ +static const char *const ietf_bfd_types_features[] = { "*", NULL }; + +const struct frr_yang_module_info ietf_bfd_types_info = { + .name = "ietf-bfd-types", + .features = (const char **)ietf_bfd_types_features, + .ignore_cfg_cbs = true, + .nodes = { + { + .xpath = NULL, + }, + }, +}; diff --git a/lib/bfd.h b/lib/bfd.h index 881f263e7d15..13875c686e9a 100644 --- a/lib/bfd.h +++ b/lib/bfd.h @@ -9,11 +9,25 @@ #define _ZEBRA_BFD_H #include "lib/zclient.h" +#include "lib/northbound.h" #ifdef __cplusplus extern "C" { #endif +/* + * Module info for ietf-bfd-types. Backend clients that publish data + * nodes inheriting from `bfd-types:client-cfg-parms` (e.g. OSPFv2 and + * OSPFv3 via RFC 9129's ietf-ospf BFD container) must list this in + * their `frr_yang_module_info` table so libyang enables the foreign + * module's `client-base-cfg-parms` if-feature. Without it the + * inherited multiplier / tx-rx-interval leaves get elided from the + * compiled schema and mgmtd reports "unknown data path" for every + * write but `enabled`. Mirrors the existing `ietf_key_chain_info` + * pattern (lib/keychain.h). + */ +extern const struct frr_yang_module_info ietf_bfd_types_info; + #define BFD_DEF_MIN_RX 300 #define BFD_DEF_MIN_TX 300 #define BFD_DEF_DETECT_MULT 3 diff --git a/lib/command.c b/lib/command.c index 3982c7956a94..f757ba276cbb 100644 --- a/lib/command.c +++ b/lib/command.c @@ -94,6 +94,12 @@ struct host host; /* for vtysh, put together CLI trees only when switching into node */ static bool defer_cli_tree; +/* Direct daemon config-file loads normally preserve legacy per-line commit + * behavior. Daemons with cross-leaf YANG validation can opt in to batching + * their file loads into one northbound transaction. + */ +static bool batch_config_file; + /* * Returns host.name if any, otherwise * it returns the system hostname. @@ -121,6 +127,11 @@ const char *cmd_release_get(void) return host.release; } +void cmd_config_file_batching_set(bool enabled) +{ + batch_config_file = enabled; +} + const char *cmd_version_get(void) { return host.version; @@ -1317,7 +1328,11 @@ int command_config_read_one_line(struct vty *vty, int config_from_file(struct vty *vty, FILE *fp, unsigned int *line_num) { int ret, error_ret = 0; + bool saved_pending_allowed = vty->pending_allowed; + *line_num = 0; + if (batch_config_file) + vty->pending_allowed = true; while (fgets(vty->buf, VTY_BUFSIZ, fp)) { ++(*line_num); @@ -1337,6 +1352,13 @@ int config_from_file(struct vty *vty, FILE *fp, unsigned int *line_num) error_ret = ret; } + vty->pending_allowed = saved_pending_allowed; + if (batch_config_file && !saved_pending_allowed) { + ret = nb_cli_pending_commit_check(vty); + if (ret != CMD_SUCCESS) + error_ret = ret; + } + if (error_ret) { return error_ret; } diff --git a/lib/command.h b/lib/command.h index ce9d894855f0..80064d8fb1fc 100644 --- a/lib/command.h +++ b/lib/command.h @@ -584,6 +584,7 @@ extern int command_config_read_one_line(struct vty *vty, const struct cmd_element **cmd, uint32_t line_num, int use_config_node); extern int config_from_file(struct vty *vty, FILE *fp, unsigned int *line_num); +extern void cmd_config_file_batching_set(bool enabled); extern enum node_type node_parent(enum node_type node); /* * Execute command under the given vty context. diff --git a/lib/libfrr.c b/lib/libfrr.c index be6bbd6536d5..961c61195ad3 100644 --- a/lib/libfrr.c +++ b/lib/libfrr.c @@ -861,6 +861,11 @@ const char *frr_get_progname(void) return di ? di->progname : NULL; } +unsigned short frr_get_instance(void) +{ + return di ? di->instance : 0; +} + enum frr_cli_mode frr_get_cli_mode(void) { return di ? di->cli_mode : FRR_CLI_CLASSIC; diff --git a/lib/libfrr.h b/lib/libfrr.h index f0829854a626..85f20a5c7f06 100644 --- a/lib/libfrr.h +++ b/lib/libfrr.h @@ -197,6 +197,7 @@ extern FRR_NORETURN void frr_help_exit(int status); extern struct event_loop *frr_init(void); extern const char *frr_get_progname(void); +extern unsigned short frr_get_instance(void); extern enum frr_cli_mode frr_get_cli_mode(void); extern uint32_t frr_get_fd_limit(void); extern bool frr_is_startup_fd(int fd); diff --git a/lib/mgmt_be_client.c b/lib/mgmt_be_client.c index 42b4ce974f18..80f139d960ad 100644 --- a/lib/mgmt_be_client.c +++ b/lib/mgmt_be_client.c @@ -12,6 +12,7 @@ #include "libfrr.h" #include "lib_errors.h" #include "mgmt_be_client.h" +#include "mgmt_defines.h" #include "mgmt_msg.h" #include "mgmt_msg_native.h" #include "network.h" @@ -1386,6 +1387,9 @@ struct mgmt_be_client *mgmt_be_client_create(const char *client_name, { struct mgmt_be_client *client; char server_path[MAXPATHLEN]; + char client_name_inst[MAXPATHLEN]; + int ret; + unsigned short instance = frr_get_instance(); if (__be_client) return NULL; @@ -1396,7 +1400,24 @@ struct mgmt_be_client *mgmt_be_client_create(const char *client_name, /* Only call after frr_init() */ assert(running_config); - client->name = XSTRDUP(MTYPE_MGMTD_BE_CLIENT_NAME, client_name); + if (instance) + ret = snprintf(client_name_inst, sizeof(client_name_inst), "%s-%u", client_name, + instance); + else + ret = snprintf(client_name_inst, sizeof(client_name_inst), "%s", client_name); + + if (ret < 0 || ret >= MGMTD_CLIENT_NAME_MAX_LEN) { + if (instance) + flog_err(EC_LIB_SYSTEM_CALL, "%s: backend client name '%s-%u' is too long", + __func__, client_name, instance); + else + flog_err(EC_LIB_SYSTEM_CALL, "%s: backend client name '%s' is too long", + __func__, client_name); + XFREE(MTYPE_MGMTD_BE_CLIENT, client); + __be_client = NULL; + return NULL; + } + client->name = XSTRDUP(MTYPE_MGMTD_BE_CLIENT_NAME, client_name_inst); client->running_config = running_config; // client->candidate_config = vty_shared_candidate_config; if (cbs) diff --git a/lib/northbound.c b/lib/northbound.c index ac004d27c607..9902b56daa8e 100644 --- a/lib/northbound.c +++ b/lib/northbound.c @@ -1814,19 +1814,16 @@ struct yang_data *nb_callback_get_elem(const struct nb_node *nb_node, { struct nb_cb_get_elem_args args = {}; - if (CHECK_FLAG(nb_node->flags, F_NB_NODE_IGNORE_CFG_CBS)) - return NULL; - DEBUGD(&nb_dbg_cbs_state, "northbound callback (get_elem): xpath [%s] list_entry [%p]", xpath, list_entry); - if (nb_node->cbs.get_elem) { - args.xpath = xpath; - args.list_entry = list_entry; - return nb_node->cbs.get_elem(&args); - } - return NULL; + if (!nb_node->cbs.get_elem) + return NULL; + + args.xpath = xpath; + args.list_entry = list_entry; + return nb_node->cbs.get_elem(&args); } const void *nb_callback_get_next(const struct nb_node *nb_node, @@ -1835,19 +1832,16 @@ const void *nb_callback_get_next(const struct nb_node *nb_node, { struct nb_cb_get_next_args args = {}; - if (CHECK_FLAG(nb_node->flags, F_NB_NODE_IGNORE_CFG_CBS)) - return NULL; - DEBUGD(&nb_dbg_cbs_state, "northbound callback (get_next): node [%s] parent_list_entry [%p] list_entry [%p]", nb_node->xpath, parent_list_entry, list_entry); - if (nb_node->cbs.get_next) { - args.parent_list_entry = parent_list_entry; - args.list_entry = list_entry; - return nb_node->cbs.get_next(&args); - } - return NULL; + if (!nb_node->cbs.get_next) + return NULL; + + args.parent_list_entry = parent_list_entry; + args.list_entry = list_entry; + return nb_node->cbs.get_next(&args); } int nb_callback_get_keys(const struct nb_node *nb_node, const void *list_entry, @@ -1855,13 +1849,13 @@ int nb_callback_get_keys(const struct nb_node *nb_node, const void *list_entry, { struct nb_cb_get_keys_args args = {}; - if (CHECK_FLAG(nb_node->flags, F_NB_NODE_IGNORE_CFG_CBS)) - return 0; - DEBUGD(&nb_dbg_cbs_state, "northbound callback (get_keys): node [%s] list_entry [%p]", nb_node->xpath, list_entry); + if (!nb_node->cbs.get_keys) + return 0; + args.list_entry = list_entry; args.keys = keys; return nb_node->cbs.get_keys(&args); @@ -1870,7 +1864,7 @@ int nb_callback_get_keys(const struct nb_node *nb_node, const void *list_entry, void nb_callback_list_entry_done(const struct nb_node *nb_node, const void *parent_list_entry, const void *list_entry) { - if (CHECK_FLAG(nb_node->flags, F_NB_NODE_IGNORE_CFG_CBS) || !nb_node->cbs.list_entry_done) + if (!nb_node->cbs.list_entry_done) return; DEBUGD(&nb_dbg_cbs_state, @@ -1886,13 +1880,13 @@ const void *nb_callback_lookup_entry(const struct nb_node *nb_node, { struct nb_cb_lookup_entry_args args = {}; - if (CHECK_FLAG(nb_node->flags, F_NB_NODE_IGNORE_CFG_CBS)) - return NULL; - DEBUGD(&nb_dbg_cbs_state, "northbound callback (lookup_entry): node [%s] parent_list_entry [%p]", nb_node->xpath, parent_list_entry); + if (!nb_node->cbs.lookup_entry) + return NULL; + args.parent_list_entry = parent_list_entry; args.keys = keys; return nb_node->cbs.lookup_entry(&args); @@ -1905,9 +1899,6 @@ const void *nb_callback_lookup_node_entry(struct lyd_node *node, struct nb_cb_lookup_entry_args args = {}; const struct nb_node *nb_node = node->schema->priv; - if (CHECK_FLAG(nb_node->flags, F_NB_NODE_IGNORE_CFG_CBS)) - return NULL; - if (yang_get_node_keys(node, &keys)) { flog_warn(EC_LIB_LIBYANG, "%s: can't get keys for lookup from existing data node %s", @@ -1919,6 +1910,9 @@ const void *nb_callback_lookup_node_entry(struct lyd_node *node, "northbound callback (lookup_node_entry): node [%s] parent_list_entry [%p]", nb_node->xpath, parent_list_entry); + if (!nb_node->cbs.lookup_entry) + return NULL; + args.parent_list_entry = parent_list_entry; args.keys = &keys; return nb_node->cbs.lookup_entry(&args); @@ -1930,13 +1924,13 @@ const void *nb_callback_lookup_next(const struct nb_node *nb_node, { struct nb_cb_lookup_entry_args args = {}; - if (CHECK_FLAG(nb_node->flags, F_NB_NODE_IGNORE_CFG_CBS)) - return NULL; - DEBUGD(&nb_dbg_cbs_state, "northbound callback (lookup_entry): node [%s] parent_list_entry [%p]", nb_node->xpath, parent_list_entry); + if (!nb_node->cbs.lookup_next) + return NULL; + args.parent_list_entry = parent_list_entry; args.keys = keys; return nb_node->cbs.lookup_next(&args); @@ -2814,6 +2808,16 @@ static void nb_load_callbacks(const struct frr_yang_module_info *module) priority = module->nodes[i].priority; if (priority != 0) nb_node->priority = priority; + + /* + * Per-node opt-in to config-callback dispatch lifts the + * module-level ignore_cfg_cbs suppression on this node so + * incremental conversions can expose a subset of writable + * leaves without supplying callbacks for the rest of the + * module's writable schema. + */ + if (module->nodes[i].cfg_opt_in) + UNSET_FLAG(nb_node->flags, F_NB_NODE_IGNORE_CFG_CBS); } } diff --git a/lib/northbound.h b/lib/northbound.h index f48cc34e3ecb..69687bae7ad4 100644 --- a/lib/northbound.h +++ b/lib/northbound.h @@ -710,6 +710,17 @@ struct frr_yang_module_info { * Ignore configuration callbacks for this module. Set this to true to * load module with only CLI-related callbacks. This is useful for * modules loaded in mgmtd. + * + * This suppresses configuration callbacks only. Operational callbacks + * remain live so a daemon can expose local state for a standard module + * even when that module's configuration surface is owned elsewhere or + * deliberately unwired. + * + * Individual nodes can opt back in by setting cfg_opt_in = true on + * their entry in the nodes[] array below. This lets a module that is + * being incrementally converted expose config callbacks on a subset + * of its writable schema without having to supply callbacks for the + * rest of the writable nodes at the same time. */ bool ignore_cfg_cbs; @@ -747,6 +758,14 @@ struct frr_yang_module_info { /* Priority - lower priorities are processed first. */ uint32_t priority; + + /* + * Opt this node into config-callback dispatch and validation + * even when the parent module has ignore_cfg_cbs = true. See + * the comment on ignore_cfg_cbs above. Has no effect when the + * module-level flag is unset (the node already participates). + */ + bool cfg_opt_in; #if defined(__GNUC__) && ((__GNUC__ - 0) < 5) && !defined(__clang__) } nodes[YANG_MODULE_MAX_NODES + 1]; #else diff --git a/lib/subdir.am b/lib/subdir.am index 20947c306d60..3a65d5dc3a9b 100644 --- a/lib/subdir.am +++ b/lib/subdir.am @@ -158,12 +158,18 @@ nodist_lib_libfrr_la_SOURCES = \ yang/frr-vrf.yang.c \ yang/frr-routing.yang.c \ yang/frr-nexthop.yang.c \ + yang/ietf/iana-bfd-types.yang.c \ + yang/ietf/iana-routing-types.yang.c \ yang/ietf/frr-deviations-ietf-key-chain.yang.c \ + yang/ietf/frr-deviations-ietf-routing-ospf.yang.c \ + yang/ietf/ietf-bfd-types.yang.c \ yang/ietf/ietf-routing-types.yang.c \ yang/ietf/ietf-syslog-types.yang.c \ yang/ietf/ietf-netconf-acm.yang.c \ yang/ietf/ietf-key-chain.yang.c \ yang/ietf/ietf-interfaces.yang.c \ + yang/ietf/ietf-ospf.yang.c \ + yang/ietf/ietf-routing.yang.c \ yang/ietf/ietf-bgp-types.yang.c \ yang/frr-module-translator.yang.c \ yang/ietf/ietf-srv6-types.yang.c \ diff --git a/mgmtd/mgmt_be_adapter.c b/mgmtd/mgmt_be_adapter.c index ce8bf4bbf841..1b31b8347a7a 100644 --- a/mgmtd/mgmt_be_adapter.c +++ b/mgmtd/mgmt_be_adapter.c @@ -149,13 +149,7 @@ static ssize_t printfrr_be_mask(struct fbuf *buf, struct printfrr_eargs *ea, con /* XPath Mapping Functions */ /* ======================= */ -/* - * Check if either map_path or xpath is a prefix of the other along path - * boundaries (i.e., either module name ending with ':' or path segment ending - * with '/' or 0 byte). Before checking - * the xpath is converted to a regular path string (i.e., removing predicates). - */ -static bool mgmt_be_xpath_prefix(const char *map_path, const char *xpath) +static bool mgmt_be_xpath_prefix_legacy(const char *map_path, const char *xpath) { int xc, pc = 0; @@ -168,16 +162,271 @@ static bool mgmt_be_xpath_prefix(const char *map_path, const char *xpath) } pc = *map_path++; if (!pc) - /* pc is done, if xpath ends at a path separator, it's a match */ return (xc == '/' || xc == ':'); if (pc != xc) return false; } pc = *map_path++; - /* if they are equal, or map_path ends at a path separator, it's a match */ return (!pc || pc == '/' || pc == ':'); } +static bool mgmt_be_xpath_boundary(char c) +{ + return c == '\0' || c == '/' || c == ':'; +} + +static bool mgmt_be_xpath_strip_predicates(const char *xpath, char *stripped, size_t stripped_len) +{ + const char *src; + char *dst = stripped; + + for (src = xpath; *src && dst < stripped + stripped_len - 1; src++) { + if (*src == '[') { + src = frrstr_skip_over_char(src + 1, ']'); + if (!src) + return false; + src--; + continue; + } + *dst++ = *src; + } + *dst = '\0'; + + if (*src) { + _log_warn("xpath too long after predicate stripping: %s", xpath); + return false; + } + + return true; +} + +static bool mgmt_be_xpath_stripped_prefix(const char *prefix, const char *path) +{ + size_t prefix_len = strlen(prefix); + + return strncmp(prefix, path, prefix_len) == 0 && mgmt_be_xpath_boundary(path[prefix_len]); +} + +static const char *mgmt_be_xpath_segment_end(const char *segment) +{ + const char *p; + + for (p = segment; *p && *p != '/'; p++) { + if (*p == '[') { + p = frrstr_skip_over_char(p + 1, ']'); + if (!p) + return NULL; + p--; + } + } + + return p; +} + +static size_t mgmt_be_xpath_segment_name_len(const char *segment) +{ + const char *p; + + for (p = segment; *p && *p != '/' && *p != '['; p++) + ; + + return p - segment; +} + +static bool mgmt_be_xpath_predicate_parse(const char *predicate, const char *segment_end, + const char **key, size_t *key_len, const char **value, + size_t *value_len, const char **next) +{ + const char *predicate_end; + const char *close; + const char *equals; + const char *value_start; + const char *value_end; + + predicate_end = frrstr_skip_over_char(predicate + 1, ']'); + if (!predicate_end || predicate_end > segment_end) + return false; + + close = predicate_end - 1; + equals = memchr(predicate + 1, '=', close - predicate - 1); + if (!equals) + return false; + + *key = predicate + 1; + *key_len = equals - *key; + + value_start = equals + 1; + value_end = close; + if (value_start < value_end && (*value_start == '\'' || *value_start == '"')) { + char quote = *value_start; + + value_start++; + value_end = memchr(value_start, quote, close - value_start); + if (!value_end) + return false; + } + + *value = value_start; + *value_len = value_end - value_start; + *next = predicate_end; + + return true; +} + +static bool mgmt_be_xpath_find_predicate(const char *segment, const char *segment_end, + const char *key, size_t key_len, const char **value, + size_t *value_len, bool *found) +{ + const char *p = segment + mgmt_be_xpath_segment_name_len(segment); + + *found = false; + while (p < segment_end) { + const char *predicate_key; + const char *predicate_value; + const char *next; + size_t predicate_key_len; + size_t predicate_value_len; + + while (p < segment_end && *p != '[') + p++; + if (p >= segment_end) + return true; + + if (!mgmt_be_xpath_predicate_parse(p, segment_end, &predicate_key, + &predicate_key_len, &predicate_value, + &predicate_value_len, &next)) + return false; + + if (predicate_key_len == key_len && strncmp(predicate_key, key, key_len) == 0) { + *value = predicate_value; + *value_len = predicate_value_len; + *found = true; + return true; + } + + p = next; + } + + return true; +} + +static bool mgmt_be_xpath_segment_predicates_compatible(const char *map_segment, + const char *map_end, + const char *xpath_segment, + const char *xpath_end) +{ + const char *p = map_segment + mgmt_be_xpath_segment_name_len(map_segment); + + while (p < map_end) { + const char *map_key; + const char *map_value; + const char *xpath_value; + const char *next; + size_t map_key_len; + size_t map_value_len; + size_t xpath_value_len; + bool found; + + while (p < map_end && *p != '[') + p++; + if (p >= map_end) + return true; + + if (!mgmt_be_xpath_predicate_parse(p, map_end, &map_key, &map_key_len, &map_value, + &map_value_len, &next)) + return false; + + if (!mgmt_be_xpath_find_predicate(xpath_segment, xpath_end, map_key, map_key_len, + &xpath_value, &xpath_value_len, &found)) + return false; + + if (found && (map_value_len != xpath_value_len || + strncmp(map_value, xpath_value, map_value_len) != 0)) + return false; + + p = next; + } + + return true; +} + +static bool mgmt_be_xpath_predicates_compatible(const char *map_path, const char *xpath) +{ + const char *map = map_path; + const char *path = xpath; + + while (*map == '/') + map++; + while (*path == '/') + path++; + + while (*map && *path) { + const char *map_end; + const char *path_end; + size_t map_name_len; + size_t path_name_len; + + map_end = mgmt_be_xpath_segment_end(map); + path_end = mgmt_be_xpath_segment_end(path); + if (!map_end || !path_end) + return false; + + map_name_len = mgmt_be_xpath_segment_name_len(map); + path_name_len = mgmt_be_xpath_segment_name_len(path); + /* The stripped paths are already known to share a prefix. A + * different segment name means we have reached the first segment + * beyond that shared prefix, so there are no more predicates in + * the registered map path that can constrain this match. + */ + if (map_name_len != path_name_len || strncmp(map, path, map_name_len) != 0) + return true; + + if (!mgmt_be_xpath_segment_predicates_compatible(map, map_end, path, path_end)) + return false; + + map = *map_end == '/' ? map_end + 1 : map_end; + path = *path_end == '/' ? path_end + 1 : path_end; + } + + return true; +} + +static bool mgmt_be_xpath_prefix_predicate_compatible(const char *map_path, const char *xpath) +{ + char map_stripped[XPATH_MAXLEN]; + char xpath_stripped[XPATH_MAXLEN]; + + if (!mgmt_be_xpath_strip_predicates(map_path, map_stripped, sizeof(map_stripped)) || + !mgmt_be_xpath_strip_predicates(xpath, xpath_stripped, sizeof(xpath_stripped))) + return false; + + if (!mgmt_be_xpath_stripped_prefix(map_stripped, xpath_stripped) && + !mgmt_be_xpath_stripped_prefix(xpath_stripped, map_stripped)) + return false; + + return mgmt_be_xpath_predicates_compatible(map_path, xpath); +} + +/* + * Check if either map_path or xpath is a prefix of the other along path + * boundaries. Existing unpredicated registrations keep the historical matcher. + * + * Predicated registrations are used when multiple backends own entries under a + * shared YANG list, such as OSPFv2 and OSPFv3 under RFC 9129's + * ietf-routing control-plane-protocol list. In that case predicates constrain + * backend ownership only when the query also specifies the same predicate key. + * A conflicting value rejects the backend, but a missing query predicate is a + * wildcard: unkeyed list and parent queries still dispatch to every matching + * backend so the frontend can merge their entries. + */ +static bool mgmt_be_xpath_prefix(const char *map_path, const char *xpath) +{ + if (!strchr(map_path, '[')) + return mgmt_be_xpath_prefix_legacy(map_path, xpath); + + return mgmt_be_xpath_prefix_predicate_compatible(map_path, xpath); +} + /* * Get the mask of clients interested in an xpath. */ diff --git a/mgmtd/mgmt_main.c b/mgmtd/mgmt_main.c index 6dd5287417fc..f3d406486984 100644 --- a/mgmtd/mgmt_main.c +++ b/mgmtd/mgmt_main.c @@ -8,6 +8,7 @@ #include #include "affinitymap.h" +#include "bfd.h" #include "filter.h" #include "frr_pthread.h" #include "keychain.h" @@ -148,6 +149,41 @@ const struct frr_yang_module_info zebra_route_map_info = { .nodes = { { .xpath = NULL } }, }; +static const struct frr_yang_module_info ietf_routing_info = { + .name = "ietf-routing", + .ignore_cfg_cbs = true, + .nodes = { { .xpath = NULL } }, +}; + +static const char *const ietf_ospf_features[] = { + "auto-cost", + "bfd", + "explicit-router-id", + "graceful-restart", + "key-chain", + "ldp-igp-sync", + "max-ecmp", + "mtu-ignore", + "ospfv3-authentication-trailer", + "prefix-suppression", + "stub-router", + "te-rid", + NULL, +}; + +static const struct frr_yang_module_info ietf_ospf_info = { + .name = "ietf-ospf", + .features = (const char **)ietf_ospf_features, + .ignore_cfg_cbs = true, + .nodes = { { .xpath = NULL } }, +}; + +static const struct frr_yang_module_info ietf_routing_ospf_deviation_info = { + .name = "frr-deviations-ietf-routing-ospf", + .ignore_cfg_cbs = true, + .nodes = { { .xpath = NULL } }, +}; + #ifdef HAVE_MGMTD_TESTC static const struct frr_yang_module_info frr_test_config_info = { .name = "frr-test-config", @@ -180,6 +216,10 @@ static const struct frr_yang_module_info *const mgmt_yang_modules[] = { &frr_zebra_cli_info, &zebra_route_map_info, + &ietf_bfd_types_info, + &ietf_routing_info, + &ietf_ospf_info, + &ietf_routing_ospf_deviation_info, &ietf_key_chain_cli_info, &ietf_key_chain_deviation_info, &ietf_srv6_types_info, diff --git a/ospf6d/ospf6_area.c b/ospf6d/ospf6_area.c index 1cae157b1109..2478976ab2b9 100644 --- a/ospf6d/ospf6_area.c +++ b/ospf6d/ospf6_area.c @@ -11,6 +11,7 @@ #include "frrevent.h" #include "vty.h" #include "command.h" +#include "northbound_cli.h" #include "if.h" #include "prefix.h" #include "table.h" @@ -26,6 +27,7 @@ #include "ospf6_area.h" #include "ospf6_message.h" #include "ospf6_neighbor.h" +#include "ospf6_nb.h" #include "ospf6_interface.h" #include "ospf6_intra.h" #include "ospf6_abr.h" @@ -192,7 +194,7 @@ static void ospf6_area_stub_update(struct ospf6_area *area) ospf6_schedule_abr_task(area->ospf6); } -static int ospf6_area_stub_set(struct ospf6 *ospf6, struct ospf6_area *area) +int ospf6_area_stub_set(struct ospf6 *ospf6, struct ospf6_area *area) { if (!IS_AREA_STUB(area)) { /* Disable NSSA first. */ @@ -213,8 +215,7 @@ void ospf6_area_stub_unset(struct ospf6 *ospf6, struct ospf6_area *area) } } -static void ospf6_area_no_summary_set(struct ospf6 *ospf6, - struct ospf6_area *area) +void ospf6_area_no_summary_set(struct ospf6 *ospf6, struct ospf6_area *area) { if (area) { if (!area->no_summary) { @@ -225,8 +226,7 @@ static void ospf6_area_no_summary_set(struct ospf6 *ospf6, } } -static void ospf6_area_no_summary_unset(struct ospf6 *ospf6, - struct ospf6_area *area) +void ospf6_area_no_summary_unset(struct ospf6 *ospf6, struct ospf6_area *area) { if (area) { if (area->no_summary) { @@ -558,9 +558,12 @@ void ospf6_area_show(struct vty *vty, struct ospf6_area *oa, } } -DEFUN (area_range, +static int ospf6_area_xpath(char *xpath, size_t size, const struct ospf6 *o, uint32_t area_id, + const char *leaf); + +DEFPY_YANG (area_range, area_range_cmd, - "area range X:X::X:X/M []", + "area $areaid range X:X::X:X/M$prefix []", "OSPF6 area parameters\n" "OSPF6 area ID in IP address format\n" "OSPF6 area ID as a decimal value\n" @@ -571,63 +574,41 @@ DEFUN (area_range, "User specified metric for this range\n" "Advertised metric for this range\n") { - int idx_ipv4 = 1; - int idx_ipv6_prefixlen = 3; - int idx_type = 4; - int ret; - struct ospf6_area *oa; - struct prefix prefix; - struct ospf6_route *range; - uint32_t cost; - VTY_DECLVAR_CONTEXT(ospf6, ospf6); + uint32_t area_id_int; + int format; + char xpath[XPATH_MAXLEN]; + char tail[XPATH_MAXLEN]; - OSPF6_CMD_AREA_GET(argv[idx_ipv4]->arg, oa, ospf6); - - ret = str2prefix(argv[idx_ipv6_prefixlen]->arg, &prefix); - if (ret != 1 || prefix.family != AF_INET6) { - vty_out(vty, "Malformed argument: %s\n", - argv[idx_ipv6_prefixlen]->arg); - return CMD_SUCCESS; - } - - range = ospf6_route_lookup(&prefix, oa->range_table); - if (range == NULL) { - range = ospf6_route_create(ospf6); - range->type = OSPF6_DEST_TYPE_RANGE; - range->prefix = prefix; - range->path.area_id = oa->area_id; - range->path.cost = OSPF_AREA_RANGE_COST_UNSPEC; - } - - /* default settings */ - cost = OSPF_AREA_RANGE_COST_UNSPEC; - UNSET_FLAG(range->flag, OSPF6_ROUTE_DO_NOT_ADVERTISE); - - if (argc > idx_type) { - if (strmatch(argv[idx_type]->text, "not-advertise")) - SET_FLAG(range->flag, OSPF6_ROUTE_DO_NOT_ADVERTISE); - else if (strmatch(argv[idx_type]->text, "cost")) - cost = strtoul(argv[5]->arg, NULL, 10); + if (str2area_id(areaid, &area_id_int, &format)) { + vty_out(vty, "Malformed Area-ID: %s\n", areaid); + return CMD_WARNING_CONFIG_FAILED; } - range->path.u.cost_config = cost; + snprintf(tail, sizeof(tail), "/ranges/range[prefix='%s']", prefix_str); + if (ospf6_area_xpath(xpath, sizeof(xpath), ospf6, area_id_int, tail) != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_CREATE, NULL); - if (range->rnode == NULL) { - ospf6_route_add(range, oa->range_table); - } + snprintf(tail, sizeof(tail), "/ranges/range[prefix='%s']/advertise", prefix_str); + if (ospf6_area_xpath(xpath, sizeof(xpath), ospf6, area_id_int, tail) != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, notadv ? "false" : "true"); - if (ospf6_check_and_set_router_abr(ospf6)) { - /* Redo summaries if required */ - ospf6_schedule_abr_task(ospf6); - } + snprintf(tail, sizeof(tail), "/ranges/range[prefix='%s']/cost", prefix_str); + if (ospf6_area_xpath(xpath, sizeof(xpath), ospf6, area_id_int, tail) != 0) + return CMD_WARNING_CONFIG_FAILED; + if (cost_str) + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, cost_str); + else + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); - return CMD_SUCCESS; + return nb_cli_apply_changes(vty, NULL); } -DEFUN (no_area_range, +DEFPY_YANG (no_area_range, no_area_range_cmd, - "no area range X:X::X:X/M []", + "no area $areaid range X:X::X:X/M$prefix []", NO_STR "OSPF6 area parameters\n" "OSPF6 area ID in IP address format\n" @@ -639,41 +620,21 @@ DEFUN (no_area_range, "User specified metric for this range\n" "Advertised metric for this range\n") { - int idx_ipv4 = 2; - int idx_ipv6 = 4; - int ret; - struct ospf6_area *oa; - struct prefix prefix; - struct ospf6_route *range; - VTY_DECLVAR_CONTEXT(ospf6, ospf6); + uint32_t area_id_int; + int format; + char xpath[XPATH_MAXLEN]; + char tail[XPATH_MAXLEN]; - OSPF6_CMD_AREA_GET(argv[idx_ipv4]->arg, oa, ospf6); - - ret = str2prefix(argv[idx_ipv6]->arg, &prefix); - if (ret != 1 || prefix.family != AF_INET6) { - vty_out(vty, "Malformed argument: %s\n", argv[idx_ipv6]->arg); - return CMD_SUCCESS; - } - - range = ospf6_route_lookup(&prefix, oa->range_table); - if (range == NULL) { - vty_out(vty, "Range %s does not exists.\n", - argv[idx_ipv6]->arg); - return CMD_SUCCESS; - } - - if (ospf6_check_and_set_router_abr(oa->ospf6)) { - /* Blow away the aggregated LSA and route */ - SET_FLAG(range->flag, OSPF6_ROUTE_REMOVE); - ospf6_schedule_abr_task(oa->ospf6); + if (str2area_id(areaid, &area_id_int, &format)) { + vty_out(vty, "Malformed Area-ID: %s\n", areaid); + return CMD_WARNING_CONFIG_FAILED; } - ospf6_route_remove(range, oa->range_table); - - /* Delete area if no interfaces or configuration. */ - ospf6_area_no_config_delete(oa); - - return CMD_SUCCESS; + snprintf(tail, sizeof(tail), "/ranges/range[prefix='%s']", prefix_str); + if (ospf6_area_xpath(xpath, sizeof(xpath), ospf6, area_id_int, tail) != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); } void ospf6_area_config_write(struct vty *vty, struct ospf6 *ospf6) @@ -1279,84 +1240,132 @@ DEFUN(show_ipv6_ospf6_simulate_spf_tree_root, return CMD_SUCCESS; } -DEFUN (ospf6_area_stub, +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area[area-id=...]/ + * + * Mirror of ospfd's ospf_area_xpath but for the ietf-ospf:ospfv3 + * control-plane-protocol type. area_id is a network-byte-order uint32_t. + * Pass NULL for `leaf` to get the list-entry xpath. + * + * Returns 0 on success, -1 if snprintf failed or the result was truncated. + */ +static int ospf6_area_xpath(char *xpath, size_t size, const struct ospf6 *o, uint32_t area_id, + const char *leaf) +{ + char area_id_str[INET_ADDRSTRLEN]; + struct in_addr addr = { .s_addr = area_id }; + int ret; + + inet_ntop(AF_INET, &addr, area_id_str, sizeof(area_id_str)); + ret = snprintf(xpath, size, + OSPF6D_IETF_ROUTING_PROTOCOL_XPATH + "/ietf-ospf:ospf/areas/area[area-id='%s']%s", + o->name ? o->name : VRF_DEFAULT_NAME, area_id_str, leaf ? leaf : ""); + if (ret < 0 || (size_t)ret >= size) + return -1; + + return 0; +} + +/* + * area X stub [no-summary] -> set area-type=stub-area, summary=true|false + * no area X stub [no-summary] -> destroy area entry or just clear summary + * + * v3 counterparts of the v2 conversions in ospfd/ospf_vty.c. ospf6d's + * area helpers operate on a struct ospf6_area pointer, but the NB layer + * does the lookup itself from the YANG keys, so the DEFPY_YANG wrappers + * only need the area_id (uint32_t, network byte order from str2area_id). + */ +DEFPY_YANG (ospf6_area_stub, ospf6_area_stub_cmd, - "area stub", + "area $area_str stub", "OSPF6 area parameters\n" "OSPF6 area ID in IP address format\n" "OSPF6 area ID as a decimal value\n" "Configure OSPF6 area as stub\n") { - int idx_ipv4_number = 1; - struct ospf6_area *area; - VTY_DECLVAR_CONTEXT(ospf6, ospf6); + uint32_t area_id; + int format; + char xpath[XPATH_MAXLEN]; - OSPF6_CMD_AREA_GET(argv[idx_ipv4_number]->arg, area, ospf6); - - if (!ospf6_area_stub_set(ospf6, area)) { - vty_out(vty, - "First deconfigure all virtual link through this area\n"); - return CMD_WARNING_CONFIG_FAILED; + if (str2area_id(area_str, &area_id, &format)) { + vty_out(vty, "Malformed Area-ID: %s\n", area_str); + return CMD_WARNING; } - ospf6_area_no_summary_unset(ospf6, area); + ospf6_area_xpath(xpath, sizeof(xpath), ospf6, area_id, "/area-type"); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "stub-area"); + ospf6_area_xpath(xpath, sizeof(xpath), ospf6, area_id, "/summary"); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "true"); - return CMD_SUCCESS; + return nb_cli_apply_changes(vty, NULL); } -DEFUN (ospf6_area_stub_no_summary, +DEFPY_YANG (ospf6_area_stub_no_summary, ospf6_area_stub_no_summary_cmd, - "area stub no-summary", + "area $area_str stub no-summary", "OSPF6 stub parameters\n" "OSPF6 area ID in IP address format\n" "OSPF6 area ID as a decimal value\n" "Configure OSPF6 area as stub\n" "Do not inject inter-area routes into stub\n") { - int idx_ipv4_number = 1; - struct ospf6_area *area; - VTY_DECLVAR_CONTEXT(ospf6, ospf6); + uint32_t area_id; + int format; + char xpath[XPATH_MAXLEN]; - OSPF6_CMD_AREA_GET(argv[idx_ipv4_number]->arg, area, ospf6); - - if (!ospf6_area_stub_set(ospf6, area)) { - vty_out(vty, - "First deconfigure all virtual link through this area\n"); - return CMD_WARNING_CONFIG_FAILED; + if (str2area_id(area_str, &area_id, &format)) { + vty_out(vty, "Malformed Area-ID: %s\n", area_str); + return CMD_WARNING; } - ospf6_area_no_summary_set(ospf6, area); + ospf6_area_xpath(xpath, sizeof(xpath), ospf6, area_id, "/area-type"); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "stub-area"); + ospf6_area_xpath(xpath, sizeof(xpath), ospf6, area_id, "/summary"); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "false"); - return CMD_SUCCESS; + return nb_cli_apply_changes(vty, NULL); } -DEFUN (no_ospf6_area_stub, +DEFPY_YANG (no_ospf6_area_stub, no_ospf6_area_stub_cmd, - "no area stub", + "no area $area_str stub", NO_STR "OSPF6 area parameters\n" "OSPF6 area ID in IP address format\n" "OSPF6 area ID as a decimal value\n" "Configure OSPF6 area as stub\n") { - int idx_ipv4_number = 2; - struct ospf6_area *area; - VTY_DECLVAR_CONTEXT(ospf6, ospf6); + uint32_t area_id; + int format; + char xpath[XPATH_MAXLEN]; - OSPF6_CMD_AREA_GET(argv[idx_ipv4_number]->arg, area, ospf6); - - ospf6_area_stub_unset(ospf6, area); - ospf6_area_no_summary_unset(ospf6, area); + if (str2area_id(area_str, &area_id, &format)) { + vty_out(vty, "Malformed Area-ID: %s\n", area_str); + return CMD_WARNING; + } - return CMD_SUCCESS; + /* + * `no area X stub` reverts area-type to normal-area AND clears the + * stub-only `summary` leaf so the YANG datastore doesn't carry a + * stale `summary = false` while the schema's `when` clause restricts + * the leaf to stub / NSSA areas. Ranges and interface attachments + * remain untouched -- matches legacy ospf6_area_stub_unset semantics + * exactly. (ospf6 has no per-area default-cost leaf to clear.) + */ + ospf6_area_xpath(xpath, sizeof(xpath), ospf6, area_id, "/area-type"); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "normal-area"); + ospf6_area_xpath(xpath, sizeof(xpath), ospf6, area_id, "/summary"); + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); } -DEFUN (no_ospf6_area_stub_no_summary, +DEFPY_YANG (no_ospf6_area_stub_no_summary, no_ospf6_area_stub_no_summary_cmd, - "no area stub no-summary", + "no area $area_str stub no-summary", NO_STR "OSPF6 area parameters\n" "OSPF6 area ID in IP address format\n" @@ -1364,20 +1373,19 @@ DEFUN (no_ospf6_area_stub_no_summary, "Configure OSPF6 area as stub\n" "Do not inject inter-area routes into area\n") { - int idx_ipv4_number = 2; - struct ospf6_area *area; - VTY_DECLVAR_CONTEXT(ospf6, ospf6); + uint32_t area_id; + int format; + char xpath[XPATH_MAXLEN]; - OSPF6_CMD_AREA_GET(argv[idx_ipv4_number]->arg, area, ospf6); - - ospf6_area_stub_unset(ospf6, area); - ospf6_area_no_summary_unset(ospf6, area); - - /* Delete area if no interfaces or configuration. */ - ospf6_area_no_config_delete(area); + if (str2area_id(area_str, &area_id, &format)) { + vty_out(vty, "Malformed Area-ID: %s\n", area_str); + return CMD_WARNING; + } - return CMD_SUCCESS; + ospf6_area_xpath(xpath, sizeof(xpath), ospf6, area_id, "/summary"); + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); } DEFPY(ospf6_area_nssa, ospf6_area_nssa_cmd, diff --git a/ospf6d/ospf6_area.h b/ospf6d/ospf6_area.h index 16049427acd8..2f63905ee6d6 100644 --- a/ospf6d/ospf6_area.h +++ b/ospf6d/ospf6_area.h @@ -149,7 +149,10 @@ extern struct ospf6_area *ospf6_area_lookup(uint32_t area_id, struct ospf6 *ospf6); extern struct ospf6_area *ospf6_area_lookup_by_area_id(uint32_t area_id); +extern int ospf6_area_stub_set(struct ospf6 *ospf6, struct ospf6_area *area); extern void ospf6_area_stub_unset(struct ospf6 *ospf6, struct ospf6_area *area); +extern void ospf6_area_no_summary_set(struct ospf6 *ospf6, struct ospf6_area *area); +extern void ospf6_area_no_summary_unset(struct ospf6 *ospf6, struct ospf6_area *area); extern void ospf6_area_enable(struct ospf6_area *oa); extern void ospf6_area_disable(struct ospf6_area *oa); diff --git a/ospf6d/ospf6_bfd.c b/ospf6d/ospf6_bfd.c index 8c3464df456a..3afa51267763 100644 --- a/ospf6d/ospf6_bfd.c +++ b/ospf6d/ospf6_bfd.c @@ -23,6 +23,7 @@ #include "ospf6_message.h" #include "ospf6_neighbor.h" #include "ospf6_interface.h" +#include "northbound_cli.h" #include "ospf6_route.h" #include "ospf6_zebra.h" #include "ospf6_bfd.h" @@ -71,8 +72,7 @@ void ospf6_bfd_trigger_event(struct ospf6_neighbor *on, int old_state, * zebra for starting/stopping the monitoring of * the neighbor rechahability. */ -static void ospf6_bfd_reg_dereg_all_nbr(struct ospf6_interface *oi, - bool install) +void ospf6_bfd_reg_dereg_all_nbr(struct ospf6_interface *oi, bool install) { struct ospf6_neighbor *on; struct listnode *node; @@ -169,7 +169,14 @@ void ospf6_bfd_write_config(struct vty *vty, struct ospf6_interface *oi) oi->bfd_config.profile); } -DEFUN(ipv6_ospf6_bfd, ipv6_ospf6_bfd_cmd, +/* + * `ipv6 ospf6 bfd` maps onto RFC 9129 `/bfd/enabled`. The + * `[profile BFDPROF]` tail has no YANG counterpart (RFC 9129's + * client-cfg-parms grouping doesn't expose BFD profiles), so the + * shim splits: the bare form routes through YANG when the interface + * is in an area; the profile form stays on the legacy direct path. + */ +DEFUN_YANG(ipv6_ospf6_bfd, ipv6_ospf6_bfd_cmd, "ipv6 ospf6 bfd [profile BFDPROF]", IP6_STR OSPF6_STR "Enables BFD support\n" @@ -179,8 +186,15 @@ DEFUN(ipv6_ospf6_bfd, ipv6_ospf6_bfd_cmd, VTY_DECLVAR_CONTEXT(interface, ifp); struct ospf6_interface *oi; int prof_idx = 4; + bool has_profile = (argc > prof_idx); + char xpath[XPATH_MAXLEN]; assert(ifp); + if (!has_profile && ospf6_per_iface_xpath(xpath, sizeof(xpath), ifp, "/bfd/enabled") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "true"); + return nb_cli_apply_changes(vty, NULL); + } + oi = (struct ospf6_interface *)ifp->info; if (oi == NULL) oi = ospf6_interface_create(ifp); @@ -190,7 +204,7 @@ DEFUN(ipv6_ospf6_bfd, ipv6_ospf6_bfd_cmd, oi->bfd_config.min_rx = BFD_DEF_MIN_RX; oi->bfd_config.min_tx = BFD_DEF_MIN_TX; oi->bfd_config.enabled = true; - if (argc > prof_idx) { + if (has_profile) { XFREE(MTYPE_TMP, oi->bfd_config.profile); oi->bfd_config.profile = XSTRDUP(MTYPE_TMP, argv[prof_idx]->arg); @@ -228,6 +242,11 @@ DEFUN(no_ipv6_ospf6_bfd_profile, no_ipv6_ospf6_bfd_profile_cmd, return CMD_SUCCESS; } +/* + * `ipv6 ospf6 bfd N N N` -- same DEFUN-with-#if-macro shape as the + * ospfd companion. Body routes through YANG when the interface is + * in an area. + */ #if HAVE_BFDD > 0 DEFUN_HIDDEN( #else @@ -248,18 +267,38 @@ DEFUN( int idx_number_2 = 4; int idx_number_3 = 5; struct ospf6_interface *oi; + uint32_t mult = strtoul(argv[idx_number]->arg, NULL, 10); + uint32_t rx_ms = strtoul(argv[idx_number_2]->arg, NULL, 10); + uint32_t tx_ms = strtoul(argv[idx_number_3]->arg, NULL, 10); + char xpath_base[XPATH_MAXLEN]; + char xpath[XPATH_MAXLEN + 64]; + char val[32]; assert(ifp); + if (ospf6_per_iface_xpath(xpath_base, sizeof(xpath_base), ifp, "/bfd") == 0) { + snprintf(xpath, sizeof(xpath), "%s/enabled", xpath_base); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "true"); + snprintf(xpath, sizeof(xpath), "%s/local-multiplier", xpath_base); + snprintf(val, sizeof(val), "%u", mult); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, val); + snprintf(xpath, sizeof(xpath), "%s/required-min-rx-interval", xpath_base); + snprintf(val, sizeof(val), "%u", rx_ms * 1000); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, val); + snprintf(xpath, sizeof(xpath), "%s/desired-min-tx-interval", xpath_base); + snprintf(val, sizeof(val), "%u", tx_ms * 1000); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, val); + return nb_cli_apply_changes(vty, NULL); + } + oi = (struct ospf6_interface *)ifp->info; if (oi == NULL) oi = ospf6_interface_create(ifp); assert(oi); - oi->bfd_config.detection_multiplier = - strtoul(argv[idx_number]->arg, NULL, 10); - oi->bfd_config.min_rx = strtoul(argv[idx_number_2]->arg, NULL, 10); - oi->bfd_config.min_tx = strtoul(argv[idx_number_3]->arg, NULL, 10); + oi->bfd_config.detection_multiplier = mult; + oi->bfd_config.min_rx = rx_ms; + oi->bfd_config.min_tx = tx_ms; oi->bfd_config.enabled = true; ospf6_bfd_reg_dereg_all_nbr(oi, true); @@ -267,7 +306,7 @@ DEFUN( return CMD_SUCCESS; } -DEFUN (no_ipv6_ospf6_bfd, +DEFUN_YANG (no_ipv6_ospf6_bfd, no_ipv6_ospf6_bfd_cmd, "no ipv6 ospf6 bfd", NO_STR @@ -278,8 +317,14 @@ DEFUN (no_ipv6_ospf6_bfd, { VTY_DECLVAR_CONTEXT(interface, ifp); struct ospf6_interface *oi; + char xpath[XPATH_MAXLEN]; assert(ifp); + if (ospf6_per_iface_xpath(xpath, sizeof(xpath), ifp, "/bfd/enabled") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } + oi = (struct ospf6_interface *)ifp->info; if (oi == NULL) oi = ospf6_interface_create(ifp); diff --git a/ospf6d/ospf6_bfd.h b/ospf6d/ospf6_bfd.h index c073626f88f3..58616c0ba16c 100644 --- a/ospf6d/ospf6_bfd.h +++ b/ospf6d/ospf6_bfd.h @@ -30,4 +30,12 @@ extern void ospf6_bfd_info_nbr_create(struct ospf6_interface *oi, extern int config_write_ospf6_debug_bfd(struct vty *vty); extern void install_element_ospf6_debug_bfd(void); +/* + * Register or deregister BFD sessions on every neighbour of the + * interface. Shared by the legacy `ipv6 ospf6 bfd` CLI and the RFC + * 9129 `/bfd/enabled` northbound callback so the two paths use + * identical session lifecycle. + */ +extern void ospf6_bfd_reg_dereg_all_nbr(struct ospf6_interface *oi, bool install); + #endif /* OSPF6_BFD_H */ diff --git a/ospf6d/ospf6_flood.c b/ospf6d/ospf6_flood.c index 6056d0b4092c..ff149acd58af 100644 --- a/ospf6d/ospf6_flood.c +++ b/ospf6d/ospf6_flood.c @@ -116,6 +116,7 @@ void ospf6_lsa_originate(struct ospf6 *ospf6, struct ospf6_lsa *lsa) } ospf6_install_lsa(lsa); + ospf6->lsa_originate_count++; ospf6_flood(NULL, lsa); } @@ -890,11 +891,13 @@ void ospf6_receive_lsa(struct ospf6_neighbor *from, struct ospf6_lsa_header *lsa_header) { struct ospf6_lsa *new = NULL, *old = NULL, *rem = NULL; + struct ospf6 *ospf6; int ismore_recent; int is_debug = 0; ismore_recent = 1; assert(from); + ospf6 = from->ospf6_if->area->ospf6; /* if we receive a LSA with invalid seqnum drop it */ if (ntohl(lsa_header->seqnum) - 1 == OSPF_MAX_SEQUENCE_NUMBER) { @@ -1001,8 +1004,7 @@ void ospf6_receive_lsa(struct ospf6_neighbor *from, /* in case we have no database copy */ ismore_recent = -1; - self_originated = (new->header->adv_router == - from->ospf6_if->area->ospf6->router_id); + self_originated = (new->header->adv_router == ospf6->router_id); /* After any restart of DUT node, receiving self-originated * LSAs with MaxAge from peers within 1 second (MinLSArrival, @@ -1036,11 +1038,11 @@ void ospf6_receive_lsa(struct ospf6_neighbor *from, /* Received non-self-originated Grace LSA. */ if (IS_GRACE_LSA(new) && !self_originated) { - struct ospf6 *ospf6; + struct ospf6 *gr_ospf6; - ospf6 = ospf6_get_by_lsdb(new); + gr_ospf6 = ospf6_get_by_lsdb(new); - assert(ospf6); + assert(gr_ospf6); if (OSPF6_LSA_IS_MAXAGE(new)) { @@ -1050,8 +1052,7 @@ void ospf6_receive_lsa(struct ospf6_neighbor *from, __func__, &new->header->adv_router); if (old) { - ospf6_process_maxage_grace_lsa( - ospf6, new, from); + ospf6_process_maxage_grace_lsa(gr_ospf6, new, from); } else { if (IS_DEBUG_OSPF6_GR) zlog_debug( @@ -1068,8 +1069,8 @@ void ospf6_receive_lsa(struct ospf6_neighbor *from, __func__, &new->header->adv_router); - if (ospf6_process_grace_lsa(ospf6, new, from) - == OSPF6_GR_NOT_HELPER) { + if (ospf6_process_grace_lsa(gr_ospf6, new, from) == + OSPF6_GR_NOT_HELPER) { if (IS_DEBUG_OSPF6_GR) zlog_debug( "%s, Not moving to HELPER role, So dicarding GraceLSA", @@ -1091,17 +1092,18 @@ void ospf6_receive_lsa(struct ospf6_neighbor *from, /* (d), installing lsdb, which may cause routing table calculation (replacing database copy) */ ospf6_install_lsa(new); + if (!self_originated) + ospf6->rx_lsa_count++; if (OSPF6_LSA_IS_MAXAGE(new)) - ospf6_maxage_remove(from->ospf6_if->area->ospf6); + ospf6_maxage_remove(ospf6); /* (e) possibly acknowledge */ ospf6_acknowledge_lsa(new, ismore_recent, from); /* (f) Self Originated LSA, section 13.4 */ if (self_originated) { - if (from->ospf6_if->area->ospf6->gr_info - .restart_in_progress) { + if (ospf6->gr_info.restart_in_progress) { if (IS_DEBUG_OSPF6_GR) zlog_debug( "Graceful Restart in progress -- not flushing self-originated LSA: %s", @@ -1124,7 +1126,6 @@ void ospf6_receive_lsa(struct ospf6_neighbor *from, } /* GR: check for network topology change. */ - struct ospf6 *ospf6 = from->ospf6_if->area->ospf6; struct ospf6_area *area = from->ospf6_if->area; if (ospf6->gr_info.restart_in_progress && (new->header->type == ntohs(OSPF6_LSTYPE_ROUTER) || @@ -1216,8 +1217,7 @@ void ospf6_receive_lsa(struct ospf6_neighbor *from, * MAXAGEd and not removed.*/ if (OSPF6_LSA_IS_MAXAGE(old) && !OSPF6_LSA_IS_MAXAGE(new)) { - if (new->header->adv_router - != from->ospf6_if->area->ospf6->router_id) { + if (new->header->adv_router != ospf6->router_id) { if (is_debug) zlog_debug( "%s: Current copy of LSA %s is MAXAGE, but new has recent age, flooding/installing.", @@ -1225,6 +1225,7 @@ void ospf6_receive_lsa(struct ospf6_neighbor *from, ospf6_lsa_purge(old); ospf6_flood(from, new); ospf6_install_lsa(new); + ospf6->rx_lsa_count++; return; } /* For self-originated LSA, only trust diff --git a/ospf6d/ospf6_gr.c b/ospf6d/ospf6_gr.c index de8feea4c1a8..94fbbd225de7 100644 --- a/ospf6d/ospf6_gr.c +++ b/ospf6d/ospf6_gr.c @@ -33,13 +33,15 @@ #include "ospf6d/ospf6_spf.h" #include "ospf6d/ospf6_tlv.h" #include "ospf6d/ospf6_gr.h" +#include "ospf6d/ospf6_top.h" +#include "ospf6_nb.h" +#include "northbound_cli.h" #include "ospf6d/ospf6_gr_clippy.c" static void ospf6_gr_grace_period_expired(struct event *event); /* Originate and install Grace-LSA for a given interface. */ -static int ospf6_gr_lsa_originate(struct ospf6_interface *oi, - enum ospf6_gr_restart_reason reason) +static int ospf6_gr_lsa_originate(struct ospf6_interface *oi, enum ospf6_gr_restart_reason reason) { struct ospf6 *ospf6 = oi->area->ospf6; struct ospf6_gr_info *gr_info = &ospf6->gr_info; @@ -50,8 +52,7 @@ static int ospf6_gr_lsa_originate(struct ospf6_interface *oi, char buffer[OSPF6_MAX_LSASIZE]; if (IS_OSPF6_DEBUG_ORIGINATE(LINK)) - zlog_debug("Originate Grace-LSA for Interface %s", - oi->interface->name); + zlog_debug("Originate Grace-LSA for Interface %s", oi->interface->name); /* prepare buffer */ memset(buffer, 0, sizeof(buffer)); @@ -65,8 +66,7 @@ static int ospf6_gr_lsa_originate(struct ospf6_interface *oi, /* Put restart reason. */ grace_lsa->tlv_reason.header.type = htons(TLV_GRACE_RESTART_REASON_TYPE); - grace_lsa->tlv_reason.header.length = - htons(TLV_GRACE_RESTART_REASON_LENGTH); + grace_lsa->tlv_reason.header.length = htons(TLV_GRACE_RESTART_REASON_LENGTH); grace_lsa->tlv_reason.reason = reason; /* Fill LSA Header */ @@ -75,9 +75,8 @@ static int ospf6_gr_lsa_originate(struct ospf6_interface *oi, lsa_header->type = htons(OSPF6_LSTYPE_GRACE_LSA); lsa_header->id = htonl(oi->interface->ifindex); lsa_header->adv_router = ospf6->router_id; - lsa_header->seqnum = - ospf6_new_ls_seqnum(lsa_header->type, lsa_header->id, - lsa_header->adv_router, oi->lsdb); + lsa_header->seqnum = ospf6_new_ls_seqnum(lsa_header->type, lsa_header->id, + lsa_header->adv_router, oi->lsdb); lsa_header->length = htons(lsa_length); /* LSA checksum */ @@ -110,11 +109,10 @@ static int ospf6_gr_lsa_originate(struct ospf6_interface *oi, /* Send packet. */ iovector[0].iov_base = lsa_header; iovector[0].iov_len = length; - n = ospf6_sendmsg(oi->linklocal_addr, &allspfrouters6, - oi->interface->ifindex, iovector, ospf6->fd); + n = ospf6_sendmsg(oi->linklocal_addr, &allspfrouters6, oi->interface->ifindex, + iovector, ospf6->fd); if (n != length) - flog_err(EC_LIB_DEVELOPMENT, - "%s: could not send entire message", __func__); + flog_err(EC_LIB_DEVELOPMENT, "%s: could not send entire message", __func__); } else { /* Create and install LSA. */ lsa = ospf6_lsa_create(lsa_header); @@ -136,20 +134,16 @@ static void ospf6_gr_flush_grace_lsas(struct ospf6 *ospf6) struct listnode *inode; if (IS_DEBUG_OSPF6_GR) - zlog_debug( - "GR: flushing self-originated Grace-LSAs [area %pI4]", - &area->area_id); + zlog_debug("GR: flushing self-originated Grace-LSAs [area %pI4]", + &area->area_id); for (ALL_LIST_ELEMENTS_RO(area->if_list, inode, oi)) { lsa = ospf6_lsdb_lookup(htons(OSPF6_LSTYPE_GRACE_LSA), htonl(oi->interface->ifindex), - oi->area->ospf6->router_id, - oi->lsdb); + oi->area->ospf6->router_id, oi->lsdb); if (!lsa) { - zlog_warn( - "%s: Grace-LSA not found [interface %s] [area %pI4]", - __func__, oi->interface->name, - &area->area_id); + zlog_warn("%s: Grace-LSA not found [interface %s] [area %pI4]", + __func__, oi->interface->name, &area->area_id); continue; } @@ -219,8 +213,7 @@ static void ospf6_gr_restart_exit(struct ospf6 *ospf6, const char *reason) */ for (route = ospf6_route_head(ospf6->external_table); route; route = ospf6_route_next(route)) - ospf6_handle_external_lsa_origination(ospf6, route, - &route->prefix); + ospf6_handle_external_lsa_origination(ospf6, route, &route->prefix); /* * 3) The router reruns its OSPF routing calculations, this time @@ -236,11 +229,13 @@ static void ospf6_gr_restart_exit(struct ospf6 *ospf6, const char *reason) /* 6) Any grace-LSAs that the router originated should be flushed. */ ospf6_gr_flush_grace_lsas(ospf6); + + /* RFC 9129 ietf-ospf:restart-status-change: now not-restarting (1). */ + ospf6d_ietf_notif_restart_status_change(ospf6, 1, OSPF6_GR_HELPER_COMPLETED); } /* Enter the Graceful Restart mode. */ -void ospf6_gr_restart_enter(struct ospf6 *ospf6, - enum ospf6_gr_restart_reason reason, +void ospf6_gr_restart_enter(struct ospf6 *ospf6, enum ospf6_gr_restart_reason reason, time_t timestamp) { unsigned long remaining_time; @@ -251,21 +246,23 @@ void ospf6_gr_restart_enter(struct ospf6 *ospf6, /* Schedule grace period timeout. */ remaining_time = timestamp - time(NULL); if (IS_DEBUG_OSPF6_GR) - zlog_debug( - "GR: remaining time until grace period expires: %lu(s)", - remaining_time); + zlog_debug("GR: remaining time until grace period expires: %lu(s)", remaining_time); + + event_add_timer(master, ospf6_gr_grace_period_expired, ospf6, remaining_time, + &ospf6->gr_info.t_grace_period); - event_add_timer(master, ospf6_gr_grace_period_expired, ospf6, - remaining_time, &ospf6->gr_info.t_grace_period); + /* RFC 9129 ietf-ospf:restart-status-change. FRR-known reasons are + * all software-initiated, so map to RFC `planned-restart` (2). + */ + ospf6d_ietf_notif_restart_status_change(ospf6, 2, OSPF6_GR_HELPER_INPROGRESS); } -#define RTR_LSA_MISSING 0 -#define RTR_LSA_ADJ_FOUND 1 +#define RTR_LSA_MISSING 0 +#define RTR_LSA_ADJ_FOUND 1 #define RTR_LSA_ADJ_NOT_FOUND 2 /* Check if a Router-LSA exists and if it contains a given link. */ -static int ospf6_router_lsa_contains_adj(struct ospf6_area *area, - in_addr_t adv_router, +static int ospf6_router_lsa_contains_adj(struct ospf6_area *area, in_addr_t adv_router, in_addr_t neighbor_router_id) { uint16_t type; @@ -278,15 +275,13 @@ static int ospf6_router_lsa_contains_adj(struct ospf6_area *area, char *start, *end, *current; empty = false; - router_lsa = (struct ospf6_router_lsa - *)((char *)lsa->header - + sizeof(struct ospf6_lsa_header)); + router_lsa = (struct ospf6_router_lsa *)((char *)lsa->header + + sizeof(struct ospf6_lsa_header)); /* Iterate over all interfaces in the Router-LSA. */ start = (char *)router_lsa + sizeof(struct ospf6_router_lsa); end = (char *)lsa->header + ntohs(lsa->header->length); - for (current = start; - current + sizeof(struct ospf6_router_lsdesc) <= end; + for (current = start; current + sizeof(struct ospf6_router_lsdesc) <= end; current += sizeof(struct ospf6_router_lsdesc)) { struct ospf6_router_lsdesc *lsdesc; @@ -307,23 +302,20 @@ static int ospf6_router_lsa_contains_adj(struct ospf6_area *area, return RTR_LSA_ADJ_NOT_FOUND; } -static bool ospf6_gr_check_router_lsa_consistency(struct ospf6 *ospf6, - struct ospf6_area *area, +static bool ospf6_gr_check_router_lsa_consistency(struct ospf6 *ospf6, struct ospf6_area *area, struct ospf6_lsa *lsa) { if (lsa->header->adv_router == ospf6->router_id) { struct ospf6_router_lsa *router_lsa; char *start, *end, *current; - router_lsa = (struct ospf6_router_lsa - *)((char *)lsa->header - + sizeof(struct ospf6_lsa_header)); + router_lsa = (struct ospf6_router_lsa *)((char *)lsa->header + + sizeof(struct ospf6_lsa_header)); /* Iterate over all interfaces in the Router-LSA. */ start = (char *)router_lsa + sizeof(struct ospf6_router_lsa); end = (char *)lsa->header + ntohs(lsa->header->length); - for (current = start; - current + sizeof(struct ospf6_router_lsdesc) <= end; + for (current = start; current + sizeof(struct ospf6_router_lsdesc) <= end; current += sizeof(struct ospf6_router_lsdesc)) { struct ospf6_router_lsdesc *lsdesc; @@ -331,10 +323,9 @@ static bool ospf6_gr_check_router_lsa_consistency(struct ospf6 *ospf6, if (lsdesc->type != OSPF6_ROUTER_LSDESC_POINTTOPOINT) continue; - if (ospf6_router_lsa_contains_adj( - area, lsdesc->neighbor_router_id, - ospf6->router_id) - == RTR_LSA_ADJ_NOT_FOUND) + if (ospf6_router_lsa_contains_adj(area, lsdesc->neighbor_router_id, + ospf6->router_id) == + RTR_LSA_ADJ_NOT_FOUND) return false; } } else { @@ -342,11 +333,10 @@ static bool ospf6_gr_check_router_lsa_consistency(struct ospf6 *ospf6, adj1 = ospf6_router_lsa_contains_adj(area, ospf6->router_id, lsa->header->adv_router); - adj2 = ospf6_router_lsa_contains_adj( - area, lsa->header->adv_router, ospf6->router_id); - if ((adj1 == RTR_LSA_ADJ_FOUND && adj2 == RTR_LSA_ADJ_NOT_FOUND) - || (adj1 == RTR_LSA_ADJ_NOT_FOUND - && adj2 == RTR_LSA_ADJ_FOUND)) + adj2 = ospf6_router_lsa_contains_adj(area, lsa->header->adv_router, + ospf6->router_id); + if ((adj1 == RTR_LSA_ADJ_FOUND && adj2 == RTR_LSA_ADJ_NOT_FOUND) || + (adj1 == RTR_LSA_ADJ_NOT_FOUND && adj2 == RTR_LSA_ADJ_FOUND)) return false; } @@ -357,8 +347,7 @@ static bool ospf6_gr_check_router_lsa_consistency(struct ospf6 *ospf6, * Check for LSAs that are inconsistent with the pre-restart LSAs, and abort the * ongoing graceful restart when that's the case. */ -void ospf6_gr_check_lsdb_consistency(struct ospf6 *ospf6, - struct ospf6_area *area) +void ospf6_gr_check_lsdb_consistency(struct ospf6 *ospf6, struct ospf6_area *area) { uint16_t type; struct ospf6_lsa *lsa; @@ -369,8 +358,8 @@ void ospf6_gr_check_lsdb_consistency(struct ospf6 *ospf6, char reason[256]; snprintfrr(reason, sizeof(reason), - "detected inconsistent LSA %s [area %pI4]", - lsa->name, &area->area_id); + "detected inconsistent LSA %s [area %pI4]", lsa->name, + &area->area_id); ospf6_lsa_unlock(&lsa); ospf6_gr_restart_exit(ospf6, reason); return; @@ -379,24 +368,21 @@ void ospf6_gr_check_lsdb_consistency(struct ospf6 *ospf6, } /* Check if there's a fully formed adjacency with the given neighbor ID. */ -static bool ospf6_gr_check_adj_id(struct ospf6_area *area, - in_addr_t neighbor_router_id) +static bool ospf6_gr_check_adj_id(struct ospf6_area *area, in_addr_t neighbor_router_id) { struct ospf6_neighbor *nbr; nbr = ospf6_area_neighbor_lookup(area, neighbor_router_id); if (!nbr || nbr->state < OSPF6_NEIGHBOR_FULL) { if (IS_DEBUG_OSPF6_GR) - zlog_debug("GR: missing adjacency to router %pI4", - &neighbor_router_id); + zlog_debug("GR: missing adjacency to router %pI4", &neighbor_router_id); return false; } return true; } -static bool ospf6_gr_check_adjs_lsa_transit(struct ospf6_area *area, - in_addr_t neighbor_router_id, +static bool ospf6_gr_check_adjs_lsa_transit(struct ospf6_area *area, in_addr_t neighbor_router_id, uint32_t neighbor_interface_id) { struct ospf6 *ospf6 = area->ospf6; @@ -409,20 +395,17 @@ static bool ospf6_gr_check_adjs_lsa_transit(struct ospf6_area *area, struct ospf6_network_lsdesc *lsdesc; /* Lookup Network LSA corresponding to this interface. */ - lsa = ospf6_lsdb_lookup(htons(OSPF6_LSTYPE_NETWORK), - neighbor_interface_id, + lsa = ospf6_lsdb_lookup(htons(OSPF6_LSTYPE_NETWORK), neighbor_interface_id, neighbor_router_id, area->lsdb); if (!lsa) return false; /* Iterate over all routers present in the network. */ - network_lsa = (struct ospf6_network_lsa - *)((char *)lsa->header - + sizeof(struct ospf6_lsa_header)); + network_lsa = (struct ospf6_network_lsa *)((char *)lsa->header + + sizeof(struct ospf6_lsa_header)); start = (char *)network_lsa + sizeof(struct ospf6_network_lsa); end = (char *)lsa->header + ntohs(lsa->header->length); - for (current = start; - current + sizeof(struct ospf6_network_lsdesc) <= end; + for (current = start; current + sizeof(struct ospf6_network_lsdesc) <= end; current += sizeof(struct ospf6_network_lsdesc)) { lsdesc = (struct ospf6_network_lsdesc *)current; @@ -444,9 +427,8 @@ static bool ospf6_gr_check_adjs_lsa_transit(struct ospf6_area *area, nbr = ospf6_area_neighbor_lookup(area, neighbor_router_id); if (!nbr || nbr->state < OSPF6_NEIGHBOR_FULL) { if (IS_DEBUG_OSPF6_GR) - zlog_debug( - "GR: missing adjacency to DR router %pI4", - &neighbor_router_id); + zlog_debug("GR: missing adjacency to DR router %pI4", + &neighbor_router_id); return false; } } @@ -454,35 +436,30 @@ static bool ospf6_gr_check_adjs_lsa_transit(struct ospf6_area *area, return true; } -static bool ospf6_gr_check_adjs_lsa(struct ospf6_area *area, - struct ospf6_lsa *lsa) +static bool ospf6_gr_check_adjs_lsa(struct ospf6_area *area, struct ospf6_lsa *lsa) { struct ospf6_router_lsa *router_lsa; char *start, *end, *current; router_lsa = - (struct ospf6_router_lsa *)((char *)lsa->header - + sizeof(struct ospf6_lsa_header)); + (struct ospf6_router_lsa *)((char *)lsa->header + sizeof(struct ospf6_lsa_header)); /* Iterate over all interfaces in the Router-LSA. */ start = (char *)router_lsa + sizeof(struct ospf6_router_lsa); end = (char *)lsa->header + ntohs(lsa->header->length); - for (current = start; - current + sizeof(struct ospf6_router_lsdesc) <= end; + for (current = start; current + sizeof(struct ospf6_router_lsdesc) <= end; current += sizeof(struct ospf6_router_lsdesc)) { struct ospf6_router_lsdesc *lsdesc; lsdesc = (struct ospf6_router_lsdesc *)current; switch (lsdesc->type) { case OSPF6_ROUTER_LSDESC_POINTTOPOINT: - if (!ospf6_gr_check_adj_id(area, - lsdesc->neighbor_router_id)) + if (!ospf6_gr_check_adj_id(area, lsdesc->neighbor_router_id)) return false; break; case OSPF6_ROUTER_LSDESC_TRANSIT_NETWORK: - if (!ospf6_gr_check_adjs_lsa_transit( - area, lsdesc->neighbor_router_id, - lsdesc->neighbor_interface_id)) + if (!ospf6_gr_check_adjs_lsa_transit(area, lsdesc->neighbor_router_id, + lsdesc->neighbor_interface_id)) return false; break; default: @@ -512,8 +489,7 @@ static bool ospf6_gr_check_adjs(struct ospf6 *ospf6) type = ntohs(OSPF6_LSTYPE_ROUTER); router = ospf6->router_id; - for (ALL_LSDB_TYPED_ADVRTR(area->lsdb, type, router, - lsa_self)) { + for (ALL_LSDB_TYPED_ADVRTR(area->lsdb, type, router, lsa_self)) { found = true; if (!ospf6_gr_check_adjs_lsa(area, lsa_self)) { ospf6_lsa_unlock(&lsa_self); @@ -546,15 +522,14 @@ void ospf6_gr_iface_send_grace_lsa(struct event *event) event_add_timer(master, ospf6_gr_iface_send_grace_lsa, oi, 1, &oi->gr.hello_delay.t_grace_send); else - event_add_event(master, ospf6_hello_send, oi, 0, - &oi->thread_send_hello); + event_add_event(master, ospf6_hello_send, oi, 0, &oi->thread_send_hello); } /* * Record in non-volatile memory that the given OSPF instance is attempting to * perform a graceful restart. */ -static void ospf6_gr_nvm_update(struct ospf6 *ospf6, bool prepare) +void ospf6_gr_nvm_update(struct ospf6 *ospf6, bool prepare) { const char *inst_name; json_object *json; @@ -574,12 +549,10 @@ static void ospf6_gr_nvm_update(struct ospf6 *ospf6, bool prepare) json_object_object_get_ex(json_instances, inst_name, &json_instance); if (!json_instance) { json_instance = json_object_new_object(); - json_object_object_add(json_instances, inst_name, - json_instance); + json_object_object_add(json_instances, inst_name, json_instance); } - json_object_int_add(json_instance, "gracePeriod", - ospf6->gr_info.grace_period); + json_object_int_add(json_instance, "gracePeriod", ospf6->gr_info.grace_period); /* * Record not only the grace period, but also a UNIX timestamp @@ -646,12 +619,10 @@ void ospf6_gr_nvm_read(struct ospf6 *ospf6) json_object_object_get_ex(json_instances, inst_name, &json_instance); if (!json_instance) { json_instance = json_object_new_object(); - json_object_object_add(json_instances, inst_name, - json_instance); + json_object_object_add(json_instances, inst_name, json_instance); } - json_object_object_get_ex(json_instance, "gracePeriod", - &json_grace_period); + json_object_object_get_ex(json_instance, "gracePeriod", &json_grace_period); json_object_object_get_ex(json_instance, "timestamp", &json_timestamp); if (json_timestamp) { time_t now; @@ -659,12 +630,10 @@ void ospf6_gr_nvm_read(struct ospf6 *ospf6) /* Planned GR: check if the grace period has already expired. */ now = time(NULL); timestamp = json_object_get_int(json_timestamp); - if (now > timestamp) { - ospf6_gr_restart_exit( - ospf6, "grace period has expired already"); - } else - ospf6_gr_restart_enter(ospf6, OSPF6_GR_SW_RESTART, - timestamp); + if (now > timestamp) + ospf6_gr_restart_exit(ospf6, "grace period has expired already"); + else + ospf6_gr_restart_enter(ospf6, OSPF6_GR_SW_RESTART, timestamp); } else if (json_grace_period) { uint32_t grace_period; @@ -675,8 +644,7 @@ void ospf6_gr_nvm_read(struct ospf6 *ospf6) grace_period = json_object_get_int(json_grace_period); ospf6->gr_info.grace_period = grace_period; ospf6_gr_restart_enter(ospf6, OSPF6_GR_UNKNOWN_RESTART, - time(NULL) + - ospf6->gr_info.grace_period); + time(NULL) + ospf6->gr_info.grace_period); } json_object_object_del(json_instance, "gracePeriod"); @@ -713,15 +681,13 @@ static void ospf6_gr_prepare(void) for (ALL_LIST_ELEMENTS_RO(om6->ospf6, onode, ospf6)) { struct ospf6_area *area; - if (!ospf6->gr_info.restart_support - || ospf6->gr_info.prepare_in_progress) + if (!ospf6->gr_info.restart_support || ospf6->gr_info.prepare_in_progress) continue; if (IS_DEBUG_OSPF6_GR) - zlog_debug( - "GR: preparing to perform a graceful restart [period %u second(s)] [vrf %s]", - ospf6->gr_info.grace_period, - ospf6_vrf_id_to_name(ospf6->vrf_id)); + zlog_debug("GR: preparing to perform a graceful restart [period %u second(s)] [vrf %s]", + ospf6->gr_info.grace_period, + ospf6_vrf_id_to_name(ospf6->vrf_id)); /* Send a Grace-LSA to all neighbors. */ for (ALL_LIST_ELEMENTS_RO(ospf6->area_list, anode, area)) { @@ -744,20 +710,16 @@ static void ospf6_gr_prepare(void) } } -static int ospf6_gr_neighbor_change(struct ospf6_neighbor *on, int next_state, - int prev_state) +static int ospf6_gr_neighbor_change(struct ospf6_neighbor *on, int next_state, int prev_state) { struct ospf6 *ospf6 = on->ospf6_if->area->ospf6; - if (next_state == OSPF6_NEIGHBOR_FULL - && ospf6->gr_info.restart_in_progress) { + if (next_state == OSPF6_NEIGHBOR_FULL && ospf6->gr_info.restart_in_progress) { if (ospf6_gr_check_adjs(ospf6)) { - ospf6_gr_restart_exit( - ospf6, "all adjacencies were reestablished"); + ospf6_gr_restart_exit(ospf6, "all adjacencies were reestablished"); } else { if (IS_DEBUG_OSPF6_GR) - zlog_debug( - "GR: not all adjacencies were reestablished yet"); + zlog_debug("GR: not all adjacencies were reestablished yet"); } } @@ -772,8 +734,7 @@ int config_write_ospf6_gr(struct vty *vty, struct ospf6 *ospf6) if (ospf6->gr_info.grace_period == OSPF6_DFLT_GRACE_INTERVAL) vty_out(vty, " graceful-restart\n"); else - vty_out(vty, " graceful-restart grace-period %u\n", - ospf6->gr_info.grace_period); + vty_out(vty, " graceful-restart grace-period %u\n", ospf6->gr_info.grace_period); return 0; } @@ -789,53 +750,89 @@ DEFPY(ospf6_graceful_restart_prepare, ospf6_graceful_restart_prepare_cmd, return CMD_SUCCESS; } -DEFPY(ospf6_graceful_restart, ospf6_graceful_restart_cmd, +/* + * Companion to `ospf_gr_restart_support_enable` in ospfd -- enable GR + * on the given ospf6 instance using its current grace_period. + * Idempotent. Shared by the legacy CLI and the RFC 9129 northbound + * callback. + */ +void ospf6_gr_restart_support_enable(struct ospf6 *ospf6) +{ + if (ospf6->gr_info.grace_period == 0) + ospf6->gr_info.grace_period = OSPF6_DFLT_GRACE_INTERVAL; + ospf6->gr_info.restart_support = true; + (void)ospf6_zebra_gr_enable(ospf6, ospf6->gr_info.grace_period); + ospf6_gr_nvm_update(ospf6, false); +} + +/* + * Disable graceful-restart restarter state. Returns -1 if a GR + * preparation is in flight; the caller surfaces the rejection. The + * grace_period is left untouched -- see the ospfd companion comment. + */ +int ospf6_gr_restart_support_disable(struct ospf6 *ospf6) +{ + if (!ospf6->gr_info.restart_support) + return 0; + if (ospf6->gr_info.prepare_in_progress) + return -1; + ospf6->gr_info.restart_support = false; + ospf6_gr_nvm_delete(ospf6); + ospf6_zebra_gr_disable(ospf6); + return 0; +} + +/* + * Set the graceful-restart grace period. Refresh the zebra GR + * stale-route timer if GR is currently enabled. + */ +void ospf6_gr_set_grace_period(struct ospf6 *ospf6, uint32_t grace_period) +{ + if (ospf6->gr_info.grace_period == grace_period) + return; + ospf6->gr_info.grace_period = grace_period; + if (ospf6->gr_info.restart_support) + (void)ospf6_zebra_gr_enable(ospf6, grace_period); +} + +DEFPY_YANG(ospf6_graceful_restart, ospf6_graceful_restart_cmd, "graceful-restart [grace-period (1-1800)$grace_period]", OSPF_GR_STR "Maximum length of the 'grace period'\n" "Maximum length of the 'grace period' in seconds\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf6, ospf6); - - /* Check and get restart period if present. */ - if (!grace_period_str) - grace_period = OSPF6_DFLT_GRACE_INTERVAL; - - ospf6->gr_info.restart_support = true; - ospf6->gr_info.grace_period = grace_period; - - /* Freeze OSPF routes in the RIB. */ - (void)ospf6_zebra_gr_enable(ospf6, ospf6->gr_info.grace_period); - - /* Record that GR is enabled in non-volatile memory. */ - ospf6_gr_nvm_update(ospf6, false); - - return CMD_SUCCESS; + char xpath[XPATH_MAXLEN]; + + if (ospf6_per_instance_xpath(xpath, sizeof(xpath), ospf6, "/graceful-restart/enabled") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "true"); + if (grace_period_str) { + if (ospf6_per_instance_xpath(xpath, sizeof(xpath), ospf6, + "/graceful-restart/restart-interval") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, grace_period_str); + } + return nb_cli_apply_changes(vty, NULL); } -DEFPY(ospf6_no_graceful_restart, ospf6_no_graceful_restart_cmd, +DEFPY_YANG(ospf6_no_graceful_restart, ospf6_no_graceful_restart_cmd, "no graceful-restart [period (1-1800)]", NO_STR OSPF_GR_STR "Maximum length of the 'grace period'\n" "Maximum length of the 'grace period' in seconds\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf6, ospf6); - - if (!ospf6->gr_info.restart_support) - return CMD_SUCCESS; - - if (ospf6->gr_info.prepare_in_progress) { - vty_out(vty, - "%% Error: Graceful Restart preparation in progress\n"); - return CMD_WARNING; - } - - ospf6->gr_info.restart_support = false; - ospf6->gr_info.grace_period = OSPF6_DFLT_GRACE_INTERVAL; - ospf6_gr_nvm_delete(ospf6); - ospf6_zebra_gr_disable(ospf6); - - return CMD_SUCCESS; + char xpath[XPATH_MAXLEN]; + + if (ospf6_per_instance_xpath(xpath, sizeof(xpath), ospf6, "/graceful-restart/enabled") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + if (ospf6_per_instance_xpath(xpath, sizeof(xpath), ospf6, + "/graceful-restart/restart-interval") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); } void ospf6_gr_init(void) diff --git a/ospf6d/ospf6_gr.h b/ospf6d/ospf6_gr.h index 3fbc236f6061..6ac95464d42d 100644 --- a/ospf6d/ospf6_gr.h +++ b/ospf6d/ospf6_gr.h @@ -22,6 +22,7 @@ /* Forward declaration(s). */ struct ospf6_neighbor; +struct ospf6_lsa; /* Debug option */ extern unsigned char conf_debug_ospf6_gr; @@ -121,8 +122,47 @@ extern void ospf6_gr_restart_enter(struct ospf6 *ospf6, extern void ospf6_gr_check_lsdb_consistency(struct ospf6 *ospf, struct ospf6_area *area); extern void ospf6_gr_nvm_read(struct ospf6 *ospf); +extern void ospf6_gr_nvm_update(struct ospf6 *ospf6, bool prepare); extern void ospf6_gr_nvm_delete(struct ospf6 *ospf6); extern void ospf6_gr_unplanned_start_interface(struct ospf6_interface *oi); extern void ospf6_gr_init(void); +/* + * Apply graceful-restart restarter state. Shared by the legacy + * `graceful-restart` CLI and the RFC 9129 `/graceful-restart/enabled` + * northbound callback so the two paths produce identical side effects. + */ +extern void ospf6_gr_restart_support_enable(struct ospf6 *ospf6); + +/* + * Disable graceful-restart restarter state. Returns -1 when a GR + * preparation is in flight (the legacy CLI rejects the same way); the + * caller surfaces the rejection. The grace_period is intentionally + * left untouched -- the restart-interval leaf has its own restore + * path. + */ +extern int ospf6_gr_restart_support_disable(struct ospf6 *ospf6); + +/* + * Set the graceful-restart grace period. Refreshes the zebra GR + * stale-route timer if GR is currently enabled. + */ +extern void ospf6_gr_set_grace_period(struct ospf6 *ospf6, uint32_t grace_period); + +/* + * Toggle global helper-mode support. Promoted from static to extern so + * the RFC 9129 `/graceful-restart/helper-enabled` callback and the + * legacy CLI shim can share the same code path. On transition to + * disabled the helper exits the role on every neighbour not pinned by + * a per-router-id entry, matching the legacy CLI semantics. + */ +extern void ospf6_gr_helper_support_set(struct ospf6 *ospf6, bool support); + +/* + * Toggle strict LSA-check on the helper. `enabled = true` enforces + * strict checking (matches the FRR default); `false` relaxes it. + * Promoted from static to extern for the same reason. + */ +extern void ospf6_gr_helper_lsacheck_set(struct ospf6 *ospf6, bool enabled); + #endif /* OSPF6_GR_H */ diff --git a/ospf6d/ospf6_gr_helper.c b/ospf6d/ospf6_gr_helper.c index b06ddba63e4d..b7909af85ae5 100644 --- a/ospf6d/ospf6_gr_helper.c +++ b/ospf6d/ospf6_gr_helper.c @@ -34,30 +34,28 @@ #include "ospf6d.h" #include "ospf6_tlv.h" #include "ospf6_gr.h" +#include "ospf6_nb.h" #include "lib/json.h" +#include "northbound_cli.h" #include "ospf6d/ospf6_gr_helper_clippy.c" DEFINE_MTYPE_STATIC(OSPF6D, OSPF6_GR_HELPER, "OSPF6 Graceful restart helper"); unsigned char conf_debug_ospf6_gr; -static int ospf6_grace_lsa_show_info(struct vty *vty, struct ospf6_lsa *lsa, - json_object *json, bool use_json); +static int ospf6_grace_lsa_show_info(struct vty *vty, struct ospf6_lsa *lsa, json_object *json, + bool use_json); -struct ospf6_lsa_handler grace_lsa_handler = {.lh_type = OSPF6_LSTYPE_GRACE_LSA, - .lh_name = "Grace", - .lh_short_name = "GR", - .lh_show = - ospf6_grace_lsa_show_info, - .lh_get_prefix_str = NULL, - .lh_debug = 0}; +struct ospf6_lsa_handler grace_lsa_handler = { .lh_type = OSPF6_LSTYPE_GRACE_LSA, + .lh_name = "Grace", + .lh_short_name = "GR", + .lh_show = ospf6_grace_lsa_show_info, + .lh_get_prefix_str = NULL, + .lh_debug = 0 }; const char *ospf6_exit_reason_desc[] = { - "Unknown reason", - "Helper in progress", - "Topology Change", - "Grace timer expiry", - "Successful graceful restart", + "Unknown reason", "Helper in progress", "Topology Change", + "Grace timer expiry", "Successful graceful restart", }; const char *ospf6_restart_reason_desc[] = { @@ -111,8 +109,7 @@ static void ospf6_enable_rtr_hash_destroy(struct ospf6 *ospf6) if (ospf6->ospf6_helper_cfg.enable_rtr_list == NULL) return; - hash_clean_and_free(&ospf6->ospf6_helper_cfg.enable_rtr_list, - ospf6_disable_rtr_hash_free); + hash_clean_and_free(&ospf6->ospf6_helper_cfg.enable_rtr_list, ospf6_disable_rtr_hash_free); } /* @@ -125,8 +122,8 @@ static void ospf6_enable_rtr_hash_destroy(struct ospf6 *ospf6) * interval : grace interval. * reason : Restarting reason. */ -static int ospf6_extract_grace_lsa_fields(struct ospf6_lsa *lsa, - uint32_t *interval, uint8_t *reason) +static int ospf6_extract_grace_lsa_fields(struct ospf6_lsa *lsa, uint32_t *interval, + uint8_t *reason) { struct ospf6_lsa_header *lsah = NULL; struct tlv_header *tlvh = NULL; @@ -138,21 +135,18 @@ static int ospf6_extract_grace_lsa_fields(struct ospf6_lsa *lsa, lsah = lsa->header; if (ntohs(lsah->length) <= OSPF6_LSA_HEADER_SIZE) { if (IS_DEBUG_OSPF6_GR) - zlog_debug("%s: undersized (%u B) lsa", __func__, - ntohs(lsah->length)); + zlog_debug("%s: undersized (%u B) lsa", __func__, ntohs(lsah->length)); return OSPF6_FAILURE; } length = ntohs(lsah->length) - OSPF6_LSA_HEADER_SIZE; - for (tlvh = lsdesc_start(lsah); sum < length && tlvh; - tlvh = TLV_HDR_NEXT(tlvh)) { + for (tlvh = lsdesc_start(lsah); sum < length && tlvh; tlvh = TLV_HDR_NEXT(tlvh)) { /* Check TLV len against overall LSA */ if (sum + TLV_SIZE(tlvh) > length) { if (IS_DEBUG_OSPF6_GR) - zlog_debug( - "%s: Malformed packet: Invalid TLV len:%d", - __func__, TLV_SIZE(tlvh)); + zlog_debug("%s: Malformed packet: Invalid TLV len:%d", __func__, + TLV_SIZE(tlvh)); return OSPF6_FAILURE; } @@ -163,8 +157,8 @@ static int ospf6_extract_grace_lsa_fields(struct ospf6_lsa *lsa, sum += TLV_SIZE(tlvh); /* Check if grace interval is valid */ - if (*interval > OSPF6_MAX_GRACE_INTERVAL - || *interval < OSPF6_MIN_GRACE_INTERVAL) + if (*interval > OSPF6_MAX_GRACE_INTERVAL || + *interval < OSPF6_MIN_GRACE_INTERVAL) return OSPF6_FAILURE; break; case TLV_GRACE_RESTART_REASON_TYPE: @@ -178,8 +172,8 @@ static int ospf6_extract_grace_lsa_fields(struct ospf6_lsa *lsa, default: sum += TLV_SIZE(tlvh); if (IS_DEBUG_OSPF6_GR) - zlog_debug("%s, Ignoring unknown TLV type:%d", - __func__, ntohs(tlvh->type)); + zlog_debug("%s, Ignoring unknown TLV type:%d", __func__, + ntohs(tlvh->type)); } } @@ -224,9 +218,8 @@ static bool ospf6_check_chg_in_rxmt_list(struct ospf6_neighbor *nbr) /* Fetching the same copy of LSA form LSDB to validate the * topochange. */ - lsa_in_db = - ospf6_lsdb_lookup(lsa->header->type, lsa->header->id, - lsa->header->adv_router, lsa->lsdb); + lsa_in_db = ospf6_lsdb_lookup(lsa->header->type, lsa->header->id, + lsa->header->adv_router, lsa->lsdb); if (lsa_in_db && lsa_in_db->tobe_acknowledged) { ospf6_lsa_unlock(&lsa); @@ -269,8 +262,7 @@ int ospf6_process_grace_lsa(struct ospf6 *ospf6, struct ospf6_lsa *lsa, int ret; /* Extract the grace lsa packet fields */ - ret = ospf6_extract_grace_lsa_fields(lsa, &grace_interval, - &restart_reason); + ret = ospf6_extract_grace_lsa_fields(lsa, &grace_interval, &restart_reason); if (ret != OSPF6_SUCCESS) { if (IS_DEBUG_OSPF6_GR) zlog_debug("%s, Wrong Grace LSA packet.", __func__); @@ -278,11 +270,9 @@ int ospf6_process_grace_lsa(struct ospf6 *ospf6, struct ospf6_lsa *lsa, } if (IS_DEBUG_OSPF6_GR) - zlog_debug( - "%s, Grace LSA received from %s(%pI4), grace interval:%u, restart reason:%s", - __func__, restarter->name, &restarter->router_id, - grace_interval, - ospf6_restart_reason_desc[restart_reason]); + zlog_debug("%s, Grace LSA received from %s(%pI4), grace interval:%u, restart reason:%s", + __func__, restarter->name, &restarter->router_id, grace_interval, + ospf6_restart_reason_desc[restart_reason]); /* Verify Helper enabled globally */ if (!ospf6->ospf6_helper_cfg.is_helper_supported) { @@ -291,14 +281,11 @@ int ospf6_process_grace_lsa(struct ospf6 *ospf6, struct ospf6_lsa *lsa, */ lookup.advRtrAddr = restarter->router_id; - if (!hash_lookup(ospf6->ospf6_helper_cfg.enable_rtr_list, - &lookup)) { + if (!hash_lookup(ospf6->ospf6_helper_cfg.enable_rtr_list, &lookup)) { if (IS_DEBUG_OSPF6_GR) - zlog_debug( - "%s, HELPER support is disabled, So not a HELPER", - __func__); - restarter->gr_helper_info.rejected_reason = - OSPF6_HELPER_SUPPORT_DISABLED; + zlog_debug("%s, HELPER support is disabled, So not a HELPER", + __func__); + restarter->gr_helper_info.rejected_reason = OSPF6_HELPER_SUPPORT_DISABLED; return OSPF6_GR_NOT_HELPER; } } @@ -308,61 +295,48 @@ int ospf6_process_grace_lsa(struct ospf6 *ospf6, struct ospf6_lsa *lsa, */ if (!IS_NBR_STATE_FULL(restarter)) { if (IS_DEBUG_OSPF6_GR) - zlog_debug( - "%s, This Neighbour %pI6 is not in FULL state.", - __func__, &restarter->linklocal_addr); - restarter->gr_helper_info.rejected_reason = - OSPF6_HELPER_NOT_A_VALID_NEIGHBOUR; + zlog_debug("%s, This Neighbour %pI6 is not in FULL state.", __func__, + &restarter->linklocal_addr); + restarter->gr_helper_info.rejected_reason = OSPF6_HELPER_NOT_A_VALID_NEIGHBOUR; return OSPF6_GR_NOT_HELPER; } /* Based on the restart reason from grace lsa * check the current router is supporting or not */ - if (ospf6->ospf6_helper_cfg.only_planned_restart - && !OSPF6_GR_IS_PLANNED_RESTART(restart_reason)) { + if (ospf6->ospf6_helper_cfg.only_planned_restart && + !OSPF6_GR_IS_PLANNED_RESTART(restart_reason)) { if (IS_DEBUG_OSPF6_GR) - zlog_debug( - "%s, Router supports only planned restarts but received the GRACE LSA due to an unplanned restart", - __func__); - restarter->gr_helper_info.rejected_reason = - OSPF6_HELPER_PLANNED_ONLY_RESTART; + zlog_debug("%s, Router supports only planned restarts but received the GRACE LSA due to an unplanned restart", + __func__); + restarter->gr_helper_info.rejected_reason = OSPF6_HELPER_PLANNED_ONLY_RESTART; return OSPF6_GR_NOT_HELPER; } /* Check the retransmission list of this * neighbour, check any change in lsas. */ - if (ospf6->ospf6_helper_cfg.strict_lsa_check - && restarter->retrans_list->count - && ospf6_check_chg_in_rxmt_list(restarter)) { + if (ospf6->ospf6_helper_cfg.strict_lsa_check && restarter->retrans_list->count && + ospf6_check_chg_in_rxmt_list(restarter)) { if (IS_DEBUG_OSPF6_GR) - zlog_debug( - "%s, Changed LSA in Rxmt list.So not Helper.", - __func__); - restarter->gr_helper_info.rejected_reason = - OSPF6_HELPER_TOPO_CHANGE_RTXMT_LIST; + zlog_debug("%s, Changed LSA in Rxmt list.So not Helper.", __func__); + restarter->gr_helper_info.rejected_reason = OSPF6_HELPER_TOPO_CHANGE_RTXMT_LIST; return OSPF6_GR_NOT_HELPER; } /* LSA age must be less than the grace period */ if (ntohs(lsa->header->age) >= grace_interval) { if (IS_DEBUG_OSPF6_GR) - zlog_debug( - "%s, Grace LSA age(%d) is more than the grace interval(%d)", - __func__, lsa->header->age, grace_interval); - restarter->gr_helper_info.rejected_reason = - OSPF6_HELPER_LSA_AGE_MORE; + zlog_debug("%s, Grace LSA age(%d) is more than the grace interval(%d)", + __func__, lsa->header->age, grace_interval); + restarter->gr_helper_info.rejected_reason = OSPF6_HELPER_LSA_AGE_MORE; return OSPF6_GR_NOT_HELPER; } if (ospf6->gr_info.restart_in_progress) { if (IS_DEBUG_OSPF6_GR) - zlog_debug( - "%s: router is in the process of graceful restart", - __func__); - restarter->gr_helper_info.rejected_reason = - OSPF6_HELPER_RESTARTING; + zlog_debug("%s: router is in the process of graceful restart", __func__); + restarter->gr_helper_info.rejected_reason = OSPF6_HELPER_RESTARTING; return OSPF6_GR_NOT_HELPER; } @@ -374,12 +348,10 @@ int ospf6_process_grace_lsa(struct ospf6 *ospf6, struct ospf6_lsa *lsa, actual_grace_interval = grace_interval; if (grace_interval > ospf6->ospf6_helper_cfg.supported_grace_time) { if (IS_DEBUG_OSPF6_GR) - zlog_debug( - "%s, Received grace period %d is larger than supported grace %d", - __func__, grace_interval, - ospf6->ospf6_helper_cfg.supported_grace_time); - actual_grace_interval = - ospf6->ospf6_helper_cfg.supported_grace_time; + zlog_debug("%s, Received grace period %d is larger than supported grace %d", + __func__, grace_interval, + ospf6->ospf6_helper_cfg.supported_grace_time); + actual_grace_interval = ospf6->ospf6_helper_cfg.supported_grace_time; } if (OSPF6_GR_IS_ACTIVE_HELPER(restarter)) { @@ -389,14 +361,12 @@ int ospf6_process_grace_lsa(struct ospf6 *ospf6, struct ospf6_lsa *lsa, ospf6->ospf6_helper_cfg.active_restarter_cnt--; if (IS_DEBUG_OSPF6_GR) - zlog_debug( - "%s, Router is already acting as a HELPER for this nbr,so restart the grace timer", - __func__); + zlog_debug("%s, Router is already acting as a HELPER for this nbr,so restart the grace timer", + __func__); } else { if (IS_DEBUG_OSPF6_GR) - zlog_debug( - "%s, This Router becomes a HELPER for the neighbour %pI6", - __func__, &restarter->linklocal_addr); + zlog_debug("%s, This Router becomes a HELPER for the neighbour %pI6", + __func__, &restarter->linklocal_addr); } /* Became a Helper to the RESTART neighbour. @@ -412,14 +382,17 @@ int ospf6_process_grace_lsa(struct ospf6 *ospf6, struct ospf6_lsa *lsa, ospf6->ospf6_helper_cfg.active_restarter_cnt++; if (IS_DEBUG_OSPF6_GR) - zlog_debug("%s, Grace timer started.interval:%u", __func__, - actual_grace_interval); + zlog_debug("%s, Grace timer started.interval:%u", __func__, actual_grace_interval); /* Start the grace timer */ - event_add_timer(master, ospf6_handle_grace_timer_expiry, restarter, - actual_grace_interval, + event_add_timer(master, ospf6_handle_grace_timer_expiry, restarter, actual_grace_interval, &restarter->gr_helper_info.t_grace_timer); + /* RFC 9129 ietf-ospf:nbr-restart-helper-status-change: now helping. */ + ospf6d_ietf_notif_nbr_restart_helper_status_change(restarter, 2, + (uint16_t)actual_grace_interval, + OSPF6_GR_HELPER_INPROGRESS); + return OSPF6_GR_ACTIVE_HELPER; } @@ -440,8 +413,7 @@ int ospf6_process_grace_lsa(struct ospf6 *ospf6, struct ospf6_lsa *lsa, * Returns: * Nothing. */ -void ospf6_gr_helper_exit(struct ospf6_neighbor *nbr, - enum ospf6_helper_exit_reason reason) +void ospf6_gr_helper_exit(struct ospf6_neighbor *nbr, enum ospf6_helper_exit_reason reason) { struct ospf6_interface *oi = nbr->ospf6_if; struct ospf6 *ospf6; @@ -455,9 +427,8 @@ void ospf6_gr_helper_exit(struct ospf6_neighbor *nbr, return; if (IS_DEBUG_OSPF6_GR) - zlog_debug("%s, Exiting from HELPER support to %pI6, due to %s", - __func__, &nbr->linklocal_addr, - ospf6_exit_reason_desc[reason]); + zlog_debug("%s, Exiting from HELPER support to %pI6, due to %s", __func__, + &nbr->linklocal_addr, ospf6_exit_reason_desc[reason]); /* Reset helper status*/ nbr->gr_helper_info.gr_helper_status = OSPF6_GR_NOT_HELPER; @@ -486,8 +457,8 @@ void ospf6_gr_helper_exit(struct ospf6_neighbor *nbr, */ if (reason != OSPF6_GR_HELPER_COMPLETED) { if (IS_DEBUG_OSPF6_GR) - zlog_debug("%s, Unsuccessful GR exit. RESTARTER : %pI6", - __func__, &nbr->linklocal_addr); + zlog_debug("%s, Unsuccessful GR exit. RESTARTER : %pI6", __func__, + &nbr->linklocal_addr); } /*Recalculate the DR for the network segment */ @@ -499,6 +470,9 @@ void ospf6_gr_helper_exit(struct ospf6_neighbor *nbr, /* Originate network lsa if it is an DR in the LAN */ if (nbr->ospf6_if->state == OSPF6_INTERFACE_DR) OSPF6_NETWORK_LSA_SCHEDULE(nbr->ospf6_if); + + /* RFC 9129 ietf-ospf:nbr-restart-helper-status-change: not-helping. */ + ospf6d_ietf_notif_nbr_restart_helper_status_change(nbr, 1, 0, reason); } /* @@ -527,8 +501,7 @@ void ospf6_process_maxage_grace_lsa(struct ospf6 *ospf6, struct ospf6_lsa *lsa, int ret; /* Extract the grace lsa packet fields */ - ret = ospf6_extract_grace_lsa_fields(lsa, &grace_interval, - &restart_reason); + ret = ospf6_extract_grace_lsa_fields(lsa, &grace_interval, &restart_reason); if (ret != OSPF6_SUCCESS) { if (IS_DEBUG_OSPF6_GR) zlog_debug("%s, Wrong Grace LSA packet.", __func__); @@ -536,8 +509,8 @@ void ospf6_process_maxage_grace_lsa(struct ospf6 *ospf6, struct ospf6_lsa *lsa, } if (IS_DEBUG_OSPF6_GR) - zlog_debug("%s, GraceLSA received for neighbour %pI4.", - __func__, &restarter->router_id); + zlog_debug("%s, GraceLSA received for neighbour %pI4.", __func__, + &restarter->router_id); ospf6_gr_helper_exit(restarter, OSPF6_GR_HELPER_COMPLETED); } @@ -571,32 +544,26 @@ void ospf6_helper_handle_topo_chg(struct ospf6 *ospf6, struct ospf6_lsa *lsa) return; if (IS_DEBUG_OSPF6_GR) - zlog_debug("%s, Topo change detected due to lsa details : %s", - __func__, lsa->name); + zlog_debug("%s, Topo change detected due to lsa details : %s", __func__, lsa->name); lsa->tobe_acknowledged = OSPF6_TRUE; for (ALL_LIST_ELEMENTS_RO(ospf6->area_list, i, oa)) for (ALL_LIST_ELEMENTS_RO(oa->if_list, j, oi)) { - /* Ref rfc3623 section 3.2.3.b and rfc5187 * If change due to external LSA and if the area is * stub, then it is not a topo change. Since Type-5 * lsas will not be flooded in stub area. */ - if (IS_AREA_STUB(oi->area) - && ((lsa->header->type == OSPF6_LSTYPE_AS_EXTERNAL) - || (lsa->header->type == OSPF6_LSTYPE_TYPE_7) - || (lsa->header->type - == OSPF6_LSTYPE_INTER_ROUTER))) { + if (IS_AREA_STUB(oi->area) && + ((lsa->header->type == OSPF6_LSTYPE_AS_EXTERNAL) || + (lsa->header->type == OSPF6_LSTYPE_TYPE_7) || + (lsa->header->type == OSPF6_LSTYPE_INTER_ROUTER))) { continue; } - for (ALL_LIST_ELEMENTS_RO(oi->neighbor_list, k, nbr)) { - - ospf6_gr_helper_exit(nbr, - OSPF6_GR_HELPER_TOPO_CHG); - } + for (ALL_LIST_ELEMENTS_RO(oi->neighbor_list, k, nbr)) + ospf6_gr_helper_exit(nbr, OSPF6_GR_HELPER_TOPO_CHG); } } @@ -613,7 +580,7 @@ void ospf6_helper_handle_topo_chg(struct ospf6 *ospf6, struct ospf6_lsa *lsa) * Returns: * Nothing. */ -static void ospf6_gr_helper_support_set(struct ospf6 *ospf6, bool support) +void ospf6_gr_helper_support_set(struct ospf6 *ospf6, bool support) { struct ospf6_interface *oi; struct advRtr lookup; @@ -632,24 +599,18 @@ static void ospf6_gr_helper_support_set(struct ospf6 *ospf6, bool support) if (support == OSPF6_FALSE) { for (ALL_LIST_ELEMENTS_RO(ospf6->area_list, i, oa)) for (ALL_LIST_ELEMENTS_RO(oa->if_list, j, oi)) { - - for (ALL_LIST_ELEMENTS_RO(oi->neighbor_list, k, - nbr)) { - + for (ALL_LIST_ELEMENTS_RO(oi->neighbor_list, k, nbr)) { lookup.advRtrAddr = nbr->router_id; /* check if helper support enabled for * the corresponding routerid. * If enabled, * dont exit from helper role. */ - if (hash_lookup( - ospf6->ospf6_helper_cfg - .enable_rtr_list, - &lookup)) + if (hash_lookup(ospf6->ospf6_helper_cfg.enable_rtr_list, + &lookup)) continue; - ospf6_gr_helper_exit( - nbr, OSPF6_GR_HELPER_TOPO_CHG); + ospf6_gr_helper_exit(nbr, OSPF6_GR_HELPER_TOPO_CHG); } } } @@ -668,7 +629,7 @@ static void ospf6_gr_helper_support_set(struct ospf6 *ospf6, bool support) * Returns: * Nothing. */ -static void ospf6_gr_helper_lsacheck_set(struct ospf6 *ospf6, bool enabled) +void ospf6_gr_helper_lsacheck_set(struct ospf6 *ospf6, bool enabled) { if (ospf6->ospf6_helper_cfg.strict_lsa_check == enabled) return; @@ -690,9 +651,8 @@ static void ospf6_gr_helper_lsacheck_set(struct ospf6 *ospf6, bool enabled) * Nothing. */ -static void -ospf6_gr_helper_set_supported_onlyPlanned_restart(struct ospf6 *ospf6, - bool only_planned) +static void ospf6_gr_helper_set_supported_onlyPlanned_restart(struct ospf6 *ospf6, + bool only_planned) { ospf6->ospf6_helper_cfg.only_planned_restart = only_planned; } @@ -709,15 +669,13 @@ ospf6_gr_helper_set_supported_onlyPlanned_restart(struct ospf6 *ospf6, * Returns: * Nothing. */ -static void ospf6_gr_helper_supported_gracetime_set(struct ospf6 *ospf6, - uint32_t interval) +static void ospf6_gr_helper_supported_gracetime_set(struct ospf6 *ospf6, uint32_t interval) { ospf6->ospf6_helper_cfg.supported_grace_time = interval; } /* API to walk and print all the Helper supported router ids */ -static int ospf6_print_vty_helper_dis_rtr_walkcb(struct hash_bucket *bucket, - void *arg) +static int ospf6_print_vty_helper_dis_rtr_walkcb(struct hash_bucket *bucket, void *arg) { struct advRtr *rtr = bucket->data; struct vty *vty = (struct vty *)arg; @@ -733,8 +691,7 @@ static int ospf6_print_vty_helper_dis_rtr_walkcb(struct hash_bucket *bucket, } /* API to walk and print all the Helper supported router ids.*/ -static int ospf6_print_json_helper_dis_rtr_walkcb(struct hash_bucket *bucket, - void *arg) +static int ospf6_print_json_helper_dis_rtr_walkcb(struct hash_bucket *bucket, void *arg) { struct advRtr *rtr = bucket->data; struct json_object *json_rid_array = (struct json_object *)arg; @@ -768,8 +725,7 @@ static int ospf6_print_json_helper_dis_rtr_walkcb(struct hash_bucket *bucket, * Returns: * Nothing. */ -static void ospf6_gr_helper_support_set_per_routerid(struct ospf6 *ospf6, - struct in_addr router_id, +static void ospf6_gr_helper_support_set_per_routerid(struct ospf6 *ospf6, struct in_addr router_id, bool support) { struct advRtr temp; @@ -783,12 +739,10 @@ static void ospf6_gr_helper_support_set_per_routerid(struct ospf6 *ospf6, if (support == OSPF6_FALSE) { /*Delete the routerid from the enable router hash table */ - rtr = hash_lookup(ospf6->ospf6_helper_cfg.enable_rtr_list, - &temp); + rtr = hash_lookup(ospf6->ospf6_helper_cfg.enable_rtr_list, &temp); if (rtr) { - hash_release(ospf6->ospf6_helper_cfg.enable_rtr_list, - rtr); + hash_release(ospf6->ospf6_helper_cfg.enable_rtr_list, rtr); ospf6_disable_rtr_hash_free(rtr); } @@ -803,17 +757,12 @@ static void ospf6_gr_helper_support_set_per_routerid(struct ospf6 *ospf6, */ for (ALL_LIST_ELEMENTS_RO(ospf6->area_list, i, oa)) for (ALL_LIST_ELEMENTS_RO(oa->if_list, j, oi)) { - - for (ALL_LIST_ELEMENTS_RO(oi->neighbor_list, k, - nbr)) { - + for (ALL_LIST_ELEMENTS_RO(oi->neighbor_list, k, nbr)) { if (nbr->router_id != router_id.s_addr) continue; if (OSPF6_GR_IS_ACTIVE_HELPER(nbr)) - ospf6_gr_helper_exit( - nbr, - OSPF6_GR_HELPER_TOPO_CHG); + ospf6_gr_helper_exit(nbr, OSPF6_GR_HELPER_TOPO_CHG); } } @@ -824,8 +773,8 @@ static void ospf6_gr_helper_support_set_per_routerid(struct ospf6 *ospf6, } } -static void show_ospfv6_gr_helper_per_nbr(struct vty *vty, json_object *json, - bool uj, struct ospf6_neighbor *nbr) +static void show_ospfv6_gr_helper_per_nbr(struct vty *vty, json_object *json, bool uj, + struct ospf6_neighbor *nbr) { if (!uj) { vty_out(vty, " Routerid : %pI4\n", &nbr->router_id); @@ -834,11 +783,9 @@ static void show_ospfv6_gr_helper_per_nbr(struct vty *vty, json_object *json, vty_out(vty, " Actual Grace period : %d(in seconds)\n", nbr->gr_helper_info.actual_grace_period); vty_out(vty, " Remaining GraceTime:%ld(in seconds).\n", - event_timer_remain_second( - nbr->gr_helper_info.t_grace_timer)); + event_timer_remain_second(nbr->gr_helper_info.t_grace_timer)); vty_out(vty, " Graceful Restart reason: %s.\n\n", - ospf6_restart_reason_desc[nbr->gr_helper_info - .gr_restart_reason]); + ospf6_restart_reason_desc[nbr->gr_helper_info.gr_restart_reason]); } else { char nbrid[16]; json_object *json_neigh = NULL; @@ -849,19 +796,18 @@ static void show_ospfv6_gr_helper_per_nbr(struct vty *vty, json_object *json, json_object_int_add(json_neigh, "recvdGraceInterval", nbr->gr_helper_info.recvd_grace_period); json_object_int_add(json_neigh, "actualGraceInterval", - nbr->gr_helper_info.actual_grace_period); + nbr->gr_helper_info.actual_grace_period); json_object_int_add(json_neigh, "remainGracetime", - event_timer_remain_second( - nbr->gr_helper_info.t_grace_timer)); + event_timer_remain_second(nbr->gr_helper_info.t_grace_timer)); json_object_string_add(json_neigh, "restartReason", - ospf6_restart_reason_desc[ - nbr->gr_helper_info.gr_restart_reason]); + ospf6_restart_reason_desc[nbr->gr_helper_info + .gr_restart_reason]); json_object_object_add(json, nbr->name, json_neigh); } } -static void show_ospf6_gr_helper_details(struct vty *vty, struct ospf6 *ospf6, - json_object *json, bool uj, bool detail) +static void show_ospf6_gr_helper_details(struct vty *vty, struct ospf6 *ospf6, json_object *json, + bool uj, bool detail) { struct ospf6_interface *oi; @@ -869,22 +815,17 @@ static void show_ospf6_gr_helper_details(struct vty *vty, struct ospf6 *ospf6, if (uj) { char router_id[16]; - inet_ntop(AF_INET, &ospf6->router_id, router_id, - sizeof(router_id)); + inet_ntop(AF_INET, &ospf6->router_id, router_id, sizeof(router_id)); json_object_string_add(json, "routerId", router_id); } else - vty_out(vty, - " OSPFv3 Routing Process (0) with Router-ID %pI4\n", + vty_out(vty, " OSPFv3 Routing Process (0) with Router-ID %pI4\n", &ospf6->router_id); if (!uj) { - if (ospf6->ospf6_helper_cfg.is_helper_supported) - vty_out(vty, - " Graceful restart helper support enabled.\n"); + vty_out(vty, " Graceful restart helper support enabled.\n"); else - vty_out(vty, - " Graceful restart helper support disabled.\n"); + vty_out(vty, " Graceful restart helper support disabled.\n"); if (ospf6->ospf6_helper_cfg.strict_lsa_check) vty_out(vty, " Strict LSA check is enabled.\n"); @@ -892,14 +833,11 @@ static void show_ospf6_gr_helper_details(struct vty *vty, struct ospf6 *ospf6, vty_out(vty, " Strict LSA check is disabled.\n"); if (ospf6->ospf6_helper_cfg.only_planned_restart) - vty_out(vty, - " Helper supported for planned restarts only.\n"); + vty_out(vty, " Helper supported for planned restarts only.\n"); else - vty_out(vty, - " Helper supported for Planned and Unplanned Restarts.\n"); + vty_out(vty, " Helper supported for Planned and Unplanned Restarts.\n"); - vty_out(vty, - " Supported Graceful restart interval: %d(in seconds).\n", + vty_out(vty, " Supported Graceful restart interval: %d(in seconds).\n", ospf6->ospf6_helper_cfg.supported_grace_time); if (OSPF6_HELPER_ENABLE_RTR_COUNT(ospf)) { @@ -910,68 +848,51 @@ static void show_ospf6_gr_helper_details(struct vty *vty, struct ospf6 *ospf6, vty_out(vty, "\n\n"); } - if (ospf6->ospf6_helper_cfg.last_exit_reason - != OSPF6_GR_HELPER_EXIT_NONE) { + if (ospf6->ospf6_helper_cfg.last_exit_reason != OSPF6_GR_HELPER_EXIT_NONE) { vty_out(vty, " Last Helper exit Reason :%s\n", - ospf6_exit_reason_desc - [ospf6->ospf6_helper_cfg - .last_exit_reason]); + ospf6_exit_reason_desc[ospf6->ospf6_helper_cfg.last_exit_reason]); if (ospf6->ospf6_helper_cfg.active_restarter_cnt) vty_out(vty, " Number of Active neighbours in graceful restart: %d\n", - ospf6->ospf6_helper_cfg - .active_restarter_cnt); + ospf6->ospf6_helper_cfg.active_restarter_cnt); else vty_out(vty, "\n"); } } else { - json_object_string_add( - json, "helperSupport", - (ospf6->ospf6_helper_cfg.is_helper_supported) - ? "Enabled" - : "Disabled"); - json_object_string_add( - json, "strictLsaCheck", - (ospf6->ospf6_helper_cfg.strict_lsa_check) - ? "Enabled" - : "Disabled"); - - json_object_string_add( - json, "restartSupport", - (ospf6->ospf6_helper_cfg.only_planned_restart) - ? "Planned Restart only" - : "Planned and Unplanned Restarts"); - - json_object_int_add( - json, "supportedGracePeriod", - ospf6->ospf6_helper_cfg.supported_grace_time); - - if (ospf6->ospf6_helper_cfg.last_exit_reason != - OSPF6_GR_HELPER_EXIT_NONE) - json_object_string_add( - json, "lastExitReason", - ospf6_exit_reason_desc - [ospf6->ospf6_helper_cfg - .last_exit_reason]); + json_object_string_add(json, "helperSupport", + (ospf6->ospf6_helper_cfg.is_helper_supported) ? "Enabled" + : "Disabled"); + json_object_string_add(json, "strictLsaCheck", + (ospf6->ospf6_helper_cfg.strict_lsa_check) ? "Enabled" + : "Disabled"); + + json_object_string_add(json, "restartSupport", + (ospf6->ospf6_helper_cfg.only_planned_restart) + ? "Planned Restart only" + : "Planned and Unplanned Restarts"); + + json_object_int_add(json, "supportedGracePeriod", + ospf6->ospf6_helper_cfg.supported_grace_time); + + if (ospf6->ospf6_helper_cfg.last_exit_reason != OSPF6_GR_HELPER_EXIT_NONE) + json_object_string_add(json, "lastExitReason", + ospf6_exit_reason_desc[ospf6->ospf6_helper_cfg + .last_exit_reason]); if (ospf6->ospf6_helper_cfg.active_restarter_cnt) - json_object_int_add( - json, "activeRestarterCnt", - ospf6->ospf6_helper_cfg.active_restarter_cnt); + json_object_int_add(json, "activeRestarterCnt", + ospf6->ospf6_helper_cfg.active_restarter_cnt); if (OSPF6_HELPER_ENABLE_RTR_COUNT(ospf6)) { - struct json_object *json_rid_array = - json_object_new_array(); + struct json_object *json_rid_array = json_object_new_array(); - json_object_object_add(json, "enabledRouterIds", - json_rid_array); + json_object_object_add(json, "enabledRouterIds", json_rid_array); hash_walk(ospf6->ospf6_helper_cfg.enable_rtr_list, - ospf6_print_json_helper_dis_rtr_walkcb, - json_rid_array); + ospf6_print_json_helper_dis_rtr_walkcb, json_rid_array); } } @@ -986,39 +907,35 @@ static void show_ospf6_gr_helper_details(struct vty *vty, struct ospf6 *ospf6, struct ospf6_neighbor *nbr; if (uj) { - json_object_object_get_ex( - json, "neighbors", - &json_neighbors); + json_object_object_get_ex(json, "neighbors", + &json_neighbors); if (!json_neighbors) { - json_neighbors = - json_object_new_object(); - json_object_object_add( - json, "neighbors", - json_neighbors); + json_neighbors = json_object_new_object(); + json_object_object_add(json, "neighbors", + json_neighbors); } } - for (ALL_LIST_ELEMENTS_RO(oi->neighbor_list, k, - nbr)) { - + for (ALL_LIST_ELEMENTS_RO(oi->neighbor_list, k, nbr)) { if (!OSPF6_GR_IS_ACTIVE_HELPER(nbr)) continue; if (!uj) - vty_out(vty, - " Neighbour %d :\n", - cnt++); - - show_ospfv6_gr_helper_per_nbr( - vty, json_neighbors, uj, nbr); + vty_out(vty, " Neighbour %d :\n", cnt++); + show_ospfv6_gr_helper_per_nbr(vty, json_neighbors, uj, nbr); } } } } -/* Graceful Restart HELPER config Commands */ -DEFPY(ospf6_gr_helper_enable, +/* + * `graceful-restart helper enable` maps onto RFC 9129 + * `/graceful-restart/helper-enabled`. The per-router-id form has no + * RFC counterpart; the YANG model has no enable-list, so the per- + * router-id case stays on the legacy direct mutation path. + */ +DEFPY_YANG(ospf6_gr_helper_enable, ospf6_gr_helper_enable_cmd, "graceful-restart helper enable [A.B.C.D$rtr_id]", "ospf6 graceful restart\n" @@ -1027,21 +944,21 @@ DEFPY(ospf6_gr_helper_enable, "Advertisement Router-ID\n") { VTY_DECLVAR_CONTEXT(ospf6, ospf6); + char xpath[XPATH_MAXLEN]; if (rtr_id_str != NULL) { - - ospf6_gr_helper_support_set_per_routerid(ospf6, rtr_id, - OSPF6_TRUE); - + ospf6_gr_helper_support_set_per_routerid(ospf6, rtr_id, OSPF6_TRUE); return CMD_SUCCESS; } - ospf6_gr_helper_support_set(ospf6, OSPF6_TRUE); - - return CMD_SUCCESS; + if (ospf6_per_instance_xpath(xpath, sizeof(xpath), ospf6, + "/graceful-restart/helper-enabled") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "true"); + return nb_cli_apply_changes(vty, NULL); } -DEFPY(ospf6_gr_helper_disable, +DEFPY_YANG(ospf6_gr_helper_disable, ospf6_gr_helper_disable_cmd, "no graceful-restart helper enable [A.B.C.D$rtr_id]", NO_STR @@ -1051,21 +968,28 @@ DEFPY(ospf6_gr_helper_disable, "Advertisement Router-ID\n") { VTY_DECLVAR_CONTEXT(ospf6, ospf6); + char xpath[XPATH_MAXLEN]; if (rtr_id_str != NULL) { - - ospf6_gr_helper_support_set_per_routerid(ospf6, rtr_id, - OSPF6_FALSE); - + ospf6_gr_helper_support_set_per_routerid(ospf6, rtr_id, OSPF6_FALSE); return CMD_SUCCESS; } - ospf6_gr_helper_support_set(ospf6, OSPF6_FALSE); - - return CMD_SUCCESS; + if (ospf6_per_instance_xpath(xpath, sizeof(xpath), ospf6, + "/graceful-restart/helper-enabled") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); } -DEFPY(ospf6_gr_helper_disable_lsacheck, +/* + * ospf6d's strict-LSA-check CLI uses an inverted form + * (`graceful-restart helper lsa-check-disable` to disable strict + * checking, `no ...` to restore it) -- v2 uses the positive form. + * The YANG leaf is positive (`helper-strict-lsa-checking`), so this + * shim flips the meaning before enqueueing. + */ +DEFPY_YANG(ospf6_gr_helper_disable_lsacheck, ospf6_gr_helper_disable_lsacheck_cmd, "graceful-restart helper lsa-check-disable", "ospf6 graceful restart\n" @@ -1073,12 +997,16 @@ DEFPY(ospf6_gr_helper_disable_lsacheck, "disable strict LSA check\n") { VTY_DECLVAR_CONTEXT(ospf6, ospf6); + char xpath[XPATH_MAXLEN]; - ospf6_gr_helper_lsacheck_set(ospf6, OSPF6_FALSE); - return CMD_SUCCESS; + if (ospf6_per_instance_xpath(xpath, sizeof(xpath), ospf6, + "/graceful-restart/helper-strict-lsa-checking") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "false"); + return nb_cli_apply_changes(vty, NULL); } -DEFPY(no_ospf6_gr_helper_disable_lsacheck, +DEFPY_YANG(no_ospf6_gr_helper_disable_lsacheck, no_ospf6_gr_helper_disable_lsacheck_cmd, "no graceful-restart helper lsa-check-disable", NO_STR @@ -1087,9 +1015,13 @@ DEFPY(no_ospf6_gr_helper_disable_lsacheck, "diasble strict LSA check\n") { VTY_DECLVAR_CONTEXT(ospf6, ospf6); + char xpath[XPATH_MAXLEN]; - ospf6_gr_helper_lsacheck_set(ospf6, OSPF6_TRUE); - return CMD_SUCCESS; + if (ospf6_per_instance_xpath(xpath, sizeof(xpath), ospf6, + "/graceful-restart/helper-strict-lsa-checking") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "true"); + return nb_cli_apply_changes(vty, NULL); } DEFPY(ospf6_gr_helper_planned_only, @@ -1145,8 +1077,7 @@ DEFPY(no_ospf6_gr_helper_supported_grace_time, { VTY_DECLVAR_CONTEXT(ospf6, ospf6); - ospf6_gr_helper_supported_gracetime_set(ospf6, - OSPF6_MAX_GRACE_INTERVAL); + ospf6_gr_helper_supported_gracetime_set(ospf6, OSPF6_MAX_GRACE_INTERVAL); return CMD_SUCCESS; } @@ -1213,8 +1144,8 @@ DEFPY(debug_ospf6_gr, debug_ospf6_gr_cmd, * Returns: * Nothing. */ -static int ospf6_grace_lsa_show_info(struct vty *vty, struct ospf6_lsa *lsa, - json_object *json, bool use_json) +static int ospf6_grace_lsa_show_info(struct vty *vty, struct ospf6_lsa *lsa, json_object *json, + bool use_json) { struct ospf6_lsa_header *lsah = NULL; struct tlv_header *tlvh = NULL; @@ -1226,8 +1157,7 @@ static int ospf6_grace_lsa_show_info(struct vty *vty, struct ospf6_lsa *lsa, lsah = lsa->header; if (ntohs(lsah->length) <= OSPF6_LSA_HEADER_SIZE) { if (IS_DEBUG_OSPF6_GR) - zlog_debug("%s: undersized (%u B) lsa", __func__, - ntohs(lsah->length)); + zlog_debug("%s: undersized (%u B) lsa", __func__, ntohs(lsah->length)); return OSPF6_FAILURE; } @@ -1240,16 +1170,13 @@ static int ospf6_grace_lsa_show_info(struct vty *vty, struct ospf6_lsa *lsa, zlog_debug(" TLV info:"); } - for (tlvh = lsdesc_start(lsah); sum < length && tlvh; - tlvh = TLV_HDR_NEXT(tlvh)) { + for (tlvh = lsdesc_start(lsah); sum < length && tlvh; tlvh = TLV_HDR_NEXT(tlvh)) { /* Check TLV len */ if (sum + TLV_SIZE(tlvh) > length) { if (vty) - vty_out(vty, "%% Invalid TLV length: %d\n", - TLV_SIZE(tlvh)); + vty_out(vty, "%% Invalid TLV length: %d\n", TLV_SIZE(tlvh)); else if (IS_DEBUG_OSPF6_GR) - zlog_debug("%% Invalid TLV length: %d", - TLV_SIZE(tlvh)); + zlog_debug("%% Invalid TLV length: %d", TLV_SIZE(tlvh)); return OSPF6_FAILURE; } @@ -1260,15 +1187,13 @@ static int ospf6_grace_lsa_show_info(struct vty *vty, struct ospf6_lsa *lsa, if (vty) { if (use_json) - json_object_int_add( - json, "gracePeriod", - ntohl(gracePeriod->interval)); + json_object_int_add(json, "gracePeriod", + ntohl(gracePeriod->interval)); else vty_out(vty, " Grace period:%d\n", ntohl(gracePeriod->interval)); } else { - zlog_debug(" Grace period:%d", - ntohl(gracePeriod->interval)); + zlog_debug(" Grace period:%d", ntohl(gracePeriod->interval)); } break; case TLV_GRACE_RESTART_REASON_TYPE: @@ -1276,18 +1201,15 @@ static int ospf6_grace_lsa_show_info(struct vty *vty, struct ospf6_lsa *lsa, sum += TLV_SIZE(tlvh); if (vty) { if (use_json) - json_object_string_add( - json, "restartReason", - ospf6_restart_reason_desc - [grReason->reason]); + json_object_string_add(json, "restartReason", + ospf6_restart_reason_desc + [grReason->reason]); else vty_out(vty, " Restart reason:%s\n", - ospf6_restart_reason_desc - [grReason->reason]); + ospf6_restart_reason_desc[grReason->reason]); } else { zlog_debug(" Restart reason:%s", - ospf6_restart_reason_desc - [grReason->reason]); + ospf6_restart_reason_desc[grReason->reason]); } break; default: @@ -1300,7 +1222,6 @@ static int ospf6_grace_lsa_show_info(struct vty *vty, struct ospf6_lsa *lsa, void ospf6_gr_helper_config_init(void) { - ospf6_install_lsa_handler(&grace_lsa_handler); install_element(OSPF6_NODE, &ospf6_gr_helper_enable_cmd); @@ -1310,8 +1231,7 @@ void ospf6_gr_helper_config_init(void) install_element(OSPF6_NODE, &ospf6_gr_helper_planned_only_cmd); install_element(OSPF6_NODE, &no_ospf6_gr_helper_planned_only_cmd); install_element(OSPF6_NODE, &ospf6_gr_helper_supported_grace_time_cmd); - install_element(OSPF6_NODE, - &no_ospf6_gr_helper_supported_grace_time_cmd); + install_element(OSPF6_NODE, &no_ospf6_gr_helper_supported_grace_time_cmd); install_element(VIEW_NODE, &show_ipv6_ospf6_gr_helper_cmd); @@ -1341,9 +1261,9 @@ void ospf6_gr_helper_init(struct ospf6 *ospf6) ospf6->ospf6_helper_cfg.last_exit_reason = OSPF6_GR_HELPER_EXIT_NONE; ospf6->ospf6_helper_cfg.active_restarter_cnt = 0; - ospf6->ospf6_helper_cfg.enable_rtr_list = hash_create( - ospf6_enable_rtr_hash_key, ospf6_enable_rtr_hash_cmp, - "Ospf6 enable router hash"); + ospf6->ospf6_helper_cfg.enable_rtr_list = hash_create(ospf6_enable_rtr_hash_key, + ospf6_enable_rtr_hash_cmp, + "Ospf6 enable router hash"); } /* @@ -1357,15 +1277,13 @@ void ospf6_gr_helper_init(struct ospf6 *ospf6) */ void ospf6_gr_helper_deinit(struct ospf6 *ospf6) { - if (IS_DEBUG_OSPF6_GR) zlog_debug("%s, GR helper deinit.", __func__); ospf6_enable_rtr_hash_destroy(ospf6); } -static int ospf6_cfg_write_helper_enable_rtr_walkcb(struct hash_bucket *backet, - void *arg) +static int ospf6_cfg_write_helper_enable_rtr_walkcb(struct hash_bucket *backet, void *arg) { struct advRtr *rtr = backet->data; struct vty *vty = (struct vty *)arg; @@ -1385,10 +1303,8 @@ int config_write_ospf6_gr_helper(struct vty *vty, struct ospf6 *ospf6) if (ospf6->ospf6_helper_cfg.only_planned_restart) vty_out(vty, " graceful-restart helper planned-only\n"); - if (ospf6->ospf6_helper_cfg.supported_grace_time - != OSPF6_MAX_GRACE_INTERVAL) - vty_out(vty, - " graceful-restart helper supported-grace-time %d\n", + if (ospf6->ospf6_helper_cfg.supported_grace_time != OSPF6_MAX_GRACE_INTERVAL) + vty_out(vty, " graceful-restart helper supported-grace-time %d\n", ospf6->ospf6_helper_cfg.supported_grace_time); if (OSPF6_HELPER_ENABLE_RTR_COUNT(ospf6)) { diff --git a/ospf6d/ospf6_interface.c b/ospf6d/ospf6_interface.c index 7f16a96d4d01..8041a051283d 100644 --- a/ospf6d/ospf6_interface.c +++ b/ospf6d/ospf6_interface.c @@ -17,6 +17,8 @@ #include "prefix.h" #include "plist.h" #include "zclient.h" +#include "northbound_cli.h" +#include "bfd.h" #include "ospf6_proto.h" #include "ospf6_lsa.h" @@ -29,6 +31,7 @@ #include "ospf6_abr.h" #include "ospf6_interface.h" #include "ospf6_neighbor.h" +#include "ospf6_nb.h" #include "ospf6_intra.h" #include "ospf6_spf.h" #include "ospf6d.h" @@ -119,7 +122,7 @@ static void ospf6_interface_lsdb_hook_remove(struct ospf6_lsa *lsa) ospf6_interface_lsdb_hook(lsa, ospf6_lsremove_to_spf_reason(lsa)); } -static uint8_t ospf6_default_iftype(struct interface *ifp) +uint8_t ospf6_default_iftype(struct interface *ifp) { if (if_is_pointopoint(ifp)) return OSPF_IFTYPE_POINTOPOINT; @@ -171,7 +174,7 @@ static uint32_t ospf6_interface_get_cost(struct ospf6_interface *oi) return cost; } -static void ospf6_interface_force_recalculate_cost(struct ospf6_interface *oi) +void ospf6_interface_force_recalculate_cost(struct ospf6_interface *oi) { /* update cost held in route_connected list in ospf6_interface */ ospf6_interface_connected_route_update(oi->interface); @@ -186,7 +189,7 @@ static void ospf6_interface_force_recalculate_cost(struct ospf6_interface *oi) } } -static void ospf6_interface_recalculate_cost(struct ospf6_interface *oi) +void ospf6_interface_recalculate_cost(struct ospf6_interface *oi) { uint32_t newcost; @@ -216,15 +219,18 @@ struct ospf6_interface *ospf6_interface_create(struct interface *ifp) oi->transdelay = OSPF6_INTERFACE_TRANSDELAY; oi->priority = OSPF6_INTERFACE_PRIORITY; - oi->hello_interval = OSPF_HELLO_INTERVAL_DEFAULT; + oi->hello_interval = OSPF6_INTERFACE_HELLO_INTERVAL; oi->gr.hello_delay.interval = OSPF_HELLO_DELAY_DEFAULT; - oi->dead_interval = OSPF_ROUTER_DEAD_INTERVAL_DEFAULT; + oi->dead_interval = OSPF6_INTERFACE_DEAD_INTERVAL; oi->rxmt_interval = OSPF_RETRANSMIT_INTERVAL_DEFAULT; oi->type = ospf6_default_iftype(ifp); oi->state = OSPF6_INTERFACE_DOWN; oi->flag = 0; oi->mtu_ignore = 0; oi->c_ifmtu = 0; + oi->bfd_config.detection_multiplier = BFD_DEF_DETECT_MULT; + oi->bfd_config.min_rx = BFD_DEF_MIN_RX; + oi->bfd_config.min_tx = BFD_DEF_MIN_TX; /* Try to adjust I/O buffer size with IfMtu */ oi->ifmtu = ifp->mtu6; @@ -286,8 +292,18 @@ void ospf6_interface_delete(struct ospf6_interface *oi) ospf6_fifo_free(oi->obuf); - for (ALL_LIST_ELEMENTS(oi->neighbor_list, node, nnode, on)) + /* + * Walk the neighbour through OSPF6_NEIGHBOR_DOWN before freeing it so + * the ospf6_neighbor_change hook fires. Subscribers to RFC 9129 + * nbr-state-change would otherwise miss every interface-removal + * tear-down: the dead-timer path is the only other site that + * transitions to DOWN before delete, so admin-down and interface + * destroy were silent. + */ + for (ALL_LIST_ELEMENTS(oi->neighbor_list, node, nnode, on)) { + ospf6_neighbor_force_down(on); ospf6_neighbor_delete(on); + } list_delete(&oi->neighbor_list); @@ -995,8 +1011,16 @@ void interface_down(struct event *event) ospf6_gr_helper_exit(nbr, OSPF6_GR_HELPER_TOPO_CHG); } - for (ALL_LIST_ELEMENTS(oi->neighbor_list, node, nnode, on)) + /* + * Walk each neighbour through OSPF6_NEIGHBOR_DOWN so the + * ospf6_neighbor_change hook fires on interface state reset. + * Without this, RFC 9129 nbr-state-change subscribers miss + * tear-down events triggered by admin-down / link-down on v3. + */ + for (ALL_LIST_ELEMENTS(oi->neighbor_list, node, nnode, on)) { + ospf6_neighbor_force_down(on); ospf6_neighbor_delete(on); + } list_delete_all_node(oi->neighbor_list); @@ -1865,9 +1889,9 @@ void ospf6_interface_stop(struct ospf6_interface *oi) } /* interface variable set command */ -DEFUN (ipv6_ospf6_area, +DEFPY_YANG (ipv6_ospf6_area, ipv6_ospf6_area_cmd, - "ipv6 ospf6 area ", + "ipv6 ospf6 area $areaid", IP6_STR OSPF6_STR "Specify the OSPF6 area ID\n" @@ -1876,9 +1900,12 @@ DEFUN (ipv6_ospf6_area, { VTY_DECLVAR_CONTEXT(interface, ifp); struct ospf6_interface *oi; - int idx_ipv4 = 3; - uint32_t area_id; + struct ospf6 *o6; + uint32_t area_id_int; int format; + struct in_addr area_addr; + char area_id_str[INET_ADDRSTRLEN]; + char xpath[XPATH_MAXLEN]; assert(ifp); @@ -1893,12 +1920,31 @@ DEFUN (ipv6_ospf6_area, return CMD_SUCCESS; } - if (str2area_id(argv[idx_ipv4]->arg, &area_id, &format)) { - vty_out(vty, "Malformed Area-ID: %s\n", argv[idx_ipv4]->arg); + if (str2area_id(areaid, &area_id_int, &format)) { + vty_out(vty, "Malformed Area-ID: %s\n", areaid); return CMD_WARNING_CONFIG_FAILED; } - oi->area_id = area_id; + /* + * Route attachment through the ietf-ospf YANG + * areas/area/interfaces/interface list-create callback. v3 has no + * per-address override or per-instance form, so the only fallback + * is when no ospf6 instance is configured yet (which mgmtd would + * also fail to satisfy because the instance keys the xpath). + */ + o6 = ospf6_lookup_by_vrf_id(ifp->vrf->vrf_id); + if (o6) { + area_addr.s_addr = area_id_int; + inet_ntop(AF_INET, &area_addr, area_id_str, sizeof(area_id_str)); + snprintf(xpath, sizeof(xpath), + OSPF6D_IETF_ROUTING_PROTOCOL_XPATH + "/ietf-ospf:ospf/areas/area[area-id='%s']/interfaces/interface[name='%s']", + o6->name ? o6->name : VRF_DEFAULT_NAME, area_id_str, ifp->name); + nb_cli_enqueue_change(vty, xpath, NB_OP_CREATE, NULL); + return nb_cli_apply_changes(vty, NULL); + } + + oi->area_id = area_id_int; oi->area_id_format = format; ospf6_interface_start(oi); @@ -1906,9 +1952,9 @@ DEFUN (ipv6_ospf6_area, return CMD_SUCCESS; } -DEFUN (no_ipv6_ospf6_area, +DEFPY_YANG (no_ipv6_ospf6_area, no_ipv6_ospf6_area_cmd, - "no ipv6 ospf6 area []", + "no ipv6 ospf6 area [$areaid]", NO_STR IP6_STR OSPF6_STR @@ -1918,6 +1964,10 @@ DEFUN (no_ipv6_ospf6_area, { VTY_DECLVAR_CONTEXT(interface, ifp); struct ospf6_interface *oi; + struct ospf6 *o6; + struct in_addr area_addr; + char area_id_str[INET_ADDRSTRLEN]; + char xpath[XPATH_MAXLEN]; assert(ifp); @@ -1926,6 +1976,18 @@ DEFUN (no_ipv6_ospf6_area, oi = ospf6_interface_create(ifp); assert(oi); + if (oi->area) { + o6 = oi->area->ospf6; + area_addr.s_addr = oi->area->area_id; + inet_ntop(AF_INET, &area_addr, area_id_str, sizeof(area_id_str)); + snprintf(xpath, sizeof(xpath), + OSPF6D_IETF_ROUTING_PROTOCOL_XPATH + "/ietf-ospf:ospf/areas/area[area-id='%s']/interfaces/interface[name='%s']", + o6->name ? o6->name : VRF_DEFAULT_NAME, area_id_str, ifp->name); + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } + ospf6_interface_stop(oi); oi->area_id = 0; @@ -2036,44 +2098,76 @@ DEFUN (no_ipv6_ospf6_ifmtu, return CMD_SUCCESS; } -DEFUN (ipv6_ospf6_cost, +/* + * Per-interface CLI conversion for ospf6d. v3 commands don't take + * per-address overrides, so the YANG-dispatch branch only needs to + * check that the interface is in an ospf6 area; otherwise we fall back + * to direct mutation. Returns 0 on success and writes the xpath into + * buf; returns -1 if YANG is not applicable. + */ +int ospf6_per_iface_xpath(char *xpath, size_t size, const struct interface *ifp, const char *leaf) +{ + const struct ospf6_interface *oi; + const struct ospf6 *ospf6; + char area_id_str[INET_ADDRSTRLEN]; + struct in_addr addr; + int ret; + + if (!ifp) + return -1; + oi = (const struct ospf6_interface *)ifp->info; + if (!oi || !oi->area) + return -1; + ospf6 = oi->area->ospf6; + if (!ospf6) + return -1; + + addr.s_addr = oi->area->area_id; + inet_ntop(AF_INET, &addr, area_id_str, sizeof(area_id_str)); + ret = snprintf(xpath, size, + OSPF6D_IETF_ROUTING_PROTOCOL_XPATH + "/ietf-ospf:ospf/areas/area[area-id='%s']/interfaces/interface[name='%s']%s", + ospf6->name ? ospf6->name : VRF_DEFAULT_NAME, area_id_str, ifp->name, + leaf ? leaf : ""); + if (ret < 0 || (size_t)ret >= size) + return -1; + + return 0; +} + +DEFPY_YANG (ipv6_ospf6_cost, ipv6_ospf6_cost_cmd, - "ipv6 ospf6 cost (1-65535)", + "ipv6 ospf6 cost (1-65535)$cost", IP6_STR OSPF6_STR "Interface cost\n" "Outgoing metric of this interface\n") { VTY_DECLVAR_CONTEXT(interface, ifp); - int idx_number = 3; struct ospf6_interface *oi; - unsigned long int lcost; + char xpath[XPATH_MAXLEN]; assert(ifp); + if (ospf6_per_iface_xpath(xpath, sizeof(xpath), ifp, "/cost") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, cost_str); + return nb_cli_apply_changes(vty, NULL); + } + oi = (struct ospf6_interface *)ifp->info; - if (oi == NULL) + if (!oi) oi = ospf6_interface_create(ifp); assert(oi); - lcost = strtol(argv[idx_number]->arg, NULL, 10); - - if (lcost > UINT32_MAX) { - vty_out(vty, "Cost %ld is out of range\n", lcost); - return CMD_WARNING_CONFIG_FAILED; - } - SET_FLAG(oi->flag, OSPF6_INTERFACE_NOAUTOCOST); - if (oi->cost == lcost) + if (oi->cost == (uint32_t)cost) return CMD_SUCCESS; - - oi->cost = lcost; + oi->cost = cost; ospf6_interface_force_recalculate_cost(oi); - return CMD_SUCCESS; } -DEFUN (no_ipv6_ospf6_cost, +DEFPY_YANG (no_ipv6_ospf6_cost, no_ipv6_ospf6_cost_cmd, "no ipv6 ospf6 cost [(1-65535)]", NO_STR @@ -2084,53 +2178,49 @@ DEFUN (no_ipv6_ospf6_cost, { VTY_DECLVAR_CONTEXT(interface, ifp); struct ospf6_interface *oi; + char xpath[XPATH_MAXLEN]; + assert(ifp); + if (ospf6_per_iface_xpath(xpath, sizeof(xpath), ifp, "/cost") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } + oi = (struct ospf6_interface *)ifp->info; - if (oi == NULL) + if (!oi) oi = ospf6_interface_create(ifp); assert(oi); - UNSET_FLAG(oi->flag, OSPF6_INTERFACE_NOAUTOCOST); - ospf6_interface_recalculate_cost(oi); - return CMD_SUCCESS; } -DEFUN (auto_cost_reference_bandwidth, +/* + * `auto-cost reference-bandwidth` maps onto the RFC 9129 + * `/auto-cost/reference-bandwidth` leaf. The deviations file pins + * `/auto-cost/enabled` to `true` so the YANG when-clause is always + * satisfied; ospf6d's runtime semantics match (always compute cost + * from bandwidth). + */ +DEFPY_YANG (auto_cost_reference_bandwidth, auto_cost_reference_bandwidth_cmd, - "auto-cost reference-bandwidth (1-4294967)", + "auto-cost reference-bandwidth (1-4294967)$refbw", "Calculate OSPF interface cost according to bandwidth\n" "Use reference bandwidth method to assign OSPF cost\n" "The reference bandwidth in terms of Mbits per second\n") { VTY_DECLVAR_CONTEXT(ospf6, o); - int idx_number = 2; - struct ospf6_area *oa; - struct ospf6_interface *oi; - struct listnode *i, *j; - uint32_t refbw; + char xpath[XPATH_MAXLEN]; - refbw = strtol(argv[idx_number]->arg, NULL, 10); - if (refbw < 1 || refbw > 4294967) { - vty_out(vty, "reference-bandwidth value is invalid\n"); + if (ospf6_per_instance_xpath(xpath, sizeof(xpath), o, "/auto-cost/reference-bandwidth") != + 0) return CMD_WARNING_CONFIG_FAILED; - } - - /* If reference bandwidth is changed. */ - if ((refbw) == o->ref_bandwidth) - return CMD_SUCCESS; - - o->ref_bandwidth = refbw; - for (ALL_LIST_ELEMENTS_RO(o->area_list, i, oa)) - for (ALL_LIST_ELEMENTS_RO(oa->if_list, j, oi)) - ospf6_interface_recalculate_cost(oi); - - return CMD_SUCCESS; + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, refbw_str); + return nb_cli_apply_changes(vty, NULL); } -DEFUN (no_auto_cost_reference_bandwidth, +DEFPY_YANG (no_auto_cost_reference_bandwidth, no_auto_cost_reference_bandwidth_cmd, "no auto-cost reference-bandwidth [(1-4294967)]", NO_STR @@ -2139,19 +2229,13 @@ DEFUN (no_auto_cost_reference_bandwidth, "The reference bandwidth in terms of Mbits per second\n") { VTY_DECLVAR_CONTEXT(ospf6, o); - struct ospf6_area *oa; - struct ospf6_interface *oi; - struct listnode *i, *j; - - if (o->ref_bandwidth == OSPF6_REFERENCE_BANDWIDTH) - return CMD_SUCCESS; - - o->ref_bandwidth = OSPF6_REFERENCE_BANDWIDTH; - for (ALL_LIST_ELEMENTS_RO(o->area_list, i, oa)) - for (ALL_LIST_ELEMENTS_RO(oa->if_list, j, oi)) - ospf6_interface_recalculate_cost(oi); + char xpath[XPATH_MAXLEN]; - return CMD_SUCCESS; + if (ospf6_per_instance_xpath(xpath, sizeof(xpath), o, "/auto-cost/reference-bandwidth") != + 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); } @@ -2187,41 +2271,65 @@ DEFUN (no_ospf6_write_multiplier, return CMD_SUCCESS; } -DEFUN (ipv6_ospf6_hellointerval, +void ospf6_hello_reschedule(struct ospf6_interface *oi) +{ + if (event_is_scheduled(oi->thread_send_hello)) { + event_cancel(&oi->thread_send_hello); + event_add_timer(master, ospf6_hello_send, oi, 0, &oi->thread_send_hello); + } +} + +static bool ospf6_yang_default_timer_edit_is_noop(struct vty *vty, const char *xpath, + uint32_t current, uint32_t default_value) +{ + /* + * Interface timer commands can run while the interface is detached + * from OSPF6, in which case they update runtime-only legacy state. + * Once the interface is attached again, editing a YANG leaf back to + * its schema default is a candidate no-op when the leaf is already + * absent. No northbound callback will fire, so the CLI bridge must + * restore the runtime default itself. + */ + return current != default_value && !yang_dnode_exists(vty->candidate_config->dnode, xpath); +} + +DEFPY_YANG (ipv6_ospf6_hellointerval, ipv6_ospf6_hellointerval_cmd, - "ipv6 ospf6 hello-interval (1-65535)", + "ipv6 ospf6 hello-interval (1-65535)$interval", IP6_STR OSPF6_STR "Time between HELLO packets\n" SECONDS_STR) { VTY_DECLVAR_CONTEXT(interface, ifp); - int idx_number = 3; struct ospf6_interface *oi; + char xpath[XPATH_MAXLEN]; + assert(ifp); + if (ospf6_per_iface_xpath(xpath, sizeof(xpath), ifp, "/hello-interval") == 0) { + oi = (struct ospf6_interface *)ifp->info; + if (oi && interval == OSPF6_INTERFACE_HELLO_INTERVAL && + ospf6_yang_default_timer_edit_is_noop(vty, xpath, oi->hello_interval, + OSPF6_INTERFACE_HELLO_INTERVAL)) { + oi->hello_interval = OSPF6_INTERFACE_HELLO_INTERVAL; + ospf6_hello_reschedule(oi); + return CMD_SUCCESS; + } + + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, interval_str); + return nb_cli_apply_changes(vty, NULL); + } oi = (struct ospf6_interface *)ifp->info; if (oi == NULL) oi = ospf6_interface_create(ifp); assert(oi); - - oi->hello_interval = strmatch(argv[0]->text, "no") - ? OSPF_HELLO_INTERVAL_DEFAULT - : strtoul(argv[idx_number]->arg, NULL, 10); - - /* - * If the thread is scheduled, send the new hello now. - */ - if (event_is_scheduled(oi->thread_send_hello)) { - event_cancel(&oi->thread_send_hello); - - event_add_timer(master, ospf6_hello_send, oi, 0, - &oi->thread_send_hello); - } + oi->hello_interval = interval; + ospf6_hello_reschedule(oi); return CMD_SUCCESS; } -ALIAS (ipv6_ospf6_hellointerval, +DEFPY_YANG (no_ipv6_ospf6_hellointerval, no_ipv6_ospf6_hellointerval_cmd, "no ipv6 ospf6 hello-interval [(1-65535)]", NO_STR @@ -2229,33 +2337,88 @@ ALIAS (ipv6_ospf6_hellointerval, OSPF6_STR "Time between HELLO packets\n" SECONDS_STR) +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + struct ospf6_interface *oi; + char xpath[XPATH_MAXLEN]; -/* interface variable set command */ -DEFUN (ipv6_ospf6_deadinterval, + assert(ifp); + if (ospf6_per_iface_xpath(xpath, sizeof(xpath), ifp, "/hello-interval") == 0) { + oi = (struct ospf6_interface *)ifp->info; + if (oi && ospf6_yang_default_timer_edit_is_noop(vty, xpath, oi->hello_interval, + OSPF6_INTERFACE_HELLO_INTERVAL)) { + oi->hello_interval = OSPF6_INTERFACE_HELLO_INTERVAL; + ospf6_hello_reschedule(oi); + return CMD_SUCCESS; + } + + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } + + oi = (struct ospf6_interface *)ifp->info; + if (oi == NULL) + oi = ospf6_interface_create(ifp); + assert(oi); + oi->hello_interval = OSPF6_INTERFACE_HELLO_INTERVAL; + ospf6_hello_reschedule(oi); + return CMD_SUCCESS; +} + +DEFPY_YANG (ipv6_ospf6_deadinterval, ipv6_ospf6_deadinterval_cmd, - "ipv6 ospf6 dead-interval (1-65535)", + "ipv6 ospf6 dead-interval (1-65535)$interval", IP6_STR OSPF6_STR "Interval time after which a neighbor is declared down\n" SECONDS_STR) { VTY_DECLVAR_CONTEXT(interface, ifp); - int idx_number = 3; struct ospf6_interface *oi; + char xpath[XPATH_MAXLEN]; + char hello_xpath[XPATH_MAXLEN]; + char hello_buf[16]; + assert(ifp); + if (ospf6_per_iface_xpath(xpath, sizeof(xpath), ifp, "/dead-interval") == 0) { + oi = (struct ospf6_interface *)ifp->info; + if (oi && interval == OSPF6_INTERFACE_DEAD_INTERVAL && + ospf6_yang_default_timer_edit_is_noop(vty, xpath, oi->dead_interval, + OSPF6_INTERFACE_DEAD_INTERVAL)) { + if (oi->hello_interval != OSPF6_INTERFACE_HELLO_INTERVAL && + ospf6_per_iface_xpath(hello_xpath, sizeof(hello_xpath), ifp, + "/hello-interval") == 0 && + !yang_dnode_exists(vty->candidate_config->dnode, hello_xpath)) { + snprintf(hello_buf, sizeof(hello_buf), "%u", oi->hello_interval); + nb_cli_enqueue_change(vty, hello_xpath, NB_OP_MODIFY, hello_buf); + if (nb_cli_apply_changes(vty, NULL) != CMD_SUCCESS) + return CMD_WARNING_CONFIG_FAILED; + } + oi->dead_interval = OSPF6_INTERFACE_DEAD_INTERVAL; + return CMD_SUCCESS; + } + + if (oi && oi->hello_interval != OSPF6_INTERFACE_HELLO_INTERVAL && + oi->hello_interval < interval && + ospf6_per_iface_xpath(hello_xpath, sizeof(hello_xpath), ifp, + "/hello-interval") == 0 && + !yang_dnode_exists(vty->candidate_config->dnode, hello_xpath)) { + snprintf(hello_buf, sizeof(hello_buf), "%u", oi->hello_interval); + nb_cli_enqueue_change(vty, hello_xpath, NB_OP_MODIFY, hello_buf); + } + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, interval_str); + return nb_cli_apply_changes(vty, NULL); + } oi = (struct ospf6_interface *)ifp->info; if (oi == NULL) oi = ospf6_interface_create(ifp); assert(oi); - - oi->dead_interval = strmatch(argv[0]->arg, "no") - ? OSPF_ROUTER_DEAD_INTERVAL_DEFAULT - : strtoul(argv[idx_number]->arg, NULL, 10); + oi->dead_interval = interval; return CMD_SUCCESS; } -ALIAS (ipv6_ospf6_deadinterval, +DEFPY_YANG (no_ipv6_ospf6_deadinterval, no_ipv6_ospf6_deadinterval_cmd, "no ipv6 ospf6 dead-interval [(1-65535)]", NO_STR @@ -2263,6 +2426,31 @@ ALIAS (ipv6_ospf6_deadinterval, OSPF6_STR "Interval time after which a neighbor is declared down\n" SECONDS_STR) +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + struct ospf6_interface *oi; + char xpath[XPATH_MAXLEN]; + + assert(ifp); + if (ospf6_per_iface_xpath(xpath, sizeof(xpath), ifp, "/dead-interval") == 0) { + oi = (struct ospf6_interface *)ifp->info; + if (oi && ospf6_yang_default_timer_edit_is_noop(vty, xpath, oi->dead_interval, + OSPF6_INTERFACE_DEAD_INTERVAL)) { + oi->dead_interval = OSPF6_INTERFACE_DEAD_INTERVAL; + return CMD_SUCCESS; + } + + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } + + oi = (struct ospf6_interface *)ifp->info; + if (oi == NULL) + oi = ospf6_interface_create(ifp); + assert(oi); + oi->dead_interval = OSPF6_INTERFACE_DEAD_INTERVAL; + return CMD_SUCCESS; +} DEFPY(ipv6_ospf6_gr_hdelay, ipv6_ospf6_gr_hdelay_cmd, @@ -2311,31 +2499,33 @@ DEFPY(no_ipv6_ospf6_gr_hdelay, } /* interface variable set command */ -DEFUN (ipv6_ospf6_transmitdelay, +DEFPY_YANG (ipv6_ospf6_transmitdelay, ipv6_ospf6_transmitdelay_cmd, - "ipv6 ospf6 transmit-delay (1-3600)", + "ipv6 ospf6 transmit-delay (1-3600)$interval", IP6_STR OSPF6_STR "Link state transmit delay\n" SECONDS_STR) { VTY_DECLVAR_CONTEXT(interface, ifp); - int idx_number = 3; struct ospf6_interface *oi; + char xpath[XPATH_MAXLEN]; + assert(ifp); + if (ospf6_per_iface_xpath(xpath, sizeof(xpath), ifp, "/transmit-delay") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, interval_str); + return nb_cli_apply_changes(vty, NULL); + } oi = (struct ospf6_interface *)ifp->info; if (oi == NULL) oi = ospf6_interface_create(ifp); assert(oi); - - oi->transdelay = strmatch(argv[0]->text, "no") - ? OSPF6_INTERFACE_TRANSDELAY - : strtoul(argv[idx_number]->arg, NULL, 10); + oi->transdelay = interval; return CMD_SUCCESS; } -ALIAS (ipv6_ospf6_transmitdelay, +DEFPY_YANG (no_ipv6_ospf6_transmitdelay, no_ipv6_ospf6_transmitdelay_cmd, "no ipv6 ospf6 transmit-delay [(1-3600)]", NO_STR @@ -2343,33 +2533,53 @@ ALIAS (ipv6_ospf6_transmitdelay, OSPF6_STR "Link state transmit delay\n" SECONDS_STR) +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + struct ospf6_interface *oi; + char xpath[XPATH_MAXLEN]; + + assert(ifp); + if (ospf6_per_iface_xpath(xpath, sizeof(xpath), ifp, "/transmit-delay") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } + + oi = (struct ospf6_interface *)ifp->info; + if (oi == NULL) + oi = ospf6_interface_create(ifp); + assert(oi); + oi->transdelay = OSPF6_INTERFACE_TRANSDELAY; + return CMD_SUCCESS; +} /* interface variable set command */ -DEFUN (ipv6_ospf6_retransmitinterval, +DEFPY_YANG (ipv6_ospf6_retransmitinterval, ipv6_ospf6_retransmitinterval_cmd, - "ipv6 ospf6 retransmit-interval (1-65535)", + "ipv6 ospf6 retransmit-interval (1-65535)$interval", IP6_STR OSPF6_STR "Time between retransmitting lost link state advertisements\n" SECONDS_STR) { VTY_DECLVAR_CONTEXT(interface, ifp); - int idx_number = 3; struct ospf6_interface *oi; + char xpath[XPATH_MAXLEN]; + assert(ifp); + if (ospf6_per_iface_xpath(xpath, sizeof(xpath), ifp, "/retransmit-interval") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, interval_str); + return nb_cli_apply_changes(vty, NULL); + } oi = (struct ospf6_interface *)ifp->info; if (oi == NULL) oi = ospf6_interface_create(ifp); assert(oi); - - oi->rxmt_interval = strmatch(argv[0]->text, "no") - ? OSPF_RETRANSMIT_INTERVAL_DEFAULT - : strtoul(argv[idx_number]->arg, NULL, 10); + oi->rxmt_interval = interval; return CMD_SUCCESS; } -ALIAS (ipv6_ospf6_retransmitinterval, +DEFPY_YANG (no_ipv6_ospf6_retransmitinterval, no_ipv6_ospf6_retransmitinterval_cmd, "no ipv6 ospf6 retransmit-interval [(1-65535)]", NO_STR @@ -2377,41 +2587,62 @@ ALIAS (ipv6_ospf6_retransmitinterval, OSPF6_STR "Time between retransmitting lost link state advertisements\n" SECONDS_STR) - -/* interface variable set command */ -DEFUN (ipv6_ospf6_priority, - ipv6_ospf6_priority_cmd, - "ipv6 ospf6 priority (0-255)", - IP6_STR - OSPF6_STR - "Router priority\n" - "Priority value\n") { VTY_DECLVAR_CONTEXT(interface, ifp); - int idx_number = 3; struct ospf6_interface *oi; + char xpath[XPATH_MAXLEN]; + assert(ifp); + if (ospf6_per_iface_xpath(xpath, sizeof(xpath), ifp, "/retransmit-interval") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } oi = (struct ospf6_interface *)ifp->info; if (oi == NULL) oi = ospf6_interface_create(ifp); assert(oi); + oi->rxmt_interval = OSPF_RETRANSMIT_INTERVAL_DEFAULT; + return CMD_SUCCESS; +} - oi->priority = strmatch(argv[0]->text, "no") - ? OSPF6_INTERFACE_PRIORITY - : strtoul(argv[idx_number]->arg, NULL, 10); - +void ospf6_priority_recompute(struct ospf6_interface *oi) +{ if (oi->area && (oi->state == OSPF6_INTERFACE_DROTHER || - oi->state == OSPF6_INTERFACE_BDR || - oi->state == OSPF6_INTERFACE_DR)) { + oi->state == OSPF6_INTERFACE_BDR || oi->state == OSPF6_INTERFACE_DR)) { if (ospf6_interface_state_change(dr_election(oi), oi) == -1) OSPF6_LINK_LSA_SCHEDULE(oi); } +} + +DEFPY_YANG (ipv6_ospf6_priority, + ipv6_ospf6_priority_cmd, + "ipv6 ospf6 priority (0-255)$priority", + IP6_STR + OSPF6_STR + "Router priority\n" + "Priority value\n") +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + struct ospf6_interface *oi; + char xpath[XPATH_MAXLEN]; + + assert(ifp); + if (ospf6_per_iface_xpath(xpath, sizeof(xpath), ifp, "/priority") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, priority_str); + return nb_cli_apply_changes(vty, NULL); + } + oi = (struct ospf6_interface *)ifp->info; + if (oi == NULL) + oi = ospf6_interface_create(ifp); + assert(oi); + oi->priority = priority; + ospf6_priority_recompute(oi); return CMD_SUCCESS; } -ALIAS (ipv6_ospf6_priority, +DEFPY_YANG (no_ipv6_ospf6_priority, no_ipv6_ospf6_priority_cmd, "no ipv6 ospf6 priority [(0-255)]", NO_STR @@ -2419,6 +2650,25 @@ ALIAS (ipv6_ospf6_priority, OSPF6_STR "Router priority\n" "Priority value\n") +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + struct ospf6_interface *oi; + char xpath[XPATH_MAXLEN]; + + assert(ifp); + if (ospf6_per_iface_xpath(xpath, sizeof(xpath), ifp, "/priority") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } + + oi = (struct ospf6_interface *)ifp->info; + if (oi == NULL) + oi = ospf6_interface_create(ifp); + assert(oi); + oi->priority = OSPF6_INTERFACE_PRIORITY; + ospf6_priority_recompute(oi); + return CMD_SUCCESS; +} DEFUN (ipv6_ospf6_instance, ipv6_ospf6_instance_cmd, @@ -2453,7 +2703,7 @@ ALIAS (ipv6_ospf6_instance, "Instance ID for this interface\n" "Instance ID value\n") -DEFUN (ipv6_ospf6_passive, +DEFPY_YANG (ipv6_ospf6_passive, ipv6_ospf6_passive_cmd, "ipv6 ospf6 passive", IP6_STR @@ -2465,8 +2715,13 @@ DEFUN (ipv6_ospf6_passive, struct ospf6_interface *oi; struct listnode *node, *nnode; struct ospf6_neighbor *on; + char xpath[XPATH_MAXLEN]; assert(ifp); + if (ospf6_per_iface_xpath(xpath, sizeof(xpath), ifp, "/passive") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "true"); + return nb_cli_apply_changes(vty, NULL); + } oi = (struct ospf6_interface *)ifp->info; if (oi == NULL) @@ -2485,7 +2740,7 @@ DEFUN (ipv6_ospf6_passive, return CMD_SUCCESS; } -DEFUN (no_ipv6_ospf6_passive, +DEFPY_YANG (no_ipv6_ospf6_passive, no_ipv6_ospf6_passive_cmd, "no ipv6 ospf6 passive", NO_STR @@ -2496,7 +2751,13 @@ DEFUN (no_ipv6_ospf6_passive, { VTY_DECLVAR_CONTEXT(interface, ifp); struct ospf6_interface *oi; + char xpath[XPATH_MAXLEN]; + assert(ifp); + if (ospf6_per_iface_xpath(xpath, sizeof(xpath), ifp, "/passive") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } oi = (struct ospf6_interface *)ifp->info; if (oi == NULL) @@ -2515,7 +2776,7 @@ DEFUN (no_ipv6_ospf6_passive, return CMD_SUCCESS; } -DEFUN (ipv6_ospf6_mtu_ignore, +DEFPY_YANG (ipv6_ospf6_mtu_ignore, ipv6_ospf6_mtu_ignore_cmd, "ipv6 ospf6 mtu-ignore", IP6_STR @@ -2525,19 +2786,23 @@ DEFUN (ipv6_ospf6_mtu_ignore, { VTY_DECLVAR_CONTEXT(interface, ifp); struct ospf6_interface *oi; + char xpath[XPATH_MAXLEN]; + assert(ifp); + if (ospf6_per_iface_xpath(xpath, sizeof(xpath), ifp, "/mtu-ignore") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "true"); + return nb_cli_apply_changes(vty, NULL); + } oi = (struct ospf6_interface *)ifp->info; if (oi == NULL) oi = ospf6_interface_create(ifp); assert(oi); - oi->mtu_ignore = 1; - return CMD_SUCCESS; } -DEFUN (no_ipv6_ospf6_mtu_ignore, +DEFPY_YANG (no_ipv6_ospf6_mtu_ignore, no_ipv6_ospf6_mtu_ignore_cmd, "no ipv6 ospf6 mtu-ignore", NO_STR @@ -2548,15 +2813,19 @@ DEFUN (no_ipv6_ospf6_mtu_ignore, { VTY_DECLVAR_CONTEXT(interface, ifp); struct ospf6_interface *oi; + char xpath[XPATH_MAXLEN]; + assert(ifp); + if (ospf6_per_iface_xpath(xpath, sizeof(xpath), ifp, "/mtu-ignore") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } oi = (struct ospf6_interface *)ifp->info; if (oi == NULL) oi = ospf6_interface_create(ifp); assert(oi); - oi->mtu_ignore = 0; - return CMD_SUCCESS; } @@ -2633,9 +2902,9 @@ DEFUN(no_ipv6_ospf6_advertise_prefix_list, return CMD_SUCCESS; } -DEFUN (ipv6_ospf6_network, +DEFPY_YANG (ipv6_ospf6_network, ipv6_ospf6_network_cmd, - "ipv6 ospf6 network ", + "ipv6 ospf6 network $network", IP6_STR OSPF6_STR "Network type\n" @@ -2645,29 +2914,34 @@ DEFUN (ipv6_ospf6_network, ) { VTY_DECLVAR_CONTEXT(interface, ifp); - int idx_network = 3; struct ospf6_interface *oi; + char xpath[XPATH_MAXLEN]; + assert(ifp); + if (ospf6_per_iface_xpath(xpath, sizeof(xpath), ifp, "/interface-type") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, network); + return nb_cli_apply_changes(vty, NULL); + } + oi = (struct ospf6_interface *)ifp->info; - if (oi == NULL) { + if (oi == NULL) oi = ospf6_interface_create(ifp); - } assert(oi); oi->type_cfg = true; - if (strncmp(argv[idx_network]->arg, "b", 1) == 0) { + if (strncmp(network, "b", 1) == 0) { if (oi->type == OSPF_IFTYPE_BROADCAST) return CMD_SUCCESS; oi->type = OSPF_IFTYPE_BROADCAST; - } else if (strncmp(argv[idx_network]->arg, "point-to-p", 10) == 0) { + } else if (strncmp(network, "point-to-p", 10) == 0) { if (oi->type == OSPF_IFTYPE_POINTOPOINT) { return CMD_SUCCESS; } oi->type = OSPF_IFTYPE_POINTOPOINT; - } else if (strncmp(argv[idx_network]->arg, "point-to-m", 10) == 0) { + } else if (strncmp(network, "point-to-m", 10) == 0) { if (oi->type == OSPF_IFTYPE_POINTOMULTIPOINT) { return CMD_SUCCESS; } @@ -2681,7 +2955,7 @@ DEFUN (ipv6_ospf6_network, return CMD_SUCCESS; } -DEFUN (no_ipv6_ospf6_network, +DEFPY_YANG (no_ipv6_ospf6_network, no_ipv6_ospf6_network_cmd, "no ipv6 ospf6 network []", NO_STR @@ -2695,9 +2969,15 @@ DEFUN (no_ipv6_ospf6_network, VTY_DECLVAR_CONTEXT(interface, ifp); struct ospf6_interface *oi; int type; + char xpath[XPATH_MAXLEN]; assert(ifp); + if (ospf6_per_iface_xpath(xpath, sizeof(xpath), ifp, "/interface-type") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } + oi = (struct ospf6_interface *)ifp->info; if (oi == NULL) return CMD_SUCCESS; diff --git a/ospf6d/ospf6_interface.h b/ospf6d/ospf6_interface.h index 88eeded3390b..8d5042618a77 100644 --- a/ospf6d/ospf6_interface.h +++ b/ospf6d/ospf6_interface.h @@ -12,6 +12,7 @@ #include "ospf6d.h" DECLARE_MTYPE(OSPF6_AUTH_MANUAL_KEY); +DECLARE_MTYPE(OSPF6_AUTH_KEYCHAIN); /* Debug option */ extern unsigned char conf_debug_ospf6_interface; @@ -239,6 +240,10 @@ extern const char *const ospf6_interface_state_str[]; extern void ospf6_interface_start(struct ospf6_interface *oi); extern void ospf6_interface_stop(struct ospf6_interface *oi); +extern void ospf6_interface_force_recalculate_cost(struct ospf6_interface *oi); +extern void ospf6_interface_recalculate_cost(struct ospf6_interface *oi); +extern void ospf6_hello_reschedule(struct ospf6_interface *oi); +extern void ospf6_priority_recompute(struct ospf6_interface *oi); extern struct ospf6_interface * ospf6_interface_lookup_by_ifindex(ifindex_t, vrf_id_t vrf_id); @@ -257,6 +262,7 @@ ospf6_interface_get_global_address(struct interface *ifp); /* interface event */ extern void interface_up(struct event *event); extern void interface_down(struct event *event); +extern uint8_t ospf6_default_iftype(struct interface *ifp); extern void wait_timer(struct event *event); extern void backup_seen(struct event *event); extern void neighbor_change(struct event *event); @@ -274,6 +280,15 @@ extern uint8_t dr_election(struct ospf6_interface *oi); extern void ospf6_interface_auth_trailer_cmd_init(void); extern void ospf6_auth_write_config(struct vty *vty, struct ospf6_auth_data *at_data); + +/* + * Build the per-interface RFC 9129 ietf-ospf xpath for `leaf` (must + * start with '/'). Returns 0 on success, -1 when the interface is + * not yet bound to an OSPFv3 area -- callers should fall back to + * direct mutation in that case. + */ +extern int ospf6_per_iface_xpath(char *xpath, size_t size, const struct interface *ifp, + const char *leaf); DECLARE_HOOK(ospf6_interface_change, (struct ospf6_interface * oi, int state, int old_state), (oi, state, old_state)); diff --git a/ospf6d/ospf6_main.c b/ospf6d/ospf6_main.c index 1ae55557b843..215df7714f61 100644 --- a/ospf6d/ospf6_main.c +++ b/ospf6d/ospf6_main.c @@ -25,6 +25,7 @@ #include "bfd.h" #include "libfrr.h" #include "libagentx.h" +#include "mgmt_be_client.h" #include "ospf6d.h" #include "ospf6_top.h" @@ -34,6 +35,7 @@ #include "ospf6_lsa.h" #include "ospf6_interface.h" #include "ospf6_zebra.h" +#include "ospf6_nb.h" #include "ospf6_routemap_nb.h" /* Default configuration file name for ospf6d. */ @@ -69,6 +71,7 @@ struct option longopts[] = {{0}}; /* Master of threads. */ struct event_loop *master; +static struct mgmt_be_client *mgmt_be_client; static void __attribute__((noreturn)) ospf6_exit(int status) { @@ -80,6 +83,7 @@ static void __attribute__((noreturn)) ospf6_exit(int status) frr_early_fini(); bfd_protocol_integration_set_shutdown(true); + mgmt_be_client_destroy(mgmt_be_client); for (ALL_LIST_ELEMENTS(om6->ospf6, node, nnode, ospf6)) { vrf = vrf_lookup_by_id(ospf6->vrf_id); @@ -168,12 +172,50 @@ static const struct frr_yang_module_info *const ospf6d_yang_modules[] = { &frr_interface_info, &frr_route_map_info, &frr_vrf_info, + &ietf_bfd_types_info, + &ospf6d_ietf_routing_info, + &ospf6d_ietf_ospf_info, + &ospf6d_ietf_routing_ospf_deviation_info, &frr_ospf_route_map_info, &frr_ospf6_route_map_info, &ietf_key_chain_info, &ietf_key_chain_deviation_info, }; +/* + * ospfd and ospf6d both register the RFC 9129 ietf-ospf control-plane-protocol + * subtree. Filter on the `type` list-key so mgmtd dispatches each change to + * the daemon that owns that protocol family. See the predicate-aware matching + * in mgmtd/mgmt_be_adapter.c::mgmt_be_xpath_prefix(). + */ +static const char *const ospf6d_oper_xpaths[] = { + OSPF6D_IETF_ROUTING_PROTOCOL_TYPE_XPATH, +}; + +static const char *const ospf6d_config_xpaths[] = { + OSPF6D_IETF_ROUTING_PROTOCOL_TYPE_XPATH, +}; + +/* + * RFC 9129 places clear-neighbor / clear-database at the module root rather + * than under control-plane-protocol, so both daemons register the same xpaths + * and mgmtd fans the call out. The handler for the daemon that doesn't own + * the named instance returns NB_OK silently. + */ +static const char *const ospf6d_rpc_xpaths[] = { + "/ietf-ospf:clear-neighbor", + "/ietf-ospf:clear-database", +}; + +struct mgmt_be_client_cbs ospf6d_be_client_data = { + .config_xpaths = ospf6d_config_xpaths, + .nconfig_xpaths = array_size(ospf6d_config_xpaths), + .oper_xpaths = ospf6d_oper_xpaths, + .noper_xpaths = array_size(ospf6d_oper_xpaths), + .rpc_xpaths = ospf6d_rpc_xpaths, + .nrpc_xpaths = array_size(ospf6d_rpc_xpaths), +}; + /* actual paths filled in main() */ static char state_path[512]; static char state_compat_path[512]; @@ -271,11 +313,14 @@ int main(int argc, char *argv[], char *envp[]) prefix_list_init(); /* initialize ospf6 */ + cmd_config_file_batching_set(true); ospf6_init(master); /* Configuration processing callback initialization. */ cmd_init_config_callbacks(ospf6_config_start, ospf6_config_end); + mgmt_be_client = mgmt_be_client_create("ospf6d", &ospf6d_be_client_data, 0, master); + frr_config_fork(); frr_run(master); diff --git a/ospf6d/ospf6_message.c b/ospf6d/ospf6_message.c index 0701b2e1c004..d9a9915cf626 100644 --- a/ospf6d/ospf6_message.c +++ b/ospf6d/ospf6_message.c @@ -39,6 +39,7 @@ #include "lib/libospf.h" #include "lib/keychain.h" #include "ospf6_auth_trailer.h" +#include "ospf6_nb.h" DEFINE_MTYPE_STATIC(OSPF6D, OSPF6_MESSAGE, "OSPF6 message"); DEFINE_MTYPE_STATIC(OSPF6D, OSPF6_PACKET, "OSPF6 packet"); @@ -434,6 +435,7 @@ static void ospf6_hello_recv(struct in6_addr *src, struct in6_addr *dst, oi->interface->vrf->name, oi->interface->name, src, &oh->router_id, oi->hello_interval, ntohs(hello->hello_interval)); + ospf6d_ietf_notif_if_config_error(oi, *src, oh->type, "hello-interval-mismatch"); return; } @@ -444,6 +446,7 @@ static void ospf6_hello_recv(struct in6_addr *src, struct in6_addr *dst, oi->interface->vrf->name, oi->interface->name, src, &oh->router_id, oi->dead_interval, ntohs(hello->dead_interval)); + ospf6d_ietf_notif_if_config_error(oi, *src, oh->type, "dead-interval-mismatch"); return; } @@ -1883,8 +1886,10 @@ static int ospf6_read_helper(int sockfd, struct ospf6 *ospf6) return OSPF6_READ_CONTINUE; } - if (ospf6_rxpacket_examin(oi, oh, len) != MSG_OK) + if (ospf6_rxpacket_examin(oi, oh, len) != MSG_OK) { + ospf6d_ietf_notif_if_rx_bad_packet(oi, src, oh->type); return OSPF6_READ_CONTINUE; + } /* Being here means, that no sizing/alignment issues were detected in the input packet. This renders the additional checks performed below diff --git a/ospf6d/ospf6_nb.c b/ospf6d/ospf6_nb.c new file mode 100644 index 000000000000..2e3992e73770 --- /dev/null +++ b/ospf6d/ospf6_nb.c @@ -0,0 +1,468 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * OSPFv3 northbound interface. + * Copyright (C) 2026 Eric Parsonage + */ + +#include + +#include "vrf.h" + +#include "ospf6_nb.h" +#include "ospf6_top.h" + +/* clang-format off */ +const struct frr_yang_module_info ospf6d_ietf_routing_info = { + .name = "ietf-routing", + .ignore_cfg_cbs = true, + .nodes = { + { + .xpath = OSPF6D_IETF_ROUTING_CP_XPATH, + .cbs = { + .create = ospf6d_ietf_routing_control_plane_protocol_create, + .destroy = ospf6d_ietf_routing_control_plane_protocol_destroy, + .get_next = ospf6d_ietf_routing_control_plane_protocol_get_next, + .get_keys = ospf6d_ietf_routing_control_plane_protocol_get_keys, + .lookup_entry = + ospf6d_ietf_routing_control_plane_protocol_lookup_entry, + }, + .cfg_opt_in = true, + }, + { + .xpath = NULL, + }, + }, +}; + +const struct frr_yang_module_info ospf6d_ietf_routing_ospf_deviation_info = { + .name = "frr-deviations-ietf-routing-ospf", + .ignore_cfg_cbs = true, + .nodes = { + { + .xpath = NULL, + }, + }, +}; + +static const char * const ospf6d_ietf_ospf_features[] = { + "auto-cost", + "bfd", + "explicit-router-id", + "graceful-restart", + "key-chain", + "max-ecmp", + "mtu-ignore", + "ospfv3-authentication-trailer", + NULL, +}; + +const char *ospf6d_ietf_ospf_instance_name(const struct ospf6 *ospf6) +{ + return ospf6->name ? ospf6->name : VRF_DEFAULT_NAME; +} + +const struct frr_yang_module_info ospf6d_ietf_ospf_info = { + .name = "ietf-ospf", + .features = (const char **)ospf6d_ietf_ospf_features, + .ignore_cfg_cbs = true, + .nodes = { + { + .xpath = OSPF6D_IETF_OSPF_XPATH "/router-id", + .cbs = { + .get_elem = ospf6d_ietf_ospf_router_id_get_elem, + }, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH "/explicit-router-id", + .cbs = { + .modify = ospf6d_ietf_ospf_explicit_router_id_modify, + .destroy = ospf6d_ietf_ospf_explicit_router_id_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH "/preference/all", + .cbs = { + .modify = ospf6d_ietf_ospf_preference_all_modify, + .destroy = ospf6d_ietf_ospf_preference_all_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH "/preference/intra-area", + .cbs = { + .modify = ospf6d_ietf_ospf_preference_intra_area_modify, + .destroy = ospf6d_ietf_ospf_preference_intra_area_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH "/preference/inter-area", + .cbs = { + .modify = ospf6d_ietf_ospf_preference_inter_area_modify, + .destroy = ospf6d_ietf_ospf_preference_inter_area_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH "/preference/internal", + .cbs = { + .modify = ospf6d_ietf_ospf_preference_internal_modify, + .destroy = ospf6d_ietf_ospf_preference_internal_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH "/preference/external", + .cbs = { + .modify = ospf6d_ietf_ospf_preference_external_modify, + .destroy = ospf6d_ietf_ospf_preference_external_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH "/spf-control/paths", + .cbs = { + .modify = ospf6d_ietf_ospf_spf_control_paths_modify, + .destroy = ospf6d_ietf_ospf_spf_control_paths_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH "/auto-cost/enabled", + .cbs = { + .modify = ospf6d_ietf_ospf_auto_cost_enabled_modify, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH "/auto-cost/reference-bandwidth", + .cbs = { + .modify = ospf6d_ietf_ospf_auto_cost_reference_bandwidth_modify, + .destroy = ospf6d_ietf_ospf_auto_cost_reference_bandwidth_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH "/graceful-restart/enabled", + .cbs = { + .modify = ospf6d_ietf_ospf_graceful_restart_enabled_modify, + .destroy = ospf6d_ietf_ospf_graceful_restart_enabled_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH "/graceful-restart/restart-interval", + .cbs = { + .modify = ospf6d_ietf_ospf_graceful_restart_restart_interval_modify, + .destroy = ospf6d_ietf_ospf_graceful_restart_restart_interval_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH "/graceful-restart/helper-enabled", + .cbs = { + .modify = ospf6d_ietf_ospf_graceful_restart_helper_enabled_modify, + .destroy = ospf6d_ietf_ospf_graceful_restart_helper_enabled_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH "/graceful-restart/helper-strict-lsa-checking", + .cbs = { + .modify = ospf6d_ietf_ospf_graceful_restart_helper_strict_lsa_checking_modify, + .destroy = ospf6d_ietf_ospf_graceful_restart_helper_strict_lsa_checking_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/statistics/originate-new-lsa-count", + .cbs = { + .get_elem = + ospf6d_ietf_ospf_statistics_originate_new_lsa_count_get_elem, + }, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/statistics/rx-new-lsas-count", + .cbs = { + .get_elem = ospf6d_ietf_ospf_statistics_rx_new_lsas_count_get_elem, + }, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH "/areas/area", + .cbs = { + .create = ospf6d_ietf_ospf_areas_area_create, + .destroy = ospf6d_ietf_ospf_areas_area_destroy, + .get_next = ospf6d_ietf_ospf_areas_area_get_next, + .get_keys = ospf6d_ietf_ospf_areas_area_get_keys, + .lookup_entry = ospf6d_ietf_ospf_areas_area_lookup_entry, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH "/areas/area/area-type", + .cbs = { + .modify = ospf6d_ietf_ospf_areas_area_type_modify, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH "/areas/area/summary", + .cbs = { + .modify = ospf6d_ietf_ospf_areas_area_summary_modify, + .destroy = ospf6d_ietf_ospf_areas_area_summary_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/statistics/spf-runs-count", + .cbs = { + .get_elem = + ospf6d_ietf_ospf_areas_area_statistics_spf_runs_count_get_elem, + }, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/statistics/abr-count", + .cbs = { + .get_elem = + ospf6d_ietf_ospf_areas_area_statistics_abr_count_get_elem, + }, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/statistics/asbr-count", + .cbs = { + .get_elem = + ospf6d_ietf_ospf_areas_area_statistics_asbr_count_get_elem, + }, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/statistics/area-scope-lsa-count", + .cbs = { + .get_elem = + ospf6d_ietf_ospf_areas_area_statistics_area_scope_lsa_count_get_elem, + }, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/interfaces/interface", + .cbs = { + .create = ospf6d_ietf_ospf_areas_area_interfaces_interface_create, + .destroy = ospf6d_ietf_ospf_areas_area_interfaces_interface_destroy, + .get_next = ospf6d_ietf_ospf_areas_area_interfaces_interface_get_next, + .get_keys = ospf6d_ietf_ospf_areas_area_interfaces_interface_get_keys, + .lookup_entry = + ospf6d_ietf_ospf_areas_area_interfaces_interface_lookup_entry, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/cost", + .cbs = { + .modify = ospf6d_ietf_ospf_areas_area_interfaces_interface_cost_modify, + .destroy = ospf6d_ietf_ospf_areas_area_interfaces_interface_cost_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/hello-interval", + .cbs = { + .modify = ospf6d_ietf_ospf_areas_area_interfaces_interface_hello_interval_modify, + .destroy = ospf6d_ietf_ospf_areas_area_interfaces_interface_hello_interval_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/dead-interval", + .cbs = { + .modify = ospf6d_ietf_ospf_areas_area_interfaces_interface_dead_interval_modify, + .destroy = ospf6d_ietf_ospf_areas_area_interfaces_interface_dead_interval_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/retransmit-interval", + .cbs = { + .modify = ospf6d_ietf_ospf_areas_area_interfaces_interface_retransmit_interval_modify, + .destroy = ospf6d_ietf_ospf_areas_area_interfaces_interface_retransmit_interval_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/priority", + .cbs = { + .modify = ospf6d_ietf_ospf_areas_area_interfaces_interface_priority_modify, + .destroy = ospf6d_ietf_ospf_areas_area_interfaces_interface_priority_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/mtu-ignore", + .cbs = { + .modify = ospf6d_ietf_ospf_areas_area_interfaces_interface_mtu_ignore_modify, + .destroy = ospf6d_ietf_ospf_areas_area_interfaces_interface_mtu_ignore_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/transmit-delay", + .cbs = { + .modify = ospf6d_ietf_ospf_areas_area_interfaces_interface_transmit_delay_modify, + .destroy = ospf6d_ietf_ospf_areas_area_interfaces_interface_transmit_delay_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/interface-type", + .cbs = { + .modify = ospf6d_ietf_ospf_areas_area_interfaces_interface_interface_type_modify, + .destroy = ospf6d_ietf_ospf_areas_area_interfaces_interface_interface_type_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/passive", + .cbs = { + .modify = ospf6d_ietf_ospf_areas_area_interfaces_interface_passive_modify, + .destroy = ospf6d_ietf_ospf_areas_area_interfaces_interface_passive_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/bfd", + .cbs = { + .apply_finish = ospf6d_ietf_ospf_areas_area_interfaces_interface_bfd_apply_finish, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/bfd/enabled", + .cbs = { + .modify = ospf6d_ietf_ospf_areas_area_interfaces_interface_bfd_enabled_modify, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/bfd/local-multiplier", + .cbs = { + .modify = ospf6d_ietf_ospf_areas_area_interfaces_interface_bfd_local_multiplier_modify, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/bfd/desired-min-tx-interval", + .cbs = { + .modify = ospf6d_ietf_ospf_areas_area_interfaces_interface_bfd_desired_min_tx_interval_modify, + .destroy = ospf6d_ietf_ospf_areas_area_interfaces_interface_bfd_desired_min_tx_interval_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/bfd/required-min-rx-interval", + .cbs = { + .modify = ospf6d_ietf_ospf_areas_area_interfaces_interface_bfd_required_min_rx_interval_modify, + .destroy = ospf6d_ietf_ospf_areas_area_interfaces_interface_bfd_required_min_rx_interval_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/authentication/ospfv3-key-chain", + .cbs = { + .modify = ospf6d_ietf_ospf_areas_area_interfaces_interface_authentication_ospfv3_key_chain_modify, + .destroy = ospf6d_ietf_ospf_areas_area_interfaces_interface_authentication_ospfv3_key_chain_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/ranges/range", + .cbs = { + .create = ospf6d_ietf_ospf_areas_area_ranges_range_create, + .destroy = ospf6d_ietf_ospf_areas_area_ranges_range_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/ranges/range/advertise", + .cbs = { + .modify = ospf6d_ietf_ospf_areas_area_ranges_range_advertise_modify, + .destroy = ospf6d_ietf_ospf_areas_area_ranges_range_advertise_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/ranges/range/cost", + .cbs = { + .modify = ospf6d_ietf_ospf_areas_area_ranges_range_cost_modify, + .destroy = ospf6d_ietf_ospf_areas_area_ranges_range_cost_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/neighbors/neighbor", + .cbs = { + .get_next = + ospf6d_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_get_next, + .get_keys = + ospf6d_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_get_keys, + .lookup_entry = + ospf6d_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_lookup_entry, + }, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/neighbors/neighbor/address", + .cbs = { + .get_elem = + ospf6d_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_address_get_elem, + }, + }, + { + .xpath = OSPF6D_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/neighbors/neighbor/state", + .cbs = { + .get_elem = + ospf6d_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_state_get_elem, + }, + }, + { + .xpath = "/ietf-ospf:clear-neighbor", + .cbs = { + .rpc = ospf6d_ietf_ospf_clear_neighbor_rpc, + }, + }, + { + .xpath = "/ietf-ospf:clear-database", + .cbs = { + .rpc = ospf6d_ietf_ospf_clear_database_rpc, + }, + }, + { + .xpath = NULL, + }, + }, +}; +/* clang-format on */ diff --git a/ospf6d/ospf6_nb.h b/ospf6d/ospf6_nb.h new file mode 100644 index 000000000000..e5d835e9a984 --- /dev/null +++ b/ospf6d/ospf6_nb.h @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * OSPFv3 northbound interface. + * Copyright (C) 2026 Eric Parsonage + */ + +#ifndef FRR_OSPF6_NB_H +#define FRR_OSPF6_NB_H + +#include "northbound.h" + +struct ospf6; + +#define OSPF6D_IETF_ROUTING_CP_XPATH \ + "/ietf-routing:routing/control-plane-protocols/" \ + "control-plane-protocol" +#define OSPF6D_IETF_ROUTING_PROTOCOL_TYPE_XPATH \ + OSPF6D_IETF_ROUTING_CP_XPATH "[type='ietf-ospf:ospfv3']" +#define OSPF6D_IETF_ROUTING_PROTOCOL_XPATH \ + OSPF6D_IETF_ROUTING_PROTOCOL_TYPE_XPATH "[name='%s']" +#define OSPF6D_IETF_OSPF_XPATH \ + OSPF6D_IETF_ROUTING_CP_XPATH "/ietf-ospf:ospf" + +extern const struct frr_yang_module_info ospf6d_ietf_routing_info; +extern const struct frr_yang_module_info ospf6d_ietf_routing_ospf_deviation_info; +extern const struct frr_yang_module_info ospf6d_ietf_ospf_info; + +/* Shared lookup: find an OSPFv3 instance by the ietf-routing instance name. */ +const char *ospf6d_ietf_ospf_instance_name(const struct ospf6 *ospf6); +struct ospf6 *ospf6d_ietf_ospf_lookup_instance(const char *name); + +int ospf6d_ietf_routing_control_plane_protocol_create(struct nb_cb_create_args *args); +int ospf6d_ietf_routing_control_plane_protocol_destroy(struct nb_cb_destroy_args *args); +const void *ospf6d_ietf_routing_control_plane_protocol_get_next(struct nb_cb_get_next_args *args); +int ospf6d_ietf_routing_control_plane_protocol_get_keys(struct nb_cb_get_keys_args *args); +const void * +ospf6d_ietf_routing_control_plane_protocol_lookup_entry(struct nb_cb_lookup_entry_args *args); + +/* Config callbacks. */ +int ospf6d_ietf_ospf_explicit_router_id_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_explicit_router_id_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_areas_area_create(struct nb_cb_create_args *args); +int ospf6d_ietf_ospf_areas_area_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_areas_area_type_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_areas_area_summary_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_areas_area_summary_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_create(struct nb_cb_create_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_cost_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_cost_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_hello_interval_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_hello_interval_destroy( + struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_dead_interval_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_dead_interval_destroy( + struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_retransmit_interval_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_retransmit_interval_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_priority_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_priority_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_mtu_ignore_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_mtu_ignore_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_transmit_delay_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_transmit_delay_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_areas_area_ranges_range_create(struct nb_cb_create_args *args); +int ospf6d_ietf_ospf_areas_area_ranges_range_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_areas_area_ranges_range_advertise_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_areas_area_ranges_range_advertise_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_areas_area_ranges_range_cost_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_areas_area_ranges_range_cost_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_interface_type_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_interface_type_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_passive_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_passive_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_preference_all_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_preference_all_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_preference_intra_area_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_preference_intra_area_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_preference_inter_area_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_preference_inter_area_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_preference_internal_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_preference_internal_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_preference_external_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_preference_external_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_spf_control_paths_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_spf_control_paths_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_auto_cost_enabled_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_auto_cost_reference_bandwidth_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_auto_cost_reference_bandwidth_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_graceful_restart_enabled_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_graceful_restart_enabled_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_graceful_restart_restart_interval_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_graceful_restart_restart_interval_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_graceful_restart_helper_enabled_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_graceful_restart_helper_enabled_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_graceful_restart_helper_strict_lsa_checking_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_graceful_restart_helper_strict_lsa_checking_destroy(struct nb_cb_destroy_args *args); +void ospf6d_ietf_ospf_areas_area_interfaces_interface_bfd_apply_finish(struct nb_cb_apply_finish_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_bfd_enabled_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_bfd_local_multiplier_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_bfd_desired_min_tx_interval_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_bfd_desired_min_tx_interval_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_bfd_required_min_rx_interval_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_bfd_required_min_rx_interval_destroy(struct nb_cb_destroy_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_authentication_ospfv3_key_chain_modify(struct nb_cb_modify_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_authentication_ospfv3_key_chain_destroy(struct nb_cb_destroy_args *args); + +struct yang_data *ospf6d_ietf_ospf_router_id_get_elem(struct nb_cb_get_elem_args *args); +struct yang_data * +ospf6d_ietf_ospf_statistics_originate_new_lsa_count_get_elem(struct nb_cb_get_elem_args *args); +struct yang_data * +ospf6d_ietf_ospf_statistics_rx_new_lsas_count_get_elem(struct nb_cb_get_elem_args *args); +const void *ospf6d_ietf_ospf_areas_area_get_next(struct nb_cb_get_next_args *args); +int ospf6d_ietf_ospf_areas_area_get_keys(struct nb_cb_get_keys_args *args); +const void *ospf6d_ietf_ospf_areas_area_lookup_entry(struct nb_cb_lookup_entry_args *args); +struct yang_data * +ospf6d_ietf_ospf_areas_area_statistics_spf_runs_count_get_elem(struct nb_cb_get_elem_args *args); +struct yang_data * +ospf6d_ietf_ospf_areas_area_statistics_abr_count_get_elem(struct nb_cb_get_elem_args *args); +struct yang_data * +ospf6d_ietf_ospf_areas_area_statistics_asbr_count_get_elem(struct nb_cb_get_elem_args *args); +struct yang_data *ospf6d_ietf_ospf_areas_area_statistics_area_scope_lsa_count_get_elem( + struct nb_cb_get_elem_args *args); + +const void * +ospf6d_ietf_ospf_areas_area_interfaces_interface_get_next(struct nb_cb_get_next_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_get_keys(struct nb_cb_get_keys_args *args); +const void * +ospf6d_ietf_ospf_areas_area_interfaces_interface_lookup_entry(struct nb_cb_lookup_entry_args *args); + +const void *ospf6d_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_get_next( + struct nb_cb_get_next_args *args); +int ospf6d_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_get_keys( + struct nb_cb_get_keys_args *args); +const void *ospf6d_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_lookup_entry( + struct nb_cb_lookup_entry_args *args); +struct yang_data * +ospf6d_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_address_get_elem( + struct nb_cb_get_elem_args *args); +struct yang_data * +ospf6d_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_state_get_elem( + struct nb_cb_get_elem_args *args); + +/* RPC callbacks (RFC 9129). */ +int ospf6d_ietf_ospf_clear_neighbor_rpc(struct nb_cb_rpc_args *args); +int ospf6d_ietf_ospf_clear_database_rpc(struct nb_cb_rpc_args *args); + +/* Notification emitters (RFC 9129). */ +struct ospf6_neighbor; +struct ospf6_interface; +void ospf6d_ietf_notif_init(void); +void ospf6d_ietf_notif_restart_status_change(struct ospf6 *ospf6, int status, int exit_reason); +void ospf6d_ietf_notif_nbr_restart_helper_status_change(struct ospf6_neighbor *on, int status, + uint16_t age, int exit_reason); +void ospf6d_ietf_notif_if_rx_bad_packet(struct ospf6_interface *oi, struct in6_addr src, + uint8_t packet_type); +void ospf6d_ietf_notif_if_config_error(struct ospf6_interface *oi, struct in6_addr src, + uint8_t packet_type, const char *error_name); + +#endif /* FRR_OSPF6_NB_H */ diff --git a/ospf6d/ospf6_nb_config.c b/ospf6d/ospf6_nb_config.c new file mode 100644 index 000000000000..368460893a21 --- /dev/null +++ b/ospf6d/ospf6_nb_config.c @@ -0,0 +1,2231 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * OSPFv3 northbound configuration callbacks. + * Copyright (C) 2026 Eric Parsonage + */ + +#include + +#include "northbound.h" +#include "yang.h" +#include "yang_wrappers.h" + +#include "if.h" + +#include "ospf6_proto.h" +#include "ospf6_lsa.h" +#include "ospf6_top.h" +#include "ospf6_area.h" +#include "ospf6_abr.h" +#include "ospf6_interface.h" +#include "ospf6_message.h" +#include "ospf6_neighbor.h" +#include "ospf6_route.h" +#include "ospf6_tlv.h" +#include "ospf6_gr.h" +#include "ospf6_bfd.h" +#include "ospf6_auth_trailer.h" +#include "ospf6_nb.h" +#include "ospf6_nssa.h" + +#include "lib/bfd.h" + +/* + * RFC 9129 ietf-ospf area-type identityrefs. Accept both bare and + * module-qualified forms; see the matching helper in ospfd/ospf_nb_config.c. + */ +#define OSPF6_AREA_TYPE_NORMAL "normal-area" +#define OSPF6_AREA_TYPE_STUB "stub-area" +#define OSPF6_AREA_TYPE_NSSA "nssa-area" + +static bool ospf6_area_type_is(const char *val, const char *name) +{ + if (!val) + return false; + if (!strcmp(val, name)) + return true; + if (strncmp(val, "ietf-ospf:", strlen("ietf-ospf:")) == 0) + return !strcmp(val + strlen("ietf-ospf:"), name); + return false; +} + +static bool ospf6d_ietf_ospf_type_is(const char *val) +{ + return val && (!strcmp(val, "ospfv3") || + !strcmp(val, "ietf-ospf:ospfv3")); +} + +static int ospf6d_ietf_ospf_parse_error(enum nb_event event) +{ + return event == NB_EV_VALIDATE ? NB_ERR_VALIDATION : NB_ERR; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol + * + * Keep the IETF routing protocol list present in the local candidate whenever + * the legacy `router ospf6` CLI creates the daemon instance directly. Child + * commands converted to RFC 9129 leaves, such as explicit-router-id, then have + * a real parent list entry to modify during the pending NB commit. + */ +int ospf6d_ietf_routing_control_plane_protocol_create(struct nb_cb_create_args *args) +{ + const char *type; + const char *name; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + type = yang_dnode_get_string(args->dnode, "type"); + if (!ospf6d_ietf_ospf_type_is(type)) + return NB_OK; + + name = yang_dnode_get_string(args->dnode, "name"); + if (!name) + name = VRF_DEFAULT_NAME; + + if (!ospf6d_ietf_ospf_lookup_instance(name)) + ospf6_instance_create(name); + + return NB_OK; +} + +int ospf6d_ietf_routing_control_plane_protocol_destroy(struct nb_cb_destroy_args *args) +{ + const char *type; + const char *name; + struct ospf6 *ospf6; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + type = yang_dnode_get_string(args->dnode, "type"); + if (!ospf6d_ietf_ospf_type_is(type)) + return NB_OK; + + name = yang_dnode_get_string(args->dnode, "name"); + if (!name) + name = VRF_DEFAULT_NAME; + + ospf6 = ospf6d_ietf_ospf_lookup_instance(name); + if (!ospf6) + return NB_OK; + + if (ospf6->gr_info.restart_support) + ospf6_gr_nvm_delete(ospf6); + ospf6_delete(&ospf6); + + return NB_OK; +} + +/* + * Look up the OSPFv3 instance corresponding to an ietf-ospf config dnode. + * Walks up to the parent control-plane-protocol list entry to read the + * instance name, then resolves it through the shared helper. + * + * mgmtd's predicate-aware dispatch (mgmt_be_xpath_prefix) routes only + * control-plane-protocol[type='ietf-ospf:ospfv3'] entries to ospf6d, so + * the type check is handled at the dispatch layer and not repeated here. + * + * Returns NULL when no FRR-side OSPFv3 instance exists for the named + * control-plane-protocol; the caller should treat the configuration as a + * no-op until the instance is created (today: via `router ospf6`). + */ +static struct ospf6 *ospf6d_ietf_ospf_instance_from_dnode(const struct lyd_node *dnode) +{ + const struct lyd_node *cpp; + const char *name; + + cpp = yang_dnode_get_parent(dnode, "control-plane-protocol"); + if (!cpp) + return NULL; + + name = yang_dnode_get_string(cpp, "name"); + return ospf6d_ietf_ospf_lookup_instance(name); +} + +/* + * Resolve the OSPFv3 instance for create / modify callbacks. See the OSPFv2 + * equivalent in ospfd/ospf_nb_config.c for the rationale; APPLY-phase + * tolerance preserves commit progress if the instance is torn down + * mid-transaction. Returns NB_OK + *ospf6_out set on success, NB_OK + NULL + * for the APPLY-race case, NB_ERR_INCONSISTENCY when VALIDATE rejects. + */ +static int ospf6d_ietf_ospf_resolve_instance(const struct lyd_node *dnode, enum nb_event event, + char *errmsg, size_t errmsg_len, + struct ospf6 **ospf6_out) +{ + struct ospf6 *ospf6; + + ospf6 = ospf6d_ietf_ospf_instance_from_dnode(dnode); + *ospf6_out = ospf6; + if (ospf6) + return NB_OK; + + if (event == NB_EV_VALIDATE) { + const struct lyd_node *cpp = yang_dnode_get_parent(dnode, "control-plane-protocol"); + + snprintf(errmsg, errmsg_len, + "OSPFv3 instance '%s' is not configured (use 'router ospf6' first)", + cpp ? yang_dnode_get_string(cpp, "name") : "?"); + return NB_ERR_INCONSISTENCY; + } + return NB_OK; +} + +static bool ospf6d_ietf_ospf_resolve_destroy_instance(struct nb_cb_destroy_args *args, + struct ospf6 **ospf) +{ + *ospf = NULL; + if (args->event == NB_EV_APPLY) + *ospf = ospf6d_ietf_ospf_instance_from_dnode(args->dnode); + + return *ospf != NULL; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/explicit-router-id + */ +int ospf6d_ietf_ospf_explicit_router_id_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + struct in_addr router_id; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + yang_dnode_get_ipv4(&router_id, args->dnode, NULL); + /* ospf6d stores router IDs as network-byte-order uint32_t values. */ + ospf6->router_id_static = router_id.s_addr; + + if (ospf6_router_id_update(ospf6, false)) + ospf6_process_reset(ospf6); + + return NB_OK; +} + +int ospf6d_ietf_ospf_explicit_router_id_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + + if (!ospf6d_ietf_ospf_resolve_destroy_instance(args, &ospf6)) + return NB_OK; + + ospf6->router_id_static = 0; + + if (ospf6_router_id_update(ospf6, false)) + ospf6_process_reset(ospf6); + + return NB_OK; +} + +/* + * Walk up from a dnode within the areas/area subtree to extract the + * area-id key. Returns 0 on success, -1 on failure. ospf6d stores area + * IDs as network-byte-order uint32_t. + */ +static int ospf6d_ietf_ospf_area_id_from_dnode(const struct lyd_node *dnode, uint32_t *area_id) +{ + const struct lyd_node *area_node; + const char *area_id_str; + struct in_addr addr; + + area_node = yang_dnode_get_parent(dnode, "area"); + if (!area_node) + return -1; + + area_id_str = yang_dnode_get_string(area_node, "area-id"); + if (inet_pton(AF_INET, area_id_str, &addr) != 1) + return -1; + *area_id = addr.s_addr; + return 0; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area + * + * Mirrors the OSPFv2 area pilot. ospf6_area_create takes a display format; + * areas declared through YANG always use the dotted-quad form because that + * is how rt-types:area-id-type serialises. + */ +int ospf6d_ietf_ospf_areas_area_create(struct nb_cb_create_args *args) +{ + struct ospf6 *ospf6; + int ret; + struct ospf6_area *area; + uint32_t area_id; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + if (ospf6d_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return NB_ERR; + + area = ospf6_area_lookup(area_id, ospf6); + if (!area) + (void)ospf6_area_create(area_id, ospf6, OSPF6_AREA_FMT_DOTTEDQUAD); + + return NB_OK; +} + +static void ospf6d_ietf_ospf_area_ranges_clear(struct ospf6 *ospf6, + struct ospf6_route_table *table, + bool abr) +{ + struct ospf6_route *range; + + while ((range = ospf6_route_head(table)) != NULL) { + if (abr) { + SET_FLAG(range->flag, OSPF6_ROUTE_REMOVE); + ospf6_schedule_abr_task(ospf6); + } + ospf6_route_remove(range, table); + ospf6_route_unlock(range); + } +} + +int ospf6d_ietf_ospf_areas_area_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + struct ospf6_area *area; + uint32_t area_id; + bool abr; + + if (!ospf6d_ietf_ospf_resolve_destroy_instance(args, &ospf6)) + return NB_OK; + + if (ospf6d_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return NB_ERR; + + area = ospf6_area_lookup(area_id, ospf6); + if (!area) + return NB_OK; + + /* + * Reset every area attr back to defaults so + * ospf6_area_no_config_delete can actually free the area. Per-leaf + * destroy isn't dispatched for area-type (it has a YANG default), so + * the cleanup is centralised here. + */ + ospf6_area_stub_unset(ospf6, area); + ospf6_area_nssa_unset(ospf6, area); + ospf6_area_no_summary_unset(ospf6, area); + + /* + * Drop any area ranges; ospf6_area_no_config_delete inspects both + * range table counts and refuses to free if either is non-zero. Use + * the head-then-remove drain pattern -- same shape as the post-fix + * ospf6_route_remove_all -- so the loop stays safe if a future + * hook_remove is ever attached to either range table. + */ + abr = ospf6_check_and_set_router_abr(ospf6); + ospf6d_ietf_ospf_area_ranges_clear(ospf6, area->range_table, abr); + ospf6d_ietf_ospf_area_ranges_clear(ospf6, area->nssa_range_table, abr); + + ospf6_area_no_config_delete(area); + + return NB_OK; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/area-type + * + * No virtual-link VALIDATE guard here, in contrast to the OSPFv2 sibling + * in ospfd/ospf_nb_config.c. ospf6_area_stub_set / ospf6_area_nssa_set + * unconditionally return 1 -- they have no failure mode -- so there is no + * deferred-APPLY-error class to push earlier. Adding a defensive guard + * would be a no-op today and would invite drift if ospf6d ever gains a + * matching restriction; the right place to add one is alongside such a + * restriction in the v3 helpers, not preemptively here. + */ +int ospf6d_ietf_ospf_areas_area_type_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + struct ospf6_area *area; + uint32_t area_id; + const char *type; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + if (ospf6d_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return NB_ERR; + + /* + * Idempotent at APPLY: if the area was torn down between VALIDATE + * and APPLY, silently no-op rather than returning + * NB_ERR_INCONSISTENCY (which mgmtd would log-and-drop anyway, and + * which diverged from the OSPFv2 callback's behaviour where the + * area helpers handle missing areas internally). + */ + area = ospf6_area_lookup(area_id, ospf6); + if (!area) + return NB_OK; + + type = yang_dnode_get_string(args->dnode, NULL); + if (ospf6_area_type_is(type, OSPF6_AREA_TYPE_NORMAL)) { + ospf6_area_stub_unset(ospf6, area); + ospf6_area_nssa_unset(ospf6, area); + } else if (ospf6_area_type_is(type, OSPF6_AREA_TYPE_STUB)) { + ospf6_area_nssa_unset(ospf6, area); + if (!ospf6_area_stub_set(ospf6, area)) + return NB_ERR_INCONSISTENCY; + } else if (ospf6_area_type_is(type, OSPF6_AREA_TYPE_NSSA)) { + ospf6_area_stub_unset(ospf6, area); + if (!ospf6_area_nssa_set(ospf6, area)) + return NB_ERR_INCONSISTENCY; + } else { + return NB_ERR; + } + + return NB_OK; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/summary + * + * Same inverted semantics as the OSPFv2 side: summary=true means summary + * LSAs are injected, summary=false means totally stubby. + * + * NOTE: RFC 9129 also defines areas/area/default-cost. ospf6d has no + * per-area stub default-cost knob, so that leaf is intentionally + * unimplemented here; setting it through YANG against an ospf6 instance + * is rejected by mgmtd as "no backend handles this path". This matches + * FRR's existing v3 CLI surface (which has no `area X default-cost` + * equivalent) and is a pre-existing v2/v3 feature gap, not introduced + * by this conversion. + */ +int ospf6d_ietf_ospf_areas_area_summary_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + struct ospf6_area *area; + uint32_t area_id; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + if (ospf6d_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return NB_ERR; + area = ospf6_area_lookup(area_id, ospf6); + if (!area) + return NB_OK; + + if (yang_dnode_get_bool(args->dnode, NULL)) + ospf6_area_no_summary_unset(ospf6, area); + else + ospf6_area_no_summary_set(ospf6, area); + + return NB_OK; +} + +int ospf6d_ietf_ospf_areas_area_summary_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + struct ospf6_area *area; + uint32_t area_id; + + if (!ospf6d_ietf_ospf_resolve_destroy_instance(args, &ospf6)) + return NB_OK; + if (ospf6d_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return NB_ERR; + area = ospf6_area_lookup(area_id, ospf6); + if (!area) + return NB_OK; + + ospf6_area_no_summary_unset(ospf6, area); + + return NB_OK; +} + +/* + * Walk up from a dnode within the area/interfaces/interface subtree to + * extract the interface name. NULL on schema-shape failure (defensive). + */ +static const char *ospf6d_ietf_ospf_interface_name_from_dnode(const struct lyd_node *dnode) +{ + const struct lyd_node *iface_node; + + iface_node = yang_dnode_get_parent(dnode, "interface"); + if (!iface_node) + return NULL; + return yang_dnode_get_string(iface_node, "name"); +} + +static struct interface *ospf6d_ietf_ospf_interface_from_dnode(const struct ospf6 *ospf6, + const struct lyd_node *dnode) +{ + const char *name; + + name = ospf6d_ietf_ospf_interface_name_from_dnode(dnode); + if (!name) + return NULL; + return if_lookup_by_name(name, ospf6->vrf_id); +} + +static bool ospf6d_ietf_ospf_area_has_interface(const struct lyd_node *area_node, + const char *ifname) +{ + const struct lyd_node *interfaces_node; + const struct lyd_node *iface_node; + + interfaces_node = yang_dnode_get(area_node, "interfaces"); + if (!interfaces_node) + return false; + + LY_LIST_FOR (lyd_child(interfaces_node), iface_node) { + const char *name; + + if (strcmp(iface_node->schema->name, "interface")) + continue; + + name = yang_dnode_get_string(iface_node, "name"); + if (name && !strcmp(name, ifname)) + return true; + } + + return false; +} + +static int ospf6d_ietf_ospf_validate_interface_area_unique(const struct lyd_node *dnode, + const char *ifname, char *errmsg, + size_t errmsg_len) +{ + const struct lyd_node *area_node; + const struct lyd_node *areas_node; + const struct lyd_node *other_area_node; + const char *area_id; + + area_node = yang_dnode_get_parent(dnode, "area"); + if (!area_node) + return NB_OK; + areas_node = yang_dnode_get_parent(area_node, "areas"); + if (!areas_node) + return NB_OK; + + area_id = yang_dnode_get_string(area_node, "area-id"); + LY_LIST_FOR (lyd_child(areas_node), other_area_node) { + const char *other_area_id; + + if (other_area_node == area_node || + strcmp(other_area_node->schema->name, "area")) + continue; + if (!ospf6d_ietf_ospf_area_has_interface(other_area_node, ifname)) + continue; + + other_area_id = yang_dnode_get_string(other_area_node, "area-id"); + snprintf(errmsg, errmsg_len, + "interface '%s' is configured under multiple OSPF areas (%s and %s)", + ifname, area_id, other_area_id); + return NB_ERR_INCONSISTENCY; + } + + return NB_OK; +} + +/* + * VALIDATE-rejecting variant for per-interface create / modify callbacks. + * frr-deviations-ietf-routing-ospf relaxes the RFC 9129 interface-name + * leafref so configuration can be emitted ahead of interface plumbing. + * The relaxation removes libyang's referential check; this helper restores + * it inside the callback, returning NB_ERR_INCONSISTENCY at VALIDATE when + * the interface is not present in the OSPF instance's VRF. + * + * Returns: + * NB_OK + *ifp_out set -- proceed. + * NB_OK + *ifp_out == NULL -- APPLY-phase race tolerated. + * NB_ERR_INCONSISTENCY -- VALIDATE rejected. + */ +static int ospf6d_ietf_ospf_resolve_interface(const struct ospf6 *ospf, + const struct lyd_node *dnode, enum nb_event event, + char *errmsg, size_t errmsg_len, + struct interface **ifp_out) +{ + struct interface *ifp; + + ifp = ospf6d_ietf_ospf_interface_from_dnode(ospf, dnode); + *ifp_out = ifp; + if (ifp) + return NB_OK; + + if (event == NB_EV_VALIDATE) { + const struct lyd_node *iface_node = yang_dnode_get_parent(dnode, "interface"); + + snprintf(errmsg, errmsg_len, "interface '%s' is not present in vrf-id %u", + iface_node ? yang_dnode_get_string(iface_node, "name") : "?", + ospf->vrf_id); + return NB_ERR_INCONSISTENCY; + } + return NB_OK; +} + +static int ospf6d_ietf_ospf_resolve_modify_interface(struct nb_cb_modify_args *args, + struct ospf6 **ospf, + struct interface **ifp) +{ + struct ospf6_interface *oi; + int ret; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, + args->errmsg, args->errmsg_len, + ospf); + if (ret != NB_OK || !*ospf) + return ret; + + ret = ospf6d_ietf_ospf_resolve_interface(*ospf, args->dnode, args->event, + args->errmsg, args->errmsg_len, + ifp); + if (ret != NB_OK || !*ifp) + return ret; + + if (args->event == NB_EV_APPLY) { + oi = (*ifp)->info; + if (!oi) { + oi = ospf6_interface_create(*ifp); + if (!oi) + return NB_ERR; + } + } + + return NB_OK; +} + +static int ospf6d_ietf_ospf_resolve_create_interface(struct nb_cb_create_args *args, + struct ospf6 **ospf, + struct interface **ifp) +{ + struct ospf6_interface *oi; + int ret; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, + args->errmsg, args->errmsg_len, + ospf); + if (ret != NB_OK || !*ospf) + return ret; + + ret = ospf6d_ietf_ospf_resolve_interface(*ospf, args->dnode, args->event, + args->errmsg, args->errmsg_len, + ifp); + if (ret != NB_OK || !*ifp) + return ret; + + if (args->event == NB_EV_APPLY) { + oi = (*ifp)->info; + if (!oi) { + oi = ospf6_interface_create(*ifp); + if (!oi) + return NB_ERR; + } + } + + return NB_OK; +} + +static bool ospf6d_ietf_ospf_resolve_destroy_interface(struct nb_cb_destroy_args *args, + struct ospf6 **ospf, + struct interface **ifp) +{ + *ospf = NULL; + *ifp = NULL; + if (args->event != NB_EV_APPLY) + return false; + + *ospf = ospf6d_ietf_ospf_instance_from_dnode(args->dnode); + if (!*ospf) + return false; + + *ifp = ospf6d_ietf_ospf_interface_from_dnode(*ospf, args->dnode); + return *ifp != NULL; +} + + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/interfaces/interface + * + * Same area-keyed model as the OSPFv2 side. One interface, one area: + * VALIDATE rejects a candidate that contains the same interface under + * more than one area, matching FRR's OSPFv3 interface model and the + * legacy ipv6_ospf6_area "already attached to Area X" restriction. + */ +int ospf6d_ietf_ospf_areas_area_interfaces_interface_create(struct nb_cb_create_args *args) +{ + struct ospf6 *ospf6; + int ret; + struct interface *ifp; + struct ospf6_interface *oi; + uint32_t area_id; + + ret = ospf6d_ietf_ospf_resolve_create_interface(args, &ospf6, &ifp); + if (ret != NB_OK || !ospf6 || !ifp) + return ret; + + if (args->event == NB_EV_VALIDATE) { + ret = ospf6d_ietf_ospf_validate_interface_area_unique( + args->dnode, ifp->name, args->errmsg, args->errmsg_len); + if (ret != NB_OK) + return ret; + } + + if (ospf6d_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return ospf6d_ietf_ospf_parse_error(args->event); + + if (args->event != NB_EV_APPLY) + return NB_OK; + + oi = (struct ospf6_interface *)ifp->info; + if (!oi) + oi = ospf6_interface_create(ifp); + + oi->area_id = area_id; + oi->area_id_format = OSPF6_AREA_FMT_DOTTEDQUAD; + + if (!oi->area) + ospf6_interface_start(oi); + + return NB_OK; +} + +int ospf6d_ietf_ospf_areas_area_interfaces_interface_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + struct interface *ifp; + struct ospf6_interface *oi; + + if (!ospf6d_ietf_ospf_resolve_destroy_interface(args, &ospf6, &ifp)) + return NB_OK; + + oi = (struct ospf6_interface *)ifp->info; + if (!oi) + return NB_OK; + + /* + * Reset every per-interface attr to its compile-time + * default so a subsequent re-create starts clean. The cost field + * is restored by ospf6_interface_recalculate_cost after we drop + * the NOAUTOCOST flag. + */ + UNSET_FLAG(oi->flag, OSPF6_INTERFACE_NOAUTOCOST); + UNSET_FLAG(oi->flag, OSPF6_INTERFACE_PASSIVE); + oi->hello_interval = OSPF6_INTERFACE_HELLO_INTERVAL; + oi->dead_interval = OSPF6_INTERFACE_DEAD_INTERVAL; + oi->rxmt_interval = OSPF6_INTERFACE_RXMT_INTERVAL; + oi->priority = OSPF6_INTERFACE_PRIORITY; + oi->mtu_ignore = 0; + oi->type_cfg = false; + oi->type = ospf6_default_iftype(ifp); + ospf6_interface_stop(oi); + oi->area_id = 0; + oi->area_id_format = OSPF6_AREA_FMT_UNSET; + + return NB_OK; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/interfaces/interface/cost + */ +int ospf6d_ietf_ospf_areas_area_interfaces_interface_cost_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + struct interface *ifp; + struct ospf6_interface *oi; + uint32_t cost; + + ret = ospf6d_ietf_ospf_resolve_modify_interface(args, &ospf6, &ifp); + if (ret != NB_OK || !ospf6 || !ifp) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + oi = (struct ospf6_interface *)ifp->info; + if (!oi) + return NB_OK; + + /* ospf-link-metric is uint16; widen into oi->cost (uint32_t). */ + cost = yang_dnode_get_uint16(args->dnode, NULL); + SET_FLAG(oi->flag, OSPF6_INTERFACE_NOAUTOCOST); + if (oi->cost == cost) + return NB_OK; + oi->cost = cost; + ospf6_interface_force_recalculate_cost(oi); + + return NB_OK; +} + +int ospf6d_ietf_ospf_areas_area_interfaces_interface_cost_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + struct interface *ifp; + struct ospf6_interface *oi; + + if (!ospf6d_ietf_ospf_resolve_destroy_interface(args, &ospf6, &ifp)) + return NB_OK; + + oi = (struct ospf6_interface *)ifp->info; + if (!oi) + return NB_OK; + + UNSET_FLAG(oi->flag, OSPF6_INTERFACE_NOAUTOCOST); + ospf6_interface_recalculate_cost(oi); + + return NB_OK; +} + +/* + * Per-interface uint16/uint8/boolean leaves for OSPFv3. + * + * ospf6d stores per-interface state directly on struct ospf6_interface + * rather than via a parallel "default params" struct, so each modify + * just writes the field and (where needed) re-arms whatever timer or + * SPF the change should provoke. destroy reverts to the FRR-side + * compile-time default. + */ + +/* XPath: .../interface/hello-interval */ +int ospf6d_ietf_ospf_areas_area_interfaces_interface_hello_interval_modify( + struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + struct interface *ifp; + struct ospf6_interface *oi; + + ret = ospf6d_ietf_ospf_resolve_modify_interface(args, &ospf6, &ifp); + if (ret != NB_OK || !ospf6 || !ifp) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + oi = (struct ospf6_interface *)ifp->info; + if (!oi) + return NB_OK; + + oi->hello_interval = yang_dnode_get_uint16(args->dnode, NULL); + /* + * Reschedule the next hello immediately so the new interval takes + * effect within the current hello cycle rather than after one full + * old-interval delay. Mirrors the legacy `ipv6 ospf6 hello-interval` + * direct-mutation path. + */ + ospf6_hello_reschedule(oi); + return NB_OK; +} + +int ospf6d_ietf_ospf_areas_area_interfaces_interface_hello_interval_destroy( + struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + struct interface *ifp; + struct ospf6_interface *oi; + + if (!ospf6d_ietf_ospf_resolve_destroy_interface(args, &ospf6, &ifp)) + return NB_OK; + + oi = (struct ospf6_interface *)ifp->info; + if (!oi) + return NB_OK; + + oi->hello_interval = OSPF6_INTERFACE_HELLO_INTERVAL; + ospf6_hello_reschedule(oi); + return NB_OK; +} + +/* XPath: .../interface/dead-interval */ +int ospf6d_ietf_ospf_areas_area_interfaces_interface_dead_interval_modify( + struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + struct interface *ifp; + struct ospf6_interface *oi; + + ret = ospf6d_ietf_ospf_resolve_modify_interface(args, &ospf6, &ifp); + if (ret != NB_OK || !ospf6 || !ifp) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + oi = (struct ospf6_interface *)ifp->info; + if (!oi) + return NB_OK; + + oi->dead_interval = yang_dnode_get_uint16(args->dnode, NULL); + return NB_OK; +} + +int ospf6d_ietf_ospf_areas_area_interfaces_interface_dead_interval_destroy( + struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + struct interface *ifp; + struct ospf6_interface *oi; + + if (!ospf6d_ietf_ospf_resolve_destroy_interface(args, &ospf6, &ifp)) + return NB_OK; + + oi = (struct ospf6_interface *)ifp->info; + if (!oi) + return NB_OK; + + oi->dead_interval = OSPF6_INTERFACE_DEAD_INTERVAL; + return NB_OK; +} + +/* XPath: .../interface/retransmit-interval */ +int ospf6d_ietf_ospf_areas_area_interfaces_interface_retransmit_interval_modify( + struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + struct interface *ifp; + struct ospf6_interface *oi; + + ret = ospf6d_ietf_ospf_resolve_modify_interface(args, &ospf6, &ifp); + if (ret != NB_OK || !ospf6 || !ifp) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + oi = (struct ospf6_interface *)ifp->info; + if (!oi) + return NB_OK; + + oi->rxmt_interval = yang_dnode_get_uint16(args->dnode, NULL); + return NB_OK; +} + +int ospf6d_ietf_ospf_areas_area_interfaces_interface_retransmit_interval_destroy( + struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + struct interface *ifp; + struct ospf6_interface *oi; + + if (!ospf6d_ietf_ospf_resolve_destroy_interface(args, &ospf6, &ifp)) + return NB_OK; + oi = (struct ospf6_interface *)ifp->info; + if (!oi) + return NB_OK; + + oi->rxmt_interval = OSPF6_INTERFACE_RXMT_INTERVAL; + return NB_OK; +} + +/* XPath: .../interface/priority */ +int ospf6d_ietf_ospf_areas_area_interfaces_interface_priority_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + struct interface *ifp; + struct ospf6_interface *oi; + + ret = ospf6d_ietf_ospf_resolve_modify_interface(args, &ospf6, &ifp); + if (ret != NB_OK || !ospf6 || !ifp) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + oi = (struct ospf6_interface *)ifp->info; + if (!oi) + return NB_OK; + + oi->priority = yang_dnode_get_uint8(args->dnode, NULL); + /* + * Re-run DR election immediately so the new priority is reflected + * in the next hello without waiting for the current DR/BDR state + * to time out. Mirrors the legacy `ipv6 ospf6 priority` direct- + * mutation path. + */ + ospf6_priority_recompute(oi); + return NB_OK; +} + +int ospf6d_ietf_ospf_areas_area_interfaces_interface_priority_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + struct interface *ifp; + struct ospf6_interface *oi; + + if (!ospf6d_ietf_ospf_resolve_destroy_interface(args, &ospf6, &ifp)) + return NB_OK; + oi = (struct ospf6_interface *)ifp->info; + if (!oi) + return NB_OK; + + oi->priority = OSPF6_INTERFACE_PRIORITY; + ospf6_priority_recompute(oi); + return NB_OK; +} + +/* XPath: .../interface/mtu-ignore */ +int ospf6d_ietf_ospf_areas_area_interfaces_interface_mtu_ignore_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + struct interface *ifp; + struct ospf6_interface *oi; + + ret = ospf6d_ietf_ospf_resolve_modify_interface(args, &ospf6, &ifp); + if (ret != NB_OK || !ospf6 || !ifp) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + oi = (struct ospf6_interface *)ifp->info; + if (!oi) + return NB_OK; + + oi->mtu_ignore = yang_dnode_get_bool(args->dnode, NULL) ? 1 : 0; + return NB_OK; +} + +int ospf6d_ietf_ospf_areas_area_interfaces_interface_mtu_ignore_destroy( + struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + struct interface *ifp; + struct ospf6_interface *oi; + + if (!ospf6d_ietf_ospf_resolve_destroy_interface(args, &ospf6, &ifp)) + return NB_OK; + oi = (struct ospf6_interface *)ifp->info; + if (!oi) + return NB_OK; + + oi->mtu_ignore = 0; + return NB_OK; +} + +/* XPath: .../interface/transmit-delay */ +int ospf6d_ietf_ospf_areas_area_interfaces_interface_transmit_delay_modify( + struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + struct interface *ifp; + struct ospf6_interface *oi; + + ret = ospf6d_ietf_ospf_resolve_modify_interface(args, &ospf6, &ifp); + if (ret != NB_OK || !ospf6 || !ifp) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + oi = (struct ospf6_interface *)ifp->info; + if (!oi) + return NB_OK; + + oi->transdelay = yang_dnode_get_uint16(args->dnode, NULL); + return NB_OK; +} + +int ospf6d_ietf_ospf_areas_area_interfaces_interface_transmit_delay_destroy( + struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + struct interface *ifp; + struct ospf6_interface *oi; + + if (!ospf6d_ietf_ospf_resolve_destroy_interface(args, &ospf6, &ifp)) + return NB_OK; + oi = (struct ospf6_interface *)ifp->info; + if (!oi) + return NB_OK; + + oi->transdelay = OSPF6_INTERFACE_TRANSDELAY; + return NB_OK; +} + +/* + * Helper for areas/area/ranges/range callbacks: extract the prefix + * key as struct prefix (libyang gives us the full prefix, callers narrow + * to ipv6 since this is the ospf6d side). Returns 0 on success. + */ +static int ospf6d_ietf_ospf_range_prefix_from_dnode(const struct lyd_node *dnode, struct prefix *p) +{ + const struct lyd_node *range_node; + + range_node = yang_dnode_get_parent(dnode, "range"); + if (!range_node) + return -1; + + yang_dnode_get_prefix(p, range_node, "prefix"); + if (p->family != AF_INET6) + return -1; + return 0; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/ranges/range + * + * v3 area ranges live as ospf6_route entries in oa->range_table with + * type=OSPF6_DEST_TYPE_RANGE. The route abstraction is the public API + * (there is no ospf6_area_range_set helper), so the callback inlines + * the same sequence the legacy `area X range PREFIX` DEFUN uses: + * create-or-lookup the route, set advertise flag and cost, add to the + * table, schedule ABR. + */ +int ospf6d_ietf_ospf_areas_area_ranges_range_create(struct nb_cb_create_args *args) +{ + struct ospf6 *ospf6; + int ret; + struct ospf6_area *oa; + uint32_t area_id; + struct prefix p; + struct ospf6_route *range; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + if (ospf6d_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return NB_ERR; + if (ospf6d_ietf_ospf_range_prefix_from_dnode(args->dnode, &p) < 0) + return NB_ERR; + + oa = ospf6_area_lookup(area_id, ospf6); + if (!oa) + oa = ospf6_area_create(area_id, ospf6, OSPF6_AREA_FMT_DOTTEDQUAD); + + range = ospf6_route_lookup(&p, oa->range_table); + if (!range) { + range = ospf6_route_create(ospf6); + range->type = OSPF6_DEST_TYPE_RANGE; + range->prefix = p; + range->path.area_id = oa->area_id; + range->path.cost = OSPF_AREA_RANGE_COST_UNSPEC; + range->path.u.cost_config = OSPF_AREA_RANGE_COST_UNSPEC; + ospf6_route_add(range, oa->range_table); + } + + if (ospf6_check_and_set_router_abr(ospf6)) + ospf6_schedule_abr_task(ospf6); + + return NB_OK; +} + +int ospf6d_ietf_ospf_areas_area_ranges_range_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + struct ospf6_area *oa; + uint32_t area_id; + struct prefix p; + struct ospf6_route *range; + + if (!ospf6d_ietf_ospf_resolve_destroy_instance(args, &ospf6)) + return NB_OK; + if (ospf6d_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return NB_ERR; + if (ospf6d_ietf_ospf_range_prefix_from_dnode(args->dnode, &p) < 0) + return NB_ERR; + + oa = ospf6_area_lookup(area_id, ospf6); + if (!oa) + return NB_OK; + range = ospf6_route_lookup(&p, oa->range_table); + if (!range) + return NB_OK; + + if (ospf6_check_and_set_router_abr(oa->ospf6)) { + SET_FLAG(range->flag, OSPF6_ROUTE_REMOVE); + ospf6_schedule_abr_task(oa->ospf6); + } + ospf6_route_remove(range, oa->range_table); + ospf6_area_no_config_delete(oa); + return NB_OK; +} + +/* XPath: .../ranges/range/advertise */ +int ospf6d_ietf_ospf_areas_area_ranges_range_advertise_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + struct ospf6_area *oa; + uint32_t area_id; + struct prefix p; + struct ospf6_route *range; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + if (ospf6d_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return NB_ERR; + if (ospf6d_ietf_ospf_range_prefix_from_dnode(args->dnode, &p) < 0) + return NB_ERR; + oa = ospf6_area_lookup(area_id, ospf6); + if (!oa) + return NB_OK; + range = ospf6_route_lookup(&p, oa->range_table); + if (!range) + return NB_OK; + + if (yang_dnode_get_bool(args->dnode, NULL)) + UNSET_FLAG(range->flag, OSPF6_ROUTE_DO_NOT_ADVERTISE); + else + SET_FLAG(range->flag, OSPF6_ROUTE_DO_NOT_ADVERTISE); + + if (ospf6_check_and_set_router_abr(ospf6)) + ospf6_schedule_abr_task(ospf6); + return NB_OK; +} + +int ospf6d_ietf_ospf_areas_area_ranges_range_advertise_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + struct ospf6_area *oa; + uint32_t area_id; + struct prefix p; + struct ospf6_route *range; + + if (!ospf6d_ietf_ospf_resolve_destroy_instance(args, &ospf6)) + return NB_OK; + if (ospf6d_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return NB_ERR; + if (ospf6d_ietf_ospf_range_prefix_from_dnode(args->dnode, &p) < 0) + return NB_ERR; + oa = ospf6_area_lookup(area_id, ospf6); + if (!oa) + return NB_OK; + range = ospf6_route_lookup(&p, oa->range_table); + if (!range) + return NB_OK; + + /* Revert to FRR's natural default (advertise on). */ + UNSET_FLAG(range->flag, OSPF6_ROUTE_DO_NOT_ADVERTISE); + if (ospf6_check_and_set_router_abr(ospf6)) + ospf6_schedule_abr_task(ospf6); + return NB_OK; +} + +/* XPath: .../ranges/range/cost */ +int ospf6d_ietf_ospf_areas_area_ranges_range_cost_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + struct ospf6_area *oa; + uint32_t area_id; + struct prefix p; + struct ospf6_route *range; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + if (ospf6d_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return NB_ERR; + if (ospf6d_ietf_ospf_range_prefix_from_dnode(args->dnode, &p) < 0) + return NB_ERR; + oa = ospf6_area_lookup(area_id, ospf6); + if (!oa) + return NB_OK; + range = ospf6_route_lookup(&p, oa->range_table); + if (!range) + return NB_OK; + + range->path.u.cost_config = yang_dnode_get_uint32(args->dnode, NULL); + if (ospf6_check_and_set_router_abr(ospf6)) + ospf6_schedule_abr_task(ospf6); + return NB_OK; +} + +int ospf6d_ietf_ospf_areas_area_ranges_range_cost_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + struct ospf6_area *oa; + uint32_t area_id; + struct prefix p; + struct ospf6_route *range; + + if (!ospf6d_ietf_ospf_resolve_destroy_instance(args, &ospf6)) + return NB_OK; + if (ospf6d_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return NB_ERR; + if (ospf6d_ietf_ospf_range_prefix_from_dnode(args->dnode, &p) < 0) + return NB_ERR; + oa = ospf6_area_lookup(area_id, ospf6); + if (!oa) + return NB_OK; + range = ospf6_route_lookup(&p, oa->range_table); + if (!range) + return NB_OK; + + range->path.u.cost_config = OSPF_AREA_RANGE_COST_UNSPEC; + if (ospf6_check_and_set_router_abr(ospf6)) + ospf6_schedule_abr_task(ospf6); + return NB_OK; +} + +/* + * XPath: .../interface/interface-type (OSPFv3) + * + * RFC 9129 declares broadcast, non-broadcast, point-to-multipoint, + * point-to-point and hybrid. ospf6d's `ipv6 ospf6 network` CLI only + * accepts broadcast, point-to-point, and point-to-multipoint; NBMA and + * hybrid have no v3 surface and are rejected here. The legacy DEFUN + * triggers a full interface_down/up cycle on type change; the NB + * callback follows the same pattern via event_execute. + */ +static int ospf6_iftype_from_yang(const char *val) +{ + if (!strcmp(val, "broadcast")) + return OSPF_IFTYPE_BROADCAST; + if (!strcmp(val, "point-to-point")) + return OSPF_IFTYPE_POINTOPOINT; + if (!strcmp(val, "point-to-multipoint")) + return OSPF_IFTYPE_POINTOMULTIPOINT; + return -1; +} + +int ospf6d_ietf_ospf_areas_area_interfaces_interface_interface_type_modify( + struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + struct interface *ifp; + struct ospf6_interface *oi; + const char *val; + int type; + + ret = ospf6d_ietf_ospf_resolve_modify_interface(args, &ospf6, &ifp); + if (ret != NB_OK || !ospf6 || !ifp) + return ret; + + /* + * Loopback interfaces have a fixed OSPF type. Reject at VALIDATE + * before we touch the struct ospf6_interface or trigger any flap. + * Use if_is_loopback rather than checking oi->type, because oi might + * not be allocated yet (the per-interface attachment may arrive + * before any ospf6 area binding) and oi->type would be unset. + */ + if (if_is_loopback(ifp)) { + if (args->event == NB_EV_VALIDATE) + snprintf(args->errmsg, args->errmsg_len, + "cannot set interface-type on loopback interface %s", ifp->name); + return NB_ERR_INCONSISTENCY; + } + + /* + * Reject unsupported enum values at VALIDATE. ospf6d only accepts + * broadcast / point-to-point / point-to-multipoint; the YANG model + * also declares non-broadcast and hybrid which ospf6d cannot + * implement. + */ + val = yang_dnode_get_string(args->dnode, NULL); + type = ospf6_iftype_from_yang(val); + if (type < 0) { + if (args->event == NB_EV_VALIDATE) + snprintf(args->errmsg, args->errmsg_len, + "unsupported interface-type enum '%s' for ospf6", val); + return ospf6d_ietf_ospf_parse_error(args->event); + } + + if (args->event != NB_EV_APPLY) + return NB_OK; + + oi = (struct ospf6_interface *)ifp->info; + if (!oi) + oi = ospf6_interface_create(ifp); + + oi->type_cfg = true; + if (oi->type == type) + return NB_OK; + + oi->type = type; + event_execute(master, interface_down, oi, 0, NULL); + event_execute(master, interface_up, oi, 0, NULL); + return NB_OK; +} + +int ospf6d_ietf_ospf_areas_area_interfaces_interface_interface_type_destroy( + struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + struct interface *ifp; + struct ospf6_interface *oi; + uint8_t type; + + if (!ospf6d_ietf_ospf_resolve_destroy_interface(args, &ospf6, &ifp)) + return NB_OK; + oi = (struct ospf6_interface *)ifp->info; + if (!oi) + return NB_OK; + + oi->type_cfg = false; + type = ospf6_default_iftype(ifp); + if (oi->type == type) + return NB_OK; + + oi->type = type; + event_execute(master, interface_down, oi, 0, NULL); + event_execute(master, interface_up, oi, 0, NULL); + return NB_OK; +} + +/* + * XPath: .../interface/passive (OSPFv3) + */ +int ospf6d_ietf_ospf_areas_area_interfaces_interface_passive_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + struct interface *ifp; + struct ospf6_interface *oi; + struct listnode *node, *nnode; + struct ospf6_neighbor *on; + bool passive; + + ret = ospf6d_ietf_ospf_resolve_modify_interface(args, &ospf6, &ifp); + if (ret != NB_OK || !ospf6 || !ifp) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + oi = (struct ospf6_interface *)ifp->info; + if (!oi) + oi = ospf6_interface_create(ifp); + + passive = yang_dnode_get_bool(args->dnode, NULL); + + if (passive) { + SET_FLAG(oi->flag, OSPF6_INTERFACE_PASSIVE); + event_cancel(&oi->thread_send_hello); + event_cancel(&oi->thread_sso); + for (ALL_LIST_ELEMENTS(oi->neighbor_list, node, nnode, on)) { + event_cancel(&on->inactivity_timer); + event_add_event(master, inactivity_timer, on, 0, NULL); + } + } else { + UNSET_FLAG(oi->flag, OSPF6_INTERFACE_PASSIVE); + event_cancel(&oi->thread_send_hello); + event_cancel(&oi->thread_sso); + if (!if_is_loopback(oi->interface)) + event_add_timer(master, ospf6_hello_send, oi, 0, &oi->thread_send_hello); + } + return NB_OK; +} + +int ospf6d_ietf_ospf_areas_area_interfaces_interface_passive_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + struct interface *ifp; + struct ospf6_interface *oi; + + if (!ospf6d_ietf_ospf_resolve_destroy_interface(args, &ospf6, &ifp)) + return NB_OK; + oi = (struct ospf6_interface *)ifp->info; + if (!oi) + return NB_OK; + + /* Revert to FRR's natural default (active). */ + if (CHECK_FLAG(oi->flag, OSPF6_INTERFACE_PASSIVE)) { + UNSET_FLAG(oi->flag, OSPF6_INTERFACE_PASSIVE); + event_cancel(&oi->thread_send_hello); + event_cancel(&oi->thread_sso); + if (!if_is_loopback(oi->interface)) + event_add_timer(master, ospf6_hello_send, oi, 0, &oi->thread_send_hello); + } + return NB_OK; +} + +/* + * Per-instance preference (admin distance) leaves, v3. + * + * Same mapping as ospfd: distance_all (single scope), distance_intra/ + * distance_inter/distance_external (multi-values scope), with a + * "coarse" alias `internal` covering both intra and inter. + * + * ospf6d's struct ospf6 has the same distance_* fields as ospfd's + * struct ospf, so the callbacks are identical in shape. We trigger an + * SPF restart via ospf6_restart_spf so the new distances take effect. + */ + +/* XPath: .../ospf/preference/all */ +int ospf6d_ietf_ospf_preference_all_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + uint8_t distance; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + distance = yang_dnode_get_uint8(args->dnode, NULL); + if (ospf6->distance_all == distance) + return NB_OK; + ospf6->distance_all = distance; + ospf6_restart_spf(ospf6); + return NB_OK; +} + +int ospf6d_ietf_ospf_preference_all_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf6 = ospf6d_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf6) + return NB_OK; + if (!ospf6->distance_all) + return NB_OK; + ospf6->distance_all = 0; + ospf6_restart_spf(ospf6); + return NB_OK; +} + +/* XPath: .../ospf/preference/intra-area */ +int ospf6d_ietf_ospf_preference_intra_area_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf6->distance_intra = yang_dnode_get_uint8(args->dnode, NULL); + ospf6_restart_spf(ospf6); + return NB_OK; +} + +int ospf6d_ietf_ospf_preference_intra_area_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf6 = ospf6d_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf6) + return NB_OK; + ospf6->distance_intra = 0; + ospf6_restart_spf(ospf6); + return NB_OK; +} + +/* XPath: .../ospf/preference/inter-area */ +int ospf6d_ietf_ospf_preference_inter_area_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf6->distance_inter = yang_dnode_get_uint8(args->dnode, NULL); + ospf6_restart_spf(ospf6); + return NB_OK; +} + +int ospf6d_ietf_ospf_preference_inter_area_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf6 = ospf6d_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf6) + return NB_OK; + ospf6->distance_inter = 0; + ospf6_restart_spf(ospf6); + return NB_OK; +} + +/* XPath: .../ospf/preference/internal */ +int ospf6d_ietf_ospf_preference_internal_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + uint8_t distance; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + distance = yang_dnode_get_uint8(args->dnode, NULL); + ospf6->distance_intra = distance; + ospf6->distance_inter = distance; + ospf6_restart_spf(ospf6); + return NB_OK; +} + +int ospf6d_ietf_ospf_preference_internal_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf6 = ospf6d_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf6) + return NB_OK; + ospf6->distance_intra = 0; + ospf6->distance_inter = 0; + ospf6_restart_spf(ospf6); + return NB_OK; +} + +/* XPath: .../ospf/preference/external */ +int ospf6d_ietf_ospf_preference_external_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf6->distance_external = yang_dnode_get_uint8(args->dnode, NULL); + ospf6_restart_spf(ospf6); + return NB_OK; +} + +int ospf6d_ietf_ospf_preference_external_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf6 = ospf6d_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf6) + return NB_OK; + ospf6->distance_external = 0; + ospf6_restart_spf(ospf6); + return NB_OK; +} + +/* + * XPath: .../ospf/spf-control/paths + * + * Per-instance maximum ECMP paths. Mirrors the legacy `maximum-paths + * N` CLI. RFC 9129 types `paths` as uint16 (range 1..65535), so FRR's + * configured MULTIPATH_NUM cap (typically 16..64) fits trivially. The + * destroy callback restores FRR's "no maximum-paths" semantics + * (MULTIPATH_NUM), not RFC 9129's absent YANG default. + */ +int ospf6d_ietf_ospf_spf_control_paths_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + uint16_t paths; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + + if (args->event == NB_EV_VALIDATE) { + paths = yang_dnode_get_uint16(args->dnode, NULL); + if (paths > MULTIPATH_NUM) { + snprintf(args->errmsg, args->errmsg_len, + "maximum-paths exceeds platform max %u", + MULTIPATH_NUM); + return NB_ERR_INCONSISTENCY; + } + } + + if (args->event != NB_EV_APPLY) + return NB_OK; + + paths = yang_dnode_get_uint16(args->dnode, NULL); + if (ospf6->max_multipath == paths) + return NB_OK; + ospf6->max_multipath = paths; + ospf6_restart_spf(ospf6); + return NB_OK; +} + +int ospf6d_ietf_ospf_spf_control_paths_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf6 = ospf6d_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf6) + return NB_OK; + if (ospf6->max_multipath == MULTIPATH_NUM) + return NB_OK; + ospf6->max_multipath = MULTIPATH_NUM; + ospf6_restart_spf(ospf6); + return NB_OK; +} + +/* + * XPath: .../ospf/auto-cost/enabled + * + * See the ospfd companion comment. ospf6d shares FRR's "always-on + * auto-cost" semantics: there's no on/off switch, so modify=true is a + * no-op, modify=false is rejected at validate, and destroy is a no-op + * because the deviation file pins the leaf's default to "true". + */ +int ospf6d_ietf_ospf_auto_cost_enabled_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + bool enabled; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + + enabled = yang_dnode_get_bool(args->dnode, NULL); + if (args->event == NB_EV_VALIDATE) { + if (!enabled) { + snprintf(args->errmsg, args->errmsg_len, + "FRR auto-cost cannot be disabled; " + "set per-interface 'ipv6 ospf6 cost' instead"); + return NB_ERR_VALIDATION; + } + return NB_OK; + } + return NB_OK; +} + +/* + * XPath: .../ospf/auto-cost/reference-bandwidth + * + * Per-instance reference bandwidth for the auto-cost computation. + * Mirrors the legacy `auto-cost reference-bandwidth N` CLI; RFC 9129 + * units are Mbits, matching FRR's ospf6->ref_bandwidth. Destroy + * restores `OSPF6_REFERENCE_BANDWIDTH`. + */ +int ospf6d_ietf_ospf_auto_cost_reference_bandwidth_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + struct ospf6_area *oa; + struct ospf6_interface *oi; + struct listnode *i, *j; + int ret; + uint32_t refbw; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + refbw = yang_dnode_get_uint32(args->dnode, NULL); + if (ospf6->ref_bandwidth == refbw) + return NB_OK; + ospf6->ref_bandwidth = refbw; + for (ALL_LIST_ELEMENTS_RO(ospf6->area_list, i, oa)) + for (ALL_LIST_ELEMENTS_RO(oa->if_list, j, oi)) + ospf6_interface_recalculate_cost(oi); + return NB_OK; +} + +int ospf6d_ietf_ospf_auto_cost_reference_bandwidth_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + struct ospf6_area *oa; + struct ospf6_interface *oi; + struct listnode *i, *j; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf6 = ospf6d_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf6) + return NB_OK; + if (ospf6->ref_bandwidth == OSPF6_REFERENCE_BANDWIDTH) + return NB_OK; + ospf6->ref_bandwidth = OSPF6_REFERENCE_BANDWIDTH; + for (ALL_LIST_ELEMENTS_RO(ospf6->area_list, i, oa)) + for (ALL_LIST_ELEMENTS_RO(oa->if_list, j, oi)) + ospf6_interface_recalculate_cost(oi); + return NB_OK; +} + +/* + * XPath: .../ospf/graceful-restart/enabled + * + * Companion to the ospfd callback above. Same semantics: shared + * helper in `ospf6_gr_restart_support_{enable,disable}`, validate- + * time rejection when a GR prepare is in flight, sibling + * `restart-interval` leaf is not touched here. + */ +int ospf6d_ietf_ospf_graceful_restart_enabled_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + bool enabled; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + + enabled = yang_dnode_get_bool(args->dnode, NULL); + + if (args->event == NB_EV_VALIDATE) { + if (!enabled && ospf6->gr_info.prepare_in_progress) { + snprintf(args->errmsg, args->errmsg_len, + "Graceful Restart preparation in progress"); + return NB_ERR_VALIDATION; + } + return NB_OK; + } + + if (args->event != NB_EV_APPLY) + return NB_OK; + + if (enabled) + ospf6_gr_restart_support_enable(ospf6); + else + (void)ospf6_gr_restart_support_disable(ospf6); + return NB_OK; +} + +int ospf6d_ietf_ospf_graceful_restart_enabled_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf6 = ospf6d_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf6) + return NB_OK; + (void)ospf6_gr_restart_support_disable(ospf6); + return NB_OK; +} + +/* + * XPath: .../ospf/graceful-restart/restart-interval + * + * Per-instance grace period. Destroy restores the RFC default (120s, + * which also matches FRR's `OSPF6_DFLT_GRACE_INTERVAL`). + */ +int ospf6d_ietf_ospf_graceful_restart_restart_interval_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + uint16_t period; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + period = yang_dnode_get_uint16(args->dnode, NULL); + ospf6_gr_set_grace_period(ospf6, period); + return NB_OK; +} + +int ospf6d_ietf_ospf_graceful_restart_restart_interval_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + ospf6 = ospf6d_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf6) + return NB_OK; + + ospf6_gr_set_grace_period(ospf6, OSPF6_DFLT_GRACE_INTERVAL); + return NB_OK; +} + +/* + * XPath: .../ospf/graceful-restart/helper-enabled + * + * Companion to the ospfd callback above; same per-router-id caveat. + */ +int ospf6d_ietf_ospf_graceful_restart_helper_enabled_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + ospf6_gr_helper_support_set(ospf6, yang_dnode_get_bool(args->dnode, NULL)); + return NB_OK; +} + +int ospf6d_ietf_ospf_graceful_restart_helper_enabled_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf6 = ospf6d_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf6) + return NB_OK; + ospf6_gr_helper_support_set(ospf6, false); + return NB_OK; +} + +/* + * XPath: .../ospf/graceful-restart/helper-strict-lsa-checking + * + * Same semantics as ospfd: default-true, FRR's writer only emits a + * line when the value is false. + */ +int ospf6d_ietf_ospf_graceful_restart_helper_strict_lsa_checking_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + int ret; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + ospf6_gr_helper_lsacheck_set(ospf6, yang_dnode_get_bool(args->dnode, NULL)); + return NB_OK; +} + +int ospf6d_ietf_ospf_graceful_restart_helper_strict_lsa_checking_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf6 = ospf6d_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf6) + return NB_OK; + ospf6_gr_helper_lsacheck_set(ospf6, true); + return NB_OK; +} + +/* + * RFC 9129 BFD leaves: see the ospfd companion notes. Parameter leaves are + * accepted regardless of `/bfd/enabled` state. They are stored in the YANG + * running datastore while disabled, and applied to the embedded + * `ospf6_interface` BFD state when BFD is enabled. A session refresh is + * achieved via `ospf6_bfd_reg_dereg_all_nbr(oi, true)`. Disable tears every + * session down. + */ +#define OSPF6D_IETF_BFD_MIN_INTERVAL_US (50UL * 1000) +#define OSPF6D_IETF_BFD_MAX_INTERVAL_US (60000UL * 1000) + +static int ospf6d_ietf_bfd_validate_interval_us(uint32_t us, const char *leaf, char *errmsg, + size_t errmsg_len) +{ + if (us % 1000 != 0) { + snprintf(errmsg, errmsg_len, + "FRR BFD %s must be a whole millisecond (multiple of 1000 us); got %u", + leaf, us); + return NB_ERR_VALIDATION; + } + if (us < OSPF6D_IETF_BFD_MIN_INTERVAL_US || us > OSPF6D_IETF_BFD_MAX_INTERVAL_US) { + snprintf(errmsg, errmsg_len, + "FRR BFD %s must be %u..%u us (50..60000 ms); got %u", leaf, + (unsigned int)OSPF6D_IETF_BFD_MIN_INTERVAL_US, + (unsigned int)OSPF6D_IETF_BFD_MAX_INTERVAL_US, us); + return NB_ERR_VALIDATION; + } + return NB_OK; +} + +static void ospf6d_ietf_bfd_sync_config_from_dnode(struct ospf6_interface *oi, + const struct lyd_node *dnode) +{ + oi->bfd_config.detection_multiplier = + yang_dnode_get_uint8(dnode, "local-multiplier"); + oi->bfd_config.min_rx = + yang_dnode_get_uint32(dnode, "required-min-rx-interval") / 1000; + oi->bfd_config.min_tx = + yang_dnode_get_uint32(dnode, "desired-min-tx-interval") / 1000; +} + +/* + * XPath: .../ospf/areas/area/interfaces/interface/bfd + * + * The BFD container is an aggregate. Leaf callbacks maintain daemon + * configuration state, then this finish callback applies the settled state to + * BFD sessions once per transaction. + */ +void ospf6d_ietf_ospf_areas_area_interfaces_interface_bfd_apply_finish(struct nb_cb_apply_finish_args *args) +{ + struct ospf6 *ospf6; + struct interface *ifp; + struct ospf6_interface *oi; + int ret; + bool enabled; + + /* apply_finish is APPLY-only; the literal event is intentional. */ + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, NB_EV_APPLY, args->errmsg, + args->errmsg_len, &ospf6); + if (ret != NB_OK || !ospf6) + return; + ret = ospf6d_ietf_ospf_resolve_interface(ospf6, args->dnode, NB_EV_APPLY, + args->errmsg, args->errmsg_len, + &ifp); + if (ret != NB_OK || !ifp) + return; + oi = ifp->info; + if (!oi) { + oi = ospf6_interface_create(ifp); + if (!oi) + return; + } + + enabled = yang_dnode_get_bool(args->dnode, "enabled"); + ospf6d_ietf_bfd_sync_config_from_dnode(oi, args->dnode); + if (enabled) { + oi->bfd_config.enabled = true; + ospf6_bfd_reg_dereg_all_nbr(oi, true); + } else if (oi->bfd_config.enabled) { + oi->bfd_config.enabled = false; + ospf6_bfd_reg_dereg_all_nbr(oi, false); + } +} + +/* + * XPath: .../ospf/areas/area/interfaces/interface/bfd/enabled + * + * Administrative BFD toggle. Actual session changes are done by the parent + * `/bfd` apply_finish callback after all BFD leaves have settled. + */ +int ospf6d_ietf_ospf_areas_area_interfaces_interface_bfd_enabled_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + struct interface *ifp; + int ret; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, + args->errmsg, args->errmsg_len, + &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + ret = ospf6d_ietf_ospf_resolve_interface(ospf6, args->dnode, + args->event, args->errmsg, + args->errmsg_len, &ifp); + if (ret != NB_OK || !ifp) + return ret; + + return NB_OK; +} + +int ospf6d_ietf_ospf_areas_area_interfaces_interface_bfd_local_multiplier_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + struct interface *ifp; + int ret; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, + args->errmsg, args->errmsg_len, + &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + ret = ospf6d_ietf_ospf_resolve_interface(ospf6, args->dnode, + args->event, args->errmsg, + args->errmsg_len, &ifp); + if (ret != NB_OK || !ifp) + return ret; + + /* APPLY is intentionally a no-op; the parent /bfd apply_finish applies. */ + return NB_OK; +} + +int ospf6d_ietf_ospf_areas_area_interfaces_interface_bfd_desired_min_tx_interval_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + struct interface *ifp; + int ret; + uint32_t us; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, + args->errmsg, args->errmsg_len, + &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + ret = ospf6d_ietf_ospf_resolve_interface(ospf6, args->dnode, + args->event, args->errmsg, + args->errmsg_len, &ifp); + if (ret != NB_OK || !ifp) + return ret; + + us = yang_dnode_get_uint32(args->dnode, NULL); + if (args->event == NB_EV_VALIDATE) + return ospf6d_ietf_bfd_validate_interval_us(us, "desired-min-tx-interval", + args->errmsg, args->errmsg_len); + /* APPLY is intentionally a no-op; the parent /bfd apply_finish applies. */ + return NB_OK; +} + +int ospf6d_ietf_ospf_areas_area_interfaces_interface_bfd_desired_min_tx_interval_destroy(struct nb_cb_destroy_args *args) +{ + /* Deletion restores the YANG default; the parent /bfd apply_finish applies it. */ + return NB_OK; +} + +int ospf6d_ietf_ospf_areas_area_interfaces_interface_bfd_required_min_rx_interval_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + struct interface *ifp; + int ret; + uint32_t us; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, + args->errmsg, args->errmsg_len, + &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + ret = ospf6d_ietf_ospf_resolve_interface(ospf6, args->dnode, + args->event, args->errmsg, + args->errmsg_len, &ifp); + if (ret != NB_OK || !ifp) + return ret; + + us = yang_dnode_get_uint32(args->dnode, NULL); + if (args->event == NB_EV_VALIDATE) + return ospf6d_ietf_bfd_validate_interval_us(us, "required-min-rx-interval", + args->errmsg, args->errmsg_len); + /* APPLY is intentionally a no-op; the parent /bfd apply_finish applies. */ + return NB_OK; +} + +int ospf6d_ietf_ospf_areas_area_interfaces_interface_bfd_required_min_rx_interval_destroy(struct nb_cb_destroy_args *args) +{ + /* Deletion restores the YANG default; the parent /bfd apply_finish applies it. */ + return NB_OK; +} + +/* + * RFC 9129's `authentication/ospfv3-key-chain` lives under + * `choice ospfv3-auth-trailer / case auth-key-chain`. Maps onto + * FRR's per-interface `oi->at_data.keychain` and the + * OSPF6_AUTH_TRAILER_KEYCHAIN flag, mirroring `ipv6 ospf6 + * authentication keychain X` exactly (see + * ospf6_interface.c:3438-3470). Rejects at NB_EV_VALIDATE if the + * interface already carries a MANUAL_KEY -- matches the legacy CLI's + * mutually-exclusive lock. Other authentication leaves (explicit + * key-id / key / crypto-algo, OSPFv3 IPsec SA, OSPFv2-only leaves) + * are marked not-supported via deviation. + */ +static int ospf6d_ietf_ospf_authentication_ospfv3_key_chain_validate(struct interface *ifp, + char *errmsg, + size_t errmsg_len) +{ + struct ospf6_interface *oi = ifp->info; + + if (oi && CHECK_FLAG(oi->at_data.flags, OSPF6_AUTH_TRAILER_MANUAL_KEY)) { + snprintf(errmsg, errmsg_len, + "Manual key configured on %s; remove it before configuring a key chain", + ifp->name); + return NB_ERR_VALIDATION; + } + + return NB_OK; +} + +int ospf6d_ietf_ospf_areas_area_interfaces_interface_authentication_ospfv3_key_chain_modify(struct nb_cb_modify_args *args) +{ + struct ospf6 *ospf6; + struct interface *ifp; + struct ospf6_interface *oi; + int ret; + + ret = ospf6d_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf6); + if (ret != NB_OK || !ospf6) + return ret; + ret = ospf6d_ietf_ospf_resolve_interface(ospf6, args->dnode, args->event, args->errmsg, + args->errmsg_len, &ifp); + if (ret != NB_OK || !ifp) + return ret; + + oi = ifp->info; + if (args->event == NB_EV_VALIDATE) + return ospf6d_ietf_ospf_authentication_ospfv3_key_chain_validate(ifp, args->errmsg, + args->errmsg_len); + + if (args->event != NB_EV_APPLY) + return NB_OK; + + if (!oi) + oi = ospf6_interface_create(ifp); + SET_FLAG(oi->at_data.flags, OSPF6_AUTH_TRAILER_KEYCHAIN); + if (oi->at_data.keychain) + XFREE(MTYPE_OSPF6_AUTH_KEYCHAIN, oi->at_data.keychain); + oi->at_data.keychain = XSTRDUP(MTYPE_OSPF6_AUTH_KEYCHAIN, + yang_dnode_get_string(args->dnode, NULL)); + return NB_OK; +} + +int ospf6d_ietf_ospf_areas_area_interfaces_interface_authentication_ospfv3_key_chain_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf6 *ospf6; + struct interface *ifp; + struct ospf6_interface *oi; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf6 = ospf6d_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf6) + return NB_OK; + ifp = ospf6d_ietf_ospf_interface_from_dnode(ospf6, args->dnode); + if (!ifp) + return NB_OK; + oi = ifp->info; + if (!oi || !CHECK_FLAG(oi->at_data.flags, OSPF6_AUTH_TRAILER_KEYCHAIN)) + return NB_OK; + if (oi->at_data.keychain) + XFREE(MTYPE_OSPF6_AUTH_KEYCHAIN, oi->at_data.keychain); + oi->at_data.flags = 0; + return NB_OK; +} diff --git a/ospf6d/ospf6_nb_notifications.c b/ospf6d/ospf6_nb_notifications.c new file mode 100644 index 000000000000..de660ea92105 --- /dev/null +++ b/ospf6d/ospf6_nb_notifications.c @@ -0,0 +1,395 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * OSPFv3 northbound notifications (RFC 9129 ietf-ospf). + * Copyright (C) 2026 Eric Parsonage + * + * Wires the existing ospf6_neighbor_change / ospf6_interface_change / + * GR helper hooks to the YANG notification dispatcher so mgmtd (and any + * frontend subscribed to it) sees an ietf-ospf event each time an + * OSPFv3 state transitions. + */ + +#include + +#include "debug.h" +#include "if.h" +#include "linklist.h" +#include "log.h" +#include "northbound.h" +#include "vrf.h" +#include "yang.h" +#include "yang_wrappers.h" + +#include "ospf6d/ospf6d.h" +#include "ospf6d/ospf6_area.h" +#include "ospf6d/ospf6_tlv.h" +#include "ospf6d/ospf6_gr.h" +#include "ospf6d/ospf6_interface.h" +#include "ospf6d/ospf6_neighbor.h" +#include "ospf6d/ospf6_top.h" +#include "ospf6_nb.h" + +#define _dbg(fmt, ...) DEBUGD(&nb_dbg_notif, "OSPF6-NOTIF: %s: " fmt, __func__, ##__VA_ARGS__) + +/* + * OSPFv3 NSM state values already match RFC 9129's nbr-state-type 1:1 + * (down=1, attempt=2, init=3, twoway=4, exstart=5, exchange=6, + * loading=7, full=8). + */ +static const int ospf6d_ietf_nbr_state_table[OSPF6_NEIGHBOR_FULL + 1] = { + [OSPF6_NEIGHBOR_DOWN] = 1, /* down */ + [OSPF6_NEIGHBOR_ATTEMPT] = 2, /* attempt */ + [OSPF6_NEIGHBOR_INIT] = 3, /* init */ + [OSPF6_NEIGHBOR_TWOWAY] = 4, /* 2-way */ + [OSPF6_NEIGHBOR_EXSTART] = 5, /* exstart */ + [OSPF6_NEIGHBOR_EXCHANGE] = 6, /* exchange */ + [OSPF6_NEIGHBOR_LOADING] = 7, /* loading */ + [OSPF6_NEIGHBOR_FULL] = 8, /* full */ +}; + +static int ospf6d_ietf_nbr_state_yang(int nsm_state) +{ + int val; + + if (nsm_state < 0 || + (size_t)nsm_state >= array_size(ospf6d_ietf_nbr_state_table)) + return -1; + + val = ospf6d_ietf_nbr_state_table[nsm_state]; + return val ? val : -1; +} + +/* + * OSPFv3 ISM state codes deviate from RFC 9129's `if-state-type`: + * FRR uses 5 for point-to-multipoint and orders DROther=6, BDR=7, DR=8, + * while the RFC orders dr=5, bdr=6, dr-other=7. RFC 9129 has no separate + * P2MP state, so fold it into point-to-point as OSPFv2 does. + * Translate via lookup table. + * + * FRR reserves OSPF6_INTERFACE_NONE (0) for the pre-init lifecycle slot. + * It folds into the RFC's `down` so an interface lifecycle transition + * through that state stays observable through if-state-change rather + * than disappearing into a -1 returned by the default arm. + */ +static const int ospf6d_ietf_if_state_table[OSPF6_INTERFACE_MAX] = { + [OSPF6_INTERFACE_NONE] = 1, /* down */ + [OSPF6_INTERFACE_DOWN] = 1, /* down */ + [OSPF6_INTERFACE_LOOPBACK] = 2, /* loopback */ + [OSPF6_INTERFACE_WAITING] = 3, /* waiting */ + [OSPF6_INTERFACE_POINTTOPOINT] = 4, /* point-to-point */ + [OSPF6_INTERFACE_POINTTOMULTIPOINT] = 4, /* point-to-point */ + [OSPF6_INTERFACE_DR] = 5, /* dr */ + [OSPF6_INTERFACE_BDR] = 6, /* bdr */ + [OSPF6_INTERFACE_DROTHER] = 7, /* dr-other */ +}; + +static int ospf6d_ietf_if_state_yang(int ism_state) +{ + int val; + + if (ism_state < 0 || + (size_t)ism_state >= array_size(ospf6d_ietf_if_state_table)) + return -1; + + val = ospf6d_ietf_if_state_table[ism_state]; + return val ? val : -1; +} + +static void ospf6d_ietf_notif_add_instance_hdr(struct list *args, const char *xpath, + const struct ospf6 *ospf6) +{ + char xpath_arg[XPATH_MAXLEN]; + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/routing-protocol-name", xpath); + listnode_add(args, yang_data_new_string(xpath_arg, + ospf6d_ietf_ospf_instance_name(ospf6))); +} + +static void ospf6d_ietf_notif_add_interface_hdr(struct list *args, const char *xpath, + const struct interface *ifp) +{ + char xpath_arg[XPATH_MAXLEN]; + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/interface/interface", xpath); + listnode_add(args, yang_data_new_string(xpath_arg, ifp->name)); +} + +static void ospf6d_ietf_notif_add_neighbor_hdr(struct list *args, const char *xpath, + const struct ospf6_neighbor *on) +{ + char xpath_arg[XPATH_MAXLEN]; + char buf[INET6_ADDRSTRLEN]; + struct in_addr rid; + + rid.s_addr = on->router_id; + snprintf(xpath_arg, sizeof(xpath_arg), "%s/neighbor-router-id", xpath); + inet_ntop(AF_INET, &rid, buf, sizeof(buf)); + listnode_add(args, yang_data_new_string(xpath_arg, buf)); + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/neighbor-ip-addr", xpath); + inet_ntop(AF_INET6, &on->linklocal_addr, buf, sizeof(buf)); + listnode_add(args, yang_data_new_string(xpath_arg, buf)); +} + +/* + * XPath: /ietf-ospf:nbr-state-change + * + * Emitted on every NSM transition. The OSPF-v3 hook fires after the state + * change has been recorded in `on->state`; the `next_state` and `prev_state` + * arguments come from the hook signature. + */ +static int ospf6d_ietf_nbr_state_change(struct ospf6_neighbor *on, int next_state, int prev_state) +{ + const char *xpath = "/ietf-ospf:nbr-state-change"; + struct list *args; + char xpath_arg[XPATH_MAXLEN]; + int yang_state; + + yang_state = ospf6d_ietf_nbr_state_yang(next_state); + if (yang_state < 0) + return 0; + (void)prev_state; + + if (!on->ospf6_if || !on->ospf6_if->interface || !on->ospf6_if->area || + !on->ospf6_if->area->ospf6) + return 0; + + args = yang_data_list_new(); + ospf6d_ietf_notif_add_instance_hdr(args, xpath, on->ospf6_if->area->ospf6); + ospf6d_ietf_notif_add_interface_hdr(args, xpath, on->ospf6_if->interface); + ospf6d_ietf_notif_add_neighbor_hdr(args, xpath, on); + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/state", xpath); + listnode_add(args, yang_data_new_enum(xpath_arg, yang_state)); + + _dbg("nbr router-id 0x%08x on %s state %d", ntohl(on->router_id), + on->ospf6_if->interface->name, next_state); + + nb_notification_send(xpath, args); + return 0; +} + +/* + * XPath: /ietf-ospf:if-state-change + * + * Emitted on every OSPFv3 ISM transition. The hook fires after the state + * has been swapped in. `old_state` is unused by the RFC notification but + * passed by the hook caller. + */ +static int ospf6d_ietf_if_state_change(struct ospf6_interface *oi, int state, int old_state) +{ + const char *xpath = "/ietf-ospf:if-state-change"; + struct list *args; + char xpath_arg[XPATH_MAXLEN]; + int yang_state; + + yang_state = ospf6d_ietf_if_state_yang(state); + if (yang_state < 0) + return 0; + (void)old_state; + + if (!oi->interface || !oi->area || !oi->area->ospf6) + return 0; + + args = yang_data_list_new(); + ospf6d_ietf_notif_add_instance_hdr(args, xpath, oi->area->ospf6); + ospf6d_ietf_notif_add_interface_hdr(args, xpath, oi->interface); + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/state", xpath); + listnode_add(args, yang_data_new_enum(xpath_arg, yang_state)); + + _dbg("iface %s state %d", oi->interface->name, state); + + nb_notification_send(xpath, args); + return 0; +} + +/* + * Translate FRR's `ospf6_helper_exit_reason` (0..4) into RFC 9129's + * `restart-exit-reason-type` (1..5). FRR's enum order differs from the + * RFC value order: TOPO_CHG=2 maps to topology-changed=5, while + * COMPLETED=4 maps to completed=3. A simple offset does not work. + */ +static const int + ospf6d_ietf_helper_exit_reason_table[OSPF6_GR_HELPER_COMPLETED + 1] = { + [OSPF6_GR_HELPER_EXIT_NONE] = 1, /* none */ + [OSPF6_GR_HELPER_INPROGRESS] = 2, /* in-progress */ + [OSPF6_GR_HELPER_COMPLETED] = 3, /* completed */ + [OSPF6_GR_HELPER_GRACE_TIMEOUT] = 4, /* timed-out */ + [OSPF6_GR_HELPER_TOPO_CHG] = 5, /* topology-changed */ + }; + +static int ospf6d_ietf_helper_exit_reason_yang(int exit_reason) +{ + int val; + + if (exit_reason < 0 || + (size_t)exit_reason >= array_size(ospf6d_ietf_helper_exit_reason_table)) + return -1; + + val = ospf6d_ietf_helper_exit_reason_table[exit_reason]; + return val ? val : -1; +} + +/* + * XPath: /ietf-ospf:restart-status-change + * + * Emit when the local OSPFv3 instance transitions in/out of graceful- + * restart. status follows RFC restart-status-type values. + */ +void ospf6d_ietf_notif_restart_status_change(struct ospf6 *ospf6, int status, int exit_reason) +{ + const char *xpath = "/ietf-ospf:restart-status-change"; + struct list *args; + char xpath_arg[XPATH_MAXLEN]; + int yang_exit; + + if (!ospf6) + return; + + args = yang_data_list_new(); + ospf6d_ietf_notif_add_instance_hdr(args, xpath, ospf6); + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/status", xpath); + listnode_add(args, yang_data_new_enum(xpath_arg, status)); + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/restart-interval", xpath); + listnode_add(args, yang_data_new_uint16(xpath_arg, ospf6->gr_info.grace_period)); + + yang_exit = ospf6d_ietf_helper_exit_reason_yang(exit_reason); + if (yang_exit < 0) { + zlog_warn("%s: unrecognised GR exit reason %d, suppressing notification", + __func__, exit_reason); + list_delete(&args); + return; + } + snprintf(xpath_arg, sizeof(xpath_arg), "%s/exit-reason", xpath); + listnode_add(args, yang_data_new_enum(xpath_arg, yang_exit)); + + _dbg("instance %s gr status %d exit %d", ospf6->name ?: VRF_DEFAULT_NAME, status, + exit_reason); + nb_notification_send(xpath, args); +} + +/* + * XPath: /ietf-ospf:nbr-restart-helper-status-change + * + * Emit when this router enters or leaves helper mode for an OSPFv3 + * neighbour's graceful restart. + */ +void ospf6d_ietf_notif_nbr_restart_helper_status_change(struct ospf6_neighbor *on, int status, + uint16_t age, int exit_reason) +{ + const char *xpath = "/ietf-ospf:nbr-restart-helper-status-change"; + struct list *args; + char xpath_arg[XPATH_MAXLEN]; + int yang_exit; + + if (!on || !on->ospf6_if || !on->ospf6_if->interface || !on->ospf6_if->area || + !on->ospf6_if->area->ospf6) + return; + + args = yang_data_list_new(); + ospf6d_ietf_notif_add_instance_hdr(args, xpath, on->ospf6_if->area->ospf6); + ospf6d_ietf_notif_add_interface_hdr(args, xpath, on->ospf6_if->interface); + ospf6d_ietf_notif_add_neighbor_hdr(args, xpath, on); + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/status", xpath); + listnode_add(args, yang_data_new_enum(xpath_arg, status)); + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/age", xpath); + listnode_add(args, yang_data_new_uint16(xpath_arg, age)); + + yang_exit = ospf6d_ietf_helper_exit_reason_yang(exit_reason); + if (yang_exit < 0) { + zlog_warn("%s: unrecognised GR helper exit reason %d, suppressing notification", + __func__, exit_reason); + list_delete(&args); + return; + } + snprintf(xpath_arg, sizeof(xpath_arg), "%s/exit-reason", xpath); + listnode_add(args, yang_data_new_enum(xpath_arg, yang_exit)); + + _dbg("nbr router-id 0x%08x helper status %d exit %d", ntohl(on->router_id), status, + exit_reason); + nb_notification_send(xpath, args); +} +/* + * XPath: /ietf-ospf:if-rx-bad-packet + * + * Emit when an OSPFv3 packet cannot be parsed on a given interface. + * `src` is the source IPv6 address; the packet-type leaf is omitted + * when unknown (e.g. truncated header). + */ +void ospf6d_ietf_notif_if_rx_bad_packet(struct ospf6_interface *oi, struct in6_addr src, + uint8_t packet_type) +{ + const char *xpath = "/ietf-ospf:if-rx-bad-packet"; + struct list *args; + char xpath_arg[XPATH_MAXLEN]; + char buf[INET6_ADDRSTRLEN]; + + if (!oi || !oi->interface || !oi->area || !oi->area->ospf6) + return; + + args = yang_data_list_new(); + ospf6d_ietf_notif_add_instance_hdr(args, xpath, oi->area->ospf6); + ospf6d_ietf_notif_add_interface_hdr(args, xpath, oi->interface); + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/packet-source", xpath); + inet_ntop(AF_INET6, &src, buf, sizeof(buf)); + listnode_add(args, yang_data_new_string(xpath_arg, buf)); + + if (packet_type >= 1 && packet_type <= 5) { + snprintf(xpath_arg, sizeof(xpath_arg), "%s/packet-type", xpath); + listnode_add(args, yang_data_new_enum(xpath_arg, packet_type)); + } + + _dbg("bad packet on %s from %s type %u", oi->interface->name, buf, packet_type); + nb_notification_send(xpath, args); +} + +/* + * XPath: /ietf-ospf:if-config-error + * + * Emit when an OSPFv3 packet's contents diverge from the local + * interface configuration. `error_name` is the RFC enum identifier + * string. + */ +void ospf6d_ietf_notif_if_config_error(struct ospf6_interface *oi, struct in6_addr src, + uint8_t packet_type, const char *error_name) +{ + const char *xpath = "/ietf-ospf:if-config-error"; + struct list *args; + char xpath_arg[XPATH_MAXLEN]; + char buf[INET6_ADDRSTRLEN]; + + if (!oi || !oi->interface || !oi->area || !oi->area->ospf6 || !error_name) + return; + + args = yang_data_list_new(); + ospf6d_ietf_notif_add_instance_hdr(args, xpath, oi->area->ospf6); + ospf6d_ietf_notif_add_interface_hdr(args, xpath, oi->interface); + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/packet-source", xpath); + inet_ntop(AF_INET6, &src, buf, sizeof(buf)); + listnode_add(args, yang_data_new_string(xpath_arg, buf)); + + if (packet_type >= 1 && packet_type <= 5) { + snprintf(xpath_arg, sizeof(xpath_arg), "%s/packet-type", xpath); + listnode_add(args, yang_data_new_enum(xpath_arg, packet_type)); + } + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/error", xpath); + listnode_add(args, yang_data_new_string(xpath_arg, error_name)); + + _dbg("config error on %s from %s type %u: %s", oi->interface->name, buf, packet_type, + error_name); + nb_notification_send(xpath, args); +} + +void ospf6d_ietf_notif_init(void) +{ + hook_register(ospf6_neighbor_change, ospf6d_ietf_nbr_state_change); + hook_register(ospf6_interface_change, ospf6d_ietf_if_state_change); +} diff --git a/ospf6d/ospf6_nb_rpcs.c b/ospf6d/ospf6_nb_rpcs.c new file mode 100644 index 000000000000..ca450e6708a0 --- /dev/null +++ b/ospf6d/ospf6_nb_rpcs.c @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * OSPFv3 northbound RPC handlers (RFC 9129 ietf-ospf). + * Copyright (C) 2026 Eric Parsonage + */ + +#include + +#include "if.h" +#include "log.h" +#include "northbound.h" +#include "vrf.h" +#include "yang.h" + +#include "ospf6d/ospf6d.h" +#include "ospf6d/ospf6_top.h" +#include "ospf6d/ospf6_interface.h" +#include "ospf6_nb.h" + +/* + * XPath: /ietf-ospf:clear-neighbor + * + * Reset OSPFv3 neighbors on the named instance. Optional `interface` narrows + * the reset to one OSPFv3-running interface; without it, every interface + * bound to the instance is cleared. ospfd registers the same RPC xpath; + * mgmtd fans out to both backends, so if the named instance doesn't belong + * to this daemon we return NB_OK silently. + */ +int ospf6d_ietf_ospf_clear_neighbor_rpc(struct nb_cb_rpc_args *args) +{ + const char *name; + const char *ifname = NULL; + struct ospf6 *ospf6; + struct vrf *vrf; + struct interface *ifp; + + if (!args->input || !yang_dnode_exists(args->input, "routing-protocol-name")) + return NB_OK; + + name = yang_dnode_get_string(args->input, "routing-protocol-name"); + ospf6 = ospf6d_ietf_ospf_lookup_instance(name); + if (!ospf6) + return NB_OK; + + vrf = vrf_lookup_by_id(ospf6->vrf_id); + if (!vrf) + return NB_OK; + + if (yang_dnode_exists(args->input, "interface")) + ifname = yang_dnode_get_string(args->input, "interface"); + + if (ifname) { + ifp = if_lookup_by_name_vrf(ifname, vrf); + if (!ifp || !ifp->info) { + /* See ospfd's matching handler for the NB_ERR_NOT_ + * FOUND vs NB_ERR_RESOURCE choice; same rationale. + */ + snprintf(args->errmsg, args->errmsg_len, "ospf-interface-not-found: %s", + ifname); + return NB_ERR_NOT_FOUND; + } + ospf6_interface_clear(ifp); + return NB_OK; + } + + FOR_ALL_INTERFACES (vrf, ifp) + if (ifp->info) + ospf6_interface_clear(ifp); + return NB_OK; +} + +/* + * XPath: /ietf-ospf:clear-database + * + * Force every OSPFv3 neighbor adjacency on the named instance down and + * reoriginate self-originated LSAs. `ospf6_process_reset` is the helper the + * legacy `clear ipv6 ospf6 process` command uses. + */ +int ospf6d_ietf_ospf_clear_database_rpc(struct nb_cb_rpc_args *args) +{ + const char *name; + struct ospf6 *ospf6; + + if (!args->input || !yang_dnode_exists(args->input, "routing-protocol-name")) + return NB_OK; + + name = yang_dnode_get_string(args->input, "routing-protocol-name"); + ospf6 = ospf6d_ietf_ospf_lookup_instance(name); + if (!ospf6) + return NB_OK; + + ospf6_router_id_update(ospf6, true); + ospf6_process_reset(ospf6); + return NB_OK; +} diff --git a/ospf6d/ospf6_nb_state.c b/ospf6d/ospf6_nb_state.c new file mode 100644 index 000000000000..a3c6422c35a3 --- /dev/null +++ b/ospf6d/ospf6_nb_state.c @@ -0,0 +1,378 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * OSPFv3 northbound operational state. + * Copyright (C) 2026 Eric Parsonage + */ + +#include + +#include "debug.h" +#include "if.h" +#include "linklist.h" +#include "vrf.h" + +#include "ospf6_area.h" +#include "ospf6_proto.h" +#include "ospf6_lsa.h" +#include "ospf6_lsdb.h" +#include "ospf6_nb.h" +#include "ospf6_interface.h" +#include "ospf6_neighbor.h" +#include "ospf6_route.h" +#include "ospf6_top.h" + +static void ospf6d_ietf_interface_key(const struct interface *ifp, char *key, + size_t key_len) +{ + if (vrf_is_backend_netns()) + snprintf(key, key_len, "%s:%s", ifp->vrf->name, ifp->name); + else + snprintf(key, key_len, "%s", ifp->name); +} + +static bool ospf6d_ietf_interface_key_match(const struct interface *ifp, + const char *key) +{ + char ifkey[XPATH_MAXLEN]; + + ospf6d_ietf_interface_key(ifp, ifkey, sizeof(ifkey)); + + return !strcmp(ifkey, key); +} + +static void *ospf6d_ietf_list_next_data(struct list *list, const void *entry) +{ + struct listnode *node; + void *data; + + if (!list) + return NULL; + + if (!entry) { + node = listhead(list); + return node ? listgetdata(node) : NULL; + } + + for (ALL_LIST_ELEMENTS_RO(list, node, data)) { + if (data != entry) + continue; + + node = listnextnode(node); + return node ? listgetdata(node) : NULL; + } + + return NULL; +} + +struct ospf6 *ospf6d_ietf_ospf_lookup_instance(const char *name) +{ + const struct listnode *node; + struct ospf6 *ospf6; + + for (ALL_LIST_ELEMENTS_RO(om6->ospf6, node, ospf6)) + if (!strcmp(ospf6d_ietf_ospf_instance_name(ospf6), name)) + return ospf6; + + return NULL; +} + +static uint32_t ospf6d_ietf_ospf_area_router_count(const struct ospf6_area *area, + uint8_t router_bit) +{ + struct ospf6_route *route; + uint32_t count = 0; + + if (!area->ospf6->brouter_table) + return 0; + + for (route = ospf6_route_head(area->ospf6->brouter_table); route; + route = ospf6_route_next(route)) { + if (route->path.area_id != area->area_id) + continue; + + if (CHECK_FLAG(route->path.router_bits, router_bit)) + count++; + } + + return count; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol + */ +const void *ospf6d_ietf_routing_control_plane_protocol_get_next(struct nb_cb_get_next_args *args) +{ + return ospf6d_ietf_list_next_data(om6->ospf6, args->list_entry); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol + */ +int ospf6d_ietf_routing_control_plane_protocol_get_keys(struct nb_cb_get_keys_args *args) +{ + const struct ospf6 *ospf6 = args->list_entry; + + args->keys->num = 2; + strlcpy(args->keys->key[0], "ietf-ospf:ospfv3", sizeof(args->keys->key[0])); + strlcpy(args->keys->key[1], ospf6d_ietf_ospf_instance_name(ospf6), + sizeof(args->keys->key[1])); + + return NB_OK; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol + */ +const void * +ospf6d_ietf_routing_control_plane_protocol_lookup_entry(struct nb_cb_lookup_entry_args *args) +{ + const char *type = args->keys->key[0]; + + if (strcmp(type, "ietf-ospf:ospfv3")) + return NULL; + + return ospf6d_ietf_ospf_lookup_instance(args->keys->key[1]); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/router-id + */ +struct yang_data *ospf6d_ietf_ospf_router_id_get_elem(struct nb_cb_get_elem_args *args) +{ + const struct ospf6 *ospf6 = args->list_entry; + /* ospf6d stores router IDs as network-byte-order uint32_t values. */ + struct in_addr router_id = { .s_addr = ospf6->router_id }; + + return yang_data_new_ipv4(args->xpath, &router_id); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/statistics/originate-new-lsa-count + */ +struct yang_data * +ospf6d_ietf_ospf_statistics_originate_new_lsa_count_get_elem(struct nb_cb_get_elem_args *args) +{ + const struct ospf6 *ospf6 = args->list_entry; + + return yang_data_new_uint32(args->xpath, ospf6->lsa_originate_count); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/statistics/rx-new-lsas-count + */ +struct yang_data * +ospf6d_ietf_ospf_statistics_rx_new_lsas_count_get_elem(struct nb_cb_get_elem_args *args) +{ + const struct ospf6 *ospf6 = args->list_entry; + + return yang_data_new_uint32(args->xpath, ospf6->rx_lsa_count); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area + */ +const void *ospf6d_ietf_ospf_areas_area_get_next(struct nb_cb_get_next_args *args) +{ + const struct ospf6 *ospf6 = args->parent_list_entry; + + return ospf6d_ietf_list_next_data(ospf6->area_list, args->list_entry); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area + */ +int ospf6d_ietf_ospf_areas_area_get_keys(struct nb_cb_get_keys_args *args) +{ + const struct ospf6_area *area = args->list_entry; + + args->keys->num = 1; + snprintfrr(args->keys->key[0], sizeof(args->keys->key[0]), "%pI4", &area->area_id); + + return NB_OK; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area + */ +const void *ospf6d_ietf_ospf_areas_area_lookup_entry(struct nb_cb_lookup_entry_args *args) +{ + const struct ospf6 *ospf6 = args->parent_list_entry; + const char *key = args->keys->key[0]; + struct ospf6_area *area; + struct listnode *node; + uint32_t area_id; + + if (str2area_id(key, &area_id, &(int){ 0 })) { + DEBUGD(&nb_dbg_cbs_state, "invalid OSPFv3 area-id key: %s", key); + return NULL; + } + + for (ALL_LIST_ELEMENTS_RO(ospf6->area_list, node, area)) + if (area->area_id == area_id) + return area; + + return NULL; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/statistics/spf-runs-count + */ +struct yang_data * +ospf6d_ietf_ospf_areas_area_statistics_spf_runs_count_get_elem(struct nb_cb_get_elem_args *args) +{ + const struct ospf6_area *area = args->list_entry; + + return yang_data_new_uint32(args->xpath, area->spf_calculation); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/statistics/abr-count + */ +struct yang_data * +ospf6d_ietf_ospf_areas_area_statistics_abr_count_get_elem(struct nb_cb_get_elem_args *args) +{ + return yang_data_new_uint32(args->xpath, + ospf6d_ietf_ospf_area_router_count(args->list_entry, + OSPF6_ROUTER_BIT_B)); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/statistics/asbr-count + */ +struct yang_data * +ospf6d_ietf_ospf_areas_area_statistics_asbr_count_get_elem(struct nb_cb_get_elem_args *args) +{ + return yang_data_new_uint32(args->xpath, + ospf6d_ietf_ospf_area_router_count(args->list_entry, + OSPF6_ROUTER_BIT_E)); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/statistics/area-scope-lsa-count + */ +struct yang_data *ospf6d_ietf_ospf_areas_area_statistics_area_scope_lsa_count_get_elem( + struct nb_cb_get_elem_args *args) +{ + const struct ospf6_area *area = args->list_entry; + + return yang_data_new_uint32(args->xpath, area->lsdb->count); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/interfaces/interface + */ +const void * +ospf6d_ietf_ospf_areas_area_interfaces_interface_get_next(struct nb_cb_get_next_args *args) +{ + const struct ospf6_area *area = args->parent_list_entry; + + return ospf6d_ietf_list_next_data(area->if_list, args->list_entry); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/interfaces/interface + */ +int ospf6d_ietf_ospf_areas_area_interfaces_interface_get_keys(struct nb_cb_get_keys_args *args) +{ + const struct ospf6_interface *oi = args->list_entry; + + args->keys->num = 1; + ospf6d_ietf_interface_key(oi->interface, args->keys->key[0], + sizeof(args->keys->key[0])); + + return NB_OK; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/interfaces/interface + */ +const void * +ospf6d_ietf_ospf_areas_area_interfaces_interface_lookup_entry(struct nb_cb_lookup_entry_args *args) +{ + const struct ospf6_area *area = args->parent_list_entry; + const char *name = args->keys->key[0]; + const struct listnode *node; + struct ospf6_interface *oi; + + for (ALL_LIST_ELEMENTS_RO(area->if_list, node, oi)) + if (ospf6d_ietf_interface_key_match(oi->interface, name)) + return oi; + + return NULL; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/interfaces/interface/neighbors/neighbor + */ +const void *ospf6d_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_get_next( + struct nb_cb_get_next_args *args) +{ + const struct ospf6_interface *oi = args->parent_list_entry; + + return ospf6d_ietf_list_next_data(oi->neighbor_list, args->list_entry); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/interfaces/interface/neighbors/neighbor + */ +int ospf6d_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_get_keys( + struct nb_cb_get_keys_args *args) +{ + const struct ospf6_neighbor *on = args->list_entry; + /* ospf6d stores neighbor router IDs in network byte order too. */ + struct in_addr router_id = { .s_addr = on->router_id }; + + args->keys->num = 1; + snprintfrr(args->keys->key[0], sizeof(args->keys->key[0]), "%pI4", &router_id); + + return NB_OK; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/interfaces/interface/neighbors/neighbor + */ +const void *ospf6d_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_lookup_entry( + struct nb_cb_lookup_entry_args *args) +{ + const struct ospf6_interface *oi = args->parent_list_entry; + const char *key = args->keys->key[0]; + const struct listnode *node; + struct ospf6_neighbor *on; + struct in_addr router_id; + + if (inet_pton(AF_INET, key, &router_id) != 1) { + DEBUGD(&nb_dbg_cbs_state, "invalid OSPFv3 neighbor router-id key: %s", key); + return NULL; + } + + for (ALL_LIST_ELEMENTS_RO(oi->neighbor_list, node, on)) + if (on->router_id == router_id.s_addr) + return on; + + return NULL; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/interfaces/interface/neighbors/neighbor/address + */ +struct yang_data * +ospf6d_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_address_get_elem( + struct nb_cb_get_elem_args *args) +{ + const struct ospf6_neighbor *on = args->list_entry; + + return yang_data_new_ipv6(args->xpath, &on->linklocal_addr); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/interfaces/interface/neighbors/neighbor/state + */ +struct yang_data * +ospf6d_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_state_get_elem( + struct nb_cb_get_elem_args *args) +{ + const struct ospf6_neighbor *on = args->list_entry; + + /* OSPF6_NEIGHBOR_* values 1..8 match RFC 9129's enum encoding. */ + return yang_data_new_enum(args->xpath, on->state); +} diff --git a/ospf6d/ospf6_neighbor.c b/ospf6d/ospf6_neighbor.c index a64cec23ea12..827d348479b8 100644 --- a/ospf6d/ospf6_neighbor.c +++ b/ospf6d/ospf6_neighbor.c @@ -285,6 +285,11 @@ static void ospf6_neighbor_state_change(uint8_t next_state, ospf6_bfd_trigger_event(on, prev_state, next_state); } +void ospf6_neighbor_force_down(struct ospf6_neighbor *on) +{ + ospf6_neighbor_state_change(OSPF6_NEIGHBOR_DOWN, on, OSPF6_NEIGHBOR_EVENT_NO_EVENT); +} + /* RFC2328 section 10.4 */ static int need_adjacency(struct ospf6_neighbor *on) { diff --git a/ospf6d/ospf6_neighbor.h b/ospf6d/ospf6_neighbor.h index c2db8c18f83a..63d6cc0a125e 100644 --- a/ospf6d/ospf6_neighbor.h +++ b/ospf6d/ospf6_neighbor.h @@ -200,6 +200,16 @@ struct ospf6_neighbor *ospf6_neighbor_create(uint32_t router_id, struct ospf6_interface *oi); void ospf6_neighbor_delete(struct ospf6_neighbor *on); +/* + * Force the neighbour to OSPF6_NEIGHBOR_DOWN, firing the + * ospf6_neighbor_change hook so that YANG nbr-state-change subscribers + * observe the tear-down. Intended for programmatic teardown paths + * (interface destroy, interface state reset) that delete neighbours + * without going through the inactivity-timer flow. No-op if the + * neighbour is already in DOWN. + */ +void ospf6_neighbor_force_down(struct ospf6_neighbor *on); + void ospf6_neighbor_lladdr_set(struct ospf6_neighbor *on, const struct in6_addr *addr); struct ospf6_if_p2xp_neighcfg *ospf6_if_p2xp_find(struct ospf6_interface *oi, diff --git a/ospf6d/ospf6_route.c b/ospf6d/ospf6_route.c index efd1a47ce785..71d53b70b53e 100644 --- a/ospf6d/ospf6_route.c +++ b/ospf6d/ospf6_route.c @@ -1078,9 +1078,17 @@ struct ospf6_route *ospf6_route_match_next(struct prefix *prefix, void ospf6_route_remove_all(struct ospf6_route_table *table) { struct ospf6_route *route; - for (route = ospf6_route_head(table); route; - route = ospf6_route_next(route)) + + /* + * hook_remove may remove sibling routes from the same table, so do + * not cache route->next across ospf6_route_remove(). Re-read the + * head after each removal and release the iterator lock taken by + * ospf6_route_head() once the table-owned lock has been dropped. + */ + while ((route = ospf6_route_head(table)) != NULL) { ospf6_route_remove(route, table); + ospf6_route_unlock(route); + } } struct ospf6_route_table *ospf6_route_table_create(int s, int t) diff --git a/ospf6d/ospf6_top.c b/ospf6d/ospf6_top.c index 2fb81a02203f..717431a6dd74 100644 --- a/ospf6d/ospf6_top.c +++ b/ospf6d/ospf6_top.c @@ -14,6 +14,7 @@ #include "frrevent.h" #include "command.h" #include "defaults.h" +#include "northbound_cli.h" #include "lib/json.h" #include "lib_errors.h" #include "frrdistance.h" @@ -29,6 +30,7 @@ #include "ospf6_area.h" #include "ospf6_interface.h" #include "ospf6_neighbor.h" +#include "ospf6_nb.h" #include "ospf6_network.h" #include "ospf6_flood.h" @@ -421,6 +423,7 @@ static struct ospf6 *ospf6_create(const char *name) o->write_oi_count = OSPF6_WRITE_INTERFACE_COUNT_DEFAULT; o->ref_bandwidth = OSPF6_REFERENCE_BANDWIDTH; + o->gr_info.grace_period = OSPF6_DFLT_GRACE_INTERVAL; o->distance_table = route_table_init(); @@ -586,6 +589,9 @@ void ospf6_master_init(struct event_loop *mst) om6 = &ospf6_master; om6->ospf6 = list_new(); om6->master = mst; + + /* Hook RFC 9129 ietf-ospf notifications onto the state-change hooks. */ + ospf6d_ietf_notif_init(); } void ospf6_master_delete(void) @@ -675,13 +681,21 @@ bool ospf6_router_id_update(struct ospf6 *ospf6, bool init) return true; } +static int ospf6_ietf_routing_protocol_xpath(char *xpath, size_t size, const struct ospf6 *ospf6) +{ + return snprintf(xpath, size, OSPF6D_IETF_ROUTING_PROTOCOL_XPATH, + ospf6->name ? ospf6->name : VRF_DEFAULT_NAME); +} + /* start ospf6 */ DEFUN_NOSH(router_ospf6, router_ospf6_cmd, "router ospf6 [vrf NAME]", ROUTER_STR OSPF6_STR VRF_CMD_HELP_STR) { struct ospf6 *ospf6; const char *vrf_name = VRF_DEFAULT_NAME; + char xpath[XPATH_MAXLEN]; int idx_vrf = 0; + int ret; if (argv_find(argv, argc, "vrf", &idx_vrf)) { vrf_name = argv[idx_vrf + 1]->arg; @@ -691,6 +705,12 @@ DEFUN_NOSH(router_ospf6, router_ospf6_cmd, "router ospf6 [vrf NAME]", if (ospf6 == NULL) ospf6 = ospf6_instance_create(vrf_name); + ospf6_ietf_routing_protocol_xpath(xpath, sizeof(xpath), ospf6); + nb_cli_enqueue_change(vty, xpath, NB_OP_CREATE, NULL); + ret = nb_cli_apply_changes(vty, NULL); + if (ret != CMD_SUCCESS) + return ret; + /* set current ospf point. */ VTY_PUSH_CONTEXT(OSPF6_NODE, ospf6); @@ -703,7 +723,9 @@ DEFUN(no_router_ospf6, no_router_ospf6_cmd, "no router ospf6 [vrf NAME]", { struct ospf6 *ospf6; const char *vrf_name = VRF_DEFAULT_NAME; + char xpath[XPATH_MAXLEN]; int idx_vrf = 0; + int ret = CMD_SUCCESS; if (argv_find(argv, argc, "vrf", &idx_vrf)) { vrf_name = argv[idx_vrf + 1]->arg; @@ -713,16 +735,19 @@ DEFUN(no_router_ospf6, no_router_ospf6_cmd, "no router ospf6 [vrf NAME]", if (ospf6 == NULL) vty_out(vty, "OSPFv3 is not configured\n"); else { + ospf6_ietf_routing_protocol_xpath(xpath, sizeof(xpath), ospf6); if (ospf6->gr_info.restart_support) ospf6_gr_nvm_delete(ospf6); ospf6_delete(&ospf6); + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + ret = nb_cli_apply_changes_clear_pending(vty, "%s", xpath); } /* return to config node . */ VTY_PUSH_CONTEXT_NULL(CONFIG_NODE); - return CMD_SUCCESS; + return ret; } static void ospf6_db_clear(struct ospf6 *ospf6) @@ -757,7 +782,7 @@ static void ospf6_db_clear(struct ospf6 *ospf6) ospf6_route_remove_all(ospf6->brouter_table); } -static void ospf6_process_reset(struct ospf6 *ospf6) +void ospf6_process_reset(struct ospf6 *ospf6) { struct interface *ifp; struct vrf *vrf = vrf_lookup_by_id(ospf6->vrf_id); @@ -798,8 +823,57 @@ DEFPY (clear_router_ospf6, return CMD_SUCCESS; } +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/explicit-router-id + * + * Build the absolute ietf-ospf explicit-router-id xpath for the given OSPFv3 + * instance. The `router ospf6` block is not yet converted to YANG, so the + * instance keys (type + name) come from the FRR-side context rather than a + * vty xpath context push. + */ +static int ospf6_router_id_xpath(char *xpath, size_t size, const struct ospf6 *o) +{ + return snprintf(xpath, size, + OSPF6D_IETF_ROUTING_PROTOCOL_XPATH "/ietf-ospf:ospf/explicit-router-id", + o->name ? o->name : VRF_DEFAULT_NAME); +} + +/* + * Build an absolute ietf-ospf instance-level leaf xpath for the given OSPFv3 + * instance. The `router ospf6` block is not yet converted to YANG, so the + * instance keys come from FRR-side context rather than a vty xpath push. + * Returns -1 on truncation. + */ +int ospf6_per_instance_xpath(char *xpath, size_t size, const struct ospf6 *o, const char *leaf) +{ + int ret; + + if (!o || !leaf) + return -1; + ret = snprintf(xpath, size, OSPF6D_IETF_ROUTING_PROTOCOL_XPATH "/ietf-ospf:ospf%s", + o->name ? o->name : VRF_DEFAULT_NAME, leaf); + if (ret < 0 || (size_t)ret >= size) + return -1; + return 0; +} + +/* + * Mirror the legacy advisory: if the static value and the runtime router-id + * disagree after apply, the change was staged but adjacencies must be cleared + * for it to take effect. ospf6_router_id_update() encodes this by returning + * false, but the NB callback can't print to vty, so the DEFPY_YANG wrapper + * recomputes the condition from the post-apply state. + */ +static void ospf6_router_id_change_advise(struct vty *vty, const struct ospf6 *o) +{ + if (o->router_id_static == o->router_id) + return; + vty_out(vty, + "For this router-id change to take effect run the \"clear ipv6 ospf6 process\" command\n"); +} + /* change Router_ID commands. */ -DEFUN(ospf6_router_id, +DEFPY_YANG (ospf6_router_id, ospf6_router_id_cmd, "ospf6 router-id A.B.C.D", OSPF6_STR @@ -807,32 +881,20 @@ DEFUN(ospf6_router_id, V4NOTATION_STR) { VTY_DECLVAR_CONTEXT(ospf6, o); - int idx = 0; + char xpath[XPATH_MAXLEN]; int ret; - const char *router_id_str; - uint32_t router_id; - argv_find(argv, argc, "A.B.C.D", &idx); - router_id_str = argv[idx]->arg; + ospf6_router_id_xpath(xpath, sizeof(xpath), o); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, router_id_str); - ret = inet_pton(AF_INET, router_id_str, &router_id); - if (ret == 0) { - vty_out(vty, "malformed OSPF Router-ID: %s\n", router_id_str); - return CMD_SUCCESS; - } + ret = nb_cli_apply_changes(vty, NULL); + if (ret == CMD_SUCCESS) + ospf6_router_id_change_advise(vty, o); - o->router_id_static = router_id; - - if (ospf6_router_id_update(o, false)) - ospf6_process_reset(o); - else - vty_out(vty, - "For this router-id change to take effect run the \"clear ipv6 ospf6 process\" command\n"); - - return CMD_SUCCESS; + return ret; } -DEFUN(no_ospf6_router_id, +DEFPY_YANG (no_ospf6_router_id, no_ospf6_router_id_cmd, "no ospf6 router-id [A.B.C.D]", NO_STR OSPF6_STR @@ -840,17 +902,17 @@ DEFUN(no_ospf6_router_id, V4NOTATION_STR) { VTY_DECLVAR_CONTEXT(ospf6, o); + char xpath[XPATH_MAXLEN]; + int ret; - o->router_id_static = 0; - + ospf6_router_id_xpath(xpath, sizeof(xpath), o); + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); - if (ospf6_router_id_update(o, false)) - ospf6_process_reset(o); - else - vty_out(vty, - "For this router-id change to take effect run the \"clear ipv6 ospf6 process\" command\n"); + ret = nb_cli_apply_changes(vty, NULL); + if (ret == CMD_SUCCESS) + ospf6_router_id_change_advise(vty, o); - return CMD_SUCCESS; + return ret; } DEFUN (ospf6_log_adjacency_changes, @@ -982,43 +1044,40 @@ DEFUN (no_ospf6_timers_lsa, } -DEFUN (ospf6_distance, +DEFPY_YANG (ospf6_distance, ospf6_distance_cmd, - "distance (1-255)", + "distance (1-255)$distance", "Administrative distance\n" "OSPF6 Administrative distance\n") { VTY_DECLVAR_CONTEXT(ospf6, o); - uint8_t distance; + char xpath[XPATH_MAXLEN]; - distance = atoi(argv[1]->arg); - if (o->distance_all != distance) { - o->distance_all = distance; - ospf6_restart_spf(o); - } - - return CMD_SUCCESS; + if (ospf6_per_instance_xpath(xpath, sizeof(xpath), o, "/preference/all") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, distance_str); + return nb_cli_apply_changes(vty, NULL); } -DEFUN (no_ospf6_distance, +DEFPY_YANG (no_ospf6_distance, no_ospf6_distance_cmd, - "no distance (1-255)", + "no distance (1-255)$distance", NO_STR "Administrative distance\n" "OSPF6 Administrative distance\n") { VTY_DECLVAR_CONTEXT(ospf6, o); + char xpath[XPATH_MAXLEN]; - if (o->distance_all) { - o->distance_all = 0; - ospf6_restart_spf(o); - } - return CMD_SUCCESS; + if (ospf6_per_instance_xpath(xpath, sizeof(xpath), o, "/preference/all") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); } -DEFUN (ospf6_distance_ospf6, +DEFPY_YANG (ospf6_distance_ospf6, ospf6_distance_ospf6_cmd, - "distance ospf6 {intra-area (1-255)|inter-area (1-255)|external (1-255)}", + "distance ospf6 {intra-area (1-255)$intra|inter-area (1-255)$inter|external (1-255)$external}", "Administrative distance\n" "OSPF6 administrative distance\n" "Intra-area routes\n" @@ -1029,27 +1088,35 @@ DEFUN (ospf6_distance_ospf6, "Distance for external routes\n") { VTY_DECLVAR_CONTEXT(ospf6, o); - int idx = 0; + char xpath[XPATH_MAXLEN]; - o->distance_intra = 0; - o->distance_inter = 0; - o->distance_external = 0; + if (ospf6_per_instance_xpath(xpath, sizeof(xpath), o, "/preference/intra-area") != 0) + return CMD_WARNING_CONFIG_FAILED; + if (intra) + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, intra_str); + else + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); - if (argv_find(argv, argc, "intra-area", &idx)) - o->distance_intra = atoi(argv[idx + 1]->arg); - idx = 0; - if (argv_find(argv, argc, "inter-area", &idx)) - o->distance_inter = atoi(argv[idx + 1]->arg); - idx = 0; - if (argv_find(argv, argc, "external", &idx)) - o->distance_external = atoi(argv[idx + 1]->arg); + if (ospf6_per_instance_xpath(xpath, sizeof(xpath), o, "/preference/inter-area") != 0) + return CMD_WARNING_CONFIG_FAILED; + if (inter) + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, inter_str); + else + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); - return CMD_SUCCESS; + if (ospf6_per_instance_xpath(xpath, sizeof(xpath), o, "/preference/external") != 0) + return CMD_WARNING_CONFIG_FAILED; + if (external) + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, external_str); + else + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + + return nb_cli_apply_changes(vty, NULL); } -DEFUN (no_ospf6_distance_ospf6, +DEFPY_YANG (no_ospf6_distance_ospf6, no_ospf6_distance_ospf6_cmd, - "no distance ospf6 [{intra-area [(1-255)]|inter-area [(1-255)]|external [(1-255)]}]", + "no distance ospf6 [{intra-area$intra [(1-255)]|inter-area$inter [(1-255)]|external$external [(1-255)]}]", NO_STR "Administrative distance\n" "OSPF6 distance\n" @@ -1061,16 +1128,27 @@ DEFUN (no_ospf6_distance_ospf6, "Distance for external routes\n") { VTY_DECLVAR_CONTEXT(ospf6, o); - int idx = 0; - - if (argv_find(argv, argc, "intra-area", &idx) || argc == 3) - idx = o->distance_intra = 0; - if (argv_find(argv, argc, "inter-area", &idx) || argc == 3) - idx = o->distance_inter = 0; - if (argv_find(argv, argc, "external", &idx) || argc == 3) - o->distance_external = 0; - - return CMD_SUCCESS; + char xpath[XPATH_MAXLEN]; + bool all_scopes = (!intra && !inter && !external); + + if (intra || all_scopes) { + if (ospf6_per_instance_xpath(xpath, sizeof(xpath), o, "/preference/intra-area") != + 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + } + if (inter || all_scopes) { + if (ospf6_per_instance_xpath(xpath, sizeof(xpath), o, "/preference/inter-area") != + 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + } + if (external || all_scopes) { + if (ospf6_per_instance_xpath(xpath, sizeof(xpath), o, "/preference/external") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + } + return nb_cli_apply_changes(vty, NULL); } DEFUN (ospf6_stub_router_admin, @@ -1144,33 +1222,54 @@ static void ospf6_maxpath_set(struct ospf6 *ospf6, uint16_t paths) ospf6_restart_spf(ospf6); } -/* Ospf Maximum-paths config support */ -DEFUN(ospf6_max_multipath, +/* + * Ospf Maximum-paths config support. + * + * RFC 9129 models the limit as `/spf-control/paths` (uint16, range + * 1..65535), which covers FRR's MULTIPATH_NUM cap for every supported + * build. Routes through the ietf-ospf YANG callback unconditionally; + * the legacy direct-mutation path stays only as the fallback when no + * OSPFv3 instance owns the xpath context. + */ +DEFPY_YANG(ospf6_max_multipath, ospf6_max_multipath_cmd, - "maximum-paths " CMD_RANGE_STR(1, MULTIPATH_NUM), + "maximum-paths (1-65535)$maxpaths", "Max no of multiple paths for ECMP support\n" "Number of paths\n") { - VTY_DECLVAR_CONTEXT(ospf6, ospf6); - int idx_number = 1; - int maximum_paths = strtol(argv[idx_number]->arg, NULL, 10); + VTY_DECLVAR_CONTEXT(ospf6, o); + char xpath[XPATH_MAXLEN]; - ospf6_maxpath_set(ospf6, maximum_paths); + if (maxpaths > MULTIPATH_NUM) { + vty_out(vty, "%% maximum-paths exceeds platform max %u\n", MULTIPATH_NUM); + return CMD_WARNING_CONFIG_FAILED; + } + if (ospf6_per_instance_xpath(xpath, sizeof(xpath), o, "/spf-control/paths") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, maxpaths_str); + return nb_cli_apply_changes(vty, NULL); + } + + ospf6_maxpath_set(o, (uint16_t)maxpaths); return CMD_SUCCESS; } -DEFUN(no_ospf6_max_multipath, +DEFPY_YANG(no_ospf6_max_multipath, no_ospf6_max_multipath_cmd, - "no maximum-paths [" CMD_RANGE_STR(1, MULTIPATH_NUM)"]", + "no maximum-paths [(1-65535)]", NO_STR "Max no of multiple paths for ECMP support\n" "Number of paths\n") { - VTY_DECLVAR_CONTEXT(ospf6, ospf6); + VTY_DECLVAR_CONTEXT(ospf6, o); + char xpath[XPATH_MAXLEN]; - ospf6_maxpath_set(ospf6, MULTIPATH_NUM); + if (ospf6_per_instance_xpath(xpath, sizeof(xpath), o, "/spf-control/paths") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } + ospf6_maxpath_set(o, MULTIPATH_NUM); return CMD_SUCCESS; } diff --git a/ospf6d/ospf6_top.h b/ospf6d/ospf6_top.h index 7b4a323ee6e9..1d421734111b 100644 --- a/ospf6d/ospf6_top.h +++ b/ospf6d/ospf6_top.h @@ -168,6 +168,9 @@ struct ospf6 { struct timeval ts_spf_duration; /* Execution time of last SPF */ unsigned int last_spf_reason; /* Last SPF reason */ + uint32_t lsa_originate_count; /* Number of new LSAs originated. */ + uint32_t rx_lsa_count; /* Number of new LSAs received. */ + int fd; /* Threads */ struct event *t_spf_calc; /* SPF calculation timer. */ @@ -241,7 +244,10 @@ extern void install_element_ospf6_clear_process(void); extern void ospf6_top_init(void); extern void ospf6_delete(struct ospf6 **o); extern bool ospf6_router_id_update(struct ospf6 *ospf6, bool init); +extern void ospf6_process_reset(struct ospf6 *ospf6); void ospf6_restart_spf(struct ospf6 *ospf6); +extern int ospf6_per_instance_xpath(char *xpath, size_t size, const struct ospf6 *o, + const char *leaf); extern void ospf6_maxage_remove(struct ospf6 *o); extern struct ospf6 *ospf6_instance_create(const char *name); diff --git a/ospf6d/subdir.am b/ospf6d/subdir.am index a7bd94bd7704..3f80f846d7d8 100644 --- a/ospf6d/subdir.am +++ b/ospf6d/subdir.am @@ -29,6 +29,11 @@ ospf6d_libospf6_a_SOURCES = \ ospf6d/ospf6_lsdb.c \ ospf6d/ospf6_message.c \ ospf6d/ospf6_neighbor.c \ + ospf6d/ospf6_nb.c \ + ospf6d/ospf6_nb_config.c \ + ospf6d/ospf6_nb_notifications.c \ + ospf6d/ospf6_nb_rpcs.c \ + ospf6d/ospf6_nb_state.c \ ospf6d/ospf6_network.c \ ospf6d/ospf6_proto.c \ ospf6d/ospf6_route.c \ @@ -53,6 +58,7 @@ noinst_HEADERS += \ ospf6d/ospf6_lsdb.h \ ospf6d/ospf6_message.h \ ospf6d/ospf6_neighbor.h \ + ospf6d/ospf6_nb.h \ ospf6d/ospf6_network.h \ ospf6d/ospf6_proto.h \ ospf6d/ospf6_route.h \ diff --git a/ospfd/ospf_abr.c b/ospfd/ospf_abr.c index 7baaad809eb5..267d45adada1 100644 --- a/ospfd/ospf_abr.c +++ b/ospfd/ospf_abr.c @@ -35,6 +35,7 @@ #include "ospfd/ospf_zebra.h" #include "ospfd/ospf_dump.h" #include "ospfd/ospf_errors.h" +#include "ospf_nb.h" static struct ospf_area_range *ospf_area_range_new(struct prefix_ipv4 *p) { @@ -446,6 +447,9 @@ void ospf_abr_nssa_check_status(struct ospf *ospf) == OSPF_NSSA_TRANSLATE_DISABLED) ospf_asbr_status_update(ospf, --ospf->redistribute); + + /* RFC 9129 ietf-ospf:nssa-translator-status-change. */ + ospfd_ietf_notif_nssa_translator_state_change(area); } } } diff --git a/ospfd/ospf_bfd.c b/ospfd/ospf_bfd.c index f866638afe79..d149fb5f29c6 100644 --- a/ospfd/ospf_bfd.c +++ b/ospfd/ospf_bfd.c @@ -30,6 +30,7 @@ #include "ospf_dump.h" #include "ospf_vty.h" #include "ospf_quicknbr.h" +#include "northbound_cli.h" DEFINE_MTYPE_STATIC(OSPFD, BFD_CONFIG, "BFD configuration data"); DEFINE_MTYPE_STATIC(OSPFD, OSPF_BFD_SESSION_ENTRY, "OSPF BFD session entry"); @@ -144,7 +145,7 @@ void ospf_bfd_trigger_event(struct ospf_neighbor *nbr, int old_state, int state) /* In quick neighbor mode, ignore the neighbor state changes. Just keep the session * installed to allow quick neighbor re-add */ - if (!oip->bfd_config || oip->bfd_config->quick) + if (!oip->bfd_config || !oip->bfd_config->enabled || oip->bfd_config->quick) return; if ((old_state < NSM_TwoWay) && (state >= NSM_TwoWay)) @@ -211,7 +212,8 @@ static void ospf_bfd_session_change(struct bfd_session_params *bsp, IF_NAME(oi), &entry->endpoint); } - if (oi && entry && oip && oip->bfd_config && oip->bfd_config->quick) + if (oi && entry && oip && oip->bfd_config && oip->bfd_config->enabled && + oip->bfd_config->quick) ospf_qn_add(oi, &entry->endpoint); } } @@ -223,7 +225,7 @@ void ospf_neighbor_bfd_apply(struct ospf_neighbor *nbr) struct ospf_bfd_session_entry *entry; /* BFD configuration was removed. */ - if (oip->bfd_config == NULL) { + if (oip->bfd_config == NULL || !oip->bfd_config->enabled) { ospf_neighbor_bfd_clear(nbr); return; } @@ -284,7 +286,7 @@ void ospf_neighbor_bfd_clear(struct ospf_neighbor *nbr) * IMPORTANT: the neighbor object is being freed, so we must drop * the weak pointer. */ - if (oip && oip->bfd_config && oip->bfd_config->quick) { + if (oip && oip->bfd_config && oip->bfd_config->enabled && oip->bfd_config->quick) { entry->nbr = NULL; if (entry->bsp) bfd_sess_install(entry->bsp); @@ -363,7 +365,7 @@ static void ospf_bfd_if_prune_nonquick(struct ospf_interface *oi) } } -static void ospf_interface_bfd_apply(struct interface *ifp) +void ospf_interface_bfd_apply(struct interface *ifp) { struct ospf_interface *oi; struct route_table *nbrs; @@ -386,10 +388,9 @@ static void ospf_interface_bfd_apply(struct interface *ifp) } } -static void ospf_interface_enable_bfd(struct interface *ifp, bool quick) +struct bfd_configuration *ospf_interface_bfd_config_get(struct interface *ifp) { struct ospf_if_params *oip = IF_DEF_PARAMS(ifp); - bool old_quick = false; if (!oip->bfd_config) { /* Allocate memory for configurations and set defaults. */ @@ -397,9 +398,21 @@ static void ospf_interface_enable_bfd(struct interface *ifp, bool quick) oip->bfd_config->detection_multiplier = BFD_DEF_DETECT_MULT; oip->bfd_config->min_rx = BFD_DEF_MIN_RX; oip->bfd_config->min_tx = BFD_DEF_MIN_TX; - } else + } + + return oip->bfd_config; +} + +void ospf_interface_enable_bfd(struct interface *ifp, bool quick) +{ + struct ospf_if_params *oip = IF_DEF_PARAMS(ifp); + bool old_quick = false; + + if (oip->bfd_config) old_quick = oip->bfd_config->quick; + ospf_interface_bfd_config_get(ifp); + oip->bfd_config->enabled = true; oip->bfd_config->quick = quick; /* Remove any down sessions kept alive for quick mode if quick @@ -424,7 +437,10 @@ static void ospf_interface_enable_bfd(struct interface *ifp, bool quick) void ospf_interface_disable_bfd(struct interface *ifp, struct ospf_if_params *oip) { - XFREE(MTYPE_BFD_CONFIG, oip->bfd_config); + if (!oip->bfd_config) + return; + + oip->bfd_config->enabled = false; ospf_interface_bfd_apply(ifp); /* Ensure any interface-owned entries are removed too. */ { @@ -440,12 +456,24 @@ void ospf_interface_disable_bfd(struct interface *ifp, } } +void ospf_interface_bfd_free_config(struct interface *ifp, struct ospf_if_params *oip) +{ + if (!oip->bfd_config) + return; + + ospf_interface_disable_bfd(ifp, oip); + XFREE(MTYPE_BFD_CONFIG, oip->bfd_config); +} + /* * ospf_bfd_write_config - Write the interface BFD configuration. */ void ospf_bfd_write_config(struct vty *vty, const struct ospf_if_params *params __attribute__((unused))) { + if (!params->bfd_config || !params->bfd_config->enabled) + return; + #if HAVE_BFDD == 0 if (params->bfd_config->detection_multiplier != BFD_DEF_DETECT_MULT || params->bfd_config->min_rx != BFD_DEF_MIN_RX @@ -469,7 +497,7 @@ void ospf_interface_bfd_show(struct vty *vty, const struct interface *ifp, struct bfd_configuration *bfd_config = params->bfd_config; struct json_object *json_bfd; - if (bfd_config == NULL) + if (bfd_config == NULL || !bfd_config->enabled) return; if (json) { @@ -488,7 +516,15 @@ void ospf_interface_bfd_show(struct vty *vty, const struct interface *ifp, bfd_config->min_tx); } -DEFUN (ip_ospf_bfd, +/* + * `ip ospf bfd` maps onto RFC 9129 `/bfd/enabled`. The `quick` flag + * has no YANG counterpart (FRR-specific quick-establishment mode) so + * the shim splits: the bare form routes through YANG when the + * interface is in an area; `[quick]` and not-yet-bound interfaces stay + * on the legacy direct mutation path so existing behaviour is + * preserved. + */ +DEFUN_YANG (ip_ospf_bfd, ip_ospf_bfd_cmd, "ip ospf bfd [quick]", "IP Information\n" @@ -497,11 +533,28 @@ DEFUN (ip_ospf_bfd, "Quick neighbor establishment mode\n") { VTY_DECLVAR_CONTEXT(interface, ifp); - ospf_interface_enable_bfd(ifp, argc >= 4); + bool has_quick = (argc >= 4); + char xpath[XPATH_MAXLEN]; + + if (!has_quick && ospf_per_iface_xpath(xpath, sizeof(xpath), ifp, "/bfd/enabled") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "true"); + return nb_cli_apply_changes(vty, NULL); + } + + ospf_interface_enable_bfd(ifp, has_quick); ospf_interface_bfd_apply(ifp); return CMD_SUCCESS; } +/* + * The parametrised `ip ospf bfd N N N [quick]` form keeps its legacy + * DEFUN shape because the surrounding `#if HAVE_BFDD` switches between + * DEFUN_HIDDEN and DEFUN at the macro level; the clippy scanner can't + * cope with CPP directives within macro arg lists, so the DEFPY-family + * conversion can't apply here. The body still routes through YANG + * when the interface is in an area and `quick` is absent, mirroring + * the simpler `ip ospf bfd` form above. + */ #if HAVE_BFDD > 0 DEFUN_HIDDEN( #else @@ -523,15 +576,34 @@ DEFUN( int idx_number = 3; int idx_number_2 = 4; int idx_number_3 = 5; + bool has_quick = (argc >= 7); + uint32_t mult = strtol(argv[idx_number]->arg, NULL, 10); + uint32_t rx_ms = strtol(argv[idx_number_2]->arg, NULL, 10); + uint32_t tx_ms = strtol(argv[idx_number_3]->arg, NULL, 10); + char xpath_base[XPATH_MAXLEN]; + char xpath[XPATH_MAXLEN + 64]; + char val[32]; + + if (!has_quick && ospf_per_iface_xpath(xpath_base, sizeof(xpath_base), ifp, "/bfd") == 0) { + snprintf(xpath, sizeof(xpath), "%s/enabled", xpath_base); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "true"); + snprintf(xpath, sizeof(xpath), "%s/local-multiplier", xpath_base); + snprintf(val, sizeof(val), "%u", mult); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, val); + snprintf(xpath, sizeof(xpath), "%s/required-min-rx-interval", xpath_base); + snprintf(val, sizeof(val), "%u", rx_ms * 1000); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, val); + snprintf(xpath, sizeof(xpath), "%s/desired-min-tx-interval", xpath_base); + snprintf(val, sizeof(val), "%u", tx_ms * 1000); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, val); + return nb_cli_apply_changes(vty, NULL); + } - ospf_interface_enable_bfd(ifp, argc >= 7); - + ospf_interface_enable_bfd(ifp, has_quick); params = IF_DEF_PARAMS(ifp); - params->bfd_config->detection_multiplier = - strtol(argv[idx_number]->arg, NULL, 10); - params->bfd_config->min_rx = strtol(argv[idx_number_2]->arg, NULL, 10); - params->bfd_config->min_tx = strtol(argv[idx_number_3]->arg, NULL, 10); - + params->bfd_config->detection_multiplier = mult; + params->bfd_config->min_rx = rx_ms; + params->bfd_config->min_tx = tx_ms; ospf_interface_bfd_apply(ifp); return CMD_SUCCESS; @@ -551,7 +623,7 @@ DEFUN (ip_ospf_bfd_prof, int idx_prof = 4; params = IF_DEF_PARAMS(ifp); - if (!params->bfd_config) { + if (!params->bfd_config || !params->bfd_config->enabled) { vty_out(vty, "ip ospf bfd has not been set\n"); return CMD_WARNING; } @@ -577,7 +649,7 @@ DEFUN (no_ip_ospf_bfd_prof, struct ospf_if_params *params; params = IF_DEF_PARAMS(ifp); - if (!params->bfd_config) + if (!params->bfd_config || !params->bfd_config->enabled) return CMD_SUCCESS; params->bfd_config->profile[0] = 0; @@ -586,6 +658,11 @@ DEFUN (no_ip_ospf_bfd_prof, return CMD_SUCCESS; } +/* + * `no ip ospf bfd` -- same #if-inside-grammar problem as the param + * form, so this stays a DEFUN. The body routes through YANG when the + * interface is in an area. + */ DEFUN (no_ip_ospf_bfd, no_ip_ospf_bfd_cmd, #if HAVE_BFDD > 0 @@ -606,6 +683,13 @@ DEFUN (no_ip_ospf_bfd, ) { VTY_DECLVAR_CONTEXT(interface, ifp); + char xpath[XPATH_MAXLEN]; + + if (ospf_per_iface_xpath(xpath, sizeof(xpath), ifp, "/bfd/enabled") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } + ospf_interface_disable_bfd(ifp, IF_DEF_PARAMS(ifp)); return CMD_SUCCESS; } diff --git a/ospfd/ospf_bfd.h b/ospfd/ospf_bfd.h index 2c23a88444ed..2865a11eb474 100644 --- a/ospfd/ospf_bfd.h +++ b/ospfd/ospf_bfd.h @@ -27,12 +27,32 @@ extern void ospf_interface_bfd_show(struct vty *vty, const struct interface *ifp, struct json_object *json); -/** - * Disables interface BFD configuration and remove settings from all peers. - */ +/* Disable interface BFD sessions while preserving configured parameters. */ extern void ospf_interface_disable_bfd(struct interface *ifp, struct ospf_if_params *oip); +/* Free interface BFD configuration during interface parameter teardown. */ +extern void ospf_interface_bfd_free_config(struct interface *ifp, struct ospf_if_params *oip); + +/* Allocate interface BFD configuration with FRR defaults if needed. */ +extern struct bfd_configuration *ospf_interface_bfd_config_get(struct interface *ifp); + +/* + * Enables interface BFD sessions, allocating `bfd_config` with FRR defaults + * on first call. `quick` controls FRR's quick-establishment + * mode (no YANG counterpart; the RFC 9129 northbound only flips the + * `enabled` leaf, so it always passes `false`). Promoted from static + * to extern so the RFC 9129 `/bfd/enabled` callback and the legacy + * `ip ospf bfd` CLI share the same allocation path. + */ +extern void ospf_interface_enable_bfd(struct interface *ifp, bool quick); + +/* + * Push BFD configuration changes to live sessions on this interface. + * Idempotent. + */ +extern void ospf_interface_bfd_apply(struct interface *ifp); + /** * Create/update BFD session for this OSPF neighbor. */ diff --git a/ospfd/ospf_gr.c b/ospfd/ospf_gr.c index 860d247a55a0..6ca2d1d74291 100644 --- a/ospfd/ospf_gr.c +++ b/ospfd/ospf_gr.c @@ -31,13 +31,15 @@ #include "ospfd/ospf_gr.h" #include "ospfd/ospf_errors.h" #include "ospfd/ospf_dump.h" +#include "ospfd/ospf_vty.h" +#include "ospf_nb.h" +#include "northbound_cli.h" #include "ospfd/ospf_gr_clippy.c" static void ospf_gr_grace_period_expired(struct event *event); /* Lookup self-originated Grace-LSA in the LSDB. */ -static struct ospf_lsa *ospf_gr_lsa_lookup(struct ospf *ospf, - struct ospf_area *area) +static struct ospf_lsa *ospf_gr_lsa_lookup(struct ospf *ospf, struct ospf_area *area) { struct ospf_lsa *lsa; struct in_addr lsa_id; @@ -45,17 +47,14 @@ static struct ospf_lsa *ospf_gr_lsa_lookup(struct ospf *ospf, lsa_id_host_byte_order = SET_OPAQUE_LSID(OPAQUE_TYPE_GRACE_LSA, 0); lsa_id.s_addr = htonl(lsa_id_host_byte_order); - lsa = ospf_lsa_lookup(ospf, area, OSPF_OPAQUE_LINK_LSA, lsa_id, - ospf->router_id); + lsa = ospf_lsa_lookup(ospf, area, OSPF_OPAQUE_LINK_LSA, lsa_id, ospf->router_id); return lsa; } /* Fill in fields of the Grace-LSA that is being originated. */ -static void ospf_gr_lsa_body_set(struct ospf_gr_info *gr_info, - struct ospf_interface *oi, - enum ospf_gr_restart_reason reason, - struct stream *s) +static void ospf_gr_lsa_body_set(struct ospf_gr_info *gr_info, struct ospf_interface *oi, + enum ospf_gr_restart_reason reason, struct stream *s) { struct grace_tlv_graceperiod tlv_period = {}; struct grace_tlv_restart_reason tlv_reason = {}; @@ -74,8 +73,8 @@ static void ospf_gr_lsa_body_set(struct ospf_gr_info *gr_info, stream_put(s, &tlv_reason, sizeof(tlv_reason)); /* Put IP address. */ - if (oi->type == OSPF_IFTYPE_BROADCAST || oi->type == OSPF_IFTYPE_NBMA - || oi->type == OSPF_IFTYPE_POINTOMULTIPOINT) { + if (oi->type == OSPF_IFTYPE_BROADCAST || oi->type == OSPF_IFTYPE_NBMA || + oi->type == OSPF_IFTYPE_POINTOMULTIPOINT) { tlv_address.header.type = htons(RESTARTER_IP_ADDR_TYPE); tlv_address.header.length = htons(RESTARTER_IP_ADDR_LEN); tlv_address.addr = oi->address->u.prefix4; @@ -122,8 +121,7 @@ static struct ospf_lsa *ospf_gr_lsa_new(struct ospf_interface *oi, new = ospf_lsa_new_and_data(length); if (IS_DEBUG_OSPF_GR) - zlog_debug("LSA[Type%d:%pI4]: Create an Opaque-LSA/GR instance", - lsa_type, &lsa_id); + zlog_debug("LSA[Type%d:%pI4]: Create an Opaque-LSA/GR instance", lsa_type, &lsa_id); new->area = oi->area; new->oi = oi; @@ -135,8 +133,7 @@ static struct ospf_lsa *ospf_gr_lsa_new(struct ospf_interface *oi, } /* Originate and install Grace-LSA for a given interface. */ -static void ospf_gr_lsa_originate(struct ospf_interface *oi, - enum ospf_gr_restart_reason reason, +static void ospf_gr_lsa_originate(struct ospf_interface *oi, enum ospf_gr_restart_reason reason, bool maxage) { struct ospf_lsa *lsa, *old; @@ -207,9 +204,8 @@ static void ospf_gr_flush_grace_lsas(struct ospf *ospf) for (ALL_LIST_ELEMENTS_RO(area->oiflist, inode, oi)) { if (IS_DEBUG_OSPF_GR) - zlog_debug( - "GR: flushing self-originated Grace-LSA [area %pI4] [interface %s]", - &area->area_id, oi->ifp->name); + zlog_debug("GR: flushing self-originated Grace-LSA [area %pI4] [interface %s]", + &area->area_id, oi->ifp->name); ospf_gr_lsa_originate(oi, ospf->gr_info.reason, true); } @@ -217,7 +213,8 @@ static void ospf_gr_flush_grace_lsas(struct ospf *ospf) } /* Exit from the Graceful Restart mode. */ -static void ospf_gr_restart_exit(struct ospf *ospf, const char *reason) +static void ospf_gr_restart_exit(struct ospf *ospf, const char *reason, + enum ospf_helper_exit_reason exit_reason) { struct ospf_area *area; struct listnode *onode, *anode; @@ -243,8 +240,7 @@ static void ospf_gr_restart_exit(struct ospf *ospf, const char *reason) if (oi->gr.hello_delay.t_grace_send) { oi->gr.hello_delay.elapsed_seconds = 0; event_cancel(&oi->gr.hello_delay.t_grace_send); - OSPF_ISM_TIMER_MSEC_ON(oi->t_hello, - ospf_hello_timer, 1); + OSPF_ISM_TIMER_MSEC_ON(oi->t_hello, ospf_hello_timer, 1); } /* @@ -279,11 +275,15 @@ static void ospf_gr_restart_exit(struct ospf *ospf, const char *reason) /* 6) Any grace-LSAs that the router originated should be flushed. */ ospf_gr_flush_grace_lsas(ospf); + + /* RFC 9129 ietf-ospf:restart-status-change: now in not-restarting + * (1) state. + */ + ospfd_ietf_notif_restart_status_change(ospf, 1, exit_reason); } /* Enter the Graceful Restart mode. */ -void ospf_gr_restart_enter(struct ospf *ospf, - enum ospf_gr_restart_reason reason, time_t timestamp) +void ospf_gr_restart_enter(struct ospf *ospf, enum ospf_gr_restart_reason reason, time_t timestamp) { unsigned long remaining_time; @@ -293,17 +293,21 @@ void ospf_gr_restart_enter(struct ospf *ospf, /* Schedule grace period timeout. */ remaining_time = timestamp - time(NULL); if (IS_DEBUG_OSPF_GR) - zlog_debug( - "GR: remaining time until grace period expires: %lu(s)", - remaining_time); + zlog_debug("GR: remaining time until grace period expires: %lu(s)", remaining_time); - event_add_timer(master, ospf_gr_grace_period_expired, ospf, - remaining_time, &ospf->gr_info.t_grace_period); + event_add_timer(master, ospf_gr_grace_period_expired, ospf, remaining_time, + &ospf->gr_info.t_grace_period); + + /* RFC 9129 ietf-ospf:restart-status-change. FRR's restart reasons + * are all software-initiated, so they map to RFC `planned-restart` + * (2); the only `unplanned-restart` (3) value would correspond to a + * crash recovery FRR does not currently signal. + */ + ospfd_ietf_notif_restart_status_change(ospf, 2, OSPF_GR_HELPER_INPROGRESS); } /* Check if a Router-LSA contains a given link. */ -static bool ospf_router_lsa_contains_adj(struct ospf_lsa *lsa, - struct in_addr *id) +static bool ospf_router_lsa_contains_adj(struct ospf_lsa *lsa, struct in_addr *id) { struct router_lsa *rl; @@ -321,8 +325,7 @@ static bool ospf_router_lsa_contains_adj(struct ospf_lsa *lsa, return false; } -static bool ospf_gr_check_router_lsa_consistency(struct ospf *ospf, - struct ospf_area *area, +static bool ospf_gr_check_router_lsa_consistency(struct ospf *ospf, struct ospf_area *area, struct ospf_lsa *lsa) { if (CHECK_FLAG(lsa->flags, OSPF_LSA_SELF)) { @@ -336,26 +339,22 @@ static bool ospf_gr_check_router_lsa_consistency(struct ospf *ospf, if (rl->link[i].type != LSA_LINK_TYPE_POINTOPOINT) continue; - lsa_adj = ospf_lsa_lookup_by_id(area, OSPF_ROUTER_LSA, - *link_id); + lsa_adj = ospf_lsa_lookup_by_id(area, OSPF_ROUTER_LSA, *link_id); if (!lsa_adj) continue; - if (!ospf_router_lsa_contains_adj(lsa_adj, - &lsa_self->data->id)) + if (!ospf_router_lsa_contains_adj(lsa_adj, &lsa_self->data->id)) return false; } } else { struct ospf_lsa *lsa_self; - lsa_self = ospf_lsa_lookup_by_id(area, OSPF_ROUTER_LSA, - ospf->router_id); - if (!lsa_self - || !CHECK_FLAG(lsa_self->flags, OSPF_LSA_RECEIVED)) + lsa_self = ospf_lsa_lookup_by_id(area, OSPF_ROUTER_LSA, ospf->router_id); + if (!lsa_self || !CHECK_FLAG(lsa_self->flags, OSPF_LSA_RECEIVED)) return true; - if (ospf_router_lsa_contains_adj(lsa, &ospf->router_id) - != ospf_router_lsa_contains_adj(lsa_self, &lsa->data->id)) + if (ospf_router_lsa_contains_adj(lsa, &ospf->router_id) != + ospf_router_lsa_contains_adj(lsa_self, &lsa->data->id)) return false; } @@ -380,9 +379,9 @@ void ospf_gr_check_lsdb_consistency(struct ospf *ospf, struct ospf_area *area) char reason[256]; snprintfrr(reason, sizeof(reason), - "detected inconsistent LSA[%s] [area %pI4]", - dump_lsa_key(lsa), &area->area_id); - ospf_gr_restart_exit(ospf, reason); + "detected inconsistent LSA[%s] [area %pI4]", dump_lsa_key(lsa), + &area->area_id); + ospf_gr_restart_exit(ospf, reason, OSPF_GR_HELPER_TOPO_CHG); route_unlock_node(rn); return; } @@ -390,8 +389,8 @@ void ospf_gr_check_lsdb_consistency(struct ospf *ospf, struct ospf_area *area) } /* Lookup neighbor by address in a given OSPF area. */ -static struct ospf_neighbor * -ospf_area_nbr_lookup_by_addr(struct ospf_area *area, struct in_addr *addr) +static struct ospf_neighbor *ospf_area_nbr_lookup_by_addr(struct ospf_area *area, + struct in_addr *addr) { struct ospf_interface *oi; struct ospf_neighbor *nbr; @@ -407,8 +406,8 @@ ospf_area_nbr_lookup_by_addr(struct ospf_area *area, struct in_addr *addr) } /* Lookup neighbor by Router ID in a given OSPF area. */ -static struct ospf_neighbor * -ospf_area_nbr_lookup_by_routerid(struct ospf_area *area, struct in_addr *id) +static struct ospf_neighbor *ospf_area_nbr_lookup_by_routerid(struct ospf_area *area, + struct in_addr *id) { struct ospf_interface *oi; struct ospf_neighbor *nbr; @@ -424,24 +423,21 @@ ospf_area_nbr_lookup_by_routerid(struct ospf_area *area, struct in_addr *id) } /* Check if there's a fully formed adjacency with the given neighbor ID. */ -static bool ospf_gr_check_adj_id(struct ospf_area *area, - struct in_addr *nbr_id) +static bool ospf_gr_check_adj_id(struct ospf_area *area, struct in_addr *nbr_id) { struct ospf_neighbor *nbr; nbr = ospf_area_nbr_lookup_by_routerid(area, nbr_id); if (!nbr || nbr->state < NSM_Full) { if (IS_DEBUG_OSPF_GR) - zlog_debug("GR: missing adjacency to router %pI4", - nbr_id); + zlog_debug("GR: missing adjacency to router %pI4", nbr_id); return false; } return true; } -static bool ospf_gr_check_adjs_lsa_transit(struct ospf_area *area, - struct in_addr *link_id) +static bool ospf_gr_check_adjs_lsa_transit(struct ospf_area *area, struct in_addr *link_id) { struct ospf *ospf = area->ospf; struct ospf_interface *oi; @@ -485,9 +481,7 @@ static bool ospf_gr_check_adjs_lsa_transit(struct ospf_area *area, nbr = ospf_area_nbr_lookup_by_addr(area, link_id); if (!nbr || nbr->state < NSM_Full) { if (IS_DEBUG_OSPF_GR) - zlog_debug( - "GR: missing adjacency to DR router %pI4", - link_id); + zlog_debug("GR: missing adjacency to DR router %pI4", link_id); return false; } } @@ -533,18 +527,16 @@ void ospf_gr_check_adjs(struct ospf *ospf) for (ALL_LIST_ELEMENTS_RO(ospf->areas, node, area)) { struct ospf_lsa *lsa_self; - lsa_self = ospf_lsa_lookup_by_id(area, OSPF_ROUTER_LSA, - ospf->router_id); + lsa_self = ospf_lsa_lookup_by_id(area, OSPF_ROUTER_LSA, ospf->router_id); if (!lsa_self || !ospf_gr_check_adjs_lsa(area, lsa_self)) { if (IS_DEBUG_OSPF_GR) - zlog_debug( - "GR: not all adjacencies were reestablished yet [area %pI4]", - &area->area_id); + zlog_debug("GR: not all adjacencies were reestablished yet [area %pI4]", + &area->area_id); return; } } - ospf_gr_restart_exit(ospf, "all adjacencies were reestablished"); + ospf_gr_restart_exit(ospf, "all adjacencies were reestablished", OSPF_GR_HELPER_COMPLETED); } /* Handling of grace period expiry. */ @@ -552,7 +544,7 @@ static void ospf_gr_grace_period_expired(struct event *event) { struct ospf *ospf = EVENT_ARG(event); - ospf_gr_restart_exit(ospf, "grace period has expired"); + ospf_gr_restart_exit(ospf, "grace period has expired", OSPF_GR_HELPER_GRACE_TIMEOUT); } /* Send extra Grace-LSA out the interface (unplanned outages only). */ @@ -574,7 +566,7 @@ void ospf_gr_iface_send_grace_lsa(struct event *event) * Record in non-volatile memory that the given OSPF instance is attempting to * perform a graceful restart. */ -static void ospf_gr_nvm_update(struct ospf *ospf, bool prepare) +void ospf_gr_nvm_update(struct ospf *ospf, bool prepare) { const char *inst_name; json_object *json; @@ -594,12 +586,10 @@ static void ospf_gr_nvm_update(struct ospf *ospf, bool prepare) json_object_object_get_ex(json_instances, inst_name, &json_instance); if (!json_instance) { json_instance = json_object_new_object(); - json_object_object_add(json_instances, inst_name, - json_instance); + json_object_object_add(json_instances, inst_name, json_instance); } - json_object_int_add(json_instance, "gracePeriod", - ospf->gr_info.grace_period); + json_object_int_add(json_instance, "gracePeriod", ospf->gr_info.grace_period); /* * Record not only the grace period, but also a UNIX timestamp @@ -666,12 +656,10 @@ void ospf_gr_nvm_read(struct ospf *ospf) json_object_object_get_ex(json_instances, inst_name, &json_instance); if (!json_instance) { json_instance = json_object_new_object(); - json_object_object_add(json_instances, inst_name, - json_instance); + json_object_object_add(json_instances, inst_name, json_instance); } - json_object_object_get_ex(json_instance, "gracePeriod", - &json_grace_period); + json_object_object_get_ex(json_instance, "gracePeriod", &json_grace_period); json_object_object_get_ex(json_instance, "timestamp", &json_timestamp); if (json_timestamp) { @@ -680,12 +668,11 @@ void ospf_gr_nvm_read(struct ospf *ospf) /* Planned GR: check if the grace period has already expired. */ now = time(NULL); timestamp = json_object_get_int(json_timestamp); - if (now > timestamp) { - ospf_gr_restart_exit( - ospf, "grace period has expired already"); - } else - ospf_gr_restart_enter(ospf, OSPF_GR_SW_RESTART, - timestamp); + if (now > timestamp) + ospf_gr_restart_exit(ospf, "grace period has expired already", + OSPF_GR_HELPER_GRACE_TIMEOUT); + else + ospf_gr_restart_enter(ospf, OSPF_GR_SW_RESTART, timestamp); } else if (json_grace_period) { uint32_t grace_period; @@ -725,32 +712,26 @@ static void ospf_gr_prepare(void) for (ALL_LIST_ELEMENTS_RO(om->ospf, onode, ospf)) { struct listnode *inode; - if (!ospf->gr_info.restart_support - || ospf->gr_info.prepare_in_progress) + if (!ospf->gr_info.restart_support || ospf->gr_info.prepare_in_progress) continue; if (IS_DEBUG_OSPF_GR) - zlog_debug( - "GR: preparing to perform a graceful restart [period %u second(s)] [vrf %s]", - ospf->gr_info.grace_period, - ospf_vrf_id_to_name(ospf->vrf_id)); + zlog_debug("GR: preparing to perform a graceful restart [period %u second(s)] [vrf %s]", + ospf->gr_info.grace_period, ospf_vrf_id_to_name(ospf->vrf_id)); if (!CHECK_FLAG(ospf->config, OSPF_OPAQUE_CAPABLE)) { - zlog_warn( - "%s: failed to activate graceful restart: opaque capability not enabled", - __func__); + zlog_warn("%s: failed to activate graceful restart: opaque capability not enabled", + __func__); continue; } /* Send a Grace-LSA to all neighbors. */ for (ALL_LIST_ELEMENTS_RO(ospf->oiflist, inode, oi)) { if (OSPF_IF_PARAM(oi, opaque_capable)) - ospf_gr_lsa_originate(oi, OSPF_GR_SW_RESTART, - false); + ospf_gr_lsa_originate(oi, OSPF_GR_SW_RESTART, false); else - zlog_debug( - "GR: skipping grace LSA on interface %s (%s) with opaque capability disabled", - IF_NAME(oi), ospf_get_name(oi->ospf)); + zlog_debug("GR: skipping grace LSA on interface %s (%s) with opaque capability disabled", + IF_NAME(oi), ospf_get_name(oi->ospf)); } /* Record end of the grace period in non-volatile memory. */ @@ -788,53 +769,94 @@ DEFPY(graceful_restart_prepare, graceful_restart_prepare_cmd, return CMD_SUCCESS; } -DEFPY(graceful_restart, graceful_restart_cmd, +/* + * Enable graceful-restart support on `ospf` using its current + * `gr_info.grace_period`. Idempotent: safe to call when GR is already + * enabled (e.g. by a YANG modify that only changed restart-interval). + * Extracted from the legacy `graceful-restart` DEFUN so the CLI shim + * and the RFC 9129 `/graceful-restart/enabled` callback share one + * code path. + */ +void ospf_gr_restart_support_enable(struct ospf *ospf) +{ + if (ospf->gr_info.grace_period == 0) + ospf->gr_info.grace_period = OSPF_DFLT_GRACE_INTERVAL; + ospf->gr_info.restart_support = true; + (void)ospf_zebra_gr_enable(ospf, ospf->gr_info.grace_period); + ospf_gr_nvm_update(ospf, false); +} + +/* + * Disable graceful-restart support on `ospf`. Returns -1 if a GR + * preparation is already in flight (the legacy CLI rejects the same + * way); the caller is responsible for surfacing the rejection. The + * grace_period is left untouched -- the RFC 9129 `restart-interval` + * leaf is a separate northbound knob with its own restore path. + */ +int ospf_gr_restart_support_disable(struct ospf *ospf) +{ + if (!ospf->gr_info.restart_support) + return 0; + if (ospf->gr_info.prepare_in_progress) + return -1; + ospf->gr_info.restart_support = false; + ospf_gr_nvm_delete(ospf); + ospf_zebra_gr_disable(ospf); + return 0; +} + +/* + * Set the graceful-restart grace period. If GR is currently enabled, + * refresh the zebra GR stale-route timer so the new value takes + * effect on the next restart. + */ +void ospf_gr_set_grace_period(struct ospf *ospf, uint32_t grace_period) +{ + if (ospf->gr_info.grace_period == grace_period) + return; + ospf->gr_info.grace_period = grace_period; + if (ospf->gr_info.restart_support) + (void)ospf_zebra_gr_enable(ospf, grace_period); +} + +DEFPY_YANG(graceful_restart, graceful_restart_cmd, "graceful-restart [grace-period (1-1800)$grace_period]", OSPF_GR_STR "Maximum length of the 'grace period'\n" "Maximum length of the 'grace period' in seconds\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - - /* Check and get restart period if present. */ - if (!grace_period_str) - grace_period = OSPF_DFLT_GRACE_INTERVAL; - - ospf->gr_info.restart_support = true; - ospf->gr_info.grace_period = grace_period; - - /* Freeze OSPF routes in the RIB. */ - (void)ospf_zebra_gr_enable(ospf, ospf->gr_info.grace_period); - - /* Record that GR is enabled in non-volatile memory. */ - ospf_gr_nvm_update(ospf, false); - - return CMD_SUCCESS; + char xpath[XPATH_MAXLEN]; + + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, "/graceful-restart/enabled") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "true"); + if (grace_period_str) { + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, + "/graceful-restart/restart-interval") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, grace_period_str); + } + return nb_cli_apply_changes(vty, NULL); } -DEFPY(no_graceful_restart, no_graceful_restart_cmd, +DEFPY_YANG(no_graceful_restart, no_graceful_restart_cmd, "no graceful-restart [grace-period (1-1800)]", NO_STR OSPF_GR_STR "Maximum length of the 'grace period'\n" "Maximum length of the 'grace period' in seconds\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - - if (!ospf->gr_info.restart_support) - return CMD_SUCCESS; - - if (ospf->gr_info.prepare_in_progress) { - vty_out(vty, - "%% Error: Graceful Restart preparation in progress\n"); - return CMD_WARNING; - } - - ospf->gr_info.restart_support = false; - ospf->gr_info.grace_period = OSPF_DFLT_GRACE_INTERVAL; - ospf_gr_nvm_delete(ospf); - ospf_zebra_gr_disable(ospf); - - return CMD_SUCCESS; + char xpath[XPATH_MAXLEN]; + + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, "/graceful-restart/enabled") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, + "/graceful-restart/restart-interval") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); } void ospf_gr_init(void) diff --git a/ospfd/ospf_gr.h b/ospfd/ospf_gr.h index 77e79da03ac8..4dfaa8198684 100644 --- a/ospfd/ospf_gr.h +++ b/ospfd/ospf_gr.h @@ -174,8 +174,31 @@ extern void ospf_gr_check_lsdb_consistency(struct ospf *ospf, struct ospf_area *area); extern void ospf_gr_check_adjs(struct ospf *ospf); extern void ospf_gr_nvm_read(struct ospf *ospf); +extern void ospf_gr_nvm_update(struct ospf *ospf, bool prepare); extern void ospf_gr_nvm_delete(struct ospf *ospf); extern void ospf_gr_unplanned_start_interface(struct ospf_interface *oi); extern void ospf_gr_init(void); +/* + * Apply graceful-restart restarter state. Shared by the legacy + * `graceful-restart` CLI and the RFC 9129 `/graceful-restart/enabled` + * northbound callback so the two paths produce identical side effects. + */ +extern void ospf_gr_restart_support_enable(struct ospf *ospf); + +/* + * Disable graceful-restart restarter state. Returns -1 when a GR + * preparation is in flight (the legacy CLI rejects the same way); the + * caller surfaces the rejection. The grace_period is intentionally + * left untouched -- the restart-interval leaf has its own restore + * path. + */ +extern int ospf_gr_restart_support_disable(struct ospf *ospf); + +/* + * Set the graceful-restart grace period. Refreshes the zebra GR + * stale-route timer if GR is currently enabled. + */ +extern void ospf_gr_set_grace_period(struct ospf *ospf, uint32_t grace_period); + #endif /* _ZEBRA_OSPF_GR_H */ diff --git a/ospfd/ospf_gr_helper.c b/ospfd/ospf_gr_helper.c index 790dfa17a24a..d4a99687cbf5 100644 --- a/ospfd/ospf_gr_helper.c +++ b/ospfd/ospf_gr_helper.c @@ -35,23 +35,21 @@ #include "ospfd/ospf_nsm.h" #include "ospfd/ospf_ism.h" #include "ospfd/ospf_gr.h" +#include "ospf_nb.h" -static const char * const ospf_exit_reason_desc[] = { - "Unknown reason", - "Helper in progress", - "Topology Change", - "Grace timer expiry", - "Successful graceful restart", +static const char *const ospf_exit_reason_desc[] = { + "Unknown reason", "Helper in progress", "Topology Change", + "Grace timer expiry", "Successful graceful restart", }; -static const char * const ospf_restart_reason_desc[] = { +static const char *const ospf_restart_reason_desc[] = { "Unknown restart", "Software restart", "Software reload/upgrade", "Switch to redundant control processor", }; -static const char * const ospf_rejected_reason_desc[] = { +static const char *const ospf_rejected_reason_desc[] = { "Unknown reason", "Helper support disabled", "Neighbour is not in FULL state", @@ -109,7 +107,7 @@ static void ospf_enable_rtr_hash_destroy(struct ospf *ospf) const char *ospf_exit_reason2str(unsigned int reason) { if (reason < array_size(ospf_exit_reason_desc)) - return(ospf_exit_reason_desc[reason]); + return (ospf_exit_reason_desc[reason]); else return "Invalid reason"; } @@ -120,7 +118,7 @@ const char *ospf_exit_reason2str(unsigned int reason) const char *ospf_restart_reason2str(unsigned int reason) { if (reason < array_size(ospf_restart_reason_desc)) - return(ospf_restart_reason_desc[reason]); + return (ospf_restart_reason_desc[reason]); else return "Invalid reason"; } @@ -131,7 +129,7 @@ const char *ospf_restart_reason2str(unsigned int reason) const char *ospf_rejected_reason2str(unsigned int reason) { if (reason < array_size(ospf_rejected_reason_desc)) - return(ospf_rejected_reason_desc[reason]); + return (ospf_rejected_reason_desc[reason]); else return "Invalid reason"; } @@ -157,9 +155,8 @@ void ospf_gr_helper_instance_init(struct ospf *ospf) ospf->last_exit_reason = OSPF_GR_HELPER_EXIT_NONE; ospf->active_restarter_cnt = 0; - ospf->enable_rtr_list = - hash_create(ospf_enable_rtr_hash_key, ospf_enable_rtr_hash_cmp, - "OSPF enable router hash"); + ospf->enable_rtr_list = hash_create(ospf_enable_rtr_hash_key, ospf_enable_rtr_hash_cmp, + "OSPF enable router hash"); } /* @@ -192,14 +189,12 @@ void ospf_gr_helper_init(void) if (IS_DEBUG_OSPF_GR) zlog_debug("%s, GR Helper init.", __func__); - rc = ospf_register_opaque_functab( - OSPF_OPAQUE_LINK_LSA, OPAQUE_TYPE_GRACE_LSA, NULL, NULL, NULL, - NULL, NULL, NULL, NULL, show_ospf_grace_lsa_info, NULL, NULL, - NULL, NULL); + rc = ospf_register_opaque_functab(OSPF_OPAQUE_LINK_LSA, OPAQUE_TYPE_GRACE_LSA, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, show_ospf_grace_lsa_info, + NULL, NULL, NULL, NULL); if (rc != 0) { flog_warn(EC_OSPF_OPAQUE_REGISTRATION, - "%s: Failed to register Grace LSA functions", - __func__); + "%s: Failed to register Grace LSA functions", __func__); } } @@ -228,8 +223,7 @@ void ospf_gr_helper_stop(void) * addr : RESTARTER address. * reason : Restarting reason. */ -static int ospf_extract_grace_lsa_fields(struct ospf_lsa *lsa, - uint32_t *interval, +static int ospf_extract_grace_lsa_fields(struct ospf_lsa *lsa, uint32_t *interval, struct in_addr *addr, uint8_t *reason) { struct lsa_header *lsah = NULL; @@ -245,28 +239,24 @@ static int ospf_extract_grace_lsa_fields(struct ospf_lsa *lsa, /* Check LSA len */ if (lsa->size <= OSPF_LSA_HEADER_SIZE) { if (IS_DEBUG_OSPF_GR) - zlog_debug("%s: Malformed packet: Invalid LSA len:%d", - __func__, length); + zlog_debug("%s: Malformed packet: Invalid LSA len:%d", __func__, length); return OSPF_GR_FAILURE; } length = lsa->size - OSPF_LSA_HEADER_SIZE; - for (tlvh = TLV_HDR_TOP(lsah); sum < length && tlvh; - tlvh = TLV_HDR_NEXT(tlvh)) { - + for (tlvh = TLV_HDR_TOP(lsah); sum < length && tlvh; tlvh = TLV_HDR_NEXT(tlvh)) { /* Check TLV len against overall LSA */ if (sum + TLV_SIZE(tlvh) > length) { if (IS_DEBUG_OSPF_GR) - zlog_debug("%s: Malformed packet: Invalid TLV len:%u", - __func__, TLV_SIZE(tlvh)); + zlog_debug("%s: Malformed packet: Invalid TLV len:%u", __func__, + TLV_SIZE(tlvh)); return OSPF_GR_FAILURE; } switch (ntohs(tlvh->type)) { case GRACE_PERIOD_TYPE: - if (TLV_SIZE(tlvh) < - sizeof(struct grace_tlv_graceperiod)) { + if (TLV_SIZE(tlvh) < sizeof(struct grace_tlv_graceperiod)) { zlog_debug("%s: Malformed packet: Invalid grace TLV len:%u", __func__, TLV_SIZE(tlvh)); return OSPF_GR_FAILURE; @@ -277,13 +267,12 @@ static int ospf_extract_grace_lsa_fields(struct ospf_lsa *lsa, sum += TLV_SIZE(tlvh); /* Check if grace interval is valid */ - if (*interval > OSPF_MAX_GRACE_INTERVAL - || *interval < OSPF_MIN_GRACE_INTERVAL) + if (*interval > OSPF_MAX_GRACE_INTERVAL || + *interval < OSPF_MIN_GRACE_INTERVAL) return OSPF_GR_FAILURE; break; case RESTART_REASON_TYPE: - if (TLV_SIZE(tlvh) < - sizeof(struct grace_tlv_restart_reason)) { + if (TLV_SIZE(tlvh) < sizeof(struct grace_tlv_restart_reason)) { zlog_debug("%s: Malformed packet: Invalid reason TLV len:%u", __func__, TLV_SIZE(tlvh)); return OSPF_GR_FAILURE; @@ -297,8 +286,7 @@ static int ospf_extract_grace_lsa_fields(struct ospf_lsa *lsa, return OSPF_GR_FAILURE; break; case RESTARTER_IP_ADDR_TYPE: - if (TLV_SIZE(tlvh) < - sizeof(struct grace_tlv_restart_addr)) { + if (TLV_SIZE(tlvh) < sizeof(struct grace_tlv_restart_addr)) { zlog_debug("%s: Malformed packet: Invalid addr TLV len:%u", __func__, TLV_SIZE(tlvh)); return OSPF_GR_FAILURE; @@ -310,9 +298,8 @@ static int ospf_extract_grace_lsa_fields(struct ospf_lsa *lsa, break; default: if (IS_DEBUG_OSPF_GR) - zlog_debug( - "%s, Malformed packet.Invalid TLV type:%d", - __func__, ntohs(tlvh->type)); + zlog_debug("%s, Malformed packet.Invalid TLV type:%d", __func__, + ntohs(tlvh->type)); return OSPF_GR_FAILURE; } } @@ -356,10 +343,9 @@ static void ospf_handle_grace_timer_expiry(struct event *event) * If supported as HELPER : OSPF_GR_HELPER_INPROGRESS * If Not supported as HELPER : OSPF_GR_HELPER_NONE */ -int ospf_process_grace_lsa(struct ospf *ospf, struct ospf_lsa *lsa, - struct ospf_neighbor *nbr) +int ospf_process_grace_lsa(struct ospf *ospf, struct ospf_lsa *lsa, struct ospf_neighbor *nbr) { - struct in_addr restart_addr = {0}; + struct in_addr restart_addr = { 0 }; uint8_t restart_reason = 0; uint32_t grace_interval = 0; uint32_t actual_grace_interval = 0; @@ -370,8 +356,7 @@ int ospf_process_grace_lsa(struct ospf *ospf, struct ospf_lsa *lsa, /* Extract the grace lsa packet fields */ - ret = ospf_extract_grace_lsa_fields(lsa, &grace_interval, &restart_addr, - &restart_reason); + ret = ospf_extract_grace_lsa_fields(lsa, &grace_interval, &restart_addr, &restart_reason); if (ret != OSPF_GR_SUCCESS) { if (IS_DEBUG_OSPF_GR) zlog_debug("%s, Wrong Grace LSA packet.", __func__); @@ -379,10 +364,9 @@ int ospf_process_grace_lsa(struct ospf *ospf, struct ospf_lsa *lsa, } if (IS_DEBUG_OSPF_GR) - zlog_debug( - "%s, Grace LSA received from %pI4, grace interval:%u, restart reason:%s", - __func__, &restart_addr, grace_interval, - ospf_restart_reason2str(restart_reason)); + zlog_debug("%s, Grace LSA received from %pI4, grace interval:%u, restart reason:%s", + __func__, &restart_addr, grace_interval, + ospf_restart_reason2str(restart_reason)); /* In case of broadcast links, if RESTARTER is DR_OTHER, * grace LSA might be received from DR, so need to get @@ -393,9 +377,8 @@ int ospf_process_grace_lsa(struct ospf *ospf, struct ospf_lsa *lsa, if (!restarter) { if (IS_DEBUG_OSPF_GR) - zlog_debug( - "%s, Restarter is not a nbr(%pI4) for this router.", - __func__, &restart_addr); + zlog_debug("%s, Restarter is not a nbr(%pI4) for this router.", + __func__, &restart_addr); return OSPF_GR_NOT_HELPER; } } else @@ -410,11 +393,9 @@ int ospf_process_grace_lsa(struct ospf *ospf, struct ospf_lsa *lsa, if (!hash_lookup(ospf->enable_rtr_list, &lookup)) { if (IS_DEBUG_OSPF_GR) - zlog_debug( - "%s, HELPER support is disabled, So not a HELPER", - __func__); - restarter->gr_helper_info.rejected_reason = - OSPF_HELPER_SUPPORT_DISABLED; + zlog_debug("%s, HELPER support is disabled, So not a HELPER", + __func__); + restarter->gr_helper_info.rejected_reason = OSPF_HELPER_SUPPORT_DISABLED; return OSPF_GR_NOT_HELPER; } } @@ -425,60 +406,47 @@ int ospf_process_grace_lsa(struct ospf *ospf, struct ospf_lsa *lsa, */ if (!IS_NBR_STATE_FULL(restarter)) { if (IS_DEBUG_OSPF_GR) - zlog_debug( - "%s, This Neighbour %pI4 is not in FULL state.", - __func__, &restarter->src); - restarter->gr_helper_info.rejected_reason = - OSPF_HELPER_NOT_A_VALID_NEIGHBOUR; + zlog_debug("%s, This Neighbour %pI4 is not in FULL state.", __func__, + &restarter->src); + restarter->gr_helper_info.rejected_reason = OSPF_HELPER_NOT_A_VALID_NEIGHBOUR; return OSPF_GR_NOT_HELPER; } /* Based on the restart reason from grace lsa * check the current router is supporting or not */ - if (ospf->only_planned_restart - && !OSPF_GR_IS_PLANNED_RESTART(restart_reason)) { + if (ospf->only_planned_restart && !OSPF_GR_IS_PLANNED_RESTART(restart_reason)) { if (IS_DEBUG_OSPF_GR) - zlog_debug( - "%s, Router supports only planned restarts but received the GRACE LSA for an unplanned restart.", - __func__); - restarter->gr_helper_info.rejected_reason = - OSPF_HELPER_PLANNED_ONLY_RESTART; + zlog_debug("%s, Router supports only planned restarts but received the GRACE LSA for an unplanned restart.", + __func__); + restarter->gr_helper_info.rejected_reason = OSPF_HELPER_PLANNED_ONLY_RESTART; return OSPF_GR_NOT_HELPER; } /* Check the retransmission list of this * neighbour, check any change in lsas. */ - if (ospf->strict_lsa_check && !ospf_ls_retransmit_isempty(restarter) - && ospf_check_change_in_rxmt_list(restarter)) { + if (ospf->strict_lsa_check && !ospf_ls_retransmit_isempty(restarter) && + ospf_check_change_in_rxmt_list(restarter)) { if (IS_DEBUG_OSPF_GR) - zlog_debug( - "%s, Changed LSA in Rxmt list. So not Helper.", - __func__); - restarter->gr_helper_info.rejected_reason = - OSPF_HELPER_TOPO_CHANGE_RTXMT_LIST; + zlog_debug("%s, Changed LSA in Rxmt list. So not Helper.", __func__); + restarter->gr_helper_info.rejected_reason = OSPF_HELPER_TOPO_CHANGE_RTXMT_LIST; return OSPF_GR_NOT_HELPER; } /*LSA age must be less than the grace period */ if (LS_AGE_RAW(lsa) >= grace_interval) { if (IS_DEBUG_OSPF_GR) - zlog_debug( - "%s, Grace LSA age(%d) is more than the grace interval(%d)", - __func__, LS_AGE_RAW(lsa), grace_interval); - restarter->gr_helper_info.rejected_reason = - OSPF_HELPER_LSA_AGE_MORE; + zlog_debug("%s, Grace LSA age(%d) is more than the grace interval(%d)", + __func__, LS_AGE_RAW(lsa), grace_interval); + restarter->gr_helper_info.rejected_reason = OSPF_HELPER_LSA_AGE_MORE; return OSPF_GR_NOT_HELPER; } if (ospf->gr_info.restart_in_progress) { if (IS_DEBUG_OSPF_GR) - zlog_debug( - "%s: router is in the process of graceful restart", - __func__); - restarter->gr_helper_info.rejected_reason = - OSPF_HELPER_RESTARTING; + zlog_debug("%s: router is in the process of graceful restart", __func__); + restarter->gr_helper_info.rejected_reason = OSPF_HELPER_RESTARTING; return OSPF_GR_NOT_HELPER; } @@ -490,10 +458,8 @@ int ospf_process_grace_lsa(struct ospf *ospf, struct ospf_lsa *lsa, actual_grace_interval = grace_interval; if (grace_interval > ospf->supported_grace_time) { if (IS_DEBUG_OSPF_GR) - zlog_debug( - "%s, Received grace period %d is larger than supported grace %d", - __func__, grace_interval, - ospf->supported_grace_time); + zlog_debug("%s, Received grace period %d is larger than supported grace %d", + __func__, grace_interval, ospf->supported_grace_time); actual_grace_interval = ospf->supported_grace_time; } @@ -504,14 +470,12 @@ int ospf_process_grace_lsa(struct ospf *ospf, struct ospf_lsa *lsa, ospf->active_restarter_cnt--; if (IS_DEBUG_OSPF_GR) - zlog_debug( - "%s, Router is already acting as a HELPER for this nbr,so restart the grace timer", - __func__); + zlog_debug("%s, Router is already acting as a HELPER for this nbr,so restart the grace timer", + __func__); } else { if (IS_DEBUG_OSPF_GR) - zlog_debug( - "%s, This Router becomes a HELPER for the neighbour %pI4", - __func__, &restarter->src); + zlog_debug("%s, This Router becomes a HELPER for the neighbour %pI4", + __func__, &restarter->src); } /* Became a Helper to the RESTART neighbour. @@ -527,14 +491,20 @@ int ospf_process_grace_lsa(struct ospf *ospf, struct ospf_lsa *lsa, ospf->active_restarter_cnt++; if (IS_DEBUG_OSPF_GR) - zlog_debug("%s, Grace timer started.interval:%d", __func__, - actual_grace_interval); + zlog_debug("%s, Grace timer started.interval:%d", __func__, actual_grace_interval); /* Start the grace timer */ - event_add_timer(master, ospf_handle_grace_timer_expiry, restarter, - actual_grace_interval, + event_add_timer(master, ospf_handle_grace_timer_expiry, restarter, actual_grace_interval, &restarter->gr_helper_info.t_grace_timer); + /* RFC 9129 ietf-ospf:nbr-restart-helper-status-change: now helping (2) + * the named restarter for the negotiated grace period. exit-reason + * is `in-progress` while the helper relationship is active. + */ + ospfd_ietf_notif_nbr_restart_helper_status_change(restarter, 2, + (uint16_t)actual_grace_interval, + OSPF_GR_HELPER_INPROGRESS); + return OSPF_GR_ACTIVE_HELPER; } @@ -611,8 +581,7 @@ void ospf_helper_handle_topo_chg(struct ospf *ospf, struct ospf_lsa *lsa) return; if (IS_DEBUG_OSPF_GR) - zlog_debug("%s: Topo change detected due to LSA[%s]", __func__, - dump_lsa_key(lsa)); + zlog_debug("%s: Topo change detected due to LSA[%s]", __func__, dump_lsa_key(lsa)); lsa->to_be_acknowledged = OSPF_GR_TRUE; @@ -627,8 +596,8 @@ void ospf_helper_handle_topo_chg(struct ospf *ospf, struct ospf_lsa *lsa) * stub, then it is not a topo change. Since Type-5 * lsas will not be flooded in stub area. */ - if ((oi->area->external_routing == OSPF_AREA_STUB) - && (lsa->data->type == OSPF_AS_EXTERNAL_LSA)) { + if ((oi->area->external_routing == OSPF_AREA_STUB) && + (lsa->data->type == OSPF_AS_EXTERNAL_LSA)) { continue; } @@ -641,8 +610,7 @@ void ospf_helper_handle_topo_chg(struct ospf *ospf, struct ospf_lsa *lsa) nbr = rn->info; if (OSPF_GR_IS_ACTIVE_HELPER(nbr)) - ospf_gr_helper_exit(nbr, - OSPF_GR_HELPER_TOPO_CHG); + ospf_gr_helper_exit(nbr, OSPF_GR_HELPER_TOPO_CHG); } } } @@ -664,8 +632,7 @@ void ospf_helper_handle_topo_chg(struct ospf *ospf, struct ospf_lsa *lsa) * Returns: * Nothing. */ -void ospf_gr_helper_exit(struct ospf_neighbor *nbr, - enum ospf_helper_exit_reason reason) +void ospf_gr_helper_exit(struct ospf_neighbor *nbr, enum ospf_helper_exit_reason reason) { struct ospf_interface *oi = nbr->oi; struct ospf *ospf = oi->ospf; @@ -674,8 +641,8 @@ void ospf_gr_helper_exit(struct ospf_neighbor *nbr, return; if (IS_DEBUG_OSPF_GR) - zlog_debug("%s, Exiting from HELPER support to %pI4, due to %s", - __func__, &nbr->src, ospf_exit_reason2str(reason)); + zlog_debug("%s, Exiting from HELPER support to %pI4, due to %s", __func__, + &nbr->src, ospf_exit_reason2str(reason)); /* Reset helper status*/ nbr->gr_helper_info.gr_helper_status = OSPF_GR_NOT_HELPER; @@ -717,6 +684,13 @@ void ospf_gr_helper_exit(struct ospf_neighbor *nbr, /* Originate network lsa if it is an DR in the LAN */ if (oi->state == ISM_DR) ospf_network_lsa_update(oi); + + /* RFC 9129 ietf-ospf:nbr-restart-helper-status-change: now + * not-helping (1); age is 0 because the helper relationship is + * over. exit_reason maps from FRR's enum into the RFC enum + * inside the notification builder. + */ + ospfd_ietf_notif_nbr_restart_helper_status_change(nbr, 1, 0, reason); } /* @@ -740,7 +714,7 @@ void ospf_gr_helper_exit(struct ospf_neighbor *nbr, void ospf_process_maxage_grace_lsa(struct ospf *ospf, struct ospf_lsa *lsa, struct ospf_neighbor *nbr) { - struct in_addr restartAddr = {0}; + struct in_addr restartAddr = { 0 }; uint8_t restartReason = 0; uint32_t graceInterval = 0; struct ospf_neighbor *restarter = NULL; @@ -748,8 +722,7 @@ void ospf_process_maxage_grace_lsa(struct ospf *ospf, struct ospf_lsa *lsa, int ret; /* Extract the grace lsa packet fields */ - ret = ospf_extract_grace_lsa_fields(lsa, &graceInterval, &restartAddr, - &restartReason); + ret = ospf_extract_grace_lsa_fields(lsa, &graceInterval, &restartAddr, &restartReason); if (ret != OSPF_GR_SUCCESS) { if (IS_DEBUG_OSPF_GR) zlog_debug("%s, Wrong Grace LSA packet.", __func__); @@ -757,8 +730,7 @@ void ospf_process_maxage_grace_lsa(struct ospf *ospf, struct ospf_lsa *lsa, } if (IS_DEBUG_OSPF_GR) - zlog_debug("%s, GraceLSA received for neighbour %pI4", __func__, - &restartAddr); + zlog_debug("%s, GraceLSA received for neighbour %pI4", __func__, &restartAddr); /* In case of broadcast links, if RESTARTER is DR_OTHER, * grace LSA might be received from DR, so fetching the @@ -769,9 +741,8 @@ void ospf_process_maxage_grace_lsa(struct ospf *ospf, struct ospf_lsa *lsa, if (!restarter) { if (IS_DEBUG_OSPF_GR) - zlog_debug( - "%s, Restarter is not a neighbour for this router.", - __func__); + zlog_debug("%s, Restarter is not a neighbour for this router.", + __func__); return; } } else { @@ -815,8 +786,7 @@ void ospf_gr_helper_support_set(struct ospf *ospf, bool support) if (ospf_interface_neighbor_count(oi) == 0) continue; - for (rn = route_top(oi->nbrs); rn; - rn = route_next(rn)) { + for (rn = route_top(oi->nbrs); rn; rn = route_next(rn)) { struct ospf_neighbor *nbr = NULL; if (!rn->info) @@ -824,8 +794,7 @@ void ospf_gr_helper_support_set(struct ospf *ospf, bool support) nbr = rn->info; - lookup.advRtrAddr.s_addr = - nbr->router_id.s_addr; + lookup.advRtrAddr.s_addr = nbr->router_id.s_addr; /* check if helper support enabled for the * corresponding routerid.If enabled, don't * exit from helper role. @@ -834,8 +803,7 @@ void ospf_gr_helper_support_set(struct ospf *ospf, bool support) continue; if (OSPF_GR_IS_ACTIVE_HELPER(nbr)) - ospf_gr_helper_exit( - nbr, OSPF_GR_HELPER_TOPO_CHG); + ospf_gr_helper_exit(nbr, OSPF_GR_HELPER_TOPO_CHG); } } } @@ -859,8 +827,7 @@ void ospf_gr_helper_support_set(struct ospf *ospf, bool support) * Nothing. */ -void ospf_gr_helper_support_set_per_routerid(struct ospf *ospf, - struct in_addr *advrtr, +void ospf_gr_helper_support_set_per_routerid(struct ospf *ospf, struct in_addr *advrtr, bool support) { struct advRtr temp; @@ -894,8 +861,7 @@ void ospf_gr_helper_support_set_per_routerid(struct ospf *ospf, if (ospf_interface_neighbor_count(oi) == 0) continue; - for (rn = route_top(oi->nbrs); rn; - rn = route_next(rn)) { + for (rn = route_top(oi->nbrs); rn; rn = route_next(rn)) { struct ospf_neighbor *nbr = NULL; if (!rn->info) @@ -907,15 +873,13 @@ void ospf_gr_helper_support_set_per_routerid(struct ospf *ospf, continue; if (OSPF_GR_IS_ACTIVE_HELPER(nbr)) - ospf_gr_helper_exit( - nbr, OSPF_GR_HELPER_TOPO_CHG); + ospf_gr_helper_exit(nbr, OSPF_GR_HELPER_TOPO_CHG); } } } else { /* Add the routerid to the enable router hash table */ - (void)hash_get(ospf->enable_rtr_list, &temp, - ospf_enable_rtr_hash_alloc); + (void)hash_get(ospf->enable_rtr_list, &temp, ospf_enable_rtr_hash_alloc); } } @@ -952,8 +916,7 @@ void ospf_gr_helper_lsa_check_set(struct ospf *ospf, bool enabled) * Returns: * Nothing. */ -void ospf_gr_helper_supported_gracetime_set(struct ospf *ospf, - uint32_t interval) +void ospf_gr_helper_supported_gracetime_set(struct ospf *ospf, uint32_t interval) { ospf->supported_grace_time = interval; } @@ -971,8 +934,7 @@ void ospf_gr_helper_supported_gracetime_set(struct ospf *ospf, * Returns: * Nothing. */ -void ospf_gr_helper_set_supported_planned_only_restart(struct ospf *ospf, - bool planned_only) +void ospf_gr_helper_set_supported_planned_only_restart(struct ospf *ospf, bool planned_only) { ospf->only_planned_restart = planned_only; } @@ -1021,31 +983,25 @@ static void show_ospf_grace_lsa_info(struct vty *vty, struct json_object *json, else zlog_debug(" TLV info:"); - for (tlvh = TLV_HDR_TOP(lsah); sum < length && tlvh; - tlvh = TLV_HDR_NEXT(tlvh)) { + for (tlvh = TLV_HDR_TOP(lsah); sum < length && tlvh; tlvh = TLV_HDR_NEXT(tlvh)) { /* Check TLV len */ if (sum + TLV_SIZE(tlvh) > length) { if (vty) - vty_out(vty, "%% Invalid TLV length: %u\n", - TLV_SIZE(tlvh)); + vty_out(vty, "%% Invalid TLV length: %u\n", TLV_SIZE(tlvh)); else - zlog_debug("%% Invalid TLV length: %u", - TLV_SIZE(tlvh)); + zlog_debug("%% Invalid TLV length: %u", TLV_SIZE(tlvh)); return; } switch (ntohs(tlvh->type)) { case GRACE_PERIOD_TYPE: - if (TLV_SIZE(tlvh) - < sizeof(struct grace_tlv_graceperiod)) { + if (TLV_SIZE(tlvh) < sizeof(struct grace_tlv_graceperiod)) { if (vty) - vty_out(vty, - "%% Invalid grace TLV length %u\n", + vty_out(vty, "%% Invalid grace TLV length %u\n", TLV_SIZE(tlvh)); else - zlog_debug( - "%% Invalid grace TLV length %u", - TLV_SIZE(tlvh)); + zlog_debug("%% Invalid grace TLV length %u", + TLV_SIZE(tlvh)); return; } @@ -1053,23 +1009,18 @@ static void show_ospf_grace_lsa_info(struct vty *vty, struct json_object *json, sum += TLV_SIZE(tlvh); if (vty) - vty_out(vty, " Grace period:%d\n", - ntohl(gracePeriod->interval)); + vty_out(vty, " Grace period:%d\n", ntohl(gracePeriod->interval)); else - zlog_debug(" Grace period:%d", - ntohl(gracePeriod->interval)); + zlog_debug(" Grace period:%d", ntohl(gracePeriod->interval)); break; case RESTART_REASON_TYPE: - if (TLV_SIZE(tlvh) - < sizeof(struct grace_tlv_restart_reason)) { + if (TLV_SIZE(tlvh) < sizeof(struct grace_tlv_restart_reason)) { if (vty) - vty_out(vty, - "%% Invalid reason TLV length %u\n", + vty_out(vty, "%% Invalid reason TLV length %u\n", TLV_SIZE(tlvh)); else - zlog_debug( - "%% Invalid reason TLV length %u", - TLV_SIZE(tlvh)); + zlog_debug("%% Invalid reason TLV length %u", + TLV_SIZE(tlvh)); return; } @@ -1078,24 +1029,18 @@ static void show_ospf_grace_lsa_info(struct vty *vty, struct json_object *json, if (vty) vty_out(vty, " Restart reason:%s\n", - ospf_restart_reason2str( - grReason->reason)); + ospf_restart_reason2str(grReason->reason)); else zlog_debug(" Restart reason:%s", - ospf_restart_reason2str( - grReason->reason)); + ospf_restart_reason2str(grReason->reason)); break; case RESTARTER_IP_ADDR_TYPE: - if (TLV_SIZE(tlvh) - < sizeof(struct grace_tlv_restart_addr)) { + if (TLV_SIZE(tlvh) < sizeof(struct grace_tlv_restart_addr)) { if (vty) - vty_out(vty, - "%% Invalid addr TLV length %u\n", + vty_out(vty, "%% Invalid addr TLV length %u\n", TLV_SIZE(tlvh)); else - zlog_debug( - "%% Invalid addr TLV length %u", - TLV_SIZE(tlvh)); + zlog_debug("%% Invalid addr TLV length %u", TLV_SIZE(tlvh)); return; } @@ -1103,19 +1048,15 @@ static void show_ospf_grace_lsa_info(struct vty *vty, struct json_object *json, sum += TLV_SIZE(tlvh); if (vty) - vty_out(vty, " Restarter address:%pI4\n", - &restartAddr->addr); + vty_out(vty, " Restarter address:%pI4\n", &restartAddr->addr); else - zlog_debug(" Restarter address:%pI4", - &restartAddr->addr); + zlog_debug(" Restarter address:%pI4", &restartAddr->addr); break; default: if (vty) - vty_out(vty, " Unknown TLV type %d\n", - ntohs(tlvh->type)); + vty_out(vty, " Unknown TLV type %d\n", ntohs(tlvh->type)); else - zlog_debug(" Unknown TLV type %d", - ntohs(tlvh->type)); + zlog_debug(" Unknown TLV type %d", ntohs(tlvh->type)); break; } diff --git a/ospfd/ospf_interface.c b/ospfd/ospf_interface.c index dd6abe1ff58f..129f2efef4b3 100644 --- a/ospfd/ospf_interface.c +++ b/ospfd/ospf_interface.c @@ -68,6 +68,23 @@ int ospf_interface_neighbor_count(struct ospf_interface *oi) return count; } +void ospf_nbr_timer_update(struct ospf_interface *oi) +{ + struct route_node *rn; + struct ospf_neighbor *nbr; + + for (rn = route_top(oi->nbrs); rn; rn = route_next(rn)) { + nbr = rn->info; + + if (!nbr) + continue; + + nbr->v_inactivity = OSPF_IF_PARAM(oi, v_wait); + nbr->v_db_desc = OSPF_IF_PARAM(oi, retransmit_interval); + nbr->v_ls_req = OSPF_IF_PARAM(oi, retransmit_interval); + nbr->v_ls_rxmt = OSPF_IF_PARAM(oi, retransmit_interval); + } +} void ospf_intf_neighbor_filter_apply(struct ospf_interface *oi) { @@ -549,6 +566,10 @@ void ospf_if_stream_unset(struct ospf_interface *oi) ospf_if_reset_stats(oi); } +static void ospf_crypt_key_free(void *data) +{ + XFREE(MTYPE_OSPF_CRYPT_KEY, data); +} static struct ospf_if_params *ospf_new_if_params(void) { @@ -579,6 +600,7 @@ static struct ospf_if_params *ospf_new_if_params(void) UNSET_IF_PARAM(oip, dscp_low_control); oip->auth_crypt = list_new(); + oip->auth_crypt->del = ospf_crypt_key_free; oip->network_lsa_seqnum = htonl(OSPF_INITIAL_SEQUENCE_NUMBER); oip->is_v_wait_set = false; @@ -600,7 +622,7 @@ static void ospf_del_if_params(struct interface *ifp, list_delete(&oip->auth_crypt); XFREE(MTYPE_OSPF_IF_PARAMS, oip->keychain_name); XFREE(MTYPE_OSPF_IF_PARAMS, oip->nbr_filter_name); - ospf_interface_disable_bfd(ifp, oip); + ospf_interface_bfd_free_config(ifp, oip); ldp_sync_info_free(&(oip->ldp_sync_info)); XFREE(MTYPE_OSPF_IF_PARAMS, oip); } diff --git a/ospfd/ospf_interface.h b/ospfd/ospf_interface.h index 4df3572b47ff..f177d29b9568 100644 --- a/ospfd/ospf_interface.h +++ b/ospfd/ospf_interface.h @@ -103,6 +103,8 @@ struct ospf_if_params { /* BFD configuration */ struct bfd_configuration { + /** BFD session administrative state. */ + bool enabled; /** BFD session detection multiplier. */ uint8_t detection_multiplier; /** BFD session minimum required receive interval. */ @@ -389,6 +391,7 @@ extern void ospf_crypt_key_add(struct list *list, struct crypt_key *key); extern int ospf_crypt_key_delete(struct list *list, uint8_t key_id); extern uint8_t ospf_default_iftype(struct interface *ifp); extern int ospf_interface_neighbor_count(struct ospf_interface *oi); +extern void ospf_nbr_timer_update(struct ospf_interface *oi); extern void ospf_intf_neighbor_filter_apply(struct ospf_interface *oi); /* Set all multicast memberships appropriately based on the type and diff --git a/ospfd/ospf_ism.c b/ospfd/ospf_ism.c index 0c1e72318648..d70fbd566c68 100644 --- a/ospfd/ospf_ism.c +++ b/ospfd/ospf_ism.c @@ -237,7 +237,7 @@ int ospf_dr_election(struct ospf_interface *oi) /* Trigger an extra hello on DR/BDR changes when using quick neighbors to speed up * correct DR/BDR election. */ - if (oip->bfd_config && oip->bfd_config->quick) + if (oip->bfd_config && oip->bfd_config->enabled && oip->bfd_config->quick) ospf_hello_send(oi); } diff --git a/ospfd/ospf_ldp_sync.c b/ospfd/ospf_ldp_sync.c index f0649ca73858..514c42337406 100644 --- a/ospfd/ospf_ldp_sync.c +++ b/ospfd/ospf_ldp_sync.c @@ -10,6 +10,7 @@ #include "monotime.h" #include "memory.h" #include "frrevent.h" +#include "northbound_cli.h" #include "prefix.h" #include "table.h" #include "vty.h" @@ -737,35 +738,30 @@ void ospf_ldp_sync_if_write_config(struct vty *vty, */ #include "ospfd/ospf_ldp_sync_clippy.c" -DEFPY (ospf_mpls_ldp_sync, +/* + * `mpls ldp-sync` / `no mpls ldp-sync` map directly onto the RFC 9129 + * `/mpls/ldp/igp-sync` boolean leaf. The DEFPY_YANG bodies enqueue + * the YANG edit so vtysh dispatches through mgmtd and the same + * callback that processes `mgmt set-config` invocations does the + * mutation, keeping the candidate datastore consistent regardless of + * which front the operator drove the change from. + */ +DEFPY_YANG (ospf_mpls_ldp_sync, ospf_mpls_ldp_sync_cmd, "mpls ldp-sync", "MPLS specific commands\n" "Enable MPLS LDP-IGP Sync\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - struct vrf *vrf = vrf_lookup_by_id(ospf->vrf_id); - struct interface *ifp; - - if (ospf->vrf_id != VRF_DEFAULT) { - vty_out(vty, "ldp-sync only runs on DEFAULT VRF\n"); - return CMD_ERR_NOTHING_TODO; - } - - /* register with opaque client to recv LDP-IGP Sync msgs */ - zclient_register_opaque(ospf_zclient, LDP_IGP_SYNC_IF_STATE_UPDATE); - zclient_register_opaque(ospf_zclient, LDP_IGP_SYNC_ANNOUNCE_UPDATE); + char xpath[XPATH_MAXLEN]; - if (!CHECK_FLAG(ospf->ldp_sync_cmd.flags, LDP_SYNC_FLAG_ENABLE)) { - SET_FLAG(ospf->ldp_sync_cmd.flags, LDP_SYNC_FLAG_ENABLE); - /* turn on LDP-IGP Sync on all ptop OSPF interfaces */ - FOR_ALL_INTERFACES (vrf, ifp) - ospf_if_set_ldp_sync_enable(ospf, ifp); - } - return CMD_SUCCESS; + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, "/mpls/ldp/igp-sync") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "true"); + return nb_cli_apply_changes(vty, NULL); } -DEFPY (no_ospf_mpls_ldp_sync, +DEFPY_YANG (no_ospf_mpls_ldp_sync, no_ospf_mpls_ldp_sync_cmd, "no mpls ldp-sync", NO_STR @@ -773,8 +769,12 @@ DEFPY (no_ospf_mpls_ldp_sync, "Disable MPLS LDP-IGP Sync\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - ospf_ldp_sync_gbl_exit(ospf, true); - return CMD_SUCCESS; + char xpath[XPATH_MAXLEN]; + + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, "/mpls/ldp/igp-sync") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); } DEFPY (ospf_mpls_ldp_sync_holddown, diff --git a/ospfd/ospf_main.c b/ospfd/ospf_main.c index d75214df9638..4942322b9ff0 100644 --- a/ospfd/ospf_main.c +++ b/ospfd/ospf_main.c @@ -29,6 +29,7 @@ #include "routemap.h" #include "keychain.h" #include "libagentx.h" +#include "mgmt_be_client.h" #include "ospfd/ospfd.h" #include "ospfd/ospf_interface.h" @@ -42,6 +43,7 @@ #include "ospfd/ospf_vty.h" #include "ospfd/ospf_bfd.h" #include "ospfd/ospf_gr.h" +#include "ospfd/ospf_nb.h" #include "ospfd/ospf_errors.h" #include "ospfd/ospf_ldp_sync.h" #include "ospfd/ospf_routemap_nb.h" @@ -91,6 +93,7 @@ const struct option longopts[] = { /* Master of threads. */ struct event_loop *master; +static struct mgmt_be_client *mgmt_be_client; /* SIGHUP handler. */ static void sighup(void) @@ -103,6 +106,7 @@ static FRR_NORETURN void sigint(void) { zlog_notice("Terminating on signal"); bfd_protocol_integration_set_shutdown(true); + mgmt_be_client_destroy(mgmt_be_client); ospf_terminate(); exit(0); @@ -138,11 +142,75 @@ static const struct frr_yang_module_info *const ospfd_yang_modules[] = { &frr_interface_info, &frr_route_map_info, &frr_vrf_info, + &ietf_bfd_types_info, + &ospfd_ietf_routing_info, + &ospfd_ietf_ospf_info, + &ospfd_ietf_routing_ospf_deviation_info, &frr_ospf_route_map_info, &ietf_key_chain_info, &ietf_key_chain_deviation_info, }; +/* + * ospfd and ospf6d both register the RFC 9129 ietf-ospf control-plane-protocol + * subtree. Filter on the `type` list-key so mgmtd dispatches each change to + * the daemon that owns that protocol family. In ospfd daemon-instance mode, + * each process narrows this further to its own `name` key so `ospfd-1` and + * `ospfd-2` do not both receive the same YANG edit. + * + * See the predicate-aware matching in + * mgmtd/mgmt_be_adapter.c::mgmt_be_xpath_prefix(). + */ +static const char *const ospfd_oper_xpaths[] = { + OSPFD_IETF_ROUTING_PROTOCOL_TYPE_XPATH, +}; + +static const char *const ospfd_config_xpaths[] = { + OSPFD_IETF_ROUTING_PROTOCOL_TYPE_XPATH, +}; + +/* + * RFC 9129 puts clear-neighbor / clear-database at the module root rather than + * under control-plane-protocol, so both ospfd and ospf6d register the same + * xpaths; mgmtd fans the call out and each backend silently returns NB_OK + * when the named instance isn't local. + */ +static const char *const ospfd_rpc_xpaths[] = { + "/ietf-ospf:clear-neighbor", + "/ietf-ospf:clear-database", +}; + +static char ospfd_instance_xpath[XPATH_MAXLEN]; +static const char *const ospfd_instance_oper_xpaths[] = { + ospfd_instance_xpath, +}; +static const char *const ospfd_instance_config_xpaths[] = { + ospfd_instance_xpath, +}; + +struct mgmt_be_client_cbs ospfd_be_client_data = { + .config_xpaths = ospfd_config_xpaths, + .nconfig_xpaths = array_size(ospfd_config_xpaths), + .oper_xpaths = ospfd_oper_xpaths, + .noper_xpaths = array_size(ospfd_oper_xpaths), + .rpc_xpaths = ospfd_rpc_xpaths, + .nrpc_xpaths = array_size(ospfd_rpc_xpaths), +}; + +static void ospfd_mgmt_be_init(void) +{ + if (!ospf_instance) + return; + + ospfd_ietf_routing_protocol_instance_xpath(ospfd_instance_xpath, + sizeof(ospfd_instance_xpath), ospf_instance, + VRF_DEFAULT_NAME); + ospfd_be_client_data.config_xpaths = ospfd_instance_config_xpaths; + ospfd_be_client_data.nconfig_xpaths = array_size(ospfd_instance_config_xpaths); + ospfd_be_client_data.oper_xpaths = ospfd_instance_oper_xpaths; + ospfd_be_client_data.noper_xpaths = array_size(ospfd_instance_oper_xpaths); +} + /* actual paths filled in main() */ static char state_path[512]; static char state_compat1_path[512]; @@ -264,6 +332,7 @@ int main(int argc, char **argv) /* OSPF master init. */ ospf_master_init(frr_init()); + cmd_config_file_batching_set(true); /* Initializations. */ master = om->master; @@ -303,6 +372,9 @@ int main(int argc, char **argv) /* OSPF errors init */ ospf_error_init(); + ospfd_mgmt_be_init(); + mgmt_be_client = mgmt_be_client_create("ospfd", &ospfd_be_client_data, 0, master); + frr_config_fork(); frr_run(master); diff --git a/ospfd/ospf_nb.c b/ospfd/ospf_nb.c new file mode 100644 index 000000000000..c6a5d3d91014 --- /dev/null +++ b/ospfd/ospf_nb.c @@ -0,0 +1,567 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * OSPF northbound interface. + * Copyright (C) 2026 Eric Parsonage + */ + +#include + +#include "ospfd/ospfd.h" +#include "ospf_nb.h" + +const char *ospfd_ietf_instance_name(unsigned short instance, const char *name, + char *buf, size_t buf_len) +{ + if (instance) { + snprintf(buf, buf_len, "%u", instance); + return buf; + } + + return name ? name : VRF_DEFAULT_NAME; +} + +const char *ospfd_ietf_ospf_instance_name(const struct ospf *ospf, char *buf, + size_t buf_len) +{ + return ospfd_ietf_instance_name(ospf->instance, ospf_get_name(ospf), + buf, buf_len); +} + +int ospfd_ietf_routing_protocol_instance_xpath(char *xpath, size_t xpath_len, + unsigned short instance, + const char *name) +{ + char instance_name[XPATH_MAXLEN]; + + return snprintf(xpath, xpath_len, OSPFD_IETF_ROUTING_PROTOCOL_XPATH, + ospfd_ietf_instance_name(instance, name, instance_name, + sizeof(instance_name))); +} + +int ospfd_ietf_routing_protocol_xpath(char *xpath, size_t xpath_len, + const struct ospf *ospf) +{ + return ospfd_ietf_routing_protocol_instance_xpath( + xpath, xpath_len, ospf->instance, ospf_get_name(ospf)); +} + +/* clang-format off */ +const struct frr_yang_module_info ospfd_ietf_routing_info = { + .name = "ietf-routing", + .ignore_cfg_cbs = true, + .nodes = { + { + .xpath = OSPFD_IETF_ROUTING_CP_XPATH, + .cbs = { + .create = ospfd_ietf_routing_control_plane_protocol_create, + .destroy = ospfd_ietf_routing_control_plane_protocol_destroy, + .get_next = ospfd_ietf_routing_control_plane_protocol_get_next, + .get_keys = ospfd_ietf_routing_control_plane_protocol_get_keys, + .lookup_entry = + ospfd_ietf_routing_control_plane_protocol_lookup_entry, + }, + .cfg_opt_in = true, + }, + { + .xpath = NULL, + }, + }, +}; + +const struct frr_yang_module_info ospfd_ietf_routing_ospf_deviation_info = { + .name = "frr-deviations-ietf-routing-ospf", + .ignore_cfg_cbs = true, + .nodes = { + { + .xpath = NULL, + }, + }, +}; + +static const char * const ospfd_ietf_ospf_features[] = { + "auto-cost", + "bfd", + "explicit-router-id", + "graceful-restart", + "key-chain", + "ldp-igp-sync", + "max-ecmp", + "mtu-ignore", + "prefix-suppression", + "stub-router", + "te-rid", + NULL, +}; + +const struct frr_yang_module_info ospfd_ietf_ospf_info = { + .name = "ietf-ospf", + .features = (const char **)ospfd_ietf_ospf_features, + .ignore_cfg_cbs = true, + .nodes = { + { + .xpath = OSPFD_IETF_OSPF_XPATH "/router-id", + .cbs = { + .get_elem = ospfd_ietf_ospf_router_id_get_elem, + }, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH "/explicit-router-id", + .cbs = { + .modify = ospfd_ietf_ospf_explicit_router_id_modify, + .destroy = ospfd_ietf_ospf_explicit_router_id_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH "/preference/all", + .cbs = { + .modify = ospfd_ietf_ospf_preference_all_modify, + .destroy = ospfd_ietf_ospf_preference_all_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH "/preference/intra-area", + .cbs = { + .modify = ospfd_ietf_ospf_preference_intra_area_modify, + .destroy = ospfd_ietf_ospf_preference_intra_area_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH "/preference/inter-area", + .cbs = { + .modify = ospfd_ietf_ospf_preference_inter_area_modify, + .destroy = ospfd_ietf_ospf_preference_inter_area_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH "/preference/internal", + .cbs = { + .modify = ospfd_ietf_ospf_preference_internal_modify, + .destroy = ospfd_ietf_ospf_preference_internal_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH "/preference/external", + .cbs = { + .modify = ospfd_ietf_ospf_preference_external_modify, + .destroy = ospfd_ietf_ospf_preference_external_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH "/spf-control/paths", + .cbs = { + .modify = ospfd_ietf_ospf_spf_control_paths_modify, + .destroy = ospfd_ietf_ospf_spf_control_paths_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH "/mpls/ldp/igp-sync", + .cbs = { + .modify = ospfd_ietf_ospf_mpls_ldp_igp_sync_modify, + .destroy = ospfd_ietf_ospf_mpls_ldp_igp_sync_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH "/stub-router/always", + .cbs = { + .create = ospfd_ietf_ospf_stub_router_always_create, + .destroy = ospfd_ietf_ospf_stub_router_always_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH "/auto-cost/enabled", + .cbs = { + .modify = ospfd_ietf_ospf_auto_cost_enabled_modify, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH "/auto-cost/reference-bandwidth", + .cbs = { + .modify = ospfd_ietf_ospf_auto_cost_reference_bandwidth_modify, + .destroy = ospfd_ietf_ospf_auto_cost_reference_bandwidth_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH "/mpls/te-rid/ipv4-router-id", + .cbs = { + .modify = ospfd_ietf_ospf_mpls_te_rid_ipv4_router_id_modify, + .destroy = ospfd_ietf_ospf_mpls_te_rid_ipv4_router_id_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH "/graceful-restart/enabled", + .cbs = { + .modify = ospfd_ietf_ospf_graceful_restart_enabled_modify, + .destroy = ospfd_ietf_ospf_graceful_restart_enabled_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH "/graceful-restart/restart-interval", + .cbs = { + .modify = ospfd_ietf_ospf_graceful_restart_restart_interval_modify, + .destroy = ospfd_ietf_ospf_graceful_restart_restart_interval_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH "/graceful-restart/helper-enabled", + .cbs = { + .modify = ospfd_ietf_ospf_graceful_restart_helper_enabled_modify, + .destroy = ospfd_ietf_ospf_graceful_restart_helper_enabled_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH "/graceful-restart/helper-strict-lsa-checking", + .cbs = { + .modify = ospfd_ietf_ospf_graceful_restart_helper_strict_lsa_checking_modify, + .destroy = ospfd_ietf_ospf_graceful_restart_helper_strict_lsa_checking_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/statistics/originate-new-lsa-count", + .cbs = { + .get_elem = + ospfd_ietf_ospf_statistics_originate_new_lsa_count_get_elem, + }, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/statistics/rx-new-lsas-count", + .cbs = { + .get_elem = ospfd_ietf_ospf_statistics_rx_new_lsas_count_get_elem, + }, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH "/areas/area", + .cbs = { + .create = ospfd_ietf_ospf_areas_area_create, + .destroy = ospfd_ietf_ospf_areas_area_destroy, + .get_next = ospfd_ietf_ospf_areas_area_get_next, + .get_keys = ospfd_ietf_ospf_areas_area_get_keys, + .lookup_entry = ospfd_ietf_ospf_areas_area_lookup_entry, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH "/areas/area/area-type", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_type_modify, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH "/areas/area/summary", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_summary_modify, + .destroy = ospfd_ietf_ospf_areas_area_summary_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH "/areas/area/default-cost", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_default_cost_modify, + .destroy = ospfd_ietf_ospf_areas_area_default_cost_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/statistics/spf-runs-count", + .cbs = { + .get_elem = + ospfd_ietf_ospf_areas_area_statistics_spf_runs_count_get_elem, + }, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/statistics/abr-count", + .cbs = { + .get_elem = + ospfd_ietf_ospf_areas_area_statistics_abr_count_get_elem, + }, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/statistics/asbr-count", + .cbs = { + .get_elem = + ospfd_ietf_ospf_areas_area_statistics_asbr_count_get_elem, + }, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/statistics/area-scope-lsa-count", + .cbs = { + .get_elem = + ospfd_ietf_ospf_areas_area_statistics_area_scope_lsa_count_get_elem, + }, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface", + .cbs = { + .create = ospfd_ietf_ospf_areas_area_interfaces_interface_create, + .destroy = ospfd_ietf_ospf_areas_area_interfaces_interface_destroy, + .get_next = ospfd_ietf_ospf_areas_area_interfaces_interface_get_next, + .get_keys = ospfd_ietf_ospf_areas_area_interfaces_interface_get_keys, + .lookup_entry = + ospfd_ietf_ospf_areas_area_interfaces_interface_lookup_entry, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/cost", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_interfaces_interface_cost_modify, + .destroy = ospfd_ietf_ospf_areas_area_interfaces_interface_cost_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/hello-interval", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_interfaces_interface_hello_interval_modify, + .destroy = ospfd_ietf_ospf_areas_area_interfaces_interface_hello_interval_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/dead-interval", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_interfaces_interface_dead_interval_modify, + .destroy = ospfd_ietf_ospf_areas_area_interfaces_interface_dead_interval_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/retransmit-interval", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_interfaces_interface_retransmit_interval_modify, + .destroy = ospfd_ietf_ospf_areas_area_interfaces_interface_retransmit_interval_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/priority", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_interfaces_interface_priority_modify, + .destroy = ospfd_ietf_ospf_areas_area_interfaces_interface_priority_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/mtu-ignore", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_interfaces_interface_mtu_ignore_modify, + .destroy = ospfd_ietf_ospf_areas_area_interfaces_interface_mtu_ignore_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/transmit-delay", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_interfaces_interface_transmit_delay_modify, + .destroy = ospfd_ietf_ospf_areas_area_interfaces_interface_transmit_delay_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/interface-type", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_interfaces_interface_interface_type_modify, + .destroy = ospfd_ietf_ospf_areas_area_interfaces_interface_interface_type_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/passive", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_interfaces_interface_passive_modify, + .destroy = ospfd_ietf_ospf_areas_area_interfaces_interface_passive_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/prefix-suppression", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_interfaces_interface_prefix_suppression_modify, + .destroy = ospfd_ietf_ospf_areas_area_interfaces_interface_prefix_suppression_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/bfd", + .cbs = { + .apply_finish = ospfd_ietf_ospf_areas_area_interfaces_interface_bfd_apply_finish, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/bfd/enabled", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_interfaces_interface_bfd_enabled_modify, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/bfd/local-multiplier", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_interfaces_interface_bfd_local_multiplier_modify, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/bfd/desired-min-tx-interval", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_interfaces_interface_bfd_desired_min_tx_interval_modify, + .destroy = ospfd_ietf_ospf_areas_area_interfaces_interface_bfd_desired_min_tx_interval_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/bfd/required-min-rx-interval", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_interfaces_interface_bfd_required_min_rx_interval_modify, + .destroy = ospfd_ietf_ospf_areas_area_interfaces_interface_bfd_required_min_rx_interval_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/static-neighbors/neighbor", + .cbs = { + .create = ospfd_ietf_ospf_areas_area_interfaces_interface_static_neighbors_neighbor_create, + .apply_finish = ospfd_ietf_ospf_areas_area_interfaces_interface_static_neighbors_neighbor_apply_finish, + .destroy = ospfd_ietf_ospf_areas_area_interfaces_interface_static_neighbors_neighbor_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/static-neighbors/neighbor/poll-interval", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_interfaces_interface_static_neighbors_neighbor_poll_interval_modify, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/static-neighbors/neighbor/priority", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_interfaces_interface_static_neighbors_neighbor_priority_modify, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/authentication/ospfv2-key-chain", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_interfaces_interface_authentication_ospfv2_key_chain_modify, + .destroy = ospfd_ietf_ospf_areas_area_interfaces_interface_authentication_ospfv2_key_chain_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/ranges/range", + .cbs = { + .create = ospfd_ietf_ospf_areas_area_ranges_range_create, + .destroy = ospfd_ietf_ospf_areas_area_ranges_range_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/ranges/range/advertise", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_ranges_range_advertise_modify, + .destroy = ospfd_ietf_ospf_areas_area_ranges_range_advertise_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/ranges/range/cost", + .cbs = { + .modify = ospfd_ietf_ospf_areas_area_ranges_range_cost_modify, + .destroy = ospfd_ietf_ospf_areas_area_ranges_range_cost_destroy, + }, + .cfg_opt_in = true, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/neighbors/neighbor", + .cbs = { + .get_next = + ospfd_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_get_next, + .get_keys = + ospfd_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_get_keys, + .lookup_entry = + ospfd_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_lookup_entry, + }, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/neighbors/neighbor/address", + .cbs = { + .get_elem = + ospfd_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_address_get_elem, + }, + }, + { + .xpath = OSPFD_IETF_OSPF_XPATH + "/areas/area/interfaces/interface/neighbors/neighbor/state", + .cbs = { + .get_elem = + ospfd_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_state_get_elem, + }, + }, + { + .xpath = "/ietf-ospf:clear-neighbor", + .cbs = { + .rpc = ospfd_ietf_ospf_clear_neighbor_rpc, + }, + }, + { + .xpath = "/ietf-ospf:clear-database", + .cbs = { + .rpc = ospfd_ietf_ospf_clear_database_rpc, + }, + }, + { + .xpath = NULL, + }, + }, +}; +/* clang-format on */ diff --git a/ospfd/ospf_nb.h b/ospfd/ospf_nb.h new file mode 100644 index 000000000000..8f647f455e08 --- /dev/null +++ b/ospfd/ospf_nb.h @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * OSPF northbound interface. + * Copyright (C) 2026 Eric Parsonage + */ + +#ifndef FRR_OSPF_NB_H +#define FRR_OSPF_NB_H + +#include "northbound.h" + +struct ospf; +struct ospf_area; + +#define OSPFD_IETF_ROUTING_CP_XPATH \ + "/ietf-routing:routing/control-plane-protocols/" \ + "control-plane-protocol" +#define OSPFD_IETF_ROUTING_PROTOCOL_TYPE_XPATH \ + OSPFD_IETF_ROUTING_CP_XPATH "[type='ietf-ospf:ospfv2']" +#define OSPFD_IETF_ROUTING_PROTOCOL_XPATH \ + OSPFD_IETF_ROUTING_PROTOCOL_TYPE_XPATH "[name='%s']" +#define OSPFD_IETF_OSPF_XPATH \ + OSPFD_IETF_ROUTING_CP_XPATH "/ietf-ospf:ospf" + +extern const struct frr_yang_module_info ospfd_ietf_routing_info; +extern const struct frr_yang_module_info ospfd_ietf_routing_ospf_deviation_info; +extern const struct frr_yang_module_info ospfd_ietf_ospf_info; + +const char *ospfd_ietf_instance_name(unsigned short instance, const char *name, + char *buf, size_t buf_len); +const char *ospfd_ietf_ospf_instance_name(const struct ospf *ospf, char *buf, + size_t buf_len); +int ospfd_ietf_routing_protocol_xpath(char *xpath, size_t xpath_len, + const struct ospf *ospf); +int ospfd_ietf_routing_protocol_instance_xpath(char *xpath, size_t xpath_len, + unsigned short instance, + const char *name); + +/* Shared lookup: find an OSPF instance by the ietf-routing instance name. */ +struct ospf *ospfd_ietf_ospf_lookup_instance(const char *name); + +int ospfd_ietf_routing_control_plane_protocol_create(struct nb_cb_create_args *args); +int ospfd_ietf_routing_control_plane_protocol_destroy(struct nb_cb_destroy_args *args); +const void *ospfd_ietf_routing_control_plane_protocol_get_next(struct nb_cb_get_next_args *args); +int ospfd_ietf_routing_control_plane_protocol_get_keys(struct nb_cb_get_keys_args *args); +const void * +ospfd_ietf_routing_control_plane_protocol_lookup_entry(struct nb_cb_lookup_entry_args *args); + +/* Config callbacks. */ +int ospfd_ietf_ospf_explicit_router_id_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_explicit_router_id_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_areas_area_create(struct nb_cb_create_args *args); +int ospfd_ietf_ospf_areas_area_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_areas_area_type_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_summary_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_summary_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_areas_area_default_cost_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_default_cost_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_create(struct nb_cb_create_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_cost_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_cost_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_hello_interval_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_hello_interval_destroy( + struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_dead_interval_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_dead_interval_destroy( + struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_retransmit_interval_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_retransmit_interval_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_priority_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_priority_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_mtu_ignore_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_mtu_ignore_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_transmit_delay_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_transmit_delay_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_areas_area_ranges_range_create(struct nb_cb_create_args *args); +int ospfd_ietf_ospf_areas_area_ranges_range_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_areas_area_ranges_range_advertise_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_ranges_range_advertise_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_areas_area_ranges_range_cost_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_ranges_range_cost_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_interface_type_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_interface_type_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_passive_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_passive_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_preference_all_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_preference_all_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_preference_intra_area_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_preference_intra_area_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_preference_inter_area_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_preference_inter_area_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_preference_internal_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_preference_internal_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_preference_external_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_preference_external_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_spf_control_paths_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_spf_control_paths_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_mpls_ldp_igp_sync_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_mpls_ldp_igp_sync_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_stub_router_always_create(struct nb_cb_create_args *args); +int ospfd_ietf_ospf_stub_router_always_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_prefix_suppression_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_prefix_suppression_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_auto_cost_enabled_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_auto_cost_reference_bandwidth_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_auto_cost_reference_bandwidth_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_mpls_te_rid_ipv4_router_id_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_mpls_te_rid_ipv4_router_id_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_graceful_restart_enabled_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_graceful_restart_enabled_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_graceful_restart_restart_interval_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_graceful_restart_restart_interval_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_graceful_restart_helper_enabled_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_graceful_restart_helper_enabled_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_graceful_restart_helper_strict_lsa_checking_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_graceful_restart_helper_strict_lsa_checking_destroy(struct nb_cb_destroy_args *args); +void ospfd_ietf_ospf_areas_area_interfaces_interface_bfd_apply_finish(struct nb_cb_apply_finish_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_bfd_enabled_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_bfd_local_multiplier_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_bfd_desired_min_tx_interval_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_bfd_desired_min_tx_interval_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_bfd_required_min_rx_interval_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_bfd_required_min_rx_interval_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_static_neighbors_neighbor_create(struct nb_cb_create_args *args); +void ospfd_ietf_ospf_areas_area_interfaces_interface_static_neighbors_neighbor_apply_finish(struct nb_cb_apply_finish_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_static_neighbors_neighbor_destroy(struct nb_cb_destroy_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_static_neighbors_neighbor_poll_interval_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_static_neighbors_neighbor_priority_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_authentication_ospfv2_key_chain_modify(struct nb_cb_modify_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_authentication_ospfv2_key_chain_destroy(struct nb_cb_destroy_args *args); + +struct yang_data *ospfd_ietf_ospf_router_id_get_elem(struct nb_cb_get_elem_args *args); +struct yang_data * +ospfd_ietf_ospf_statistics_originate_new_lsa_count_get_elem(struct nb_cb_get_elem_args *args); +struct yang_data * +ospfd_ietf_ospf_statistics_rx_new_lsas_count_get_elem(struct nb_cb_get_elem_args *args); +const void *ospfd_ietf_ospf_areas_area_get_next(struct nb_cb_get_next_args *args); +int ospfd_ietf_ospf_areas_area_get_keys(struct nb_cb_get_keys_args *args); +const void *ospfd_ietf_ospf_areas_area_lookup_entry(struct nb_cb_lookup_entry_args *args); +struct yang_data * +ospfd_ietf_ospf_areas_area_statistics_spf_runs_count_get_elem(struct nb_cb_get_elem_args *args); +struct yang_data * +ospfd_ietf_ospf_areas_area_statistics_abr_count_get_elem(struct nb_cb_get_elem_args *args); +struct yang_data * +ospfd_ietf_ospf_areas_area_statistics_asbr_count_get_elem(struct nb_cb_get_elem_args *args); +struct yang_data *ospfd_ietf_ospf_areas_area_statistics_area_scope_lsa_count_get_elem( + struct nb_cb_get_elem_args *args); + +const void * +ospfd_ietf_ospf_areas_area_interfaces_interface_get_next(struct nb_cb_get_next_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_get_keys(struct nb_cb_get_keys_args *args); +const void * +ospfd_ietf_ospf_areas_area_interfaces_interface_lookup_entry(struct nb_cb_lookup_entry_args *args); + +const void *ospfd_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_get_next( + struct nb_cb_get_next_args *args); +int ospfd_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_get_keys( + struct nb_cb_get_keys_args *args); +const void *ospfd_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_lookup_entry( + struct nb_cb_lookup_entry_args *args); +struct yang_data * +ospfd_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_address_get_elem( + struct nb_cb_get_elem_args *args); +struct yang_data *ospfd_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_state_get_elem( + struct nb_cb_get_elem_args *args); + +/* RPC callbacks (RFC 9129). */ +int ospfd_ietf_ospf_clear_neighbor_rpc(struct nb_cb_rpc_args *args); +int ospfd_ietf_ospf_clear_database_rpc(struct nb_cb_rpc_args *args); + +/* Notification emitters (RFC 9129). */ +struct ospf_neighbor; +struct ospf_interface; +void ospfd_ietf_notif_init(void); +void ospfd_ietf_notif_restart_status_change(struct ospf *ospf, int status, int exit_reason); +void ospfd_ietf_notif_nbr_restart_helper_status_change(struct ospf_neighbor *nbr, int status, + uint16_t age, int exit_reason); +void ospfd_ietf_notif_if_rx_bad_packet(struct ospf_interface *oi, struct in_addr src, + uint8_t packet_type); +void ospfd_ietf_notif_if_config_error(struct ospf_interface *oi, struct in_addr src, + uint8_t packet_type, const char *error_name); +void ospfd_ietf_notif_nssa_translator_state_change(struct ospf_area *area); + +#endif /* FRR_OSPF_NB_H */ diff --git a/ospfd/ospf_nb_config.c b/ospfd/ospf_nb_config.c new file mode 100644 index 000000000000..b7007e82bd0b --- /dev/null +++ b/ospfd/ospf_nb_config.c @@ -0,0 +1,2893 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * OSPF northbound configuration callbacks. + * Copyright (C) 2026 Eric Parsonage + */ + +#include + +#include "northbound.h" +#include "yang.h" +#include "yang_wrappers.h" + +#include +#include "if.h" +#include "libospf.h" +#include "prefix.h" +#include "table.h" + +#include "ospfd/ospfd.h" +#include "ospfd/ospf_abr.h" +#include "ospfd/ospf_bfd.h" +#include "ospfd/ospf_interface.h" +#include "ospfd/ospf_ism.h" +#include "ospfd/ospf_ldp_sync.h" +#include "ospfd/ospf_lsa.h" +#include "ospfd/ospf_lsdb.h" +#include "ospfd/ospf_nb.h" +#include "ospfd/ospf_neighbor.h" +#include "ospfd/ospf_opaque.h" +#include "ospfd/ospf_gr.h" +#include "ospfd/ospf_spf.h" +#include "ospfd/ospf_te.h" +#include "ospfd/ospf_vty.h" +#include "ospfd/ospf_zebra.h" + +/* + * RFC 9129 ietf-ospf area-type identityref values. Accept both the bare + * identity name and the module-qualified form so callers do not depend on the + * exact libyang serialisation context used for this dnode. + */ +#define OSPF_AREA_TYPE_NORMAL "normal-area" +#define OSPF_AREA_TYPE_STUB "stub-area" +#define OSPF_AREA_TYPE_NSSA "nssa-area" + +static bool ospf_area_type_is(const char *val, const char *name) +{ + if (!val) + return false; + if (!strcmp(val, name)) + return true; + if (strncmp(val, "ietf-ospf:", strlen("ietf-ospf:")) == 0) + return !strcmp(val + strlen("ietf-ospf:"), name); + return false; +} + +static bool ospfd_ietf_ospf_type_is(const char *val) +{ + return val && (!strcmp(val, "ospfv2") || + !strcmp(val, "ietf-ospf:ospfv2")); +} + +static int ospfd_ietf_ospf_parse_error(enum nb_event event) +{ + return event == NB_EV_VALIDATE ? NB_ERR_VALIDATION : NB_ERR; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol + * + * Keep the IETF routing protocol list present in the local candidate whenever + * the legacy `router ospf` CLI creates the daemon instance directly. Child + * commands converted to RFC 9129 leaves, such as explicit-router-id, then have + * a real parent list entry to modify during the pending NB commit. + */ +int ospfd_ietf_routing_control_plane_protocol_create(struct nb_cb_create_args *args) +{ + const char *type; + const char *name; + bool created = false; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + type = yang_dnode_get_string(args->dnode, "type"); + if (!ospfd_ietf_ospf_type_is(type)) + return NB_OK; + + name = yang_dnode_get_string(args->dnode, "name"); + if (!name) + name = VRF_DEFAULT_NAME; + + ospf_get(ospf_instance, name, &created); + + return NB_OK; +} + +int ospfd_ietf_routing_control_plane_protocol_destroy(struct nb_cb_destroy_args *args) +{ + const char *type; + const char *name; + struct ospf *ospf; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + type = yang_dnode_get_string(args->dnode, "type"); + if (!ospfd_ietf_ospf_type_is(type)) + return NB_OK; + + name = yang_dnode_get_string(args->dnode, "name"); + if (!name) + name = VRF_DEFAULT_NAME; + + ospf = ospfd_ietf_ospf_lookup_instance(name); + if (!ospf) + return NB_OK; + + if (ospf->gr_info.restart_support) + ospf_gr_nvm_delete(ospf); + ospf_finish(ospf); + + return NB_OK; +} + +/* + * Look up the OSPF instance corresponding to an ietf-ospf config dnode. + * Walks up to the parent control-plane-protocol list entry to read the + * instance name, then resolves it through the shared helper. + * + * mgmtd's predicate-aware dispatch (mgmt_be_xpath_prefix) routes only + * control-plane-protocol[type='ietf-ospf:ospfv2'] entries to ospfd. In + * daemon-instance mode, each ospfd backend also registers its own `name` + * predicate, so the instance-name check is handled at the dispatch layer + * and not repeated here. + * + * Returns NULL when no FRR-side OSPF instance exists for the named + * control-plane-protocol; the caller should treat the configuration as a + * no-op until the instance is created (today: via `router ospf`). + */ +static struct ospf *ospfd_ietf_ospf_instance_from_dnode(const struct lyd_node *dnode) +{ + const struct lyd_node *cpp; + const char *name; + + cpp = yang_dnode_get_parent(dnode, "control-plane-protocol"); + if (!cpp) + return NULL; + + name = yang_dnode_get_string(cpp, "name"); + return ospfd_ietf_ospf_lookup_instance(name); +} + +/* + * Resolve the OSPF instance for create / modify callbacks. Per-leaf callbacks + * route through this so mgmtd cannot accept a commit whose APPLY phase would + * be a silent no-op. + * + * Returns: + * NB_OK + *ospf_out set -- proceed. + * NB_OK + *ospf_out == NULL -- APPLY-phase race tolerated (the instance + * vanished between VALIDATE and APPLY). + * Caller should bail out cleanly. + * NB_ERR_INCONSISTENCY -- VALIDATE rejected the commit. + */ +static int ospfd_ietf_ospf_resolve_instance(const struct lyd_node *dnode, enum nb_event event, + char *errmsg, size_t errmsg_len, struct ospf **ospf_out) +{ + struct ospf *ospf; + + ospf = ospfd_ietf_ospf_instance_from_dnode(dnode); + *ospf_out = ospf; + if (ospf) + return NB_OK; + + if (event == NB_EV_VALIDATE) { + const struct lyd_node *cpp = yang_dnode_get_parent(dnode, "control-plane-protocol"); + + snprintf(errmsg, errmsg_len, + "OSPF instance '%s' is not configured (use 'router ospf' first)", + cpp ? yang_dnode_get_string(cpp, "name") : "?"); + return NB_ERR_INCONSISTENCY; + } + return NB_OK; +} + +static bool ospfd_ietf_ospf_resolve_destroy_instance(struct nb_cb_destroy_args *args, + struct ospf **ospf) +{ + *ospf = NULL; + if (args->event == NB_EV_APPLY) + *ospf = ospfd_ietf_ospf_instance_from_dnode(args->dnode); + + return *ospf != NULL; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/explicit-router-id + */ +int ospfd_ietf_ospf_explicit_router_id_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + struct in_addr router_id; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + yang_dnode_get_ipv4(&router_id, args->dnode, NULL); + ospf->router_id_static = router_id; + ospf_router_id_update(ospf); + + return NB_OK; +} + +int ospfd_ietf_ospf_explicit_router_id_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + + if (!ospfd_ietf_ospf_resolve_destroy_instance(args, &ospf)) + return NB_OK; + + ospf->router_id_static.s_addr = INADDR_ANY; + ospf_router_id_update(ospf); + + return NB_OK; +} + +/* + * Walk up from a dnode within the areas/area subtree to extract the + * area-id key. Returns 0 on success, -1 on failure. + */ +static int ospfd_ietf_ospf_area_id_from_dnode(const struct lyd_node *dnode, struct in_addr *area_id) +{ + const struct lyd_node *area_node; + const char *area_id_str; + + area_node = yang_dnode_get_parent(dnode, "area"); + if (!area_node) + return -1; + + area_id_str = yang_dnode_get_string(area_node, "area-id"); + if (inet_pton(AF_INET, area_id_str, area_id) != 1) + return -1; + return 0; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area + * + * area-id key is an RFC 9129 area-id-type, which serialises as A.B.C.D. + * The list create callback materialises the FRR-side ospf_area struct via + * ospf_area_get (the FRR-internal lookup-or-create). Per-leaf callbacks + * re-derive the area from the parent control-plane-protocol's name key + * plus the area's area-id key rather than going through nb_running_get_entry, + * because the ospf instance itself is not yet NB-managed (still CLI-owned) so + * nb_running_get_entry walks may return stale entries from unrelated paths. + */ +int ospfd_ietf_ospf_areas_area_create(struct nb_cb_create_args *args) +{ + struct ospf *ospf; + int ret; + struct in_addr area_id; + const char *area_id_str; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + area_id_str = yang_dnode_get_string(args->dnode, "area-id"); + if (inet_pton(AF_INET, area_id_str, &area_id) != 1) + return NB_ERR; + + (void)ospf_area_get(ospf, area_id); + + return NB_OK; +} + +static void ospfd_ietf_ospf_area_ranges_clear(struct ospf *ospf, + struct ospf_area *area, + struct route_table *ranges) +{ + struct route_node *rn; + struct route_node *next; + struct prefix_ipv4 p; + bool has_info; + + /* + * Snapshot the prefix BEFORE calling route_next: for radix glue nodes + * (rn->info == NULL, held only by the iterator at lock = 1) + * route_next unlocks rn to 0 and frees it, so any rn->p access + * afterwards is a use-after-free. Skip glue nodes outright; only data + * nodes (rn->info != NULL) have a stored lock that keeps rn alive + * after route_next. + */ + for (rn = route_top(ranges); rn; rn = next) { + has_info = rn->info != NULL; + if (has_info) { + p.family = AF_INET; + p.prefix = rn->p.u.prefix4; + p.prefixlen = rn->p.prefixlen; + } + next = route_next(rn); + if (has_info) + ospf_area_range_unset(ospf, area, ranges, &p); + } +} + +int ospfd_ietf_ospf_areas_area_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + struct in_addr area_id; + const char *area_id_str; + + if (!ospfd_ietf_ospf_resolve_destroy_instance(args, &ospf)) + return NB_OK; + + area_id_str = yang_dnode_get_string(args->dnode, "area-id"); + if (inet_pton(AF_INET, area_id_str, &area_id) != 1) + return NB_ERR; + + /* + * Reset every area attr back to FRR defaults so + * ospf_area_check_free's precondition (external_routing == DEFAULT, + * no_summary == 0, default_cost == 1, no ranges) can match. Per-leaf + * destroy isn't dispatched for leaves with YANG defaults (area-type + * defaults to normal-area), so we mirror those resets here. + */ + ospf_area_stub_unset(ospf, area_id); + ospf_area_nssa_unset(ospf, area_id); + ospf_area_no_summary_unset(ospf, area_id); + { + struct ospf_area *area; + + area = ospf_area_lookup_by_area_id(ospf, area_id); + if (area) { + area->default_cost = 1; + + /* + * Drop all range tables before the check_free below; + * ospf_area_check_free requires both to be empty. + */ + ospfd_ietf_ospf_area_ranges_clear(ospf, area, + area->ranges); + ospfd_ietf_ospf_area_ranges_clear(ospf, area, + area->nssa_ranges); + } + } + + ospf_area_check_free(ospf, area_id); + + return NB_OK; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/area-type + * + * Map the RFC 9129 identityref values to FRR's area type setters: + * normal-area -> ospf_area_stub_unset + ospf_area_nssa_unset (back to DEFAULT) + * stub-area -> ospf_area_stub_set + * nssa-area -> deferred; NSSA-specific attrs handled in a follow-up slice + */ +int ospfd_ietf_ospf_areas_area_type_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + struct in_addr area_id; + const char *type; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (ospfd_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) { + if (args->event == NB_EV_VALIDATE) + snprintf(args->errmsg, args->errmsg_len, "malformed area-id"); + return ospfd_ietf_ospf_parse_error(args->event); + } + + type = yang_dnode_get_string(args->dnode, NULL); + + /* + * Reject stub / NSSA conversions at VALIDATE when the area has + * virtual links traversing it. ospf_area_{stub,nssa}_set return 0 + * for this case but a deferred APPLY-phase NB_ERR_INCONSISTENCY is + * logged-and-dropped by mgmtd, leaving the YANG datastore recording + * stub-area/nssa-area while ospfd keeps running the area as normal + * -- a persistent invisible split between the two planes. Look up + * the area only if it already exists; atomic-create transactions + * cannot have virtual links yet so are tolerated by falling through + * to APPLY. + */ + if (ospf_area_type_is(type, OSPF_AREA_TYPE_STUB) || + ospf_area_type_is(type, OSPF_AREA_TYPE_NSSA)) { + struct ospf_area *area = ospf_area_lookup_by_area_id(ospf, area_id); + + if (area && ospf_area_vlink_count(ospf, area)) { + if (args->event == NB_EV_VALIDATE) + snprintf(args->errmsg, args->errmsg_len, + "area %pI4 has virtual links traversing it; remove them before converting to %s", + &area_id, type); + return NB_ERR_INCONSISTENCY; + } + } + + if (args->event != NB_EV_APPLY) + return NB_OK; + + if (ospf_area_type_is(type, OSPF_AREA_TYPE_NORMAL)) { + ospf_area_stub_unset(ospf, area_id); + ospf_area_nssa_unset(ospf, area_id); + } else if (ospf_area_type_is(type, OSPF_AREA_TYPE_STUB)) { + ospf_area_nssa_unset(ospf, area_id); + if (!ospf_area_stub_set(ospf, area_id)) + return NB_ERR_INCONSISTENCY; + /* + * Stub areas don't carry AS-external LSAs; purge any + * lingering ones from when the area was non-stub. The + * legacy CLI did this explicitly after stub_set, so we + * replicate it here to keep CLI- and YANG-driven paths + * semantically identical. + */ + ospf_flush_lsa_from_area(ospf, area_id, OSPF_AS_EXTERNAL_LSA); + } else if (ospf_area_type_is(type, OSPF_AREA_TYPE_NSSA)) { + ospf_area_stub_unset(ospf, area_id); + if (!ospf_area_nssa_set(ospf, area_id)) + return NB_ERR_INCONSISTENCY; + } else { + return NB_ERR; + } + + return NB_OK; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/summary + * + * RFC 9129 inverts FRR's no_summary flag: summary=true means summary LSAs + * ARE injected (the default for a stub area), summary=false means they're + * suppressed (totally stubby). + */ +int ospfd_ietf_ospf_areas_area_summary_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + struct in_addr area_id; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + if (ospfd_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return NB_ERR; + + if (yang_dnode_get_bool(args->dnode, NULL)) + ospf_area_no_summary_unset(ospf, area_id); + else + ospf_area_no_summary_set(ospf, area_id); + + return NB_OK; +} + +int ospfd_ietf_ospf_areas_area_summary_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + struct in_addr area_id; + + if (!ospfd_ietf_ospf_resolve_destroy_instance(args, &ospf)) + return NB_OK; + if (ospfd_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return NB_ERR; + + /* + * summary has no YANG default; destroy means the operator removed the + * explicit setting. Revert to FRR's natural default (summary LSAs on). + */ + ospf_area_no_summary_unset(ospf, area_id); + + return NB_OK; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/default-cost + * + * The YANG `when` clause restricts this leaf to stub or NSSA areas. The + * modify callback still defensively checks external_routing at APPLY so + * an impossible normal-area candidate cannot mutate the daemon. + */ +int ospfd_ietf_ospf_areas_area_default_cost_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + struct in_addr area_id; + struct ospf_area *area; + struct prefix_ipv4 p = { + .family = AF_INET, + .prefix.s_addr = OSPF_DEFAULT_DESTINATION, + .prefixlen = 0, + }; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (ospfd_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) { + if (args->event == NB_EV_VALIDATE) + snprintf(args->errmsg, args->errmsg_len, "malformed area-id"); + return ospfd_ietf_ospf_parse_error(args->event); + } + + if (args->event != NB_EV_APPLY) + return NB_OK; + + /* + * Defer the area-existence and area-type checks to APPLY: at + * VALIDATE the area may be in the same candidate transaction as + * this leaf (atomic stub-area + default-cost commit), so + * ospf_area_lookup_by_area_id would return NULL even though the + * area-create APPLY runs first within the same transaction. RFC + * 9129's `when` clause on default-cost (restricts to stub / NSSA + * in the candidate datastore) is the authoritative area-type + * cross-leaf check; libyang enforces it before any callback + * fires. The check here is defensive at APPLY time only: + * silently no-op if the area is gone, defensively skip the + * mutation on a normal area (shouldn't happen, but matches the + * "logged and dropped" APPLY-error semantics). + */ + area = ospf_area_lookup_by_area_id(ospf, area_id); + if (!area) + return NB_OK; + if (area->external_routing == OSPF_AREA_DEFAULT) + return NB_OK; + + area->default_cost = yang_dnode_get_uint32(args->dnode, NULL); + ospf_abr_announce_network_to_area(&p, area->default_cost, area); + + return NB_OK; +} + +/* + * Walk up from a dnode within the area/interfaces/interface subtree to + * extract the interface name. Returns NULL if the parent list entry + * cannot be found (the schema guarantees it under areas/area, so this + * is purely defensive). + */ +static const char *ospfd_ietf_ospf_interface_name_from_dnode(const struct lyd_node *dnode) +{ + const struct lyd_node *iface_node; + + iface_node = yang_dnode_get_parent(dnode, "interface"); + if (!iface_node) + return NULL; + return yang_dnode_get_string(iface_node, "name"); +} + +/* + * Resolve a YANG per-interface dnode to the FRR-side struct interface in + * the OSPF instance's VRF. ietf-interfaces names are unqualified for + * vrf-lite mode and "::" qualified under netns backend (the + * same convention zebra uses to populate /ietf-interfaces:interfaces). + * Returns NULL if no matching interface exists, which the caller treats + * as NB_OK (no-op apply). + */ +static struct interface *ospfd_ietf_ospf_interface_from_dnode(const struct ospf *ospf, + const struct lyd_node *dnode) +{ + const char *name; + + name = ospfd_ietf_ospf_interface_name_from_dnode(dnode); + if (!name) + return NULL; + return if_lookup_by_name(name, ospf->vrf_id); +} + +static bool ospfd_ietf_ospf_area_has_interface(const struct lyd_node *area_node, + const char *ifname) +{ + const struct lyd_node *interfaces_node; + const struct lyd_node *iface_node; + + interfaces_node = yang_dnode_get(area_node, "interfaces"); + if (!interfaces_node) + return false; + + LY_LIST_FOR (lyd_child(interfaces_node), iface_node) { + const char *name; + + if (strcmp(iface_node->schema->name, "interface")) + continue; + + name = yang_dnode_get_string(iface_node, "name"); + if (name && !strcmp(name, ifname)) + return true; + } + + return false; +} + +static int ospfd_ietf_ospf_validate_interface_area_unique(const struct lyd_node *dnode, + const char *ifname, char *errmsg, + size_t errmsg_len) +{ + const struct lyd_node *area_node; + const struct lyd_node *areas_node; + const struct lyd_node *other_area_node; + const char *area_id; + + area_node = yang_dnode_get_parent(dnode, "area"); + if (!area_node) + return NB_OK; + areas_node = yang_dnode_get_parent(area_node, "areas"); + if (!areas_node) + return NB_OK; + + area_id = yang_dnode_get_string(area_node, "area-id"); + LY_LIST_FOR (lyd_child(areas_node), other_area_node) { + const char *other_area_id; + + if (other_area_node == area_node || + strcmp(other_area_node->schema->name, "area")) + continue; + if (!ospfd_ietf_ospf_area_has_interface(other_area_node, ifname)) + continue; + + other_area_id = yang_dnode_get_string(other_area_node, "area-id"); + snprintf(errmsg, errmsg_len, + "interface '%s' is configured under multiple OSPF areas (%s and %s)", + ifname, area_id, other_area_id); + return NB_ERR_INCONSISTENCY; + } + + return NB_OK; +} + +/* + * VALIDATE-rejecting variant for per-interface create / modify callbacks. + * frr-deviations-ietf-routing-ospf relaxes the RFC 9129 interface-name + * leafref so configuration can be emitted ahead of interface plumbing. + * The relaxation removes libyang's referential check; this helper restores + * it inside the callback, returning NB_ERR_INCONSISTENCY at VALIDATE when + * the interface is not present in the OSPF instance's VRF. + * + * Returns: + * NB_OK + *ifp_out set -- proceed. + * NB_OK + *ifp_out == NULL -- APPLY-phase race tolerated. + * NB_ERR_INCONSISTENCY -- VALIDATE rejected. + */ +static int ospfd_ietf_ospf_resolve_interface(const struct ospf *ospf, const struct lyd_node *dnode, + enum nb_event event, char *errmsg, size_t errmsg_len, + struct interface **ifp_out) +{ + struct interface *ifp; + + ifp = ospfd_ietf_ospf_interface_from_dnode(ospf, dnode); + *ifp_out = ifp; + if (ifp) + return NB_OK; + + if (event == NB_EV_VALIDATE) { + const struct lyd_node *iface_node = yang_dnode_get_parent(dnode, "interface"); + + snprintf(errmsg, errmsg_len, "interface '%s' is not present in vrf-id %u", + iface_node ? yang_dnode_get_string(iface_node, "name") : "?", + ospf->vrf_id); + return NB_ERR_INCONSISTENCY; + } + return NB_OK; +} + +static bool ospfd_ietf_ospf_ensure_if_info(struct interface *ifp) +{ + if (!IF_OSPF_IF_INFO(ifp) && ospf_if_new_hook(ifp) != 0) + return false; + + return IF_OSPF_IF_INFO(ifp) && IF_DEF_PARAMS(ifp); +} + +static int ospfd_ietf_ospf_resolve_modify_interface(struct nb_cb_modify_args *args, + struct ospf **ospf, + struct interface **ifp) +{ + int ret; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, + args->errmsg, args->errmsg_len, + ospf); + if (ret != NB_OK || !*ospf) + return ret; + + ret = ospfd_ietf_ospf_resolve_interface(*ospf, args->dnode, args->event, + args->errmsg, args->errmsg_len, + ifp); + if (ret != NB_OK || !*ifp) + return ret; + + if (args->event == NB_EV_APPLY && !ospfd_ietf_ospf_ensure_if_info(*ifp)) + return NB_ERR; + + return NB_OK; +} + +static int ospfd_ietf_ospf_resolve_create_interface(struct nb_cb_create_args *args, + struct ospf **ospf, + struct interface **ifp) +{ + int ret; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, + args->errmsg, args->errmsg_len, + ospf); + if (ret != NB_OK || !*ospf) + return ret; + + ret = ospfd_ietf_ospf_resolve_interface(*ospf, args->dnode, args->event, + args->errmsg, args->errmsg_len, + ifp); + if (ret != NB_OK || !*ifp) + return ret; + + if (args->event == NB_EV_APPLY && !ospfd_ietf_ospf_ensure_if_info(*ifp)) + return NB_ERR; + + return NB_OK; +} + +static bool ospfd_ietf_ospf_resolve_destroy_interface(struct nb_cb_destroy_args *args, + struct ospf **ospf, + struct interface **ifp) +{ + *ospf = NULL; + *ifp = NULL; + if (args->event != NB_EV_APPLY) + return false; + + *ospf = ospfd_ietf_ospf_instance_from_dnode(args->dnode); + if (!*ospf) + return false; + + *ifp = ospfd_ietf_ospf_interface_from_dnode(*ospf, args->dnode); + if (!*ifp) + return false; + + return IF_OSPF_IF_INFO(*ifp) && IF_DEF_PARAMS(*ifp); +} + +static void ospfd_ietf_ospf_nbr_timer_update(struct interface *ifp) +{ + struct route_node *rn; + struct ospf_interface *oi; + + if (!IF_OSPF_IF_INFO(ifp) || !IF_OIFS(ifp)) + return; + + for (rn = route_top(IF_OIFS(ifp)); rn; rn = route_next(rn)) { + oi = rn->info; + if (oi) + ospf_nbr_timer_update(oi); + } +} + +static void ospfd_ietf_ospf_priority_update(struct interface *ifp) +{ + struct route_node *rn; + struct ospf_interface *oi; + + if (!IF_OSPF_IF_INFO(ifp) || !IF_OIFS(ifp)) + return; + + for (rn = route_top(IF_OIFS(ifp)); rn; rn = route_next(rn)) { + oi = rn->info; + if (!oi) + continue; + + if (PRIORITY(oi) == OSPF_IF_PARAM(oi, priority)) + continue; + + PRIORITY(oi) = OSPF_IF_PARAM(oi, priority); + OSPF_ISM_EVENT_SCHEDULE(oi, ISM_NeighborChange); + } +} + +int ospfd_ietf_ospf_areas_area_default_cost_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + struct in_addr area_id; + struct ospf_area *area; + struct prefix_ipv4 p = { + .family = AF_INET, + .prefix.s_addr = OSPF_DEFAULT_DESTINATION, + .prefixlen = 0, + }; + + if (!ospfd_ietf_ospf_resolve_destroy_instance(args, &ospf)) + return NB_OK; + if (ospfd_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return NB_ERR; + + area = ospf_area_lookup_by_area_id(ospf, area_id); + if (!area) + return NB_OK; + + area->default_cost = 1; + if (area->external_routing != OSPF_AREA_DEFAULT) + ospf_abr_announce_network_to_area(&p, area->default_cost, area); + + return NB_OK; +} + + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/interfaces/interface + * + * RFC 9129 keys the interface entry by name with the area as the + * structural parent: creating the entry assigns the interface to that + * area. This is the same semantic as the legacy `ip ospf area X` + * per-interface command, but expressed through the area-centric YANG + * shape rather than the interface-centric CLI shape. + * + * One interface, one area. VALIDATE rejects a candidate that contains + * the same interface entry under more than one area, matching FRR's + * OSPF interface model and the legacy CLI's "Must remove previous + * area/address config before changing ospf area" restriction. + * + * Per-address overrides (`ip ospf area X A.B.C.D`) have no RFC 9129 + * counterpart and stay reachable only via the legacy CLI on the + * direct-mutation path. The YANG-managed default-params attachment + * is the canonical one for newly-emitted config. + */ +int ospfd_ietf_ospf_areas_area_interfaces_interface_create(struct nb_cb_create_args *args) +{ + struct ospf *ospf; + int ret; + struct interface *ifp; + struct ospf_if_params *params; + struct in_addr area_id; + int format = OSPF_AREA_ID_FMT_DOTTEDQUAD; + + ret = ospfd_ietf_ospf_resolve_create_interface(args, &ospf, &ifp); + if (ret != NB_OK || !ospf || !ifp) + return ret; + + if (args->event == NB_EV_VALIDATE) { + ret = ospfd_ietf_ospf_validate_interface_area_unique( + args->dnode, ifp->name, args->errmsg, args->errmsg_len); + if (ret != NB_OK) + return ret; + } + + if (ospfd_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return ospfd_ietf_ospf_parse_error(args->event); + + if (args->event != NB_EV_APPLY) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + SET_IF_PARAM(params, if_area); + params->if_area = area_id; + params->if_area_id_fmt = format; + + ospf_interface_area_set(ospf, ifp); + + return NB_OK; +} + +int ospfd_ietf_ospf_areas_area_interfaces_interface_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + struct interface *ifp; + struct ospf_if_params *params; + struct in_addr area_id; + + if (!ospfd_ietf_ospf_resolve_destroy_interface(args, &ospf, &ifp)) + return NB_OK; + if (ospfd_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return NB_ERR; + + params = IF_DEF_PARAMS(ifp); + if (!OSPF_IF_PARAM_CONFIGURED(params, if_area)) + return NB_OK; + + /* + * Mirror the legacy `no ip ospf area` semantics exactly: only clear + * the area binding. Per-interface attrs (cost, hello-interval, + * dead-interval, etc.) are intentionally preserved across area + * changes -- operators rely on `no ip ospf area` / + * `ip ospf area ` to re-bind without losing tuning. + */ + UNSET_IF_PARAM(params, if_area); + ospf_interface_area_unset(ospf, ifp); + ospf_area_check_free(ospf, area_id); + + return NB_OK; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/interfaces/interface/cost + * + * Maps to the per-interface output cost via IF_DEF_PARAMS. The YANG + * model is strictly per-interface, so we always mutate the default + * params -- the legacy per-address overrides reachable through + * `ip ospf cost N A.B.C.D` remain on the direct-mutation path. + */ +int ospfd_ietf_ospf_areas_area_interfaces_interface_cost_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + struct interface *ifp; + struct ospf_if_params *params; + + ret = ospfd_ietf_ospf_resolve_modify_interface(args, &ospf, &ifp); + if (ret != NB_OK || !ospf || !ifp) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + SET_IF_PARAM(params, output_cost_cmd); + /* ospf-link-metric is uint16; widen on store into output_cost_cmd. */ + params->output_cost_cmd = yang_dnode_get_uint16(args->dnode, NULL); + + ospf_if_recalculate_output_cost(ifp); + + return NB_OK; +} + +int ospfd_ietf_ospf_areas_area_interfaces_interface_cost_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + struct interface *ifp; + struct ospf_if_params *params; + + if (!ospfd_ietf_ospf_resolve_destroy_interface(args, &ospf, &ifp)) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + if (!OSPF_IF_PARAM_CONFIGURED(params, output_cost_cmd)) + return NB_OK; + + UNSET_IF_PARAM(params, output_cost_cmd); + ospf_if_recalculate_output_cost(ifp); + + return NB_OK; +} + +/* + * Per-interface uint16/uint8/boolean leaves under + * /ietf-routing:routing/.../ospf/areas/area/interfaces/interface. + * + * All of them mutate IF_DEF_PARAMS only. The YANG model is strictly + * per-interface; legacy per-address overrides (e.g. `ip ospf hello-interval N A.B.C.D`) + * remain accessible only via the legacy CLI direct-mutation path. + */ + +/* XPath: .../interface/hello-interval */ +int ospfd_ietf_ospf_areas_area_interfaces_interface_hello_interval_modify( + struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + struct interface *ifp; + struct ospf_if_params *params; + uint16_t seconds; + struct in_addr addr = { .s_addr = 0L }; + + ret = ospfd_ietf_ospf_resolve_modify_interface(args, &ospf, &ifp); + if (ret != NB_OK || !ospf || !ifp) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + seconds = yang_dnode_get_uint16(args->dnode, NULL); + if (params->v_hello == seconds) + return NB_OK; + + SET_IF_PARAM(params, v_hello); + params->v_hello = seconds; + + /* + * Mirror the legacy `ip ospf hello-interval` side effect: if the + * operator hasn't explicitly set dead-interval, derive it from the + * hello (RFC 4062 recommends roughly 4x). The YANG dead-interval + * `must` clause (dead > hello) is enforced by libyang at commit. + */ + if (!params->is_v_wait_set) { + SET_IF_PARAM(params, v_wait); + params->v_wait = 4 * seconds; + ospfd_ietf_ospf_nbr_timer_update(ifp); + } + + ospf_reset_hello_timer(ifp, addr, false); + return NB_OK; +} + +int ospfd_ietf_ospf_areas_area_interfaces_interface_hello_interval_destroy( + struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + struct interface *ifp; + struct ospf_if_params *params; + struct in_addr addr = { .s_addr = 0L }; + + if (!ospfd_ietf_ospf_resolve_destroy_interface(args, &ospf, &ifp)) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + UNSET_IF_PARAM(params, v_hello); + params->v_hello = OSPF_HELLO_INTERVAL_DEFAULT; + + if (!params->is_v_wait_set) { + UNSET_IF_PARAM(params, v_wait); + params->v_wait = OSPF_ROUTER_DEAD_INTERVAL_DEFAULT; + ospfd_ietf_ospf_nbr_timer_update(ifp); + } + + ospf_reset_hello_timer(ifp, addr, false); + return NB_OK; +} + +/* XPath: .../interface/dead-interval */ +int ospfd_ietf_ospf_areas_area_interfaces_interface_dead_interval_modify( + struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + struct interface *ifp; + struct ospf_if_params *params; + + ret = ospfd_ietf_ospf_resolve_modify_interface(args, &ospf, &ifp); + if (ret != NB_OK || !ospf || !ifp) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + SET_IF_PARAM(params, v_wait); + params->v_wait = yang_dnode_get_uint16(args->dnode, NULL); + params->is_v_wait_set = true; + ospfd_ietf_ospf_nbr_timer_update(ifp); + return NB_OK; +} + +int ospfd_ietf_ospf_areas_area_interfaces_interface_dead_interval_destroy( + struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + struct interface *ifp; + struct ospf_if_params *params; + + if (!ospfd_ietf_ospf_resolve_destroy_interface(args, &ospf, &ifp)) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + UNSET_IF_PARAM(params, v_wait); + params->v_wait = OSPF_ROUTER_DEAD_INTERVAL_DEFAULT; + params->is_v_wait_set = false; + UNSET_IF_PARAM(params, fast_hello); + params->fast_hello = OSPF_FAST_HELLO_DEFAULT; + ospfd_ietf_ospf_nbr_timer_update(ifp); + return NB_OK; +} + +/* XPath: .../interface/retransmit-interval */ +int ospfd_ietf_ospf_areas_area_interfaces_interface_retransmit_interval_modify( + struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + struct interface *ifp; + struct ospf_if_params *params; + + ret = ospfd_ietf_ospf_resolve_modify_interface(args, &ospf, &ifp); + if (ret != NB_OK || !ospf || !ifp) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + SET_IF_PARAM(params, retransmit_interval); + params->retransmit_interval = yang_dnode_get_uint16(args->dnode, NULL); + return NB_OK; +} + +int ospfd_ietf_ospf_areas_area_interfaces_interface_retransmit_interval_destroy( + struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + struct interface *ifp; + struct ospf_if_params *params; + + if (!ospfd_ietf_ospf_resolve_destroy_interface(args, &ospf, &ifp)) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + UNSET_IF_PARAM(params, retransmit_interval); + params->retransmit_interval = OSPF_RETRANSMIT_INTERVAL_DEFAULT; + return NB_OK; +} + +/* XPath: .../interface/priority */ +int ospfd_ietf_ospf_areas_area_interfaces_interface_priority_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + struct interface *ifp; + struct ospf_if_params *params; + + ret = ospfd_ietf_ospf_resolve_modify_interface(args, &ospf, &ifp); + if (ret != NB_OK || !ospf || !ifp) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + SET_IF_PARAM(params, priority); + params->priority = yang_dnode_get_uint8(args->dnode, NULL); + ospfd_ietf_ospf_priority_update(ifp); + return NB_OK; +} + +int ospfd_ietf_ospf_areas_area_interfaces_interface_priority_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + struct interface *ifp; + struct ospf_if_params *params; + + if (!ospfd_ietf_ospf_resolve_destroy_interface(args, &ospf, &ifp)) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + UNSET_IF_PARAM(params, priority); + params->priority = OSPF_ROUTER_PRIORITY_DEFAULT; + ospfd_ietf_ospf_priority_update(ifp); + return NB_OK; +} + +/* XPath: .../interface/mtu-ignore */ +int ospfd_ietf_ospf_areas_area_interfaces_interface_mtu_ignore_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + struct interface *ifp; + struct ospf_if_params *params; + + ret = ospfd_ietf_ospf_resolve_modify_interface(args, &ospf, &ifp); + if (ret != NB_OK || !ospf || !ifp) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + SET_IF_PARAM(params, mtu_ignore); + params->mtu_ignore = yang_dnode_get_bool(args->dnode, NULL) ? 1 : 0; + return NB_OK; +} + +int ospfd_ietf_ospf_areas_area_interfaces_interface_mtu_ignore_destroy( + struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + struct interface *ifp; + struct ospf_if_params *params; + + if (!ospfd_ietf_ospf_resolve_destroy_interface(args, &ospf, &ifp)) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + UNSET_IF_PARAM(params, mtu_ignore); + params->mtu_ignore = 0; + return NB_OK; +} + +/* XPath: .../interface/transmit-delay */ +int ospfd_ietf_ospf_areas_area_interfaces_interface_transmit_delay_modify( + struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + struct interface *ifp; + struct ospf_if_params *params; + + ret = ospfd_ietf_ospf_resolve_modify_interface(args, &ospf, &ifp); + if (ret != NB_OK || !ospf || !ifp) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + SET_IF_PARAM(params, transmit_delay); + params->transmit_delay = yang_dnode_get_uint16(args->dnode, NULL); + return NB_OK; +} + +int ospfd_ietf_ospf_areas_area_interfaces_interface_transmit_delay_destroy( + struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + struct interface *ifp; + struct ospf_if_params *params; + + if (!ospfd_ietf_ospf_resolve_destroy_interface(args, &ospf, &ifp)) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + UNSET_IF_PARAM(params, transmit_delay); + params->transmit_delay = OSPF_TRANSMIT_DELAY_DEFAULT; + return NB_OK; +} + +/* + * Helper for areas/area/ranges/range list and per-leaf callbacks: + * walk up to the range list entry, extract the prefix key, narrow to + * struct prefix_ipv4. Returns 0 on success, -1 if the dnode shape is + * unexpected or the prefix isn't IPv4. + */ +static int ospfd_ietf_ospf_range_prefix_from_dnode(const struct lyd_node *dnode, + struct prefix_ipv4 *p) +{ + const struct lyd_node *range_node; + struct prefix pref; + + range_node = yang_dnode_get_parent(dnode, "range"); + if (!range_node) + return -1; + + yang_dnode_get_prefix(&pref, range_node, "prefix"); + if (pref.family != AF_INET) + return -1; + + memset(p, 0, sizeof(*p)); + p->family = AF_INET; + p->prefixlen = pref.prefixlen; + p->prefix = pref.u.prefix4; + return 0; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/ranges/range + * + * RFC 9129's ranges/range list lives under address-family-area-config + * and is keyed by prefix. List create sets up the FRR-side range entry + * with advertise=true (FRR's default, matching the legacy + * `area X range A.B.C.D/M` form without an explicit not-advertise). + * advertise and cost leaves can then mutate per-range state. + * + * The legacy `area X range A.B.C.D/M substitute A.B.C.D/M` form is FRR + * specific and has no RFC 9129 counterpart; it stays reachable only + * via the legacy CLI direct-mutation path. + */ +int ospfd_ietf_ospf_areas_area_ranges_range_create(struct nb_cb_create_args *args) +{ + struct ospf *ospf; + int ret; + struct ospf_area *area; + struct in_addr area_id; + struct prefix_ipv4 p; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + if (ospfd_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return NB_ERR; + if (ospfd_ietf_ospf_range_prefix_from_dnode(args->dnode, &p) < 0) + return NB_ERR; + + area = ospf_area_get(ospf, area_id); + ospf_area_range_set(ospf, area, area->ranges, &p, OSPF_AREA_RANGE_ADVERTISE, false); + + return NB_OK; +} + +int ospfd_ietf_ospf_areas_area_ranges_range_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + struct ospf_area *area; + struct in_addr area_id; + struct prefix_ipv4 p; + + if (!ospfd_ietf_ospf_resolve_destroy_instance(args, &ospf)) + return NB_OK; + if (ospfd_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return NB_ERR; + if (ospfd_ietf_ospf_range_prefix_from_dnode(args->dnode, &p) < 0) + return NB_ERR; + + area = ospf_area_lookup_by_area_id(ospf, area_id); + if (!area) + return NB_OK; + + ospf_area_range_unset(ospf, area, area->ranges, &p); + ospf_area_check_free(ospf, area_id); + return NB_OK; +} + +/* XPath: .../ranges/range/advertise */ +int ospfd_ietf_ospf_areas_area_ranges_range_advertise_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + struct ospf_area *area; + struct in_addr area_id; + struct prefix_ipv4 p; + int advertise; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + if (ospfd_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return NB_ERR; + if (ospfd_ietf_ospf_range_prefix_from_dnode(args->dnode, &p) < 0) + return NB_ERR; + area = ospf_area_lookup_by_area_id(ospf, area_id); + if (!area) + return NB_OK; + + advertise = yang_dnode_get_bool(args->dnode, NULL) ? OSPF_AREA_RANGE_ADVERTISE : 0; + ospf_area_range_set(ospf, area, area->ranges, &p, advertise, false); + return NB_OK; +} + +int ospfd_ietf_ospf_areas_area_ranges_range_advertise_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + struct ospf_area *area; + struct in_addr area_id; + struct prefix_ipv4 p; + + if (!ospfd_ietf_ospf_resolve_destroy_instance(args, &ospf)) + return NB_OK; + if (ospfd_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return NB_ERR; + if (ospfd_ietf_ospf_range_prefix_from_dnode(args->dnode, &p) < 0) + return NB_ERR; + area = ospf_area_lookup_by_area_id(ospf, area_id); + if (!area) + return NB_OK; + + /* No YANG default; revert to FRR's natural default (advertise on). */ + ospf_area_range_set(ospf, area, area->ranges, &p, OSPF_AREA_RANGE_ADVERTISE, false); + return NB_OK; +} + +/* XPath: .../ranges/range/cost */ +int ospfd_ietf_ospf_areas_area_ranges_range_cost_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + struct ospf_area *area; + struct in_addr area_id; + struct prefix_ipv4 p; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + if (ospfd_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return NB_ERR; + if (ospfd_ietf_ospf_range_prefix_from_dnode(args->dnode, &p) < 0) + return NB_ERR; + area = ospf_area_lookup_by_area_id(ospf, area_id); + if (!area) + return NB_OK; + + ospf_area_range_cost_set(ospf, area, area->ranges, &p, + yang_dnode_get_uint32(args->dnode, NULL)); + return NB_OK; +} + +int ospfd_ietf_ospf_areas_area_ranges_range_cost_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + struct ospf_area *area; + struct in_addr area_id; + struct prefix_ipv4 p; + + if (!ospfd_ietf_ospf_resolve_destroy_instance(args, &ospf)) + return NB_OK; + if (ospfd_ietf_ospf_area_id_from_dnode(args->dnode, &area_id) < 0) + return NB_ERR; + if (ospfd_ietf_ospf_range_prefix_from_dnode(args->dnode, &p) < 0) + return NB_ERR; + area = ospf_area_lookup_by_area_id(ospf, area_id); + if (!area) + return NB_OK; + + /* "no explicit cost" -> the auto-cost path takes over. */ + ospf_area_range_cost_set(ospf, area, area->ranges, &p, OSPF_AREA_RANGE_COST_UNSPEC); + return NB_OK; +} + +/* + * XPath: .../interface/interface-type + * + * RFC 9129's interface-type enumeration maps onto FRR's OSPF_IFTYPE_* + * macros. The legacy `ip ospf network` DEFUN also supports FRR-specific + * modifiers (dmvpn, delay-reflood, non-broadcast) that have no RFC 9129 + * counterpart; those stay reachable only through the legacy CLI. + * The YANG modify clears any previously-set FRR modifiers so the + * resulting state is the canonical RFC-9129-shaped one. + */ +static int ospf_iftype_from_yang(const char *val) +{ + if (!strcmp(val, "broadcast")) + return OSPF_IFTYPE_BROADCAST; + if (!strcmp(val, "non-broadcast")) + return OSPF_IFTYPE_NBMA; + if (!strcmp(val, "point-to-multipoint")) + return OSPF_IFTYPE_POINTOMULTIPOINT; + if (!strcmp(val, "point-to-point")) + return OSPF_IFTYPE_POINTOPOINT; + return -1; +} + +static void ospf_apply_interface_type(struct interface *ifp, int new_type) +{ + struct ospf_if_params *params; + struct route_node *rn; + int old_type; + uint8_t old_ptp_dmvpn; + uint8_t old_p2mp_delay_reflood; + uint8_t old_p2mp_non_broadcast; + + if (!ospfd_ietf_ospf_ensure_if_info(ifp)) + return; + + params = IF_DEF_PARAMS(ifp); + old_type = params->type; + old_ptp_dmvpn = params->ptp_dmvpn; + old_p2mp_delay_reflood = params->p2mp_delay_reflood; + old_p2mp_non_broadcast = params->p2mp_non_broadcast; + + /* RFC 9129 has no FRR-specific modifiers; reset them. */ + params->ptp_dmvpn = 0; + params->p2mp_delay_reflood = OSPF_P2MP_DELAY_REFLOOD_DEFAULT; + params->p2mp_non_broadcast = OSPF_P2MP_NON_BROADCAST_DEFAULT; + params->type = new_type; + params->type_cfg = true; + SET_IF_PARAM(params, type); + + if (params->type == old_type && params->ptp_dmvpn == old_ptp_dmvpn && + params->p2mp_delay_reflood == old_p2mp_delay_reflood && + params->p2mp_non_broadcast == old_p2mp_non_broadcast) + return; + + if (!IF_OIFS(ifp)) + return; + + for (rn = route_top(IF_OIFS(ifp)); rn; rn = route_next(rn)) { + struct ospf_interface *oi = rn->info; + + if (!oi) + continue; + oi->type = params->type; + oi->ptp_dmvpn = params->ptp_dmvpn; + oi->p2mp_delay_reflood = params->p2mp_delay_reflood; + oi->p2mp_non_broadcast = params->p2mp_non_broadcast; + + if (oi->type != old_type || oi->ptp_dmvpn != old_ptp_dmvpn || + oi->p2mp_non_broadcast != old_p2mp_non_broadcast) { + if (oi->state > ISM_Down) { + OSPF_ISM_EVENT_EXECUTE(oi, ISM_InterfaceDown); + OSPF_ISM_EVENT_EXECUTE(oi, ISM_InterfaceUp); + } + } + } +} + +int ospfd_ietf_ospf_areas_area_interfaces_interface_interface_type_modify( + struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + struct interface *ifp; + const char *val; + int type; + + ret = ospfd_ietf_ospf_resolve_modify_interface(args, &ospf, &ifp); + if (ret != NB_OK || !ospf || !ifp) + return ret; + + /* + * Loopback interfaces have a fixed OSPF type. Use if_is_loopback, + * the kernel-flag-based check from libfrr, rather than inspecting + * IF_DEF_PARAMS(ifp)->type, which is an OSPF-internal classification + * that is unset until the interface is first picked up by OSPF and + * therefore returns the wrong answer for a brand-new loopback that + * config arrives on before the network statement runs. + */ + if (if_is_loopback(ifp)) { + if (args->event == NB_EV_VALIDATE) + snprintf(args->errmsg, args->errmsg_len, + "cannot set interface-type on loopback interface %s", ifp->name); + return NB_ERR_INCONSISTENCY; + } + + val = yang_dnode_get_string(args->dnode, NULL); + type = ospf_iftype_from_yang(val); + if (type < 0) { + if (args->event == NB_EV_VALIDATE) + snprintf(args->errmsg, args->errmsg_len, + "unsupported interface-type enum '%s'", val); + return ospfd_ietf_ospf_parse_error(args->event); + } + + if (args->event != NB_EV_APPLY) + return NB_OK; + + ospf_apply_interface_type(ifp, type); + return NB_OK; +} + +int ospfd_ietf_ospf_areas_area_interfaces_interface_interface_type_destroy( + struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + struct interface *ifp; + struct ospf_if_params *params; + + if (!ospfd_ietf_ospf_resolve_destroy_interface(args, &ospf, &ifp)) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + if (!OSPF_IF_PARAM_CONFIGURED(params, type)) + return NB_OK; + + UNSET_IF_PARAM(params, type); + params->type_cfg = false; + /* + * Without an explicit setting, ospfd will re-derive the type from + * the underlying kernel interface on the next state recompute. The + * cleanest way to trigger that here is to flap any existing + * ospf_interface objects. + */ + { + struct route_node *rn; + + if (!IF_OIFS(ifp)) + return NB_OK; + + for (rn = route_top(IF_OIFS(ifp)); rn; rn = route_next(rn)) { + struct ospf_interface *oi = rn->info; + + if (oi && oi->state > ISM_Down) { + OSPF_ISM_EVENT_EXECUTE(oi, ISM_InterfaceDown); + OSPF_ISM_EVENT_EXECUTE(oi, ISM_InterfaceUp); + } + } + } + return NB_OK; +} + +/* + * XPath: .../interface/passive + */ +int ospfd_ietf_ospf_areas_area_interfaces_interface_passive_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + struct interface *ifp; + struct ospf_if_params *params; + struct in_addr addr = { .s_addr = INADDR_ANY }; + uint8_t newval; + + ret = ospfd_ietf_ospf_resolve_modify_interface(args, &ospf, &ifp); + if (ret != NB_OK || !ospf || !ifp) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + newval = yang_dnode_get_bool(args->dnode, NULL) ? OSPF_IF_PASSIVE : OSPF_IF_ACTIVE; + ospf_passive_interface_update(ifp, params, addr, newval); + return NB_OK; +} + +int ospfd_ietf_ospf_areas_area_interfaces_interface_passive_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + struct interface *ifp; + struct ospf_if_params *params; + struct in_addr addr = { .s_addr = INADDR_ANY }; + + if (!ospfd_ietf_ospf_resolve_destroy_interface(args, &ospf, &ifp)) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + if (!OSPF_IF_PARAM_CONFIGURED(params, passive_interface)) + return NB_OK; + /* Revert to FRR's natural default (active). */ + ospf_passive_interface_update(ifp, params, addr, OSPF_IF_ACTIVE); + UNSET_IF_PARAM(params, passive_interface); + return NB_OK; +} + +/* + * Per-instance preference (admin distance) leaves. + * + * RFC 9129's preference container is a `choice` between three scopes: + * case single-value -> leaf all (one distance for everything) + * case multi-values/detail -> leaf intra-area, leaf inter-area + * case multi-values/coarse -> leaf internal (both intra+inter), leaf external + * Plus leaf external in both multi-values branches. + * + * FRR's ospf struct has distance_all (single) and distance_intra/ + * distance_inter/distance_external (multi). Setting any "multi" + * leaf to non-zero implicitly forces the multi-values choice; + * mgmtd's libyang validation handles the choice-arm exclusivity at + * commit time. Every modify ends with ospf_restart_spf so the new + * distances reflect in the next SPF result. + */ + +/* XPath: .../ospf/preference/all */ +int ospfd_ietf_ospf_preference_all_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + uint8_t distance; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + distance = yang_dnode_get_uint8(args->dnode, NULL); + if (ospf->distance_all == distance) + return NB_OK; + ospf->distance_all = distance; + ospf_restart_spf(ospf); + return NB_OK; +} + +int ospfd_ietf_ospf_preference_all_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf = ospfd_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf) + return NB_OK; + if (!ospf->distance_all) + return NB_OK; + ospf->distance_all = 0; + ospf_restart_spf(ospf); + return NB_OK; +} + +/* XPath: .../ospf/preference/intra-area */ +int ospfd_ietf_ospf_preference_intra_area_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf->distance_intra = yang_dnode_get_uint8(args->dnode, NULL); + ospf_restart_spf(ospf); + return NB_OK; +} + +int ospfd_ietf_ospf_preference_intra_area_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf = ospfd_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf) + return NB_OK; + ospf->distance_intra = 0; + ospf_restart_spf(ospf); + return NB_OK; +} + +/* XPath: .../ospf/preference/inter-area */ +int ospfd_ietf_ospf_preference_inter_area_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf->distance_inter = yang_dnode_get_uint8(args->dnode, NULL); + ospf_restart_spf(ospf); + return NB_OK; +} + +int ospfd_ietf_ospf_preference_inter_area_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf = ospfd_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf) + return NB_OK; + ospf->distance_inter = 0; + ospf_restart_spf(ospf); + return NB_OK; +} + +/* XPath: .../ospf/preference/internal -- coarse-mode shorthand: both intra & inter to the same value. */ +int ospfd_ietf_ospf_preference_internal_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + uint8_t distance; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + distance = yang_dnode_get_uint8(args->dnode, NULL); + ospf->distance_intra = distance; + ospf->distance_inter = distance; + ospf_restart_spf(ospf); + return NB_OK; +} + +int ospfd_ietf_ospf_preference_internal_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf = ospfd_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf) + return NB_OK; + ospf->distance_intra = 0; + ospf->distance_inter = 0; + ospf_restart_spf(ospf); + return NB_OK; +} + +/* XPath: .../ospf/preference/external */ +int ospfd_ietf_ospf_preference_external_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf->distance_external = yang_dnode_get_uint8(args->dnode, NULL); + ospf_restart_spf(ospf); + return NB_OK; +} + +int ospfd_ietf_ospf_preference_external_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf = ospfd_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf) + return NB_OK; + ospf->distance_external = 0; + ospf_restart_spf(ospf); + return NB_OK; +} + +/* + * XPath: .../ospf/spf-control/paths + * + * Per-instance maximum ECMP paths. Mirrors the legacy `maximum-paths + * N` CLI. RFC 9129 types `paths` as uint16 (range 1..65535), so FRR's + * configured MULTIPATH_NUM cap (typically 16..64) fits trivially. The + * destroy callback restores FRR's "no maximum-paths" semantics + * (MULTIPATH_NUM), not RFC 9129's absent YANG default. + */ +int ospfd_ietf_ospf_spf_control_paths_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + uint16_t paths; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event == NB_EV_VALIDATE) { + paths = yang_dnode_get_uint16(args->dnode, NULL); + if (paths > MULTIPATH_NUM) { + snprintf(args->errmsg, args->errmsg_len, + "maximum-paths exceeds platform max %u", + MULTIPATH_NUM); + return NB_ERR_INCONSISTENCY; + } + } + + if (args->event != NB_EV_APPLY) + return NB_OK; + + paths = yang_dnode_get_uint16(args->dnode, NULL); + if (ospf->max_multipath == paths) + return NB_OK; + ospf->max_multipath = paths; + ospf_restart_spf(ospf); + return NB_OK; +} + +int ospfd_ietf_ospf_spf_control_paths_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf = ospfd_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf) + return NB_OK; + if (ospf->max_multipath == MULTIPATH_NUM) + return NB_OK; + ospf->max_multipath = MULTIPATH_NUM; + ospf_restart_spf(ospf); + return NB_OK; +} + +/* + * XPath: .../ospf/mpls/ldp/igp-sync + * + * Per-instance MPLS LDP/IGP sync toggle. Mirrors the legacy + * `mpls ldp-sync` / `no mpls ldp-sync` CLI. Enabling registers the + * opaque LDP-IGP zclient handlers and walks all point-to-point OSPF + * interfaces in the default VRF to start sync; disabling unwinds via + * `ospf_ldp_sync_gbl_exit`, which clears the flag, resets the + * holddown timer, and tears down the per-interface state. FRR's + * LDP/IGP sync is restricted to the default VRF, mirroring the + * legacy CLI's `ldp-sync only runs on DEFAULT VRF` precondition. + */ +int ospfd_ietf_ospf_mpls_ldp_igp_sync_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + struct vrf *vrf; + struct interface *ifp; + int ret; + bool enable; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event == NB_EV_VALIDATE) { + if (ospf->vrf_id != VRF_DEFAULT) { + snprintf(args->errmsg, args->errmsg_len, + "ldp-sync only runs on DEFAULT VRF"); + return NB_ERR_VALIDATION; + } + return NB_OK; + } + + if (args->event != NB_EV_APPLY) + return NB_OK; + + enable = yang_dnode_get_bool(args->dnode, NULL); + + if (enable) { + if (CHECK_FLAG(ospf->ldp_sync_cmd.flags, LDP_SYNC_FLAG_ENABLE)) + return NB_OK; + zclient_register_opaque(ospf_zclient, + LDP_IGP_SYNC_IF_STATE_UPDATE); + zclient_register_opaque(ospf_zclient, + LDP_IGP_SYNC_ANNOUNCE_UPDATE); + SET_FLAG(ospf->ldp_sync_cmd.flags, LDP_SYNC_FLAG_ENABLE); + vrf = vrf_lookup_by_id(ospf->vrf_id); + FOR_ALL_INTERFACES (vrf, ifp) + ospf_if_set_ldp_sync_enable(ospf, ifp); + } else { + ospf_ldp_sync_gbl_exit(ospf, true); + } + return NB_OK; +} + +int ospfd_ietf_ospf_mpls_ldp_igp_sync_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf = ospfd_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf) + return NB_OK; + ospf_ldp_sync_gbl_exit(ospf, true); + return NB_OK; +} + +/* + * XPath: .../ospf/stub-router/always + * + * Presence container that mirrors the legacy + * `max-metric router-lsa administrative` CLI (RFC 6987 unconditional + * stub router). The intermediate `choice trigger` YANG node is not + * a data-tree node, so the data path skips it (RFC 7950 sec 7.9.2). + * Create sets `OSPF_AREA_ADMIN_STUB_ROUTED` on every existing area + * and arms the `stub_router_admin_set` flag so later-created areas + * inherit the property; destroy unwinds, preserving any startup- + * timer-driven stub state already in flight. + */ +int ospfd_ietf_ospf_stub_router_always_create(struct nb_cb_create_args *args) +{ + struct ospf *ospf; + int ret; + struct listnode *ln; + struct ospf_area *area; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + for (ALL_LIST_ELEMENTS_RO(ospf->areas, ln, area)) { + SET_FLAG(area->stub_router_state, OSPF_AREA_ADMIN_STUB_ROUTED); + if (!CHECK_FLAG(area->stub_router_state, OSPF_AREA_IS_STUB_ROUTED)) + ospf_router_lsa_update_area(area); + } + ospf->stub_router_admin_set = OSPF_STUB_ROUTER_ADMINISTRATIVE_SET; + return NB_OK; +} + +int ospfd_ietf_ospf_stub_router_always_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + struct listnode *ln; + struct ospf_area *area; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf = ospfd_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf) + return NB_OK; + + for (ALL_LIST_ELEMENTS_RO(ospf->areas, ln, area)) { + UNSET_FLAG(area->stub_router_state, OSPF_AREA_ADMIN_STUB_ROUTED); + /* Don't trample on the start-up stub timer */ + if (CHECK_FLAG(area->stub_router_state, OSPF_AREA_IS_STUB_ROUTED) + && !area->t_stub_router) { + UNSET_FLAG(area->stub_router_state, + OSPF_AREA_IS_STUB_ROUTED); + ospf_router_lsa_update_area(area); + } + } + ospf->stub_router_admin_set = OSPF_STUB_ROUTER_ADMINISTRATIVE_UNSET; + return NB_OK; +} + +/* + * Reorigninate the Router-LSA (and Network-LSA when DR) on every OSPF + * interface attached to `ifp`, mirroring the legacy `ip ospf + * prefix-suppression` DEFPY's per-interface fan-out so the LSA contents + * track the flag change immediately. + */ +static void ospfd_ietf_ospf_prefix_suppression_lsa_update(struct interface *ifp) +{ + struct route_node *rn; + + if (!IF_OSPF_IF_INFO(ifp) || !IF_OIFS(ifp)) + return; + + for (rn = route_top(IF_OIFS(ifp)); rn; rn = route_next(rn)) { + struct ospf_interface *oi = rn->info; + + if (oi && oi->state > ISM_Down) { + (void)ospf_router_lsa_update_area(oi->area); + if (oi->state == ISM_DR) + ospf_network_lsa_update(oi); + } + } +} + +/* + * XPath: .../interface/prefix-suppression + * + * Per-interface prefix-suppression flag (RFC 6860). Mirrors the + * legacy `ip ospf prefix-suppression` CLI's whole-interface form + * (per-address overrides stay on the legacy direct path because RFC + * 9129 doesn't model them). Toggling the flag reoriginates the + * Router-LSA on every adjacency, plus the Network-LSA on any + * interface where this router is the DR. + */ +int ospfd_ietf_ospf_areas_area_interfaces_interface_prefix_suppression_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + struct interface *ifp; + struct ospf_if_params *params; + bool old_value, new_value; + + ret = ospfd_ietf_ospf_resolve_modify_interface(args, &ospf, &ifp); + if (ret != NB_OK || !ospf || !ifp) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + old_value = params->prefix_suppression; + new_value = yang_dnode_get_bool(args->dnode, NULL); + if (new_value != OSPF_PREFIX_SUPPRESSION_DEFAULT) + SET_IF_PARAM(params, prefix_suppression); + else + UNSET_IF_PARAM(params, prefix_suppression); + params->prefix_suppression = new_value; + if (old_value != new_value) + ospfd_ietf_ospf_prefix_suppression_lsa_update(ifp); + return NB_OK; +} + +int ospfd_ietf_ospf_areas_area_interfaces_interface_prefix_suppression_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + struct interface *ifp; + struct ospf_if_params *params; + bool old_value; + + if (!ospfd_ietf_ospf_resolve_destroy_interface(args, &ospf, &ifp)) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + old_value = params->prefix_suppression; + UNSET_IF_PARAM(params, prefix_suppression); + params->prefix_suppression = OSPF_PREFIX_SUPPRESSION_DEFAULT; + if (old_value != OSPF_PREFIX_SUPPRESSION_DEFAULT) + ospfd_ietf_ospf_prefix_suppression_lsa_update(ifp); + return NB_OK; +} + +/* + * XPath: .../ospf/auto-cost/enabled + * + * RFC 9129 models interface auto-cost as a two-leaf container -- an + * `enabled` boolean and the `reference-bandwidth` value gated by + * `when "../enabled = 'true'"`. FRR has no on/off switch: it always + * computes interface cost from `reference-bandwidth / interface + * speed` when the operator hasn't set an explicit per-interface cost. + * + * Modify with `true` is a no-op (FRR is always in this state). Modify + * with `false` is rejected at NB_EV_VALIDATE -- FRR can't honour + * `enabled=false` without losing the cost computation that drives + * every other interface metric. The destroy callback is also a + * no-op since the deviation file declares `default "true"` so the + * leaf is always present and the `when` clause on + * `reference-bandwidth` is always satisfied. + */ +int ospfd_ietf_ospf_auto_cost_enabled_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + bool enabled; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + enabled = yang_dnode_get_bool(args->dnode, NULL); + if (args->event == NB_EV_VALIDATE) { + if (!enabled) { + snprintf(args->errmsg, args->errmsg_len, + "FRR auto-cost cannot be disabled; " + "set per-interface 'ip ospf cost' instead"); + return NB_ERR_VALIDATION; + } + return NB_OK; + } + return NB_OK; +} + +/* + * XPath: .../ospf/auto-cost/reference-bandwidth + * + * Per-instance reference bandwidth used by the auto-cost computation. + * Mirrors the legacy `auto-cost reference-bandwidth N` CLI. RFC 9129 + * units are Mbits; FRR stores the same units in `ospf->ref_bandwidth` + * (despite the `Kbps` comment in ospfd.h -- both CLI and computation + * treat the value as Mbits/s, see ospf_if_recalculate_output_cost). + */ +int ospfd_ietf_ospf_auto_cost_reference_bandwidth_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + struct vrf *vrf; + struct interface *ifp; + int ret; + uint32_t refbw; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + refbw = yang_dnode_get_uint32(args->dnode, NULL); + if (ospf->ref_bandwidth == refbw) + return NB_OK; + ospf->ref_bandwidth = refbw; + vrf = vrf_lookup_by_id(ospf->vrf_id); + FOR_ALL_INTERFACES (vrf, ifp) + ospf_if_recalculate_output_cost(ifp); + return NB_OK; +} + +int ospfd_ietf_ospf_auto_cost_reference_bandwidth_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + struct vrf *vrf; + struct interface *ifp; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf = ospfd_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf) + return NB_OK; + if (ospf->ref_bandwidth == OSPF_DEFAULT_REF_BANDWIDTH) + return NB_OK; + ospf->ref_bandwidth = OSPF_DEFAULT_REF_BANDWIDTH; + vrf = vrf_lookup_by_id(ospf->vrf_id); + FOR_ALL_INTERFACES (vrf, ifp) + ospf_if_recalculate_output_cost(ifp); + return NB_OK; +} + +/* + * XPath: .../ospf/mpls/te-rid/ipv4-router-id + * + * RFC 9129's MPLS-TE Router-ID maps onto FRR's per-process global + * `OspfMplsTE.router_addr`. The legacy `mpls-te router-address` + * CLI is process-wide and rejected outside the default VRF; this + * callback mirrors that constraint. The actual mutation + + * Opaque-LSA reorigination lives in `ospf_mpls_te_apply_router_addr` + * so the CLI shim and the YANG path share identical behaviour. + */ +int ospfd_ietf_ospf_mpls_te_rid_ipv4_router_id_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + struct in_addr value; + int ret; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event == NB_EV_VALIDATE) { + if (ospf->vrf_id != VRF_DEFAULT) { + snprintf(args->errmsg, args->errmsg_len, + "mpls-te router-address only runs on DEFAULT VRF"); + return NB_ERR_VALIDATION; + } + return NB_OK; + } + + if (args->event != NB_EV_APPLY) + return NB_OK; + + yang_dnode_get_ipv4(&value, args->dnode, NULL); + ospf_mpls_te_apply_router_addr(value); + return NB_OK; +} + +int ospfd_ietf_ospf_mpls_te_rid_ipv4_router_id_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf = ospfd_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf) + return NB_OK; + if (ospf->vrf_id != VRF_DEFAULT) + return NB_OK; + ospf_mpls_te_clear_router_addr(); + return NB_OK; +} + +/* + * XPath: .../ospf/graceful-restart/enabled + * + * RFC 9129's graceful-restart enable flag. Maps onto FRR's per- + * instance `ospf->gr_info.restart_support` and the matching + * zebra/nvm bookkeeping in `ospf_gr_restart_support_enable` / + * `_disable`. Disable is rejected at NB_EV_VALIDATE if a GR + * preparation is in flight -- the legacy CLI rejects the same way. + * The `restart-interval` leaf is a sibling and is intentionally + * not touched here; see the `restart_interval` callback below. + */ +int ospfd_ietf_ospf_graceful_restart_enabled_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + bool enabled; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + enabled = yang_dnode_get_bool(args->dnode, NULL); + + if (args->event == NB_EV_VALIDATE) { + if (!enabled && ospf->gr_info.prepare_in_progress) { + snprintf(args->errmsg, args->errmsg_len, + "Graceful Restart preparation in progress"); + return NB_ERR_VALIDATION; + } + return NB_OK; + } + + if (args->event != NB_EV_APPLY) + return NB_OK; + + if (enabled) + ospf_gr_restart_support_enable(ospf); + else + (void)ospf_gr_restart_support_disable(ospf); + return NB_OK; +} + +int ospfd_ietf_ospf_graceful_restart_enabled_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf = ospfd_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf) + return NB_OK; + (void)ospf_gr_restart_support_disable(ospf); + return NB_OK; +} + +/* + * XPath: .../ospf/graceful-restart/restart-interval + * + * Per-instance grace period. Modify sets the value and, when GR is + * currently enabled, refreshes the zebra stale-route timer. Destroy + * restores the RFC default (120s, which also matches FRR's + * `OSPF_DFLT_GRACE_INTERVAL`). + */ +int ospfd_ietf_ospf_graceful_restart_restart_interval_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + uint16_t period; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + period = yang_dnode_get_uint16(args->dnode, NULL); + ospf_gr_set_grace_period(ospf, period); + return NB_OK; +} + +int ospfd_ietf_ospf_graceful_restart_restart_interval_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + ospf = ospfd_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf) + return NB_OK; + + ospf_gr_set_grace_period(ospf, OSPF_DFLT_GRACE_INTERVAL); + return NB_OK; +} + +/* + * XPath: .../ospf/graceful-restart/helper-enabled + * + * RFC 9129's global helper-mode flag. Maps onto FRR's + * `ospf->is_helper_supported`. The legacy `graceful-restart helper + * enable [A.B.C.D]` CLI conflates this global with a per-router-id + * enable list; the YANG model has no per-router-id concept, so the + * northbound only touches the global flag and the legacy CLI keeps + * the per-router-id form on its direct mutation path. + */ +int ospfd_ietf_ospf_graceful_restart_helper_enabled_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + ospf_gr_helper_support_set(ospf, yang_dnode_get_bool(args->dnode, NULL)); + return NB_OK; +} + +int ospfd_ietf_ospf_graceful_restart_helper_enabled_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf = ospfd_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf) + return NB_OK; + ospf_gr_helper_support_set(ospf, false); + return NB_OK; +} + +/* + * XPath: .../ospf/graceful-restart/helper-strict-lsa-checking + * + * Strict-LSA-check on the helper. FRR defaults to true; the running + * config writer only emits `no graceful-restart helper strict-lsa- + * checking` when the value is false, so leaving the leaf unset + * matches FRR's default behaviour. Destroy restores the default. + */ +int ospfd_ietf_ospf_graceful_restart_helper_strict_lsa_checking_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + ospf_gr_helper_lsa_check_set(ospf, yang_dnode_get_bool(args->dnode, NULL)); + return NB_OK; +} + +int ospfd_ietf_ospf_graceful_restart_helper_strict_lsa_checking_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf = ospfd_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf) + return NB_OK; + ospf_gr_helper_lsa_check_set(ospf, true); + return NB_OK; +} + +/* + * RFC 9129 `bfd-types:client-cfg-parms` uses microsecond units for the + * timer leaves; FRR stores milliseconds internally (see + * lib/bfd.c:1120-1125 where bfdd multiplies by 1000 again for the + * protocol). Operators expect to write whole-millisecond values via + * YANG, so reject non-multiples of 1000 microseconds at VALIDATE and + * convert at APPLY. Range checks mirror the legacy CLI grammar: + * 50..60000 ms. + */ +#define OSPFD_IETF_BFD_MIN_INTERVAL_US (50UL * 1000) +#define OSPFD_IETF_BFD_MAX_INTERVAL_US (60000UL * 1000) + +static int ospfd_ietf_bfd_validate_interval_us(uint32_t us, const char *leaf, char *errmsg, + size_t errmsg_len) +{ + if (us % 1000 != 0) { + snprintf(errmsg, errmsg_len, + "FRR BFD %s must be a whole millisecond (multiple of 1000 us); got %u", + leaf, us); + return NB_ERR_VALIDATION; + } + if (us < OSPFD_IETF_BFD_MIN_INTERVAL_US || us > OSPFD_IETF_BFD_MAX_INTERVAL_US) { + snprintf(errmsg, errmsg_len, + "FRR BFD %s must be %u..%u us (50..60000 ms); got %u", leaf, + (unsigned int)OSPFD_IETF_BFD_MIN_INTERVAL_US, + (unsigned int)OSPFD_IETF_BFD_MAX_INTERVAL_US, us); + return NB_ERR_VALIDATION; + } + return NB_OK; +} + +static bool ospfd_ietf_bfd_config_is_default(uint8_t multiplier, uint32_t min_rx, + uint32_t min_tx) +{ + return multiplier == BFD_DEF_DETECT_MULT && min_rx == BFD_DEF_MIN_RX && + min_tx == BFD_DEF_MIN_TX; +} + +static void ospfd_ietf_bfd_sync_config_from_dnode(struct interface *ifp, + const struct lyd_node *dnode, + bool enabled) +{ + struct ospf_if_params *params = IF_DEF_PARAMS(ifp); + uint8_t multiplier = yang_dnode_get_uint8(dnode, "local-multiplier"); + uint32_t min_rx = yang_dnode_get_uint32(dnode, "required-min-rx-interval") / 1000; + uint32_t min_tx = yang_dnode_get_uint32(dnode, "desired-min-tx-interval") / 1000; + + if (!enabled && + ospfd_ietf_bfd_config_is_default(multiplier, min_rx, min_tx) && + !params->bfd_config) + return; + + ospf_interface_bfd_config_get(ifp); + params->bfd_config->detection_multiplier = multiplier; + params->bfd_config->min_rx = min_rx; + params->bfd_config->min_tx = min_tx; +} + +/* + * XPath: .../ospf/areas/area/interfaces/interface/bfd + * + * The BFD container is an aggregate. Leaf callbacks maintain daemon + * configuration state, then this finish callback applies the settled + * state to BFD sessions once per transaction. + */ +void ospfd_ietf_ospf_areas_area_interfaces_interface_bfd_apply_finish(struct nb_cb_apply_finish_args *args) +{ + struct ospf *ospf; + struct interface *ifp; + struct ospf_if_params *params; + int ret; + bool enabled; + + /* apply_finish is APPLY-only; the literal event is intentional. */ + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, NB_EV_APPLY, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return; + ret = ospfd_ietf_ospf_resolve_interface(ospf, args->dnode, NB_EV_APPLY, args->errmsg, + args->errmsg_len, &ifp); + if (ret != NB_OK || !ifp) + return; + + if (!ospfd_ietf_ospf_ensure_if_info(ifp)) + return; + + enabled = yang_dnode_get_bool(args->dnode, "enabled"); + params = IF_DEF_PARAMS(ifp); + ospfd_ietf_bfd_sync_config_from_dnode(ifp, args->dnode, enabled); + if (enabled) { + ospf_interface_enable_bfd(ifp, false); + ospf_interface_bfd_apply(ifp); + } else if (params->bfd_config && params->bfd_config->enabled) { + ospf_interface_disable_bfd(ifp, params); + } +} + +/* + * XPath: .../ospf/areas/area/interfaces/interface/bfd/enabled + * + * Administrative BFD toggle. Actual session changes are done by the parent + * `/bfd` apply_finish callback after all BFD leaves have settled. + */ +int ospfd_ietf_ospf_areas_area_interfaces_interface_bfd_enabled_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + struct interface *ifp; + int ret; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + ret = ospfd_ietf_ospf_resolve_interface(ospf, args->dnode, args->event, + args->errmsg, args->errmsg_len, + &ifp); + if (ret != NB_OK || !ifp) + return ret; + + return NB_OK; +} + +/* + * XPath: .../ospf/areas/area/interfaces/interface/bfd/local-multiplier + * + * Maps to `bfd_config->detection_multiplier`. Type is `multiplier` + * (uint8 1..255) in ietf-bfd-types; FRR's CLI accepts the same range. + * Setting this leaf is permitted regardless of `/bfd/enabled` state. The + * value is applied to the running BFD session when BFD is active, or stored + * in the YANG running datastore for later application when BFD is enabled. + */ +int ospfd_ietf_ospf_areas_area_interfaces_interface_bfd_local_multiplier_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + struct interface *ifp; + int ret; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + ret = ospfd_ietf_ospf_resolve_interface(ospf, args->dnode, args->event, + args->errmsg, args->errmsg_len, + &ifp); + if (ret != NB_OK || !ifp) + return ret; + + /* APPLY is intentionally a no-op; the parent /bfd apply_finish applies. */ + return NB_OK; +} + +/* + * XPath: .../ospf/areas/area/interfaces/interface/bfd/desired-min-tx-interval + * + * RFC unit is microseconds; FRR stores milliseconds. Reject values + * that are not whole milliseconds, and clamp the range to FRR's CLI + * grammar (50..60000 ms). As with local-multiplier, setting this leaf is + * permitted regardless of `/bfd/enabled` state. The value is applied to the + * running BFD session when BFD is active, or stored in the YANG running + * datastore for later application when BFD is enabled. + */ +int ospfd_ietf_ospf_areas_area_interfaces_interface_bfd_desired_min_tx_interval_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + struct interface *ifp; + int ret; + uint32_t us; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + ret = ospfd_ietf_ospf_resolve_interface(ospf, args->dnode, args->event, + args->errmsg, args->errmsg_len, + &ifp); + if (ret != NB_OK || !ifp) + return ret; + + us = yang_dnode_get_uint32(args->dnode, NULL); + if (args->event == NB_EV_VALIDATE) + return ospfd_ietf_bfd_validate_interval_us(us, "desired-min-tx-interval", + args->errmsg, args->errmsg_len); + /* APPLY is intentionally a no-op; the parent /bfd apply_finish applies. */ + return NB_OK; +} + +int ospfd_ietf_ospf_areas_area_interfaces_interface_bfd_desired_min_tx_interval_destroy(struct nb_cb_destroy_args *args) +{ + /* Deletion restores the YANG default; the parent /bfd apply_finish applies it. */ + return NB_OK; +} + +/* + * XPath: .../ospf/areas/area/interfaces/interface/bfd/required-min-rx-interval + * + * Companion to desired-min-tx-interval. Same unit conversion, range and + * activation semantics. + */ +int ospfd_ietf_ospf_areas_area_interfaces_interface_bfd_required_min_rx_interval_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + struct interface *ifp; + int ret; + uint32_t us; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + ret = ospfd_ietf_ospf_resolve_interface(ospf, args->dnode, args->event, + args->errmsg, args->errmsg_len, + &ifp); + if (ret != NB_OK || !ifp) + return ret; + + us = yang_dnode_get_uint32(args->dnode, NULL); + if (args->event == NB_EV_VALIDATE) + return ospfd_ietf_bfd_validate_interval_us(us, "required-min-rx-interval", + args->errmsg, args->errmsg_len); + /* APPLY is intentionally a no-op; the parent /bfd apply_finish applies. */ + return NB_OK; +} + +int ospfd_ietf_ospf_areas_area_interfaces_interface_bfd_required_min_rx_interval_destroy(struct nb_cb_destroy_args *args) +{ + /* Deletion restores the YANG default; the parent /bfd apply_finish applies it. */ + return NB_OK; +} + +/* + * RFC 9129 `static-neighbors` are modeled per-(area, interface, + * identifier) but FRR's NBMA neighbour table is per-(instance, addr) + * -- there is no per-interface or per-area NBMA neighbour state in + * `struct ospf_nbr_nbma`. FRR auto-binds the entry to whichever OI's + * subnet matches the neighbour address via + * `ospf_nbr_nbma_set`/`_if_update`. We therefore: + * + * - ignore the area/interface labels for FRR-side lookup; only + * `identifier` is used to find the FRR-side neighbour; + * - reject duplicate identifiers inside one OSPF instance at + * VALIDATE so the candidate cannot contain two YANG entries that + * would collapse onto one FRR-side neighbour; + * - mark `/cost` not-supported via deviation (FRR has no NBMA + * cost knob, only priority and v_poll); + * - leave the legacy `neighbor A.B.C.D` CLI on the direct mutation + * path because it is instance-level and cannot synthesise a + * credible YANG area/interface key from CLI alone. + */ +static const struct lyd_node * +ospfd_ietf_ospf_static_neighbor_dnode(const struct lyd_node *dnode) +{ + if (!strcmp(dnode->schema->name, "neighbor")) + return dnode; + return yang_dnode_get_parent(dnode, "neighbor"); +} + +static int ospfd_ietf_ospf_static_neighbor_addr(const struct lyd_node *dnode, + struct in_addr *addr) +{ + const char *id_str; + + id_str = yang_dnode_get_string(dnode, "identifier"); + if (!id_str || inet_pton(AF_INET, id_str, addr) != 1) + return -1; + return 0; +} + +static int ospfd_ietf_ospf_static_neighbor_validate(const struct lyd_node *dnode, + char *errmsg, size_t errmsg_len) +{ + const struct lyd_node *neigh; + struct ly_set *matches = NULL; + const char *id; + char xpath[XPATH_MAXLEN]; + struct in_addr addr; + LY_ERR err; + + neigh = ospfd_ietf_ospf_static_neighbor_dnode(dnode); + if (!neigh || ospfd_ietf_ospf_static_neighbor_addr(neigh, &addr) < 0) { + snprintf(errmsg, errmsg_len, + "static-neighbors/neighbor identifier is not a valid IPv4 address"); + return NB_ERR_VALIDATION; + } + + id = yang_dnode_get_string(neigh, "identifier"); + + /* Find all neighbour entries with this identifier in the instance. */ + snprintf(xpath, sizeof(xpath), + "../../../../../../areas/area/interfaces/interface/static-neighbors/neighbor[identifier='%s']", + id); + err = lyd_find_xpath(neigh, xpath, &matches); + if (err) { + snprintf(errmsg, errmsg_len, + "failed to validate static-neighbors/neighbor identifier '%s'", + id); + return NB_ERR_VALIDATION; + } + + if (matches->count > 1) { + snprintf(errmsg, errmsg_len, + "static-neighbors/neighbor identifier '%s' is already configured in this OSPF instance", + id); + ly_set_free(matches, NULL); + return NB_ERR_VALIDATION; + } + + ly_set_free(matches, NULL); + return NB_OK; +} + +static void ospfd_ietf_ospf_static_neighbor_apply(struct ospf *ospf, + const struct lyd_node *neigh) +{ + struct in_addr addr; + + if (ospfd_ietf_ospf_static_neighbor_addr(neigh, &addr) < 0) + return; + + /* + * apply_finish cannot report errors to mgmtd. These helpers return 0 + * only for APPLY-phase races or idempotent no-ops, both of which the + * northbound contract requires us to tolerate. + */ + (void)ospf_nbr_nbma_set(ospf, addr); + (void)ospf_nbr_nbma_poll_interval_set(ospf, addr, + yang_dnode_get_uint16(neigh, "poll-interval")); + (void)ospf_nbr_nbma_priority_set(ospf, addr, + yang_dnode_get_uint8(neigh, "priority")); +} + +/* + * XPath: .../areas/area/interfaces/interface/static-neighbors/neighbor + */ +int ospfd_ietf_ospf_areas_area_interfaces_interface_static_neighbors_neighbor_create(struct nb_cb_create_args *args) +{ + struct ospf *ospf; + int ret; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event == NB_EV_VALIDATE) + return ospfd_ietf_ospf_static_neighbor_validate(args->dnode, args->errmsg, + args->errmsg_len); + if (args->event != NB_EV_APPLY) + return NB_OK; + + return NB_OK; +} + +void ospfd_ietf_ospf_areas_area_interfaces_interface_static_neighbors_neighbor_apply_finish(struct nb_cb_apply_finish_args *args) +{ + struct ospf *ospf; + int ret; + + /* apply_finish is APPLY-only; the literal event is intentional. */ + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, NB_EV_APPLY, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return; + + ospfd_ietf_ospf_static_neighbor_apply(ospf, args->dnode); +} + +int ospfd_ietf_ospf_areas_area_interfaces_interface_static_neighbors_neighbor_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + struct in_addr addr; + + if (args->event != NB_EV_APPLY) + return NB_OK; + ospf = ospfd_ietf_ospf_instance_from_dnode(args->dnode); + if (!ospf) + return NB_OK; + if (ospfd_ietf_ospf_static_neighbor_addr(args->dnode, &addr) < 0) + return NB_OK; + (void)ospf_nbr_nbma_unset(ospf, addr); + return NB_OK; +} + +/* + * XPath: .../static-neighbors/neighbor/poll-interval + * + * Maps to FRR's per-neighbour `v_poll`. The leaf callback validates + * the containing static neighbour; the list entry's apply_finish + * callback applies the settled poll-interval and priority values once + * per transaction. + */ +int ospfd_ietf_ospf_areas_area_interfaces_interface_static_neighbors_neighbor_poll_interval_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event == NB_EV_VALIDATE) + return ospfd_ietf_ospf_static_neighbor_validate(args->dnode, args->errmsg, + args->errmsg_len); + if (args->event != NB_EV_APPLY) + return NB_OK; + + return NB_OK; +} + +/* + * XPath: .../static-neighbors/neighbor/priority + */ +int ospfd_ietf_ospf_areas_area_interfaces_interface_static_neighbors_neighbor_priority_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + int ret; + + ret = ospfd_ietf_ospf_resolve_instance(args->dnode, args->event, args->errmsg, + args->errmsg_len, &ospf); + if (ret != NB_OK || !ospf) + return ret; + + if (args->event == NB_EV_VALIDATE) + return ospfd_ietf_ospf_static_neighbor_validate(args->dnode, args->errmsg, + args->errmsg_len); + if (args->event != NB_EV_APPLY) + return NB_OK; + + return NB_OK; +} + +/* + * RFC 9129's `authentication/ospfv2-key-chain` lives under the + * `case auth-key-chain` of the `choice ospfv2-auth-specification` + * inside `case ospfv2-auth`. Choice / case nodes do not appear in + * the data path (RFC 7950 \xc2\xa77.9.2), so the runtime xpath is + * simply `.../authentication/ospfv2-key-chain`. Maps onto FRR's + * per-interface `params->keychain_name` and forces `auth_type` to + * `OSPF_AUTH_CRYPTOGRAPHIC`, mirroring `ip ospf authentication + * key-chain X` exactly (see ospf_vty.c:7754 onwards). The other + * leaves in the authentication container (explicit-key, trailer-rfc, + * v3 IPsec SA) are marked not-supported via deviation -- this slice + * intentionally covers the keychain case only. + */ +int ospfd_ietf_ospf_areas_area_interfaces_interface_authentication_ospfv2_key_chain_modify(struct nb_cb_modify_args *args) +{ + struct ospf *ospf; + struct interface *ifp; + struct ospf_if_params *params; + int ret; + + ret = ospfd_ietf_ospf_resolve_modify_interface(args, &ospf, &ifp); + if (ret != NB_OK || !ifp) + return ret; + + if (args->event != NB_EV_APPLY) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + SET_IF_PARAM(params, auth_type); + params->auth_type = OSPF_AUTH_CRYPTOGRAPHIC; + SET_IF_PARAM(params, keychain_name); + XFREE(MTYPE_OSPF_IF_PARAMS, params->keychain_name); + params->keychain_name = XSTRDUP(MTYPE_OSPF_IF_PARAMS, + yang_dnode_get_string(args->dnode, NULL)); + UNSET_IF_PARAM(params, auth_crypt); + return NB_OK; +} + +int ospfd_ietf_ospf_areas_area_interfaces_interface_authentication_ospfv2_key_chain_destroy(struct nb_cb_destroy_args *args) +{ + struct ospf *ospf; + struct interface *ifp; + struct ospf_if_params *params; + + if (!ospfd_ietf_ospf_resolve_destroy_interface(args, &ospf, &ifp)) + return NB_OK; + + params = IF_DEF_PARAMS(ifp); + UNSET_IF_PARAM(params, keychain_name); + XFREE(MTYPE_OSPF_IF_PARAMS, params->keychain_name); + UNSET_IF_PARAM(params, auth_type); + params->auth_type = OSPF_AUTH_NOTSET; + return NB_OK; +} diff --git a/ospfd/ospf_nb_notifications.c b/ospfd/ospf_nb_notifications.c new file mode 100644 index 000000000000..03658562d95d --- /dev/null +++ b/ospfd/ospf_nb_notifications.c @@ -0,0 +1,481 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * OSPFv2 northbound notifications (RFC 9129 ietf-ospf). + * Copyright (C) 2026 Eric Parsonage + * + * Wires the existing ospf_nsm_change / ospf_ism_change / GR helper / + * packet validation hooks to the YANG notification dispatcher so mgmtd + * (and any frontend subscribed to it) sees an ietf-ospf event each time + * an OSPFv2 state transitions. + */ + +#include + +#include "debug.h" +#include "if.h" +#include "linklist.h" +#include "log.h" +#include "northbound.h" +#include "yang.h" +#include "yang_wrappers.h" + +#include "ospfd/ospfd.h" +#include "ospfd/ospf_opaque.h" +#include "ospfd/ospf_gr.h" +#include "ospfd/ospf_interface.h" +#include "ospfd/ospf_ism.h" +#include "ospfd/ospf_lsa.h" +#include "ospfd/ospf_lsdb.h" +#include "ospfd/ospf_neighbor.h" +#include "ospfd/ospf_nsm.h" +#include "ospf_nb.h" + +#define _dbg(fmt, ...) DEBUGD(&nb_dbg_notif, "OSPF-NOTIF: %s: " fmt, __func__, ##__VA_ARGS__) + +/* + * Translate FRR's internal NSM state code into the integer value RFC 9129's + * `nbr-state-type` enum assigns to the same name. yang_data_new_enum() takes + * the YANG-defined numeric value and looks up the corresponding name. + * + * FRR reserves 0 / 1 for the DependUpon / Deleted lifecycle codes that have + * no protocol existence in RFC 2328; both fold into the RFC's `down` state, + * which the protocol defines as "no recent information received from the + * neighbor". Without this fold every adjacency tear-down (KillNbr, dead + * timer, explicit clear) lands on NSM_Deleted and the hook silently drops + * the notification, so subscribers never see neighbours go away. + */ +static const int ospfd_ietf_nbr_state_table[OSPF_NSM_STATE_MAX] = { + [NSM_DependUpon] = 1, /* down */ + [NSM_Deleted] = 1, /* down */ + [NSM_Down] = 1, /* down */ + [NSM_Attempt] = 2, /* attempt */ + [NSM_Init] = 3, /* init */ + [NSM_TwoWay] = 4, /* 2-way */ + [NSM_ExStart] = 5, /* exstart */ + [NSM_Exchange] = 6, /* exchange */ + [NSM_Loading] = 7, /* loading */ + [NSM_Full] = 8, /* full */ +}; + +static int ospfd_ietf_nbr_state_yang(int nsm_state) +{ + int val; + + if (nsm_state < 0 || + (size_t)nsm_state >= array_size(ospfd_ietf_nbr_state_table)) + return -1; + + val = ospfd_ietf_nbr_state_table[nsm_state]; + return val ? val : -1; +} + +/* + * Translate FRR's internal ISM state code into RFC 9129's `if-state-type` + * enum. Numeric values agree for Down/Loopback/Waiting/PointToPoint but + * diverge for the DR-election trio: FRR orders DROther=5, Backup=6, DR=7 + * while the RFC orders dr=5, bdr=6, dr-other=7. + * + * FRR reserves 0 for the DependUpon lifecycle code that has no protocol + * existence; it folds into the RFC's `down` so a tear-down through that + * state stays observable through if-state-change. + */ +static const int ospfd_ietf_if_state_table[OSPF_ISM_STATE_MAX] = { + [ISM_DependUpon] = 1, /* down */ + [ISM_Down] = 1, /* down */ + [ISM_Loopback] = 2, /* loopback */ + [ISM_Waiting] = 3, /* waiting */ + [ISM_PointToPoint] = 4, /* point-to-point */ + [ISM_DR] = 5, /* dr */ + [ISM_Backup] = 6, /* bdr */ + [ISM_DROther] = 7, /* dr-other */ +}; + +static int ospfd_ietf_if_state_yang(int ism_state) +{ + int val; + + if (ism_state < 0 || + (size_t)ism_state >= array_size(ospfd_ietf_if_state_table)) + return -1; + + val = ospfd_ietf_if_state_table[ism_state]; + return val ? val : -1; +} + +static void ospfd_ietf_notif_add_instance_hdr(struct list *args, const char *xpath, + const struct ospf *ospf) +{ + char xpath_arg[XPATH_MAXLEN]; + char buf[XPATH_MAXLEN]; + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/routing-protocol-name", xpath); + listnode_add(args, + yang_data_new_string(xpath_arg, + ospfd_ietf_ospf_instance_name(ospf, buf, sizeof(buf)))); +} + +static void ospfd_ietf_notif_add_interface_hdr(struct list *args, const char *xpath, + const struct interface *ifp) +{ + char xpath_arg[XPATH_MAXLEN]; + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/interface/interface", xpath); + listnode_add(args, yang_data_new_string(xpath_arg, ifp->name)); +} + +static void ospfd_ietf_notif_add_neighbor_hdr(struct list *args, const char *xpath, + const struct ospf_neighbor *nbr) +{ + char xpath_arg[XPATH_MAXLEN]; + char buf[INET_ADDRSTRLEN]; + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/neighbor-router-id", xpath); + inet_ntop(AF_INET, &nbr->router_id, buf, sizeof(buf)); + listnode_add(args, yang_data_new_string(xpath_arg, buf)); + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/neighbor-ip-addr", xpath); + inet_ntop(AF_INET, &nbr->src, buf, sizeof(buf)); + listnode_add(args, yang_data_new_string(xpath_arg, buf)); +} + +/* + * XPath: /ietf-ospf:nbr-state-change + * + * Emitted on every NSM transition. The OSPF-v2 NSM hook fires after the + * state has been swapped in, so `nbr->state` is already `next_state` here; + * the `oldstate` argument is supplied by the hook caller. + */ +static int ospfd_ietf_nbr_state_change(struct ospf_neighbor *nbr, int next_state, int old_state) +{ + const char *xpath = "/ietf-ospf:nbr-state-change"; + struct list *args; + char xpath_arg[XPATH_MAXLEN]; + int yang_state; + + yang_state = ospfd_ietf_nbr_state_yang(next_state); + if (yang_state < 0) + return 0; + (void)old_state; + + if (!nbr->oi || !nbr->oi->ifp || !nbr->oi->ospf) + return 0; + + args = yang_data_list_new(); + ospfd_ietf_notif_add_instance_hdr(args, xpath, nbr->oi->ospf); + ospfd_ietf_notif_add_interface_hdr(args, xpath, nbr->oi->ifp); + ospfd_ietf_notif_add_neighbor_hdr(args, xpath, nbr); + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/state", xpath); + listnode_add(args, yang_data_new_enum(xpath_arg, yang_state)); + + _dbg("nbr %pI4 on %s -> %s", &nbr->router_id, nbr->oi->ifp->name, + lookup_msg(ospf_nsm_state_msg, next_state, NULL)); + + nb_notification_send(xpath, args); + return 0; +} + +/* + * Translate FRR's `ospf_helper_exit_reason` (0..4 enum) into RFC 9129's + * `restart-exit-reason-type` (1..5 enum). FRR's enum order differs from + * the RFC value order: TOPO_CHG=2 maps to topology-changed=5, while + * COMPLETED=4 maps to completed=3. A simple offset does not work. + * + * The default returns -1 so an unfamiliar reason surfaces as an error the + * caller can log and suppress. Folding an unknown reason into `none` + * would falsely claim "the helper has not exited" when in fact it just + * exited for a reason this build does not yet know about. + */ +static const int + ospfd_ietf_helper_exit_reason_table[OSPF_GR_HELPER_COMPLETED + 1] = { + [OSPF_GR_HELPER_EXIT_NONE] = 1, /* none */ + [OSPF_GR_HELPER_INPROGRESS] = 2, /* in-progress */ + [OSPF_GR_HELPER_COMPLETED] = 3, /* completed */ + [OSPF_GR_HELPER_GRACE_TIMEOUT] = 4, /* timed-out */ + [OSPF_GR_HELPER_TOPO_CHG] = 5, /* topology-changed */ + }; + +static int ospfd_ietf_helper_exit_reason_yang(int exit_reason) +{ + int val; + + if (exit_reason < 0 || + (size_t)exit_reason >= array_size(ospfd_ietf_helper_exit_reason_table)) + return -1; + + val = ospfd_ietf_helper_exit_reason_table[exit_reason]; + return val ? val : -1; +} + +/* + * XPath: /ietf-ospf:restart-status-change + * + * Emit when the local OSPFv2 instance transitions in or out of graceful- + * restart mode. `status` follows RFC 9129's restart-status-type values + * (1=not-restarting, 2=planned-restart, 3=unplanned-restart). + * `exit_reason` is in FRR's `enum ospf_helper_exit_reason` space; we + * translate to the RFC enum. All FRR-known restart reasons are SW- + * initiated and map to planned-restart. + */ +void ospfd_ietf_notif_restart_status_change(struct ospf *ospf, int status, int exit_reason) +{ + const char *xpath = "/ietf-ospf:restart-status-change"; + struct list *args; + char xpath_arg[XPATH_MAXLEN]; + int yang_exit; + + if (!ospf) + return; + + args = yang_data_list_new(); + ospfd_ietf_notif_add_instance_hdr(args, xpath, ospf); + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/status", xpath); + listnode_add(args, yang_data_new_enum(xpath_arg, status)); + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/restart-interval", xpath); + listnode_add(args, yang_data_new_uint16(xpath_arg, ospf->gr_info.grace_period)); + + yang_exit = ospfd_ietf_helper_exit_reason_yang(exit_reason); + if (yang_exit < 0) { + zlog_warn("%s: unrecognised GR exit reason %d, suppressing notification", + __func__, exit_reason); + list_delete(&args); + return; + } + snprintf(xpath_arg, sizeof(xpath_arg), "%s/exit-reason", xpath); + listnode_add(args, yang_data_new_enum(xpath_arg, yang_exit)); + + _dbg("instance %s gr status %d exit %d", ospf->name ?: VRF_DEFAULT_NAME, status, + exit_reason); + nb_notification_send(xpath, args); +} + +/* + * XPath: /ietf-ospf:nbr-restart-helper-status-change + * + * Emit when this router enters or leaves helper mode for a neighbour's + * graceful restart. `status` is RFC restart-helper-status-type + * (1=not-helping, 2=helping). `age` is the remaining helper time in + * seconds. `exit_reason` is FRR's enum ospf_helper_exit_reason. + */ +void ospfd_ietf_notif_nbr_restart_helper_status_change(struct ospf_neighbor *nbr, int status, + uint16_t age, int exit_reason) +{ + const char *xpath = "/ietf-ospf:nbr-restart-helper-status-change"; + struct list *args; + char xpath_arg[XPATH_MAXLEN]; + int yang_exit; + + if (!nbr || !nbr->oi || !nbr->oi->ifp || !nbr->oi->ospf) + return; + + args = yang_data_list_new(); + ospfd_ietf_notif_add_instance_hdr(args, xpath, nbr->oi->ospf); + ospfd_ietf_notif_add_interface_hdr(args, xpath, nbr->oi->ifp); + ospfd_ietf_notif_add_neighbor_hdr(args, xpath, nbr); + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/status", xpath); + listnode_add(args, yang_data_new_enum(xpath_arg, status)); + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/age", xpath); + listnode_add(args, yang_data_new_uint16(xpath_arg, age)); + + yang_exit = ospfd_ietf_helper_exit_reason_yang(exit_reason); + if (yang_exit < 0) { + zlog_warn("%s: unrecognised GR helper exit reason %d, suppressing notification", + __func__, exit_reason); + list_delete(&args); + return; + } + snprintf(xpath_arg, sizeof(xpath_arg), "%s/exit-reason", xpath); + listnode_add(args, yang_data_new_enum(xpath_arg, yang_exit)); + + _dbg("nbr %pI4 helper status %d exit %d", &nbr->router_id, status, exit_reason); + nb_notification_send(xpath, args); +} + +/* + * XPath: /ietf-ospf:if-state-change + * + * Emitted on every ISM transition. The OSPFv2 ISM hook fires after the + * state has been swapped in. The `old_state` argument is not part of the + * RFC notification so we only consume it to silence the unused-parameter + * warning -- it is, however, available for future hooks that may want to + * filter on the transition direction. + */ +static int ospfd_ietf_if_state_change(struct ospf_interface *oi, int state, int old_state) +{ + const char *xpath = "/ietf-ospf:if-state-change"; + struct list *args; + char xpath_arg[XPATH_MAXLEN]; + int yang_state; + + yang_state = ospfd_ietf_if_state_yang(state); + if (yang_state < 0) + return 0; + (void)old_state; + + if (!oi->ifp || !oi->ospf) + return 0; + + args = yang_data_list_new(); + ospfd_ietf_notif_add_instance_hdr(args, xpath, oi->ospf); + ospfd_ietf_notif_add_interface_hdr(args, xpath, oi->ifp); + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/state", xpath); + listnode_add(args, yang_data_new_enum(xpath_arg, yang_state)); + + _dbg("iface %s -> %s", oi->ifp->name, lookup_msg(ospf_ism_state_msg, state, NULL)); + + nb_notification_send(xpath, args); + return 0; +} + +/* + * XPath: /ietf-ospf:if-rx-bad-packet + * + * Emit when an OSPFv2 packet cannot be parsed on a given interface. + * Caller supplies the source address (in network byte order) and the + * OSPF packet type (1..5 matching the RFC packet-type enum); the type + * is left as 1 (hello) when the packet didn't parse far enough to + * extract it. + */ +void ospfd_ietf_notif_if_rx_bad_packet(struct ospf_interface *oi, struct in_addr src, + uint8_t packet_type) +{ + const char *xpath = "/ietf-ospf:if-rx-bad-packet"; + struct list *args; + char xpath_arg[XPATH_MAXLEN]; + char buf[INET_ADDRSTRLEN]; + + if (!oi || !oi->ifp || !oi->ospf) + return; + + args = yang_data_list_new(); + ospfd_ietf_notif_add_instance_hdr(args, xpath, oi->ospf); + ospfd_ietf_notif_add_interface_hdr(args, xpath, oi->ifp); + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/packet-source", xpath); + inet_ntop(AF_INET, &src, buf, sizeof(buf)); + listnode_add(args, yang_data_new_string(xpath_arg, buf)); + + if (packet_type >= 1 && packet_type <= 5) { + snprintf(xpath_arg, sizeof(xpath_arg), "%s/packet-type", xpath); + listnode_add(args, yang_data_new_enum(xpath_arg, packet_type)); + } + + _dbg("bad packet on %s from %s type %u", oi->ifp->name, buf, packet_type); + nb_notification_send(xpath, args); +} + +/* + * XPath: /ietf-ospf:if-config-error + * + * Emit when an OSPFv2 packet's contents diverge from the local interface + * configuration (mismatched hello/dead interval, area mismatch, version, + * MTU, auth, etc). `error_name` is the RFC enum identifier string; + * we pass it as a yang_data_new_string so libyang validates and accepts + * it without depending on the enum's numeric value (the RFC leaves + * if-config-error's numeric values implicit). + */ +void ospfd_ietf_notif_if_config_error(struct ospf_interface *oi, struct in_addr src, + uint8_t packet_type, const char *error_name) +{ + const char *xpath = "/ietf-ospf:if-config-error"; + struct list *args; + char xpath_arg[XPATH_MAXLEN]; + char buf[INET_ADDRSTRLEN]; + + if (!oi || !oi->ifp || !oi->ospf || !error_name) + return; + + args = yang_data_list_new(); + ospfd_ietf_notif_add_instance_hdr(args, xpath, oi->ospf); + ospfd_ietf_notif_add_interface_hdr(args, xpath, oi->ifp); + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/packet-source", xpath); + inet_ntop(AF_INET, &src, buf, sizeof(buf)); + listnode_add(args, yang_data_new_string(xpath_arg, buf)); + + if (packet_type >= 1 && packet_type <= 5) { + snprintf(xpath_arg, sizeof(xpath_arg), "%s/packet-type", xpath); + listnode_add(args, yang_data_new_enum(xpath_arg, packet_type)); + } + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/error", xpath); + listnode_add(args, yang_data_new_string(xpath_arg, error_name)); + + _dbg("config error on %s from %s type %u: %s", oi->ifp->name, buf, packet_type, error_name); + nb_notification_send(xpath, args); +} + +/* + * XPath: /ietf-ospf:nssa-translator-status-change + * + * Emit when an OSPFv2 area's NSSA translator state transitions. RFC 9129 + * folds configured translator role and operational translator state into + * one enum: enabled=1, elected=2, disabled=3. FRR keeps those as separate + * fields on the area, so the notification mapping needs both. + * OSPFv3 has no NSSA translator surface in FRR, so this notification is + * OSPFv2-only. + */ +static int ospfd_ietf_nssa_translator_state_yang(const struct ospf_area *area) +{ + if (area->NSSATranslatorState == OSPF_NSSA_TRANSLATE_DISABLED) + return 3; /* disabled */ + + if (area->NSSATranslatorState != OSPF_NSSA_TRANSLATE_ENABLED) + return -1; + + switch (area->NSSATranslatorRole) { + case OSPF_NSSA_ROLE_ALWAYS: + return 1; /* enabled */ + case OSPF_NSSA_ROLE_CANDIDATE: + return 2; /* elected */ + case OSPF_NSSA_ROLE_NEVER: + default: + return -1; + } +} + +void ospfd_ietf_notif_nssa_translator_state_change(struct ospf_area *area) +{ + const char *xpath = "/ietf-ospf:nssa-translator-status-change"; + struct list *args; + char xpath_arg[XPATH_MAXLEN]; + char buf[INET_ADDRSTRLEN]; + int yang_state; + + if (!area || !area->ospf) + return; + + yang_state = ospfd_ietf_nssa_translator_state_yang(area); + if (yang_state < 0) { + zlog_warn("%s: unable to map NSSA translator state %u role %u for area %pI4", + __func__, area->NSSATranslatorState, + area->NSSATranslatorRole, &area->area_id); + return; + } + + args = yang_data_list_new(); + ospfd_ietf_notif_add_instance_hdr(args, xpath, area->ospf); + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/area-id", xpath); + inet_ntop(AF_INET, &area->area_id, buf, sizeof(buf)); + listnode_add(args, yang_data_new_string(xpath_arg, buf)); + + snprintf(xpath_arg, sizeof(xpath_arg), "%s/status", xpath); + listnode_add(args, yang_data_new_enum(xpath_arg, yang_state)); + + _dbg("area %pI4 nssa-translator state %u role %u status %d", + &area->area_id, area->NSSATranslatorState, + area->NSSATranslatorRole, yang_state); + nb_notification_send(xpath, args); +} + +void ospfd_ietf_notif_init(void) +{ + hook_register(ospf_nsm_change, ospfd_ietf_nbr_state_change); + hook_register(ospf_ism_change, ospfd_ietf_if_state_change); +} diff --git a/ospfd/ospf_nb_rpcs.c b/ospfd/ospf_nb_rpcs.c new file mode 100644 index 000000000000..a2a0a5ee7bc1 --- /dev/null +++ b/ospfd/ospf_nb_rpcs.c @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * OSPFv2 northbound RPC handlers (RFC 9129 ietf-ospf). + * Copyright (C) 2026 Eric Parsonage + */ + +#include + +#include "if.h" +#include "linklist.h" +#include "log.h" +#include "northbound.h" +#include "prefix.h" +#include "table.h" +#include "vrf.h" +#include "yang.h" + +#include "ospfd/ospfd.h" +#include "ospfd/ospf_interface.h" +#include "ospfd/ospf_lsa.h" +#include "ospfd/ospf_lsdb.h" +#include "ospfd/ospf_neighbor.h" +#include "ospfd/ospf_nsm.h" +#include "ospf_nb.h" + +/* + * Locate the ospf_interface for `ifname` inside `ospf`. Returns NULL if the + * interface is not currently bound to this OSPFv2 instance (which is also the + * indicator the RPC must propagate as an `ospf-interface-not-found` failure + * per RFC 9129). + */ +static struct ospf_interface *ospfd_ietf_lookup_oi(struct ospf *ospf, const char *ifname) +{ + struct listnode *node; + struct ospf_interface *oi; + + for (ALL_LIST_ELEMENTS_RO(ospf->oiflist, node, oi)) + if (oi->ifp && strcmp(oi->ifp->name, ifname) == 0) + return oi; + return NULL; +} + +static void ospfd_ietf_reset_neighbors_on_oi(struct ospf_interface *oi) +{ + struct route_node *rn; + struct ospf_neighbor *nbr; + + for (rn = route_top(oi->nbrs); rn; rn = route_next(rn)) { + nbr = rn->info; + if (nbr && nbr != oi->nbr_self) + OSPF_NSM_EVENT_EXECUTE(nbr, NSM_KillNbr); + } +} + +/* + * XPath: /ietf-ospf:clear-neighbor + * + * Reset OSPFv2 neighbors on the named instance. Optional `interface` input + * narrows the reset to one OSPF interface. This daemon handles only OSPFv2; + * if the named instance isn't local (it may belong to ospf6d) return NB_OK + * silently -- mgmtd fans the RPC out to every backend that registered the + * xpath, so non-owners must not surface an error. + */ +int ospfd_ietf_ospf_clear_neighbor_rpc(struct nb_cb_rpc_args *args) +{ + const char *name; + const char *ifname = NULL; + struct ospf *ospf; + struct ospf_interface *oi; + + if (!args->input || !yang_dnode_exists(args->input, "routing-protocol-name")) + return NB_OK; + + name = yang_dnode_get_string(args->input, "routing-protocol-name"); + ospf = ospfd_ietf_ospf_lookup_instance(name); + if (!ospf) + return NB_OK; + + if (yang_dnode_exists(args->input, "interface")) + ifname = yang_dnode_get_string(args->input, "interface"); + + if (ifname) { + oi = ospfd_ietf_lookup_oi(ospf, ifname); + if (!oi) { + /* + * RFC 9129 specifies error-tag `data-missing` plus + * error-app-tag `ospf-interface-not-found`. FRR's + * nb_cb_rpc_args carries only an unstructured errmsg, + * so the app-tag string goes in the message and we + * pick NB_ERR_NOT_FOUND (mgmtd maps to + * MGMTD_INVALID_PARAM, the closest "client supplied + * a bad reference" signal); NB_ERR_RESOURCE would + * map to MGMTD_INTERNAL_ERROR which incorrectly + * implies a daemon-side failure. + */ + snprintf(args->errmsg, args->errmsg_len, "ospf-interface-not-found: %s", + ifname); + return NB_ERR_NOT_FOUND; + } + ospfd_ietf_reset_neighbors_on_oi(oi); + return NB_OK; + } + + if (ospf->oi_running) { + struct in_addr any = { .s_addr = 0 }; + + ospf_neighbor_reset(ospf, any, NULL); + } + return NB_OK; +} + +/* + * XPath: /ietf-ospf:clear-database + * + * Flush every neighbor adjacency on the named OSPFv2 instance and reoriginate + * self-originated LSAs. `ospf_process_reset` is exactly the helper the + * legacy `clear ip ospf process` command uses. + */ +int ospfd_ietf_ospf_clear_database_rpc(struct nb_cb_rpc_args *args) +{ + const char *name; + struct ospf *ospf; + + if (!args->input || !yang_dnode_exists(args->input, "routing-protocol-name")) + return NB_OK; + + name = yang_dnode_get_string(args->input, "routing-protocol-name"); + ospf = ospfd_ietf_ospf_lookup_instance(name); + if (!ospf) + return NB_OK; + + if (ospf->oi_running) + ospf_process_reset(ospf); + return NB_OK; +} diff --git a/ospfd/ospf_nb_state.c b/ospfd/ospf_nb_state.c new file mode 100644 index 000000000000..0af1975df722 --- /dev/null +++ b/ospfd/ospf_nb_state.c @@ -0,0 +1,457 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * OSPF northbound operational state. + * Copyright (C) 2026 Eric Parsonage + */ + +#include + +#include "debug.h" +#include "if.h" +#include "linklist.h" +#include "table.h" +#include "vrf.h" + +#include "ospfd/ospfd.h" +#include "ospfd/ospf_interface.h" +#include "ospfd/ospf_lsa.h" +#include "ospfd/ospf_lsdb.h" +#include "ospfd/ospf_neighbor.h" +#include "ospfd/ospf_nsm.h" +#include "ospf_nb.h" +#include "ospf_vty.h" + +static void ospfd_ietf_interface_key(const struct interface *ifp, char *key, + size_t key_len) +{ + if (vrf_is_backend_netns()) + snprintf(key, key_len, "%s:%s", ifp->vrf->name, ifp->name); + else + snprintf(key, key_len, "%s", ifp->name); +} + +static bool ospfd_ietf_interface_key_match(const struct interface *ifp, + const char *key) +{ + char ifkey[XPATH_MAXLEN]; + + ospfd_ietf_interface_key(ifp, ifkey, sizeof(ifkey)); + + return !strcmp(ifkey, key); +} + +static void *ospfd_ietf_list_next_data(struct list *list, const void *entry) +{ + struct listnode *node; + void *data; + + if (!list) + return NULL; + + if (!entry) { + node = listhead(list); + return node ? listgetdata(node) : NULL; + } + + for (ALL_LIST_ELEMENTS_RO(list, node, data)) { + if (data != entry) + continue; + + node = listnextnode(node); + return node ? listgetdata(node) : NULL; + } + + return NULL; +} + +struct ospf *ospfd_ietf_ospf_lookup_instance(const char *name) +{ + const struct listnode *node; + struct ospf *ospf; + char instance_name[XPATH_MAXLEN]; + + for (ALL_LIST_ELEMENTS_RO(om->ospf, node, ospf)) + if (!strcmp(ospfd_ietf_ospf_instance_name(ospf, instance_name, + sizeof(instance_name)), + name)) + return ospf; + + return NULL; +} + +static uint8_t ospfd_ietf_neighbor_state(uint8_t state) +{ + switch (state) { + case NSM_Deleted: + case NSM_Down: + return 1; + case NSM_Attempt: + return 2; + case NSM_Init: + return 3; + case NSM_TwoWay: + return 4; + case NSM_ExStart: + return 5; + case NSM_Exchange: + return 6; + case NSM_Loading: + return 7; + case NSM_Full: + return 8; + default: + return 1; + } +} + +static bool ospfd_ietf_interface_name_seen(const struct ospf_area *area, + const struct ospf_interface *end) +{ + const struct listnode *node; + struct ospf_interface *oi; + char endkey[XPATH_MAXLEN]; + + ospfd_ietf_interface_key(end->ifp, endkey, sizeof(endkey)); + + for (ALL_LIST_ELEMENTS_RO(area->oiflist, node, oi)) { + if (oi == end) + break; + + if (ospfd_ietf_interface_key_match(oi->ifp, endkey)) + return true; + } + + return false; +} + +static const struct ospf_interface * +ospfd_ietf_next_unique_interface(const struct ospf_area *area, + const struct ospf_interface *entry) +{ + const struct listnode *node; + struct ospf_interface *oi; + bool after_entry = entry == NULL; + + for (ALL_LIST_ELEMENTS_RO(area->oiflist, node, oi)) { + if (!after_entry) { + if (oi == entry) + after_entry = true; + continue; + } + + if (!ospfd_ietf_interface_name_seen(area, oi)) + return oi; + } + + return NULL; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol + */ +const void *ospfd_ietf_routing_control_plane_protocol_get_next(struct nb_cb_get_next_args *args) +{ + return ospfd_ietf_list_next_data(om->ospf, args->list_entry); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol + */ +int ospfd_ietf_routing_control_plane_protocol_get_keys(struct nb_cb_get_keys_args *args) +{ + const struct ospf *ospf = args->list_entry; + char instance_name[XPATH_MAXLEN]; + + args->keys->num = 2; + strlcpy(args->keys->key[0], "ietf-ospf:ospfv2", sizeof(args->keys->key[0])); + strlcpy(args->keys->key[1], + ospfd_ietf_ospf_instance_name(ospf, instance_name, sizeof(instance_name)), + sizeof(args->keys->key[1])); + + return NB_OK; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol + */ +const void * +ospfd_ietf_routing_control_plane_protocol_lookup_entry(struct nb_cb_lookup_entry_args *args) +{ + const char *type = args->keys->key[0]; + + if (strcmp(type, "ietf-ospf:ospfv2")) + return NULL; + + return ospfd_ietf_ospf_lookup_instance(args->keys->key[1]); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/router-id + */ +struct yang_data *ospfd_ietf_ospf_router_id_get_elem(struct nb_cb_get_elem_args *args) +{ + const struct ospf *ospf = args->list_entry; + + return yang_data_new_ipv4(args->xpath, &ospf->router_id); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/statistics/originate-new-lsa-count + */ +struct yang_data * +ospfd_ietf_ospf_statistics_originate_new_lsa_count_get_elem(struct nb_cb_get_elem_args *args) +{ + const struct ospf *ospf = args->list_entry; + + return yang_data_new_uint32(args->xpath, ospf->lsa_originate_count); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/statistics/rx-new-lsas-count + */ +struct yang_data * +ospfd_ietf_ospf_statistics_rx_new_lsas_count_get_elem(struct nb_cb_get_elem_args *args) +{ + const struct ospf *ospf = args->list_entry; + + return yang_data_new_uint32(args->xpath, ospf->rx_lsa_count); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area + */ +const void *ospfd_ietf_ospf_areas_area_get_next(struct nb_cb_get_next_args *args) +{ + const struct ospf *ospf = args->parent_list_entry; + + return ospfd_ietf_list_next_data(ospf->areas, args->list_entry); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area + */ +int ospfd_ietf_ospf_areas_area_get_keys(struct nb_cb_get_keys_args *args) +{ + const struct ospf_area *area = args->list_entry; + + args->keys->num = 1; + snprintfrr(args->keys->key[0], sizeof(args->keys->key[0]), "%pI4", &area->area_id); + + return NB_OK; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area + */ +const void *ospfd_ietf_ospf_areas_area_lookup_entry(struct nb_cb_lookup_entry_args *args) +{ + const struct ospf *ospf = args->parent_list_entry; + const char *key = args->keys->key[0]; + struct ospf_area *area; + struct listnode *node; + struct in_addr area_id; + + if (str2area_id(key, &area_id, &(int){ 0 })) { + DEBUGD(&nb_dbg_cbs_state, "invalid OSPF area-id key: %s", key); + return NULL; + } + + for (ALL_LIST_ELEMENTS_RO(ospf->areas, node, area)) + if (area->area_id.s_addr == area_id.s_addr) + return area; + + return NULL; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/statistics/spf-runs-count + */ +struct yang_data * +ospfd_ietf_ospf_areas_area_statistics_spf_runs_count_get_elem(struct nb_cb_get_elem_args *args) +{ + const struct ospf_area *area = args->list_entry; + + return yang_data_new_uint32(args->xpath, area->spf_calculation); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/statistics/abr-count + */ +struct yang_data * +ospfd_ietf_ospf_areas_area_statistics_abr_count_get_elem(struct nb_cb_get_elem_args *args) +{ + const struct ospf_area *area = args->list_entry; + + return yang_data_new_uint32(args->xpath, area->abr_count); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/statistics/asbr-count + */ +struct yang_data * +ospfd_ietf_ospf_areas_area_statistics_asbr_count_get_elem(struct nb_cb_get_elem_args *args) +{ + const struct ospf_area *area = args->list_entry; + + return yang_data_new_uint32(args->xpath, area->asbr_count); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/statistics/area-scope-lsa-count + */ +struct yang_data *ospfd_ietf_ospf_areas_area_statistics_area_scope_lsa_count_get_elem( + struct nb_cb_get_elem_args *args) +{ + const struct ospf_area *area = args->list_entry; + + return yang_data_new_uint32(args->xpath, area->lsdb->total); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/interfaces/interface + */ +const void * +ospfd_ietf_ospf_areas_area_interfaces_interface_get_next(struct nb_cb_get_next_args *args) +{ + const struct ospf_area *area = args->parent_list_entry; + + return ospfd_ietf_next_unique_interface(area, args->list_entry); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/interfaces/interface + */ +int ospfd_ietf_ospf_areas_area_interfaces_interface_get_keys(struct nb_cb_get_keys_args *args) +{ + const struct ospf_interface *oi = args->list_entry; + + args->keys->num = 1; + ospfd_ietf_interface_key(oi->ifp, args->keys->key[0], + sizeof(args->keys->key[0])); + + return NB_OK; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/interfaces/interface + */ +const void * +ospfd_ietf_ospf_areas_area_interfaces_interface_lookup_entry(struct nb_cb_lookup_entry_args *args) +{ + const struct ospf_area *area = args->parent_list_entry; + const char *name = args->keys->key[0]; + const struct listnode *node; + struct ospf_interface *oi; + + for (ALL_LIST_ELEMENTS_RO(area->oiflist, node, oi)) + if (ospfd_ietf_interface_key_match(oi->ifp, name)) + return oi; + + return NULL; +} + +static const void *ospfd_ietf_neighbor_next(struct route_table *nbrs, + const struct ospf_neighbor *entry) +{ + struct ospf_neighbor *nbr; + struct route_node *rn; + bool after_entry = entry == NULL; + + for (rn = route_top(nbrs); rn; rn = route_next(rn)) { + nbr = rn->info; + + if (!nbr || nbr == nbr->oi->nbr_self || nbr->state == NSM_Down) + continue; + + if (!after_entry) { + if (nbr == entry) + after_entry = true; + continue; + } + + route_unlock_node(rn); + return nbr; + } + + return NULL; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/interfaces/interface/neighbors/neighbor + */ +const void *ospfd_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_get_next( + struct nb_cb_get_next_args *args) +{ + const struct ospf_interface *oi = args->parent_list_entry; + + return ospfd_ietf_neighbor_next(oi->nbrs, args->list_entry); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/interfaces/interface/neighbors/neighbor + */ +int ospfd_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_get_keys( + struct nb_cb_get_keys_args *args) +{ + const struct ospf_neighbor *nbr = args->list_entry; + + args->keys->num = 1; + snprintfrr(args->keys->key[0], sizeof(args->keys->key[0]), "%pI4", &nbr->router_id); + + return NB_OK; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/interfaces/interface/neighbors/neighbor + */ +const void *ospfd_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_lookup_entry( + struct nb_cb_lookup_entry_args *args) +{ + const struct ospf_interface *oi = args->parent_list_entry; + struct in_addr router_id; + struct ospf_neighbor *nbr; + struct route_node *rn; + + if (inet_pton(AF_INET, args->keys->key[0], &router_id) != 1) { + DEBUGD(&nb_dbg_cbs_state, "invalid OSPF neighbor router-id key: %s", + args->keys->key[0]); + return NULL; + } + + for (rn = route_top(oi->nbrs); rn; rn = route_next(rn)) { + nbr = rn->info; + + if (!nbr || nbr == oi->nbr_self || nbr->state == NSM_Down) + continue; + + if (nbr->router_id.s_addr == router_id.s_addr) { + route_unlock_node(rn); + return nbr; + } + } + + return NULL; +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/interfaces/interface/neighbors/neighbor/address + */ +struct yang_data * +ospfd_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_address_get_elem( + struct nb_cb_get_elem_args *args) +{ + const struct ospf_neighbor *nbr = args->list_entry; + + return yang_data_new_ipv4(args->xpath, &nbr->address.u.prefix4); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area/interfaces/interface/neighbors/neighbor/state + */ +struct yang_data *ospfd_ietf_ospf_areas_area_interfaces_interface_neighbors_neighbor_state_get_elem( + struct nb_cb_get_elem_args *args) +{ + const struct ospf_neighbor *nbr = args->list_entry; + + return yang_data_new_enum(args->xpath, ospfd_ietf_neighbor_state(nbr->state)); +} diff --git a/ospfd/ospf_packet.c b/ospfd/ospf_packet.c index 57f755e5839a..7b45fb1a3a2d 100644 --- a/ospfd/ospf_packet.c +++ b/ospfd/ospf_packet.c @@ -47,6 +47,7 @@ #include "ospfd/ospf_zebra.h" #include "ospfd/ospf_gr.h" #include "ospfd/ospf_auth.h" +#include "ospf_nb.h" /* * OSPF Fragmentation / fragmented writes @@ -821,6 +822,8 @@ static void ospf_hello(struct ip *iph, struct ospf_header *ospfh, "Packet %pI4 [Hello:RECV]: RouterDeadInterval mismatch on %s (expected %u, but received %u).", &ospfh->router_id, IF_NAME(oi), OSPF_IF_PARAM(oi, v_wait), ntohl(hello->dead_interval)); + ospfd_ietf_notif_if_config_error(oi, iph->ip_src, ospfh->type, + "dead-interval-mismatch"); return; } @@ -834,6 +837,8 @@ static void ospf_hello(struct ip *iph, struct ospf_header *ospfh, &ospfh->router_id, IF_NAME(oi), OSPF_IF_PARAM(oi, v_hello), ntohs(hello->hello_interval)); + ospfd_ietf_notif_if_config_error(oi, iph->ip_src, ospfh->type, + "hello-interval-mismatch"); return; } } @@ -3106,6 +3111,8 @@ static enum ospf_read_return_enum ospf_read_helper(struct ospf *ospf) if (ret < 0) { if (IS_DEBUG_OSPF_PACKET(0, RECV)) zlog_debug("ospf_read[%pI4]: Header check failed, dropping.", &srcaddr); + /* RFC 9129 ietf-ospf:if-rx-bad-packet. */ + ospfd_ietf_notif_if_rx_bad_packet(oi, srcaddr, ospfh ? ospfh->type : 0); return OSPF_READ_CONTINUE; } diff --git a/ospfd/ospf_te.c b/ospfd/ospf_te.c index 773ed7330592..22a0d6101f3e 100644 --- a/ospfd/ospf_te.c +++ b/ospfd/ospf_te.c @@ -54,6 +54,9 @@ #include "ospfd/ospf_ext.h" #include "ospfd/ospf_vty.h" #include "ospfd/ospf_errors.h" +#include "northbound_cli.h" + +#include "ospfd/ospf_te_clippy.c" /* * Global variable to manage Opaque-LSA/MPLS-TE on this node. @@ -280,6 +283,63 @@ static void set_mpls_te_router_addr(struct in_addr ipv4) return; } +/* + * Apply a new MPLS-TE router address: store the value and, if MPLS-TE + * is enabled, refresh / reoriginate the affected Opaque Router-Address + * LSAs. Extracted from `DEFUN ospf_mpls_te_router_addr` so the + * RFC 9129 `/mpls/te-rid/ipv4-router-id` config-write callback and the + * legacy CLI can share the same logic. + */ +void ospf_mpls_te_apply_router_addr(struct in_addr value) +{ + struct te_tlv_router_addr *ra = &OspfMplsTE.router_addr; + struct listnode *node, *nnode; + struct mpls_te_link *lp; + int need_to_reoriginate = 0; + + if (ntohs(ra->header.type) != 0 && ntohl(ra->value.s_addr) == ntohl(value.s_addr)) + return; + + set_mpls_te_router_addr(value); + + if (!OspfMplsTE.enabled) + return; + + for (ALL_LIST_ELEMENTS(OspfMplsTE.iflist, node, nnode, lp)) { + if ((lp->area == NULL) || IS_FLOOD_AS(lp->flags)) + continue; + if (!CHECK_FLAG(lp->flags, LPFLG_LSA_ENGAGED)) { + need_to_reoriginate = 1; + break; + } + } + + for (ALL_LIST_ELEMENTS(OspfMplsTE.iflist, node, nnode, lp)) { + if ((lp->area == NULL) || IS_FLOOD_AS(lp->flags)) + continue; + if (need_to_reoriginate) + SET_FLAG(lp->flags, LPFLG_LSA_FORCED_REFRESH); + else + ospf_mpls_te_lsa_schedule(lp, REFRESH_THIS_LSA); + } + + if (need_to_reoriginate) + ospf_mpls_te_foreach_area(ospf_mpls_te_lsa_schedule, REORIGINATE_THIS_LSA); +} + +/* + * Clear the MPLS-TE router-address (set header.type back to zero so the + * TLV is treated as unset). No reorigination: the running-config write + * is gated on `OspfMplsTE.enabled`, and unset router-address has no + * meaningful LSA representation. + */ +void ospf_mpls_te_clear_router_addr(void) +{ + OspfMplsTE.router_addr.header.type = 0; + OspfMplsTE.router_addr.header.length = 0; + OspfMplsTE.router_addr.value.s_addr = 0; +} + static void set_linkparams_link_header(struct mpls_te_link *lp) { uint16_t length = 0; @@ -4293,8 +4353,9 @@ static void ospf_mpls_te_config_write_router(struct vty *vty) if (OspfMplsTE.enabled) { vty_out(vty, " mpls-te on\n"); - vty_out(vty, " mpls-te router-address %pI4\n", - &OspfMplsTE.router_addr.value); + if (ntohs(OspfMplsTE.router_addr.header.type) != 0) + vty_out(vty, " mpls-te router-address %pI4\n", + &OspfMplsTE.router_addr.value); if (OspfMplsTE.inter_as == AS) vty_out(vty, " mpls-te inter-as as\n"); @@ -4394,62 +4455,39 @@ DEFUN (no_ospf_mpls_te, return CMD_SUCCESS; } -DEFUN (ospf_mpls_te_router_addr, +/* + * The legacy `mpls-te router-address A.B.C.D` CLI maps onto the RFC 9129 + * `/mpls/te-rid/ipv4-router-id` leaf via mgmtd. The DEFPY_YANG shim + * enqueues the YANG edit; the NB modify callback in `ospf_nb_config.c` + * calls `ospf_mpls_te_apply_router_addr` (extracted above) so the + * legacy CLI and the YANG path share identical side effects. + */ +DEFPY_YANG (ospf_mpls_te_router_addr, ospf_mpls_te_router_addr_cmd, - "mpls-te router-address A.B.C.D", + "mpls-te router-address A.B.C.D$value", MPLS_TE_STR "Stable IP address of the advertising router\n" "MPLS-TE router address in IPv4 address format\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - int idx_ipv4 = 2; - struct te_tlv_router_addr *ra = &OspfMplsTE.router_addr; - struct in_addr value; - - if (!inet_aton(argv[idx_ipv4]->arg, &value)) { - vty_out(vty, "Please specify Router-Addr by A.B.C.D\n"); - return CMD_WARNING; - } - - if (ntohs(ra->header.type) == 0 - || ntohl(ra->value.s_addr) != ntohl(value.s_addr)) { - struct listnode *node, *nnode; - struct mpls_te_link *lp; - int need_to_reoriginate = 0; - - set_mpls_te_router_addr(value); - - if (!OspfMplsTE.enabled) - return CMD_SUCCESS; - - for (ALL_LIST_ELEMENTS(OspfMplsTE.iflist, node, nnode, lp)) { - if ((lp->area == NULL) || IS_FLOOD_AS(lp->flags)) - continue; + char xpath[XPATH_MAXLEN]; - if (!CHECK_FLAG(lp->flags, LPFLG_LSA_ENGAGED)) { - need_to_reoriginate = 1; - break; - } - } - - for (ALL_LIST_ELEMENTS(OspfMplsTE.iflist, node, nnode, lp)) { - if ((lp->area == NULL) || IS_FLOOD_AS(lp->flags)) - continue; - - if (need_to_reoriginate) - SET_FLAG(lp->flags, LPFLG_LSA_FORCED_REFRESH); - else - ospf_mpls_te_lsa_schedule(lp, REFRESH_THIS_LSA); - } - - if (need_to_reoriginate) - ospf_mpls_te_foreach_area(ospf_mpls_te_lsa_schedule, - REORIGINATE_THIS_LSA); - } - - return CMD_SUCCESS; + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, "/mpls/te-rid/ipv4-router-id") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, value_str); + return nb_cli_apply_changes(vty, NULL); } +/* + * The legacy CLI never exposed `no mpls-te router-address` -- users + * cleared the address by running `no mpls-te` (which disables MPLS-TE + * entirely). We preserve that semantics and rely on YANG + * `mgmt delete-config .../mpls/te-rid/ipv4-router-id` for the + * targeted clear. Adding a new `no mpls-te router-address` form here + * conflicts with the optional-on branch of `no mpls-te [on]` in the + * CLI graph and breaks both commands. + */ + static int set_inter_as_mode(struct vty *vty, const char *mode_name, const char *area_id) { diff --git a/ospfd/ospf_te.h b/ospfd/ospf_te.h index dd040a80fd3d..816b20b19ec5 100644 --- a/ospfd/ospf_te.h +++ b/ospfd/ospf_te.h @@ -418,4 +418,20 @@ struct zapi_opaque_reg_info; */ extern int ospf_te_sync_ted(struct zapi_opaque_reg_info dst); +/* + * Applies a new MPLS-TE router address and, when MPLS-TE is enabled, + * refreshes or reoriginates the affected Opaque Router-Address LSAs. + * Shared by the legacy `mpls-te router-address` CLI and the RFC 9129 + * `/mpls/te-rid/ipv4-router-id` northbound callback. + */ +extern void ospf_mpls_te_apply_router_addr(struct in_addr value); + +/* + * Marks the MPLS-TE router-address as unset (TLV header type back to + * zero). No LSA reorigination: the `mpls-te` config block is gated on + * `OspfMplsTE.enabled` and an unset router-address has no meaningful + * LSA representation. + */ +extern void ospf_mpls_te_clear_router_addr(void); + #endif /* _ZEBRA_OSPF_MPLS_TE_H */ diff --git a/ospfd/ospf_vty.c b/ospfd/ospf_vty.c index 902e4072db8c..a21bfee26363 100644 --- a/ospfd/ospf_vty.c +++ b/ospfd/ospf_vty.c @@ -23,6 +23,7 @@ #include "lib/printfrr.h" #include "keychain.h" #include "frrdistance.h" +#include "northbound_cli.h" #include "ospfd/ospfd.h" #include "ospfd/ospf_asbr.h" @@ -42,6 +43,7 @@ #include "ospfd/ospf_bfd.h" #include "ospfd/ospf_ldp_sync.h" #include "ospfd/ospf_network.h" +#include "ospfd/ospf_nb.h" #include "ospfd/ospf_memory.h" FRR_CFG_DEFAULT_BOOL(OSPF_LOG_ADJACENCY_CHANGES, @@ -185,6 +187,7 @@ DEFUN_NOSH (router_ospf, const char *vrf_name; bool created = false; struct ospf *ospf; + char xpath[XPATH_MAXLEN]; int ret; ret = ospf_router_cmd_parse(vty, argv, argc, &instance, &vrf_name); @@ -208,6 +211,12 @@ DEFUN_NOSH (router_ospf, ospf->instance, ospf_get_name(ospf), ospf->vrf_id, ospf->oi_running); + ospfd_ietf_routing_protocol_xpath(xpath, sizeof(xpath), ospf); + nb_cli_enqueue_change(vty, xpath, NB_OP_CREATE, NULL); + ret = nb_cli_apply_changes(vty, NULL); + if (ret != CMD_SUCCESS) + return ret; + VTY_PUSH_CONTEXT(OSPF_NODE, ospf); return ret; @@ -225,6 +234,7 @@ DEFUN (no_router_ospf, unsigned short instance; const char *vrf_name; struct ospf *ospf; + char xpath[XPATH_MAXLEN]; int ret; ret = ospf_router_cmd_parse(vty, argv, argc, &instance, &vrf_name); @@ -236,10 +246,13 @@ DEFUN (no_router_ospf, ospf = ospf_lookup(instance, vrf_name); if (ospf) { + ospfd_ietf_routing_protocol_xpath(xpath, sizeof(xpath), ospf); if (ospf->gr_info.restart_support) ospf_gr_nvm_delete(ospf); ospf_finish(ospf); + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + ret = nb_cli_apply_changes_clear_pending(vty, "%s", xpath); } else ret = CMD_WARNING_CONFIG_FAILED; @@ -247,33 +260,171 @@ DEFUN (no_router_ospf, } -DEFPY (ospf_router_id, - ospf_router_id_cmd, - "ospf router-id A.B.C.D", - "OSPF specific commands\n" - "router-id for the OSPF process\n" - "OSPF router-id in IP address format\n") +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/explicit-router-id + * + * Build the absolute ietf-ospf explicit-router-id xpath for the given OSPF + * instance. The `router ospf` block is not yet converted to YANG, so the + * instance keys (type + name) come from the FRR-side context rather than a + * vty xpath context push. + */ +static int ospf_router_id_xpath(char *xpath, size_t size, const struct ospf *ospf) { - VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); + char instance_name[XPATH_MAXLEN]; + + return snprintf(xpath, size, + OSPFD_IETF_ROUTING_PROTOCOL_XPATH "/ietf-ospf:ospf/explicit-router-id", + ospfd_ietf_ospf_instance_name(ospf, instance_name, sizeof(instance_name))); +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol[ospf]/ietf-ospf:ospf/ + * + * Build an absolute ietf-ospf instance-level leaf xpath for the given OSPF + * instance. The `router ospf` block is not yet converted to YANG, so the + * instance keys come from FRR-side context rather than a vty xpath push. + * Returns -1 on truncation. + */ +int ospf_per_instance_xpath(char *xpath, size_t size, const struct ospf *ospf, const char *leaf) +{ + char instance_name[XPATH_MAXLEN]; + int ret; + + if (!ospf || !leaf) + return -1; + ret = snprintf(xpath, size, OSPFD_IETF_ROUTING_PROTOCOL_XPATH "/ietf-ospf:ospf%s", + ospfd_ietf_ospf_instance_name(ospf, instance_name, sizeof(instance_name)), + leaf); + if (ret < 0 || (size_t)ret >= size) + return -1; + return 0; +} +static void ospf_router_id_change_advise(struct vty *vty, const struct ospf *ospf) +{ struct listnode *node; struct ospf_area *area; - ospf->router_id_static = router_id; - for (ALL_LIST_ELEMENTS_RO(ospf->areas, node, area)) if (area->full_nbrs) { vty_out(vty, "For this router-id change to take effect, use \"clear ip ospf process\" command\n"); - return CMD_SUCCESS; + return; + } +} + +/* + * XPath: /ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/areas/area[area-id=...]/ + * + * Build the absolute ietf-ospf area-list-entry xpath given a parsed area + * id, optionally suffixed with a leaf path. The area-id key always + * serialises as A.B.C.D in YANG even if the operator typed it as a + * decimal value. `leaf` may be NULL to get just the list-entry xpath, or + * a leaf path like "/area-type" or "/summary" to land on a child node. + * + * Returns 0 on success, -1 if snprintf failed or the result was truncated. + */ +static int ospf_area_xpath(char *xpath, size_t size, const struct ospf *ospf, + struct in_addr area_id, const char *leaf) +{ + char area_id_str[INET_ADDRSTRLEN]; + char instance_name[XPATH_MAXLEN]; + int ret; + + inet_ntop(AF_INET, &area_id, area_id_str, sizeof(area_id_str)); + ret = snprintf(xpath, size, + OSPFD_IETF_ROUTING_PROTOCOL_XPATH + "/ietf-ospf:ospf/areas/area[area-id='%s']%s", + ospfd_ietf_ospf_instance_name(ospf, instance_name, sizeof(instance_name)), + area_id_str, leaf ? leaf : ""); + if (ret < 0 || (size_t)ret >= size) + return -1; + + return 0; +} + +/* + * Build the absolute ietf-ospf per-interface xpath if the interface is + * eligible for YANG dispatch (has an explicit if_area set via + * `ip ospf area` or a network statement). Returns 0 on success, -1 if + * the YANG path doesn't apply -- the caller falls back to direct + * mutation in that case. Per-address overrides (legacy + * `ip ospf cost N A.B.C.D`) are out of RFC 9129's scope and never go + * through YANG; the caller checks ifaddr_str before calling this. + */ +int ospf_per_iface_xpath(char *xpath, size_t size, const struct interface *ifp, const char *leaf) +{ + const struct ospf *ospf = NULL; + const struct ospf_if_info *oii; + const struct ospf_if_params *params; + struct route_node *rn; + char area_id_str[INET_ADDRSTRLEN]; + char instance_name[XPATH_MAXLEN]; + int ret; + + if (!ifp) + return -1; + params = IF_DEF_PARAMS((struct interface *)ifp); + if (!OSPF_IF_PARAM_CONFIGURED(params, if_area)) + return -1; + + /* + * Find the OSPF instance that owns this interface via + * ifp->info->oifs, which holds the per-address struct ospf_interface + * entries (each with an `ospf` backpointer). All entries on the same + * interface belong to the same instance, so taking the first one is + * correct. This works for multi-instance OSPF in the default VRF, + * where ifp->vrf->info would point at the wrong (unnamed) instance. + */ + oii = (const struct ospf_if_info *)ifp->info; + if (oii && oii->oifs) { + for (rn = route_top(oii->oifs); rn; rn = route_next(rn)) { + const struct ospf_interface *oi = rn->info; + + if (oi && oi->ospf) { + ospf = oi->ospf; + route_unlock_node(rn); + break; + } } + } + if (!ospf) + return -1; + + inet_ntop(AF_INET, ¶ms->if_area, area_id_str, sizeof(area_id_str)); + ret = snprintf(xpath, size, + OSPFD_IETF_ROUTING_PROTOCOL_XPATH + "/ietf-ospf:ospf/areas/area[area-id='%s']/interfaces/interface[name='%s']%s", + ospfd_ietf_ospf_instance_name(ospf, instance_name, sizeof(instance_name)), + area_id_str, ifp->name, leaf ? leaf : ""); + if (ret < 0 || (size_t)ret >= size) + return -1; - ospf_router_id_update(ospf); + return 0; +} - return CMD_SUCCESS; +DEFPY_YANG (ospf_router_id, + ospf_router_id_cmd, + "ospf router-id A.B.C.D", + "OSPF specific commands\n" + "router-id for the OSPF process\n" + "OSPF router-id in IP address format\n") +{ + VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); + char xpath[XPATH_MAXLEN]; + int ret; + + ospf_router_id_xpath(xpath, sizeof(xpath), ospf); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, router_id_str); + + ret = nb_cli_apply_changes(vty, NULL); + if (ret == CMD_SUCCESS) + ospf_router_id_change_advise(vty, ospf); + + return ret; } -DEFUN_HIDDEN (ospf_router_id_old, +DEFUN_YANG_HIDDEN (ospf_router_id_old, ospf_router_id_old_cmd, "router-id A.B.C.D", "router-id for the OSPF process\n" @@ -281,32 +432,26 @@ DEFUN_HIDDEN (ospf_router_id_old, { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); int idx_ipv4 = 1; - struct listnode *node; - struct ospf_area *area; struct in_addr router_id; + char xpath[XPATH_MAXLEN]; int ret; - ret = inet_aton(argv[idx_ipv4]->arg, &router_id); - if (!ret) { + if (inet_aton(argv[idx_ipv4]->arg, &router_id) != 1) { vty_out(vty, "Please specify Router ID by A.B.C.D\n"); return CMD_WARNING_CONFIG_FAILED; } - ospf->router_id_static = router_id; - - for (ALL_LIST_ELEMENTS_RO(ospf->areas, node, area)) - if (area->full_nbrs) { - vty_out(vty, - "For this router-id change to take effect, use \"clear ip ospf process\" command\n"); - return CMD_SUCCESS; - } + ospf_router_id_xpath(xpath, sizeof(xpath), ospf); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, argv[idx_ipv4]->arg); - ospf_router_id_update(ospf); + ret = nb_cli_apply_changes(vty, NULL); + if (ret == CMD_SUCCESS) + ospf_router_id_change_advise(vty, ospf); - return CMD_SUCCESS; + return ret; } -DEFPY (no_ospf_router_id, +DEFPY_YANG (no_ospf_router_id, no_ospf_router_id_cmd, "no ospf router-id [A.B.C.D]", NO_STR @@ -315,8 +460,8 @@ DEFPY (no_ospf_router_id, "OSPF router-id in IP address format\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - struct listnode *node; - struct ospf_area *area; + char xpath[XPATH_MAXLEN]; + int ret; if (router_id_str) { if (!IPV4_ADDR_SAME(&ospf->router_id_static, &router_id)) { @@ -325,26 +470,20 @@ DEFPY (no_ospf_router_id, } } - ospf->router_id_static.s_addr = 0; - - for (ALL_LIST_ELEMENTS_RO(ospf->areas, node, area)) - if (area->full_nbrs) { - vty_out(vty, - "For this router-id change to take effect, use \"clear ip ospf process\" command\n"); - return CMD_SUCCESS; - } + ospf_router_id_xpath(xpath, sizeof(xpath), ospf); + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); - ospf_router_id_update(ospf); + ret = nb_cli_apply_changes(vty, NULL); + if (ret == CMD_SUCCESS) + ospf_router_id_change_advise(vty, ospf); - return CMD_SUCCESS; + return ret; } -ALIAS_HIDDEN (no_ospf_router_id, - no_router_id_cmd, - "no router-id [A.B.C.D]", - NO_STR - "router-id for the OSPF process\n" - "OSPF router-id in IP address format\n") +ALIAS_ATTR(no_ospf_router_id, no_router_id_cmd, "no router-id [A.B.C.D]", + NO_STR "router-id for the OSPF process\n" + "OSPF router-id in IP address format\n", + CMD_ATTR_YANG | CMD_ATTR_HIDDEN) static void ospf_passive_interface_default_update(struct ospf *ospf, uint8_t newval) @@ -359,9 +498,8 @@ static void ospf_passive_interface_default_update(struct ospf *ospf, ospf_if_set_multicast(oi); } -static void ospf_passive_interface_update(struct interface *ifp, - struct ospf_if_params *params, - struct in_addr addr, uint8_t newval) +void ospf_passive_interface_update(struct interface *ifp, struct ospf_if_params *params, + struct in_addr addr, uint8_t newval) { struct route_node *rn; @@ -609,9 +747,9 @@ DEFUN (no_ospf_network_area, return CMD_SUCCESS; } -DEFUN (ospf_area_range, +DEFPY_YANG (ospf_area_range, ospf_area_range_cmd, - "area range A.B.C.D/M [advertise [cost (0-16777215)]]", + "area $areaid range A.B.C.D/M$prefix [advertise [cost (0-16777215)$cost]]", "OSPF area parameters\n" "OSPF area ID in IP address format\n" "OSPF area ID as a decimal value\n" @@ -621,30 +759,31 @@ DEFUN (ospf_area_range, "User specified metric for this range\n" "Advertised metric for this range\n") { - VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - struct ospf_area *area; - int idx_ipv4_number = 1; - int idx_ipv4_prefixlen = 3; - int idx_cost = 6; - struct prefix_ipv4 p; + VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf_inst); struct in_addr area_id; int format; - uint32_t cost; + char xpath[XPATH_MAXLEN]; + char tail[XPATH_MAXLEN]; - VTY_GET_OSPF_AREA_ID(area_id, format, argv[idx_ipv4_number]->arg); - str2prefix_ipv4(argv[idx_ipv4_prefixlen]->arg, &p); + VTY_GET_OSPF_AREA_ID(area_id, format, areaid); + snprintf(tail, sizeof(tail), "/ranges/range[prefix='%s']", prefix_str); + if (ospf_area_xpath(xpath, sizeof(xpath), ospf_inst, area_id, tail) != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_CREATE, NULL); - area = ospf_area_get(ospf, area_id); - ospf_area_display_format_set(ospf, area, format); + snprintf(tail, sizeof(tail), "/ranges/range[prefix='%s']/advertise", prefix_str); + if (ospf_area_xpath(xpath, sizeof(xpath), ospf_inst, area_id, tail) != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "true"); - ospf_area_range_set(ospf, area, area->ranges, &p, - OSPF_AREA_RANGE_ADVERTISE, false); - if (argc > 5) { - cost = strtoul(argv[idx_cost]->arg, NULL, 10); - ospf_area_range_cost_set(ospf, area, area->ranges, &p, cost); + if (cost_str) { + snprintf(tail, sizeof(tail), "/ranges/range[prefix='%s']/cost", prefix_str); + if (ospf_area_xpath(xpath, sizeof(xpath), ospf_inst, area_id, tail) != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, cost_str); } - return CMD_SUCCESS; + return nb_cli_apply_changes(vty, NULL); } DEFUN (ospf_area_range_cost, @@ -722,9 +861,9 @@ DEFUN (ospf_area_range_not_advertise, return CMD_SUCCESS; } -DEFUN (no_ospf_area_range, +DEFPY_YANG (no_ospf_area_range, no_ospf_area_range_cmd, - "no area range A.B.C.D/M []", + "no area $areaid range A.B.C.D/M$prefix []", NO_STR "OSPF area parameters\n" "OSPF area ID in IP address format\n" @@ -738,25 +877,18 @@ DEFUN (no_ospf_area_range, "Advertised metric for this range\n" "DoNotAdvertise this range\n") { - VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - struct ospf_area *area; - int idx_ipv4_number = 2; - int idx_ipv4_prefixlen = 4; - struct prefix_ipv4 p; + VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf_inst); struct in_addr area_id; int format; + char xpath[XPATH_MAXLEN]; + char tail[XPATH_MAXLEN]; - VTY_GET_OSPF_AREA_ID(area_id, format, argv[idx_ipv4_number]->arg); - str2prefix_ipv4(argv[idx_ipv4_prefixlen]->arg, &p); - - area = ospf_area_get(ospf, area_id); - ospf_area_display_format_set(ospf, area, format); - - ospf_area_range_unset(ospf, area, area->ranges, &p); - - ospf_area_check_free(ospf, area_id); - - return CMD_SUCCESS; + VTY_GET_OSPF_AREA_ID(area_id, format, areaid); + snprintf(tail, sizeof(tail), "/ranges/range[prefix='%s']", prefix_str); + if (ospf_area_xpath(xpath, sizeof(xpath), ospf_inst, area_id, tail) != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); } DEFUN (no_ospf_area_range_substitute, @@ -1398,41 +1530,56 @@ DEFUN (no_ospf_area_shortcut, } -DEFUN (ospf_area_stub, +/* + * area X stub [no-summary] -> set area-type=stub-area, summary=true|false + * no area X stub [no-summary] -> destroy area entry or just clear summary + * + * The YANG layer's area-type modify callback maps stub-area onto + * ospf_area_stub_set + the external-LSA flush; the summary modify maps + * true/false onto ospf_area_no_summary_unset/set. The DEFPY_YANG + * wrappers just enqueue the right combination and forward the display + * format (a FRR-internal concern that doesn't live in YANG). + */ +static void ospf_area_display_format_set_if_present(struct ospf *ospf, struct in_addr area_id, + int format) +{ + struct ospf_area *area; + + area = ospf_area_lookup_by_area_id(ospf, area_id); + if (area) + ospf_area_display_format_set(ospf, area, format); +} + +DEFPY_YANG (ospf_area_stub, ospf_area_stub_cmd, - "area stub", + "area $area_str stub", "OSPF area parameters\n" "OSPF area ID in IP address format\n" "OSPF area ID as a decimal value\n" "Configure OSPF area as stub\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - int idx_ipv4_number = 1; struct in_addr area_id; - int ret, format; + int format, ret; + char xpath[XPATH_MAXLEN]; - VTY_GET_OSPF_AREA_ID_NO_BB("stub", area_id, format, - argv[idx_ipv4_number]->arg); + VTY_GET_OSPF_AREA_ID_NO_BB("stub", area_id, format, area_str); - ret = ospf_area_stub_set(ospf, area_id); - ospf_area_display_format_set(ospf, ospf_area_get(ospf, area_id), - format); - if (ret == 0) { - vty_out(vty, - "First deconfigure all virtual link through this area\n"); - return CMD_WARNING_CONFIG_FAILED; - } + ospf_area_xpath(xpath, sizeof(xpath), ospf, area_id, "/area-type"); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "stub-area"); + ospf_area_xpath(xpath, sizeof(xpath), ospf, area_id, "/summary"); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "true"); - /* Flush the external LSAs from the specified area */ - ospf_flush_lsa_from_area(ospf, area_id, OSPF_AS_EXTERNAL_LSA); - ospf_area_no_summary_unset(ospf, area_id); + ret = nb_cli_apply_changes(vty, NULL); + if (ret == CMD_SUCCESS) + ospf_area_display_format_set_if_present(ospf, area_id, format); - return CMD_SUCCESS; + return ret; } -DEFUN (ospf_area_stub_no_summary, +DEFPY_YANG (ospf_area_stub_no_summary, ospf_area_stub_no_summary_cmd, - "area stub no-summary", + "area $area_str stub no-summary", "OSPF stub parameters\n" "OSPF area ID in IP address format\n" "OSPF area ID as a decimal value\n" @@ -1440,30 +1587,27 @@ DEFUN (ospf_area_stub_no_summary, "Do not inject inter-area routes into stub\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - int idx_ipv4_number = 1; struct in_addr area_id; - int ret, format; + int format, ret; + char xpath[XPATH_MAXLEN]; - VTY_GET_OSPF_AREA_ID_NO_BB("stub", area_id, format, - argv[idx_ipv4_number]->arg); + VTY_GET_OSPF_AREA_ID_NO_BB("stub", area_id, format, area_str); - ret = ospf_area_stub_set(ospf, area_id); - ospf_area_display_format_set(ospf, ospf_area_get(ospf, area_id), - format); - if (ret == 0) { - vty_out(vty, - "%% Area cannot be stub as it contains a virtual link\n"); - return CMD_WARNING_CONFIG_FAILED; - } + ospf_area_xpath(xpath, sizeof(xpath), ospf, area_id, "/area-type"); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "stub-area"); + ospf_area_xpath(xpath, sizeof(xpath), ospf, area_id, "/summary"); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "false"); - ospf_area_no_summary_set(ospf, area_id); + ret = nb_cli_apply_changes(vty, NULL); + if (ret == CMD_SUCCESS) + ospf_area_display_format_set_if_present(ospf, area_id, format); - return CMD_SUCCESS; + return ret; } -DEFUN (no_ospf_area_stub, +DEFPY_YANG (no_ospf_area_stub, no_ospf_area_stub_cmd, - "no area stub", + "no area $area_str stub", NO_STR "OSPF area parameters\n" "OSPF area ID in IP address format\n" @@ -1471,22 +1615,41 @@ DEFUN (no_ospf_area_stub, "Configure OSPF area as stub\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - int idx_ipv4_number = 2; struct in_addr area_id; int format; + char xpath[XPATH_MAXLEN]; - VTY_GET_OSPF_AREA_ID_NO_BB("stub", area_id, format, - argv[idx_ipv4_number]->arg); - - ospf_area_stub_unset(ospf, area_id); - ospf_area_no_summary_unset(ospf, area_id); + VTY_GET_OSPF_AREA_ID_NO_BB("stub", area_id, format, area_str); - return CMD_SUCCESS; + /* + * `no area X stub` reverts area-type to normal-area AND clears the + * stub-only leaves (summary, default-cost) from the YANG datastore in + * the same commit: + * + * - Setting area-type to normal alone would leave a stale + * `summary = false` (if the operator had previously typed + * `area X stub no-summary`) in the datastore. RFC 9129 restricts + * `summary` and `default-cost` to stub / NSSA via a `when` clause, + * so libyang considers them inapplicable on a normal area; + * leaving them set produces a datastore inconsistent with the + * schema. + * + * - Other per-area state (ranges, interface attachments) is + * deliberately untouched -- matches legacy `ospf_area_stub_unset` + * semantics, which only flipped external_routing back to DEFAULT. + */ + ospf_area_xpath(xpath, sizeof(xpath), ospf, area_id, "/area-type"); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "normal-area"); + ospf_area_xpath(xpath, sizeof(xpath), ospf, area_id, "/summary"); + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + ospf_area_xpath(xpath, sizeof(xpath), ospf, area_id, "/default-cost"); + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); } -DEFUN (no_ospf_area_stub_no_summary, +DEFPY_YANG (no_ospf_area_stub_no_summary, no_ospf_area_stub_no_summary_cmd, - "no area stub no-summary", + "no area $area_str stub no-summary", NO_STR "OSPF area parameters\n" "OSPF area ID in IP address format\n" @@ -1495,15 +1658,25 @@ DEFUN (no_ospf_area_stub_no_summary, "Do not inject inter-area routes into area\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - int idx_ipv4_number = 2; struct in_addr area_id; int format; + char xpath[XPATH_MAXLEN]; - VTY_GET_OSPF_AREA_ID_NO_BB("stub", area_id, format, - argv[idx_ipv4_number]->arg); - ospf_area_no_summary_unset(ospf, area_id); + VTY_GET_OSPF_AREA_ID_NO_BB("stub", area_id, format, area_str); - return CMD_SUCCESS; + /* + * `no area X stub no-summary` only clears the no_summary flag -- + * the area stays stub. Matches the legacy DEFUN exactly, which + * called ospf_area_no_summary_unset(ospf, area_id) and nothing + * else; deleting the YANG summary leaf reverts no_summary to + * FRR's natural default (0 = summary LSAs injected) through the + * same internal helper. NOT a behavioural change from the + * legacy CLI; this verb does NOT also clear the stub flag (that + * is what `no area X stub` does, dispatched separately above). + */ + ospf_area_xpath(xpath, sizeof(xpath), ospf, area_id, "/summary"); + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); } DEFPY (ospf_area_nssa, @@ -1709,9 +1882,17 @@ DEFPY (no_ospf_area_nssa_range, return CMD_SUCCESS; } -DEFUN (ospf_area_default_cost, +/* + * area X default-cost N -> set default-cost leaf + * no area X default-cost -> delete default-cost leaf (reverts to FRR default 1) + * + * The YANG modify callback enforces the stub-or-NSSA precondition; the + * legacy "neither stub, nor NSSA" error becomes an NB_ERR_VALIDATION + * surfaced through nb_cli_apply_changes. + */ +DEFPY_YANG (ospf_area_default_cost, ospf_area_default_cost_cmd, - "area default-cost (0-16777215)", + "area $area_str default-cost (0-16777215)$cost", "OSPF area parameters\n" "OSPF area ID in IP address format\n" "OSPF area ID as a decimal value\n" @@ -1719,43 +1900,28 @@ DEFUN (ospf_area_default_cost, "Stub's advertised default summary cost\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - int idx_ipv4_number = 1; - int idx_number = 3; - struct ospf_area *area; struct in_addr area_id; - uint32_t cost; - int format; - struct prefix_ipv4 p; - - VTY_GET_OSPF_AREA_ID_NO_BB("default-cost", area_id, format, - argv[idx_ipv4_number]->arg); - cost = strtoul(argv[idx_number]->arg, NULL, 10); - - area = ospf_area_get(ospf, area_id); - ospf_area_display_format_set(ospf, area, format); + int format, ret; + char xpath[XPATH_MAXLEN]; - if (area->external_routing == OSPF_AREA_DEFAULT) { - vty_out(vty, "The area is neither stub, nor NSSA\n"); - return CMD_WARNING_CONFIG_FAILED; - } + VTY_GET_OSPF_AREA_ID_NO_BB("default-cost", area_id, format, area_str); - area->default_cost = cost; + /* `cost_str` is the DEFPY-generated string form of the (0-16777215) + * argument; passing it straight through avoids a redundant snprintf. + */ + ospf_area_xpath(xpath, sizeof(xpath), ospf, area_id, "/default-cost"); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, cost_str); - p.family = AF_INET; - p.prefix.s_addr = OSPF_DEFAULT_DESTINATION; - p.prefixlen = 0; - if (IS_DEBUG_OSPF_EVENT) - zlog_debug( - "ospf_abr_announce_stub_defaults(): announcing 0.0.0.0/0 to area %pI4", - &area->area_id); - ospf_abr_announce_network_to_area(&p, area->default_cost, area); + ret = nb_cli_apply_changes(vty, NULL); + if (ret == CMD_SUCCESS) + ospf_area_display_format_set_if_present(ospf, area_id, format); - return CMD_SUCCESS; + return ret; } -DEFUN (no_ospf_area_default_cost, +DEFPY_YANG (no_ospf_area_default_cost, no_ospf_area_default_cost_cmd, - "no area default-cost [(0-16777215)]", + "no area $area_str default-cost [(0-16777215)]", NO_STR "OSPF area parameters\n" "OSPF area ID in IP address format\n" @@ -1764,39 +1930,15 @@ DEFUN (no_ospf_area_default_cost, "Stub's advertised default summary cost\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - int idx_ipv4_number = 2; - struct ospf_area *area; struct in_addr area_id; int format; - struct prefix_ipv4 p; - - VTY_GET_OSPF_AREA_ID_NO_BB("default-cost", area_id, format, - argv[idx_ipv4_number]->arg); - - area = ospf_area_lookup_by_area_id(ospf, area_id); - if (area == NULL) - return CMD_SUCCESS; - - if (area->external_routing == OSPF_AREA_DEFAULT) { - vty_out(vty, "The area is neither stub, nor NSSA\n"); - return CMD_WARNING_CONFIG_FAILED; - } - - area->default_cost = 1; - - p.family = AF_INET; - p.prefix.s_addr = OSPF_DEFAULT_DESTINATION; - p.prefixlen = 0; - if (IS_DEBUG_OSPF_EVENT) - zlog_debug( - "ospf_abr_announce_stub_defaults(): announcing 0.0.0.0/0 to area %pI4", - &area->area_id); - ospf_abr_announce_network_to_area(&p, area->default_cost, area); + char xpath[XPATH_MAXLEN]; + VTY_GET_OSPF_AREA_ID_NO_BB("default-cost", area_id, format, area_str); - ospf_area_check_free(ospf, area_id); - - return CMD_SUCCESS; + ospf_area_xpath(xpath, sizeof(xpath), ospf, area_id, "/default-cost"); + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); } DEFUN (ospf_area_export_list, @@ -2489,37 +2631,32 @@ DEFUN (no_ospf_refresh_timer, } -DEFUN (ospf_auto_cost_reference_bandwidth, +/* + * `auto-cost reference-bandwidth` maps onto the RFC 9129 + * `/auto-cost/reference-bandwidth` leaf. The deviations file pins + * `/auto-cost/enabled` to `true` so the YANG `when "../enabled = 'true'"` + * constraint on the bandwidth leaf is always satisfied from the CLI + * side, matching FRR's runtime "always compute cost from bandwidth" + * semantics. + */ +DEFPY_YANG (ospf_auto_cost_reference_bandwidth, ospf_auto_cost_reference_bandwidth_cmd, - "auto-cost reference-bandwidth (1-4294967)", + "auto-cost reference-bandwidth (1-4294967)$refbw", "Calculate OSPF interface cost according to bandwidth\n" "Use reference bandwidth method to assign OSPF cost\n" "The reference bandwidth in terms of Mbits per second\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - struct vrf *vrf = vrf_lookup_by_id(ospf->vrf_id); - int idx_number = 2; - uint32_t refbw; - struct interface *ifp; + char xpath[XPATH_MAXLEN]; - refbw = strtol(argv[idx_number]->arg, NULL, 10); - if (refbw < 1 || refbw > 4294967) { - vty_out(vty, "reference-bandwidth value is invalid\n"); + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, + "/auto-cost/reference-bandwidth") != 0) return CMD_WARNING_CONFIG_FAILED; - } - - /* If reference bandwidth is changed. */ - if ((refbw) == ospf->ref_bandwidth) - return CMD_SUCCESS; - - ospf->ref_bandwidth = refbw; - FOR_ALL_INTERFACES (vrf, ifp) - ospf_if_recalculate_output_cost(ifp); - - return CMD_SUCCESS; + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, refbw_str); + return nb_cli_apply_changes(vty, NULL); } -DEFUN (no_ospf_auto_cost_reference_bandwidth, +DEFPY_YANG (no_ospf_auto_cost_reference_bandwidth, no_ospf_auto_cost_reference_bandwidth_cmd, "no auto-cost reference-bandwidth [(1-4294967)]", NO_STR @@ -2528,21 +2665,13 @@ DEFUN (no_ospf_auto_cost_reference_bandwidth, "The reference bandwidth in terms of Mbits per second\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - struct vrf *vrf = vrf_lookup_by_id(ospf->vrf_id); - struct interface *ifp; + char xpath[XPATH_MAXLEN]; - if (ospf->ref_bandwidth == OSPF_DEFAULT_REF_BANDWIDTH) - return CMD_SUCCESS; - - ospf->ref_bandwidth = OSPF_DEFAULT_REF_BANDWIDTH; - vty_out(vty, "%% OSPF: Reference bandwidth is changed.\n"); - vty_out(vty, - " Please ensure reference bandwidth is consistent across all routers\n"); - - FOR_ALL_INTERFACES (vrf, ifp) - ospf_if_recalculate_output_cost(ifp); - - return CMD_SUCCESS; + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, + "/auto-cost/reference-bandwidth") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); } DEFUN (ospf_write_multiplier, @@ -2645,34 +2774,56 @@ static void ospf_maxpath_set(struct vty *vty, struct ospf *ospf, uint16_t paths) ospf_restart_spf(ospf); } -/* Ospf Maximum multiple paths config support */ -DEFUN (ospf_max_multipath, +/* + * Ospf Maximum multiple paths config support. + * + * RFC 9129 models the limit as `/spf-control/paths` (uint16, range + * 1..65535), which covers FRR's MULTIPATH_NUM cap for every supported + * build. Routes through the ietf-ospf YANG callback unconditionally; + * the legacy direct-mutation path stays only as the fallback when no + * OSPF instance owns the xpath context (e.g. before the first + * `router ospf` line at boot, when the candidate datastore lacks the + * control-plane-protocol parent). + */ +DEFPY_YANG (ospf_max_multipath, ospf_max_multipath_cmd, - "maximum-paths " CMD_RANGE_STR(1, MULTIPATH_NUM), + "maximum-paths (1-65535)$maxpaths", "Max no of multiple paths for ECMP support\n" "Number of paths\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - int idx_number = 1; - uint16_t maxpaths; + char xpath[XPATH_MAXLEN]; + + if (maxpaths > MULTIPATH_NUM) { + vty_out(vty, "%% maximum-paths exceeds platform max %u\n", MULTIPATH_NUM); + return CMD_WARNING_CONFIG_FAILED; + } - maxpaths = strtol(argv[idx_number]->arg, NULL, 10); + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, "/spf-control/paths") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, maxpaths_str); + return nb_cli_apply_changes(vty, NULL); + } - ospf_maxpath_set(vty, ospf, maxpaths); + ospf_maxpath_set(vty, ospf, (uint16_t)maxpaths); return CMD_SUCCESS; } -DEFUN (no_ospf_max_multipath, +DEFPY_YANG (no_ospf_max_multipath, no_ospf_max_multipath_cmd, - "no maximum-paths [" CMD_RANGE_STR(1, MULTIPATH_NUM)"]", + "no maximum-paths [(1-65535)]", NO_STR "Max no of multiple paths for ECMP support\n" "Number of paths\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - uint16_t maxpaths = MULTIPATH_NUM; + char xpath[XPATH_MAXLEN]; + + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, "/spf-control/paths") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } - ospf_maxpath_set(vty, ospf, maxpaths); + ospf_maxpath_set(vty, ospf, MULTIPATH_NUM); return CMD_SUCCESS; } @@ -8029,87 +8180,90 @@ DEFUN_HIDDEN (no_ospf_message_digest_key, return no_ip_ospf_message_digest_key(self, vty, argc, argv); } -DEFUN (ip_ospf_cost, - ip_ospf_cost_cmd, - "ip ospf cost (1-65535) [A.B.C.D]", - "IP Information\n" - "OSPF interface commands\n" - "Interface cost\n" - "Cost\n" - "Address of interface\n") -{ - VTY_DECLVAR_CONTEXT(interface, ifp); - int idx = 0; - uint32_t cost = OSPF_OUTPUT_COST_DEFAULT; - struct in_addr addr; - struct ospf_if_params *params; - params = IF_DEF_PARAMS(ifp); +/* + * Per-interface OSPF leaves route through ietf-ospf + * YANG when (a) no per-address [A.B.C.D] override is given and (b) the + * interface has been assigned to an area. Otherwise we fall back to the + * legacy direct-mutation path so per-address overrides and + * not-yet-area-attached interfaces continue to work exactly as before. + * + * Each pair of (set, unset) helpers handles both the main `ip ospf X` + * form and the hidden backwards-compat `ospf X` alias. + */ - // get arguments - char *coststr = NULL, *ifaddr = NULL; +static int ospf_cost_set_apply(struct vty *vty, struct interface *ifp, uint32_t cost, + const char *ifaddr_str) +{ + struct ospf_if_params *params; + struct in_addr addr = { .s_addr = 0L }; + char xpath[XPATH_MAXLEN]; + char buf[16]; - argv_find(argv, argc, "(1-65535)", &idx); - coststr = argv[idx]->arg; - cost = strtol(coststr, NULL, 10); + if (!ifaddr_str && ospf_per_iface_xpath(xpath, sizeof(xpath), ifp, "/cost") == 0) { + snprintf(buf, sizeof(buf), "%u", cost); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, buf); + return nb_cli_apply_changes(vty, NULL); + } - ifaddr = argv_find(argv, argc, "A.B.C.D", &idx) ? argv[idx]->arg : NULL; - if (ifaddr) { - if (!inet_aton(ifaddr, &addr)) { + params = IF_DEF_PARAMS(ifp); + if (ifaddr_str) { + if (!inet_aton(ifaddr_str, &addr)) { vty_out(vty, "Please specify interface address by A.B.C.D\n"); return CMD_WARNING_CONFIG_FAILED; } - params = ospf_get_if_params(ifp, addr); ospf_if_update_params(ifp, addr); } SET_IF_PARAM(params, output_cost_cmd); params->output_cost_cmd = cost; - ospf_if_recalculate_output_cost(ifp); - return CMD_SUCCESS; } -DEFUN_HIDDEN (ospf_cost, +DEFPY_YANG (ip_ospf_cost, + ip_ospf_cost_cmd, + "ip ospf cost (1-65535)$cost [A.B.C.D]$ifaddr", + "IP Information\n" + "OSPF interface commands\n" + "Interface cost\n" + "Cost\n" + "Address of interface\n") +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_cost_set_apply(vty, ifp, cost, ifaddr_str); +} + +DEFPY_YANG_HIDDEN (ospf_cost, ospf_cost_cmd, - "ospf cost (1-65535) [A.B.C.D]", + "ospf cost (1-65535)$cost [A.B.C.D]$ifaddr", "OSPF interface commands\n" "Interface cost\n" "Cost\n" "Address of interface\n") { - return ip_ospf_cost(self, vty, argc, argv); + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_cost_set_apply(vty, ifp, cost, ifaddr_str); } -DEFUN (no_ip_ospf_cost, - no_ip_ospf_cost_cmd, - "no ip ospf cost [(1-65535)] [A.B.C.D]", - NO_STR - "IP Information\n" - "OSPF interface commands\n" - "Interface cost\n" - "Cost\n" - "Address of interface\n") +static int ospf_cost_unset_apply(struct vty *vty, struct interface *ifp, const char *ifaddr_str) { - VTY_DECLVAR_CONTEXT(interface, ifp); - int idx = 0; - struct in_addr addr; struct ospf_if_params *params; + struct in_addr addr = { .s_addr = 0L }; + char xpath[XPATH_MAXLEN]; - params = IF_DEF_PARAMS(ifp); - - // get arguments - char *ifaddr = NULL; - ifaddr = argv_find(argv, argc, "A.B.C.D", &idx) ? argv[idx]->arg : NULL; + if (!ifaddr_str && ospf_per_iface_xpath(xpath, sizeof(xpath), ifp, "/cost") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } - /* According to the semantics we are mimicking "no ip ospf cost N" is - * always treated as "no ip ospf cost" regardless of the actual value - * of N already configured for the interface. Thus ignore cost. */ + params = IF_DEF_PARAMS(ifp); - if (ifaddr) { - if (!inet_aton(ifaddr, &addr)) { + if (ifaddr_str) { + if (!inet_aton(ifaddr_str, &addr)) { vty_out(vty, "Please specify interface address by A.B.C.D\n"); return CMD_WARNING_CONFIG_FAILED; @@ -8132,34 +8286,35 @@ DEFUN (no_ip_ospf_cost, return CMD_SUCCESS; } -DEFUN_HIDDEN (no_ospf_cost, +DEFPY_YANG (no_ip_ospf_cost, + no_ip_ospf_cost_cmd, + "no ip ospf cost [(1-65535)] [A.B.C.D]$ifaddr", + NO_STR + "IP Information\n" + "OSPF interface commands\n" + "Interface cost\n" + "Cost\n" + "Address of interface\n") +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + + /* `no ip ospf cost N` is treated as `no ip ospf cost` regardless of + * any N already configured -- ignore the cost argument. */ + return ospf_cost_unset_apply(vty, ifp, ifaddr_str); +} + +DEFPY_YANG_HIDDEN (no_ospf_cost, no_ospf_cost_cmd, - "no ospf cost [(1-65535)] [A.B.C.D]", + "no ospf cost [(1-65535)] [A.B.C.D]$ifaddr", NO_STR "OSPF interface commands\n" "Interface cost\n" "Cost\n" "Address of interface\n") { - return no_ip_ospf_cost(self, vty, argc, argv); -} - -static void ospf_nbr_timer_update(struct ospf_interface *oi) -{ - struct route_node *rn; - struct ospf_neighbor *nbr; - - for (rn = route_top(oi->nbrs); rn; rn = route_next(rn)) { - nbr = rn->info; - - if (!nbr) - continue; + VTY_DECLVAR_CONTEXT(interface, ifp); - nbr->v_inactivity = OSPF_IF_PARAM(oi, v_wait); - nbr->v_db_desc = OSPF_IF_PARAM(oi, retransmit_interval); - nbr->v_ls_req = OSPF_IF_PARAM(oi, retransmit_interval); - nbr->v_ls_rxmt = OSPF_IF_PARAM(oi, retransmit_interval); - } + return ospf_cost_unset_apply(vty, ifp, ifaddr_str); } static int ospf_vty_dead_interval_set(struct vty *vty, const char *interval_str, @@ -8232,34 +8387,62 @@ static int ospf_vty_dead_interval_set(struct vty *vty, const char *interval_str, return CMD_SUCCESS; } -DEFUN (ip_ospf_dead_interval, +static int ospf_dead_interval_set_apply(struct vty *vty, struct interface *ifp, uint32_t seconds, + const char *ifaddr_str) +{ + char xpath[XPATH_MAXLEN]; + char hello_xpath[XPATH_MAXLEN]; + char buf[16]; + char hello_buf[16]; + + snprintf(buf, sizeof(buf), "%u", seconds); + if (!ifaddr_str && ospf_per_iface_xpath(xpath, sizeof(xpath), ifp, "/dead-interval") == 0) { + struct ospf_if_params *params = IF_DEF_PARAMS(ifp); + + if (params->v_hello != OSPF_HELLO_INTERVAL_DEFAULT && params->v_hello < seconds && + ospf_per_iface_xpath(hello_xpath, sizeof(hello_xpath), ifp, + "/hello-interval") == 0 && + !yang_dnode_exists(vty->candidate_config->dnode, hello_xpath)) { + snprintf(hello_buf, sizeof(hello_buf), "%u", params->v_hello); + nb_cli_enqueue_change(vty, hello_xpath, NB_OP_MODIFY, hello_buf); + } + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, buf); + return nb_cli_apply_changes(vty, NULL); + } + + /* Legacy fallback uses the shared helper that also handles the + * minimal-hello-multiplier form -- the simple seconds case threads + * through with fast_hello_str=NULL. ifp comes from the vty context + * inside the helper via VTY_DECLVAR_CONTEXT. + */ + return ospf_vty_dead_interval_set(vty, buf, ifaddr_str, NULL); +} + +DEFPY_YANG (ip_ospf_dead_interval, ip_ospf_dead_interval_cmd, - "ip ospf dead-interval (1-65535) [A.B.C.D]", + "ip ospf dead-interval (1-65535)$seconds [A.B.C.D]$ifaddr", "IP Information\n" "OSPF interface commands\n" "Interval time after which a neighbor is declared down\n" "Seconds\n" "Address of interface\n") { - int idx = 0; - char *interval = argv_find(argv, argc, "(1-65535)", &idx) - ? argv[idx]->arg - : NULL; - char *ifaddr = - argv_find(argv, argc, "A.B.C.D", &idx) ? argv[idx]->arg : NULL; - return ospf_vty_dead_interval_set(vty, interval, ifaddr, NULL); -} + VTY_DECLVAR_CONTEXT(interface, ifp); + return ospf_dead_interval_set_apply(vty, ifp, seconds, ifaddr_str); +} -DEFUN_HIDDEN (ospf_dead_interval, +DEFPY_YANG_HIDDEN (ospf_dead_interval, ospf_dead_interval_cmd, - "ospf dead-interval (1-65535) [A.B.C.D]", + "ospf dead-interval (1-65535)$seconds [A.B.C.D]$ifaddr", "OSPF interface commands\n" "Interval time after which a neighbor is declared down\n" "Seconds\n" "Address of interface\n") { - return ip_ospf_dead_interval(self, vty, argc, argv); + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_dead_interval_set_apply(vty, ifp, seconds, ifaddr_str); } DEFUN (ip_ospf_dead_interval_minimal, @@ -8283,37 +8466,27 @@ DEFUN (ip_ospf_dead_interval_minimal, argv[idx_number]->arg); } -DEFUN (no_ip_ospf_dead_interval, - no_ip_ospf_dead_interval_cmd, - "no ip ospf dead-interval [<(1-65535)|minimal hello-multiplier [(2-20)]> [A.B.C.D]]", - NO_STR - "IP Information\n" - "OSPF interface commands\n" - "Interval time after which a neighbor is declared down\n" - "Seconds\n" - "Minimal 1s dead-interval with fast sub-second hellos\n" - "Hello multiplier factor\n" - "Number of Hellos to send each second\n" - "Address of interface\n") +static int ospf_dead_interval_unset_apply(struct vty *vty, struct interface *ifp, + const char *ifaddr_str, bool had_args) { - VTY_DECLVAR_CONTEXT(interface, ifp); - int idx_ipv4 = argc - 1; - struct in_addr addr = {.s_addr = 0L}; - int ret; + struct in_addr addr = { .s_addr = 0L }; struct ospf_if_params *params; struct ospf_interface *oi; struct route_node *rn; + char xpath[XPATH_MAXLEN]; - params = IF_DEF_PARAMS(ifp); + if (!ifaddr_str && ospf_per_iface_xpath(xpath, sizeof(xpath), ifp, "/dead-interval") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } - if (argv[idx_ipv4]->type == IPV4_TKN) { - ret = inet_aton(argv[idx_ipv4]->arg, &addr); - if (!ret) { + params = IF_DEF_PARAMS(ifp); + if (ifaddr_str) { + if (!inet_aton(ifaddr_str, &addr)) { vty_out(vty, "Please specify interface address by A.B.C.D\n"); return CMD_WARNING_CONFIG_FAILED; } - params = ospf_lookup_if_params(ifp, addr); if (params == NULL) return CMD_SUCCESS; @@ -8322,7 +8495,6 @@ DEFUN (no_ip_ospf_dead_interval, UNSET_IF_PARAM(params, v_wait); params->v_wait = OSPF_ROUTER_DEAD_INTERVAL_DEFAULT; params->is_v_wait_set = false; - UNSET_IF_PARAM(params, fast_hello); params->fast_hello = OSPF_FAST_HELLO_DEFAULT; @@ -8332,10 +8504,9 @@ DEFUN (no_ip_ospf_dead_interval, } /* Update timer values in neighbor structure. */ - if (argc == 1) { - struct ospf *ospf = NULL; + if (!had_args) { + struct ospf *ospf = ifp->vrf->info; - ospf = ifp->vrf->info; if (ospf) { oi = ospf_if_lookup_by_local_addr(ospf, ifp, addr); if (oi) @@ -8350,9 +8521,27 @@ DEFUN (no_ip_ospf_dead_interval, return CMD_SUCCESS; } -DEFUN_HIDDEN (no_ospf_dead_interval, +DEFPY_YANG (no_ip_ospf_dead_interval, + no_ip_ospf_dead_interval_cmd, + "no ip ospf dead-interval [<(1-65535)|minimal hello-multiplier [(2-20)]> [A.B.C.D]$ifaddr]", + NO_STR + "IP Information\n" + "OSPF interface commands\n" + "Interval time after which a neighbor is declared down\n" + "Seconds\n" + "Minimal 1s dead-interval with fast sub-second hellos\n" + "Hello multiplier factor\n" + "Number of Hellos to send each second\n" + "Address of interface\n") +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_dead_interval_unset_apply(vty, ifp, ifaddr_str, argc > 4); +} + +DEFPY_YANG_HIDDEN (no_ospf_dead_interval, no_ospf_dead_interval_cmd, - "no ospf dead-interval [<(1-65535)|minimal hello-multiplier (2-20)> [A.B.C.D]]", + "no ospf dead-interval [<(1-65535)|minimal hello-multiplier (2-20)> [A.B.C.D]$ifaddr]", NO_STR "OSPF interface commands\n" "Interval time after which a neighbor is declared down\n" @@ -8362,103 +8551,120 @@ DEFUN_HIDDEN (no_ospf_dead_interval, "Number of Hellos to send each second\n" "Address of interface\n") { - return no_ip_ospf_dead_interval(self, vty, argc, argv); + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_dead_interval_unset_apply(vty, ifp, ifaddr_str, argc > 3); } -DEFUN (ip_ospf_hello_interval, - ip_ospf_hello_interval_cmd, - "ip ospf hello-interval (1-65535) [A.B.C.D]", - "IP Information\n" - "OSPF interface commands\n" - "Time between HELLO packets\n" - "Seconds\n" - "Address of interface\n") +static int ospf_hello_set_apply(struct vty *vty, struct interface *ifp, uint32_t seconds, + const char *ifaddr_str) { - VTY_DECLVAR_CONTEXT(interface, ifp); - int idx = 0; - struct in_addr addr = {.s_addr = 0L}; struct ospf_if_params *params; - params = IF_DEF_PARAMS(ifp); - uint32_t seconds = 0; + struct in_addr addr = { .s_addr = 0L }; bool is_addr = false; - uint32_t old_interval = 0; - - argv_find(argv, argc, "(1-65535)", &idx); - seconds = strtol(argv[idx]->arg, NULL, 10); + uint32_t old_interval; + uint32_t dead_seconds; + char xpath[XPATH_MAXLEN]; + char dead_xpath[XPATH_MAXLEN]; + char buf[16]; + char dead_buf[16]; + + if (!ifaddr_str && ospf_per_iface_xpath(xpath, sizeof(xpath), ifp, "/hello-interval") == 0) { + params = IF_DEF_PARAMS(ifp); + if (params->v_wait <= seconds && seconds < UINT16_MAX && + ospf_per_iface_xpath(dead_xpath, sizeof(dead_xpath), ifp, "/dead-interval") == + 0) { + /* + * The legacy CLI allowed operators to raise hello first + * and fix dead-interval with the next command. YANG + * validates each CLI command as a complete transaction, so + * stage a compatible dead-interval when the current value + * would make this hello update fail the RFC 9129 must. + */ + dead_seconds = MIN(4 * seconds, UINT16_MAX); + snprintf(dead_buf, sizeof(dead_buf), "%u", dead_seconds); + nb_cli_enqueue_change(vty, dead_xpath, NB_OP_MODIFY, dead_buf); + } + snprintf(buf, sizeof(buf), "%u", seconds); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, buf); + return nb_cli_apply_changes(vty, NULL); + } - if (argv_find(argv, argc, "A.B.C.D", &idx)) { - if (!inet_aton(argv[idx]->arg, &addr)) { + params = IF_DEF_PARAMS(ifp); + if (ifaddr_str) { + if (!inet_aton(ifaddr_str, &addr)) { vty_out(vty, "Please specify interface address by A.B.C.D\n"); return CMD_WARNING_CONFIG_FAILED; } - params = ospf_get_if_params(ifp, addr); ospf_if_update_params(ifp, addr); is_addr = true; } old_interval = params->v_hello; - - /* Return, if same interval is configured. */ if (old_interval == seconds) return CMD_SUCCESS; SET_IF_PARAM(params, v_hello); params->v_hello = seconds; + /* RFC 4062: dead-interval should track 4 * hello unless explicit. */ if (!params->is_v_wait_set) { SET_IF_PARAM(params, v_wait); - /* As per RFC 4062 - * The router dead interval should - * be some multiple of the HelloInterval (perhaps 4 times the - * hello interval) and must be the same for all routers - * attached to a common network. - */ - params->v_wait = 4 * seconds; + params->v_wait = 4 * seconds; } ospf_reset_hello_timer(ifp, addr, is_addr); - return CMD_SUCCESS; } -DEFUN_HIDDEN (ospf_hello_interval, +DEFPY_YANG (ip_ospf_hello_interval, + ip_ospf_hello_interval_cmd, + "ip ospf hello-interval (1-65535)$seconds [A.B.C.D]$ifaddr", + "IP Information\n" + "OSPF interface commands\n" + "Time between HELLO packets\n" + "Seconds\n" + "Address of interface\n") +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_hello_set_apply(vty, ifp, seconds, ifaddr_str); +} + +DEFPY_YANG_HIDDEN (ospf_hello_interval, ospf_hello_interval_cmd, - "ospf hello-interval (1-65535) [A.B.C.D]", + "ospf hello-interval (1-65535)$seconds [A.B.C.D]$ifaddr", "OSPF interface commands\n" "Time between HELLO packets\n" "Seconds\n" "Address of interface\n") { - return ip_ospf_hello_interval(self, vty, argc, argv); + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_hello_set_apply(vty, ifp, seconds, ifaddr_str); } -DEFUN (no_ip_ospf_hello_interval, - no_ip_ospf_hello_interval_cmd, - "no ip ospf hello-interval [(1-65535) [A.B.C.D]]", - NO_STR - "IP Information\n" - "OSPF interface commands\n" - "Time between HELLO packets\n" // ignored - "Seconds\n" - "Address of interface\n") +static int ospf_hello_unset_apply(struct vty *vty, struct interface *ifp, const char *ifaddr_str) { - VTY_DECLVAR_CONTEXT(interface, ifp); - int idx = 0; - struct in_addr addr = {.s_addr = 0L}; struct ospf_if_params *params; + struct in_addr addr = { .s_addr = 0L }; struct route_node *rn; + char xpath[XPATH_MAXLEN]; - params = IF_DEF_PARAMS(ifp); + if (!ifaddr_str && ospf_per_iface_xpath(xpath, sizeof(xpath), ifp, "/hello-interval") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } - if (argv_find(argv, argc, "A.B.C.D", &idx)) { - if (!inet_aton(argv[idx]->arg, &addr)) { + params = IF_DEF_PARAMS(ifp); + if (ifaddr_str) { + if (!inet_aton(ifaddr_str, &addr)) { vty_out(vty, "Please specify interface address by A.B.C.D\n"); return CMD_WARNING_CONFIG_FAILED; } - params = ospf_lookup_if_params(ifp, addr); if (params == NULL) return CMD_SUCCESS; @@ -8469,7 +8675,7 @@ DEFUN (no_ip_ospf_hello_interval, if (!params->is_v_wait_set) { UNSET_IF_PARAM(params, v_wait); - params->v_wait = OSPF_ROUTER_DEAD_INTERVAL_DEFAULT; + params->v_wait = OSPF_ROUTER_DEAD_INTERVAL_DEFAULT; } for (rn = route_top(IF_OIFS(ifp)); rn; rn = route_next(rn)) { @@ -8477,10 +8683,8 @@ DEFUN (no_ip_ospf_hello_interval, if (!oi) continue; - oi->type = IF_DEF_PARAMS(ifp)->type; oi->ptp_dmvpn = IF_DEF_PARAMS(ifp)->ptp_dmvpn; - if (oi->state > ISM_Down) { OSPF_ISM_EVENT_EXECUTE(oi, ISM_InterfaceDown); OSPF_ISM_EVENT_EXECUTE(oi, ISM_InterfaceUp); @@ -8491,40 +8695,76 @@ DEFUN (no_ip_ospf_hello_interval, ospf_free_if_params(ifp, addr); ospf_if_update_params(ifp, addr); } - return CMD_SUCCESS; } -DEFUN_HIDDEN (no_ospf_hello_interval, +DEFPY_YANG (no_ip_ospf_hello_interval, + no_ip_ospf_hello_interval_cmd, + "no ip ospf hello-interval [(1-65535) [A.B.C.D]$ifaddr]", + NO_STR + "IP Information\n" + "OSPF interface commands\n" + "Time between HELLO packets\n" // ignored + "Seconds\n" + "Address of interface\n") +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_hello_unset_apply(vty, ifp, ifaddr_str); +} + +DEFPY_YANG_HIDDEN (no_ospf_hello_interval, no_ospf_hello_interval_cmd, - "no ospf hello-interval [(1-65535) [A.B.C.D]]", + "no ospf hello-interval [(1-65535) [A.B.C.D]$ifaddr]", NO_STR "OSPF interface commands\n" "Time between HELLO packets\n" // ignored "Seconds\n" "Address of interface\n") { - return no_ip_ospf_hello_interval(self, vty, argc, argv); + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_hello_unset_apply(vty, ifp, ifaddr_str); } -DEFUN(ip_ospf_network, ip_ospf_network_cmd, - "ip ospf network ", - "IP Information\n" - "OSPF interface commands\n" - "Network type\n" - "Specify OSPF broadcast multi-access network\n" - "Specify OSPF NBMA network\n" - "Specify OSPF point-to-multipoint network\n" - "Specify OSPF delayed reflooding of LSAs received on P2MP interface\n" - "Specify OSPF point-to-multipoint network doesn't support broadcast\n" - "Specify OSPF point-to-point network\n" - "Specify OSPF point-to-point DMVPN network\n") +/* + * Apply a `ip ospf network` selection through the legacy direct-mutation + * path. Used as the fallback when the YANG path is unavailable (interface + * not in an area) or when the user requested an FRR-specific modifier + * (dmvpn, delay-reflood, p2mp non-broadcast) that has no RFC 9129 + * counterpart. The four type-flag arguments map to the DEFPY named + * captures: exactly one of bcast / nbma / p2mp / p2p is non-NULL. + */ +/* + * Clear FRR-augment per-interface modifiers (dmvpn, delay-reflood, p2mp + * non-broadcast) on both IF_DEF_PARAMS and any allocated ospf_interfaces. + * Used before routing an interface-type change through YANG so that a no-op + * MODIFY (same enum value) still drops modifiers left over from a previous + * legacy invocation. + */ +static void ospf_network_legacy_reset_frr_modifiers(struct interface *ifp) +{ + struct route_node *rn; + struct ospf_interface *oi; + + IF_DEF_PARAMS(ifp)->ptp_dmvpn = 0; + IF_DEF_PARAMS(ifp)->p2mp_delay_reflood = OSPF_P2MP_DELAY_REFLOOD_DEFAULT; + IF_DEF_PARAMS(ifp)->p2mp_non_broadcast = OSPF_P2MP_NON_BROADCAST_DEFAULT; + + for (rn = route_top(IF_OIFS(ifp)); rn; rn = route_next(rn)) { + oi = rn->info; + if (oi == NULL) + continue; + oi->ptp_dmvpn = 0; + oi->p2mp_delay_reflood = OSPF_P2MP_DELAY_REFLOOD_DEFAULT; + oi->p2mp_non_broadcast = OSPF_P2MP_NON_BROADCAST_DEFAULT; + } +} + +static int ospf_network_legacy_apply(struct vty *vty, struct interface *ifp, const char *bcast, + const char *nbma, const char *p2mp, const char *delay_reflood, + const char *p2mp_nbma, const char *p2p, const char *dmvpn) { - VTY_DECLVAR_CONTEXT(interface, ifp); - int idx = 0; int old_type = IF_DEF_PARAMS(ifp)->type; uint8_t old_ptp_dmvpn = IF_DEF_PARAMS(ifp)->ptp_dmvpn; uint8_t old_p2mp_delay_reflood = IF_DEF_PARAMS(ifp)->p2mp_delay_reflood; @@ -8542,19 +8782,19 @@ DEFUN(ip_ospf_network, ip_ospf_network_cmd, OSPF_P2MP_DELAY_REFLOOD_DEFAULT; IF_DEF_PARAMS(ifp)->p2mp_non_broadcast = OSPF_P2MP_NON_BROADCAST_DEFAULT; - if (argv_find(argv, argc, "broadcast", &idx)) + if (bcast) IF_DEF_PARAMS(ifp)->type = OSPF_IFTYPE_BROADCAST; - else if (argv_find(argv, argc, "point-to-multipoint", &idx)) { + else if (p2mp) { IF_DEF_PARAMS(ifp)->type = OSPF_IFTYPE_POINTOMULTIPOINT; - if (argv_find(argv, argc, "delay-reflood", &idx)) + if (delay_reflood) IF_DEF_PARAMS(ifp)->p2mp_delay_reflood = true; - if (argv_find(argv, argc, "non-broadcast", &idx)) + if (p2mp_nbma) IF_DEF_PARAMS(ifp)->p2mp_non_broadcast = true; - } else if (argv_find(argv, argc, "non-broadcast", &idx)) + } else if (nbma) IF_DEF_PARAMS(ifp)->type = OSPF_IFTYPE_NBMA; - else if (argv_find(argv, argc, "point-to-point", &idx)) { + else if (p2p) { IF_DEF_PARAMS(ifp)->type = OSPF_IFTYPE_POINTOPOINT; - if (argv_find(argv, argc, "dmvpn", &idx)) + if (dmvpn) IF_DEF_PARAMS(ifp)->ptp_dmvpn = 1; } @@ -8597,9 +8837,53 @@ DEFUN(ip_ospf_network, ip_ospf_network_cmd, return CMD_SUCCESS; } -DEFUN_HIDDEN (ospf_network, +DEFPY_YANG(ip_ospf_network, ip_ospf_network_cmd, + "ip ospf network ", + "IP Information\n" + "OSPF interface commands\n" + "Network type\n" + "Specify OSPF broadcast multi-access network\n" + "Specify OSPF NBMA network\n" + "Specify OSPF point-to-multipoint network\n" + "Specify OSPF delayed reflooding of LSAs received on P2MP interface\n" + "Specify OSPF point-to-multipoint network doesn't support broadcast\n" + "Specify OSPF point-to-point network\n" + "Specify OSPF point-to-point DMVPN network\n") +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + const char *type_str = NULL; + bool has_frr_modifier = false; + char xpath[XPATH_MAXLEN]; + + if (bcast) + type_str = "broadcast"; + else if (nbma) + type_str = "non-broadcast"; + else if (p2mp) { + type_str = "point-to-multipoint"; + has_frr_modifier = delay_reflood || p2mp_nbma; + } else if (p2p) { + type_str = "point-to-point"; + has_frr_modifier = dmvpn != NULL; + } + + if (type_str && !has_frr_modifier && + ospf_per_iface_xpath(xpath, sizeof(xpath), ifp, "/interface-type") == 0) { + ospf_network_legacy_reset_frr_modifiers(ifp); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, type_str); + return nb_cli_apply_changes(vty, NULL); + } + + return ospf_network_legacy_apply(vty, ifp, bcast, nbma, p2mp, delay_reflood, p2mp_nbma, + p2p, dmvpn); +} + +DEFPY_YANG_HIDDEN (ospf_network, ospf_network_cmd, - "ospf network ", + "ospf network ", "OSPF interface commands\n" "Network type\n" "Specify OSPF broadcast multi-access network\n" @@ -8607,25 +8891,34 @@ DEFUN_HIDDEN (ospf_network, "Specify OSPF point-to-multipoint network\n" "Specify OSPF point-to-point network\n") { - return ip_ospf_network(self, vty, argc, argv); + VTY_DECLVAR_CONTEXT(interface, ifp); + const char *type_str = NULL; + char xpath[XPATH_MAXLEN]; + + if (bcast) + type_str = "broadcast"; + else if (nbma) + type_str = "non-broadcast"; + else if (p2mp) + type_str = "point-to-multipoint"; + else if (p2p) + type_str = "point-to-point"; + + if (type_str && ospf_per_iface_xpath(xpath, sizeof(xpath), ifp, "/interface-type") == 0) { + ospf_network_legacy_reset_frr_modifiers(ifp); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, type_str); + return nb_cli_apply_changes(vty, NULL); + } + + return ospf_network_legacy_apply(vty, ifp, bcast, nbma, p2mp, NULL, NULL, p2p, NULL); } -DEFUN (no_ip_ospf_network, - no_ip_ospf_network_cmd, - "no ip ospf network []", - NO_STR - "IP Information\n" - "OSPF interface commands\n" - "Network type\n" - "Specify OSPF broadcast multi-access network\n" - "Specify OSPF NBMA network\n" - "Specify OSPF point-to-multipoint network\n" - "Specify OSPF point-to-point network\n") +static int ospf_network_legacy_unset(struct vty *vty, struct interface *ifp) { - VTY_DECLVAR_CONTEXT(interface, ifp); int old_type = IF_DEF_PARAMS(ifp)->type; struct route_node *rn; + UNSET_IF_PARAM(IF_DEF_PARAMS(ifp), type); IF_DEF_PARAMS(ifp)->type = ospf_default_iftype(ifp); IF_DEF_PARAMS(ifp)->type_cfg = false; IF_DEF_PARAMS(ifp)->ptp_dmvpn = 0; @@ -8655,7 +8948,42 @@ DEFUN (no_ip_ospf_network, return CMD_SUCCESS; } -DEFUN_HIDDEN (no_ospf_network, +DEFPY_YANG (no_ip_ospf_network, + no_ip_ospf_network_cmd, + "no ip ospf network []", + NO_STR + "IP Information\n" + "OSPF interface commands\n" + "Network type\n" + "Specify OSPF broadcast multi-access network\n" + "Specify OSPF NBMA network\n" + "Specify OSPF point-to-multipoint network\n" + "Specify OSPF point-to-point network\n") +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + char xpath[XPATH_MAXLEN]; + + /* + * Always clear the legacy ifp state first: an earlier `ip ospf + * network dmvpn` (or any other FRR-modifier form) writes + * directly to params without touching the YANG candidate, so a + * YANG-only DESTROY wouldn't undo it. Once legacy state is + * clean, the YANG callback's early `!OSPF_IF_PARAM_CONFIGURED` + * bail makes the DESTROY a no-op for the FRR side; its only + * remaining job is to remove any YANG candidate entry that a + * previous canonical-type set may have created. + */ + ospf_network_legacy_unset(vty, ifp); + + if (ospf_per_iface_xpath(xpath, sizeof(xpath), ifp, "/interface-type") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } + + return CMD_SUCCESS; +} + +DEFPY_YANG_HIDDEN (no_ospf_network, no_ospf_network_cmd, "no ospf network []", NO_STR @@ -8666,36 +8994,41 @@ DEFUN_HIDDEN (no_ospf_network, "Specify OSPF point-to-multipoint network\n" "Specify OSPF point-to-point network\n") { - return no_ip_ospf_network(self, vty, argc, argv); + VTY_DECLVAR_CONTEXT(interface, ifp); + char xpath[XPATH_MAXLEN]; + + ospf_network_legacy_unset(vty, ifp); + + if (ospf_per_iface_xpath(xpath, sizeof(xpath), ifp, "/interface-type") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } + + return CMD_SUCCESS; } -DEFUN (ip_ospf_priority, - ip_ospf_priority_cmd, - "ip ospf priority (0-255) [A.B.C.D]", - "IP Information\n" - "OSPF interface commands\n" - "Router priority\n" - "Priority\n" - "Address of interface\n") +static int ospf_priority_set_apply(struct vty *vty, struct interface *ifp, uint8_t priority, + const char *ifaddr_str) { - VTY_DECLVAR_CONTEXT(interface, ifp); - int idx = 0; - long priority; - struct route_node *rn; - struct in_addr addr; struct ospf_if_params *params; - params = IF_DEF_PARAMS(ifp); + struct in_addr addr = { 0 }; + struct route_node *rn; + char xpath[XPATH_MAXLEN]; + char buf[8]; - argv_find(argv, argc, "(0-255)", &idx); - priority = strtol(argv[idx]->arg, NULL, 10); + if (!ifaddr_str && ospf_per_iface_xpath(xpath, sizeof(xpath), ifp, "/priority") == 0) { + snprintf(buf, sizeof(buf), "%u", priority); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, buf); + return nb_cli_apply_changes(vty, NULL); + } - if (argv_find(argv, argc, "A.B.C.D", &idx)) { - if (!inet_aton(argv[idx]->arg, &addr)) { + params = IF_DEF_PARAMS(ifp); + if (ifaddr_str) { + if (!inet_aton(ifaddr_str, &addr)) { vty_out(vty, "Please specify interface address by A.B.C.D\n"); return CMD_WARNING_CONFIG_FAILED; } - params = ospf_get_if_params(ifp, addr); ospf_if_update_params(ifp, addr); } @@ -8708,52 +9041,60 @@ DEFUN (ip_ospf_priority, if (!oi) continue; - if (PRIORITY(oi) != OSPF_IF_PARAM(oi, priority)) { PRIORITY(oi) = OSPF_IF_PARAM(oi, priority); OSPF_ISM_EVENT_SCHEDULE(oi, ISM_NeighborChange); } } - return CMD_SUCCESS; } -DEFUN_HIDDEN (ospf_priority, +DEFPY_YANG (ip_ospf_priority, + ip_ospf_priority_cmd, + "ip ospf priority (0-255)$priority [A.B.C.D]$ifaddr", + "IP Information\n" + "OSPF interface commands\n" + "Router priority\n" + "Priority\n" + "Address of interface\n") +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_priority_set_apply(vty, ifp, priority, ifaddr_str); +} + +DEFPY_YANG_HIDDEN (ospf_priority, ospf_priority_cmd, - "ospf priority (0-255) [A.B.C.D]", + "ospf priority (0-255)$priority [A.B.C.D]$ifaddr", "OSPF interface commands\n" "Router priority\n" "Priority\n" "Address of interface\n") { - return ip_ospf_priority(self, vty, argc, argv); + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_priority_set_apply(vty, ifp, priority, ifaddr_str); } -DEFUN (no_ip_ospf_priority, - no_ip_ospf_priority_cmd, - "no ip ospf priority [(0-255) [A.B.C.D]]", - NO_STR - "IP Information\n" - "OSPF interface commands\n" - "Router priority\n" // ignored - "Priority\n" - "Address of interface\n") +static int ospf_priority_unset_apply(struct vty *vty, struct interface *ifp, const char *ifaddr_str) { - VTY_DECLVAR_CONTEXT(interface, ifp); - int idx = 0; - struct route_node *rn; - struct in_addr addr; struct ospf_if_params *params; + struct in_addr addr = { 0 }; + struct route_node *rn; + char xpath[XPATH_MAXLEN]; - params = IF_DEF_PARAMS(ifp); + if (!ifaddr_str && ospf_per_iface_xpath(xpath, sizeof(xpath), ifp, "/priority") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } - if (argv_find(argv, argc, "A.B.C.D", &idx)) { - if (!inet_aton(argv[idx]->arg, &addr)) { + params = IF_DEF_PARAMS(ifp); + if (ifaddr_str) { + if (!inet_aton(ifaddr_str, &addr)) { vty_out(vty, "Please specify interface address by A.B.C.D\n"); return CMD_WARNING_CONFIG_FAILED; } - params = ospf_lookup_if_params(ifp, addr); if (params == NULL) return CMD_SUCCESS; @@ -8772,99 +9113,94 @@ DEFUN (no_ip_ospf_priority, if (!oi) continue; - if (PRIORITY(oi) != OSPF_IF_PARAM(oi, priority)) { PRIORITY(oi) = OSPF_IF_PARAM(oi, priority); OSPF_ISM_EVENT_SCHEDULE(oi, ISM_NeighborChange); } } - return CMD_SUCCESS; } -DEFUN_HIDDEN (no_ospf_priority, +DEFPY_YANG (no_ip_ospf_priority, + no_ip_ospf_priority_cmd, + "no ip ospf priority [(0-255) [A.B.C.D]$ifaddr]", + NO_STR + "IP Information\n" + "OSPF interface commands\n" + "Router priority\n" // ignored + "Priority\n" + "Address of interface\n") +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_priority_unset_apply(vty, ifp, ifaddr_str); +} + +DEFPY_YANG_HIDDEN (no_ospf_priority, no_ospf_priority_cmd, - "no ospf priority [(0-255) [A.B.C.D]]", + "no ospf priority [(0-255) [A.B.C.D]$ifaddr]", NO_STR "OSPF interface commands\n" "Router priority\n" "Priority\n" "Address of interface\n") { - return no_ip_ospf_priority(self, vty, argc, argv); + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_priority_unset_apply(vty, ifp, ifaddr_str); } -DEFUN (ip_ospf_retransmit_interval, - ip_ospf_retransmit_interval_addr_cmd, - "ip ospf retransmit-interval (1-65535) [A.B.C.D]", - "IP Information\n" - "OSPF interface commands\n" - "Time between retransmitting lost link state advertisements\n" - "Seconds\n" - "Address of interface\n") +static int ospf_retransmit_interval_set_apply(struct vty *vty, struct interface *ifp, + uint32_t seconds, const char *ifaddr_str) { - VTY_DECLVAR_CONTEXT(interface, ifp); - int idx = 0; - uint32_t seconds; - struct in_addr addr; struct ospf_if_params *params; - params = IF_DEF_PARAMS(ifp); + struct in_addr addr = { .s_addr = 0L }; + char xpath[XPATH_MAXLEN]; + char buf[16]; - argv_find(argv, argc, "(1-65535)", &idx); - seconds = strtol(argv[idx]->arg, NULL, 10); + if (!ifaddr_str && + ospf_per_iface_xpath(xpath, sizeof(xpath), ifp, "/retransmit-interval") == 0) { + snprintf(buf, sizeof(buf), "%u", seconds); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, buf); + return nb_cli_apply_changes(vty, NULL); + } - if (argv_find(argv, argc, "A.B.C.D", &idx)) { - if (!inet_aton(argv[idx]->arg, &addr)) { + params = IF_DEF_PARAMS(ifp); + if (ifaddr_str) { + if (!inet_aton(ifaddr_str, &addr)) { vty_out(vty, "Please specify interface address by A.B.C.D\n"); return CMD_WARNING_CONFIG_FAILED; } - params = ospf_get_if_params(ifp, addr); ospf_if_update_params(ifp, addr); } SET_IF_PARAM(params, retransmit_interval); params->retransmit_interval = seconds; - return CMD_SUCCESS; } -DEFUN_HIDDEN (ospf_retransmit_interval, - ospf_retransmit_interval_cmd, - "ospf retransmit-interval (1-65535) [A.B.C.D]", - "OSPF interface commands\n" - "Time between retransmitting lost link state advertisements\n" - "Seconds\n" - "Address of interface\n") +static int ospf_retransmit_interval_unset_apply(struct vty *vty, struct interface *ifp, + const char *ifaddr_str) { - return ip_ospf_retransmit_interval(self, vty, argc, argv); -} - -DEFUN (no_ip_ospf_retransmit_interval, - no_ip_ospf_retransmit_interval_addr_cmd, - "no ip ospf retransmit-interval [(1-65535)] [A.B.C.D]", - NO_STR - "IP Information\n" - "OSPF interface commands\n" - "Time between retransmitting lost link state advertisements\n" - "Seconds\n" - "Address of interface\n") -{ - VTY_DECLVAR_CONTEXT(interface, ifp); - int idx = 0; - struct in_addr addr; struct ospf_if_params *params; + struct in_addr addr = { .s_addr = 0L }; + char xpath[XPATH_MAXLEN]; - params = IF_DEF_PARAMS(ifp); + if (!ifaddr_str && + ospf_per_iface_xpath(xpath, sizeof(xpath), ifp, "/retransmit-interval") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } - if (argv_find(argv, argc, "A.B.C.D", &idx)) { - if (!inet_aton(argv[idx]->arg, &addr)) { + params = IF_DEF_PARAMS(ifp); + if (ifaddr_str) { + if (!inet_aton(ifaddr_str, &addr)) { vty_out(vty, "Please specify interface address by A.B.C.D\n"); return CMD_WARNING_CONFIG_FAILED; } - params = ospf_lookup_if_params(ifp, addr); if (params == NULL) return CMD_SUCCESS; @@ -8877,20 +9213,63 @@ DEFUN (no_ip_ospf_retransmit_interval, ospf_free_if_params(ifp, addr); ospf_if_update_params(ifp, addr); } - return CMD_SUCCESS; } -DEFUN_HIDDEN (no_ospf_retransmit_interval, +DEFPY_YANG (ip_ospf_retransmit_interval, + ip_ospf_retransmit_interval_addr_cmd, + "ip ospf retransmit-interval (1-65535)$seconds [A.B.C.D]$ifaddr", + "IP Information\n" + "OSPF interface commands\n" + "Time between retransmitting lost link state advertisements\n" + "Seconds\n" + "Address of interface\n") +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_retransmit_interval_set_apply(vty, ifp, seconds, ifaddr_str); +} + +DEFPY_YANG_HIDDEN (ospf_retransmit_interval, + ospf_retransmit_interval_cmd, + "ospf retransmit-interval (1-65535)$seconds [A.B.C.D]$ifaddr", + "OSPF interface commands\n" + "Time between retransmitting lost link state advertisements\n" + "Seconds\n" + "Address of interface\n") +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_retransmit_interval_set_apply(vty, ifp, seconds, ifaddr_str); +} + +DEFPY_YANG (no_ip_ospf_retransmit_interval, + no_ip_ospf_retransmit_interval_addr_cmd, + "no ip ospf retransmit-interval [(1-65535) [A.B.C.D]$ifaddr]", + NO_STR + "IP Information\n" + "OSPF interface commands\n" + "Time between retransmitting lost link state advertisements\n" + "Seconds\n" + "Address of interface\n") +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_retransmit_interval_unset_apply(vty, ifp, ifaddr_str); +} + +DEFPY_YANG_HIDDEN (no_ospf_retransmit_interval, no_ospf_retransmit_interval_cmd, - "no ospf retransmit-interval [(1-65535)] [A.B.C.D]", + "no ospf retransmit-interval [(1-65535) [A.B.C.D]$ifaddr]", NO_STR "OSPF interface commands\n" "Time between retransmitting lost link state advertisements\n" "Seconds\n" "Address of interface\n") { - return no_ip_ospf_retransmit_interval(self, vty, argc, argv); + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_retransmit_interval_unset_apply(vty, ifp, ifaddr_str); } DEFPY(ip_ospf_retransmit_window, ip_ospf_retransmit_window_addr_cmd, @@ -8980,77 +9359,54 @@ DEFPY (no_ip_ospf_gr_hdelay, return CMD_SUCCESS; } -DEFUN (ip_ospf_transmit_delay, - ip_ospf_transmit_delay_addr_cmd, - "ip ospf transmit-delay (1-65535) [A.B.C.D]", - "IP Information\n" - "OSPF interface commands\n" - "Link state transmit delay\n" - "Seconds\n" - "Address of interface\n") +static int ospf_transmit_delay_set_apply(struct vty *vty, struct interface *ifp, uint32_t seconds, + const char *ifaddr_str) { - VTY_DECLVAR_CONTEXT(interface, ifp); - int idx = 0; - uint32_t seconds; - struct in_addr addr; struct ospf_if_params *params; + struct in_addr addr = { .s_addr = 0L }; + char xpath[XPATH_MAXLEN]; + char buf[16]; - params = IF_DEF_PARAMS(ifp); - argv_find(argv, argc, "(1-65535)", &idx); - seconds = strtol(argv[idx]->arg, NULL, 10); + if (!ifaddr_str && ospf_per_iface_xpath(xpath, sizeof(xpath), ifp, "/transmit-delay") == 0) { + snprintf(buf, sizeof(buf), "%u", seconds); + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, buf); + return nb_cli_apply_changes(vty, NULL); + } - if (argv_find(argv, argc, "A.B.C.D", &idx)) { - if (!inet_aton(argv[idx]->arg, &addr)) { - vty_out(vty, - "Please specify interface address by A.B.C.D\n"); + params = IF_DEF_PARAMS(ifp); + if (ifaddr_str) { + if (!inet_aton(ifaddr_str, &addr)) { + vty_out(vty, "Please specify interface address by A.B.C.D\n"); return CMD_WARNING_CONFIG_FAILED; } - params = ospf_get_if_params(ifp, addr); ospf_if_update_params(ifp, addr); } SET_IF_PARAM(params, transmit_delay); params->transmit_delay = seconds; - return CMD_SUCCESS; } -DEFUN_HIDDEN (ospf_transmit_delay, - ospf_transmit_delay_cmd, - "ospf transmit-delay (1-65535) [A.B.C.D]", - "OSPF interface commands\n" - "Link state transmit delay\n" - "Seconds\n" - "Address of interface\n") -{ - return ip_ospf_transmit_delay(self, vty, argc, argv); -} - -DEFUN (no_ip_ospf_transmit_delay, - no_ip_ospf_transmit_delay_addr_cmd, - "no ip ospf transmit-delay [(1-65535)] [A.B.C.D]", - NO_STR - "IP Information\n" - "OSPF interface commands\n" - "Link state transmit delay\n" - "Seconds\n" - "Address of interface\n") +static int ospf_transmit_delay_unset_apply(struct vty *vty, struct interface *ifp, + const char *ifaddr_str) { - VTY_DECLVAR_CONTEXT(interface, ifp); - int idx = 0; - struct in_addr addr; struct ospf_if_params *params; + struct in_addr addr = { .s_addr = 0L }; + char xpath[XPATH_MAXLEN]; - params = IF_DEF_PARAMS(ifp); + if (!ifaddr_str && ospf_per_iface_xpath(xpath, sizeof(xpath), ifp, "/transmit-delay") == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } - if (argv_find(argv, argc, "A.B.C.D", &idx)) { - if (!inet_aton(argv[idx]->arg, &addr)) { + params = IF_DEF_PARAMS(ifp); + if (ifaddr_str) { + if (!inet_aton(ifaddr_str, &addr)) { vty_out(vty, "Please specify interface address by A.B.C.D\n"); return CMD_WARNING_CONFIG_FAILED; } - params = ospf_lookup_if_params(ifp, addr); if (params == NULL) return CMD_SUCCESS; @@ -9063,26 +9419,93 @@ DEFUN (no_ip_ospf_transmit_delay, ospf_free_if_params(ifp, addr); ospf_if_update_params(ifp, addr); } - return CMD_SUCCESS; } +DEFPY_YANG (ip_ospf_transmit_delay, + ip_ospf_transmit_delay_addr_cmd, + "ip ospf transmit-delay (1-65535)$seconds [A.B.C.D]$ifaddr", + "IP Information\n" + "OSPF interface commands\n" + "Link state transmit delay\n" + "Seconds\n" + "Address of interface\n") +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_transmit_delay_set_apply(vty, ifp, seconds, ifaddr_str); +} + +DEFPY_YANG_HIDDEN (ospf_transmit_delay, + ospf_transmit_delay_cmd, + "ospf transmit-delay (1-65535)$seconds [A.B.C.D]$ifaddr", + "OSPF interface commands\n" + "Link state transmit delay\n" + "Seconds\n" + "Address of interface\n") +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_transmit_delay_set_apply(vty, ifp, seconds, ifaddr_str); +} + +DEFPY_YANG (no_ip_ospf_transmit_delay, + no_ip_ospf_transmit_delay_addr_cmd, + "no ip ospf transmit-delay [(1-65535) [A.B.C.D]$ifaddr]", + NO_STR + "IP Information\n" + "OSPF interface commands\n" + "Link state transmit delay\n" + "Seconds\n" + "Address of interface\n") +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_transmit_delay_unset_apply(vty, ifp, ifaddr_str); +} -DEFUN_HIDDEN (no_ospf_transmit_delay, +DEFPY_YANG_HIDDEN (no_ospf_transmit_delay, no_ospf_transmit_delay_cmd, - "no ospf transmit-delay [(1-65535) [A.B.C.D]]", + "no ospf transmit-delay [(1-65535) [A.B.C.D]$ifaddr]", NO_STR "OSPF interface commands\n" "Link state transmit delay\n" "Seconds\n" "Address of interface\n") { - return no_ip_ospf_transmit_delay(self, vty, argc, argv); + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_transmit_delay_unset_apply(vty, ifp, ifaddr_str); +} + +/* + * Build an absolute ietf-ospf areas/area/interfaces/interface list-entry + * xpath for (ospf instance, area-id, ifname). Used by the + * `ip ospf area` / `ipv6 ospf6 area` CLI conversions to issue NB_OP_CREATE + * and NB_OP_DESTROY against the same xpath the per-interface leaf + * callbacks already key off. + */ +static int ospf_iface_area_xpath(char *xpath, size_t size, const struct ospf *ospf, + const char *area_id_str, const char *ifname) +{ + char instance_name[XPATH_MAXLEN]; + int ret; + + if (!ospf || !area_id_str || !ifname) + return -1; + ret = snprintf(xpath, size, + OSPFD_IETF_ROUTING_PROTOCOL_XPATH + "/ietf-ospf:ospf/areas/area[area-id='%s']/interfaces/interface[name='%s']", + ospfd_ietf_ospf_instance_name(ospf, instance_name, sizeof(instance_name)), + area_id_str, ifname); + if (ret < 0 || (size_t)ret >= size) + return -1; + return 0; } -DEFUN (ip_ospf_area, +DEFPY_YANG (ip_ospf_area, ip_ospf_area_cmd, - "ip ospf [(1-65535)] area [A.B.C.D]", + "ip ospf [(1-65535)$instance_id] area $areaid [A.B.C.D$ifaddr]", "IP Information\n" "OSPF interface commands\n" "Instance ID\n" @@ -9098,21 +9521,40 @@ DEFUN (ip_ospf_area, struct in_addr addr; struct ospf_if_params *params = NULL; struct route_node *rn; - struct ospf *ospf = NULL; - unsigned short instance = 0; - char *areaid; + struct ospf *ospf_inst = NULL; + unsigned short instance = (instance_id > 0) ? (unsigned short)instance_id : 0; uint32_t count = 0; + char xpath[XPATH_MAXLEN]; - if (argv_find(argv, argc, "(1-65535)", &idx)) - instance = strtol(argv[idx]->arg, NULL, 10); - - argv_find(argv, argc, "area", &idx); - areaid = argv[idx + 1]->arg; + (void)argv_find(argv, argc, "area", &idx); if (!instance) - ospf = ifp->vrf->info; + ospf_inst = ifp->vrf->info; else - ospf = ospf_lookup_instance(instance); + ospf_inst = ospf_lookup_instance(instance); + + /* + * Route the common case (default instance, no per-address override, + * no conflicting `network` statements) through the ietf-ospf YANG + * areas/area/interfaces/interface list-create callback. Per-address + * overrides and non-default instances stay on the legacy direct- + * mutation path: RFC 9129's interface list is per-interface, so + * those FRR-specific surfaces have no YANG counterpart. + */ + if (!instance && !ifaddr_str && ospf_inst) { + bool has_network = false; + + for (rn = route_top(ospf_inst->networks); rn; rn = route_next(rn)) + if (rn->info) { + has_network = true; + break; + } + if (!has_network && ospf_iface_area_xpath(xpath, sizeof(xpath), ospf_inst, areaid, + ifp->name) == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_CREATE, NULL); + return nb_cli_apply_changes(vty, NULL); + } + } if (instance && instance != ospf_instance) { /* @@ -9141,9 +9583,9 @@ DEFUN (ip_ospf_area, } if (count > 0) { - ospf = ifp->vrf->info; - if (ospf) - ospf_interface_area_unset(ospf, ifp); + ospf_inst = ifp->vrf->info; + if (ospf_inst) + ospf_interface_area_unset(ospf_inst, ifp); } return CMD_NOT_MY_INSTANCE; @@ -9159,8 +9601,8 @@ DEFUN (ip_ospf_area, return CMD_WARNING_CONFIG_FAILED; } - if (ospf) { - for (rn = route_top(ospf->networks); rn; rn = route_next(rn)) { + if (ospf_inst) { + for (rn = route_top(ospf_inst->networks); rn; rn = route_next(rn)) { if (rn->info != NULL) { vty_out(vty, "Please remove all network commands first.\n"); @@ -9177,14 +9619,13 @@ DEFUN (ip_ospf_area, return CMD_WARNING_CONFIG_FAILED; } - // Check if we have an address arg and process it - if (argc == idx + 3) { - if (!inet_aton(argv[idx + 2]->arg, &addr)) { + /* Per-address override [A.B.C.D] tail. */ + if (ifaddr_str) { + if (!inet_aton(ifaddr_str, &addr)) { vty_out(vty, "Please specify Intf Address by A.B.C.D\n"); return CMD_WARNING_CONFIG_FAILED; } - // update/create address-level params params = ospf_get_if_params((ifp), (addr)); if (OSPF_IF_PARAM_CONFIGURED(params, if_area)) { if (!IPV4_ADDR_SAME(¶ms->if_area, &area_id)) { @@ -9204,15 +9645,15 @@ DEFUN (ip_ospf_area, params->if_area_id_fmt = format; } - if (ospf) - ospf_interface_area_set(ospf, ifp); + if (ospf_inst) + ospf_interface_area_set(ospf_inst, ifp); return CMD_SUCCESS; } -DEFUN (no_ip_ospf_area, +DEFPY_YANG (no_ip_ospf_area, no_ip_ospf_area_cmd, - "no ip ospf [(1-65535)] area [ [A.B.C.D]]", + "no ip ospf [(1-65535)$instance_id] area [$areaid [A.B.C.D$ifaddr]]", NO_STR "IP Information\n" "OSPF interface commands\n" @@ -9223,29 +9664,43 @@ DEFUN (no_ip_ospf_area, "Address of interface\n") { VTY_DECLVAR_CONTEXT(interface, ifp); - int idx = 0; - struct ospf *ospf; + struct ospf *ospf_inst; struct ospf_if_params *params; - unsigned short instance = 0; + unsigned short instance = (instance_id > 0) ? (unsigned short)instance_id : 0; struct in_addr addr; struct in_addr area_id; - - if (argv_find(argv, argc, "(1-65535)", &idx)) - instance = strtol(argv[idx]->arg, NULL, 10); + char xpath[XPATH_MAXLEN]; + char area_id_str[INET_ADDRSTRLEN]; if (!instance) - ospf = ifp->vrf->info; + ospf_inst = ifp->vrf->info; else - ospf = ospf_lookup_instance(instance); + ospf_inst = ospf_lookup_instance(instance); if (instance && instance != ospf_instance) return CMD_NOT_MY_INSTANCE; - argv_find(argv, argc, "area", &idx); + /* + * Route the common case through the ietf-ospf YANG list-destroy + * callback. Per-address overrides stay on the legacy path because + * RFC 9129 has no representation for them. + */ + if (!instance && !ifaddr_str && ospf_inst) { + params = IF_DEF_PARAMS(ifp); + if (!OSPF_IF_PARAM_CONFIGURED(params, if_area)) { + vty_out(vty, "Can't find specified interface area configuration.\n"); + return CMD_WARNING_CONFIG_FAILED; + } + inet_ntop(AF_INET, ¶ms->if_area, area_id_str, sizeof(area_id_str)); + if (ospf_iface_area_xpath(xpath, sizeof(xpath), ospf_inst, area_id_str, + ifp->name) == 0) { + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } + } - // Check if we have an address arg and process it - if (argc == idx + 3) { - if (!inet_aton(argv[idx + 2]->arg, &addr)) { + if (ifaddr_str) { + if (!inet_aton(ifaddr_str, &addr)) { vty_out(vty, "Please specify Intf Address by A.B.C.D\n"); return CMD_WARNING_CONFIG_FAILED; @@ -9269,49 +9724,67 @@ DEFUN (no_ip_ospf_area, ospf_if_update_params((ifp), (addr)); } - if (ospf) { - ospf_interface_area_unset(ospf, ifp); - ospf_area_check_free(ospf, area_id); + if (ospf_inst) { + ospf_interface_area_unset(ospf_inst, ifp); + ospf_area_check_free(ospf_inst, area_id); } return CMD_SUCCESS; } -DEFUN (ip_ospf_passive, - ip_ospf_passive_cmd, - "ip ospf passive [A.B.C.D]", - "IP Information\n" - "OSPF interface commands\n" - "Suppress routing updates on an interface\n" - "Address of interface\n") +static int ospf_iface_passive_apply(struct vty *vty, struct interface *ifp, uint8_t newval, + const char *ifaddr_str) { - VTY_DECLVAR_CONTEXT(interface, ifp); - int idx_ipv4 = 3; - struct in_addr addr = {.s_addr = INADDR_ANY}; struct ospf_if_params *params; - int ret; + struct in_addr addr = { .s_addr = INADDR_ANY }; + char xpath[XPATH_MAXLEN]; - if (argc == 4) { - ret = inet_aton(argv[idx_ipv4]->arg, &addr); - if (!ret) { + if (!ifaddr_str && ospf_per_iface_xpath(xpath, sizeof(xpath), ifp, "/passive") == 0) { + if (newval == OSPF_IF_PASSIVE) + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "true"); + else + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } + + if (ifaddr_str) { + if (!inet_aton(ifaddr_str, &addr)) { vty_out(vty, "Please specify interface address by A.B.C.D\n"); return CMD_WARNING_CONFIG_FAILED; } - params = ospf_get_if_params(ifp, addr); - ospf_if_update_params(ifp, addr); + if (newval == OSPF_IF_PASSIVE) { + params = ospf_get_if_params(ifp, addr); + ospf_if_update_params(ifp, addr); + } else { + params = ospf_lookup_if_params(ifp, addr); + if (params == NULL) + return CMD_SUCCESS; + } } else { params = IF_DEF_PARAMS(ifp); } - ospf_passive_interface_update(ifp, params, addr, OSPF_IF_PASSIVE); - + ospf_passive_interface_update(ifp, params, addr, newval); return CMD_SUCCESS; } -DEFUN (no_ip_ospf_passive, +DEFPY_YANG (ip_ospf_passive, + ip_ospf_passive_cmd, + "ip ospf passive [A.B.C.D]$ifaddr", + "IP Information\n" + "OSPF interface commands\n" + "Suppress routing updates on an interface\n" + "Address of interface\n") +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_iface_passive_apply(vty, ifp, OSPF_IF_PASSIVE, ifaddr_str); +} + +DEFPY_YANG (no_ip_ospf_passive, no_ip_ospf_passive_cmd, - "no ip ospf passive [A.B.C.D]", + "no ip ospf passive [A.B.C.D]$ifaddr", NO_STR "IP Information\n" "OSPF interface commands\n" @@ -9319,28 +9792,8 @@ DEFUN (no_ip_ospf_passive, "Address of interface\n") { VTY_DECLVAR_CONTEXT(interface, ifp); - int idx_ipv4 = 4; - struct in_addr addr = {.s_addr = INADDR_ANY}; - struct ospf_if_params *params; - int ret; - - if (argc == 5) { - ret = inet_aton(argv[idx_ipv4]->arg, &addr); - if (!ret) { - vty_out(vty, - "Please specify interface address by A.B.C.D\n"); - return CMD_WARNING_CONFIG_FAILED; - } - params = ospf_lookup_if_params(ifp, addr); - if (params == NULL) - return CMD_SUCCESS; - } else { - params = IF_DEF_PARAMS(ifp); - } - - ospf_passive_interface_update(ifp, params, addr, OSPF_IF_ACTIVE); - return CMD_SUCCESS; + return ospf_iface_passive_apply(vty, ifp, OSPF_IF_ACTIVE, ifaddr_str); } DEFUN (ospf_redistribute_source, @@ -9757,26 +10210,22 @@ DEFPY (ospf_forwarding_address_self, } -DEFUN (ospf_distance, +DEFPY_YANG (ospf_distance, ospf_distance_cmd, - "distance (1-255)", + "distance (1-255)$distance", "Administrative distance\n" "OSPF Administrative distance\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - int idx_number = 1; - uint8_t distance; - - distance = atoi(argv[idx_number]->arg); - if (ospf->distance_all != distance) { - ospf->distance_all = distance; - ospf_restart_spf(ospf); - } + char xpath[XPATH_MAXLEN]; - return CMD_SUCCESS; + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, "/preference/all") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, distance_str); + return nb_cli_apply_changes(vty, NULL); } -DEFUN (no_ospf_distance, +DEFPY_YANG (no_ospf_distance, no_ospf_distance_cmd, "no distance [(1-255)]", NO_STR @@ -9784,18 +10233,17 @@ DEFUN (no_ospf_distance, "OSPF Administrative distance\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); + char xpath[XPATH_MAXLEN]; - if (ospf->distance_all) { - ospf->distance_all = 0; - ospf_restart_spf(ospf); - } - - return CMD_SUCCESS; + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, "/preference/all") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); } -DEFUN (no_ospf_distance_ospf, +DEFPY_YANG (no_ospf_distance_ospf, no_ospf_distance_ospf_cmd, - "no distance ospf [{intra-area [(1-255)]|inter-area [(1-255)]|external [(1-255)]}]", + "no distance ospf [{intra-area$intra [(1-255)]|inter-area$inter [(1-255)]|external$external [(1-255)]}]", NO_STR "Administrative distance\n" "OSPF administrative distance\n" @@ -9807,21 +10255,33 @@ DEFUN (no_ospf_distance_ospf, "Distance for external routes\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - int idx = 0; + char xpath[XPATH_MAXLEN]; + bool all_scopes = (!intra && !inter && !external); - if (argv_find(argv, argc, "intra-area", &idx) || argc == 3) - idx = ospf->distance_intra = 0; - if (argv_find(argv, argc, "inter-area", &idx) || argc == 3) - idx = ospf->distance_inter = 0; - if (argv_find(argv, argc, "external", &idx) || argc == 3) - ospf->distance_external = 0; - - return CMD_SUCCESS; + if (intra || all_scopes) { + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, + "/preference/intra-area") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + } + if (inter || all_scopes) { + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, + "/preference/inter-area") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + } + if (external || all_scopes) { + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, "/preference/external") != + 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + } + return nb_cli_apply_changes(vty, NULL); } -DEFUN (ospf_distance_ospf, +DEFPY_YANG (ospf_distance_ospf, ospf_distance_ospf_cmd, - "distance ospf {intra-area (1-255)|inter-area (1-255)|external (1-255)}", + "distance ospf {intra-area (1-255)$intra|inter-area (1-255)$inter|external (1-255)$external}", "Administrative distance\n" "OSPF administrative distance\n" "Intra-area routes\n" @@ -9832,43 +10292,54 @@ DEFUN (ospf_distance_ospf, "Distance for external routes\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - int idx = 0; + char xpath[XPATH_MAXLEN]; - ospf->distance_intra = 0; - ospf->distance_inter = 0; - ospf->distance_external = 0; + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, "/preference/intra-area") != 0) + return CMD_WARNING_CONFIG_FAILED; + if (intra) + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, intra_str); + else + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); - if (argv_find(argv, argc, "intra-area", &idx)) - ospf->distance_intra = atoi(argv[idx + 1]->arg); - idx = 0; - if (argv_find(argv, argc, "inter-area", &idx)) - ospf->distance_inter = atoi(argv[idx + 1]->arg); - idx = 0; - if (argv_find(argv, argc, "external", &idx)) - ospf->distance_external = atoi(argv[idx + 1]->arg); + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, "/preference/inter-area") != 0) + return CMD_WARNING_CONFIG_FAILED; + if (inter) + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, inter_str); + else + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); - return CMD_SUCCESS; + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, "/preference/external") != 0) + return CMD_WARNING_CONFIG_FAILED; + if (external) + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, external_str); + else + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + + return nb_cli_apply_changes(vty, NULL); } -DEFUN (ip_ospf_mtu_ignore, - ip_ospf_mtu_ignore_addr_cmd, - "ip ospf mtu-ignore [A.B.C.D]", - "IP Information\n" - "OSPF interface commands\n" - "Disable MTU mismatch detection on this interface\n" - "Address of interface\n") +/* Shared body for both "ip ospf mtu-ignore" and "no ip ospf mtu-ignore": + * the legacy DEFUNs are symmetric except for which value they write + * to params->mtu_ignore. + */ +static int ospf_mtu_ignore_apply(struct vty *vty, struct interface *ifp, uint8_t new_value, + const char *ifaddr_str) { - VTY_DECLVAR_CONTEXT(interface, ifp); - int idx_ipv4 = 3; - struct in_addr addr; - int ret; - struct ospf_if_params *params; - params = IF_DEF_PARAMS(ifp); + struct in_addr addr = { 0 }; + char xpath[XPATH_MAXLEN]; - if (argc == 4) { - ret = inet_aton(argv[idx_ipv4]->arg, &addr); - if (!ret) { + if (!ifaddr_str && ospf_per_iface_xpath(xpath, sizeof(xpath), ifp, "/mtu-ignore") == 0) { + if (new_value) + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "true"); + else + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); + } + + params = IF_DEF_PARAMS(ifp); + if (ifaddr_str) { + if (!inet_aton(ifaddr_str, &addr)) { vty_out(vty, "Please specify interface address by A.B.C.D\n"); return CMD_WARNING_CONFIG_FAILED; @@ -9876,7 +10347,7 @@ DEFUN (ip_ospf_mtu_ignore, params = ospf_get_if_params(ifp, addr); ospf_if_update_params(ifp, addr); } - params->mtu_ignore = 1; + params->mtu_ignore = new_value; if (params->mtu_ignore != OSPF_MTU_IGNORE_DEFAULT) SET_IF_PARAM(params, mtu_ignore); else { @@ -9889,9 +10360,22 @@ DEFUN (ip_ospf_mtu_ignore, return CMD_SUCCESS; } -DEFUN (no_ip_ospf_mtu_ignore, +DEFPY_YANG (ip_ospf_mtu_ignore, + ip_ospf_mtu_ignore_addr_cmd, + "ip ospf mtu-ignore [A.B.C.D]$ifaddr", + "IP Information\n" + "OSPF interface commands\n" + "Disable MTU mismatch detection on this interface\n" + "Address of interface\n") +{ + VTY_DECLVAR_CONTEXT(interface, ifp); + + return ospf_mtu_ignore_apply(vty, ifp, 1, ifaddr_str); +} + +DEFPY_YANG (no_ip_ospf_mtu_ignore, no_ip_ospf_mtu_ignore_addr_cmd, - "no ip ospf mtu-ignore [A.B.C.D]", + "no ip ospf mtu-ignore [A.B.C.D]$ifaddr", NO_STR "IP Information\n" "OSPF interface commands\n" @@ -9899,34 +10383,8 @@ DEFUN (no_ip_ospf_mtu_ignore, "Address of interface\n") { VTY_DECLVAR_CONTEXT(interface, ifp); - int idx_ipv4 = 4; - struct in_addr addr; - int ret; - - struct ospf_if_params *params; - params = IF_DEF_PARAMS(ifp); - if (argc == 5) { - ret = inet_aton(argv[idx_ipv4]->arg, &addr); - if (!ret) { - vty_out(vty, - "Please specify interface address by A.B.C.D\n"); - return CMD_WARNING_CONFIG_FAILED; - } - params = ospf_get_if_params(ifp, addr); - ospf_if_update_params(ifp, addr); - } - params->mtu_ignore = 0; - if (params->mtu_ignore != OSPF_MTU_IGNORE_DEFAULT) - SET_IF_PARAM(params, mtu_ignore); - else { - UNSET_IF_PARAM(params, mtu_ignore); - if (params != IF_DEF_PARAMS(ifp)) { - ospf_free_if_params(ifp, addr); - ospf_if_update_params(ifp, addr); - } - } - return CMD_SUCCESS; + return ospf_mtu_ignore_apply(vty, ifp, 0, ifaddr_str); } DEFPY(ip_ospf_capability_opaque, ip_ospf_capability_opaque_addr_cmd, @@ -9985,7 +10443,17 @@ DEFPY(ip_ospf_capability_opaque, ip_ospf_capability_opaque_addr_cmd, } -DEFPY(ip_ospf_prefix_suppression, ip_ospf_prefix_suppression_addr_cmd, +/* + * `[no] ip ospf prefix-suppression [A.B.C.D]` maps onto the RFC 9129 + * per-interface boolean `/areas/area/interfaces/interface/prefix-` + * `suppression`. The whole-interface form (no per-address override + * and the interface already in an area) routes through the YANG + * callback; per-address overrides stay on the legacy direct-mutation + * path because RFC 9129's per-interface list cannot express them, and + * not-yet-attached interfaces fall back too because the YANG xpath is + * gated on `OSPF_IF_PARAM_CONFIGURED(if_area)`. + */ +DEFPY_YANG(ip_ospf_prefix_suppression, ip_ospf_prefix_suppression_addr_cmd, "[no] ip ospf prefix-suppression [A.B.C.D]$ip_addr", NO_STR "IP Information\n" "OSPF interface commands\n" @@ -9996,6 +10464,16 @@ DEFPY(ip_ospf_prefix_suppression, ip_ospf_prefix_suppression_addr_cmd, struct route_node *rn; bool prefix_suppression_change; struct ospf_if_params *params; + char xpath[XPATH_MAXLEN]; + + if (!ip_addr_str && + ospf_per_iface_xpath(xpath, sizeof(xpath), ifp, "/prefix-suppression") == 0) { + if (no) + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + else + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "true"); + return nb_cli_apply_changes(vty, NULL); + } params = IF_DEF_PARAMS(ifp); @@ -10087,7 +10565,15 @@ DEFPY(ip_ospf_neighbor_filter, ip_ospf_neighbor_filter_addr_cmd, return CMD_SUCCESS; } -DEFUN (ospf_max_metric_router_lsa_admin, +/* + * `max-metric router-lsa administrative` maps onto the RFC 9129 + * presence container `/stub-router/always` (RFC 6987 unconditional + * stub router; the intermediate `choice trigger` skips the data + * path per RFC 7950). Create -> NB_OP_CREATE on the container; + * remove -> NB_OP_DESTROY. The callback owns the per-area flag + * flip and the LSA reorigination. + */ +DEFPY_YANG (ospf_max_metric_router_lsa_admin, ospf_max_metric_router_lsa_admin_cmd, "max-metric router-lsa administrative", "OSPF maximum / infinite-distance metric\n" @@ -10095,24 +10581,15 @@ DEFUN (ospf_max_metric_router_lsa_admin, "Administratively applied, for an indefinite period\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - struct listnode *ln; - struct ospf_area *area; - - for (ALL_LIST_ELEMENTS_RO(ospf->areas, ln, area)) { - SET_FLAG(area->stub_router_state, OSPF_AREA_ADMIN_STUB_ROUTED); - - if (!CHECK_FLAG(area->stub_router_state, - OSPF_AREA_IS_STUB_ROUTED)) - ospf_router_lsa_update_area(area); - } - - /* Allows for areas configured later to get the property */ - ospf->stub_router_admin_set = OSPF_STUB_ROUTER_ADMINISTRATIVE_SET; + char xpath[XPATH_MAXLEN]; - return CMD_SUCCESS; + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, "/stub-router/always") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_CREATE, NULL); + return nb_cli_apply_changes(vty, NULL); } -DEFUN (no_ospf_max_metric_router_lsa_admin, +DEFPY_YANG (no_ospf_max_metric_router_lsa_admin, no_ospf_max_metric_router_lsa_admin_cmd, "no max-metric router-lsa administrative", NO_STR @@ -10121,24 +10598,12 @@ DEFUN (no_ospf_max_metric_router_lsa_admin, "Administratively applied, for an indefinite period\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); - struct listnode *ln; - struct ospf_area *area; + char xpath[XPATH_MAXLEN]; - for (ALL_LIST_ELEMENTS_RO(ospf->areas, ln, area)) { - UNSET_FLAG(area->stub_router_state, - OSPF_AREA_ADMIN_STUB_ROUTED); - - /* Don't trample on the start-up stub timer */ - if (CHECK_FLAG(area->stub_router_state, - OSPF_AREA_IS_STUB_ROUTED) - && !area->t_stub_router) { - UNSET_FLAG(area->stub_router_state, - OSPF_AREA_IS_STUB_ROUTED); - ospf_router_lsa_update_area(area); - } - } - ospf->stub_router_admin_set = OSPF_STUB_ROUTER_ADMINISTRATIVE_UNSET; - return CMD_SUCCESS; + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, "/stub-router/always") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); } DEFUN (ospf_max_metric_router_lsa_startup, @@ -10263,7 +10728,15 @@ DEFUN (no_ospf_proactive_arp, } /* Graceful Restart HELPER Commands */ -DEFPY(ospf_gr_helper_enable, ospf_gr_helper_enable_cmd, +/* + * `graceful-restart helper enable` maps onto RFC 9129 + * `/graceful-restart/helper-enabled`. The per-router-id form + * (`graceful-restart helper enable A.B.C.D`) has no RFC counterpart; + * the YANG model has no enable-list, so the per-router-id case + * stays on the legacy direct mutation path and the bare form alone + * routes through the northbound. + */ +DEFPY_YANG(ospf_gr_helper_enable, ospf_gr_helper_enable_cmd, "graceful-restart helper enable [A.B.C.D$address]", "OSPF Graceful Restart\n" "OSPF GR Helper\n" @@ -10271,6 +10744,7 @@ DEFPY(ospf_gr_helper_enable, ospf_gr_helper_enable_cmd, "Advertising Router-ID\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); + char xpath[XPATH_MAXLEN]; if (address_str) { ospf_gr_helper_support_set_per_routerid(ospf, &address, @@ -10278,12 +10752,14 @@ DEFPY(ospf_gr_helper_enable, ospf_gr_helper_enable_cmd, return CMD_SUCCESS; } - ospf_gr_helper_support_set(ospf, OSPF_GR_TRUE); - - return CMD_SUCCESS; + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, + "/graceful-restart/helper-enabled") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "true"); + return nb_cli_apply_changes(vty, NULL); } -DEFPY(no_ospf_gr_helper_enable, +DEFPY_YANG(no_ospf_gr_helper_enable, no_ospf_gr_helper_enable_cmd, "no graceful-restart helper enable [A.B.C.D$address]", NO_STR @@ -10293,6 +10769,7 @@ DEFPY(no_ospf_gr_helper_enable, "Advertising Router-ID\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); + char xpath[XPATH_MAXLEN]; if (address_str) { ospf_gr_helper_support_set_per_routerid(ospf, &address, @@ -10300,11 +10777,14 @@ DEFPY(no_ospf_gr_helper_enable, return CMD_SUCCESS; } - ospf_gr_helper_support_set(ospf, OSPF_GR_FALSE); - return CMD_SUCCESS; + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, + "/graceful-restart/helper-enabled") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_DESTROY, NULL); + return nb_cli_apply_changes(vty, NULL); } -DEFPY(ospf_gr_helper_enable_lsacheck, +DEFPY_YANG(ospf_gr_helper_enable_lsacheck, ospf_gr_helper_enable_lsacheck_cmd, "graceful-restart helper strict-lsa-checking", "OSPF Graceful Restart\n" @@ -10312,12 +10792,16 @@ DEFPY(ospf_gr_helper_enable_lsacheck, "Enable strict LSA check\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); + char xpath[XPATH_MAXLEN]; - ospf_gr_helper_lsa_check_set(ospf, OSPF_GR_TRUE); - return CMD_SUCCESS; + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, + "/graceful-restart/helper-strict-lsa-checking") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "true"); + return nb_cli_apply_changes(vty, NULL); } -DEFPY(no_ospf_gr_helper_enable_lsacheck, +DEFPY_YANG(no_ospf_gr_helper_enable_lsacheck, no_ospf_gr_helper_enable_lsacheck_cmd, "no graceful-restart helper strict-lsa-checking", NO_STR @@ -10326,9 +10810,13 @@ DEFPY(no_ospf_gr_helper_enable_lsacheck, "Disable strict LSA check\n") { VTY_DECLVAR_INSTANCE_CONTEXT(ospf, ospf); + char xpath[XPATH_MAXLEN]; - ospf_gr_helper_lsa_check_set(ospf, OSPF_GR_FALSE); - return CMD_SUCCESS; + if (ospf_per_instance_xpath(xpath, sizeof(xpath), ospf, + "/graceful-restart/helper-strict-lsa-checking") != 0) + return CMD_WARNING_CONFIG_FAILED; + nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, "false"); + return nb_cli_apply_changes(vty, NULL); } DEFPY(ospf_gr_helper_supported_grace_time, diff --git a/ospfd/ospf_vty.h b/ospfd/ospf_vty.h index 9ad20a7ae6f7..42ad8c052aae 100644 --- a/ospfd/ospf_vty.h +++ b/ospfd/ospf_vty.h @@ -34,10 +34,18 @@ } /* Prototypes. */ +struct ospf_if_params; + extern void ospf_vty_init(void); extern void ospf_vty_show_init(void); extern void ospf_vty_clear_init(void); extern int str2area_id(const char *str, struct in_addr *area_id, int *area_id_fmt); +extern void ospf_passive_interface_update(struct interface *ifp, struct ospf_if_params *params, + struct in_addr addr, uint8_t newval); +extern int ospf_per_instance_xpath(char *xpath, size_t size, const struct ospf *ospf, + const char *leaf); +extern int ospf_per_iface_xpath(char *xpath, size_t size, const struct interface *ifp, + const char *leaf); /* unit tests */ void show_ip_ospf_database_summary(struct vty *vty, struct ospf *ospf, int self, diff --git a/ospfd/ospfd.c b/ospfd/ospfd.c index d9e9c8ab82c3..48726d7ae406 100644 --- a/ospfd/ospfd.c +++ b/ospfd/ospfd.c @@ -47,6 +47,7 @@ #include "ospfd/ospf_ldp_sync.h" #include "ospfd/ospf_gr.h" #include "ospfd/ospf_apiserver.h" +#include "ospf_nb.h" DEFINE_QOBJ_TYPE(ospf); @@ -399,6 +400,7 @@ struct ospf *ospf_new_alloc(unsigned short instance, const char *name) new->oi_write_q = list_new(); new->write_oi_count = OSPF_WRITE_INTERFACE_COUNT_DEFAULT; + new->gr_info.grace_period = OSPF_DFLT_GRACE_INTERVAL; new->proactive_arp = OSPF_PROACTIVE_ARP_DEFAULT; @@ -1589,7 +1591,7 @@ int ospf_area_shortcut_unset(struct ospf *ospf, struct ospf_area *area) return 1; } -static int ospf_area_vlink_count(struct ospf *ospf, struct ospf_area *area) +int ospf_area_vlink_count(struct ospf *ospf, struct ospf_area *area) { struct ospf_vl_data *vl; struct listnode *node; @@ -1712,7 +1714,10 @@ int ospf_area_nssa_unset(struct ospf *ospf, struct in_addr area_id) area = ospf_area_lookup_by_area_id(ospf, area_id); if (area == NULL) - return 0; + return 1; + + if (area->external_routing != OSPF_AREA_NSSA) + return 1; ospf->anyNSSA--; /* set NSSA area defaults */ @@ -2214,6 +2219,9 @@ void ospf_master_init(struct event_loop *mst) om = &ospf_master; om->ospf = list_new(); om->master = mst; + + /* Hook RFC 9129 ietf-ospf notifications onto the state-change hooks. */ + ospfd_ietf_notif_init(); } /* Link OSPF instance to VRF. */ diff --git a/ospfd/ospfd.h b/ospfd/ospfd.h index 3130b7cc9d29..a23ccb6fdfa9 100644 --- a/ospfd/ospfd.h +++ b/ospfd/ospfd.h @@ -730,6 +730,7 @@ extern int ospf_area_display_format_set(struct ospf *ospf, struct ospf_area *area, int df); extern int ospf_area_stub_set(struct ospf *ospf, struct in_addr area_id); extern int ospf_area_stub_unset(struct ospf *ospf, struct in_addr area_id); +extern int ospf_area_vlink_count(struct ospf *ospf, struct ospf_area *area); extern int ospf_area_no_summary_set(struct ospf *ospf, struct in_addr area_id); extern int ospf_area_no_summary_unset(struct ospf *ospf, struct in_addr area_id); diff --git a/ospfd/subdir.am b/ospfd/subdir.am index 9af250cf6955..f7eaade4b977 100644 --- a/ospfd/subdir.am +++ b/ospfd/subdir.am @@ -39,6 +39,11 @@ ospfd_libfrrospf_a_SOURCES = \ ospfd/ospf_lsdb.c \ ospfd/ospf_memory.c \ ospfd/ospf_neighbor.c \ + ospfd/ospf_nb.c \ + ospfd/ospf_nb_config.c \ + ospfd/ospf_nb_notifications.c \ + ospfd/ospf_nb_rpcs.c \ + ospfd/ospf_nb_state.c \ ospfd/ospf_network.c \ ospfd/ospf_nsm.c \ ospfd/ospf_opaque.c \ @@ -81,6 +86,7 @@ clippy_scan += \ ospfd/ospf_ldp_sync.c \ ospfd/ospf_dump.c \ ospfd/ospf_gr.c \ + ospfd/ospf_te.c \ # end noinst_HEADERS += \ @@ -96,6 +102,7 @@ noinst_HEADERS += \ ospfd/ospf_ldp_sync.h \ ospfd/ospf_memory.h \ ospfd/ospf_neighbor.h \ + ospfd/ospf_nb.h \ ospfd/ospf_network.h \ ospfd/ospf_packet.h \ ospfd/ospf_quicknbr.h \ diff --git a/tests/topotests/ospf6_gr_topo1/rt1/ospf6d.conf b/tests/topotests/ospf6_gr_topo1/rt1/ospf6d.conf index 811fad9e188d..e5770e553e85 100644 --- a/tests/topotests/ospf6_gr_topo1/rt1/ospf6d.conf +++ b/tests/topotests/ospf6_gr_topo1/rt1/ospf6d.conf @@ -10,6 +10,7 @@ log commands ! debug ospf6 flooding ! debug ospf6 graceful-restart ! debug ospf6 spf process +debug northbound notifications ! interface lo ipv6 ospf area 1 diff --git a/tests/topotests/ospf6_gr_topo1/test_ospf6_gr_topo1.py b/tests/topotests/ospf6_gr_topo1/test_ospf6_gr_topo1.py index 12479a105ca4..bdd6d327cb5a 100755 --- a/tests/topotests/ospf6_gr_topo1/test_ospf6_gr_topo1.py +++ b/tests/topotests/ospf6_gr_topo1/test_ospf6_gr_topo1.py @@ -259,6 +259,39 @@ def check_gr_capability(): assert result is True, assertmsg +def _grep_daemon_log(router, daemon, pattern, since_marker=None): + log_path = os.path.join(router.logdir, router.name, "{}.log".format(daemon)) + try: + with open(log_path, "r") as fh: + lines = fh.readlines() + except OSError: + return [] + if since_marker: + for i, line in enumerate(lines): + if since_marker in line: + lines = lines[i:] + break + else: + return [] + return [line for line in lines if pattern in line] + + +def _expect_ospf6_notification(router, xpath, marker): + def saw_notification(): + hits = _grep_daemon_log( + router, "ospf6d", "northbound notification: {}".format(xpath), marker + ) + hits += [ + line + for line in _grep_daemon_log(router, "ospf6d", "OSPF6-NOTIF", marker) + if xpath in line + ] + return None if hits else "no {} notification log".format(xpath) + + _, result = topotest.run_and_expect(saw_notification, None, count=60, wait=0.5) + assert result is None, result + + # # Test initial network convergence # @@ -284,13 +317,28 @@ def test_gr_rt1(): if tgen.routers_have_failure(): pytest.skip(tgen.errors) + rt1 = tgen.gears["rt1"] + rt2 = tgen.gears["rt2"] + marker = "=== test_gr_rt1_yang_notifications BEGIN ===" + rt1.vtysh_cmd("send log level info {}".format(marker)) + rt2.vtysh_cmd("send log level info {}".format(marker)) + rt1.vtysh_cmd("configure terminal\ndebug northbound notifications\n") + rt2.vtysh_cmd("configure terminal\ndebug northbound notifications\n") + tgen.net["rt1"].cmd('vtysh -c "graceful-restart prepare ipv6 ospf"') expect_grace_lsa(restarting="1.1.1.1", helper="rt2") + _expect_ospf6_notification( + rt2, "/ietf-ospf:nbr-restart-helper-status-change", marker + ) ensure_gr_is_in_zebra("rt1") kill_router_daemons(tgen, "rt1", ["ospf6d"], save_config=False) check_routers(exiting="rt1") start_router_daemons(tgen, "rt1", ["ospf6d"]) check_routers(restarting="rt1") + _expect_ospf6_notification(rt1, "/ietf-ospf:restart-status-change", marker) + _expect_ospf6_notification( + rt2, "/ietf-ospf:nbr-restart-helper-status-change", marker + ) # diff --git a/tests/topotests/ospf_gr_topo1/rt1/ospfd.conf b/tests/topotests/ospf_gr_topo1/rt1/ospfd.conf index 57030f140188..b8e20aa45ed2 100644 --- a/tests/topotests/ospf_gr_topo1/rt1/ospfd.conf +++ b/tests/topotests/ospf_gr_topo1/rt1/ospfd.conf @@ -13,6 +13,7 @@ log commands ! debug ospf nsm ! debug ospf nssa ! debug ospf graceful-restart +debug northbound notifications ! interface lo ip ospf area 1 diff --git a/tests/topotests/ospf_gr_topo1/test_ospf_gr_topo1.py b/tests/topotests/ospf_gr_topo1/test_ospf_gr_topo1.py index 73b660e59031..41f4e0bd295e 100755 --- a/tests/topotests/ospf_gr_topo1/test_ospf_gr_topo1.py +++ b/tests/topotests/ospf_gr_topo1/test_ospf_gr_topo1.py @@ -202,6 +202,39 @@ def expect_grace_lsa(restarting, area, helper): assert result is None, assertmsg +def _grep_daemon_log(router, daemon, pattern, since_marker=None): + log_path = os.path.join(router.logdir, router.name, "{}.log".format(daemon)) + try: + with open(log_path, "r") as fh: + lines = fh.readlines() + except OSError: + return [] + if since_marker: + for i, line in enumerate(lines): + if since_marker in line: + lines = lines[i:] + break + else: + return [] + return [line for line in lines if pattern in line] + + +def _expect_ospf_notification(router, xpath, marker): + def saw_notification(): + hits = _grep_daemon_log( + router, "ospfd", "northbound notification: {}".format(xpath), marker + ) + hits += [ + line + for line in _grep_daemon_log(router, "ospfd", "OSPF-NOTIF", marker) + if xpath in line + ] + return None if hits else "no {} notification log".format(xpath) + + _, result = topotest.run_and_expect(saw_notification, None, count=60, wait=0.5) + assert result is None, result + + def check_routers(initial_convergence=False, exiting=None, restarting=None): for rname in ["rt1", "rt2", "rt3", "rt4", "rt5", "rt6", "rt7"]: # Check the RIB first, which should be preserved across restarts in @@ -293,14 +326,29 @@ def test_gr_rt1(): if tgen.routers_have_failure(): pytest.skip(tgen.errors) + rt1 = tgen.gears["rt1"] + rt2 = tgen.gears["rt2"] + marker = "=== test_gr_rt1_yang_notifications BEGIN ===" + rt1.vtysh_cmd("send log level info {}".format(marker)) + rt2.vtysh_cmd("send log level info {}".format(marker)) + rt1.vtysh_cmd("configure terminal\ndebug northbound notifications\n") + rt2.vtysh_cmd("configure terminal\ndebug northbound notifications\n") + tgen.net["rt1"].cmd('vtysh -c "graceful-restart prepare ip ospf"') expect_grace_lsa(restarting="1.1.1.1", area="0.0.0.1", helper="rt2") + _expect_ospf_notification( + rt2, "/ietf-ospf:nbr-restart-helper-status-change", marker + ) ensure_gr_is_in_zebra("rt1") kill_router_daemons(tgen, "rt1", ["ospfd"], save_config=False) check_routers(exiting="rt1") start_router_daemons(tgen, "rt1", ["ospfd"]) check_routers(restarting="rt1") + _expect_ospf_notification(rt1, "/ietf-ospf:restart-status-change", marker) + _expect_ospf_notification( + rt2, "/ietf-ospf:nbr-restart-helper-status-change", marker + ) # diff --git a/tests/topotests/ospf_multi_instance/test_ospf_multi_instance.py b/tests/topotests/ospf_multi_instance/test_ospf_multi_instance.py index e8b3e00f1c87..56f88989fc09 100644 --- a/tests/topotests/ospf_multi_instance/test_ospf_multi_instance.py +++ b/tests/topotests/ospf_multi_instance/test_ospf_multi_instance.py @@ -8,6 +8,7 @@ # Acee Lindem # +import json import os import sys from functools import partial @@ -51,6 +52,43 @@ pytestmark = [pytest.mark.ospfd, pytest.mark.bgpd] +def _as_list(value): + if isinstance(value, list): + return value + return [value] + + +def _yang_ospf_instance_protocol(router, name): + xpath = ( + "/ietf-routing:routing/control-plane-protocols/" + 'control-plane-protocol[type="ietf-ospf:ospfv2"][name="{}"]' + ).format(name) + + try: + output = json.loads( + router.vtysh_cmd( + "show mgmt get-data {} datastore operational".format(xpath) + ) + ) + protocols = output["ietf-routing:routing"]["control-plane-protocols"][ + "control-plane-protocol" + ] + except (KeyError, json.JSONDecodeError) as error: + return "OSPFv2 YANG instance {} is not ready: {}".format(name, error) + + protocols = _as_list(protocols) + if len(protocols) != 1: + return "OSPFv2 YANG instance {} returned {} entries".format( + name, len(protocols) + ) + + protocol = protocols[0] + if protocol.get("type") != "ietf-ospf:ospfv2" or protocol.get("name") != name: + return "OSPFv2 YANG instance {} returned {}".format(name, protocol) + + return None + + def build_topo(tgen): "Build function" @@ -107,6 +145,26 @@ def test_multi_instance_default_origination(): if tgen.routers_have_failure(): pytest.skip("Skipped because of router(s) failure") + r2 = tgen.gears["r2"] + + backend_adapters = r2.vtysh_cmd("show mgmt backend-adapter all") + assert "Client: \t\t\tospfd-1" in backend_adapters + assert "Client: \t\t\tospfd-2" in backend_adapters + assert "Client: \t\t\tospfd\n" not in backend_adapters + + xpath_registry = r2.vtysh_cmd("show mgmt backend-yang-xpath-registry oper") + assert "control-plane-protocol[type='ietf-ospf:ospfv2'][name='1']" in xpath_registry + assert "control-plane-protocol[type='ietf-ospf:ospfv2'][name='2']" in xpath_registry + + for instance_name in ("1", "2"): + _, result = topotest.run_and_expect( + partial(_yang_ospf_instance_protocol, r2, instance_name), + None, + count=30, + wait=1, + ) + assert result is None, result + step("Configure a local default route") r1 = tgen.gears["r1"] r1.vtysh_cmd("conf t\nip route 0.0.0.0/0 Null0") @@ -170,7 +228,6 @@ def check_default_route_config(): topotest.router_json_cmp, r1, "show ip ospf database json", input_dict ) - r2 = tgen.gears["r2"] step("Verify the OSPF instance 1 installation of default route on router 2") input_dict = { "0.0.0.0/0": [ diff --git a/tests/topotests/ospf_topo1/test_ospf_topo1.py b/tests/topotests/ospf_topo1/test_ospf_topo1.py index 763a25e28fd4..bb3831c775c5 100644 --- a/tests/topotests/ospf_topo1/test_ospf_topo1.py +++ b/tests/topotests/ospf_topo1/test_ospf_topo1.py @@ -13,9 +13,11 @@ test_ospf_topo1.py: Test the FRR OSPF routing daemon. """ +import json import os import re import sys +import time from functools import partial import pytest @@ -98,86 +100,148 @@ def teardown_module(): tgen.stop_topology() -def test_wait_protocol_convergence(): - "Wait for OSPFv2/OSPFv3 to converge" +def _expect_ospfv2_neighbor_full(router, neighbor): + "Wait until OSPFv2 neighbor reaches Full." tgen = get_topogen() - if tgen.routers_have_failure(): - pytest.skip(tgen.errors) + logger.info("waiting OSPFv2 router '{}' for neighbor {}".format(router, neighbor)) - logger.info("waiting for protocols to converge") + def run_command_and_expect(): + result = tgen.gears[router].vtysh_cmd("show ip ospf neighbor json", isjson=True) + return topotest.json_cmp( + result, {"neighbors": {neighbor: [{"converged": "Full"}]}} + ) - def expect_ospfv2_neighbor_full(router, neighbor): - "Wait until OSPFv2 convergence." - logger.info("waiting OSPFv2 router '{}'".format(router)) - - def run_command_and_expect(): - """ - Function that runs command and expect the following outcomes: - * Full/DR - * Full/DROther - * Full/Backup - """ - result = tgen.gears[router].vtysh_cmd( - "show ip ospf neighbor json", isjson=True - ) - if ( - topotest.json_cmp( - result, {"neighbors": {neighbor: [{"converged": "Full"}]}} - ) - is None - ): - return None + _, result = topotest.run_and_expect(run_command_and_expect, None, count=130, wait=1) + assert result is None, '"{}" convergence failure for {}'.format(router, neighbor) - if ( - topotest.json_cmp( - result, {"neighbors": {neighbor: [{"converged": "Full"}]}} - ) - is None - ): - return None - return topotest.json_cmp( - result, {"neighbors": {neighbor: [{"converged": "Full"}]}} - ) +def _expect_ospfv3_neighbor_full(router, neighbor): + "Wait until OSPFv3 neighbor reaches Full." + tgen = get_topogen() + logger.info("waiting OSPFv3 router '{}' for neighbor {}".format(router, neighbor)) + test_func = partial( + topotest.router_json_cmp, + tgen.gears[router], + "show ipv6 ospf6 neighbor json", + {"neighbors": [{"neighborId": neighbor, "state": "Full"}]}, + ) + _, result = topotest.run_and_expect(test_func, None, count=130, wait=1) + assert result is None, '"{}" convergence failure for {}'.format(router, neighbor) - _, result = topotest.run_and_expect( - run_command_and_expect, None, count=130, wait=1 - ) - assertmsg = '"{}" convergence failure'.format(router) - assert result is None, assertmsg - def expect_ospfv3_neighbor_full(router, neighbor): - "Wait until OSPFv3 convergence." - logger.info("waiting OSPFv3 router '{}'".format(router)) - test_func = partial( - topotest.router_json_cmp, - tgen.gears[router], - "show ipv6 ospf6 neighbor json", - {"neighbors": [{"neighborId": neighbor, "state": "Full"}]}, - ) - _, result = topotest.run_and_expect(test_func, None, count=130, wait=1) - assertmsg = '"{}" convergence failure'.format(router) - assert result is None, assertmsg +def _force_ospf_reconvergence_to_steady_state(): + """Restart ospfd / ospf6d on every router, then wait for the steady + state LSDB. + + Several YANG mutation tests in this suite drive OSPF state + transitions that the routing protocol can't fully clean up on its + own. Two distinct failure modes: + + * router-id changes (test_ospf_yang_router_id_config) leave + old-router-id self-LSAs in remote LSDBs. The local LSDB is + wiped before re-origination, so the in-flight flush to + neighbours never reaches them. + * Interface priority / network-type / passive transitions + (test_ospf_yang_area_interface_b3b_leaves_config, + test_ospf_per_iface_cli_routes_through_yang, + test_ospf_yang_interface_type_and_passive_config) move DR + election around. The transient DR originates a Network LSA; + when it stops being DR, FRR flushes that self-originated LSA + but the resulting MaxAge entry is excluded from + ospf_lsa_maxage_walker (line 3452 of ospf_lsa.c skips + self-originated MaxAge LSAs), so it lingers in the LSDB + indefinitely. + + `clear ip ospf process` is not enough -- the maxage state survives + via flooding back from neighbours that also kept the phantom. + A full daemon restart re-reads frr.conf cleanly and the LSDB + starts from empty everywhere simultaneously, with no stale state + in any router's memory for the phantoms to come back from. + + After restart we wait for r1's area 0 LSDB to settle at the + steady-state count (3 router LSAs + 1 network LSA + 2 inter-area + summary LSAs from r3 + 1 ASBR LSA = 7) so downstream read-only + tests see deterministic state. + """ + tgen = get_topogen() + # Restart ospfd + ospf6d on every router. Daemon restart re-reads + # frr.conf from scratch and gives a guaranteed clean LSDB on every + # node, which `clear ip ospf process` alone cannot. SIGTERM each + # daemon directly; the topogen wrapper only exposes a whole-router + # stop, which would also tear down zebra / mgmtd. + for rname in ("r1", "r2", "r3", "r4"): + rnode = tgen.gears[rname] + rnode.cmd("pkill -TERM -x ospfd") + rnode.cmd("pkill -TERM -x ospf6d") + # Brief settle for the kernel to reap the processes before restart. + time.sleep(2) + for rname in ("r1", "r2", "r3", "r4"): + rnode = tgen.gears[rname] + rnode.net.startRouterDaemons(["ospfd", "ospf6d"]) + + _expect_ospfv2_neighbor_full("r1", "10.0.255.2") + _expect_ospfv2_neighbor_full("r1", "10.0.255.3") + _expect_ospfv3_neighbor_full("r1", "10.0.255.2") + _expect_ospfv3_neighbor_full("r1", "10.0.255.3") + _expect_ospfv2_lsdb_equal("r1", "0.0.0.0", 7) + + +def _expect_ospfv2_lsdb_equal(router, area, expected_lsa_count): + """Wait until `router` reports exactly expected_lsa_count LSAs in `area`. + + NSM Full happens once DBD exchange completes, but full LSDB + propagation (especially summary / ASBR / network LSAs) requires + further LSU flooding. After an OSPF process reset (e.g. router-id + change) the adjacency reaches Full quickly while the LSDB is still + refilling. Equality (not just min) lets us catch phantom LSAs from + pre-restart state (e.g. stale network LSAs from a transient DR + election) that must age out before downstream tests run. + """ + tgen = get_topogen() + + def lsdb_matches(): + result = tgen.gears[router].vtysh_cmd("show ip ospf json", isjson=True) + try: + lsa_count = result["areas"][area]["lsaNumber"] + except (KeyError, TypeError): + return "LSA count not yet available" + if lsa_count == expected_lsa_count: + return None + return "lsaNumber={} != {}".format(lsa_count, expected_lsa_count) + + _, diag = topotest.run_and_expect(lsdb_matches, None, count=180, wait=1) + assert diag is None, '"{}" LSDB did not converge to {} LSAs in area {}: {}'.format( + router, expected_lsa_count, area, diag + ) + + +def test_wait_protocol_convergence(): + "Wait for OSPFv2/OSPFv3 to converge" + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + logger.info("waiting for protocols to converge") # Wait for OSPFv2 convergence - expect_ospfv2_neighbor_full("r1", "10.0.255.2") - expect_ospfv2_neighbor_full("r1", "10.0.255.3") - expect_ospfv2_neighbor_full("r2", "10.0.255.1") - expect_ospfv2_neighbor_full("r2", "10.0.255.3") - expect_ospfv2_neighbor_full("r3", "10.0.255.1") - expect_ospfv2_neighbor_full("r3", "10.0.255.2") - expect_ospfv2_neighbor_full("r3", "10.0.255.4") - expect_ospfv2_neighbor_full("r4", "10.0.255.3") + _expect_ospfv2_neighbor_full("r1", "10.0.255.2") + _expect_ospfv2_neighbor_full("r1", "10.0.255.3") + _expect_ospfv2_neighbor_full("r2", "10.0.255.1") + _expect_ospfv2_neighbor_full("r2", "10.0.255.3") + _expect_ospfv2_neighbor_full("r3", "10.0.255.1") + _expect_ospfv2_neighbor_full("r3", "10.0.255.2") + _expect_ospfv2_neighbor_full("r3", "10.0.255.4") + _expect_ospfv2_neighbor_full("r4", "10.0.255.3") # Wait for OSPFv3 convergence - expect_ospfv3_neighbor_full("r1", "10.0.255.2") - expect_ospfv3_neighbor_full("r1", "10.0.255.3") - expect_ospfv3_neighbor_full("r2", "10.0.255.1") - expect_ospfv3_neighbor_full("r2", "10.0.255.3") - expect_ospfv3_neighbor_full("r3", "10.0.255.1") - expect_ospfv3_neighbor_full("r3", "10.0.255.2") - expect_ospfv3_neighbor_full("r3", "10.0.255.4") - expect_ospfv3_neighbor_full("r4", "10.0.255.3") + _expect_ospfv3_neighbor_full("r1", "10.0.255.2") + _expect_ospfv3_neighbor_full("r1", "10.0.255.3") + _expect_ospfv3_neighbor_full("r2", "10.0.255.1") + _expect_ospfv3_neighbor_full("r2", "10.0.255.3") + _expect_ospfv3_neighbor_full("r3", "10.0.255.1") + _expect_ospfv3_neighbor_full("r3", "10.0.255.2") + _expect_ospfv3_neighbor_full("r3", "10.0.255.4") + _expect_ospfv3_neighbor_full("r4", "10.0.255.3") def compare_show_ipv6_ospf6(rname, expected): @@ -214,6 +278,3479 @@ def compare_ipv6_kernel_routes(router, expected): return topotest.json_cmp(topotest.ip6_route(router), expected) +def _as_list(value): + if isinstance(value, list): + return value + return [value] + + +def _yang_get_data(router, xpath, datastore="operational"): + return json.loads( + router.vtysh_cmd("show mgmt get-data {} datastore {}".format(xpath, datastore)) + ) + + +def _yang_get_running_config(router, xpath): + return json.loads( + router.vtysh_cmd( + "show mgmt get-data {} datastore running only-config exact".format(xpath) + ) + ) + + +def _yang_xpath_subscription(router, xpath): + return router.vtysh_cmd("show mgmt yang-xpath-subscription {}".format(xpath)) + + +def _assert_xpath_client(output, client, oper): + match = re.search( + r"Client: {}\s+config:\d+ notify:\d+ oper:(\d+) rpc:\d+".format(client), + output, + ) + assert match, output + assert match.group(1) == str(int(oper)), output + + +def _yang_operational_root(router): + return _yang_get_data(router, "/*") + + +def _yang_ospf_protocol(output, protocol_type, protocol_name): + assert "ietf-routing:routing" in output, output + protocols = output["ietf-routing:routing"]["control-plane-protocols"][ + "control-plane-protocol" + ] + for protocol in _as_list(protocols): + if ( + protocol.get("type") == protocol_type + and protocol.get("name") == protocol_name + ): + return protocol + + raise AssertionError( + "missing {} control-plane-protocol named {}".format( + protocol_type, protocol_name + ) + ) + + +def _yang_interface(output, ifname): + assert "ietf-interfaces:interfaces" in output, output + interfaces = output["ietf-interfaces:interfaces"]["interface"] + for interface in _as_list(interfaces): + if interface.get("name") == ifname: + return interface + + raise AssertionError("missing ietf-interfaces interface {}".format(ifname)) + + +def _yang_ospf_container(protocol): + return protocol.get("ietf-ospf:ospf", protocol.get("ospf")) + + +def _yang_ospf_area(ospf, area_id): + for area in _as_list(ospf["areas"]["area"]): + if area.get("area-id") == area_id: + return area + + raise AssertionError("missing OSPF area {}".format(area_id)) + + +def _yang_ospf_interface(area, ifname): + interfaces = area["interfaces"]["interface"] + for interface in _as_list(interfaces): + if interface.get("name") == ifname: + return interface + + raise AssertionError("missing OSPF interface {}".format(ifname)) + + +def _yang_ospf_neighbor(interface, router_id): + neighbors = interface["neighbors"]["neighbor"] + for neighbor in _as_list(neighbors): + if neighbor.get("neighbor-router-id") == router_id: + return neighbor + + raise AssertionError("missing OSPF neighbor {}".format(router_id)) + + +def _yang_ospf_neighbor_state(router, protocol_type, expected_address_prefix): + """Return None when the YANG operational tree shows a Full neighbor. + + Used by run_and_expect to poll until OSPF converges before the YANG + operational test asserts adjacency state. Returns a diagnostic string + while convergence is in progress. + """ + try: + output = _yang_operational_root(router) + ospf = _yang_ospf_container( + _yang_ospf_protocol(output, protocol_type, "default") + ) + area = _yang_ospf_area(ospf, "0.0.0.0") + interface = _yang_ospf_interface(area, "r1-eth1") + neighbor = _yang_ospf_neighbor(interface, "10.0.255.2") + except (AssertionError, KeyError): + return "neighbor entry not yet visible" + if neighbor.get("state") != "full": + return "neighbor state is {}".format(neighbor.get("state")) + if not neighbor.get("address", "").startswith(expected_address_prefix): + return "neighbor address {} not in expected family".format( + neighbor.get("address") + ) + return None + + +def test_ospf_yang_operational_data(): + "Verify RFC 9129 OSPF operational data is exposed through YANG callbacks." + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + backend_adapters = r1.vtysh_cmd("show mgmt backend-adapter all") + assert "ospfd" in backend_adapters + assert "ospf6d" in backend_adapters + xpath_registry = r1.vtysh_cmd("show mgmt backend-yang-xpath-registry oper") + assert ( + "/ietf-routing:routing/control-plane-protocols/control-plane-protocol" + in xpath_registry + ) + assert "/ietf-interfaces:interfaces/interface" in xpath_registry + + # Wait for OSPFv2 and OSPFv3 adjacencies to reach Full before asserting + # operational state. ospf_topo1 fixture brings the topology up but does + # not guarantee convergence by the time this test runs. + for protocol_type, addr_prefix in ( + ("ietf-ospf:ospfv2", "10."), + ("ietf-ospf:ospfv3", "fe80:"), + ): + test_func = partial(_yang_ospf_neighbor_state, r1, protocol_type, addr_prefix) + _, diag = topotest.run_and_expect(test_func, None, count=160, wait=0.5) + assert diag is None, "OSPF {} did not converge: {}".format(protocol_type, diag) + + output = _yang_operational_root(r1) + _yang_interface(output, "r1-eth1") + + ospfv2 = _yang_ospf_container( + _yang_ospf_protocol(output, "ietf-ospf:ospfv2", "default") + ) + assert ospfv2["router-id"] == "10.0.255.1" + assert isinstance(ospfv2["statistics"]["originate-new-lsa-count"], int) + assert isinstance(ospfv2["statistics"]["rx-new-lsas-count"], int) + area = _yang_ospf_area(ospfv2, "0.0.0.0") + interface = _yang_ospf_interface(area, "r1-eth1") + neighbor = _yang_ospf_neighbor(interface, "10.0.255.2") + assert neighbor["state"] == "full" + assert "address" in neighbor + + ospfv3 = _yang_ospf_container( + _yang_ospf_protocol(output, "ietf-ospf:ospfv3", "default") + ) + assert ospfv3["router-id"] == "10.0.255.1" + assert isinstance(ospfv3["statistics"]["originate-new-lsa-count"], int) + assert isinstance(ospfv3["statistics"]["rx-new-lsas-count"], int) + area = _yang_ospf_area(ospfv3, "0.0.0.0") + interface = _yang_ospf_interface(area, "r1-eth1") + neighbor = _yang_ospf_neighbor(interface, "10.0.255.2") + assert neighbor["state"] == "full" + assert neighbor["address"].startswith("fe80:") + + +def test_ospf_yang_mgmtd_predicate_dispatch(): + "Verify mgmtd dispatches typed OSPF xpaths to the correct backend." + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + output = _yang_operational_root(r1) + _yang_ospf_protocol(output, "ietf-ospf:ospfv2", "default") + _yang_ospf_protocol(output, "ietf-ospf:ospfv3", "default") + + for protocol_type in ("ietf-ospf:ospfv2", "ietf-ospf:ospfv3"): + protocol_path = ( + "/ietf-routing:routing/control-plane-protocols/" + "control-plane-protocol[type='" + protocol_type + "'][name='default']" + ) + subscription = _yang_xpath_subscription(r1, protocol_path) + if protocol_type == "ietf-ospf:ospfv2": + _assert_xpath_client(subscription, "ospfd", True) + assert "Client: ospf6d" not in subscription, subscription + else: + assert "Client: ospfd" not in subscription, subscription + _assert_xpath_client(subscription, "ospf6d", True) + + selected = _yang_get_data(r1, protocol_path) + protocols = _as_list( + selected["ietf-routing:routing"]["control-plane-protocols"][ + "control-plane-protocol" + ] + ) + assert len(protocols) == 1, selected + protocol = protocols[0] + assert protocol["type"] == protocol_type, selected + assert protocol["name"] == "default", selected + assert _yang_ospf_container(protocol)["router-id"] == "10.0.255.1" + + for protocol_path in ( + "/ietf-routing:routing/control-plane-protocols", + "/ietf-routing:routing/control-plane-protocols/control-plane-protocol", + "/ietf-routing:routing/control-plane-protocols/control-plane-protocol/ietf-ospf:ospf/router-id", + ): + subscription = _yang_xpath_subscription(r1, protocol_path) + _assert_xpath_client(subscription, "ospfd", True) + _assert_xpath_client(subscription, "ospf6d", True) + + # Zebra registers the RFC 8343 interface list without an OSPF-style + # protocol-type predicate. Keep one predicate query here so the test + # catches regressions in both typed and untyped backend registrations. + selected = _yang_get_data( + r1, "/ietf-interfaces:interfaces/interface[name='r1-eth1']" + ) + assert _yang_interface(selected, "r1-eth1")["oper-status"] == "up" + + +def _yang_explicit_router_id_xpath(protocol_type): + return ( + "/ietf-routing:routing/control-plane-protocols/" + "control-plane-protocol[type='" + protocol_type + "'][name='default']/" + "ietf-ospf:ospf/explicit-router-id" + ) + + +def _mgmt_set_and_commit(router, xpath, value): + """Run a single mgmt set-config + commit apply with explicit DS locks. + + `configure terminal file-lock` acquires both candidate and running locks + on the mgmtd session; without the lock, `mgmt commit apply` fails with + "source not locked by session-id". + """ + router.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {} {}\n" + "mgmt commit apply".format(xpath, value) + ) + + +def _set_yang_router_id(router, protocol_type, daemon, running_line, new_value): + """Set explicit-router-id via mgmtd, verify it lands, then restore. + + Exercises the config-write path end-to-end: candidate datastore set, + commit apply, modify callback mutates FRR state, running-config reflects + the change. + """ + xpath = _yang_explicit_router_id_xpath(protocol_type) + + _mgmt_set_and_commit(router, xpath, new_value) + + running = router.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "{} {}".format(running_line, new_value) in running + ), "expected '{} {}' in running-config after YANG set, got:\n{}".format( + running_line, new_value, running + ) + + # Restore the original value through the same YANG path so subsequent + # convergence-dependent tests in this module see the topology they expect. + _mgmt_set_and_commit(router, xpath, "10.0.255.1") + + running = router.vtysh_cmd("show running-config {}".format(daemon)) + assert "{} 10.0.255.1".format(running_line) in running + + +def test_ospf_yang_router_id_config(): + "Verify RFC 9129 explicit-router-id is writable via mgmtd." + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + _set_yang_router_id( + r1, "ietf-ospf:ospfv2", "ospfd", "ospf router-id", "10.0.255.21" + ) + _set_yang_router_id( + r1, "ietf-ospf:ospfv3", "ospf6d", "ospf6 router-id", "10.0.255.31" + ) + + # Router-id mutation leaves phantom self-LSAs in the other routers' + # LSDBs that the routing protocol can't fully reconcile on its own; + # bounce every adjacency so the LSDB is rebuilt cleanly before any + # downstream test depends on it. + _force_ospf_reconvergence_to_steady_state() + + +def _yang_area_xpath(protocol_type, area_id): + return ( + "/ietf-routing:routing/control-plane-protocols/" + "control-plane-protocol[type='" + + protocol_type + + "'][name='default']/ietf-ospf:ospf/areas/area[area-id='" + + area_id + + "']" + ) + + +def _set_yang_area_type( + router, protocol_type, daemon, area_id, area_type, expect_running_line +): + """Set areas/area[id=X]/area-type via mgmtd and verify it lands. + + area_type is one of "stub-area", "nssa-area", "normal-area" (the RFC 9129 + identityref values). expect_running_line is the substring to look for in + `show running-config ` once the change has applied. + """ + area_path = _yang_area_xpath(protocol_type, area_id) + type_path = area_path + "/area-type" + router.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {} {}\n" + "mgmt commit apply".format(type_path, area_type) + ) + + running = router.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + expect_running_line in running + ), "expected '{}' in running-config after YANG area-type set to {}, got:\n{}".format( + expect_running_line, area_type, running + ) + + +def _delete_yang_area_type(router, protocol_type, daemon, area_id, absent_line): + """Delete areas/area[id=X]/area-type and verify it returns to normal-area.""" + area_path = _yang_area_xpath(protocol_type, area_id) + router.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}/area-type\n" + "mgmt commit apply".format(area_path) + ) + + running = router.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + absent_line not in running + ), "'{}' should be gone after YANG area-type delete, got:\n{}".format( + absent_line, running + ) + + +def _clear_yang_area(router, protocol_type, area_id): + """Remove an area list entry via mgmtd.""" + area_path = _yang_area_xpath(protocol_type, area_id) + router.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}\n" + "mgmt commit apply".format(area_path) + ) + + +def _restore_r1_eth1_fixture_timers(router, protocol_type): + """Restore the explicit timer leaves from the r1 fixture.""" + iface_path = ( + _yang_area_xpath(protocol_type, "0.0.0.0") + + "/interfaces/interface[name='r1-eth1']" + ) + + router.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/hello-interval 2\n" + "mgmt set-config {}/dead-interval 10\n" + "mgmt commit apply".format(iface_path, iface_path) + ) + + +def test_ospf_yang_area_type_config(): + "Verify areas/area[area-id]/area-type is writable via mgmtd for both daemons." + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + # OSPFv2: create a new stub area via YANG, verify it appears in + # show running-config, then remove it. + _set_yang_area_type( + r1, + "ietf-ospf:ospfv2", + "ospfd", + "0.0.0.42", + "stub-area", + "area 0.0.0.42 stub", + ) + _delete_yang_area_type( + r1, "ietf-ospf:ospfv2", "ospfd", "0.0.0.42", "area 0.0.0.42 stub" + ) + _set_yang_area_type( + r1, + "ietf-ospf:ospfv2", + "ospfd", + "0.0.0.42", + "stub-area", + "area 0.0.0.42 stub", + ) + _clear_yang_area(r1, "ietf-ospf:ospfv2", "0.0.0.42") + running = r1.vtysh_cmd("show running-config ospfd") + assert "area 0.0.0.42 stub" not in running, ( + "area 0.0.0.42 should be removed after YANG delete, running:\n" + running + ) + + # OSPFv3: same shape. + _set_yang_area_type( + r1, + "ietf-ospf:ospfv3", + "ospf6d", + "0.0.0.42", + "stub-area", + "area 0.0.0.42 stub", + ) + _delete_yang_area_type( + r1, "ietf-ospf:ospfv3", "ospf6d", "0.0.0.42", "area 0.0.0.42 stub" + ) + _set_yang_area_type( + r1, + "ietf-ospf:ospfv3", + "ospf6d", + "0.0.0.42", + "stub-area", + "area 0.0.0.42 stub", + ) + _clear_yang_area(r1, "ietf-ospf:ospfv3", "0.0.0.42") + running = r1.vtysh_cmd("show running-config ospf6d") + assert "area 0.0.0.42 stub" not in running, ( + "area 0.0.0.42 should be removed after YANG delete, running:\n" + running + ) + + +def _set_yang_area_attrs(router, protocol_type, area_id, attrs): + """Set multiple area attrs in one configure-and-commit block. + + `attrs` is a list of (sub_path, value) tuples; each becomes one + `mgmt set-config` line, with all changes batched into a single + commit apply. Uses `configure terminal file-lock` so the commit + has the candidate+running DS locks it requires. + """ + area_path = _yang_area_xpath(protocol_type, area_id) + lines = ["configure terminal file-lock"] + for sub_path, value in attrs: + lines.append("mgmt set-config {}/{} {}".format(area_path, sub_path, value)) + lines.append("mgmt commit apply") + router.vtysh_cmd("\n".join(lines)) + + +def test_ospf_distance_cli_routes_through_yang(): + """The legacy single-value `distance N` / `no distance` and multi-value + `distance ospf intra-area X inter-area Y external Z` / `no distance ospf` + forms continue to work via vtysh but now route through the ietf-ospf + YANG `/preference/all` and `/preference/{intra-area,inter-area,external}` + leaves. Verify by issuing the legacy CLI and confirming the daemon's + running-config reflects the change exactly as before. + + The OSPFv3 path is covered indirectly by test_ospf_yang_preference_config; + legacy `distance` invocations in OSPF6_NODE additionally trigger + ospf6_restart_spf, which trips a pre-existing ospf6_route_remove_all + use-after-free when the route table is populated, so this test sticks + to OSPFv2 for the legacy CLI.""" + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + daemon = "ospfd" + router_block = "router ospf" + cli_proto = "ospf" + + # single-value scope (preference/all) + r1.vtysh_cmd("configure terminal\n" "{}\n" " distance 137\n".format(router_block)) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert " distance 137" in running, running + + r1.vtysh_cmd("configure terminal\n" "{}\n" " no distance\n".format(router_block)) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert "distance 137" not in running, running + + # multi-value scope (preference/intra-area + /inter-area + /external) + r1.vtysh_cmd( + "configure terminal\n" + "{}\n" + " distance {} intra-area 21 inter-area 22 external 23\n".format( + router_block, cli_proto + ) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "distance {} intra-area 21".format(cli_proto) in running + ), "expected 'distance {} intra-area 21' in {} running-config, got:\n{}".format( + cli_proto, daemon, running + ) + assert "inter-area 22" in running, running + assert "external 23" in running, running + + r1.vtysh_cmd( + "configure terminal\n" + "{}\n" + " no distance {}\n".format(router_block, cli_proto) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert "intra-area 21" not in running, running + assert "inter-area 22" not in running, running + assert "external 23" not in running, running + + +def test_ospf_area_cli_routes_through_yang(): + """The legacy `area X stub`, `area X stub no-summary`, `area X + default-cost N`, and their `no` forms continue to work via vtysh but + now route through the ietf-ospf YANG layer. Verify by issuing the + legacy CLI and confirming the daemons running-config reflects the + change exactly as before.""" + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + # OSPFv2: stub, then totally-stubby, then add default-cost, then unwind. + r1.vtysh_cmd( + "configure terminal\n" + "router ospf\n" + " area 0.0.0.51 stub\n" + " area 0.0.0.51 stub no-summary\n" + " area 0.0.0.51 default-cost 17\n" + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert "area 0.0.0.51 stub no-summary" in running, running + assert "area 0.0.0.51 default-cost 17" in running, running + + r1.vtysh_cmd( + "configure terminal\n" + "router ospf\n" + " no area 0.0.0.51 default-cost\n" + " no area 0.0.0.51 stub no-summary\n" + ) + running = r1.vtysh_cmd("show running-config ospfd") + # `no stub no-summary` clears only the no-summary, area stays stub. + assert "area 0.0.0.51 stub" in running, running + assert "area 0.0.0.51 stub no-summary" not in running, running + assert "default-cost 17" not in running, running + + r1.vtysh_cmd("configure terminal\nrouter ospf\n no area 0.0.0.51 stub\n") + running = r1.vtysh_cmd("show running-config ospfd") + assert "0.0.0.51" not in running, running + + # OSPFv3: same cycle, minus default-cost (no v3 surface). + r1.vtysh_cmd( + "configure terminal\n" "router ospf6\n" " area 0.0.0.52 stub no-summary\n" + ) + running = r1.vtysh_cmd("show running-config ospf6d") + assert "area 0.0.0.52 stub no-summary" in running, running + + r1.vtysh_cmd( + "configure terminal\n" "router ospf6\n" " no area 0.0.0.52 stub no-summary\n" + ) + running = r1.vtysh_cmd("show running-config ospf6d") + assert "area 0.0.0.52 stub" in running, running + assert "area 0.0.0.52 stub no-summary" not in running, running + + r1.vtysh_cmd("configure terminal\nrouter ospf6\n no area 0.0.0.52 stub\n") + running = r1.vtysh_cmd("show running-config ospf6d") + assert "0.0.0.52" not in running, running + + +def test_ospf_yang_area_interface_cost_config(): + """areas/area[id]/interfaces/interface[name]/cost via mgmtd. + + Exercises the per-interface YANG path end-to-end: creating the + /areas/area[id='0.0.0.0']/interfaces/interface[name='r1-eth1'] + entry attaches r1-eth1 to area 0 (the same area it's already in + from the fixture config), setting cost mutates the FRR-side + output-cost-cmd. Then unwinds: deleting cost reverts to default, + deleting the interface entry detaches it from the area. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + # OSPFv2: r1-eth1 is already in area 0.0.0.0 via `network 10.0.1.0/24 area 0` + # in the fixture. The YANG list create is idempotent (same area), so this + # is a clean set + cost change. + area_path = _yang_area_xpath("ietf-ospf:ospfv2", "0.0.0.0") + iface_path = area_path + "/interfaces/interface[name='r1-eth1']" + cost_path = iface_path + "/cost" + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {} 77\n" + "mgmt commit apply".format(cost_path) + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert "ip ospf cost 77" in running, ( + "expected 'ip ospf cost 77' in running-config after YANG set, got:\n" + running + ) + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}\n" + "mgmt commit apply".format(cost_path) + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert "ip ospf cost 77" not in running, ( + "ip ospf cost should be removed after YANG delete, got:\n" + running + ) + + # OSPFv3: r1-eth1 is already in area 0 via the fixture's + # `ipv6 ospf6 area 0` per-interface command. Same set + clear cycle. + area_path = _yang_area_xpath("ietf-ospf:ospfv3", "0.0.0.0") + iface_path = area_path + "/interfaces/interface[name='r1-eth1']" + cost_path = iface_path + "/cost" + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {} 88\n" + "mgmt commit apply".format(cost_path) + ) + running = r1.vtysh_cmd("show running-config ospf6d") + assert "ipv6 ospf6 cost 88" in running, ( + "expected 'ipv6 ospf6 cost 88' in running-config after YANG set, got:\n" + + running + ) + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}\n" + "mgmt commit apply".format(cost_path) + ) + running = r1.vtysh_cmd("show running-config ospf6d") + assert "ipv6 ospf6 cost 88" not in running, ( + "ipv6 ospf6 cost should be removed after YANG delete, got:\n" + running + ) + + +def test_ospf_yang_area_interface_b3b_leaves_config(): + """hello/dead/retransmit/priority/mtu-ignore round-trip. + + Sets all five per-interface YANG leaves in a single mgmt commit + apply for both OSPFv2 and OSPFv3, verifies each lands in + `show running-config `, then deletes them and verifies + the reverts. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + # OSPFv2: r1-eth1 is already in area 0.0.0.0 via the fixture's + # network statement. Existing fixture also sets hello-interval 2 + # and dead-interval 10, so we use distinguishable test values. + iface = ( + _yang_area_xpath("ietf-ospf:ospfv2", "0.0.0.0") + + "/interfaces/interface[name='r1-eth1']" + ) + cmds = ( + "configure terminal file-lock\n" + "mgmt set-config {}/hello-interval 7\n" + "mgmt set-config {}/dead-interval 29\n" + "mgmt set-config {}/retransmit-interval 11\n" + "mgmt set-config {}/priority 13\n" + "mgmt set-config {}/mtu-ignore true\n" + "mgmt commit apply" + ).format(iface, iface, iface, iface, iface) + r1.vtysh_cmd(cmds) + + running = r1.vtysh_cmd("show running-config ospfd") + for expected in ( + "ip ospf hello-interval 7", + "ip ospf dead-interval 29", + "ip ospf retransmit-interval 11", + "ip ospf priority 13", + "ip ospf mtu-ignore", + ): + assert ( + expected in running + ), "expected '{}' in v2 running-config, got:\n{}".format(expected, running) + + # Tear down individual leaves + cmds = ( + "configure terminal file-lock\n" + "mgmt delete-config {}/hello-interval\n" + "mgmt delete-config {}/dead-interval\n" + "mgmt delete-config {}/retransmit-interval\n" + "mgmt delete-config {}/priority\n" + "mgmt delete-config {}/mtu-ignore\n" + "mgmt commit apply" + ).format(iface, iface, iface, iface, iface) + r1.vtysh_cmd(cmds) + running = r1.vtysh_cmd("show running-config ospfd") + for unexpected in ( + "ip ospf hello-interval 7", + "ip ospf dead-interval 29", + "ip ospf retransmit-interval 11", + "ip ospf priority 13", + "ip ospf mtu-ignore", + ): + assert ( + unexpected not in running + ), "'{}' should be gone after YANG delete, got:\n{}".format(unexpected, running) + _restore_r1_eth1_fixture_timers(r1, "ietf-ospf:ospfv2") + running = r1.vtysh_cmd("show running-config ospfd") + assert "ip ospf hello-interval 2" in running, running + assert "ip ospf dead-interval 10" in running, running + + # OSPFv3: same leaves, same path shape. + iface = ( + _yang_area_xpath("ietf-ospf:ospfv3", "0.0.0.0") + + "/interfaces/interface[name='r1-eth1']" + ) + cmds = ( + "configure terminal file-lock\n" + "mgmt set-config {}/hello-interval 7\n" + "mgmt set-config {}/dead-interval 29\n" + "mgmt set-config {}/retransmit-interval 11\n" + "mgmt set-config {}/priority 13\n" + "mgmt set-config {}/mtu-ignore true\n" + "mgmt commit apply" + ).format(iface, iface, iface, iface, iface) + r1.vtysh_cmd(cmds) + running = r1.vtysh_cmd("show running-config ospf6d") + for expected in ( + "ipv6 ospf6 hello-interval 7", + "ipv6 ospf6 dead-interval 29", + "ipv6 ospf6 retransmit-interval 11", + "ipv6 ospf6 priority 13", + "ipv6 ospf6 mtu-ignore", + ): + assert ( + expected in running + ), "expected '{}' in v3 running-config, got:\n{}".format(expected, running) + + cmds = ( + "configure terminal file-lock\n" + "mgmt delete-config {}/hello-interval\n" + "mgmt delete-config {}/dead-interval\n" + "mgmt delete-config {}/retransmit-interval\n" + "mgmt delete-config {}/priority\n" + "mgmt delete-config {}/mtu-ignore\n" + "mgmt commit apply" + ).format(iface, iface, iface, iface, iface) + r1.vtysh_cmd(cmds) + running = r1.vtysh_cmd("show running-config ospf6d") + for unexpected in ( + "ipv6 ospf6 hello-interval 7", + "ipv6 ospf6 dead-interval 29", + "ipv6 ospf6 retransmit-interval 11", + "ipv6 ospf6 priority 13", + "ipv6 ospf6 mtu-ignore", + ): + assert ( + unexpected not in running + ), "'{}' should be gone after YANG delete, got:\n{}".format(unexpected, running) + _restore_r1_eth1_fixture_timers(r1, "ietf-ospf:ospfv3") + running = r1.vtysh_cmd("show running-config ospf6d") + assert "ipv6 ospf6 hello-interval 2" in running, running + assert "ipv6 ospf6 dead-interval 10" in running, running + + +def test_ospf_yang_area_interface_transmit_delay_config(): + """areas/area[id]/interfaces/interface[name]/transmit-delay via mgmtd. + + Round-trips the per-interface transmit-delay leaf on both + daemons: set via mgmt, verify in `show running-config`, delete + via mgmt, verify the line is gone and FRR is back at the + compile-time default (OSPF_TRANSMIT_DELAY_DEFAULT for ospfd, + OSPF6_INTERFACE_TRANSDELAY for ospf6d -- both 1). + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + # OSPFv2 + area_path = _yang_area_xpath("ietf-ospf:ospfv2", "0.0.0.0") + iface_path = area_path + "/interfaces/interface[name='r1-eth1']" + leaf_path = iface_path + "/transmit-delay" + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {} 17\n" + "mgmt commit apply".format(leaf_path) + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert "ip ospf transmit-delay 17" in running, ( + "expected 'ip ospf transmit-delay 17' in running-config after YANG set, got:\n" + + running + ) + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}\n" + "mgmt commit apply".format(leaf_path) + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert "ip ospf transmit-delay" not in running, ( + "ip ospf transmit-delay should be removed after YANG delete, got:\n" + running + ) + + # OSPFv3 + area_path = _yang_area_xpath("ietf-ospf:ospfv3", "0.0.0.0") + iface_path = area_path + "/interfaces/interface[name='r1-eth1']" + leaf_path = iface_path + "/transmit-delay" + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {} 19\n" + "mgmt commit apply".format(leaf_path) + ) + running = r1.vtysh_cmd("show running-config ospf6d") + assert "ipv6 ospf6 transmit-delay 19" in running, ( + "expected 'ipv6 ospf6 transmit-delay 19' in running-config after YANG set, got:\n" + + running + ) + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}\n" + "mgmt commit apply".format(leaf_path) + ) + running = r1.vtysh_cmd("show running-config ospf6d") + assert "ipv6 ospf6 transmit-delay" not in running, ( + "ipv6 ospf6 transmit-delay should be removed after YANG delete, got:\n" + + running + ) + + +def test_ospf_per_iface_cli_routes_through_yang(): + """legacy per-interface CLI commands route through the + ietf-ospf YANG layer when the interface is in an area. + + Drives `ip ospf cost N`, `ip ospf hello-interval N`, `ip ospf + dead-interval N`, `ip ospf priority N`, `ip ospf mtu-ignore`, + `ip ospf passive`, `ip ospf retransmit-interval N`, `ip ospf + transmit-delay N`, `ip ospf network point-to-point` and their + v3 siblings via vtysh on r1-eth1 (which the fixture already + attached to area 0 for both daemons), confirms each lands in + running-config, then unwinds via the corresponding `no` form. + Covers both the conversion of the main DEFUN to DEFPY_YANG and + the routing through the NB callbacks landed in the per-interface + slices. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + # OSPFv2 on r1-eth1 + r1.vtysh_cmd( + "configure terminal\n" + "interface r1-eth1\n" + " ip ospf cost 41\n" + " ip ospf hello-interval 3\n" + " ip ospf dead-interval 15\n" + " ip ospf priority 19\n" + " ip ospf mtu-ignore\n" + " ip ospf passive\n" + " ip ospf retransmit-interval 23\n" + " ip ospf transmit-delay 31\n" + " ip ospf network point-to-point\n" + ) + running = r1.vtysh_cmd("show running-config ospfd") + for expected in ( + "ip ospf cost 41", + "ip ospf hello-interval 3", + "ip ospf dead-interval 15", + "ip ospf priority 19", + "ip ospf mtu-ignore", + "ip ospf passive", + "ip ospf retransmit-interval 23", + "ip ospf transmit-delay 31", + "ip ospf network point-to-point", + ): + assert ( + expected in running + ), "expected '{}' in v2 running-config after CLI set, got:\n{}".format( + expected, running + ) + + r1.vtysh_cmd( + "configure terminal\n" + "interface r1-eth1\n" + " no ip ospf cost\n" + " no ip ospf hello-interval\n" + " no ip ospf dead-interval\n" + " no ip ospf priority\n" + " no ip ospf mtu-ignore\n" + " no ip ospf passive\n" + " no ip ospf retransmit-interval\n" + " no ip ospf transmit-delay\n" + " no ip ospf network\n" + ) + running = r1.vtysh_cmd("show running-config ospfd") + for unexpected in ( + "ip ospf cost 41", + "ip ospf hello-interval 3", + "ip ospf dead-interval 15", + "ip ospf priority 19", + "ip ospf mtu-ignore", + "ip ospf passive", + "ip ospf retransmit-interval 23", + "ip ospf transmit-delay 31", + "ip ospf network point-to-point", + ): + assert ( + unexpected not in running + ), "'{}' should be gone after CLI no form, got:\n{}".format(unexpected, running) + + # OSPFv3 on r1-eth1 + r1.vtysh_cmd( + "configure terminal\n" + "interface r1-eth1\n" + " ipv6 ospf6 cost 41\n" + " ipv6 ospf6 hello-interval 3\n" + " ipv6 ospf6 dead-interval 15\n" + " ipv6 ospf6 priority 19\n" + " ipv6 ospf6 mtu-ignore\n" + " ipv6 ospf6 passive\n" + " ipv6 ospf6 retransmit-interval 23\n" + " ipv6 ospf6 transmit-delay 31\n" + " ipv6 ospf6 network point-to-point\n" + ) + running = r1.vtysh_cmd("show running-config ospf6d") + for expected in ( + "ipv6 ospf6 cost 41", + "ipv6 ospf6 hello-interval 3", + "ipv6 ospf6 dead-interval 15", + "ipv6 ospf6 priority 19", + "ipv6 ospf6 mtu-ignore", + "ipv6 ospf6 passive", + "ipv6 ospf6 retransmit-interval 23", + "ipv6 ospf6 transmit-delay 31", + "ipv6 ospf6 network point-to-point", + ): + assert ( + expected in running + ), "expected '{}' in v3 running-config after CLI set, got:\n{}".format( + expected, running + ) + + r1.vtysh_cmd( + "configure terminal\n" + "interface r1-eth1\n" + " no ipv6 ospf6 cost\n" + " no ipv6 ospf6 hello-interval\n" + " no ipv6 ospf6 dead-interval\n" + " no ipv6 ospf6 priority\n" + " no ipv6 ospf6 mtu-ignore\n" + " no ipv6 ospf6 passive\n" + " no ipv6 ospf6 retransmit-interval\n" + " no ipv6 ospf6 transmit-delay\n" + " no ipv6 ospf6 network\n" + ) + running = r1.vtysh_cmd("show running-config ospf6d") + for unexpected in ( + "ipv6 ospf6 cost 41", + "ipv6 ospf6 hello-interval 3", + "ipv6 ospf6 dead-interval 15", + "ipv6 ospf6 priority 19", + "ipv6 ospf6 mtu-ignore", + "ipv6 ospf6 passive", + "ipv6 ospf6 retransmit-interval 23", + "ipv6 ospf6 transmit-delay 31", + "ipv6 ospf6 network point-to-point", + ): + assert ( + unexpected not in running + ), "'{}' should be gone after CLI no form, got:\n{}".format(unexpected, running) + + r1.vtysh_cmd( + "configure terminal\n" + "interface r1-eth1\n" + " ip ospf hello-interval 2\n" + " ip ospf dead-interval 10\n" + " ipv6 ospf6 hello-interval 2\n" + " ipv6 ospf6 dead-interval 10\n" + ) + + +def test_ospf_network_dmvpn_falls_back_to_legacy(): + """`ip ospf network point-to-point dmvpn` keeps working via the + legacy direct-mutation path because RFC 9129's interface-type + enum doesn't model the FRR-specific dmvpn flag. + + The DEFPY_YANG body classifies the FRR-modifier as out-of-scope + for YANG and falls through to ospf_network_legacy_apply. This + test confirms the running-config still shows the dmvpn modifier + after the conversion. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + r1.vtysh_cmd( + "configure terminal\n" + "interface r1-eth1\n" + " ip ospf network point-to-point dmvpn\n" + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert "ip ospf network point-to-point dmvpn" in running, ( + "expected 'ip ospf network point-to-point dmvpn' in running-config " + "after legacy CLI set, got:\n" + running + ) + + r1.vtysh_cmd("configure terminal\n" "interface r1-eth1\n" " no ip ospf network\n") + running = r1.vtysh_cmd("show running-config ospfd") + assert "ip ospf network" not in running, ( + "ip ospf network should be removed after CLI no form, got:\n" + running + ) + + +def test_ospf_yang_preference_config(): + """per-instance preference (admin distance) round-trip via mgmtd. + + Covers the single-value scope (preference/all -> distance N) and the + multi-values scope (preference/intra-area, /inter-area, /external). + For both OSPFv2 and OSPFv3. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + for proto, daemon, cli_proto in ( + ("ietf-ospf:ospfv2", "ospfd", "ospf"), + ("ietf-ospf:ospfv3", "ospf6d", "ospf6"), + ): + # v2 renders `distance ospf intra-area X`; v3 renders `distance ospf6 ...` + cli_prefix = "distance" + instance = ( + "/ietf-routing:routing/control-plane-protocols/" + "control-plane-protocol[type='" + + proto + + "'][name='default']/ietf-ospf:ospf" + ) + + # single-value scope + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/preference/all 137\n" + "mgmt commit apply".format(instance) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "{} 137".format(cli_prefix) in running + ), "expected '{} 137' in {} running-config, got:\n{}".format( + cli_prefix, daemon, running + ) + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}/preference/all\n" + "mgmt commit apply".format(instance) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert "{} 137".format(cli_prefix) not in running + + # multi-values scope (intra + inter + external set together) + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/preference/intra-area 21\n" + "mgmt set-config {}/preference/inter-area 22\n" + "mgmt set-config {}/preference/external 23\n" + "mgmt commit apply".format(instance, instance, instance) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "{} {} intra-area 21".format(cli_prefix, cli_proto) in running + ), "expected '{} {} intra-area 21' in {} running-config, got:\n{}".format( + cli_prefix, cli_proto, daemon, running + ) + assert "inter-area 22" in running, running + assert "external 23" in running, running + + # cleanup + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}/preference/intra-area\n" + "mgmt delete-config {}/preference/inter-area\n" + "mgmt delete-config {}/preference/external\n" + "mgmt commit apply".format(instance, instance, instance) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert "intra-area 21" not in running + assert "inter-area 22" not in running + assert "external 23" not in running + + +def test_ospf_yang_spf_control_paths_config(): + """per-instance spf-control/paths round-trip via mgmtd. + + RFC 9129's `/spf-control/paths` is a uint16; the legacy CLI's + `maximum-paths` accepts up to MULTIPATH_NUM (platform-defined). + The conversion routes normal writes through YANG and keeps the + legacy direct-mutation path as a fallback if an instance XPath + cannot be built; this test covers the direct YANG path. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + for proto, daemon in ( + ("ietf-ospf:ospfv2", "ospfd"), + ("ietf-ospf:ospfv3", "ospf6d"), + ): + instance = ( + "/ietf-routing:routing/control-plane-protocols/" + "control-plane-protocol[type='" + + proto + + "'][name='default']/ietf-ospf:ospf" + ) + + # YANG set within RFC range. + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/spf-control/paths 7\n" + "mgmt commit apply".format(instance) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "maximum-paths 7" in running + ), "expected 'maximum-paths 7' after YANG set, got:\n{}".format(running) + + # YANG delete restores no-config (FRR semantics). + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}/spf-control/paths\n" + "mgmt commit apply".format(instance) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "maximum-paths 7" not in running + ), "maximum-paths 7 should be gone after YANG delete, got:\n{}".format(running) + + +def test_ospf_yang_spf_control_paths_platform_limit_rejected(): + """Direct mgmtd writes honour FRR's platform ECMP cap. + + The RFC 9129 type allows values up to 65535, but FRR must still + reject anything above MULTIPATH_NUM in the daemon callback so CLI + and YANG clients observe the same platform limit. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + for proto, daemon in ( + ("ietf-ospf:ospfv2", "ospfd"), + ("ietf-ospf:ospfv3", "ospf6d"), + ): + instance = ( + "/ietf-routing:routing/control-plane-protocols/" + "control-plane-protocol[type='" + + proto + + "'][name='default']/ietf-ospf:ospf" + ) + out = _mgmt_commit_attempt( + r1, + "mgmt set-config {}/spf-control/paths 65535".format(instance), + ) + assert ( + "maximum-paths exceeds platform max" in out + ), "expected platform-limit rejection for {}, got:\n{}".format(proto, out) + + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "maximum-paths 65535" not in running + ), "rejected maximum-paths value must not land on {}, got:\n{}".format( + daemon, running + ) + + +def test_ospf_yang_mpls_ldp_igp_sync_config(): + """per-instance mpls/ldp/igp-sync round-trip via mgmtd (OSPFv2 only). + + cEOS-style: set the leaf to true, verify `mpls ldp-sync` lands in + `show running-config`, then delete the leaf and verify it is + gone. ospf6d has no LDP/IGP sync implementation; the OSPFv3 + callback is intentionally absent. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + instance = ( + "/ietf-routing:routing/control-plane-protocols/" + "control-plane-protocol[type='ietf-ospf:ospfv2'][name='default']" + "/ietf-ospf:ospf" + ) + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/mpls/ldp/igp-sync true\n" + "mgmt commit apply".format(instance) + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert ( + "mpls ldp-sync" in running + ), "expected 'mpls ldp-sync' after YANG set, got:\n{}".format(running) + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}/mpls/ldp/igp-sync\n" + "mgmt commit apply".format(instance) + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert ( + "mpls ldp-sync" not in running + ), "mpls ldp-sync should be gone after YANG delete, got:\n{}".format(running) + + +def test_ospf_yang_auto_cost_reference_bandwidth_config(): + """per-instance /auto-cost/reference-bandwidth round-trip via mgmtd + on both daemons. RFC 9129 wraps the leaf in a `when ../enabled + = 'true'` constraint; the deviation file pins enabled to true so + a bare reference-bandwidth set works without first toggling + enabled. Setting enabled=false is rejected at NB_EV_VALIDATE + because FRR has no off-switch for auto-cost; a separate negative + test exercises that path.""" + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + for proto, daemon in ( + ("ietf-ospf:ospfv2", "ospfd"), + ("ietf-ospf:ospfv3", "ospf6d"), + ): + instance = ( + "/ietf-routing:routing/control-plane-protocols/" + "control-plane-protocol[type='" + + proto + + "'][name='default']/ietf-ospf:ospf" + ) + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/auto-cost/reference-bandwidth 50000\n" + "mgmt commit apply".format(instance) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "auto-cost reference-bandwidth 50000" in running + ), "expected 'auto-cost reference-bandwidth 50000' after YANG set, got:\n{}".format( + running + ) + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}/auto-cost/reference-bandwidth\n" + "mgmt commit apply".format(instance) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "auto-cost reference-bandwidth 50000" not in running + ), "'auto-cost reference-bandwidth 50000' should be gone after YANG delete, got:\n{}".format( + running + ) + + +def test_ospf_yang_auto_cost_disable_rejected(): + """Setting /auto-cost/enabled=false is rejected at NB_EV_VALIDATE on + both daemons -- FRR has no mechanism to honour an auto-cost + off-switch; the validate callback returns NB_ERR_VALIDATION with + the documented error message. + + Uses `_mgmt_commit_attempt` so the rejected candidate edit is + aborted before returning, otherwise the bad `enabled=false` value + survives in the candidate datastore and poisons every subsequent + mgmt commit in the same test session. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + for proto, daemon in ( + ("ietf-ospf:ospfv2", "ospfd"), + ("ietf-ospf:ospfv3", "ospf6d"), + ): + instance = ( + "/ietf-routing:routing/control-plane-protocols/" + "control-plane-protocol[type='" + + proto + + "'][name='default']/ietf-ospf:ospf" + ) + out = _mgmt_commit_attempt( + r1, + "mgmt set-config {}/auto-cost/enabled false".format(instance), + ) + assert ( + "FRR auto-cost cannot be disabled" in out + ), "expected validate-time rejection for {}, got:\n{}".format(proto, out) + # And confirm nothing actually landed. + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "no auto-cost" not in running + ), "rejected auto-cost disable must not appear in running-config, got:\n{}".format( + running + ) + + +def test_ospf_auto_cost_cli_routes_through_yang(): + """Legacy `auto-cost reference-bandwidth N` / `no ...` on both + daemons drives the YANG /auto-cost/reference-bandwidth callback.""" + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + for router_block, daemon in ( + ("router ospf", "ospfd"), + ("router ospf6", "ospf6d"), + ): + r1.vtysh_cmd( + "configure terminal\n" + "{}\n" + " auto-cost reference-bandwidth 25000\n".format(router_block) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "auto-cost reference-bandwidth 25000" in running + ), "expected legacy CLI to land 25000 on {}, got:\n{}".format(daemon, running) + + r1.vtysh_cmd( + "configure terminal\n" + "{}\n" + " no auto-cost reference-bandwidth\n".format(router_block) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "auto-cost reference-bandwidth 25000" not in running + ), "'auto-cost reference-bandwidth 25000' should be gone after 'no', got:\n{}".format( + running + ) + + +def test_ospf_yang_mpls_te_router_addr_config(): + """per-instance /mpls/te-rid/ipv4-router-id round-trip via mgmtd + (OSPFv2 only -- ospf6d has no MPLS-TE module). + + MPLS-TE state is a process-wide global in FRR. The running-config + writer only emits `mpls-te router-address` once `mpls-te on` has + been set, so the test enables MPLS-TE up front and tears it down at + the end. Cleanup is mandatory: the global `OspfMplsTE` would + otherwise survive across tests and corrupt later assertions. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + instance = ( + "/ietf-routing:routing/control-plane-protocols/" + "control-plane-protocol[type='ietf-ospf:ospfv2']" + "[name='default']/ietf-ospf:ospf" + ) + + try: + r1.vtysh_cmd("configure terminal\n" "router ospf\n" " mpls-te on\n") + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/mpls/te-rid/ipv4-router-id 10.99.0.1\n" + "mgmt commit apply".format(instance) + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert ( + "mpls-te router-address 10.99.0.1" in running + ), "expected 'mpls-te router-address 10.99.0.1' after YANG set, got:\n{}".format( + running + ) + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}/mpls/te-rid/ipv4-router-id\n" + "mgmt commit apply".format(instance) + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert ( + "mpls-te router-address" not in running + ), "'mpls-te router-address' should be gone after YANG delete, got:\n{}".format( + running + ) + finally: + r1.vtysh_cmd("configure terminal\n" "router ospf\n" " no mpls-te\n") + + +def test_ospf_mpls_te_router_addr_cli_routes_through_yang(): + """Legacy `mpls-te router-address A.B.C.D` drives the YANG + /mpls/te-rid/ipv4-router-id callback (OSPFv2 only). + + The legacy CLI never exposed a `no mpls-te router-address` form + (operators clear the value by disabling MPLS-TE wholesale via + `no mpls-te`). We preserve that semantics; the targeted clear is + only available through `mgmt delete-config` and is exercised by + `test_ospf_yang_mpls_te_router_addr_config`. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + try: + r1.vtysh_cmd( + "configure terminal\n" + "router ospf\n" + " mpls-te on\n" + " mpls-te router-address 10.99.0.2\n" + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert ( + "mpls-te router-address 10.99.0.2" in running + ), "expected legacy CLI to land 10.99.0.2, got:\n{}".format(running) + finally: + r1.vtysh_cmd("configure terminal\n" "router ospf\n" " no mpls-te\n") + + +def test_ospf_yang_graceful_restart_config(): + """per-instance /graceful-restart/{enabled,restart-interval} round-trip + via mgmtd on both daemons. + + RFC 9129 defaults restart-interval to 120s; FRR's compile-time + defaults (`OSPF_DFLT_GRACE_INTERVAL` / `OSPF6_DFLT_GRACE_INTERVAL`) + are also 120s, so no deviation is needed. The two leaves are + independent in the YANG model but the legacy CLI ties them + together (`graceful-restart [grace-period N]`); both behaviours + are exercised here. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + for proto, daemon in ( + ("ietf-ospf:ospfv2", "ospfd"), + ("ietf-ospf:ospfv3", "ospf6d"), + ): + instance = ( + "/ietf-routing:routing/control-plane-protocols/" + "control-plane-protocol[type='" + + proto + + "'][name='default']/ietf-ospf:ospf" + ) + + # Set enabled=true with a non-default restart-interval. + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/graceful-restart/enabled true\n" + "mgmt set-config {}/graceful-restart/restart-interval 240\n" + "mgmt commit apply".format(instance, instance) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "graceful-restart grace-period 240" in running + ), "expected 'graceful-restart grace-period 240' on {}, got:\n{}".format( + daemon, running + ) + + # Drop the restart-interval alone -- enabled stays true, + # period restores to the FRR/RFC default 120s. The legacy + # writer collapses that to bare `graceful-restart`. + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}/graceful-restart/restart-interval\n" + "mgmt commit apply".format(instance) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "graceful-restart\n" in running and "grace-period" not in running + ), "expected bare 'graceful-restart' on {} after interval delete, got:\n{}".format( + daemon, running + ) + + # And tear it all down. + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}/graceful-restart/enabled\n" + "mgmt commit apply".format(instance) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "graceful-restart" not in running + ), "'graceful-restart' should be gone on {} after disable, got:\n{}".format( + daemon, running + ) + + +def test_ospf_yang_graceful_restart_helper_config(): + """per-instance /graceful-restart/{helper-enabled,helper-strict-lsa-checking} + round-trip via mgmtd on both daemons. + + FRR defaults strict-lsa-checking to true; the running-config + writer only emits a line when the leaf is false, so the test + asserts the line appears after a false write and disappears after + delete. Helper-enabled appears in running-config as + `graceful-restart helper enable` on both daemons. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + for proto, daemon in ( + ("ietf-ospf:ospfv2", "ospfd"), + ("ietf-ospf:ospfv3", "ospf6d"), + ): + instance = ( + "/ietf-routing:routing/control-plane-protocols/" + "control-plane-protocol[type='" + + proto + + "'][name='default']/ietf-ospf:ospf" + ) + + # Enable helper + relax strict-lsa-checking. v3's writer + # emits `lsa-check-disable`; v2's emits the negated positive + # form `no ... strict-lsa-checking` -- the assertion below + # accepts either. + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/graceful-restart/helper-enabled true\n" + "mgmt set-config {}/graceful-restart/helper-strict-lsa-checking false\n" + "mgmt commit apply".format(instance, instance) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "graceful-restart helper enable" in running + ), "expected helper enable line on {}, got:\n{}".format(daemon, running) + assert ( + "lsa-check-disable" in running + or "no graceful-restart helper strict-lsa-checking" in running + ), "expected relaxed strict-lsa-check on {}, got:\n{}".format(daemon, running) + + # Drop strict-lsa-checking alone -- helper stays on, strict + # check restores to FRR's default true (line disappears). + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}/graceful-restart/helper-strict-lsa-checking\n" + "mgmt commit apply".format(instance) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "lsa-check-disable" not in running + and "no graceful-restart helper strict-lsa-checking" not in running + ), "strict-lsa-check line should be gone after delete on {}, got:\n{}".format( + daemon, running + ) + + # And tear down helper. + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}/graceful-restart/helper-enabled\n" + "mgmt commit apply".format(instance) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "graceful-restart helper enable" not in running + ), "'graceful-restart helper enable' should be gone on {}, got:\n{}".format( + daemon, running + ) + + +def test_ospf_graceful_restart_helper_cli_routes_through_yang(): + """Legacy helper CLI on both daemons drives the YANG callbacks. + + v3's `lsa-check-disable` CLI is inverted from v2's strict-lsa- + checking form; the DEFPY_YANG shim flips the meaning before + enqueueing. Verified by toggling each daemon's relax form and + confirming the line appears, then dropping it and confirming the + line is gone. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + for router_block, daemon, relax, relax_off in ( + ( + "router ospf", + "ospfd", + "no graceful-restart helper strict-lsa-checking", + "graceful-restart helper strict-lsa-checking", + ), + ( + "router ospf6", + "ospf6d", + "graceful-restart helper lsa-check-disable", + "no graceful-restart helper lsa-check-disable", + ), + ): + try: + r1.vtysh_cmd( + "configure terminal\n" + "{}\n" + " graceful-restart helper enable\n" + " {}\n".format(router_block, relax) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "graceful-restart helper enable" in running + ), "expected helper enable on {}, got:\n{}".format(daemon, running) + assert ( + "lsa-check-disable" in running + or "no graceful-restart helper strict-lsa-checking" in running + ), "expected relaxed strict-lsa-check on {}, got:\n{}".format( + daemon, running + ) + + # Restore strict-lsa-check default. + r1.vtysh_cmd( + "configure terminal\n" "{}\n" " {}\n".format(router_block, relax_off) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "lsa-check-disable" not in running + and "no graceful-restart helper strict-lsa-checking" not in running + ), "strict-lsa-check should be back to default on {}, got:\n{}".format( + daemon, running + ) + finally: + r1.vtysh_cmd( + "configure terminal\n" + "{}\n" + " no graceful-restart helper enable\n".format(router_block) + ) + + +def test_ospf_graceful_restart_cli_routes_through_yang(): + """Legacy `graceful-restart [grace-period N]` / `no graceful-restart` + on both daemons drive the YANG /graceful-restart callbacks.""" + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + for router_block, daemon in ( + ("router ospf", "ospfd"), + ("router ospf6", "ospf6d"), + ): + try: + r1.vtysh_cmd( + "configure terminal\n" + "{}\n" + " graceful-restart grace-period 180\n".format(router_block) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "graceful-restart grace-period 180" in running + ), "expected legacy CLI to land GR period 180 on {}, got:\n{}".format( + daemon, running + ) + + r1.vtysh_cmd( + "configure terminal\n" + "{}\n" + " no graceful-restart\n".format(router_block) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "graceful-restart" not in running + ), "'graceful-restart' should be gone on {} after legacy 'no', got:\n{}".format( + daemon, running + ) + finally: + r1.vtysh_cmd( + "configure terminal\n" + "{}\n" + " no graceful-restart\n".format(router_block) + ) + + +def test_ospf_yang_prefix_suppression_config(): + """per-interface prefix-suppression round-trip via mgmtd (OSPFv2 only).""" + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + iface = ( + _yang_area_xpath("ietf-ospf:ospfv2", "0.0.0.0") + + "/interfaces/interface[name='r1-eth1']" + ) + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/prefix-suppression true\n" + "mgmt commit apply".format(iface) + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert ( + "ip ospf prefix-suppression" in running + ), "expected 'ip ospf prefix-suppression' after YANG set, got:\n{}".format(running) + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}/prefix-suppression\n" + "mgmt commit apply".format(iface) + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert ( + "ip ospf prefix-suppression" not in running + ), "'ip ospf prefix-suppression' should be gone after YANG delete, got:\n{}".format( + running + ) + + +def test_ospf_yang_interface_bfd_config(): + """per-interface BFD round-trip via mgmtd on both daemons. + + Exercises the four leaves under .../interface/bfd: enabled (the + presence-style on/off), local-multiplier, and the tx/rx interval + pair. The RFC unit is microseconds; FRR stores milliseconds and + NB_EV_VALIDATE rejects non-multiple-of-1000 values, so the test + writes whole-millisecond microsecond values (300000us = 300ms, + 400000us = 400ms). + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + for proto, daemon, expect_line in ( + ("ietf-ospf:ospfv2", "ospfd", "ip ospf bfd"), + ("ietf-ospf:ospfv3", "ospf6d", "ipv6 ospf6 bfd"), + ): + iface = ( + _yang_area_xpath(proto, "0.0.0.0") + "/interfaces/interface[name='r1-eth1']" + ) + + # Enable BFD with non-default timers. + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/bfd/enabled true\n" + "mgmt set-config {}/bfd/local-multiplier 5\n" + "mgmt set-config {}/bfd/desired-min-tx-interval 400000\n" + "mgmt set-config {}/bfd/required-min-rx-interval 400000\n" + "mgmt commit apply".format(iface, iface, iface, iface) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + expect_line in running + ), "expected '{}' on {} after YANG set, got:\n{}".format( + expect_line, daemon, running + ) + bfd_data = _yang_get_running_config(r1, "{}/bfd".format(iface)) + bfd_text = json.dumps(bfd_data) + assert "5" in bfd_text, ( + "same-transaction BFD multiplier was not retained for {}, got:\n{}" + ).format(proto, json.dumps(bfd_data, indent=2)) + assert "400000" in bfd_text, ( + "same-transaction BFD intervals were not retained for {}, got:\n{}" + ).format(proto, json.dumps(bfd_data, indent=2)) + + # Disable BFD by deleting the explicit enabled leaf. + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}/bfd/enabled\n" + "mgmt commit apply".format(iface) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + expect_line not in running + ), "'{}' should be gone on {} after YANG delete, got:\n{}".format( + expect_line, daemon, running + ) + + +def test_ospf_yang_interface_bfd_interval_rejection(): + """NB_EV_VALIDATE rejects BFD interval values that are not whole + milliseconds (multiple of 1000 us) or fall outside FRR's 50..60000 + ms grammar on both daemons. The unsupported single-interval BFD + form is also rejected by the FRR deviation module. Uses + `_mgmt_commit_attempt` so the rejected candidate is aborted and + does not poison subsequent commits. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + for proto, daemon, expect_line in ( + ("ietf-ospf:ospfv2", "ospfd", "ip ospf bfd"), + ("ietf-ospf:ospfv3", "ospf6d", "ipv6 ospf6 bfd"), + ): + iface = ( + _yang_area_xpath(proto, "0.0.0.0") + "/interfaces/interface[name='r1-eth1']" + ) + # Parameter leaves can be configured while BFD is disabled, but they + # do not independently activate BFD. + out = r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/bfd/local-multiplier 6\n" + "mgmt commit apply".format(iface) + ) + assert "commit failed" not in out.lower(), out + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + expect_line not in running + ), "'{}' should stay disabled after parameter-only YANG set, got:\n{}".format( + expect_line, running + ) + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/bfd/enabled true\n" + "mgmt commit apply".format(iface) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert expect_line in running, "BFD was not enabled for {}, got:\n{}".format( + proto, running + ) + bfd_data = _yang_get_running_config(r1, "{}/bfd".format(iface)) + assert "6" in json.dumps(bfd_data), ( + "staged BFD multiplier was not retained in running datastore " + "for {}, got:\n{}".format(proto, json.dumps(bfd_data, indent=2)) + ) + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}/bfd/enabled\n" + "mgmt commit apply".format(iface) + ) + + # Not a multiple of 1000us. + out = _mgmt_commit_attempt( + r1, + "mgmt set-config {}/bfd/enabled true\n" + "mgmt set-config {}/bfd/desired-min-tx-interval 300500".format( + iface, iface + ), + ) + assert ( + "Failed to edit configuration" in out + or "Couldn't apply changes" in out + or "Configuration failed" in out + or "commit failed" in out + ), "expected commit rejection on {}, got:\n{}".format(proto, out) + # FRR exposes the tx/rx interval form only, not the single + # min-interval case from ietf-bfd-types. + out = _mgmt_commit_attempt( + r1, + "mgmt set-config {}/bfd/min-interval 300000".format(iface), + ) + assert ( + "Failed to edit configuration" in out or "Couldn't apply changes" in out + ), "expected single-interval rejection on {}, got:\n{}".format(proto, out) + # Below 50ms. + out = _mgmt_commit_attempt( + r1, + "mgmt set-config {}/bfd/enabled true\n" + "mgmt set-config {}/bfd/required-min-rx-interval 1000".format(iface, iface), + ) + assert ( + "Failed to edit configuration" in out + or "Couldn't apply changes" in out + or "Configuration failed" in out + or "commit failed" in out + ), "expected commit rejection on {}, got:\n{}".format(proto, out) + + +def test_ospf_bfd_cli_routes_through_yang(): + """Legacy BFD CLI on both daemons drives the YANG callback when + the interface is in an area. Exercises both the bare enable form + and the N N N parametrised form.""" + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + for cli, daemon, expect in ( + ("ip ospf bfd", "ospfd", "ip ospf bfd"), + ( + "ipv6 ospf6 bfd", + "ospf6d", + "ipv6 ospf6 bfd", + ), + ): + try: + r1.vtysh_cmd( + "configure terminal\n" "interface r1-eth1\n" " {}\n".format(cli) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + expect in running + ), "expected '{}' on {} after CLI set, got:\n{}".format( + expect, daemon, running + ) + + # Parametrised form: detect-mult 5, rx 400, tx 400. + r1.vtysh_cmd( + "configure terminal\n" + "interface r1-eth1\n" + " {} 5 400 400\n".format(cli) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + expect in running + ), "expected '{}' on {} after param CLI set, got:\n{}".format( + expect, daemon, running + ) + + r1.vtysh_cmd( + "configure terminal\n" "interface r1-eth1\n" " no {}\n".format(cli) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + expect not in running + ), "'{}' should be gone on {} after legacy 'no', got:\n{}".format( + expect, daemon, running + ) + finally: + r1.vtysh_cmd( + "configure terminal\n" "interface r1-eth1\n" " no {}\n".format(cli) + ) + + +def test_ospf_yang_interface_static_neighbor_config(): + """per-interface /static-neighbors/neighbor round-trip via mgmtd + (OSPFv2 only -- ospf6d has no NBMA neighbour surface). + + RFC 9129 keys the list per-(area, interface, identifier). FRR's + NBMA table is per-(instance, addr); area/interface labels are + stored in the candidate but ignored on the FRR side. Exercises + create + priority + poll-interval + destroy, then verifies the + `cost` leaf is rejected by the deviation module. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + iface = ( + _yang_area_xpath("ietf-ospf:ospfv2", "0.0.0.0") + + "/interfaces/interface[name='r1-eth1']" + ) + nbr = iface + "/static-neighbors/neighbor[identifier='192.0.2.7']" + + try: + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/poll-interval 90\n" + "mgmt set-config {}/priority 7\n" + "mgmt commit apply".format(nbr, nbr) + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert ( + "neighbor 192.0.2.7" in running + ), "expected 'neighbor 192.0.2.7' after YANG set, got:\n{}".format(running) + assert ( + "poll-interval 90" in running + ), "expected static neighbour poll interval after YANG set, got:\n{}".format( + running + ) + assert ( + "priority 7" in running + ), "expected static neighbour priority after YANG set, got:\n{}".format(running) + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}\n" + "mgmt commit apply".format(nbr) + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert ( + "neighbor 192.0.2.7" not in running + ), "'neighbor 192.0.2.7' should be gone after YANG delete, got:\n{}".format( + running + ) + finally: + # Defensive cleanup in case the assert above tripped. + r1.vtysh_cmd("configure terminal\n" "router ospf\n" " no neighbor 192.0.2.7\n") + + +def test_ospf_yang_static_neighbor_duplicate_rejected(): + """The RFC keys static-neighbors per area/interface, but FRR's NBMA + table is per instance and address. Reject duplicate identifiers + rather than letting two YANG entries collapse onto one daemon + neighbour.""" + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + area = _yang_area_xpath("ietf-ospf:ospfv2", "0.0.0.0") + nbr1 = ( + area + + "/interfaces/interface[name='r1-eth1']" + + "/static-neighbors/neighbor[identifier='192.0.2.9']" + ) + nbr2 = ( + area + + "/interfaces/interface[name='r1-eth2']" + + "/static-neighbors/neighbor[identifier='192.0.2.9']" + ) + + out = _mgmt_commit_attempt( + r1, + "mgmt set-config {}/poll-interval 90\n" + "mgmt set-config {}/priority 7".format(nbr1, nbr2), + ) + assert ( + "already configured in this OSPF instance" in out + or "Couldn't apply changes" in out + or "Configuration failed" in out + or "commit failed" in out + ), "expected duplicate static-neighbor rejection, got:\n{}".format(out) + + +def test_ospf_yang_static_neighbor_partial_leaves(): + """Static-neighbor optional leaves default to FRR's NBMA values. + + RFC 9129 does not provide defaults for poll-interval or priority. + FRR deviates them to the daemon defaults so apply_finish can read a + complete settled subtree even when the operator creates a neighbour + with only one optional leaf. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + iface = ( + _yang_area_xpath("ietf-ospf:ospfv2", "0.0.0.0") + + "/interfaces/interface[name='r1-eth1']" + ) + nbr = iface + "/static-neighbors/neighbor[identifier='192.0.2.10']" + + try: + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/poll-interval 90\n" + "mgmt commit apply".format(nbr) + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert ( + "neighbor 192.0.2.10 poll-interval 90" in running + ), "expected poll-only static neighbour, got:\n{}".format(running) + assert ( + "neighbor 192.0.2.10 priority" not in running + ), "default static neighbour priority should not be written, got:\n{}".format( + running + ) + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/priority 7\n" + "mgmt delete-config {}/poll-interval\n" + "mgmt commit apply".format(nbr, nbr) + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert ( + "neighbor 192.0.2.10 priority 7" in running + ), "expected priority-only static neighbour, got:\n{}".format(running) + assert ( + "neighbor 192.0.2.10 poll-interval" not in running + ), "default static neighbour poll interval should not be written, got:\n{}".format( + running + ) + finally: + r1.vtysh_cmd("configure terminal\n" "router ospf\n" " no neighbor 192.0.2.10\n") + + +def test_ospf_yang_interface_authentication_keychain_config(): + """per-interface /authentication/ospfv2-key-chain (OSPFv2) and + /authentication/ospfv3-key-chain (OSPFv3) round-trip via mgmtd. + + Covers only the key-chain case of the RFC 9129 authentication + container -- the explicit-key triplet and OSPFv3 IPsec SA forms + map onto different FRR-side surfaces and are marked not-supported + by the FRR deviation module. + + RFC 9129 types the key-chain leaves as a leafref into + /key-chain:key-chains/key-chain/name, so libyang rejects writes + against not-yet-existing keychains. Create one via the CLI + before driving the YANG round-trip. The legacy CLI is more + relaxed but matching the YANG semantics is the principled + behaviour for this slice. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + # Set up a real keychain so the YANG leafref resolves. + r1.vtysh_cmd( + "configure terminal\n" "key chain kc-test\n" " key 1\n" " key-string secret\n" + ) + + try: + for proto, daemon, leaf, expect in ( + ( + "ietf-ospf:ospfv2", + "ospfd", + "ospfv2-key-chain", + "ip ospf authentication key-chain", + ), + ( + "ietf-ospf:ospfv3", + "ospf6d", + "ospfv3-key-chain", + "ipv6 ospf6 authentication keychain", + ), + ): + iface = ( + _yang_area_xpath(proto, "0.0.0.0") + + "/interfaces/interface[name='r1-eth1']" + ) + path = "{}/authentication/{}".format(iface, leaf) + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {} kc-test\n" + "mgmt commit apply".format(path) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "{} kc-test".format(expect) in running + ), "expected '{} kc-test' on {} after YANG set, got:\n{}".format( + expect, daemon, running + ) + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}\n" + "mgmt commit apply".format(path) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + expect not in running + ), "'{}' should be gone on {} after YANG delete, got:\n{}".format( + expect, daemon, running + ) + finally: + # Defensive cleanup: tear down auth + the helper keychain. + r1.vtysh_cmd( + "configure terminal\n" + "interface r1-eth1\n" + " no ip ospf authentication\n" + " no ipv6 ospf6 authentication keychain\n" + "exit\n" + "no key chain kc-test\n" + ) + + +def test_ospf_yang_authentication_unsupported_leaves_rejected(): + """Unsupported RFC authentication choices are rejected by deviation. + + Only the key-chain case is implemented through YANG. The explicit-key, + OSPFv2 authentication trailer and OSPFv3 IPsec SA branches remain + legacy-CLI-only, so mgmtd must reject YANG writes instead of accepting + config with no daemon-side effect. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + for proto, leaf, value in ( + ("ietf-ospf:ospfv2", "ospfv2-key-id", "1"), + ("ietf-ospf:ospfv2", "ospfv2-auth-trailer-rfc", "rfc7474"), + ("ietf-ospf:ospfv3", "sa", "SA-1"), + ("ietf-ospf:ospfv3", "ospfv3-sa-id", "1"), + ): + iface = _yang_area_xpath(proto, "0.0.0.0") + "/interfaces/interface[name='r1-eth1']" + path = "{}/authentication/{}".format(iface, leaf) + out = _mgmt_commit_attempt(r1, "mgmt set-config {} {}".format(path, value)) + _assert_mgmt_rejected(out, "unsupported authentication leaf {}".format(leaf)) + + +def test_ospf_yang_static_neighbor_cost_rejected(): + """The /static-neighbors/neighbor/cost leaf is marked not-supported + in the FRR deviations because FRR has no NBMA cost knob. mgmtd + must reject writes against it.""" + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + iface = ( + _yang_area_xpath("ietf-ospf:ospfv2", "0.0.0.0") + + "/interfaces/interface[name='r1-eth1']" + ) + nbr = iface + "/static-neighbors/neighbor[identifier='192.0.2.8']" + + out = _mgmt_commit_attempt( + r1, + "mgmt set-config {}/cost 50".format(nbr), + ) + assert ( + "Failed to edit configuration" in out + or "Couldn't apply changes" in out + or "Configuration failed" in out + or "commit failed" in out + ), "expected cost rejection, got:\n{}".format(out) + + +def test_ospf_prefix_suppression_cli_routes_through_yang(): + """Legacy `ip ospf prefix-suppression` / `no ip ospf prefix-suppression` + on r1-eth1 (no per-address override) drives the YANG callback.""" + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + r1.vtysh_cmd( + "configure terminal\n" "interface r1-eth1\n" " ip ospf prefix-suppression\n" + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert ( + "ip ospf prefix-suppression" in running + ), "expected 'ip ospf prefix-suppression' after CLI set, got:\n{}".format(running) + + r1.vtysh_cmd( + "configure terminal\n" "interface r1-eth1\n" " no ip ospf prefix-suppression\n" + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert ( + "ip ospf prefix-suppression" not in running + ), "'ip ospf prefix-suppression' should be gone after 'no', got:\n{}".format( + running + ) + + +def test_ospf_max_metric_router_lsa_admin_cli_routes_through_yang(): + """Legacy `max-metric router-lsa administrative` / `no max-metric + router-lsa administrative` continues to work via vtysh and drives + the YANG `/stub-router/trigger/always` create/destroy callbacks. + + RFC 9129's `/stub-router/trigger/always` is a presence container -- + a node with no value -- and FRR's mgmtd CLI `mgmt set-config WORD + VALUE` grammar requires a VALUE token, so direct mgmtd-side + round-trip testing of this leaf has no clean entry point. The + legacy CLI invokes the same NB_OP_CREATE / NB_OP_DESTROY + callbacks the YANG path would, so this test covers both.""" + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + r1.vtysh_cmd( + "configure terminal\n" "router ospf\n" " max-metric router-lsa administrative\n" + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert ( + "max-metric router-lsa administrative" in running + ), "expected 'max-metric router-lsa administrative' in ospfd running-config, got:\n{}".format( + running + ) + + r1.vtysh_cmd( + "configure terminal\n" + "router ospf\n" + " no max-metric router-lsa administrative\n" + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert ( + "max-metric router-lsa administrative" not in running + ), "max-metric router-lsa administrative should be gone after 'no ...', got:\n{}".format( + running + ) + + +def test_ospf_mpls_ldp_sync_cli_routes_through_yang(): + """Legacy `mpls ldp-sync` / `no mpls ldp-sync` continues to work + via vtysh and drives the YANG `/mpls/ldp/igp-sync` callback.""" + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + r1.vtysh_cmd("configure terminal\n" "router ospf\n" " mpls ldp-sync\n") + running = r1.vtysh_cmd("show running-config ospfd") + assert ( + "mpls ldp-sync" in running + ), "expected 'mpls ldp-sync' in ospfd running-config, got:\n{}".format(running) + + r1.vtysh_cmd("configure terminal\n" "router ospf\n" " no mpls ldp-sync\n") + running = r1.vtysh_cmd("show running-config ospfd") + assert ( + "mpls ldp-sync" not in running + ), "mpls ldp-sync should be gone after 'no mpls ldp-sync', got:\n{}".format(running) + + +def test_ospf_max_multipath_cli_routes_through_yang(): + """Legacy `maximum-paths N` continues to work via vtysh; values + within RFC 9129's 1..32 range route through the YANG + `/spf-control/paths` callback, the rest stay on the legacy + direct-mutation path.""" + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + for router_block, daemon in ( + ("router ospf", "ospfd"), + ("router ospf6", "ospf6d"), + ): + r1.vtysh_cmd( + "configure terminal\n" "{}\n" " maximum-paths 5\n".format(router_block) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "maximum-paths 5" in running + ), "expected 'maximum-paths 5' in {} running-config, got:\n{}".format( + daemon, running + ) + r1.vtysh_cmd( + "configure terminal\n" "{}\n" " no maximum-paths\n".format(router_block) + ) + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "maximum-paths 5" not in running + ), "maximum-paths 5 should be gone after 'no maximum-paths', got:\n{}".format( + running + ) + + +def test_ospf_yang_interface_type_and_passive_config(): + """interface-type and passive leaves round-trip via mgmtd.""" + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + # OSPFv2: r1-eth1 already in area 0 via the fixture. + iface_v2 = ( + _yang_area_xpath("ietf-ospf:ospfv2", "0.0.0.0") + + "/interfaces/interface[name='r1-eth1']" + ) + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/interface-type point-to-point\n" + "mgmt set-config {}/passive true\n" + "mgmt commit apply".format(iface_v2, iface_v2) + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert "ip ospf network point-to-point" in running, ( + "expected ip ospf network point-to-point in v2 running-config, got:\n" + running + ) + assert "ip ospf passive" in running, ( + "expected ip ospf passive in v2 running-config, got:\n" + running + ) + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}/interface-type\n" + "mgmt delete-config {}/passive\n" + "mgmt commit apply".format(iface_v2, iface_v2) + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert "ip ospf network point-to-point" not in running, ( + "ip ospf network should be cleared, got:\n" + running + ) + assert "ip ospf passive" not in running, ( + "ip ospf passive should be cleared, got:\n" + running + ) + + # OSPFv3: r1-eth1 already attached to area 0 via the fixture. + iface_v3 = ( + _yang_area_xpath("ietf-ospf:ospfv3", "0.0.0.0") + + "/interfaces/interface[name='r1-eth1']" + ) + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/interface-type point-to-point\n" + "mgmt set-config {}/passive true\n" + "mgmt commit apply".format(iface_v3, iface_v3) + ) + running = r1.vtysh_cmd("show running-config ospf6d") + assert "ipv6 ospf6 network point-to-point" in running, ( + "expected ipv6 ospf6 network point-to-point in v3 running-config, got:\n" + + running + ) + assert "ipv6 ospf6 passive" in running, ( + "expected ipv6 ospf6 passive in v3 running-config, got:\n" + running + ) + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt delete-config {}/interface-type\n" + "mgmt delete-config {}/passive\n" + "mgmt commit apply".format(iface_v3, iface_v3) + ) + running = r1.vtysh_cmd("show running-config ospf6d") + assert "ipv6 ospf6 network point-to-point" not in running, ( + "ipv6 ospf6 network should be cleared, got:\n" + running + ) + assert "ipv6 ospf6 passive" not in running, ( + "ipv6 ospf6 passive should be cleared, got:\n" + running + ) + + +def test_ospf_yang_area_ranges_config(): + """areas/area/ranges/range list + advertise/cost leaves via mgmtd. + + Creates a range under a stub area, sets advertise=false and a cost, + verifies `area X range PREFIX not-advertise` rendering. Then clears + via per-leaf revert and via list-entry destroy. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + # OSPFv2: create a fresh area 0.0.0.55 (to keep this orthogonal to + # the existing fixture areas), add a range with cost. + area_v2 = _yang_area_xpath("ietf-ospf:ospfv2", "0.0.0.55") + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/area-type stub-area\n" + "mgmt set-config {}/ranges/range[prefix='10.55.0.0/16']/cost 99\n" + "mgmt commit apply".format(area_v2, area_v2) + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert "area 0.0.0.55 range 10.55.0.0/16" in running, ( + "expected area-range line in v2 running-config, got:\n" + running + ) + assert "cost 99" in running, ( + "expected range cost 99 in v2 running-config, got:\n" + running + ) + + # Flip advertise to false + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/ranges/range[prefix='10.55.0.0/16']/advertise false\n" + "mgmt commit apply".format(area_v2) + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert "area 0.0.0.55 range 10.55.0.0/16 not-advertise" in running, ( + "expected not-advertise after advertise=false, got:\n" + running + ) + + # Tear down: delete the area entry (cascades range removal too) + _clear_yang_area(r1, "ietf-ospf:ospfv2", "0.0.0.55") + running = r1.vtysh_cmd("show running-config ospfd") + assert "0.0.0.55" not in running, ( + "area 0.0.0.55 should be fully removed, got:\n" + running + ) + + # OSPFv3: same shape with an IPv6 prefix. + area_v3 = _yang_area_xpath("ietf-ospf:ospfv3", "0.0.0.55") + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/area-type stub-area\n" + "mgmt set-config {}/ranges/range[prefix='2001:db8:55::/48']/cost 99\n" + "mgmt commit apply".format(area_v3, area_v3) + ) + running = r1.vtysh_cmd("show running-config ospf6d") + assert "area 0.0.0.55 range 2001:db8:55::/48" in running, ( + "expected v3 range line in running-config, got:\n" + running + ) + assert "cost 99" in running + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/ranges/range[prefix='2001:db8:55::/48']/advertise false\n" + "mgmt commit apply".format(area_v3) + ) + running = r1.vtysh_cmd("show running-config ospf6d") + assert "area 0.0.0.55 range 2001:db8:55::/48 not-advertise" in running, ( + "expected v3 not-advertise after advertise=false, got:\n" + running + ) + + _clear_yang_area(r1, "ietf-ospf:ospfv3", "0.0.0.55") + running = r1.vtysh_cmd("show running-config ospf6d") + assert "0.0.0.55" not in running, ( + "v3 area 0.0.0.55 should be fully removed, got:\n" + running + ) + + +def test_ospf_yang_area_summary_default_cost_config(): + "Verify areas/area[]/summary and /default-cost round-trip via mgmtd." + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + # OSPFv2: stub-area + summary=false + default-cost=42 should yield + # area 0.0.0.43 stub no-summary + # area 0.0.0.43 default-cost 42 + _set_yang_area_attrs( + r1, + "ietf-ospf:ospfv2", + "0.0.0.43", + [("area-type", "stub-area"), ("summary", "false"), ("default-cost", "42")], + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert "area 0.0.0.43 stub no-summary" in running, ( + "expected 'area 0.0.0.43 stub no-summary' in running-config, got:\n" + running + ) + assert "area 0.0.0.43 default-cost 42" in running, ( + "expected 'area 0.0.0.43 default-cost 42' in running-config, got:\n" + running + ) + _clear_yang_area(r1, "ietf-ospf:ospfv2", "0.0.0.43") + running = r1.vtysh_cmd("show running-config ospfd") + assert "0.0.0.43" not in running, ( + "area 0.0.0.43 should be fully removed after YANG delete, running:\n" + running + ) + + # OSPFv3: summary only (FRR ospf6d has no per-area stub default-cost + # surface, so the default-cost leaf is intentionally unimplemented on + # the v3 side; see ospf6_nb_config.c for the rationale). + _set_yang_area_attrs( + r1, + "ietf-ospf:ospfv3", + "0.0.0.43", + [("area-type", "stub-area"), ("summary", "false")], + ) + running = r1.vtysh_cmd("show running-config ospf6d") + assert "area 0.0.0.43 stub no-summary" in running, ( + "expected 'area 0.0.0.43 stub no-summary' in running-config, got:\n" + running + ) + _clear_yang_area(r1, "ietf-ospf:ospfv3", "0.0.0.43") + running = r1.vtysh_cmd("show running-config ospf6d") + assert "0.0.0.43" not in running, ( + "area 0.0.0.43 should be fully removed after YANG delete, running:\n" + running + ) + + +def _mgmt_commit_attempt(router, set_cmd): + """Run a mgmt set-config + commit and return the vty output. + + The caller verifies the rejection by confirming the candidate value + did NOT land in `show running-config` (more robust than parsing the + error string, which varies by libyang version). `mgmt commit abort` + follows the apply so a rejected candidate doesn't survive into the + next test's commit attempt. + """ + return router.vtysh_cmd( + "configure terminal file-lock\n" + "{}\n" + "mgmt commit apply\n" + "mgmt commit abort".format(set_cmd), + isjson=False, + ) + + +def _assert_mgmt_rejected(output, what): + assert ( + "Failed to edit configuration" in output + or "Couldn't apply changes" in output + or "Configuration failed" in output + or "commit failed" in output.lower() + ), "expected {} rejection, got:\n{}".format(what, output) + + +def test_ospf_yang_deviated_enabled_leaves_rejected(): + """The FRR deviation module marks OSPF enable switches not-supported. + + FRR has no independent protocol-level or per-interface OSPF + on/off switch: an instance exists when the control-plane-protocol + entry exists, and an interface participates when it is attached to + an area. Writes to the RFC 9129 `ospf/enabled` and + `interface/enabled` leaves must therefore fail instead of being + accepted into the candidate with no daemon-side effect. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + for proto, daemon in ( + ("ietf-ospf:ospfv2", "ospfd"), + ("ietf-ospf:ospfv3", "ospf6d"), + ): + instance = ( + "/ietf-routing:routing/control-plane-protocols/" + "control-plane-protocol[type='" + + proto + + "'][name='default']/ietf-ospf:ospf" + ) + out = _mgmt_commit_attempt( + r1, + "mgmt set-config {}/enabled false".format(instance), + ) + _assert_mgmt_rejected(out, "{}/enabled".format(proto)) + + iface = ( + _yang_area_xpath(proto, "0.0.0.0") + "/interfaces/interface[name='r1-eth1']" + ) + out = _mgmt_commit_attempt( + r1, + "mgmt set-config {}/enabled false".format(iface), + ) + _assert_mgmt_rejected(out, "{}/interface/enabled".format(proto)) + + running = r1.vtysh_cmd("show running-config {}".format(daemon)) + assert ( + "enabled false" not in running + ), "rejected enabled leaf must not land on {}, got:\n{}".format(daemon, running) + + + out = _mgmt_commit_attempt( + r1, + "mgmt set-config {}/mpls/te-rid/ipv6-router-id 2001:db8::1".format( + instance + ), + ) + _assert_mgmt_rejected(out, "ospf/mpls/te-rid/ipv6-router-id") + + out = _mgmt_commit_attempt( + r1, + "mgmt set-config {}/interfaces/interface[name='r1-eth1']" + "/instance-id 1".format(_yang_area_xpath("ietf-ospf:ospfv3", "0.0.0.0")), + ) + _assert_mgmt_rejected(out, "ospfv3 interface instance-id") + + out = _mgmt_commit_attempt( + r1, + "mgmt set-config {}/virtual-links/virtual-link" + "[transit-area-id='0.0.0.1'][router-id='1.1.1.1']" + "/hello-interval 10".format(_yang_area_xpath("ietf-ospf:ospfv2", "0.0.0.0")), + ) + _assert_mgmt_rejected(out, "ospfv2 virtual-link") + + +def test_ospf_yang_negative_missing_instance(): + """Reject YANG config that targets an OSPF instance the daemon doesn't have. + + The branch's resolve_instance helper rejects at NB_EV_VALIDATE when no + FRR-side ospf / ospf6 instance matches the control-plane-protocol name + key. Confirm a commit against name='ghost' does not land. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + ghost_v2 = ( + "/ietf-routing:routing/control-plane-protocols/" + "control-plane-protocol[type='ietf-ospf:ospfv2'][name='ghost']/" + "ietf-ospf:ospf/explicit-router-id" + ) + _mgmt_commit_attempt( + r1, + "mgmt set-config {} 9.9.9.9".format(ghost_v2), + ) + # The default instance is unchanged; the ghost one was never created. + running = r1.vtysh_cmd("show running-config ospfd") + assert "router-id 9.9.9.9" not in running, ( + "ghost router-id must not have landed, got:\n" + running + ) + + ghost_v3 = ( + "/ietf-routing:routing/control-plane-protocols/" + "control-plane-protocol[type='ietf-ospf:ospfv3'][name='ghost']/" + "ietf-ospf:ospf/explicit-router-id" + ) + _mgmt_commit_attempt( + r1, + "mgmt set-config {} 9.9.9.9".format(ghost_v3), + ) + running = r1.vtysh_cmd("show running-config ospf6d") + assert "router-id 9.9.9.9" not in running, ( + "ghost ospf6 router-id must not have landed, got:\n" + running + ) + + +def test_ospf_yang_negative_missing_interface(): + """Reject per-interface YANG config that names a non-existent interface. + + frr-deviations-ietf-routing-ospf relaxes the RFC 9129 interface-name + leafref, so libyang no longer rejects names that aren't in + /ietf-interfaces. The branch's resolve_interface helper restores that + rejection inside the callback at NB_EV_VALIDATE. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + bogus_v2 = ( + _yang_area_xpath("ietf-ospf:ospfv2", "0.0.0.0") + + "/interfaces/interface[name='r1-eth42']/cost" + ) + _mgmt_commit_attempt( + r1, + "mgmt set-config {} 7".format(bogus_v2), + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert "r1-eth42" not in running, ( + "interface r1-eth42 must not appear in running-config, got:\n" + running + ) + + bogus_v3 = ( + _yang_area_xpath("ietf-ospf:ospfv3", "0.0.0.0") + + "/interfaces/interface[name='r1-eth42']/cost" + ) + _mgmt_commit_attempt( + r1, + "mgmt set-config {} 7".format(bogus_v3), + ) + running = r1.vtysh_cmd("show running-config ospf6d") + assert "r1-eth42" not in running, ( + "interface r1-eth42 must not appear in v3 running-config, got:\n" + running + ) + + +def test_ospf_yang_negative_duplicate_area_interface(): + """Reject a candidate that attaches one interface to two OSPF areas.""" + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + other_v2 = ( + _yang_area_xpath("ietf-ospf:ospfv2", "0.0.0.99") + + "/interfaces/interface[name='r1-eth1']/cost" + ) + _mgmt_commit_attempt( + r1, + "mgmt set-config {} 7".format(other_v2), + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert "0.0.0.99" not in running, ( + "r1-eth1 must not attach to a second OSPFv2 area, got:\n" + running + ) + + other_v3 = ( + _yang_area_xpath("ietf-ospf:ospfv3", "0.0.0.99") + + "/interfaces/interface[name='r1-eth1']/cost" + ) + _mgmt_commit_attempt( + r1, + "mgmt set-config {} 7".format(other_v3), + ) + running = r1.vtysh_cmd("show running-config ospf6d") + assert "0.0.0.99" not in running, ( + "r1-eth1 must not attach to a second OSPFv3 area, got:\n" + running + ) + + +def test_ospf_yang_negative_default_cost_on_normal_area(): + """Reject default-cost on a non-stub / non-NSSA area. + + RFC 9129's `when` clause restricts default-cost to stub or NSSA areas; + the callback also rejects at VALIDATE as a defence-in-depth measure. + Area 0 is the backbone (normal); setting default-cost on it must fail. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + area_path = _yang_area_xpath("ietf-ospf:ospfv2", "0.0.0.0") + _mgmt_commit_attempt( + r1, + "mgmt set-config {}/default-cost 99".format(area_path), + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert "default-cost 99" not in running, ( + "default-cost 99 must not appear on the backbone area, got:\n" + running + ) + + +def test_ospf_yang_negative_v3_ospfv2_only_leaves_rejected(): + """Reject OSPFv3 writes that the daemon cannot support. + + RFC 9129 declares non-broadcast and hybrid; ospf6d only supports + broadcast, point-to-point and point-to-multipoint. The callback must + reject the unsupported enum values at VALIDATE. mgmtd also loads the + union of ospfd and ospf6d feature sets, so OSPFv2-only feature leaves + must not be accepted for an OSPFv3 instance. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + iface_v3 = ( + _yang_area_xpath("ietf-ospf:ospfv3", "0.0.0.0") + + "/interfaces/interface[name='r1-eth1']" + ) + _mgmt_commit_attempt( + r1, + "mgmt set-config {}/interface-type non-broadcast".format(iface_v3), + ) + running = r1.vtysh_cmd("show running-config ospf6d") + assert "non-broadcast" not in running, ( + "unsupported interface-type must not appear in v3 running-config, " + "got:\n" + running + ) + + instance_v3 = ( + "/ietf-routing:routing/control-plane-protocols/" + "control-plane-protocol[type='ietf-ospf:ospfv3'][name='default']" + "/ietf-ospf:ospf" + ) + for command, path in ( + ( + "mgmt set-config {}/address-family ipv4".format(instance_v3), + "{}/address-family".format(instance_v3), + ), + ( + "mgmt set-config {}/mpls/ldp/igp-sync true".format(instance_v3), + "{}/mpls/ldp/igp-sync".format(instance_v3), + ), + ( + "mgmt set-config {}/mpls/te-rid/ipv4-router-id 192.0.2.9".format( + instance_v3 + ), + "{}/mpls/te-rid/ipv4-router-id".format(instance_v3), + ), + ( + "mgmt set-config {}/default-cost 99".format( + _yang_area_xpath("ietf-ospf:ospfv3", "0.0.0.0") + ), + "{}/default-cost".format( + _yang_area_xpath("ietf-ospf:ospfv3", "0.0.0.0") + ), + ), + ( + "mgmt set-config {}/prefix-suppression true".format(iface_v3), + "{}/prefix-suppression".format(iface_v3), + ), + ( + "mgmt set-config {}/static-neighbors/neighbor" + "[identifier='2001:db8::99']/priority 1".format(iface_v3), + "{}/static-neighbors/neighbor[identifier='2001:db8::99']" + "/priority".format(iface_v3), + ), + ): + out = _mgmt_commit_attempt(r1, command) + _assert_mgmt_rejected(out, path) + + running = r1.vtysh_cmd("show running-config ospf6d") + assert "igp-sync" not in running, running + assert "192.0.2.9" not in running, running + assert "stub-router administrative" not in running, running + + +def test_ospf_yang_area_delete_clears_native_nssa_ranges(): + """Deleting a YANG NSSA area must also clear native NSSA range state.""" + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + area_path = _yang_area_xpath("ietf-ospf:ospfv2", "0.0.0.52") + + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/area-type nssa-area\n" + "mgmt commit apply".format(area_path) + ) + r1.vtysh_cmd( + "configure terminal\n" + "router ospf\n" + "area 0.0.0.52 nssa range 10.52.0.0/16 cost 52" + ) + + running = r1.vtysh_cmd("show running-config ospfd") + assert "area 0.0.0.52 nssa" in running, running + assert "area 0.0.0.52 nssa range 10.52.0.0/16 cost 52" in running, ( + "expected native NSSA range before YANG area delete, got:\n" + running + ) + + _clear_yang_area(r1, "ietf-ospf:ospfv2", "0.0.0.52") + running = r1.vtysh_cmd("show running-config ospfd") + assert "0.0.0.52" not in running, ( + "area 0.0.0.52 and its NSSA range must be gone after delete, got:\n" + running + ) + + +def test_ospf_yang_area_delete_recreate_cleanup(): + """Delete then recreate an area; per-leaf state must reset cleanly. + + Sets a non-default summary and default-cost on a stub area, then deletes + the area via the list-destroy path, then recreates it as a normal area, + and confirms the previous stub / default-cost state is gone. Catches + regressions where destroy callbacks leave stale per-leaf state. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + area_path = _yang_area_xpath("ietf-ospf:ospfv2", "0.0.0.51") + + # Set area-type=stub + summary=false + default-cost=77 in one commit. + # libyang materialises the area list entry implicitly when a child leaf + # is set; no separate list-create step is needed. + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/area-type stub-area\n" + "mgmt set-config {}/summary false\n" + "mgmt set-config {}/default-cost 77\n" + "mgmt commit apply".format(area_path, area_path, area_path) + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert "area 0.0.0.51 stub no-summary" in running, running + assert "area 0.0.0.51 default-cost 77" in running, running + + _clear_yang_area(r1, "ietf-ospf:ospfv2", "0.0.0.51") + running = r1.vtysh_cmd("show running-config ospfd") + assert "0.0.0.51" not in running, ( + "area 0.0.0.51 must be gone after delete, got:\n" + running + ) + + # Recreate by setting only area-type=normal-area; previous stub / + # default-cost must not bleed through. + r1.vtysh_cmd( + "configure terminal file-lock\n" + "mgmt set-config {}/area-type normal-area\n" + "mgmt commit apply".format(area_path) + ) + running = r1.vtysh_cmd("show running-config ospfd") + assert "area 0.0.0.51 stub" not in running, ( + "stub setting must not survive delete + recreate, got:\n" + running + ) + assert "area 0.0.0.51 default-cost" not in running, ( + "default-cost must not survive delete + recreate, got:\n" + running + ) + + _clear_yang_area(r1, "ietf-ospf:ospfv2", "0.0.0.51") + + # This is the last YANG mutation test before the read-only downstream + # tests (test_ospf_convergence, test_ospf_json, ...). The interface + # priority / passive / network-type round-trips in the preceding tests + # all bounce DR election on r1-eth1; OSPF leaves stale MAX_AGE + # Network LSAs behind that don't fully flush. Force a full + # reconvergence so downstream tests see deterministic LSDB state. + _force_ospf_reconvergence_to_steady_state() + + +def _send_ietf_ospf_rpc(router, rpc_xpath, input_json): + """Issue a mgmt rpc and surface the daemon-side error if any. + + mgmtd reports backend errors as `% ` lines in the vty output. + Catch any '%' or 'can\\'t' / 'error' / 'fail' / 'no backends' marker so + a parse failure or unknown-xpath dispatch doesn't slip past the + convergence checks (OSPF was already Full before the RPC, so a no-op + RPC would otherwise look like a pass). + """ + out = router.vtysh_cmd( + "configure terminal\nmgmt rpc {} json {}".format(rpc_xpath, input_json) + ) + lowered = out.lower() + bad = ("% ", "can't", "error", "fail", "no backends", "invalid") + for marker in bad: + assert ( + marker not in lowered + ), "RPC {} on {} returned an error (matched '{}'):\n{}".format( + rpc_xpath, router.name, marker, out + ) + return out + + +def test_ospf_yang_clear_neighbor_rpc(): + """RFC 9129 /ietf-ospf:clear-neighbor round-trip on both daemons. + + Both ospfd and ospf6d register the same RPC xpath; mgmtd fans the call + out to each backend. The daemon that owns the named instance kills its + neighbors, the other daemon returns silently. We verify the kill by + waiting for OSPF to renegotiate back to Full. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + # Pre-state: r1 must already be Full with r2 on both daemons. + _expect_ospfv2_neighbor_full("r1", "10.0.255.2") + _expect_ospfv3_neighbor_full("r1", "10.0.255.2") + + # Whole-instance reset (no `interface` filter). The libyang RPC parser + # expects the input leaves wrapped in the RPC name, namespace-qualified. + _send_ietf_ospf_rpc( + r1, + "/ietf-ospf:clear-neighbor", + '{"ietf-ospf:clear-neighbor":{"routing-protocol-name":"default"}}', + ) + + # The kill drives every neighbor to Down; OSPF must renegotiate. + _expect_ospfv2_neighbor_full("r1", "10.0.255.2") + _expect_ospfv3_neighbor_full("r1", "10.0.255.2") + + # Per-interface reset using the RFC's optional `interface` input. r1-eth1 + # is the interface r1 shares with r2 on both v2 and v3. + _send_ietf_ospf_rpc( + r1, + "/ietf-ospf:clear-neighbor", + '{"ietf-ospf:clear-neighbor":{"routing-protocol-name":"default","interface":"r1-eth1"}}', + ) + + _expect_ospfv2_neighbor_full("r1", "10.0.255.2") + _expect_ospfv3_neighbor_full("r1", "10.0.255.2") + + +def test_ospf_yang_clear_database_rpc(): + """RFC 9129 /ietf-ospf:clear-database round-trip on both daemons. + + Maps to `ospf_process_reset` / `ospf6_process_reset`. Flushes self- + originated LSAs, drops all adjacencies, rebuilds. Verify by waiting + for r1 to come back Full with r2 on both daemons. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + _expect_ospfv2_neighbor_full("r1", "10.0.255.2") + _expect_ospfv3_neighbor_full("r1", "10.0.255.2") + + _send_ietf_ospf_rpc( + r1, + "/ietf-ospf:clear-database", + '{"ietf-ospf:clear-database":{"routing-protocol-name":"default"}}', + ) + + _expect_ospfv2_neighbor_full("r1", "10.0.255.2") + _expect_ospfv3_neighbor_full("r1", "10.0.255.2") + + +def test_ospf_yang_rpc_unknown_instance_silent(): + """RPC against an instance name no daemon owns must return silently. + + Mirrors the non-owner case: both daemons' handlers look up the named + instance and, if not found, return NB_OK. mgmtd surfaces the combined + success. No error to the caller, no state change on r1. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + _expect_ospfv2_neighbor_full("r1", "10.0.255.2") + + _send_ietf_ospf_rpc( + r1, + "/ietf-ospf:clear-neighbor", + '{"ietf-ospf:clear-neighbor":{"routing-protocol-name":"does-not-exist"}}', + ) + + # r1's v2 neighbor must still be Full -- the RPC did not touch it. + out = r1.vtysh_cmd("show ip ospf neighbor json", isjson=True) + nbr = out.get("neighbors", {}).get("10.0.255.2", []) + assert ( + nbr and nbr[0].get("converged") == "Full" + ), "neighbor was disturbed by an RPC against an unknown instance:\n{}".format(out) + + +def test_ospf_yang_clear_neighbor_rpc_unknown_interface(): + """clear-neighbor with `interface` for an interface that exists on the + box but isn't in the OSPF instance must surface ospf-interface-not-found. + + r1 has lo (loopback) configured as an interface but it isn't bound into + the OSPFv2 area, so ospfd's lookup returns NULL and the handler returns + NB_ERR_RESOURCE with the RFC-mandated app-tag. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + # lo is not in any OSPF area on r1; ospfd_ietf_lookup_oi returns NULL + # for it. ospf6d may also error on the same input. Either backend + # returning NB_ERR_RESOURCE surfaces an error in the vty output -- which + # is exactly what we want to verify. + out = r1.vtysh_cmd( + "configure terminal\nmgmt rpc /ietf-ospf:clear-neighbor json " + '{"ietf-ospf:clear-neighbor":{"routing-protocol-name":"default","interface":"lo"}}' + ) + assert ( + "ospf-interface-not-found" in out.lower() or "error" in out.lower() + ), "expected error for unknown interface, got:\n{}".format(out) + + # The clear-database RPC above flushed and re-originated r1's LSAs. + # Adjacencies come back Full quickly but the rest of the area takes + # longer to re-learn the flushed LSAs. Downstream read-only tests + # (test_ospf_json, test_ospf_kernel_route) compare full LSDB snapshots + # against a stable expected baseline, so force a clean reconvergence + # before handing off to them. + _force_ospf_reconvergence_to_steady_state() + + +def _grep_daemon_log(router, daemon, pattern, since_marker=None): + """Return matching lines from the daemon's log under topotest's tmpdir. + + `since_marker` skips everything in the log before the first line containing + that string; used to scope the search to the current test window. + """ + log_path = os.path.join(router.logdir, router.name, "{}.log".format(daemon)) + try: + with open(log_path, "r") as fh: + lines = fh.readlines() + except OSError: + return [] + if since_marker: + for i, line in enumerate(lines): + if since_marker in line: + lines = lines[i:] + break + else: + return [] + return [ln for ln in lines if pattern in ln] + + +def test_ospf_yang_nbr_state_change_notification(): + """RFC 9129 /ietf-ospf:nbr-state-change emitted on both daemons. + + The notification handler hooks ospf_nsm_change (v2) / ospf6_neighbor_change + (v3) and dispatches a YANG notification with state + neighbour identity. + There is no in-test subscriber, but `nb_notification_send` and the + handler itself both emit DEBUG log lines that prove the notification + fired -- so we enable `debug northbound notifications` on r1, trigger + a neighbour reset via the existing clear-neighbor RPC, and grep for + the markers. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + _expect_ospfv2_neighbor_full("r1", "10.0.255.2") + _expect_ospfv3_neighbor_full("r1", "10.0.255.2") + + # Drop a marker into the log so the grep below ignores anything earlier. + marker = "=== test_ospf_yang_nbr_state_change_notification BEGIN ===" + r1.vtysh_cmd("send log level info {}".format(marker)) + + r1.vtysh_cmd( + "configure terminal\n" + "debug northbound notifications\n" + "do log levels file file:ospfd.log debug\n" + ) + r1.vtysh_cmd( + "configure terminal\nmgmt rpc /ietf-ospf:clear-neighbor json " + '{"ietf-ospf:clear-neighbor":{"routing-protocol-name":"default"}}' + ) + + def saw_v2_notif(): + # Either the handler's own _dbg marker or the libfrr-side + # "northbound notification: /ietf-ospf:nbr-state-change" line is + # sufficient evidence; we accept either. + hits = _grep_daemon_log(r1, "ospfd", "OSPF-NOTIF", marker) + hits += _grep_daemon_log( + r1, "ospfd", "northbound notification: /ietf-ospf:nbr-state-change", marker + ) + return None if hits else "no v2 nbr-state-change notification log" + + def saw_v3_notif(): + hits = _grep_daemon_log(r1, "ospf6d", "OSPF6-NOTIF", marker) + hits += _grep_daemon_log( + r1, + "ospf6d", + "northbound notification: /ietf-ospf:nbr-state-change", + marker, + ) + return None if hits else "no v3 nbr-state-change notification log" + + _, result = topotest.run_and_expect(saw_v2_notif, None, count=60, wait=0.5) + assert result is None, "v2 nbr-state-change notification was not emitted" + + _, result = topotest.run_and_expect(saw_v3_notif, None, count=60, wait=0.5) + assert result is None, "v3 nbr-state-change notification was not emitted" + + _expect_ospfv2_neighbor_full("r1", "10.0.255.2") + _expect_ospfv3_neighbor_full("r1", "10.0.255.2") + + # Restore steady-state so downstream LSDB-snapshot tests stay deterministic. + _force_ospf_reconvergence_to_steady_state() + + +def test_ospf_yang_if_state_change_notification(): + """RFC 9129 /ietf-ospf:if-state-change emitted on both daemons. + + Toggle r1-eth1 down/up to force an ISM transition on both ospfd and + ospf6d, then verify both daemon logs show the notification marker. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + _expect_ospfv2_neighbor_full("r1", "10.0.255.2") + _expect_ospfv3_neighbor_full("r1", "10.0.255.2") + + marker = "=== test_ospf_yang_if_state_change_notification BEGIN ===" + r1.vtysh_cmd("send log level info {}".format(marker)) + r1.vtysh_cmd("configure terminal\ndebug northbound notifications\n") + + # Bounce r1-eth1 to force a v2 ISM transition (Down -> ... back to DR-elect) + # and a v3 ISM transition (Down -> ... back). Kernel link toggle inside + # the netns is the most direct trigger. + r1.cmd("ip link set dev r1-eth1 down") + time.sleep(1) + r1.cmd("ip link set dev r1-eth1 up") + + def saw_v2_if_notif(): + hits = _grep_daemon_log(r1, "ospfd", "OSPF-NOTIF", marker) + hits += _grep_daemon_log( + r1, "ospfd", "northbound notification: /ietf-ospf:if-state-change", marker + ) + return None if hits else "no v2 if-state-change notification log" + + def saw_v3_if_notif(): + hits = _grep_daemon_log(r1, "ospf6d", "OSPF6-NOTIF", marker) + hits += _grep_daemon_log( + r1, + "ospf6d", + "northbound notification: /ietf-ospf:if-state-change", + marker, + ) + return None if hits else "no v3 if-state-change notification log" + + _, result = topotest.run_and_expect(saw_v2_if_notif, None, count=60, wait=0.5) + assert result is None, "v2 if-state-change notification was not emitted" + + _, result = topotest.run_and_expect(saw_v3_if_notif, None, count=60, wait=0.5) + assert result is None, "v3 if-state-change notification was not emitted" + + _expect_ospfv2_neighbor_full("r1", "10.0.255.2") + _expect_ospfv3_neighbor_full("r1", "10.0.255.2") + + _force_ospf_reconvergence_to_steady_state() + + +def test_ospf_yang_if_config_error_notification(): + """RFC 9129 /ietf-ospf:if-config-error emitted on Hello mismatch. + + The emit sites are in the normal OSPF Hello receive paths. Change r2's + hello interval on the shared segment so r1 rejects the packet and emits + if-config-error, then restore the interval and reconverge the topology. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + r2 = tgen.gears["r2"] + + _expect_ospfv2_neighbor_full("r1", "10.0.255.2") + _expect_ospfv3_neighbor_full("r1", "10.0.255.2") + + marker = "=== test_ospf_yang_if_config_error_notification BEGIN ===" + r1.vtysh_cmd("send log level info {}".format(marker)) + r1.vtysh_cmd( + "configure terminal\n" + "debug northbound notifications\n" + "do log levels file file:ospfd.log debug\n" + "do log levels file file:ospf6d.log debug\n" + ) + + try: + r2.vtysh_cmd( + "configure terminal\n" + "interface r2-eth1\n" + " ip ospf hello-interval 3\n" + " ipv6 ospf6 hello-interval 3\n" + ) + + def saw_v2_config_error(): + hits = _grep_daemon_log( + r1, + "ospfd", + "northbound notification: /ietf-ospf:if-config-error", + marker, + ) + hits += [ + line + for line in _grep_daemon_log(r1, "ospfd", "OSPF-NOTIF", marker) + if "if-config-error" in line + ] + return None if hits else "no v2 if-config-error notification log" + + def saw_v3_config_error(): + hits = _grep_daemon_log( + r1, + "ospf6d", + "northbound notification: /ietf-ospf:if-config-error", + marker, + ) + hits += [ + line + for line in _grep_daemon_log(r1, "ospf6d", "OSPF6-NOTIF", marker) + if "if-config-error" in line + ] + return None if hits else "no v3 if-config-error notification log" + + _, result = topotest.run_and_expect( + saw_v2_config_error, None, count=60, wait=0.5 + ) + assert result is None, "v2 if-config-error notification was not emitted" + + _, result = topotest.run_and_expect( + saw_v3_config_error, None, count=60, wait=0.5 + ) + assert result is None, "v3 if-config-error notification was not emitted" + finally: + r2.vtysh_cmd( + "configure terminal\n" + "interface r2-eth1\n" + " ip ospf hello-interval 2\n" + " ipv6 ospf6 hello-interval 2\n" + ) + + _expect_ospfv2_neighbor_full("r1", "10.0.255.2") + _expect_ospfv3_neighbor_full("r1", "10.0.255.2") + _force_ospf_reconvergence_to_steady_state() + + +def test_ospf_yang_nbr_state_change_lifecycle_down_notification(): + """RFC 9129 /ietf-ospf:nbr-state-change fires on tear-down too. + + FRR's NSM has two terminal lifecycle states (NSM_DependUpon, NSM_Deleted) + that have no protocol existence in RFC 9129's nbr-state-type enum. The + state-map folds both into RFC `down` so every tear-down (KillNbr, dead + timer, explicit clear) stays observable. Without the fold, the v2 hook + silently early-returns on those states and the subscriber never sees + neighbours leave Full. + + Trigger a clear-neighbor, which walks the v2 NSM through Full -> + Deleted -> Init -> ... -> Full. The handler's OSPF-NOTIF debug line + only fires when the state-map produced a valid YANG code, so the + presence of an OSPF-NOTIF entry mentioning "Deleted" proves the fold + is wired up. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + _expect_ospfv2_neighbor_full("r1", "10.0.255.2") + + marker = "=== test_ospf_yang_nbr_state_change_lifecycle_down_notification BEGIN ===" + r1.vtysh_cmd("send log level info {}".format(marker)) + r1.vtysh_cmd( + "configure terminal\n" + "debug northbound notifications\n" + "do log levels file file:ospfd.log debug\n" + ) + r1.vtysh_cmd( + "configure terminal\nmgmt rpc /ietf-ospf:clear-neighbor json " + '{"ietf-ospf:clear-neighbor":{"routing-protocol-name":"default"}}' + ) + + def saw_deleted_notif(): + hits = _grep_daemon_log(r1, "ospfd", "OSPF-NOTIF", marker) + for line in hits: + if "Deleted" in line: + return None + return "no v2 nbr-state-change notification for NSM_Deleted" + + _, result = topotest.run_and_expect(saw_deleted_notif, None, count=60, wait=0.5) + assert result is None, ( + "v2 nbr-state-change was not emitted for tear-down (NSM_Deleted); " + "state-map fold may be missing" + ) + + _expect_ospfv2_neighbor_full("r1", "10.0.255.2") + _expect_ospfv3_neighbor_full("r1", "10.0.255.2") + + _force_ospf_reconvergence_to_steady_state() + + +def test_ospf6_yang_nbr_state_change_admin_down_notification(): + """RFC 9129 /ietf-ospf:nbr-state-change fires on v3 admin-down tear-down. + + OSPFv3's nbr-state map is total over the enum, but the ospf6d teardown + paths in ospf6_interface.c (interface destroy, interface state reset + via admin-down) historically called ospf6_neighbor_delete directly + without first emitting a state change to OSPF6_NEIGHBOR_DOWN. The + ospf6_neighbor_change hook never fired, so subscribers missed every + admin-down event. Both call sites now walk through DOWN before + delete; this test admin-shuts r1-eth1 and asserts the resulting v3 + nbr-state-change notification reaches the log. + """ + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + _expect_ospfv2_neighbor_full("r1", "10.0.255.2") + _expect_ospfv3_neighbor_full("r1", "10.0.255.2") + + marker = "=== test_ospf6_yang_nbr_state_change_admin_down_notification BEGIN ===" + r1.vtysh_cmd("send log level info {}".format(marker)) + r1.vtysh_cmd( + "configure terminal\n" + "debug northbound notifications\n" + "do log levels file file:ospf6d.log debug\n" + ) + + # Admin-down the link. ospf6_interface_state_change runs the reset + # path which deletes neighbours; the new DOWN state transition there + # makes the YANG hook fire. + r1.cmd("ip link set dev r1-eth1 down") + + def saw_v3_down_notif(): + hits = _grep_daemon_log(r1, "ospf6d", "OSPF6-NOTIF", marker) + hits += _grep_daemon_log( + r1, + "ospf6d", + "northbound notification: /ietf-ospf:nbr-state-change", + marker, + ) + return None if hits else "no v3 nbr-state-change on admin-down" + + _, result = topotest.run_and_expect(saw_v3_down_notif, None, count=60, wait=0.5) + + # Restore the link before asserting so a failure does not leave the + # topology in a broken state for downstream tests. + r1.cmd("ip link set dev r1-eth1 up") + + assert result is None, ( + "v3 nbr-state-change was not emitted on admin-down; " + "ospf6_interface.c tear-down may not be transitioning through DOWN" + ) + + _expect_ospfv2_neighbor_full("r1", "10.0.255.2") + _expect_ospfv3_neighbor_full("r1", "10.0.255.2") + + _force_ospf_reconvergence_to_steady_state() + + def test_ospf_convergence(): "Test OSPF daemon convergence" tgen = get_topogen() diff --git a/tests/topotests/ospf_yang_startup_config/__init__.py b/tests/topotests/ospf_yang_startup_config/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/topotests/ospf_yang_startup_config/r1/ospf6d.conf b/tests/topotests/ospf_yang_startup_config/r1/ospf6d.conf new file mode 100644 index 000000000000..7f4c4d95af77 --- /dev/null +++ b/tests/topotests/ospf_yang_startup_config/r1/ospf6d.conf @@ -0,0 +1,5 @@ +! +router ospf6 + ospf6 router-id 10.0.255.1 + area 0.0.0.62 stub no-summary +! diff --git a/tests/topotests/ospf_yang_startup_config/r1/ospfd.conf b/tests/topotests/ospf_yang_startup_config/r1/ospfd.conf new file mode 100644 index 000000000000..7d934aa39b66 --- /dev/null +++ b/tests/topotests/ospf_yang_startup_config/r1/ospfd.conf @@ -0,0 +1,6 @@ +! +router ospf + ospf router-id 10.0.255.1 + area 0.0.0.61 default-cost 31 + area 0.0.0.61 stub no-summary +! diff --git a/tests/topotests/ospf_yang_startup_config/r1/zebra.conf b/tests/topotests/ospf_yang_startup_config/r1/zebra.conf new file mode 100644 index 000000000000..cdf4cb4feba5 --- /dev/null +++ b/tests/topotests/ospf_yang_startup_config/r1/zebra.conf @@ -0,0 +1 @@ +! diff --git a/tests/topotests/ospf_yang_startup_config/test_ospf_yang_startup_config.py b/tests/topotests/ospf_yang_startup_config/test_ospf_yang_startup_config.py new file mode 100644 index 000000000000..27c74cd38b6a --- /dev/null +++ b/tests/topotests/ospf_yang_startup_config/test_ospf_yang_startup_config.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: ISC +# +# Copyright (c) 2026 by +# Eric Parsonage +# + +""" +test_ospf_yang_startup_config.py: Test OSPF YANG startup config batching. +""" + +import os +import sys + +import pytest + +CWD = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(os.path.join(CWD, "../")) + +# pylint: disable=C0413 +from lib.topogen import Topogen, TopoRouter, get_topogen + +pytestmark = [pytest.mark.ospfd, pytest.mark.ospf6d] + + +def build_topo(tgen): + "Build a single-router topology for startup config parsing." + tgen.add_router("r1") + + +def setup_module(mod): + "Sets up the pytest environment." + tgen = Topogen(build_topo, mod.__name__) + tgen.start_topology() + + r1 = tgen.gears["r1"] + r1.load_config(TopoRouter.RD_ZEBRA, os.path.join(CWD, "r1/zebra.conf")) + r1.load_config(TopoRouter.RD_OSPF, os.path.join(CWD, "r1/ospfd.conf")) + r1.load_config(TopoRouter.RD_OSPF6, os.path.join(CWD, "r1/ospf6d.conf")) + + tgen.start_router() + + +def teardown_module(): + "Teardown the pytest environment." + tgen = get_topogen() + tgen.stop_topology() + + +def test_ospf_yang_startup_config_file_batching(): + "Verify direct daemon startup config loads commit cross-leaf OSPF changes." + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip("skipped because of router(s) failure") + + r1 = tgen.gears["r1"] + + running = r1.vtysh_cmd("show running-config ospfd") + assert "area 0.0.0.61 stub no-summary" in running, running + assert "area 0.0.0.61 default-cost 31" in running, running + + running = r1.vtysh_cmd("show running-config ospf6d") + assert "area 0.0.0.62 stub no-summary" in running, running diff --git a/yang/ietf/frr-deviations-ietf-routing-ospf.yang b/yang/ietf/frr-deviations-ietf-routing-ospf.yang new file mode 100644 index 000000000000..c12144d37bd1 --- /dev/null +++ b/yang/ietf/frr-deviations-ietf-routing-ospf.yang @@ -0,0 +1,392 @@ +// SPDX-License-Identifier: BSD-2-Clause +module frr-deviations-ietf-routing-ospf { + yang-version 1.1; + namespace "http://frrouting.org/yang/frr-deviations-ietf-routing-ospf"; + prefix frr-deviations-ietf-routing-ospf; + + import ietf-routing { + prefix ietf-routing; + } + import ietf-ospf { + prefix ospf; + } + + organization + "FRRouting"; + + contact + "FRR Users List: + FRR Development List: "; + + description + "This module defines the ietf-routing and ietf-ospf deviations + needed by OSPF daemons using the RFC 9129 ietf-ospf NMDA tree. + + Copyright (C) 2026 Eric Parsonage"; + + revision 2026-05-24 { + description + "Initial revision."; + reference + "RFC 8349: A YANG Data Model for Routing Management. + RFC 9129: YANG Data Model for the OSPF Protocol."; + } + + deviation "/ietf-routing:routing-state" { + deviate not-supported; + } + + /* + * FRR's OSPF interface timer defaults are hello-interval 10 seconds and + * dead-interval 40 seconds for both ospfd and ospf6d. RFC 9129 leaves the + * leaves without defaults, but its dead-interval `must` expression compares + * against hello-interval. Without an FRR default, a legacy CLI sequence such + * as `ip ospf dead-interval 15` followed by `no ip ospf hello-interval` + * cannot be represented: removing the hello leaf makes the still-explicit + * dead leaf fail validation even though FRR correctly falls back to hello 10. + */ + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:areas/ospf:area" + + "/ospf:interfaces/ospf:interface/ospf:hello-interval" { + deviate add { + default "10"; + } + } + + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:areas/ospf:area" + + "/ospf:interfaces/ospf:interface/ospf:dead-interval" { + deviate add { + default "40"; + } + } + + /* + * RFC 9129 models interface auto-cost as a two-leaf container with a + * `when "../enabled = 'true'"` constraint on reference-bandwidth. + * FRR has no on/off switch: it always computes cost from + * `reference-bandwidth / interface speed` unless the operator sets + * an explicit per-interface cost. Pinning the default to "true" + * keeps the when-clause satisfied for every YANG client that wants + * to set reference-bandwidth without first toggling enabled, and + * matches the FRR runtime semantics exactly. Setting enabled to + * false is rejected by the NB_EV_VALIDATE callback because FRR has + * no mechanism to honour it short of dropping all cost computation. + */ + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:auto-cost/ospf:enabled" { + deviate add { + default "true"; + } + } + + /* + * RFC 9129 keys areas/area/interfaces/interface by a YANG + * leafref into /ietf-interfaces:interfaces/interface/name. In FRR, + * interfaces are discovered from the kernel and exposed by zebra + * as operational state only -- they are never authored via YANG + * config. mgmtd validates the leafref against the candidate + * datastore at commit time, which doesn't contain the operational + * interface list, so any reference to a real interface fails + * libyang's leafref check. + * + * Loosen the typedef ref to a plain string so config commits + * referencing an existing kernel interface succeed. Validation + * that the named interface actually exists still happens in the + * NB callback via if_lookup_by_name; we just give up the + * libyang-level cross-module integrity guarantee that vendors + * with a YANG-managed interface inventory get for free. + */ + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:areas/ospf:area" + + "/ospf:interfaces/ospf:interface/ospf:name" { + deviate replace { + type string; + } + } + + /* + * RFC 9129's per-interface BFD container uses + * bfd-types:client-cfg-parms, whose default tx/rx intervals are + * 1000000 us (1 s). FRR's compile-time defaults + * (BFD_DEF_MIN_TX / BFD_DEF_MIN_RX) are both 300 ms. Pin the + * deviated defaults to FRR's values so an operator who writes + * only `/bfd/enabled = true` via mgmtd gets the same end state as + * `ip ospf bfd` from the CLI. + */ + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:areas/ospf:area" + + "/ospf:interfaces/ospf:interface" + + "/ospf:bfd/ospf:interval-config-type" + + "/ospf:tx-rx-intervals" + + "/ospf:desired-min-tx-interval" { + deviate replace { + default "300000"; + } + } + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:areas/ospf:area" + + "/ospf:interfaces/ospf:interface" + + "/ospf:bfd/ospf:interval-config-type" + + "/ospf:tx-rx-intervals" + + "/ospf:required-min-rx-interval" { + deviate replace { + default "300000"; + } + } + + /* + * FRR's BFD library exposes only the (tx, rx) interval form, not + * the single-interval case from ietf-bfd-types. Mark its only + * writable leaf not-supported so mgmtd rejects writes against it + * cleanly instead of letting them silently accumulate in the + * candidate datastore with no callback to fire. + */ + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:areas/ospf:area" + + "/ospf:interfaces/ospf:interface" + + "/ospf:bfd/ospf:interval-config-type" + + "/ospf:single-interval/ospf:min-interval" { + deviate not-supported; + } + + /* + * FRR's NBMA neighbour table (struct ospf_nbr_nbma) carries only + * `priority` and `v_poll`. RFC 9129 leaves both without defaults, + * while FRR creates a fresh NBMA neighbour with priority 0 and poll + * interval 60 seconds. Pin those defaults so a YANG client can create + * a static neighbour with only an identifier, or with only one optional + * leaf, and get the same end state as the legacy CLI. + */ + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:areas/ospf:area" + + "/ospf:interfaces/ospf:interface" + + "/ospf:static-neighbors/ospf:neighbor/ospf:poll-interval" { + deviate add { + default "60"; + } + } + + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:areas/ospf:area" + + "/ospf:interfaces/ospf:interface" + + "/ospf:static-neighbors/ospf:neighbor/ospf:priority" { + deviate add { + default "0"; + } + } + + /* + * FRR has no NBMA neighbour `cost` knob. RFC 9129 exposes `cost` on + * every static neighbour entry. Mark it not-supported so mgmtd rejects + * writes cleanly rather than accepting them into the candidate with no + * callback to fire. + */ + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:areas/ospf:area" + + "/ospf:interfaces/ospf:interface" + + "/ospf:static-neighbors/ospf:neighbor/ospf:cost" { + deviate not-supported; + } + + /* + * FRR has no separate OSPF on/off toggle: the protocol runs whenever + * an ospfd / ospf6d instance is configured (control-plane-protocol + * create) and on every interface that is bound into an area. Mark + * `ospf/enabled` and per-interface `interface/enabled` not-supported + * so mgmtd rejects writes at validate time rather than silently + * accepting `enabled=false` with no FRR mutation. + */ + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:enabled" { + deviate not-supported; + } + + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:areas/ospf:area" + + "/ospf:interfaces/ospf:interface/ospf:enabled" { + deviate not-supported; + } + + /* + * RFC 9129 uses ospf/address-family in notification leafrefs, so + * the leaf must stay visible. FRR ospf6d is intrinsically IPv6 in + * this PR; reject other values in the model rather than accepting + * config with no daemon effect. + */ + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:address-family" { + deviate add { + must ". = 'ipv6'"; + } + } + + + /* + * mgmtd loads one ietf-ospf context for both backends, so feature + * advertisement is necessarily the union of the converted OSPFv2 and + * OSPFv3 surfaces. Guard OSPFv2-only converted leaves with a type + * constraint so writes to OSPFv3 instances are rejected by the model + * instead of being retained in mgmtd with no ospf6d callback. + */ + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:areas/ospf:area" + + "/ospf:default-cost" { + deviate add { + must "derived-from-or-self(../../../../ietf-routing:type, " + + "'ospf:ospfv2')"; + } + } + + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:mpls/ospf:te-rid" + + "/ospf:ipv4-router-id" { + deviate add { + must "derived-from-or-self(../../../../ietf-routing:type, " + + "'ospf:ospfv2')"; + } + } + + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:mpls/ospf:ldp" + + "/ospf:igp-sync" { + deviate add { + must "derived-from-or-self(../../../../ietf-routing:type, " + + "'ospf:ospfv2')"; + } + } + + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:stub-router" + + "/ospf:trigger/ospf:always/ospf:always" { + deviate add { + must "derived-from-or-self(../../../ietf-routing:type, " + + "'ospf:ospfv2')"; + } + } + + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:areas/ospf:area" + + "/ospf:interfaces/ospf:interface" + + "/ospf:prefix-suppression" { + deviate add { + must "derived-from-or-self(../../../../../../ietf-routing:type, " + + "'ospf:ospfv2')"; + } + } + + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:areas/ospf:area" + + "/ospf:interfaces/ospf:interface" + + "/ospf:static-neighbors/ospf:neighbor" { + deviate add { + must "derived-from-or-self(../../../../../../../ietf-routing:type, " + + "'ospf:ospfv2')"; + } + } + + /* + * The OSPFv3 instance ID has legacy CLI support, but this PR does + * not expose it through the RFC 9129 northbound callbacks. + */ + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:areas/ospf:area" + + "/ospf:interfaces/ospf:interface/ospf:instance-id" { + deviate not-supported; + } + + /* + * FRR's OSPFv2 TE router-id callback maps the IPv4 Router Address + * TLV. The RFC 9129 IPv6 TE router-id leaf is not wired here. + */ + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:mpls/ospf:te-rid" + + "/ospf:ipv6-router-id" { + deviate not-supported; + } + + /* + * FRR supports OSPFv2 virtual links through the legacy CLI, but this + * PR does not wire the RFC 9129 virtual-link subtree into the daemon + * northbound callbacks. Keep the advertised YANG surface honest by + * hiding the subtree until the timers and authentication leaves below + * it are implemented as a coherent unit. + */ + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:areas/ospf:area" + + "/ospf:virtual-links" { + deviate not-supported; + } + + /* + * Only the RFC 9129 authentication key-chain leaves are wired to FRR + * today. libyang does not accept not-supported deviation targets for + * the nested choice/case leaves here, so reject unsupported explicit-key + * alternatives at the authentication container instead. + */ + deviation "/ietf-routing:routing" + + "/ietf-routing:control-plane-protocols" + + "/ietf-routing:control-plane-protocol" + + "/ospf:ospf/ospf:areas/ospf:area" + + "/ospf:interfaces/ospf:interface" + + "/ospf:authentication" { + deviate add { + must "not(ospf:ospfv2-key-id or ospf:ospfv2-key or " + + "ospf:ospfv2-crypto-algorithm or ospf:ospfv3-sa-id or " + + "ospf:ospfv3-key or ospf:ospfv3-crypto-algorithm)" { + error-message + "Only key-chain authentication is supported by FRR OSPF YANG"; + } + } + } + + /* + * `ospf/address-family` remains visible because RFC 9129 notification + * headers leafref it. The deviation above constrains the only reachable + * FRR value to IPv6 instead of accepting unused configuration. + */ +} diff --git a/yang/ietf/iana-bfd-types.yang b/yang/ietf/iana-bfd-types.yang new file mode 100644 index 000000000000..3435578cfa64 --- /dev/null +++ b/yang/ietf/iana-bfd-types.yang @@ -0,0 +1,159 @@ +module iana-bfd-types { + yang-version 1.1; + namespace "urn:ietf:params:xml:ns:yang:iana-bfd-types"; + prefix iana-bfd-types; + + organization + "IANA"; + contact + "Internet Assigned Numbers Authority + + Postal: ICANN + 12025 Waterfront Drive, Suite 300 + Los Angeles, CA 90094-2536 + United States of America + Tel: +1 310 301 5800 + "; + description + "This module defines YANG data types for IANA-registered + BFD parameters. + + This YANG module is maintained by IANA and reflects the + 'BFD Diagnostic Codes' and 'BFD Authentication Types' + registries. + + Copyright (c) 2021 IETF Trust and the persons identified as + authors of the code. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject to + the license terms contained in, the Simplified BSD License set + forth in Section 4.c of the IETF Trust's Legal Provisions + Relating to IETF Documents + (https://trustee.ietf.org/license-info). + + This version of this YANG module is part of RFC 9127; see the + RFC itself for full legal notices."; + reference + "RFC 9127: YANG Data Model for Bidirectional Forwarding + Detection (BFD)"; + + revision 2021-10-21 { + description + "Initial revision."; + reference + "RFC 9127: YANG Data Model for Bidirectional Forwarding + Detection (BFD)"; + } + + /* + * Type definitions + */ + + typedef diagnostic { + type enumeration { + enum none { + value 0; + description + "No Diagnostic."; + } + enum control-expiry { + value 1; + description + "Control Detection Time Expired."; + } + enum echo-failed { + value 2; + description + "Echo Function Failed."; + } + enum neighbor-down { + value 3; + description + "Neighbor Signaled Session Down."; + } + enum forwarding-reset { + value 4; + description + "Forwarding Plane Reset."; + } + enum path-down { + value 5; + description + "Path Down."; + } + enum concatenated-path-down { + value 6; + description + "Concatenated Path Down."; + } + enum admin-down { + value 7; + description + "Administratively Down."; + } + enum reverse-concatenated-path-down { + value 8; + description + "Reverse Concatenated Path Down."; + } + enum mis-connectivity-defect { + value 9; + description + "Mis-connectivity defect."; + reference + "RFC 5880: Bidirectional Forwarding Detection (BFD) + RFC 6428: Proactive Connectivity Verification, Continuity + Check, and Remote Defect Indication for the MPLS Transport + Profile"; + } + } + description + "BFD diagnostic codes as defined in RFC 5880. Values are + maintained in the 'BFD Diagnostic Codes' IANA registry. + Range is 0 to 31."; + reference + "RFC 5880: Bidirectional Forwarding Detection (BFD)"; + } + + typedef auth-type { + type enumeration { + enum reserved { + value 0; + description + "Reserved."; + } + enum simple-password { + value 1; + description + "Simple Password."; + } + enum keyed-md5 { + value 2; + description + "Keyed MD5."; + } + enum meticulous-keyed-md5 { + value 3; + description + "Meticulous Keyed MD5."; + } + enum keyed-sha1 { + value 4; + description + "Keyed SHA1."; + } + enum meticulous-keyed-sha1 { + value 5; + description + "Meticulous Keyed SHA1."; + } + } + description + "BFD authentication type as defined in RFC 5880. Values are + maintained in the 'BFD Authentication Types' IANA registry. + Range is 0 to 255."; + reference + "RFC 5880: Bidirectional Forwarding Detection (BFD)"; + } +} diff --git a/yang/ietf/iana-routing-types.yang b/yang/ietf/iana-routing-types.yang new file mode 100644 index 000000000000..25e498b3713a --- /dev/null +++ b/yang/ietf/iana-routing-types.yang @@ -0,0 +1,488 @@ +module iana-routing-types { + namespace "urn:ietf:params:xml:ns:yang:iana-routing-types"; + prefix iana-rt-types; + + organization + "IANA"; + contact + "Internet Assigned Numbers Authority + + Postal: ICANN + 12025 Waterfront Drive, Suite 300 + Los Angeles, CA 90094-2536 + United States of America + Tel: +1 310 301 5800 + "; + + description + "This module contains a collection of YANG data types + considered defined by IANA and used for routing + protocols. + + Copyright (c) 2017 IETF Trust and the persons + identified as authors of the code. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject + to the license terms contained in, the Simplified BSD License + set forth in Section 4.c of the IETF Trust's Legal Provisions + Relating to IETF Documents + (https://trustee.ietf.org/license-info). + + This version of this YANG module is part of RFC 8294; see + the RFC itself for full legal notices."; + + revision 2017-12-04 { + description "Initial revision."; + reference + "RFC 8294: Common YANG Data Types for the Routing Area. + Section 4."; + } + + + + + /*** Collection of IANA types related to routing ***/ + /*** IANA Address Family enumeration ***/ + + typedef address-family { + type enumeration { + enum ipv4 { + value 1; + description + "IPv4 Address Family."; + } + + enum ipv6 { + value 2; + description + "IPv6 Address Family."; + } + + enum nsap { + value 3; + description + "OSI Network Service Access Point (NSAP) Address Family."; + } + + enum hdlc { + value 4; + description + "High-Level Data Link Control (HDLC) Address Family."; + } + + enum bbn1822 { + value 5; + description + "Bolt, Beranek, and Newman Report 1822 (BBN 1822) + Address Family."; + } + + enum ieee802 { + value 6; + description + "IEEE 802 Committee Address Family + (aka Media Access Control (MAC) address)."; + } + + enum e163 { + value 7; + description + "ITU-T E.163 Address Family."; + } + enum e164 { + value 8; + description + "ITU-T E.164 (Switched Multimegabit Data Service (SMDS), + Frame Relay, ATM) Address Family."; + } + + enum f69 { + value 9; + description + "ITU-T F.69 (Telex) Address Family."; + } + + enum x121 { + value 10; + description + "ITU-T X.121 (X.25, Frame Relay) Address Family."; + } + + enum ipx { + value 11; + description + "Novell Internetwork Packet Exchange (IPX) + Address Family."; + } + + enum appletalk { + value 12; + description + "Apple AppleTalk Address Family."; + } + + enum decnet-iv { + value 13; + description + "Digital Equipment DECnet Phase IV Address Family."; + } + + enum vines { + value 14; + description + "Banyan Vines Address Family."; + } + + + + + + enum e164-nsap { + value 15; + description + "ITU-T E.164 with NSAP sub-address Address Family."; + } + + enum dns { + value 16; + description + "Domain Name System (DNS) Address Family."; + } + + enum distinguished-name { + value 17; + description + "Distinguished Name Address Family."; + } + + enum as-num { + value 18; + description + "Autonomous System (AS) Number Address Family."; + } + + enum xtp-v4 { + value 19; + description + "Xpress Transport Protocol (XTP) over IPv4 + Address Family."; + } + + enum xtp-v6 { + value 20; + description + "XTP over IPv6 Address Family."; + } + + enum xtp-native { + value 21; + description + "XTP native mode Address Family."; + } + + enum fc-port { + value 22; + description + "Fibre Channel (FC) World-Wide Port Name Address Family."; + } + enum fc-node { + value 23; + description + "FC World-Wide Node Name Address Family."; + } + + enum gwid { + value 24; + description + "ATM Gateway Identifier (GWID) Number Address Family."; + } + + enum l2vpn { + value 25; + description + "Layer 2 VPN (L2VPN) Address Family."; + } + + enum mpls-tp-section-eid { + value 26; + description + "MPLS Transport Profile (MPLS-TP) Section Endpoint + Identifier Address Family."; + } + + enum mpls-tp-lsp-eid { + value 27; + description + "MPLS-TP Label Switched Path (LSP) Endpoint Identifier + Address Family."; + } + + enum mpls-tp-pwe-eid { + value 28; + description + "MPLS-TP Pseudowire Endpoint Identifier Address Family."; + } + + enum mt-v4 { + value 29; + description + "Multi-Topology IPv4 Address Family."; + } + + + + + + enum mt-v6 { + value 30; + description + "Multi-Topology IPv6 Address Family."; + } + + enum eigrp-common-sf { + value 16384; + description + "Enhanced Interior Gateway Routing Protocol (EIGRP) + Common Service Family Address Family."; + } + + enum eigrp-v4-sf { + value 16385; + description + "EIGRP IPv4 Service Family Address Family."; + } + + enum eigrp-v6-sf { + value 16386; + description + "EIGRP IPv6 Service Family Address Family."; + } + + enum lcaf { + value 16387; + description + "Locator/ID Separation Protocol (LISP) + Canonical Address Format (LCAF) Address Family."; + } + + enum bgp-ls { + value 16388; + description + "Border Gateway Protocol - Link State (BGP-LS) + Address Family."; + } + + enum mac-48 { + value 16389; + description + "IEEE 48-bit MAC Address Family."; + } + + + + + enum mac-64 { + value 16390; + description + "IEEE 64-bit MAC Address Family."; + } + + enum trill-oui { + value 16391; + description + "Transparent Interconnection of Lots of Links (TRILL) + IEEE Organizationally Unique Identifier (OUI) + Address Family."; + } + + enum trill-mac-24 { + value 16392; + description + "TRILL final 3 octets of 48-bit MAC Address Family."; + } + + enum trill-mac-40 { + value 16393; + description + "TRILL final 5 octets of 64-bit MAC Address Family."; + } + + enum ipv6-64 { + value 16394; + description + "First 8 octets (64 bits) of IPv6 address + Address Family."; + } + + enum trill-rbridge-port-id { + value 16395; + description + "TRILL Routing Bridge (RBridge) Port ID Address Family."; + } + + enum trill-nickname { + value 16396; + description + "TRILL Nickname Address Family."; + } + } + + + + description + "Enumeration containing all the IANA-defined + Address Families."; + + } + + /*** Subsequent Address Family Identifiers (SAFIs) ***/ + /*** for multiprotocol BGP enumeration ***/ + + typedef bgp-safi { + type enumeration { + enum unicast-safi { + value 1; + description + "Unicast SAFI."; + } + + enum multicast-safi { + value 2; + description + "Multicast SAFI."; + } + + enum labeled-unicast-safi { + value 4; + description + "Labeled Unicast SAFI."; + } + + enum multicast-vpn-safi { + value 5; + description + "Multicast VPN SAFI."; + } + + enum pseudowire-safi { + value 6; + description + "Multi-segment Pseudowire VPN SAFI."; + } + + enum tunnel-encap-safi { + value 7; + description + "Tunnel Encap SAFI."; + } + + + enum mcast-vpls-safi { + value 8; + description + "Multicast Virtual Private LAN Service (VPLS) SAFI."; + } + + enum tunnel-safi { + value 64; + description + "Tunnel SAFI."; + } + + enum vpls-safi { + value 65; + description + "VPLS SAFI."; + } + + enum mdt-safi { + value 66; + description + "Multicast Distribution Tree (MDT) SAFI."; + } + + enum v4-over-v6-safi { + value 67; + description + "IPv4 over IPv6 SAFI."; + } + + enum v6-over-v4-safi { + value 68; + description + "IPv6 over IPv4 SAFI."; + } + + enum l1-vpn-auto-discovery-safi { + value 69; + description + "Layer 1 VPN Auto-Discovery SAFI."; + } + + enum evpn-safi { + value 70; + description + "Ethernet VPN (EVPN) SAFI."; + } + + enum bgp-ls-safi { + value 71; + description + "BGP-LS SAFI."; + } + + enum bgp-ls-vpn-safi { + value 72; + description + "BGP-LS VPN SAFI."; + } + + enum sr-te-safi { + value 73; + description + "Segment Routing - Traffic Engineering (SR-TE) SAFI."; + } + + enum labeled-vpn-safi { + value 128; + description + "MPLS Labeled VPN SAFI."; + } + + enum multicast-mpls-vpn-safi { + value 129; + description + "Multicast for BGP/MPLS IP VPN SAFI."; + } + + enum route-target-safi { + value 132; + description + "Route Target SAFI."; + } + + enum ipv4-flow-spec-safi { + value 133; + description + "IPv4 Flow Specification SAFI."; + } + + enum vpnv4-flow-spec-safi { + value 134; + description + "IPv4 VPN Flow Specification SAFI."; + } + + enum vpn-auto-discovery-safi { + value 140; + description + "VPN Auto-Discovery SAFI."; + } + } + description + "Enumeration for BGP SAFI."; + reference + "RFC 4760: Multiprotocol Extensions for BGP-4."; + } +} diff --git a/yang/ietf/ietf-bfd-types.yang b/yang/ietf/ietf-bfd-types.yang new file mode 100644 index 000000000000..82799ea03517 --- /dev/null +++ b/yang/ietf/ietf-bfd-types.yang @@ -0,0 +1,690 @@ +module ietf-bfd-types { + yang-version 1.1; + namespace "urn:ietf:params:xml:ns:yang:ietf-bfd-types"; + prefix bfd-types; + + import iana-bfd-types { + prefix iana-bfd-types; + reference + "RFC 9127: YANG Data Model for Bidirectional Forwarding + Detection (BFD)"; + } + import ietf-inet-types { + prefix inet; + reference + "RFC 6991: Common YANG Data Types"; + } + import ietf-yang-types { + prefix yang; + reference + "RFC 6991: Common YANG Data Types"; + } + import ietf-routing { + prefix rt; + reference + "RFC 8349: A YANG Data Model for Routing Management + (NMDA Version)"; + } + import ietf-key-chain { + prefix key-chain; + reference + "RFC 8177: YANG Data Model for Key Chains"; + } + + organization + "IETF BFD Working Group"; + contact + "WG Web: + WG List: + + Editor: Reshad Rahman + + + Editor: Lianshu Zheng + + + Editor: Mahesh Jethanandani + "; + description + "This module contains a collection of BFD-specific YANG data type + definitions, as per RFC 5880, and also groupings that are common + to other BFD YANG modules. + + Copyright (c) 2022 IETF Trust and the persons identified as + authors of the code. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject + to the license terms contained in, the Revised BSD License + set forth in Section 4.c of the IETF Trust's Legal Provisions + Relating to IETF Documents + (https://trustee.ietf.org/license-info). + + This version of this YANG module is part of RFC 9314; see the + RFC itself for full legal notices."; + reference + "RFC 5880: Bidirectional Forwarding Detection (BFD) + RFC 9314: YANG Data Model for Bidirectional Forwarding + Detection (BFD)"; + + revision 2022-09-22 { + description + "This revision is not backwards compatible with the + previous version of this model. + + This revision adds an 'if-feature' statement called + 'client-base-cfg-parms' for client configuration parameters. + Clients expecting to use those parameters now need to + verify that the server declares support of the feature + before depending on the presence of the parameters. + + The change was introduced for clients that do not need + them and have to deviate to prevent them from being + included."; + reference + "RFC 9314: YANG Data Model for Bidirectional Forwarding + Detection (BFD)."; + } + revision 2021-10-21 { + description + "Initial revision."; + reference + "RFC 9127: YANG Data Model for Bidirectional Forwarding + Detection (BFD)"; + } + + /* + * Feature definitions + */ + + feature single-minimum-interval { + description + "This feature indicates that the server supports configuration + of one minimum interval value that is used for both transmit + and receive minimum intervals."; + } + + feature authentication { + description + "This feature indicates that the server supports BFD + authentication."; + reference + "RFC 5880: Bidirectional Forwarding Detection (BFD), + Section 6.7"; + } + + feature demand-mode { + description + "This feature indicates that the server supports BFD Demand + mode."; + reference + "RFC 5880: Bidirectional Forwarding Detection (BFD), + Section 6.6"; + } + + feature echo-mode { + description + "This feature indicates that the server supports BFD Echo + mode."; + reference + "RFC 5880: Bidirectional Forwarding Detection (BFD), + Section 6.4"; + } + + feature client-base-cfg-parms { + description + "This feature allows protocol models to configure BFD client + session parameters."; + reference + "RFC 9314: YANG Data Model for Bidirectional Forwarding + Detection (BFD)."; + } + + /* + * Identity definitions + */ + + identity bfdv1 { + base rt:control-plane-protocol; + description + "BFD protocol version 1."; + reference + "RFC 5880: Bidirectional Forwarding Detection (BFD)"; + } + + identity path-type { + description + "Base identity for the BFD path type. The path type indicates + the type of path on which BFD is running."; + } + + identity path-ip-sh { + base path-type; + description + "BFD on IP single-hop."; + reference + "RFC 5881: Bidirectional Forwarding Detection (BFD) + for IPv4 and IPv6 (Single Hop)"; + } + + identity path-ip-mh { + base path-type; + description + "BFD on IP multihop paths."; + reference + "RFC 5883: Bidirectional Forwarding Detection (BFD) for + Multihop Paths"; + } + + identity path-mpls-te { + base path-type; + description + "BFD on MPLS Traffic Engineering."; + reference + "RFC 5884: Bidirectional Forwarding Detection (BFD) + for MPLS Label Switched Paths (LSPs)"; + } + + identity path-mpls-lsp { + base path-type; + description + "BFD on an MPLS Label Switched Path."; + reference + "RFC 5884: Bidirectional Forwarding Detection (BFD) + for MPLS Label Switched Paths (LSPs)"; + } + + identity path-lag { + base path-type; + description + "Micro-BFD on LAG member links."; + reference + "RFC 7130: Bidirectional Forwarding Detection (BFD) on + Link Aggregation Group (LAG) Interfaces"; + } + + identity encap-type { + description + "Base identity for BFD encapsulation type."; + } + + identity encap-ip { + base encap-type; + description + "BFD with IP encapsulation."; + } + + /* + * Type definitions + */ + + typedef discriminator { + type uint32; + description + "BFD Discriminator as described in RFC 5880."; + reference + "RFC 5880: Bidirectional Forwarding Detection (BFD)"; + } + + typedef state { + type enumeration { + enum adminDown { + value 0; + description + "'adminDown' state."; + } + enum down { + value 1; + description + "'Down' state."; + } + enum init { + value 2; + description + "'Init' state."; + } + enum up { + value 3; + description + "'Up' state."; + } + } + description + "BFD states as defined in RFC 5880."; + } + + typedef multiplier { + type uint8 { + range "1..255"; + } + description + "BFD multiplier as described in RFC 5880."; + } + + typedef hops { + type uint8 { + range "1..255"; + } + description + "This corresponds to Time To Live for IPv4 and corresponds to + the hop limit for IPv6."; + } + + /* + * Groupings + */ + + grouping auth-parms { + description + "Grouping for BFD authentication parameters + (see Section 6.7 of RFC 5880)."; + container authentication { + if-feature "authentication"; + presence "Enables BFD authentication (see Section 6.7 + of RFC 5880)."; + description + "Parameters for BFD authentication."; + reference + "RFC 5880: Bidirectional Forwarding Detection (BFD), + Section 6.7"; + leaf key-chain { + type key-chain:key-chain-ref; + description + "Name of the 'key-chain' as per RFC 8177."; + } + leaf meticulous { + type boolean; + description + "Enables a meticulous mode as per Section 6.7 of + RFC 5880."; + } + } + } + + grouping base-cfg-parms { + description + "BFD grouping for base configuration parameters."; + leaf local-multiplier { + type multiplier; + default "3"; + description + "Multiplier transmitted by the local system."; + } + choice interval-config-type { + default "tx-rx-intervals"; + description + "Two interval values or one value used for both transmit and + receive."; + case tx-rx-intervals { + leaf desired-min-tx-interval { + type uint32; + units "microseconds"; + default "1000000"; + description + "Desired minimum transmit interval of control packets."; + } + leaf required-min-rx-interval { + type uint32; + units "microseconds"; + default "1000000"; + description + "Required minimum receive interval of control packets."; + } + } + case single-interval { + if-feature "single-minimum-interval"; + leaf min-interval { + type uint32; + units "microseconds"; + default "1000000"; + description + "Desired minimum transmit interval and required + minimum receive interval of control packets."; + } + } + } + } + + grouping client-cfg-parms { + description + "BFD grouping for configuration parameters + used by BFD clients, e.g., IGP or MPLS."; + leaf enabled { + type boolean; + default "false"; + description + "Indicates whether BFD is enabled."; + } + uses base-cfg-parms { + if-feature "client-base-cfg-parms"; + } + } + + grouping common-cfg-parms { + description + "BFD grouping for common configuration parameters."; + uses base-cfg-parms; + leaf demand-enabled { + if-feature "demand-mode"; + type boolean; + default "false"; + description + "To enable Demand mode."; + } + leaf admin-down { + type boolean; + default "false"; + description + "Indicates whether the BFD session is administratively + down."; + } + uses auth-parms; + } + + grouping all-session { + description + "BFD session operational information."; + leaf path-type { + type identityref { + base path-type; + } + config false; + description + "BFD path type. This indicates the path type that BFD is + running on."; + } + leaf ip-encapsulation { + type boolean; + config false; + description + "Indicates whether BFD encapsulation uses IP."; + } + leaf local-discriminator { + type discriminator; + config false; + description + "Local discriminator."; + } + leaf remote-discriminator { + type discriminator; + config false; + description + "Remote discriminator."; + } + leaf remote-multiplier { + type multiplier; + config false; + description + "Remote multiplier."; + } + leaf demand-capability { + if-feature "demand-mode"; + type boolean; + config false; + description + "Local Demand mode capability."; + } + leaf source-port { + when "../ip-encapsulation = 'true'" { + description + "Source port valid only when IP encapsulation is used."; + } + type inet:port-number; + config false; + description + "Source UDP port."; + } + leaf dest-port { + when "../ip-encapsulation = 'true'" { + description + "Destination port valid only when IP encapsulation + is used."; + } + type inet:port-number; + config false; + description + "Destination UDP port."; + } + container session-running { + config false; + description + "BFD 'session-running' information."; + leaf session-index { + type uint32; + description + "An index used to uniquely identify BFD sessions."; + } + leaf local-state { + type state; + description + "Local state."; + } + leaf remote-state { + type state; + description + "Remote state."; + } + leaf local-diagnostic { + type iana-bfd-types:diagnostic; + description + "Local diagnostic."; + } + leaf remote-diagnostic { + type iana-bfd-types:diagnostic; + description + "Remote diagnostic."; + } + leaf remote-authenticated { + type boolean; + description + "Indicates whether incoming BFD control packets are + authenticated."; + } + leaf remote-authentication-type { + when "../remote-authenticated = 'true'" { + description + "Only valid when incoming BFD control packets are + authenticated."; + } + if-feature "authentication"; + type iana-bfd-types:auth-type; + description + "Authentication type of incoming BFD control packets."; + } + leaf detection-mode { + type enumeration { + enum async-with-echo { + value 1; + description + "Async with echo."; + } + enum async-without-echo { + value 2; + description + "Async without echo."; + } + enum demand-with-echo { + value 3; + description + "Demand with echo."; + } + enum demand-without-echo { + value 4; + description + "Demand without echo."; + } + } + description + "Detection mode."; + } + leaf negotiated-tx-interval { + type uint32; + units "microseconds"; + description + "Negotiated transmit interval."; + } + leaf negotiated-rx-interval { + type uint32; + units "microseconds"; + description + "Negotiated receive interval."; + } + leaf detection-time { + type uint32; + units "microseconds"; + description + "Detection time."; + } + leaf echo-tx-interval-in-use { + when "../../path-type = 'bfd-types:path-ip-sh'" { + description + "Echo is supported for IP single-hop only."; + } + if-feature "echo-mode"; + type uint32; + units "microseconds"; + description + "Echo transmit interval in use."; + } + } + container session-statistics { + config false; + description + "BFD per-session statistics."; + leaf create-time { + type yang:date-and-time; + description + "Time and date when this session was created."; + } + leaf last-down-time { + type yang:date-and-time; + description + "Time and date of the last time this session went down."; + } + leaf last-up-time { + type yang:date-and-time; + description + "Time and date of the last time this session went up."; + } + leaf down-count { + type yang:counter32; + description + "The number of times this session has transitioned to the + 'down' state."; + } + leaf admin-down-count { + type yang:counter32; + description + "The number of times this session has transitioned to the + 'admin-down' state."; + } + leaf receive-packet-count { + type yang:counter64; + description + "Count of received packets in this session. This includes + valid and invalid received packets."; + } + leaf send-packet-count { + type yang:counter64; + description + "Count of sent packets in this session."; + } + leaf receive-invalid-packet-count { + type yang:counter64; + description + "Count of invalid received packets in this session."; + } + leaf send-failed-packet-count { + type yang:counter64; + description + "Count of packets that failed to be sent in this session."; + } + } + } + + grouping session-statistics-summary { + description + "Grouping for session statistics summary."; + container summary { + config false; + description + "BFD session statistics summary."; + leaf number-of-sessions { + type yang:gauge32; + description + "Number of BFD sessions."; + } + leaf number-of-sessions-up { + type yang:gauge32; + description + "Number of BFD sessions currently in the 'Up' state + (as defined in RFC 5880)."; + } + leaf number-of-sessions-down { + type yang:gauge32; + description + "Number of BFD sessions currently in the 'Down' or 'Init' + state but not 'adminDown' (as defined in RFC 5880)."; + } + leaf number-of-sessions-admin-down { + type yang:gauge32; + description + "Number of BFD sessions currently in the 'adminDown' state + (as defined in RFC 5880)."; + } + } + } + + grouping notification-parms { + description + "This group describes common parameters that will be sent + as part of BFD notifications."; + leaf local-discr { + type discriminator; + description + "BFD local discriminator."; + } + leaf remote-discr { + type discriminator; + description + "BFD remote discriminator."; + } + leaf new-state { + type state; + description + "Current BFD state."; + } + leaf state-change-reason { + type iana-bfd-types:diagnostic; + description + "Reason for the BFD state change."; + } + leaf time-of-last-state-change { + type yang:date-and-time; + description + "Calendar time of the most recent previous state change."; + } + leaf dest-addr { + type inet:ip-address; + description + "BFD peer address."; + } + leaf source-addr { + type inet:ip-address; + description + "BFD local address."; + } + leaf session-index { + type uint32; + description + "An index used to uniquely identify BFD sessions."; + } + leaf path-type { + type identityref { + base path-type; + } + description + "BFD path type."; + } + } +} diff --git a/yang/ietf/ietf-ospf.yang b/yang/ietf/ietf-ospf.yang new file mode 100644 index 000000000000..8839563b6105 --- /dev/null +++ b/yang/ietf/ietf-ospf.yang @@ -0,0 +1,5006 @@ +module ietf-ospf { + yang-version 1.1; + namespace "urn:ietf:params:xml:ns:yang:ietf-ospf"; + + prefix ospf; + + import ietf-inet-types { + prefix inet; + reference + "RFC 6991: Common YANG Data Types"; + } + + import ietf-yang-types { + prefix yang; + reference + "RFC 6991: Common YANG Data Types"; + } + + import ietf-interfaces { + prefix if; + reference + "RFC 8343: A YANG Data Model for Interface Management"; + } + + import ietf-routing-types { + prefix rt-types; + reference + "RFC 8294: Common YANG Data Types for the Routing Area"; + } + + import iana-routing-types { + prefix iana-rt-types; + reference + "RFC 8294: Common YANG Data Types for the Routing Area"; + } + + import ietf-routing { + prefix rt; + reference + "RFC 8349: A YANG Data Model for Routing Management + (NMDA Version)"; + } + + import ietf-key-chain { + prefix key-chain; + reference + "RFC 8177: YANG Data Model for Key Chains"; + } + + import ietf-bfd-types { + prefix bfd-types; + reference + "RFC 9314: YANG Data Model for Bidirectional Forwarding + Detection (BFD)"; + } + + organization + "IETF Link State Routing (lsr) Working Group"; + + contact + "WG Web: + WG List: + + Editor: Derek Yeung + + Author: Acee Lindem + + Author: Yingzhen Qu + + Author: Jeffrey Zhang + + Author: Ing-Wher Chen + "; + + description + "This YANG module defines the generic configuration and + operational state for the OSPF protocol common to all + vendor implementations. It is intended that the module + will be extended by vendors to define vendor-specific + OSPF configuration parameters and policies -- + for example, route maps or route policies. + + This YANG data model conforms to the Network Management + Datastore Architecture (NMDA) as described in RFC 8342. + + The key words 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL', 'SHALL + NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED', 'NOT RECOMMENDED', + 'MAY', and 'OPTIONAL' in this document are to be interpreted as + described in BCP 14 (RFC 2119) (RFC 8174) when, and only when, + they appear in all capitals, as shown here. + + Copyright (c) 2022 IETF Trust and the persons identified as + authors of the code. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject to + the license terms contained in, the Revised BSD License set + forth in Section 4.c of the IETF Trust's Legal Provisions + Relating to IETF Documents + (https://trustee.ietf.org/license-info). + + This version of this YANG module is part of RFC 9129; see the + RFC itself for full legal notices."; + + revision 2022-10-19 { + description + "Initial revision."; + reference + "RFC 9129: YANG Data Model for the OSPF Protocol"; + } + + feature multi-topology { + description + "Support for Multi-Topology (MT) routing."; + reference + "RFC 4915: Multi-Topology (MT) Routing in OSPF"; + } + + feature multi-area-adj { + description + "Support for OSPF multi-area adjacencies as described in + RFC 5185."; + reference + "RFC 5185: OSPF Multi-Area Adjacency"; + } + + feature explicit-router-id { + description + "Sets the Router ID per instance explicitly."; + } + + feature demand-circuit { + description + "Support for OSPF demand circuits as defined in RFC 1793."; + reference + "RFC 1793: Extending OSPF to Support Demand Circuits"; + } + + feature mtu-ignore { + description + "Disable OSPF Database Description packet MTU + mismatch checking as specified in the OSPFv2 + protocol specification (RFC 2328). This mismatch checking + also applies to OSPFv3 (RFC 5340)."; + reference + "RFC 2328: OSPF Version 2, Section 10.6 + RFC 5340: OSPF for IPv6"; + } + + feature lls { + description + "OSPF link-local signaling (LLS) as defined in RFC 5613."; + reference + "RFC 5613: OSPF Link-Local Signaling"; + } + + feature prefix-suppression { + description + "OSPF prefix suppression support as described in RFC 6860."; + reference + "RFC 6860: Hiding Transit-Only Networks in OSPF"; + } + + feature ttl-security { + description + "Support for OSPF Time to Live (TTL) security checking."; + reference + "RFC 5082: The Generalized TTL Security Mechanism (GTSM)"; + } + + feature nsr { + description + "Non-Stop-Routing (NSR) support. The OSPF NSR feature + allows a router with redundant control-plane capability + (e.g., dual Route Processor (RP) cards) to maintain its + state and adjacencies during planned and unplanned + OSPF instance restarts. It differs from graceful restart + or Non-Stop Forwarding (NSF) in that no protocol signaling + or assistance from adjacent OSPF neighbors is required to + recover control-plane state."; + } + + feature graceful-restart { + description + "Graceful OSPF restart as defined in RFCs 3623 and 5187."; + reference + "RFC 3623: Graceful OSPF Restart + RFC 5187: OSPFv3 Graceful Restart"; + } + + feature auto-cost { + description + "Calculates the OSPF interface cost according to + reference bandwidth."; + reference + "RFC 2328: OSPF Version 2"; + } + + feature max-ecmp { + description + "Sets the maximum number of ECMP paths."; + } + + feature max-lsa { + description + "Sets the maximum number of Link State Advertisements (LSAs) + the OSPF instance will accept."; + reference + "RFC 1765: OSPF Database Overflow"; + } + + feature te-rid { + description + "Support for configuration of the Traffic Engineering (TE) + Router ID, i.e., the Router Address TLV as described in + Section 2.4.1 of RFC 3630 or the Router IPv6 Address TLV + as described in Section 3 of RFC 5329."; + reference + "RFC 3630: Traffic Engineering (TE) Extensions to + OSPF Version 2, Section 2.4.1 + RFC 5329: Traffic Engineering Extensions to OSPF Version 3, + Section 3"; + } + + feature ldp-igp-sync { + description + "LDP IGP synchronization."; + reference + "RFC 5443: LDP IGP Synchronization"; + } + + feature ospfv2-authentication-trailer { + description + "Support for the OSPFv2 authentication trailer."; + reference + "RFC 5709: OSPFv2 HMAC-SHA Cryptographic Authentication + RFC 7474: Security Extension for OSPFv2 When + Using Manual Key Management"; + } + + feature ospfv3-authentication-ipsec { + description + "Support for IPsec for OSPFv3 authentication."; + reference + "RFC 4552: Authentication/Confidentiality for OSPFv3"; + } + + feature ospfv3-authentication-trailer { + description + "Support for the OSPFv3 authentication trailer."; + reference + "RFC 7166: Supporting Authentication Trailer for OSPFv3"; + } + + feature fast-reroute { + description + "Support for IP Fast Reroute (IP-FRR)."; + reference + "RFC 5714: IP Fast Reroute Framework"; + } + + feature key-chain { + description + "Support of key chains for authentication."; + reference + "RFC 8177: YANG Data Model for Key Chains"; + } + + feature node-flag { + description + "Support for node flags for OSPF prefixes."; + reference + "RFC 7684: OSPFv2 Prefix/Link Attribute Advertisement"; + } + + feature node-tag { + description + "Support for node administrative tags for OSPF routing + instances."; + reference + "RFC 7777: Advertising Node Administrative Tags in OSPF"; + } + + feature lfa { + description + "Support for Loop-Free Alternates (LFAs)."; + reference + "RFC 5286: Basic Specification for IP Fast Reroute: + Loop-Free Alternates"; + } + + feature remote-lfa { + description + "Support for Remote LFAs (R-LFAs)."; + reference + "RFC 7490: Remote Loop-Free Alternate (LFA) Fast Reroute + (FRR)"; + } + + feature stub-router { + description + "Support for OSPF stub router advertisement as defined in + RFC 6987."; + reference + "RFC 6987: OSPF Stub Router Advertisement"; + } + + feature pe-ce-protocol { + description + "Support for OSPF as a Provider Edge to Customer Edge (PE-CE) + protocol."; + reference + "RFC 4577: OSPF as the Provider/Customer Edge Protocol + for BGP/MPLS IP Virtual Private Networks (VPNs) + RFC 6565: OSPFv3 as a Provider Edge to Customer Edge (PE-CE) + Routing Protocol"; + } + + feature ietf-spf-delay { + description + "Support for the IETF Shortest Path First (SPF) delay + algorithm."; + reference + "RFC 8405: Shortest Path First (SPF) Back-Off Delay Algorithm + for Link-State IGPs"; + } + + feature bfd { + description + "Support for BFD to detect OSPF neighbor reachability."; + reference + "RFC 5880: Bidirectional Forwarding Detection (BFD) + RFC 5881: Bidirectional Forwarding Detection + (BFD) for IPv4 and IPv6 (Single Hop)"; + } + + feature hybrid-interface { + description + "Support for the OSPF hybrid interface type."; + reference + "RFC 6845: OSPF Hybrid Broadcast and + Point-to-Multipoint Interface Type"; + } + + identity ospf { + base rt:routing-protocol; + description + "Any OSPF protocol version."; + } + + identity ospfv2 { + base ospf; + description + "OSPFv2 protocol."; + } + + identity ospfv3 { + base ospf; + description + "OSPFv3 protocol."; + } + + identity area-type { + description + "Base identity for an OSPF area type."; + } + + identity normal-area { + base area-type; + description + "OSPF normal area."; + } + + identity stub-nssa-area { + base area-type; + description + "OSPF stub area or Not-So-Stubby Area (NSSA)."; + } + + identity stub-area { + base stub-nssa-area; + description + "OSPF stub area."; + } + + identity nssa-area { + base stub-nssa-area; + description + "OSPF NSSA."; + reference + "RFC 3101: The OSPF Not-So-Stubby Area (NSSA) Option"; + } + + identity ospf-lsa-type { + description + "Base identity for OSPFv2 and OSPFv3 + Link State Advertisement (LSA) types."; + } + + identity ospfv2-lsa-type { + base ospf-lsa-type; + description + "OSPFv2 LSA types."; + } + + identity ospfv2-router-lsa { + base ospfv2-lsa-type; + description + "OSPFv2 Router-LSA - Type 1."; + } + + identity ospfv2-network-lsa { + base ospfv2-lsa-type; + description + "OSPFv2 Network-LSA - Type 2."; + } + + identity ospfv2-summary-lsa-type { + base ospfv2-lsa-type; + description + "OSPFv2 summary LSA types."; + } + + identity ospfv2-network-summary-lsa { + base ospfv2-summary-lsa-type; + description + "OSPFv2 Network summary LSA - Type 3."; + } + + identity ospfv2-asbr-summary-lsa { + base ospfv2-summary-lsa-type; + description + "OSPFv2 Autonomous System Boundary Router (ASBR) summary LSA - + Type 4."; + } + + identity ospfv2-external-lsa-type { + base ospfv2-lsa-type; + description + "OSPFv2 External-LSA types."; + } + + identity ospfv2-as-external-lsa { + base ospfv2-external-lsa-type; + description + "OSPFv2 AS-External-LSA - Type 5."; + } + + identity ospfv2-nssa-lsa { + base ospfv2-external-lsa-type; + description + "OSPFv2 NSSA-LSA - Type 7."; + } + + identity ospfv2-opaque-lsa-type { + base ospfv2-lsa-type; + description + "OSPFv2 Opaque-LSA types."; + reference + "RFC 5250: The OSPF Opaque LSA Option"; + } + + identity ospfv2-link-scope-opaque-lsa { + base ospfv2-opaque-lsa-type; + description + "OSPFv2 Link-Scope Opaque-LSA - Type 9."; + } + + identity ospfv2-area-scope-opaque-lsa { + base ospfv2-opaque-lsa-type; + description + "OSPFv2 Area-Scope Opaque-LSA - Type 10."; + } + + identity ospfv2-as-scope-opaque-lsa { + base ospfv2-opaque-lsa-type; + description + "OSPFv2 AS-Scope Opaque-LSA - Type 11."; + } + + identity ospfv2-unknown-lsa-type { + base ospfv2-lsa-type; + description + "OSPFv2 unknown LSA type."; + } + + identity ospfv3-lsa-type { + base ospf-lsa-type; + description + "OSPFv3 LSA types."; + reference + "RFC 5340: OSPF for IPv6"; + } + + identity ospfv3-router-lsa { + base ospfv3-lsa-type; + description + "OSPFv3 Router-LSA - Type 0x2001."; + } + + identity ospfv3-network-lsa { + base ospfv3-lsa-type; + description + "OSPFv3 Network-LSA - Type 0x2002."; + } + + identity ospfv3-summary-lsa-type { + base ospfv3-lsa-type; + description + "OSPFv3 summary LSA types."; + } + + identity ospfv3-inter-area-prefix-lsa { + base ospfv3-summary-lsa-type; + description + "OSPFv3 Inter-Area-Prefix-LSA - Type 0x2003."; + } + + identity ospfv3-inter-area-router-lsa { + base ospfv3-summary-lsa-type; + description + "OSPFv3 Inter-Area-Router-LSA - Type 0x2004."; + } + + identity ospfv3-external-lsa-type { + base ospfv3-lsa-type; + description + "OSPFv3 External-LSA types."; + } + + identity ospfv3-as-external-lsa { + base ospfv3-external-lsa-type; + description + "OSPFv3 AS-External-LSA - Type 0x4005."; + } + + identity ospfv3-nssa-lsa { + base ospfv3-external-lsa-type; + description + "OSPFv3 NSSA-LSA - Type 0x2007."; + } + + identity ospfv3-link-lsa { + base ospfv3-lsa-type; + description + "OSPFv3 Link-LSA - Type 0x0008."; + } + + identity ospfv3-intra-area-prefix-lsa { + base ospfv3-lsa-type; + description + "OSPFv3 Intra-Area-Prefix-LSA - Type 0x2009."; + } + + identity ospfv3-router-information-lsa { + base ospfv3-lsa-type; + description + "OSPFv3 Router-Information-LSA - Types 0x800C, + 0xA00C, and 0xC00C."; + } + + identity ospfv3-unknown-lsa-type { + base ospfv3-lsa-type; + description + "OSPFv3 unknown LSA type."; + } + + identity lsa-log-reason { + description + "Base identity for an LSA log reason."; + } + + identity lsa-refresh { + base lsa-log-reason; + description + "Identity used when an LSA is logged + as a result of receiving a refresh LSA."; + } + + identity lsa-content-change { + base lsa-log-reason; + description + "Identity used when an LSA is logged + as a result of a change in the contents + of the LSA."; + } + + identity lsa-purge { + base lsa-log-reason; + description + "Identity used when an LSA is logged + as a result of being purged."; + } + + identity informational-capability { + description + "Base identity for router informational capabilities."; + } + + identity graceful-restart { + base informational-capability; + description + "When set, the router is capable of restarting + gracefully."; + reference + "RFC 3623: Graceful OSPF Restart + RFC 5187: OSPFv3 Graceful Restart"; + } + + identity graceful-restart-helper { + base informational-capability; + description + "When set, the router is capable of acting as + a graceful restart helper."; + reference + "RFC 3623: Graceful OSPF Restart + RFC 5187: OSPFv3 Graceful Restart"; + } + + identity stub-router { + base informational-capability; + description + "When set, the router is capable of acting as + an OSPF stub router."; + reference + "RFC 6987: OSPF Stub Router Advertisement"; + } + + identity traffic-engineering { + base informational-capability; + description + "When set, the router is capable of OSPF TE."; + reference + "RFC 3630: Traffic Engineering (TE) Extensions to + OSPF Version 2 + RFC 5329: Traffic Engineering Extensions to OSPF Version 3"; + } + + identity p2p-over-lan { + base informational-capability; + description + "When set, the router is capable of OSPF point-to-point + over a LAN."; + reference + "RFC 5309: Point-to-Point Operation over LAN in Link State + Routing Protocols"; + } + + identity experimental-te { + base informational-capability; + description + "When set, the router is capable of OSPF experimental TE."; + reference + "RFC 4973: OSPF-xTE: Experimental Extension to OSPF for + Traffic Engineering"; + } + + identity router-lsa-bit { + description + "Base identity for Router-LSA bits."; + } + + identity vlink-end-bit { + base router-lsa-bit; + description + "V-bit. When set, the router is an endpoint of one or + more virtual links."; + } + + identity asbr-bit { + base router-lsa-bit; + description + "E-bit. When set, the router is an Autonomous System + Boundary Router (ASBR)."; + } + + identity abr-bit { + base router-lsa-bit; + description + "B-bit. When set, the router is an Area Border + Router (ABR)."; + } + + identity nssa-bit { + base router-lsa-bit; + description + "Nt-bit. When set, the router is an NSSA border router + that is unconditionally translating NSSA-LSAs into + AS-External-LSAs."; + } + + identity ospfv3-lsa-option { + description + "Base identity for OSPF LSA Options."; + } + + identity af-bit { + base ospfv3-lsa-option; + description + "AF-bit. When set, the router supports OSPFv3 Address + Families (AFs) as described in RFC 5838."; + reference + "RFC 5838: Support of Address Families in OSPFv3"; + } + + identity dc-bit { + base ospfv3-lsa-option; + description + "DC-bit. When set, the router supports demand circuits."; + } + + identity r-bit { + base ospfv3-lsa-option; + description + "R-bit. When set, the originator is an active router."; + } + + identity n-bit { + base ospfv3-lsa-option; + description + "N-bit. When set, the router is attached to an NSSA."; + } + + identity e-bit { + base ospfv3-lsa-option; + description + "E-bit. This bit describes the way AS-External-LSAs + are flooded."; + } + + identity v6-bit { + base ospfv3-lsa-option; + description + "V6-bit. If clear, the router/link should be excluded + from IPv6 routing calculations."; + } + + identity ospfv3-prefix-option { + description + "Base identity for OSPFv3 prefix options."; + } + + identity nu-bit { + base ospfv3-prefix-option; + description + "NU-bit. When set, the prefix should be excluded + from IPv6 unicast calculations."; + } + + identity la-bit { + base ospfv3-prefix-option; + description + "LA-bit. When set, the prefix is actually an IPv6 + interface address of the advertising router."; + } + + identity p-bit { + base ospfv3-prefix-option; + description + "P-bit. When set, the NSSA prefix should be + translated to an AS-External-LSA and advertised + by the translating NSSA border router."; + } + + identity dn-bit { + base ospfv3-prefix-option; + description + "DN-bit. When set, the Inter-Area-Prefix-LSA or + AS-External-LSA prefix has been advertised as an + L3VPN prefix."; + } + + identity ospfv2-lsa-option { + description + "Base identity for OSPFv2 LSA Options."; + } + + identity mt-bit { + base ospfv2-lsa-option; + description + "MT-bit. When set, the router supports multi-topology as + described in RFC 4915."; + reference + "RFC 4915: Multi-Topology (MT) Routing in OSPF"; + } + + identity v2-dc-bit { + base ospfv2-lsa-option; + description + "DC-bit. When set, the router supports demand circuits."; + } + + identity v2-p-bit { + base ospfv2-lsa-option; + description + "P-bit. Only used in type-7 LSAs. When set, an NSSA + border router should translate the type-7 LSA + to a type-5 LSA."; + } + + identity mc-bit { + base ospfv2-lsa-option; + description + "MC-bit. When set, the router supports + Multicast Extensions to OSPF (MOSPF)."; + } + + identity v2-e-bit { + base ospfv2-lsa-option; + description + "E-bit. This bit describes the way AS-External-LSAs + are flooded."; + } + + identity o-bit { + base ospfv2-lsa-option; + description + "O-bit. When set, the router is opaque capable as described + in RFC 5250."; + reference + "RFC 5250: The OSPF Opaque LSA Option"; + } + + identity v2-dn-bit { + base ospfv2-lsa-option; + description + "DN-bit. When a type 3, type 5, or type 7 LSA is sent from a + PE to a CE, the DN-bit must be set. See RFC 4576."; + reference + "RFC 4576: Using a Link State Advertisement (LSA) Options Bit + to Prevent Looping in BGP/MPLS IP Virtual Private Networks + (VPNs)"; + } + + identity ospfv2-extended-prefix-flag { + description + "Base identity for the Extended Prefix TLV flag."; + } + + identity a-flag { + base ospfv2-extended-prefix-flag; + description + "Attach flag. When set, it indicates that the prefix + corresponds to a route that is directly connected to + the advertising router."; + } + + identity node-flag { + base ospfv2-extended-prefix-flag; + description + "Node flag. When set, it indicates that the prefix is + used to represent the advertising node, e.g., a loopback + address."; + } + + typedef ospf-metric { + type uint32 { + range "0 .. 16777215"; + } + description + "OSPF metric. 24-bit unsigned integer."; + } + + typedef ospf-link-metric { + type uint16 { + range "0 .. 65535"; + } + description + "OSPF link metric. 16-bit unsigned integer."; + } + + typedef opaque-id { + type uint32 { + range "0 .. 16777215"; + } + description + "Opaque-LSA ID. 24-bit unsigned integer."; + } + + typedef area-id-type { + type yang:dotted-quad; + description + "Area ID type."; + } + + typedef route-type { + type enumeration { + enum intra-area { + description + "OSPF intra-area route."; + } + enum inter-area { + description + "OSPF inter-area route."; + } + enum external-1 { + description + "OSPF type 1 external route."; + } + enum external-2 { + description + "OSPF type 2 external route."; + } + enum nssa-1 { + description + "OSPF type 1 NSSA route."; + } + enum nssa-2 { + description + "OSPF type 2 NSSA route."; + } + } + description + "OSPF route type."; + } + + typedef if-state-type { + type enumeration { + enum down { + value 1; + description + "Interface is in the 'Down' state."; + } + enum loopback { + value 2; + description + "Interface is in the 'Loopback' state."; + } + enum waiting { + value 3; + description + "Interface is in the 'Waiting' state."; + } + enum point-to-point { + value 4; + description + "Interface is in the 'Point-to-point' state."; + } + enum dr { + value 5; + description + "Interface is in the 'DR' (Designated Router) state."; + } + enum bdr { + value 6; + description + "Interface is in the 'Backup' (Backup Designated Router + (BDR)) state."; + } + enum dr-other { + value 7; + description + "Interface is in the 'DR Other' state."; + } + } + description + "OSPF interface state type."; + reference + "RFC 2328: OSPF Version 2"; + } + + typedef router-link-type { + type enumeration { + enum point-to-point-link { + value 1; + description + "Point-to-point link to another router."; + } + enum transit-network-link { + value 2; + description + "Link to a transit network, identified by the DR."; + } + enum stub-network-link { + value 3; + description + "Link to a stub network, identified by the subnet."; + } + enum virtual-link { + value 4; + description + "Virtual link across a transit area."; + } + } + description + "OSPF router link type."; + } + + typedef nbr-state-type { + type enumeration { + enum down { + value 1; + description + "Neighbor is in the 'Down' state."; + } + enum attempt { + value 2; + description + "Neighbor is in the 'Attempt' state."; + } + enum init { + value 3; + description + "Neighbor is in the 'Init' state."; + } + enum 2-way { + value 4; + description + "Neighbor is in the '2-Way' state."; + } + enum exstart { + value 5; + description + "Neighbor is in the 'ExStart' (exchange start) state."; + } + enum exchange { + value 6; + description + "Neighbor is in the 'Exchange' state."; + } + enum loading { + value 7; + description + "Neighbor is in the 'Loading' state."; + } + enum full { + value 8; + description + "Neighbor is in the 'Full' state."; + } + } + description + "OSPF neighbor state type."; + reference + "RFC 2328: OSPF Version 2"; + } + + typedef restart-helper-status-type { + type enumeration { + enum not-helping { + value 1; + description + "Restart helper status of 'not-helping'."; + } + enum helping { + value 2; + description + "Restart helper status of 'helping'."; + } + } + description + "Restart helper status type."; + } + + typedef restart-exit-reason-type { + type enumeration { + enum none { + value 1; + description + "Restart not attempted."; + } + enum in-progress { + value 2; + description + "Restart in progress."; + } + enum completed { + value 3; + description + "Restart successfully completed."; + } + enum timed-out { + value 4; + description + "Restart timed out."; + } + enum topology-changed { + value 5; + description + "Restart aborted due to a topology change."; + } + } + description + "Describes the outcome of the last graceful restart attempt. + The local router is being restarted or acting as a helper."; + } + + typedef packet-type { + type enumeration { + enum hello { + value 1; + description + "OSPF Hello packet."; + } + enum database-description { + value 2; + description + "OSPF Database Description packet."; + } + enum link-state-request { + value 3; + description + "OSPF Link State Request packet."; + } + enum link-state-update { + value 4; + description + "OSPF Link State Update packet."; + } + enum link-state-ack { + value 5; + description + "OSPF Link State Acknowledgment packet."; + } + } + description + "OSPF packet type."; + } + + typedef nssa-translator-state-type { + type enumeration { + enum enabled { + value 1; + description + "NSSATranslatorState is 'enabled'."; + } + enum elected { + value 2; + description + "NSSATranslatorState is 'elected'."; + } + enum disabled { + value 3; + description + "NSSATranslatorState is 'disabled'."; + } + } + description + "OSPF NSSA translator state type."; + reference + "RFC 3101: The OSPF Not-So-Stubby Area (NSSA) Option"; + } + + typedef restart-status-type { + type enumeration { + enum not-restarting { + value 1; + description + "The router is not restarting."; + } + enum planned-restart { + value 2; + description + "The router is going through a planned restart."; + } + enum unplanned-restart { + value 3; + description + "The router is going through an unplanned restart."; + } + } + description + "OSPF graceful restart status type."; + } + + typedef fletcher-checksum16-type { + type string { + pattern '(0x)?[0-9a-fA-F]{4}'; + } + description + "Fletcher 16-bit checksum in hex-string format 0xXXXX."; + reference + "RFC 905: ISO Transport Protocol Specification ISO DP 8073"; + } + + typedef ospfv2-auth-trailer-rfc-version { + type enumeration { + enum rfc5709 { + description + "Support for the OSPF authentication trailer as + described in RFC 5709."; + reference + "RFC 5709: OSPFv2 HMAC-SHA Cryptographic Authentication"; + } + enum rfc7474 { + description + "Support for the OSPF authentication trailer as + described in RFC 7474."; + reference + "RFC 7474: Security Extension for OSPFv2 + When Using Manual Key Management"; + } + } + description + "OSPFv2 authentication trailer support."; + } + + grouping tlv { + description + "Type-Length-Value (TLV)."; + leaf type { + type uint16; + description + "TLV type."; + } + leaf length { + type uint16; + description + "TLV length (octets)."; + } + leaf value { + type yang:hex-string; + description + "TLV value."; + } + } + + grouping unknown-tlvs { + description + "Grouping used for unknown TLVs or unknown sub-TLVs."; + container unknown-tlvs { + description + "All unknown TLVs."; + list unknown-tlv { + description + "Unknown TLV."; + uses tlv; + } + } + } + + grouping node-tag-tlv { + description + "OSPF Node Admin Tag TLV grouping."; + list node-tag { + leaf tag { + type uint32; + description + "Value of the node administrative tag."; + } + description + "List of tags."; + } + } + + grouping router-capabilities-tlv { + description + "Grouping for OSPF router capabilities TLV types."; + reference + "RFC 7770: Extensions to OSPF for Advertising Optional + Router Capabilities"; + container router-informational-capabilities { + leaf-list informational-capabilities { + type identityref { + base informational-capability; + } + description + "List of informational capabilities. This list will + contain the identities for the informational + capabilities supported by the router."; + } + description + "OSPF Router Informational Flag definitions."; + } + list informational-capabilities-flags { + leaf informational-flag { + type uint32; + description + "Individual informational capability flag."; + } + description + "List of informational capability flags. This will + return all the 32-bit informational flags, irrespective + of whether or not they are known to the device."; + } + list functional-capabilities { + leaf functional-flag { + type uint32; + description + "Individual functional capability flag."; + } + description + "List of functional capability flags. This will + return all the 32-bit functional flags, irrespective + of whether or not they are known to the device."; + } + } + + grouping dynamic-hostname-tlv { + description + "Dynamic Hostname TLV."; + reference + "RFC 5642: Dynamic Hostname Exchange Mechanism for OSPF"; + leaf hostname { + type string { + length "1..255"; + } + description + "Dynamic hostname."; + } + } + + grouping sbfd-discriminator-tlv { + description + "S-BFD Discriminator TLV."; + reference + "RFC 7884: OSPF Extensions to Advertise Seamless Bidirectional + Forwarding Detection (S-BFD) Target Discriminators"; + list sbfd-discriminators { + leaf sbfd-discriminator { + type uint32; + description + "Individual S-BFD Discriminator."; + } + description + "List of S-BFD Discriminators."; + } + } + + grouping maximum-sid-depth-tlv { + description + "Node MSD TLV (TLV for Maximum SID Depth)."; + reference + "RFC 8476: Signaling Maximum SID Depth (MSD) Using OSPF"; + list msd-type { + leaf msd-type { + type uint8; + description + "Maximum SID Depth (MSD) type."; + } + leaf msd-value { + type uint8; + description + "MSD value for the type."; + } + description + "List of MSD tuples."; + } + } + + grouping ospf-router-lsa-bits { + container router-bits { + leaf-list rtr-lsa-bits { + type identityref { + base router-lsa-bit; + } + description + "List of Router-LSA bits. This list will contain + identities for the bits; these identities are set + in the Router-LSA bits."; + } + description + "Router-LSA bits."; + } + description + "Router-LSA bits. Currently common to both OSPFv2 and + OSPFv3 but may diverge with future augmentations."; + } + + grouping ospfv2-router-link { + description + "OSPFv2 router link."; + leaf link-id { + type union { + type inet:ipv4-address; + type yang:dotted-quad; + } + description + "Router-LSA Link ID."; + } + leaf link-data { + type union { + type inet:ipv4-address; + type uint32; + } + description + "Router-LSA link data."; + } + leaf type { + type router-link-type; + description + "Router-LSA link type."; + } + } + + grouping ospfv2-lsa-body { + description + "OSPFv2 LSA body."; + container router { + when "derived-from-or-self(../../header/type, " + + "'ospfv2-router-lsa')" { + description + "Only applies to Router-LSAs."; + } + description + "Router-LSA."; + uses ospf-router-lsa-bits; + leaf num-of-links { + type uint16; + description + "Number of links in the Router-LSA."; + } + container links { + description + "All router links."; + list link { + description + "Router-LSA link."; + uses ospfv2-router-link; + container topologies { + description + "All topologies for the link."; + list topology { + description + "Topology-specific information."; + leaf mt-id { + type uint8; + description + "The MT-ID for the topology enabled on the link."; + } + leaf metric { + type uint16; + description + "Metric for the topology."; + } + } + } + } + } + } + container network { + when "derived-from-or-self(../../header/type, " + + "'ospfv2-network-lsa')" { + description + "Only applies to Network-LSAs."; + } + description + "Network-LSA."; + leaf network-mask { + type yang:dotted-quad; + description + "The IP address mask for the network."; + } + container attached-routers { + description + "All attached routers."; + leaf-list attached-router { + type inet:ipv4-address; + description + "List of the routers attached to the network."; + } + } + } + container summary { + when "derived-from(../../header/type, " + + "'ospfv2-summary-lsa-type')" { + description + "Only applies to summary LSAs."; + } + description + "Summary LSA."; + leaf network-mask { + type inet:ipv4-address; + description + "The IP address mask for the network."; + } + container topologies { + description + "All topologies for the summary LSA."; + list topology { + description + "Topology-specific information."; + leaf mt-id { + type uint8; + description + "The MT-ID for the topology enabled for the summary."; + } + leaf metric { + type ospf-metric; + description + "Metric for the topology."; + } + } + } + } + container external { + when "derived-from(../../header/type, " + + "'ospfv2-external-lsa-type')" { + description + "Only applies to AS-External-LSAs and NSSA-LSAs."; + } + description + "External-LSA."; + leaf network-mask { + type inet:ipv4-address; + description + "The IP address mask for the network."; + } + container topologies { + description + "All topologies for the External-LSA."; + list topology { + description + "Topology-specific information."; + leaf mt-id { + type uint8; + description + "The MT-ID for the topology enabled for the + external or NSSA prefix."; + } + leaf flags { + type bits { + bit E { + description + "When set, the metric specified is a Type 2 + external metric."; + } + } + description + "Topology flags."; + } + leaf metric { + type ospf-metric; + description + "Metric for the topology."; + } + leaf forwarding-address { + type inet:ipv4-address; + description + "IPv4 Forwarding address."; + } + leaf external-route-tag { + type uint32; + description + "Route tag for the topology."; + } + } + } + } + container opaque { + when "derived-from(../../header/type, " + + "'ospfv2-opaque-lsa-type')" { + description + "Only applies to Opaque-LSAs."; + } + description + "Opaque-LSA."; + + container ri-opaque { + description + "OSPF Router-Information-Opaque-LSA."; + reference + "RFC 7770: Extensions to OSPF for Advertising Optional + Router Capabilities"; + + container router-capabilities-tlv { + description + "Informational and functional router capabilities."; + uses router-capabilities-tlv; + } + + container node-tag-tlvs { + description + "All Node Admin Tag TLVs."; + list node-tag-tlv { + description + "Node Admin Tag TLV."; + uses node-tag-tlv; + } + } + + container dynamic-hostname-tlv { + description + "OSPF Dynamic Hostname TLV."; + uses dynamic-hostname-tlv; + } + + container sbfd-discriminator-tlv { + description + "OSPF S-BFD Discriminator TLV."; + uses sbfd-discriminator-tlv; + } + + container maximum-sid-depth-tlv { + description + "OSPF Node MSD TLV."; + uses maximum-sid-depth-tlv; + } + uses unknown-tlvs; + } + + container te-opaque { + description + "OSPFv2 TE Opaque-LSA."; + reference + "RFC 3630: Traffic Engineering (TE) Extensions to + OSPF Version 2"; + + container router-address-tlv { + description + "Router address TLV."; + leaf router-address { + type inet:ipv4-address; + description + "Router address."; + } + } + + container link-tlv { + description + "Describes a single link. It is constructed + from a set of sub-TLVs."; + leaf link-type { + type router-link-type; + mandatory true; + description + "Link type."; + } + leaf link-id { + type union { + type inet:ipv4-address; + type yang:dotted-quad; + } + mandatory true; + description + "Link ID."; + } + container local-if-ipv4-addrs { + description + "All local interface IPv4 addresses."; + leaf-list local-if-ipv4-addr { + type inet:ipv4-address; + description + "List of local interface IPv4 addresses."; + } + } + container remote-if-ipv4-addrs { + description + "All remote interface IPv4 addresses."; + leaf-list remote-if-ipv4-addr { + type inet:ipv4-address; + description + "List of remote interface IPv4 addresses."; + } + } + leaf te-metric { + type uint32; + description + "TE metric."; + } + leaf max-bandwidth { + type rt-types:bandwidth-ieee-float32; + description + "Maximum bandwidth."; + } + leaf max-reservable-bandwidth { + type rt-types:bandwidth-ieee-float32; + description + "Maximum reservable bandwidth."; + } + container unreserved-bandwidths { + description + "All unreserved bandwidths."; + list unreserved-bandwidth { + leaf priority { + type uint8 { + range "0 .. 7"; + } + description + "Priority from 0 to 7."; + } + leaf unreserved-bandwidth { + type rt-types:bandwidth-ieee-float32; + description + "Unreserved bandwidth."; + } + description + "List of unreserved bandwidths for different + priorities."; + } + } + leaf admin-group { + type uint32; + description + "Administrative Group / Resource Class/Color."; + } + uses unknown-tlvs; + } + } + + container extended-prefix-opaque { + description + "All Extended Prefix TLVs in the LSA."; + list extended-prefix-tlv { + description + "Extended Prefix TLV."; + leaf route-type { + type enumeration { + enum unspecified { + value 0; + description + "Unspecified."; + } + enum intra-area { + value 1; + description + "OSPF intra-area route."; + } + enum inter-area { + value 3; + description + "OSPF inter-area route."; + } + enum external { + value 5; + description + "OSPF external route."; + } + enum nssa { + value 7; + description + "OSPF NSSA external route."; + } + } + description + "Route type."; + } + container flags { + leaf-list extended-prefix-flags { + type identityref { + base ospfv2-extended-prefix-flag; + } + description + "List of Extended Prefix TLV flags. This list will + contain identities for the prefix flags; these + identities are set in the extended prefix flags."; + } + description + "Prefix flags."; + } + leaf prefix { + type inet:ip-prefix; + description + "Address prefix."; + } + uses unknown-tlvs; + } + } + + container extended-link-opaque { + description + "All Extended Link TLVs in the LSA."; + reference + "RFC 7684: OSPFv2 Prefix/Link Attribute Advertisement"; + container extended-link-tlv { + description + "Extended Link TLV."; + uses ospfv2-router-link; + container maximum-sid-depth-tlv { + description + "OSPF Node MSD TLV."; + uses maximum-sid-depth-tlv; + } + uses unknown-tlvs; + } + } + } + } + + grouping ospfv3-lsa-options { + description + "OSPFv3 LSA Options."; + container lsa-options { + leaf-list lsa-options { + type identityref { + base ospfv3-lsa-option; + } + description + "List of OSPFv3 LSA Options. This list will contain + the identities for the OSPFv3 LSA Options that are + set for the LSA."; + } + description + "OSPFv3 LSA Options."; + } + } + + grouping ospfv3-lsa-prefix { + description + "OSPFv3 LSA prefix."; + + leaf prefix { + type inet:ip-prefix; + description + "LSA prefix."; + } + container prefix-options { + leaf-list prefix-options { + type identityref { + base ospfv3-prefix-option; + } + description + "List of OSPFv3 prefix Options. This list will + contain the identities for the OSPFv3 options + that are set for the OSPFv3 prefix."; + } + description + "Prefix options."; + } + } + + grouping ospfv3-lsa-external { + description + "AS-External-LSA or NSSA-LSA."; + leaf metric { + type ospf-metric; + description + "AS-External-LSA or NSSA-LSA Metric."; + } + leaf flags { + type bits { + bit E { + description + "When set, the metric specified is a Type 2 + external metric."; + } + bit F { + description + "When set, a forwarding address is included + in the LSA."; + } + bit T { + description + "When set, an external route tag is included + in the LSA."; + } + } + description + "AS-External-LSA or NSSA-LSA flags."; + } + + leaf referenced-ls-type { + type identityref { + base ospfv3-lsa-type; + } + description + "Referenced Link State (LS) Type."; + reference + "RFC 5340: OSPF for IPv6"; + } + leaf unknown-referenced-ls-type { + type uint16; + description + "Value for an unknown Referenced LS Type."; + } + + uses ospfv3-lsa-prefix; + + leaf forwarding-address { + type inet:ipv6-address; + description + "IPv6 Forwarding address."; + } + + leaf external-route-tag { + type uint32; + description + "Route tag."; + } + leaf referenced-link-state-id { + type uint32; + description + "Referenced Link State ID."; + reference + "RFC 5340: OSPF for IPv6"; + } + } + + grouping ospfv3-lsa-body { + description + "OSPFv3 LSA body."; + container router { + when "derived-from-or-self(../../header/type, " + + "'ospfv3-router-lsa')" { + description + "Only applies to Router-LSAs."; + } + description + "Router-LSA."; + uses ospf-router-lsa-bits; + uses ospfv3-lsa-options; + + container links { + description + "All router links."; + list link { + description + "Router-LSA link."; + leaf interface-id { + type uint32; + description + "Interface ID for the link."; + } + leaf neighbor-interface-id { + type uint32; + description + "Neighbor's Interface ID for the link."; + } + leaf neighbor-router-id { + type rt-types:router-id; + description + "Neighbor's Router ID for the link."; + } + leaf type { + type router-link-type; + description + "Link type: 1 - Point-to-Point Link + 2 - Transit Network Link + 3 - Reserved for OSPFv3 Links + 4 - Virtual Link."; + } + leaf metric { + type uint16; + description + "Link metric."; + } + } + } + } + container network { + when "derived-from-or-self(../../header/type, " + + "'ospfv3-network-lsa')" { + description + "Only applies to Network-LSAs."; + } + description + "Network-LSA."; + + uses ospfv3-lsa-options; + + container attached-routers { + description + "All attached routers."; + leaf-list attached-router { + type rt-types:router-id; + description + "List of the routers attached to the network."; + } + } + } + container inter-area-prefix { + when "derived-from-or-self(../../header/type, " + + "'ospfv3-inter-area-prefix-lsa')" { + description + "Only applies to Inter-Area-Prefix-LSAs."; + } + leaf metric { + type ospf-metric; + description + "Inter-Area Prefix metric."; + } + uses ospfv3-lsa-prefix; + description + "Prefix-LSA."; + } + container inter-area-router { + when "derived-from-or-self(../../header/type, " + + "'ospfv3-inter-area-router-lsa')" { + description + "Only applies to Inter-Area-Router-LSAs."; + } + uses ospfv3-lsa-options; + leaf metric { + type ospf-metric; + description + "Autonomous System Boundary Router (ASBR) metric."; + } + leaf destination-router-id { + type rt-types:router-id; + description + "The Router ID of the ASBR described by the LSA."; + } + description + "Inter-Area-Router-LSA."; + } + container as-external { + when "derived-from-or-self(../../header/type, " + + "'ospfv3-as-external-lsa')" { + description + "Only applies to AS-External-LSAs."; + } + + uses ospfv3-lsa-external; + + description + "AS-External-LSA."; + } + container nssa { + when "derived-from-or-self(../../header/type, " + + "'ospfv3-nssa-lsa')" { + description + "Only applies to NSSA-LSAs."; + } + uses ospfv3-lsa-external; + + description + "NSSA-LSA."; + } + container link { + when "derived-from-or-self(../../header/type, " + + "'ospfv3-link-lsa')" { + description + "Only applies to Link-LSAs."; + } + leaf rtr-priority { + type uint8; + description + "Router priority for DR election. A router with a + higher priority will be preferred in the election. + A value of 0 indicates that the router is not eligible + to become the DR or BDR."; + } + uses ospfv3-lsa-options; + + leaf link-local-interface-address { + type inet:ipv6-address; + description + "The originating router's link-local + interface address for the link."; + } + + leaf num-of-prefixes { + type uint32; + description + "Number of prefixes."; + } + + container prefixes { + description + "All prefixes for the link."; + list prefix { + description + "List of prefixes associated with the link."; + uses ospfv3-lsa-prefix; + } + } + description + "Link-LSA."; + } + container intra-area-prefix { + when "derived-from-or-self(../../header/type, " + + "'ospfv3-intra-area-prefix-lsa')" { + description + "Only applies to Intra-Area-Prefix-LSAs."; + } + description + "Intra-Area-Prefix-LSA."; + + leaf referenced-ls-type { + type identityref { + base ospfv3-lsa-type; + } + description + "Referenced LS Type."; + } + leaf unknown-referenced-ls-type { + type uint16; + description + "Value for an unknown Referenced LS Type."; + } + leaf referenced-link-state-id { + type uint32; + description + "Referenced Link State ID."; + } + leaf referenced-adv-router { + type rt-types:router-id; + description + "Referenced Advertising Router."; + reference + "RFC 5340: OSPF for IPv6"; + } + + leaf num-of-prefixes { + type uint16; + description + "Number of prefixes."; + } + container prefixes { + description + "All prefixes in this LSA."; + list prefix { + description + "List of prefixes in this LSA."; + uses ospfv3-lsa-prefix; + leaf metric { + type uint16; + description + "Prefix metric."; + } + } + } + } + container router-information { + when "derived-from-or-self(../../header/type, " + + "'ospfv3-router-information-lsa')" { + description + "Only applies to Router-Information-LSAs (RFC 7770)."; + reference + "RFC 7770: Extensions to OSPF for Advertising Optional + Router Capabilities"; + } + container router-capabilities-tlv { + description + "Informational and functional router capabilities."; + uses router-capabilities-tlv; + } + container node-tag-tlvs { + description + "All Node Admin Tag TLVs."; + list node-tag-tlv { + description + "Node Admin Tag TLV."; + uses node-tag-tlv; + } + } + container dynamic-hostname-tlv { + description + "OSPF Dynamic Hostname TLV."; + uses dynamic-hostname-tlv; + } + + container sbfd-discriminator-tlv { + description + "OSPF S-BFD Discriminator TLV."; + uses sbfd-discriminator-tlv; + } + + description + "Router-Information-LSA."; + reference + "RFC 7770: Extensions to OSPF for Advertising Optional + Router Capabilities"; + } + } + + grouping lsa-header { + description + "Common LSA for OSPFv2 and OSPFv3."; + leaf age { + type uint16; + mandatory true; + description + "LSA age."; + } + leaf type { + type identityref { + base ospf-lsa-type; + } + mandatory true; + description + "LSA type."; + } + leaf adv-router { + type rt-types:router-id; + mandatory true; + description + "LSA advertising router."; + } + leaf seq-num { + type uint32; + mandatory true; + description + "LSA sequence number."; + } + leaf checksum { + type fletcher-checksum16-type; + mandatory true; + description + "LSA checksum."; + } + leaf length { + type uint16; + mandatory true; + description + "LSA length, including the header."; + } + } + + grouping ospfv2-lsa { + description + "OSPFv2 LSA. LSAs are uniquely identified by + the + tuple, with the sequence number differentiating the + LSA instances."; + container header { + must "(derived-from(type, " + + "'ospfv2-opaque-lsa-type') and " + + "opaque-id and opaque-type) or " + + "(not(derived-from(type, " + + "'ospfv2-opaque-lsa-type')) " + + "and not(opaque-id) and not(opaque-type))" { + description + "The opaque-type and the opaque-id only apply to + Opaque-LSAs."; + } + description + "Decoded OSPFv2 LSA header data."; + + container lsa-options { + leaf-list lsa-options { + type identityref { + base ospfv2-lsa-option; + } + description + "List of LSA Options. This list will contain the + identities for the OSPFv2 LSA Options that are set."; + } + description + "LSA Options."; + } + + leaf lsa-id { + type yang:dotted-quad; + mandatory true; + description + "Link State ID."; + } + + leaf opaque-type { + type uint8; + description + "Opaque-LSA type."; + } + + leaf opaque-id { + type opaque-id; + description + "Opaque-LSA ID."; + } + + uses lsa-header; + } + container body { + description + "Decoded OSPFv2 LSA body data."; + uses ospfv2-lsa-body; + } + } + + grouping ospfv3-lsa { + description + "Decoded OSPFv3 LSA."; + container header { + description + "Decoded OSPFv3 LSA header data."; + leaf lsa-id { + type uint32; + mandatory true; + description + "OSPFv3 LSA ID."; + } + uses lsa-header; + } + container body { + description + "Decoded OSPF LSA body data."; + uses ospfv3-lsa-body; + } + } + grouping lsa-common { + description + "Common fields for OSPF LSA representation."; + leaf decode-completed { + type boolean; + description + "The OSPF LSA body was successfully decoded, except for + unknown TLVs. Unknown LSA types and OSPFv2 unknown + Opaque-LSA types are not decoded. Additionally, + malformed LSAs are generally not accepted and will + not be in the Link State Database (LSDB)."; + } + leaf raw-data { + type yang:hex-string; + description + "The hexadecimal representation of the complete LSA as + received or originated, in network byte order."; + } + } + + grouping lsa { + description + "OSPF LSA."; + uses lsa-common; + choice version { + description + "OSPFv2 or OSPFv3 LSA body."; + container ospfv2 { + description + "OSPFv2 LSA."; + uses ospfv2-lsa; + } + container ospfv3 { + description + "OSPFv3 LSA."; + uses ospfv3-lsa; + } + } + } + + grouping lsa-key { + description + "OSPF LSA key. The database key for each LSA of a given + type in the LSDB."; + leaf lsa-id { + type union { + type yang:dotted-quad; + type uint32; + } + description + "Link State ID."; + } + leaf adv-router { + type rt-types:router-id; + description + "Advertising router."; + } + } + + grouping instance-stat { + description + "Per-instance statistics."; + leaf discontinuity-time { + type yang:date-and-time; + description + "The time of the most recent occasion at which any one or + more of this OSPF instance's counters suffered a + discontinuity. If no such discontinuities have occurred + since the OSPF instance was last reinitialized, then + this node contains the time the OSPF instance was + reinitialized, which normally occurs when it was + created."; + } + leaf originate-new-lsa-count { + type yang:counter32; + description + "The number of new LSAs originated. Discontinuities in the + value of this counter can occur when the OSPF instance is + reinitialized."; + } + leaf rx-new-lsas-count { + type yang:counter32; + description + "The number of new LSAs received. Discontinuities in the + value of this counter can occur when the OSPF instance is + reinitialized."; + } + leaf as-scope-lsa-count { + type yang:gauge32; + description + "The number of AS-Scope LSAs."; + } + leaf as-scope-lsa-chksum-sum { + type uint32; + description + "The modulo 2^32 sum of the LSA checksums + for AS-Scope LSAs. The value should be treated as + unsigned when comparing two sums of checksums. While + differing checksums indicate a different combination + of LSAs, equivalent checksums don't guarantee that the + LSAs are the same, given that multiple combinations of + LSAs can result in the same checksum."; + } + container database { + description + "Container for per-AS-Scope LSA statistics."; + list as-scope-lsa-type { + description + "List of AS-Scope LSA statistics."; + leaf lsa-type { + type uint16; + description + "AS-Scope LSA type."; + } + leaf lsa-count { + type yang:gauge32; + description + "The number of LSAs of this LSA type."; + } + leaf lsa-cksum-sum { + type uint32; + description + "The modulo 2^32 sum of the LSA checksums + for LSAs of this type. The value should be + treated as unsigned when comparing two sums of + checksums. While differing checksums indicate a + different combination of LSAs, equivalent checksums + don't guarantee that the LSAs are the same, given that + multiple combinations of LSAs can result in the same + checksum."; + } + } + } + uses instance-fast-reroute-state; + } + + grouping area-stat { + description + "Per-area statistics."; + leaf discontinuity-time { + type yang:date-and-time; + description + "The time of the most recent occasion at which any one or + more of this OSPF area's counters suffered a + discontinuity. If no such discontinuities have occurred + since the OSPF area was last reinitialized, then + this node contains the time the OSPF area was + reinitialized, which normally occurs when it was + created."; + } + leaf spf-runs-count { + type yang:counter32; + description + "The number of times the intra-area SPF has run. + Discontinuities in the value of this counter can occur + when the OSPF area is reinitialized."; + } + leaf abr-count { + type yang:gauge32; + description + "The total number of Area Border Routers (ABRs) + reachable within this area."; + } + leaf asbr-count { + type yang:gauge32; + description + "The total number of AS Boundary Routers (ASBRs) + reachable within this area."; + } + leaf ar-nssa-translator-event-count { + type yang:counter32; + description + "The number of NSSA translator-state changes. + Discontinuities in the value of this counter can occur + when the OSPF area is reinitialized."; + } + leaf area-scope-lsa-count { + type yang:gauge32; + description + "The number of area-scope LSAs in the area."; + } + leaf area-scope-lsa-cksum-sum { + type uint32; + description + "The modulo 2^32 sum of the LSA checksums + for area-scope LSAs. The value should be treated as + unsigned when comparing two sums of checksums. While + differing checksums indicate a different combination + of LSAs, equivalent checksums don't guarantee that the + LSAs are the same, given that multiple combinations of + LSAs can result in the same checksum."; + } + container database { + description + "Container for area-scope LSA type statistics."; + list area-scope-lsa-type { + description + "List of area-scope LSA statistics."; + leaf lsa-type { + type uint16; + description + "Area-scope LSA type."; + } + leaf lsa-count { + type yang:gauge32; + description + "The number of LSAs of this LSA type."; + } + leaf lsa-cksum-sum { + type uint32; + description + "The modulo 2^32 sum of the LSA checksums + for LSAs of this type. The value should be + treated as unsigned when comparing two sums of + checksums. While differing checksums indicate a + different combination of LSAs, equivalent checksums + don't guarantee that the LSAs are the same, given that + multiple combinations of LSAs can result in the same + checksum."; + } + } + } + } + + grouping interface-stat { + description + "Per-interface statistics."; + leaf discontinuity-time { + type yang:date-and-time; + description + "The time of the most recent occasion at which any one or + more of this OSPF interface's counters suffered a + discontinuity. If no such discontinuities have occurred + since the OSPF interface was last reinitialized, then + this node contains the time the OSPF interface was + reinitialized, which normally occurs when it was + created."; + } + leaf if-event-count { + type yang:counter32; + description + "The number of times this interface has changed its + state or an error has occurred. Discontinuities in the + value of this counter can occur when the OSPF interface + is reinitialized."; + } + leaf link-scope-lsa-count { + type yang:gauge32; + description + "The number of link-scope LSAs."; + } + leaf link-scope-lsa-cksum-sum { + type uint32; + description + "The modulo 2^32 sum of the LSA checksums + for link-scope LSAs. The value should be treated as + unsigned when comparing two sums of checksums. While + differing checksums indicate a different combination + of LSAs, equivalent checksums don't guarantee that the + LSAs are the same, given that multiple combinations of + LSAs can result in the same checksum."; + } + container database { + description + "Container for link-scope LSA type statistics."; + list link-scope-lsa-type { + description + "List of link-scope LSA statistics."; + leaf lsa-type { + type uint16; + description + "Link-scope LSA type."; + } + leaf lsa-count { + type yang:gauge32; + description + "The number of LSAs of this LSA type."; + } + leaf lsa-cksum-sum { + type uint32; + description + "The modulo 2^32 sum of the LSA checksums + for LSAs of this type. The value should be + treated as unsigned when comparing two sums of + checksums. While differing checksums indicate a + different combination of LSAs, equivalent checksums + don't guarantee that the LSAs are the same, given that + multiple combinations of LSAs can result in the same + checksum."; + } + } + } + } + + grouping neighbor-stat { + description + "Per-neighbor statistics."; + leaf discontinuity-time { + type yang:date-and-time; + description + "The time of the most recent occasion at which any one or + more of this OSPF neighbor's counters suffered a + discontinuity. If no such discontinuities have occurred + since the OSPF neighbor was last reinitialized, then + this node contains the time the OSPF neighbor was + reinitialized, which normally occurs when the neighbor + is dynamically discovered and created."; + } + leaf nbr-event-count { + type yang:counter32; + description + "The number of times this neighbor has changed + state or an error has occurred. Discontinuities in the + value of this counter can occur when the OSPF neighbor + is reinitialized."; + } + leaf nbr-retrans-qlen { + type yang:gauge32; + description + "The current length of the retransmission queue."; + } + } + + grouping instance-fast-reroute-config { + description + "This group defines the global configuration of + IP Fast Reroute (IP-FRR)."; + container fast-reroute { + if-feature "fast-reroute"; + description + "This container may be augmented with global + parameters for IP-FRR."; + container lfa { + if-feature "lfa"; + description + "This container may be augmented with + global parameters for Loop-Free Alternates (LFAs). + Container creation has no effect on LFA activation."; + } + } + } + + grouping instance-fast-reroute-state { + description + "IP-FRR state data grouping."; + + container protected-routes { + if-feature "fast-reroute"; + config false; + description + "Instance protection statistics."; + + list address-family-stats { + key "address-family prefix alternate"; + description + "Per-Address-Family (AF) protected prefix information."; + + leaf address-family { + type iana-rt-types:address-family; + description + "Address family."; + } + leaf prefix { + type inet:ip-prefix; + description + "Protected prefix."; + } + leaf alternate { + type inet:ip-address; + description + "Alternate next hop for the prefix."; + } + leaf alternate-type { + type enumeration { + enum equal-cost { + description + "ECMP-based alternate."; + } + enum lfa { + description + "LFA-based alternate."; + } + enum remote-lfa { + description + "Remote-LFA-based alternate."; + } + enum tunnel { + description + "Tunnel-based alternate (like RSVP-TE or GRE)."; + } + enum ti-lfa { + description + "An alternate based on Topology-Independent + Loop-Free Alternate (TI-LFA)."; + } + enum mrt { + description + "An alternate based on Maximally Redundant Trees + (MRTs)."; + } + enum other { + description + "Unknown alternate type."; + } + } + description + "Type of alternate."; + } + leaf best { + type boolean; + description + "Indicates that this alternate is preferred."; + } + leaf non-best-reason { + type string { + length "1..255"; + } + description + "Information field used to describe why the alternate + is not the best choice."; + } + leaf protection-available { + type bits { + bit node-protect { + position 0; + description + "Node protection available."; + } + bit link-protect { + position 1; + description + "Link protection available."; + } + bit srlg-protect { + position 2; + description + "Shared Risk Link Group (SRLG) protection + available."; + } + bit downstream-protect { + position 3; + description + "Downstream protection available."; + } + bit other { + position 4; + description + "Other protection available."; + } + } + description + "Protection provided by the alternate."; + } + leaf alternate-metric-1 { + type uint32; + description + "Metric from the Point of Local Repair (PLR) to + the destination through the alternate path."; + } + leaf alternate-metric-2 { + type uint32; + description + "Metric from the PLR to the alternate node."; + } + leaf alternate-metric-3 { + type uint32; + description + "Metric from the alternate node to the destination."; + } + } + } + + container unprotected-routes { + if-feature "fast-reroute"; + config false; + description + "List of prefixes that are not protected."; + + list address-family-stats { + key "address-family prefix"; + description + "Per-AF unprotected prefix statistics."; + + leaf address-family { + type iana-rt-types:address-family; + description + "Address family."; + } + leaf prefix { + type inet:ip-prefix; + description + "Unprotected prefix."; + } + } + } + + list protection-statistics { + key "frr-protection-method"; + config false; + description + "List of protection method statistics."; + + leaf frr-protection-method { + type string; + description + "Protection method used."; + } + list address-family-stats { + key "address-family"; + description + "Per-AF protection statistics."; + + leaf address-family { + type iana-rt-types:address-family; + description + "Address family."; + } + leaf total-routes { + type uint32; + description + "Total prefixes."; + } + leaf unprotected-routes { + type uint32; + description + "Total prefixes that are not protected."; + } + leaf protected-routes { + type uint32; + description + "Total prefixes that are protected."; + } + leaf linkprotected-routes { + type uint32; + description + "Total prefixes that are link protected."; + } + leaf nodeprotected-routes { + type uint32; + description + "Total prefixes that are node protected."; + } + } + } + } + + grouping interface-fast-reroute-config { + description + "This group defines interface configuration of IP-FRR."; + container fast-reroute { + if-feature "fast-reroute"; + container lfa { + if-feature "lfa"; + leaf candidate-enabled { + type boolean; + default "true"; + description + "Enables the interface to be used as a backup."; + } + leaf enabled { + type boolean; + default "false"; + description + "Activates an LFA. Per-prefix LFA computation + is assumed."; + } + container remote-lfa { + if-feature "remote-lfa"; + leaf enabled { + type boolean; + default "false"; + description + "Activates a Remote LFA (R-LFA)."; + } + description + "R-LFA configuration."; + } + description + "LFA configuration."; + } + description + "Interface IP-FRR configuration."; + } + } + + grouping interface-physical-link-config { + description + "Interface cost configuration that only applies to + physical interfaces (non-virtual) and sham links."; + leaf cost { + type ospf-link-metric; + description + "Interface's cost."; + } + leaf mtu-ignore { + if-feature "mtu-ignore"; + type boolean; + description + "Enables/disables bypassing the MTU mismatch check in + Database Description packets as specified in Section 10.6 + of RFC 2328."; + reference + "RFC 2328: OSPF Version 2, Section 10.6"; + } + leaf prefix-suppression { + if-feature "prefix-suppression"; + type boolean; + description + "Suppresses advertisement of the prefixes associated + with the interface."; + } + } + + grouping interface-common-config { + description + "Common configuration for all types of interfaces, + including virtual links and sham links."; + + leaf hello-interval { + type uint16; + units "seconds"; + description + "Interval between Hello packets (seconds). It must + be the same for all routers on the same network. + Different networks, implementations, and deployments + will use different Hello intervals. A sample value + for a LAN network would be 10 seconds."; + reference + "RFC 2328: OSPF Version 2, Appendix C.3"; + } + + leaf dead-interval { + type uint16; + units "seconds"; + must '../dead-interval > ../hello-interval' { + error-message "The dead interval must be " + + "larger than the Hello interval"; + description + "The value must be greater than 'hello-interval'."; + } + description + "Interval after which a neighbor is declared down + (seconds) if Hello packets are not received. It is + typically 3 or 4 times the 'hello-interval' period. + A typical value for LAN networks is 40 seconds."; + reference + "RFC 2328: OSPF Version 2, Appendix C.3"; + } + + leaf retransmit-interval { + type uint16 { + range "1..3600"; + } + units "seconds"; + description + "Interval between retransmitting unacknowledged Link + State Advertisements (LSAs) (seconds). This should + be well over the round-trip transmit delay for + any two routers on the network. A sample value + would be 5 seconds."; + reference + "RFC 2328: OSPF Version 2, Appendix C.3"; + } + + leaf transmit-delay { + type uint16; + units "seconds"; + description + "Estimated time needed to transmit Link State Update + (LSU) packets on the interface (seconds). LSAs have + their age incremented by this amount when advertised + on the interface. A sample value would be 1 second."; + reference + "RFC 2328: OSPF Version 2, Appendix C.3"; + } + + leaf lls { + if-feature "lls"; + type boolean; + description + "Enables/disables link-local signaling (LLS) support."; + } + + container ttl-security { + if-feature "ttl-security"; + description + "Time to Live (TTL) security checking."; + leaf enabled { + type boolean; + description + "Enables/disables TTL security checking."; + } + leaf hops { + type uint8 { + range "1..254"; + } + default "1"; + description + "Maximum number of hops that an OSPF packet may + have traversed before reception."; + } + } + leaf enabled { + type boolean; + default "true"; + description + "Enables/disables the OSPF protocol on the interface."; + } + + container authentication { + description + "Authentication configuration."; + choice auth-type-selection { + description + "Options for OSPFv2/OSPFv3 authentication + configuration."; + case ospfv2-auth { + when "derived-from-or-self(../../../../../../rt:type, " + + "'ospfv2')" { + description + "Applied to OSPFv2 only."; + } + leaf ospfv2-auth-trailer-rfc { + if-feature "ospfv2-authentication-trailer"; + type ospfv2-auth-trailer-rfc-version; + description + "Version of OSPFv2 authentication trailer support. + See RFCs 5709 and 7474."; + reference + "RFC 5709: OSPFv2 HMAC-SHA Cryptographic Authentication + RFC 7474: Security Extension for OSPFv2 When Using + Manual Key Management"; + } + choice ospfv2-auth-specification { + description + "Key chain or explicit key parameter specification."; + case auth-key-chain { + if-feature "key-chain"; + leaf ospfv2-key-chain { + type key-chain:key-chain-ref; + description + "Name of the key chain."; + } + } + case auth-key-explicit { + leaf ospfv2-key-id { + type uint32; + description + "Key identifier."; + } + leaf ospfv2-key { + type string; + description + "OSPFv2 authentication key. The + length of the key may be dependent on the + cryptographic algorithm."; + } + leaf ospfv2-crypto-algorithm { + type identityref { + base key-chain:crypto-algorithm; + } + description + "Cryptographic algorithm associated with the key."; + } + } + } + } + case ospfv3-auth-ipsec { + when "derived-from-or-self(../../../../../../rt:type, " + + "'ospfv3')" { + description + "Applied to OSPFv3 only."; + } + if-feature "ospfv3-authentication-ipsec"; + leaf sa { + type string; + description + "Name of the Security Association (SA)."; + } + } + case ospfv3-auth-trailer { + when "derived-from-or-self(../../../../../../rt:type, " + + "'ospfv3')" { + description + "Applied to OSPFv3 only."; + } + if-feature "ospfv3-authentication-trailer"; + choice ospfv3-auth-specification { + description + "Key chain or explicit key parameter specification."; + case auth-key-chain { + if-feature "key-chain"; + leaf ospfv3-key-chain { + type key-chain:key-chain-ref; + description + "Name of the key chain."; + } + } + case auth-key-explicit { + leaf ospfv3-sa-id { + type uint16; + description + "Security Association (SA) Identifier."; + } + leaf ospfv3-key { + type string; + description + "OSPFv3 authentication key. The + length of the key may be dependent on the + cryptographic algorithm."; + } + leaf ospfv3-crypto-algorithm { + type identityref { + base key-chain:crypto-algorithm; + } + description + "Cryptographic algorithm associated with the key."; + } + } + } + } + } + } + } + + grouping interface-config { + description + "Configuration for normal OSPF interfaces (not virtual + or sham interfaces)."; + + leaf interface-type { + type enumeration { + enum broadcast { + description + "Specifies an OSPF broadcast multi-access network."; + } + enum non-broadcast { + description + "Specifies an OSPF Non-Broadcast Multi-Access + (NBMA) network."; + } + enum point-to-multipoint { + description + "Specifies an OSPF point-to-multipoint network."; + } + enum point-to-point { + description + "Specifies an OSPF point-to-point network."; + } + enum hybrid { + if-feature "hybrid-interface"; + description + "Specifies an OSPF hybrid broadcast / + point-to-multipoint network."; + } + } + description + "Interface type."; + } + + leaf passive { + type boolean; + description + "Enables/disables a passive interface. A passive + interface's prefix will be advertised, but no neighbor + adjacencies will be formed on the interface."; + } + + leaf demand-circuit { + if-feature "demand-circuit"; + type boolean; + description + "Enables/disables a demand circuit."; + } + + leaf priority { + type uint8; + description + "Configures OSPF router priority. In a multi-access + network, this value is for Designated Router (DR) election. + The priority is ignored on other interface types. A router + with a higher priority will be preferred in the election. + A value of 0 indicates that the router is not eligible to + become the DR or Backup DR (BDR)."; + } + + container multi-areas { + if-feature "multi-area-adj"; + description + "Container for multi-area configuration."; + list multi-area { + key "multi-area-id"; + description + "Configures an OSPF multi-area adjacency."; + leaf multi-area-id { + type area-id-type; + description + "Multi-area adjacency area ID."; + } + leaf cost { + type ospf-link-metric; + description + "Interface cost for a multi-area adjacency."; + } + } + } + + container static-neighbors { + description + "Statically configured neighbors."; + + list neighbor { + key "identifier"; + description + "Specifies a static OSPF neighbor."; + + leaf identifier { + type inet:ip-address; + description + "Neighbor's Router ID, IPv4 address, or IPv6 address."; + } + + leaf cost { + type ospf-link-metric; + description + "Interface cost. Different implementations have + different default costs, with some defaulting to a + cost inversely proportional to the interface speed. + Others will default to 1, equating the cost to a + hop count."; + } + leaf poll-interval { + type uint16; + units "seconds"; + description + "Neighbor's poll interval (seconds) for sending OSPF + Hello packets to discover the neighbor on NBMA + networks. This interval dictates the granularity for + discovery of new neighbors. A sample would be + 120 seconds (2 minutes) for a legacy Packet Data + Network (PDN) X.25 network."; + reference + "RFC 2328: OSPF Version 2, Appendix C.5"; + } + leaf priority { + type uint8; + description + "Neighbor's priority for DR election. A router with a + higher priority will be preferred in the election. + A value of 0 indicates that the router is not + eligible to become the DR or BDR."; + } + } + } + + leaf node-flag { + if-feature "node-flag"; + type boolean; + default "false"; + description + "Sets the prefix as identifying the advertising router."; + reference + "RFC 7684: OSPFv2 Prefix/Link Attribute Advertisement"; + } + + container bfd { + if-feature "bfd"; + description + "BFD interface configuration."; + uses bfd-types:client-cfg-parms; + reference + "RFC 5880: Bidirectional Forwarding Detection (BFD) + RFC 5881: Bidirectional Forwarding Detection + (BFD) for IPv4 and IPv6 (Single Hop) + RFC 9314: YANG Data Model for Bidirectional Forwarding + Detection (BFD)"; + } + + uses interface-fast-reroute-config; + uses interface-common-config; + uses interface-physical-link-config; + } + + grouping neighbor-state { + description + "OSPF neighbor operational state."; + + leaf address { + type inet:ip-address; + config false; + description + "Neighbor's address."; + } + leaf dr-router-id { + type rt-types:router-id; + config false; + description + "Neighbor's DR Router ID."; + } + + leaf dr-ip-addr { + type inet:ip-address; + config false; + description + "Neighbor's DR IP address."; + } + + leaf bdr-router-id { + type rt-types:router-id; + config false; + description + "Neighbor's BDR Router ID."; + } + + leaf bdr-ip-addr { + type inet:ip-address; + config false; + description + "Neighbor's BDR IP address."; + } + leaf state { + type nbr-state-type; + config false; + description + "OSPF neighbor state."; + } + leaf cost { + type ospf-link-metric; + config false; + description + "Cost to reach the neighbor for point-to-multipoint + and Hybrid networks."; + } + leaf dead-timer { + type rt-types:timer-value-seconds16; + config false; + description + "This timer tracks the remaining time before + the neighbor is declared dead."; + } + container statistics { + config false; + description + "Per-neighbor statistics."; + uses neighbor-stat; + } + } + + grouping interface-common-state { + description + "OSPF interface common operational state."; + reference + "RFC 2328: OSPF Version 2, Section 9"; + + leaf state { + type if-state-type; + config false; + description + "Interface state."; + } + + leaf hello-timer { + type rt-types:timer-value-seconds16; + config false; + description + "This timer tracks the remaining time before the + next Hello packet is sent on the interface."; + } + + leaf wait-timer { + type rt-types:timer-value-seconds16; + config false; + description + "This timer tracks the remaining time before + the interface exits the 'Waiting' state."; + } + + leaf dr-router-id { + type rt-types:router-id; + config false; + description + "DR Router ID."; + } + + leaf dr-ip-addr { + type inet:ip-address; + config false; + description + "DR IP address."; + } + + leaf bdr-router-id { + type rt-types:router-id; + config false; + description + "BDR Router ID."; + } + + leaf bdr-ip-addr { + type inet:ip-address; + config false; + description + "BDR IP address."; + } + + container statistics { + config false; + description + "Per-interface statistics."; + uses interface-stat; + } + + container neighbors { + config false; + description + "All neighbors for the interface."; + list neighbor { + key "neighbor-router-id"; + description + "List of interface OSPF neighbors."; + leaf neighbor-router-id { + type rt-types:router-id; + description + "Neighbor's Router ID."; + } + uses neighbor-state; + } + } + container database { + config false; + description + "Link-scope LSDB."; + list link-scope-lsa-type { + key "lsa-type"; + description + "List of OSPF link-scope LSAs."; + leaf lsa-type { + type uint16; + description + "OSPF link-scope LSA type."; + } + container link-scope-lsas { + description + "All link-scope LSAs of this LSA type."; + list link-scope-lsa { + key "lsa-id adv-router"; + description + "List of OSPF link-scope LSAs."; + uses lsa-key; + uses lsa { + refine "version/ospfv2/ospfv2" { + must "derived-from-or-self( " + + "../../../../../../../../../../" + + "rt:type, 'ospfv2')" { + description + "OSPFv2 LSA."; + } + } + refine "version/ospfv3/ospfv3" { + must "derived-from-or-self( " + + "../../../../../../../../../../" + + "rt:type, 'ospfv3')" { + description + "OSPFv3 LSA."; + } + } + } + } + } + } + } + } + + grouping interface-state { + description + "OSPF interface operational state."; + reference + "RFC 2328: OSPF Version 2, Section 9"; + + uses interface-common-state; + } + + grouping virtual-link-config { + description + "OSPF virtual link configuration state."; + + uses interface-common-config; + } + + grouping virtual-link-state { + description + "OSPF virtual link operational state."; + + leaf cost { + type ospf-link-metric; + config false; + description + "Virtual link interface's cost."; + } + uses interface-common-state; + } + + grouping sham-link-config { + description + "OSPF sham link configuration state."; + + uses interface-common-config; + uses interface-physical-link-config; + } + + grouping sham-link-state { + description + "OSPF sham link operational state."; + uses interface-common-state; + } + + grouping address-family-area-config { + description + "OSPF address-family-specific area configuration state."; + + container ranges { + description + "Container for summary ranges."; + + list range { + key "prefix"; + description + "Summarizes routes matching the address/mask. + Applicable to Area Border Routers (ABRs) only."; + leaf prefix { + type inet:ip-prefix; + description + "IPv4 or IPv6 prefix."; + } + leaf advertise { + type boolean; + description + "Advertise or hide."; + } + leaf cost { + type ospf-metric; + description + "Advertised cost of a summary route."; + } + } + } + } + + grouping area-common-config { + description + "OSPF area common configuration state."; + + leaf summary { + when "derived-from(../area-type,'stub-nssa-area')" { + description + "Summary advertisement into the stub area or NSSA."; + } + type boolean; + description + "Enables/disables summary advertisement into the stub + area or NSSA."; + } + leaf default-cost { + when "derived-from(../area-type,'stub-nssa-area')" { + description + "Cost for the LSA default route advertised into the + stub area or NSSA."; + } + type ospf-metric; + description + "Sets the summary default route cost for a stub area + or NSSA."; + } + } + + grouping area-config { + description + "OSPF area configuration state."; + + leaf area-type { + type identityref { + base area-type; + } + default "normal-area"; + description + "Area type."; + } + + uses area-common-config; + uses address-family-area-config; + } + + grouping area-state { + description + "OSPF area operational state."; + + container statistics { + config false; + description + "Per-area statistics."; + uses area-stat; + } + + container database { + config false; + description + "Area-scope LSDB."; + list area-scope-lsa-type { + key "lsa-type"; + description + "List of OSPF area-scope LSAs."; + leaf lsa-type { + type uint16; + description + "OSPF area-scope LSA type."; + } + container area-scope-lsas { + description + "All area-scope LSAs."; + list area-scope-lsa { + key "lsa-id adv-router"; + description + "List of OSPF area-scope LSAs."; + uses lsa-key; + uses lsa { + refine "version/ospfv2/ospfv2" { + must "derived-from-or-self( " + + "../../../../../../../../" + + "rt:type, 'ospfv2')" { + description + "OSPFv2 LSA."; + } + } + refine "version/ospfv3/ospfv3" { + must "derived-from-or-self( " + + "../../../../../../../../" + + "rt:type, 'ospfv3')" { + description + "OSPFv3 LSA."; + } + } + } + } + } + } + } + } + + grouping local-rib { + description + "Local RIB. RIB for routes computed by the local + OSPF routing instance."; + container local-rib { + config false; + description + "Local RIB."; + list route { + key "prefix"; + description + "OSPF instance's Local Routes."; + leaf prefix { + type inet:ip-prefix; + description + "Destination prefix."; + } + container next-hops { + description + "Next hops for the route."; + list next-hop { + description + "List of next hops for the route."; + leaf outgoing-interface { + type if:interface-ref; + description + "Name of the outgoing interface."; + } + leaf next-hop { + type inet:ip-address; + description + "Address of the next hop."; + } + } + } + leaf metric { + type uint32; + description + "Metric for this route."; + } + leaf route-type { + type route-type; + description + "Route type for this route."; + } + leaf route-tag { + type uint32; + description + "Route tag for this route."; + } + } + } + } + + grouping ietf-spf-delay { + leaf initial-delay { + type uint32; + units "milliseconds"; + default "50"; + description + "Delay used while in the 'QUIET' state (milliseconds)."; + } + leaf short-delay { + type uint32; + units "milliseconds"; + default "200"; + description + "Delay used while in the 'SHORT_WAIT' state (milliseconds)."; + } + leaf long-delay { + type uint32; + units "milliseconds"; + default "5000"; + description + "Delay used while in the 'LONG_WAIT' state (milliseconds)."; + } + leaf hold-down { + type uint32; + units "milliseconds"; + default "10000"; + description + "This timer value defines the period without any changes + for the IGP to be considered stable (milliseconds)."; + } + leaf time-to-learn { + type uint32; + units "milliseconds"; + default "500"; + description + "Duration used to learn all the IGP events + related to a single network event (milliseconds)."; + } + leaf current-state { + type enumeration { + enum quiet { + description + "'QUIET' state."; + } + enum short-wait { + description + "'SHORT_WAIT' state."; + } + enum long-wait { + description + "'LONG_WAIT' state."; + } + } + config false; + description + "Current SPF back-off algorithm state."; + } + leaf remaining-time-to-learn { + type rt-types:timer-value-milliseconds; + config false; + description + "Remaining time until the time-to-learn timer fires."; + } + leaf remaining-hold-down { + type rt-types:timer-value-milliseconds; + config false; + description + "Remaining time until the hold-down timer fires."; + } + leaf last-event-received { + type yang:timestamp; + config false; + description + "Time of the last SPF triggering event."; + } + leaf next-spf-time { + type yang:timestamp; + config false; + description + "Time when the next SPF has been scheduled."; + } + leaf last-spf-time { + type yang:timestamp; + config false; + description + "Time of the last SPF computation."; + } + description + "Grouping for IETF SPF delay configuration and state."; + reference + "RFC 8405: Shortest Path First (SPF) Back-Off Delay Algorithm + for Link-State IGPs"; + } + + grouping node-tag-config { + description + "OSPF node tag configuration state."; + container node-tags { + if-feature "node-tag"; + list node-tag { + key "tag"; + leaf tag { + type uint32; + description + "Node tag value."; + } + description + "List of node tags."; + } + description + "Container for node administrative tags."; + } + } + + grouping instance-config { + description + "OSPF instance configuration state."; + + leaf enabled { + type boolean; + default "true"; + description + "Enables/disables the protocol."; + } + + leaf explicit-router-id { + if-feature "explicit-router-id"; + type rt-types:router-id; + description + "Defined in RFC 2328. A 32-bit number + that uniquely identifies the router."; + reference + "RFC 2328: OSPF Version 2"; + } + + container preference { + description + "Route preference configuration. In many + implementations, preference is referred to as + administrative distance."; + reference + "RFC 8349: A YANG Data Model for Routing Management + (NMDA Version)"; + choice scope { + description + "Options for expressing preference + as single or multiple values."; + case single-value { + leaf all { + type uint8; + description + "Preference for intra-area, inter-area, and + external routes."; + } + } + case multi-values { + choice granularity { + description + "Options for expressing preference + for intra-area and inter-area routes."; + case detail { + leaf intra-area { + type uint8; + description + "Preference for intra-area routes."; + } + leaf inter-area { + type uint8; + description + "Preference for inter-area routes."; + } + } + case coarse { + leaf internal { + type uint8; + description + "Preference for both intra-area and + inter-area routes."; + } + } + } + leaf external { + type uint8; + description + "Preference for AS external and NSSA routes."; + } + } + } + } + + container nsr { + if-feature "nsr"; + description + "Non-Stop Routing (NSR) configuration state."; + leaf enabled { + type boolean; + description + "Enables/disables NSR."; + } + } + + container graceful-restart { + if-feature "graceful-restart"; + description + "Graceful restart configuration state."; + reference + "RFC 3623: Graceful OSPF Restart + RFC 5187: OSPFv3 Graceful Restart"; + leaf enabled { + type boolean; + description + "Enables/disables graceful restart as defined in RFC 3623 + for OSPFv2 and RFC 5187 for OSPFv3."; + } + leaf helper-enabled { + type boolean; + description + "Enables graceful restart helper support for restarting + routers (Section 3 of RFC 3623)."; + reference + "RFC 3623: Graceful OSPF Restart, Section 3"; + } + leaf restart-interval { + type uint16 { + range "1..1800"; + } + units "seconds"; + default "120"; + description + "Interval during which to attempt graceful restart prior + to failing (seconds) (Appendix B.1 of RFC 3623)."; + reference + "RFC 3623: Graceful OSPF Restart, Appendix B.1"; + } + leaf helper-strict-lsa-checking { + type boolean; + description + "Terminates graceful restart when an LSA topology change + is detected (Appendix B.2 of RFC 3623)."; + reference + "RFC 3623: Graceful OSPF Restart, Appendix B.2"; + } + } + + container auto-cost { + if-feature "auto-cost"; + description + "Interface auto-cost configuration state."; + leaf enabled { + type boolean; + description + "Enables/disables interface auto-cost."; + } + leaf reference-bandwidth { + when "../enabled = 'true'" { + description + "Only when auto-cost is enabled."; + } + type uint32 { + range "1..4294967"; + } + units "Mbits"; + description + "Configures reference bandwidth used to automatically + determine interface cost (Mbits). The cost is the + reference bandwidth divided by the interface speed, + with 1 being the minimum cost."; + } + } + + container spf-control { + leaf paths { + if-feature "max-ecmp"; + type uint16 { + range "1..65535"; + } + description + "Maximum number of Equal-Cost Multi-Path (ECMP) paths."; + } + container ietf-spf-delay { + if-feature "ietf-spf-delay"; + uses ietf-spf-delay; + description + "IETF SPF delay algorithm configuration."; + } + description + "SPF calculation control."; + } + + container database-control { + leaf max-lsa { + if-feature "max-lsa"; + type uint32 { + range "1..4294967294"; + } + description + "Maximum number of OSPF LSAs the router will accept."; + } + description + "Database maintenance control."; + } + + container stub-router { + if-feature "stub-router"; + description + "Sets the maximum metric configuration."; + + choice trigger { + description + "Specific triggers that will enable stub router state."; + container always { + presence "Enables unconditional stub router support"; + description + "Unconditional stub router state (advertises + transit links with 'MaxLinkMetric')."; + reference + "RFC 6987: OSPF Stub Router Advertisement"; + } + } + } + + container mpls { + description + "OSPF MPLS configuration state."; + container te-rid { + if-feature "te-rid"; + description + "Stable OSPF Router IP address used for TE."; + leaf ipv4-router-id { + type inet:ipv4-address; + description + "Explicitly configures a TE IPv4 Router ID."; + } + leaf ipv6-router-id { + type inet:ipv6-address; + description + "Explicitly configures a TE IPv6 Router ID."; + } + } + container ldp { + description + "OSPF MPLS LDP configuration state."; + leaf igp-sync { + if-feature "ldp-igp-sync"; + type boolean; + description + "Enables LDP IGP synchronization."; + } + } + } + uses instance-fast-reroute-config; + uses node-tag-config; + } + + grouping instance-state { + description + "OSPF instance operational state."; + + leaf router-id { + type rt-types:router-id; + config false; + description + "Defined in RFC 2328. A 32-bit number + that uniquely identifies the router."; + reference + "RFC 2328: OSPF Version 2"; + } + + uses local-rib; + + container statistics { + config false; + description + "Per-instance statistics."; + uses instance-stat; + } + + container database { + config false; + description + "AS-Scope LSDB."; + list as-scope-lsa-type { + key "lsa-type"; + description + "List of OSPF AS-Scope LSAs."; + leaf lsa-type { + type uint16; + description + "OSPF AS-Scope LSA type."; + } + container as-scope-lsas { + description + "All AS-Scope LSAs of this LSA type."; + list as-scope-lsa { + key "lsa-id adv-router"; + description + "List of OSPF AS-Scope LSAs."; + uses lsa-key; + uses lsa { + refine "version/ospfv2/ospfv2" { + must "derived-from-or-self( " + + "../../../../../../" + + "rt:type, 'ospfv2')" { + description + "OSPFv2 LSA."; + } + } + refine "version/ospfv3/ospfv3" { + must "derived-from-or-self( " + + "../../../../../../" + + "rt:type, 'ospfv3')" { + description + "OSPFv3 LSA."; + } + } + } + } + } + } + } + uses spf-log; + uses lsa-log; + } + + grouping multi-topology-area-common-config { + description + "OSPF multi-topology area common configuration state."; + leaf summary { + when "derived-from(../../../area-type, 'stub-nssa-area')" { + description + "Summary advertisement into the stub area or NSSA."; + } + type boolean; + description + "Enables/disables a summary advertisement into the + topology in the stub area or NSSA."; + } + leaf default-cost { + when "derived-from(../../../area-type, 'stub-nssa-area')" { + description + "Cost for the LSA default route advertised into the + topology in the stub area or NSSA."; + } + type ospf-metric; + description + "Sets the summary default route cost for a + stub area or NSSA."; + } + } + + grouping multi-topology-area-config { + description + "OSPF multi-topology area configuration state."; + + uses multi-topology-area-common-config; + uses address-family-area-config; + } + + grouping multi-topology-state { + description + "OSPF multi-topology operational state."; + + uses local-rib; + } + + grouping multi-topology-interface-config { + description + "OSPF multi-topology configuration state."; + + leaf cost { + type ospf-link-metric; + description + "Interface cost for this topology."; + } + } + + grouping ospfv3-interface-config { + description + "OSPFv3 interface-specific configuration state."; + + leaf instance-id { + type uint8; + default "0"; + description + "OSPFv3 instance ID."; + } + } + + grouping ospfv3-interface-state { + description + "OSPFv3 interface-specific operational state."; + + leaf interface-id { + type uint32; + config false; + description + "OSPFv3 interface ID."; + } + } + + grouping lsa-identifiers { + description + "The parameters that uniquely identify an LSA."; + leaf area-id { + type area-id-type; + description + "Area ID."; + } + leaf type { + type uint16; + description + "LSA type."; + } + leaf lsa-id { + type union { + type inet:ipv4-address; + type yang:dotted-quad; + } + description + "Link State ID."; + } + leaf adv-router { + type rt-types:router-id; + description + "LSA advertising router."; + } + leaf seq-num { + type uint32; + description + "LSA sequence number."; + } + } + + grouping spf-log { + description + "Grouping for the SPF log."; + container spf-log { + config false; + description + "This container lists the SPF log entries."; + list event { + key "id"; + description + "List of SPF log entries represented + as a wrapping buffer in chronological + order, with the oldest entry returned + first."; + leaf id { + type uint32; + description + "Event identifier. A purely internal value."; + } + leaf spf-type { + type enumeration { + enum full { + description + "The SPF computation was for a full SPF."; + } + enum intra { + description + "The SPF computation was only for intra-area + routes."; + } + enum inter { + description + "The SPF computation was only for inter-area + summary routes."; + } + enum external { + description + "The SPF computation was only for AS external + and NSSA routes."; + } + } + description + "The SPF computation type for the SPF log entry."; + } + leaf schedule-timestamp { + type yang:timestamp; + description + "This is the timestamp when the computation was + scheduled."; + } + leaf start-timestamp { + type yang:timestamp; + description + "This is the timestamp when the computation was + started."; + } + leaf end-timestamp { + type yang:timestamp; + description + "This is the timestamp when the computation was + completed."; + } + list trigger-lsa { + description + "The list of LSAs that triggered the computation."; + uses lsa-identifiers; + } + } + } + } + + grouping lsa-log { + description + "Grouping for the LSA log."; + container lsa-log { + config false; + description + "This container lists the LSA log entries. + Local LSA modifications are also included + in the list."; + list event { + key "id"; + description + "List of LSA log entries represented + as a wrapping buffer in chronological order, + with the oldest entry returned first."; + leaf id { + type uint32; + description + "Event identifier. A purely internal value."; + } + container lsa { + description + "This container describes the LSA that was logged."; + uses lsa-identifiers; + } + leaf received-timestamp { + type yang:timestamp; + description + "This is the timestamp when the LSA was received. + In the case of a local LSA update, the timestamp + refers to the LSA origination time."; + } + leaf reason { + type identityref { + base lsa-log-reason; + } + description + "Reason for the LSA log entry."; + } + } + } + } + + augment "/rt:routing/rt:control-plane-protocols/" + + "rt:control-plane-protocol" { + when "derived-from(rt:type, 'ospf')" { + description + "This augmentation is only valid for a routing protocol + instance of OSPF (type 'ospfv2' or 'ospfv3')."; + } + description + "OSPF protocol 'ietf-routing' module 'control-plane-protocol' + augmentation."; + + container ospf { + description + "OSPF protocol instance."; + + leaf address-family { + when "derived-from-or-self(../../rt:type, 'ospfv3')" { + description + "Only applicable to OSPFv3."; + } + type iana-rt-types:address-family; + description + "Address family of the instance."; + } + + uses instance-config; + uses instance-state; + + container areas { + description + "All OSPF areas."; + list area { + key "area-id"; + description + "List of OSPF areas."; + leaf area-id { + type area-id-type; + description + "Area ID."; + } + + uses area-config; + uses area-state; + + container virtual-links { + when "derived-from-or-self(../area-type, 'normal-area') " + + "and ../area-id = '0.0.0.0'" { + description + "Virtual links must be in a backbone area."; + } + description + "All virtual links."; + list virtual-link { + key "transit-area-id router-id"; + description + "OSPF virtual link."; + leaf transit-area-id { + type leafref { + path "../../../../area/area-id"; + } + must "derived-from-or-self(" + + "../../../../area[area-id=current()]" + + "/area-type, 'normal-area') and " + + "../../../../area[area-id=current()]" + + "/area-id != '0.0.0.0'" { + error-message "The virtual link transit area must " + + "not be the backbone area."; + description + "The virtual link transit area must not be the + backbone area (0.0.0.0)."; + } + description + "Virtual link transit area ID."; + } + leaf router-id { + type rt-types:router-id; + description + "Virtual link remote endpoint Router ID."; + } + + uses virtual-link-config; + uses virtual-link-state; + } + } + container sham-links { + if-feature "pe-ce-protocol"; + description + "All sham links."; + list sham-link { + key "local-id remote-id"; + description + "OSPF sham link."; + leaf local-id { + type inet:ip-address; + description + "Address of the local sham link endpoint."; + } + leaf remote-id { + type inet:ip-address; + description + "Address of the remote sham link endpoint."; + } + uses sham-link-config; + uses sham-link-state; + } + } + container interfaces { + description + "All OSPF interfaces."; + list interface { + key "name"; + description + "List of OSPF interfaces."; + leaf name { + type if:interface-ref; + description + "Interface name reference."; + } + uses interface-config; + uses interface-state; + } + } + } + } + } + } + + augment "/rt:routing/rt:control-plane-protocols/" + + "rt:control-plane-protocol/ospf" { + when "derived-from(../rt:type, 'ospf')" { + description + "This augmentation is only valid for OSPF + (type 'ospfv2' or 'ospfv3')."; + } + if-feature "multi-topology"; + description + "OSPF multi-topology instance configuration + state augmentation."; + container topologies { + description + "All topologies."; + list topology { + key "name"; + description + "OSPF topology. The OSPF topology address family + must coincide with the routing instance's + address family."; + leaf name { + type leafref { + path "../../../../../../rt:ribs/rt:rib/rt:name"; + } + description + "RIB name corresponding to the OSPF topology."; + } + + uses multi-topology-state; + } + } + } + + augment "/rt:routing/rt:control-plane-protocols/" + + "rt:control-plane-protocol/ospf/" + + "areas/area" { + when "derived-from-or-self(../../../rt:type, " + + "'ospfv2')" { + description + "This augmentation is only valid for OSPFv2."; + } + if-feature "multi-topology"; + description + "OSPF multi-topology area configuration state + augmentation."; + container topologies { + description + "All topologies for the area."; + list topology { + key "name"; + description + "OSPF area topology."; + leaf name { + type leafref { + path "../../../../../../../../" + + "rt:ribs/rt:rib/rt:name"; + } + description + "Single topology enabled for this area."; + } + + uses multi-topology-area-config; + } + } + } + + augment "/rt:routing/rt:control-plane-protocols/" + + "rt:control-plane-protocol/ospf/" + + "areas/area/interfaces/interface" { + when "derived-from-or-self(../../../../../rt:type, " + + "'ospfv2')" { + description + "This augmentation is only valid for OSPFv2."; + } + if-feature "multi-topology"; + description + "OSPF multi-topology interface configuration state + augmentation."; + container topologies { + description + "All topologies for the interface."; + list topology { + key "name"; + description + "OSPF interface topology."; + leaf name { + type leafref { + path "../../../../../../../../../../" + + "rt:ribs/rt:rib/rt:name"; + } + description + "Single topology enabled on this interface."; + } + + uses multi-topology-interface-config; + } + } + } + + augment "/rt:routing/rt:control-plane-protocols/" + + "rt:control-plane-protocol/ospf/" + + "areas/area/interfaces/interface" { + when "derived-from-or-self(../../../../../rt:type, " + + "'ospfv3')" { + description + "This augmentation is only valid for OSPFv3."; + } + description + "OSPFv3 interface-specific configuration state + augmentation."; + uses ospfv3-interface-config; + uses ospfv3-interface-state; + } + + grouping route-content { + description + "This grouping defines OSPF-specific route attributes."; + leaf metric { + type uint32; + description + "OSPF route metric."; + } + leaf tag { + type uint32; + default "0"; + description + "OSPF route tag."; + } + leaf route-type { + type route-type; + description + "OSPF route type."; + } + } + + augment "/rt:routing/rt:ribs/rt:rib/rt:routes/rt:route" { + when "derived-from(rt:source-protocol, 'ospf')" { + description + "This augmentation is only valid for routes whose + source protocol is OSPF."; + } + description + "OSPF-specific route attributes."; + uses route-content; + } + + /* + * RPCs + */ + + rpc clear-neighbor { + description + "This RPC request clears a particular set of OSPF neighbors. + If the operation fails for an OSPF-internal reason, then + 'error-tag' and 'error-app-tag' should be set to values + indicating the error."; + input { + leaf routing-protocol-name { + type leafref { + path "/rt:routing/rt:control-plane-protocols/" + + "rt:control-plane-protocol/rt:name"; + } + mandatory true; + description + "OSPF protocol instance for which information for neighbors + is to be cleared. + + If the referenced OSPF instance doesn't exist, then + this operation SHALL fail with an 'error-tag' setting of + 'data-missing' and an 'error-app-tag' setting of + 'routing-protocol-instance-not-found'."; + } + + leaf interface { + type if:interface-ref; + description + "Name of the OSPF interface for which neighbors are to + be cleared. + + If the referenced OSPF interface doesn't exist, then + this operation SHALL fail with an 'error-tag' setting + of 'data-missing' and an 'error-app-tag' setting of + 'ospf-interface-not-found'."; + } + } + } + + rpc clear-database { + description + "This RPC request clears a particular OSPF Link State + Database. Additionally, all neighbor adjacencies will + be forced to the DOWN state and self-originated LSAs + will be reoriginated. If the operation fails for an + OSPF-internal reason, then 'error-tag' and 'error-app-tag' + should be set to values indicating the error."; + input { + leaf routing-protocol-name { + type leafref { + path "/rt:routing/rt:control-plane-protocols/" + + "rt:control-plane-protocol/rt:name"; + } + mandatory true; + description + "OSPF protocol instance whose LSDB is to be cleared. + + If the referenced OSPF instance doesn't exist, then + this operation SHALL fail with an 'error-tag' setting of + 'data-missing' and an 'error-app-tag' setting of + 'routing-protocol-instance-not-found'."; + } + } + } + + /* + * Notifications + */ + + grouping notification-instance-hdr { + description + "This grouping describes common instance-specific + data for OSPF notifications."; + + leaf routing-protocol-name { + type leafref { + path "/rt:routing/rt:control-plane-protocols/" + + "rt:control-plane-protocol/rt:name"; + } + must "derived-from( " + + "/rt:routing/rt:control-plane-protocols/" + + "rt:control-plane-protocol[rt:name=current()]/" + + "rt:type, 'ospf')"; + description + "Name of the OSPF routing protocol instance."; + } + + leaf address-family { + type leafref { + path "/rt:routing/" + + "rt:control-plane-protocols/rt:control-plane-protocol" + + "[rt:name=current()/../routing-protocol-name]/" + + "ospf/address-family"; + } + description + "Address family of the OSPF instance."; + } + } + + grouping notification-interface { + description + "This grouping provides interface information + for OSPF interface-specific notifications."; + + choice if-link-type-selection { + description + "Options for link types."; + container interface { + description + "Normal interface."; + leaf interface { + type if:interface-ref; + description + "Interface."; + } + } + container virtual-link { + description + "Virtual link."; + leaf transit-area-id { + type area-id-type; + description + "Area ID."; + } + leaf neighbor-router-id { + type rt-types:router-id; + description + "Neighbor's Router ID."; + } + } + container sham-link { + description + "Sham link."; + leaf area-id { + type area-id-type; + description + "Area ID."; + } + leaf local-ip-addr { + type inet:ip-address; + description + "Sham link's local address."; + } + leaf remote-ip-addr { + type inet:ip-address; + description + "Sham link's remote address."; + } + } + } + } + + grouping notification-neighbor { + description + "This grouping provides the neighbor information + for neighbor-specific notifications."; + + leaf neighbor-router-id { + type rt-types:router-id; + description + "Neighbor's Router ID."; + } + + leaf neighbor-ip-addr { + type inet:ip-address; + description + "Neighbor's address."; + } + } + + notification if-state-change { + uses notification-instance-hdr; + uses notification-interface; + + leaf state { + type if-state-type; + description + "Interface state."; + } + description + "This notification is sent when an interface + state change is detected."; + } + + notification if-config-error { + uses notification-instance-hdr; + uses notification-interface; + + leaf packet-source { + type inet:ip-address; + description + "Source address."; + } + + leaf packet-type { + type packet-type; + description + "OSPF packet type."; + } + + leaf error { + type enumeration { + enum bad-version { + description + "Bad version."; + } + enum area-mismatch { + description + "Area mismatch."; + } + enum unknown-nbma-nbr { + description + "Unknown NBMA neighbor."; + } + enum unknown-virtual-nbr { + description + "Unknown virtual link neighbor."; + } + enum auth-type-mismatch { + description + "Authentication type mismatch."; + } + enum auth-failure { + description + "Authentication failure."; + } + enum net-mask-mismatch { + description + "Network mask mismatch."; + } + enum hello-interval-mismatch { + description + "Hello interval mismatch."; + } + enum dead-interval-mismatch { + description + "Dead interval mismatch."; + } + enum option-mismatch { + description + "Option mismatch."; + } + enum mtu-mismatch { + description + "MTU mismatch."; + } + enum duplicate-router-id { + description + "Duplicate Router ID."; + } + enum no-error { + description + "No error."; + } + } + description + "Error codes."; + } + description + "This notification is sent when a packet is received indicating + an interface configuration error on the sending OSPF router."; + } + + notification nbr-state-change { + uses notification-instance-hdr; + uses notification-interface; + uses notification-neighbor; + + leaf state { + type nbr-state-type; + description + "Neighbor state."; + } + + description + "This notification is sent when a neighbor + state change is detected."; + } + + notification nbr-restart-helper-status-change { + uses notification-instance-hdr; + uses notification-interface; + uses notification-neighbor; + + leaf status { + type restart-helper-status-type; + description + "Restart helper status."; + } + + leaf age { + type rt-types:timer-value-seconds16; + description + "Remaining time in the current OSPF graceful restart + interval when the router is acting as a restart + helper for the neighbor."; + } + + leaf exit-reason { + type restart-exit-reason-type; + description + "Restart helper exit reason."; + } + description + "This notification is sent when a neighbor restart + helper status change is detected."; + } + + notification if-rx-bad-packet { + uses notification-instance-hdr; + uses notification-interface; + + leaf packet-source { + type inet:ip-address; + description + "Source address."; + } + + leaf packet-type { + type packet-type; + description + "OSPF packet type."; + } + + description + "This notification is sent when an OSPF packet that + cannot be parsed is received on an OSPF interface."; + } + + notification lsdb-approaching-overflow { + uses notification-instance-hdr; + + leaf ext-lsdb-limit { + type uint32; + description + "The maximum number of non-default AS-External-LSA + entries that can be stored in the LSDB."; + } + + description + "This notification is sent when the number of LSAs + in the router's LSDB has exceeded ninety percent of the + AS-External-LSA limit ('ext-lsdb-limit')."; + } + + notification lsdb-overflow { + uses notification-instance-hdr; + + leaf ext-lsdb-limit { + type uint32; + description + "The maximum number of non-default AS-External-LSA + entries that can be stored in the LSDB."; + } + + description + "This notification is sent when the number of LSAs + in the router's LSDB has exceeded the AS-External-LSA limit + ('ext-lsdb-limit')."; + } + + notification nssa-translator-status-change { + uses notification-instance-hdr; + + leaf area-id { + type area-id-type; + description + "Area ID."; + } + + leaf status { + type nssa-translator-state-type; + description + "NSSA translator status."; + } + + description + "This notification is sent when there is a change + in the router's role in translating OSPF NSSA-LSAs + to OSPF AS-External-LSAs."; + } + + notification restart-status-change { + uses notification-instance-hdr; + + leaf status { + type restart-status-type; + description + "Restart status."; + } + + leaf restart-interval { + type uint16 { + range "1..1800"; + } + units "seconds"; + default "120"; + description + "Restart interval."; + } + + leaf exit-reason { + type restart-exit-reason-type; + description + "Restart exit reason."; + } + + description + "This notification is sent when the graceful restart + state for the router has changed."; + } +} diff --git a/yang/ietf/ietf-routing.yang b/yang/ietf/ietf-routing.yang new file mode 100644 index 000000000000..e8aba972ebe3 --- /dev/null +++ b/yang/ietf/ietf-routing.yang @@ -0,0 +1,686 @@ +module ietf-routing { + yang-version "1.1"; + namespace "urn:ietf:params:xml:ns:yang:ietf-routing"; + prefix "rt"; + + import ietf-yang-types { + prefix "yang"; + } + + import ietf-interfaces { + prefix "if"; + description + "An 'ietf-interfaces' module version that is compatible with + the Network Management Datastore Architecture (NMDA) + is required."; + } + + organization + "IETF NETMOD (Network Modeling) Working Group"; + contact + "WG Web: + WG List: + + Editor: Ladislav Lhotka + + Acee Lindem + + Yingzhen Qu + "; + + description + "This YANG module defines essential components for the management + of a routing subsystem. The model fully conforms to the Network + Management Datastore Architecture (NMDA). + + Copyright (c) 2018 IETF Trust and the persons + identified as authors of the code. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject + to the license terms contained in, the Simplified BSD License + set forth in Section 4.c of the IETF Trust's Legal Provisions + Relating to IETF Documents + (https://trustee.ietf.org/license-info). + + This version of this YANG module is part of RFC 8349; see + the RFC itself for full legal notices."; + + revision 2018-03-13 { + description + "Network Management Datastore Architecture (NMDA) revision."; + reference + "RFC 8349: A YANG Data Model for Routing Management + (NMDA Version)"; + } + + revision 2016-11-04 { + description + "Initial revision."; + reference + "RFC 8022: A YANG Data Model for Routing Management"; + } + + /* Features */ + feature multiple-ribs { + description + "This feature indicates that the server supports + user-defined RIBs. + + Servers that do not advertise this feature SHOULD provide + exactly one system-controlled RIB per supported address family + and also make it the default RIB. This RIB then appears as an + entry in the list '/routing/ribs/rib'."; + } + + feature router-id { + description + "This feature indicates that the server supports an explicit + 32-bit router ID that is used by some routing protocols. + + Servers that do not advertise this feature set a router ID + algorithmically, usually to one of the configured IPv4 + addresses. However, this algorithm is implementation + specific."; + } + + /* Identities */ + + identity address-family { + description + "Base identity from which identities describing address + families are derived."; + } + + identity ipv4 { + base address-family; + description + "This identity represents an IPv4 address family."; + } + + identity ipv6 { + base address-family; + description + "This identity represents an IPv6 address family."; + } + + identity control-plane-protocol { + description + "Base identity from which control-plane protocol identities are + derived."; + } + + identity routing-protocol { + base control-plane-protocol; + description + "Identity from which Layer 3 routing protocol identities are + derived."; + } + + identity direct { + base routing-protocol; + description + "Routing pseudo-protocol that provides routes to directly + connected networks."; + } + + identity static { + base routing-protocol; + description + "'Static' routing pseudo-protocol."; + } + + /* Type Definitions */ + + typedef route-preference { + type uint32; + description + "This type is used for route preferences."; + } + + /* Groupings */ + + grouping address-family { + description + "This grouping provides a leaf identifying an address + family."; + leaf address-family { + type identityref { + base address-family; + } + mandatory true; + description + "Address family."; + } + } + + grouping router-id { + description + "This grouping provides a router ID."; + leaf router-id { + type yang:dotted-quad; + description + "A 32-bit number in the form of a dotted quad that is used by + some routing protocols identifying a router."; + reference + "RFC 2328: OSPF Version 2"; + } + } + + grouping special-next-hop { + description + "This grouping provides a leaf with an enumeration of special + next hops."; + leaf special-next-hop { + type enumeration { + enum blackhole { + description + "Silently discard the packet."; + } + enum unreachable { + description + "Discard the packet and notify the sender with an error + message indicating that the destination host is + unreachable."; + } + enum prohibit { + description + "Discard the packet and notify the sender with an error + message indicating that the communication is + administratively prohibited."; + } + enum receive { + description + "The packet will be received by the local system."; + } + } + description + "Options for special next hops."; + } + } + + grouping next-hop-content { + description + "Generic parameters of next hops in static routes."; + choice next-hop-options { + mandatory true; + description + "Options for next hops in static routes. + + It is expected that further cases will be added through + augments from other modules."; + case simple-next-hop { + description + "This case represents a simple next hop consisting of the + next-hop address and/or outgoing interface. + + Modules for address families MUST augment this case with a + leaf containing a next-hop address of that address + family."; + leaf outgoing-interface { + type if:interface-ref; + description + "Name of the outgoing interface."; + } + } + case special-next-hop { + uses special-next-hop; + } + case next-hop-list { + container next-hop-list { + description + "Container for multiple next hops."; + list next-hop { + key "index"; + description + "An entry in a next-hop list. + + Modules for address families MUST augment this list + with a leaf containing a next-hop address of that + address family."; + leaf index { + type string; + description + "A user-specified identifier utilized to uniquely + reference the next-hop entry in the next-hop list. + The value of this index has no semantic meaning + other than for referencing the entry."; + } + leaf outgoing-interface { + type if:interface-ref; + description + "Name of the outgoing interface."; + } + } + } + } + } + } + + grouping next-hop-state-content { + description + "Generic state parameters of next hops."; + choice next-hop-options { + mandatory true; + description + "Options for next hops. + + It is expected that further cases will be added through + augments from other modules, e.g., for recursive + next hops."; + case simple-next-hop { + description + "This case represents a simple next hop consisting of the + next-hop address and/or outgoing interface. + + Modules for address families MUST augment this case with a + leaf containing a next-hop address of that address + family."; + leaf outgoing-interface { + type if:interface-ref; + description + "Name of the outgoing interface."; + } + } + case special-next-hop { + uses special-next-hop; + } + case next-hop-list { + container next-hop-list { + description + "Container for multiple next hops."; + list next-hop { + description + "An entry in a next-hop list. + + Modules for address families MUST augment this list + with a leaf containing a next-hop address of that + address family."; + leaf outgoing-interface { + type if:interface-ref; + description + "Name of the outgoing interface."; + } + } + } + } + } + } + + grouping route-metadata { + description + "Common route metadata."; + leaf source-protocol { + type identityref { + base routing-protocol; + } + mandatory true; + description + "Type of the routing protocol from which the route + originated."; + } + leaf active { + type empty; + description + "The presence of this leaf indicates that the route is + preferred among all routes in the same RIB that have the + same destination prefix."; + } + leaf last-updated { + type yang:date-and-time; + description + "Timestamp of the last modification of the route. If the + route was never modified, it is the time when the route was + inserted into the RIB."; + } + } + + /* Data nodes */ + + container routing { + description + "Configuration parameters for the routing subsystem."; + uses router-id { + if-feature "router-id"; + description + "Support for the global router ID. Routing protocols + that use a router ID can use this parameter or override it + with another value."; + } + container interfaces { + config false; + description + "Network-layer interfaces used for routing."; + leaf-list interface { + type if:interface-ref; + description + "Each entry is a reference to the name of a configured + network-layer interface."; + } + } + container control-plane-protocols { + description + "Support for control-plane protocol instances."; + list control-plane-protocol { + key "type name"; + description + "Each entry contains a control-plane protocol instance."; + leaf type { + type identityref { + base control-plane-protocol; + } + description + "Type of the control-plane protocol -- an identity + derived from the 'control-plane-protocol' + base identity."; + } + leaf name { + type string; + description + "An arbitrary name of the control-plane protocol + instance."; + } + leaf description { + type string; + description + "Textual description of the control-plane protocol + instance."; + } + container static-routes { + when "derived-from-or-self(../type, 'rt:static')" { + description + "This container is only valid for the 'static' routing + protocol."; + } + description + "Support for the 'static' pseudo-protocol. + + Address-family-specific modules augment this node with + their lists of routes."; + } + } + } + container ribs { + description + "Support for RIBs."; + list rib { + key "name"; + description + "Each entry contains a configuration for a RIB identified + by the 'name' key. + + Entries having the same key as a system-controlled entry + in the list '/routing/ribs/rib' are used for + configuring parameters of that entry. Other entries + define additional user-controlled RIBs."; + leaf name { + type string; + description + "The name of the RIB. + + For system-controlled entries, the value of this leaf + must be the same as the name of the corresponding entry + in the operational state. + + For user-controlled entries, an arbitrary name can be + used."; + } + uses address-family { + description + "The address family of the system-controlled RIB."; + } + + leaf default-rib { + if-feature "multiple-ribs"; + type boolean; + default "true"; + config false; + description + "This flag has the value of 'true' if and only if the RIB + is the default RIB for the given address family. + + By default, control-plane protocols place their routes + in the default RIBs."; + } + container routes { + config false; + description + "Current contents of the RIB."; + list route { + description + "A RIB route entry. This data node MUST be augmented + with information specific to routes of each address + family."; + leaf route-preference { + type route-preference; + description + "This route attribute, also known as 'administrative + distance', allows for selecting the preferred route + among routes with the same destination prefix. A + smaller value indicates a route that is + more preferred."; + } + container next-hop { + description + "Route's next-hop attribute."; + uses next-hop-state-content; + } + uses route-metadata; + } + } + action active-route { + description + "Return the active RIB route that is used for the + destination address. + + Address-family-specific modules MUST augment input + parameters with a leaf named 'destination-address'."; + output { + container route { + description + "The active RIB route for the specified destination. + + If no route exists in the RIB for the destination + address, no output is returned. + + Address-family-specific modules MUST augment this + container with appropriate route contents."; + container next-hop { + description + "Route's next-hop attribute."; + uses next-hop-state-content; + } + uses route-metadata; + } + } + } + leaf description { + type string; + description + "Textual description of the RIB."; + } + } + } + } + + /* + * The subsequent data nodes are obviated and obsoleted + * by the Network Management Datastore Architecture + * as described in RFC 8342. + */ + container routing-state { + config false; + status obsolete; + description + "State data of the routing subsystem."; + uses router-id { + status obsolete; + description + "Global router ID. + + It may be either configured or assigned algorithmically by + the implementation."; + } + container interfaces { + status obsolete; + description + "Network-layer interfaces used for routing."; + leaf-list interface { + type if:interface-state-ref; + status obsolete; + description + "Each entry is a reference to the name of a configured + network-layer interface."; + } + } + container control-plane-protocols { + status obsolete; + description + "Container for the list of routing protocol instances."; + list control-plane-protocol { + key "type name"; + status obsolete; + description + "State data of a control-plane protocol instance. + + An implementation MUST provide exactly one + system-controlled instance of the 'direct' + pseudo-protocol. Instances of other control-plane + protocols MAY be created by configuration."; + leaf type { + type identityref { + base control-plane-protocol; + } + status obsolete; + description + "Type of the control-plane protocol."; + } + leaf name { + type string; + status obsolete; + description + "The name of the control-plane protocol instance. + + For system-controlled instances, this name is + persistent, i.e., it SHOULD NOT change across + reboots."; + } + } + } + container ribs { + status obsolete; + description + "Container for RIBs."; + list rib { + key "name"; + min-elements 1; + status obsolete; + description + "Each entry represents a RIB identified by the 'name' + key. All routes in a RIB MUST belong to the same address + family. + + An implementation SHOULD provide one system-controlled + default RIB for each supported address family."; + leaf name { + type string; + status obsolete; + description + "The name of the RIB."; + } + uses address-family { + status obsolete; + description + "The address family of the RIB."; + } + leaf default-rib { + if-feature "multiple-ribs"; + type boolean; + default "true"; + status obsolete; + description + "This flag has the value of 'true' if and only if the + RIB is the default RIB for the given address family. + + By default, control-plane protocols place their routes + in the default RIBs."; + } + container routes { + status obsolete; + description + "Current contents of the RIB."; + list route { + status obsolete; + description + "A RIB route entry. This data node MUST be augmented + with information specific to routes of each address + family."; + leaf route-preference { + type route-preference; + status obsolete; + description + "This route attribute, also known as 'administrative + distance', allows for selecting the preferred route + among routes with the same destination prefix. A + smaller value indicates a route that is + more preferred."; + } + container next-hop { + status obsolete; + description + "Route's next-hop attribute."; + uses next-hop-state-content { + status obsolete; + description + "Route's next-hop attribute operational state."; + } + } + uses route-metadata { + status obsolete; + description + "Route metadata."; + } + } + } + action active-route { + status obsolete; + description + "Return the active RIB route that is used for the + destination address. + + Address-family-specific modules MUST augment input + parameters with a leaf named 'destination-address'."; + output { + container route { + status obsolete; + description + "The active RIB route for the specified + destination. + + If no route exists in the RIB for the destination + address, no output is returned. + + Address-family-specific modules MUST augment this + container with appropriate route contents."; + container next-hop { + status obsolete; + description + "Route's next-hop attribute."; + uses next-hop-state-content { + status obsolete; + description + "Active route state data."; + } + } + uses route-metadata { + status obsolete; + description + "Active route metadata."; + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/yang/subdir.am b/yang/subdir.am index a8dd6e67d99a..4c7e486551b2 100644 --- a/yang/subdir.am +++ b/yang/subdir.am @@ -40,7 +40,13 @@ dist_yangmodels_DATA += yang/frr-vrf.yang dist_yangmodels_DATA += yang/frr-route-types.yang dist_yangmodels_DATA += yang/frr-routing.yang dist_yangmodels_DATA += yang/ietf/frr-deviations-ietf-key-chain.yang +dist_yangmodels_DATA += yang/ietf/frr-deviations-ietf-routing-ospf.yang +dist_yangmodels_DATA += yang/ietf/iana-bfd-types.yang +dist_yangmodels_DATA += yang/ietf/iana-routing-types.yang +dist_yangmodels_DATA += yang/ietf/ietf-bfd-types.yang +dist_yangmodels_DATA += yang/ietf/ietf-ospf.yang dist_yangmodels_DATA += yang/ietf/ietf-routing-types.yang +dist_yangmodels_DATA += yang/ietf/ietf-routing.yang dist_yangmodels_DATA += yang/ietf/ietf-interfaces.yang dist_yangmodels_DATA += yang/ietf/ietf-bgp-types.yang dist_yangmodels_DATA += yang/ietf/ietf-key-chain.yang diff --git a/zebra/main.c b/zebra/main.c index ada1c151e965..d14960ed5189 100644 --- a/zebra/main.c +++ b/zebra/main.c @@ -46,6 +46,7 @@ #include "zebra/zebra_vxlan.h" #include "zebra/zebra_routemap.h" #include "zebra/zebra_nb.h" +#include "zebra/zebra_ietf_interfaces_nb.h" #include "zebra/zebra_opaque.h" #include "zebra/zebra_srte.h" #include "zebra/zebra_srv6.h" @@ -294,6 +295,7 @@ static const struct frr_yang_module_info *const zebra_yang_modules[] = { &frr_backend_info, &frr_filter_info, &frr_interface_info, + &zebra_ietf_interfaces_info, &frr_route_map_info, &frr_zebra_info, &frr_vrf_info, @@ -332,6 +334,7 @@ static const char *const zebra_config_xpaths[] = { static const char *const zebra_oper_xpaths[] = { "/frr-backend:clients", "/frr-interface:lib/interface", + "/ietf-interfaces:interfaces/interface", "/frr-vrf:lib/vrf", "/frr-zebra:zebra", }; diff --git a/zebra/subdir.am b/zebra/subdir.am index d4a4e4f9338a..bc9ff64fe3fd 100644 --- a/zebra/subdir.am +++ b/zebra/subdir.am @@ -70,6 +70,7 @@ zebra_zebra_SOURCES = \ zebra/zebra_dplane.c \ zebra/zebra_errors.c \ zebra/zebra_gr.c \ + zebra/zebra_ietf_interfaces_nb.c \ zebra/zebra_l2.c \ zebra/zebra_l2_bridge_if.c \ zebra/zebra_evpn.c \ @@ -164,6 +165,7 @@ nobase_pkginclude_HEADERS += \ zebra/zebra_evpn_neigh.h \ zebra/zebra_evpn_vxlan.h \ zebra/zebra_fpm_private.h \ + zebra/zebra_ietf_interfaces_nb.h \ zebra/zebra_l2.h \ zebra/zebra_mlag.h \ zebra/zebra_mlag_vty.h \ diff --git a/zebra/zebra_ietf_interfaces_nb.c b/zebra/zebra_ietf_interfaces_nb.c new file mode 100644 index 000000000000..f3cf57749e38 --- /dev/null +++ b/zebra/zebra_ietf_interfaces_nb.c @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Zebra ietf-interfaces northbound interface. + * Copyright (C) 2026 Eric Parsonage + */ + +#include + +#include "if.h" +#include "vrf.h" + +#include "zebra/zebra_ietf_interfaces_nb.h" + +static struct interface *zebra_ietf_interfaces_lookup(const char *name) +{ + struct vrf *vrf; + struct interface if_tmp; + + if (vrf_is_backend_netns()) { + char ifname[XPATH_MAXLEN]; + char vrfname[XPATH_MAXLEN]; + char *separator; + + strlcpy(vrfname, name, sizeof(vrfname)); + separator = strchr(vrfname, ':'); + if (!separator) + return NULL; + + *separator = '\0'; + strlcpy(ifname, separator + 1, sizeof(ifname)); + vrf = vrf_lookup_by_name(vrfname); + if (!vrf) + return NULL; + + strlcpy(if_tmp.name, ifname, sizeof(if_tmp.name)); + return RB_FIND(if_name_head, &vrf->ifaces_by_name, &if_tmp); + } + + RB_FOREACH (vrf, vrf_name_head, &vrfs_by_name) { + struct interface *ifp; + + strlcpy(if_tmp.name, name, sizeof(if_tmp.name)); + ifp = RB_FIND(if_name_head, &vrf->ifaces_by_name, &if_tmp); + + if (ifp) + return ifp; + } + + return NULL; +} + +static void zebra_ietf_interfaces_key(const struct interface *ifp, char *key, + size_t key_len) +{ + if (vrf_is_backend_netns()) + snprintf(key, key_len, "%s:%s", ifp->vrf->name, ifp->name); + else + snprintf(key, key_len, "%s", ifp->name); +} + +/* + * XPath: /ietf-interfaces:interfaces/interface + */ +static const void *zebra_ietf_interfaces_interface_get_next(struct nb_cb_get_next_args *args) +{ + struct interface *ifp = (struct interface *)args->list_entry; + struct vrf *vrf; + + if (ifp == NULL) { + vrf = RB_MIN(vrf_name_head, &vrfs_by_name); + return vrf ? RB_MIN(if_name_head, &vrf->ifaces_by_name) : NULL; + } + + vrf = ifp->vrf; + ifp = RB_NEXT(if_name_head, ifp); + + while (ifp == NULL) { + vrf = RB_NEXT(vrf_name_head, vrf); + if (!vrf) + return NULL; + ifp = RB_MIN(if_name_head, &vrf->ifaces_by_name); + } + + return ifp; +} + +static int zebra_ietf_interfaces_interface_get_keys(struct nb_cb_get_keys_args *args) +{ + const struct interface *ifp = args->list_entry; + + args->keys->num = 1; + zebra_ietf_interfaces_key(ifp, args->keys->key[0], + sizeof(args->keys->key[0])); + + return NB_OK; +} + +static const void * +zebra_ietf_interfaces_interface_lookup_entry(struct nb_cb_lookup_entry_args *args) +{ + return zebra_ietf_interfaces_lookup(args->keys->key[0]); +} + +/* + * XPath: /ietf-interfaces:interfaces/interface/name + */ +static struct yang_data * +zebra_ietf_interfaces_interface_name_get_elem(struct nb_cb_get_elem_args *args) +{ + const struct interface *ifp = args->list_entry; + char name[XPATH_MAXLEN]; + + zebra_ietf_interfaces_key(ifp, name, sizeof(name)); + + return yang_data_new_string(args->xpath, name); +} + +/* + * XPath: /ietf-interfaces:interfaces/interface/oper-status + */ +static struct yang_data * +zebra_ietf_interfaces_interface_oper_status_get_elem(struct nb_cb_get_elem_args *args) +{ + const struct interface *ifp = args->list_entry; + + return yang_data_new_enum(args->xpath, + CHECK_FLAG(ifp->status, ZEBRA_INTERFACE_ACTIVE) + ? 1 + : 2); +} + +/* + * RFC 9129 references ietf-interfaces:interfaces/interface/name from OSPF + * interface state. Zebra owns FRR's interface table, so it provides the + * standard interface name list as operational state for those leafrefs. + */ + +/* clang-format off */ +const struct frr_yang_module_info zebra_ietf_interfaces_info = { + .name = "ietf-interfaces", + .ignore_cfg_cbs = true, + .nodes = { + { + .xpath = "/ietf-interfaces:interfaces/interface", + .cbs = { + .get_next = zebra_ietf_interfaces_interface_get_next, + .get_keys = zebra_ietf_interfaces_interface_get_keys, + .lookup_entry = zebra_ietf_interfaces_interface_lookup_entry, + }, + }, + { + .xpath = "/ietf-interfaces:interfaces/interface/name", + .cbs = { + .get_elem = zebra_ietf_interfaces_interface_name_get_elem, + }, + }, + { + .xpath = "/ietf-interfaces:interfaces/interface/oper-status", + .cbs = { + .get_elem = zebra_ietf_interfaces_interface_oper_status_get_elem, + }, + }, + { + .xpath = NULL, + }, + }, +}; +/* clang-format on */ diff --git a/zebra/zebra_ietf_interfaces_nb.h b/zebra/zebra_ietf_interfaces_nb.h new file mode 100644 index 000000000000..bdc8e29bcce5 --- /dev/null +++ b/zebra/zebra_ietf_interfaces_nb.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Zebra ietf-interfaces northbound interface. + * Copyright (C) 2026 Eric Parsonage + */ + +#ifndef ZEBRA_IETF_INTERFACES_NB_H +#define ZEBRA_IETF_INTERFACES_NB_H + +extern const struct frr_yang_module_info zebra_ietf_interfaces_info; + +#endif /* ZEBRA_IETF_INTERFACES_NB_H */