1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merge branch 'develop' into feature

This commit is contained in:
Jeremy Stretch
2021-03-09 20:04:20 -05:00
31 changed files with 368 additions and 223 deletions

View File

@@ -709,6 +709,7 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_8GFC_SFP_PLUS = '8gfc-sfpp'
TYPE_16GFC_SFP_PLUS = '16gfc-sfpp'
TYPE_32GFC_SFP28 = '32gfc-sfp28'
TYPE_64GFC_QSFP_PLUS = '64gfc-qsfpp'
TYPE_128GFC_QSFP28 = '128gfc-sfp28'
# InfiniBand
@@ -824,6 +825,7 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'),
(TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'),
(TYPE_32GFC_SFP28, 'SFP28 (32GFC)'),
(TYPE_64GFC_QSFP_PLUS, 'QSFP+ (64GFC)'),
(TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'),
)
),

View File

@@ -210,7 +210,7 @@ class PathEndpoint(models.Model):
# Console ports
#
@extras_features('custom_fields', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
"""
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
@@ -254,7 +254,7 @@ class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
# Console server ports
#
@extras_features('custom_fields', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
"""
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
@@ -298,7 +298,7 @@ class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
# Power ports
#
@extras_features('custom_fields', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class PowerPort(ComponentModel, CableTermination, PathEndpoint):
"""
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
@@ -410,7 +410,7 @@ class PowerPort(ComponentModel, CableTermination, PathEndpoint):
# Power outlets
#
@extras_features('custom_fields', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class PowerOutlet(ComponentModel, CableTermination, PathEndpoint):
"""
A physical power outlet (output) within a Device which provides power to a PowerPort.
@@ -511,7 +511,7 @@ class BaseInterface(models.Model):
return super().save(*args, **kwargs)
@extras_features('custom_fields', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
"""
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
@@ -684,7 +684,7 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
# Pass-through ports
#
@extras_features('custom_fields', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class FrontPort(ComponentModel, CableTermination):
"""
A pass-through port on the front of a Device.
@@ -750,7 +750,7 @@ class FrontPort(ComponentModel, CableTermination):
})
@extras_features('custom_fields', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class RearPort(ComponentModel, CableTermination):
"""
A pass-through port on the rear of a Device.
@@ -804,7 +804,7 @@ class RearPort(ComponentModel, CableTermination):
# Device bays
#
@extras_features('custom_fields', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class DeviceBay(ComponentModel):
"""
An empty space within a Device which can house a child device
@@ -864,7 +864,7 @@ class DeviceBay(ComponentModel):
# Inventory items
#
@extras_features('custom_fields', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class InventoryItem(MPTTModel, ComponentModel):
"""
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.

View File

@@ -1,5 +1,6 @@
import django_tables2 as tables
from django_tables2.utils import Accessor
from django.conf import settings
from dcim.models import (
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, Platform,
@@ -128,11 +129,18 @@ class DeviceTable(BaseTable):
verbose_name='Type',
text=lambda record: record.device_type.display_name
)
primary_ip = tables.Column(
linkify=True,
order_by=('primary_ip6', 'primary_ip4'),
verbose_name='IP Address'
)
if settings.PREFER_IPV4:
primary_ip = tables.Column(
linkify=True,
order_by=('primary_ip4', 'primary_ip6'),
verbose_name='IP Address'
)
else:
primary_ip = tables.Column(
linkify=True,
order_by=('primary_ip6', 'primary_ip4'),
verbose_name='IP Address'
)
primary_ip4 = tables.Column(
linkify=True,
verbose_name='IPv4 Address'

View File

@@ -1,3 +1,4 @@
from cacheops import invalidate_model
from django.apps import apps
from django.core.management.base import BaseCommand, CommandError
@@ -27,7 +28,7 @@ class Command(BaseCommand):
app_label, model_name = name.split('.')
except ValueError:
raise CommandError(
"Invalid format: {}. Models must be specified in the form app_label.ModelName.".format(name)
f"Invalid format: {name}. Models must be specified in the form app_label.ModelName."
)
try:
app_config = apps.get_app_config(app_label)
@@ -36,13 +37,13 @@ class Command(BaseCommand):
try:
model = app_config.get_model(model_name)
except LookupError:
raise CommandError("Unknown model: {}.{}".format(app_label, model_name))
raise CommandError(f"Unknown model: {app_label}.{model_name}")
fields = [
field for field in model._meta.concrete_fields if type(field) is NaturalOrderingField
]
if not fields:
raise CommandError(
"Invalid model: {}.{} does not employ natural ordering".format(app_label, model_name)
f"Invalid model: {app_label}.{model_name} does not employ natural ordering"
)
models.append(
(model, fields)
@@ -67,7 +68,7 @@ class Command(BaseCommand):
models = self._get_models(args)
if options['verbosity']:
self.stdout.write("Renaturalizing {} models.".format(len(models)))
self.stdout.write(f"Renaturalizing {len(models)} models.")
for model, fields in models:
for field in fields:
@@ -78,7 +79,7 @@ class Command(BaseCommand):
# Print the model and field name
if options['verbosity']:
self.stdout.write(
"{}.{} ({})... ".format(model._meta.label, field.target_field, field.name),
f"{model._meta.label}.{field.target_field} ({field.name})... ",
ending='\n' if options['verbosity'] >= 2 else ''
)
self.stdout.flush()
@@ -89,23 +90,26 @@ class Command(BaseCommand):
naturalized_value = naturalize(value, max_length=field.max_length)
if options['verbosity'] >= 2:
self.stdout.write(" {} -> {}".format(value, naturalized_value), ending='')
self.stdout.write(f" {value} -> {naturalized_value}", ending='')
self.stdout.flush()
# Update each unique field value in bulk
changed = model.objects.filter(name=value).update(**{field.name: naturalized_value})
if options['verbosity'] >= 2:
self.stdout.write(" ({})".format(changed))
self.stdout.write(f" ({changed})")
count += changed
# Print the total count of alterations for the field
if options['verbosity'] >= 2:
self.stdout.write(self.style.SUCCESS("{} {} updated ({} unique values)".format(
count, model._meta.verbose_name_plural, queryset.count()
)))
self.stdout.write(self.style.SUCCESS(
f"{count} {model._meta.verbose_name_plural} updated ({queryset.count()} unique values)"
))
elif options['verbosity']:
self.stdout.write(self.style.SUCCESS(str(count)))
# Invalidate cached queries
invalidate_model(model)
if options['verbosity']:
self.stdout.write(self.style.SUCCESS("Done."))

View File

@@ -192,7 +192,7 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet
field_name='prefix',
lookup_expr='family'
)
prefix = django_filters.CharFilter(
prefix = MultiValueCharFilter(
method='filter_prefix',
label='Prefix',
)
@@ -317,13 +317,13 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet
return queryset.filter(qs_filter)
def filter_prefix(self, queryset, name, value):
if not value.strip():
return queryset
try:
query = str(netaddr.IPNetwork(value).cidr)
return queryset.filter(prefix=query)
except (AddrFormatError, ValueError):
return queryset.none()
query_values = []
for v in value:
try:
query_values.append(netaddr.IPNetwork(v))
except (AddrFormatError, ValueError):
pass
return queryset.filter(prefix__in=query_values)
def search_within(self, queryset, name, value):
value = value.strip()

View File

@@ -33,7 +33,7 @@ IPADDRESS_LINK = """
{% if record.pk %}
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
{% elif perms.ipam.add_ipaddress %}
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}{% if prefix.tenant %}&tenant={{ prefix.tenant.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available</a>
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.tenant %}&tenant={{ object.tenant.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available</a>
{% else %}
{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available
{% endif %}
@@ -46,8 +46,8 @@ IPADDRESS_ASSIGN_LINK = """
VRF_LINK = """
{% if record.vrf %}
<a href="{{ record.vrf.get_absolute_url }}">{{ record.vrf }}</a>
{% elif prefix.vrf %}
{{ prefix.vrf }}
{% elif object.vrf %}
<a href="{{ object.vrf.get_absolute_url }}">{{ object.vrf }}</a>
{% else %}
Global
{% endif %}
@@ -401,6 +401,9 @@ class InterfaceIPAddressTable(BaseTable):
)
status = ChoiceFieldColumn()
tenant = TenantColumn()
actions = ButtonsColumn(
model=IPAddress
)
class Meta(BaseTable.Meta):
model = IPAddress

View File

@@ -429,6 +429,11 @@ class PrefixTestCase(TestCase):
params = {'family': '6'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
def test_prefix(self):
prefixes = Prefix.objects.all()[:2]
params = {'prefix': [prefixes[0].prefix, prefixes[1].prefix]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_is_pool(self):
params = {'is_pool': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

@@ -30,6 +30,7 @@ class VRFView(generic.ObjectView):
def get_extra_context(self, request, instance):
prefix_count = Prefix.objects.restrict(request.user, 'view').filter(vrf=instance).count()
ipaddress_count = IPAddress.objects.restrict(request.user, 'view').filter(vrf=instance).count()
import_targets_table = tables.RouteTargetTable(
instance.import_targets.prefetch_related('tenant'),
@@ -42,6 +43,7 @@ class VRFView(generic.ObjectView):
return {
'prefix_count': prefix_count,
'ipaddress_count': ipaddress_count,
'import_targets_table': import_targets_table,
'export_targets_table': export_targets_table,
}

View File

@@ -1,6 +1,7 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load perms %}
{% load custom_links %}
{% load plugins %}
{% block title %}{{ object.device }} / {{ object }}{% endblock %}

View File

@@ -1,6 +1,7 @@
{% extends 'dcim/device_component.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% block content %}
<div class="row">
@@ -241,7 +242,23 @@
</div>
<div class="row">
<div class="col-md-12">
{% include 'panel_table.html' with table=ipaddress_table heading="IP Addresses" %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>IP Addresses</strong>
</div>
{% if ipaddress_table.rows %}
{% render_table ipaddress_table 'inc/table.html' %}
{% else %}
<div class="panel-body text-muted">None</div>
{% endif %}
{% if perms.ipam.add_ipaddress %}
<div class="panel-footer text-right noprint">
<a href="{% url 'ipam:ipaddress_add' %}?device={{ object.device.pk }}&interface={{ object.pk }}" class="btn btn-xs btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add IP Address
</a>
</div>
{% endif %}
</div>
</div>
</div>
<div class="row">

View File

@@ -52,6 +52,12 @@
<td>
<a href="{% url 'ipam:prefix_list' %}?vrf_id={{ object.pk }}">{{ prefix_count }}</a>
</td>
</tr>
<tr>
<td>IP Addresses</td>
<td>
<a href="{% url 'ipam:ipaddress_list' %}?vrf_id={{ object.pk }}">{{ ipaddress_count }}</a>
</td>
</tr>
</table>
</div>

View File

@@ -1,6 +1,7 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% block title %}{{ object.virtual_machine }} / {{ object.name }}{% endblock %}
@@ -65,7 +66,23 @@
</div>
<div class="row">
<div class="col-md-12">
{% include 'panel_table.html' with table=ipaddress_table heading="IP Addresses" %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>IP Addresses</strong>
</div>
{% if ipaddress_table.rows %}
{% render_table ipaddress_table 'inc/table.html' %}
{% else %}
<div class="panel-body text-muted">None</div>
{% endif %}
{% if perms.ipam.add_ipaddress %}
<div class="panel-footer text-right noprint">
<a href="{% url 'ipam:ipaddress_add' %}?virtual_machine={{ object.virtual_machine.pk }}&vminterface={{ object.pk }}" class="btn btn-xs btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add IP Address
</a>
</div>
{% endif %}
</div>
</div>
</div>
<div class="row">

View File

@@ -223,7 +223,7 @@ class ObjectTypeListFilter(admin.SimpleListFilter):
parameter_name = 'object_type'
def lookups(self, request, model_admin):
object_types = ObjectPermission.objects.values_list('id', flat=True).distinct()
object_types = ObjectPermission.objects.values_list('object_types__pk', flat=True).distinct()
content_types = ContentType.objects.filter(pk__in=object_types).order_by('app_label', 'model')
return [
(ct.pk, ct) for ct in content_types

View File

@@ -1,5 +1,5 @@
import django_tables2 as tables
from django.conf import settings
from dcim.tables.devices import BaseInterfaceTable
from tenancy.tables import TenantColumn
from utilities.tables import (
@@ -123,10 +123,18 @@ class VirtualMachineDetailTable(VirtualMachineTable):
linkify=True,
verbose_name='IPv6 Address'
)
primary_ip = tables.Column(
linkify=True,
verbose_name='IP Address'
)
if settings.PREFER_IPV4:
primary_ip = tables.Column(
linkify=True,
order_by=('primary_ip4', 'primary_ip6'),
verbose_name='IP Address'
)
else:
primary_ip = tables.Column(
linkify=True,
order_by=('primary_ip6', 'primary_ip4'),
verbose_name='IP Address'
)
tags = TagColumn(
url_name='virtualization:virtualmachine_list'
)