nautobot 2.4.4__py3-none-any.whl → 2.4.5__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.

Files changed (42) hide show
  1. nautobot/__init__.py +19 -3
  2. nautobot/core/celery/__init__.py +5 -3
  3. nautobot/core/jobs/__init__.py +3 -2
  4. nautobot/core/testing/__init__.py +2 -0
  5. nautobot/core/testing/mixins.py +9 -0
  6. nautobot/core/tests/test_jobs.py +26 -27
  7. nautobot/dcim/tests/test_jobs.py +4 -6
  8. nautobot/extras/choices.py +8 -3
  9. nautobot/extras/jobs.py +181 -103
  10. nautobot/extras/management/utils.py +13 -2
  11. nautobot/extras/models/datasources.py +4 -1
  12. nautobot/extras/models/jobs.py +20 -17
  13. nautobot/extras/tables.py +25 -29
  14. nautobot/extras/test_jobs/atomic_transaction.py +6 -6
  15. nautobot/extras/test_jobs/fail.py +75 -1
  16. nautobot/extras/tests/test_api.py +1 -1
  17. nautobot/extras/tests/test_datasources.py +64 -54
  18. nautobot/extras/tests/test_jobs.py +69 -62
  19. nautobot/extras/tests/test_models.py +1 -1
  20. nautobot/extras/tests/test_relationships.py +5 -5
  21. nautobot/extras/views.py +7 -1
  22. nautobot/ipam/forms.py +15 -0
  23. nautobot/ipam/querysets.py +6 -0
  24. nautobot/ipam/templates/ipam/rir.html +1 -43
  25. nautobot/ipam/tests/test_models.py +16 -0
  26. nautobot/ipam/urls.py +1 -21
  27. nautobot/ipam/views.py +24 -41
  28. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +43 -5
  29. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +35 -0
  30. nautobot/project-static/docs/development/jobs/index.html +27 -14
  31. nautobot/project-static/docs/objects.inv +0 -0
  32. nautobot/project-static/docs/release-notes/version-2.4.html +183 -0
  33. nautobot/project-static/docs/requirements.txt +1 -1
  34. nautobot/project-static/docs/search/search_index.json +1 -1
  35. nautobot/project-static/docs/sitemap.xml +290 -290
  36. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  37. {nautobot-2.4.4.dist-info → nautobot-2.4.5.dist-info}/METADATA +3 -3
  38. {nautobot-2.4.4.dist-info → nautobot-2.4.5.dist-info}/RECORD +42 -42
  39. {nautobot-2.4.4.dist-info → nautobot-2.4.5.dist-info}/LICENSE.txt +0 -0
  40. {nautobot-2.4.4.dist-info → nautobot-2.4.5.dist-info}/NOTICE +0 -0
  41. {nautobot-2.4.4.dist-info → nautobot-2.4.5.dist-info}/WHEEL +0 -0
  42. {nautobot-2.4.4.dist-info → nautobot-2.4.5.dist-info}/entry_points.txt +0 -0
