nautobot 2.4.4__py3-none-any.whl → 2.4.6__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/__init__.py +19 -3
- nautobot/core/api/mixins.py +10 -0
- nautobot/core/celery/__init__.py +5 -3
- nautobot/core/celery/encoders.py +2 -2
- nautobot/core/forms/fields.py +21 -5
- nautobot/core/forms/utils.py +1 -0
- nautobot/core/jobs/__init__.py +3 -2
- nautobot/core/jobs/bulk_actions.py +1 -1
- nautobot/core/management/commands/generate_test_data.py +1 -1
- nautobot/core/models/name_color_content_types.py +9 -0
- nautobot/core/models/validators.py +7 -0
- nautobot/core/settings.py +0 -14
- nautobot/core/settings.yaml +0 -28
- nautobot/core/tables.py +6 -1
- nautobot/core/templates/generic/object_retrieve.html +1 -1
- nautobot/core/testing/__init__.py +2 -0
- nautobot/core/testing/api.py +18 -0
- nautobot/core/testing/mixins.py +9 -0
- nautobot/core/tests/nautobot_config.py +0 -2
- nautobot/core/tests/runner.py +17 -140
- nautobot/core/tests/test_api.py +4 -4
- nautobot/core/tests/test_authentication.py +83 -4
- nautobot/core/tests/test_forms.py +11 -8
- nautobot/core/tests/test_graphql.py +9 -0
- nautobot/core/tests/test_jobs.py +33 -27
- nautobot/core/ui/object_detail.py +31 -0
- nautobot/dcim/factory.py +2 -0
- nautobot/dcim/filters/__init__.py +5 -0
- nautobot/dcim/forms.py +17 -1
- nautobot/dcim/migrations/0068_alter_softwareimagefile_download_url.py +19 -0
- nautobot/dcim/migrations/0069_softwareimagefile_external_integration.py +25 -0
- nautobot/dcim/models/devices.py +9 -2
- nautobot/dcim/tables/devices.py +1 -0
- nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +4 -0
- nautobot/dcim/tests/test_api.py +74 -31
- nautobot/dcim/tests/test_filters.py +2 -0
- nautobot/dcim/tests/test_jobs.py +4 -6
- nautobot/dcim/tests/test_models.py +65 -0
- nautobot/dcim/tests/test_views.py +3 -0
- nautobot/extras/choices.py +8 -3
- nautobot/extras/forms/forms.py +7 -3
- nautobot/extras/jobs.py +181 -103
- nautobot/extras/management/utils.py +13 -2
- nautobot/extras/models/datasources.py +4 -1
- nautobot/extras/models/jobs.py +20 -17
- nautobot/extras/plugins/marketplace_manifest.yml +18 -0
- nautobot/extras/tables.py +29 -34
- nautobot/extras/templates/extras/inc/panel_changelog.html +1 -1
- nautobot/extras/templates/extras/inc/panel_jobhistory.html +1 -1
- nautobot/extras/templates/extras/status.html +1 -37
- nautobot/extras/test_jobs/atomic_transaction.py +6 -6
- nautobot/extras/test_jobs/fail.py +75 -1
- nautobot/extras/tests/integration/test_notes.py +1 -1
- nautobot/extras/tests/test_api.py +23 -8
- nautobot/extras/tests/test_changelog.py +4 -4
- nautobot/extras/tests/test_customfields.py +3 -0
- nautobot/extras/tests/test_datasources.py +64 -54
- nautobot/extras/tests/test_jobs.py +69 -62
- nautobot/extras/tests/test_models.py +1 -1
- nautobot/extras/tests/test_plugins.py +19 -13
- nautobot/extras/tests/test_relationships.py +14 -5
- nautobot/extras/tests/test_tags.py +2 -2
- nautobot/extras/tests/test_views.py +15 -6
- nautobot/extras/urls.py +1 -30
- nautobot/extras/views.py +17 -55
- nautobot/ipam/forms.py +15 -0
- nautobot/ipam/querysets.py +6 -0
- nautobot/ipam/tables.py +6 -2
- nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -41
- nautobot/ipam/templates/ipam/rir.html +1 -43
- nautobot/ipam/templates/ipam/service.html +2 -46
- nautobot/ipam/templates/ipam/service_edit.html +1 -17
- nautobot/ipam/templates/ipam/service_retrieve.html +7 -0
- nautobot/ipam/tests/migration/__init__.py +0 -0
- nautobot/ipam/tests/migration/test_migrations.py +510 -0
- nautobot/ipam/tests/test_api.py +66 -36
- nautobot/ipam/tests/test_filters.py +0 -10
- nautobot/ipam/tests/test_models.py +16 -0
- nautobot/ipam/tests/test_views.py +44 -2
- nautobot/ipam/urls.py +2 -67
- nautobot/ipam/utils/migrations.py +185 -152
- nautobot/ipam/utils/testing.py +177 -0
- nautobot/ipam/views.py +119 -198
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +43 -5
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +47 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +18 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +35 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +63 -0
- nautobot/project-static/docs/development/apps/api/testing.html +0 -87
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +1 -1
- nautobot/project-static/docs/development/core/best-practices.html +3 -3
- nautobot/project-static/docs/development/core/getting-started.html +78 -107
- nautobot/project-static/docs/development/core/release-checklist.html +1 -1
- nautobot/project-static/docs/development/core/style-guide.html +1 -1
- nautobot/project-static/docs/development/core/testing.html +24 -198
- nautobot/project-static/docs/development/jobs/index.html +27 -14
- nautobot/project-static/docs/media/user-guide/administration/getting-started/nautobot-cloud.png +0 -0
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +1 -1
- nautobot/project-static/docs/release-notes/version-2.4.html +409 -1
- nautobot/project-static/docs/requirements.txt +1 -1
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +290 -290
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +2 -48
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +71 -0
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +3 -1
- nautobot/project-static/docs/user-guide/administration/installation/index.html +257 -16
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +4 -0
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +8 -8
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +1 -0
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +40 -25
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +77 -5
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +0 -1
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +1 -1
- nautobot/project-static/docs/user-guide/index.html +89 -2
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +207 -122
- nautobot/virtualization/forms.py +20 -0
- nautobot/virtualization/templates/virtualization/clustergroup.html +1 -39
- nautobot/virtualization/templates/virtualization/clustertype.html +1 -0
- nautobot/virtualization/tests/test_api.py +14 -3
- nautobot/virtualization/tests/test_views.py +10 -2
- nautobot/virtualization/urls.py +10 -93
- nautobot/virtualization/views.py +33 -72
- {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/METADATA +8 -7
- {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/RECORD +137 -132
- {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/WHEEL +1 -1
- nautobot/core/tests/performance_baselines.yml +0 -8900
- nautobot/ipam/tests/test_migrations.py +0 -462
- /nautobot/ipam/templates/ipam/{namespace_ipaddresses.html → namespace_ip_addresses.html} +0 -0
- {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/NOTICE +0 -0
- {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Utilities for testing IPAM functionality, including data migrations."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
|
|
5
|
+
from django.apps import apps
|
|
6
|
+
from netaddr import IPNetwork
|
|
7
|
+
|
|
8
|
+
# Calculate the probabilities to use for the maybe_subdivide() function defined below.
|
|
9
|
+
|
|
10
|
+
# Frequency of IPv4 (leaf, network) Prefixes by each given mask length in a "realistic" data set.
|
|
11
|
+
# Based loosely on a survey of one large real-world deployment's IP space usage
|
|
12
|
+
FREQUENCY_BY_MASK_LENGTH = [
|
|
13
|
+
0, # /0
|
|
14
|
+
0,
|
|
15
|
+
0,
|
|
16
|
+
0,
|
|
17
|
+
0, # /4
|
|
18
|
+
0,
|
|
19
|
+
0,
|
|
20
|
+
0,
|
|
21
|
+
0, # /8
|
|
22
|
+
0,
|
|
23
|
+
0,
|
|
24
|
+
0,
|
|
25
|
+
0, # /12
|
|
26
|
+
1,
|
|
27
|
+
2,
|
|
28
|
+
4,
|
|
29
|
+
16, # /16
|
|
30
|
+
8,
|
|
31
|
+
12,
|
|
32
|
+
16,
|
|
33
|
+
20, # /20
|
|
34
|
+
120,
|
|
35
|
+
150,
|
|
36
|
+
400,
|
|
37
|
+
5000, # /24
|
|
38
|
+
13000,
|
|
39
|
+
16000,
|
|
40
|
+
19000,
|
|
41
|
+
17000, # /28
|
|
42
|
+
16000,
|
|
43
|
+
32000,
|
|
44
|
+
4000,
|
|
45
|
+
1200, # /32
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
# Amount of IP space needed at each mask length (frequency at this length, plus the rollup of subdivided nets)
|
|
49
|
+
# Start calculation from the /32 frequency:
|
|
50
|
+
CUMULATIVE_BY_MASK_LENGTH = [FREQUENCY_BY_MASK_LENGTH[-1]]
|
|
51
|
+
# Then work backwards to each parent mask length
|
|
52
|
+
for mask_len in range(len(FREQUENCY_BY_MASK_LENGTH) - 2, -1, -1): # 31, ... 0
|
|
53
|
+
CUMULATIVE_BY_MASK_LENGTH.append(CUMULATIVE_BY_MASK_LENGTH[-1] // 2 + FREQUENCY_BY_MASK_LENGTH[mask_len])
|
|
54
|
+
# Reverse the list to get order by ascending prefix length, same as FREQUENCY_BY_MASK_LENGTH
|
|
55
|
+
CUMULATIVE_BY_MASK_LENGTH.reverse()
|
|
56
|
+
|
|
57
|
+
# Chance to stop subdividing at any given prefix length and create a network Prefix with this length
|
|
58
|
+
CHANCE_TO_STOP = [
|
|
59
|
+
0 if not cumulative else frequency / cumulative
|
|
60
|
+
for frequency, cumulative in zip(FREQUENCY_BY_MASK_LENGTH, CUMULATIVE_BY_MASK_LENGTH)
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def maybe_subdivide(network):
|
|
65
|
+
"""
|
|
66
|
+
Generator for recursively and probabilistically subdividing a network into subnets.
|
|
67
|
+
|
|
68
|
+
Yields:
|
|
69
|
+
IPNetwork: each constructed subdivision of the given network
|
|
70
|
+
"""
|
|
71
|
+
if random.random() < CHANCE_TO_STOP[network.prefixlen]: # noqa: S311 # suspicious-non-cryptographic-random-usage
|
|
72
|
+
# Do not subdivide any further
|
|
73
|
+
yield network
|
|
74
|
+
else:
|
|
75
|
+
# Split it into its two child networks and recurse
|
|
76
|
+
subnets = network.subnet(network.prefixlen + 1)
|
|
77
|
+
for subnet in subnets:
|
|
78
|
+
yield from maybe_subdivide(subnet)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def maybe_random_instance(queryset, chance_of_none=0.75):
|
|
82
|
+
"""
|
|
83
|
+
Helper function - randomly return either a random instance of the given queryset or None.
|
|
84
|
+
"""
|
|
85
|
+
if random.random() < chance_of_none: # noqa: S311 # suspicious-non-cryptographic-random-usage
|
|
86
|
+
return None
|
|
87
|
+
return random.choice(queryset) # noqa: S311 # suspicious-non-cryptographic-random-usage
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def create_prefixes_and_ips(initial_subnet: str, apps=apps, seed="Nautobot"): # pylint: disable=redefined-outer-name
|
|
91
|
+
"""
|
|
92
|
+
Create many (Nautobot 1.x) Prefix and IPAddress records under a given initial_subnet.
|
|
93
|
+
|
|
94
|
+
The specific records created are pseudo-random (consistent for any given `initial_subnet` and `seed` values),
|
|
95
|
+
but will *in general* consist of about 95% coverage of the subnet by non-overlapping Prefix partitions and about
|
|
96
|
+
5% coverage of the subnet by individual IPAddress records. Additionally, about 10% of Prefixes and IPAddresses
|
|
97
|
+
respectively will be duplicated one or more times.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
initial_subnet (str): The parent subnet ("10.0.0.0/16") that will encompass the Prefix and IPAddress records.
|
|
101
|
+
apps: Django application registry containing definitions of the (historical) Prefix, IPAddress, etc. models.
|
|
102
|
+
seed: Random generator seed to ensure reproducible pseudo-random construction of the data.
|
|
103
|
+
"""
|
|
104
|
+
IPAddress = apps.get_model("ipam", "IPAddress")
|
|
105
|
+
Prefix = apps.get_model("ipam", "Prefix")
|
|
106
|
+
Status = apps.get_model("extras", "Status")
|
|
107
|
+
Tenant = apps.get_model("tenancy", "Tenant")
|
|
108
|
+
VRF = apps.get_model("ipam", "VRF")
|
|
109
|
+
|
|
110
|
+
print(f"Seeding the PRNG with seed {seed}")
|
|
111
|
+
random.seed(seed) # suspicious-non-cryptographic-random-usage
|
|
112
|
+
|
|
113
|
+
status_active, _ = Status.objects.get_or_create(name="Active", defaults={"slug": "active"})
|
|
114
|
+
|
|
115
|
+
for i in range(1, 11):
|
|
116
|
+
Tenant.objects.create(name=f"{initial_subnet} Tenant {i}")
|
|
117
|
+
VRF.objects.create(name=f"{initial_subnet} VRF {i}", enforce_unique=False) # TODO should some enforce_unique?
|
|
118
|
+
|
|
119
|
+
all_tenants = list(Tenant.objects.all())
|
|
120
|
+
all_vrfs = list(VRF.objects.all())
|
|
121
|
+
|
|
122
|
+
print(f"Creating Prefixes to subdivide {initial_subnet}")
|
|
123
|
+
unique_prefix_count = 0
|
|
124
|
+
duplicate_prefix_count = 0
|
|
125
|
+
for subnet in maybe_subdivide(IPNetwork(initial_subnet)):
|
|
126
|
+
if random.random() < 0.95: # noqa: S311 # suspicious-non-cryptographic-random-usage
|
|
127
|
+
# 95% chance to create any given Prefix
|
|
128
|
+
Prefix.objects.create(
|
|
129
|
+
network=str(subnet.network),
|
|
130
|
+
broadcast=str(subnet.broadcast if subnet.broadcast else subnet[-1]),
|
|
131
|
+
prefix_length=subnet.prefixlen,
|
|
132
|
+
status=status_active,
|
|
133
|
+
tenant=maybe_random_instance(all_tenants),
|
|
134
|
+
vrf=maybe_random_instance(all_vrfs),
|
|
135
|
+
)
|
|
136
|
+
unique_prefix_count += 1
|
|
137
|
+
while random.random() < 0.1: # noqa: S311 # suspicious-non-cryptographic-random-usage
|
|
138
|
+
# 10% repeating chance to create a duplicate(s) of this Prefix
|
|
139
|
+
Prefix.objects.create(
|
|
140
|
+
network=str(subnet.network),
|
|
141
|
+
broadcast=str(subnet.broadcast if subnet.broadcast else subnet[-1]),
|
|
142
|
+
prefix_length=subnet.prefixlen,
|
|
143
|
+
status=status_active,
|
|
144
|
+
tenant=maybe_random_instance(all_tenants),
|
|
145
|
+
vrf=maybe_random_instance(all_vrfs),
|
|
146
|
+
)
|
|
147
|
+
duplicate_prefix_count += 1
|
|
148
|
+
print(f"Created {unique_prefix_count} unique Prefixes and {duplicate_prefix_count} duplicates")
|
|
149
|
+
|
|
150
|
+
print(f"Creating IPAddresses within {initial_subnet}")
|
|
151
|
+
unique_ip_count = 0
|
|
152
|
+
duplicate_ip_count = 0
|
|
153
|
+
for ip in IPNetwork(initial_subnet):
|
|
154
|
+
if random.random() < 0.05: # noqa: S311 # suspicious-non-cryptographic-random-usage
|
|
155
|
+
# 5% chance to create any given IP address
|
|
156
|
+
network = IPNetwork(ip)
|
|
157
|
+
IPAddress.objects.create(
|
|
158
|
+
host=str(network.ip),
|
|
159
|
+
broadcast=str(network.broadcast if network.broadcast else network[-1]),
|
|
160
|
+
prefix_length=network.prefixlen,
|
|
161
|
+
status=status_active,
|
|
162
|
+
tenant=maybe_random_instance(all_tenants),
|
|
163
|
+
vrf=maybe_random_instance(all_vrfs),
|
|
164
|
+
)
|
|
165
|
+
unique_ip_count += 1
|
|
166
|
+
while random.random() < 0.1: # noqa: S311 # suspicious-non-cryptographic-random-usage
|
|
167
|
+
# 10% repeating chance to create a duplicate(s) of this IP
|
|
168
|
+
IPAddress.objects.create(
|
|
169
|
+
host=str(network.ip),
|
|
170
|
+
broadcast=str(network.broadcast if network.broadcast else network[-1]),
|
|
171
|
+
prefix_length=network.prefixlen,
|
|
172
|
+
status=status_active,
|
|
173
|
+
tenant=maybe_random_instance(all_tenants),
|
|
174
|
+
vrf=maybe_random_instance(all_vrfs),
|
|
175
|
+
)
|
|
176
|
+
duplicate_ip_count += 1
|
|
177
|
+
print(f"Created {unique_ip_count} unique IPAddresses and {duplicate_ip_count} duplicates")
|
nautobot/ipam/views.py
CHANGED
|
@@ -16,6 +16,8 @@ from django.utils.http import urlencode
|
|
|
16
16
|
from django.views.generic import View
|
|
17
17
|
from django_tables2 import RequestConfig
|
|
18
18
|
import netaddr
|
|
19
|
+
from rest_framework.decorators import action
|
|
20
|
+
from rest_framework.response import Response
|
|
19
21
|
|
|
20
22
|
from nautobot.cloud.tables import CloudNetworkTable
|
|
21
23
|
from nautobot.core.constants import MAX_PAGE_SIZE_DEFAULT
|
|
@@ -63,27 +65,7 @@ logger = logging.getLogger(__name__)
|
|
|
63
65
|
#
|
|
64
66
|
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
"""Return counts of all IPAM objects related to the given Namespace."""
|
|
68
|
-
return {
|
|
69
|
-
"vrf_count": instance.vrfs.restrict(request.user, "view").count(),
|
|
70
|
-
"prefix_count": instance.prefixes.restrict(request.user, "view").count(),
|
|
71
|
-
"ip_address_count": instance.ip_addresses.restrict(request.user, "view").count(),
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
class NamespaceUIViewSet(
|
|
76
|
-
view_mixins.ObjectDetailViewMixin,
|
|
77
|
-
view_mixins.ObjectListViewMixin,
|
|
78
|
-
view_mixins.ObjectEditViewMixin,
|
|
79
|
-
view_mixins.ObjectDestroyViewMixin,
|
|
80
|
-
view_mixins.ObjectChangeLogViewMixin,
|
|
81
|
-
view_mixins.ObjectBulkCreateViewMixin, # 3.0 TODO: remove, no longer used
|
|
82
|
-
view_mixins.ObjectBulkDestroyViewMixin,
|
|
83
|
-
view_mixins.ObjectBulkUpdateViewMixin,
|
|
84
|
-
view_mixins.ObjectNotesViewMixin,
|
|
85
|
-
):
|
|
86
|
-
lookup_field = "pk"
|
|
68
|
+
class NamespaceUIViewSet(NautobotUIViewSet):
|
|
87
69
|
form_class = forms.NamespaceForm
|
|
88
70
|
bulk_update_form_class = forms.NamespaceBulkEditForm
|
|
89
71
|
filterset_class = filters.NamespaceFilterSet
|
|
@@ -91,134 +73,95 @@ class NamespaceUIViewSet(
|
|
|
91
73
|
queryset = Namespace.objects.all()
|
|
92
74
|
serializer_class = serializers.NamespaceSerializer
|
|
93
75
|
table_class = tables.NamespaceTable
|
|
76
|
+
object_detail_content = object_detail.ObjectDetailContent(
|
|
77
|
+
panels=(object_detail.ObjectFieldsPanel(section=SectionChoices.LEFT_HALF, weight=100, fields="__all__"),),
|
|
78
|
+
extra_tabs=(
|
|
79
|
+
object_detail.DistinctViewTab(
|
|
80
|
+
weight=800,
|
|
81
|
+
tab_id="vrfs",
|
|
82
|
+
label="VRFs",
|
|
83
|
+
url_name="ipam:namespace_vrfs",
|
|
84
|
+
related_object_attribute="vrfs",
|
|
85
|
+
),
|
|
86
|
+
object_detail.DistinctViewTab(
|
|
87
|
+
weight=900,
|
|
88
|
+
tab_id="prefixes",
|
|
89
|
+
label="Prefixes",
|
|
90
|
+
url_name="ipam:namespace_prefixes",
|
|
91
|
+
related_object_attribute="prefixes",
|
|
92
|
+
),
|
|
93
|
+
object_detail.DistinctViewTab(
|
|
94
|
+
weight=1000,
|
|
95
|
+
tab_id="ip_addresses",
|
|
96
|
+
label="IP Addresses",
|
|
97
|
+
url_name="ipam:namespace_ip_addresses",
|
|
98
|
+
related_object_attribute="ip_addresses",
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
)
|
|
94
102
|
|
|
95
103
|
def get_extra_context(self, request, instance):
|
|
96
104
|
context = super().get_extra_context(request, instance)
|
|
97
|
-
|
|
98
|
-
if self.action == "retrieve":
|
|
99
|
-
context.update(get_namespace_related_counts(instance, request))
|
|
100
|
-
|
|
105
|
+
context.update({"object_detail_content": self.object_detail_content})
|
|
101
106
|
return context
|
|
102
107
|
|
|
108
|
+
@action(detail=True, url_path="vrfs")
|
|
109
|
+
def vrfs(self, request, *args, **kwargs):
|
|
110
|
+
instance = self.get_object()
|
|
111
|
+
vrfs = instance.vrfs.restrict(request.user, "view")
|
|
112
|
+
vrf_table = tables.VRFTable(
|
|
113
|
+
data=vrfs,
|
|
114
|
+
user=request.user,
|
|
115
|
+
exclude=["namespace"],
|
|
116
|
+
)
|
|
117
|
+
if request.user.has_perm("ipam.change_vrf") or request.user.has_perm("ipam.delete_vrf"):
|
|
118
|
+
vrf_table.columns.show("pk")
|
|
103
119
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def get_extra_context(self, request, instance):
|
|
109
|
-
# Find all IPAddresses belonging to this Namespace
|
|
110
|
-
ip_addresses = instance.ip_addresses.restrict(request.user, "view").select_related("role", "status", "tenant")
|
|
111
|
-
|
|
112
|
-
ip_address_table = tables.IPAddressTable(ip_addresses, exclude=["namespace"])
|
|
113
|
-
if request.user.has_perm("ipam.change_ipaddress") or request.user.has_perm("ipam.delete_ipaddress"):
|
|
114
|
-
ip_address_table.columns.show("pk")
|
|
115
|
-
|
|
116
|
-
paginate = {
|
|
117
|
-
"paginator_class": EnhancedPaginator,
|
|
118
|
-
"per_page": get_paginate_count(request),
|
|
119
|
-
}
|
|
120
|
-
RequestConfig(request, paginate).configure(ip_address_table)
|
|
121
|
-
|
|
122
|
-
# Compile permissions list for rendering the object table
|
|
123
|
-
permissions = {
|
|
124
|
-
"add": request.user.has_perm("ipam.add_ipaddress"),
|
|
125
|
-
"change": request.user.has_perm("ipam.change_ipaddress"),
|
|
126
|
-
"delete": request.user.has_perm("ipam.delete_ipaddress"),
|
|
127
|
-
}
|
|
128
|
-
bulk_querystring = f"namespace={instance.id}"
|
|
129
|
-
|
|
130
|
-
context = super().get_extra_context(request, instance)
|
|
131
|
-
context.update(
|
|
120
|
+
RequestConfig(
|
|
121
|
+
request, paginate={"paginator_class": EnhancedPaginator, "per_page": get_paginate_count(request)}
|
|
122
|
+
).configure(vrf_table)
|
|
123
|
+
return Response(
|
|
132
124
|
{
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"bulk_querystring": bulk_querystring,
|
|
136
|
-
"active_tab": "ip-addresses",
|
|
125
|
+
"vrf_table": vrf_table,
|
|
126
|
+
"active_tab": "vrfs",
|
|
137
127
|
}
|
|
138
128
|
)
|
|
139
|
-
context.update(get_namespace_related_counts(instance, request))
|
|
140
129
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
class NamespacePrefixesView(generic.ObjectView):
|
|
145
|
-
queryset = Namespace.objects.all()
|
|
146
|
-
template_name = "ipam/namespace_prefixes.html"
|
|
147
|
-
|
|
148
|
-
def get_extra_context(self, request, instance):
|
|
149
|
-
# Find all Prefixes belonging to this Namespace
|
|
130
|
+
@action(detail=True, url_path="prefixes")
|
|
131
|
+
def prefixes(self, request, *args, **kwargs):
|
|
132
|
+
instance = self.get_object()
|
|
150
133
|
prefixes = instance.prefixes.restrict(request.user, "view").select_related("status")
|
|
151
|
-
|
|
152
|
-
prefix_table = tables.PrefixTable(prefixes, exclude=["namespace"])
|
|
134
|
+
prefix_table = tables.PrefixTable(data=prefixes, user=request.user, exclude=["namespace"])
|
|
153
135
|
if request.user.has_perm("ipam.change_prefix") or request.user.has_perm("ipam.delete_prefix"):
|
|
154
136
|
prefix_table.columns.show("pk")
|
|
155
137
|
|
|
156
|
-
|
|
157
|
-
"paginator_class": EnhancedPaginator,
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
RequestConfig(request, paginate).configure(prefix_table)
|
|
161
|
-
|
|
162
|
-
# Compile permissions list for rendering the object table
|
|
163
|
-
permissions = {
|
|
164
|
-
"add": request.user.has_perm("ipam.add_prefix"),
|
|
165
|
-
"change": request.user.has_perm("ipam.change_prefix"),
|
|
166
|
-
"delete": request.user.has_perm("ipam.delete_prefix"),
|
|
167
|
-
}
|
|
168
|
-
bulk_querystring = f"namespace={instance.id}"
|
|
169
|
-
|
|
170
|
-
context = super().get_extra_context(request, instance)
|
|
171
|
-
context.update(
|
|
138
|
+
RequestConfig(
|
|
139
|
+
request, paginate={"paginator_class": EnhancedPaginator, "per_page": get_paginate_count(request)}
|
|
140
|
+
).configure(prefix_table)
|
|
141
|
+
return Response(
|
|
172
142
|
{
|
|
173
143
|
"prefix_table": prefix_table,
|
|
174
|
-
"permissions": permissions,
|
|
175
|
-
"bulk_querystring": bulk_querystring,
|
|
176
144
|
"active_tab": "prefixes",
|
|
177
145
|
}
|
|
178
146
|
)
|
|
179
|
-
context.update(get_namespace_related_counts(instance, request))
|
|
180
|
-
|
|
181
|
-
return context
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
class NamespaceVRFsView(generic.ObjectView):
|
|
185
|
-
queryset = Namespace.objects.all()
|
|
186
|
-
template_name = "ipam/namespace_vrfs.html"
|
|
187
|
-
|
|
188
|
-
def get_extra_context(self, request, instance):
|
|
189
|
-
# Find all VRFs belonging to this Namespace
|
|
190
|
-
vrfs = instance.vrfs.restrict(request.user, "view")
|
|
191
|
-
|
|
192
|
-
vrf_table = tables.VRFTable(vrfs, exclude=["namespace"])
|
|
193
|
-
if request.user.has_perm("ipam.change_vrf") or request.user.has_perm("ipam.delete_vrf"):
|
|
194
|
-
vrf_table.columns.show("pk")
|
|
195
|
-
|
|
196
|
-
paginate = {
|
|
197
|
-
"paginator_class": EnhancedPaginator,
|
|
198
|
-
"per_page": get_paginate_count(request),
|
|
199
|
-
}
|
|
200
|
-
RequestConfig(request, paginate).configure(vrf_table)
|
|
201
147
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
148
|
+
@action(detail=True, url_path="ip-addresses", url_name="ip_addresses")
|
|
149
|
+
def ip_addresses(self, request, *args, **kwargs):
|
|
150
|
+
instance = self.get_object()
|
|
151
|
+
ip_addresses = instance.ip_addresses.restrict(request.user, "view").select_related("role", "status", "tenant")
|
|
152
|
+
ip_address_table = tables.IPAddressTable(data=ip_addresses, user=request.user, exclude=["namespace"])
|
|
153
|
+
if request.user.has_perm("ipam.change_ipaddress") or request.user.has_perm("ipam.delete_ipaddress"):
|
|
154
|
+
ip_address_table.columns.show("pk")
|
|
209
155
|
|
|
210
|
-
|
|
211
|
-
|
|
156
|
+
RequestConfig(
|
|
157
|
+
request, paginate={"paginator_class": EnhancedPaginator, "per_page": get_paginate_count(request)}
|
|
158
|
+
).configure(ip_address_table)
|
|
159
|
+
return Response(
|
|
212
160
|
{
|
|
213
|
-
"
|
|
214
|
-
"
|
|
215
|
-
"bulk_querystring": bulk_querystring,
|
|
216
|
-
"active_tab": "vrfs",
|
|
161
|
+
"ip_address_table": ip_address_table,
|
|
162
|
+
"active_tab": "ip_addresses",
|
|
217
163
|
}
|
|
218
164
|
)
|
|
219
|
-
context.update(get_namespace_related_counts(instance, request))
|
|
220
|
-
|
|
221
|
-
return context
|
|
222
165
|
|
|
223
166
|
|
|
224
167
|
#
|
|
@@ -325,49 +268,32 @@ class RouteTargetUIViewSet(NautobotUIViewSet):
|
|
|
325
268
|
#
|
|
326
269
|
|
|
327
270
|
|
|
328
|
-
class
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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):
|
|
271
|
+
class RIRUIViewSet(NautobotUIViewSet):
|
|
272
|
+
bulk_update_form_class = forms.RIRBulkEditForm
|
|
273
|
+
filterset_class = filters.RIRFilterSet
|
|
274
|
+
filterset_form_class = forms.RIRFilterForm
|
|
275
|
+
form_class = forms.RIRForm
|
|
354
276
|
queryset = RIR.objects.all()
|
|
355
|
-
|
|
356
|
-
|
|
277
|
+
serializer_class = serializers.RIRSerializer
|
|
278
|
+
table_class = tables.RIRTable
|
|
357
279
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
280
|
+
object_detail_content = object_detail.ObjectDetailContent(
|
|
281
|
+
panels=(
|
|
282
|
+
object_detail.ObjectFieldsPanel(
|
|
283
|
+
section=SectionChoices.LEFT_HALF,
|
|
284
|
+
weight=100,
|
|
285
|
+
fields="__all__",
|
|
286
|
+
),
|
|
287
|
+
object_detail.ObjectsTablePanel(
|
|
288
|
+
section=SectionChoices.FULL_WIDTH,
|
|
289
|
+
weight=100,
|
|
290
|
+
table_title="Assigned Prefixes",
|
|
291
|
+
table_class=tables.PrefixTable,
|
|
292
|
+
table_filter="rir",
|
|
293
|
+
hide_hierarchy_ui=True,
|
|
294
|
+
),
|
|
295
|
+
),
|
|
296
|
+
)
|
|
371
297
|
|
|
372
298
|
|
|
373
299
|
#
|
|
@@ -1373,19 +1299,7 @@ class VLANBulkDeleteView(generic.BulkDeleteView):
|
|
|
1373
1299
|
#
|
|
1374
1300
|
|
|
1375
1301
|
|
|
1376
|
-
class
|
|
1377
|
-
queryset = Service.objects.all()
|
|
1378
|
-
filterset = filters.ServiceFilterSet
|
|
1379
|
-
filterset_form = forms.ServiceFilterForm
|
|
1380
|
-
table = tables.ServiceTable
|
|
1381
|
-
action_buttons = ("add", "import", "export")
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
class ServiceView(generic.ObjectView):
|
|
1385
|
-
queryset = Service.objects.prefetch_related("ip_addresses")
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
class ServiceEditView(generic.ObjectEditView):
|
|
1302
|
+
class ServiceEditView(generic.ObjectEditView): # This view is used to assign services to devices and VMs
|
|
1389
1303
|
queryset = Service.objects.prefetch_related("ip_addresses")
|
|
1390
1304
|
model_form = forms.ServiceForm
|
|
1391
1305
|
template_name = "ipam/service_edit.html"
|
|
@@ -1401,23 +1315,30 @@ class ServiceEditView(generic.ObjectEditView):
|
|
|
1401
1315
|
return obj
|
|
1402
1316
|
|
|
1403
1317
|
|
|
1404
|
-
class
|
|
1405
|
-
|
|
1406
|
-
|
|
1318
|
+
class ServiceUIViewSet(NautobotUIViewSet): # 3.0 TODO: remove, unused BulkImportView
|
|
1319
|
+
model = Service
|
|
1320
|
+
bulk_update_form_class = forms.ServiceBulkEditForm
|
|
1321
|
+
filterset_class = filters.ServiceFilterSet
|
|
1322
|
+
filterset_form_class = forms.ServiceFilterForm
|
|
1323
|
+
form_class = forms.ServiceForm
|
|
1324
|
+
queryset = Service.objects.select_related("device", "virtual_machine").prefetch_related("ip_addresses")
|
|
1325
|
+
serializer_class = serializers.ServiceSerializer
|
|
1326
|
+
table_class = tables.ServiceTable
|
|
1407
1327
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1328
|
+
object_detail_content = object_detail.ObjectDetailContent(
|
|
1329
|
+
panels=(
|
|
1330
|
+
object_detail.ObjectFieldsPanel(
|
|
1331
|
+
section=SectionChoices.LEFT_HALF,
|
|
1332
|
+
weight=100,
|
|
1333
|
+
fields=["name", "parent", "protocol", "port_list", "description"],
|
|
1334
|
+
),
|
|
1335
|
+
object_detail.ObjectsTablePanel(
|
|
1336
|
+
weight=200,
|
|
1337
|
+
section=SectionChoices.RIGHT_HALF,
|
|
1338
|
+
table_class=tables.IPAddressTable,
|
|
1339
|
+
table_filter="services",
|
|
1340
|
+
select_related_fields=["tenant", "status", "role"],
|
|
1341
|
+
add_button_route=None,
|
|
1342
|
+
),
|
|
1343
|
+
)
|
|
1344
|
+
)
|
|
@@ -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>
|
|
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
|
|
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">¶</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>
|
|
10954
|
+
<code>Any</code>
|
|
10918
10955
|
</td>
|
|
10919
10956
|
<td>
|
|
10920
10957
|
<div class="doc-md-description">
|
|
10921
|
-
<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>
|