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.
Files changed (104) hide show
  1. {netbox-config-diff-2.0.1/netbox_config_diff.egg-info → netbox-config-diff-2.2.0}/PKG-INFO +20 -7
  2. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/README.md +17 -3
  3. netbox-config-diff-2.2.0/docs/media/screenshots/cr-completed.png +0 -0
  4. netbox-config-diff-2.2.0/docs/media/screenshots/cr-created.png +0 -0
  5. netbox-config-diff-2.2.0/docs/media/screenshots/navbar.png +0 -0
  6. netbox-config-diff-2.2.0/docs/media/screenshots/script.png +0 -0
  7. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/__init__.py +2 -1
  8. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/api/serializers.py +5 -0
  9. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/compliance/base.py +61 -40
  10. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/compliance/secrets.py +24 -15
  11. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/compliance/utils.py +11 -0
  12. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/configurator/base.py +16 -18
  13. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/configurator/utils.py +1 -1
  14. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/forms.py +5 -1
  15. netbox-config-diff-2.2.0/netbox_config_diff/migrations/0008_alter_configcompliance_device.py +21 -0
  16. netbox-config-diff-2.2.0/netbox_config_diff/models/__init__.py +11 -0
  17. netbox-config-diff-2.2.0/netbox_config_diff/models/base.py +10 -0
  18. 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
  19. {netbox-config-diff-2.0.1/netbox_config_diff → netbox-config-diff-2.2.0/netbox_config_diff/models}/models.py +26 -36
  20. netbox-config-diff-2.2.0/netbox_config_diff/navigation.py +47 -0
  21. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/templates/netbox_config_diff/configcompliance/config.html +1 -1
  22. 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
  23. {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
  24. 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
  25. netbox-config-diff-2.2.0/netbox_config_diff/views/base.py +13 -0
  26. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/views/compliance.py +46 -4
  27. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/views/configuration.py +7 -5
  28. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0/netbox_config_diff.egg-info}/PKG-INFO +20 -7
  29. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff.egg-info/SOURCES.txt +9 -3
  30. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff.egg-info/requires.txt +1 -2
  31. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/pyproject.toml +1 -12
  32. netbox-config-diff-2.2.0/requirements/dev.txt +1 -0
  33. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/tests/conftest.py +9 -6
  34. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/tests/test_compliance.py +7 -5
  35. netbox-config-diff-2.0.1/docs/media/screenshots/navbar.png +0 -0
  36. netbox-config-diff-2.0.1/docs/media/screenshots/script.png +0 -0
  37. netbox-config-diff-2.0.1/netbox_config_diff/navigation.py +0 -61
  38. netbox-config-diff-2.0.1/requirements/dev.txt +0 -2
  39. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/LICENSE +0 -0
  40. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/MANIFEST.in +0 -0
  41. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/development/configuration.py +0 -0
  42. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/compliance-diff.png +0 -0
  43. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/compliance-error.png +0 -0
  44. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/compliance-list.png +0 -0
  45. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/compliance-missing-extra.png +0 -0
  46. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/compliance-ok.png +0 -0
  47. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/config-temp-substitute.png +0 -0
  48. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/cr-approve-button.png +0 -0
  49. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/cr-approved.png +0 -0
  50. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/cr-collecting-diff-button.png +0 -0
  51. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/cr-diffs-tab.png +0 -0
  52. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/cr-job-log.png +0 -0
  53. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/cr-schedule-button.png +0 -0
  54. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/cr-scheduled.png +0 -0
  55. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/cr-unapprove-button.png +0 -0
  56. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/cr-unschedule-button.png +0 -0
  57. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/platformsetting.png +0 -0
  58. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/render-temp-substitute.png +0 -0
  59. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/script-list.png +0 -0
  60. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/docs/media/screenshots/substitute.png +0 -0
  61. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/api/__init__.py +0 -0
  62. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/api/urls.py +0 -0
  63. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/api/views.py +0 -0
  64. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/choices.py +0 -0
  65. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/compliance/__init__.py +0 -0
  66. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/configurator/__init__.py +0 -0
  67. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/configurator/exceptions.py +0 -0
  68. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/configurator/factory.py +0 -0
  69. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/configurator/platforms.py +0 -0
  70. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/constants.py +0 -0
  71. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/filtersets.py +0 -0
  72. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/graphql.py +0 -0
  73. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/jobs.py +0 -0
  74. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/migrations/0001_initial.py +0 -0
  75. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/migrations/0002_add_script.py +0 -0
  76. {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
  77. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/migrations/0004_update_script.py +0 -0
  78. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/migrations/0005_configcompliance_extra_missing.py +0 -0
  79. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/migrations/0006_substitute.py +0 -0
  80. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/migrations/0007_configurationrequest.py +0 -0
  81. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/migrations/__init__.py +0 -0
  82. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/scripts/config_diff.py +0 -0
  83. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/search.py +0 -0
  84. {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
  85. {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
  86. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/static/netbox_config_diff/diff2html.min.css +0 -0
  87. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/tables.py +0 -0
  88. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/templates/netbox_config_diff/configurationrequest/base.html +0 -0
  89. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/templates/netbox_config_diff/configurationrequest/diffs.html +0 -0
  90. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/templates/netbox_config_diff/configurationrequest.html +0 -0
  91. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/templates/netbox_config_diff/inc/diff.html +0 -0
  92. {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
  93. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/templates/netbox_config_diff/platformsetting.html +0 -0
  94. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/templates/netbox_config_diff/substitute.html +0 -0
  95. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/urls.py +0 -0
  96. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff/views/__init__.py +0 -0
  97. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff.egg-info/dependency_links.txt +0 -0
  98. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/netbox_config_diff.egg-info/top_level.txt +0 -0
  99. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/requirements/base.txt +0 -0
  100. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/requirements/test.txt +0 -0
  101. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/setup.cfg +0 -0
  102. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/tests/factories.py +0 -0
  103. {netbox-config-diff-2.0.1 → netbox-config-diff-2.2.0}/tests/test_compliance_utils.py +0 -0
  104. {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.1
4
- Summary: Find diff between the intended device configuration and actual.
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: black==23.10.0; extra == "dev"
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
  [![NetBox version](https://img.shields.io/badge/NetBox-3.5|3.6-blue.svg)](https://github.com/netbox-community/netbox)
234
233
  [![Supported Versions](https://img.shields.io/pypi/pyversions/netbox-config-diff.svg)](https://pypi.org/project/netbox-config-diff/)
235
234
  [![PyPI version](https://badge.fury.io/py/netbox-config-diff.svg)](https://badge.fury.io/py/netbox-config-diff)
236
- [![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
237
235
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
238
236
  [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
239
237
  [![CI](https://github.com/miaow2/netbox-config-diff/actions/workflows/commit.yaml/badge.svg?branch=develop)](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](docs/colliecting-diffs.md) about collecting diffs, for configuration management read [this](docs/configuratiom-management.md)
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
+ [![October NetBox community call](https://img.youtube.com/vi/B4uhtYh278o/0.jpg)](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
  ![Screenshot of the compliance ok](docs/media/screenshots/compliance-ok.png)
354
359
 
360
+ Configuration request
361
+
362
+ ![Screenshot of the CR](docs/media/screenshots/cr-created.png)
363
+
364
+ Completed Configuration request
365
+
366
+ ![Screenshot of the completed CR](docs/media/screenshots/cr-completed.png)
367
+
355
368
  ## Credits
356
369
 
357
370
  Based on the NetBox plugin tutorial:
@@ -1,7 +1,6 @@
1
1
  [![NetBox version](https://img.shields.io/badge/NetBox-3.5|3.6-blue.svg)](https://github.com/netbox-community/netbox)
2
2
  [![Supported Versions](https://img.shields.io/pypi/pyversions/netbox-config-diff.svg)](https://pypi.org/project/netbox-config-diff/)
3
3
  [![PyPI version](https://badge.fury.io/py/netbox-config-diff.svg)](https://badge.fury.io/py/netbox-config-diff)
4
- [![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
5
4
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
6
5
  [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
7
6
  [![CI](https://github.com/miaow2/netbox-config-diff/actions/workflows/commit.yaml/badge.svg?branch=develop)](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](docs/colliecting-diffs.md) about collecting diffs, for configuration management read [this](docs/configuratiom-management.md)
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
+ [![October NetBox community call](https://img.youtube.com/vi/B4uhtYh278o/0.jpg)](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
  ![Screenshot of the compliance ok](docs/media/screenshots/compliance-ok.png)
122
128
 
129
+ Configuration request
130
+
131
+ ![Screenshot of the CR](docs/media/screenshots/cr-created.png)
132
+
133
+ Completed Configuration request
134
+
135
+ ![Screenshot of the completed CR](docs/media/screenshots/cr-completed.png)
136
+
123
137
  ## Credits
124
138
 
125
139
  Based on the NetBox plugin tutorial:
@@ -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.1"
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.core.exceptions import ObjectDoesNotExist
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 ConfigCompliance
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 status Active, "
29
- "primary IP, platform and config template) in this site",
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, the Site field will be ignored",
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=DeviceStatusChoices.STATUS_ACTIVE,
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
- devices = Device.objects.filter(
79
- site=data["site"],
80
- status=DeviceStatusChoices.STATUS_ACTIVE,
81
- platform__platform_setting__isnull=False,
82
- config_template__isnull=False,
83
- ).exclude(
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 `Active`, primary IP, platform and platformsetting"
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[DeviceDataClass]) -> None:
116
+ def update_in_db(self, devices: list[ConplianceDeviceDataClass]) -> None:
100
117
  for device in devices:
101
118
  self.log_results(device)
102
- try:
103
- obj = ConfigCompliance.objects.get(device_id=device.pk)
104
- obj.snapshot()
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[DeviceDataClass]:
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 DeviceDataClass(
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[DeviceDataClass]) -> None:
167
+ def get_config_from_datasource(self, devices: list[ConplianceDeviceDataClass]) -> None:
155
168
  for device in devices:
156
- if df := DataFile.objects.filter(source=self.data["data_source"], path__icontains=device.name).first():
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 device {device.name}"
183
+ device.error = f"Not found file in DataSource for name {device_name}"
163
184
 
164
- def get_actual_configs(self, devices: list[DeviceDataClass]) -> None:
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[DeviceDataClass]) -> None:
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
@@ -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['netbox_secrets_sessionid'])
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['HTTP_X_SESSION_KEY'])
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
- if secret := device.secrets.filter(role__name=self.user_role).first():
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
- return self.username, self.password
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
- else:
67
- self.username = get_plugin_config("netbox_config_diff", "USERNAME")
68
- self.password = get_plugin_config("netbox_config_diff", "PASSWORD")
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")
@@ -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(),
@@ -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 ConfigCompliance
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 = DeviceDataClass(
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
- try:
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: DeviceDataClass) -> None:
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: DeviceDataClass) -> None:
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, operation: str, conn: AsyncScrapliCfgPlatform, response: ScrapliCfgResponse, device: DeviceDataClass
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: DeviceDataClass) -> None:
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()
@@ -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('%Y-%m-%d %H:%M:%S'), log_level, message))
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
+ )
@@ -0,0 +1,10 @@
1
+ from django.db import models
2
+ from django.urls import reverse
3
+
4
+
5
+ class AbsoluteURLMixin(models.Model):
6
+ class Meta:
7
+ abstract = True
8
+
9
+ def get_absolute_url(self):
10
+ return reverse(f"plugins:netbox_config_diff:{self._meta.model_name}", args=[self.pk])