This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
netbox-bgp is a NetBox plugin that adds BGP-related objects (Sessions, Peer Groups, Communities, Community Lists, Routing Policies + Rules, Prefix Lists + Rules, AS Path Lists + Rules). The plugin is distributed on PyPI as netbox-bgp and installs as a Django app named netbox_bgp.
The NetBox ⇄ plugin version pairing is strict (declared in netbox_bgp/__init__.py via min_version/max_version and summarised in README.md). The current branch (0.19.0) targets NetBox 4.5.x. Changing the plugin's NetBox version range almost always requires migrations and import changes against NetBox internals.
The develop/ directory contains a full Docker Compose stack (NetBox + worker + Postgres + Redis). The plugin source is bind-mounted into the NetBox container, so edits on the host take effect after a container restart (or runserver autoreload). The Makefile wraps everything — prefer it over raw docker compose.
Common commands (all run via make):
make cbuild— build the dev image. PassNETBOX_VER=vX.Y.Z PYTHON_VER=3.12to target a different NetBox tag.make debug/make start/make stop— run the stack in foreground / detached / stop.make destroy— stop the stack and drop the Postgres volume (netbox_bgp_pgdata_netbox_bgp). Use when migrations get wedged.make migrations— generate Django migrations for the plugin (writes intonetbox_bgp/migrations/). Required after anymodels.pychange.make test— runpython manage.py test netbox_bgpinside the container. Tests live innetbox_bgp/tests/(test_api.py,test_filtersets.py,test_forms.py,test_models.py,test_views.py). Run a single test with:docker compose -f develop/docker-compose.yml -p netbox_bgp run netbox python manage.py test netbox_bgp.tests.test_api.SomeTestCase.test_method.make nbshell/make shell— NetBox shell / Django shell.make adduser— create a superuser.make pbuild/make pypipub— build sdist/wheel / upload to PyPI.make relpatch— bump patch version on a release branch (requires clean working tree; runspysemver bump patchagainstnetbox_bgp/version.py).
The version string lives in a single place: netbox_bgp/version.py (read by both setup.py and PluginConfig).
This is a standard NetBox plugin — netbox_bgp/__init__.py exposes a PluginConfig subclass as config, which NetBox discovers when the plugin is listed in PLUGINS. Everything else follows NetBox's plugin conventions; when adding features, mirror the patterns used by existing models rather than inventing new ones.
All models inherit from netbox.models.NetBoxModel (giving them change-logging, tags, custom fields, journaling). Two shapes are used heavily:
- List / Rule pairs:
PrefixList↔PrefixListRule,CommunityList↔CommunityListRule,ASPathList↔ASPathListRule,RoutingPolicy↔RoutingPolicyRule. Rules are ordered by(parent, index)andon_delete=CASCADEfrom the parent. BGPBaseabstract: providessite,tenant,status,role,description,comments. Currently onlyCommunityinherits it;BGPSessionis a standalone model with its own richer FK set (device/vm, local/remote IP + AS, peer group, import/export policies, prefix lists in/out,remote_as_macro,extra_attributes).BGPPeerGroupalso carries session-level config fields (local_as,remote_as,prefix_list_in,prefix_list_out,extra_attributes) that serve as group-wide defaults.
BGPSession.label falls back to f'{remote_address}:{remote_as}' when name is unset — don't introduce code that assumes name is always populated. BGPSession.Meta.unique_together covers both device- and VM-scoped tuples; a session must have exactly one of device/VM in practice (the clean() enforcement is currently commented out — see models.py:489).
PrefixListRule has dual prefix fields: prefix (FK to ipam.Prefix) and prefix_custom (IPNetworkField). Exactly one must be set; this is enforced in clean(). The network property returns whichever is populated.
api/— DRF viewsets/serializers mounted viaNetBoxRouter. Note: bothbgpsession/sessionandbgppeergroup/peer-groupare registered as aliases for backwards compatibility (api/urls.py). When adding endpoints, register both spellings only if you're preserving an alias; otherwise pick one.graphql/— strawberry-django schema, types, filters, enums. The top-levelNetBoxBGPQuerytype exposesnetbox_bgp_*fields; field names are load-bearing for GraphQL clients.filtersets.py/forms.py/tables.py— FilterSets (django-filter), edit/filter/bulk forms, and django-tables2 tables.urls.py— usesutilities.urls.get_model_urls()(NetBox 4.x pattern) rather than hand-written URL patterns.detail=Falsemounts list/add/import; the<int:pk>/line mounts the detail subtree.views.py— model views. Registered viaget_model_urls()discovery, not manual URL conf.template_content.py— registers extra tabs/pages on NetBox core objects (Device, Interface, Site, Tenant, VirtualMachine, ASN, IPAddress) usingregister_model_view+ViewTab. ReadsPLUGINS_CONFIG['netbox_bgp']['device_ext_page']at import time to decide whether to register a Device tab; changing that config requires a restart. The Device-specific inline (left/right/full_width) uses aPluginTemplateExtensioninstead.navigation.py— menu items; honourstop_level_menusetting.templates/netbox_bgp/— per-object detail templates.migrations/— 0001 through 0041 at time of writing. Always add new ones viamake migrations, don't hand-write.
Defined in BGPConfig.default_settings:
device_ext_page(default"right"):"left"/"right"/"full_width"/"tab"/""(disabled).top_level_menu(defaultFalse).remote_address_strict(defaultFalse): whenTrue,BGPSessionAddFormreplaces the free-text CIDR field forremote_addresswith aDynamicModelChoiceFieldlimited to existingIPAddressobjects. WhenFalse(default), typing a CIDR that doesn't match an existing IP will auto-create one.
Access at runtime via settings.PLUGINS_CONFIG['netbox_bgp'].
Inherited from NetBox core (https://github.com/netbox-community/netbox/blob/main/CLAUDE.md) — this plugin follows the same conventions as a NetBox Django app:
- Apps: Each Django app owns its models, views, API serializers, filtersets, forms, and tests.
- Views: Use
register_model_view()to register model views by action (e.g. "add", "list", etc.). List views typically don't need to addselect_related()orprefetch_related()on their querysets: Prefetching is handled dynamically by the table class so that only relevant fields are prefetched. - REST API: DRF serializers live in
<app>/api/serializers.py; viewsets in<app>/api/views.py; URLs auto-registered in<app>/api/urls.py. REST API views typically don't need to addselect_related()orprefetch_related()on their querysets: Prefetching is handled dynamically by the serializer so that only relevant fields are prefetched. - GraphQL: Strawberry types in
<app>/graphql/types.py. - Filtersets:
<app>/filtersets.py— used for both UI filtering and API?filter=params. - Tables:
django-tables2used for all object list views (<app>/tables.py). - Templates: Django templates in
netbox/templates/<app>/(in this plugin:netbox_bgp/templates/netbox_bgp/). - Tests: Mirror the app structure in
<app>/tests/. Usenetbox.configuration_testingfor test config. The suite currently has ~623 tests across five files.
Inherited from NetBox core:
- Follow existing Django conventions; don't reinvent patterns already present in the codebase.
- New models must include
created,last_updatedfields (inherit fromNetBoxModelwhere appropriate). - Every model exposed in the UI needs: model, serializer, filterset, form, table, views, URL route, and tests.
- API serializers must include a
urlfield (absolute URL of the object). - Use
FeatureQueryfor generic relations (config contexts, custom fields, tags, etc.). - Avoid adding new dependencies without strong justification.
- Avoid running
ruff formaton existing files, as this tends to introduce unnecessary style changes. - Don't craft Django database migrations manually: Prompt the user to run
make migrationsinstead (which runsmanage.py makemigrationsinside the dev container).
| File | What it covers |
|---|---|
test_models.py |
Model __str__, clean() validation, property logic, unique constraints |
test_api.py |
REST API + GraphQL via APIViewTestCases base classes |
test_views.py |
UI views via ViewTestCases base classes |
test_filtersets.py |
FilterSet search() and explicit filter fields |
test_forms.py |
Form-level logic not exercised by view tests (e.g. remote_address_strict) |
_get_base_url()must return'plugins:netbox_bgp:{model_name}_{{}}'(the{}is the action placeholder filled by the base class).- Use
setUpTestDatafor all fixtures; set dynamiccls.form_data(FK PKs) insidesetUpTestData, not as a class attribute. - Rule models (
ASPathListRule,CommunityListRule,RoutingPolicyRule,PrefixListRule) have noBulkEditViewregistered — don't includeBulkEditObjectsViewTestCasefor them. CommunityListRulehas noBulkImportView— don't includeBulkImportObjectsViewTestCasefor it.BGPSessionCreate/Edit incompatibility: the Add view usesBGPSessionAddForm(IPNetworkFormFieldforremote_address) while the Edit view usesBGPSessionForm(DynamicModelChoiceField). A singleform_datacannot satisfy both, so omitCreateObjectViewTestCaseandEditObjectViewTestCaseforBGPSession.PrefixListRule.prefix_customis stored asIPNetwork, not a string; addvalidation_excluded_fields = ['prefix_custom']to skip the post-save value comparison in Create/Edit tests.
NetBoxModelFilterSet generates different filter types from class Meta: fields:
CharField(name, description, pattern, …) →MultiValueCharFilter— pass a list:{'name': ['my-value']}CharFieldwithchoices(status, family, action) →ChoiceFilter— pass a single string:{'status': 'active'}- Explicitly declared
ModelMultipleChoiceFilterfields accept lists of lookup values:{'local_as': [65001]},{'device': ['router1']}.
- Several
search()methods in filtersets useQ(fk_field__icontains=value)orQ(integer_field__icontains=value)(e.g.ASPathListRuleFilterSet,CommunityListRuleFilterSet,RoutingPolicyRuleFilterSet,PrefixListRuleFilterSet). These are potentially broken on PostgreSQL — avoid adding similar patterns; write explicit field lookups instead. BGPSessionFilterSet.search()includesQ(remote_as__asn__icontains=value)whereasnis aBigIntegerField. Skip testing theqparameter forBGPSessionFilterSet; test the explicitby_remote_address/by_local_addressmethods and declaredModelMultipleChoiceFilterfields instead.develop/configuration.pysetsDEBUG = 'test' not in sys.argvso thatdebug_toolbaris excluded fromINSTALLED_APPSduring test runs (NetBox's settings.py removes it only whenDEBUG=Falseat import time).
- When adding a model, add it across all of:
models.py,forms.py,tables.py,filtersets.py,api/serializers.py,api/views.py,api/urls.py,graphql/types.py,graphql/filters.py,graphql/schema.py,navigation.py,urls.py,templates/netbox_bgp/<name>.html, and a migration. Missing any of these breaks either the UI, REST API, or GraphQL. - API URL registration keeps legacy aliases (
session+bgpsession,peer-group+bgppeergroup). Don't remove them without a deprecation cycle — external tooling depends on these paths. - Verbose plural names are set explicitly on several models (
Communities,Routing Policies,Peer Groups,Prefix Lists,AS Path Lists) because Django's default pluraliser gets them wrong. Preserve these when editingMeta.