nautobot 3.0.3__py3-none-any.whl → 3.0.4__py3-none-any.whl

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 (57) hide show
  1. nautobot/core/authentication.py +0 -1
  2. nautobot/core/celery/schedulers.py +1 -3
  3. nautobot/core/cli/__init__.py +81 -39
  4. nautobot/core/settings.yaml +12 -4
  5. nautobot/core/tests/test_cli.py +120 -1
  6. nautobot/dcim/forms.py +1 -0
  7. nautobot/dcim/tables/devices.py +2 -1
  8. nautobot/dcim/templates/dcim/platform_create.html +3 -4
  9. nautobot/extras/models/jobs.py +7 -1
  10. nautobot/extras/signals.py +143 -113
  11. nautobot/extras/tests/test_utils.py +116 -1
  12. nautobot/extras/utils.py +18 -16
  13. nautobot/extras/views.py +2 -14
  14. nautobot/ipam/apps.py +1 -0
  15. nautobot/project-static/docs/development/core/release-checklist.html +2 -0
  16. nautobot/project-static/docs/release-notes/version-3.0.html +235 -0
  17. nautobot/project-static/docs/search/search_index.json +1 -1
  18. nautobot/project-static/docs/sitemap.xml +329 -329
  19. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  20. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +11 -4
  21. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +11 -4
  22. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +21 -9
  23. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/12-add-tenant-dark.png +0 -0
  24. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/12-add-tenant-light.png +0 -0
  25. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/13-assign-tenant-to-device-dark.png +0 -0
  26. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/13-assign-tenant-to-device-light.png +0 -0
  27. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/14-assign-tenant-to-device-2-dark.png +0 -0
  28. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/14-assign-tenant-to-device-2-light.png +0 -0
  29. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/22-create-vlans-dark.png +0 -0
  30. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/22-create-vlans-light.png +0 -0
  31. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/23-create-vlans-2-dark.png +0 -0
  32. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/23-create-vlans-2-light.png +0 -0
  33. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/24-vlan-main-page-dark.png +0 -0
  34. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/24-vlan-main-page-light.png +0 -0
  35. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/25-add-vlan-to-interface-dark.png +0 -0
  36. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/25-add-vlan-to-interface-light.png +0 -0
  37. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/26-add-vlan-to-interface-2-dark.png +0 -0
  38. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/26-add-vlan-to-interface-2-light.png +0 -0
  39. nautobot/tenancy/tables.py +1 -1
  40. nautobot/ui/package-lock.json +36 -36
  41. nautobot/ui/package.json +3 -3
  42. nautobot/users/models.py +33 -0
  43. nautobot/users/tests/test_models.py +83 -0
  44. {nautobot-3.0.3.dist-info → nautobot-3.0.4.dist-info}/METADATA +4 -4
  45. {nautobot-3.0.3.dist-info → nautobot-3.0.4.dist-info}/RECORD +49 -41
  46. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/12-add-tenant.png +0 -0
  47. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/13-assign-tenant-to-device.png +0 -0
  48. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/14-assign-tenant-to-device-2.png +0 -0
  49. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/22-create-vlans.png +0 -0
  50. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/23-create-vlans-2.png +0 -0
  51. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/24-vlan-main-page.png +0 -0
  52. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/25-add-vlan-to-interface.png +0 -0
  53. nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/26-add-vlan-to-interface-2.png +0 -0
  54. {nautobot-3.0.3.dist-info → nautobot-3.0.4.dist-info}/LICENSE.txt +0 -0
  55. {nautobot-3.0.3.dist-info → nautobot-3.0.4.dist-info}/NOTICE +0 -0
  56. {nautobot-3.0.3.dist-info → nautobot-3.0.4.dist-info}/WHEEL +0 -0
  57. {nautobot-3.0.3.dist-info → nautobot-3.0.4.dist-info}/entry_points.txt +0 -0
@@ -52,7 +52,6 @@ class ObjectPermissionBackend(ModelBackend):
52
52
  return user_obj.is_active and (user_obj.is_staff or user_obj.is_superuser)
53
53
 
