diff --git a/.claude/skills/README.md b/.claude/skills/README.md new file mode 100644 index 000000000..2c1021aba --- /dev/null +++ b/.claude/skills/README.md @@ -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/.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/.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//` 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. diff --git a/.claude/skills/add-model-field/SKILL.md b/.claude/skills/add-model-field/SKILL.md new file mode 100644 index 000000000..a8e0b05cb --- /dev/null +++ b/.claude/skills/add-model-field/SKILL.md @@ -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//models/.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 -n --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//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//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//filtersets.py` + +- **Simple scalar field**: add to `Meta.fields` if a basic exact/contains filter suffices. +- **FK field**: add both `` (name lookup) and `_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//tables/.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__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//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//`) rather than a declarative panel, add a `` row to the relevant `` in that template instead. + +## 8. Update the SearchIndex (if applicable) + +**File:** `netbox//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('.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('.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_` 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//.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/.py` | Add field; add to `clone_fields`; add `clean()` validation | +| 2 | (user runs) | `makemigrations -n --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/.py` | Add column; add to `Meta.fields`; update `default_columns` | +| 7 | `/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//.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 `/ui/panels.py`. Only fall back to editing `templates//` 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//ui/panels.py` +- Base filterset classes: `netbox/netbox/filtersets.py` +- Contributing guide: `docs/development/extending-models.md` diff --git a/.claude/skills/add-model/SKILL.md b/.claude/skills/add-model/SKILL.md new file mode 100644 index 000000000..a016157eb --- /dev/null +++ b/.claude/skills/add-model/SKILL.md @@ -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//models/.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//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//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 `` (name/slug lookup) and `_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//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//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//forms/__init__.py`. + +## 5. Create the Table + +**File:** `netbox//tables/.py` + +```python +class MyModelTable(PrimaryModelTable): + name = tables.Column(linkify=True) + some_fk = tables.Column(linkify=True) + tags = columns.TagColumn(url_name=':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//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//urls.py` + +```python +from utilities.urls import get_model_urls + +urlpatterns = [ + # ...existing routes... + path('my-models/', include(get_model_urls('', 'mymodel', detail=False))), + path('my-models//', include(get_model_urls('', '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//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//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//api/urls.py` + +```python +router.register('my-models', views.MyModelViewSet) +``` + +## 9. GraphQL + +### Filter + +**File:** `netbox//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('.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//graphql/types.py` + +```python +@strawberry_django.type( + models.MyModel, + fields='__all__', + filters=MyModelFilter, + pagination=True, +) +class MyModelType(PrimaryObjectType): + some_fk: Annotated['RelatedModelType', strawberry.lazy('.graphql.types')] | None +``` + +Add `'MyModelType'` to `__all__`. + +### Schema + +**File:** `netbox//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//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('', '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//.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//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//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//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//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` diff --git a/.claude/skills/run-tests/SKILL.md b/.claude/skills/run-tests/SKILL.md new file mode 100644 index 000000000..a42c68fb1 --- /dev/null +++ b/.claude/skills/run-tests/SKILL.md @@ -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. diff --git a/.gitignore b/.gitignore index 71d517354..42f47b8b0 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..6d948793b --- /dev/null +++ b/AGENTS.md @@ -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 +/ +├── __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 `/api/serializers.py`; viewsets in `/api/views.py`; URLs auto-registered in `/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 `/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 `/filtersets.py`, using `NetBoxModelFilterSet` as the base. Used for both UI filtering and API `?field=` params. FK filters must declare an explicit `_id = ModelMultipleChoiceFilter(field_name='', ...)` — 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 `/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//`. +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 `_id = ModelMultipleChoiceFilter(field_name='', ...)` for FK filters. +5. Add an integration test in `tests/test_api.py`. + +### Add a GraphQL type + +1. Add a Strawberry type to `/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 `/filtersets.py`. Use `NetBoxModelFilterSet` as the base. +2. For FK relations, add both `` (name/slug lookup) and `_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 `_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: `-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: diff --git a/CLAUDE.md b/CLAUDE.md index 5ba40d78a..285e0f5b3 100644 --- a/CLAUDE.md +++ b/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//` — 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 `/api/serializers.py`; viewsets in `/api/views.py`; URLs auto-registered in `/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 `/graphql/types.py`. -- **Filtersets**: `/filtersets.py` — used for both UI filtering and API `?filter=` params. -- **Tables**: `django-tables2` used for all object list views (`/tables.py`). -- **Templates**: Django templates in `netbox/templates//`. -- **Tests**: Mirror the app structure in `/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: `-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