nautobot 2.4.3__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 (198) hide show
  1. nautobot/__init__.py +19 -3
  2. nautobot/apps/filters.py +2 -0
  3. nautobot/circuits/filters.py +1 -1
  4. nautobot/circuits/tests/test_models.py +5 -3
  5. nautobot/cloud/filters.py +3 -6
  6. nautobot/cloud/tests/test_filters.py +21 -0
  7. nautobot/core/admin.py +2 -0
  8. nautobot/core/celery/__init__.py +5 -3
  9. nautobot/core/jobs/__init__.py +5 -3
  10. nautobot/core/management/commands/generate_performance_test_endpoints.py +9 -6
  11. nautobot/core/models/utils.py +6 -1
  12. nautobot/core/templates/inc/javascript.html +1 -0
  13. nautobot/core/templatetags/ui_framework.py +20 -4
  14. nautobot/core/testing/__init__.py +2 -0
  15. nautobot/core/testing/forms.py +1 -1
  16. nautobot/core/testing/mixins.py +9 -0
  17. nautobot/core/tests/test_api.py +1 -1
  18. nautobot/core/tests/test_graphql.py +3 -3
  19. nautobot/core/tests/test_jobs.py +30 -28
  20. nautobot/core/ui/object_detail.py +1 -1
  21. nautobot/dcim/api/serializers.py +36 -0
  22. nautobot/dcim/api/views.py +1 -1
  23. nautobot/dcim/elevations.py +17 -4
  24. nautobot/dcim/factory.py +9 -1
  25. nautobot/dcim/filters/__init__.py +27 -1
  26. nautobot/dcim/forms.py +13 -1
  27. nautobot/dcim/models/devices.py +11 -5
  28. nautobot/dcim/signals.py +26 -0
  29. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -62
  30. nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +6 -0
  31. nautobot/dcim/tests/test_api.py +176 -0
  32. nautobot/dcim/tests/test_filters.py +56 -3
  33. nautobot/dcim/tests/test_jobs.py +4 -6
  34. nautobot/dcim/tests/test_models.py +40 -0
  35. nautobot/dcim/views.py +24 -14
  36. nautobot/extras/api/mixins.py +1 -1
  37. nautobot/extras/api/views.py +2 -2
  38. nautobot/extras/choices.py +8 -3
  39. nautobot/extras/filters/__init__.py +4 -0
  40. nautobot/extras/jobs.py +181 -103
  41. nautobot/extras/management/utils.py +13 -2
  42. nautobot/extras/models/datasources.py +11 -4
  43. nautobot/extras/models/jobs.py +20 -17
  44. nautobot/extras/plugins/__init__.py +26 -1
  45. nautobot/extras/tables.py +25 -29
  46. nautobot/extras/templates/extras/inc/jobresult.html +12 -13
  47. nautobot/extras/templates/extras/objectchange.html +28 -12
  48. nautobot/extras/test_jobs/atomic_transaction.py +6 -6
  49. nautobot/extras/test_jobs/fail.py +75 -1
  50. nautobot/extras/tests/test_api.py +17 -16
  51. nautobot/extras/tests/test_datasources.py +64 -54
  52. nautobot/extras/tests/test_filters.py +2 -0
  53. nautobot/extras/tests/test_jobs.py +69 -62
  54. nautobot/extras/tests/test_models.py +1 -1
  55. nautobot/extras/tests/test_plugins.py +32 -1
  56. nautobot/extras/tests/test_relationships.py +5 -5
  57. nautobot/extras/tests/test_views.py +12 -2
  58. nautobot/extras/views.py +10 -1
  59. nautobot/ipam/api/serializers.py +7 -8
  60. nautobot/ipam/api/views.py +2 -2
  61. nautobot/ipam/factory.py +27 -8
  62. nautobot/ipam/filters.py +67 -29
  63. nautobot/ipam/formfields.py +51 -0
  64. nautobot/ipam/forms.py +28 -1
  65. nautobot/ipam/migrations/0051_added_optional_vrf_relationship_to_vdc.py +41 -0
  66. nautobot/ipam/models.py +63 -5
  67. nautobot/ipam/querysets.py +6 -0
  68. nautobot/ipam/tables.py +21 -7
  69. nautobot/ipam/templates/ipam/rir.html +1 -43
  70. nautobot/ipam/tests/test_api.py +107 -66
  71. nautobot/ipam/tests/test_filters.py +145 -5
  72. nautobot/ipam/tests/test_models.py +16 -0
  73. nautobot/ipam/tests/test_views.py +15 -2
  74. nautobot/ipam/urls.py +1 -21
  75. nautobot/ipam/views.py +24 -41
  76. nautobot/project-static/css/base.css +11 -0
  77. nautobot/project-static/css/dark.css +2 -1
  78. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +62 -0
  79. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +43 -5
  80. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +35 -0
  81. nautobot/project-static/docs/development/apps/api/configuration-view.html +0 -3
  82. nautobot/project-static/docs/development/apps/api/models/graphql.html +0 -4
  83. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +94 -1
  84. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +0 -3
  85. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +0 -3
  86. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +0 -3
  87. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +0 -3
  88. nautobot/project-static/docs/development/apps/api/prometheus.html +0 -3
  89. nautobot/project-static/docs/development/apps/api/testing.html +0 -6
  90. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +0 -3
  91. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +0 -3
  92. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +0 -3
  93. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +0 -3
  94. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +1 -7
  95. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +0 -7
  96. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +0 -4
  97. nautobot/project-static/docs/development/apps/api/views/notes.html +0 -3
  98. nautobot/project-static/docs/development/apps/index.html +2 -35
  99. nautobot/project-static/docs/development/apps/migration/code-updates.html +1 -1
  100. nautobot/project-static/docs/development/core/application-registry.html +0 -6
  101. nautobot/project-static/docs/development/core/best-practices.html +0 -27
  102. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +58 -4
  103. nautobot/project-static/docs/development/core/getting-started.html +12 -16
  104. nautobot/project-static/docs/development/core/homepage.html +0 -3
  105. nautobot/project-static/docs/development/core/style-guide.html +0 -5
  106. nautobot/project-static/docs/development/core/templates.html +0 -3
  107. nautobot/project-static/docs/development/core/testing.html +0 -9
  108. nautobot/project-static/docs/development/jobs/index.html +30 -43
  109. nautobot/project-static/docs/objects.inv +0 -0
  110. nautobot/project-static/docs/overview/application_stack.html +0 -18
  111. nautobot/project-static/docs/release-notes/version-2.4.html +374 -0
  112. nautobot/project-static/docs/requirements.txt +2 -2
  113. nautobot/project-static/docs/search/search_index.json +1 -1
  114. nautobot/project-static/docs/sitemap.xml +290 -290
  115. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  116. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +0 -10
  117. nautobot/project-static/docs/user-guide/administration/guides/docker.html +0 -15
  118. nautobot/project-static/docs/user-guide/administration/installation/index.html +0 -16
  119. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +1 -4
  120. nautobot/project-static/docs/user-guide/administration/installation/services.html +0 -11
  121. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +3 -3
  122. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +5 -35
  123. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/tables/v2-code-location-changes.yaml +1 -1
  124. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +1 -1
  125. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +0 -4
  126. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +0 -3
  127. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +0 -4
  128. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +0 -4
  129. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +0 -4
  130. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +0 -4
  131. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +0 -4
  132. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +0 -4
  133. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +0 -3
  134. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +0 -4
  135. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +0 -4
  136. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +0 -4
  137. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +1 -17
  138. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +0 -3
  139. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +0 -4
  140. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +0 -4
  141. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +0 -3
  142. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +1 -7
  143. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +0 -4
  144. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +0 -4
  145. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +0 -4
  146. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +0 -4
  147. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +0 -4
  148. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +0 -4
  149. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +0 -4
  150. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +0 -6
  151. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +0 -3
  152. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +0 -4
  153. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +0 -4
  154. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +0 -8
  155. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +3 -3
  156. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +0 -6
  157. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +0 -3
  158. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +3 -15
  159. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +0 -26
  160. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +0 -8
  161. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +0 -3
  162. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +0 -8
  163. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +0 -7
  164. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +0 -3
  165. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +0 -3
  166. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +0 -14
  167. nautobot/project-static/docs/user-guide/platform-functionality/note.html +0 -3
  168. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +1 -10
  169. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +0 -3
  170. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +0 -14
  171. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +0 -19
  172. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +3 -9
  173. nautobot/project-static/docs/user-guide/platform-functionality/status.html +0 -8
  174. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +0 -4
  175. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +1 -13
  176. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +0 -5
  177. nautobot/project-static/js/editor.js +292 -0
  178. nautobot/project-static/monaco-editor-0.52.2/README.md +81 -0
  179. nautobot/project-static/monaco-editor-0.52.2/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  180. nautobot/project-static/monaco-editor-0.52.2/vs/base/worker/workerMain.js +31 -0
  181. nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/xml/xml.js +10 -0
  182. nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/yaml/yaml.js +10 -0
  183. nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.css +8 -0
  184. nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.js +798 -0
  185. nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonMode.js +19 -0
  186. nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonWorker.js +42 -0
  187. nautobot/project-static/monaco-editor-0.52.2/vs/loader.js +11 -0
  188. nautobot/tenancy/filters/__init__.py +3 -5
  189. nautobot/tenancy/tests/test_filters.py +10 -0
  190. nautobot/virtualization/views.py +0 -1
  191. nautobot/wireless/tables.py +9 -4
  192. nautobot/wireless/tests/test_api.py +0 -9
  193. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/METADATA +4 -4
  194. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/RECORD +198 -186
  195. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/LICENSE.txt +0 -0
  196. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/NOTICE +0 -0
  197. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/WHEEL +0 -0
  198. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/entry_points.txt +0 -0
