nautobot 2.4.20__py3-none-any.whl → 2.4.21__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nautobot/circuits/templates/circuits/circuit.html +1 -1
- nautobot/circuits/templates/circuits/circuittermination.html +1 -1
- nautobot/circuits/templates/circuits/circuittype.html +1 -1
- nautobot/circuits/templates/circuits/providernetwork.html +1 -1
- nautobot/core/cli/migrate_deprecated_templates.py +200 -0
- nautobot/core/jobs/__init__.py +2 -1
- nautobot/core/jobs/groups.py +31 -1
- nautobot/core/models/tree_queries.py +10 -5
- nautobot/core/signals.py +12 -1
- nautobot/core/templates/components/panel/panel.html +1 -1
- nautobot/core/templates/inc/image_attachments.html +2 -1
- nautobot/core/templatetags/helpers.py +22 -0
- nautobot/core/tests/runner.py +3 -0
- nautobot/core/tests/test_cli.py +40 -0
- nautobot/core/tests/test_forms.py +41 -0
- nautobot/core/tests/test_jobs.py +75 -1
- nautobot/core/tests/test_tree_queries.py +14 -1
- nautobot/core/ui/object_detail.py +41 -5
- nautobot/core/utils/filtering.py +11 -9
- nautobot/core/views/generic.py +3 -3
- nautobot/dcim/models/device_components.py +81 -68
- nautobot/dcim/templates/dcim/device/config.html +1 -1
- nautobot/dcim/templates/dcim/device/consoleports.html +1 -1
- nautobot/dcim/templates/dcim/device/consoleserverports.html +1 -1
- nautobot/dcim/templates/dcim/device/devicebays.html +1 -1
- nautobot/dcim/templates/dcim/device/frontports.html +1 -1
- nautobot/dcim/templates/dcim/device/interfaces.html +1 -1
- nautobot/dcim/templates/dcim/device/inventory.html +1 -1
- nautobot/dcim/templates/dcim/device/lldp_neighbors.html +1 -1
- nautobot/dcim/templates/dcim/device/modulebays.html +1 -1
- nautobot/dcim/templates/dcim/device/poweroutlets.html +1 -1
- nautobot/dcim/templates/dcim/device/powerports.html +1 -1
- nautobot/dcim/templates/dcim/device/rearports.html +1 -1
- nautobot/dcim/templates/dcim/device/status.html +1 -1
- nautobot/dcim/templates/dcim/device/wireless.html +1 -1
- nautobot/dcim/templates/dcim/device.html +1 -1
- nautobot/dcim/templates/dcim/device_interface_delete.html +1 -1
- nautobot/dcim/templates/dcim/devicetype.html +1 -1
- nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +14 -0
- nautobot/dcim/templates/dcim/interface_bulk_delete.html +1 -1
- nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
- nautobot/dcim/templates/dcim/location_retrieve.html +1 -242
- nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
- nautobot/dcim/templates/dcim/powerfeed.html +1 -1
- nautobot/dcim/templates/dcim/powerpanel.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis.html +1 -1
- nautobot/dcim/tests/test_models.py +43 -3
- nautobot/dcim/tests/test_views.py +52 -21
- nautobot/dcim/views.py +203 -87
- nautobot/extras/api/views.py +9 -1
- nautobot/extras/filters/customfields.py +9 -3
- nautobot/extras/models/groups.py +42 -5
- nautobot/extras/signals.py +20 -19
- nautobot/extras/tables.py +31 -2
- nautobot/extras/templates/extras/computedfield.html +1 -1
- nautobot/extras/templates/extras/configcontext.html +1 -1
- nautobot/extras/templates/extras/configcontextschema_validation.html +1 -1
- nautobot/extras/templates/extras/customfield.html +1 -1
- nautobot/extras/templates/extras/dynamicgroup_retrieve.html +11 -5
- nautobot/extras/templates/extras/gitrepository_result.html +0 -2
- nautobot/extras/templates/extras/graphqlquery_retrieve.html +1 -96
- nautobot/extras/templates/extras/inc/graphqlquery_execute.html +71 -0
- nautobot/extras/templates/extras/object_dynamicgroups.html +2 -2
- nautobot/extras/templates/extras/secretsgroup.html +1 -1
- nautobot/extras/templates/extras/tag.html +1 -1
- nautobot/extras/tests/integration/test_dynamicgroups.py +5 -1
- nautobot/extras/tests/test_api.py +1 -0
- nautobot/extras/tests/test_changelog.py +28 -0
- nautobot/extras/tests/test_customfields.py +10 -2
- nautobot/extras/tests/test_dynamicgroups.py +37 -1
- nautobot/extras/views.py +49 -19
- nautobot/ipam/signals.py +71 -0
- nautobot/ipam/templates/ipam/prefix_delete.html +1 -1
- nautobot/ipam/templates/ipam/service.html +1 -1
- nautobot/ipam/templates/ipam/vlan.html +1 -1
- nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
- nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
- nautobot/ipam/tests/test_models.py +42 -0
- nautobot/users/templates/users/sessionkey_delete.html +1 -1
- nautobot/users/views.py +2 -2
- nautobot/virtualization/models.py +1 -68
- nautobot/virtualization/templates/virtualization/virtual_machine_vminterface_delete.html +1 -1
- nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
- nautobot/virtualization/tests/test_models.py +42 -3
- {nautobot-2.4.20.dist-info → nautobot-2.4.21.dist-info}/METADATA +9 -9
- {nautobot-2.4.20.dist-info → nautobot-2.4.21.dist-info}/RECORD +90 -86
- nautobot-2.4.21.dist-info/entry_points.txt +4 -0
- nautobot-2.4.20.dist-info/entry_points.txt +0 -3
- {nautobot-2.4.20.dist-info → nautobot-2.4.21.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.20.dist-info → nautobot-2.4.21.dist-info}/NOTICE +0 -0
- {nautobot-2.4.20.dist-info → nautobot-2.4.21.dist-info}/WHEEL +0 -0
|
@@ -1,243 +1,2 @@
|
|
|
1
1
|
{% extends 'generic/object_retrieve.html' %}
|
|
2
|
-
{%
|
|
3
|
-
{% load plugins %}
|
|
4
|
-
{% load helpers %}
|
|
5
|
-
{% load tz %}
|
|
6
|
-
|
|
7
|
-
{% block content_left_page %}
|
|
8
|
-
<div class="panel panel-default">
|
|
9
|
-
<div class="panel-heading">
|
|
10
|
-
<strong>Location</strong>
|
|
11
|
-
</div>
|
|
12
|
-
<table class="table table-hover panel-body attr-table">
|
|
13
|
-
<tr>
|
|
14
|
-
<td>Location Type</td>
|
|
15
|
-
<td>{{ object.location_type|hyperlinked_object:"name" }}</td>
|
|
16
|
-
</tr>
|
|
17
|
-
<tr>
|
|
18
|
-
<td>Status</td>
|
|
19
|
-
<td>
|
|
20
|
-
{{ object.status| hyperlinked_object_with_color }}
|
|
21
|
-
</td>
|
|
22
|
-
</tr>
|
|
23
|
-
<tr>
|
|
24
|
-
<td>Hierarchy</td>
|
|
25
|
-
<td>
|
|
26
|
-
{% include 'dcim/inc/location_hierarchy.html' with location=object %}
|
|
27
|
-
</td>
|
|
28
|
-
</tr>
|
|
29
|
-
{% include 'inc/tenant_table_row.html' %}
|
|
30
|
-
<tr>
|
|
31
|
-
<td>Facility</td>
|
|
32
|
-
<td>{{ object.facility|placeholder }}</td>
|
|
33
|
-
</tr>
|
|
34
|
-
<tr>
|
|
35
|
-
<td>AS Number</td>
|
|
36
|
-
<td>{{ object.asn|placeholder }}</td>
|
|
37
|
-
</tr>
|
|
38
|
-
<tr>
|
|
39
|
-
<td>Time Zone</td>
|
|
40
|
-
<td>
|
|
41
|
-
{% if object.time_zone %}
|
|
42
|
-
{{ object.time_zone }} (UTC {{ object.time_zone|tzoffset }})<br />
|
|
43
|
-
<small class="text-muted">Local time: {% timezone object.time_zone %}{% now "SHORT_DATETIME_FORMAT" %}{% endtimezone %}</small>
|
|
44
|
-
{% else %}
|
|
45
|
-
<span class="text-muted">—</span>
|
|
46
|
-
{% endif %}
|
|
47
|
-
</td>
|
|
48
|
-
</tr>
|
|
49
|
-
<tr>
|
|
50
|
-
<td>Description</td>
|
|
51
|
-
<td>{{ object.description|placeholder }}</td>
|
|
52
|
-
</tr>
|
|
53
|
-
<tr>
|
|
54
|
-
<td>Children</td>
|
|
55
|
-
<td>
|
|
56
|
-
{% if object.children.all %}
|
|
57
|
-
<a href="{% url 'dcim:location_list' %}?parent={{ object.pk }}">{{ children_table.rows|length }}</a>
|
|
58
|
-
{% else %}
|
|
59
|
-
{{ None|placeholder }}
|
|
60
|
-
{% endif %}
|
|
61
|
-
</td>
|
|
62
|
-
</tr>
|
|
63
|
-
</table>
|
|
64
|
-
</div>
|
|
65
|
-
<div class="panel panel-default">
|
|
66
|
-
<div class="panel-heading">
|
|
67
|
-
<strong>Geographical Info</strong>
|
|
68
|
-
</div>
|
|
69
|
-
<table class="table table-hover panel-body attr-table">
|
|
70
|
-
<tr>
|
|
71
|
-
<td>Physical Address</td>
|
|
72
|
-
<td>
|
|
73
|
-
{% if object.physical_address %}
|
|
74
|
-
<div class="pull-right noprint">
|
|
75
|
-
<a href="https://maps.google.com/?q={{ object.physical_address|urlencode }}" target="_blank" class="btn btn-primary btn-xs">
|
|
76
|
-
<i class="mdi mdi-map-marker"></i> Map it
|
|
77
|
-
</a>
|
|
78
|
-
</div>
|
|
79
|
-
<span>{{ object.physical_address|linebreaksbr }}</span>
|
|
80
|
-
{% else %}
|
|
81
|
-
<span class="text-muted">—</span>
|
|
82
|
-
{% endif %}
|
|
83
|
-
</td>
|
|
84
|
-
</tr>
|
|
85
|
-
<tr>
|
|
86
|
-
<td>Shipping Address</td>
|
|
87
|
-
<td>{{ object.shipping_address|linebreaksbr|placeholder }}</td>
|
|
88
|
-
</tr>
|
|
89
|
-
<tr>
|
|
90
|
-
<td>GPS Coordinates</td>
|
|
91
|
-
<td>
|
|
92
|
-
{% if object.latitude and object.longitude %}
|
|
93
|
-
<div class="pull-right noprint">
|
|
94
|
-
<a href="https://maps.google.com/?q={{ object.latitude }},{{ object.longitude }}" target="_blank" class="btn btn-primary btn-xs">
|
|
95
|
-
<i class="mdi mdi-map-marker"></i> Map it
|
|
96
|
-
</a>
|
|
97
|
-
</div>
|
|
98
|
-
<span>{{ object.latitude }}, {{ object.longitude }}</span>
|
|
99
|
-
{% else %}
|
|
100
|
-
<span class="text-muted">—</span>
|
|
101
|
-
{% endif %}
|
|
102
|
-
</td>
|
|
103
|
-
</tr>
|
|
104
|
-
</table>
|
|
105
|
-
</div>
|
|
106
|
-
{% if show_convert_to_contact_button %}
|
|
107
|
-
<div class="panel panel-default">
|
|
108
|
-
<div class="panel-heading">
|
|
109
|
-
<strong>Contact Info</strong>
|
|
110
|
-
</div>
|
|
111
|
-
<table class="table table-hover panel-body attr-table">
|
|
112
|
-
<tr>
|
|
113
|
-
<td>Contact Name</td>
|
|
114
|
-
<td>{{ object.contact_name|placeholder }}</td>
|
|
115
|
-
</tr>
|
|
116
|
-
<tr>
|
|
117
|
-
<td>Contact Phone</td>
|
|
118
|
-
<td>{{ object.contact_phone|hyperlinked_phone_number }}</td>
|
|
119
|
-
</tr>
|
|
120
|
-
<tr>
|
|
121
|
-
<td>Contact E-Mail</td>
|
|
122
|
-
<td>{{ object.contact_email|hyperlinked_email }}</td>
|
|
123
|
-
</tr>
|
|
124
|
-
</table>
|
|
125
|
-
{% if request.user|has_perms:contact_association_permission %}
|
|
126
|
-
{% with request.path|add:"?tab=contacts"|urlencode as return_url %}
|
|
127
|
-
<div class="panel-footer text-right noprint">
|
|
128
|
-
<a href="{% url 'dcim:location_migrate_data_to_contact' pk=object.pk %}?return_url={{return_url}}" class="btn btn-primary btn-xs">
|
|
129
|
-
<span class="mdi mdi-account-edit" aria-hidden="true"></span>
|
|
130
|
-
Convert to contact/team record
|
|
131
|
-
</a>
|
|
132
|
-
</div>
|
|
133
|
-
{% endwith %}
|
|
134
|
-
{% endif %}
|
|
135
|
-
</div>
|
|
136
|
-
{% endif %}
|
|
137
|
-
<div class="panel panel-default">
|
|
138
|
-
<div class="panel-heading">
|
|
139
|
-
<strong>Comments</strong>
|
|
140
|
-
</div>
|
|
141
|
-
<div class="panel-body rendered-markdown">
|
|
142
|
-
{% if object.comments %}
|
|
143
|
-
{{ object.comments|render_markdown }}
|
|
144
|
-
{% else %}
|
|
145
|
-
<span class="text-muted">None</span>
|
|
146
|
-
{% endif %}
|
|
147
|
-
</div>
|
|
148
|
-
</div>
|
|
149
|
-
{% endblock content_left_page %}
|
|
150
|
-
|
|
151
|
-
{% block content_right_page %}
|
|
152
|
-
<div class="panel panel-default">
|
|
153
|
-
<div class="panel-heading">
|
|
154
|
-
<strong>Stats</strong>
|
|
155
|
-
</div>
|
|
156
|
-
<div class="panel-body">
|
|
157
|
-
<div class="col-md-4 text-center">
|
|
158
|
-
<h2><a href="{% url 'dcim:rack_list' %}?location={{ object.pk }}" class="btn {% if stats.rack_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.rack_count }}</a></h2>
|
|
159
|
-
<p>Racks</p>
|
|
160
|
-
</div>
|
|
161
|
-
<div class="col-md-4 text-center">
|
|
162
|
-
<h2><a href="{% url 'dcim:device_list' %}?location={{ object.pk }}" class="btn {% if stats.device_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.device_count }}</a></h2>
|
|
163
|
-
<p>Devices</p>
|
|
164
|
-
</div>
|
|
165
|
-
<div class="col-md-4 text-center">
|
|
166
|
-
<h2><a href="{% url 'ipam:prefix_list' %}?locations={{ object.pk }}" class="btn {% if stats.prefix_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.prefix_count }}</a></h2>
|
|
167
|
-
<p>Prefixes</p>
|
|
168
|
-
</div>
|
|
169
|
-
<div class="col-md-4 text-center">
|
|
170
|
-
<h2><a href="{% url 'ipam:vlan_list' %}?locations={{ object.pk }}" class="btn {% if stats.vlan_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.vlan_count }}</a></h2>
|
|
171
|
-
<p>VLANs</p>
|
|
172
|
-
</div>
|
|
173
|
-
<div class="col-md-4 text-center">
|
|
174
|
-
<h2><a href="{% url 'circuits:circuit_list' %}?location={{ object.pk }}" class="btn {% if stats.circuit_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.circuit_count }}</a></h2>
|
|
175
|
-
<p>Circuits</p>
|
|
176
|
-
</div>
|
|
177
|
-
<div class="col-md-4 text-center">
|
|
178
|
-
<h2><a href="{% url 'virtualization:virtualmachine_list' %}?location={{ object.pk }}" class="btn {% if stats.vm_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.vm_count }}</a></h2>
|
|
179
|
-
<p>Virtual Machines</p>
|
|
180
|
-
</div>
|
|
181
|
-
</div>
|
|
182
|
-
</div>
|
|
183
|
-
<div class="panel panel-default">
|
|
184
|
-
<div class="panel-heading">
|
|
185
|
-
<strong>Rack Groups</strong>
|
|
186
|
-
</div>
|
|
187
|
-
<table class="table table-hover panel-body">
|
|
188
|
-
{% for rg in rack_groups %}
|
|
189
|
-
<tr>
|
|
190
|
-
<td style="padding-left: {{ rg.tree_depth }}8px"><i class="mdi mdi-folder-open"></i> <a href="{{ rg.get_absolute_url }}">{{ rg }}</a></td>
|
|
191
|
-
<td>{{ rg.rack_count }}</td>
|
|
192
|
-
<td class="text-right noprint">
|
|
193
|
-
<a href="{% url 'dcim:rack_elevation_list' %}?rack_group={{ rg.pk }}" class="btn btn-xs btn-primary" title="View elevations">
|
|
194
|
-
<i class="mdi mdi-server"></i>
|
|
195
|
-
</a>
|
|
196
|
-
</td>
|
|
197
|
-
</tr>
|
|
198
|
-
{% endfor %}
|
|
199
|
-
<tr>
|
|
200
|
-
<td><i class="mdi mdi-folder-open"></i> All racks</td>
|
|
201
|
-
<td>{{ stats.rack_count }}</td>
|
|
202
|
-
<td class="text-right noprint">
|
|
203
|
-
<a href="{% url 'dcim:rack_elevation_list' %}?location={{ object.pk }}" class="btn btn-xs btn-primary" title="View elevations">
|
|
204
|
-
<i class="mdi mdi-server"></i>
|
|
205
|
-
</a>
|
|
206
|
-
</td>
|
|
207
|
-
</tr>
|
|
208
|
-
</table>
|
|
209
|
-
</div>
|
|
210
|
-
<div class="panel panel-default">
|
|
211
|
-
<div class="panel-heading">
|
|
212
|
-
<strong>Images</strong>
|
|
213
|
-
</div>
|
|
214
|
-
{% include 'inc/image_attachments.html' with images=object.images.all %}
|
|
215
|
-
{% if perms.extras.add_imageattachment %}
|
|
216
|
-
<div class="panel-footer text-right noprint">
|
|
217
|
-
<a href="{% url 'dcim:location_add_image' object_id=object.pk %}" class="btn btn-primary btn-xs">
|
|
218
|
-
<span class="mdi mdi-plus-thick" aria-hidden="true"></span>
|
|
219
|
-
Attach an image
|
|
220
|
-
</a>
|
|
221
|
-
</div>
|
|
222
|
-
{% endif %}
|
|
223
|
-
</div>
|
|
224
|
-
{% endblock content_right_page %}
|
|
225
|
-
|
|
226
|
-
{% block content_full_width_page %}
|
|
227
|
-
{% if object.children.all %}
|
|
228
|
-
<div class="panel panel-default">
|
|
229
|
-
<div class="panel-heading">
|
|
230
|
-
<strong>Children</strong>
|
|
231
|
-
</div>
|
|
232
|
-
{% include 'inc/table.html' with table=children_table %}
|
|
233
|
-
{% if perms.dcim.add_location %}
|
|
234
|
-
<div class="panel-footer text-right noprint">
|
|
235
|
-
<a href="{% url 'dcim:location_add' %}?parent={{ object.pk }}" class="btn btn-xs btn-primary">
|
|
236
|
-
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add child
|
|
237
|
-
</a>
|
|
238
|
-
</div>
|
|
239
|
-
{% endif %}
|
|
240
|
-
</div>
|
|
241
|
-
{% include 'inc/paginator.html' with paginator=children_table.paginator page=children_table.page %}
|
|
242
|
-
{% endif %}
|
|
243
|
-
{% 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 %}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
{% extends '
|
|
1
|
+
{% extends 'generic/object_retrieve.html' %}
|
|
2
2
|
{% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
{% extends '
|
|
1
|
+
{% extends 'generic/object_retrieve.html' %}
|
|
2
2
|
{% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
{% extends '
|
|
1
|
+
{% extends 'generic/object_retrieve.html' %}
|
|
2
2
|
{% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
|
|
@@ -2834,6 +2834,11 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
|
|
|
2834
2834
|
self.assertEqual(count, 1)
|
|
2835
2835
|
self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ips[-1], interface=interface).count(), 1)
|
|
2836
2836
|
|
|
2837
|
+
# add a single instance which is already there
|
|
2838
|
+
count = interface.add_ip_addresses(ips[-1])
|
|
2839
|
+
self.assertEqual(count, 0)
|
|
2840
|
+
self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ips[-1], interface=interface).count(), 1)
|
|
2841
|
+
|
|
2837
2842
|
# add multiple instances
|
|
2838
2843
|
count = interface.add_ip_addresses(ips[:5])
|
|
2839
2844
|
self.assertEqual(count, 5)
|
|
@@ -2841,6 +2846,20 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
|
|
|
2841
2846
|
for ip in ips[:5]:
|
|
2842
2847
|
self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ip, interface=interface).count(), 1)
|
|
2843
2848
|
|
|
2849
|
+
# add multiple instances all of which are already there
|
|
2850
|
+
count = interface.add_ip_addresses(ips[:5])
|
|
2851
|
+
self.assertEqual(count, 0)
|
|
2852
|
+
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 6)
|
|
2853
|
+
for ip in ips[:5]:
|
|
2854
|
+
self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ip, interface=interface).count(), 1)
|
|
2855
|
+
|
|
2856
|
+
# add multiple IPs some of which are there
|
|
2857
|
+
count = interface.add_ip_addresses(ips[3:7])
|
|
2858
|
+
self.assertEqual(count, 2)
|
|
2859
|
+
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 8)
|
|
2860
|
+
for ip in ips[3:7]:
|
|
2861
|
+
self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ip, interface=interface).count(), 1)
|
|
2862
|
+
|
|
2844
2863
|
def test_remove_ip_addresses(self):
|
|
2845
2864
|
"""Test the `remove_ip_addresses` helper method on `Interface`"""
|
|
2846
2865
|
interface = Interface.objects.create(
|
|
@@ -2863,13 +2882,28 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
|
|
|
2863
2882
|
self.assertEqual(count, 1)
|
|
2864
2883
|
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 9)
|
|
2865
2884
|
|
|
2885
|
+
# remove a single instance which has already been removed
|
|
2886
|
+
count = interface.remove_ip_addresses(ips[-1])
|
|
2887
|
+
self.assertEqual(count, 0)
|
|
2888
|
+
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 9)
|
|
2889
|
+
|
|
2866
2890
|
# remove multiple instances
|
|
2867
2891
|
count = interface.remove_ip_addresses(ips[:5])
|
|
2868
2892
|
self.assertEqual(count, 5)
|
|
2869
2893
|
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 4)
|
|
2870
2894
|
|
|
2895
|
+
# remove multiple instances all which have already been removed
|
|
2896
|
+
count = interface.remove_ip_addresses(ips[:5])
|
|
2897
|
+
self.assertEqual(count, 0)
|
|
2898
|
+
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 4)
|
|
2899
|
+
|
|
2900
|
+
# remove multiple instances some of which have already been removed
|
|
2901
|
+
count = interface.remove_ip_addresses(ips[3:7])
|
|
2902
|
+
self.assertEqual(count, 2)
|
|
2903
|
+
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 2)
|
|
2904
|
+
|
|
2871
2905
|
count = interface.remove_ip_addresses(ips)
|
|
2872
|
-
self.assertEqual(count,
|
|
2906
|
+
self.assertEqual(count, 2)
|
|
2873
2907
|
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 0)
|
|
2874
2908
|
|
|
2875
2909
|
# Test the pre_delete signal for IPAddressToInterface instances
|
|
@@ -2877,10 +2911,16 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
|
|
|
2877
2911
|
self.device.primary_ip4 = interface.ip_addresses.all().filter(ip_version=4).first()
|
|
2878
2912
|
self.device.primary_ip6 = interface.ip_addresses.all().filter(ip_version=6).first()
|
|
2879
2913
|
self.device.save()
|
|
2880
|
-
|
|
2914
|
+
|
|
2915
|
+
count = interface.remove_ip_addresses(self.device.primary_ip4)
|
|
2916
|
+
self.assertEqual(count, 1)
|
|
2881
2917
|
self.device.refresh_from_db()
|
|
2882
2918
|
self.assertEqual(self.device.primary_ip4, None)
|
|
2883
|
-
|
|
2919
|
+
# NOTE: This effectively tests what happens when you pass remove_ip_addresses None; it
|
|
2920
|
+
# NOTE: does not remove a v6 address, because there are no v6 IPs created in this test
|
|
2921
|
+
# NOTE: class.
|
|
2922
|
+
count = interface.remove_ip_addresses(self.device.primary_ip6)
|
|
2923
|
+
self.assertEqual(count, 0)
|
|
2884
2924
|
self.device.refresh_from_db()
|
|
2885
2925
|
self.assertEqual(self.device.primary_ip6, None)
|
|
2886
2926
|
|
|
@@ -7,7 +7,6 @@ from django.contrib.auth import get_user_model
|
|
|
7
7
|
from django.contrib.contenttypes.models import ContentType
|
|
8
8
|
from django.db.models import Q
|
|
9
9
|
from django.test import override_settings
|
|
10
|
-
from django.test.client import RequestFactory
|
|
11
10
|
from django.urls import reverse
|
|
12
11
|
from django.utils.html import strip_spaces_between_tags
|
|
13
12
|
from netaddr import EUI
|
|
@@ -109,7 +108,6 @@ from nautobot.dcim.models import (
|
|
|
109
108
|
from nautobot.dcim.views import (
|
|
110
109
|
ConsoleConnectionsListView,
|
|
111
110
|
InterfaceConnectionsListView,
|
|
112
|
-
LocationUIViewSet,
|
|
113
111
|
PowerConnectionsListView,
|
|
114
112
|
)
|
|
115
113
|
from nautobot.extras.choices import CustomFieldTypeChoices, RelationshipTypeChoices
|
|
@@ -211,8 +209,10 @@ class LocationTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase, ViewT
|
|
|
211
209
|
|
|
212
210
|
class LocationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
213
211
|
model = Location
|
|
214
|
-
# One query for the natural slug, one for `LocationViewSet.get_extra_context
|
|
215
|
-
|
|
212
|
+
# One query for the natural slug, one for `LocationViewSet.get_extra_context`, and additional for distinct (which may be fixable?)
|
|
213
|
+
# See: https://github.com/nautobot/nautobot/pull/7530#discussion_r2432836062
|
|
214
|
+
# and https://github.com/nautobot/nautobot/pull/7530#discussion_r2432620239 for additional context
|
|
215
|
+
allowed_number_of_tree_queries_per_view_type = {"retrieve": 3}
|
|
216
216
|
|
|
217
217
|
@classmethod
|
|
218
218
|
def setUpTestData(cls):
|
|
@@ -442,7 +442,6 @@ class LocationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
442
442
|
self.assertEqual(location.contact_email, "")
|
|
443
443
|
|
|
444
444
|
def test_get_extra_context(self):
|
|
445
|
-
view_set = LocationUIViewSet()
|
|
446
445
|
child_1, child_2 = Location.objects.filter(name__startswith="Leaf ")
|
|
447
446
|
parent_location = child_1.parent
|
|
448
447
|
child_1.location_type.content_types.add(ContentType.objects.get_for_model(Prefix))
|
|
@@ -451,12 +450,24 @@ class LocationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
451
450
|
prefix_2 = Prefix.objects.create(network="192.0.2.128", prefix_length=25, status=status)
|
|
452
451
|
prefix_1.locations.set([child_1, child_2])
|
|
453
452
|
prefix_2.locations.set([child_1, child_2])
|
|
454
|
-
|
|
455
|
-
request.user = self.user
|
|
453
|
+
|
|
456
454
|
self.add_permissions("dcim.view_location")
|
|
457
455
|
self.add_permissions("ipam.view_prefix")
|
|
458
|
-
|
|
459
|
-
|
|
456
|
+
|
|
457
|
+
url = parent_location.get_absolute_url()
|
|
458
|
+
context = self.client.get(url).context
|
|
459
|
+
prefix_count = (
|
|
460
|
+
Prefix.objects.filter(location__in=parent_location.descendants(include_self=True)).distinct().count()
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
# Ensure that the context contains "stats" and the expected stat for prefix_list
|
|
464
|
+
self.assertIn("stats", context)
|
|
465
|
+
found = False
|
|
466
|
+
for stat in context["stats"].values():
|
|
467
|
+
if stat[0] == "ipam:prefix_list":
|
|
468
|
+
self.assertEqual(stat[1], prefix_count)
|
|
469
|
+
found = True
|
|
470
|
+
self.assertTrue(found, "ipam:prefix_list stat not found in context['stats']")
|
|
460
471
|
|
|
461
472
|
|
|
462
473
|
class RackGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase, ViewTestCases.BulkEditObjectsViewTestCase):
|
|
@@ -2185,7 +2196,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
2185
2196
|
)
|
|
2186
2197
|
cls.custom_fields[0].content_types.set([ContentType.objects.get_for_model(Device)])
|
|
2187
2198
|
|
|
2188
|
-
devices = (
|
|
2199
|
+
cls.devices = (
|
|
2189
2200
|
Device.objects.create(
|
|
2190
2201
|
name="Device 1",
|
|
2191
2202
|
location=locations[0],
|
|
@@ -2250,16 +2261,40 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
2250
2261
|
intf_status = Status.objects.get_for_model(Interface).first()
|
|
2251
2262
|
intf_role = Role.objects.get_for_model(Interface).first()
|
|
2252
2263
|
cls.interfaces = (
|
|
2253
|
-
Interface.objects.create(device=devices[0], name="Interface A1", status=intf_status, role=intf_role),
|
|
2254
|
-
Interface.objects.create(device=devices[0], name="Interface A2", status=intf_status),
|
|
2255
|
-
Interface.objects.create(device=devices[0], name="Interface A3", status=intf_status, role=intf_role),
|
|
2264
|
+
Interface.objects.create(device=cls.devices[0], name="Interface A1", status=intf_status, role=intf_role),
|
|
2265
|
+
Interface.objects.create(device=cls.devices[0], name="Interface A2", status=intf_status),
|
|
2266
|
+
Interface.objects.create(device=cls.devices[0], name="Interface A3", status=intf_status, role=intf_role),
|
|
2256
2267
|
)
|
|
2257
2268
|
|
|
2258
|
-
for device, ipaddress in zip(devices, ipaddresses):
|
|
2269
|
+
for device, ipaddress in zip(cls.devices, ipaddresses):
|
|
2259
2270
|
RelationshipAssociation(
|
|
2260
2271
|
relationship=cls.relationships[0], source=device, destination=ipaddress
|
|
2261
2272
|
).validated_save()
|
|
2262
2273
|
|
|
2274
|
+
powerports = (
|
|
2275
|
+
PowerPort.objects.create(device=cls.devices[0], name="Power Port 1"),
|
|
2276
|
+
PowerPort.objects.create(device=cls.devices[0], name="Power Port 2"),
|
|
2277
|
+
PowerPort.objects.create(device=cls.devices[0], name="Power Port 3"),
|
|
2278
|
+
)
|
|
2279
|
+
PowerOutlet.objects.create(device=cls.devices[0], name="Power Outlet A")
|
|
2280
|
+
|
|
2281
|
+
powerpanel = PowerPanel.objects.create(location=locations[0], name="Power Panel 1")
|
|
2282
|
+
pf_status = Status.objects.get_for_model(PowerFeed).first()
|
|
2283
|
+
powerfeed = PowerFeed.objects.create(
|
|
2284
|
+
power_panel=powerpanel,
|
|
2285
|
+
name="Power Feed 1",
|
|
2286
|
+
status=pf_status,
|
|
2287
|
+
available_power=1000,
|
|
2288
|
+
phase=PowerFeedPhaseChoices.PHASE_3PHASE,
|
|
2289
|
+
)
|
|
2290
|
+
|
|
2291
|
+
status_connected = Status.objects.get(name="Connected")
|
|
2292
|
+
|
|
2293
|
+
Cable.objects.create(termination_a=powerports[0], termination_b=powerfeed, status=status_connected)
|
|
2294
|
+
|
|
2295
|
+
poweroutlet = PowerOutlet.objects.create(device=cls.devices[1], name="Power Outlet 1")
|
|
2296
|
+
Cable.objects.create(termination_a=powerports[1], termination_b=poweroutlet, status=status_connected)
|
|
2297
|
+
|
|
2263
2298
|
cls.form_data = {
|
|
2264
2299
|
"device_type": devicetypes[1].pk,
|
|
2265
2300
|
"role": deviceroles[1].pk,
|
|
@@ -2407,11 +2442,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
2407
2442
|
|
|
2408
2443
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
2409
2444
|
def test_device_powerports(self):
|
|
2410
|
-
device =
|
|
2411
|
-
|
|
2412
|
-
PowerPort.objects.create(device=device, name="Power Port 1")
|
|
2413
|
-
PowerPort.objects.create(device=device, name="Power Port 2")
|
|
2414
|
-
PowerPort.objects.create(device=device, name="Power Port 3")
|
|
2445
|
+
device = self.devices[0]
|
|
2415
2446
|
|
|
2416
2447
|
url = reverse("dcim:device_powerports", kwargs={"pk": device.pk})
|
|
2417
2448
|
self.assertHttpStatus(self.client.get(url), 200)
|
|
@@ -4075,8 +4106,8 @@ class PowerConnectionsTestCase(ViewTestCases.ListObjectsViewTestCase):
|
|
|
4075
4106
|
)
|
|
4076
4107
|
|
|
4077
4108
|
poweroutlets = (
|
|
4078
|
-
PowerOutlet.objects.create(device=device_2, name="Power Outlet 1"
|
|
4079
|
-
PowerOutlet.objects.create(device=device_2, name="Power Outlet 2"
|
|
4109
|
+
PowerOutlet.objects.create(device=device_2, name="Power Outlet 1"),
|
|
4110
|
+
PowerOutlet.objects.create(device=device_2, name="Power Outlet 2"),
|
|
4080
4111
|
)
|
|
4081
4112
|
|
|
4082
4113
|
powerpanel = PowerPanel.objects.create(location=location, name="Power Panel 1")
|