54
54
  app_label, _action, model_name = resolve_permission(perm)
55
-
56
55
  if app_label == "users" and model_name == "admingroup":
57
56
  perm = perm.replace("users", "auth").replace("admingroup", "group")
58
57
 
@@ -138,9 +138,7 @@ class NautobotDatabaseScheduler(DatabaseScheduler):
138
138
  task_name=scheduled_job.job_model.class_path,
139
139
  celery_kwargs=entry.options,
140
140
  )
141
- job_result = run_kubernetes_job_and_return_job_result(
142
- job_queue, job_result, json.dumps(entry_kwargs)
143
- )
141
+ job_result = run_kubernetes_job_and_return_job_result(job_result, json.dumps(entry_kwargs))
144
142
  # Return an AsyncResult object to mimic the behavior of Celery tasks after the job is finished by Kubernetes Job Pod.
145
143
  resp = AsyncResult(job_result.id)
146
144
  else:
@@ -3,7 +3,9 @@ Utilities and primitives for the `nautobot-server` CLI command.
3
3
  """
4
4
 
5
5
  import argparse
6
+ from copy import deepcopy
6
7
  import importlib.util
8
+ import logging
7
9
  import os
8
10
  import sys
9
11
 
@@ -34,43 +36,46 @@ USAGE = """%(prog)s --help
34
36
  %(prog)s [-c CONFIG_PATH] SUBCOMMAND ..."""
35
37
 
36
38
 
37
- def _preprocess_settings(settings, config_path):
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ def _preprocess_settings(settings_module, config_path):
38
43
  """
39
44
  After loading nautobot_config.py and nautobot.core.settings, but before starting Django, modify the settings module.
40
45
 
41
- - Set settings.SETTINGS_PATH for ease of reference
46
+ - Set settings_module.SETTINGS_PATH for ease of reference
42
47
  - Handle `EXTRA_*` settings
43
48
  - Create Nautobot storage directories if they don't already exist
44
49
  - Change database backends to django-prometheus if appropriate
45
50
  - Set up 'job_logs' database mirror
46
51
  - Handle our custom `STORAGE_BACKEND` setting.
