Closes CAP-100: Adopt AI best practices

This commit is contained in:
Jeremy Stretch
2026-05-05 16:55:28 -04:00
parent 7e9eac5b87
commit be4ef40b68
7 changed files with 1321 additions and 87 deletions

45
.claude/skills/README.md Normal file
View File

@@ -0,0 +1,45 @@
# .claude/
Project-local Claude Code configuration for NetBox.
The tool-agnostic content layer for this repo is [`AGENTS.md`](../AGENTS.md) at the repo root, with its `CLAUDE.md` shim. This `.claude/` directory is the Claude-specific action layer that complements `AGENTS.md` with project-local skills, slash commands, and per-developer settings.
## Layout
- `skills/` — Project-local Claude Code skills. Each skill is its own subdirectory containing a `SKILL.md` describing what it does and when to use it. Use this for repo-specific procedures.
- `commands/` — Project-local slash commands. One Markdown file per command: `commands/<command-name>.md`. Use this for `/foo` shortcuts that only make sense in this repo.
- `settings.local.json` — Per-developer Claude Code settings (tool permissions, MCP server paths, IDE preferences). **Never committed** — this filename is in the repo's `.gitignore`.
## When to add a skill (vs. inlining in AGENTS.md or promoting upstream)
Add a skill here when:
- The procedure is repo-specific (it would not be useful in other NBL repos as-is).
- The procedure is non-trivial (more than a one-line note that fits naturally inside `AGENTS.md`).
- The procedure is a recipe an agent or engineer might re-run, not a one-off.
## When to add a slash command
Add a command here when:
- The action is something you find yourself typing the same prompt for repeatedly.
- The repo has a non-obvious workflow that benefits from a shortcut.
## Conventions
- Skill and command names use `lowercase-kebab-case`, matching the [folder naming convention in `AGENTS.md`](../AGENTS.md).
- Each skill directory has a `SKILL.md` (the entry point); supporting files (references, examples, sample data) live alongside it inside the skill's directory.
- Each command is a single Markdown file named for the slash command: `commands/<command-name>.md`.
- Skills and commands document *why* they make the choices they do — the rationale is more durable than the bare instruction.
## How to add your first skill
1. Pick a kebab-case name describing the action: e.g., `parse-linear-issues`, `render-delivery-row`.
2. `mkdir .claude/skills/<skill-name>/` and create `SKILL.md` inside it.
3. The `SKILL.md` opens with a short YAML-ish header (name, description, version) and then the prompt content.
4. Open a PR — the new directory and its `SKILL.md` are tracked once committed.
## References
- [`AGENTS.md`](../AGENTS.md) — this repo's primary agent-context file (open standard).
- [Claude Code skills documentation](https://docs.claude.com/en/docs/claude-code/skills) — what a `SKILL.md` looks like and how Claude Code resolves them.

View File

@@ -0,0 +1,408 @@
---
name: add-model-field
description: Step-by-step checklist for adding a new field to an existing NetBox model, covering all required touch points (model, migration, validation, serializer, forms, filterset, table, panel/template, search, GraphQL, tests, docs). Use when the user asks to add a field or attribute to an existing model.
---
# Adding a Field to an Existing NetBox Model
Adding a field to an existing model touches many files. The scope depends on the field type and how it will be used. Work through the checklist below in order — each section builds on the previous.
## Before You Start
Determine upfront:
- **Field type**: scalar (CharField, IntegerField, etc.), FK/M2M, GenericForeignKey, or a special type like JSONField
- **Nullable/optional?** Most new fields should be `blank=True, null=True` unless there's a strong reason otherwise
- **Searchable?** Should it appear in global search results?
- **Filterable?** Should it be exposed in the FilterSet?
- **Displayable in list view?** Should it be a column in the object table?
- **Displayable in detail view?** Should it appear in the detail panel?
## 1. Add the Field to the Model
**File:** `netbox/<app>/models/<module>.py`
```python
class MyModel(PrimaryModel):
# ... existing fields ...
new_field = models.CharField(
verbose_name=_('new field'),
max_length=100,
blank=True,
)
# FK example:
related_thing = models.ForeignKey(
to='app.RelatedModel',
on_delete=models.PROTECT,
related_name='my_models',
blank=True,
null=True,
)
```
**Special cases:**
- **GenericForeignKey**: If this is a non-unique GFK, add a composite index in `Meta`:
```python
class Meta:
indexes = (
models.Index(fields=('object_type', 'object_id')),
)
```
- **`clone_fields`**: If the field should be pre-filled when cloning an object, add it to `clone_fields` on the model class:
```python
clone_fields = ('existing_field', 'new_field')
```
- **Validation**: If the new field introduces cross-field constraints, add logic to `clean()`:
```python
def clean(self):
super().clean()
if self.new_field and not self.related_field:
raise ValidationError({'new_field': _('...')})
```
## 2. Generate the Migration
**Do NOT write migrations manually.** Tell the user to run:
```bash
python netbox/manage.py makemigrations <app> -n <short_descriptive_name> --no-header
```
Set `DEVELOPER = True` in `configuration.py` if the command is blocked.
For FK fields, also run:
```bash
python netbox/manage.py migrate
```
before continuing, so the DB is in sync for manual testing.
## 3. Update the API Serializer
**File:** `netbox/<app>/api/serializers_.py` (or the appropriate sub-file)
- **Simple field**: just add the field name to `fields` in `Meta`:
```python
class Meta:
fields = [..., 'new_field', ...]
```
- **FK field**: add a nested serializer for read, plus a writable `_id` field:
```python
related_thing = NestedRelatedThingSerializer(read_only=True)
related_thing_id = serializers.PrimaryKeyRelatedField(
queryset=RelatedModel.objects.all(),
source='related_thing',
allow_null=True,
required=False,
)
# Add both to Meta.fields
```
- **`brief_fields`**: only add to `brief_fields` if the field is truly essential for compact/nested representations.
## 4. Update Forms
There are typically up to four forms to update. Find them under `netbox/<app>/forms/`.
### 4a. Model form (create/edit) — `model_forms.py`
Add the field to the `fieldsets` tuple and to `Meta.fields`:
```python
class MyModelForm(PrimaryModelForm):
fieldsets = (
FieldSet('name', 'new_field', 'related_thing', name=_('My Model')),
...
)
class Meta:
model = MyModel
fields = ('name', 'new_field', 'related_thing', ...)
```
For FK fields, use `DynamicModelChoiceField`:
```python
related_thing = DynamicModelChoiceField(
queryset=RelatedModel.objects.all(),
required=False,
)
```
### 4b. Bulk edit form — `bulk_edit.py`
Add the field as optional (so it can be blanked):
```python
new_field = forms.CharField(required=False)
# or for FK:
related_thing = DynamicModelChoiceField(queryset=..., required=False)
nullable_fields = ('new_field', 'related_thing') # if it can be set to null
```
Add to `fieldsets` and `Meta.fields` here too.
### 4c. Bulk import form — `bulk_import.py`
If the field should be importable via CSV, add it to the import form:
```python
class MyModelImportForm(NetBoxModelImportForm):
new_field = forms.CharField(required=False)
class Meta:
model = MyModel
fields = ('name', 'new_field', ...)
```
### 4d. Filter form — `filtersets.py` (the forms version)
```python
class MyModelFilterForm(NetBoxModelFilterSetForm):
fieldsets = (
FieldSet('q', 'filter_id'),
FieldSet('new_field', 'related_thing_id', name=_('Attributes')),
)
new_field = forms.CharField(required=False)
related_thing_id = DynamicModelMultipleChoiceField(
queryset=RelatedModel.objects.all(),
required=False,
label=_('Related Thing'),
)
```
## 5. Update the FilterSet
**File:** `netbox/<app>/filtersets.py`
- **Simple scalar field**: add to `Meta.fields` if a basic exact/contains filter suffices.
- **FK field**: add both `<field>` (name lookup) and `<field>_id` (PK lookup) explicitly — do not rely on `Meta.fields` to generate them:
```python
class MyModelFilterSet(PrimaryModelFilterSet):
related_thing = django_filters.ModelMultipleChoiceFilter(
field_name='related_thing__name',
queryset=RelatedModel.objects.all(),
to_field_name='name',
label=_('Related thing (name)'),
)
related_thing_id = django_filters.ModelMultipleChoiceFilter(
queryset=RelatedModel.objects.all(),
label=_('Related thing (ID)'),
)
class Meta:
model = MyModel
fields = ('id', 'name', 'new_field', ...) # add new_field here for simple fields
```
If the field should be searchable from the search box (`q=`), add it to the `search()` method:
```python
def search(self, queryset, name, value):
return queryset.filter(
Q(name__icontains=value) |
Q(new_field__icontains=value) | # add here
...
)
```
## 6. Update the Table
**File:** `netbox/<app>/tables/<module>.py`
- **Simple field**: just add the field name to `Meta.fields`. Add to `default_columns` if it should show by default.
- **FK field** (linking to another object):
```python
related_thing = tables.Column(linkify=True)
```
Add `related_thing` to both `Meta.fields` and `default_columns` if appropriate.
- **Choice field**: display just works if the model uses `get_<field>_display()`; no custom column needed.
- **Traversed FK** (field accessed through another relation):
```python
related_thing = tables.Column(
accessor=tables.A('some_fk__related_thing'),
linkify=True,
)
```
## 7. Update the Detail View Panel
The detail view display is controlled by a panel class (not an HTML template), defined under `netbox/<app>/ui/panels.py`.
Find the panel for the model and add a new attribute declaration:
```python
from netbox.ui import attrs, panels
class MyModelPanel(panels.ObjectAttributesPanel):
existing_field = attrs.TextAttr('existing_field')
new_field = attrs.TextAttr('new_field') # simple text
related_thing = attrs.RelatedObjectAttr('related_thing', linkify=True) # FK
status = attrs.ChoiceAttr('status') # choice field with badge
is_active = attrs.BooleanAttr('is_active') # boolean
color = attrs.ColorAttr('color') # color swatch
```
**Available attr types** (from `netbox.ui.attrs`):
| Class | Use for |
|---|---|
| `TextAttr` | Plain text / CharField |
| `NumericAttr` | Numbers, optionally with a unit |
| `ChoiceAttr` | Choice fields (renders a colored badge) |
| `BooleanAttr` | Boolean fields |
| `ColorAttr` | Color hex fields |
| `RelatedObjectAttr` | Direct ForeignKey |
| `NestedObjectAttr` | ForeignKey on a nested/hierarchical model (e.g. region.parent) |
| `RelatedObjectListAttr` | ManyToMany or reverse FK list |
| `GenericForeignKeyAttr` | GenericForeignKey |
| `DateTimeAttr` | DateTimeField |
| `TimezoneAttr` | Timezone fields |
| `AddressAttr` | Address text (optionally with map link) |
| `TemplatedAttr` | Custom per-field HTML template |
| `JSONPanel` | Full JSON block (use as a panel, not attr) |
If the model uses a legacy HTML template (under `netbox/templates/<app>/`) rather than a declarative panel, add a `<tr>` row to the relevant `<table>` in that template instead.
## 8. Update the SearchIndex (if applicable)
**File:** `netbox/<app>/search.py`
If the new field should be indexed for global search, add it to the model's `SearchIndex`:
```python
@register_search
class MyModelIndex(SearchIndex):
model = models.MyModel
fields = (
('name', 100),
('new_field', 300), # add here with an appropriate weight
('description', 500),
('comments', 5000),
)
```
Weight guide: lower = higher search priority. Name fields ~100, short descriptors ~300500, long-form comments ~5000.
## 9. Update GraphQL
### Filter — `graphql/filters.py`
Add a filter field to the model's `Filter` class:
```python
@strawberry_django.filter_type(models.MyModel, lookups=True)
class MyModelFilter(PrimaryModelFilter):
# simple field (lookups=True auto-generates eq/icontains/etc.)
new_field: StrFilterLookup[str] | None = strawberry_django.filter_field()
# FK field:
related_thing: Annotated['RelatedThingFilter', strawberry.lazy('<app>.graphql.filters')] | None = strawberry_django.filter_field()
related_thing_id: ID | None = strawberry_django.filter_field()
```
### Type — `graphql/types.py`
For simple fields, `fields='__all__'` on the type decorator will pick up the new field automatically. No change needed unless:
- The field is in an `exclude` list on the type — remove it.
- The field requires a custom type annotation (e.g. a lazy FK reference or a special scalar):
```python
@strawberry_django.type(models.MyModel, fields='__all__', ...)
class MyModelType(PrimaryObjectType):
related_thing: Annotated['RelatedThingType', strawberry.lazy('<app>.graphql.types')] | None
```
> **Prefetch null failures:** If GraphQL unit tests fail citing null values on a non-nullable field, change the field definition to use `select_related`:
> ```python
> related_thing: ... = strawberry_django.field(select_related=['related_thing'])
> ```
## 10. Write Tests
### FilterSet tests — `tests/test_filtersets.py`
Add test methods for any new FilterSet fields:
```python
def test_new_field(self):
params = {'new_field': ['value1', 'value2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), expected)
def test_related_thing(self):
# Test both name and _id variants
related = RelatedModel.objects.filter(...)
params = {'related_thing_id': [related[0].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), expected)
params = {'related_thing': [related[0].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), expected)
```
Ensure `setUpTestData` creates test objects with diverse values for the new field.
### API tests — `tests/test_api.py`
- Update `setUpTestData` to populate the new field in test instances.
- Update `create_data` and (if applicable) `bulk_update_data` to include the new field.
- If the field is filterable via the API, add a `test_list_objects_by_<field>` test.
### View tests — `tests/test_views.py`
- Update `form_data` in `setUpTestData` to include the new field.
- Update `bulk_edit_data` if the field is bulk-editable.
- Update `csv_data` if the field is importable.
### Model tests — `tests/test_models.py` (if validation was added)
Add a test for any custom `clean()` logic:
```python
def test_clean_new_field_validation(self):
instance = MyModel(new_field='invalid_value', ...)
with self.assertRaises(ValidationError):
instance.clean()
```
## 11. Update Documentation
**File:** `docs/models/<app>/<modelname>.md`
Add the new field to the model's documentation page. Include:
- The field name and description
- Valid values (for choice fields)
- Any constraints or dependencies
## Summary Checklist
| # | File(s) | Action |
|---|---|---|
| 1 | `models/<module>.py` | Add field; add to `clone_fields`; add `clean()` validation |
| 2 | (user runs) | `makemigrations <app> -n <name> --no-header` |
| 3 | `api/serializers_.py` | Add field to `fields`; add nested serializer + `_id` for FK |
| 4a | `forms/model_forms.py` | Add to `fieldsets` and `Meta.fields` |
| 4b | `forms/bulk_edit.py` | Add as optional; add to `nullable_fields` if nullable |
| 4c | `forms/bulk_import.py` | Add if CSV-importable |
| 4d | `forms/filtersets.py` | Add filter field and to `fieldsets` |
| 5 | `filtersets.py` | Add to FilterSet; add FK + FK_id pair; update `search()` |
| 6 | `tables/<module>.py` | Add column; add to `Meta.fields`; update `default_columns` |
| 7 | `<app>/ui/panels.py` | Add attr to the model's panel class |
| 8 | `search.py` | Add to SearchIndex `fields` tuple with appropriate weight |
| 9 | `graphql/filters.py`, `types.py` | Add filter field; update type if excluded or needs custom annotation |
| 10 | `tests/test_*.py` | Update filterset, API, view, and model tests |
| 11 | `docs/models/<app>/<model>.md` | Document the new field |
## Common Gotchas
- **Always add `_id` variants in FilterSets** for FK fields — don't rely on `Meta.fields` to auto-generate them.
- **Migrations must be generated, not written manually.** If `makemigrations` is blocked, ensure `DEVELOPER = True` is set in `configuration.py`.
- **List views and API serializers don't need manual `prefetch_related()`** — this is handled dynamically. Only add explicit prefetches in a viewset if required for a custom endpoint.
- **`clone_fields` must be declared explicitly** on the model. Fields not in this list are not copied when cloning an object.
- **`brief_fields` on serializers is explicit** — just listing a field in `Meta.fields` does not include it in brief/nested representations.
- **Panel attrs, not HTML templates** — new models use `ObjectAttributesPanel` subclasses in `<app>/ui/panels.py`. Only fall back to editing `templates/<app>/` HTML files if the model predates the declarative layout system.
- **GraphQL `fields='__all__'`** picks up simple new fields automatically; only explicit overrides needed for FKs, excluded fields, or special scalars.
- **No `ruff format`** on existing files — use `ruff check` only.
## References
- Real example (adding FK filter field): `git show 87b17ff26` — adds `profile`/`profile_id` to the Module filterset, filter form, table, template, and tests
- Real example (adding a JSONField): `git show 5f802bb18` — adds `choice_colors` to CustomFieldChoiceSet across model, forms, filterset, serializer, GraphQL, and tests
- Panel attrs reference: `netbox/netbox/ui/attrs.py`
- Panel classes: `netbox/<app>/ui/panels.py`
- Base filterset classes: `netbox/netbox/filtersets.py`
- Contributing guide: `docs/development/extending-models.md`

View File

@@ -0,0 +1,473 @@
---
name: add-model
description: Step-by-step guide for adding a new model to NetBox, including all required components (model, filterset, serializer, views, forms, tables, GraphQL, tests, docs, navigation). Use when the user asks to add a new model or object type to NetBox.
---
# Adding a New Model to NetBox
Adding a model requires wiring up ~12 components. Work through them in order — each builds on the previous. If the user hasn't specified which app to place the model in, ask first.
## 0. Before You Start
Decide on:
- **App**: which existing app owns this model (`dcim`, `ipam`, `extras`, etc.)
- **Base class**: see the hierarchy below
- **URL slug**: the kebab-case name used in URLs (e.g. `virtual-chassis`)
- **Model name**: PascalCase (e.g. `VirtualChassis`)
- **Verbose names**: for `Meta.verbose_name` / `verbose_name_plural`
### Base Class Hierarchy
| Class | Use when |
|---|---|
| `PrimaryModel` | Real infrastructure objects with description, comments, and owner. Most new models. |
| `OrganizationalModel` | Purely organizational/grouping objects (roles, types, categories). |
| `NestedGroupModel` | Hierarchical tree objects (regions, locations). Uses MPTT. |
| `NetBoxModel` | Direct subclass of the feature set — use only when neither Primary nor Organizational fits. |
| `ChangeLoggedModel` | Lightweight ancillary objects; no custom fields, tags, etc. |
All of these live in `netbox/netbox/models/__init__.py`.
## 1. Define the Model
**File:** `netbox/<app>/models/<module>.py` (or `models.py` for smaller apps)
```python
class MyModel(PrimaryModel):
name = models.CharField(
verbose_name=_('name'),
max_length=100,
db_collation='natural_sort', # for alphabetic-aware sorting
)
some_fk = models.ForeignKey(
to='app.RelatedModel',
on_delete=models.PROTECT,
related_name='my_models',
blank=True,
null=True,
)
class Meta:
ordering = ['name']
verbose_name = _('my model')
verbose_name_plural = _('my models')
def __str__(self):
return self.name
```
- Add the model to `__all__` in the models module's `__init__.py`.
- `db_collation='natural_sort'` on name fields enables natural sort order; omit if not needed.
- Use `models.PROTECT` for FK `on_delete` unless cascade deletion is explicitly desired.
- `PrimaryModel` already provides `description`, `comments`, and `owner` — don't redeclare them.
**Do NOT run `makemigrations` yourself.** Tell the user to run:
```bash
python netbox/manage.py makemigrations
```
## 2. Define Field Choices (if needed)
**File:** `netbox/<app>/choices.py`
```python
class MyModelStatusChoices(ChoiceSet):
STATUS_ACTIVE = 'active'
STATUS_PLANNED = 'planned'
CHOICES = [
(STATUS_ACTIVE, _('Active'), 'blue'),
(STATUS_PLANNED, _('Planned'), 'cyan'),
]
```
Reference with `choices=MyModelStatusChoices` on the model field and `choices=MyModelStatusChoices.CHOICES` in forms.
## 3. Create the FilterSet
**File:** `netbox/<app>/filtersets.py`
```python
class MyModelFilterSet(PrimaryModelFilterSet):
some_fk = django_filters.ModelMultipleChoiceFilter(
field_name='some_fk__name',
queryset=RelatedModel.objects.all(),
to_field_name='name',
label=_('Related model (name)'),
)
some_fk_id = django_filters.ModelMultipleChoiceFilter(
queryset=RelatedModel.objects.all(),
label=_('Related model (ID)'),
)
class Meta:
model = MyModel
fields = ('id', 'name', 'description')
```
**Critical:** Always add both `<field>` (name/slug lookup) and `<field>_id` (PK lookup) for every FK. Do not rely on `Meta.fields` to auto-generate `_id` variants — it won't work correctly.
Match the base class to the model: `PrimaryModelFilterSet`, `OrganizationalModelFilterSet`, `NetBoxModelFilterSet`, or `ChangeLoggedModelFilterSet`.
## 4. Create Forms
**File:** `netbox/<app>/forms/model_forms.py`
```python
class MyModelForm(PrimaryModelForm):
fieldsets = (
FieldSet('name', 'some_fk', name=_('My Model')),
FieldSet('description', 'tags', name=_('Other')),
)
class Meta:
model = MyModel
fields = ('name', 'some_fk', 'description', 'owner', 'comments', 'tags')
```
**File:** `netbox/<app>/forms/filtersets.py` (for the filter form)
```python
class MyModelFilterForm(NetBoxModelFilterSetForm):
model = MyModel
fieldsets = (
FieldSet('q', 'filter_id'),
FieldSet('some_fk_id', name=_('Related')),
)
some_fk_id = DynamicModelMultipleChoiceField(
queryset=RelatedModel.objects.all(),
required=False,
label=_('Related Model'),
)
```
Export both from `netbox/<app>/forms/__init__.py`.
## 5. Create the Table
**File:** `netbox/<app>/tables/<module>.py`
```python
class MyModelTable(PrimaryModelTable):
name = tables.Column(linkify=True)
some_fk = tables.Column(linkify=True)
tags = columns.TagColumn(url_name='<app>:mymodel_list')
class Meta(PrimaryModelTable.Meta):
model = MyModel
fields = ('pk', 'id', 'name', 'some_fk', 'description', 'tags', 'created', 'last_updated')
default_columns = ('pk', 'name', 'some_fk', 'description')
```
Export from the tables package's `__init__.py`.
## 6. Add Views
**File:** `netbox/<app>/views.py`
```python
@register_model_view(MyModel, 'list', path='', detail=False)
class MyModelListView(generic.ObjectListView):
queryset = MyModel.objects.all()
table = tables.MyModelTable
filterset = filtersets.MyModelFilterSet
filterset_form = forms.MyModelFilterForm
@register_model_view(MyModel)
class MyModelView(generic.ObjectView):
queryset = MyModel.objects.all()
@register_model_view(MyModel, 'add', detail=False)
@register_model_view(MyModel, 'edit')
class MyModelEditView(generic.ObjectEditView):
queryset = MyModel.objects.all()
form = forms.MyModelForm
@register_model_view(MyModel, 'delete')
class MyModelDeleteView(generic.ObjectDeleteView):
queryset = MyModel.objects.all()
@register_model_view(MyModel, 'bulk_import', detail=False)
class MyModelBulkImportView(generic.BulkImportView):
queryset = MyModel.objects.all()
model_form = forms.MyModelImportForm
@register_model_view(MyModel, 'bulk_edit', detail=False)
class MyModelBulkEditView(generic.BulkEditView):
queryset = MyModel.objects.all()
filterset = filtersets.MyModelFilterSet
table = tables.MyModelTable
form = forms.MyModelBulkEditForm
@register_model_view(MyModel, 'bulk_delete', detail=False)
class MyModelBulkDeleteView(generic.BulkDeleteView):
queryset = MyModel.objects.all()
filterset = filtersets.MyModelFilterSet
table = tables.MyModelTable
```
The detail view (`generic.ObjectView`) uses `generic/object.html` by default. For a custom layout with panels, add a `layout` attribute:
```python
from netbox.ui.layouts import SimpleLayout
from netbox.ui.panels import CustomFieldsPanel, TagsPanel, CommentsPanel
@register_model_view(MyModel)
class MyModelView(generic.ObjectView):
queryset = MyModel.objects.all()
layout = SimpleLayout(
left_panels=[TagsPanel(), CustomFieldsPanel()],
right_panels=[CommentsPanel()],
)
```
## 7. Add URL Routes
**File:** `netbox/<app>/urls.py`
```python
from utilities.urls import get_model_urls
urlpatterns = [
# ...existing routes...
path('my-models/', include(get_model_urls('<app>', 'mymodel', detail=False))),
path('my-models/<int:pk>/', include(get_model_urls('<app>', 'mymodel'))),
]
```
`get_model_urls()` auto-generates routes for all registered views. `detail=False` covers the list/create routes; the second `path` covers detail/edit/delete routes.
## 8. REST API
### Serializer
**File:** `netbox/<app>/api/serializers_.py` (or a new file like `serializers_/mymodel.py`)
```python
class MyModelSerializer(PrimaryModelSerializer):
some_fk = NestedRelatedModelSerializer(read_only=True)
some_fk_id = serializers.PrimaryKeyRelatedField(
queryset=RelatedModel.objects.all(),
source='some_fk',
)
class Meta:
model = MyModel
fields = [
'id', 'url', 'display_url', 'display',
'name', 'some_fk', 'some_fk_id',
'description', 'owner', 'comments', 'tags', 'custom_fields',
'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
```
### ViewSet
**File:** `netbox/<app>/api/views.py`
```python
class MyModelViewSet(NetBoxModelViewSet):
queryset = MyModel.objects.prefetch_related('some_fk', 'tags')
serializer_class = serializers.MyModelSerializer
filterset_class = filtersets.MyModelFilterSet
```
### API URL Route
**File:** `netbox/<app>/api/urls.py`
```python
router.register('my-models', views.MyModelViewSet)
```
## 9. GraphQL
### Filter
**File:** `netbox/<app>/graphql/filters.py`
```python
@strawberry_django.filter_type(models.MyModel, lookups=True)
class MyModelFilter(PrimaryModelFilter):
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
some_fk: Annotated['RelatedModelFilter', strawberry.lazy('<app>.graphql.filters')] | None = strawberry_django.filter_field()
some_fk_id: ID | None = strawberry_django.filter_field()
```
Add `'MyModelFilter'` to `__all__` at the top of the file.
### Type
**File:** `netbox/<app>/graphql/types.py`
```python
@strawberry_django.type(
models.MyModel,
fields='__all__',
filters=MyModelFilter,
pagination=True,
)
class MyModelType(PrimaryObjectType):
some_fk: Annotated['RelatedModelType', strawberry.lazy('<app>.graphql.types')] | None
```
Add `'MyModelType'` to `__all__`.
### Schema
**File:** `netbox/<app>/graphql/schema.py`
```python
@strawberry.type
class MyAppQuery:
# ...existing fields...
my_model: MyModelType = strawberry_django.field()
my_model_list: list[MyModelType] = strawberry_django.field()
```
> **Note:** GraphQL unit tests may fail citing null values on a non-nullable field if related objects are prefetched. Fix by using `= strawberry_django.field(select_related=['some_fk'])` instead.
## 10. Register in Search
**File:** `netbox/<app>/search.py`
```python
@register_search
class MyModelIndex(SearchIndex):
model = models.MyModel
fields = (
('name', 100),
('description', 500),
('comments', 5000),
)
display_attrs = ('some_fk', 'description')
```
Field weights: lower = higher priority in results. Typical: name=100, description=500, comments=5000.
## 11. Add Navigation Menu Entry
**File:** `netbox/netbox/navigation/menu.py`
Find the relevant `MenuGroup` and add:
```python
get_model_item('<app>', 'mymodel', _('My Models')),
```
The model name must be lowercase (not the URL slug). This auto-links to the list view.
## 12. Add Documentation
**File:** `docs/models/<app>/<modelname>.md`
Create a new page with at minimum:
- A description of what the model represents
- A fields table
- Any relevant notes
Then add it to:
- `docs/models/<app>/index.md` (or wherever the app's models are listed)
- `mkdocs.yml` under the appropriate nav section
Also add to the model index at `docs/development/models.md`.
## 13. Write Tests
### API Tests
**File:** `netbox/<app>/tests/test_api.py`
```python
class MyModelTest(APIViewTestCases.APIViewTestCase):
model = MyModel
brief_fields = ['description', 'display', 'id', 'name', 'url']
@classmethod
def setUpTestData(cls):
# Create 3+ instances for list/bulk tests
my_models = (
MyModel(name='My Model 1', ...),
MyModel(name='My Model 2', ...),
MyModel(name='My Model 3', ...),
)
MyModel.objects.bulk_create(my_models)
cls.create_data = [
{'name': 'My Model 4', ...},
{'name': 'My Model 5', ...},
{'name': 'My Model 6', ...},
]
```
### View Tests
**File:** `netbox/<app>/tests/test_views.py`
```python
class MyModelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = MyModel
@classmethod
def setUpTestData(cls):
my_models = (
MyModel(name='My Model 1', ...),
MyModel(name='My Model 2', ...),
MyModel(name='My Model 3', ...),
)
MyModel.objects.bulk_create(my_models)
cls.form_data = {
'name': 'My Model X',
# all required form fields
}
cls.bulk_edit_data = {
'description': 'New description',
}
cls.csv_data = (
'name',
'My Model 4',
'My Model 5',
'My Model 6',
)
```
### FilterSet Tests
**File:** `netbox/<app>/tests/test_filtersets.py`
```python
class MyModelFilterSetTestCase(TestCase):
queryset = MyModel.objects.all()
filterset = MyModelFilterSet
@classmethod
def setUpTestData(cls):
# Create diverse test data
def test_name(self):
params = {'name': ['My Model 1', 'My Model 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_some_fk(self):
# Test FK and FK_id filters
```
## Common Gotchas
- **Never write migrations manually.** Always run `python netbox/manage.py makemigrations` and let Django generate them. Set `DEVELOPER = True` in `configuration.py` to enable this.
- **FK filters need explicit `_id` variants** in FilterSets. `Meta.fields` does not auto-generate them.
- **`manage.py` lives in `netbox/`**, not the repo root.
- **Brief fields** in API serializers must be declared explicitly via `brief_fields` on the `Meta` class; they are used for nested representations.
- **GraphQL null prefetch failures**: if tests fail on non-nullable fields, add `select_related=[...]` to the `strawberry_django.field()` call.
- **Template**: the detail view uses `generic/object.html` automatically via `generic.ObjectView` — no template file needed unless you are customizing the layout beyond panels.
- **`PrimaryModel`** already has `description`, `comments`, `owner`. Don't re-add them.
- **No `ruff format`** on existing files. Use ruff check only.
## References
- Model base classes: `netbox/netbox/models/__init__.py`
- Concrete example (VirtualChassis): `netbox/dcim/models/devices.py`, `netbox/dcim/filtersets.py`, `netbox/dcim/api/`, `netbox/dcim/graphql/`, `netbox/dcim/tests/`
- Contributing guide: `docs/development/adding-models.md`
- Navigation menu: `netbox/netbox/navigation/menu.py`

View File

@@ -0,0 +1,92 @@
---
name: run-tests
description: Run NetBox's Django test suite locally. Use when the user asks to run tests, run a specific test module/class/method, or verify changes pass before opening a PR.
---
# Run the NetBox test suite
NetBox uses `django.test.TestCase` (not pytest). The suite is invoked via `manage.py test` from the repo root. CI runs this exact command in `.github/workflows/ci.yml`.
## Canonical command
From the repo root, with the venv active:
```bash
NETBOX_CONFIGURATION=netbox.configuration_testing python netbox/manage.py test netbox/ --parallel
```
`--parallel` runs test processes in parallel and is used in CI. Drop it to debug failures that only appear in parallel mode.
## Prerequisites
1. PostgreSQL and Redis reachable on localhost at their default ports (credentials: `netbox`/`netbox`/`netbox`).
2. `configuration.py` in place — copy from the example and fill in DATABASE, REDIS, SECRET_KEY, ALLOWED_HOSTS. This file is gitignored and must never be committed.
3. Dependencies installed: `pip install -r requirements.txt`.
4. `NETBOX_CONFIGURATION` set to `netbox.configuration_testing` — the test config sets `DATABASES`, `REDIS`, and `PLUGINS` appropriately.
If any of these are missing, surface the gap to the user — do not silently skip.
## Useful variants
Run a single app's tests:
```bash
NETBOX_CONFIGURATION=netbox.configuration_testing python netbox/manage.py test dcim --parallel
```
Run a single module, class, or method (Django dotted-path target):
```bash
NETBOX_CONFIGURATION=netbox.configuration_testing python netbox/manage.py test dcim.tests.test_api
NETBOX_CONFIGURATION=netbox.configuration_testing python netbox/manage.py test dcim.tests.test_api.RackTestCase
NETBOX_CONFIGURATION=netbox.configuration_testing python netbox/manage.py test dcim.tests.test_api.RackTestCase.test_list_objects
```
Speed options:
- `--keepdb` — skip DB rebuild between runs (safe for most iterative work)
- `--parallel` — run tests in parallel across CPU cores (used in CI; don't combine with `--keepdb` without testing first)
- `--failfast` — stop on first failure
- `-v 2` — print each test name as it runs
## Standard test modules per app
| Module | Coverage area |
|---|---|
| `test_api.py` | REST API endpoints (CRUD, filtering, bulk operations) |
| `test_filtersets.py` | FilterSet fields and query behavior |
| `test_models.py` | Model methods, validation, constraints |
| `test_views.py` | UI views (list, create, edit, delete, bulk actions) |
| `test_forms.py` | Form validation |
| `test_tables.py` | Table column rendering |
Specialized modules in some apps: `test_cablepaths.py` (dcim), `test_lookups.py` (ipam).
## After model changes
Always generate migrations before running tests; the test DB build will fail if migrations are missing:
```bash
python netbox/manage.py makemigrations
```
Never write migrations manually — let Django generate them.
## Coverage (matches CI)
```bash
coverage run --source="netbox/" netbox/manage.py test netbox/ --parallel
coverage report --skip-covered --omit '*/migrations/*,*/tests/*'
```
## Why these choices
- **Don't substitute pytest.** The suite uses `django.test.TestCase`; switching to pytest requires `pytest-django` configured against NetBox's settings, which is not set up. Run via `manage.py test` to match CI.
- **Always set `NETBOX_CONFIGURATION`.** Without it, Django loads `configuration.py` (the production config), which likely has a different database or may not exist in dev environments.
- **`--parallel` for full-suite runs.** CI runs parallel; running without it locally can mask race conditions (rare) and is slower on multi-core machines.
## References
- [`AGENTS.md`](../../../AGENTS.md) — Testing and development sections.
- [`.github/workflows/ci.yml`](../../../.github/workflows/ci.yml) — Authoritative CI invocation.
- [`netbox/netbox/configuration_testing.py`](../../../netbox/netbox/configuration_testing.py) — Test configuration used by the runner.

3
.gitignore vendored
View File

@@ -11,6 +11,9 @@ __pycache__/
yarn-debug.log*
yarn-error.log*
# AI tooling
.claude/settings.local.json
# Documentation generated by the upgrade/build workflow
/netbox/project-static/docs/

299
AGENTS.md Normal file
View File

@@ -0,0 +1,299 @@
# NetBox
## Repository Overview
NetBox is an extensible open-source network source-of-truth application powering network automation. It manages network infrastructure data including data center infrastructure (DCIM), IP address management (IPAM), circuits, virtualization, wireless, VPNs, and more. It supports a plugin ecosystem and exposes both a REST API and GraphQL API.
NetBox is the core product maintained by NetBox Labs. The current version is 4.6 (Python 3.12+, Django 6.x).
## Tech Stack
- Python 3.12+ / Django 6.x / Django REST Framework 3.x
- PostgreSQL (required), Redis (required for caching/queuing)
- GraphQL via Strawberry, background jobs via django-rq
- django-tables2 for list views, django-filter for filtering
- drf-spectacular for OpenAPI/Swagger schema generation
- Docs: MkDocs with mkdocs-material theme (in `docs/`)
- Ruff for lint (config in `pyproject.toml`)
## Repository Map
```text
.
├── netbox/ — Django project root (run manage.py from here)
│ ├── manage.py
│ ├── netbox/ — Core settings, URLs, WSGI, plugin infrastructure
│ │ ├── settings.py — Main Django settings
│ │ ├── configuration.py — Instance configuration (gitignored)
│ │ ├── configuration_example.py — Configuration template
│ │ ├── configuration_testing.py — Test configuration
│ │ ├── urls.py — Root URL routing
│ │ ├── wsgi.py — WSGI entrypoint
│ │ ├── api/ — Core REST API infrastructure
│ │ ├── graphql/ — Core GraphQL schema
│ │ ├── models/ — Core model infrastructure (features, mixins)
│ │ ├── navigation/ — Navigation menu system
│ │ ├── plugins/ — Plugin system infrastructure
│ │ ├── registry.py — Object registry
│ │ ├── search/ — Full-text search implementation
│ │ ├── ui/ — UI utilities
│ │ └── tests/ — Core framework tests
│ ├── account/ — User account management
│ ├── circuits/ — Circuit and provider management
│ ├── core/ — Core data management (data sources, jobs)
│ ├── dcim/ — Data center infrastructure (devices, racks, cables, etc.)
│ ├── extras/ — Cross-cutting features (custom fields, tags, webhooks, scripts)
│ ├── ipam/ — IP address management (prefixes, addresses, VLANs, etc.)
│ ├── tenancy/ — Tenancy and organization
│ ├── users/ — User management and tokens
│ ├── utilities/ — Shared utilities (no models)
│ ├── virtualization/ — Virtual machines and clusters
│ ├── vpn/ — VPN tunnels and configurations
│ ├── wireless/ — Wireless LANs and links
│ ├── templates/ — Django templates (per-app subdirectories)
│ ├── static/ — Compiled static assets
│ ├── project-static/ — Source static assets
│ ├── media/ — User-uploaded media
│ └── translations/ — i18n translation files
├── docs/ — MkDocs documentation source
│ ├── administration/
│ ├── configuration/
│ ├── customization/
│ ├── development/ — Contributing guide, code style
│ ├── features/
│ ├── getting-started/
│ ├── installation/
│ ├── integrations/
│ ├── models/ — Per-model documentation (by app)
│ ├── plugins/
│ ├── reference/
│ └── release-notes/
├── scripts/ — Database management and verification scripts
├── contrib/ — Example configs (systemd, nginx, generated schemas)
├── pyproject.toml — Project metadata, ruff config
├── requirements.txt — Python dependencies
└── mkdocs.yml — Docs site configuration
```
## Architecture
### App Structure
Each Django app (account, circuits, core, dcim, extras, ipam, tenancy, users, virtualization, vpn, wireless) follows a standard layout:
```text
<app>/
├── __init__.py
├── models/ — Database models (or models.py for smaller apps)
├── migrations/ — Database migrations
├── api/
│ ├── serializers.py
│ ├── views.py — DRF viewsets
│ └── urls.py — NetBoxRouter registrations
├── forms/ — Django forms (model forms, filter forms, bulk edit, etc.)
├── tables/ — django-tables2 table definitions
├── graphql/
│ └── types.py — Strawberry GraphQL types
├── filtersets.py — django-filter FilterSets
├── choices.py — ChoiceSet subclasses
├── views.py — UI views (registered with register_model_view())
├── urls.py — URL routing
├── search.py — SearchIndex registrations
├── signals.py — Django signal definitions (where applicable)
└── tests/
├── test_api.py
├── test_filtersets.py
├── test_models.py
├── test_views.py
└── test_forms.py
```
### Views
Use `register_model_view()` to register model views by action (e.g. "add", "list", etc.). List views typically don't need to add `select_related()` or `prefetch_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`. `NetBoxModelSerializer` provides standard fields including `url`, `display`, `tags`, and `custom_fields`. drf-spectacular generates the OpenAPI schema automatically. REST API views typically don't need to add `select_related()` or `prefetch_related()` — prefetching is handled dynamically by the serializer.
### GraphQL
Strawberry types live in `<app>/graphql/types.py`. The core GraphQL schema is assembled in `netbox/netbox/graphql/`. Use Strawberry's `@strawberry.type` and `auto` field resolution, following the patterns in existing apps.
### Background Jobs
django-rq drives background task processing. Job classes live in `core/jobs.py` and app-specific `jobs.py` files. Use `JobRunner` subclasses (from `netbox.jobs`) for all background work. The `core` app exposes job status in the UI.
### Plugin System
Plugin infrastructure lives in `netbox/netbox/plugins/`. Plugins are Django apps registered in `PLUGINS` (configuration.py). The plugin API exposes stable extension points: custom models, views, navigation, template extensions, search indexes, object actions, and event rules. Internal NetBox APIs are subject to change without notice.
### Filtering
FilterSets live in `<app>/filtersets.py`, using `NetBoxModelFilterSet` as the base. Used for both UI filtering and API `?field=` params. FK filters must declare an explicit `<field>_id = ModelMultipleChoiceFilter(field_name='<field>', ...)` — don't rely on `Meta.fields` to auto-generate `_id` variants.
### Extras App
`extras` is a catch-all for cross-cutting features: custom fields, custom links, tags, webhooks/event rules, export templates, config contexts, saved filters, bookmarks, notifications, scripts, and reports. New cross-cutting features belong here. Use `FeatureQuery` for generic relations (config contexts, custom fields, tags, etc.).
## Commands
All commands run from the `netbox/` subdirectory with the venv active. There is no Makefile or Justfile; use raw commands.
| Command | What it does |
|---|---|
| `python manage.py runserver` | Start development server |
| `python manage.py test` | Run full test suite (set `NETBOX_CONFIGURATION` first — see Testing) |
| `python manage.py test --keepdb --parallel 4` | Faster test run (no DB rebuild, parallel) |
| `python manage.py test dcim.tests.test_api` | Run a single test module |
| `python manage.py makemigrations` | Generate migrations after model changes |
| `python manage.py migrate` | Apply migrations |
| `python manage.py nbshell` | NetBox-enhanced interactive shell |
| `python manage.py collectstatic` | Collect static assets |
| `ruff check` | Lint (run from repo root) |
| `mkdocs serve` | Preview documentation |
| `mkdocs build` | Build static docs site |
## Development Setup
```bash
python -m venv ~/.venv/netbox
source ~/.venv/netbox/bin/activate
pip install -r requirements.txt
# Copy and configure
cp netbox/netbox/configuration.example.py netbox/netbox/configuration.py
# Edit configuration.py: set DATABASE, REDIS, SECRET_KEY, ALLOWED_HOSTS
cd netbox/
python manage.py migrate
python manage.py runserver
```
Requires PostgreSQL and Redis on localhost at their default ports.
## Testing
Tests use `django.test.TestCase` (not pytest). Test modules mirror the app structure in `<app>/tests/`. Always set the `NETBOX_CONFIGURATION` environment variable before running tests:
```bash
export NETBOX_CONFIGURATION=netbox.configuration_testing
python manage.py test
# Faster runs
python manage.py test --keepdb --parallel 4
# Single module
python manage.py test dcim.tests.test_api
```
**Standard test modules per app:**
| Module | Coverage area |
|---|---|
| `test_api.py` | REST API endpoints (CRUD, filtering, bulk operations) |
| `test_filtersets.py` | FilterSet fields and query behavior |
| `test_models.py` | Model methods, validation, constraints |
| `test_views.py` | UI views (list, create, edit, delete, bulk actions) |
| `test_forms.py` | Form validation |
| `test_tables.py` | Table column rendering |
Additional specialized test modules exist in some apps (e.g., `test_cablepaths.py` in dcim, `test_lookups.py` in ipam).
## CI/CD
GitHub Actions workflows in `.github/workflows/`:
- **`ci.yml`** — Main CI pipeline: runs on every PR. Executes linting (ruff) and the full test suite across the supported Python version matrix.
- **`codeql.yml`** — CodeQL security scanning.
- **`claude.yml`** — Claude Code automation hook; triggers on issue/PR comments mentioning `@claude`.
- **`claude-issue-triage.yml`** — Automated issue triage via Claude AI.
- **`close-stale-issues.yml`** / **`close-incomplete-issues.yml`** — Issue hygiene automation.
- **`lock-threads.yml`** — Locks closed issue/PR threads after a period.
- **`update-translation-strings.yml`** — Extracts and updates i18n translation strings.
## Common Tasks
### Add a new model
1. Add the model to the appropriate app's `models/` directory (or create a new module imported from `models/__init__.py`). Inherit from `NetBoxModel` for full feature support (custom fields, tags, etc.).
2. Prompt the user to run `python manage.py makemigrations` — never write migrations manually.
3. Wire up the full surface area: filterset (`filtersets.py`), forms (`forms/`), table (`tables/`), serializer (`api/serializers.py`), viewset (`api/views.py`), URL routes (`api/urls.py`, `urls.py`), UI views (`views.py`), navigation, and a template under `templates/<app>/`.
4. Register a `SearchIndex` in `search.py` if the model should appear in global search.
5. Add tests covering model logic, API, filtersets, forms, and views.
### Add a REST API endpoint
1. Add the serializer to `api/serializers.py` using `NetBoxModelSerializer` for `NetBoxModel`-based models. Include a `url` field.
2. Add the viewset to `api/views.py`. For custom actions use `@action(detail=True, methods=['post'])`.
3. Register the route in `api/urls.py` via `NetBoxRouter`.
4. Ensure a corresponding `FilterSet` exists in `filtersets.py`; add explicit `<field>_id = ModelMultipleChoiceFilter(field_name='<field>', ...)` for FK filters.
5. Add an integration test in `tests/test_api.py`.
### Add a GraphQL type
1. Add a Strawberry type to `<app>/graphql/types.py`, inheriting from the appropriate base (see existing types for examples).
2. Register any new query fields in the app's GraphQL module and ensure it is included in the root schema.
3. Follow the patterns in existing apps — use `auto` fields and lazy-resolve relations.
### Add a filterset field
1. Add the field to `<app>/filtersets.py`. Use `NetBoxModelFilterSet` as the base.
2. For FK relations, add both `<field>` (name/slug lookup) and `<field>_id` (ID lookup) as explicit `ModelMultipleChoiceFilter` entries.
3. Update the filter form in `forms/filtersets.py` to expose the field in the UI.
4. Add a test in `tests/test_filtersets.py`.
### Cut a release
1. Bump `version` in `pyproject.toml`.
2. Update `docs/release-notes/`.
3. Tag and publish a GitHub release.
## Conventions and Patterns
- **Apps**: Each app owns its models, views, serializers, filtersets, forms, and tests. Don't reach across app boundaries except via FK relations and public APIs.
- **Views**: Use `register_model_view()`. List views don't need manual `select_related()`/`prefetch_related()` — the table handles it.
- **REST API**: Serializers don't need manual `select_related()`/`prefetch_related()` — handled dynamically.
- **New models**: Inherit from `NetBoxModel`; include `created` and `last_updated` fields.
- **Every UI model**: Needs model, serializer, filterset, form, table, views, URL route, and tests.
- **API serializers**: Must include a `url` field (absolute URL of the object).
- **Generic relations**: Use `FeatureQuery` for config contexts, custom fields, tags, etc.
- **FK filters**: Always add explicit `<field>_id` variants in FilterSets; don't rely on `Meta.fields`.
- **No new dependencies** without strong justification.
- **No manual migrations**: Prompt the user to run `manage.py makemigrations`.
- **No `ruff format`** on existing files — tends to introduce unnecessary style changes.
- **Linting**: Ruff config in `pyproject.toml`. Line length 120, single quotes. Enabled rules: E/W/F/I/RET/UP/RUF022. Ignored: F403, F405, RET504, UP032.
- **Extras**: Cross-cutting features (custom fields, tags, webhooks, scripts) belong in the `extras` app.
- **Plugin API**: Only documented public APIs are stable. Internal code may change without notice.
## Branch & PR Conventions
- Branch naming: `<issue-number>-short-description` (e.g., `1234-device-typerror`)
- Use the `main` branch for patch releases; `feature` tracks work for the upcoming minor/major release.
- Every PR must reference an approved GitHub issue.
- PRs must include tests for new functionality.
## Troubleshooting
- **Wrong directory for `manage.py`** — `manage.py` lives in `netbox/`, not the repo root. Always `cd netbox/` first or use the full path.
- **Wrong configuration loaded** — Set `NETBOX_CONFIGURATION=netbox.configuration_testing` for tests.
- **`configuration.py` not found** — Copy `configuration.example.py` to `configuration.py` and fill in DATABASE, REDIS, SECRET_KEY, ALLOWED_HOSTS. This file is gitignored and must never be committed.
- **Migration errors** — Never write migrations manually. Run `python manage.py makemigrations` and let Django generate them.
- **Plugin issues** — Only documented public APIs are stable. Internal NetBox code may change without notice.
## Gotchas
- `configuration.py` is gitignored — never commit it.
- `manage.py` lives in `netbox/`, NOT the repo root. Running from the wrong directory is a common mistake.
- `NETBOX_CONFIGURATION` env var controls which settings module loads; set to `netbox.configuration_testing` for tests.
- The `extras` app is a catch-all for cross-cutting features (custom fields, tags, webhooks, scripts).
- Plugins API: only documented public APIs are stable. Internal NetBox code is subject to change without notice.
- See `docs/development/` for the full contributing guide and code style details.
## References
- Documentation: [`docs/`](./docs/)
- Contributing guide: [`docs/development/`](./docs/development/)
- Release notes: [`docs/release-notes/`](./docs/release-notes/)
- Plugin development: [`docs/plugins/`](./docs/plugins/)
- NetBox Labs: <https://netboxlabs.com>

View File

@@ -1,87 +1 @@
# NetBox
Network source-of-truth and infrastructure resource modeling (IRM) tool combining DCIM and IPAM. Built on Django + PostgreSQL + Redis.
## Tech Stack
- Python 3.12+ / Django / Django REST Framework
- PostgreSQL (required), Redis (required for caching/queuing)
- GraphQL via Strawberry, background jobs via RQ
- Docs: MkDocs (in `docs/`)
## Repository Layout
- `netbox/` — Django project root; run all `manage.py` commands from here
- `netbox/netbox/` — Core settings, URLs, WSGI entrypoint
- `netbox/<app>/` — Django apps: `circuits`, `core`, `dcim`, `ipam`, `extras`, `tenancy`, `virtualization`, `wireless`, `users`, `vpn`
- `docs/` — MkDocs documentation source
- `contrib/` — Example configs (systemd, nginx, etc.) and other resources
## Development Setup
```bash
python -m venv ~/.venv/netbox
source ~/.venv/netbox/bin/activate
pip install -r requirements.txt
# Copy and configure
cp netbox/netbox/configuration.example.py netbox/netbox/configuration.py
# Edit configuration.py: set DATABASE, REDIS, SECRET_KEY, ALLOWED_HOSTS
cd netbox/
python manage.py migrate
python manage.py runserver
```
## Key Commands
All commands run from the `netbox/` subdirectory with venv active.
```bash
# Development server
python manage.py runserver
# Run full test suite
export NETBOX_CONFIGURATION=netbox.configuration_testing
python manage.py test
# Faster test runs (no DB rebuild, parallel)
python manage.py test --keepdb --parallel 4
# Migrations
python manage.py makemigrations
python manage.py migrate
# Shell
python manage.py nbshell # NetBox-enhanced shell
```
## Architecture Conventions
- **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 add `select_related()` or `prefetch_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 add `select_related()` or `prefetch_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-tables2` used for all object list views (`<app>/tables.py`).
- **Templates**: Django templates in `netbox/templates/<app>/`.
- **Tests**: Mirror the app structure in `<app>/tests/`. Use `netbox.configuration_testing` for test config.
## Coding Standards
- Follow existing Django conventions; don't reinvent patterns already present in the codebase.
- New models must include `created`, `last_updated` fields (inherit from `NetBoxModel` where appropriate).
- Every model exposed in the UI needs: model, serializer, filterset, form, table, views, URL route, and tests.
- API serializers must include a `url` field (absolute URL of the object).
- Use `FeatureQuery` for generic relations (config contexts, custom fields, tags, etc.).
- Avoid adding new dependencies without strong justification.
- Avoid running `ruff format` on existing files, as this tends to introduce unnecessary style changes.
- Don't craft Django database migrations manually: Prompt the user to run `manage.py makemigrations` instead.
## Branch & PR Conventions
- Branch naming: `<issue-number>-short-description` (e.g., `1234-device-typerror`)
- Use the `main` branch for patch releases; `feature` tracks work for the upcoming minor/major release.
- Every PR must reference an approved GitHub issue.
- PRs must include tests for new functionality.
## Gotchas
- `configuration.py` is gitignored — never commit it.
- `manage.py` lives in `netbox/`, NOT the repo root. Running from the wrong directory is a common mistake.
- `NETBOX_CONFIGURATION` env var controls which settings module loads; set to `netbox.configuration_testing` for tests.
- The `extras` app is a catch-all for cross-cutting features (custom fields, tags, webhooks, scripts).
- Plugins API: only documented public APIs are stable. Internal NetBox code is subject to change without notice.
- See `docs/development/` for the full contributing guide and code style details.
@./AGENTS.md