@@ -12,45 +12,43 @@
12
12
  <strong>Summary of Results</strong>
13
13
  </div>
14
14
  <table class="table table-hover panel-body">
15
- {% if result.job_model is not None %}
16
15
  <tr>
17
16
  <td>Job Description</td>
18
- <td>{{ result.job_model.description | render_markdown }}</td>
17
+ <td>{{ result.job_model.description | render_markdown | placeholder }}</td>
19
18
  </tr>
20
- {% endif %}
21
19
  <tr>
22
20
  <td>Status</td>
23
21
  <td><span id="pending-result-label">{% include 'extras/inc/job_label.html' with result=result %}</span></td>
24
22
  </tr>
25
23
  <tr>
26
24
  <td>Started at</td>
27
- <td>{{ result.date_created }}</td>
25
+ <td>{{ result.date_created | placeholder }}</td>
28
26
  </tr>
29
27
  <tr>
30
28
  <td>User</td>
31
- <td>{{ result.user }}</td>
29
+ <td>{{ result.user | placeholder }}</td>
32
30
  </tr>
33
31
  <tr>
34
32
  <td>Duration</td>
35
33
  <td>
36
- {% if result.date_done %}
37
- {{ result.duration }}
38
- {% else %}
34
+ {% if result.date_created and not result.date_done %}
39
35
  <img src="{% static 'img/ajax-loader.gif' %}">
