mirror of
https://github.com/netbox-community/netbox.git
synced 2026-05-06 14:04:12 +08:00
Closes CAP-100: Adopt AI best practices
This commit is contained in:
45
.claude/skills/README.md
Normal file
45
.claude/skills/README.md
Normal 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.
|
||||
408
.claude/skills/add-model-field/SKILL.md
Normal file
408
.claude/skills/add-model-field/SKILL.md
Normal 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 ~300–500, 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`
|
||||
473
.claude/skills/add-model/SKILL.md
Normal file
473
.claude/skills/add-model/SKILL.md
Normal 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`
|
||||
92
.claude/skills/run-tests/SKILL.md
Normal file
92
.claude/skills/run-tests/SKILL.md
Normal 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
3
.gitignore
vendored
@@ -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
299
AGENTS.md
Normal 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>
|
||||
88
CLAUDE.md
88
CLAUDE.md
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user