47
- - Load plugins based on settings.PLUGINS (potentially affecting INSTALLED_APPS, MIDDLEWARE, and CONSTANCE_CONFIG)
48
- - Load event brokers based on settings.EVENT_BROKERS
52
+ - Load plugins based on settings_module.PLUGINS (may affect INSTALLED_APPS, MIDDLEWARE, and CONSTANCE_CONFIG)
53
+ - Load event brokers based on settings_module.EVENT_BROKERS
49
54
  """
50
- settings.SETTINGS_PATH = config_path
55
+ settings_module.SETTINGS_PATH = config_path
51
56
 
52
57
  # Any setting that starts with EXTRA_ and matches a setting that is a list or tuple
53
58
  # will automatically append the values to the current setting.
54
59
  # "It might make sense to make this less magical"
55
60
  extras = {}
56
- for setting in dir(settings):
61
+ for setting in dir(settings_module):
57
62
  if setting == setting.upper() and setting.startswith("EXTRA_"):
58
63
  base_setting = setting[6:]
59
- if isinstance(getattr(settings, base_setting), (list, tuple)):
60
- extras[base_setting] = getattr(settings, setting)
64
+ if isinstance(getattr(settings_module, base_setting), (list, tuple)):
65
+ extras[base_setting] = getattr(settings_module, setting)
61
66
  for base_setting, extra_values in extras.items():
62
- base_value = getattr(settings, base_setting)
63
- setattr(settings, base_setting, base_value + type(base_value)(extra_values))
67
+ base_value = getattr(settings_module, base_setting)
68
+ setattr(settings_module, base_setting, base_value + type(base_value)(extra_values))
64
69
 
65
70
  #
66
71
  # Storage directories
67
72
  #
68
- os.makedirs(settings.GIT_ROOT, exist_ok=True)
69
- os.makedirs(settings.JOBS_ROOT, exist_ok=True)
70
- os.makedirs(settings.MEDIA_ROOT, exist_ok=True)
71
- os.makedirs(os.path.join(settings.MEDIA_ROOT, "devicetype-images"), exist_ok=True)
72
- os.makedirs(os.path.join(settings.MEDIA_ROOT, "image-attachments"), exist_ok=True)
73
- os.makedirs(settings.STATIC_ROOT, exist_ok=True)
73
+ os.makedirs(settings_module.GIT_ROOT, exist_ok=True)
74
+ os.makedirs(settings_module.JOBS_ROOT, exist_ok=True)
75
+ os.makedirs(settings_module.MEDIA_ROOT, exist_ok=True)
76
+ os.makedirs(os.path.join(settings_module.MEDIA_ROOT, "devicetype-images"), exist_ok=True)
77
+ os.makedirs(os.path.join(settings_module.MEDIA_ROOT, "image-attachments"), exist_ok=True)
78
+ os.makedirs(settings_module.STATIC_ROOT, exist_ok=True)
74
79
 
75
80
  #
76
81
  # Databases
@@ -78,62 +83,99 @@ def _preprocess_settings(settings, config_path):
78
83
 
79
84
  # If metrics are enabled and postgres is the backend, set the driver to the
80
85
  # one provided by django-prometheus.
81
- if settings.METRICS_ENABLED:
82
- if "postgres" in settings.DATABASES["default"]["ENGINE"]:
83
- settings.DATABASES["default"]["ENGINE"] = "django_prometheus.db.backends.postgresql"
84
- elif "mysql" in settings.DATABASES["default"]["ENGINE"]:
85
- settings.DATABASES["default"]["ENGINE"] = "django_prometheus.db.backends.mysql"
86
+ if settings_module.METRICS_ENABLED:
87
+ # Avoid modifying nautobot.core.settings.DATABASES by accident!
88
+ settings_module.DATABASES = deepcopy(settings_module.DATABASES)
89
+
90
+ if "postgres" in settings_module.DATABASES["default"]["ENGINE"]:
91
+ settings_module.DATABASES["default"]["ENGINE"] = "django_prometheus.db.backends.postgresql"
92
+ elif "mysql" in settings_module.DATABASES["default"]["ENGINE"]:
93
+ settings_module.DATABASES["default"]["ENGINE"] = "django_prometheus.db.backends.mysql"
86
94
 
87
95
  # Create secondary db connection for job logging. This still writes to the default db, but because it's a separate
88
96
  # connection, it allows allows us to "escape" from transaction.atomic() and ensure that job log entries are saved
89
97
  # to the database even when the rest of the job transaction is rolled back.
90
- settings.DATABASES["job_logs"] = settings.DATABASES["default"].copy()
98
+ settings_module.DATABASES["job_logs"] = deepcopy(settings_module.DATABASES["default"])
91
99
  # When running unit tests, treat it as a mirror of the default test DB, not a separate test DB of its own
92
- settings.DATABASES["job_logs"]["TEST"] = {"MIRROR": "default"}
100
+ settings_module.DATABASES["job_logs"]["TEST"] = {"MIRROR": "default"}
93
101
 
94
102
  #
95
103
  # Media storage
96
104
  #
97
105
 
98
- if hasattr(settings, "JOB_FILE_IO_STORAGE"):
99
- settings.STORAGES.setdefault("nautobotjobfiles", {})["BACKEND"] = settings.JOB_FILE_IO_STORAGE
106
+ # Avoid modifying nautobot.core.settings.STORAGES by accident!
107
+ settings_module.STORAGES = deepcopy(settings_module.STORAGES)
100
108
 
101
- if hasattr(settings, "STORAGE_BACKEND") and settings.STORAGE_BACKEND is not None:
102
- settings.STORAGES["default"]["BACKEND"] = settings.STORAGE_BACKEND
109
+ if hasattr(settings_module, "JOB_FILE_IO_STORAGE"):
110
+ settings_module.STORAGES.setdefault("nautobotjobfiles", {})["BACKEND"] = settings_module.JOB_FILE_IO_STORAGE
111
+
112
+ if hasattr(settings_module, "STORAGE_BACKEND") and settings_module.STORAGE_BACKEND is not None:
113
+ settings_module.STORAGES["default"]["BACKEND"] = settings_module.STORAGE_BACKEND
103
114
 
104
115
  # django-storages
105
- if hasattr(settings, "STORAGE_BACKEND") and settings.STORAGE_BACKEND.startswith("storages."):
116
+ if hasattr(settings_module, "STORAGE_BACKEND") and settings_module.STORAGE_BACKEND.startswith("storages."):
106
117
  try:
107
118
  import storages.utils
108
119
  except ModuleNotFoundError as e:
109
120
  if getattr(e, "name") == "storages":
110
121
  raise ImproperlyConfigured(
111
- f"STORAGE_BACKEND is set to {settings.STORAGE_BACKEND} but django-storages is not present. It "
112
- f"can be installed by running 'pip install django-storages'."
122
+ f"STORAGE_BACKEND is set to {settings_module.STORAGE_BACKEND} but django-storages is not present. "
123
+ "It can be installed by running 'pip install django-storages'."
113
124
  )
114
125
  raise e
115
126
 
116
127
  # Monkey-patch django-storages to fetch settings from STORAGE_CONFIG or fall back to settings
117
128
  def _setting(name, default=None):
118
- if name in settings.STORAGE_CONFIG:
119
- return settings.STORAGE_CONFIG[name]
120
- return getattr(settings, name, default)
129
+ if name in settings_module.STORAGE_CONFIG:
130
+ return settings_module.STORAGE_CONFIG[name]
131
+ return getattr(settings_module, name, default)
121
132
 
122
133
  storages.utils.setting = _setting
123
134
 
135
+ # Django 4.2 will throw an exception if both:
136
+ # - DEFAULT_FILE_STORAGE/STATICFILES_STORAGE is set in nautobot_config.py (recommended until Nautobot v2.4.24)
137
+ # - STORAGES is configured in nautobot.core.settings (which it is nowadays).
138
+ # Unfortunately, it's not implemented as a standard system check (which we could opt out of) but is instead
139
+ # hard-coded, so we hack around it instead by explicitly copying any non-default *_STORAGE to STORAGES
140
+ # and then unsetting *_STORAGE.
141
+ for setting_name, storages_key, default_value in [
142
+ ("DEFAULT_FILE_STORAGE", "default", "django.core.files.storage.FileSystemStorage"),
143
+ ("STATICFILES_STORAGE", "staticfiles", "django.contrib.staticfiles.storage.StaticFilesStorage"),
144
+ ]:
145
+ if hasattr(settings_module, setting_name):
146
+ # Make sure we don't clobber any existing explicit configuration in STORAGES:
147
+ if settings_module.STORAGES[storages_key]["BACKEND"] not in (
148
+ default_value, # Nautobot/Django default
149
+ getattr(settings_module, setting_name), # same as explicitly set value for setting_name
150
+ ):
151
+ raise ImproperlyConfigured(
152
+ f"It looks like you've configured both {setting_name} and STORAGES['{storages_key}']['BACKEND'],"
153
+ "but their values do not match."
154
+ )
155
+
156
+ # No clobbering, but undesired, so warn the user and handle it:
157
+ logger.warning(
158
+ f"It looks like you've configured {setting_name} in {settings_module.SETTINGS_PATH}. "
159
+ "This setting is deprecated since Nautobot v2.4.24, and support will be removed in Nautobot v3.1. "
160
+ f"You should migrate to configuring STORAGES['{storages_key}']['BACKEND'] instead. Refer to "
161
+ "https://docs.nautobot.com/projects/core/en/stable/user-guide/administration/configuration/settings/#storages for guidance."
162
+ )
163
+ settings_module.STORAGES[storages_key]["BACKEND"] = getattr(settings_module, setting_name)
164
+ delattr(settings_module, setting_name)
165
+
124
166
  #
125
167
  # Plugins
126
168
  #
127
169
 
128
170
  # Process the plugins and manipulate the specified config settings that are
129
171
  # passed in.
130
- load_plugins(settings)
172
+ load_plugins(settings_module)
131
173
 
132
174
  #
133
175
  # Event Broker
134
176
  #
135
177
 
136
- load_event_brokers(settings.EVENT_BROKERS)
178
+ load_event_brokers(settings_module.EVENT_BROKERS)
137
179
 
138
180
 
139
181
  def load_settings(config_path):
@@ -144,10 +186,10 @@ def load_settings(config_path):
144
186
  "Please provide a valid --config-path path, or use 'nautobot-server init' to create a new configuration."
145
187
  )
146
188
  spec = importlib.util.spec_from_file_location("nautobot_config", config_path)
147
- module = importlib.util.module_from_spec(spec)
148
- sys.modules["nautobot_config"] = module
149
- spec.loader.exec_module(module)
150
- _preprocess_settings(module, config_path)
189
+ settings_module = importlib.util.module_from_spec(spec)
190
+ sys.modules["nautobot_config"] = settings_module
191
+ spec.loader.exec_module(settings_module)
192
+ _preprocess_settings(settings_module, config_path)
151
193
 
152
194
 
153
195
  class _VerboseHelpFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter):
@@ -1886,8 +1886,8 @@ properties:
1886
1886
  For an example of using `django-storages` with AWS S3 buckets, visit the
1887
1887
  [django-storages with S3](../guides/s3-django-storage.md) user-guide.
1888
1888
 
1889
- The configuration parameters for the specified storage backend are defined under the
1890
- [`STORAGE_CONFIG`](#storage_config) setting.
1889
+ The configuration parameters for the specified storage backend are defined under the settings
1890
+ [`STORAGE_CONFIG`](#storage_config) (deprecated) or [`STORAGES`](#storages) (recommended).
1891
1891
  see_also:
1892
1892
  "`STORAGES`": "#storages"
1893
1893
  type: "string"
@@ -1895,8 +1895,16 @@ properties:
1895
1895
  description: "(Deprecated) Dictionary of config parameters for the storage backend configured as STORAGE_BACKEND."
1896
1896
  details: |-
1897
1897
  !!! warning
1898
- This setting is deprecated and will be removed in Nautobot v3.1.
1899
- In its place, you should set [`STORAGES["default"]["OPTIONS"]`](#storages).
1898
+ This setting is deprecated and will be removed in Nautobot v3.1. In its place, you should set
1899
+ [`STORAGES["default"]["OPTIONS"]` and/or `STORAGES["staticfiles"]["OPTIONS"]`](#storages).
1900
+
1901
+ Note that `STORAGE_CONFIG` is implemented to provide a bit of configuration "magic" that
1902
+ `STORAGES["..."]["OPTIONS"]` does not; specifically, when `STORAGE_BACKEND` is using any module from
1903
+ `django-storages`, if `STATICFILES_STORAGE` is also using `django-storages`, the `STORAGE_CONFIG` will be
1904
+ automatically applied to both file storage types. When using `STORAGES` instead of `STORAGE_CONFIG`, this is
1905
+ not automatically the case, permitting the two types to be configured independently, but also potentially
1906
+ requiring duplicate configuration under `STORAGES["default"]["OPTIONS"]` and
1907
+ `STORAGES["staticfiles"]["OPTIONS"]` if both types are using the same backend.
1900
1908
 
1901
1909
  The specific parameters to be used here are specific to each backend.
1902
1910
 
@@ -1,4 +1,9 @@
1
- from nautobot.core.cli import migrate_deprecated_templates
1
+ import importlib.util
2
+ import os.path
3
+ import sys
4
+ from unittest import mock
5
+
6
+ from nautobot.core.cli import _preprocess_settings, migrate_deprecated_templates
2
7
  from nautobot.core.testing import TestCase
3
8
 
4
9
 
@@ -38,3 +43,117 @@ class TestMigrateTemplates(TestCase):
38
43
  replaced_content, was_updated = migrate_deprecated_templates.replace_template_references(original_content)
39
44
  self.assertTrue(was_updated)
40
45
  self.assertEqual(replaced_content, new_content)
46
+
47
+
48
+ @mock.patch("nautobot.core.cli.load_plugins")
49
+ @mock.patch("nautobot.core.cli.load_event_brokers")
50
+ class TestPreprocessSettings(TestCase):
51
+ """Tests for the `_preprocess_settings` function in nautobot.core.cli, as it's important to Nautobot startup."""
52
+
53
+ def load_settings_module(self):
54
+ # Load the testing nautobot_config.py as a self-contained module
55
+ config_path = os.path.join(os.path.dirname(__file__), "nautobot_config.py")
56
+ spec = importlib.util.spec_from_file_location("test_nautobot_config", config_path)
57
+ settings_module = importlib.util.module_from_spec(spec)
58
+ # nautobot.core.cli.load_settings would do the below, but obviously we don't want to do that here:
59
+ # sys.modules["nautobot_config"] = settings_module
60
+ spec.loader.exec_module(settings_module)
61
+ return settings_module, config_path
62
+
63
+ def test_basic_path(self, mock_load_event_brokers, mock_load_plugins):
64
+ """Basic operation of the function."""
65
+ settings_module, config_path = self.load_settings_module()
66
+
67
+ # Process the settings module
68
+ _preprocess_settings(settings_module, config_path)
69
+
70
+ # _preprocess_settings should have set SETTINGS_PATH on the module
71
+ self.assertEqual(settings_module.SETTINGS_PATH, config_path)
72
+
73
+ # the default test settings have no EXTRA_* settings to handle
74
+
75
+ # all media paths should exist
76
+ self.assertTrue(os.path.isdir(settings_module.GIT_ROOT))
77
+ self.assertTrue(os.path.isdir(settings_module.JOBS_ROOT))
78
+ self.assertTrue(os.path.isdir(settings_module.MEDIA_ROOT))
79
+ self.assertTrue(os.path.isdir(os.path.join(settings_module.MEDIA_ROOT, "devicetype-images")))
80
+ self.assertTrue(os.path.isdir(os.path.join(settings_module.MEDIA_ROOT, "image-attachments")))
81
+ self.assertTrue(os.path.isdir(settings_module.STATIC_ROOT))
82
+
83
+ # databases should be using the prometheus backends
84
+ self.assertTrue(settings_module.METRICS_ENABLED)
85
+ self.assertIn("django_prometheus.db.backends", settings_module.DATABASES["default"]["ENGINE"])
86
+
87
+ # job_logs database connection should exist
88
+ self.assertIn("job_logs", settings_module.DATABASES)
89
+ self.assertIn("TEST", settings_module.DATABASES["job_logs"])
90
+ self.assertEqual(settings_module.DATABASES["job_logs"]["TEST"], {"MIRROR": "default"})
91
+ for key, value in settings_module.DATABASES["default"].items():
92
+ if key == "TEST":
93
+ continue
94
+ self.assertEqual(value, settings_module.DATABASES["job_logs"][key])
95
+
96
+ # STORAGES should remain as default
97
+ self.assertEqual(
98
+ settings_module.STORAGES,
99
+ {
100
+ "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
101
+ "staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"},
102
+ "nautobotjobfiles": {"BACKEND": "db_file_storage.storage.DatabaseFileStorage"},
103
+ },
104
+ )
105
+
106
+ mock_load_plugins.assert_called_with(settings_module)
107
+ mock_load_event_brokers.assert_called_with(settings_module.EVENT_BROKERS)
108
+
109
+ def test_EXTRA_behavior(self, *args):
110
+ """Handling of special settings like EXTRA_INSTALLED_APPS and EXTRA_MIDDLEWARE."""
111
+ settings_module, config_path = self.load_settings_module()
112
+
113
+ # Inject EXTRA_INSTALLED_APPS and EXTRA_MIDDLEWARE to the settings module for test purposes
114
+ settings_module.EXTRA_INSTALLED_APPS = ["foo.bar"]
115
+ settings_module.EXTRA_MIDDLEWARE = ("baz.bat",)
116
+
117
+ # Process the settings module
118
+ _preprocess_settings(settings_module, config_path)
119
+
120
+ self.assertIn("foo.bar", settings_module.INSTALLED_APPS)
121
+ # more specifically:
122
+ self.assertEqual("foo.bar", settings_module.INSTALLED_APPS[-1])
123
+ self.assertIn("baz.bat", settings_module.MIDDLEWARE)
124
+ # more specifically:
125
+ self.assertEqual("baz.bat", settings_module.MIDDLEWARE[-1])
126
+
127
+ def test_legacy_storage_behavior(self, *args):
128
+ """Handling legacy storage settings."""
129
+ settings_module, config_path = self.load_settings_module()
130
+
131
+ settings_module.DEFAULT_FILE_STORAGE = "storages.some_custom_backend"
132
+ settings_module.JOB_FILE_IO_STORAGE = "some_custom_job_file_storage"
133
+ settings_module.STATICFILES_STORAGE = "some_custom_static_storage"
134
+ settings_module.STORAGE_BACKEND = "storages.some_custom_backend"
135
+ settings_module.STORAGE_CONFIG = {"MY_BACKEND_OPTION": "some_value"}
136
+
137
+ import storages.utils
138
+
139
+ original_setting = storages.utils.setting
140
+ del sys.modules["storages.utils"]
141
+ del storages.utils
142
+
143
+ try:
144
+ # Process the settings module
145
+ _preprocess_settings(settings_module, config_path)
146
+
147
+ self.assertFalse(hasattr(settings_module, "DEFAULT_FILE_STORAGE")) # unset to avoid a Django exception
148
+ self.assertEqual("storages.some_custom_backend", settings_module.STORAGES["default"]["BACKEND"])
149
+ self.assertEqual("some_custom_job_file_storage", settings_module.STORAGES["nautobotjobfiles"]["BACKEND"])
150
+ self.assertFalse(hasattr(settings_module, "STATICFILES_STORAGE")) # unset to avoid a Django exception
151
+ self.assertEqual("some_custom_static_storage", settings_module.STORAGES["staticfiles"]["BACKEND"])
152
+
153
+ self.assertEqual("some_value", storages.utils.setting("MY_BACKEND_OPTION"))
154
+ finally:
155
+ # Clean up the STORAGE_CONFIG monkeypatch
156
+ import storages.utils # pylint: disable=reimported
157
+
158
+ storages.utils.setting = original_setting
159
+ self.assertIsNone(storages.utils.setting("MY_BACKEND_OPTION"))
nautobot/dcim/forms.py CHANGED
@@ -4516,6 +4516,7 @@ class CableFilterForm(BootstrapMixin, StatusModelFilterFormMixin, forms.Form):
4516
4516
  color = forms.CharField(max_length=6, required=False, widget=ColorSelect()) # RGB color code
