netbox-config-diff 2.14.3__tar.gz → 2.15.1__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 (114) hide show
  1. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/PKG-INFO +3 -2
  2. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/README.md +2 -1
  3. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/__init__.py +3 -2
  4. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/compliance/secrets.py +37 -21
  5. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff.egg-info/PKG-INFO +3 -2
  6. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff.egg-info/SOURCES.txt +1 -0
  7. netbox_config_diff-2.15.1/tests/test_secrets.py +135 -0
  8. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/LICENSE +0 -0
  9. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/MANIFEST.in +0 -0
  10. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/development/configuration.py +0 -0
  11. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/compliance-diff.png +0 -0
  12. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/compliance-error.png +0 -0
  13. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/compliance-list.png +0 -0
  14. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/compliance-missing-extra.png +0 -0
  15. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/compliance-ok.png +0 -0
  16. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/compliance-patch.png +0 -0
  17. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/config-temp-substitute.png +0 -0
  18. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/cr-approve-button.png +0 -0
  19. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/cr-approved.png +0 -0
  20. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/cr-collecting-diff-button.png +0 -0
  21. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/cr-completed.png +0 -0
  22. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/cr-created.png +0 -0
  23. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/cr-diffs-tab.png +0 -0
  24. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/cr-job-log.png +0 -0
  25. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/cr-schedule-button.png +0 -0
  26. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/cr-scheduled.png +0 -0
  27. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/cr-unapprove-button.png +0 -0
  28. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/cr-unschedule-button.png +0 -0
  29. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/navbar.png +0 -0
  30. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/platformsetting.png +0 -0
  31. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/render-temp-substitute.png +0 -0
  32. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/script-list.png +0 -0
  33. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/script.png +0 -0
  34. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/docs/media/screenshots/substitute.png +0 -0
  35. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/api/__init__.py +0 -0
  36. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/api/serializers.py +0 -0
  37. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/api/urls.py +0 -0
  38. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/api/views.py +0 -0
  39. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/choices.py +0 -0
  40. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/compliance/__init__.py +0 -0
  41. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/compliance/base.py +0 -0
  42. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/compliance/utils.py +0 -0
  43. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/configurator/__init__.py +0 -0
  44. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/configurator/base.py +0 -0
  45. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/configurator/exceptions.py +0 -0
  46. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/configurator/factory.py +0 -0
  47. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/configurator/platforms.py +0 -0
  48. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/configurator/utils.py +0 -0
  49. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/constants.py +0 -0
  50. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/filtersets.py +0 -0
  51. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/forms/__init__.py +0 -0
  52. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/forms/general.py +0 -0
  53. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/graphql/__init__.py +0 -0
  54. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/graphql/enums.py +0 -0
  55. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/graphql/filters.py +0 -0
  56. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/graphql/schema.py +0 -0
  57. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/graphql/types.py +0 -0
  58. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/jobs.py +0 -0
  59. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/migrations/0001_initial.py +0 -0
  60. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/migrations/0002_add_script.py +0 -0
  61. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/migrations/0003_configcompliance_actual_config_and_more.py +0 -0
  62. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/migrations/0004_update_script.py +0 -0
  63. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/migrations/0005_configcompliance_extra_missing.py +0 -0
  64. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/migrations/0006_substitute.py +0 -0
  65. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/migrations/0007_configurationrequest.py +0 -0
  66. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/migrations/0008_alter_configcompliance_device.py +0 -0
  67. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/migrations/0009_configcompliance_patch.py +0 -0
  68. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/migrations/0010_create_script.py +0 -0
  69. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/migrations/0011_configurationrequest_owner.py +0 -0
  70. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/migrations/__init__.py +0 -0
  71. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/models/__init__.py +0 -0
  72. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/models/base.py +0 -0
  73. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/models/data_models.py +0 -0
  74. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/models/models.py +0 -0
  75. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/navigation.py +0 -0
  76. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/scripts/config_diff.py +0 -0
  77. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/search.py +0 -0
  78. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/static/netbox_config_diff/diff2html-ui.min.js +0 -0
  79. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/static/netbox_config_diff/diff2html.dark.min.css +0 -0
  80. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/static/netbox_config_diff/diff2html.min.css +0 -0
  81. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/tables.py +0 -0
  82. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/templates/netbox_config_diff/configcompliance/config.html +0 -0
  83. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/templates/netbox_config_diff/configcompliance/data.html +0 -0
  84. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/templates/netbox_config_diff/configcompliance/missing_extra.html +0 -0
  85. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/templates/netbox_config_diff/configcompliance.html +0 -0
  86. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/templates/netbox_config_diff/configurationrequest/base.html +0 -0
  87. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/templates/netbox_config_diff/configurationrequest/diffs.html +0 -0
  88. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/templates/netbox_config_diff/configurationrequest.html +0 -0
  89. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/templates/netbox_config_diff/inc/commands_card.html +0 -0
  90. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/templates/netbox_config_diff/inc/diff.html +0 -0
  91. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/templates/netbox_config_diff/inc/job_log.html +0 -0
  92. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/templates/netbox_config_diff/platformsetting.html +0 -0
  93. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/templates/netbox_config_diff/substitute.html +0 -0
  94. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/templatetags/__init__.py +0 -0
  95. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/templatetags/string_filters.py +0 -0
  96. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/urls.py +0 -0
  97. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/views/__init__.py +0 -0
  98. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/views/base.py +0 -0
  99. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/views/compliance.py +0 -0
  100. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff/views/configuration.py +0 -0
  101. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff.egg-info/dependency_links.txt +0 -0
  102. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff.egg-info/requires.txt +0 -0
  103. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/netbox_config_diff.egg-info/top_level.txt +0 -0
  104. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/pyproject.toml +0 -0
  105. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/requirements/base.txt +0 -0
  106. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/requirements/dev.txt +0 -0
  107. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/requirements/test.txt +0 -0
  108. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/setup.cfg +0 -0
  109. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/tests/conftest.py +0 -0
  110. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/tests/factories.py +0 -0
  111. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/tests/test_compliance.py +0 -0
  112. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/tests/test_compliance_utils.py +0 -0
  113. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/tests/test_configurtion_request.py +0 -0
  114. {netbox_config_diff-2.14.3 → netbox_config_diff-2.15.1}/tests/test_urls.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-config-diff