36
+ {% else %}
37
+ {{ result.duration | placeholder}}
40
38
  {% endif %}
41
39
  </td>
42
40
  </tr>
43
41
  <tr>
44
42
  <td>Return Value</td>
45
43
  <td>
46
- {% if result.date_done %}
44
+ {% if result.date_created and not result.date_done %}
45
+ <img src="{% static 'img/ajax-loader.gif' %}">
46
+ {% else %}
47
47
  {% if result.result %}
48
48
  <pre>{{ result.result | render_json }}</pre>
49
49
  {% else %}
50
50
  {{ result.result | placeholder }}
51
51
  {% endif %}
52
- {% else %}
53
- <img src="{% static 'img/ajax-loader.gif' %}">
54
52
  {% endif %}
55
53
  </td>
56
54
  </tr>
@@ -89,6 +87,7 @@
89
87
  <input class="form-control" id="log-filter" type="text" placeholder="Filter log level or message" title="Filter log level or message" style="height: 23px" />
90
88
  </div>
91
89
  </div>
92
- {% ajax_table "log_table" "extras:jobresult_log-table" pk=result.pk %}
90
+ {% if result and result.pk %}
91
+ {% ajax_table "log_table" "extras:jobresult_log-table" pk=result.pk %}
92
+ {% endif %}
93
93
  </div>
