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
|
@@ -532,6 +532,7 @@ class Panel(Component):
|
|
|
532
532
|
self,
|
|
533
533
|
*,
|
|
534
534
|
label="",
|
|
535
|
+
css_class="default",
|
|
535
536
|
section=SectionChoices.FULL_WIDTH,
|
|
536
537
|
body_id=None,
|
|
537
538
|
body_content_template_path=None,
|
|
@@ -546,6 +547,7 @@ class Panel(Component):
|
|
|
546
547
|
|
|
547
548
|
Args:
|
|
548
549
|
label (str): Label to display for this panel. Optional; if an empty string, the panel will have no label.
|
|
550
|
+
css_class (str): Panel variant to render as, e.g. "default", "warning", "info".
|
|
549
551
|
section (str): One of the [`SectionChoices`](./ui.md#nautobot.apps.ui.SectionChoices) values, indicating the layout section this Panel belongs to.
|
|
550
552
|
body_id (str): HTML element `id` to attach to the rendered body wrapper of the panel.
|
|
551
553
|
body_content_template_path (str): Template path to render the content contained *within* the panel body.
|
|
@@ -557,6 +559,7 @@ class Panel(Component):
|
|
|
557
559
|
(a `div` or `table`) as well as its contents. Generally you won't override this as a user.
|
|
558
560
|
"""
|
|
559
561
|
self.label = label
|
|
562
|
+
self.css_class = css_class
|
|
560
563
|
self.section = section
|
|
561
564
|
self.body_id = body_id
|
|
562
565
|
self.body_content_template_path = body_content_template_path
|
|
@@ -583,6 +586,7 @@ class Panel(Component):
|
|
|
583
586
|
self.template_path,
|
|
584
587
|
context,
|
|
585
588
|
label=self.render_label(context),
|
|
589
|
+
css_class=self.css_class,
|
|
586
590
|
header_extra_content=self.render_header_extra_content(context),
|
|
587
591
|
body=self.render_body(context),
|
|
588
592
|
footer_content=self.render_footer_content(context),
|
|
@@ -1609,7 +1613,7 @@ class StatsPanel(Panel):
|
|
|
1609
1613
|
instance = get_obj_from_context(context)
|
|
1610
1614
|
request = context["request"]
|
|
1611
1615
|
if isinstance(instance, TreeModel):
|
|
1612
|
-
self.filter_pks = (
|
|
1616
|
+
self.filter_pks = list(
|
|
1613
1617
|
instance.descendants(include_self=True).restrict(request.user, "view").values_list("pk", flat=True)
|
|
1614
1618
|
)
|
|
1615
1619
|
else:
|
|
@@ -1625,9 +1629,10 @@ class StatsPanel(Panel):
|
|
|
1625
1629
|
else:
|
|
1626
1630
|
related_object_model_class, query = related_field, f"{self.filter_name}__in"
|
|
1627
1631
|
filter_dict = {query: self.filter_pks}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1632
|
+
qs = related_object_model_class.objects.restrict(request.user, "view").filter(**filter_dict)
|
|
1633
|
+
if len(self.filter_pks) > 1:
|
|
1634
|
+
qs = qs.distinct()
|
|
1635
|
+
related_object_count = qs.count()
|
|
1631
1636
|
related_object_model_class_meta = related_object_model_class._meta
|
|
1632
1637
|
related_object_list_url = validated_viewname(related_object_model_class, "list")
|
|
1633
1638
|
related_object_title = bettertitle(related_object_model_class_meta.verbose_name_plural)
|
|
@@ -2034,6 +2039,36 @@ class _ObjectDetailContactsTab(Tab):
|
|
|
2034
2039
|
)
|
|
2035
2040
|
|
|
2036
2041
|
|
|
2042
|
+
class DynamicGroupsTextPanel(BaseTextPanel):
|
|
2043
|
+
"""Panel displaying a note about caching of dynamic groups."""
|
|
2044
|
+
|
|
2045
|
+
def __init__(
|
|
2046
|
+
self,
|
|
2047
|
+
*,
|
|
2048
|
+
weight,
|
|
2049
|
+
render_as=BaseTextPanel.RenderOptions.MARKDOWN,
|
|
2050
|
+
label="Dynamic Group caching",
|
|
2051
|
+
css_class="warning",
|
|
2052
|
+
**kwargs,
|
|
2053
|
+
):
|
|
2054
|
+
super().__init__(weight=weight, render_as=render_as, label=label, css_class=css_class, **kwargs)
|
|
2055
|
+
|
|
2056
|
+
def get_value(self, context):
|
|
2057
|
+
dg_list_url = reverse("extras:dynamicgroup_list")
|
|
2058
|
+
job_run_url = reverse(
|
|
2059
|
+
"extras:job_run_by_class_path",
|
|
2060
|
+
kwargs={"class_path": "nautobot.core.jobs.groups.RefreshDynamicGroupCaches"},
|
|
2061
|
+
)
|
|
2062
|
+
return (
|
|
2063
|
+
"Dynamic group membership is cached for performance reasons, "
|
|
2064
|
+
"therefore this page may not always be up-to-date.\n\n"
|
|
2065
|
+
"You can refresh the membership of any specific group by accessing it from the list below or from the "
|
|
2066
|
+
f'[Dynamic Groups list view]({dg_list_url}) and clicking the "Refresh Members" button.\n\n'
|
|
2067
|
+
"You can also refresh the membership of **all** groups by running the "
|
|
2068
|
+
f"[Refresh Dynamic Group Caches job]({job_run_url})."
|
|
2069
|
+
)
|
|
2070
|
+
|
|
2071
|
+
|
|
2037
2072
|
@dataclass
|
|
2038
2073
|
class _ObjectDetailGroupsTab(Tab):
|
|
2039
2074
|
"""Built-in class for a Tab displaying information about associated dynamic groups."""
|
|
@@ -2050,8 +2085,9 @@ class _ObjectDetailGroupsTab(Tab):
|
|
|
2050
2085
|
):
|
|
2051
2086
|
if panels is None:
|
|
2052
2087
|
panels = (
|
|
2088
|
+
DynamicGroupsTextPanel(weight=100),
|
|
2053
2089
|
ObjectsTablePanel(
|
|
2054
|
-
weight=
|
|
2090
|
+
weight=200,
|
|
2055
2091
|
table_class=DynamicGroupTable,
|
|
2056
2092
|
table_attribute="dynamic_groups",
|
|
2057
2093
|
exclude_columns=["content_type"],
|
nautobot/core/utils/filtering.py
CHANGED
|
@@ -17,6 +17,15 @@ from nautobot.core.utils.lookup import get_filterset_for_model
|
|
|
17
17
|
# e.g `name__ic` has lookup expr `ic (icontains)` while `name` has no lookup expr
|
|
18
18
|
CONTAINS_LOOKUP_EXPR_RE = re.compile(r"(?<=__)\w+")
|
|
19
19
|
|
|
20
|
+
MODEL_VERBOSE_NAME_PLURAL_TO_FEATURE_NAME_MAPPING = {
|
|
21
|
+
"cables": "cable_terminations",
|
|
22
|
+
"metadata_types": "metadata",
|
|
23
|
+
"object_metadata": "metadata",
|
|
24
|
+
"location_types": "locations",
|
|
25
|
+
"static_group_associations": "dynamic_groups",
|
|
26
|
+
"relationship_associations": "relationships",
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
|
|
21
30
|
def build_lookup_label(field_name, _verbose_name):
|
|
22
31
|
"""
|
|
@@ -131,18 +140,11 @@ def get_filterset_parameter_form_field(model, parameter, filterset=None):
|
|
|
131
140
|
elif isinstance(
|
|
132
141
|
field, ContentTypeMultipleChoiceFilter
|
|
133
142
|
): # While there are other objects using `ContentTypeMultipleChoiceFilter`, the case where
|
|
134
|
-
# models that have such a filter and the `verbose_name_plural`
|
|
143
|
+
# models that have such a filter and the `verbose_name_plural` does not match, we can lookup the feature name.
|
|
135
144
|
from nautobot.core.models.fields import slugify_dashes_to_underscores # Avoid circular import
|
|
136
145
|
|
|
137
146
|
plural_name = slugify_dashes_to_underscores(model._meta.verbose_name_plural)
|
|
138
|
-
|
|
139
|
-
# Cable-connectable models use "cable_terminations", not "cables", as the feature name
|
|
140
|
-
if plural_name == "cables":
|
|
141
|
-
plural_name = "cable_terminations"
|
|
142
|
-
elif plural_name == "metadata_types":
|
|
143
|
-
plural_name = "metadata"
|
|
144
|
-
elif plural_name == "object_metadata":
|
|
145
|
-
plural_name = "metadata"
|
|
147
|
+
plural_name = MODEL_VERBOSE_NAME_PLURAL_TO_FEATURE_NAME_MAPPING.get(plural_name, plural_name)
|
|
146
148
|
try:
|
|
147
149
|
form_field = MultipleContentTypeField(choices_as_strings=True, feature=plural_name)
|
|
148
150
|
except KeyError:
|
nautobot/core/views/generic.py
CHANGED
|
@@ -580,7 +580,7 @@ class ObjectDeleteView(UIComponentsMixin, GetReturnURLMixin, ObjectPermissionReq
|
|
|
580
580
|
"""
|
|
581
581
|
|
|
582
582
|
queryset: Optional[QuerySet] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
583
|
-
template_name = "generic/
|
|
583
|
+
template_name = "generic/object_destroy.html"
|
|
584
584
|
|
|
585
585
|
def get_required_permission(self):
|
|
586
586
|
return get_permission_for_model(self.queryset.model, "delete")
|
|
@@ -1037,7 +1037,7 @@ class BulkEditView(
|
|
|
1037
1037
|
filterset: Optional[type[FilterSet]] = None
|
|
1038
1038
|
table: Optional[type[Table]] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
1039
1039
|
form: Optional[type[Form]] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
1040
|
-
template_name = "generic/
|
|
1040
|
+
template_name = "generic/object_bulk_update.html"
|
|
1041
1041
|
|
|
1042
1042
|
def get_required_permission(self):
|
|
1043
1043
|
return get_permission_for_model(self.queryset.model, "change")
|
|
@@ -1239,7 +1239,7 @@ class BulkDeleteView(
|
|
|
1239
1239
|
filterset: Optional[type[FilterSet]] = None
|
|
1240
1240
|
table: Optional[type[Table]] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
1241
1241
|
form: Optional[type[Form]] = None
|
|
1242
|
-
template_name = "generic/
|
|
1242
|
+
template_name = "generic/object_bulk_destroy.html"
|
|
1243
1243
|
|
|
1244
1244
|
def get_required_permission(self):
|
|
1245
1245
|
return get_permission_for_model(self.queryset.model, "delete")
|
|
@@ -6,7 +6,7 @@ from django.contrib.contenttypes.models import ContentType
|
|
|
6
6
|
from django.core.cache import cache
|
|
7
7
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
8
8
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
|
9
|
-
from django.db import models
|
|
9
|
+
from django.db import models
|
|
10
10
|
from django.db.models import Sum
|
|
11
11
|
from django.utils.functional import classproperty
|
|
12
12
|
|
|
@@ -595,6 +595,86 @@ class BaseInterface(RelationshipModel):
|
|
|
595
595
|
|
|
596
596
|
return super().save(*args, **kwargs)
|
|
597
597
|
|
|
598
|
+
def add_ip_addresses(
|
|
599
|
+
self,
|
|
600
|
+
ip_addresses,
|
|
601
|
+
is_source=False,
|
|
602
|
+
is_destination=False,
|
|
603
|
+
is_default=False,
|
|
604
|
+
is_preferred=False,
|
|
605
|
+
is_primary=False,
|
|
606
|
+
is_secondary=False,
|
|
607
|
+
is_standby=False,
|
|
608
|
+
):
|
|
609
|
+
"""Add one or more IPAddress instances to this interface's `ip_addresses` many-to-many relationship.
|
|
610
|
+
|
|
611
|
+
Args:
|
|
612
|
+
ip_addresses (:obj:`list` or `IPAddress`): Instance of `nautobot.ipam.models.IPAddress` or list of `IPAddress` instances.
|
|
613
|
+
is_source (bool, optional): Is source address. Defaults to False.
|
|
614
|
+
is_destination (bool, optional): Is destination address. Defaults to False.
|
|
615
|
+
is_default (bool, optional): Is default address. Defaults to False.
|
|
616
|
+
is_preferred (bool, optional): Is preferred address. Defaults to False.
|
|
617
|
+
is_primary (bool, optional): Is primary address. Defaults to False.
|
|
618
|
+
is_secondary (bool, optional): Is secondary address. Defaults to False.
|
|
619
|
+
is_standby (bool, optional): Is standby address. Defaults to False.
|
|
620
|
+
|
|
621
|
+
Returns:
|
|
622
|
+
Number of instances added.
|
|
623
|
+
"""
|
|
624
|
+
through_defaults = {
|
|
625
|
+
"is_source": is_source,
|
|
626
|
+
"is_destination": is_destination,
|
|
627
|
+
"is_default": is_default,
|
|
628
|
+
"is_preferred": is_preferred,
|
|
629
|
+
"is_primary": is_primary,
|
|
630
|
+
"is_secondary": is_secondary,
|
|
631
|
+
"is_standby": is_standby,
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if not isinstance(ip_addresses, (tuple, list)):
|
|
635
|
+
ip_addresses = [ip_addresses]
|
|
636
|
+
|
|
637
|
+
# This ensures that ips_to_add only contains IPs which need to be added to the interface. This ensures
|
|
638
|
+
# that len(ips_to_add) accurately represents the results of the action.
|
|
639
|
+
ips_to_add = set(ip_addresses) - set(self.ip_addresses.all())
|
|
640
|
+
|
|
641
|
+
if ips_to_add:
|
|
642
|
+
self.ip_addresses.add(*ips_to_add, through_defaults=through_defaults) # pylint: disable=no-member # Intf/VMIntf both have ip_addresses
|
|
643
|
+
|
|
644
|
+
return len(ips_to_add)
|
|
645
|
+
|
|
646
|
+
add_ip_addresses.alters_data = True
|
|
647
|
+
|
|
648
|
+
def remove_ip_addresses(self, ip_addresses):
|
|
649
|
+
"""Remove one or more IPAddress instances from this interface's `ip_addresses` many-to-many relationship.
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
ip_addresses (:obj:`list` or `IPAddress`): Instance of `nautobot.ipam.models.IPAddress` or list of `IPAddress` instances.
|
|
653
|
+
|
|
654
|
+
Returns:
|
|
655
|
+
Number of instances removed.
|
|
656
|
+
"""
|
|
657
|
+
if not isinstance(ip_addresses, (tuple, list)):
|
|
658
|
+
ip_addresses = [ip_addresses]
|
|
659
|
+
|
|
660
|
+
# The delete() call used previously (ref: https://github.com/nautobot/nautobot/issues/3236)
|
|
661
|
+
# meant that if None was passed in, it was silently ignored. Rather that raise an exception,
|
|
662
|
+
# this comprehension maintains backwards compatibility.
|
|
663
|
+
ip_addresses = {ip for ip in ip_addresses if ip is not None}
|
|
664
|
+
|
|
665
|
+
# This checks that the IPs passed in are actually on the interface. By populating
|
|
666
|
+
# ips_to_remove correctly, we ensure that the only IPs passed to remove() are IPs known
|
|
667
|
+
# to be on the interface. This ensures that len(ips_to_remove) accurately represents
|
|
668
|
+
# the results of the action.
|
|
669
|
+
ips_to_remove = ip_addresses & set(self.ip_addresses.all())
|
|
670
|
+
|
|
671
|
+
if ips_to_remove:
|
|
672
|
+
self.ip_addresses.remove(*ips_to_remove) # pylint: disable=no-member # Intf/VMIntf both have ip_addresses
|
|
673
|
+
|
|
674
|
+
return len(ips_to_remove)
|
|
675
|
+
|
|
676
|
+
remove_ip_addresses.alters_data = True
|
|
677
|
+
|
|
598
678
|
|
|
599
679
|
@extras_features(
|
|
600
680
|
"cable_terminations",
|
|
@@ -797,73 +877,6 @@ class Interface(ModularComponentModel, CableTermination, PathEndpoint, BaseInter
|
|
|
797
877
|
}
|
|
798
878
|
)
|
|
799
879
|
|
|
800
|
-
def add_ip_addresses(
|
|
801
|
-
self,
|
|
802
|
-
ip_addresses,
|
|
803
|
-
is_source=False,
|
|
804
|
-
is_destination=False,
|
|
805
|
-
is_default=False,
|
|
806
|
-
is_preferred=False,
|
|
807
|
-
is_primary=False,
|
|
808
|
-
is_secondary=False,
|
|
809
|
-
is_standby=False,
|
|
810
|
-
):
|
|
811
|
-
"""Add one or more IPAddress instances to this interface's `ip_addresses` many-to-many relationship.
|
|
812
|
-
|
|
813
|
-
Args:
|
|
814
|
-
ip_addresses (:obj:`list` or `IPAddress`): Instance of `nautobot.ipam.models.IPAddress` or list of `IPAddress` instances.
|
|
815
|
-
is_source (bool, optional): Is source address. Defaults to False.
|
|
816
|
-
is_destination (bool, optional): Is destination address. Defaults to False.
|
|
817
|
-
is_default (bool, optional): Is default address. Defaults to False.
|
|
818
|
-
is_preferred (bool, optional): Is preferred address. Defaults to False.
|
|
819
|
-
is_primary (bool, optional): Is primary address. Defaults to False.
|
|
820
|
-
is_secondary (bool, optional): Is secondary address. Defaults to False.
|
|
821
|
-
is_standby (bool, optional): Is standby address. Defaults to False.
|
|
822
|
-
|
|
823
|
-
Returns:
|
|
824
|
-
Number of instances added.
|
|
825
|
-
"""
|
|
826
|
-
if not isinstance(ip_addresses, (tuple, list)):
|
|
827
|
-
ip_addresses = [ip_addresses]
|
|
828
|
-
with transaction.atomic():
|
|
829
|
-
for ip in ip_addresses:
|
|
830
|
-
instance = self.ip_addresses.through(
|
|
831
|
-
ip_address=ip,
|
|
832
|
-
interface=self,
|
|
833
|
-
is_source=is_source,
|
|
834
|
-
is_destination=is_destination,
|
|
835
|
-
is_default=is_default,
|
|
836
|
-
is_preferred=is_preferred,
|
|
837
|
-
is_primary=is_primary,
|
|
838
|
-
is_secondary=is_secondary,
|
|
839
|
-
is_standby=is_standby,
|
|
840
|
-
)
|
|
841
|
-
instance.validated_save()
|
|
842
|
-
return len(ip_addresses)
|
|
843
|
-
|
|
844
|
-
add_ip_addresses.alters_data = True
|
|
845
|
-
|
|
846
|
-
def remove_ip_addresses(self, ip_addresses):
|
|
847
|
-
"""Remove one or more IPAddress instances from this interface's `ip_addresses` many-to-many relationship.
|
|
848
|
-
|
|
849
|
-
Args:
|
|
850
|
-
ip_addresses (:obj:`list` or `IPAddress`): Instance of `nautobot.ipam.models.IPAddress` or list of `IPAddress` instances.
|
|
851
|
-
|
|
852
|
-
Returns:
|
|
853
|
-
Number of instances removed.
|
|
854
|
-
"""
|
|
855
|
-
count = 0
|
|
856
|
-
if not isinstance(ip_addresses, (tuple, list)):
|
|
857
|
-
ip_addresses = [ip_addresses]
|
|
858
|
-
with transaction.atomic():
|
|
859
|
-
for ip in ip_addresses:
|
|
860
|
-
qs = self.ip_addresses.through.objects.filter(ip_address=ip, interface=self)
|
|
861
|
-
deleted_count, _ = qs.delete()
|
|
862
|
-
count += deleted_count
|
|
863
|
-
return count
|
|
864
|
-
|
|
865
|
-
remove_ip_addresses.alters_data = True
|
|
866
|
-
|
|
867
880
|
@property
|
|
868
881
|
def is_connectable(self):
|
|
869
882
|
return self.type not in NONCONNECTABLE_IFACE_TYPES
|
|
@@ -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 %}
|
|
@@ -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 %}
|
|
@@ -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 %}
|
|
@@ -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 %}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{% load perms %}
|
|
2
|
+
{% load helpers %}
|
|
3
|
+
{% if perms.dcim.contact_association %}
|
|
4
|
+
{% if object.contact_name or object.contact_phone or object.contact_email %}
|
|
5
|
+
{% with request.path|add:"?tab=contacts"|urlencode as return_url %}
|
|
6
|
+
<div class="text-right noprint">
|
|
7
|
+
<a href="{% url 'dcim:location_migrate_data_to_contact' pk=object.pk %}?return_url={{ return_url }}" class="btn btn-primary btn-xs">
|
|
8
|
+
<span class="mdi mdi-account-edit" aria-hidden="true"></span>
|
|
9
|
+
Convert to contact/team record
|
|
10
|
+
</a>
|
|
11
|
+
</div>
|
|
12
|
+
{% endwith %}
|
|
13
|
+
{% endif %}
|
|
14
|
+
{% endif %}
|