3
- Version: 2.14.3
3
+ Version: 2.15.1
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
@@ -232,7 +232,7 @@ Requires-Dist: pytest==8.3.4; extra == "test"
232
232
  Requires-Dist: pytest-django==4.9.0; extra == "test"
233
233
  Dynamic: license-file
234
234
 
235
- [![NetBox version](https://img.shields.io/badge/NetBox-4.5-blue.svg)](https://github.com/netbox-community/netbox)
235
+ [![NetBox version](https://img.shields.io/badge/NetBox-4.5|4.6-blue.svg)](https://github.com/netbox-community/netbox)
236
236
  [![Supported Versions](https://img.shields.io/pypi/pyversions/netbox-config-diff.svg)](https://pypi.org/project/netbox-config-diff/)
237
237
  [![PyPI version](https://badge.fury.io/py/netbox-config-diff.svg)](https://badge.fury.io/py/netbox-config-diff)
238
238
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
@@ -289,6 +289,7 @@ This is possible thanks to the scrapli_cfg. Read [Scrapli](https://github.com/sc
289
289
  | 4.3 | =>2.10.0, <=2.12.0 |
290
290
  | 4.4 | =>2.11.0, <=2.13.0 |
291
291
  | 4.5 | =>2.14.0 |
292
+ | 4.6 | =>2.15.0 |
292
293
 
293
294
  <!--install-start-->
294
295
  ## Installing
@@ -1,4 +1,4 @@
1
- [![NetBox version](https://img.shields.io/badge/NetBox-4.5-blue.svg)](https://github.com/netbox-community/netbox)
1
+ [![NetBox version](https://img.shields.io/badge/NetBox-4.5|4.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
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)
@@ -55,6 +55,7 @@ This is possible thanks to the scrapli_cfg. Read [Scrapli](https://github.com/sc
55
55
  | 4.3 | =>2.10.0, <=2.12.0 |
56
56
  | 4.4 | =>2.11.0, <=2.13.0 |
57
57
  | 4.5 | =>2.14.0 |
58
+ | 4.6 | =>2.15.0 |
58
59
 
59
60
  <!--install-start-->
60
61
  ## Installing
@@ -2,7 +2,7 @@ from netbox.plugins import PluginConfig
2
2
 
3
3
  __author__ = "Artem Kotik"
4
4
  __email__ = "miaow2@yandex.ru"
5
- __version__ = "2.14.3"
5
+ __version__ = "2.15.1"
6
6
 
7
7
 
8
8
  class ConfigDiffConfig(PluginConfig):
@@ -15,12 +15,13 @@ class ConfigDiffConfig(PluginConfig):
15
15
  base_url = "config-diff"
16
16
  required_settings = ["USERNAME", "PASSWORD"]
17
17
  min_version = "4.5.0"
18
- max_version = "4.5.99"
18
+ max_version = "4.6.99"
19
19
  default_settings = {
20
20
  "USER_SECRET_ROLE": "Username",
21
21
  "PASSWORD_SECRET_ROLE": "Password",
22
22
  "SECOND_AUTH_SECRET_ROLE": "Second Auth",
23
23
  "PATH_TO_SSH_CONFIG_FILE": "",
24
+ "SECRETS_PRECEDENCE": ["device", "role", "platform"],
24
25
  }
25
26
 
26
27
 
@@ -1,4 +1,5 @@
1
1
  import base64
2
+ from operator import attrgetter
2
3
  from typing import TYPE_CHECKING
3
4
 
4
5
  from dcim.models import Device
@@ -34,8 +35,8 @@ class SecretsMixin:
34
35
  sk = SessionKey.objects.get(userkey__user=self.request.user)
35
36
  self.master_key = sk.get_master_key(self.session_key)
36
37
  except Exception as e:
37
- if getattr(self, "logger"):
38
- if getattr(self.logger, "log_failure"):
38
+ if hasattr(self, "logger"):
39
+ if hasattr(self.logger, "log_failure"):
39
40
  self.logger.log_failure(f"Can't fetch master_key: {str(e)}")
40
41
  else:
41
42
  self.logger.error(f"Can't fetch master_key: {str(e)}")
@@ -49,28 +50,42 @@ class SecretsMixin:
49
50
  return None
50
51
  return secret.plaintext
51
52
 
53
+ def get_secret_value(self, objects: tuple[object | None, ...], role_name: str) -> str | None:
54
+ for obj in objects:
55
+ if not obj:
56
+ continue
57
+
58
+ secrets = getattr(obj, "secrets", None)
59
+ if not secrets:
60
+ continue
61
+
62
+ if secret := secrets.filter(role__name=role_name).first():
63
+ if value := self.get_secret(secret):
64
+ return value
65
+
66
+ return None
67
+
52
68
  def get_credentials(self, device: Device) -> tuple[str, str, str, str]:
53
- if not self.netbox_secrets_installed:
69
+ if not self.netbox_secrets_installed or not self.secrets_precedence:
54
70
  return self.username, self.password, self.auth_secondary, self.default_desired_privilege_level
55
71
 
56
- if secret := device.secrets.filter(role__name=self.user_role).first():
57
- username = value if (value := self.get_secret(secret)) else self.username
58
- else:
59
- username = self.username
60
- if secret := device.secrets.filter(role__name=self.password_role).first():
61
- password = value if (value := self.get_secret(secret)) else self.password
62
- else:
63
- password = self.password
64
- if secret := device.secrets.filter(role__name=self.auth_secondary_role).first():
65
- auth_secondary = value if (value := self.get_secret(secret)) else self.auth_secondary
66
- else:
67
- auth_secondary = self.auth_secondary
68
- if secret := device.secrets.filter(role__name=self.default_desired_privilege_level_role).first():
69
- default_desired_privilege_level = (
70
- value if (value := self.get_secret(secret)) else self.default_desired_privilege_level
71
- )
72
- else:
73
- default_desired_privilege_level = self.default_desired_privilege_level
72
+ secret_objects: list[object] = []
73
+ for entry in self.secrets_precedence:
74
+ if entry == "device":
75
+ secret_objects.append(device)
76
+ continue
77
+ try:
78
+ secret_objects.append(attrgetter(entry)(device))
79
+ except AttributeError:
80
+ pass
81
+
82
+ username = self.get_secret_value(secret_objects, self.user_role) or self.username
83
+ password = self.get_secret_value(secret_objects, self.password_role) or self.password
84
+ auth_secondary = self.get_secret_value(secret_objects, self.auth_secondary_role) or self.auth_secondary
85
+ default_desired_privilege_level = (
86
+ self.get_secret_value(secret_objects, self.default_desired_privilege_level_role)
87
+ or self.default_desired_privilege_level
88
+ )
74
89
 
75
90
  return username, password, auth_secondary, default_desired_privilege_level
76
91
 
@@ -83,6 +98,7 @@ class SecretsMixin:
83
98
  self.default_desired_privilege_level_role = get_plugin_config(
84
99
  "netbox_config_diff", "DEFAULT_DESIRED_PRIVILEGE_LEVEL_ROLE"
85
100
  )
101
+ self.secrets_precedence = get_plugin_config("netbox_config_diff", "SECRETS_PRECEDENCE")
86
102
  self.netbox_secrets_installed = True
87
103
 
88
104
  self.username = get_plugin_config("netbox_config_diff", "USERNAME")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-config-diff
3
- Version: 2.14.3
3
+ Version: 2.15.1
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
@@ -232,7 +232,7 @@ Requires-Dist: pytest==8.3.4; extra == "test"
232
232
  Requires-Dist: pytest-django==4.9.0; extra == "test"
233
233
  Dynamic: license-file
234
234
 
235
- [![NetBox version](https://img.shields.io/badge/NetBox-4.5-blue.svg)](https://github.com/netbox-community/netbox)
235
+ [![NetBox version](https://img.shields.io/badge/NetBox-4.5|4.6-blue.svg)](https://github.com/netbox-community/netbox)
236
236
  [![Supported Versions](https://img.shields.io/pypi/pyversions/netbox-config-diff.svg)](https://pypi.org/project/netbox-config-diff/)
237
237
  [![PyPI version](https://badge.fury.io/py/netbox-config-diff.svg)](https://badge.fury.io/py/netbox-config-diff)
238
238
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
@@ -289,6 +289,7 @@ This is possible thanks to the scrapli_cfg. Read [Scrapli](https://github.com/sc
289
289
  | 4.3 | =>2.10.0, <=2.12.0 |
290
290
  | 4.4 | =>2.11.0, <=2.13.0 |
291
291
  | 4.5 | =>2.14.0 |
292
+ | 4.6 | =>2.15.0 |
292
293
 
293
294
  <!--install-start-->
294
295
  ## Installing
@@ -108,4 +108,5 @@ tests/factories.py
108
108
  tests/test_compliance.py
109
109
  tests/test_compliance_utils.py
110
110
  tests/test_configurtion_request.py
111
+ tests/test_secrets.py
111
112
  tests/test_urls.py
@@ -0,0 +1,135 @@
1
+ from types import SimpleNamespace
2
+
3
+ from netbox_config_diff.compliance.secrets import SecretsMixin
4
+
5
+
6
+ class DummySecretsManager:
7
+ def __init__(self, by_role: dict[str, object]) -> None:
8
+ self.by_role = by_role
9
+
10
+ def filter(self, **kwargs):
11
+ return SimpleNamespace(first=lambda: self.by_role.get(kwargs["role__name"]))
12
+
13
+
14
+ class DummySecretsMixin(SecretsMixin):
15
+ pass
16
+
17
+
18
+ def build_mixin() -> DummySecretsMixin:
19
+ mixin = DummySecretsMixin()
20
+ mixin.netbox_secrets_installed = True
21
+ mixin.username = "default-user"
22
+ mixin.password = "default-pass"
23
+ mixin.auth_secondary = "default-secondary"
24
+ mixin.default_desired_privilege_level = "default-priv"
25
+ mixin.secrets_precedence = ["device", "role", "platform"]
26
+
27
+ mixin.user_role = "user-role"
28
+ mixin.password_role = "password-role"
29
+ mixin.auth_secondary_role = "secondary-role"
30
+ mixin.default_desired_privilege_level_role = "priv-role"
31
+ return mixin
32
+
33
+
34
+ def test_get_credentials_prefers_device_then_role_then_platform() -> None:
35
+ mixin = build_mixin()
36
+
37
+ device = SimpleNamespace(
38
+ secrets=DummySecretsManager(
39
+ {
40
+ "user-role": SimpleNamespace(value="device-user"),
41
+ }
42
+ ),
43
+ role=SimpleNamespace(
44
+ secrets=DummySecretsManager(
45
+ {
46
+ "user-role": SimpleNamespace(value="role-user"),
47
+ "password-role": SimpleNamespace(value="role-pass"),
48
+ }
49
+ )
50
+ ),
51
+ platform=SimpleNamespace(
52
+ secrets=DummySecretsManager(
53
+ {
54
+ "user-role": SimpleNamespace(value="platform-user"),
55
+ "password-role": SimpleNamespace(value="platform-pass"),
56
+ "secondary-role": SimpleNamespace(value="platform-secondary"),
57
+ "priv-role": SimpleNamespace(value="platform-priv"),
58
+ }
59
+ )
60
+ ),
61
+ )
62
+ mixin.get_secret = lambda secret: secret.value
63
+
64
+ assert mixin.get_credentials(device) == (
65
+ "device-user",
66
+ "role-pass",
67
+ "platform-secondary",
68
+ "platform-priv",
69
+ )
70
+
71
+
72
+ def test_get_credentials_skips_empty_secret_value() -> None:
73
+ mixin = build_mixin()
74
+
75
+ device = SimpleNamespace(
76
+ secrets=DummySecretsManager(
77
+ {
78
+ "password-role": SimpleNamespace(value=""),
79
+ }
80
+ ),
81
+ role=SimpleNamespace(
82
+ secrets=DummySecretsManager(
83
+ {
84
+ "password-role": SimpleNamespace(value="role-pass"),
85
+ }
86
+ )
87
+ ),
88
+ platform=SimpleNamespace(secrets=DummySecretsManager({})),
89
+ )
90
+ mixin.get_secret = lambda secret: secret.value
91
+
92
+ username, password, auth_secondary, default_desired_privilege_level = mixin.get_credentials(device)
93
+
94
+ assert username == "default-user"
95
+ assert password == "role-pass"
96
+ assert auth_secondary == "default-secondary"
97
+ assert default_desired_privilege_level == "default-priv"
98
+
99
+
100
+ def test_get_credentials_respects_custom_precedence() -> None:
101
+ mixin = build_mixin()
102
+
103
+ device = SimpleNamespace(
104
+ secrets=DummySecretsManager(
105
+ {
106
+ "password-role": SimpleNamespace(value="device-pass"),
107
+ }
108
+ ),
109
+ role=SimpleNamespace(
110
+ secrets=DummySecretsManager(
111
+ {
112
+ "user-role": SimpleNamespace(value="role-user"),
113
+ "password-role": SimpleNamespace(value="role-pass"),
114
+ }
115
+ )
116
+ ),
117
+ platform=SimpleNamespace(
118
+ secrets=DummySecretsManager(
119
+ {
120
+ "user-role": SimpleNamespace(value="platform-user"),
121
+ }
122
+ )
123
+ ),
124
+ )
125
+ mixin.get_secret = lambda secret: secret.value
126
+
127
+ # Force precedence: platform -> device -> role
128
+ mixin.secrets_precedence = ["platform", "device", "role"]
129
+
130
+ assert mixin.get_credentials(device) == (
131
+ "platform-user",
132
+ "device-pass",
133
+ "default-secondary",
134
+ "default-priv",
135
+ )