Skip to content

NetBox Create ASN Task¤

task API name: create_asn

The create_asn task creates or updates one NetBox ASN. It can use an explicit ASN or allocate the next available value from an existing named ASN range.

Inputs¤

Parameter Required Default Description
asn_range Conditional None Existing ASN range name used for allocation
sites No None Site names to assign the ASN to
role No None IPAM role name
asn Conditional None Explicit ASN from 1 through 4294967295
rir Conditional None Existing RIR name required for an explicit ASN without asn_range
description No None ASN description and allocation deduplication value within a range
tenant No None Tenant name
tags No None Tag names
custom_fields No None NetBox custom-field values
instance No Worker default NetBox instance to target
dry_run No False Return the selected ASN without writing
branch No None NetBox Branching plugin branch name

Provide asn or asn_range. When asn is supplied without a range, rir is required.

Explicit ASN numbers take precedence. Additional searches only run when asn_range is provided. With both role and a nonempty sites list, the task searches that range for the role and any requested site. Otherwise, it searches the range by description. Multiple matches cause an error. If the selected lookup finds no match, it allocates the next available ASN. Role/site lookup does not fall back to description.

asns:
  - create_asn:
      asn_range: CUSTOMER ASNS
      role: CUSTOMER EDGE
      sites: [BRANCH-1]

Repeated calls reuse the range's CUSTOMER EDGE ASN for BRANCH-1, even without a description. The range, role, and site must already exist.

Output¤

{
    "asn": 64512,
    "description": "edge routing",
    "status": "created",
}

status is create or update during dry-run and created or updated after an applied operation.

Examples¤

nf# netbox create asn asn-range PRIVATE-ASNS description "edge routing" sites BRANCH-001,BRANCH-002
nf# netbox create asn asn 65001 rir Private sites BRANCH-001
from norfab.core.nfapi import NorFab

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

    allocated = client.run_job(
        "netbox",
        "create_asn",
        workers="any",
        kwargs={
            "asn_range": "PRIVATE-ASNS",
            "sites": ["BRANCH-001", "BRANCH-002"],
            "description": "edge routing",
        },
    )

    explicit = client.run_job(
        "netbox",
        "create_asn",
        workers="any",
        kwargs={"asn": 65001, "rir": "Private"},
    )

In a NetBox design:

asns:
  - create_asn:
      asn_range: PRIVATE-ASNS
      description: edge routing
      sites: [BRANCH-001]
      role: BRANCH-ROUTING

Notes / Gotchas¤

  • The named ASN range and its RIR must already exist. ASN ranges have no site scope; sites associates the ASN with sites.
  • When updating an ASN, new sites and tags are added to the existing lists. Existing entries stay in place. The same applies to custom fields whose current and new values are both lists. An empty list ([]) makes no change to an existing list. For custom fields, null clears the value, and a new scalar value replaces the old one.
  • For object and multiobject custom fields, supply the related objects by name or ID. For example, a custom field referencing devices can receive ["router-1", "router-2"]. The task looks up names in the field's related NetBox object type and sends their IDs. A name must match exactly one object; missing or ambiguous names cause an error.
  • In NFCLI, separate multiple site names with commas.
  • Repeated range allocations reuse an ASN with the same description inside that range.
  • Without a description, repeated range calls allocate the next available ASN.
  • Branch writes require the NetBox Branching plugin.

Troubleshooting¤

  • ASN range not found: verify the exact range name in NetBox.
  • No available ASNs: expand the range or remove an unused allocation.
  • RIR not found: create the RIR or correct the rir name.
  • Multiple description matches: make descriptions unique inside the ASN range.

Task Command Shell Reference¤

nf# man tree netbox create asn
netbox create asn
├── asn-range
├── asn
├── rir
├── description
├── tenant
├── sites
├── role
├── tags
├── custom-fields
├── instance
├── branch
└── dry-run

Python API Reference¤

Create, update, or allocate one ASN in NetBox.

An explicit ASN is matched globally by number. Without one, range searches use role and sites when both are supplied, matching an ASN in the range with that role assigned to any requested site. Otherwise, it matches by description within the range. If the selected lookup finds no match, it allocates the first available number. Without either lookup, repeated range calls can allocate different ASNs. A non-empty sites list adds to the ASN's site assignments; an empty or omitted list leaves them alone. Tags and array custom fields are also additive. Dry-run reports the selected number without writing relationships.

Parameters:

Name Type Description Default
job Job

