Contributing
Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.
We love your input! We want to make contributing to this project as easy and transparent as possible, whether it’s:
Reporting a bug
Discussing the current state of the code
Submitting a fix
Proposing new features
Becoming a maintainer
General Tips for Working on GitHub
Register for a free GitHub account if you haven’t already.
You can use GitHub Markdown for formatting text and adding images.
To help mitigate notification spam, please avoid “bumping” issues with no activity. (To vote an issue up or down, use a :thumbsup: or :thumbsdown: reaction.)
Please avoid pinging members with
@unless they’ve previously expressed interest or involvement with that particular issue.Familiarize yourself with this list of discussion anti-patterns and make every effort to avoid them.
Types of Contributions
Report Bugs
Report bugs at https://github.com/menckend/netbox_rpki/issues.
If you are reporting a bug, please include:
Your operating system name and version.
Any details about your local setup that might be helpful in troubleshooting.
Detailed steps to reproduce the bug.
Fix Bugs
Look through the GitHub issues for bugs. Anything tagged with “bug” and “help wanted” is open to whoever wants to implement it.
Implement Features
Look through the GitHub issues for features. Anything tagged with “enhancement” and “help wanted” is open to whoever wants to implement it.
Write Documentation
NetBox RPKI Plugin could always use more documentation, whether as part of the official NetBox RPKI Plugin docs, in docstrings, or even on the web in blog posts, articles, and such.
Submit Feedback
The best way to send feedback is to file an issue at https://github.com/menckend/netbox_rpki/issues.
If you are proposing a feature:
Explain in detail how it would work.
Keep the scope as narrow as possible, to make it easier to implement.
Remember that this is a volunteer-driven project, and that contributions are welcome :)
Get Started!
For local development setup instructions, see LOCAL_DEV_SETUP.md.
Generating Migrations Locally
In the standard local workspace layout from LOCAL_DEV_SETUP.md, NetBox’s management entry point lives at:
~/src/netbox-v4.5.7/netbox/manage.py
Use the pinned NetBox virtualenv and the plugin’s test settings when generating plugin migrations:
~/.virtualenvs/netbox-4.5.7/bin/python \
~/src/netbox-v4.5.7/netbox/manage.py \
makemigrations netbox_rpki \
--settings=netbox_rpki.tests.netbox_configuration
If your local checkout or virtualenv uses a different pinned NetBox version, adjust the netbox-v4.5.7 and netbox-4.5.7 path segments accordingly.
Test lane expectations
Use the devrun wrapper for all local test runs:
cd ~/src/netbox_rpki/devrun
./dev.sh test fast
./dev.sh test contract
./dev.sh test provider
./dev.sh test live-provider
./dev.sh test full
Lane intent:
fast: low-cost structural smoke checkscontract: registry/UI/API/GraphQL contract coverageprovider: fixture-backed hosted-provider sync/write behaviorlive-provider: opt-in real-backend integration tests onlyfull: the full suite, with any live-provider tests expected to self-skip unless explicitly enabled
Live-provider tests must live in netbox_rpki/tests/test_live_*.py and must be guarded so they skip unless NETBOX_RPKI_ENABLE_LIVE_PROVIDER_TESTS=1 is set. This keeps day-to-day development independent from external provider credentials or live backends.
For the blessed no-RIR-credentials path, use the documented public Krill testbed helper:
cd ~/src/netbox_rpki/devrun
./public-krill-testbed.sh env
./public-krill-testbed.sh check
./public-krill-testbed.sh run
That path is documented in LOCAL_DEV_SETUP.md and is the preferred contributor route for real hosted-provider integration work when normal CI coverage is not enough. Certification CI does not provision public-testbed credentials and does not run that path automatically.
Pull Request Guidelines
Before you submit a pull request, check that it meets these guidelines:
The pull request should include tests.
If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.md.
The pull request should work for Python 3.12, 3.13 and 3.14. Check https://github.com/menckend/netbox_rpki/actions and make sure that the tests pass for all supported Python versions.
Registry-Based Architecture
This section is the implementation-facing guide for the registry-based architecture in this plugin. It explains what is generated, what must stay explicit, and the exact wiring required when you add new models or extend an existing object family.
Read this together with these source-of-truth files when you are doing architecture work:
netbox_rpki/object_specs.pynetbox_rpki/object_registry.pynetbox_rpki/detail_specs.pynetbox_rpki/views.pynetbox_rpki/urls.pynetbox_rpki/forms.pynetbox_rpki/filtersets.pynetbox_rpki/tables.pynetbox_rpki/api/serializers.pynetbox_rpki/api/views.pynetbox_rpki/api/urls.pynetbox_rpki/graphql/filters.pynetbox_rpki/graphql/types.pynetbox_rpki/graphql/schema.pynetbox_rpki/tests/registry_scenarios.pynetbox_rpki/tests/test_views.pynetbox_rpki/tests/test_api.pynetbox_rpki/tests/test_graphql.pydevrun/work_in_progress/netbox_rpki_metadata_refactor_plan.mddevrun/work_in_progress/netbox_rpki_surface_contract_checklist.md
Core architecture
The plugin has a deliberate split between the explicit Django model layer and the generated plugin-surface layer.
Keep explicit:
Django models
Django migrations
domain rules and business logic
jobs and services
complex validation
any detail page that cannot be described cleanly with metadata
any API action that is not standard CRUD
Keep registry-driven:
standard REST serializers
standard REST viewsets
API router registration
standard forms
filter forms
filtersets
tables
standard list, detail, edit, and delete views
standard UI URL registration
navigation/menu registration
GraphQL filters, types, and query field registration
shared smoke and surface-contract tests
The registry exists to eliminate repeated plumbing. It does not exist to hide domain behavior or to dynamically invent the model layer.
The spec contract
netbox_rpki/object_specs.py defines the active metadata contract.
Each ObjectSpec has these major parts:
registry_key: the plugin-internal identity for the object familymodel: the explicit Django model classlabels: singular and plural UI labelsroutes: public UI naming and path metadataapi: serializer, viewset, and REST basename metadatafilterset: filter and search configurationgraphql: GraphQL filter, type, and query-field metadatanavigation: menu group, label, ordering, and add-button visibilityform: standard edit-form class metadatafilter_form: list filter-form class metadatatable: standard list/detail related-table metadataview: standard list/detail/edit/delete class metadata and mutability
The important point is that ObjectSpec is not a loose suggestion. The generator modules build real classes and routes from it.
Internal identity versus public naming
This plugin used to overload one identifier for too many jobs. That caused route mismatches, broken reverse lookups, GraphQL drift, and UI links that pointed at routes which did not actually exist.
Do not reintroduce that mistake.
There are now separate naming layers:
registry_key: internal plugin identity used for maps and generator lookupsRouteSpec.slug: public UI route name stemRouteSpec.path_prefix: public UI path segment when the path must not be derived from the slugApiSpec.basename: public REST router basenameGraphQLSpec.detail_field_name: public singular GraphQL field nameGraphQLSpec.list_field_name: public list GraphQL field name
Rules:
registry_keyshould be stable and internal.Public names should be set explicitly whenever there is any chance of collision, drift, or future rename pressure.
Do not assume that model class names are safe public names.
Do not assume that pluralized URLs can be derived mechanically.
Current examples:
RpkiProviderAccountkeeps a plugin-specific model name to avoid collisions, but its public slug, API basename, and GraphQL fields are exposed asprovideraccount.Legacy inventory objects keep exact path prefixes such as
orgs,roaprefixes,certificateprefixes, andcertificateasnseven though their slugs are not those exact strings.
If you are tempted to collapse these fields back into one identifier, stop. The split is intentional and was added to fix real breakage.
Generation pipeline
The registry feeds nearly every standard surface in the plugin.
UI generation
netbox_rpki/views.py generates standard list, detail, edit, and delete view classes.
Important behavior:
list actions are built from
ViewSpec.supports_createdetail actions are built from
ViewSpec.supports_createandViewSpec.supports_deleteread-only objects do not get generated clone, edit, delete, or add actions
simple detail pages are generated from
spec.api.fieldsricher detail pages are pulled from
detail_specs.py
netbox_rpki/urls.py registers the generated routes using spec.routes.slug and spec.routes.resolved_path_prefix.
netbox_rpki/navigation.py builds the plugin menu from NavigationSpec and suppresses the add button unless the object is actually creatable.
netbox_rpki/tables.py builds standard tables and row actions. Row-action menus are also derived from mutability. This matters because NetBox table defaults otherwise assume edit and delete routes exist.
Form and filter generation
netbox_rpki/forms.py generates:
standard model forms from
spec.formstandard filter forms from
spec.filter_form
The standard generated model form injects tenant and comments, and the standard generated filter form injects q, tenant, and tag.
netbox_rpki/filtersets.py generates standard filtersets from spec.filterset and implements the shared text-search behavior using spec.filterset.search_fields.
REST API generation
netbox_rpki/api/serializers.py generates standard NetBoxModelSerializer subclasses and exposes them through SERIALIZER_CLASS_MAP keyed by registry_key.
netbox_rpki/api/views.py generates standard NetBoxModelViewSet subclasses and exposes them through VIEWSET_CLASS_MAP keyed by registry_key.
Important behavior:
read-only objects get
http_method_names = ["get", "head", "options"]writable objects use the normal NetBox model viewset behavior
custom actions are implemented by subclassing the generated viewset and replacing the relevant entry in
VIEWSET_CLASS_MAP
netbox_rpki/api/urls.py registers every API object through the NetBox router using spec.api.basename.
GraphQL generation
netbox_rpki/graphql/filters.py generates filter classes and exposes GRAPHQL_FILTER_CLASS_MAP.
netbox_rpki/graphql/types.py generates types and exposes GRAPHQL_TYPE_CLASS_MAP.
netbox_rpki/graphql/schema.py generates the query type and exposes GRAPHQL_FIELD_NAME_MAP so tests can assert stable public field names.
Important behavior:
GraphQL field names are explicit metadata, not derived from model names
the plugin GraphQL package must continue to re-export
schemafromnetbox_rpki/graphql/__init__.pystable public field names matter more than saving a few metadata lines
Rich detail pages versus simple generated detail pages
There are two detail-view paths.
Use the simple generated path when:
a straight field list is enough
field ordering can follow
spec.api.fieldsno curated related tables are needed
no prefilled add actions are needed
no custom rendering beyond relation links and URL fields is needed
Use netbox_rpki/detail_specs.py when:
the object is a top-level workflow or dashboard page
related tables are part of the main value of the page
you need custom field ordering
you need action buttons that prefill child forms
a field should render as code or with special formatting
Do not force every object into the rich-detail system. Most objects should stay simple.
Also do not rely on old assumptions about NetBox defaults. Generated detail pages now explicitly control their action buttons because inherited defaults caused broken links on read-only surfaces.
Model-side action URL resolution
netbox_rpki/models.py attaches _get_action_url to concrete plugin NetBoxModel subclasses. That helper first tries registry-aware route names and only falls back to NetBox’s generic view-name resolver if the registry route is not available.
This matters whenever public route names differ from model names.
Implications:
every model that participates in plugin surfaces should have a correct registry entry
route slugs and API basenames must be right before you trust action URLs
missing or wrong registry metadata will surface as incorrect add, edit, delete, changelog, or API links
Adding a new model: required wiring
This is the checklist to follow when you add a new data-model object.
Step 1: add the model explicitly
Add the Django model in netbox_rpki/models.py.
Also add or confirm:
Meta.orderingif needed__str__get_absolute_urlany validation or clean methods
any managers or queryset behavior
the migration
Do not dynamically generate Django models.
Step 2: decide whether the model belongs in the registry
Ask these questions:
Does it need a normal list/detail page?
Does it need add/edit/delete UI?
Does it need a standard REST API surface?
Does it need GraphQL exposure?
Does it need a standard filterset, form, and table?
If yes, it belongs in netbox_rpki/object_registry.py.
If no, keep it explicit and local.
The default for standard object families is to use the registry.
Step 3: create the ObjectSpec
Prefer build_standard_object_spec(...) when the object can use the shared pattern.
Provide:
registry_keymodelclass_prefixlabel_singularlabel_pluralapi_fieldsbrief_fieldsfilter_fieldssearch_fieldsgraphql_fieldsoptional menu metadata
optional naming overrides
optional read-only flags
Use a fully explicit ObjectSpec(...) when:
the object has legacy path compatibility requirements
the object predates the builder and has intentionally custom metadata
the standard builder would obscure an important exception
Step 4: choose the public names deliberately
For every new object, decide whether the defaults are safe for:
UI route names
UI path prefixes
API basename
GraphQL singular field name
GraphQL list field name
Override them in the spec when needed.
Do this up front. Do not wait for tests or runtime errors to tell you the defaults were unsafe.
Step 5: decide whether the object is writable or read-only
Use ui_read_only=True when the object should not expose add, edit, clone, or delete surfaces.
Use api_read_only=True when the object should not expose create, update, partial-update, or delete through the REST API.
When ui_read_only=True, the shared builder omits:
the generated form metadata
edit view class generation
delete view class generation
add buttons in navigation
The generated list, detail, and row-action surfaces now honor that metadata. This is a critical contract, not an optional cosmetic hint.
Examples of current read-only reporting families include:
IntentDerivationRunROAIntentROAIntentMatchROAReconciliationRunROAIntentResultPublishedROAResultImportedRoaAuthorizationROAChangePlanProviderSnapshotROAChangePlanItemProviderSyncRun
Step 7: decide whether it needs a rich detail page
If the generated detail page is enough, stop here.
If the object needs related tables, action buttons, custom field ordering, or code-style field rendering, add a DetailSpec in netbox_rpki/detail_specs.py and register it in DETAIL_SPEC_BY_MODEL.
Only top-level objects should usually get this treatment.
Step 8: add explicit object-specific behavior where required
The registry does not replace real behavior.
Add explicit code for things like:
custom API actions
service-layer orchestration
jobs
business validation
computed summaries
object-specific detail rendering helpers
Current examples:
routing-intent profile
runactionprovider-account
syncactionreconciliation-run
create_planaction
Custom API actions belong in netbox_rpki/api/views.py as subclasses of the generated viewsets, plus matching tests.
Step 9: add test builders and scenario support
If the object participates in registry-driven surfaces, it must be constructible in shared tests.
That usually means adding or extending:
a
create_test_*helper innetbox_rpki/tests/utils.pyscenario hooks in
netbox_rpki/tests/registry_scenarios.pyany read-only instance builder coverage used by surface-contract tests
If the object has special behavior, keep the special tests explicit. Do not bury meaningful behavior inside generic loops just to make the test file shorter.
Step 10: update documentation
If the object changes user-facing functionality, update the relevant docs at the same time.
Typical places:
README.mdCHANGELOG.mdLOCAL_DEV_SETUP.mdandtests/e2e/README.mdwhen test commands or test-lane workflows changedevrun/work_in_progress/netbox_rpki_surface_contract_checklist.mdwhen the surface contract or release gate changesthe Sphinx docs under
docs/when user-facing documentation is affected
Adding specific categories of objects
Writable top-level object
Use the standard builder with writable defaults.
Expected result:
list page
detail page
add page
edit page
delete page
menu add button if the object has navigation metadata
standard REST CRUD
GraphQL type and query fields
Read-only reporting object
Use:
ui_read_only=Trueapi_read_only=Trueshow_add_button=False
Expected result:
list page still exists
detail page still exists
no add page
no edit page
no delete page
no clone action
no row edit/delete actions
no bogus add button or
/NonelinkREST API is read-only unless you add a deliberate custom action
Supporting relation object
If the object is useful as a routed and queryable entity but should not clutter the menu, keep it in the registry with no navigation metadata.
Examples in the plugin include assignment and supporting relation models that are real objects but not top-level menu entries.
Object needing stable public names different from the model name
Keep the model name explicit and safe for Django and NetBox internals, then override the public names in the spec.
This is the pattern for RpkiProviderAccount.
Do not rename the model just to get a prettier slug.
Testing and the definition of green
The plugin now treats surface contracts as part of correctness.
“Green” does not mean only that a broad test command passed. It also means the generated surfaces actually match the registry contract.
At minimum, a new or changed object family must prove:
list-view actions match whether the object is creatable
detail-view actions match whether the object is editable and deletable
table row-action menus match whether edit and delete routes exist
API methods match read-only versus writable intent
custom actions are exposed only where intended
GraphQL fields are registered with the intended stable names
the object can be built in shared scenario-driven tests
Registry-wide contract coverage already lives in:
netbox_rpki/tests/test_views.pynetbox_rpki/tests/test_api.pynetbox_rpki/tests/test_graphql.pynetbox_rpki/tests/registry_scenarios.py
Do not add a new registry object and skip the contract tests. That is how false greens happen.
Also watch for a routine false-green pattern on generated detail pages: a page can pass shared surface tests when the fixture leaves an optional related object unset, but fail in real use once that relation is populated and the template tries to render a link.
Common example in this plugin:
an imported object detail page renders
external_referenceas a linkthe shared builder defaults
external_reference=Nonethe generic detail-view test gets HTTP 200 because it only exercises the empty-state rendering path
real synced data populates
external_reference, and the page fails at render time because the linked object has no valid UI route orget_absolute_url()path
Treat populated relation rendering as part of the surface contract. When a detail page renders any optional relation, URL field, or computed link, add at least one test that builds the object in the populated state and renders the real page. Do not rely only on empty-state fixtures or list-page smoke coverage.
Required verification habits
Run focused tests while iterating.
Run the full plugin suite before claiming the work is done.
Use non-interactive test commands only.
Treat browser coverage as a separate confirmation lane, not as a substitute for Python-level contract tests.
Known-good full-suite command:
cd /home/mencken/src/netbox_rpki/devrun
./dev.sh test full
Focused contract command used during this refactor:
cd /home/mencken/src/netbox_rpki/devrun
./dev.sh test contract
Fast structural smoke lane:
cd /home/mencken/src/netbox_rpki/devrun
./dev.sh test fast
Optional provider-backed sync/write lane:
cd /home/mencken/src/netbox_rpki/devrun
./dev.sh test provider
Use explicit test labels through the same wrapper when you want a targeted run without the normal dev bootstrap path:
cd /home/mencken/src/netbox_rpki/devrun
./dev.sh test netbox_rpki.tests.test_provider_sync --verbosity 2
For local development and browser testing, prefer the wrappers in devrun/:
./dev.sh test fast./dev.sh test contract./dev.sh test provider./dev.sh test full./dev.sh startwhen you actually need the full NetBox stack./dev.sh status./dev.sh validator./dev.sh seed./dev.sh e2e./dev.sh stop
Lessons learned
These are not abstract style preferences. They were learned by breaking the plugin and then fixing the root cause.
Do not overload one identifier to serve as registry key, URL stem, API basename, and GraphQL field name.
Do not assume NetBox defaults are safe for generated read-only objects. Explicitly control list actions, detail actions, menu buttons, and row-action menus.
Structural smoke coverage is not enough. Surface-contract tests are required.
Public names must be stable and explicit.
Generated code should be inspectable and boring. Stable named classes and exported maps are better than clever metaprogramming.
Rich detail pages should be reserved for objects that genuinely need curated related tables or action buttons.
Compatibility shims must be deliberate and narrow. Preserve public URLs intentionally rather than by accident.
Keep business logic out of surface metadata.
Do not name plugin models after existing NetBox core models. Collisions can show up in generated reverse accessors, GraphQL types, and other derived names even if routes are different.
The plugin GraphQL package must continue to export
schema; GraphQL registration depends on that package-level contract.Browser tests are useful, but they do not replace registry-wide Python contract tests.
Use non-interactive test commands. Interactive
manage.py testprompts are friction, not validation.Keep source checkouts on the Linux filesystem and use the existing WSL-native
devrun/workflow instead of ad hoc environment setup.When functionality changes, update the docs in the same slice. Code and documentation drifting apart is how future contributors reintroduce fixed bugs.
Practical decision rules
When you are unsure how to add something, follow these defaults:
Add the model and migration explicitly.
Put standard surfaces in the registry.
Keep custom workflow logic explicit.
Use explicit public naming metadata early.
Mark reporting objects read-only in both UI and API metadata.
Add a rich detail spec only when the simple generated detail page is insufficient.
Extend shared registry-driven tests in the same change.
Do not call work complete until the full plugin suite and the surface-contract expectations are both green.
Deploying
A reminder for the maintainers on how to deploy.
Make sure all your changes are committed, the changelog is updated, and the docs build and test suite pass.
Push changes to
mainto publish the Sphinx documentation site to GitHub Pages.Create and push a release tag such as
v0.1.6to build the package, publish it to PyPI, sign the artifacts, and create the GitHub release.Use the manual
Publish Release Artifactsworkflow with thetestpypitarget when you want a TestPyPI dry run before cutting a release tag.