94
-
@@ -1,5 +1,6 @@
1
1
  {% extends 'generic/object_retrieve.html' %}
2
2
  {% load helpers %}
3
+ {% load static %}
3
4
 
4
5
  {% block title %}{{ object }}{% endblock %}
5
6
 
@@ -95,6 +96,20 @@
95
96
  </tr>
96
97
  </table>
97
98
  </div>
99
+ <div class="panel panel-default">
100
+ <div class="panel-heading">
101
+ <strong>Object Data</strong>
102
+ </div>
103
+ <div class="panel-body">
104
+ <div class="editor-container"
105
+ data-lang="json"
106
+ data-value="{{ object.object_data|render_json:False }}"
107
+ style="max-height: 300px">
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ <div class="col-md-7">
98
113
  <div class="panel panel-default">
99
114
  <div class="panel-heading">
100
115
  <strong>Difference</strong>
@@ -119,22 +134,17 @@
119
134
  {% endif %}
120
135
  </span>
121
136
  {% else %}
122
- <pre class="diff-removed">{{ diff_removed|render_json }}</pre>
123
- <pre class="diff-added">{{ diff_added|render_json }}</pre>
137
+ <div class="editor-container"
138
+ data-mode="diff"
139
+ data-original="{{ diff_removed | render_json:False }}"
140
+ data-modified="{{ diff_added | render_json:False }}"
141
+ data-lang="json"
142
+ style="max-height: 730px">
143
+ </div>
124
144
  {% endif %}
125
145
  </div>
126
146
  </div>
127
147
  </div>
128
- <div class="col-md-7">
129
- <div class="panel panel-default">
130
- <div class="panel-heading">
131
- <strong>Object Data</strong>
132
- </div>
133
- <div class="panel-body">
134
- <pre>{{ object.object_data|render_json }}</pre>
135
- </div>
136
- </div>
137
- </div>
138
148
  </div>
139
149
  <div class="row">
140
150
  <div class="col-md-12">
@@ -147,3 +157,9 @@
147
157
  </div>
148
158
  </div>
149
159
  {% endblock %}
160
+
161
+ {% block javascript %}
162
+ {{ block.super }}
163
+ <script src="{% static 'js/editor.js' %}"></script>
164
+ {% endblock %}
165
+
@@ -16,13 +16,13 @@ class TestAtomicDecorator(Job):
16
16
  Job that uses @transaction.atomic decorator to roll back changes.
17
17
  """
18
18
 
19
- fail = BooleanVar()
19
+ should_fail = BooleanVar()
20
20
 
21
21
  @transaction.atomic
22
- def run(self, fail=False): # pylint:disable=arguments-differ
22
+ def run(self, should_fail=False): # pylint:disable=arguments-differ
23
23
  try:
24
24
  Status.objects.create(name="Test database atomic rollback 1")
25
- if fail:
25
+ if should_fail:
26
26
  raise SimulatedError("simulated failure")
27
27
  except Exception:
28
28
  logger.error("Job failed, all database changes have been rolled back.")
@@ -35,13 +35,13 @@ class TestAtomicContextManager(Job):
35
35
  Job that uses `with transaction.atomic()` context manager to roll back changes.
36
36
  """
37
37
 
38
- fail = BooleanVar()
38
+ should_fail = BooleanVar()
39
39
 