Current NorFab job for events and branch provisioning.

required
asn_range Union[None, str]

Existing range name for next-available allocation.

None
asn Union[None, int]

Explicit ASN number; omit to allocate from asn_range.

None
rir Union[None, str]

Existing RIR name required whenever an explicit ASN is supplied without a range, including updates.

None
description Union[None, str]

Description used to reuse allocations within a range.

None
tenant Union[None, str]

Optional tenant name.

None
tags Union[None, list]

Optional tag names to add without removing existing tags.

None
custom_fields Union[None, dict]

Optional NetBox custom-field values. Object and multiobject fields accept related object names or IDs; names must identify exactly one object. Arrays merged with existing values; empty arrays preserve existing arrays. Null and scalar values replace existing values.

None
sites Union[None, list[str]]

Optional site names to add to the ASN's assignments. With role, also used to reuse a range allocation assigned to any of these sites.

None
role Union[None, str]

Optional IPAM role name. With sites, also used for range lookup before description matching.

None
instance Union[None, str]

NetBox instance name, or the worker default when omitted.

None
dry_run bool

Select an ASN without creating or updating it.

False
branch Union[None, str]

Optional NetBox Branching plugin branch name.

None

Returns:

Name Type Description
Result Result

Selected ASN, description, and action. Dry-run actions

Result

are create or update; applied actions are created or

Result

updated.

Raises:

Type Description
ValueError

The range, RIR, site, or related custom-field object is missing; a related name is ambiguous; more than one ASN matches the description or role/sites; or the range has no free ASNs.

