nautobot 3.0.0rc1__py3-none-any.whl → 3.0.1__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.
Potentially problematic release.
This version of nautobot might be problematic. Click here for more details.
- nautobot/apps/forms.py +8 -0
- nautobot/apps/templatetags.py +231 -0
- nautobot/apps/testing.py +11 -1
- nautobot/apps/ui.py +21 -1
- nautobot/apps/utils.py +26 -1
- nautobot/core/celery/__init__.py +46 -1
- nautobot/core/cli/bootstrap_v3_to_v5.py +185 -44
- nautobot/core/cli/bootstrap_v3_to_v5_changes.yaml +314 -0
- nautobot/core/graphql/generators.py +2 -2
- nautobot/core/jobs/bulk_actions.py +12 -6
- nautobot/core/jobs/cleanup.py +13 -1
- nautobot/core/settings.py +13 -0
- nautobot/core/settings.yaml +22 -0
- nautobot/core/settings_funcs.py +11 -1
- nautobot/core/tables.py +19 -1
- nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
- nautobot/core/templates/components/panel/header_extra_content_table.html +9 -3
- nautobot/core/templates/generic/object_create.html +1 -1
- nautobot/core/templates/inc/header.html +9 -10
- nautobot/core/templates/login.html +16 -1
- nautobot/core/templates/nautobot_config.py.j2 +14 -1
- nautobot/core/templates/redoc_ui.html +3 -0
- nautobot/core/templatetags/helpers.py +3 -3
- nautobot/core/testing/views.py +3 -1
- nautobot/core/tests/test_graphql.py +13 -0
- nautobot/core/tests/test_jobs.py +118 -0
- nautobot/core/tests/test_views.py +24 -0
- nautobot/core/ui/bulk_buttons.py +2 -3
- nautobot/core/utils/lookup.py +2 -3
- nautobot/core/utils/permissions.py +1 -1
- nautobot/core/views/generic.py +1 -0
- nautobot/core/views/mixins.py +37 -10
- nautobot/core/views/renderers.py +1 -0
- nautobot/core/views/utils.py +3 -3
- nautobot/data_validation/views.py +1 -9
- nautobot/dcim/forms.py +9 -9
- nautobot/dcim/models/devices.py +3 -3
- nautobot/dcim/tables/power.py +3 -0
- nautobot/dcim/templates/dcim/controllermanageddevicegroup_create.html +1 -1
- nautobot/dcim/views.py +30 -44
- nautobot/extras/api/views.py +14 -3
- nautobot/extras/choices.py +3 -0
- nautobot/extras/jobs.py +48 -2
- nautobot/extras/migrations/0132_approval_workflow_seed_data.py +127 -0
- nautobot/extras/models/approvals.py +11 -1
- nautobot/extras/models/models.py +19 -0
- nautobot/extras/models/relationships.py +3 -1
- nautobot/extras/tables.py +35 -18
- nautobot/extras/templates/extras/approval_workflow/approve.html +9 -2
- nautobot/extras/templates/extras/approval_workflow/deny.html +9 -3
- nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +1 -1
- nautobot/extras/templates/extras/customfield_update.html +1 -1
- nautobot/extras/templates/extras/dynamicgroup_update.html +2 -2
- nautobot/extras/templates/extras/inc/approval_buttons_column.html +10 -2
- nautobot/extras/templates/extras/inc/job_tiles.html +2 -2
- nautobot/extras/templates/extras/inc/jobresult.html +1 -1
- nautobot/extras/templates/extras/metadatatype_create.html +1 -1
- nautobot/extras/templates/extras/object_approvalworkflow.html +2 -3
- nautobot/extras/templates/extras/secretsgroup_update.html +1 -1
- nautobot/extras/tests/test_api.py +57 -3
- nautobot/extras/tests/test_customfields_filters.py +84 -4
- nautobot/extras/tests/test_views.py +323 -6
- nautobot/extras/views.py +114 -39
- nautobot/ipam/constants.py +2 -2
- nautobot/ipam/tables.py +7 -6
- nautobot/load_balancers/constants.py +6 -0
- nautobot/load_balancers/migrations/0001_initial.py +14 -3
- nautobot/load_balancers/models.py +5 -4
- nautobot/load_balancers/tables.py +5 -0
- nautobot/project-static/dist/css/nautobot.css +1 -1
- nautobot/project-static/dist/css/nautobot.css.map +1 -1
- nautobot/project-static/dist/js/graphql-libraries.js +1 -1
- nautobot/project-static/dist/js/graphql-libraries.js.map +1 -1
- nautobot/project-static/dist/js/libraries.js +1 -1
- nautobot/project-static/dist/js/libraries.js.LICENSE.txt +38 -2
- nautobot/project-static/dist/js/libraries.js.map +1 -1
- nautobot/project-static/dist/js/nautobot-graphiql.js +1 -1
- nautobot/project-static/dist/js/nautobot-graphiql.js.map +1 -1
- nautobot/project-static/dist/js/nautobot.js +1 -1
- nautobot/project-static/dist/js/nautobot.js.map +1 -1
- nautobot/project-static/img/dark-theme.png +0 -0
- nautobot/project-static/img/light-theme.png +0 -0
- nautobot/project-static/img/system-theme.png +0 -0
- nautobot/project-static/js/forms.js +1 -85
- nautobot/tenancy/tables.py +3 -2
- nautobot/tenancy/views.py +3 -2
- nautobot/ui/package-lock.json +553 -569
- nautobot/ui/package.json +10 -10
- nautobot/ui/src/js/checkbox.js +132 -0
- nautobot/ui/src/js/nautobot.js +6 -0
- nautobot/ui/src/js/select2.js +69 -73
- nautobot/ui/src/js/theme.js +129 -39
- nautobot/ui/src/scss/nautobot.scss +11 -1
- nautobot/vpn/templates/vpn/vpnprofile_create.html +2 -2
- nautobot/wireless/filters.py +15 -1
- nautobot/wireless/tables.py +18 -14
- nautobot/wireless/templates/wireless/wirelessnetwork_create.html +1 -1
- {nautobot-3.0.0rc1.dist-info → nautobot-3.0.1.dist-info}/METADATA +2 -2
- {nautobot-3.0.0rc1.dist-info → nautobot-3.0.1.dist-info}/RECORD +103 -98
- {nautobot-3.0.0rc1.dist-info → nautobot-3.0.1.dist-info}/LICENSE.txt +0 -0
- {nautobot-3.0.0rc1.dist-info → nautobot-3.0.1.dist-info}/NOTICE +0 -0
- {nautobot-3.0.0rc1.dist-info → nautobot-3.0.1.dist-info}/WHEEL +0 -0
- {nautobot-3.0.0rc1.dist-info → nautobot-3.0.1.dist-info}/entry_points.txt +0 -0
nautobot/core/views/mixins.py
CHANGED
|
@@ -17,7 +17,7 @@ from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput
|
|
|
17
17
|
from django.http import HttpResponse
|
|
18
18
|
from django.shortcuts import get_object_or_404, redirect
|
|
19
19
|
from django.template.loader import select_template, TemplateDoesNotExist
|
|
20
|
-
from django.urls import reverse
|
|
20
|
+
from django.urls import resolve, reverse
|
|
21
21
|
from django.urls.exceptions import NoReverseMatch
|
|
22
22
|
from django.utils.encoding import iri_to_uri
|
|
23
23
|
from django.utils.html import format_html
|
|
@@ -57,7 +57,7 @@ from nautobot.core.views.utils import (
|
|
|
57
57
|
)
|
|
58
58
|
from nautobot.extras.context_managers import deferred_change_logging_for_bulk_operation
|
|
59
59
|
from nautobot.extras.forms import NoteForm
|
|
60
|
-
from nautobot.extras.models import ExportTemplate, Job, JobResult, SavedView, UserSavedViewAssociation
|
|
60
|
+
from nautobot.extras.models import ExportTemplate, Job, JobResult, SavedView, ScheduledJob, UserSavedViewAssociation
|
|
61
61
|
from nautobot.extras.tables import NoteTable, ObjectChangeTable
|
|
62
62
|
from nautobot.extras.utils import bulk_delete_with_bulk_change_logging, get_base_template, remove_prefix_from_cf_key
|
|
63
63
|
|
|
@@ -74,8 +74,6 @@ PERMISSIONS_ACTION_MAP = {
|
|
|
74
74
|
"changelog": "view",
|
|
75
75
|
"notes": "view",
|
|
76
76
|
"data_compliance": "view",
|
|
77
|
-
"approve": "change",
|
|
78
|
-
"deny": "change",
|
|
79
77
|
}
|
|
80
78
|
|
|
81
79
|
|
|
@@ -416,11 +414,11 @@ class NautobotViewSetMixin(GenericViewSet, UIComponentsMixin, AccessMixin, GetRe
|
|
|
416
414
|
"""
|
|
417
415
|
model_permissions = []
|
|
418
416
|
for action in actions:
|
|
419
|
-
# Append additional object permissions if specified.
|
|
420
|
-
if self.custom_view_additional_permissions:
|
|
421
|
-
model_permissions.append(*self.custom_view_additional_permissions)
|
|
422
417
|
# Append the model-level permissions for the action.
|
|
423
418
|
model_permissions.append(f"{model._meta.app_label}.{action}_{model._meta.model_name}")
|
|
419
|
+
# Append additional object permissions if specified.
|
|
420
|
+
if self.custom_view_additional_permissions:
|
|
421
|
+
model_permissions.extend(self.custom_view_additional_permissions)
|
|
424
422
|
return model_permissions
|
|
425
423
|
|
|
426
424
|
def get_required_permission(self):
|
|
@@ -931,10 +929,12 @@ class ObjectListViewMixin(NautobotViewSetMixin, mixins.ListModelMixin):
|
|
|
931
929
|
self.filterset_class(),
|
|
932
930
|
)
|
|
933
931
|
|
|
932
|
+
resolved_path = resolve(request.path)
|
|
933
|
+
# Note that `resolved_path.app_name` does work even for nested paths like `plugins:example_app:...`
|
|
934
|
+
view_name = f"{resolved_path.app_name}:{resolved_path.url_name}"
|
|
935
|
+
|
|
934
936
|
# Check if there is a default for this view for this specific user
|
|
935
937
|
user_default_saved_view = None
|
|
936
|
-
app_label, model_name = queryset.model._meta.label.split(".")
|
|
937
|
-
view_name = f"{app_label}:{model_name.lower()}_list"
|
|
938
938
|
user = request.user
|
|
939
939
|
if not isinstance(user, AnonymousUser):
|
|
940
940
|
try:
|
|
@@ -1065,7 +1065,8 @@ class ObjectEditViewMixin(NautobotViewSetMixin, mixins.CreateModelMixin, mixins.
|
|
|
1065
1065
|
if hasattr(obj, "clone_fields"):
|
|
1066
1066
|
url = f"{request.path}?{prepare_cloned_fields(obj)}"
|
|
1067
1067
|
self.success_url = url
|
|
1068
|
-
|
|
1068
|
+
else:
|
|
1069
|
+
self.success_url = request.get_full_path()
|
|
1069
1070
|
else:
|
|
1070
1071
|
return_url = form.cleaned_data.get("return_url")
|
|
1071
1072
|
if url_has_allowed_host_and_scheme(url=return_url, allowed_hosts=request.get_host()):
|
|
@@ -1152,6 +1153,19 @@ class BulkEditAndBulkDeleteModelMixin:
|
|
|
1152
1153
|
# BulkDeleteObjects job form cannot be invalid; Hence no handling of invalid case.
|
|
1153
1154
|
job_form.is_valid()
|
|
1154
1155
|
job_kwargs = BulkDeleteObjects.prepare_job_kwargs(job_form.cleaned_data)
|
|
1156
|
+
# adapted from nautobot/extras/views JobRunView.post() - TODO: deduplicate this code and unify code paths
|
|
1157
|
+
with transaction.atomic():
|
|
1158
|
+
scheduled_job = ScheduledJob.create_schedule(
|
|
1159
|
+
job_model,
|
|
1160
|
+
request.user,
|
|
1161
|
+
**BulkDeleteObjects.serialize_data(job_kwargs),
|
|
1162
|
+
)
|
|
1163
|
+
if scheduled_job.has_approval_workflow_definition():
|
|
1164
|
+
messages.success(request, "Job '{scheduled_job.name}' successfully submitted for approval")
|
|
1165
|
+
return redirect("extras:scheduledjob_approvalworkflow", pk=scheduled_job.pk)
|
|
1166
|
+
else:
|
|
1167
|
+
scheduled_job.delete()
|
|
1168
|
+
|
|
1155
1169
|
job_result = JobResult.enqueue_job(
|
|
1156
1170
|
job_model,
|
|
1157
1171
|
request.user,
|
|
@@ -1174,6 +1188,19 @@ class BulkEditAndBulkDeleteModelMixin:
|
|
|
1174
1188
|
# NOTE: BulkEditObjects cant be invalid, so there is no need for handling invalid error
|
|
1175
1189
|
job_form.is_valid()
|
|
1176
1190
|
job_kwargs = BulkEditObjects.prepare_job_kwargs(job_form.cleaned_data)
|
|
1191
|
+
# adapted from nautobot/extras/views JobRunView.post() - TODO: deduplicate this code and unify code paths
|
|
1192
|
+
with transaction.atomic():
|
|
1193
|
+
scheduled_job = ScheduledJob.create_schedule(
|
|
1194
|
+
job_model,
|
|
1195
|
+
request.user,
|
|
1196
|
+
**BulkEditObjects.serialize_data(job_kwargs),
|
|
1197
|
+
)
|
|
1198
|
+
if scheduled_job.has_approval_workflow_definition():
|
|
1199
|
+
messages.success(request, "Job '{scheduled_job.name}' successfully submitted for approval")
|
|
1200
|
+
return redirect("extras:scheduledjob_approvalworkflow", pk=scheduled_job.pk)
|
|
1201
|
+
else:
|
|
1202
|
+
scheduled_job.delete()
|
|
1203
|
+
|
|
1177
1204
|
job_result = JobResult.enqueue_job(
|
|
1178
1205
|
job_model,
|
|
1179
1206
|
request.user,
|
nautobot/core/views/renderers.py
CHANGED
|
@@ -311,6 +311,7 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
|
|
|
311
311
|
valid_actions = self.validate_action_buttons(view, request)
|
|
312
312
|
# Query SavedViews for dropdown button
|
|
313
313
|
resolved_path = resolve(request.path)
|
|
314
|
+
# Note that `resolved_path.app_name` does work even for nested paths like `plugins:example_app:...`
|
|
314
315
|
list_url = f"{resolved_path.app_name}:{resolved_path.url_name}"
|
|
315
316
|
saved_views = None
|
|
316
317
|
if model.is_saved_view_model:
|
nautobot/core/views/utils.py
CHANGED
|
@@ -591,9 +591,9 @@ def get_bulk_queryset_from_view(
|
|
|
591
591
|
|
|
592
592
|
queryset = view_class.queryset.restrict(user, action)
|
|
593
593
|
|
|
594
|
-
# The filterset_class is determined from model on purpose
|
|
595
|
-
# with a job. It is better to be consistent
|
|
596
|
-
# always be available from to the confirmation page and to the job.
|
|
594
|
+
# The filterset_class is determined from model on purpose versus getting it from the view itself. This is
|
|
595
|
+
# because the filterset_class on the view as a param, will not work with a job. It is better to be consistent
|
|
596
|
+
# with each with sending the same params that will always be available from to the confirmation page and to the job.
|
|
597
597
|
filterset_class = get_filterset_for_model(model)
|
|
598
598
|
|
|
599
599
|
if not filterset_class:
|
|
@@ -5,12 +5,12 @@ from django.contrib import messages
|
|
|
5
5
|
from django.contrib.contenttypes.models import ContentType
|
|
6
6
|
from django.shortcuts import redirect, render
|
|
7
7
|
|
|
8
|
-
from nautobot.apps.ui import Breadcrumbs, Titles, ViewNameBreadcrumbItem
|
|
9
8
|
from nautobot.core.ui.choices import SectionChoices
|
|
10
9
|
from nautobot.core.ui.object_detail import (
|
|
11
10
|
ObjectDetailContent,
|
|
12
11
|
ObjectFieldsPanel,
|
|
13
12
|
)
|
|
13
|
+
from nautobot.core.ui.titles import Titles
|
|
14
14
|
from nautobot.core.views.generic import GenericView
|
|
15
15
|
from nautobot.core.views.mixins import (
|
|
16
16
|
ObjectBulkDestroyViewMixin,
|
|
@@ -174,13 +174,6 @@ class DataComplianceUIViewSet( # pylint: disable=W0223
|
|
|
174
174
|
class DeviceConstraintsView(GenericView):
|
|
175
175
|
template_name = "data_validation/device_constraints.html"
|
|
176
176
|
view_titles = Titles(titles={"*": "Device Constraints"})
|
|
177
|
-
breadcrumbs = Breadcrumbs(
|
|
178
|
-
items={
|
|
179
|
-
"*": [
|
|
180
|
-
ViewNameBreadcrumbItem(view_name="data_validation:device-constraints", label="Device Constraints"),
|
|
181
|
-
],
|
|
182
|
-
},
|
|
183
|
-
)
|
|
184
177
|
|
|
185
178
|
def get(self, request):
|
|
186
179
|
form = forms.DeviceConstraintsForm(user=request.user)
|
|
@@ -190,7 +183,6 @@ class DeviceConstraintsView(GenericView):
|
|
|
190
183
|
{
|
|
191
184
|
"form": form,
|
|
192
185
|
"view_titles": self.get_view_titles(),
|
|
193
|
-
"breadcrumbs": self.get_breadcrumbs(),
|
|
194
186
|
},
|
|
195
187
|
)
|
|
196
188
|
|
nautobot/dcim/forms.py
CHANGED
|
@@ -1491,7 +1491,7 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
|
|
|
1491
1491
|
|
|
1492
1492
|
|
|
1493
1493
|
class InterfaceTemplateCreateForm(ModularComponentTemplateCreateForm):
|
|
1494
|
-
type = forms.ChoiceField(choices=InterfaceTypeChoices, widget=StaticSelect2())
|
|
1494
|
+
type = forms.ChoiceField(choices=add_blank_choice(InterfaceTypeChoices), widget=StaticSelect2())
|
|
1495
1495
|
mgmt_only = forms.BooleanField(required=False, label="Management only")
|
|
1496
1496
|
speed = forms.IntegerField(
|
|
1497
1497
|
required=False, min_value=0, label="Speed (Kbps)", widget=NumberWithSelect(choices=InterfaceSpeedChoices)
|
|
@@ -1567,7 +1567,7 @@ class FrontPortTemplateForm(ModularComponentTemplateForm):
|
|
|
1567
1567
|
|
|
1568
1568
|
|
|
1569
1569
|
class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm):
|
|
1570
|
-
type = forms.ChoiceField(choices=PortTypeChoices, widget=StaticSelect2())
|
|
1570
|
+
type = forms.ChoiceField(choices=add_blank_choice(PortTypeChoices), widget=StaticSelect2())
|
|
1571
1571
|
rear_port_template_set = forms.MultipleChoiceField(
|
|
1572
1572
|
choices=[],
|
|
1573
1573
|
label="Rear ports",
|
|
@@ -1677,7 +1677,7 @@ class RearPortTemplateForm(ModularComponentTemplateForm):
|
|
|
1677
1677
|
|
|
1678
1678
|
class RearPortTemplateCreateForm(ModularComponentTemplateCreateForm):
|
|
1679
1679
|
type = forms.ChoiceField(
|
|
1680
|
-
choices=PortTypeChoices,
|
|
1680
|
+
choices=add_blank_choice(PortTypeChoices),
|
|
1681
1681
|
widget=StaticSelect2(),
|
|
1682
1682
|
)
|
|
1683
1683
|
positions = forms.IntegerField(
|
|
@@ -3306,7 +3306,7 @@ class InterfaceForm(InterfaceCommonForm, ModularComponentEditForm):
|
|
|
3306
3306
|
class InterfaceCreateForm(ModularComponentCreateForm, InterfaceCommonForm, RoleNotRequiredModelFormMixin):
|
|
3307
3307
|
model = Interface
|
|
3308
3308
|
type = forms.ChoiceField(
|
|
3309
|
-
choices=InterfaceTypeChoices,
|
|
3309
|
+
choices=add_blank_choice(InterfaceTypeChoices),
|
|
3310
3310
|
widget=StaticSelect2(),
|
|
3311
3311
|
)
|
|
3312
3312
|
status = DynamicModelChoiceField(
|
|
@@ -3435,7 +3435,7 @@ class InterfaceBulkCreateForm(
|
|
|
3435
3435
|
):
|
|
3436
3436
|
model = Interface
|
|
3437
3437
|
type = forms.ChoiceField(
|
|
3438
|
-
choices=InterfaceTypeChoices,
|
|
3438
|
+
choices=add_blank_choice(InterfaceTypeChoices),
|
|
3439
3439
|
widget=StaticSelect2(),
|
|
3440
3440
|
)
|
|
3441
3441
|
status = DynamicModelChoiceField(
|
|
@@ -3473,7 +3473,7 @@ class ModuleInterfaceBulkCreateForm(
|
|
|
3473
3473
|
):
|
|
3474
3474
|
model = Interface
|
|
3475
3475
|
type = forms.ChoiceField(
|
|
3476
|
-
choices=InterfaceTypeChoices,
|
|
3476
|
+
choices=add_blank_choice(InterfaceTypeChoices),
|
|
3477
3477
|
widget=StaticSelect2(),
|
|
3478
3478
|
)
|
|
3479
3479
|
status = DynamicModelChoiceField(
|
|
@@ -3678,7 +3678,7 @@ class FrontPortForm(ModularComponentEditForm):
|
|
|
3678
3678
|
# TODO: Merge with FrontPortTemplateCreateForm to remove duplicate logic
|
|
3679
3679
|
class FrontPortCreateForm(ModularComponentCreateForm):
|
|
3680
3680
|
type = forms.ChoiceField(
|
|
3681
|
-
choices=PortTypeChoices,
|
|
3681
|
+
choices=add_blank_choice(PortTypeChoices),
|
|
3682
3682
|
widget=StaticSelect2(),
|
|
3683
3683
|
)
|
|
3684
3684
|
rear_port_set = forms.MultipleChoiceField(
|
|
@@ -3806,7 +3806,7 @@ class RearPortForm(ModularComponentEditForm):
|
|
|
3806
3806
|
|
|
3807
3807
|
class RearPortCreateForm(ModularComponentCreateForm):
|
|
3808
3808
|
type = forms.ChoiceField(
|
|
3809
|
-
choices=PortTypeChoices,
|
|
3809
|
+
choices=add_blank_choice(PortTypeChoices),
|
|
3810
3810
|
widget=StaticSelect2(),
|
|
3811
3811
|
)
|
|
3812
3812
|
positions = forms.IntegerField(
|
|
@@ -5134,7 +5134,7 @@ class InterfaceRedundancyGroupBulkEditForm(
|
|
|
5134
5134
|
queryset=InterfaceRedundancyGroup.objects.all(),
|
|
5135
5135
|
widget=forms.MultipleHiddenInput,
|
|
5136
5136
|
)
|
|
5137
|
-
protocol = forms.ChoiceField(choices=InterfaceRedundancyGroupProtocolChoices, required=False)
|
|
5137
|
+
protocol = forms.ChoiceField(choices=add_blank_choice(InterfaceRedundancyGroupProtocolChoices), required=False)
|
|
5138
5138
|
description = forms.CharField(required=False)
|
|
5139
5139
|
virtual_ip = DynamicModelChoiceField(queryset=IPAddress.objects.all(), required=False)
|
|
5140
5140
|
secrets_group = DynamicModelChoiceField(queryset=SecretsGroup.objects.all(), required=False)
|
nautobot/dcim/models/devices.py
CHANGED
|
@@ -1962,11 +1962,11 @@ class Module(PrimaryModel):
|
|
|
1962
1962
|
@property
|
|
1963
1963
|
def page_title(self):
|
|
1964
1964
|
if self.location:
|
|
1965
|
-
return f"{self.
|
|
1965
|
+
return f"{self.location} {self.module_type!s}"
|
|
1966
1966
|
if self.parent_module_bay.parent_device is not None:
|
|
1967
|
-
return f"{self.
|
|
1967
|
+
return f"{self.parent_module_bay.parent_device.display} {self.module_type!s}"
|
|
1968
1968
|
|
|
1969
|
-
return f"{self.module_type!s}
|
|
1969
|
+
return f"{self.parent_module_bay.parent_module.module_type!s} {self.module_type!s}"
|
|
1970
1970
|
|
|
1971
1971
|
@property
|
|
1972
1972
|
def device(self):
|
nautobot/dcim/tables/power.py
CHANGED
|
@@ -2,6 +2,7 @@ import django_tables2 as tables
|
|
|
2
2
|
|
|
3
3
|
from nautobot.core.tables import (
|
|
4
4
|
BaseTable,
|
|
5
|
+
ButtonsColumn,
|
|
5
6
|
ChoiceFieldColumn,
|
|
6
7
|
LinkedCountColumn,
|
|
7
8
|
TagColumn,
|
|
@@ -36,6 +37,7 @@ class PowerPanelTable(BaseTable):
|
|
|
36
37
|
verbose_name="Feeds",
|
|
37
38
|
)
|
|
38
39
|
tags = TagColumn(url_name="dcim:powerpanel_list")
|
|
40
|
+
actions = ButtonsColumn(PowerPanel)
|
|
39
41
|
|
|
40
42
|
class Meta(BaseTable.Meta):
|
|
41
43
|
model = PowerPanel
|
|
@@ -81,6 +83,7 @@ class PowerFeedTable(StatusTableMixin, CableTerminationTable):
|
|
|
81
83
|
max_utilization = tables.TemplateColumn(template_code="{{ value }}%")
|
|
82
84
|
available_power = tables.Column(verbose_name="Available power (VA)")
|
|
83
85
|
tags = TagColumn(url_name="dcim:powerfeed_list")
|
|
86
|
+
actions = ButtonsColumn(PowerFeed)
|
|
84
87
|
|
|
85
88
|
class Meta(BaseTable.Meta):
|
|
86
89
|
model = PowerFeed
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
</div>
|
|
21
21
|
<div class="card">
|
|
22
22
|
<div class="card-header"><strong>Wireless Network Assignment</strong></div>
|
|
23
|
-
<div class="card-body">
|
|
23
|
+
<div class="card-body overflow-auto">
|
|
24
24
|
{% if wireless_networks.errors %}
|
|
25
25
|
<div class="text-danger">
|
|
26
26
|
Please correct the error(s) below:
|
nautobot/dcim/views.py
CHANGED
|
@@ -3093,7 +3093,8 @@ class DeviceUIViewSet(NautobotUIViewSet):
|
|
|
3093
3093
|
table_title="Wireless Networks",
|
|
3094
3094
|
table_class=BaseControllerManagedDeviceGroupWirelessNetworkAssignmentTable,
|
|
3095
3095
|
table_attribute="wireless_network_assignments",
|
|
3096
|
-
related_field_name="
|
|
3096
|
+
related_field_name="controller_managed_device_groups__devices",
|
|
3097
|
+
related_list_url_name="wireless:wirelessnetwork_list",
|
|
3097
3098
|
tab_id="wireless",
|
|
3098
3099
|
include_paginator=True,
|
|
3099
3100
|
exclude_columns=["controller_managed_device_group", "controller"],
|
|
@@ -3104,7 +3105,8 @@ class DeviceUIViewSet(NautobotUIViewSet):
|
|
|
3104
3105
|
table_title="Radio Profiles",
|
|
3105
3106
|
table_class=ControllerManagedDeviceGroupRadioProfileAssignmentTable,
|
|
3106
3107
|
table_attribute="radio_profile_assignments",
|
|
3107
|
-
related_field_name="
|
|
3108
|
+
related_field_name="controller_managed_device_groups__devices",
|
|
3109
|
+
related_list_url_name="wireless:radioprofile_list",
|
|
3108
3110
|
tab_id="wireless",
|
|
3109
3111
|
include_paginator=True,
|
|
3110
3112
|
exclude_columns=["controller_managed_device_group"],
|
|
@@ -3508,6 +3510,30 @@ class ModuleUIViewSet(BulkComponentCreateUIViewSetMixin, NautobotUIViewSet):
|
|
|
3508
3510
|
serializer_class = serializers.ModuleSerializer
|
|
3509
3511
|
table_class = tables.ModuleTable
|
|
3510
3512
|
component_model = None
|
|
3513
|
+
breadcrumbs = Breadcrumbs(
|
|
3514
|
+
items={
|
|
3515
|
+
"detail": [
|
|
3516
|
+
ModelBreadcrumbItem(),
|
|
3517
|
+
InstanceBreadcrumbItem(
|
|
3518
|
+
instance=context_object_attr("parent_module_bay.parent_device"),
|
|
3519
|
+
should_render=lambda c: c["object"].parent_module_bay is not None
|
|
3520
|
+
and c["object"].parent_module_bay.parent_device is not None,
|
|
3521
|
+
),
|
|
3522
|
+
InstanceBreadcrumbItem(
|
|
3523
|
+
instance=context_object_attr("parent_module_bay.parent_module"),
|
|
3524
|
+
should_render=lambda c: c["object"].parent_module_bay is not None
|
|
3525
|
+
and c["object"].parent_module_bay.parent_module is not None,
|
|
3526
|
+
),
|
|
3527
|
+
InstanceBreadcrumbItem(instance=context_object_attr("parent_module_bay")),
|
|
3528
|
+
AncestorsInstanceBreadcrumbItem(
|
|
3529
|
+
instance=context_object_attr("location"),
|
|
3530
|
+
ancestor_item=lambda ancestor: InstanceParentBreadcrumbItem(parent_key="location", parent=ancestor),
|
|
3531
|
+
include_self=True,
|
|
3532
|
+
should_render=lambda c: c["object"].location is not None,
|
|
3533
|
+
),
|
|
3534
|
+
]
|
|
3535
|
+
}
|
|
3536
|
+
)
|
|
3511
3537
|
|
|
3512
3538
|
def get_extra_context(self, request, instance):
|
|
3513
3539
|
context = super().get_extra_context(request, instance)
|
|
@@ -4592,24 +4618,12 @@ class ModuleBayUIViewSet(ModuleBayCommonViewSetMixin, NautobotUIViewSet):
|
|
|
4592
4618
|
instance=context_object_attr("parent_device"),
|
|
4593
4619
|
should_render=context_object_attr("parent_device"),
|
|
4594
4620
|
),
|
|
4595
|
-
ViewNameBreadcrumbItem(
|
|
4596
|
-
view_name_key="device_breadcrumb_url",
|
|
4597
|
-
should_render=lambda c: c["object"].parent_device and c.get("device_breadcrumb_url"),
|
|
4598
|
-
reverse_kwargs=lambda c: {"pk": c["object"].parent_device.pk},
|
|
4599
|
-
label=lambda c: c["object"]._meta.verbose_name_plural,
|
|
4600
|
-
),
|
|
4601
4621
|
# Breadcrumb path if ModuleBay is linked with module
|
|
4602
4622
|
ModelBreadcrumbItem(model=Module, should_render=lambda c: c["object"].parent_device is None),
|
|
4603
4623
|
InstanceBreadcrumbItem(
|
|
4604
4624
|
instance=context_object_attr("parent_module"),
|
|
4605
4625
|
should_render=lambda c: c["object"].parent_device is None,
|
|
4606
4626
|
),
|
|
4607
|
-
ViewNameBreadcrumbItem(
|
|
4608
|
-
view_name_key="module_breadcrumb_url",
|
|
4609
|
-
should_render=lambda c: c["object"].parent_device is None and c.get("module_breadcrumb_url"),
|
|
4610
|
-
reverse_kwargs=lambda c: {"pk": c["object"].parent_module.pk},
|
|
4611
|
-
label=lambda c: c["object"]._meta.verbose_name_plural,
|
|
4612
|
-
),
|
|
4613
4627
|
]
|
|
4614
4628
|
}
|
|
4615
4629
|
)
|
|
@@ -4999,15 +5013,6 @@ class ConsoleConnectionsListView(ConnectionsListView):
|
|
|
4999
5013
|
template_name = "dcim/console_port_connection_list.html"
|
|
5000
5014
|
action_buttons = ("export",)
|
|
5001
5015
|
view_titles = Titles(titles={"list": "Console Connections"})
|
|
5002
|
-
breadcrumbs = Breadcrumbs(
|
|
5003
|
-
items={"list": [ViewNameBreadcrumbItem(view_name="dcim:console_connections_list", label="Console Connections")]}
|
|
5004
|
-
)
|
|
5005
|
-
|
|
5006
|
-
def extra_context(self):
|
|
5007
|
-
return {
|
|
5008
|
-
"title": "Console Connections",
|
|
5009
|
-
"list_url": "dcim:console_connections_list",
|
|
5010
|
-
}
|
|
5011
5016
|
|
|
5012
5017
|
|
|
5013
5018
|
class PowerConnectionsListView(ConnectionsListView):
|
|
@@ -5018,15 +5023,6 @@ class PowerConnectionsListView(ConnectionsListView):
|
|
|
5018
5023
|
template_name = "dcim/power_port_connection_list.html"
|
|
5019
5024
|
action_buttons = ("export",)
|
|
5020
5025
|
view_titles = Titles(titles={"list": "Power Connections"})
|
|
5021
|
-
breadcrumbs = Breadcrumbs(
|
|
5022
|
-
items={"list": [ViewNameBreadcrumbItem(view_name="dcim:power_connections_list", label="Power Connections")]}
|
|
5023
|
-
)
|
|
5024
|
-
|
|
5025
|
-
def extra_context(self):
|
|
5026
|
-
return {
|
|
5027
|
-
"title": "Power Connections",
|
|
5028
|
-
"list_url": "dcim:power_connections_list",
|
|
5029
|
-
}
|
|
5030
5026
|
|
|
5031
5027
|
|
|
5032
5028
|
class InterfaceConnectionsListView(ConnectionsListView):
|
|
@@ -5037,11 +5033,6 @@ class InterfaceConnectionsListView(ConnectionsListView):
|
|
|
5037
5033
|
template_name = "dcim/interface_connection_list.html"
|
|
5038
5034
|
action_buttons = ("export",)
|
|
5039
5035
|
view_titles = Titles(titles={"list": "Interface Connections"})
|
|
5040
|
-
breadcrumbs = Breadcrumbs(
|
|
5041
|
-
items={
|
|
5042
|
-
"list": [ViewNameBreadcrumbItem(view_name="dcim:interface_connections_list", label="Interface Connections")]
|
|
5043
|
-
}
|
|
5044
|
-
)
|
|
5045
5036
|
|
|
5046
5037
|
def __init__(self, *args, **kwargs):
|
|
5047
5038
|
super().__init__(*args, **kwargs)
|
|
@@ -5067,12 +5058,6 @@ class InterfaceConnectionsListView(ConnectionsListView):
|
|
|
5067
5058
|
|
|
5068
5059
|
return self.queryset
|
|
5069
5060
|
|
|
5070
|
-
def extra_context(self):
|
|
5071
|
-
return {
|
|
5072
|
-
"title": "Interface Connections",
|
|
5073
|
-
"list_url": "dcim:interface_connections_list",
|
|
5074
|
-
}
|
|
5075
|
-
|
|
5076
5061
|
|
|
5077
5062
|
#
|
|
5078
5063
|
# Virtual chassis
|
|
@@ -5967,12 +5952,13 @@ class ControllerUIViewSet(NautobotUIViewSet):
|
|
|
5967
5952
|
table_title="Wireless Networks",
|
|
5968
5953
|
table_class=BaseControllerManagedDeviceGroupWirelessNetworkAssignmentTable,
|
|
5969
5954
|
table_filter="controller_managed_device_group__controller",
|
|
5955
|
+
related_field_name="controller_managed_device_groups__controller",
|
|
5956
|
+
related_list_url_name="wireless:wirelessnetwork_list",
|
|
5970
5957
|
tab_id="wireless_networks",
|
|
5971
5958
|
add_button_route=None,
|
|
5972
5959
|
select_related_fields=["wireless_network"],
|
|
5973
5960
|
exclude_columns=["controller"],
|
|
5974
5961
|
include_paginator=True,
|
|
5975
|
-
enable_related_link=False,
|
|
5976
5962
|
),
|
|
5977
5963
|
),
|
|
5978
5964
|
),
|
nautobot/extras/api/views.py
CHANGED
|
@@ -311,7 +311,7 @@ class ApprovalWorkflowStageViewSet(NautobotModelViewSet):
|
|
|
311
311
|
def _is_user_approver(self, user, stage):
|
|
312
312
|
"""Checks if the user belongs to the group allowed to approve the current stage."""
|
|
313
313
|
approver_group = stage.approval_workflow_stage_definition.approver_group
|
|
314
|
-
return
|
|
314
|
+
return approver_group.user_set.filter(id=user.id).exists()
|
|
315
315
|
|
|
316
316
|
def _user_already_approved_or_denied(self, user, stage, action_type):
|
|
317
317
|
"""Checks if the user has already approved/denied to the current stage."""
|
|
@@ -425,6 +425,12 @@ class ApprovalWorkflowStageViewSet(NautobotModelViewSet):
|
|
|
425
425
|
"""Add a comment to the specific stage (without approving or denying)."""
|
|
426
426
|
stage = self.get_object()
|
|
427
427
|
|
|
428
|
+
if not stage.is_not_done_stage:
|
|
429
|
+
return Response(
|
|
430
|
+
{"detail": f"This stage is in {stage.state} state. Can't comment approved or denied stage."},
|
|
431
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
432
|
+
)
|
|
433
|
+
|
|
428
434
|
comment = request.data.get("comments", "")
|
|
429
435
|
if not comment:
|
|
430
436
|
return Response(
|
|
@@ -432,9 +438,14 @@ class ApprovalWorkflowStageViewSet(NautobotModelViewSet):
|
|
|
432
438
|
status=status.HTTP_400_BAD_REQUEST,
|
|
433
439
|
)
|
|
434
440
|
|
|
435
|
-
ApprovalWorkflowStageResponse.objects.
|
|
436
|
-
approval_workflow_stage=stage, user=request.user
|
|
441
|
+
approval_workflow_stage_response, _ = ApprovalWorkflowStageResponse.objects.get_or_create(
|
|
442
|
+
approval_workflow_stage=stage, user=request.user
|
|
437
443
|
)
|
|
444
|
+
# we don't want to change a state if is approved, denied or canceled
|
|
445
|
+
if approval_workflow_stage_response.state == ApprovalWorkflowStateChoices.PENDING:
|
|
446
|
+
approval_workflow_stage_response.state = ApprovalWorkflowStateChoices.COMMENT
|
|
447
|
+
approval_workflow_stage_response.comments = comment
|
|
448
|
+
approval_workflow_stage_response.save()
|
|
438
449
|
|
|
439
450
|
serializer = serializers.ApprovalWorkflowStageSerializer(stage, context={"request": request})
|
|
440
451
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
nautobot/extras/choices.py
CHANGED
|
@@ -19,18 +19,21 @@ class ApprovalWorkflowStateChoices(ChoiceSet):
|
|
|
19
19
|
APPROVED = "Approved"
|
|
20
20
|
DENIED = "Denied"
|
|
21
21
|
CANCELED = "Canceled"
|
|
22
|
+
COMMENT = "Comment"
|
|
22
23
|
|
|
23
24
|
CHOICES = (
|
|
24
25
|
(PENDING, "Pending"),
|
|
25
26
|
(APPROVED, "Approved"),
|
|
26
27
|
(DENIED, "Denied"),
|
|
27
28
|
(CANCELED, "Canceled"),
|
|
29
|
+
(COMMENT, "Comment"),
|
|
28
30
|
)
|
|
29
31
|
CSS_CLASSES = {
|
|
30
32
|
PENDING: "info",
|
|
31
33
|
APPROVED: "success",
|
|
32
34
|
DENIED: "danger",
|
|
33
35
|
CANCELED: "warning",
|
|
36
|
+
COMMENT: "info",
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
|
nautobot/extras/jobs.py
CHANGED
|
@@ -29,6 +29,7 @@ from django.db.models.query import QuerySet
|
|
|
29
29
|
from django.forms import ValidationError
|
|
30
30
|
from django.utils.functional import classproperty
|
|
31
31
|
import netaddr
|
|
32
|
+
from prometheus_client import Counter
|
|
32
33
|
import yaml
|
|
33
34
|
|
|
34
35
|
from nautobot.core.celery import import_jobs, nautobot_task
|
|
@@ -89,6 +90,27 @@ __all__ = [
|
|
|
89
90
|
|
|
90
91
|
logger = logging.getLogger(__name__)
|
|
91
92
|
|
|
93
|
+
started_jobs_counter = Counter(
|
|
94
|
+
name="nautobot_worker_started_jobs",
|
|
95
|
+
documentation="Job executions that started running",
|
|
96
|
+
labelnames=("job_class_name", "module_name"),
|
|
97
|
+
)
|
|
98
|
+
finished_jobs_counter = Counter(
|
|
99
|
+
name="nautobot_worker_finished_jobs",
|
|
100
|
+
documentation="Job executions that finished running",
|
|
101
|
+
labelnames=("job_class_name", "module_name", "status"),
|
|
102
|
+
)
|
|
103
|
+
exception_jobs_counter = Counter(
|
|
104
|
+
name="nautobot_worker_exception_jobs",
|
|
105
|
+
documentation="Job executions that raised an exception",
|
|
106
|
+
labelnames=("job_class_name", "module_name", "exception_type"),
|
|
107
|
+
)
|
|
108
|
+
singleton_conflict_counter = Counter(
|
|
109
|
+
name="nautobot_worker_singleton_conflict",
|
|
110
|
+
documentation="Job executions that ran into a singleton lock",
|
|
111
|
+
labelnames=("job_class_name", "module_name"),
|
|
112
|
+
)
|
|
113
|
+
|
|
92
114
|
|
|
93
115
|
class RunJobTaskFailed(Exception):
|
|
94
116
|
"""Celery task failed for some reason."""
|
|
@@ -1194,6 +1216,9 @@ def _prepare_job(job_class_path, request, kwargs) -> tuple[Job, dict]:
|
|
|
1194
1216
|
extra={"object": job.job_model, "grouping": "initialization"},
|
|
1195
1217
|
)
|
|
1196
1218
|
else:
|
|
1219
|
+
singleton_conflict_counter.labels(
|
|
1220
|
+
job_class_name=job.job_model.job_class_name, module_name=job.job_model.module_name
|
|
1221
|
+
).inc()
|
|
1197
1222
|
# TODO 3.0: maybe change to logger.failure() and return cleanly, as this is an "acceptable" failure?
|
|
1198
1223
|
job.logger.error(
|
|
1199
1224
|
"Job %s is a singleton and already running.",
|
|
@@ -1277,6 +1302,9 @@ def run_job(self, job_class_path, *args, **kwargs):
|
|
|
1277
1302
|
|
|
1278
1303
|
result = None
|
|
1279
1304
|
status = None
|
|
1305
|
+
started_jobs_counter.labels(
|
|
1306
|
+
job_class_name=job.job_model.job_class_name, module_name=job.job_model.module_name
|
|
1307
|
+
).inc()
|
|
1280
1308
|
try:
|
|
1281
1309
|
before_start_result = job.before_start(self.request.id, args, kwargs)
|
|
1282
1310
|
if not job._failed:
|
|
@@ -1293,6 +1321,9 @@ def run_job(self, job_class_path, *args, **kwargs):
|
|
|
1293
1321
|
job.on_success(result, self.request.id, args, kwargs)
|
|
1294
1322
|
else:
|
|
1295
1323
|
job.on_failure(result, self.request.id, args, kwargs, None)
|
|
1324
|
+
finished_jobs_counter.labels(
|
|
1325
|
+
job_class_name=job.job_model.job_class_name, module_name=job.job_model.module_name, status=status
|
|
1326
|
+
).inc()
|
|
1296
1327
|
|
|
1297
1328
|
job.after_return(status, result, self.request.id, args, kwargs, None)
|
|
1298
1329
|
|
|
@@ -1309,11 +1340,21 @@ def run_job(self, job_class_path, *args, **kwargs):
|
|
|
1309
1340
|
# We don't want to overwrite the manual state update that we did above, so:
|
|
1310
1341
|
raise Ignore()
|
|
1311
1342
|
|
|
1312
|
-
except Reject:
|
|
1343
|
+
except Reject as exc:
|
|
1344
|
+
exception_jobs_counter.labels(
|
|
1345
|
+
job_class_name=job.job_model.job_class_name,
|
|
1346
|
+
module_name=job.job_model.module_name,
|
|
1347
|
+
exception_type=type(exc).__name__,
|
|
1348
|
+
).inc()
|
|
1313
1349
|
status = status or JobResultStatusChoices.STATUS_REJECTED
|
|
1314
1350
|
raise
|
|
1315
1351
|
|
|
1316
|
-
except Ignore:
|
|
1352
|
+
except Ignore as exc:
|
|
1353
|
+
exception_jobs_counter.labels(
|
|
1354
|
+
job_class_name=job.job_model.job_class_name,
|
|
1355
|
+
module_name=job.job_model.module_name,
|
|
1356
|
+
exception_type=type(exc).__name__,
|
|
1357
|
+
).inc()
|
|
1317
1358
|
status = status or JobResultStatusChoices.STATUS_IGNORED
|
|
1318
1359
|
raise
|
|
1319
1360
|
|
|
@@ -1326,6 +1367,11 @@ def run_job(self, job_class_path, *args, **kwargs):
|
|
|
1326
1367
|
"exc_type": type(exc).__name__,
|
|
1327
1368
|
"exc_message": sanitize(str(exc)),
|
|
1328
1369
|
}
|
|
1370
|
+
exception_jobs_counter.labels(
|
|
1371
|
+
job_class_name=job.job_model.job_class_name,
|
|
1372
|
+
module_name=job.job_model.module_name,
|
|
1373
|
+
exception_type=type(exc).__name__,
|
|
1374
|
+
).inc()
|
|
1329
1375
|
raise
|
|
1330
1376
|
|
|
1331
1377
|
finally:
|