40
- def run(self, fail=False): # pylint:disable=arguments-differ
40
+ def run(self, should_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")
44
- if fail:
44
+ if should_fail:
45
45
  raise SimulatedError("simulated failure")
46
46
  except Exception as err:
47
47
  logger.error("Job failed, all database changes have been rolled back.")
@@ -62,6 +62,20 @@ class TestFailJob(Job):
62
62
  logger.info("after_return() was called as expected")
63
63
 
64
64
 
65
+ class TestFailInBeforeStart(TestFailJob):
66
+ """
67
+ Job that raises an exception in before_start().
68
+ """
69
+
70
+ def before_start(self, task_id, args, kwargs):
71
+ super().before_start(task_id, args, kwargs)
72
+ logger.info("I'm a test job that fails!")
73
+ raise RunJobTaskFailed("Setup failure")
74
+
75
+ def run(self):
76
+ raise RuntimeError("run() was unexpectedly called after a failure in before_start()")
77
+
78
+
65
79
  class TestFailWithSanitization(Job):
66
80
  """
67
81
  Job with fail result that should be sanitized.
@@ -91,4 +105,64 @@ class TestFailWithSanitization(Job):
91
105
  raise exc
92
106
 
93
107
 
94
- register_jobs(TestFailJob, TestFailWithSanitization)
108
+ class TestFailCleanly(TestFailJob):
109
+ """
110
+ Job that fails "cleanly" through self.fail() instead of raising an exception.
111
+ """
112
+
113
+ def run(self): # pylint: disable=arguments-differ
114
+ logger.info("I'm a test job that fails!")
115
+ self.fail("Failure")
116
+ return "We failed"
117
+
118
+ def on_failure(self, exc, task_id, args, kwargs, einfo):
119
+ if exc != "We failed":
120
+ raise RuntimeError(f"Expected exc to be the message returned from run(), but it was {exc!r}")
121
+ if task_id != self.request.id: # pylint: disable=no-member
122
+ raise RuntimeError(f"Expected task_id {task_id} to equal self.request.id {self.request.id}") # pylint: disable=no-member
123
+ if args:
124
+ raise RuntimeError(f"Expected args to be empty, but it was {args!r}")
125
+ if kwargs:
126
+ raise RuntimeError(f"Expected kwargs to be empty, but it was {kwargs!r}")
127
+ if einfo is not None:
128
+ raise RuntimeError(f"Expected einfo to be None, but it was {einfo!r}")
129
+ logger.info("on_failure() was called as expected")
130
+
131
+ def after_return(self, status, retval, task_id, args, kwargs, einfo):
132
+ if status is not JobResultStatusChoices.STATUS_FAILURE:
133
+ raise RuntimeError(f"Expected status to be {JobResultStatusChoices.STATUS_FAILURE}, but it was {status!r}")
134
+ if retval != "We failed":
135
+ raise RuntimeError(f"Expected retval to be the message returned from run(), but it was {retval!r}")
136
+ if task_id != self.request.id: # pylint: disable=no-member
137
+ raise RuntimeError(f"Expected task_id {task_id} to equal self.request.id {self.request.id}") # pylint: disable=no-member
138
+ if args:
139
+ raise RuntimeError(f"Expected args to be empty, but it was {args!r}")
140
+ if kwargs:
141
+ raise RuntimeError(f"Expected kwargs to be empty, but it was {kwargs!r}")
142
+ if einfo is not None:
143
+ raise RuntimeError(f"Expected einfo to be None, but it was {einfo!r}")
144
+ logger.info("after_return() was called as expected")
145
+
146
+
147
+ class TestFailCleanlyInBeforeStart(TestFailCleanly):
148
+ """
149
+ Job that fails "cleanly" during before_start() through self.fail() instead of raising an exception.
150
+ """
151
+
152
+ def before_start(self, task_id, args, kwargs):
153
+ super().before_start(task_id, args, kwargs)
154
+ logger.info("I'm a test job that fails!")
155
+ self.fail("We failed")
156
+ return "We failed"
157
+
158
+ def run(self):
159
+ raise RuntimeError("run() was unexpectedly called after a failure in before_start()")
160
+
161
+
162
+ register_jobs(
163
+ TestFailJob,
164
+ TestFailInBeforeStart,
165
+ TestFailWithSanitization,
166
+ TestFailCleanly,
167
+ TestFailCleanlyInBeforeStart,
168
+ )
@@ -196,20 +196,6 @@ class ComputedFieldTest(APIViewTestCases.APIViewTestCase):
196
196
 
197
197
  class ConfigContextTest(APIViewTestCases.APIViewTestCase):
198
198
  model = ConfigContext
199
- create_data = [
200
- {
201
- "name": "Config Context 4",
202
- "data": {"more_foo": True},
203
- },
204
- {
205
- "name": "Config Context 5",
206
- "data": {"more_bar": False},
207
- },
208
- {
209
- "name": "Config Context 6",
210
- "data": {"more_baz": None},
211
- },
212
- ]
213
199
  bulk_update_data = {
214
200
  "description": "New description",
215
201
  }
@@ -220,6 +206,21 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
220
206
  ConfigContext.objects.create(name="Config Context 1", weight=100, data={"foo": 123})
221
207
  ConfigContext.objects.create(name="Config Context 2", weight=200, data={"bar": 456})
222
208
  ConfigContext.objects.create(name="Config Context 3", weight=300, data={"baz": 789})
209
+ cls.create_data = [
210
+ {
211
+ "name": "Config Context 4",
212
+ "data": {"more_foo": True},
213
+ "tags": [tag.pk for tag in Tag.objects.get_for_model(Device)],
214
+ },
215
+ {
216
+ "name": "Config Context 5",
217
+ "data": {"more_bar": False},
218
+ },
219
+ {
220
+ "name": "Config Context 6",
221
+ "data": {"more_baz": None},
222
+ },
223
+ ]
223
224
 
224
225
  def test_render_configcontext_for_object(self):
225
226
  """
@@ -1797,7 +1798,7 @@ class JobTest(
1797
1798
  )
1798
1799
  self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1799
1800
  self.assertIn(
1800
- "task_queue and job_queue are both specified. Please specifiy only one or another.", str(response.content)
1801
+ "task_queue and job_queue are both specified. Please specify only one or another.", str(response.content)
1801
1802
  )
1802
1803
 
1803
1804
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
@@ -1916,7 +1917,7 @@ class JobTest(
1916
1917
  mock_get_worker_count.return_value = 1
1917
1918
  self.add_permissions("extras.run_job")
1918
1919
 
1919
- job_model = Job.objects.get(job_class_name="ExampleJob")
1920
+ job_model = Job.objects.get(job_class_name="TestHasSensitiveVariables")
1920
1921
  job_model.enabled = True
1921
1922
  job_model.validated_save()
1922
1923
 
@@ -238,11 +238,7 @@ class GitTest(TransactionTestCase):
238
238
  repository=self.repo.pk,
239
239
  )
240
240
 
241
- self.assertEqual(
242
- job_result.status,
243
- JobResultStatusChoices.STATUS_FAILURE,
244
- (job_result.result, list(job_result.job_log_entries.values_list("message", "log_object"))),
245
- )
241
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
246
242
  self.repo.refresh_from_db()
247
243
 
248
244
  log_entries = JobLogEntry.objects.filter(job_result=job_result)
@@ -308,11 +304,7 @@ class GitTest(TransactionTestCase):
308
304
  repository=self.repo.pk,
309
305
  )
