netbox-config-diff 2.2.0__tar.gz → 2.4.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 (106) hide show
  1. {netbox-config-diff-2.2.0/netbox_config_diff.egg-info → netbox_config_diff-2.4.0}/PKG-INFO +3 -2
  2. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/README.md +1 -1
  3. netbox_config_diff-2.4.0/docs/media/screenshots/compliance-patch.png +0 -0
  4. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/__init__.py +1 -1
  5. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/api/serializers.py +1 -0
  6. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/compliance/base.py +4 -1
  7. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/compliance/utils.py +18 -1
  8. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/configurator/base.py +5 -2
  9. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/configurator/platforms.py +70 -3
  10. netbox_config_diff-2.4.0/netbox_config_diff/migrations/0009_configcompliance_patch.py +16 -0
  11. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/models/data_models.py +2 -0
  12. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/models/models.py +3 -0
  13. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/templates/netbox_config_diff/configcompliance/config.html +2 -1
  14. netbox_config_diff-2.4.0/netbox_config_diff/templates/netbox_config_diff/configcompliance/missing_extra.html +14 -0
  15. netbox_config_diff-2.4.0/netbox_config_diff/templates/netbox_config_diff/configcompliance/patch.html +11 -0
  16. netbox_config_diff-2.4.0/netbox_config_diff/templates/netbox_config_diff/inc/commands_card.html +16 -0
  17. netbox_config_diff-2.4.0/netbox_config_diff/views/base.py +52 -0
  18. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/views/compliance.py +29 -40
  19. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0/netbox_config_diff.egg-info}/PKG-INFO +3 -2
  20. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff.egg-info/SOURCES.txt +4 -0
  21. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff.egg-info/requires.txt +1 -0
  22. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/requirements/base.txt +1 -0
  23. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/tests/test_compliance.py +1 -0
  24. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/tests/test_compliance_utils.py +1 -1
  25. netbox-config-diff-2.2.0/netbox_config_diff/templates/netbox_config_diff/configcompliance/missing_extra.html +0 -42
  26. netbox-config-diff-2.2.0/netbox_config_diff/views/base.py +0 -13
  27. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/LICENSE +0 -0
  28. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/MANIFEST.in +0 -0
  29. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/development/configuration.py +0 -0
  30. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/compliance-diff.png +0 -0
  31. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/compliance-error.png +0 -0
  32. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/compliance-list.png +0 -0
  33. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/compliance-missing-extra.png +0 -0
  34. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/compliance-ok.png +0 -0
  35. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/config-temp-substitute.png +0 -0
  36. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/cr-approve-button.png +0 -0
  37. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/cr-approved.png +0 -0
  38. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/cr-collecting-diff-button.png +0 -0
  39. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/cr-completed.png +0 -0
  40. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/cr-created.png +0 -0
  41. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/cr-diffs-tab.png +0 -0
  42. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/cr-job-log.png +0 -0
  43. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/cr-schedule-button.png +0 -0
  44. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/cr-scheduled.png +0 -0
  45. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/cr-unapprove-button.png +0 -0
  46. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/cr-unschedule-button.png +0 -0
  47. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/navbar.png +0 -0
  48. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/platformsetting.png +0 -0
  49. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/render-temp-substitute.png +0 -0
  50. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/script-list.png +0 -0
  51. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/script.png +0 -0
  52. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/docs/media/screenshots/substitute.png +0 -0
  53. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/api/__init__.py +0 -0
  54. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/api/urls.py +0 -0
  55. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/api/views.py +0 -0
  56. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/choices.py +0 -0
  57. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/compliance/__init__.py +0 -0
  58. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/compliance/secrets.py +0 -0
  59. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/configurator/__init__.py +0 -0
  60. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/configurator/exceptions.py +0 -0
  61. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/configurator/factory.py +0 -0
  62. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/configurator/utils.py +0 -0
  63. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/constants.py +0 -0
  64. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/filtersets.py +0 -0
  65. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/forms.py +0 -0
  66. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/graphql.py +0 -0
  67. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/jobs.py +0 -0
  68. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/migrations/0001_initial.py +0 -0
  69. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/migrations/0002_add_script.py +0 -0
  70. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/migrations/0003_configcompliance_actual_config_and_more.py +0 -0
  71. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/migrations/0004_update_script.py +0 -0
  72. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/migrations/0005_configcompliance_extra_missing.py +0 -0
  73. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/migrations/0006_substitute.py +0 -0
  74. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/migrations/0007_configurationrequest.py +0 -0
  75. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/migrations/0008_alter_configcompliance_device.py +0 -0
  76. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/migrations/__init__.py +0 -0
  77. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/models/__init__.py +0 -0
  78. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/models/base.py +0 -0
  79. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/navigation.py +0 -0
  80. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/scripts/config_diff.py +0 -0
  81. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/search.py +0 -0
  82. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/static/netbox_config_diff/diff2html-ui.min.js +0 -0
  83. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/static/netbox_config_diff/diff2html.dark.min.css +0 -0
  84. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/static/netbox_config_diff/diff2html.min.css +0 -0
  85. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/tables.py +0 -0
  86. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/templates/netbox_config_diff/configcompliance/data.html +0 -0
  87. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/templates/netbox_config_diff/configcompliance.html +0 -0
  88. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/templates/netbox_config_diff/configurationrequest/base.html +0 -0
  89. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/templates/netbox_config_diff/configurationrequest/diffs.html +0 -0
  90. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/templates/netbox_config_diff/configurationrequest.html +0 -0
  91. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/templates/netbox_config_diff/inc/diff.html +0 -0
  92. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/templates/netbox_config_diff/inc/job_log.html +0 -0
  93. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/templates/netbox_config_diff/platformsetting.html +0 -0
  94. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/templates/netbox_config_diff/substitute.html +0 -0
  95. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/urls.py +0 -0
  96. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/views/__init__.py +0 -0
  97. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff/views/configuration.py +0 -0
  98. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff.egg-info/dependency_links.txt +0 -0
  99. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/netbox_config_diff.egg-info/top_level.txt +0 -0
  100. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/pyproject.toml +0 -0
  101. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/requirements/dev.txt +0 -0
  102. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/requirements/test.txt +0 -0
  103. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/setup.cfg +0 -0
  104. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/tests/conftest.py +0 -0
  105. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/tests/factories.py +0 -0
  106. {netbox-config-diff-2.2.0 → netbox_config_diff-2.4.0}/tests/test_urls.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: netbox-config-diff
