Skip to content

NetBox Update BGP Peering Task¤

task api name: update_bgp_peering

Updates one or many existing BGP sessions in NetBox. Supports single-session mode (name plus field kwargs) and bulk mode (bulk_update list of dicts). Each bulk entry selects a session by NetBox id or existing name; id takes precedence when both are given. Use new_name to rename a session. Only non-None fields are updated. Idempotency is enforced via DeepDiff: sessions with no effective changes are reported in in_sync and no write is performed.

How It Works¤

  1. Client submits update_bgp_peering request to NetBox worker
  2. NetBox worker validates that the BGP plugin is installed
  3. Worker resolves the RIR ID once (when rir is provided) for on-demand ASN creation
  4. For each session spec, the current session is fetched from NetBox by ID or name
  5. The proposed field values are compared against current NetBox values using DeepDiff
  6. If there are no differences, the session is added to in_sync — no write is performed
  7. In dry-run mode — the diff is returned without writing
  8. Otherwise, the changed fields are resolved (IPs, ASNs, policies) and the session is updated

When updating a session, supplied tags and list-valued custom fields add missing values without removing existing ones. Object and multiobject custom fields accept related object names or IDs. Scalar custom fields replace their current values.

Prerequisites¤

  • NetBox BGP plugin (netbox-bgp) must be installed and enabled on the NetBox instance.
  • The session must already exist in NetBox (use create_bgp_peering to create new sessions).

Branching Support¤

update_bgp_peering is branch-aware. Pass branch=<name> to write all changes into a NetBox Branching Plugin branch instead of main.

Output¤

Normal mode returns updated and in-sync BGP session details. In dry-run mode, the task returns the field-level diff that would be applied without writing to NetBox.

Dry Run Mode¤

dry_run=True returns the diff without any NetBox writes:

{
    "update": [
        {"name": "<session_name>", "diff": { ... }},
        ...
    ],
    "in_sync": ["<session_name>", ...],
}

Sessions with no effective changes appear in in_sync regardless of dry-run mode. Sessions selected by ID appear under their numeric ID in updated and in_sync. Their dry-run update entries include both the current name (possibly null) and id.

VRF Custom Field¤

The VRF reference is always stored in a BGP session custom field that must be configured as type Object in NetBox pointing to the VRF content-type. By default vrf_custom_field="vrf" means the VRF object reference is read from and written to custom_fields["vrf"]. This must match the value used when the session was created.

result = client.run_job(
    "netbox",
    "update_bgp_peering",
    workers="any",
    kwargs={
        "name": "ceos-spine-1_10.0.0.1_10.0.0.2",
        "vrf": "NEW_VRF",
        "vrf_custom_field": "tenant_vrf",  # Object-type custom field -> VRF
    },
)

Examples¤

Update a single BGP session description:

nf#netbox update bgp-peering name ceos-spine-1_10.0.0.1_10.0.0.2 description "updated description"

Update status:

nf#netbox update bgp-peering name ceos-spine-1_10.0.0.1_10.0.0.2 status planned

Dry run - preview diff without writing:

nf#netbox update bgp-peering name ceos-spine-1_10.0.0.1_10.0.0.2 description "new desc" dry-run

Update session in a NetBox branch:

nf#netbox update bgp-peering name ceos-spine-1_10.0.0.1_10.0.0.2 description "branch update" branch my-bgp-branch

Context manager:

from norfab.core.nfapi import NorFab

with NorFab(inventory="./inventory.yaml") as nf:
    client = nf.make_client()

    result = client.run_job(
        "netbox",
        "update_bgp_peering",
        workers="any",
        kwargs={
            "name": "ceos-spine-1_10.0.0.1_10.0.0.2",
            "description": "updated description",
            "status": "active",
        },
    )

Direct lifecycle:

from norfab.core.nfapi import NorFab

nf = NorFab(inventory="./inventory.yaml")
try:
    nf.start()
    client = nf.make_client()

    preview = client.run_job(
        "netbox",
        "update_bgp_peering",
        workers="any",
        kwargs={
            "name": "ceos-spine-1_10.0.0.1_10.0.0.2",
            "description": "new description",
            "dry_run": True,
        },
    )

    result = client.run_job(
        "netbox",
        "update_bgp_peering",
        workers="any",
        kwargs={
            "bulk_update": [
                {
                    "name": "ceos-spine-1_10.0.0.1_10.0.0.2",
                    "description": "spine-1 session",
                    "status": "active",
                },
                {
                    "name": "ceos-spine-2_10.0.0.3_10.0.0.4",
                    "description": "spine-2 session",
                    "import_policies": ["IMPORT_FROM_LEAF"],
                },
            ],
            "branch": "my-bgp-branch",
        },
    )
