netbox-config-diff 2.0.1__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {netbox-config-diff-2.0.1/netbox_config_diff.egg-info → netbox-config-diff-2.2.0}/PKG-INFO +20 -7
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/README.md +17 -3
- netbox-config-diff-2.2.0/docs/media/screenshots/cr-completed.png +0 -0
- netbox-config-diff-2.2.0/docs/media/screenshots/cr-created.png +0 -0
- netbox-config-diff-2.2.0/docs/media/screenshots/navbar.png +0 -0
- netbox-config-diff-2.2.0/docs/media/screenshots/script.png +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/__init__.py +2 -1
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/api/serializers.py +5 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/compliance/base.py +61 -40
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/compliance/secrets.py +24 -15
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/compliance/utils.py +11 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/configurator/base.py +16 -18
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/configurator/utils.py +1 -1
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/forms.py +5 -1
- netbox-config-diff-2.2.0/netbox_config_diff/migrations/0008_alter_configcompliance_device.py +21 -0
- netbox-config-diff-2.2.0/netbox_config_diff/models/__init__.py +11 -0
- netbox-config-diff-2.2.0/netbox_config_diff/models/base.py +10 -0
- netbox-config-diff-2.0.1/netbox_config_diff/compliance/models.py → netbox-config-diff-2.2.0/netbox_config_diff/models/data_models.py +43 -14
- {netbox-config-diff-2.0.1/netbox_config_diff → netbox-config-diff-2.2.0/netbox_config_diff/models}/models.py +26 -36
- netbox-config-diff-2.2.0/netbox_config_diff/navigation.py +47 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/templates/netbox_config_diff/configcompliance/config.html +1 -1
- netbox-config-diff-2.0.1/netbox_config_diff/templates/netbox_config_diff/configcompliance.html → netbox-config-diff-2.2.0/netbox_config_diff/templates/netbox_config_diff/configcompliance/data.html +8 -8
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/templates/netbox_config_diff/configcompliance/missing_extra.html +1 -1
- netbox-config-diff-2.0.1/netbox_config_diff/templates/netbox_config_diff/configcompliance/base.html → netbox-config-diff-2.2.0/netbox_config_diff/templates/netbox_config_diff/configcompliance.html +1 -1
- netbox-config-diff-2.2.0/netbox_config_diff/views/base.py +13 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/views/compliance.py +46 -4
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/views/configuration.py +7 -5
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0/netbox_config_diff.egg-info}/PKG-INFO +20 -7
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff.egg-info/SOURCES.txt +9 -3
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff.egg-info/requires.txt +1 -2
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/pyproject.toml +1 -12
- netbox-config-diff-2.2.0/requirements/dev.txt +1 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/tests/conftest.py +9 -6
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/tests/test_compliance.py +7 -5
- netbox-config-diff-2.0.1/docs/media/screenshots/navbar.png +0 -0
- netbox-config-diff-2.0.1/docs/media/screenshots/script.png +0 -0
- netbox-config-diff-2.0.1/netbox_config_diff/navigation.py +0 -61
- netbox-config-diff-2.0.1/requirements/dev.txt +0 -2
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/LICENSE +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/MANIFEST.in +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/development/configuration.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/compliance-diff.png +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/compliance-error.png +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/compliance-list.png +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/compliance-missing-extra.png +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/compliance-ok.png +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/config-temp-substitute.png +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/cr-approve-button.png +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/cr-approved.png +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/cr-collecting-diff-button.png +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/cr-diffs-tab.png +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/cr-job-log.png +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/cr-schedule-button.png +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/cr-scheduled.png +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/cr-unapprove-button.png +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/cr-unschedule-button.png +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/platformsetting.png +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/render-temp-substitute.png +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/script-list.png +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/substitute.png +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/api/__init__.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/api/urls.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/api/views.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/choices.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/compliance/__init__.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/configurator/__init__.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/configurator/exceptions.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/configurator/factory.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/configurator/platforms.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/constants.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/filtersets.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/graphql.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/jobs.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/migrations/0001_initial.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/migrations/0002_add_script.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/migrations/0003_configcompliance_actual_config_and_more.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/migrations/0004_update_script.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/migrations/0005_configcompliance_extra_missing.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/migrations/0006_substitute.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/migrations/0007_configurationrequest.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/migrations/__init__.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/scripts/config_diff.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/search.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/static/netbox_config_diff/diff2html-ui.min.js +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/static/netbox_config_diff/diff2html.dark.min.css +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/static/netbox_config_diff/diff2html.min.css +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/tables.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/templates/netbox_config_diff/configurationrequest/base.html +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/templates/netbox_config_diff/configurationrequest/diffs.html +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/templates/netbox_config_diff/configurationrequest.html +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/templates/netbox_config_diff/inc/diff.html +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/templates/netbox_config_diff/inc/job_log.html +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/templates/netbox_config_diff/platformsetting.html +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/templates/netbox_config_diff/substitute.html +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/urls.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/views/__init__.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff.egg-info/dependency_links.txt +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff.egg-info/top_level.txt +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/requirements/base.txt +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/requirements/test.txt +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/setup.cfg +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/tests/factories.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/tests/test_compliance_utils.py +0 -0
- {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/tests/test_urls.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: netbox-config-diff
|
|
3
|
-
Version: 2.0
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: Push rendered device configurations from NetBox to devices and apply them.
|
|
5
5
|
Author: Artem Kotik
|
|
6
6
|
Author-email: miaow2@yandex.ru
|
|
7
7
|
License: Apache License
|
|
@@ -223,8 +223,7 @@ Requires-Dist: scrapli[asyncssh]==2023.07.30
|
|
|
223
223
|
Requires-Dist: scrapli-cfg==2023.07.30
|
|
224
224
|
Requires-Dist: scrapli-community==2023.07.30
|
|
225
225
|
Provides-Extra: dev
|
|
226
|
-
Requires-Dist:
|
|
227
|
-
Requires-Dist: ruff==0.1.0; extra == "dev"
|
|
226
|
+
Requires-Dist: ruff==0.1.2; extra == "dev"
|
|
228
227
|
Provides-Extra: test
|
|
229
228
|
Requires-Dist: factory_boy==3.3.0; extra == "test"
|
|
230
229
|
Requires-Dist: pytest==7.4.0; extra == "test"
|
|
@@ -233,7 +232,6 @@ Requires-Dist: pytest-django==4.5.2; extra == "test"
|
|
|
233
232
|
[](https://github.com/netbox-community/netbox)
|
|
234
233
|
[](https://pypi.org/project/netbox-config-diff/)
|
|
235
234
|
[](https://badge.fury.io/py/netbox-config-diff)
|
|
236
|
-
[](https://github.com/ambv/black)
|
|
237
235
|
[](https://github.com/astral-sh/ruff)
|
|
238
236
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
239
237
|
[](https://github.com/miaow2/netbox-config-diff/actions)
|
|
@@ -310,6 +308,7 @@ PLUGINS_CONFIG = {
|
|
|
310
308
|
"netbox_config_diff": {
|
|
311
309
|
"USERNAME": "foo",
|
|
312
310
|
"PASSWORD": "bar",
|
|
311
|
+
"AUTH_SECONDARY": "foobar", # define here password for accessing Privileged EXEC mode, this variable is optional
|
|
313
312
|
},
|
|
314
313
|
}
|
|
315
314
|
```
|
|
@@ -329,13 +328,19 @@ python manage.py collectstatic --noinput
|
|
|
329
328
|
Restart NetBox service:
|
|
330
329
|
|
|
331
330
|
```bash
|
|
332
|
-
systemctl restart netbox
|
|
331
|
+
systemctl restart netbox netbox-rq
|
|
333
332
|
```
|
|
334
333
|
<!--install-end-->
|
|
335
334
|
<!--usage-start-->
|
|
336
335
|
## Usage
|
|
337
336
|
|
|
338
|
-
Read this [doc](
|
|
337
|
+
Read this [doc](https://miaow2.github.io/netbox-config-diff/colliecting-diffs/) about collecting diffs, for configuration management read [this](https://miaow2.github.io/netbox-config-diff/configuratiom-management/)
|
|
338
|
+
|
|
339
|
+
## Video
|
|
340
|
+
|
|
341
|
+
My presention about plugin at October NetBox community call (19.10.2023).
|
|
342
|
+
|
|
343
|
+
[](https://youtu.be/B4uhtYh278o?t=425)
|
|
339
344
|
<!--usage-end-->
|
|
340
345
|
|
|
341
346
|
## Screenshots
|
|
@@ -352,6 +357,14 @@ No diff
|
|
|
352
357
|
|
|
353
358
|

|
|
354
359
|
|
|
360
|
+
Configuration request
|
|
361
|
+
|
|
362
|
+

|
|
363
|
+
|
|
364
|
+
Completed Configuration request
|
|
365
|
+
|
|
366
|
+

|
|
367
|
+
|
|
355
368
|
## Credits
|
|
356
369
|
|
|
357
370
|
Based on the NetBox plugin tutorial:
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
[](https://github.com/netbox-community/netbox)
|
|
2
2
|
[](https://pypi.org/project/netbox-config-diff/)
|
|
3
3
|
[](https://badge.fury.io/py/netbox-config-diff)
|
|
4
|
-
[](https://github.com/ambv/black)
|
|
5
4
|
[](https://github.com/astral-sh/ruff)
|
|
6
5
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
7
6
|
[](https://github.com/miaow2/netbox-config-diff/actions)
|
|
@@ -78,6 +77,7 @@ PLUGINS_CONFIG = {
|
|
|
78
77
|
"netbox_config_diff": {
|
|
79
78
|
"USERNAME": "foo",
|
|
80
79
|
"PASSWORD": "bar",
|
|
80
|
+
"AUTH_SECONDARY": "foobar", # define here password for accessing Privileged EXEC mode, this variable is optional
|
|
81
81
|
},
|
|
82
82
|
}
|
|
83
83
|
```
|
|
@@ -97,13 +97,19 @@ python manage.py collectstatic --noinput
|
|
|
97
97
|
Restart NetBox service:
|
|
98
98
|
|
|
99
99
|
```bash
|
|
100
|
-
systemctl restart netbox
|
|
100
|
+
systemctl restart netbox netbox-rq
|
|
101
101
|
```
|
|
102
102
|
<!--install-end-->
|
|
103
103
|
<!--usage-start-->
|
|
104
104
|
## Usage
|
|
105
105
|
|
|
106
|
-
Read this [doc](
|
|
106
|
+
Read this [doc](https://miaow2.github.io/netbox-config-diff/colliecting-diffs/) about collecting diffs, for configuration management read [this](https://miaow2.github.io/netbox-config-diff/configuratiom-management/)
|
|
107
|
+
|
|
108
|
+
## Video
|
|
109
|
+
|
|
110
|
+
My presention about plugin at October NetBox community call (19.10.2023).
|
|
111
|
+
|
|
112
|
+
[](https://youtu.be/B4uhtYh278o?t=425)
|
|
107
113
|
<!--usage-end-->
|
|
108
114
|
|
|
109
115
|
## Screenshots
|
|
@@ -120,6 +126,14 @@ No diff
|
|
|
120
126
|
|
|
121
127
|

|
|
122
128
|
|
|
129
|
+
Configuration request
|
|
130
|
+
|
|
131
|
+

|
|
132
|
+
|
|
133
|
+
Completed Configuration request
|
|
134
|
+
|
|
135
|
+

|
|
136
|
+
|
|
123
137
|
## Credits
|
|
124
138
|
|
|
125
139
|
Based on the NetBox plugin tutorial:
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -2,7 +2,7 @@ from extras.plugins import PluginConfig
|
|
|
2
2
|
|
|
3
3
|
__author__ = "Artem Kotik"
|
|
4
4
|
__email__ = "miaow2@yandex.ru"
|
|
5
|
-
__version__ = "2.0
|
|
5
|
+
__version__ = "2.2.0"
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class ConfigDiffConfig(PluginConfig):
|
|
@@ -18,6 +18,7 @@ class ConfigDiffConfig(PluginConfig):
|
|
|
18
18
|
default_settings = {
|
|
19
19
|
"USER_SECRET_ROLE": "Username",
|
|
20
20
|
"PASSWORD_SECRET_ROLE": "Password",
|
|
21
|
+
"SECOND_AUTH_SECRET_ROLE": "Second Auth",
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
|
|
@@ -116,6 +116,11 @@ class ConfigurationRequestSerializer(NetBoxModelSerializer):
|
|
|
116
116
|
}:
|
|
117
117
|
raise ValidationError({"devices": f"Driver(s) not supported: {', '.join(drivers)}"})
|
|
118
118
|
|
|
119
|
+
if devices := list(filter(lambda x: x.get_config_template() is None, data["devices"])):
|
|
120
|
+
raise ValidationError(
|
|
121
|
+
{"devices": f"Define config template for device(s): {', '.join(d.name for d in devices)}"}
|
|
122
|
+
)
|
|
123
|
+
|
|
119
124
|
return super().validate(data)
|
|
120
125
|
|
|
121
126
|
|
|
@@ -6,38 +6,44 @@ from typing import Iterable, Iterator
|
|
|
6
6
|
from core.choices import DataSourceStatusChoices
|
|
7
7
|
from core.models import DataFile, DataSource
|
|
8
8
|
from dcim.choices import DeviceStatusChoices
|
|
9
|
-
from dcim.models import Device, Site
|
|
10
|
-
from django.
|
|
9
|
+
from dcim.models import Device, DeviceRole, Site
|
|
10
|
+
from django.conf import settings
|
|
11
11
|
from django.db.models import Q
|
|
12
|
-
from extras.scripts import MultiObjectVar, ObjectVar
|
|
12
|
+
from extras.scripts import MultiObjectVar, ObjectVar, TextVar
|
|
13
13
|
from jinja2.exceptions import TemplateError
|
|
14
14
|
from netutils.config.compliance import diff_network_config
|
|
15
15
|
from utilities.exceptions import AbortScript
|
|
16
|
+
from utilities.utils import render_jinja2
|
|
16
17
|
|
|
17
|
-
from netbox_config_diff.models import
|
|
18
|
+
from netbox_config_diff.models import ConplianceDeviceDataClass
|
|
18
19
|
|
|
19
|
-
from .models import DeviceDataClass
|
|
20
20
|
from .secrets import SecretsMixin
|
|
21
|
-
from .utils import PLATFORM_MAPPING, exclude_lines, get_unified_diff
|
|
21
|
+
from .utils import PLATFORM_MAPPING, CustomChoiceVar, exclude_lines, get_unified_diff
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
class ConfigDiffBase(SecretsMixin):
|
|
25
25
|
site = ObjectVar(
|
|
26
26
|
model=Site,
|
|
27
27
|
required=False,
|
|
28
|
-
description="Run compliance for devices (with
|
|
29
|
-
|
|
28
|
+
description="Run compliance for devices (with primary IP, platform) in this site",
|
|
29
|
+
)
|
|
30
|
+
role = ObjectVar(
|
|
31
|
+
model=DeviceRole,
|
|
32
|
+
required=False,
|
|
33
|
+
description="Run compliance for devices with this role",
|
|
30
34
|
)
|
|
31
35
|
devices = MultiObjectVar(
|
|
32
36
|
model=Device,
|
|
33
37
|
required=False,
|
|
34
38
|
query_params={
|
|
35
|
-
"status": DeviceStatusChoices.STATUS_ACTIVE,
|
|
36
39
|
"has_primary_ip": True,
|
|
37
40
|
"platform_id__n": "null",
|
|
38
|
-
"config_template_id__n": "null",
|
|
39
41
|
},
|
|
40
|
-
description="If you define devices in this field,
|
|
42
|
+
description="If you define devices in this field, Site, Role fields will be ignored",
|
|
43
|
+
)
|
|
44
|
+
status = CustomChoiceVar(
|
|
45
|
+
choices=DeviceStatusChoices,
|
|
46
|
+
default=DeviceStatusChoices.STATUS_ACTIVE,
|
|
41
47
|
)
|
|
42
48
|
data_source = ObjectVar(
|
|
43
49
|
model=DataSource,
|
|
@@ -47,6 +53,11 @@ class ConfigDiffBase(SecretsMixin):
|
|
|
47
53
|
},
|
|
48
54
|
description="Define synced DataSource, if you want compare configs stored in it wihout connecting to devices",
|
|
49
55
|
)
|
|
56
|
+
name_template = TextVar(
|
|
57
|
+
required=False,
|
|
58
|
+
description="Jinja2 template code for the device name in Data source. "
|
|
59
|
+
"Reference the object as <code>{{ object }}</code>.",
|
|
60
|
+
)
|
|
50
61
|
|
|
51
62
|
def run_script(self, data: dict) -> None:
|
|
52
63
|
devices = self.validate_data(data)
|
|
@@ -56,8 +67,8 @@ class ConfigDiffBase(SecretsMixin):
|
|
|
56
67
|
self.update_in_db(devices)
|
|
57
68
|
|
|
58
69
|
def validate_data(self, data: dict) -> Iterable[Device]:
|
|
59
|
-
if not data["site"] and not data["devices"]:
|
|
60
|
-
raise AbortScript("Define site or devices")
|
|
70
|
+
if not data["site"] and not data["role"] and not data["devices"]:
|
|
71
|
+
raise AbortScript("Define site, role or devices")
|
|
61
72
|
if data.get("data_source") and data["data_source"].status != DataSourceStatusChoices.COMPLETED:
|
|
62
73
|
raise AbortScript("Define synced DataSource")
|
|
63
74
|
|
|
@@ -66,23 +77,29 @@ class ConfigDiffBase(SecretsMixin):
|
|
|
66
77
|
devices = (
|
|
67
78
|
data["devices"]
|
|
68
79
|
.filter(
|
|
69
|
-
status=
|
|
80
|
+
status=data["status"],
|
|
70
81
|
platform__platform_setting__isnull=False,
|
|
71
|
-
config_template__isnull=False,
|
|
72
82
|
)
|
|
73
83
|
.exclude(
|
|
74
84
|
Q(primary_ip4__isnull=True) & Q(primary_ip6__isnull=True),
|
|
75
85
|
)
|
|
76
86
|
)
|
|
77
87
|
else:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
filters = {
|
|
89
|
+
"status": data["status"],
|
|
90
|
+
"platform__platform_setting__isnull": False,
|
|
91
|
+
}
|
|
92
|
+
if data["site"]:
|
|
93
|
+
filters["site"] = data["site"]
|
|
94
|
+
elif data["role"]:
|
|
95
|
+
if settings.VERSION.split(".", 1)[1].startswith("5"):
|
|
96
|
+
filters["device_role"] = data["role"]
|
|
97
|
+
else:
|
|
98
|
+
filters["role"] = data["role"]
|
|
99
|
+
devices = Device.objects.filter(**filters).exclude(
|
|
84
100
|
Q(primary_ip4__isnull=True) & Q(primary_ip6__isnull=True),
|
|
85
101
|
)
|
|
102
|
+
|
|
86
103
|
if data.get("devices"):
|
|
87
104
|
if qs_diff := data["devices"].difference(devices):
|
|
88
105
|
platforms = {d.platform.name for d in qs_diff}
|
|
@@ -90,24 +107,18 @@ class ConfigDiffBase(SecretsMixin):
|
|
|
90
107
|
|
|
91
108
|
if not devices:
|
|
92
109
|
self.log_warning(
|
|
93
|
-
"No matching devices found, devices must have status
|
|
110
|
+
"No matching devices found, devices must have status primary IP, platform and platformsetting"
|
|
94
111
|
)
|
|
95
112
|
else:
|
|
96
113
|
self.log_info(f"Working with device(s): {', '.join(d.name for d in devices)}")
|
|
97
114
|
return devices
|
|
98
115
|
|
|
99
|
-
def update_in_db(self, devices: list[
|
|
116
|
+
def update_in_db(self, devices: list[ConplianceDeviceDataClass]) -> None:
|
|
100
117
|
for device in devices:
|
|
101
118
|
self.log_results(device)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
obj.update(**device.to_db())
|
|
106
|
-
obj.save()
|
|
107
|
-
except ObjectDoesNotExist:
|
|
108
|
-
ConfigCompliance.objects.create(**device.to_db())
|
|
109
|
-
|
|
110
|
-
def log_results(self, device: DeviceDataClass) -> None:
|
|
119
|
+
device.send_to_db()
|
|
120
|
+
|
|
121
|
+
def log_results(self, device: ConplianceDeviceDataClass) -> None:
|
|
111
122
|
if device.error:
|
|
112
123
|
self.log_failure(f"{device.name} errored")
|
|
113
124
|
elif device.diff:
|
|
@@ -115,11 +126,11 @@ class ConfigDiffBase(SecretsMixin):
|
|
|
115
126
|
else:
|
|
116
127
|
self.log_success(f"{device.name} no diff")
|
|
117
128
|
|
|
118
|
-
def get_devices_with_rendered_configs(self, devices: Iterable[Device]) -> Iterator[
|
|
129
|
+
def get_devices_with_rendered_configs(self, devices: Iterable[Device]) -> Iterator[ConplianceDeviceDataClass]:
|
|
119
130
|
self.check_netbox_secrets()
|
|
120
131
|
self.substitutes = {}
|
|
121
132
|
for device in devices:
|
|
122
|
-
username, password = self.get_credentials(device)
|
|
133
|
+
username, password, auth_secondary = self.get_credentials(device)
|
|
123
134
|
rendered_config = None
|
|
124
135
|
error = None
|
|
125
136
|
context_data = device.get_config_context()
|
|
@@ -138,7 +149,7 @@ class ConfigDiffBase(SecretsMixin):
|
|
|
138
149
|
if substitutes := device.platform.platform_setting.substitutes.all():
|
|
139
150
|
self.substitutes[platform] = [s.regexp for s in substitutes]
|
|
140
151
|
|
|
141
|
-
yield
|
|
152
|
+
yield ConplianceDeviceDataClass(
|
|
142
153
|
pk=device.pk,
|
|
143
154
|
name=device.name,
|
|
144
155
|
mgmt_ip=str(device.primary_ip.address.ip),
|
|
@@ -147,28 +158,38 @@ class ConfigDiffBase(SecretsMixin):
|
|
|
147
158
|
exclude_regex=device.platform.platform_setting.exclude_regex,
|
|
148
159
|
username=username,
|
|
149
160
|
password=password,
|
|
161
|
+
auth_secondary=auth_secondary,
|
|
150
162
|
rendered_config=rendered_config,
|
|
151
163
|
error=error,
|
|
164
|
+
device=device,
|
|
152
165
|
)
|
|
153
166
|
|
|
154
|
-
def get_config_from_datasource(self, devices: list[
|
|
167
|
+
def get_config_from_datasource(self, devices: list[ConplianceDeviceDataClass]) -> None:
|
|
155
168
|
for device in devices:
|
|
156
|
-
if
|
|
169
|
+
if self.data["name_template"]:
|
|
170
|
+
try:
|
|
171
|
+
device_name = render_jinja2(self.data["name_template"], {"object": device.device}).strip()
|
|
172
|
+
except Exception as e:
|
|
173
|
+
self.log_failure(f"Error in rendering data source name for {device.name}: {e}, using device name.")
|
|
174
|
+
device_name = device.name
|
|
175
|
+
else:
|
|
176
|
+
device_name = device.name
|
|
177
|
+
if df := DataFile.objects.filter(source=self.data["data_source"], path__icontains=device_name).first():
|
|
157
178
|
if config := df.data_as_string:
|
|
158
179
|
device.actual_config = config
|
|
159
180
|
else:
|
|
160
181
|
device.error = f"Data in file {df} is broken, skiping device {device.name}"
|
|
161
182
|
else:
|
|
162
|
-
device.error = f"Not found file in DataSource for
|
|
183
|
+
device.error = f"Not found file in DataSource for name {device_name}"
|
|
163
184
|
|
|
164
|
-
def get_actual_configs(self, devices: list[
|
|
185
|
+
def get_actual_configs(self, devices: list[ConplianceDeviceDataClass]) -> None:
|
|
165
186
|
if self.data["data_source"]:
|
|
166
187
|
self.get_config_from_datasource(devices)
|
|
167
188
|
else:
|
|
168
189
|
loop = asyncio.get_event_loop()
|
|
169
190
|
loop.run_until_complete(asyncio.gather(*(d.get_actual_config() for d in devices)))
|
|
170
191
|
|
|
171
|
-
def get_diff(self, devices: list[
|
|
192
|
+
def get_diff(self, devices: list[ConplianceDeviceDataClass]) -> None:
|
|
172
193
|
for device in devices:
|
|
173
194
|
if device.error is not None:
|
|
174
195
|
continue
|
{netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/compliance/secrets.py
RENAMED
|
@@ -15,9 +15,9 @@ class SecretsMixin:
|
|
|
15
15
|
|
|
16
16
|
def get_session_key(self) -> None:
|
|
17
17
|
if "netbox_secrets_sessionid" in self.request.COOKIES:
|
|
18
|
-
self.session_key = base64.b64decode(self.request.COOKIES[
|
|
18
|
+
self.session_key = base64.b64decode(self.request.COOKIES["netbox_secrets_sessionid"])
|
|
19
19
|
elif "HTTP_X_SESSION_KEY" in self.request.META:
|
|
20
|
-
self.session_key = base64.b64decode(self.request.META[
|
|
20
|
+
self.session_key = base64.b64decode(self.request.META["HTTP_X_SESSION_KEY"])
|
|
21
21
|
else:
|
|
22
22
|
self.session_key = None
|
|
23
23
|
|
|
@@ -45,24 +45,33 @@ class SecretsMixin:
|
|
|
45
45
|
return None
|
|
46
46
|
return secret.plaintext
|
|
47
47
|
|
|
48
|
-
def get_credentials(self, device: Device) -> tuple[str, str]:
|
|
49
|
-
if self.netbox_secrets_installed:
|
|
50
|
-
|
|
51
|
-
if value := self.get_secret(secret):
|
|
52
|
-
username = value
|
|
53
|
-
if secret := device.secrets.filter(role__name=self.password_role).first():
|
|
54
|
-
if value := self.get_secret(secret):
|
|
55
|
-
password = value
|
|
56
|
-
return username, password
|
|
48
|
+
def get_credentials(self, device: Device) -> tuple[str, str, str]:
|
|
49
|
+
if not self.netbox_secrets_installed:
|
|
50
|
+
return self.username, self.password, self.auth_secondary
|
|
57
51
|
|
|
58
|
-
|
|
52
|
+
if secret := device.secrets.filter(role__name=self.user_role).first():
|
|
53
|
+
username = value if (value := self.get_secret(secret)) else self.username
|
|
54
|
+
else:
|
|
55
|
+
username = self.username
|
|
56
|
+
if secret := device.secrets.filter(role__name=self.password_role).first():
|
|
57
|
+
password = value if (value := self.get_secret(secret)) else self.password
|
|
58
|
+
else:
|
|
59
|
+
password = self.password
|
|
60
|
+
if secret := device.secrets.filter(role__name=self.auth_secondary_role).first():
|
|
61
|
+
auth_secondary = value if (value := self.get_secret(secret)) else self.auth_secondary
|
|
62
|
+
else:
|
|
63
|
+
auth_secondary = self.auth_secondary
|
|
64
|
+
|
|
65
|
+
return username, password, auth_secondary
|
|
59
66
|
|
|
60
67
|
def check_netbox_secrets(self) -> None:
|
|
61
68
|
if "netbox_secrets" in get_installed_plugins():
|
|
62
69
|
self.get_master_key()
|
|
63
70
|
self.user_role = get_plugin_config("netbox_config_diff", "USER_SECRET_ROLE")
|
|
64
71
|
self.password_role = get_plugin_config("netbox_config_diff", "PASSWORD_SECRET_ROLE")
|
|
72
|
+
self.auth_secondary_role = get_plugin_config("netbox_config_diff", "SECOND_AUTH_SECRET_ROLE")
|
|
65
73
|
self.netbox_secrets_installed = True
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
|
|
75
|
+
self.username = get_plugin_config("netbox_config_diff", "USERNAME")
|
|
76
|
+
self.password = get_plugin_config("netbox_config_diff", "PASSWORD")
|
|
77
|
+
self.auth_secondary = get_plugin_config("netbox_config_diff", "AUTH_SECONDARY")
|
{netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/compliance/utils.py
RENAMED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from difflib import unified_diff
|
|
3
3
|
|
|
4
|
+
from django.forms import ChoiceField
|
|
5
|
+
from extras.scripts import ScriptVariable
|
|
6
|
+
|
|
4
7
|
PLATFORM_MAPPING = {
|
|
5
8
|
"arista_eos": "arista_eos",
|
|
6
9
|
"cisco_aireos": "cisco_aireos",
|
|
@@ -17,6 +20,14 @@ PLATFORM_MAPPING = {
|
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
|
|
23
|
+
class CustomChoiceVar(ScriptVariable):
|
|
24
|
+
form_field = ChoiceField
|
|
25
|
+
|
|
26
|
+
def __init__(self, choices, *args, **kwargs):
|
|
27
|
+
super().__init__(*args, **kwargs)
|
|
28
|
+
self.field_attrs["choices"] = choices
|
|
29
|
+
|
|
30
|
+
|
|
20
31
|
def get_unified_diff(rendered_config: str, actual_config: str, device: str) -> str:
|
|
21
32
|
diff = unified_diff(
|
|
22
33
|
rendered_config.strip().splitlines(),
|
{netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/configurator/base.py
RENAMED
|
@@ -13,13 +13,12 @@ from scrapli_cfg.platform.base.async_platform import AsyncScrapliCfgPlatform
|
|
|
13
13
|
from scrapli_cfg.response import ScrapliCfgResponse
|
|
14
14
|
from utilities.utils import NetBoxFakeRequest
|
|
15
15
|
|
|
16
|
-
from netbox_config_diff.compliance.models import DeviceDataClass
|
|
17
16
|
from netbox_config_diff.compliance.secrets import SecretsMixin
|
|
18
17
|
from netbox_config_diff.compliance.utils import PLATFORM_MAPPING, get_unified_diff
|
|
19
18
|
from netbox_config_diff.configurator.exceptions import DeviceConfigurationError, DeviceValidationError
|
|
20
19
|
from netbox_config_diff.configurator.utils import CustomLogger
|
|
21
20
|
from netbox_config_diff.constants import ACCEPTABLE_DRIVERS
|
|
22
|
-
from netbox_config_diff.models import
|
|
21
|
+
from netbox_config_diff.models import ConfiguratorDeviceDataClass
|
|
23
22
|
|
|
24
23
|
from .factory import AsyncScrapliCfg
|
|
25
24
|
|
|
@@ -28,9 +27,9 @@ class Configurator(SecretsMixin):
|
|
|
28
27
|
def __init__(self, devices: Iterable[Device], request: NetBoxFakeRequest) -> None:
|
|
29
28
|
self.devices = devices
|
|
30
29
|
self.request = request
|
|
31
|
-
self.unprocessed_devices = set()
|
|
32
|
-
self.processed_devices = set()
|
|
33
|
-
self.failed_devices = set()
|
|
30
|
+
self.unprocessed_devices: set[ConfiguratorDeviceDataClass] = set()
|
|
31
|
+
self.processed_devices: set[ConfiguratorDeviceDataClass] = set()
|
|
32
|
+
self.failed_devices: set[ConfiguratorDeviceDataClass] = set()
|
|
34
33
|
self.substitutes: dict[str, list] = {}
|
|
35
34
|
self.logger = CustomLogger()
|
|
36
35
|
self.connections: dict[str, AsyncScrapliCfgPlatform] = {}
|
|
@@ -38,7 +37,7 @@ class Configurator(SecretsMixin):
|
|
|
38
37
|
def validate_devices(self) -> None:
|
|
39
38
|
self.check_netbox_secrets()
|
|
40
39
|
for device in self.devices:
|
|
41
|
-
username, password = self.get_credentials(device)
|
|
40
|
+
username, password, auth_secondary = self.get_credentials(device)
|
|
42
41
|
if device.platform.platform_setting is None:
|
|
43
42
|
self.logger.log_warning(f"Skipping {device}, add PlatformSetting for {device.platform} platform")
|
|
44
43
|
elif device.platform.platform_setting.driver not in ACCEPTABLE_DRIVERS:
|
|
@@ -60,13 +59,14 @@ class Configurator(SecretsMixin):
|
|
|
60
59
|
error = "Define config template for device"
|
|
61
60
|
self.logger.log_failure(error)
|
|
62
61
|
|
|
63
|
-
d =
|
|
62
|
+
d = ConfiguratorDeviceDataClass(
|
|
64
63
|
pk=device.pk,
|
|
65
64
|
name=device.name,
|
|
66
65
|
mgmt_ip=str(device.primary_ip.address.ip),
|
|
67
66
|
platform=device.platform.platform_setting.driver,
|
|
68
67
|
username=username,
|
|
69
68
|
password=password,
|
|
69
|
+
auth_secondary=auth_secondary,
|
|
70
70
|
rendered_config=rendered_config,
|
|
71
71
|
error=error,
|
|
72
72
|
)
|
|
@@ -109,20 +109,14 @@ class Configurator(SecretsMixin):
|
|
|
109
109
|
@sync_to_async
|
|
110
110
|
def update_diffs(self) -> None:
|
|
111
111
|
for device in self.unprocessed_devices:
|
|
112
|
-
|
|
113
|
-
obj = ConfigCompliance.objects.get(device_id=device.pk)
|
|
114
|
-
obj.snapshot()
|
|
115
|
-
obj.update(**device.to_db())
|
|
116
|
-
obj.save()
|
|
117
|
-
except ConfigCompliance.DoesNotExist:
|
|
118
|
-
ConfigCompliance.objects.create(**device.to_db())
|
|
112
|
+
device.send_to_db()
|
|
119
113
|
|
|
120
114
|
async def _collect_diffs(self) -> None:
|
|
121
115
|
async with self.connection():
|
|
122
116
|
await asyncio.gather(*(self._collect_one_diff(d) for d in self.unprocessed_devices))
|
|
123
117
|
await self.update_diffs()
|
|
124
118
|
|
|
125
|
-
async def _collect_one_diff(self, device:
|
|
119
|
+
async def _collect_one_diff(self, device: ConfiguratorDeviceDataClass) -> None:
|
|
126
120
|
self.logger.log_info(f"Collecting diff on {device.name}")
|
|
127
121
|
try:
|
|
128
122
|
conn = self.connections[device.name]
|
|
@@ -168,7 +162,7 @@ class Configurator(SecretsMixin):
|
|
|
168
162
|
devices=", ".join(f"{d.name}: {d.config_error}" for d in self.failed_devices),
|
|
169
163
|
)
|
|
170
164
|
|
|
171
|
-
async def _push_one_config(self, device:
|
|
165
|
+
async def _push_one_config(self, device: ConfiguratorDeviceDataClass) -> None:
|
|
172
166
|
self.logger.log_info(f"Push config to {device.name}")
|
|
173
167
|
try:
|
|
174
168
|
conn = self.connections[device.name]
|
|
@@ -191,7 +185,11 @@ class Configurator(SecretsMixin):
|
|
|
191
185
|
self.failed_devices.add(device)
|
|
192
186
|
|
|
193
187
|
async def abort_config(
|
|
194
|
-
self,
|
|
188
|
+
self,
|
|
189
|
+
operation: str,
|
|
190
|
+
conn: AsyncScrapliCfgPlatform,
|
|
191
|
+
response: ScrapliCfgResponse,
|
|
192
|
+
device: ConfiguratorDeviceDataClass,
|
|
195
193
|
) -> None:
|
|
196
194
|
self.logger.log_failure(f"Failed to {operation} config on {device.name}: {response.result}")
|
|
197
195
|
device.config_error = response.result
|
|
@@ -204,7 +202,7 @@ class Configurator(SecretsMixin):
|
|
|
204
202
|
self.logger.log_info(f"Rollback config: {', '.join(d.name for d in self.processed_devices)}")
|
|
205
203
|
await asyncio.gather(*(self._rollback_one(d) for d in self.processed_devices))
|
|
206
204
|
|
|
207
|
-
async def _rollback_one(self, device:
|
|
205
|
+
async def _rollback_one(self, device: ConfiguratorDeviceDataClass) -> None:
|
|
208
206
|
conn = self.connections[device.name]
|
|
209
207
|
await conn.load_config(config=device.actual_config, replace=True)
|
|
210
208
|
await conn.commit_config()
|
{netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/configurator/utils.py
RENAMED
|
@@ -15,7 +15,7 @@ class CustomLogger:
|
|
|
15
15
|
raise Exception(f"Unknown logging level: {log_level}")
|
|
16
16
|
if log_level is None:
|
|
17
17
|
log_level = LogLevelChoices.LOG_DEFAULT
|
|
18
|
-
self.log_data.append((timezone.now().strftime(
|
|
18
|
+
self.log_data.append((timezone.now().strftime("%Y-%m-%d %H:%M:%S"), log_level, message))
|
|
19
19
|
|
|
20
20
|
def log(self, message: str) -> None:
|
|
21
21
|
self._log(message, log_level=LogLevelChoices.LOG_DEFAULT)
|
|
@@ -89,7 +89,6 @@ class ConfigurationRequestForm(NetBoxModelForm):
|
|
|
89
89
|
"status": DeviceStatusChoices.STATUS_ACTIVE,
|
|
90
90
|
"has_primary_ip": True,
|
|
91
91
|
"platform_id__n": "null",
|
|
92
|
-
"config_template_id__n": "null",
|
|
93
92
|
},
|
|
94
93
|
)
|
|
95
94
|
created_by = forms.ModelChoiceField(
|
|
@@ -122,6 +121,11 @@ class ConfigurationRequestForm(NetBoxModelForm):
|
|
|
122
121
|
}:
|
|
123
122
|
raise forms.ValidationError({"devices": f"Driver(s) not supported: {', '.join(drivers)}"})
|
|
124
123
|
|
|
124
|
+
if devices := list(filter(lambda x: x.get_config_template() is None, self.cleaned_data["devices"])):
|
|
125
|
+
raise forms.ValidationError(
|
|
126
|
+
{"devices": f"Define config template for device(s): {', '.join(d.name for d in devices)}"}
|
|
127
|
+
)
|
|
128
|
+
|
|
125
129
|
|
|
126
130
|
class ConfigurationRequestFilterForm(NetBoxModelFilterSetForm):
|
|
127
131
|
model = ConfigurationRequest
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from django.db import migrations, models
|
|
2
|
+
import django.db.models.deletion
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Migration(migrations.Migration):
|
|
6
|
+
dependencies = [
|
|
7
|
+
("dcim", "0181_rename_device_role_device_role"),
|
|
8
|
+
("netbox_config_diff", "0007_configurationrequest"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.AlterField(
|
|
13
|
+
model_name="configcompliance",
|
|
14
|
+
name="device",
|
|
15
|
+
field=models.OneToOneField(
|
|
16
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
17
|
+
related_name="config_compliance",
|
|
18
|
+
to="dcim.device",
|
|
19
|
+
),
|
|
20
|
+
),
|
|
21
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .data_models import ConfiguratorDeviceDataClass, ConplianceDeviceDataClass
|
|
2
|
+
from .models import ConfigCompliance, ConfigurationRequest, PlatformSetting, Substitute
|
|
3
|
+
|
|
4
|
+
__all__ = (
|
|
5
|
+
"ConfigCompliance",
|
|
6
|
+
"ConfigurationRequest",
|
|
7
|
+
"ConfiguratorDeviceDataClass",
|
|
8
|
+
"ConplianceDeviceDataClass",
|
|
9
|
+
"PlatformSetting",
|
|
10
|
+
"Substitute",
|
|
11
|
+
)
|