├── README.md
├── images
├── step01-django-admin-plugins.png
├── step05-accesslist-form.png
├── step05-accesslist-list.png
├── step06-accesslist1.png
├── step06-accesslist2.png
├── step06-accesslistrule.png
├── step07-menu-items1.png
├── step07-menu-items2.png
├── step08-filter-form.png
├── step09-rest-api1.png
├── step09-rest-api2.png
├── step10-graphiql.png
└── step11-search-results.png
└── tutorial
├── step01-initial-setup.md
├── step02-models.md
├── step03-tables.md
├── step04-forms.md
├── step05-views.md
├── step06-templates.md
├── step07-navigation.md
├── step08-filter-sets.md
├── step09-rest-api.md
├── step10-graphql-api.md
└── step11-search.md
/README.md:
--------------------------------------------------------------------------------
1 | # NetBox Plugin Development Tutorial
2 |
3 | This guide seeks to demonstrate the process of developing a custom plugin for NetBox v3.2 or later. By following each of the prescribed steps, the reader will create from scratch a simple plugin for managing access lists in NetBox, utilizing all major components of the NetBox plugin framework.
4 |
5 | A completed copy of the demo plugin created in this tutorial is available in the [`netbox-plugin-demo`](https://github.com/netbox-community/netbox-plugin-demo) repository for reference. For your convenience, the completed code corresponding to each step in the tutorial exists as a named branch in the demo repo. For example, if you want to start fresh on step 5, simply check out the `step04-forms` branch.
6 |
7 | ### Prerequisites
8 |
9 | Before attempting to create a plugin, please assess your personal ability. Plugin authors should have reasonable proficiency in the following:
10 |
11 | * Python programming
12 | * The [Django](https://www.djangoproject.com/) framework
13 | * REST API fundamentals (where applicable)
14 | * Installing, configuring, and using NetBox
15 |
16 | ### Contents
17 |
18 | * [Step 1: Initial Setup](/tutorial/step01-initial-setup.md) :arrow_left: Start here!
19 | * [Step 2: Models](/tutorial/step02-models.md)
20 | * [Step 3: Tables](/tutorial/step03-tables.md)
21 | * [Step 4: Forms](/tutorial/step04-forms.md)
22 | * [Step 5: Views](/tutorial/step05-views.md)
23 | * [Step 6: Templates](/tutorial/step06-templates.md)
24 | * [Step 7: Navigation](/tutorial/step07-navigation.md)
25 | * [Step 8: Filter Sets](/tutorial/step08-filter-sets.md)
26 | * [Step 9: REST API](/tutorial/step09-rest-api.md)
27 | * [Step 10: GraphQL API](/tutorial/step10-graphql-api.md)
28 | * [Step 11: Search](/tutorial/step11-search.md)
29 |
30 | ### Reference
31 |
32 | * [NetBox Plugin Development Documentation](https://netbox.readthedocs.io/en/stable/plugins/development/)
33 | * [NetBox Labs Certified Plugin Program](https://github.com/netbox-community/netbox/wiki/Plugin-Certification-Program)
34 |
35 | ### Getting Help
36 |
37 | If you run into any snags working through the tutorial, please join us in the **#netbox** channel on the [NetDev Community Slack](https://netdev.chat/) for help.
38 |
39 | ### Feedback and Issues
40 |
41 | If you happen to uncover an error or discrepancy in the tutorial, please be sure to [open an issue](https://github.com/netbox-community/netbox-plugin-tutorial/issues/new/choose) so that it can be documented and fixed.
42 |
43 |
--------------------------------------------------------------------------------
/images/step01-django-admin-plugins.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-plugin-tutorial/09babadc3d983d5668a62c3ac176735a4b370f9a/images/step01-django-admin-plugins.png
--------------------------------------------------------------------------------
/images/step05-accesslist-form.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-plugin-tutorial/09babadc3d983d5668a62c3ac176735a4b370f9a/images/step05-accesslist-form.png
--------------------------------------------------------------------------------
/images/step05-accesslist-list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-plugin-tutorial/09babadc3d983d5668a62c3ac176735a4b370f9a/images/step05-accesslist-list.png
--------------------------------------------------------------------------------
/images/step06-accesslist1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-plugin-tutorial/09babadc3d983d5668a62c3ac176735a4b370f9a/images/step06-accesslist1.png
--------------------------------------------------------------------------------
/images/step06-accesslist2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-plugin-tutorial/09babadc3d983d5668a62c3ac176735a4b370f9a/images/step06-accesslist2.png
--------------------------------------------------------------------------------
/images/step06-accesslistrule.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-plugin-tutorial/09babadc3d983d5668a62c3ac176735a4b370f9a/images/step06-accesslistrule.png
--------------------------------------------------------------------------------
/images/step07-menu-items1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-plugin-tutorial/09babadc3d983d5668a62c3ac176735a4b370f9a/images/step07-menu-items1.png
--------------------------------------------------------------------------------
/images/step07-menu-items2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-plugin-tutorial/09babadc3d983d5668a62c3ac176735a4b370f9a/images/step07-menu-items2.png
--------------------------------------------------------------------------------
/images/step08-filter-form.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-plugin-tutorial/09babadc3d983d5668a62c3ac176735a4b370f9a/images/step08-filter-form.png
--------------------------------------------------------------------------------
/images/step09-rest-api1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-plugin-tutorial/09babadc3d983d5668a62c3ac176735a4b370f9a/images/step09-rest-api1.png
--------------------------------------------------------------------------------
/images/step09-rest-api2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-plugin-tutorial/09babadc3d983d5668a62c3ac176735a4b370f9a/images/step09-rest-api2.png
--------------------------------------------------------------------------------
/images/step10-graphiql.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-plugin-tutorial/09babadc3d983d5668a62c3ac176735a4b370f9a/images/step10-graphiql.png
--------------------------------------------------------------------------------
/images/step11-search-results.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-plugin-tutorial/09babadc3d983d5668a62c3ac176735a4b370f9a/images/step11-search-results.png
--------------------------------------------------------------------------------
/tutorial/step01-initial-setup.md:
--------------------------------------------------------------------------------
1 | # Step 1: Initial Setup
2 |
3 | Before we can begin work on our plugin, we must first ensure that we have a suitable development environment in place.
4 |
5 | ## Set Up the Development Environment
6 |
7 | ### Install NetBox
8 |
9 | Plugin development requires a local installation of NetBox. If you don't already have NetBox installed, please consult the [installation instructions](https://netbox.readthedocs.io/en/stable/installation/).
10 |
11 | Be sure to enable debugging in your NetBox configuration by setting `DEBUG = True`. This will ensure that static assets can be served by the development server, and return complete tracebacks whenever there's a server error.
12 |
13 | :green_circle: **Tip:** If this installation will be for development use only, it is generally necessary to complete only up to step three in the installation guide, culminating with successfully running the NetBox development server (`manage.py runserver`).
14 |
15 | :warning: **Warning:** This guide requires NetBox v3.2 or later. Attempting to use an earlier NetBox release will not work.
16 |
17 | ### Clone the git Repository
18 |
19 | Next, we'll clone the demo git repository from GitHub. First, `cd` into your preferred location (your home directory is probably fine), then clone the repo with `git clone`. We're checking out the `init` branch, which will provide us with an empty workspace to start.
20 |
21 | ```bash
22 | $ git clone --branch step00-empty https://github.com/netbox-community/netbox-plugin-demo
23 | Cloning into 'netbox-plugin-demo'...
24 | remote: Enumerating objects: 58, done.
25 | remote: Counting objects: 100% (58/58), done.
26 | remote: Compressing objects: 100% (42/42), done.
27 | remote: Total 58 (delta 12), reused 58 (delta 12), pack-reused 0
28 | Unpacking objects: 100% (58/58), done.
29 | ```
30 |
31 | :blue_square: **Note:** It isn't strictly required to clone the demo repository, but it will enable you to conveniently check out snapshots of the code as the lessons progress and overcome any hiccups.
32 |
33 | ## Plugin Configuration
34 |
35 | ### Create `__init__.py`
36 |
37 | The `PluginConfig` class holds all the information needs to know about our plugin to install it. First, we'll create a subdirectory to hold our plugin's Python code, as well as an `__init__.py` file to hold the `PluginConfig` definition.
38 |
39 | ```bash
40 | $ mkdir netbox_access_lists
41 | $ touch netbox_access_lists/__init__.py
42 | ```
43 |
44 | Next, open `__init__.py` in the text editor of your choice and import the `PluginConfig` class from NetBox at the top of the file.
45 |
46 | ```python
47 | from netbox.plugins import PluginConfig
48 | ```
49 |
50 | ### Create the PluginConfig Class
51 |
52 | We'll create a new class named `NetBoxAccessListsConfig` by subclassing `PluginConfig`. This will define all the necessary parameters that control the configuration of our plugin once installed. There are [many optional attributes](https://netbox.readthedocs.io/en/stable/plugins/development/#pluginconfig-attributes) that can be set here, but for now we only need to define a few.
53 |
54 | ```python
55 | class NetBoxAccessListsConfig(PluginConfig):
56 | name = 'netbox_access_lists'
57 | verbose_name = ' NetBox Access Lists'
58 | description = 'Manage simple ACLs in NetBox'
59 | version = '0.1'
60 | base_url = 'access-lists'
61 | ```
62 |
63 | This will be sufficient to install our plugin in NetBox later on. Finally, we need to expose this class as `config` to ensure that NetBox detects it. Add this line to the end of the file:
64 |
65 | ```python
66 | config = NetBoxAccessListsConfig
67 | ```
68 |
69 | ## Create a README
70 |
71 | It's considered best practice to always include a `README` file with any code you publish. This is a brief piece of documentation that explains your project's purpose, how to install/run it, where to find help, etc. Because this is just a learning exercise, we don't have much to say about our plugin, but go ahead and create the file anyway.
72 |
73 | Back in the project's root (one level up from `__init__.py`), create a file named `README.md` and enter the following content:
74 |
75 | ```markdown
76 | ## netbox-access-lists
77 |
78 | Manage simple access control lists in NetBox
79 | ```
80 |
81 | :green_circle: **Tip:** You'll notice that we've given our `README` file a `md` extension. This tells tools which support it to render the file as Markdown for better readability.
82 |
83 | ## Install the Plugin
84 |
85 | ### Create `setup.py`
86 |
87 | To enable the installation of our plugin into the virtual environment we created above, we'll create a simple Python setup script. In the project's root directory, create a file named `setup.py` and enter the code below.
88 |
89 | ```python
90 | from setuptools import find_packages, setup
91 |
92 | setup(
93 | name='netbox-access-lists',
94 | version='0.1',
95 | description='An example NetBox plugin',
96 | install_requires=[],
97 | packages=find_packages(),
98 | include_package_data=True,
99 | zip_safe=False,
100 | )
101 | ```
102 |
103 | :warning: **Warning:** Be sure to create `setup.py` in the project root and _not_ within the `netbox_access_lists` directory.
104 |
105 | This file will call the `setup()` function provided by Python's [`setuptools`](https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/) library to install our code. There are plenty of additional arguments that can be passed, but for our example this is sufficient.
106 |
107 | :green_circle: **Tip:** There are alternative methods for installing Python code which work just as well; feel free to use your preferred approach. Just be aware that this guide assumes the use of `setuptools` and adjust accordingly.
108 |
109 | ### Activate the Virtual Environment
110 |
111 | To ensure our plugin is accessible to the NetBox installation, we first need to activate the Python [virtual environment](https://docs.python.org/3/library/venv.html) that was created when we installed NetBox. To do this, determine the virtual environment's path (this will be `/opt/netbox/venv/` if you use the documentation's defaults) and activate it:
112 |
113 | ```bash
114 | $ source /opt/netbox/venv/bin/activate
115 | ```
116 |
117 | ### Run `setup.py`
118 |
119 | We can now install our plugin by running `setup.py`. First, make sure the virtual environment is still active, then run the following command from the project's root. The `develop` argument tells `setuptools` to create a link to our local development path instead of copying files into the virtual environment. This avoids the need to re-install the plugin every time we make a change.
120 |
121 | ```bash
122 | $ python3 setup.py develop
123 | running develop
124 | running egg_info
125 | creating netbox_access_lists.egg-info
126 | writing manifest file 'netbox_access_lists.egg-info/SOURCES.txt'
127 | writing manifest file 'netbox_access_lists.egg-info/SOURCES.txt'
128 | running build_ext
129 | ```
130 |
131 | ### Configure NetBox
132 |
133 | Finally, we need to configure NetBox to enable our new plugin. Over in the NetBox installation path, open `netbox/netbox/configuration.py` and look for the `PLUGINS` parameter; this should be an empty list. (If it's not yet defined, go ahead and create it.) Add the name of our plugin to this list:
134 |
135 | ```python
136 | # configuration.py
137 | PLUGINS = [
138 | 'netbox_access_lists',
139 | ]
140 | ```
141 |
142 | Save the file and run the NetBox development server (if not already running):
143 |
144 | ```bash
145 | $ python netbox/manage.py runserver
146 | ```
147 |
148 | You should see the development server start successfully. Open NetBox in a new browser window, log in as a superuser, and navigate to the admin UI. Under **System > Installed Plugins** you should see our plugin listed.
149 |
150 | 
151 |
152 | :green_circle: **Tip:** You can check your work at the end of each step in the tutorial by running a `git diff` against the corresponding branch. For example, at the end of step one, run `git diff remotes/origin/step01-initial-setup` to compare your work with the completed step. This will help identify any tasks you might have missed.
153 |
154 | This completes our initial setup. Now, onto the fun stuff!
155 |
156 |
157 |
158 | [Step 2: Models](/tutorial/step02-models.md) :arrow_right:
159 |
160 |
161 |
162 |
--------------------------------------------------------------------------------
/tutorial/step02-models.md:
--------------------------------------------------------------------------------
1 | # Step 2: Models
2 |
3 | In this step, we're going to define some Django models to hold our plugin's data. A model is a Python class that represents a table in the underlying PostgreSQL database; each instance of a model equates to a row in the table. We use models instead of raw SQL because interacting with Python objects is much more convenient and flexible.
4 |
5 | :blue_square: **Note:** If you skipped the previous step, run `git checkout step01-initial-setup`.
6 |
7 | ## Create the Models
8 |
9 | First, `cd` into the `netbox_access_lists` directory and create a file named `models.py`. This is where our model classes will be defined.
10 |
11 | ```bash
12 | $ cd netbox_access_lists
13 | $ edit models.py
14 | ```
15 |
16 | At the top of the file, import Django's `models` library and NetBox's `NetBoxModel` class. The latter will serve as the base class for our plugin's models. We'll also import the PostgreSQL `ArrayField`; more on this in a bit.
17 |
18 | ```python
19 | from django.contrib.postgres.fields import ArrayField
20 | from django.db import models
21 | from netbox.models import NetBoxModel
22 | ```
23 |
24 | We'll create two models:
25 |
26 | * `AccessList`: This will represent an access list, with a name and one or more rules assigned to it.
27 | * `AccessListRule`: This will be an individual rule with source/destination IP addresses, port numbers, etc. assigned to an access list.
28 |
29 | ### AccessList
30 |
31 | We'll need to define a few fields for our model. Each model gets a numeric primary key field (`id`) automatically, so we don't need to worry about that, but we do need to define fields for the ACL's name, default action, and optional comments.
32 |
33 | ```python
34 | class AccessList(NetBoxModel):
35 | name = models.CharField(
36 | max_length=100
37 | )
38 | default_action = models.CharField(
39 | max_length=30
40 | )
41 | comments = models.TextField(
42 | blank=True
43 | )
44 | ```
45 |
46 | By default, model instances are ordered by their primary keys, but it would make more sense to order access lists by name. We can do that by creating a `Meta` child class and defining an `ordering` variable. (Be sure to create the `Meta` class *inside* `AccessList`, not after it.)
47 |
48 | ```python
49 | class Meta:
50 | ordering = ('name',)
51 | ```
52 |
53 | Finally, we'll add a `__str__()` method to control how an instance is coerced to a string. We'll have this return the value of the instance's `name` field. (Again, be sure to create this method *inside* the `AccessList` class.)
54 |
55 | ```python
56 | def __str__(self):
57 | return self.name
58 | ```
59 |
60 | ### AccessListRule
61 |
62 | Our second model will hold the individual rules assigned to each access list. This model will be a bit more complex. We'll need to define fields for all of the following:
63 |
64 | * Parent access list (a foreign key to an `AccessList` instance)
65 | * Index (the rule's order in the list)
66 | * Protocol
67 | * Source prefix
68 | * Source port(s)
69 | * Destination prefix
70 | * Destination port(s)
71 | * Action (permit, allow, or reject)
72 | * Description (optional)
73 |
74 | Let's start by defining a `ForeignKey` field pointing to the `AccessList` model.
75 |
76 | ```python
77 | class AccessListRule(NetBoxModel):
78 | access_list = models.ForeignKey(
79 | to=AccessList,
80 | on_delete=models.CASCADE,
81 | related_name='rules'
82 | )
83 | ```
84 |
85 | We're passing three keyword arguments to the field:
86 |
87 | * `to` references the related model class
88 | * `on_delete` tells Django what action to take if the related object is deleted. `CASCADE` will automatically delete any rules assigned to a deleted access list.
89 | * `related_name` defines the attribute of the reverse relationship being added to the related class. The rules assigned to an `AccessList` instance can be referenced as `accesslist.rules.all()`.
90 |
91 | Next we'll add an `index` field to store the rule's number (position) within the access list. We'll use `PositiveIntegerField` because only positive numbers are supported.
92 |
93 | ```python
94 | index = models.PositiveIntegerField()
95 | ```
96 |
97 | The protocol field is next. This will store the name of a protocol such as TCP or UDP. Notice that we're setting `blank=True` because it should not be required to specify a particular protocol when creating a rule.
98 |
99 | ```python
100 | protocol = models.CharField(
101 | max_length=30,
102 | blank=True
103 | )
104 | ```
105 |
106 | Next we need to define a source prefix. We're going to use a foreign key field to reference an instance of NetBox's `Prefix` model within its `ipam` app. Instead of importing the model class, we can instead reference it by name. And because we want this to be an _optional_ field, we'll also set `blank=True` and `null=True`.
107 |
108 | ```python
109 | source_prefix = models.ForeignKey(
110 | to='ipam.Prefix',
111 | on_delete=models.PROTECT,
112 | related_name='+',
113 | blank=True,
114 | null=True
115 | )
116 | ```
117 |
118 | :green_circle: **Tip:** Whereas `CASCADE` automatically deletes child objects, `PROTECT` prevents the deletion of the parent option if any child objects exist.
119 |
120 | Notice above that we've defined `related_name='+'`. This tells Django not to create a reverse relationship from the `Prefix` model to the `AccessListRule` model, because it wouldn't be very useful.
121 |
122 | We also need to add a field for the source port number(s). We could use an integer field for this, however that would limit us to defining a single source port per rule. Instead, we can add an `ArrayField` to store a list of `PositiveIntegerField` values. Like `source_prefix`, this will also be an optional field, so we add `blank=True` and `null=True` as well.
123 |
124 | ```python
125 | source_ports = ArrayField(
126 | base_field=models.PositiveIntegerField(),
127 | blank=True,
128 | null=True
129 | )
130 | ```
131 |
132 | Let's go ahead an add destination prefix and port fields as well. These are essentially duplicates of our source fields.
133 |
134 | ```python
135 | destination_prefix = models.ForeignKey(
136 | to='ipam.Prefix',
137 | on_delete=models.PROTECT,
138 | related_name='+',
139 | blank=True,
140 | null=True
141 | )
142 | destination_ports = ArrayField(
143 | base_field=models.PositiveIntegerField(),
144 | blank=True,
145 | null=True
146 | )
147 | ```
148 |
149 | Finally, we'll add fields for the rule's action and description. The action is required but a description is not.
150 |
151 | ```python
152 | action = models.CharField(
153 | max_length=30
154 | )
155 | description = models.CharField(
156 | max_length=500,
157 | blank=True
158 | )
159 | ```
160 |
161 | With our fields out of the way, this model will also need a `Meta` class to define database ordering and to ensure that every rule has a unique index number within its parent access list.
162 |
163 | ```python
164 | class Meta:
165 | ordering = ('access_list', 'index')
166 | unique_together = ('access_list', 'index')
167 | ```
168 |
169 | Finally, we'll add a `__str__()` method to display the parent access list and index number when rendering an `AccessListRule` instance as a string:
170 |
171 | ```python
172 | def __str__(self):
173 | return f'{self.access_list}: Rule {self.index}'
174 | ```
175 |
176 | ## Define Field Choices
177 |
178 | Looking back at our models, we see a few fields that would benefit from having pre-defined choices from which a user can select when creating or modifying an instance. Specifically, we expect a rule's `action` field to only ever have one of three values:
179 |
180 | * Permit
181 | * Deny
182 | * Reject
183 |
184 | We can define a `ChoiceSet` to store these pre-defined values for the user, to avoid the hassle of manually typing the name of the desired action each time. Back at the top of `models.py`, import NetBox's `ChoiceSet` class:
185 |
186 | ```python
187 | from utilities.choices import ChoiceSet
188 | ```
189 |
190 | Then, below the import statements but above the model definitions, create a child class named `ActionChoices`:
191 |
192 | ```python
193 | class ActionChoices(ChoiceSet):
194 | key = 'AccessListRule.action'
195 |
196 | CHOICES = [
197 | ('permit', 'Permit', 'green'),
198 | ('deny', 'Deny', 'red'),
199 | ('reject', 'Reject (Reset)', 'orange'),
200 | ]
201 | ```
202 |
203 | The `CHOICES` attribute must be an iterable of two- or three-value tuples, each of which defines the following:
204 |
205 | * The raw value to be stored in the database
206 | * A human-friendly string for display
207 | * A color for display in the UI (optional, see [available colors](https://docs.netbox.dev/en/stable/configuration/data-validation/#field_choices))
208 |
209 | Additionally, we've added a `key` attribute: This will allow the NetBox administrator to replace or extend the plugin's default choices via NetBox's [`FIELD_CHOICES`](https://netbox.readthedocs.io/en/stable/configuration/optional-settings/#field_choices) configuration parameter.
210 |
211 | Now, we can reference this as the set of valid choices on the `default_action` and `action` model fields by passing it as the `choices` keyword argument.
212 |
213 | ```python
214 | # AccessList
215 | default_action = models.CharField(
216 | max_length=30,
217 | choices=ActionChoices
218 | )
219 |
220 | # AccessListRule
221 | action = models.CharField(
222 | max_length=30,
223 | choices=ActionChoices
224 | )
225 | ```
226 |
227 | Let's create a set of choices for a rule's `protocol` field as well. Add this below the `ActionChoices` class:
228 |
229 | ```python
230 | class ProtocolChoices(ChoiceSet):
231 |
232 | CHOICES = [
233 | ('tcp', 'TCP', 'blue'),
234 | ('udp', 'UDP', 'orange'),
235 | ('icmp', 'ICMP', 'purple'),
236 | ]
237 | ```
238 |
239 | Then, add the `choices` keyword argument to the `protocol` field:
240 |
241 | ```python
242 | # AccessListRule
243 | protocol = models.CharField(
244 | max_length=30,
245 | choices=ProtocolChoices,
246 | blank=True
247 | )
248 | ```
249 |
250 | ### Add Choice Color Methods
251 |
252 | Now that we've defined choices for some of our model fields, we'll need to provide a method for returning the appropriate color for a selected choice. This works similar to Django's `get_FOO_display()` methods, but returns a color (defined on the field's `ChoiceSet`) rather than a label. This method will be called e.g. when displaying the field in a table.
253 |
254 | Let's add a `get_default_action_color()` method on `AccessList`:
255 |
256 | ```python
257 | class AccessList(NetBoxModel):
258 | # ...
259 | def get_default_action_color(self):
260 | return ActionChoices.colors.get(self.default_action)
261 | ```
262 |
263 | We also need to add methods for `protocol` and `action` on `AccessListRule`:
264 |
265 | ```python
266 | class AccessListRule(NetBoxModel):
267 | # ...
268 | def get_protocol_color(self):
269 | return ProtocolChoices.colors.get(self.protocol)
270 |
271 | def get_action_color(self):
272 | return ActionChoices.colors.get(self.action)
273 | ```
274 |
275 | ## Create Schema Migrations
276 |
277 | Now that we have our models defined, we need to generate a schema for the PostgreSQL database. While it's possible to create the tables and constraints by hand, it's _much_ easier to employ Django's [migrations feature](https://docs.djangoproject.com/en/4.0/topics/migrations/). This will inspect our model classes and generate the necessary migration files automatically. This is a two-step process: First we generate the migration file with the `makemigrations` management command, then we run `migrate` to apply it to the live database.
278 |
279 | :warning: **Warning:** Before continuing, check that you've set `DEVELOPER=True` in NetBox's `configuration.py` file. This is necessary to disable a safeguard intended to prevent people from creating new migrations mistakenly.
280 |
281 | ### Generate Migration Files
282 |
283 | Change into the NetBox installation root to run `manage.py`. First, we'll run `makemigrations` with the `--dry-run` argument as a sanity-check. This will report what changes have been detected, but won't actually generate any migration files.
284 |
285 | ```bash
286 | $ python netbox/manage.py makemigrations netbox_access_lists --dry-run
287 | Migrations for 'netbox_access_lists':
288 | ~/netbox-plugin-demo/netbox_access_lists/migrations/0001_initial.py
289 | - Create model AccessList
290 | - Create model AccessListRule
291 | ```
292 |
293 | We should see a plan to create our plugin's first migration file, `0001_initial.py`, with the two models we defined in `models.py`. (If you encounter an error at this point, or don't see the output above, **stop here** and review your work.) If everything looks good, proceed with creating the migration file (omitting the `--dry-run` argument):
294 |
295 | ```bash
296 | $ python netbox/manage.py makemigrations netbox_access_lists
297 | Migrations for 'netbox_access_lists':
298 | ~/netbox-plugin-demo/netbox_access_lists/migrations/0001_initial.py
299 | - Create model AccessList
300 | - Create model AccessListRule
301 | ```
302 |
303 | Back in your plugin workspace, you should now see a `migrations` directory with two files: `__init__.py` and `0001_initial.py`.
304 |
305 | ```bash
306 | $ tree
307 | .
308 | ├── __init__.py
309 | ├── migrations
310 | │ ├── 0001_initial.py
311 | │ ├── __init__.py
312 | ...
313 | ```
314 |
315 | ### Apply Migrations
316 |
317 | Finally, we can apply the migration file using the `migrate` management command:
318 |
319 | ```bash
320 | $ python netbox/manage.py migrate
321 | Operations to perform:
322 | Apply all migrations: admin, auth, circuits, contenttypes, dcim, django_rq, extras, ipam, netbox_access_lists, sessions, social_django, taggit, tenancy, users, virtualization, wireless
323 | Running migrations:
324 | Applying netbox_access_lists.0001_initial... OK
325 | ```
326 |
327 | If you're curious, you can inspect the newly created database tables, using the `dbshell` management command to enter a PostgreSQL shell:
328 |
329 | ```bash
330 | $ python netbox/manage.py dbshell
331 | psql (10.19 (Ubuntu 10.19-0ubuntu0.18.04.1))
332 | SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
333 | Type "help" for help.
334 |
335 | netbox=> \d netbox_access_lists_accesslist
336 | Table "public.netbox_access_lists_accesslist"
337 | Column | Type | Collation | Nullable | Default
338 | -------------------+--------------------------+-----------+----------+------------------------------------------------------------
339 | id | bigint | | not null | nextval('netbox_access_lists_accesslist_id_seq'::regclass)
340 | created | timestamp with time zone | | |
341 | last_updated | timestamp with time zone | | |
342 | custom_field_data | jsonb | | not null |
343 | name | character varying(100) | | not null |
344 | default_action | character varying(30) | | not null |
345 | comments | text | | not null |
346 | Indexes:
347 | "netbox_access_lists_accesslist_pkey" PRIMARY KEY, btree (id)
348 | Referenced by:
349 | TABLE "netbox_access_lists_accesslistrule" CONSTRAINT "netbox_access_lists__access_list_id_6c1b0317_fk_netbox_ac" FOREIGN KEY (access_list_id) REFERENCES netbox_access_lists_accesslist(id) DEFERRABLE INITIALLY DEFERRED
350 | ```
351 |
352 | Type `\q` to exit `dbshell`.
353 |
354 | ## Create Some Objects
355 |
356 | Now that we have our models installed, let's try creating some objects. First, enter the NetBox shell. This is an interactive Python command line interface which allows us to interact directly with NetBox objects and other resources.
357 |
358 | ```bash
359 | $ python netbox/manage.py nbshell
360 | from netbox### NetBox interactive shell
361 | ### Python 3.8.12 | Django 4.0.3 | NetBox 3.2.0
362 | ### lsmodels() will show available models. Use help() for more info.
363 | >>>
364 | ```
365 |
366 | Let's create and save an access list:
367 |
368 | ```python
369 | >>> from netbox_access_lists.models import *
370 | >>> acl = AccessList(name='MyACL1', default_action='deny')
371 | >>> acl
372 |
373 | >>> acl.save()
374 | ```
375 |
376 | Next we'll create some prefixes to reference in rules:
377 |
378 | ```python
379 | >>> prefix1 = Prefix(prefix='192.168.1.0/24')
380 | >>> prefix1.save()
381 | >>> prefix2 = Prefix(prefix='192.168.2.0/24')
382 | >>> prefix2.save()
383 | ```
384 |
385 | And finally we'll create a couple rules for our access list:
386 |
387 | ```python
388 | >>> AccessListRule(
389 | ... access_list=acl,
390 | ... index=10,
391 | ... protocol='tcp',
392 | ... destination_prefix=prefix1,
393 | ... destination_ports=[80, 443],
394 | ... action='permit',
395 | ... description='Web traffic'
396 | ... ).save()
397 | >>> AccessListRule(
398 | ... access_list=acl,
399 | ... index=20,
400 | ... protocol='udp',
401 | ... destination_prefix=prefix2,
402 | ... destination_ports=[53],
403 | ... action='permit',
404 | ... description='DNS'
405 | ... ).save()
406 | >>> acl.rules.all()
407 | , ]>
408 | ```
409 |
410 | Excellent! We can now create access lists and rules in the database. The next few steps will work on exposing this functionality in the NetBox user interface.
411 |
412 |
413 |
414 | :arrow_left: [Step 1: Initial Setup](/tutorial/step01-initial-setup.md) | [Step 3: Tables](/tutorial/step03-tables.md) :arrow_right:
415 |
416 |
417 |
418 |
--------------------------------------------------------------------------------
/tutorial/step03-tables.md:
--------------------------------------------------------------------------------
1 | # Step 3: Tables
2 |
3 | You're probably familiar with object lists in NetBox. This is how we display all the instances of a certain type of object, such as sites or devices, in the user interface. These lists are generated by table classes defined for each model, utilizing the [django-tables2](https://django-tables2.readthedocs.io/) library.
4 |
5 | While it would be feasible to generate raw HTML for `` elements directly within the template, this would be cumbersome and difficult to maintain. Additionally, these dynamic table classes provide convenient functionality like sorting and pagination.
6 |
7 | :blue_square: **Note:** If you skipped the previous step, run `git checkout step02-models`.
8 |
9 | ## Create the Tables
10 |
11 | We'll create two tables, one for each of our models. Begin by creating `tables.py` in the `netbox_access_lists/` directory.
12 |
13 | ```bash
14 | $ cd netbox_access_lists/
15 | $ edit tables.py
16 | ```
17 |
18 | At the top of this file, import the `django-tables2` library. This will provide the column classes for fields we wish to customize. We'll also import NetBox's `NetBoxTable` class, which will serve as the base class for our tables, and `ChoiceFieldColumn`. Finally we import our plugin's models from `models.py`.
19 |
20 | ```python
21 | import django_tables2 as tables
22 |
23 | from netbox.tables import NetBoxTable, ChoiceFieldColumn
24 | from .models import AccessList, AccessListRule
25 | ```
26 |
27 | ### AccessListTable
28 |
29 | Create a class named `AccessListTable` as a subclass of `NetBoxTable`. Within this class, create a child `Meta` class inheriting from `NetBoxTable.Meta`; this will define the table's model, fields, and default columns.
30 |
31 | ```python
32 | class AccessListTable(NetBoxTable):
33 |
34 | class Meta(NetBoxTable.Meta):
35 | model = AccessList
36 | fields = ('pk', 'id', 'name', 'default_action', 'comments', 'actions')
37 | default_columns = ('name', 'default_action')
38 | ```
39 |
40 | The `model` attribute tells `django-tables2` which model to use when building the table, and the `fields` attribute dictates which model fields get added to the table. `default_columns` controls which of the available columns are displayed by default.
41 |
42 | The `pk` and `actions` columns render the checkbox selectors and dropdown menus, respectively, for each table row; these are provided by the NetBoxTable class. The `id` column will display the object's numeric primary key, which is included on almost every table in NetBox but generally disabled by default. The other three columns derive from the fields we defined on the `AccessList` model.
43 |
44 | What we have so far is sufficient to render a table, but we can make some small improvements. First, let's make the `name` column a link to each object. To do this, we'll override the default column by defining `name` on the class and passing `linkify=True`.
45 |
46 | ```python
47 | class AccessListTable(NetBoxTable):
48 | name = tables.Column(
49 | linkify=True
50 | )
51 | ```
52 |
53 | Also, recall that the `default_action` field on the `AccessList` model is a choice field, with a color assigned to each choice. To display these values, we'll use NetBox's `ChoiceFieldColumn` class.
54 |
55 | ```python
56 | default_action = ChoiceFieldColumn()
57 | ```
58 |
59 | It would also be nice to include a count showing the number of rules each access list has assigned to it. We can add a custom column named `rule_count` to show this. (The data for this column will be annotated by the view; more on this in step five.) We'll also need to add this column to our `fields` and (optionally) `default_columns` under the `Meta` subclass. Our finished table should look like this:
60 |
61 | ```python
62 | class AccessListTable(NetBoxTable):
63 | name = tables.Column(
64 | linkify=True
65 | )
66 | default_action = ChoiceFieldColumn()
67 | rule_count = tables.Column()
68 |
69 | class Meta(NetBoxTable.Meta):
70 | model = AccessList
71 | fields = ('pk', 'id', 'name', 'rule_count', 'default_action', 'comments', 'actions')
72 | default_columns = ('name', 'rule_count', 'default_action')
73 | ```
74 |
75 | ### AccessListRuleTable
76 |
77 | We'll also create a table for our `AccessListRule` model using the same approach as above. Start by linkifying the `access_list` and `index` columns. The former will link to the parent access list, and the latter will link to the individual rule. We also want to declare `protocol` and `action` as `ChoiceFieldColumn` instances.
78 |
79 | ```python
80 | class AccessListRuleTable(NetBoxTable):
81 | access_list = tables.Column(
82 | linkify=True
83 | )
84 | index = tables.Column(
85 | linkify=True
86 | )
87 | protocol = ChoiceFieldColumn()
88 | action = ChoiceFieldColumn()
89 |
90 | class Meta(NetBoxTable.Meta):
91 | model = AccessListRule
92 | fields = (
93 | 'pk', 'id', 'access_list', 'index', 'source_prefix', 'source_ports', 'destination_prefix',
94 | 'destination_ports', 'protocol', 'action', 'description', 'actions',
95 | )
96 | default_columns = (
97 | 'access_list', 'index', 'source_prefix', 'source_ports', 'destination_prefix',
98 | 'destination_ports', 'protocol', 'action', 'actions',
99 | )
100 | ```
101 |
102 | This should be all we need to list these objects in the UI. Next, we'll define some forms to enable creating and modifying objects.
103 |
104 |
105 |
106 | :arrow_left: [Step 2: Models](/tutorial/step02-models.md) | [Step 4: Forms](/tutorial/step04-forms.md) :arrow_right:
107 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/tutorial/step04-forms.md:
--------------------------------------------------------------------------------
1 | # Step 4: Forms
2 |
3 | Form classes generate HTML form elements for the user interface, and process and validate user input. They are used in NetBox primarily to create, modify, and delete objects. We'll create a form class for each of our plugin's models.
4 |
5 | :blue_square: **Note:** If you skipped the previous step, run `git checkout step03-tables`.
6 |
7 | ## Create the Forms
8 |
9 | Begin by creating a file named `forms.py` in the `netbox_access_lists/` directory.
10 |
11 | ```bash
12 | $ cd netbox_access_lists/
13 | $ edit forms.py
14 | ```
15 |
16 | At the top of the file, we'll import NetBox's `NetBoxModelForm` class, which will serve as the base class for our forms. We'll also import our plugin's models.
17 |
18 | ```python
19 | from netbox.forms import NetBoxModelForm
20 | from .models import AccessList, AccessListRule
21 | ```
22 |
23 | ### AccessListForm
24 |
25 | Create a class named `AccessListForm`, subclassing `NetBoxModelForm`. Under this class, define a `Meta` subclass defining the form's `model` and `fields`. Notice that the `fields` list also includes `tags`: Tag assignment is handled by `NetBoxModel` automatically, so we didn't need to add it to our model in step two.
26 |
27 | ```python
28 | class AccessListForm(NetBoxModelForm):
29 |
30 | class Meta:
31 | model = AccessList
32 | fields = ('name', 'default_action', 'comments', 'tags')
33 | ```
34 |
35 | This alone is sufficient for our first model, but we can make one tweak: Instead of the default field that Django will generate for the `comments` model field, we can use NetBox's purpose-built `CommentField` class. (This handles some largely cosmetic details like setting a `help_text` and adjusting the field's layout.) To do this, simply import the `CommentField` class and override the form field:
36 |
37 | ```python
38 | from utilities.forms.fields import CommentField
39 | # ...
40 | class AccessListForm(NetBoxModelForm):
41 | comments = CommentField()
42 |
43 | class Meta:
44 | model = AccessList
45 | fields = ('name', 'default_action', 'comments', 'tags')
46 | ```
47 |
48 | ### AccessListRuleForm
49 |
50 | We'll create a form for `AccessListRule` following the same pattern.
51 |
52 | ```python
53 | class AccessListRuleForm(NetBoxModelForm):
54 |
55 | class Meta:
56 | model = AccessListRule
57 | fields = (
58 | 'access_list', 'index', 'description', 'source_prefix', 'source_ports', 'destination_prefix',
59 | 'destination_ports', 'protocol', 'action', 'tags',
60 | )
61 | ```
62 |
63 | By default, Django will create a "static" foreign key field for related objects. This renders as a dropdown list that's pre-populated with _all_ available objects. As you can imagine, in a NetBox instance with many thousands of objects this can get rather unwieldy.
64 |
65 | To avoid this, NetBox provides the `DynamicModelChoiceField` class. This renders foreign key fields using a special dynamic widget backed by NetBox's REST API. This avoids the overhead imposed by the static field, and allows the user to conveniently search for the desired object.
66 |
67 | :green_circle: **Tip:** The `DynamicModelMultipleChoiceField` class is also available for many-to-many fields, which support the assignment of multiple objects.
68 |
69 | We'll use `DynamicModelChoiceField` for the three foreign key fields in our form: `access_list`, `source_prefix`, and `destination_prefix`. First, we must import the field class, as well as the models of the related objects. `AccessList` is already imported, so we just need to import `Prefix` from NetBox's `ipam` app. The beginning of `forms.py` should now look like this:
70 |
71 | ```python
72 | from ipam.models import Prefix
73 | from netbox.forms import NetBoxModelForm
74 | from utilities.forms.fields import CommentField, DynamicModelChoiceField
75 | from .models import AccessList, AccessListRule
76 | ```
77 |
78 | Then, we override the three relevant fields on the form class, instantiating `DynamicModelChoiceField` with the appropriate `queryset` value for each. (Be sure to keep in place the `Meta` class we already defined.)
79 |
80 | ```python
81 | class AccessListRuleForm(NetBoxModelForm):
82 | access_list = DynamicModelChoiceField(
83 | queryset=AccessList.objects.all()
84 | )
85 | source_prefix = DynamicModelChoiceField(
86 | queryset=Prefix.objects.all()
87 | )
88 | destination_prefix = DynamicModelChoiceField(
89 | queryset=Prefix.objects.all()
90 | )
91 | ```
92 |
93 | With our models, tables, and forms all in place, next we'll create some views to bring everything together!
94 |
95 |
96 |
97 | :arrow_left: [Step 3: Tables](/tutorial/step03-tables.md) | [Step 5: Views](/tutorial/step05-views.md) :arrow_right:
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/tutorial/step05-views.md:
--------------------------------------------------------------------------------
1 | # Step 5: Views
2 |
3 | Views are responsible for the business logic of your application. Generally, this means processing incoming requests, performing some action(s), and returning a response to the client. Each view typically has a URL associated with it, and can handle one or more types of HTTP requests (i.e. `GET` and/or `POST` requests).
4 |
5 | Django provides a set of [generic view classes](https://docs.djangoproject.com/en/4.0/topics/class-based-views/generic-display/) which handle much of the boilerplate code needed to process requests. NetBox likewise provides a set of view classes to simplify the creation of views for creating, editing, deleting, and viewing objects. They also introduce support for NetBox-specific features such as custom fields and change logging.
6 |
7 | In this step, we'll create a set of views for each of our plugin's models.
8 |
9 | :blue_square: **Note:** If you skipped the previous step, run `git checkout step04-forms`.
10 |
11 | ## Create the Views
12 |
13 | Begin by creating `views.py` in the `netbox_access_lists/` directory.
14 |
15 | ```bash
16 | $ cd netbox_access_lists/
17 | $ edit views.py
18 | ```
19 |
20 | We'll need to import our plugin's `models`, `tables`, and `forms` modules: This is where everything we've built so far really comes together! We also need to import NetBox's generic views module, as it provides the base classes for our views.
21 |
22 | ```python
23 | from netbox.views import generic
24 | from . import forms, models, tables
25 | ```
26 |
27 | :green_circle: **Tip:** You'll notice that we're importing the entire model, form, and tables modules here. If you would prefer to import each of the relevant classes directly, you're certainly welcome to do so; just remember to change the class definitions below accordingly.
28 |
29 | For each model, we need to create four views:
30 |
31 | * **Detail view** - Display a single object
32 | * **List view** - Displays a table of all existing instances of a particular model
33 | * **Edit view** - Handles adding and modifying objects
34 | * **Delete view** - Handles the deletion of an object
35 |
36 | ### AccessList Views
37 |
38 | The general pattern we'll follow here is to subclass a generic view class provided by NetBox, and define the necessary attributes. We won't need to write any substantial code because the views NetBox provides takes care of the request logic for us.
39 |
40 | Let's start with a detail view. We subclass `generic.ObjectView` and define the queryset of objects we want to display.
41 |
42 | ```python
43 | class AccessListView(generic.ObjectView):
44 | queryset = models.AccessList.objects.all()
45 | ```
46 |
47 | :green_circle: **Tip:** The views require us to define a queryset rather than just a model, because it's sometimes necessary to modify the queryset, e.g. to prefetch related objects or limit by a particular attribute.
48 |
49 | Next, we'll add a list view. For this view, we need to define both `queryset` and `table`.
50 |
51 | ```python
52 | class AccessListListView(generic.ObjectListView):
53 | queryset = models.AccessList.objects.all()
54 | table = tables.AccessListTable
55 | ```
56 |
57 | :green_circle: **Tip:** It occurs to the author that having chosen a model name that ends with "List" might be a bit confusing here. Just remember that `AccessListView` is the _detail_ (single object) view, and `AccessListListView` is the _list_ (multiple objects) view.
58 |
59 | Before we move on to the next view, do you remember the extra column we added to `AccessListTable` in step three? That column expects to find a count of rules assigned for each access list in the queryset, named `rule_count`. Let's add this to our queryset now. We can employ Django's `Count()` function to extend the SQL query and annotate the count of associated rules. (Don't forget to add the import statement up top.)
60 |
61 | ```python
62 | from django.db.models import Count
63 | # ...
64 | class AccessListListView(generic.ObjectListView):
65 | queryset = models.AccessList.objects.annotate(
66 | rule_count=Count('rules')
67 | )
68 | table = tables.AccessListTable
69 | ```
70 |
71 | We'll finish up with the edit and delete views for `AccessList`. Note that for the edit view, we also need to define `form` as the form class we created in step four.
72 |
73 | ```python
74 | class AccessListEditView(generic.ObjectEditView):
75 | queryset = models.AccessList.objects.all()
76 | form = forms.AccessListForm
77 |
78 | class AccessListDeleteView(generic.ObjectDeleteView):
79 | queryset = models.AccessList.objects.all()
80 | ```
81 |
82 | That's it for the first model! We'll create another four views for `AccessListRule` as well.
83 |
84 | ### AccessListRule Views
85 |
86 | The rest of our views follow the same pattern as the first four.
87 |
88 | ```python
89 | class AccessListRuleView(generic.ObjectView):
90 | queryset = models.AccessListRule.objects.all()
91 |
92 |
93 | class AccessListRuleListView(generic.ObjectListView):
94 | queryset = models.AccessListRule.objects.all()
95 | table = tables.AccessListRuleTable
96 |
97 |
98 | class AccessListRuleEditView(generic.ObjectEditView):
99 | queryset = models.AccessListRule.objects.all()
100 | form = forms.AccessListRuleForm
101 |
102 |
103 | class AccessListRuleDeleteView(generic.ObjectDeleteView):
104 | queryset = models.AccessListRule.objects.all()
105 | ```
106 |
107 | With our views in place, we now need to make them accessible by associating each with a URL.
108 |
109 | ## Map Views to URLs
110 |
111 | In the `netbox_access_lists/` directory, create `urls.py`. This will define our view URLs.
112 |
113 | ```bash
114 | $ edit urls.py
115 | ```
116 |
117 | URL mapping for NetBox plugins is pretty much identical to regular Django apps: We'll define `urlpatterns` as an iterable of `path()` calls, mapping URL fragments to view classes.
118 |
119 | First we'll need to import Django's `path` function from its `urls` module, as well as our plugin's `models` and `views` modules.
120 |
121 | ```python
122 | from django.urls import path
123 | from . import models, views
124 | ```
125 |
126 | We have four views per model, but we actually need to define five paths for each. This is because the add and edit operations are handled by the same view, but accessed via different URLs. Along with the URL and view for each path, we'll also specify a `name`; this allows us to easily reference a URL in code.
127 |
128 | ```python
129 | urlpatterns = (
130 | path('access-lists/', views.AccessListListView.as_view(), name='accesslist_list'),
131 | path('access-lists/add/', views.AccessListEditView.as_view(), name='accesslist_add'),
132 | path('access-lists//', views.AccessListView.as_view(), name='accesslist'),
133 | path('access-lists//edit/', views.AccessListEditView.as_view(), name='accesslist_edit'),
134 | path('access-lists//delete/', views.AccessListDeleteView.as_view(), name='accesslist_delete'),
135 | )
136 | ```
137 |
138 | We've chosen `access-lists` as the base URL for our `AccessList` model, but you are free to choose something different. However, it is recommended to retain the naming scheme shown, as several NetBox features rely on it. Also note that each of the views must be invoked by its `as_view()` method when passed to `path()`.
139 |
140 | :green_circle: **Tip:** The `` string you see in some of the URLs is a [path converter](https://docs.djangoproject.com/en/stable/topics/http/urls/#path-converters). Specifically, this is an integer (`int`) variable named `pk`. This value is extracted from the request URL and passed to the view when the request is processed, so that the specified object can be located in the database.
141 |
142 | Let's add the rest of the paths now. You may find it helpful to separate the paths by model to make the file more readable.
143 |
144 | ```python
145 | urlpatterns = (
146 |
147 | # Access lists
148 | path('access-lists/', views.AccessListListView.as_view(), name='accesslist_list'),
149 | path('access-lists/add/', views.AccessListEditView.as_view(), name='accesslist_add'),
150 | path('access-lists//', views.AccessListView.as_view(), name='accesslist'),
151 | path('access-lists//edit/', views.AccessListEditView.as_view(), name='accesslist_edit'),
152 | path('access-lists//delete/', views.AccessListDeleteView.as_view(), name='accesslist_delete'),
153 |
154 | # Access list rules
155 | path('rules/', views.AccessListRuleListView.as_view(), name='accesslistrule_list'),
156 | path('rules/add/', views.AccessListRuleEditView.as_view(), name='accesslistrule_add'),
157 | path('rules//', views.AccessListRuleView.as_view(), name='accesslistrule'),
158 | path('rules//edit/', views.AccessListRuleEditView.as_view(), name='accesslistrule_edit'),
159 | path('rules//delete/', views.AccessListRuleDeleteView.as_view(), name='accesslistrule_delete'),
160 |
161 | )
162 | ```
163 |
164 | ### Adding Changelog Views
165 |
166 | You may recall that one of the features provided by NetBox is automatic [change logging](https://netbox.readthedocs.io/en/stable/additional-features/change-logging/). You can see this in action when viewing a NetBox object and selecting its "Changelog" tab. Since our models inherit from `NetBoxModel`, they too can utilize this feature.
167 |
168 | We'll add a dedicated changelog URL for each of our models. First, back at the top of `urls.py`, we need to import NetBox's `ObjectChangeLogView`:
169 |
170 | ```python
171 | from netbox.views.generic import ObjectChangeLogView
172 | ```
173 |
174 | Then, we'll add an extra path for each model inside `urlpatterns`:
175 |
176 | ```python
177 | urlpatterns = (
178 |
179 | # Access lists
180 | # ...
181 | path('access-lists//changelog/', ObjectChangeLogView.as_view(), name='accesslist_changelog', kwargs={
182 | 'model': models.AccessList
183 | }),
184 |
185 | # Access list rules
186 | # ...
187 | path('rules//changelog/', ObjectChangeLogView.as_view(), name='accesslistrule_changelog', kwargs={
188 | 'model': models.AccessListRule
189 | }),
190 |
191 | )
192 | ```
193 |
194 | Notice that we're using `ObjectChangeLogView` directly here; we did not need to create model-specific subclasses for it. Additionally, we're passing a keyword argument `model` to the view: This specifies the model to be used (which is why we didn't need to subclass the view).
195 |
196 | ## Add Model URL Methods
197 |
198 | Now that we have our URL paths in place, we can add a `get_absolute_url()` method to each of our models. The method is a [Django convention](https://docs.djangoproject.com/en/stable/ref/models/instances/#get-absolute-url); although not strictly required, it conveniently returns the absolute URL for any particular object. For example, calling `accesslist.get_absolute_url()` would return `/plugins/access-lists/access-lists/123/` (where 123 is the primary key of the object).
199 |
200 | Back in `models.py`, import Django's `reverse` function from its `urls` module at the top of the file:
201 |
202 | ```python
203 | from django.urls import reverse
204 | ```
205 |
206 | Then, add the `get_absolute_url()` method to the `AccessList` class after its `__str__()` method:
207 |
208 | ```python
209 | class AccessList(NetBoxModel):
210 | # ...
211 | def get_absolute_url(self):
212 | return reverse('plugins:netbox_access_lists:accesslist', args=[self.pk])
213 | ```
214 |
215 | `reverse()` takes two arguments here: The view name, and a list of positional arguments. The view name is formed by concatenating three components:
216 |
217 | * The string `'plugins'`
218 | * The name of our plugin
219 | * The name of the desired URL path (defined as `name='accesslist'` in `urls.py`)
220 |
221 | The object's `pk` attribute is passed as well, and replaces the `` path converter in the URL.
222 |
223 | We'll add a `get_absolute_url()` method for `AccessListRule` as well, adjusting the view name accordingly.
224 |
225 | ```python
226 | class AccessListRule(NetBoxModel):
227 | # ...
228 | def get_absolute_url(self):
229 | return reverse('plugins:netbox_access_lists:accesslistrule', args=[self.pk])
230 | ```
231 |
232 | ## Test the Views
233 |
234 | Now for the moment of truth: Has all our work thus far yielded functional UI views? Check that the development server is running, then open a browser and navigate to . You should see the access list list view and (if you followed in step two) a single access list named MyACL1.
235 |
236 | :blue_square: **Note:** This guide assumes that you're running the Django development server locally on port 8000. If your setup is different, you'll need to adjust the link above accordingly.
237 |
238 | 
239 |
240 | We see that our table has successfully render the `name`, `rule_count`, and `default_action` columns that we defined in step three, and the `rule_count` column shows two rules assigned as expected.
241 |
242 | If we click the "Add" button at top right, we'll be taken to the access list creation form. (Creating a new access list won'r work yet, but the form should render as seen below.)
243 |
244 | 
245 |
246 | However, if you click a link to an access list in the table, you'll be met by a `TemplateDoesNotExist` exception. This means exactly what it says: We have not yet defined a template for this view. Don't worry, that's coming up next!
247 |
248 | :blue_square: **Note:** You might notice that the "add" view for rules still doesn't work, raising a `NoReverseMatch` exception. This is because we haven't yet defined the REST API backends required to support the dynamic form fields. We'll take care of this when we build out the REST API functionality in step nine.
249 |
250 |
251 |
252 | :arrow_left: [Step 4: Forms](/tutorial/step04-forms.md) | [Step 6: Templates](/tutorial/step06-templates.md) :arrow_right:
253 |
254 |
255 |
256 |
--------------------------------------------------------------------------------
/tutorial/step06-templates.md:
--------------------------------------------------------------------------------
1 | # Step 6: Templates
2 |
3 | Templates are responsible for rendering HTML content for NetBox views. Each template exists as a file with a mix of HTML and template code. Generally speaking, each model in a NetBox plugin must have its own template. Templates may also be created or customized for other views, but the default templates NetBox provides are suitable in most cases.
4 |
5 | NetBox's rendering backend uses the [Django Template Language](https://docs.djangoproject.com/en/stable/topics/templates/) (DTL). It will immediately look very familiar if you've used [Jinja2](https://jinja2docs.readthedocs.io/en/stable/), but be aware that there are some important differences between the two. Generally, DTL is much more limited in the types of logic it can execute: Directly executing Python code, for instance, is not possible. Be sure to study the Django documentation before attempting to create any complex templates.
6 |
7 | :blue_square: **Note:** If you skipped the previous step, run `git checkout step05-views`.
8 |
9 | ## Template File Structure
10 |
11 | NetBox looks for templates within the `templates/` directory (if it exists) within the plugin root. Within this directory, create a subdirectory bearing the name of the plugin:
12 |
13 | ```bash
14 | $ cd netbox_access_lists/
15 | $ mkdir -p templates/netbox_access_lists/
16 | ```
17 |
18 | The template files will reside in this directory. Default templates are provided for all generic views except for `ObjectView`, so we'll need to create templates for our `AccessListView` and `AccessListRuleView` views.
19 |
20 | By default, each `ObjectView` subclass will look for a template bearing the name of its associated model. For instance, `AccessListView` will look for `accesslist.html`. This can be overriden by setting `template_name` on the view, but this behavior is suitable for our purposes.
21 |
22 | ## Create the AccessList Template
23 |
24 | Begin by creating the file `accesslist.html` in the plugin's template directory.
25 |
26 | ```bash
27 | $ edit templates/netbox_access_lists/accesslist.html
28 | ```
29 |
30 | Although we need to create our own template, NetBox has done much of the work for us, and provides a generic template that we can easily extend. At the top of the file, add an `extends` tag:
31 |
32 | ```
33 | {% extends 'generic/object.html' %}
34 | ```
35 |
36 | This tells the rendering engine to first load the NetBox template at `generic/object.html` and populate only the content we provide within `block` tags.
37 |
38 | Let's extend the generic template's `content` block with some information about the access list.
39 |
40 | ```
41 | {% block content %}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | Name |
50 | {{ object.name }} |
51 |
52 |
53 | Default Action |
54 | {{ object.get_default_action_display }} |
55 |
56 |
57 | Rules |
58 | {{ object.rules.count }} |
59 |
60 |
61 |
62 |
63 | {% include 'inc/panels/custom_fields.html' %}
64 |
65 |
66 | {% include 'inc/panels/tags.html' %}
67 | {% include 'inc/panels/comments.html' %}
68 |
69 |
70 | {% endblock content %}
71 | ```
72 |
73 | Here we've created a Boostrap 5 row and two column elements. In the first column, we have a simple card to display the access list's name and default action, as well as the number of rules assigned to it. And below it, you'll see an `include` tag which pulls in an additional template to render any custom fields associated with the model. In the second column, we've included two more templates to render tags and comments.
74 |
75 | :green_circle: **Tip:** If you're not sure how best to construct the page's layout, there are plenty of examples to reference within NetBox's core templates.
76 |
77 | Let's take a look at our new template! Navigate to the list view again (at ), and follow the link through to a particular access list. You should see something like the image below.
78 |
79 | :blue_square: **Note:** If NetBox complains that the template still does not exist, you may need to manually restart the development server (`manage.py runserver`).
80 |
81 | 
82 |
83 | This is nice, but it would be handy to include the access list's assigned rules on the page as well.
84 |
85 | ### Add a Rules Table
86 |
87 | To include the access list rules, we'll need to provide additional _context data_ under the view. Open `views.py` and find the `AccessListView` class. (It should be the first class defined.) Add a `get_extra_context()` method to this class per the code below.
88 |
89 | ```python
90 | class AccessListView(generic.ObjectView):
91 | queryset = models.AccessList.objects.all()
92 |
93 | def get_extra_context(self, request, instance):
94 | table = tables.AccessListRuleTable(instance.rules.all())
95 | table.configure(request)
96 |
97 | return {
98 | 'rules_table': table,
99 | }
100 | ```
101 |
102 | This method does three things:
103 |
104 | 1. Instantiate `AccessListRuleTable` with a queryset matching all rules assigned to this access list
105 | 2. Configure the table instance according to the current request (to honor user preferences)
106 | 3. Return a dictionary of context data referencing the table instance
107 |
108 | This makes the table available to our template as the `rules_table` context variable. Let's add it to our template.
109 |
110 | First, we need to import the `render_table` tag from the `django-tables2` library, so that we can render the table as HTML. Add this at the top of the template, immediately below the `{% extends 'generic/object.html' %}` line:
111 |
112 | ```
113 | {% load render_table from django_tables2 %}
114 | ```
115 |
116 | Then, immediately above the `{% endblock content %}` line at the end of the file, insert the following template code:
117 |
118 | ```
119 |
120 |
121 |
122 |
123 |
124 | {% render_table rules_table %}
125 |
126 |
127 |
128 |
129 | ```
130 |
131 | After refreshing the access list view in the browser, you should now see the rules table at the bottom of the page.
132 |
133 | 
134 |
135 | ## Create the AccessListRule Template
136 |
137 | Speaking of rules, let's not forget about our `AccessListRule` model: It needs a template too. Create `accesslistrule.html` alongside our first template:
138 |
139 | ```bash
140 | $ edit templates/netbox_access_lists/accesslistrule.html
141 | ```
142 |
143 | And copy the content below:
144 |
145 | ```
146 | {% extends 'generic/object.html' %}
147 |
148 | {% block content %}
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 | Access List |
157 |
158 | {{ object.access_list }}
159 | |
160 |
161 |
162 | Index |
163 | {{ object.index }} |
164 |
165 |
166 | Description |
167 | {{ object.description|placeholder }} |
168 |
169 |
170 |
171 |
172 | {% include 'inc/panels/custom_fields.html' %}
173 | {% include 'inc/panels/tags.html' %}
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 | Protocol |
182 | {{ object.get_protocol_display }} |
183 |
184 |
185 | Source Prefix |
186 |
187 | {% if object.source_prefix %}
188 | {{ object.source_prefix }}
189 | {% else %}
190 | {{ ''|placeholder }}
191 | {% endif %}
192 | |
193 |
194 |
195 | Source Ports |
196 | {{ object.source_ports|join:", "|placeholder }} |
197 |
198 |
199 | Destination Prefix |
200 |
201 | {% if object.destination_prefix %}
202 | {{ object.destination_prefix }}
203 | {% else %}
204 | {{ ''|placeholder }}
205 | {% endif %}
206 | |
207 |
208 |
209 | Destination Ports |
210 | {{ object.destination_ports|join:", "|placeholder }} |
211 |
212 |
213 | Action |
214 | {{ object.get_action_display }} |
215 |
216 |
217 |
218 |
219 |
220 |
221 | {% endblock content %}
222 | ```
223 |
224 | You'll probably be able to tell at this point what most of the above template code does, but here are a few details worth mentioning:
225 |
226 | * The URL for the rule's parent access list is retrieved by calling `object.access_list.get_absolute_url()` (the method we added in step five), _without_ the parentheses (a distinction of DTL). This method is used for related prefixes as well.
227 | * NetBox's `placeholder` filter is applied to the rule's description. (This renders a — for empty fields.)
228 | * The `protocol` and `action` attributes are rendered by calling e.g. `object.get_protocol_display()` (again without the parentheses). This is a [Django convention](https://docs.djangoproject.com/en/stable/ref/models/instances/#extra-instance-methods) for static choice fields to return the human-friendly label rather than the raw value.
229 |
230 | 
231 |
232 | Feel free to experiment with different layouts and content before proceeding with the next step.
233 |
234 |
235 |
236 | :arrow_left: [Step 5: Views](/tutorial/step05-views.md) | [Step 7: Navigation](/tutorial/step07-navigation.md) :arrow_right:
237 |
238 |
239 |
240 |
--------------------------------------------------------------------------------
/tutorial/step07-navigation.md:
--------------------------------------------------------------------------------
1 | # Step 7: Navigation
2 |
3 | So far, we've been manually entering URLs to access our plugin's views. This obviously will not suffice for regular use, so let's see about adding some links to NetBox's navigation menu.
4 |
5 | :blue_square: **Note:** If you skipped the previous step, run `git checkout step06-templates`.
6 |
7 | ## Adding Navigation Menu Items
8 |
9 | Begin by creating `navigation.py` in the `netbox_access_lists/` directory.
10 |
11 | ```bash
12 | $ cd netbox_access_lists/
13 | $ edit navigation.py
14 | ```
15 |
16 | We'll need to import the `PluginMenuItem` class provided by NetBox to add new menu items; do this at the top of the file.
17 |
18 | ```python
19 | from extras.plugins import PluginMenuItem
20 | ```
21 |
22 | Next, we'll create a tuple named `menu_items`. This will hold our customized `PluginMenuItem` instances.
23 |
24 | ```python
25 | menu_items = ()
26 | ```
27 |
28 | Let's add a link to the list view for each of our models. This is done by instantiating `PluginMenuItem` with (at minimum) two arguments:
29 |
30 | * `link` - The name of the URL path to which we're linking
31 | * `link_text` - The text of the link
32 |
33 | Create two instances of `PluginMenuItem` within `menu_items`:
34 |
35 | ```python
36 | menu_items = (
37 | PluginMenuItem(
38 | link='plugins:netbox_access_lists:accesslist_list',
39 | link_text='Access Lists'
40 | ),
41 | PluginMenuItem(
42 | link='plugins:netbox_access_lists:accesslistrule_list',
43 | link_text='Access List Rules'
44 | ),
45 | )
46 | ```
47 |
48 | Upon reloading the page, you should see a new "Plugins" section appear at the end of the navigation menu, and below it, a section titled "NetBox Access Lists", with our two links. Navigating to either of these links will highlight the corresponding menu item.
49 |
50 | :blue_square: **Note:** If the menu items do not appear, try restarting the development server (`manage.py runserver`).
51 |
52 | 
53 |
54 | That's much more convenient!
55 |
56 | ### Adding Menu Buttons
57 |
58 | While we're at it, we can add direct links to the "add" views for access lists and rules as buttons. We'll need to import two additional classes at the top of `navigation.py`: `PluginMenuButton` and `ButtonColorChoices`.
59 |
60 | ```python
61 | from extras.plugins import PluginMenuButton, PluginMenuItem
62 | from utilities.choices import ButtonColorChoices
63 | ```
64 |
65 | `PluginMenuButton` is used similarly to `PluginMenuItem`: Instantiate it with the necessary keyword arguments to effect a menu button. These arguments are:
66 |
67 | * `link` - The name of the URL path to which the button links
68 | * `title` - The text displayed when the user hovers over the button
69 | * `icon_class` - CSS class name(s) indicating the icon to display
70 | * `color` - The button's color (choices are provided by `ButtonColorChoices`)
71 |
72 | Create these instances in `navigation.py` _above_ `menu_items`. Because each menu item expects to receive an iterable of button instances, we'll create each of these inside a list.
73 |
74 | ```python
75 | accesslist_buttons = [
76 | PluginMenuButton(
77 | link='plugins:netbox_access_lists:accesslist_add',
78 | title='Add',
79 | icon_class='mdi mdi-plus-thick',
80 | color=ButtonColorChoices.GREEN
81 | )
82 | ]
83 |
84 | accesslistrule_buttons = [
85 | PluginMenuButton(
86 | link='plugins:netbox_access_lists:accesslistrule_add',
87 | title='Add',
88 | icon_class='mdi mdi-plus-thick',
89 | color=ButtonColorChoices.GREEN
90 | )
91 | ]
92 | ```
93 |
94 | The buttons can then be passed to the menu items via the `buttons` keyword argument:
95 |
96 | ```python
97 | menu_items = (
98 | PluginMenuItem(
99 | link='plugins:netbox_access_lists:accesslist_list',
100 | link_text='Access Lists',
101 | buttons=accesslist_buttons
102 | ),
103 | PluginMenuItem(
104 | link='plugins:netbox_access_lists:accesslistrule_list',
105 | link_text='Access List Rules',
106 | buttons=accesslistrule_buttons
107 | ),
108 | )
109 | ```
110 |
111 | Now we should see green "add" buttons appear next to our menu links.
112 |
113 | 
114 |
115 |
116 |
117 | :arrow_left: [Step 6: Templates](/tutorial/step06-templates.md) | [Step 8: Filter Sets](/tutorial/step08-filter-sets.md) :arrow_right:
118 |
119 |
120 |
121 |
--------------------------------------------------------------------------------
/tutorial/step08-filter-sets.md:
--------------------------------------------------------------------------------
1 | # Step 8: Filter Sets
2 |
3 | Filters enable users to request only a specific subset of objects matching a query; when filtering the sites list by status or region, for instance. NetBox employs the [`django-filters`](https://django-filter.readthedocs.io/en/stable/) library to build and apply filter sets for models. We can create filter sets to enable this same functionality for our plugin as well.
4 |
5 | :blue_square: **Note:** If you skipped the previous step, run `git checkout step07-navigation`.
6 |
7 | ## Create a Filter Set
8 |
9 | Begin by creating `filtersets.py` in the `netbox_access_lists/` directory.
10 |
11 | ```bash
12 | $ cd netbox_access_lists/
13 | $ edit filtersets.py
14 | ```
15 |
16 | At the top of this file, we'll import NetBox's `NetBoxModelFilterSet` class, which will serve as the base class for our filter set, as well as our `AccessListRule` model. (In the interest of brevity, we're only going to create a filter set for one model, but it should be clear how to replicate this approach for the `AccessList` model as well.)
17 |
18 | ```python
19 | from netbox.filtersets import NetBoxModelFilterSet
20 | from .models import AccessListRule
21 | ```
22 |
23 | Next, create a class named `AccessListRuleFilterSet` subclassing `NetBoxModelFilterSet`. Within this class, create a child `Meta` class and define the filter set's `model` and `fields` attributes. (You may notice this looks familiar; it is very similar to the process for building a model form.) The `fields` parameter should list all the model fields against which we might want to filter.
24 |
25 | ```python
26 | class AccessListRuleFilterSet(NetBoxModelFilterSet):
27 |
28 | class Meta:
29 | model = AccessListRule
30 | fields = ('id', 'access_list', 'index', 'protocol', 'action')
31 | ```
32 |
33 | `NetBoxModelFilterSet` handles some important functions for us, including support for filtering by custom field values and tags. It also creates a general-purpose `q` filter which invokes the `search()` method. (By default, this does nothing.) We can override this method to define our general-purpose search logic. Let's add a `search` method after the `Meta` child class to override the default behavior.
34 |
35 | ```python
36 | def search(self, queryset, name, value):
37 | return queryset.filter(description__icontains=value)
38 | ```
39 |
40 | This will return all rules whose description contains the queried string. Of course, you're free to extend this to match other fields as well, but for our purposes this should be sufficient.
41 |
42 | ## Create a Filter Form
43 |
44 | The filter set handles the "behind the scenes" process of filtering queries, but we also need to create a form class to render the filter fields in the UI. We'll add this to `forms.py`. First, import Django's `forms` module (which will provide the field classes we need) and append `NetBoxModelFilterSetForm` to the existing import statement for `netbox.forms`:
45 |
46 | ```python
47 | from django import forms
48 | # ...
49 | from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm
50 | ```
51 |
52 | Then create a form class named `AccessListRuleFilterForm` subclassing `NetBoxModelFilterSetForm` and declare an attribute named `model` referencing `AccessListRule` (which has already been imported for one of the existing forms).
53 |
54 | ```python
55 | class AccessListRuleFilterForm(NetBoxModelFilterSetForm):
56 | model = AccessListRule
57 | ```
58 |
59 | :blue_square: **Note:** Note that the `model` attribute is declared directly under the class: We don't need a `Meta` child class.
60 |
61 | Next, we need to define a form field for each filter that we want to appear in the UI. Let's start with the `access_list` filter: This references a related object, so we'll want to use `ModelMultipleChoiceField` (to allow users to filter by multiple objects). Add the form field with the same name as its peer filter, specifying the queryset to use when fetching related objects.
62 |
63 | ```python
64 | access_list = forms.ModelMultipleChoiceField(
65 | queryset=AccessList.objects.all(),
66 | required=False
67 | )
68 | ```
69 |
70 | Notice that we've also set `required=False`: This should be the case for _all_ fields in a filter form, because filters are never mandatory.
71 |
72 | :blue_square: **Note:** We're using Django's `ModelMultipleChoiceField` class for this field instead of NetBox's `DynamicModelChoiceField` because the latter requires a functional REST API endpoint for the model. Once we implement a REST API in step nine, you're free to revisit this form and change `access_list` to a `DynamicModelChoiceField`.
73 |
74 | Next we'll add a field for the `position` filter: This is an integer field, so `IntegerField` should work nicely:
75 |
76 | ```python
77 | index = forms.IntegerField(
78 | required=False
79 | )
80 | ```
81 |
82 | Finally, we'll add fields for the `protocol` and `action` choice-based filters. `MultipleChoiceField` should be used to allow users to select one or more choices. We must pass the set of valid choices when declaring these fields, so first extend the relevant import statement at the top of `forms.py`:
83 |
84 | ```python
85 | from .models import AccessList, AccessListRule, ActionChoices, ProtocolChoices
86 | ```
87 |
88 | Then add the form fields to `AccessListRuleFilterForm`:
89 |
90 | ```python
91 | protocol = forms.MultipleChoiceField(
92 | choices=ProtocolChoices,
93 | required=False
94 | )
95 | action = forms.MultipleChoiceField(
96 | choices=ActionChoices,
97 | required=False
98 | )
99 | ```
100 |
101 | ## Update the View
102 |
103 | The last step before we can use our new filter set and form is to enable them under the model's list view. Open `views.py` and extend the last import statement to include the `filtersets` module:
104 |
105 | ```python
106 | from . import filtersets, forms, models, tables
107 | ```
108 |
109 | Then, add the `filterset` and `filterset_form` attributes to `AccessListRuleListView`:
110 |
111 | ```python
112 | class AccessListRuleListView(generic.ObjectListView):
113 | queryset = models.AccessListRule.objects.all()
114 | table = tables.AccessListRuleTable
115 | filterset = filtersets.AccessListRuleFilterSet
116 | filterset_form = forms.AccessListRuleFilterForm
117 | ```
118 |
119 | After ensuring the development server has restarted, navigate to the rules list view in the browser. You should now see a "Filters" tab next to the "Results" tab. Under it we'll find the four fields we created on `AccessListRuleFilterForm`, as well as the built-in "search" field.
120 |
121 | 
122 |
123 | If you haven't already, create a few more access lists and rules, and experiment with the filters. Consider how you might filter by additional fields, or add more complex logic to the filter set.
124 |
125 | :green_circle: **Tip:** You may notice that we did not add a form field for the model's `id` filter: This is because it is unlikely to be useful for a human utilizing the UI. However, we still want to support filtering object by their primary keys, because it _is_ very helpful for consumers of NetBox's REST API, which we'll cover next.
126 |
127 |
128 |
129 | :arrow_left: [Step 7: Navigation](/tutorial/step07-navigation.md) | [Step 9: REST API](/tutorial/step09-rest-api.md) :arrow_right:
130 |
131 |
132 |
133 |
--------------------------------------------------------------------------------
/tutorial/step09-rest-api.md:
--------------------------------------------------------------------------------
1 | # Step 9: REST API
2 |
3 | The REST API enables powerful integration with other systems which exchange data with NetBox. It is powered by the [Django REST Framework](https://www.django-rest-framework.org/) (DRF), which is _not_ a component of Django itself. In this tutorial, we'll see how we can extend NetBox's REST API to serve our plugin.
4 |
5 | :blue_square: **Note:** If you skipped the previous step, run `git checkout step08-filter-sets`.
6 |
7 | Our API code will live in the `api/` directory under `netbox_access_lists/`. Let's go ahead and create that as well as an `__init__.py` file now:
8 |
9 | ```bash
10 | $ cd netbox_access_lists/
11 | $ mkdir api
12 | $ touch api/__init__.py
13 | ```
14 |
15 | ## Create Model Serializers
16 |
17 | Serializers are somewhat analogous to forms: They control the translation of client data to and from Python objects, while Django itself handles the database abstraction. We need to create a serializer for each of our models. Begin by creating `serializers.py` in the `api/` directory.
18 |
19 | ```bash
20 | $ edit api/serializers.py
21 | ```
22 |
23 | At the top of this file, we need to import the `serializers` module from the `rest_framework` library, as well as NetBox's `NetBoxModelSerializer` class and our plugin's own models:
24 |
25 | ```python
26 | from rest_framework import serializers
27 |
28 | from netbox.api.serializers import NetBoxModelSerializer
29 | from ..models import AccessList, AccessListRule
30 | ```
31 |
32 | ### Create AccessListSerializer
33 |
34 | First, we'll create a serializer for `AccessList`, subclassing `NetBoxModelSerializer`. Much like when creating a model form, we'll create a child `Meta` class under the serializer specifying the associated `model` and the `fields` to be included.
35 |
36 | ```python
37 | class AccessListSerializer(NetBoxModelSerializer):
38 |
39 | class Meta:
40 | model = AccessList
41 | fields = (
42 | 'id', 'display', 'name', 'default_action', 'comments', 'tags', 'custom_fields', 'created',
43 | 'last_updated',
44 | )
45 | ```
46 |
47 | It's worth discussing each of the fields we've named above. `id` is the model's primary key; it should always be included with every serializer, as it provides a guaranteed method of uniquely identifying objects. The `display` field is built into `NetBoxModelSerializer`: It is a read-only field which returns a string representation of the object. This is useful for populating form field dropdowns, for instance.
48 |
49 | The `name`, `default_action`, and `comments` fields are declared on the `AccessList` model. `tags` provides access to the object's tag manager, and `custom_fields` includes its custom field data; both of these are provided by `NetBoxModelSerializer`. Finally, the `created` and `last_updated` are read-only fields built into `NetBoxModel`.
50 |
51 | Our serializer will inspect the model to generate the necessary fields automatically, however there's one field that we need to add manually. Every serializer should include a read-only `url` field which contains the URL where the object can be reached; think of it as similar to a model's `get_absolute_url()` method. To add this, we'll use DRF's `HyperlinkedIdentityField`. Add it above the `Meta` child class:
52 |
53 | ```python
54 | class AccessListSerializer(NetBoxModelSerializer):
55 | url = serializers.HyperlinkedIdentityField(
56 | view_name='plugins-api:netbox_access_lists-api:accesslist-detail'
57 | )
58 | ```
59 |
60 | When invoking the field class, we need to specify the appropriate view name. Note that this view doesn't actually exist yet; we'll create it a bit later.
61 |
62 | Remember back in step three when we added a table column showing the number of rules assigned to each access list? That was handy. Let's add a serializer field for it too! Add this directly below the `url` field:
63 |
64 | ```python
65 | rule_count = serializers.IntegerField(read_only=True)
66 | ```
67 |
68 | Just as with the table column, we'll rely on our view (to be defined next) to annotate the rule count for each access list on the underlying queryset.
69 |
70 | Finally, we need to add both `url` and `rule_count` to `Meta.fields`:
71 |
72 | ```python
73 | class Meta:
74 | model = AccessList
75 | fields = (
76 | 'id', 'url', 'display', 'name', 'default_action', 'comments', 'tags', 'custom_fields', 'created',
77 | 'last_updated', 'rule_count',
78 | )
79 | ```
80 |
81 | :green_circle: **Tip:** The order in which fields are listed determines the order in which they appear in the object's API representation.
82 |
83 | ### Create AccessListRuleSerializer
84 |
85 | We also need to create a serializer for `AccessListRule`. Add it to `serializers.py` below `AccessListSerializer`. As with the first serializer, we'll add a `Meta` class to define the model and fields, and a `url` field.
86 |
87 | ```python
88 | class AccessListRuleSerializer(NetBoxModelSerializer):
89 | url = serializers.HyperlinkedIdentityField(
90 | view_name='plugins-api:netbox_access_lists-api:accesslistrule-detail'
91 | )
92 |
93 | class Meta:
94 | model = AccessListRule
95 | fields = (
96 | 'id', 'url', 'display', 'access_list', 'index', 'protocol', 'source_prefix', 'source_ports',
97 | 'destination_prefix', 'destination_ports', 'action', 'tags', 'custom_fields', 'created',
98 | 'last_updated',
99 | )
100 | ```
101 |
102 | There's an additional consideration when referencing related objects in a serializer. By default, the serializer will return only the primary key of the related object; its numeric ID. This requires the client to make additional API requests in order to determine _any_ other information about the related object. It is convenient to include on the serializer some information about the related object, such as its name and URL, automatically. We can do this by using a _nested serializer_.
103 |
104 | For instance, the `source_prefix` and `destination_prefix` fields both reference NetBox's core `ipam.Prefix` model. We can extend `AccessListRuleSerializer` to use NetBox's nested serializer for this model:
105 |
106 | ```python
107 | from ipam.api.serializers import NestedPrefixSerializer
108 | # ...
109 | class AccessListRuleSerializer(NetBoxModelSerializer):
110 | url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_access_lists-api:accesslistrule-detail')
111 | source_prefix = NestedPrefixSerializer()
112 | destination_prefix = NestedPrefixSerializer()
113 | ```
114 |
115 | Now, our serializer will include an abridged representation of the source and/or destination prefixes for the object. We should do this with the `access_list` field as well, however we'll first need to create a nested serializer for the `AccessList` model.
116 |
117 | ### Create Nested Serializers
118 |
119 | Begin by importing NetBox's `WritableNestedSerializer` class. This will serve as the base class for our nested serializers.
120 |
121 | ```python
122 | from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
123 | ```
124 |
125 | Then, create two nested serializer classes, one for each of our plugin's models. Each of these will have a `url` field and `Meta` child class like the regular serializers, however the `Meta.fields` attribute for each is limited to a bare minimum of fields: `id`, `url`, `display`, and a supplementary human-friendly identifier. Add these in `serializers.py` _above_ the regular serializers (because we need to define `NestedAccessListSerializer` before we can reference it).
126 |
127 | ```python
128 | class NestedAccessListSerializer(WritableNestedSerializer):
129 | url = serializers.HyperlinkedIdentityField(
130 | view_name='plugins-api:netbox_access_lists-api:accesslist-detail'
131 | )
132 |
133 | class Meta:
134 | model = AccessList
135 | fields = ('id', 'url', 'display', 'name')
136 |
137 | class NestedAccessListRuleSerializer(WritableNestedSerializer):
138 | url = serializers.HyperlinkedIdentityField(
139 | view_name='plugins-api:netbox_access_lists-api:accesslistrule-detail'
140 | )
141 |
142 | class Meta:
143 | model = AccessListRule
144 | fields = ('id', 'url', 'display', 'index')
145 | ```
146 |
147 | Now we can override the `access_list` field on `AccessListRuleSerializer` to use the nested serializer:
148 |
149 | ```python
150 | class AccessListRuleSerializer(NetBoxModelSerializer):
151 | url = serializers.HyperlinkedIdentityField(
152 | view_name='plugins-api:netbox_access_lists-api:accesslistrule-detail'
153 | )
154 | access_list = NestedAccessListSerializer()
155 | source_prefix = NestedPrefixSerializer()
156 | destination_prefix = NestedPrefixSerializer()
157 | ```
158 |
159 | ## Create the Views
160 |
161 | Next, we need to create views to handle the API logic. Just as serializers are roughly analogous to forms, API views work similarly to the UI views that we created in step five. However, because API functionality is highly standardized, view creation is substantially simpler: We generally need only to create a single _view set_ for each model. A view set is a single class that can handle the view, add, change, and delete operations which each require dedicated views under the UI.
162 |
163 | Start by creating `api/views.py` and importing NetBox's `NetBoxModelViewSet` class, as well as our plugin's `models` and `filtersets` modules, and our serializers.
164 |
165 | ```python
166 | from netbox.api.viewsets import NetBoxModelViewSet
167 |
168 | from .. import filtersets, models
169 | from .serializers import AccessListSerializer, AccessListRuleSerializer
170 | ```
171 |
172 | First we'll create a view set for access lists, by inheriting from `NetBoxModelViewSet` and defining its `queryset` and `serializer_class` attributes. (Note that we're prefetching assigned tags for the queryset.)
173 |
174 | ```python
175 | class AccessListViewSet(NetBoxModelViewSet):
176 | queryset = models.AccessList.objects.prefetch_related('tags')
177 | serializer_class = AccessListSerializer
178 | ```
179 |
180 | Recall that we added a `rule_count` field to `AccessListSerializer`; let's annotate the queryset appropriately to ensure that field gets populated (just as we did for the table column in step five). Remember to import Django's `Count` utility class.
181 |
182 | ```python
183 | from django.db.models import Count
184 | # ...
185 | class AccessListViewSet(NetBoxModelViewSet):
186 | queryset = models.AccessList.objects.prefetch_related('tags').annotate(
187 | rule_count=Count('rules')
188 | )
189 | serializer_class = AccessListSerializer
190 | ```
191 |
192 | Next, we'll add a view set for rules. In addition to `queryset` and `serializer_class`, we'll attach the filter set for this model as `filterset_class`. Note that we're also prefetching all related object fields in addition to tags to improve performance when listing many objects.
193 |
194 | ```python
195 | class AccessListRuleViewSet(NetBoxModelViewSet):
196 | queryset = models.AccessListRule.objects.prefetch_related(
197 | 'access_list', 'source_prefix', 'destination_prefix', 'tags'
198 | )
199 | serializer_class = AccessListRuleSerializer
200 | filterset_class = filtersets.AccessListRuleFilterSet
201 | ```
202 |
203 | ## Create the Endpoint URLs
204 |
205 | Finally, we'll create our API endpoint URLs. This works a bit differently from UI views: Instead of defining a series of paths, we instantiate a _router_ and register each view set to it.
206 |
207 | Create `api/urls.py` and import NetBox's `NetBoxRouter` and our API views:
208 |
209 | ```python
210 | from netbox.api.routers import NetBoxRouter
211 | from . import views
212 | ```
213 |
214 | Next, we'll define an `app_name`. This will be used to resolve API view names for our plugin.
215 |
216 | ```python
217 | app_name = 'netbox_access_list'
218 | ```
219 |
220 | Then, we create a `NetBoxRouter` instance and register each view with it using our desired URL. These are the endpoints that will be available under `/api/plugins/access-lists/`.
221 |
222 | ```python
223 | router = NetBoxRouter()
224 | router.register('access-lists', views.AccessListViewSet)
225 | router.register('access-list-rules', views.AccessListRuleViewSet)
226 | ```
227 |
228 | Finally, we expose the router's `urls` attribute as `urlpatterns` so that it will be detected by the plugins framework.
229 |
230 | ```python
231 | urlpatterns = router.urls
232 | ```
233 |
234 | :green_circle: **Tip:** The base URL for our plugin's REST API endpoints is determined by the `base_url` attribute of the plugin config class that we created in step one.
235 |
236 | With all of our REST API components now in place, we should be able to make API requests. (Note that you may first need to provision a token for authentication.) You can quickly verify that our endpoints are working properly by navigating to in your browser while logged into NetBox. You should see the two available endpoints; clicking on either will return a list of objects.
237 |
238 | :blue_square: **Note:** If the REST API endpoints do not load, try restarting the development server (`manage.py runserver`).
239 |
240 | 
241 |
242 | 
243 |
244 |
245 |
246 | :arrow_left: [Step 8: Filter Sets](/tutorial/step08-filter-sets.md) | [Step 10: GraphQL API](/tutorial/step10-graphql-api.md) :arrow_right:
247 |
248 |
249 |
250 |
--------------------------------------------------------------------------------
/tutorial/step10-graphql-api.md:
--------------------------------------------------------------------------------
1 | # Step 10: GraphQL API
2 |
3 | In addition to its REST API, NetBox also features a [GraphQL](https://graphql.org/) API. This can be used to conveniently request arbitrary collections of data about NetBox objects. NetBox's GraphQL API is built using the [Graphene](https://graphene-python.org/) and [`graphene-django`](https://docs.graphene-python.org/projects/django/en/latest/) library.
4 |
5 | :blue_square: **Note:** If you skipped the previous step, run `git checkout step09-rest-api`.
6 |
7 | Begin by creating `graphql.py`. This will hold our object type and query classes.
8 |
9 | ```bash
10 | $ cd netbox_access_lists/
11 | $ edit graphql.py
12 | ```
13 |
14 | We'll need to import several resources. First we need Graphene's `ObjectType` class, as well as NetBox's custom `NetBoxObjectType` which inherits from it. (The latter will be used for our model types.) We also need the `ObjectField` and `ObjectListField` classes provided by NetBox for our query. And finally, import our plugin's `models` and `filtersets` modules.
15 |
16 | ```python
17 | from graphene import ObjectType
18 | from netbox.graphql.types import NetBoxObjectType
19 | from netbox.graphql.fields import ObjectField, ObjectListField
20 | from . import filtersets, models
21 | ```
22 |
23 | ## Create the Object Types
24 |
25 | Subclass `NetBoxObjectType` to create two object type classes, one for each of our models. Just like with the REST API serilizers, create a child `Meta` class on each defining its `model` and `fields`. However, instead of explicitly listing each field by name, in our case we can use the special value `__all__` to indicate that we want to include all available model fields. Additionally, declare `filterset_class` on `AccessListRuleType` to attach the filter set.
26 |
27 | ```python
28 | class AccessListType(NetBoxObjectType):
29 |
30 | class Meta:
31 | model = models.AccessList
32 | fields = '__all__'
33 |
34 |
35 | class AccessListRuleType(NetBoxObjectType):
36 |
37 | class Meta:
38 | model = models.AccessListRule
39 | fields = '__all__'
40 | filterset_class = filtersets.AccessListRuleFilterSet
41 | ```
42 |
43 | ## Create the Query
44 |
45 | Then we need to create our query class. Subclass Graphene's `ObjectType` class and define two fields for each model: an object field and a list field.
46 |
47 | ```python
48 | class Query(ObjectType):
49 | access_list = ObjectField(AccessListType)
50 | access_list_list = ObjectListField(AccessListType)
51 |
52 | access_list_rule = ObjectField(AccessListRuleType)
53 | access_list_rule_list = ObjectListField(AccessListRuleType)
54 | ```
55 |
56 | Then we just need to expose our query class to the plugins framework as `schema`:
57 |
58 | ```python
59 | schema = Query
60 | ```
61 |
62 | :green_circle: **Tip:** The path to the query class can be changed by setting `graphql_schema` in the plugin's configuration class.
63 |
64 | To try out the GraphQL API, open `` in a browser and enter the following query:
65 |
66 | ```
67 | query {
68 | access_list_list {
69 | id
70 | name
71 | rules {
72 | index
73 | action
74 | description
75 | }
76 | }
77 | }
78 | ```
79 |
80 | You should receive a response showing the ID, name, and rules for each access list in NetBox. Each rule will list its index, action, and description. Experiment with different queries to see what other data you can request. (Refer back to the model definitions for inspiration.)
81 |
82 | 
83 |
84 | This completes the plugin development tutorial. Well done! Now you're all set to make a plugin of your own!
85 |
86 |
87 |
88 | :arrow_left: [Step 9: REST API](/tutorial/step09-rest-api.md) | [Step 11: Search](/tutorial/step11-search.md) :arrow_right:
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/tutorial/step11-search.md:
--------------------------------------------------------------------------------
1 | # Step 11: NetBox v3.4 Features
2 |
3 | NetBox version 3.4, released in December 2022, introduced a greatly improved global search engine, which includes the ability for plugins to register their own models. In this step, we'll add search indexers for our custom models so that they appear in NetBox's global search results.
4 |
5 | :warning: **Warning:** This feature requires NetBox v3.4 or later. If you haven't already, be sure to set `min_version = '3.4.0'` in `NetBoxAccessListsConfig`.
6 |
7 | :blue_square: **Note:** If you skipped the previous step, run `git checkout step10-graphql`.
8 |
9 | ## Create Search Indexes
10 |
11 | Our plugin has two models: `AccessList` and `AccessListRule`. We'd like users to be able to search instances of both models using NetBox's global search feature. To enable this, we need to declare and register a `SearchIndex` subclass for each model.
12 |
13 | Begin by creating `search.py` in the plugin's root directory, alongside `models.py`.
14 |
15 | ```bash
16 | $ cd netbox_access_lists/
17 | $ edit search.py
18 | ```
19 |
20 | Within this file, we'll import NetBox's `SearchIndex` class as well as our own models. Then, we'll create a subclass of `SearchIndex` for each model:
21 |
22 | ```python
23 | from netbox.search import SearchIndex
24 | from .models import AccessList, AccessListRule
25 |
26 | class AccessListIndex(SearchIndex):
27 | model = AccessList
28 |
29 | class AccessListRuleIndex(SearchIndex):
30 | model = AccessListRule
31 | ```
32 |
33 | There's a bit more to enabling search, though. We also need to tell NetBox which fields to search for each model, and how important each field is (also known as its _precedence_). The latter is accomplished by assigning a numerical weight.
34 |
35 | Consider our `AccessList` model. It has three interesting database fields: `name`, `default_action`, and `comments`. How should we treat these when searching for objects in NetBox? This can be somewhat subjective, but generally we want to assign higher precedence (_lower_ weights) to important fields, and omit fields that we don't care about. If you're unsure what weights to assign, have a look around the core NetBox code base for similar examples.
36 |
37 | * `name`: This is an important field, so we'll give it a high precedence of `100`.
38 | * `default_action` This is a choice selection field. While very useful for _filtering_, we wouldn't typically expect users to search for these values. We'll exclude this field from the search index.
39 | * `comments`: It's always recommended to include user comments in the search index, however we'll assign this field a much lower precedence of `5000` as any matches are less likely to be pertinent.
40 |
41 | After selecting our search fields and their precedences, we should have something like this:
42 |
43 | ```python
44 | class AccessListIndex(SearchIndex):
45 | model = AccessList
46 | fields = (
47 | ('name', 100),
48 | ('comments', 5000),
49 | )
50 |
51 | class AccessListRuleIndex(SearchIndex):
52 | model = AccessListRule
53 | fields = (
54 | ('description', 500),
55 | )
56 | ```
57 |
58 | Why did we exclude the source and destination parameters from `AccessListRuleIndex`? The source and destination prefixes are related objects, so we want to avoid caching their values locally: If the related object is changed, our cached copy can become outdated. And we omit the source and destination port numbers because matching on common integer values can produce a ton of irrelevant search results. All of these values are better matched using specific filters rather than general purpose search.
59 |
60 | ## Register the Indexers
61 |
62 | Finally, we need to register our indexers so that NetBox knows to run them. At the top of `search.py`, import the `register_search` decorator. Then, use it to wrap both of our index classes:
63 |
64 | ```python
65 | from netbox.search import SearchIndex, register_search
66 | from .models import AccessList, AccessListRule
67 |
68 | @register_search
69 | class AccessListIndex(SearchIndex):
70 | model = AccessList
71 | fields = (
72 | ('name', 100),
73 | ('comments', 5000),
74 | )
75 |
76 | @register_search
77 | class AccessListRuleIndex(SearchIndex):
78 | model = AccessListRule
79 | fields = (
80 | ('description', 500),
81 | )
82 | ```
83 |
84 | With our indexers now registered, we can run the `reindex` management command to index any existing objects. (New objects created from this point forward will be registered automatically upon creation.)
85 |
86 | ```
87 | $ ./manage.py reindex netbox_access_lists
88 | Reindexing 2 models.
89 | Indexing models
90 | netbox_access_lists.accesslist... 1 entries cached.
91 | netbox_access_lists.accesslistrule... 3 entries cached.
92 | ```
93 |
94 | Now we can search for access lists and rules using NetBox's global search function.
95 |
96 | 
97 |
98 |
99 |
100 | :arrow_left: [Step 10: GraphQL API](/tutorial/step10-graphql-api.md)
101 |
102 |
103 |
--------------------------------------------------------------------------------