3
- Version: 2.2.0
3
+ Version: 2.4.0
4
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
@@ -218,6 +218,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
218
218
  Requires-Python: >=3.10
219
219
  Description-Content-Type: text/markdown
220
220
  License-File: LICENSE
221
+ Requires-Dist: hier-config==2.2.3
221
222
  Requires-Dist: netutils==1.5.0
222
223
  Requires-Dist: scrapli[asyncssh]==2023.07.30
223
224
  Requires-Dist: scrapli-cfg==2023.07.30
@@ -275,7 +276,7 @@ This is possible thanks to the scrapli_cfg. Read [Scrapli](https://github.com/sc
275
276
 
276
277
  | NetBox Version | Plugin Version |
277
278
  |----------------|----------------|
278
- | 3.5, 3.6 | =>0.1.0 |
279
+ | 3.5, 3.6, 3.7 | =>0.1.0 |
279
280
 
280
281
  <!--install-start-->
281
282
  ## Installing
@@ -44,7 +44,7 @@ This is possible thanks to the scrapli_cfg. Read [Scrapli](https://github.com/sc
44
44
 
45
45
  | NetBox Version | Plugin Version |
46
46
  |----------------|----------------|
47
- | 3.5, 3.6 | =>0.1.0 |
47
+ | 3.5, 3.6, 3.7 | =>0.1.0 |
48
48
 
49
49
  <!--install-start-->
50
50
  ## Installing
@@ -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.2.0"
5
+ __version__ = "2.4.0"
6
6
 
7
7
 
8
8
  class ConfigDiffConfig(PluginConfig):
@@ -29,6 +29,7 @@ class ConfigComplianceSerializer(NetBoxModelSerializer):
29
29
  "diff",
30
30
  "rendered_config",
31
31
  "actual_config",
32
+ "patch",
32
33
  "missing",
33
34
  "extra",
34
35
  "created",
@@ -18,7 +18,7 @@ from utilities.utils import render_jinja2
18
18
  from netbox_config_diff.models import ConplianceDeviceDataClass
19
19
 
20
20
  from .secrets import SecretsMixin
21
- from .utils import PLATFORM_MAPPING, CustomChoiceVar, exclude_lines, get_unified_diff
21
+ from .utils import PLATFORM_MAPPING, CustomChoiceVar, exclude_lines, get_remediation_commands, get_unified_diff
22
22
 
23
23
 
24
24
  class ConfigDiffBase(SecretsMixin):
@@ -204,3 +204,6 @@ class ConfigDiffBase(SecretsMixin):
204
204
  device.extra = diff_network_config(
205
205
  cleaned_config, device.rendered_config, PLATFORM_MAPPING[device.platform]
206
206
  )
207
+ device.patch = get_remediation_commands(
208
+ device.name, device.platform, cleaned_config, device.rendered_config
209
+ )
@@ -3,6 +3,7 @@ from difflib import unified_diff
3
3
 
4
4
  from django.forms import ChoiceField
5
5
  from extras.scripts import ScriptVariable
6
+ from hier_config import Host
6
7
 
7
8
  PLATFORM_MAPPING = {
8
9
  "arista_eos": "arista_eos",
@@ -19,6 +20,15 @@ PLATFORM_MAPPING = {
19
20
  "ruckus_fastiron": "ruckus_fastiron",
20
21
  }
21
22
 
23
+ REMEDIATION_MAPPING = {
24
+ "arista_eos": "eos",
25
+ "cisco_iosxe": "ios",
26
+ "cisco_iosxr": "iosxr",
27
+ "cisco_nxos": "nxos",
28
+ "juniper_junos": "junos",
29
+ "vyos_vyos": "vyos",
30
+ }
31
+
22
32
 
23
33
  class CustomChoiceVar(ScriptVariable):
24
34
  form_field = ChoiceField
@@ -30,8 +40,8 @@ class CustomChoiceVar(ScriptVariable):
30
40
 
31
41
  def get_unified_diff(rendered_config: str, actual_config: str, device: str) -> str:
32
42
  diff = unified_diff(
33
- rendered_config.strip().splitlines(),
34
43
  actual_config.splitlines(),
44
+ rendered_config.strip().splitlines(),
35
45
  fromfiledate=device,
36
46
  tofiledate=device,
37
47
  lineterm="",
@@ -43,3 +53,10 @@ def exclude_lines(text: str, regexs: list) -> str:
43
53
  for item in regexs:
44
54
  text = re.sub(item, "", text, flags=re.I | re.M)
45
55
  return text.strip()
56
+
57
+
58
+ def get_remediation_commands(name: str, platform: str, actual_config: str, rendered_config: str) -> str:
59
+ host = Host(hostname=name, os=REMEDIATION_MAPPING.get(platform, "ios"))
60
+ host.load_running_config(config_text=actual_config)
61
+ host.load_generated_config(config_text=rendered_config)
62
+ return host.remediation_config_filtered_text(include_tags={}, exclude_tags={})
@@ -14,7 +14,7 @@ from scrapli_cfg.response import ScrapliCfgResponse
14
14
  from utilities.utils import NetBoxFakeRequest
15
15
 
16
16
  from netbox_config_diff.compliance.secrets import SecretsMixin
17
- from netbox_config_diff.compliance.utils import PLATFORM_MAPPING, get_unified_diff
17
+ from netbox_config_diff.compliance.utils import PLATFORM_MAPPING, get_remediation_commands, get_unified_diff
18
18
  from netbox_config_diff.configurator.exceptions import DeviceConfigurationError, DeviceValidationError
19
19
  from netbox_config_diff.configurator.utils import CustomLogger
20
20
  from netbox_config_diff.constants import ACCEPTABLE_DRIVERS
@@ -126,7 +126,7 @@ class Configurator(SecretsMixin):
126
126
  )
127
127
  device.rendered_config = rendered_config
128
128
  else:
129
- actual_config = await conn.get_config()
129
+ actual_config = await conn.get_config(config_template=device.rendered_config)
130
130
  device.actual_config = conn.clean_config(actual_config.result)
131
131
 
132
132
  device.diff = get_unified_diff(device.rendered_config, device.actual_config, device.name)
@@ -137,6 +137,9 @@ class Configurator(SecretsMixin):
137
137
  device.extra = diff_network_config(
138
138
  device.actual_config, device.rendered_config, PLATFORM_MAPPING[device.platform]
139
139
  )
140
+ device.patch = get_remediation_commands(
141
+ device.name, device.platform, device.actual_config, device.rendered_config
142
+ )
140
143
  self.logger.log_info(f"Got diff from {device.name}")
141
144
  except Exception:
142
145
  error = traceback.format_exc()
@@ -1,5 +1,5 @@
1
1
  import re
2
- from typing import Pattern
2
+ from typing import Any, Pattern
3
3
 
4
4
  from scrapli_cfg.exceptions import TemplateError
5
5
  from scrapli_cfg.platform.core.arista_eos import AsyncScrapliCfgEOS
@@ -94,13 +94,17 @@ class CustomScrapliCfg:
94
94
  """
95
95
  self.logger.info("fetching configuration and replacing with provided substitutes")
96
96
 
97
- source_config = await self.get_config(source=source)
97
+ source_config = await self.get_config(config_template=config_template, source=source)
98
98
  return source_config, self._render_substituted_config(
99
99
  config_template=config_template,
100
100
  substitutes=substitutes,
101
101
  source_config=source_config.result,
102
102
  )
103
103
 
104
+ async def get_config(self, **kwargs) -> ScrapliCfgResponse:
105
+ kwargs.pop("config_template", None)
106
+ return await super().get_config(**kwargs)
107
+
104
108
 
105
109
  class CustomAsyncScrapliCfgEOS(CustomScrapliCfg, AsyncScrapliCfgEOS):
106
110
  pass
@@ -119,4 +123,67 @@ class CustomAsyncScrapliCfgNXOS(CustomScrapliCfg, AsyncScrapliCfgNXOS):
119
123
 
120
124
 
121
125
  class CustomAsyncScrapliCfgJunos(CustomScrapliCfg, AsyncScrapliCfgJunos):
122
- pass
126
+ is_set_config = False
127
+
128
+ async def get_config(self, config_template: str, source: str = "running") -> ScrapliCfgResponse:
129
+ response = self._pre_get_config(source=source)
130
+
131
+ command = "show configuration"
132
+ if re.findall(r"^set\s+", config_template, flags=re.I | re.M):
133
+ self.is_set_config = True
134
+ command += " | display set"
135
+
136
+ if self._in_configuration_session is True:
137
+ config_result = await self.conn.send_config(config=f"run {command}")
138
+ else:
139
+ config_result = await self.conn.send_command(command=command)
140
+
141
+ return self._post_get_config(
142
+ response=response,
143
+ source=source,
144
+ scrapli_responses=[config_result],
145
+ result=config_result.result,
146
+ )
147
+
148
+ async def load_config(self, config: str, replace: bool = False, **kwargs: Any) -> ScrapliCfgResponse:
149
+ """
150
+ Load configuration to a device
151
+
152
+ Supported kwargs:
153
+ set: bool indicating config is a "set" style config (ignored if replace is True)
154
+
155
+ Args:
156
+ config: string of the configuration to load
157
+ replace: replace the configuration or not, if false configuration will be loaded as a
158
+ merge operation
159
+ kwargs: additional kwargs that the implementing classes may need for their platform,
160
+ see above for junos supported kwargs
161
+
162
+ Returns:
163
+ ScrapliCfgResponse: response object
164
+
165
+ Raises:
166
+ N/A
167
+
168
+ """
169
+ response = self._pre_load_config(config=config)
170
+
171
+ config = self._prepare_load_config(config=config, replace=replace)
172
+
173
+ config_result = await self.conn.send_config(config=config, privilege_level="root_shell")
174
+
175
+ if self.is_set_config is True:
176
+ load_config = f"load set {self.filesystem}{self.candidate_config_filename}"
177
+ else:
178
+ if self._replace is True:
179
+ load_config = f"load override {self.filesystem}{self.candidate_config_filename}"
180
+ else:
181
+ load_config = f"load merge {self.filesystem}{self.candidate_config_filename}"
182
+
183
+ load_result = await self.conn.send_config(config=load_config)
184
+ self._in_configuration_session = True
185
+
186
+ return self._post_load_config(
187
+ response=response,
188
+ scrapli_responses=[config_result, load_result],
189
+ )
@@ -0,0 +1,16 @@
1
+ from django.db import migrations, models
2
+
3
+
4
+ class Migration(migrations.Migration):
5
+
6
+ dependencies = [
7
+ ("netbox_config_diff", "0008_alter_configcompliance_device"),
8
+ ]
9
+
10
+ operations = [
11
+ migrations.AddField(
12
+ model_name="configcompliance",
13
+ name="patch",
14
+ field=models.TextField(blank=True),
15
+ ),
16
+ ]
@@ -23,6 +23,7 @@ class BaseDeviceDataClass:
23
23
  diff: str = ""
24
24
  missing: str | None = None
25
25
  extra: str | None = None
26
+ patch: str | None = None
26
27
  error: str = ""
27
28
  config_error: str | None = None
28
29
  auth_strict_key: bool = False
@@ -99,6 +100,7 @@ class BaseDeviceDataClass:
99
100
  "actual_config": self.actual_config or "",
100
101
  "missing": self.missing or "",
101
102
  "extra": self.extra or "",
103
+ "patch": self.patch or "",
102
104
  }
103
105
 
104
106
  def send_to_db(self) -> None:
@@ -49,6 +49,9 @@ class ConfigCompliance(AbsoluteURLMixin, ChangeLoggingMixin, models.Model):
49
49
  extra = models.TextField(
50
50
  blank=True,
51
51
  )
52
+ patch = models.TextField(
53
+ blank=True,
54
+ )
52
55
 
53
56
  objects = RestrictedQuerySet.as_manager()
54
57
 
@@ -8,6 +8,7 @@
8
8
  <div class="card">
9
9
  <div class="card-header">
10
10
  <div class="float-end">
11
+ {% copy_content config_field %}
11
12
  <a href="?export=True" class="btn btn-sm btn-primary" role="button">
12
13
  <i class="mdi mdi-download" aria-hidden="true"></i> Download
13
14
  </a>
@@ -15,7 +16,7 @@
15
16
  <h5>{{ header }}</h5>
16
17
  </div>
17
18
  {% if config %}
18
- <pre class="card-body">{{ config }}</pre>
19
+ <pre class="card-body" id="{{ config_field }}">{{ config }}</pre>
19
20
  {% else %}
20
21
  <div class="card-body text-muted">No configuration</div>
21
22
  {% endif %}
@@ -0,0 +1,14 @@
1
+ {% extends "netbox_config_diff/configcompliance.html" %}
2
+
3
+ {% block title %}{{ object }} - Missing/Extra{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="row">
7
+ <div class="col col-md-6">
8
+ {% include 'netbox_config_diff/inc/commands_card.html' with data=object.missing header='Missing' pre_id='missing' %}
9
+ </div>
10
+ <div class="col col-md-6">
11
+ {% include 'netbox_config_diff/inc/commands_card.html' with data=object.extra header='Extra' pre_id='extra' %}
12
+ </div>
13
+ </div>
14
+ {% endblock %}
@@ -0,0 +1,11 @@
1
+ {% extends "netbox_config_diff/configcompliance.html" %}
2
+
3
+ {% block title %}{{ object }} - Patch commands{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="row">
7
+ <div class="col col-md-6">
8
+ {% include 'netbox_config_diff/inc/commands_card.html' with data=object.patch header='Patch commands' pre_id='patch' %}
9
+ </div>
10
+ </div>
11
+ {% endblock %}
@@ -0,0 +1,16 @@
1
+ <div class="card">
2
+ <div class="card-header">
3
+ <div class="float-end">
4
+ {% copy_content pre_id %}
5
+ <a href="?export_{{ pre_id }}=True" class="btn btn-sm btn-primary" role="button">
6
+ <i class="mdi mdi-download" aria-hidden="true"></i> Download
7
+ </a>
8
+ </div>
9
+ <h5>{{ header }}</h5>
10
+ </div>
11
+ {% if data %}
12
+ <pre class="card-body" id="{{ pre_id }}">{{ data }}</pre>
13
+ {% else %}
14
+ <div class="card-body text-muted">No commands</div>
15
+ {% endif %}
16
+ </div>
@@ -0,0 +1,52 @@
1
+ from django.http import HttpResponse
2
+ from django.shortcuts import render
3
+ from django.urls import reverse
4
+ from netbox.views.generic import ObjectDeleteView, ObjectEditView, ObjectView
5
+
6
+
7
+ class BaseObjectDeleteView(ObjectDeleteView):
8
+ def get_return_url(self, request, obj=None):
9
+ return reverse(f"plugins:netbox_config_diff:{self.queryset.model._meta.model_name}_list")
10
+
11
+
12
+ class BaseObjectEditView(ObjectEditView):
13
+ @property
14
+ def default_return_url(self) -> str:
15
+ return f"plugins:netbox_config_diff:{self.queryset.model._meta.model_name}_list"
16
+
17
+
18
+ class BaseExportView(ObjectView):
19
+ def export_parts(self, name, lines, suffix):
20
+ response = HttpResponse(lines, content_type="text")
21
+ filename = f"{name}_{suffix}.txt"
22
+ response["Content-Disposition"] = f'attachment; filename="{filename}"'
23
+ return response
24
+
25
+
26
+ class BaseConfigComplianceConfigView(BaseExportView):
27
+ config_field = None
28
+ template_header = None
29
+
30
+ def get(self, request, **kwargs):
31
+ instance = self.get_object(**kwargs)
32
+ context = self.get_extra_context(request, instance)
33
+
34
+ if request.GET.get("export"):
35
+ return self.export_parts(instance.device.name, context["config"], self.config_field)
36
+
37
+ return render(
38
+ request,
39
+ self.get_template_name(),
40
+ {
41
+ "object": instance,
42
+ "tab": self.tab,
43
+ **context,
44
+ },
45
+ )
46
+
47
+ def get_extra_context(self, request, instance):
48
+ return {
49
+ "header": self.template_header,
50
+ "config": getattr(instance, self.config_field),
51
+ "config_field": self.config_field,
52
+ }
@@ -1,5 +1,4 @@
1
1
  from dcim.models import Device
2
- from django.http import HttpResponse
3
2
  from django.shortcuts import redirect, render
4
3
  from django.utils.translation import gettext as _
5
4
  from netbox.views import generic
@@ -15,38 +14,7 @@ from netbox_config_diff.forms import (
15
14
  from netbox_config_diff.models import ConfigCompliance, PlatformSetting
16
15
  from netbox_config_diff.tables import ConfigComplianceTable, PlatformSettingTable
17
16
 
18
- from .base import BaseObjectDeleteView, BaseObjectEditView
19
-
20
-
21
- class BaseConfigComplianceConfigView(generic.ObjectView):
22
- config_field = None
23
- template_header = None
24
-
25
- def get(self, request, **kwargs):
26
- instance = self.get_object(**kwargs)
27
- context = self.get_extra_context(request, instance)
28
-
29
- if request.GET.get("export"):
30
- response = HttpResponse(context["config"], content_type="text")
31
- filename = f"{instance.device.name}_{self.config_field}.txt"
32
- response["Content-Disposition"] = f'attachment; filename="{filename}"'
33
- return response
34
-
35
- return render(
36
- request,
37
- self.get_template_name(),
38
- {
39
- "object": instance,
40
- "tab": self.tab,
41
- **context,
42
- },
43
- )
44
-
45
- def get_extra_context(self, request, instance):
46
- return {
47
- "header": self.template_header,
48
- "config": getattr(instance, self.config_field),
49
- }
17
+ from .base import BaseConfigComplianceConfigView, BaseExportView, BaseObjectDeleteView, BaseObjectEditView
50
18
 
51
19
 
52
20
  @register_model_view(ConfigCompliance)
@@ -87,7 +55,7 @@ class ConfigComplianceActualConfigView(BaseConfigComplianceConfigView):
87
55
 
88
56
 
89
57
  @register_model_view(ConfigCompliance, "missing-extra")
90
- class ConfigComplianceMissingExtraConfigView(generic.ObjectView):
58
+ class ConfigComplianceMissingExtraConfigView(BaseExportView):
91
59
  queryset = ConfigCompliance.objects.all()
92
60
  template_name = "netbox_config_diff/configcompliance/missing_extra.html"
93
61
  tab = ViewTab(
@@ -95,12 +63,6 @@ class ConfigComplianceMissingExtraConfigView(generic.ObjectView):
95
63
  weight=520,
96
64
  )
97
65
 
98
- def export_parts(self, name, lines, suffix):
99
- response = HttpResponse(lines, content_type="text")
100
- filename = f"{name}_{suffix}.txt"
101
- response["Content-Disposition"] = f'attachment; filename="{filename}"'
102
- return response
103
-
104
66
  def get(self, request, **kwargs):
105
67
  instance = self.get_object(**kwargs)
106
68
  context = self.get_extra_context(request, instance)
@@ -122,6 +84,33 @@ class ConfigComplianceMissingExtraConfigView(generic.ObjectView):
122
84
  )
123
85
 
124
86
 
87
+ @register_model_view(ConfigCompliance, "patch")
88
+ class ConfigCompliancePatchView(BaseExportView):
89
+ queryset = ConfigCompliance.objects.all()
90
+ template_name = "netbox_config_diff/configcompliance/patch.html"
91
+ tab = ViewTab(
92
+ label=_("Patch"),
93
+ weight=515,
94
+ )
95
+
96
+ def get(self, request, **kwargs):
97
+ instance = self.get_object(**kwargs)
98
+ context = self.get_extra_context(request, instance)
99
+
100
+ if request.GET.get("export_patch"):
101
+ return self.export_parts(instance.device.name, instance.patch, "patch")
102
+
103
+ return render(
104
+ request,
105
+ self.get_template_name(),
106
+ {
107
+ "object": instance,
108
+ "tab": self.tab,
109
+ **context,
110
+ },
111
+ )
112
+
113
+
125
114
  @register_model_view(Device, "config_compliance", "config-compliance")
126
115
  class ConfigComplianceDeviceView(generic.ObjectView):
127
116
  queryset = Device.objects.all()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: netbox-config-diff
3
- Version: 2.2.0
3
+ Version: 2.4.0
4
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
@@ -218,6 +218,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
218
218
  Requires-Python: >=3.10
219
219
  Description-Content-Type: text/markdown
220
220
  License-File: LICENSE
221
+ Requires-Dist: hier-config==2.2.3
221
222
  Requires-Dist: netutils==1.5.0
222
223
  Requires-Dist: scrapli[asyncssh]==2023.07.30
223
224
  Requires-Dist: scrapli-cfg==2023.07.30
@@ -275,7 +276,7 @@ This is possible thanks to the scrapli_cfg. Read [Scrapli](https://github.com/sc
275
276
 
276
277
  | NetBox Version | Plugin Version |
277
278
  |----------------|----------------|
278
- | 3.5, 3.6 | =>0.1.0 |
279
+ | 3.5, 3.6, 3.7 | =>0.1.0 |
279
280
 
280
281
  <!--install-start-->
281
282
  ## Installing
@@ -8,6 +8,7 @@ docs/media/screenshots/compliance-error.png
8
8
  docs/media/screenshots/compliance-list.png
9
9
  docs/media/screenshots/compliance-missing-extra.png
10
10
  docs/media/screenshots/compliance-ok.png
11
+ docs/media/screenshots/compliance-patch.png
11
12
  docs/media/screenshots/config-temp-substitute.png
12
13
  docs/media/screenshots/cr-approve-button.png
13
14
  docs/media/screenshots/cr-approved.png
@@ -64,6 +65,7 @@ netbox_config_diff/migrations/0005_configcompliance_extra_missing.py
64
65
  netbox_config_diff/migrations/0006_substitute.py
65
66
  netbox_config_diff/migrations/0007_configurationrequest.py
66
67
  netbox_config_diff/migrations/0008_alter_configcompliance_device.py
68
+ netbox_config_diff/migrations/0009_configcompliance_patch.py
67
69
  netbox_config_diff/migrations/__init__.py
68
70
  netbox_config_diff/models/__init__.py
69
71
  netbox_config_diff/models/base.py
@@ -80,8 +82,10 @@ netbox_config_diff/templates/netbox_config_diff/substitute.html
80
82
  netbox_config_diff/templates/netbox_config_diff/configcompliance/config.html
81
83
  netbox_config_diff/templates/netbox_config_diff/configcompliance/data.html
82
84
  netbox_config_diff/templates/netbox_config_diff/configcompliance/missing_extra.html
85
+ netbox_config_diff/templates/netbox_config_diff/configcompliance/patch.html
83
86
  netbox_config_diff/templates/netbox_config_diff/configurationrequest/base.html
84
87
  netbox_config_diff/templates/netbox_config_diff/configurationrequest/diffs.html
88
+ netbox_config_diff/templates/netbox_config_diff/inc/commands_card.html
85
89
  netbox_config_diff/templates/netbox_config_diff/inc/diff.html
86
90
  netbox_config_diff/templates/netbox_config_diff/inc/job_log.html
87
91
  netbox_config_diff/views/__init__.py
@@ -1,3 +1,4 @@
1
+ hier-config==2.2.3
1
2
  netutils==1.5.0
2
3
  scrapli[asyncssh]==2023.07.30
3
4
  scrapli-cfg==2023.07.30
@@ -1,3 +1,4 @@
1
+ hier-config==2.2.3
1
2
  netutils==1.5.0
2
3
  scrapli[asyncssh]==2023.07.30
3
4
  scrapli-cfg==2023.07.30
@@ -155,4 +155,5 @@ def test_devicedataclass_to_db(
155
155
  "actual_config": "",
156
156
  "missing": "",
157
157
  "extra": "",
158
+ "patch": "",
158
159
  }
@@ -56,4 +56,4 @@ def test_exclude_lines(regex: str, expected: str) -> None:
56
56
  ids=["diff", "no diff"],
57
57
  )
58
58
  def test_get_unified_diff(render: str, actual: str, expected: str) -> None:
59
- assert get_unified_diff(render, actual, "test-1") == expected
59
+ assert get_unified_diff(actual, render, "test-1") == expected
@@ -1,42 +0,0 @@
1
- {% extends "netbox_config_diff/configcompliance.html" %}
2
-
3
- {% block title %}{{ object }} - Missing/Extra{% endblock %}
4
-
5
- {% block content %}
6
- <div class="row">
7
- <div class="col col-md-6">
8
- <div class="card">
9
- <div class="card-header">
10
- <div class="float-end">
11
- <a href="?export_missing=True" class="btn btn-sm btn-primary" role="button">
12
- <i class="mdi mdi-download" aria-hidden="true"></i> Download
13
- </a>
14
- </div>
15
- <h5>Missing</h5>
16
- </div>
17
- {% if object.missing %}
18
- <pre class="card-body">{{ object.missing }}</pre>
19
- {% else %}
20
- <div class="card-body text-muted">No lines</div>
21
- {% endif %}
22
- </div>
23
- </div>
24
- <div class="col col-md-6">
25
- <div class="card">
26
- <div class="card-header">
27
- <div class="float-end">
28
- <a href="?export_extra=True" class="btn btn-sm btn-primary" role="button">
29
- <i class="mdi mdi-download" aria-hidden="true"></i> Download
30
- </a>
31
- </div>
32
- <h5>Extra</h5>
33
- </div>
34
- {% if object.extra %}
35
- <pre class="card-body">{{ object.extra }}</pre>
36
- {% else %}
37
- <div class="card-body text-muted">No lines</div>
38
- {% endif %}
39
- </div>
40
- </div>
41
- </div>
42
- {% endblock %}
@@ -1,13 +0,0 @@
1
- from django.urls import reverse
2
- from netbox.views.generic import ObjectDeleteView, ObjectEditView
3
-
4
-
5
- class BaseObjectDeleteView(ObjectDeleteView):
6
- def get_return_url(self, request, obj=None):
7
- return reverse(f"plugins:netbox_config_diff:{self.queryset.model._meta.model_name}_list")
8
-
9
-
10
- class BaseObjectEditView(ObjectEditView):
11
- @property
12
- def default_return_url(self) -> str:
13
- return f"plugins:netbox_config_diff:{self.queryset.model._meta.model_name}_list"