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.
- nautobot/core/authentication.py +0 -1
- nautobot/core/celery/schedulers.py +1 -3
- nautobot/core/cli/__init__.py +81 -39
- nautobot/core/settings.yaml +12 -4
- nautobot/core/tests/test_cli.py +120 -1
- nautobot/dcim/forms.py +1 -0
- nautobot/dcim/tables/devices.py +2 -1
- nautobot/dcim/templates/dcim/platform_create.html +3 -4
- nautobot/extras/models/jobs.py +7 -1
- nautobot/extras/signals.py +143 -113
- nautobot/extras/tests/test_utils.py +116 -1
- nautobot/extras/utils.py +18 -16
- nautobot/extras/views.py +2 -14
- nautobot/ipam/apps.py +1 -0
- nautobot/project-static/docs/development/core/release-checklist.html +2 -0
- nautobot/project-static/docs/release-notes/version-3.0.html +235 -0
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +329 -329
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +11 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +11 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +21 -9
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/12-add-tenant-dark.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/12-add-tenant-light.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/13-assign-tenant-to-device-dark.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/13-assign-tenant-to-device-light.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/14-assign-tenant-to-device-2-dark.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/14-assign-tenant-to-device-2-light.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/22-create-vlans-dark.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/22-create-vlans-light.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/23-create-vlans-2-dark.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/23-create-vlans-2-light.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/24-vlan-main-page-dark.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/24-vlan-main-page-light.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/25-add-vlan-to-interface-dark.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/25-add-vlan-to-interface-light.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/26-add-vlan-to-interface-2-dark.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/26-add-vlan-to-interface-2-light.png +0 -0
- nautobot/tenancy/tables.py +1 -1
- nautobot/ui/package-lock.json +36 -36
- nautobot/ui/package.json +3 -3
- nautobot/users/models.py +33 -0
- nautobot/users/tests/test_models.py +83 -0
- {nautobot-3.0.3.dist-info → nautobot-3.0.4.dist-info}/METADATA +4 -4
- {nautobot-3.0.3.dist-info → nautobot-3.0.4.dist-info}/RECORD +49 -41
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/12-add-tenant.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/13-assign-tenant-to-device.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/14-assign-tenant-to-device-2.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/22-create-vlans.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/23-create-vlans-2.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/24-vlan-main-page.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/25-add-vlan-to-interface.png +0 -0
- nautobot/project-static/docs/user-guide/feature-guides/images/getting-started-nautobot-ui/26-add-vlan-to-interface-2.png +0 -0
- {nautobot-3.0.3.dist-info → nautobot-3.0.4.dist-info}/LICENSE.txt +0 -0
- {nautobot-3.0.3.dist-info → nautobot-3.0.4.dist-info}/NOTICE +0 -0
- {nautobot-3.0.3.dist-info → nautobot-3.0.4.dist-info}/WHEEL +0 -0
- {nautobot-3.0.3.dist-info → nautobot-3.0.4.dist-info}/entry_points.txt +0 -0
nautobot/core/authentication.py
CHANGED
|
@@ -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:
|
nautobot/core/cli/__init__.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
48
|
-
- Load event brokers based on
|
|
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
|
-
|
|
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(
|
|
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(
|
|
60
|
-
extras[base_setting] = getattr(
|
|
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(
|
|
63
|
-
setattr(
|
|
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(
|
|
69
|
-
os.makedirs(
|
|
70
|
-
os.makedirs(
|
|
71
|
-
os.makedirs(os.path.join(
|
|
72
|
-
os.makedirs(os.path.join(
|
|
73
|
-
os.makedirs(
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
+
settings_module.DATABASES["job_logs"]["TEST"] = {"MIRROR": "default"}
|
|
93
101
|
|
|
94
102
|
#
|
|
95
103
|
# Media storage
|
|
96
104
|
#
|
|
97
105
|
|
|
98
|
-
|
|
99
|
-
|
|
106
|
+
# Avoid modifying nautobot.core.settings.STORAGES by accident!
|
|
107
|
+
settings_module.STORAGES = deepcopy(settings_module.STORAGES)
|
|
100
108
|
|
|
101
|
-
if hasattr(
|
|
102
|
-
|
|
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(
|
|
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 {
|
|
112
|
-
|
|
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
|
|
119
|
-
return
|
|
120
|
-
return getattr(
|
|
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(
|
|
172
|
+
load_plugins(settings_module)
|
|
131
173
|
|
|
132
174
|
#
|
|
133
175
|
# Event Broker
|
|
134
176
|
#
|
|
135
177
|
|
|
136
|
-
load_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
|
-
|
|
148
|
-
sys.modules["nautobot_config"] =
|
|
149
|
-
spec.loader.exec_module(
|
|
150
|
-
_preprocess_settings(
|
|
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):
|
nautobot/core/settings.yaml
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
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
|
|
nautobot/core/tests/test_cli.py
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
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={
|
nautobot/dcim/tables/devices.py
CHANGED
|
@@ -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 %}
|
|
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-
|
|
14
|
-
<div class="col-
|
|
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
|
nautobot/extras/models/jobs.py
CHANGED
|
@@ -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(
|
|
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}"
|