finally:
    nf.destroy()

NORFAB NetBox Update BGP Peering Command Shell Reference¤

NorFab shell supports these command options for the NetBox update_bgp_peering task:

nf# man tree netbox.update.bgp-peering
root
└── netbox:    Netbox service
    └── update:    Update Netbox objects
        └── bgp-peering:    Update BGP peering session(s)
            ├── timeout:    Job timeout
            ├── workers:    Filter worker to target, default 'any'
            ├── verbose-result:    Control output details, default 'False'
            ├── progress:    Display progress events, default 'True'
            ├── instance:    Netbox instance name to target
            ├── branch:    Branching plugin branch name to use
            ├── dry-run:    Return diff without writing to NetBox
            ├── name:    Existing session name (single mode)
            ├── description:    New description value
            ├── status:    New status value
            ├── local-address:    New local IP address string
            ├── remote-address:    New remote IP address string
            ├── local-as:    New local AS number string
            ├── remote-as:    New remote AS number string
            ├── vrf:    New VRF name
            ├── peer-group:    New peer group name
            ├── import-policies:    New import routing policy names
            ├── export-policies:    New export routing policy names
            ├── prefix-list-in:    New inbound prefix list name
            ├── prefix-list-out:    New outbound prefix list name
            ├── bulk-update:    JSON list of session update dicts for bulk mode
            ├── rir:    RIR name for ASN creation
            ├── vrf-custom-field:    BGP session field for VRF reference, default 'vrf'
            └── message:    Changelog message for NetBox write operations
nf#

Python API Reference¤

Update one or many existing BGP sessions in NetBox.

Supports single-session mode (name plus field kwargs) and bulk mode (bulk_update list of dicts). Only non-None fields are updated. Idempotency is enforced: sessions with no effective changes are reported in in_sync and no write is performed.

Parameters:

Name Type Description Default
job Job

NorFab Job object.

required
instance str

NetBox instance name.

None
name str

Existing session name. Required in single-session mode.

None
description str

New description value.

None
status str

New status value.

None
local_address str

New local IP address string.

None
remote_address str

New remote IP address string.

None
local_as str

New local AS number string.

None
remote_as str

New remote AS number string.

None
vrf str

New VRF name.

None
peer_group str

New peer group name.

None
import_policies list

New list of import routing-policy names.

None
export_policies list

New list of export routing-policy names.

None
prefix_list_in str

Inbound prefix-list name.

None
prefix_list_out str

Outbound prefix-list name.

None
tags list

Tag names to add to the session.

None
custom_fields dict

Custom fields to update. Array values add missing members; object references accept names or IDs.

None
bulk_update list

List of session update dicts for bulk mode. Each entry selects an existing session by id or name; id takes precedence. Use new_name to rename it.

None
rir str

RIR name used when auto-creating ASNs.

None
message str

Changelog message recorded on every NetBox write.

None
branch str

NetBox branching plugin branch name.

None
dry_run bool

When True return diff without writing.

False
vrf_custom_field str

Name of the BGP session custom field that stores the VRF object reference. The custom field must be of type Object in NetBox pointing to the VRF content-type. The value is always a single VRF object reference read from and written into custom_fields[vrf_custom_field]. Default 'vrf' means custom_fields['vrf'].

'vrf'

Returns:

Type Description
Result

Normal run::

{"updated": ["name1", ...], "in_sync": ["name2", ...]}

Result

Dry run::

{ "update": [{"name": "name1", "diff": {...}}, ...], "in_sync": ["name2", ...], }