4517
4517
  device = DynamicModelMultipleChoiceField(
4518
4518
  queryset=Device.objects.all(),
4519
+ to_field_name="name",
4519
4520
  required=False,
4520
4521
  label="Device",
4521
4522
  query_params={
@@ -204,7 +204,7 @@ class DeviceTable(StatusTableMixin, RoleTableMixin, BaseTable):
204
204
  vc_priority = tables.Column(verbose_name="VC Priority")
205
205
  device_redundancy_group = tables.Column(linkify=True)
206
206
  device_redundancy_group_priority = tables.TemplateColumn(
207
- template_code="""{% if record.device_redundancy_group %}<span class="badge badge-default">{{ record.device_redundancy_group_priority|default:'None' }}</span>{% else %}{% endif %}"""
207
+ template_code="""{% if record.device_redundancy_group %}<span class="badge badge-default">{{ record.device_redundancy_group_priority|default:'None' }}</span>{% else %}<span class="text-secondary">—</span>{% endif %}"""
208
208
  )
209
209
  controller_managed_device_group = tables.Column(linkify=True, verbose_name="Device Group")
210
210
  software_version = tables.Column(linkify=True, verbose_name="Software Version")
@@ -1260,6 +1260,7 @@ class DeviceRedundancyGroupTable(BaseTable):
1260
1260
  fields = (
1261
1261
  "pk",
1262
1262
  "name",
1263
+ "description",
1263
1264
  "status",
1264
1265
  "failover_strategy",
1265
1266
  "controller_count",
@@ -6,12 +6,11 @@
6
6
 
7
7
  {% block form_fields %}
8
8
  {% render_field form.name %}
9
- {% render_field form.slug %}
10
9
  {% render_field form.manufacturer %}
11
10
 
12
- <div class="mb-10 d-flex justify-content-center{% if form.network_driver.errors %} has-error{% endif %}">
13
- <label class="col-lg-3 col-form-label" for="id_network_driver">Network driver</label>
14
- <div class="col-lg-9">
11
+ <div class="mb-10 d-md-flex justify-content-center{% if form.network_driver.errors %} has-error{% endif %}">
12
+ <label class="col-md-3 col-form-label" for="id_network_driver">Network driver</label>
13
+ <div class="col-md-9">
15
14
  {{ form.network_driver }}
16
15
  <span class="form-text">
17
16
  The <a href="https://netutils.readthedocs.io/en/latest/user/lib_use_cases_lib_mapper/">normalized network driver</a> to use when interacting with devices
@@ -253,6 +253,7 @@ class Job(PrimaryModel):
253
253
  )
254
254
  objects = BaseManager.from_queryset(JobQuerySet)()
255
255
  is_data_compliance_model = False
256
+ is_version_controlled = False
256
257
 
257
258
  documentation_static_path = "docs/user-guide/platform-functionality/jobs/models.html"
258
259
 
@@ -533,6 +534,7 @@ class JobLogEntry(BaseModel):
533
534
 
534
535
  is_metadata_associable_model = False
535
536
  is_data_compliance_model = False
537
+ is_version_controlled = False
536
538
 
537
539
  documentation_static_path = "docs/user-guide/platform-functionality/jobs/models.html"
538
540
  hide_in_diff_view = True
@@ -585,6 +587,7 @@ class JobQueue(PrimaryModel):
585
587
 
586
588
  documentation_static_path = "docs/user-guide/platform-functionality/jobs/jobqueue.html"
587
589
  is_data_compliance_model = False
590
+ is_version_controlled = False
588
591
 
589
592
  class Meta:
590
593
  ordering = ["name"]
@@ -616,6 +619,7 @@ class JobQueueAssignment(BaseModel):
616
619
  job_queue = models.ForeignKey(JobQueue, on_delete=models.CASCADE, related_name="job_assignments")
617
620
  is_metadata_associable_model = False
618
621
  is_data_compliance_model = False
622
+ is_version_controlled = False
619
623
 
620
624
  class Meta:
621
625
  unique_together = ["job", "job_queue"]
@@ -695,6 +699,7 @@ class JobResult(SavedViewMixin, BaseModel, CustomFieldModel):
695
699
  documentation_static_path = "docs/user-guide/platform-functionality/jobs/models.html"
696
700
  hide_in_diff_view = True
697
701
  is_data_compliance_model = False
702
+ is_version_controlled = False
698
703
 
699
704
  def __init__(self, *args, **kwargs):
700
705
  super().__init__(*args, **kwargs)
@@ -897,7 +902,7 @@ class JobResult(SavedViewMixin, BaseModel, CustomFieldModel):
897
902
  # so that `run_kubernetes_job_and_return_job_result` is not executed again and the job will be run locally.
898
903
  if job_queue.queue_type == JobQueueTypeChoices.TYPE_KUBERNETES and not synchronous:
899
904
  # TODO: make this branch aware!
900
- return run_kubernetes_job_and_return_job_result(job_queue, job_result, json.dumps(job_kwargs))
905
+ return run_kubernetes_job_and_return_job_result(job_result, json.dumps(job_kwargs))
901
906
 
902
907
  job_celery_kwargs = {
903
908
  "nautobot_job_job_model_id": job_model.id,
@@ -1277,6 +1282,7 @@ class ScheduledJob(ApprovableModelMixin, BaseModel):
1277
1282
 
1278
1283
  documentation_static_path = "docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html"
1279
1284
  is_data_compliance_model = False
1285
+ is_version_controlled = False
1280
1286
 
1281
1287
  def __str__(self):
1282
1288
  return f"{self.name}: {self.interval}"