@@ -1784,7 +1784,7 @@ class RelationshipJobTestCase(RequiredRelationshipTestMixin, TransactionTestCase
1784
1784
 
1785
1785
  pk_list = [str(vlan.id) for vlan in vlans]
1786
1786
  job_result = self.create_job(pk_list)
1787
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
1787
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
1788
1788
  error_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
1789
1789
  self.assertIn("VLANs require at least one device, but no devices exist yet.", error_log.message)
1790
1790
 
@@ -1793,7 +1793,7 @@ class RelationshipJobTestCase(RequiredRelationshipTestMixin, TransactionTestCase
1793
1793
 
1794
1794
  # Try editing all 6 VLANs without adding the required device(fails):
1795
1795
  job_result = self.create_job(pk_list)
1796
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
1796
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
1797
1797
  error_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
1798
1798
  self.assertIn(
1799
1799
  '6 VLANs require a device for the required relationship \\"VLANs require at least one Device',
@@ -1802,7 +1802,7 @@ class RelationshipJobTestCase(RequiredRelationshipTestMixin, TransactionTestCase
1802
1802
 
1803
1803
  # Try editing 3 VLANs without adding the required device(fails):
1804
1804
  job_result = self.create_job(pk_list[:3])
1805
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
1805
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
1806
1806
  error_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
1807
1807
  self.assertIn(
1808
1808
  'These VLANs require a device for the required relationship \\"VLANs require at least one Device',
@@ -1813,11 +1813,11 @@ class RelationshipJobTestCase(RequiredRelationshipTestMixin, TransactionTestCase
1813
1813
 
1814
1814
  # Try editing 6 VLANs and adding the required device (succeeds):
1815
1815
  job_result = self.create_job(pk_list, add_cr_vlans_devices_m2m__source=[str(device_for_association.id)])
1816
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
1816
+ self.assertJobResultStatus(job_result)
1817
1817
 
1818
1818
  # Try editing 6 VLANs and removing the required device (fails):
1819
1819
  job_result = self.create_job(pk_list, remove_cr_vlans_devices_m2m__source=[str(device_for_association.id)])
1820
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
1820
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
1821
1821
  error_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
1822
1822
  self.assertIn(
1823
1823
  '6 VLANs require a device for the required relationship \\"VLANs require at least one Device',
nautobot/extras/views.py CHANGED
@@ -2062,7 +2062,13 @@ def get_annotated_jobresult_queryset():
2062
2062
  error_log_count=count_related(
2063
2063
  JobLogEntry,
2064
2064
  "job_result",
2065
- filter_dict={"log_level__in": [LogLevelChoices.LOG_ERROR, LogLevelChoices.LOG_CRITICAL]},
2065
+ filter_dict={
2066
+ "log_level__in": [
2067
+ LogLevelChoices.LOG_FAILURE,
2068
+ LogLevelChoices.LOG_ERROR,
2069
+ LogLevelChoices.LOG_CRITICAL,
2070
+ ],
2071
+ },
2066
2072
  ),
2067
2073
  )
2068
2074
  )
nautobot/ipam/forms.py CHANGED
@@ -262,6 +262,21 @@ class RIRFilterForm(NautobotFilterForm):
262
262
  )
263
263
 
264
264
 
265
+ class RIRBulkEditForm(NautobotBulkEditForm):
266
+ pk = forms.ModelMultipleChoiceField(queryset=RIR.objects.all(), widget=forms.MultipleHiddenInput())
267
+ is_private = forms.NullBooleanField(
268
+ required=False,
269
+ label="Private",
270
+ widget=StaticSelect2(choices=BOOLEAN_WITH_BLANK_CHOICES),
271
+ )
272
+ description = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
273
+
274
+ class Meta:
275
+ nullable_fields = [
276
+ "description",
277
+ ]
278
+
279
+
265
280
  #
266
281
  # Prefixes
267
282
  #
@@ -405,6 +405,12 @@ class IPAddressQuerySet(BaseNetworkQuerySet):
405
405
  namespace = kwargs.pop("namespace", None)
406
406
  host = kwargs.get("host")
407
407
  mask_length = kwargs.get("mask_length")
408
+ address = kwargs.get("address")
409
+ if host is None and address is not None:
410
+ address = netaddr.IPNetwork(address)
411
+ host = str(address.ip)
412
+ mask_length = address.prefixlen
413
+
408
414
  # If `host` or `mask_length` is None skip; then there is no way of getting the closest parent;
409
415
  if parent is None and host is not None and mask_length is not None:
410
416
  if namespace is None:
@@ -1,44 +1,2 @@
1
1
  {% extends 'generic/object_retrieve.html' %}
2
- {% load helpers %}
3
-
4
- {% block content_left_page %}
5
- <div class="panel panel-default">
6
- <div class="panel-heading">
7
- <strong>RIR</strong>
8
- </div>
9
- <table class="table table-hover panel-body attr-table">
10
- <tr>
11
- <td>Description</td>
12
- <td>{{ object.description|placeholder }}</td>
13
- </tr>
14
- <tr>
15
- <td>Private</td>
16
- <td>{{ object.is_private | render_boolean }}</td>
17
- </tr>
18
- <tr>
19
- <td>Assigned Prefixes</td>
20
- <td>
21
- <a href="{% url 'ipam:prefix_list' %}?rir={{ object.name }}">{{ assigned_prefix_table.rows|length }}</a>
22
- </td>
23
- </tr>
24
- </table>
25
- </div>
26
- {% endblock content_left_page %}
27
-
28
- {% block content_full_width_page %}
29
- <div class="panel panel-default">
30
- <div class="panel-heading">
31
- <strong>Assigned Prefixes</strong>
32
- </div>
33
- {% include 'inc/table.html' with table=assigned_prefix_table %}
34
- {% if perms.ipam.add_prefix %}
35
- <div class="panel-footer text-right noprint">
36
- <a href="{% url 'ipam:prefix_add' %}?rir={{ object.pk }}" class="btn btn-xs btn-primary">
37
- <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add prefix
38
- </a>
39
- </div>
40
- {% endif %}
41
- </div>
42
- {% include 'inc/paginator.html' with paginator=assigned_prefix_table.paginator page=assigned_prefix_table.page %}
43
- <div class="row"></div>
44
- {% endblock content_full_width_page %}
2
+ {% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
@@ -1301,6 +1301,22 @@ class TestIPAddress(ModelTestCases.BaseModelTestCase):
1301
1301
  str(err.exception),
1302
1302
  )
1303
1303
 
1304
+ def test_get_or_create_address_kwarg(self):
1305
+ status = Status.objects.get(name="Active")
1306
+ namespace = Namespace.objects.create(name="Test IPAddress get_or_create with address kwarg")
1307
+ Prefix.objects.create(prefix="10.0.0.0/24", namespace=namespace, status=status)
1308
+ ip_address, created = IPAddress.objects.get_or_create(
1309
+ address="10.0.0.40/32", namespace=namespace, defaults={"status": status}
1310
+ )
1311
+ self.assertEqual(ip_address.host, "10.0.0.40")
1312
+ self.assertEqual(ip_address.mask_length, 32)
1313
+ self.assertTrue(created)
1314
+ _, created = IPAddress.objects.get_or_create(
1315
+ address="10.0.0.40/32", namespace=namespace, defaults={"status": status}
1316
+ )
1317
+ self.assertFalse(created)
1318
+ self.assertTrue(IPAddress.objects.filter(address="10.0.0.40/32", parent__namespace=namespace).exists())
1319
+
1304
1320
  def test_create_field_population(self):
1305
1321
  """Test that the various ways of creating an IPAddress result in correctly populated fields."""
1306
1322
  if self.namespace != get_default_namespace():
nautobot/ipam/urls.py CHANGED
@@ -7,7 +7,6 @@ from . import views
7
7
  from .models import (
8
8
  IPAddress,
9
9
  Prefix,
10
- RIR,
11
10
  Service,
12
11
  VLAN,
13
12
  VLANGroup,
@@ -20,28 +19,9 @@ router.register("ip-address-to-interface", views.IPAddressToInterfaceUIViewSet)
20
19
  router.register("namespaces", views.NamespaceUIViewSet)
21
20
  router.register("route-targets", views.RouteTargetUIViewSet)
22
21
  router.register("vrfs", views.VRFUIViewSet)
22
+ router.register("rirs", views.RIRUIViewSet)
23
23
 
24
24
  urlpatterns = [
25
- # RIRs
26
- path("rirs/", views.RIRListView.as_view(), name="rir_list"),
27
- path("rirs/add/", views.RIREditView.as_view(), name="rir_add"),
28
- path("rirs/import/", views.RIRBulkImportView.as_view(), name="rir_import"), # 3.0 TODO: remove, unused
29
- path("rirs/delete/", views.RIRBulkDeleteView.as_view(), name="rir_bulk_delete"),
30
- path("rirs/<uuid:pk>/", views.RIRView.as_view(), name="rir"),
31
- path("rirs/<uuid:pk>/edit/", views.RIREditView.as_view(), name="rir_edit"),
32
- path("rirs/<uuid:pk>/delete/", views.RIRDeleteView.as_view(), name="rir_delete"),
33
- path(
34
- "rirs/<uuid:pk>/changelog/",
35
- ObjectChangeLogView.as_view(),
36
- name="rir_changelog",
37
- kwargs={"model": RIR},
38
- ),
39
- path(
40
- "rirs/<uuid:pk>/notes/",
41
- ObjectNotesView.as_view(),
42
- name="rir_notes",
43
- kwargs={"model": RIR},
44
- ),
45
25
  # Namespaces
46
26
  path(
47
27
  "namespaces/<uuid:pk>/ip-addresses/",
nautobot/ipam/views.py CHANGED
@@ -325,49 +325,32 @@ class RouteTargetUIViewSet(NautobotUIViewSet):
325
325
  #
326
326
 
327
327
 
328
- class RIRListView(generic.ObjectListView):
328
+ class RIRUIViewSet(NautobotUIViewSet):
329
+ bulk_update_form_class = forms.RIRBulkEditForm
330
+ filterset_class = filters.RIRFilterSet
331
+ filterset_form_class = forms.RIRFilterForm
332
+ form_class = forms.RIRForm
329
333
  queryset = RIR.objects.all()
330
- filterset = filters.RIRFilterSet
331
- filterset_form = forms.RIRFilterForm
332
- table = tables.RIRTable
334
+ serializer_class = serializers.RIRSerializer
335
+ table_class = tables.RIRTable
333
336
 
334
-
335
- class RIRView(generic.ObjectView):
336
- queryset = RIR.objects.all()
337
-
338
- def get_extra_context(self, request, instance):
339
- # Prefixes
340
- assigned_prefixes = Prefix.objects.restrict(request.user, "view").filter(rir=instance).select_related("tenant")
341
-
342
- assigned_prefix_table = tables.PrefixTable(assigned_prefixes, hide_hierarchy_ui=True)
343
-
344
- paginate = {
345
- "paginator_class": EnhancedPaginator,
346
- "per_page": get_paginate_count(request),
347
- }
348
- RequestConfig(request, paginate).configure(assigned_prefix_table)
349
-
350
- return {"assigned_prefix_table": assigned_prefix_table, **super().get_extra_context(request, instance)}
351
-
352
-
353
- class RIREditView(generic.ObjectEditView):
354
- queryset = RIR.objects.all()
355
- model_form = forms.RIRForm
356
-
357
-
358
- class RIRDeleteView(generic.ObjectDeleteView):
359
- queryset = RIR.objects.all()
360
-
361
-
362
- class RIRBulkImportView(generic.BulkImportView): # 3.0 TODO: remove, unused
363
- queryset = RIR.objects.all()
364
- table = tables.RIRTable
365
-
366
-
367
- class RIRBulkDeleteView(generic.BulkDeleteView):
368
- queryset = RIR.objects.all()
369
- filterset = filters.RIRFilterSet
370
- table = tables.RIRTable
337
+ object_detail_content = object_detail.ObjectDetailContent(
338
+ panels=(
339
+ object_detail.ObjectFieldsPanel(
340
+ section=SectionChoices.LEFT_HALF,
341
+ weight=100,
342
+ fields="__all__",
343
+ ),
344
+ object_detail.ObjectsTablePanel(
345
+ section=SectionChoices.FULL_WIDTH,
346
+ weight=100,
347
+ table_title="Assigned Prefixes",
348
+ table_class=tables.PrefixTable,
349
+ table_filter="rir",
350
+ hide_hierarchy_ui=True,
351
+ ),
352
+ ),
353
+ )
371
354
 
372
355
 
373
356
  #
@@ -7419,6 +7419,15 @@
7419
7419
  </span>
7420
7420
  </a>
7421
7421
 
7422
+ </li>
7423
+
7424
+ <li class="md-nav__item">
7425
+ <a href="#nautobot.apps.jobs.BaseJob.fail" class="md-nav__link">
7426
+ <span class="md-ellipsis">
7427
+ fail
7428
+ </span>
7429
+ </a>
7430
+
7422
7431
  </li>
7423
7432
 
7424
7433
  <li class="md-nav__item">
@@ -9776,6 +9785,15 @@
9776
9785
  </span>
9777
9786
  </a>
9778
9787
 
9788
+ </li>
9789
+
9790
+ <li class="md-nav__item">
9791
+ <a href="#nautobot.apps.jobs.BaseJob.fail" class="md-nav__link">
9792
+ <span class="md-ellipsis">
9793
+ fail
9794
+ </span>
9795
+ </a>
9796
+
9779
9797
  </li>
9780
9798
 
9781
9799
  <li class="md-nav__item">
@@ -10624,11 +10642,13 @@ during a approval review workflow.</p>
10624
10642
  <tbody>
10625
10643
  <tr class="doc-section-item">
10626
10644
  <td>
10627
- <code>None</code>
10645
+ <code>Any</code>
10628
10646
  </td>
10629
10647
  <td>
10630
10648
  <div class="doc-md-description">
10631
- <p>The return value of this handler is ignored.</p>
10649
+ <p>The return value of this handler is ignored normally, <strong>except</strong> if <code>self.fail()</code> is called herein,
10650
+ in which case the return value will be used as the overall JobResult return value
10651
+ since <code>self.run()</code> will <strong>not</strong> be called in such a case.</p>
10632
10652
  </div>
10633
10653
  </td>
10634
10654
  </tr>
@@ -10835,6 +10855,23 @@ path would consider this a failure of the job execution, as described in <code>n
10835
10855
  <div class="doc doc-object doc-function">
10836
10856
 
10837
10857
 
10858
+ <h3 id="nautobot.apps.jobs.BaseJob.fail" class="doc doc-heading">
10859
+ <code class="highlight language-python"><span class="n">fail</span><span class="p">(</span><span class="n">msg</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span></code>
10860
+
10861
+ <a href="#nautobot.apps.jobs.BaseJob.fail" class="headerlink" title="Permanent link">&para;</a></h3>
10862
+
10863
+
10864
+ <div class="doc doc-contents ">
10865
+
10866
+ <p>Mark this job as failed without immediately raising an exception and aborting.</p>
10867
+
10868
+ </div>
10869
+
10870
+ </div>
10871
+
10872
+ <div class="doc doc-object doc-function">
10873
+
10874
+
10838
10875
  <h3 id="nautobot.apps.jobs.BaseJob.file_path" class="doc doc-heading">
10839
10876
  <code class="highlight language-python"><span class="n">file_path</span><span class="p">()</span></code>
10840
10877
 
@@ -10914,11 +10951,12 @@ path would consider this a failure of the job execution, as described in <code>n
10914
10951
  <code>exc</code>
10915
10952
  </td>
10916
10953
  <td>
10917
- <code>Exception</code>
10954
+ <code>Any</code>
10918
10955
  </td>
10919
10956
  <td>
10920
10957
  <div class="doc-md-description">
10921
- <p>The exception raised by the task.</p>
10958
+ <p>Exception raised by the task (if any) <strong>or</strong> return value from the task, if it failed cleanly,
10959
+ such as if the Job called <code>self.fail()</code> rather than raising an exception.</p>
10922
10960
  </div>
10923
10961
  </td>
10924
10962
  <td>
@@ -10982,7 +11020,7 @@ path would consider this a failure of the job execution, as described in <code>n
10982
11020
  </td>
10983
11021
  <td>
10984
11022
  <div class="doc-md-description">
10985
- <p>Exception information.</p>
11023
+ <p>Exception information, or None.</p>
10986
11024
  </div>
10987
11025
  </td>
10988
11026
  <td>
@@ -8238,6 +8238,15 @@
8238
8238
  </span>
8239
8239
  </a>
8240
8240
 
8241
+ </li>
8242
+
8243
+ <li class="md-nav__item">
8244
+ <a href="#nautobot.apps.testing.NautobotTestCaseMixin.assertJobResultStatus" class="md-nav__link">
8245
+ <span class="md-ellipsis">
8246
+ assertJobResultStatus
8247
+ </span>
8248
+ </a>
8249
+
8241
8250
  </li>
8242
8251
 
8243
8252
  <li class="md-nav__item">
@@ -11423,6 +11432,15 @@
11423
11432
  </span>
11424
11433
  </a>
11425
11434
 
11435
+ </li>
11436
+
11437
+ <li class="md-nav__item">
11438
+ <a href="#nautobot.apps.testing.NautobotTestCaseMixin.assertJobResultStatus" class="md-nav__link">
11439
+ <span class="md-ellipsis">
11440
+ assertJobResultStatus
11441
+ </span>
11442
+ </a>
11443
+
11426
11444
  </li>
11427
11445
 
11428
11446
  <li class="md-nav__item">
@@ -14322,6 +14340,23 @@ in the dictionary.</p>
14322
14340
  <div class="doc doc-object doc-function">
14323
14341
 
14324
14342
 
14343
+ <h3 id="nautobot.apps.testing.NautobotTestCaseMixin.assertJobResultStatus" class="doc doc-heading">
14344
+ <code class="highlight language-python"><span class="n">assertJobResultStatus</span><span class="p">(</span><span class="n">job_result</span><span class="p">,</span> <span class="n">expected_status</span><span class="o">=</span><span class="n">JobResultStatusChoices</span><span class="o">.</span><span class="n">STATUS_SUCCESS</span><span class="p">)</span></code>
14345
+
14346
+ <a href="#nautobot.apps.testing.NautobotTestCaseMixin.assertJobResultStatus" class="headerlink" title="Permanent link">&para;</a></h3>
14347
+
14348
+
14349
+ <div class="doc doc-contents ">
14350
+
14351
+ <p>Assert that the given job_result has the expected_status, or print the job logs to aid in debugging.</p>
14352
+
14353
+ </div>
14354
+
14355
+ </div>
14356
+
14357
+ <div class="doc doc-object doc-function">
14358
+
14359
+
14325
14360
  <h3 id="nautobot.apps.testing.NautobotTestCaseMixin.assertQuerysetEqualAndNotEmpty" class="doc doc-heading">
14326
14361
  <code class="highlight language-python"><span class="n">assertQuerysetEqualAndNotEmpty</span><span class="p">(</span><span class="n">qs</span><span class="p">,</span> <span class="n">values</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span></code>
14327
14362
 
@@ -9943,6 +9943,10 @@
9943
9943
  <td><a href="#dryrun_default">metadata property</a></td>
9944
9944
  </tr>
9945
9945
  <tr>
9946
+ <td><code>fail</code></td>
9947
+ <td><a href="#marking-a-job-as-failed">helper method</a></td>
9948
+ </tr>
9949
+ <tr>
9946
9950
  <td><code>file_path</code></td>
9947
9951
  <td>deprecated class property</td>
9948
9952
  </tr>
@@ -10367,7 +10371,7 @@ Another example of using the nested reference would be to access <a href="../../
10367
10371
  </details>
10368
10372
  <h4 id="the-before_start-method">The <code>before_start()</code> Method<a class="headerlink" href="#the-before_start-method" title="Permanent link">&para;</a></h4>
10369
10373
  <p>The <code>before_start()</code> method may optionally be implemented to perform any appropriate Job-specific setup before the <code>run()</code> method is called. It has the signature <code>before_start(self, task_id, args, kwargs)</code> for historical reasons; the <code>task_id</code> parameter will always be identical to <code>self.request.id</code>, the <code>args</code> parameter will generally be empty, and any user-specified variables passed into the Job execution will be present in the <code>kwargs</code> parameter.</p>
10370
- <p>The return value from <code>before_start()</code> is ignored, but if it raises any exception, the Job execution will be marked as a failure and <code>run()</code> will not be called.</p>
10374
+ <p>The return value from <code>before_start()</code> is ignored, but if it raises any exception, the Job result status will be marked as <code>FAILURE</code> and <code>run()</code> will not be called.</p>
10371
10375
  <h4 id="the-run-method">The <code>run()</code> Method<a class="headerlink" href="#the-run-method" title="Permanent link">&para;</a></h4>
10372
10376
  <p>The <code>run()</code> method is the primary worker of any Job, and must be implemented. After the <code>self</code> argument, it should accept keyword arguments for any variables defined on the Job:</p>
10373
10377
  <div class="highlight"><pre><span></span><code><a id="__codelineno-18-1" name="__codelineno-18-1" href="#__codelineno-18-1"></a><span class="kn">from</span> <span class="nn">nautobot.apps.jobs</span> <span class="kn">import</span> <span class="n">Job</span><span class="p">,</span> <span class="n">StringVar</span><span class="p">,</span> <span class="n">IntegerVar</span><span class="p">,</span> <span class="n">ObjectVar</span>
@@ -10389,13 +10393,13 @@ Another example of using the nested reference would be to access <a href="../../
10389
10393
  <p class="admonition-title">Be cautious around bulk operations</p>
10390
10394
  <p>The Django ORM provides methods to create/edit many objects at once, namely <code>bulk_create()</code> and <code>update()</code>. These are best avoided in most cases as they bypass a model's built-in validation and can easily lead to database corruption if not used carefully.</p>
10391
10395
  </div>
10392
- <p>If <code>run()</code> returns any value (even the implicit <code>None</code>), the Job execution will be marked as a success and the returned value will be stored in the associated JobResult database record. Conversely, if <code>run()</code> raises any exception, the Job execution will be marked as a failure and the traceback will be stored in the JobResult.</p>
10396
+ <p>If <code>run()</code> returns any value (even the implicit <code>None</code>), the returned value will be stored in the associated JobResult database record. (The Job Result will be marked as <code>SUCCESS</code> unless the <code>fail()</code> method was called at some point during <code>run()</code>, in which case it will be marked as <code>FAILURE</code>.) Conversely, if <code>run()</code> raises any exception, the Job execution will be marked as <code>FAILURE</code> and the traceback will be stored in the JobResult.</p>
10393
10397
  <h4 id="the-on_success-method">The <code>on_success()</code> Method<a class="headerlink" href="#the-on_success-method" title="Permanent link">&para;</a></h4>
10394
10398
  <p>If both <code>before_start()</code> and <code>run()</code> are successful, the <code>on_success()</code> method will be called next, if implemented. It has the signature <code>on_success(self, retval, task_id, args, kwargs)</code>; as with <code>before_start()</code> the <code>task_id</code> and <code>args</code> parameters can generally be ignored, while <code>retval</code> is the return value from <code>run()</code>, and <code>kwargs</code> will contain the user-specified variables passed into the Job execution.</p>
10395
10399
  <h4 id="the-on_failure-method">The <code>on_failure()</code> Method<a class="headerlink" href="#the-on_failure-method" title="Permanent link">&para;</a></h4>
10396
- <p>If either <code>before_start()</code> or <code>run()</code> raises any unhandled exception, the <code>on_failure()</code> method will be called next, if implemented. It has the signature <code>on_failure(self, exc, task_id, args, kwargs, einfo)</code>; of these parameters, the <code>exc</code> will contain the exception that was raised, and <code>kwargs</code> will contain the user-specified variables passed into the Job.</p>
10400
+ <p>If either <code>before_start()</code> or <code>run()</code> raises any unhandled exception, or reports a failure overall through <code>fail()</code>, the <code>on_failure()</code> method will be called next, if implemented. It has the signature <code>on_failure(self, exc, task_id, args, kwargs, einfo)</code>; of these parameters, the <code>exc</code> will contain the exception that was raised (if any) or the return value from <code>before_start()</code> or <code>run()</code> (otherwise), and <code>kwargs</code> will contain the user-specified variables passed into the Job.</p>
10397
10401
  <h4 id="the-after_return-method">The <code>after_return()</code> Method<a class="headerlink" href="#the-after_return-method" title="Permanent link">&para;</a></h4>
10398
- <p>Regardless of the overall Job execution success or failure, the <code>after_return()</code> method will be called after <code>on_success()</code> or <code>on_failure()</code>. It has the signature <code>after_return(self, status, retval, task_id, args, kwargs, einfo)</code>; the <code>status</code> will indicate success or failure (using the <code>JobResultStatusChoices</code> enum), <code>retval</code> is <em>either</em> the return value from <code>run()</code> (on success) or the exception raised (on failure), and once again <code>kwargs</code> contains the user variables.</p>
10402
+ <p>Regardless of the overall Job execution success or failure, the <code>after_return()</code> method will be called after <code>on_success()</code> or <code>on_failure()</code>. It has the signature <code>after_return(self, status, retval, task_id, args, kwargs, einfo)</code>; the <code>status</code> will indicate success or failure (using the <code>JobResultStatusChoices</code> enum), <code>retval</code> is <em>either</em> the return value from <code>run()</code> or the exception raised, and once again <code>kwargs</code> contains the user variables.</p>
10399
10403
  <h3 id="logging">Logging<a class="headerlink" href="#logging" title="Permanent link">&para;</a></h3>
10400
10404
  <details class="version-changed">
10401
10405
  <summary>Changed in version 2.0.0</summary>
@@ -10416,7 +10420,7 @@ Another example of using the nested reference would be to access <a href="../../
10416
10420
  <a id="__codelineno-19-2" name="__codelineno-19-2" href="#__codelineno-19-2"></a>
10417
10421
  <a id="__codelineno-19-3" name="__codelineno-19-3" href="#__codelineno-19-3"></a><span class="k">class</span> <span class="nc">MyJob</span><span class="p">(</span><span class="n">Job</span><span class="p">):</span>
10418
10422
  <a id="__codelineno-19-4" name="__codelineno-19-4" href="#__codelineno-19-4"></a> <span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
10419
- <a id="__codelineno-19-5" name="__codelineno-19-5" href="#__codelineno-19-5"></a> <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&quot;This job is running!&quot;</span><span class="p">,</span> <span class="n">extra</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;grouping&quot;</span><span class="p">:</span> <span class="s2">&quot;myjobisrunning&quot;</span><span class="p">,</span> <span class="s2">&quot;object&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">job_result</span><span class="p">})</span>
10423
+ <a id="__codelineno-19-5" name="__codelineno-19-5" href="#__codelineno-19-5"></a> <span class="bp">self</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&quot;This job is running!&quot;</span><span class="p">,</span> <span class="n">extra</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;grouping&quot;</span><span class="p">:</span> <span class="s2">&quot;myjobisrunning&quot;</span><span class="p">,</span> <span class="s2">&quot;object&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">job_result</span><span class="p">})</span>
10420
10424
  </code></pre></div>
10421
10425
  </div>
10422
10426
  <p>To skip writing a log entry to the database, set the <code>skip_db_logging</code> key in the "extra" kwarg to <code>True</code> when calling the log function. The output will still be written to the console.</p>
@@ -10427,7 +10431,7 @@ Another example of using the nested reference would be to access <a href="../../
10427
10431
  <a id="__codelineno-20-2" name="__codelineno-20-2" href="#__codelineno-20-2"></a>
10428
10432
  <a id="__codelineno-20-3" name="__codelineno-20-3" href="#__codelineno-20-3"></a><span class="k">class</span> <span class="nc">MyJob</span><span class="p">(</span><span class="n">Job</span><span class="p">):</span>
10429
10433
  <a id="__codelineno-20-4" name="__codelineno-20-4" href="#__codelineno-20-4"></a> <span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
10430
- <a id="__codelineno-20-5" name="__codelineno-20-5" href="#__codelineno-20-5"></a> <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&quot;This job is running!&quot;</span><span class="p">,</span> <span class="n">extra</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;skip_db_logging&quot;</span><span class="p">:</span> <span class="kc">True</span><span class="p">})</span>
10434
+ <a id="__codelineno-20-5" name="__codelineno-20-5" href="#__codelineno-20-5"></a> <span class="bp">self</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&quot;This job is running!&quot;</span><span class="p">,</span> <span class="n">extra</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;skip_db_logging&quot;</span><span class="p">:</span> <span class="kc">True</span><span class="p">})</span>
10431
10435
  </code></pre></div>
10432
10436
  </div>
10433
10437
  <p>Markdown rendering is supported for log messages, as well as <a href="../../user-guide/platform-functionality/template-filters.html#render_markdown">a limited subset of HTML</a>.</p>
@@ -10444,8 +10448,12 @@ Another example of using the nested reference would be to access <a href="../../
10444
10448
  <p>The <code>AbortTransaction</code> class was moved from the <code>nautobot.utilities.exceptions</code> module to <code>nautobot.core.exceptions</code>. Jobs should generally import it from <code>nautobot.apps.exceptions</code> if needed.</p>
10445
10449
  </details>
10446
10450
  <details class="version-added">
10447
- <summary>Added in version 2.4.0</summary>
10448
- <p>You can now use <code>self.logger.success()</code> to set the log level to <code>SUCCESS</code>.</p>
10451
+ <summary>Added in version 2.4.0 — <code>logger.success()</code> added</summary>
10452
+ <p>You can now use <code>self.logger.success()</code> to log a message at the level <code>SUCCESS</code>, which is located between the standard <code>INFO</code> and <code>WARNING</code> log levels.</p>
10453
+ </details>
10454
+ <details class="version-added">
10455
+ <summary>Added in version 2.4.5 — <code>logger.failure()</code> added</summary>
10456
+ <p>You can now use <code>self.logger.failure()</code> to log a message at the level <code>FAILURE</code>, which is located between the standard <code>WARNING</code> and <code>ERROR</code> log levels.</p>
10449
10457
  </details>
10450
10458
  <h3 id="file-output">File Output<a class="headerlink" href="#file-output" title="Permanent link">&para;</a></h3>
10451
10459
  <details class="version-added">
@@ -10462,17 +10470,22 @@ Another example of using the nested reference would be to access <a href="../../
10462
10470
  <p>The above Job when run will create two files, "greeting.txt" and "farewell.txt", that will be made available for download from the JobResult detail view's "Advanced" tab and via the REST API. These files will persist indefinitely, but can automatically be deleted if the JobResult itself is deleted; they can also be deleted manually by an administrator via the "File Proxies" link in the Admin UI.</p>
10463
10471
  <p>The maximum size of any single created file (or in other words, the maximum number of bytes that can be passed to <code>self.create_file()</code>) is controlled by the <a href="../../user-guide/administration/configuration/settings.html#job_create_file_max_size"><code>JOB_CREATE_FILE_MAX_SIZE</code></a> system setting. A <code>ValueError</code> exception will be raised if <code>create_file()</code> is called with an overly large <code>content</code> value.</p>
10464
10472
  <h3 id="marking-a-job-as-failed">Marking a Job as Failed<a class="headerlink" href="#marking-a-job-as-failed" title="Permanent link">&para;</a></h3>
10465
- <p>To mark a Job as failed, raise an exception from within the <code>run()</code> method. The exception message will be logged to the traceback of the Job Result. The Job Result status will be set to <code>failed</code>. To output a Job log message you can use the <code>self.logger.error()</code> method.</p>
10466
- <p>As an example, the following Job will fail if the user does not put the word "Taco" in <code>var1</code>:</p>
10473
+ <p>Any uncaught exception raised from within the <code>run()</code> method will abort the <code>run()</code> method immediately (as usual in Python), and will result in the Job Result status being marked as <code>FAILURE</code>. The exception message and traceback will be recorded in the Job Result.</p>
10474
+ <p>Alternatively, in Nautobot v2.4.5 and later, you can more "cleanly" fail a Job by calling <code>self.fail(...)</code> and then either returning immediately from the <code>run()</code> method or continuing with the execution of the Job, as desired. In this case, after the <code>run()</code> method completes, the Job Result status will be automatically marked as <code>FAILURE</code> and no exception or traceback will be recorded.</p>
10475
+ <p>As an example, the following Job will abort if the user does not put the word "Taco" in <code>occasion</code>, and fail (but not abort) if the variable does not additionally contain "Tuesday":</p>
10467
10476
  <div class="highlight"><pre><span></span><code><a id="__codelineno-22-1" name="__codelineno-22-1" href="#__codelineno-22-1"></a><span class="kn">from</span> <span class="nn">nautobot.apps.jobs</span> <span class="kn">import</span> <span class="n">Job</span><span class="p">,</span> <span class="n">StringVar</span>
10468
10477
  <a id="__codelineno-22-2" name="__codelineno-22-2" href="#__codelineno-22-2"></a>
10469
10478
  <a id="__codelineno-22-3" name="__codelineno-22-3" href="#__codelineno-22-3"></a><span class="k">class</span> <span class="nc">MyJob</span><span class="p">(</span><span class="n">Job</span><span class="p">):</span>
10470
- <a id="__codelineno-22-4" name="__codelineno-22-4" href="#__codelineno-22-4"></a> <span class="n">var1</span> <span class="o">=</span> <span class="n">StringVar</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
10479
+ <a id="__codelineno-22-4" name="__codelineno-22-4" href="#__codelineno-22-4"></a> <span class="n">occasion</span> <span class="o">=</span> <span class="n">StringVar</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
10471
10480
  <a id="__codelineno-22-5" name="__codelineno-22-5" href="#__codelineno-22-5"></a>
10472
- <a id="__codelineno-22-6" name="__codelineno-22-6" href="#__codelineno-22-6"></a> <span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">var1</span><span class="p">):</span>
10473
- <a id="__codelineno-22-7" name="__codelineno-22-7" href="#__codelineno-22-7"></a> <span class="k">if</span> <span class="n">var1</span> <span class="o">!=</span> <span class="s2">&quot;Taco&quot;</span><span class="p">:</span>
10474
- <a id="__codelineno-22-8" name="__codelineno-22-8" href="#__codelineno-22-8"></a> <span class="bp">self</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="s2">&quot;var1 must be &#39;Taco&#39;&quot;</span><span class="p">)</span>
10481
+ <a id="__codelineno-22-6" name="__codelineno-22-6" href="#__codelineno-22-6"></a> <span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">occasion</span><span class="p">):</span>
10482
+ <a id="__codelineno-22-7" name="__codelineno-22-7" href="#__codelineno-22-7"></a> <span class="k">if</span> <span class="ow">not</span> <span class="n">occasion</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">&quot;Taco&quot;</span><span class="p">):</span>
10483
+ <a id="__codelineno-22-8" name="__codelineno-22-8" href="#__codelineno-22-8"></a> <span class="bp">self</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">failure</span><span class="p">(</span><span class="s2">&quot;The occasion must begin with &#39;Taco&#39;&quot;</span><span class="p">)</span>
10475
10484
  <a id="__codelineno-22-9" name="__codelineno-22-9" href="#__codelineno-22-9"></a> <span class="k">raise</span> <span class="ne">Exception</span><span class="p">(</span><span class="s2">&quot;Argument input validation failed.&quot;</span><span class="p">)</span>
10485
+ <a id="__codelineno-22-10" name="__codelineno-22-10" href="#__codelineno-22-10"></a> <span class="k">if</span> <span class="ow">not</span> <span class="n">occasion</span><span class="o">.</span><span class="n">endswith</span><span class="p">(</span><span class="s2">&quot; Tuesday&quot;</span><span class="p">):</span>
10486
+ <a id="__codelineno-22-11" name="__codelineno-22-11" href="#__codelineno-22-11"></a> <span class="bp">self</span><span class="o">.</span><span class="n">fail</span><span class="p">(</span><span class="s2">&quot;It&#39;s supposed to be Tuesday&quot;</span><span class="p">)</span>
10487
+ <a id="__codelineno-22-12" name="__codelineno-22-12" href="#__codelineno-22-12"></a> <span class="bp">self</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&quot;Today is </span><span class="si">%s</span><span class="s2">&quot;</span><span class="p">,</span> <span class="n">occasion</span><span class="p">)</span>
10488
+ <a id="__codelineno-22-13" name="__codelineno-22-13" href="#__codelineno-22-13"></a> <span class="k">return</span> <span class="n">occasion</span>
10476
10489
  </code></pre></div>
10477
10490
  <h3 id="accessing-user-and-job-result">Accessing User and Job Result<a class="headerlink" href="#accessing-user-and-job-result" title="Permanent link">&para;</a></h3>
10478
10491
  <details class="version-changed">
Binary file