nautobot 2.3.15__py3-none-any.whl → 2.3.16__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/circuits/views.py +3 -3
- nautobot/cloud/models.py +1 -1
- nautobot/core/api/fields.py +5 -5
- nautobot/core/api/serializers.py +9 -9
- nautobot/core/api/views.py +3 -2
- nautobot/core/apps/__init__.py +5 -2
- nautobot/core/celery/schedulers.py +1 -1
- nautobot/core/filters.py +19 -16
- nautobot/core/forms/fields.py +5 -5
- nautobot/core/graphql/types.py +1 -1
- nautobot/core/jobs/__init__.py +4 -4
- nautobot/core/jobs/cleanup.py +1 -1
- nautobot/core/jobs/groups.py +1 -1
- nautobot/core/management/commands/validate_models.py +1 -1
- nautobot/core/models/__init__.py +1 -1
- nautobot/core/models/query_functions.py +2 -2
- nautobot/core/models/tree_queries.py +2 -2
- nautobot/core/tables.py +5 -5
- nautobot/core/testing/filters.py +7 -3
- nautobot/core/testing/views.py +5 -0
- nautobot/core/tests/runner.py +1 -1
- nautobot/core/views/generic.py +51 -43
- nautobot/core/views/mixins.py +21 -11
- nautobot/dcim/api/serializers.py +48 -48
- nautobot/dcim/forms.py +2 -0
- nautobot/dcim/graphql/types.py +2 -2
- nautobot/dcim/models/device_component_templates.py +2 -2
- nautobot/dcim/models/device_components.py +22 -20
- nautobot/dcim/models/devices.py +1 -1
- nautobot/dcim/models/locations.py +3 -3
- nautobot/dcim/models/power.py +6 -5
- nautobot/dcim/models/racks.py +4 -4
- nautobot/dcim/tables/__init__.py +3 -3
- nautobot/dcim/tables/devicetypes.py +2 -2
- nautobot/dcim/tests/test_filters.py +1 -0
- nautobot/dcim/tests/test_graphql.py +52 -0
- nautobot/dcim/tests/test_models.py +4 -1
- nautobot/dcim/views.py +1 -1
- nautobot/extras/api/customfields.py +2 -2
- nautobot/extras/api/serializers.py +72 -69
- nautobot/extras/api/views.py +4 -4
- nautobot/extras/health_checks.py +1 -2
- nautobot/extras/jobs.py +5 -5
- nautobot/extras/managers.py +3 -1
- nautobot/extras/migrations/0018_joblog_data_migration.py +7 -9
- nautobot/extras/models/groups.py +13 -9
- nautobot/extras/models/jobs.py +4 -4
- nautobot/extras/models/models.py +2 -2
- nautobot/extras/plugins/views.py +1 -1
- nautobot/extras/tables.py +5 -5
- nautobot/extras/test_jobs/api_test_job.py +1 -1
- nautobot/extras/test_jobs/atomic_transaction.py +2 -2
- nautobot/extras/test_jobs/dry_run.py +1 -1
- nautobot/extras/test_jobs/fail.py +5 -5
- nautobot/extras/test_jobs/file_output.py +1 -1
- nautobot/extras/test_jobs/file_upload_fail.py +1 -1
- nautobot/extras/test_jobs/file_upload_pass.py +1 -1
- nautobot/extras/test_jobs/ipaddress_vars.py +3 -1
- nautobot/extras/test_jobs/jobs_module/jobs_submodule/jobs.py +1 -1
- nautobot/extras/test_jobs/location_with_custom_field.py +1 -1
- nautobot/extras/test_jobs/log_redaction.py +1 -1
- nautobot/extras/test_jobs/log_skip_db_logging.py +1 -1
- nautobot/extras/test_jobs/modify_db.py +1 -1
- nautobot/extras/test_jobs/object_var_optional.py +1 -1
- nautobot/extras/test_jobs/object_var_required.py +1 -1
- nautobot/extras/test_jobs/object_vars.py +1 -1
- nautobot/extras/test_jobs/pass.py +3 -3
- nautobot/extras/test_jobs/profiling.py +1 -1
- nautobot/extras/test_jobs/relative_import.py +3 -3
- nautobot/extras/test_jobs/soft_time_limit_greater_than_time_limit.py +1 -1
- nautobot/extras/test_jobs/task_queues.py +1 -1
- nautobot/extras/tests/test_api.py +13 -13
- nautobot/extras/tests/test_customfields.py +1 -1
- nautobot/extras/tests/test_datasources.py +2 -1
- nautobot/extras/tests/test_dynamicgroups.py +1 -1
- nautobot/extras/tests/test_filters.py +6 -6
- nautobot/extras/tests/test_jobs.py +11 -11
- nautobot/extras/tests/test_models.py +10 -10
- nautobot/extras/tests/test_relationships.py +1 -1
- nautobot/extras/tests/test_views.py +16 -16
- nautobot/extras/views.py +20 -16
- nautobot/ipam/api/fields.py +3 -3
- nautobot/ipam/api/serializers.py +33 -33
- nautobot/ipam/api/views.py +37 -61
- nautobot/ipam/querysets.py +2 -2
- nautobot/ipam/tests/test_api.py +12 -1
- nautobot/ipam/tests/test_forms.py +51 -47
- nautobot/ipam/tests/test_migrations.py +30 -30
- nautobot/ipam/tests/test_querysets.py +14 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +2 -2
- nautobot/project-static/docs/release-notes/version-2.3.html +181 -99
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +270 -270
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/users/admin.py +1 -1
- nautobot/users/api/serializers.py +4 -4
- nautobot/users/api/views.py +1 -1
- nautobot/virtualization/api/serializers.py +4 -4
- {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/METADATA +1 -1
- {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/RECORD +106 -106
- {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/WHEEL +1 -1
- {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/NOTICE +0 -0
- {nautobot-2.3.15.dist-info → nautobot-2.3.16.dist-info}/entry_points.txt +0 -0
|
@@ -212,13 +212,13 @@ class ContactSerializer(NautobotModelSerializer):
|
|
|
212
212
|
"phone": {"default": ""},
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
def validate(self,
|
|
216
|
-
|
|
217
|
-
|
|
215
|
+
def validate(self, attrs):
|
|
216
|
+
local_attrs = attrs.copy()
|
|
217
|
+
local_attrs.pop("teams", None)
|
|
218
218
|
validator = UniqueTogetherValidator(queryset=Contact.objects.all(), fields=("name", "phone", "email"))
|
|
219
|
-
validator(
|
|
220
|
-
super().validate(
|
|
221
|
-
return
|
|
219
|
+
validator(local_attrs, self)
|
|
220
|
+
super().validate(local_attrs)
|
|
221
|
+
return attrs
|
|
222
222
|
|
|
223
223
|
|
|
224
224
|
class ContactAssociationSerializer(NautobotModelSerializer):
|
|
@@ -233,18 +233,18 @@ class ContactAssociationSerializer(NautobotModelSerializer):
|
|
|
233
233
|
"team": {"required": False},
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
def validate(self,
|
|
236
|
+
def validate(self, attrs):
|
|
237
237
|
# Validate uniqueness of (associated object, associated object type, contact/team, role)
|
|
238
238
|
unique_together_fields = None
|
|
239
239
|
|
|
240
|
-
if
|
|
240
|
+
if attrs.get("contact") and attrs.get("role"):
|
|
241
241
|
unique_together_fields = (
|
|
242
242
|
"associated_object_type",
|
|
243
243
|
"associated_object_id",
|
|
244
244
|
"contact",
|
|
245
245
|
"role",
|
|
246
246
|
)
|
|
247
|
-
elif
|
|
247
|
+
elif attrs.get("team") and attrs.get("role"):
|
|
248
248
|
unique_together_fields = (
|
|
249
249
|
"associated_object_type",
|
|
250
250
|
"associated_object_id",
|
|
@@ -257,11 +257,11 @@ class ContactAssociationSerializer(NautobotModelSerializer):
|
|
|
257
257
|
queryset=ContactAssociation.objects.all(),
|
|
258
258
|
fields=unique_together_fields,
|
|
259
259
|
)
|
|
260
|
-
validator(
|
|
260
|
+
validator(attrs, self)
|
|
261
261
|
|
|
262
|
-
super().validate(
|
|
262
|
+
super().validate(attrs)
|
|
263
263
|
|
|
264
|
-
return
|
|
264
|
+
return attrs
|
|
265
265
|
|
|
266
266
|
|
|
267
267
|
#
|
|
@@ -279,8 +279,8 @@ class ContentTypeSerializer(BaseModelSerializer):
|
|
|
279
279
|
fields = "__all__"
|
|
280
280
|
|
|
281
281
|
@extend_schema_field(serializers.CharField)
|
|
282
|
-
def get_display(self,
|
|
283
|
-
return
|
|
282
|
+
def get_display(self, instance):
|
|
283
|
+
return instance.app_labeled_name
|
|
284
284
|
|
|
285
285
|
|
|
286
286
|
#
|
|
@@ -501,17 +501,17 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
|
|
501
501
|
model = ImageAttachment
|
|
502
502
|
fields = "__all__"
|
|
503
503
|
|
|
504
|
-
def validate(self,
|
|
504
|
+
def validate(self, attrs):
|
|
505
505
|
# Validate that the parent object exists
|
|
506
506
|
try:
|
|
507
|
-
|
|
507
|
+
attrs["content_type"].get_object_for_this_type(id=attrs["object_id"])
|
|
508
508
|
except ObjectDoesNotExist:
|
|
509
|
-
raise serializers.ValidationError(f"Invalid parent object: {
|
|
509
|
+
raise serializers.ValidationError(f"Invalid parent object: {attrs['content_type']} ID {attrs['object_id']}")
|
|
510
510
|
|
|
511
511
|
# Enforce model validation
|
|
512
|
-
super().validate(
|
|
512
|
+
super().validate(attrs)
|
|
513
513
|
|
|
514
|
-
return
|
|
514
|
+
return attrs
|
|
515
515
|
|
|
516
516
|
@extend_schema_field(
|
|
517
517
|
PolymorphicProxySerializer(
|
|
@@ -539,24 +539,24 @@ class JobSerializer(NautobotModelSerializer, TaggedModelSerializerMixin):
|
|
|
539
539
|
model = Job
|
|
540
540
|
fields = "__all__"
|
|
541
541
|
|
|
542
|
-
def validate(self,
|
|
542
|
+
def validate(self, attrs):
|
|
543
543
|
# note no validation for on creation of jobs because we do not support user creation of Job records via API
|
|
544
544
|
if self.instance:
|
|
545
|
-
has_sensitive_variables =
|
|
546
|
-
approval_required =
|
|
545
|
+
has_sensitive_variables = attrs.get("has_sensitive_variables", self.instance.has_sensitive_variables)
|
|
546
|
+
approval_required = attrs.get("approval_required", self.instance.approval_required)
|
|
547
547
|
|
|
548
548
|
if approval_required and has_sensitive_variables:
|
|
549
549
|
error_message = "A job with sensitive variables cannot also be marked as requiring approval"
|
|
550
550
|
errors = {}
|
|
551
551
|
|
|
552
|
-
if "approval_required" in
|
|
552
|
+
if "approval_required" in attrs:
|
|
553
553
|
errors["approval_required"] = [error_message]
|
|
554
|
-
if "has_sensitive_variables" in
|
|
554
|
+
if "has_sensitive_variables" in attrs:
|
|
555
555
|
errors["has_sensitive_variables"] = [error_message]
|
|
556
556
|
|
|
557
557
|
raise serializers.ValidationError(errors)
|
|
558
558
|
|
|
559
|
-
return super().validate(
|
|
559
|
+
return super().validate(attrs)
|
|
560
560
|
|
|
561
561
|
|
|
562
562
|
class JobVariableSerializer(serializers.Serializer):
|
|
@@ -668,22 +668,22 @@ class JobHookSerializer(NautobotModelSerializer):
|
|
|
668
668
|
model = JobHook
|
|
669
669
|
fields = "__all__"
|
|
670
670
|
|
|
671
|
-
def validate(self,
|
|
672
|
-
|
|
671
|
+
def validate(self, attrs):
|
|
672
|
+
validated_attrs = super().validate(attrs)
|
|
673
673
|
|
|
674
674
|
conflicts = JobHook.check_for_conflicts(
|
|
675
675
|
instance=self.instance,
|
|
676
|
-
content_types=
|
|
677
|
-
job=
|
|
678
|
-
type_create=
|
|
679
|
-
type_update=
|
|
680
|
-
type_delete=
|
|
676
|
+
content_types=attrs.get("content_types"),
|
|
677
|
+
job=attrs.get("job"),
|
|
678
|
+
type_create=attrs.get("type_create"),
|
|
679
|
+
type_update=attrs.get("type_update"),
|
|
680
|
+
type_delete=attrs.get("type_delete"),
|
|
681
681
|
)
|
|
682
682
|
|
|
683
683
|
if conflicts:
|
|
684
684
|
raise serializers.ValidationError(conflicts)
|
|
685
685
|
|
|
686
|
-
return
|
|
686
|
+
return validated_attrs
|
|
687
687
|
|
|
688
688
|
|
|
689
689
|
class JobCreationSerializer(BaseModelSerializer):
|
|
@@ -702,15 +702,15 @@ class JobCreationSerializer(BaseModelSerializer):
|
|
|
702
702
|
model = ScheduledJob
|
|
703
703
|
fields = ["url", "name", "start_time", "interval", "crontab"]
|
|
704
704
|
|
|
705
|
-
def validate(self,
|
|
706
|
-
|
|
705
|
+
def validate(self, attrs):
|
|
706
|
+
attrs = super().validate(attrs)
|
|
707
707
|
|
|
708
|
-
if
|
|
709
|
-
if "name" not in
|
|
708
|
+
if attrs["interval"] in choices.JobExecutionType.SCHEDULE_CHOICES:
|
|
709
|
+
if "name" not in attrs:
|
|
710
710
|
raise serializers.ValidationError({"name": "Please provide a name for the job schedule."})
|
|
711
711
|
|
|
712
|
-
if ("start_time" not in
|
|
713
|
-
"start_time" in
|
|
712
|
+
if ("start_time" not in attrs and attrs["interval"] != choices.JobExecutionType.TYPE_CUSTOM) or (
|
|
713
|
+
"start_time" in attrs and attrs["start_time"] < models.ScheduledJob.earliest_possible_time()
|
|
714
714
|
):
|
|
715
715
|
raise serializers.ValidationError(
|
|
716
716
|
{
|
|
@@ -718,15 +718,15 @@ class JobCreationSerializer(BaseModelSerializer):
|
|
|
718
718
|
}
|
|
719
719
|
)
|
|
720
720
|
|
|
721
|
-
if
|
|
722
|
-
if
|
|
721
|
+
if attrs["interval"] == choices.JobExecutionType.TYPE_CUSTOM:
|
|
722
|
+
if attrs.get("crontab") is None:
|
|
723
723
|
raise serializers.ValidationError({"crontab": "Please enter a valid crontab."})
|
|
724
724
|
try:
|
|
725
|
-
models.ScheduledJob.get_crontab(
|
|
725
|
+
models.ScheduledJob.get_crontab(attrs["crontab"])
|
|
726
726
|
except Exception as e:
|
|
727
727
|
raise serializers.ValidationError({"crontab": e})
|
|
728
728
|
|
|
729
|
-
return
|
|
729
|
+
return attrs
|
|
730
730
|
|
|
731
731
|
|
|
732
732
|
class JobInputSerializer(serializers.Serializer):
|
|
@@ -744,15 +744,18 @@ class JobMultiPartInputSerializer(serializers.Serializer):
|
|
|
744
744
|
_schedule_crontab = serializers.CharField(required=False, allow_blank=True)
|
|
745
745
|
_task_queue = serializers.CharField(required=False, allow_blank=True)
|
|
746
746
|
|
|
747
|
-
def validate(self,
|
|
748
|
-
|
|
747
|
+
def validate(self, attrs):
|
|
748
|
+
attrs = super().validate(attrs)
|
|
749
749
|
|
|
750
|
-
if "_schedule_interval" in
|
|
751
|
-
if "_schedule_name" not in
|
|
750
|
+
if "_schedule_interval" in attrs and attrs["_schedule_interval"] != JobExecutionType.TYPE_IMMEDIATELY:
|
|
751
|
+
if "_schedule_name" not in attrs:
|
|
752
752
|
raise serializers.ValidationError({"_schedule_name": "Please provide a name for the job schedule."})
|
|
753
753
|
|
|
754
|
-
if (
|
|
755
|
-
"_schedule_start_time" in
|
|
754
|
+
if (
|
|
755
|
+
"_schedule_start_time" not in attrs and attrs["_schedule_interval"] != JobExecutionType.TYPE_CUSTOM
|
|
756
|
+
) or (
|
|
757
|
+
"_schedule_start_time" in attrs
|
|
758
|
+
and attrs["_schedule_start_time"] < ScheduledJob.earliest_possible_time()
|
|
756
759
|
):
|
|
757
760
|
raise serializers.ValidationError(
|
|
758
761
|
{
|
|
@@ -760,15 +763,15 @@ class JobMultiPartInputSerializer(serializers.Serializer):
|
|
|
760
763
|
}
|
|
761
764
|
)
|
|
762
765
|
|
|
763
|
-
if
|
|
764
|
-
if
|
|
766
|
+
if attrs["_schedule_interval"] == JobExecutionType.TYPE_CUSTOM:
|
|
767
|
+
if attrs.get("_schedule_crontab") is None:
|
|
765
768
|
raise serializers.ValidationError({"_schedule_crontab": "Please enter a valid crontab."})
|
|
766
769
|
try:
|
|
767
|
-
ScheduledJob.get_crontab(
|
|
770
|
+
ScheduledJob.get_crontab(attrs["_schedule_crontab"])
|
|
768
771
|
except Exception as e:
|
|
769
772
|
raise serializers.ValidationError({"_schedule_crontab": e})
|
|
770
773
|
|
|
771
|
-
return
|
|
774
|
+
return attrs
|
|
772
775
|
|
|
773
776
|
|
|
774
777
|
class JobLogEntrySerializer(BaseModelSerializer):
|
|
@@ -1054,18 +1057,18 @@ class TagSerializer(NautobotModelSerializer):
|
|
|
1054
1057
|
"color": {"help_text": "RGB color in hexadecimal (e.g. 00ff00)"},
|
|
1055
1058
|
}
|
|
1056
1059
|
|
|
1057
|
-
def validate(self,
|
|
1058
|
-
|
|
1060
|
+
def validate(self, attrs):
|
|
1061
|
+
attrs = super().validate(attrs)
|
|
1059
1062
|
|
|
1060
1063
|
# check if tag is assigned to any of the removed content_types
|
|
1061
|
-
if self.instance is not None and self.instance.present_in_database and "content_types" in
|
|
1062
|
-
content_types_id = [content_type.id for content_type in
|
|
1064
|
+
if self.instance is not None and self.instance.present_in_database and "content_types" in attrs:
|
|
1065
|
+
content_types_id = [content_type.id for content_type in attrs["content_types"]]
|
|
1063
1066
|
errors = self.instance.validate_content_types_removal(content_types_id)
|
|
1064
1067
|
|
|
1065
1068
|
if errors:
|
|
1066
1069
|
raise serializers.ValidationError(errors)
|
|
1067
1070
|
|
|
1068
|
-
return
|
|
1071
|
+
return attrs
|
|
1069
1072
|
|
|
1070
1073
|
|
|
1071
1074
|
#
|
|
@@ -1087,10 +1090,10 @@ class TeamSerializer(NautobotModelSerializer):
|
|
|
1087
1090
|
# https://www.django-rest-framework.org/api-guide/validators/#optional-fields
|
|
1088
1091
|
validators = []
|
|
1089
1092
|
|
|
1090
|
-
def validate(self,
|
|
1093
|
+
def validate(self, attrs):
|
|
1091
1094
|
validator = UniqueTogetherValidator(queryset=Team.objects.all(), fields=("name", "phone", "email"))
|
|
1092
|
-
validator(
|
|
1093
|
-
return super().validate(
|
|
1095
|
+
validator(attrs, self)
|
|
1096
|
+
return super().validate(attrs)
|
|
1094
1097
|
|
|
1095
1098
|
|
|
1096
1099
|
#
|
|
@@ -1108,19 +1111,19 @@ class WebhookSerializer(ValidatedModelSerializer, NotesSerializerMixin):
|
|
|
1108
1111
|
model = Webhook
|
|
1109
1112
|
fields = "__all__"
|
|
1110
1113
|
|
|
1111
|
-
def validate(self,
|
|
1112
|
-
|
|
1114
|
+
def validate(self, attrs):
|
|
1115
|
+
validated_attrs = super().validate(attrs)
|
|
1113
1116
|
|
|
1114
1117
|
conflicts = Webhook.check_for_conflicts(
|
|
1115
1118
|
instance=self.instance,
|
|
1116
|
-
content_types=
|
|
1117
|
-
payload_url=
|
|
1118
|
-
type_create=
|
|
1119
|
-
type_update=
|
|
1120
|
-
type_delete=
|
|
1119
|
+
content_types=attrs.get("content_types"),
|
|
1120
|
+
payload_url=attrs.get("payload_url"),
|
|
1121
|
+
type_create=attrs.get("type_create"),
|
|
1122
|
+
type_update=attrs.get("type_update"),
|
|
1123
|
+
type_delete=attrs.get("type_delete"),
|
|
1121
1124
|
)
|
|
1122
1125
|
|
|
1123
1126
|
if conflicts:
|
|
1124
1127
|
raise serializers.ValidationError(conflicts)
|
|
1125
1128
|
|
|
1126
|
-
return
|
|
1129
|
+
return validated_attrs
|
nautobot/extras/api/views.py
CHANGED
|
@@ -732,13 +732,13 @@ class JobViewSet(
|
|
|
732
732
|
):
|
|
733
733
|
lookup_value_regex = r"[-0-9a-fA-F]+"
|
|
734
734
|
|
|
735
|
-
def perform_destroy(self,
|
|
736
|
-
if
|
|
735
|
+
def perform_destroy(self, instance):
|
|
736
|
+
if instance.module_name.startswith("nautobot."):
|
|
737
737
|
raise ProtectedError(
|
|
738
|
-
f"Unable to delete Job {
|
|
738
|
+
f"Unable to delete Job {instance}. System Job cannot be deleted",
|
|
739
739
|
[],
|
|
740
740
|
)
|
|
741
|
-
super().perform_destroy(
|
|
741
|
+
super().perform_destroy(instance)
|
|
742
742
|
|
|
743
743
|
|
|
744
744
|
@extend_schema_view(
|
nautobot/extras/health_checks.py
CHANGED
|
@@ -160,5 +160,4 @@ class RedisBackend(RedisHealthCheck):
|
|
|
160
160
|
elif client_class == "django_redis.client.DefaultClient":
|
|
161
161
|
self.check_redis(redis_url=location, **options.get("CONNECTION_POOL_KWARGS", {}))
|
|
162
162
|
else:
|
|
163
|
-
|
|
164
|
-
self.add_error(ServiceUnavailable(f"{client_class} is an unsupported CLIENT_CLASS!"))
|
|
163
|
+
self.add_error(ServiceUnavailable(f"{client_class} is an unsupported CLIENT_CLASS!"))
|
nautobot/extras/jobs.py
CHANGED
|
@@ -295,7 +295,7 @@ class BaseJob:
|
|
|
295
295
|
- my_plugin.jobs.MyPluginJob - App-provided Job
|
|
296
296
|
- git_repository.jobs.myjob.MyJob - GitRepository Job
|
|
297
297
|
"""
|
|
298
|
-
return f"{cls.__module__}.{cls.__name__}"
|
|
298
|
+
return f"{cls.__module__}.{cls.__name__}" # pylint: disable=no-member
|
|
299
299
|
|
|
300
300
|
@final
|
|
301
301
|
@classproperty
|
|
@@ -332,7 +332,7 @@ class BaseJob:
|
|
|
332
332
|
@final
|
|
333
333
|
@classproperty
|
|
334
334
|
def name(cls) -> str: # pylint: disable=no-self-argument
|
|
335
|
-
return cls._get_meta_attr_and_assert_type("name", cls.__name__, expected_type=str)
|
|
335
|
+
return cls._get_meta_attr_and_assert_type("name", cls.__name__, expected_type=str) # pylint: disable=no-member
|
|
336
336
|
|
|
337
337
|
@final
|
|
338
338
|
@classproperty
|
|
@@ -420,7 +420,7 @@ class BaseJob:
|
|
|
420
420
|
@classproperty
|
|
421
421
|
def registered_name(cls) -> str: # pylint: disable=no-self-argument
|
|
422
422
|
"""Deprecated - use class_path classproperty instead."""
|
|
423
|
-
return f"{cls.__module__}.{cls.__name__}"
|
|
423
|
+
return f"{cls.__module__}.{cls.__name__}" # pylint: disable=no-member
|
|
424
424
|
|
|
425
425
|
@classmethod
|
|
426
426
|
def _get_vars(cls):
|
|
@@ -1011,7 +1011,7 @@ class JobHookReceiver(Job):
|
|
|
1011
1011
|
|
|
1012
1012
|
object_change = ObjectVar(model=ObjectChange)
|
|
1013
1013
|
|
|
1014
|
-
def run(self, object_change):
|
|
1014
|
+
def run(self, object_change): # pylint: disable=arguments-differ
|
|
1015
1015
|
"""JobHookReceiver subclasses generally shouldn't need to override this method."""
|
|
1016
1016
|
self.receive_job_hook(
|
|
1017
1017
|
change=object_change,
|
|
@@ -1040,7 +1040,7 @@ class JobButtonReceiver(Job):
|
|
|
1040
1040
|
object_pk = StringVar()
|
|
1041
1041
|
object_model_name = StringVar()
|
|
1042
1042
|
|
|
1043
|
-
def run(self, object_pk, object_model_name):
|
|
1043
|
+
def run(self, object_pk, object_model_name): # pylint: disable=arguments-differ
|
|
1044
1044
|
"""JobButtonReceiver subclasses generally shouldn't need to override this method."""
|
|
1045
1045
|
model = get_model_from_name(object_model_name)
|
|
1046
1046
|
obj = model.objects.get(pk=object_pk)
|
nautobot/extras/managers.py
CHANGED
|
@@ -22,8 +22,9 @@ class JobResultManager(BaseManager.from_queryset(RestrictedQuerySet), TaskResult
|
|
|
22
22
|
return self.model(id=task_id)
|
|
23
23
|
|
|
24
24
|
@transaction_retry(max_retries=2)
|
|
25
|
-
def store_result(
|
|
25
|
+
def store_result( # pylint:disable=arguments-differ # Nautobot adds kwargs like job_model_id and scheduled_job_id
|
|
26
26
|
self,
|
|
27
|
+
*,
|
|
27
28
|
task_id,
|
|
28
29
|
result,
|
|
29
30
|
status,
|
|
@@ -41,6 +42,7 @@ class JobResultManager(BaseManager.from_queryset(RestrictedQuerySet), TaskResult
|
|
|
41
42
|
using=None,
|
|
42
43
|
content_type=None,
|
|
43
44
|
content_encoding=None,
|
|
45
|
+
**kwargs,
|
|
44
46
|
):
|
|
45
47
|
"""
|
|
46
48
|
Store the result and status of a Celery task.
|
|
@@ -2,7 +2,6 @@ from collections import OrderedDict
|
|
|
2
2
|
|
|
3
3
|
from django.db import migrations
|
|
4
4
|
|
|
5
|
-
from nautobot.extras.choices import LogLevelChoices
|
|
6
5
|
from nautobot.extras.constants import (
|
|
7
6
|
JOB_LOG_MAX_ABSOLUTE_URL_LENGTH,
|
|
8
7
|
JOB_LOG_MAX_GROUPING_LENGTH,
|
|
@@ -123,14 +122,13 @@ def reverse_migrate_params(apps, schema_editor):
|
|
|
123
122
|
]
|
|
124
123
|
)
|
|
125
124
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
job_result.data["total"][entry.log_level] += 1
|
|
125
|
+
job_result.data[entry.grouping].setdefault(entry.log_level, 0)
|
|
126
|
+
job_result.data[entry.grouping][entry.log_level] += 1
|
|
127
|
+
if "total" not in job_result.data:
|
|
128
|
+
job_result.data["total"] = _data_grouping_struct()
|
|
129
|
+
del job_result.data["total"]["log"]
|
|
130
|
+
job_result.data["total"].setdefault(entry.log_level, 0)
|
|
131
|
+
job_result.data["total"][entry.log_level] += 1
|
|
134
132
|
|
|
135
133
|
job_result.save()
|
|
136
134
|
|
nautobot/extras/models/groups.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"""Dynamic Groups Models."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations # python 3.8
|
|
4
|
+
|
|
3
5
|
import logging
|
|
6
|
+
from typing import Optional
|
|
4
7
|
|
|
5
8
|
from django import forms
|
|
6
9
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
@@ -116,7 +119,7 @@ class DynamicGroup(PrimaryModel):
|
|
|
116
119
|
return self._model
|
|
117
120
|
|
|
118
121
|
@property
|
|
119
|
-
def filterset_class(self):
|
|
122
|
+
def filterset_class(self) -> Optional[type[django_filters.FilterSet]]:
|
|
120
123
|
if getattr(self, "_filterset_class", None) is None:
|
|
121
124
|
try:
|
|
122
125
|
self._filterset_class = get_filterset_for_model(self.model)
|
|
@@ -125,7 +128,7 @@ class DynamicGroup(PrimaryModel):
|
|
|
125
128
|
return self._filterset_class
|
|
126
129
|
|
|
127
130
|
@property
|
|
128
|
-
def filterform_class(self):
|
|
131
|
+
def filterform_class(self) -> Optional[type[forms.Form]]:
|
|
129
132
|
if getattr(self, "_filterform_class", None) is None:
|
|
130
133
|
try:
|
|
131
134
|
self._filterform_class = get_form_for_model(self.model, form_prefix="Filter")
|
|
@@ -134,7 +137,7 @@ class DynamicGroup(PrimaryModel):
|
|
|
134
137
|
return self._filterform_class
|
|
135
138
|
|
|
136
139
|
@property
|
|
137
|
-
def form_class(self):
|
|
140
|
+
def form_class(self) -> Optional[type[forms.Form]]:
|
|
138
141
|
if getattr(self, "_form_class", None) is None:
|
|
139
142
|
try:
|
|
140
143
|
self._form_class = get_form_for_model(self.model)
|
|
@@ -147,19 +150,19 @@ class DynamicGroup(PrimaryModel):
|
|
|
147
150
|
"""Return all FilterForm fields in a dictionary."""
|
|
148
151
|
|
|
149
152
|
# Fail gracefully with an empty dict if nothing is working yet.
|
|
150
|
-
if
|
|
153
|
+
if self.form_class is None or self.filterform_class is None or self.filterset_class is None:
|
|
151
154
|
return {}
|
|
152
155
|
|
|
153
156
|
# Get model form and fields
|
|
154
|
-
modelform = self.form_class()
|
|
157
|
+
modelform = self.form_class() # pylint: disable=not-callable
|
|
155
158
|
modelform_fields = modelform.fields
|
|
156
159
|
|
|
157
160
|
# Get filter form and fields
|
|
158
|
-
filterform = self.filterform_class()
|
|
161
|
+
filterform = self.filterform_class() # pylint: disable=not-callable
|
|
159
162
|
filterform_fields = filterform.fields
|
|
160
163
|
|
|
161
164
|
# Get filterset and fields
|
|
162
|
-
filterset = self.filterset_class()
|
|
165
|
+
filterset = self.filterset_class() # pylint: disable=not-callable
|
|
163
166
|
filterset_fields = filterset.filters
|
|
164
167
|
|
|
165
168
|
# Get dynamic group filter field mappings (if any)
|
|
@@ -303,6 +306,7 @@ class DynamicGroup(PrimaryModel):
|
|
|
303
306
|
# Since associated_object is a GenericForeignKey, we can't just do:
|
|
304
307
|
# return self.static_group_associations.values_list("associated_object", flat=True)
|
|
305
308
|
return self.model.objects.filter(
|
|
309
|
+
# pylint: disable=no-member # false positive about self.static_group_associations
|
|
306
310
|
pk__in=self.static_group_associations(manager="all_objects").values_list("associated_object_id", flat=True)
|
|
307
311
|
)
|
|
308
312
|
|
|
@@ -611,7 +615,7 @@ class DynamicGroup(PrimaryModel):
|
|
|
611
615
|
raise ValidationError({"filter": "Filter can only be set for groups of type `dynamic-filter`."})
|
|
612
616
|
else:
|
|
613
617
|
# Validate against the filterset's internal form validation.
|
|
614
|
-
filterset = self.filterset_class(self.filter)
|
|
618
|
+
filterset = self.filterset_class(self.filter) # pylint: disable=not-callable
|
|
615
619
|
if not filterset.is_valid():
|
|
616
620
|
raise ValidationError(filterset.errors)
|
|
617
621
|
|
|
@@ -725,7 +729,7 @@ class DynamicGroup(PrimaryModel):
|
|
|
725
729
|
if self.group_type != DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER:
|
|
726
730
|
raise RuntimeError(f"{self} is not a dynamic-filter group")
|
|
727
731
|
|
|
728
|
-
filterset = self.filterset_class(self.filter, self.model.objects.all())
|
|
732
|
+
filterset = self.filterset_class(self.filter, self.model.objects.all()) # pylint: disable=not-callable
|
|
729
733
|
query = models.Q()
|
|
730
734
|
|
|
731
735
|
# In this case we want all filters for a group's filter dict in a set intersection (boolean
|
nautobot/extras/models/jobs.py
CHANGED
|
@@ -234,13 +234,13 @@ class Job(PrimaryModel):
|
|
|
234
234
|
def __str__(self):
|
|
235
235
|
return self.name
|
|
236
236
|
|
|
237
|
-
def delete(self):
|
|
237
|
+
def delete(self, *args, **kwargs):
|
|
238
238
|
if self.module_name.startswith("nautobot."):
|
|
239
239
|
raise ProtectedError(
|
|
240
240
|
f"Unable to delete Job {self}. System Job cannot be deleted",
|
|
241
241
|
[],
|
|
242
242
|
)
|
|
243
|
-
super().delete()
|
|
243
|
+
super().delete(*args, **kwargs)
|
|
244
244
|
|
|
245
245
|
@property
|
|
246
246
|
def job_class(self):
|
|
@@ -610,7 +610,7 @@ class JobResult(BaseModel, CustomFieldModel):
|
|
|
610
610
|
# Only add metrics if we have a related job model. If we are moving to a terminal state we should always
|
|
611
611
|
# have a related job model, so this shouldn't be too tight of a restriction.
|
|
612
612
|
if self.job_model:
|
|
613
|
-
duration = self.date_done - self.
|
|
613
|
+
duration = self.date_done - self.date_created
|
|
614
614
|
JOB_RESULT_METRIC.labels(self.job_model.grouping, self.job_model.name, status).observe(
|
|
615
615
|
duration.total_seconds()
|
|
616
616
|
)
|
|
@@ -1180,7 +1180,7 @@ class ScheduledJob(BaseModel):
|
|
|
1180
1180
|
|
|
1181
1181
|
def to_cron(self):
|
|
1182
1182
|
tz = self.time_zone
|
|
1183
|
-
t = self.start_time.astimezone(tz)
|
|
1183
|
+
t = self.start_time.astimezone(tz) # pylint: disable=no-member
|
|
1184
1184
|
if self.interval == JobExecutionType.TYPE_HOURLY:
|
|
1185
1185
|
return TzAwareCrontab(minute=t.minute, tz=tz)
|
|
1186
1186
|
elif self.interval == JobExecutionType.TYPE_DAILY:
|
nautobot/extras/models/models.py
CHANGED
|
@@ -207,7 +207,7 @@ class ConfigContextModel(models.Model, ConfigContextSchemaValidationMixin):
|
|
|
207
207
|
# Annotation not available, so fall back to manually querying for the config context
|
|
208
208
|
config_context_data = ConfigContext.objects.get_for_object(self).values_list("data", flat=True)
|
|
209
209
|
else:
|
|
210
|
-
config_context_data = self.config_context_data or []
|
|
210
|
+
config_context_data = self.config_context_data or [] # pylint: disable=no-member
|
|
211
211
|
config_context_data = [
|
|
212
212
|
c["data"] for c in sorted(config_context_data, key=lambda k: (k["weight"], k["name"]))
|
|
213
213
|
]
|
|
@@ -831,7 +831,7 @@ class Note(ChangeLoggedModel, BaseModel):
|
|
|
831
831
|
unique_together = [["assigned_object_type", "assigned_object_id", "user_name", "created"]]
|
|
832
832
|
|
|
833
833
|
def __str__(self):
|
|
834
|
-
return f"{self.assigned_object} - {self.created.isoformat() if self.created else None}"
|
|
834
|
+
return f"{self.assigned_object} - {self.created.isoformat() if self.created else None}" # pylint: disable=no-member
|
|
835
835
|
|
|
836
836
|
def save(self, *args, **kwargs):
|
|
837
837
|
# Record the user's name as static strings
|
nautobot/extras/plugins/views.py
CHANGED
|
@@ -166,7 +166,7 @@ class AppsAPIRootView(AuthenticatedAPIRootView):
|
|
|
166
166
|
return entry
|
|
167
167
|
|
|
168
168
|
@extend_schema(exclude=True)
|
|
169
|
-
def get(self, request, format=None): # pylint: disable=redefined-builtin
|
|
169
|
+
def get(self, request, *args, format=None, **kwargs): # pylint: disable=redefined-builtin
|
|
170
170
|
entries = []
|
|
171
171
|
for app_name in settings.PLUGINS:
|
|
172
172
|
app_config = apps.get_app_config(app_name)
|
nautobot/extras/tables.py
CHANGED
|
@@ -242,7 +242,7 @@ class ConfigContextSchemaValidationStateColumn(tables.Column):
|
|
|
242
242
|
self.validator = validator
|
|
243
243
|
self.data_field = data_field
|
|
244
244
|
|
|
245
|
-
def render(self, record):
|
|
245
|
+
def render(self, *, record): # pylint: disable=arguments-differ # tables2 varies its kwargs
|
|
246
246
|
data = getattr(record, self.data_field)
|
|
247
247
|
try:
|
|
248
248
|
self.validator.validate(data)
|
|
@@ -626,13 +626,13 @@ class GitRepositoryTable(BaseTable):
|
|
|
626
626
|
)
|
|
627
627
|
|
|
628
628
|
def render_last_sync_time(self, record):
|
|
629
|
-
if record.name in self.context["job_results"]:
|
|
630
|
-
return self.context["job_results"][record.name].date_done
|
|
629
|
+
if record.name in self.context["job_results"]: # pylint: disable=no-member
|
|
630
|
+
return self.context["job_results"][record.name].date_done # pylint: disable=no-member
|
|
631
631
|
return self.default
|
|
632
632
|
|
|
633
633
|
def render_last_sync_user(self, record):
|
|
634
|
-
if record.name in self.context["job_results"]:
|
|
635
|
-
user = self.context["job_results"][record.name].user
|
|
634
|
+
if record.name in self.context["job_results"]: # pylint: disable=no-member
|
|
635
|
+
user = self.context["job_results"][record.name].user # pylint: disable=no-member
|
|
636
636
|
return user
|
|
637
637
|
return self.default
|
|
638
638
|
|
|
@@ -18,7 +18,7 @@ class APITestJob(Job):
|
|
|
18
18
|
var3 = BooleanVar()
|
|
19
19
|
var4 = ObjectVar(model=Role)
|
|
20
20
|
|
|
21
|
-
def run(self, var1, var2, var3, var4):
|
|
21
|
+
def run(self, var1, var2, var3, var4): # pylint:disable=arguments-differ
|
|
22
22
|
logger.debug(var1)
|
|
23
23
|
logger.info(var2)
|
|
24
24
|
logger.warning(var3)
|
|
@@ -19,7 +19,7 @@ class TestAtomicDecorator(Job):
|
|
|
19
19
|
fail = BooleanVar()
|
|
20
20
|
|
|
21
21
|
@transaction.atomic
|
|
22
|
-
def run(self, fail=False):
|
|
22
|
+
def run(self, fail=False): # pylint:disable=arguments-differ
|
|
23
23
|
try:
|
|
24
24
|
Status.objects.create(name="Test database atomic rollback 1")
|
|
25
25
|
if fail:
|
|
@@ -37,7 +37,7 @@ class TestAtomicContextManager(Job):
|
|
|
37
37
|
|
|
38
38
|
fail = BooleanVar()
|
|
39
39
|
|
|
40
|
-
def run(self, fail=False):
|
|
40
|
+
def run(self, fail=False): # pylint:disable=arguments-differ
|
|
41
41
|
try:
|
|
42
42
|
with transaction.atomic():
|
|
43
43
|
Status.objects.create(name="Test database atomic rollback 2")
|