Source code in norfab\workers\netbox_worker\bgp_peerings_tasks.py
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
@Task(
    fastapi={"methods": ["PATCH"], "schema": NetboxFastApiArgs.model_json_schema()},
    input=UpdateBgpPeeringInput,
    output=UpdateBgpPeeringResult,
    mcp={
        "annotations": {
            "title": "Update BGP Peering",
            "readOnlyHint": False,
            "destructiveHint": True,
            "idempotentHint": True,
            "openWorldHint": True,
        }
    },
)
def update_bgp_peering(
    self,
    job: Job,
    batch_size: int = 1000,
    # single-session mode
    name: Union[None, str] = None,
    description: Union[None, str] = None,
    status: Union[None, str] = None,
    local_address: Union[None, str] = None,
    remote_address: Union[None, str] = None,
    local_as: Union[None, int] = None,
    remote_as: Union[None, int] = None,
    vrf: Union[None, str] = None,
    peer_group: Union[None, str] = None,
    import_policies: Union[None, list] = None,
    export_policies: Union[None, list] = None,
    prefix_list_in: Union[None, str] = None,
    prefix_list_out: Union[None, str] = None,
    tags: Union[None, list] = None,
    custom_fields: Union[None, dict] = None,
    # bulk mode
    bulk_update: Union[None, list] = None,
    # shared
    rir: Union[None, str] = None,
    message: Union[None, str] = None,
    branch: Union[None, str] = None,
    dry_run: bool = False,
    instance: Union[None, str] = None,
    vrf_custom_field: str = "vrf",
    lookup_cache: Union[None, dict] = None,
) -> Result:
    """
    Update one or many existing BGP sessions in NetBox.

    Supports single-session mode (``name`` plus field kwargs) and bulk mode
    (``bulk_update`` list of dicts).  Only non-None fields are updated.
    Idempotency is enforced: sessions with no effective changes are reported in
    ``in_sync`` and no write is performed.

    Args:
        job: NorFab Job object.
        instance (str, optional): NetBox instance name.
        name (str, optional): Existing session name. Required in single-session mode.
        description (str, optional): New description value.
        status (str, optional): New status value.
        local_address (str, optional): New local IP address string.
        remote_address (str, optional): New remote IP address string.
        local_as (str, optional): New local AS number string.
        remote_as (str, optional): New remote AS number string.
        vrf (str, optional): New VRF name.
        peer_group (str, optional): New peer group name.
        import_policies (list, optional): New list of import routing-policy names.
        export_policies (list, optional): New list of export routing-policy names.
        prefix_list_in (str, optional): Inbound prefix-list name.
        prefix_list_out (str, optional): Outbound prefix-list name.
        tags (list, optional): Tag names to add to the session.
        custom_fields (dict, optional): Custom fields to update. Array values
            add missing members; object references accept names or IDs.
        bulk_update (list, optional): List of session update dicts for bulk mode.
            Each entry selects an existing session by ``id`` or ``name``;
            ``id`` takes precedence. Use ``new_name`` to rename it.
        rir (str, optional): RIR name used when auto-creating ASNs.
        message (str, optional): Changelog message recorded on every NetBox write.
        branch (str, optional): NetBox branching plugin branch name.
        dry_run (bool): When ``True`` return diff without writing.
        vrf_custom_field (str): Name of the BGP session custom field that stores
            the VRF object reference.  The custom field must be of type Object in
            NetBox pointing to the VRF content-type.  The value is always a single
            VRF object reference read from and written into
            ``custom_fields[vrf_custom_field]``.  Default ``'vrf'`` means
            ``custom_fields['vrf']``.

    Returns:
        Normal run::

            {"updated": ["name1", ...], "in_sync": ["name2", ...]}

        Dry run::

            {
                "update": [{"name": "name1", "diff": {...}}, ...],
                "in_sync": ["name2", ...],
            }
    """
    instance = instance or self.default_instance
    ret = Result(
        task=f"{self.name}:update_bgp_peering",
        result={},
        resources=[instance],
    )

    # Validate BGP plugin
    job.event(f"validating NetBox BGP plugin for '{instance}'")
    if not self.has_plugin("netbox_bgp", instance, strict=True):
        msg = f"'{instance}' NetBox instance has no BGP plugin installed"
        job.event(msg, severity="ERROR")
        ret.errors.append(msg)
        ret.failed = True
        return ret

    nb = self._get_pynetbox(instance, branch=branch, job=job)

    if message:
        job.event("setting NetBox changelog message for BGP session update")
        nb.http_session.headers["X-Changelog-Message"] = message

    lookup_cache = lookup_cache if lookup_cache is not None else {}

    # Validate VRF custom field and RIR
    job.event("validating BGP session VRF custom field and RIR")
    vrf_custom_field = _resolve_vrf_custom_field(
        vrf_custom_field, nb, job, self.name
    )
    rir_id = _resolve_rir_id(rir, nb, job, self.name, lookup_cache)

    # Build list of sessions to update
    if bulk_update is not None:
        bgp_sessions = bulk_update
    else:
        _single_fields = (
            "description",
            "status",
            "local_address",
            "remote_address",
            "local_as",
            "remote_as",
            "vrf",
            "peer_group",
            "import_policies",
            "export_policies",
            "prefix_list_in",
            "prefix_list_out",
            "tags",
            "custom_fields",
        )
        bgp_session = {
            k: v
            for k, v in locals().items()
            if k in _single_fields and v is not None
        }
        bgp_sessions = [{"name": name, **bgp_session}]
    job.event(
        f"preparing {len(bgp_sessions)} BGP session update request(s), dry_run={dry_run}"
    )

    result = {"updated": [], "in_sync": []}
    if dry_run is True:
        ret.dry_run = True
        result = {"update": [], "in_sync": []}

    # Fetch sessions by ID when available, otherwise by name.
    session_ids = []
    session_names = []
    for session in bgp_sessions:
        if session.get("id") is not None:
            session_ids.append(session["id"])
        else:
            session_names.append(session["name"])
    job.event(f"fetching {len(bgp_sessions)} BGP session(s) from NetBox")
    session_fields = "id,name,description,status,local_address,remote_address,local_as,remote_as,custom_fields,tags,peer_group,import_policies,export_policies,prefix_list_in,prefix_list_out"
    nb_sessions_raw = []
    if session_ids:
        nb_sessions_raw.extend(
            self.bulk_filter(
                nb.plugins.bgp.session, id=session_ids, fields=session_fields
            )
        )
    if session_names:
        nb_sessions_raw.extend(
            self.bulk_filter(
                nb.plugins.bgp.session, name=session_names, fields=session_fields
            )
        )
    normalised_nb = {
        s.id: normalise_nb_bgp_session(dict(s), vrf_custom_field=vrf_custom_field)
        for s in nb_sessions_raw
    }
    nb_sessions_by_id = {session.id: session for session in nb_sessions_raw}
    ids_by_name = {
        session.name: session.id for session in nb_sessions_raw if session.name
    }
    if any(session.get("custom_fields") is not None for session in bgp_sessions):
        lookup_cache.setdefault("custom_fields", {})
        if "netbox_bgp.bgpsession" not in lookup_cache["custom_fields"]:
            # Direct task calls fetch definitions; designs supply them in the cache.
            lookup_cache["custom_fields"]["netbox_bgp.bgpsession"] = {
                "object": [],
                "multiobject": [],
            }
            for field in self.bulk_filter(
                nb.extras.custom_fields,
                object_type="netbox_bgp.bgpsession",
                type=["object", "multiobject"],
            ):
                lookup_cache["custom_fields"]["netbox_bgp.bgpsession"][
                    field.type.value
                ].append(field)
    job.event(f"retrieved {len(normalised_nb)} BGP session(s) from NetBox")

    # Build updates dictionary by NetBox ID
    job.event("normalising BGP session update data")
    normalised_updates = {}
    selected_by_id = set(session_ids)
    for bgp_session in bgp_sessions:
        session_id = bgp_session.get("id") or ids_by_name.get(
            bgp_session.get("name")
        )
        label = bgp_session.get("name") or session_id
        # Report sessions not found in NetBox
        if session_id not in normalised_nb:
            msg = f"BGP session '{label}' not found in NetBox, skipping update"
            job.event(msg, severity="ERROR")
            log.error(f"{self.name} - {msg}")
            ret.errors.append(msg)
        else:
            # Normalise policies to list
            if isinstance(bgp_session.get("import_policies"), str):
                bgp_session["import_policies"] = [bgp_session["import_policies"]]
            if isinstance(bgp_session.get("export_policies"), str):
                bgp_session["export_policies"] = [bgp_session["export_policies"]]
            update = {
                k: v for k, v in bgp_session.items() if k not in ("new_name", "id")
            }
            update["name"] = (
                bgp_session["new_name"]
                if bgp_session.get("new_name") is not None
                else normalised_nb[session_id]["name"]
            )
            normalised_updates[session_id] = update
            if "import_policies" in update:
                update["import_policies"] = sorted(
                    update.get("import_policies") or []
                )
            if "export_policies" in update:
                update["export_policies"] = sorted(
                    update.get("export_policies") or []
                )
            if "tags" in update:
                # Compare and write the complete additive tag list.
                normalised_nb[session_id]["tags"] = merge_array_values(
                    [], nb_sessions_by_id[session_id].tags, attribute="name"
                )
                update["tags"] = merge_array_values(
                    nb_sessions_by_id[session_id].tags,
                    update["tags"],
                    attribute="name",
                )
            if update.get("custom_fields") is not None:
                fields = lookup_cache["custom_fields"]["netbox_bgp.bgpsession"]
                current, merged = merge_resolved_custom_fields(
                    self,
                    nb,
                    nb_sessions_by_id[session_id],
                    update["custom_fields"],
                    fields["object"],
                    fields["multiobject"],
                )
                normalised_nb[session_id]["custom_fields"] = current
                update["custom_fields"] = merged

    # Compare complete dictionaries using make_diff; classify in_sync vs changed
    job.event("calculating BGP session update diff")
    sessions_diff = self.make_diff(
        {"_": normalised_updates},
        {"_": normalised_nb},
    )["_"]

    result["in_sync"].extend(
        (
            session_id
            if session_id in selected_by_id
            else normalised_nb[session_id]["name"]
        )
        for session_id in sessions_diff["in_sync"]
    )
    job.event(
        f"BGP session update diff complete: {len(sessions_diff['update'])} update, "
        f"{len(sessions_diff['in_sync'])} in sync"
    )

    if dry_run is True:
        job.event(
            "dry-run requested, returning BGP session update diff without changes"
        )
        result["update"] = [
            {
                "name": normalised_nb[session_id]["name"],
                **({"id": session_id} if session_id in selected_by_id else {}),
                "diff": changes,
            }
            for session_id, changes in sessions_diff["update"].items()
        ]
        ret.result = result
        ret.dry_run = True
        job.event("BGP session update task complete")
        return ret

    # Build update payloads — iterate over diff to get only changed fields per session
    job.event("building BGP session update payloads")
    update_payloads = []
    for session_id, field_changes in sessions_diff["update"].items():
        nb_session = normalised_nb[session_id]
        addr_family = get_addr_family(nb_session["local_address"] or "0.0.0.0")
        payload = {"id": nb_session["id"]}
        payload.update(
            resolve_bgp_session_payload_fields(
                {f: c["new_value"] for f, c in field_changes.items()},
                nb,
                rir_id,
                job,
                ret,
                self.name,
                addr_family,
                lookup_cache=lookup_cache,
                vrf_custom_field=vrf_custom_field,
            )
        )
        update_payloads.append((session_id, payload))
    job.event(f"prepared {len(update_payloads)} BGP session update payload(s)")

    # Bulk update
    if update_payloads:
        job.event(f"updating {len(update_payloads)} BGP session(s)")
        total_batches = (len(update_payloads) + batch_size - 1) // batch_size
        for batch_start in range(0, len(update_payloads), batch_size):
            batch = update_payloads[batch_start : batch_start + batch_size]
            batch_number = batch_start // batch_size + 1
            msg = f"updating BGP session batch {batch_number}/{total_batches} ({len(batch)} session(s))"
            job.event(msg)
            log.info(msg)
            try:
                nb.plugins.bgp.session.update([payload for _, payload in batch])
            except Exception as exc:
                msg = f"failed to update BGP session batch {batch_number}/{total_batches}: {exc}"
                job.event(msg, severity="ERROR")
                ret.errors.append(msg)
                ret.failed = True
                ret.result = result
                log.error(f"{self.name} - {msg}")
                return ret
            result["updated"].extend(
                (
                    session_id
                    if session_id in selected_by_id
                    else normalised_nb[session_id]["name"]
                )
                for session_id, _ in batch
            )
        msg = f"updated {len(update_payloads)} BGP session(s)"
        job.event(msg)
        log.info(f"{self.name} - {msg}")
    else:
        job.event("no BGP sessions to update")

    ret.result = result
    job.event("BGP session update task complete")
    return ret