310
306
 
311
- self.assertEqual(
312
- job_result.status,
313
- JobResultStatusChoices.STATUS_SUCCESS,
314
- (job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
315
- )
307
+ self.assertJobResultStatus(job_result)
316
308
  self.repo.refresh_from_db()
317
309
  MockGitRepo.assert_called_with(
318
310
  os.path.join(tempdir, self.repo.slug),
@@ -331,11 +323,7 @@ class GitTest(TransactionTestCase):
331
323
  job_model = GitRepositorySync().job_model
332
324
  job_result = run_job_for_testing(job=job_model, repository=self.repo.pk)
333
325
  job_result.refresh_from_db()
334
- self.assertEqual(
335
- job_result.status,
336
- JobResultStatusChoices.STATUS_SUCCESS,
337
- (job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
338
- )
326
+ self.assertJobResultStatus(job_result)
339
327
 
340
328
  # Make sure explicit ConfigContext was successfully loaded from file
341
329
  self.assert_explicit_config_context_exists("Frobozz 1000 NTP servers")
@@ -383,11 +371,7 @@ class GitTest(TransactionTestCase):
383
371
  # Run the Git operation and refresh the object from the DB
384
372
  job_result = run_job_for_testing(job=job_model, repository=self.repo.pk)
385
373
  job_result.refresh_from_db()
386
- self.assertEqual(
387
- job_result.status,
388
- JobResultStatusChoices.STATUS_SUCCESS,
389
- (job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
390
- )
374
+ self.assertJobResultStatus(job_result)
391
375
 
392
376
  # Verify that objects have been removed from the database
393
377
  self.assertEqual(
@@ -442,11 +426,7 @@ class GitTest(TransactionTestCase):
442
426
  )
443
427
  job_result.refresh_from_db()
444
428
 
445
- self.assertEqual(
446
- job_result.status,
447
- JobResultStatusChoices.STATUS_FAILURE,
448
- job_result.result,
449
- )
429
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
450
430
 
451
431
  # Due to transaction rollback on failure, the database should still/again match the pre-sync state, of
452
432
  # no records owned by the repository.
@@ -547,11 +527,7 @@ class GitTest(TransactionTestCase):
547
527
  )
548
528
  job_result.refresh_from_db()
549
529
 
550
- self.assertEqual(
551
- job_result.status,
552
- JobResultStatusChoices.STATUS_SUCCESS,
553
- (job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
554
- )
530
+ self.assertJobResultStatus(job_result)
555
531
 
556
532
  # Make sure ConfigContext was successfully loaded from file
557
533
  config_context = ConfigContext.objects.get(
@@ -591,15 +567,7 @@ class GitTest(TransactionTestCase):
591
567
  delete_job_result = JobResult.objects.filter(name=repo_name).first()
592
568
  # Make sure we didn't get the wrong JobResult
593
569
  self.assertNotEqual(job_result, delete_job_result)
594
- self.assertEqual(
595
- delete_job_result.status,
596
- JobResultStatusChoices.STATUS_SUCCESS,
597
- (
598
- delete_job_result,
599
- delete_job_result.traceback,
600
- list(delete_job_result.job_log_entries.values_list("message", flat=True)),
601
- ),
602
- )
570
+ self.assertJobResultStatus(delete_job_result)
603
571
 
604
572
  with self.assertRaises(ConfigContext.DoesNotExist):
605
573
  ConfigContext.objects.get(
@@ -637,11 +605,7 @@ class GitTest(TransactionTestCase):
637
605
  job_model = GitRepositorySync().job_model
638
606
  job_result = run_job_for_testing(job=job_model, repository=self.repo.pk)
639
607
  job_result.refresh_from_db()
640
- self.assertEqual(
641
- job_result.status,
642
- JobResultStatusChoices.STATUS_SUCCESS,
643
- (job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
644
- )
608
+ self.assertJobResultStatus(job_result)
645
609
 
646
610
  self.assert_explicit_config_context_exists("Frobozz 1000 NTP servers")
647
611
  self.assert_implicit_config_context_exists("Location context")
@@ -673,11 +637,7 @@ class GitTest(TransactionTestCase):
673
637
  # Resync, attempting and failing to update to the new commit
674
638
  job_result = run_job_for_testing(job=job_model, repository=self.repo.pk)
675
639
  job_result.refresh_from_db()
676
- self.assertEqual(
677
- job_result.status,
678
- JobResultStatusChoices.STATUS_FAILURE,
679
- job_result.result,
680
- )
640
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
681
641
  log_entries = JobLogEntry.objects.filter(job_result=job_result)
682
642
 
683
643
  # Assert database changes were rolled back
@@ -718,11 +678,7 @@ class GitTest(TransactionTestCase):
718
678
  )
719
679
  job_result.refresh_from_db()
720
680
 
721
- self.assertEqual(
722
- job_result.status,
723
- JobResultStatusChoices.STATUS_SUCCESS,
724
- (job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
725
- )
681
+ self.assertJobResultStatus(job_result)
726
682
 
727
683
  log_entries = JobLogEntry.objects.filter(job_result=job_result)
728
684
 
@@ -798,3 +754,57 @@ class GitTest(TransactionTestCase):
798
754
  "provides contents overlapping with this repository.",
799
755
  str(cm.exception),
800
756
  )
757
+
758
+ @mock.patch("nautobot.extras.models.datasources.GitRepo")
759
+ def test_clone_to_directory_with_secrets(self, MockGitRepo):
760
+ """
761
+ The clone_to_directory method should correctly make use of secrets.
762
+ """
763
+ with tempfile.TemporaryDirectory() as tempdir:
764
+ # Prepare secrets values
765
+ with open(os.path.join(tempdir, "username.txt"), "wt") as handle:
766
+ handle.write("núñez")
767
+
768
+ with open(os.path.join(tempdir, "token.txt"), "wt") as handle:
769
+ handle.write("1:3@/?=ab@")
770
+
771
+ # Create secrets and assign
772
+ username_secret = Secret.objects.create(
773
+ name="Git Username",
774
+ provider="text-file",
775
+ parameters={"path": os.path.join(tempdir, "username.txt")},
776
+ )
777
+ token_secret = Secret.objects.create(
778
+ name="Git Token",
779
+ provider="text-file",
780
+ parameters={"path": os.path.join(tempdir, "token.txt")},
781
+ )
782
+ secrets_group = SecretsGroup.objects.create(name="Git Credentials")
783
+ SecretsGroupAssociation.objects.create(
784
+ secret=username_secret,
785
+ secrets_group=secrets_group,
786
+ access_type=SecretsGroupAccessTypeChoices.TYPE_HTTP,
787
+ secret_type=SecretsGroupSecretTypeChoices.TYPE_USERNAME,
788
+ )
789
+ SecretsGroupAssociation.objects.create(
790
+ secret=token_secret,
791
+ secrets_group=secrets_group,
792
+ access_type=SecretsGroupAccessTypeChoices.TYPE_HTTP,
793
+ secret_type=SecretsGroupSecretTypeChoices.TYPE_TOKEN,
794
+ )
795
+
796
+ # Configure GitRepository model
797
+ self.repo.secrets_group = secrets_group
798
+ self.repo.remote_url = "http://localhost/git.git"
799
+ self.repo.save()
800
+
801
+ # Try to clone it
802
+ self.repo.clone_to_directory(tempdir, "main")
803
+
804
+ # Assert that GitRepo was called with proper args
805
+ args, kwargs = MockGitRepo.call_args
806
+ path, from_url = args
807
+ self.assertTrue(path.startswith(os.path.join(tempdir, self.repo.slug)))
808
+ self.assertEqual(from_url, "http://n%C3%BA%C3%B1ez:1%3A3%40%2F%3F%3Dab%40@localhost/git.git")
809
+ self.assertEqual(kwargs["depth"], 0)
810
+ self.assertEqual(kwargs["branch"], "main")
@@ -880,6 +880,8 @@ class JobFilterSetTestCase(FilterTestCases.FilterTestCase):
880
880
  generic_filter_tests = (
881
881
  ("grouping",),
882
882
  ("job_class_name",),
883
+ ("job_queues", "job_queues__id"),
884
+ ("job_queues", "job_queues__name"),
883
885
  ("module_name",),
884
886
  ("name",),
885
887
  )