Source code in norfab\workers\netbox_worker\bgp_asn_tasks.py
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
@Task(
    fastapi={"methods": ["POST"], "schema": NetboxFastApiArgs.model_json_schema()},
    input=CreateBgpAsnInput,
    output=CreateBgpAsnResult,
    mcp={
        "annotations": {
            "title": "Create BGP ASN",
            "readOnlyHint": False,
            "destructiveHint": False,
            "idempotentHint": True,
            "openWorldHint": True,
        }
    },
)
def create_asn(
    self,
    job: Job,
    asn_range: Union[None, str] = None,
    asn: Union[None, int] = None,
    rir: Union[None, str] = None,
    description: Union[None, str] = None,
    tenant: Union[None, str] = None,
    tags: Union[None, list] = None,
    custom_fields: Union[None, dict] = None,
    sites: Union[None, list[str]] = None,
    role: Union[None, str] = None,
    instance: Union[None, str] = None,
    dry_run: bool = False,
    branch: Union[None, str] = None,
) -> Result:
    """Create, update, or allocate one ASN in NetBox.

    An explicit ASN is matched globally by number. Without one, range searches
    use role and sites when both are supplied, matching an ASN in the range
    with that role assigned to any requested site. Otherwise, it matches
    by description within the range. If the selected lookup finds no match,
    it allocates the first available number. Without either lookup, repeated
    range calls can allocate different ASNs. A non-empty sites
    list adds to the ASN's site assignments; an empty or omitted list
    leaves them alone. Tags and array custom fields are also additive.
    Dry-run reports the selected number without writing relationships.

    Args:
        job: Current NorFab job for events and branch provisioning.
        asn_range: Existing range name for next-available allocation.
        asn: Explicit ASN number; omit to allocate from ``asn_range``.
        rir: Existing RIR name required whenever an explicit ASN is
            supplied without a range, including updates.
        description: Description used to reuse allocations within a range.
        tenant: Optional tenant name.
        tags: Optional tag names to add without removing existing tags.
        custom_fields: Optional NetBox custom-field values. Object and
            multiobject fields accept related object names or IDs; names
            must identify exactly one object. Arrays merged with existing values;
            empty arrays preserve existing arrays. Null and scalar values
            replace existing values.
        sites: Optional site names to add to the ASN's assignments. With role,
            also used to reuse a range allocation assigned to any of these sites.
        role: Optional IPAM role name. With sites, also used for range lookup
            before description matching.
        instance: NetBox instance name, or the worker default when omitted.
        dry_run: Select an ASN without creating or updating it.
        branch: Optional NetBox Branching plugin branch name.

    Returns:
        Result: Selected ASN, description, and action. Dry-run actions
        are ``create`` or ``update``; applied actions are ``created`` or
        ``updated``.

    Raises:
        ValueError: The range, RIR, site, or related custom-field object is
            missing; a related name is ambiguous; more than one ASN matches
            the description or role/sites; or the range has no free ASNs.
    """
    instance = instance or self.default_instance
    ret = Result(
        task=f"{self.name}:create_asn",
        result={},
        resources=[instance],
        dry_run=dry_run,
    )
    nb = self._get_pynetbox(instance, branch=branch, job=job)
    nb_range = nb.ipam.asn_ranges.get(name=asn_range) if asn_range else None
    if asn_range and not nb_range:
        raise ValueError(f"ASN range '{asn_range}' not found in NetBox")

    matches = []
    if asn is not None:
        existing = nb.ipam.asns.get(asn=asn)
        matches = [existing] if existing else []
    elif nb_range and role and sites:
        matches = self.bulk_filter(
            nb.ipam.asns,
            asn__gte=nb_range.start,
            asn__lte=nb_range.end,
            role=nb.ipam.roles.get(name=role).slug,
            site=[
                site.slug for site in self.bulk_filter(nb.dcim.sites, name=sites)
            ],
        )
    elif nb_range and description:
        matches = self.bulk_filter(
            nb.ipam.asns,
            description=description,
            asn__gte=nb_range.start,
            asn__lte=nb_range.end,
        )
    if len(matches) > 1:
        raise ValueError(
            f"ASN lookup matched more than one ASN in range '{asn_range}' (description={description!r}, role={role!r}, sites={sites!r})"
        )

    nb_asn = matches[0] if matches else None
    if not nb_asn and asn is None:
        available = list(nb_range.available_asns.list())
        if not available:
            raise ValueError(f"ASN range '{asn_range}' has no available ASNs")
        asn = int(
            available[0]["asn"]
            if isinstance(available[0], dict)
            else getattr(available[0], "asn", available[0])
        )
    if dry_run:
        ret.result = {
            "asn": int(nb_asn.asn) if nb_asn else asn,
            "description": description,
            "status": "update" if nb_asn else "create",
        }
        return ret

    if not nb_asn and nb_range:
        nb_asn = nb.ipam.asns.create({"asn": asn, "rir": nb_range.rir.id})
    elif not nb_asn:
        rir_object = nb.ipam.rirs.get(name=rir)
        if not rir_object:
            raise ValueError(f"RIR '{rir}' not found in NetBox")
        nb_asn = nb.ipam.asns.create({"asn": asn, "rir": rir_object.id})

    changed = False
    if description is not None and nb_asn.description != description:
        nb_asn.description = description
        changed = True
    if tenant is not None and str(nb_asn.tenant) != tenant:
        nb_asn.tenant = {"name": tenant}
        changed = True
    if role is not None and str(nb_asn.role) != role:
        nb_asn.role = {"name": role}
        changed = True
    site_names = list(dict.fromkeys(sites or []))
    if site_names:
        nb_sites = self.bulk_filter(nb.dcim.sites, name=site_names)
        site_ids = {item.name: item.id for item in nb_sites}
        if len(site_ids) != len(site_names):
            raise ValueError(
                f"ASN sites not found: {set(site_names) - set(site_ids)}"
            )
        current_ids = [site.id for site in nb_asn.sites]
        merged_ids = merge_array_values(
            current_ids, [site_ids[name] for name in site_names]
        )
        if merged_ids != current_ids:
            nb_asn.sites = merged_ids
            changed = True
    if tags:
        current_names = [tag.name for tag in nb_asn.tags]
        merged_names = merge_array_values(current_names, tags)
        if merged_names != current_names:
            nb_asn.tags = [{"name": name} for name in merged_names]
            changed = True
    if custom_fields is not None:
        reference_fields = self.bulk_filter(
            nb.extras.custom_fields,
            object_type="ipam.asn",
            type=["object", "multiobject"],
        )
        object_fields = [
            field for field in reference_fields if field.type.value == "object"
        ]
        multiobject_fields = [
            field for field in reference_fields if field.type.value == "multiobject"
        ]
        current_fields, merged_fields = merge_resolved_custom_fields(
            self,
            nb,
            nb_asn,
            custom_fields,
            object_fields,
            multiobject_fields,
        )
        if merged_fields != current_fields:
            nb_asn.custom_fields = merged_fields
            changed = True
    if changed:
        nb_asn.save()

    ret.status = "updated" if matches else "created"
    ret.result = {
        "asn": int(nb_asn.asn),
        "description": nb_asn.description,
        "status": ret.status,
    }
    job.event(f"{ret.status} ASN {nb_asn.asn}")
    return ret