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-31 17:11:54 -04:00
12 changed files with 86 additions and 78 deletions

View File

@ -38,9 +38,13 @@ NetBox supports limited custom validation for custom field values. Following are
Each custom selection field must have at least two choices. These are specified as a comma-separated list. Choices appear in forms in the order they are listed. Note that choice values are saved exactly as they appear, so it's best to avoid superfluous punctuation or symbols where possible.
If a default value is specified for a selection field, it must exactly match one of the provided choices.
If a default value is specified for a selection field, it must exactly match one of the provided choices. The value of a multiple selection field will always return a list, even if only one value is selected.
The value of a multiple selection field will always return a list, even if only one value is selected.
## Custom Fields in Templates
Several features within NetBox, such as export templates and webhooks, utilize Jinja2 templating. For convenience, objects which support custom field assignment expose custom field data through the `cf` property. This is a bit cleaner than accessing custom field data through the actual field (`custom_field_data`).
For example, a custom field named `foo123` on the Site model is accessible on an instance as `{{ site.cf.foo123 }}`.
## Custom Fields and the REST API

View File

@ -2,6 +2,13 @@
NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to serve a proxy for operational data, fetching live data from network devices and returning it to a requester via its REST API. Note that NetBox does not store any NAPALM data locally.
The NetBox UI will display tabs for status, LLDP neighbors, and configuration under the device view if the following conditions are met:
* Device status is "Active"
* A primary IP has been assigned to the device
* A platform with a NAPALM driver has been assigned
* The authenticated user has the `dcim.napalm_read_device` permission
!!! note
To enable this integration, the NAPALM library must be installed. See [installation steps](../../installation/3-netbox/#napalm) for more information.

View File

@ -1,5 +1,21 @@
# NetBox v2.10
## v2.10.9 (FUTURE)
### Enhancements
* [#5526](https://github.com/netbox-community/netbox/issues/5526) - Add MAC address search field to VM interfaces list
* [#5756](https://github.com/netbox-community/netbox/issues/5756) - Omit child devices from non-racked devices list under rack view
* [#5840](https://github.com/netbox-community/netbox/issues/5840) - Add column to cable termination objects to display cable color
* [#6054](https://github.com/netbox-community/netbox/issues/6054) - Display NAPALM-enabled device tabs only when relevant
### Bug Fixes
* [#5805](https://github.com/netbox-community/netbox/issues/5805) - Fix missing custom field filters for cables, rack reservations
* [#6073](https://github.com/netbox-community/netbox/issues/6073) - Permit users to manage their own REST API tokens without needing explicit permission
---
## v2.10.8 (2021-03-26)
### Bug Fixes

View File

@ -1054,7 +1054,7 @@ class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField
nullable_fields = []
class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm):
class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = RackReservation
field_order = ['q', 'region_id', 'site_id', 'location_id', 'user_id', 'tenant_group_id', 'tenant_id']
q = forms.CharField(
@ -4319,7 +4319,7 @@ class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFo
})
class CableFilterForm(BootstrapMixin, forms.Form):
class CableFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Cable
q = forms.CharField(
required=False,

View File

@ -244,6 +244,11 @@ class CableTerminationTable(BaseTable):
cable = tables.Column(
linkify=True
)
cable_color = ColorColumn(
accessor='cable.color',
orderable=False,
verbose_name='Cable Color'
)
cable_peer = tables.TemplateColumn(
accessor='_cable_peer',
template_code=CABLETERMINATION,
@ -276,8 +281,8 @@ class ConsolePortTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta):
model = ConsolePort
fields = (
'pk', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_peer',
'connection', 'tags',
'pk', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
'cable_peer', 'connection', 'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'speed', 'description')
@ -296,8 +301,8 @@ class DeviceConsolePortTable(ConsolePortTable):
class Meta(DeviceComponentTable.Meta):
model = ConsolePort
fields = (
'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_peer',
'connection', 'tags', 'actions'
'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
'cable_peer', 'connection', 'tags', 'actions'
)
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
row_attrs = {
@ -319,8 +324,8 @@ class ConsoleServerPortTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta):
model = ConsoleServerPort
fields = (
'pk', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_peer',
'connection', 'tags',
'pk', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
'cable_peer', 'connection', 'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'speed', 'description')
@ -340,8 +345,8 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
class Meta(DeviceComponentTable.Meta):
model = ConsoleServerPort
fields = (
'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_peer',
'connection', 'tags', 'actions',
'pk', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color',
'cable_peer', 'connection', 'tags', 'actions',
)
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
row_attrs = {
@ -364,7 +369,7 @@ class PowerPortTable(DeviceComponentTable, PathEndpointTable):
model = PowerPort
fields = (
'pk', 'device', 'name', 'label', 'type', 'description', 'mark_connected', 'maximum_draw', 'allocated_draw',
'cable', 'cable_peer', 'connection', 'tags',
'cable', 'cable_color', 'cable_peer', 'connection', 'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
@ -385,7 +390,7 @@ class DevicePowerPortTable(PowerPortTable):
model = PowerPort
fields = (
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable',
'cable_peer', 'connection', 'tags', 'actions',
'cable_color', 'cable_peer', 'connection', 'tags', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'connection',
@ -414,7 +419,7 @@ class PowerOutletTable(DeviceComponentTable, PathEndpointTable):
model = PowerOutlet
fields = (
'pk', 'device', 'name', 'label', 'type', 'description', 'power_port', 'feed_leg', 'mark_connected', 'cable',
'cable_peer', 'connection', 'tags',
'cable_color', 'cable_peer', 'connection', 'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')
@ -434,7 +439,7 @@ class DevicePowerOutletTable(PowerOutletTable):
model = PowerOutlet
fields = (
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'mark_connected', 'cable',
'cable_peer', 'connection', 'tags', 'actions',
'cable_color', 'cable_peer', 'connection', 'tags', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'connection', 'actions',
@ -475,7 +480,7 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable
model = Interface
fields = (
'pk', 'device', 'name', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address',
'description', 'mark_connected', 'cable', 'cable_peer', 'connection', 'tags', 'ip_addresses',
'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses',
'untagged_vlan', 'tagged_vlans',
)
default_columns = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description')
@ -506,7 +511,7 @@ class DeviceInterfaceTable(InterfaceTable):
model = Interface
fields = (
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address',
'description', 'mark_connected', 'cable', 'cable_peer', 'connection', 'tags', 'ip_addresses',
'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses',
'untagged_vlan', 'tagged_vlans', 'actions',
)
default_columns = (
@ -540,7 +545,7 @@ class FrontPortTable(DeviceComponentTable, CableTerminationTable):
model = FrontPort
fields = (
'pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'mark_connected',
'cable', 'cable_peer', 'tags',
'cable', 'cable_color', 'cable_peer', 'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description')
@ -561,7 +566,7 @@ class DeviceFrontPortTable(FrontPortTable):
model = FrontPort
fields = (
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'mark_connected', 'cable',
'cable_peer', 'tags', 'actions',
'cable_color', 'cable_peer', 'tags', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'cable_peer',
@ -587,7 +592,7 @@ class RearPortTable(DeviceComponentTable, CableTerminationTable):
model = RearPort
fields = (
'pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'mark_connected', 'cable',
'cable_peer', 'tags',
'cable_color', 'cable_peer', 'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
@ -607,8 +612,8 @@ class DeviceRearPortTable(RearPortTable):
class Meta(DeviceComponentTable.Meta):
model = RearPort
fields = (
'pk', 'name', 'label', 'type', 'positions', 'description', 'mark_connected', 'cable', 'cable_peer', 'tags',
'actions',
'pk', 'name', 'label', 'type', 'positions', 'description', 'mark_connected', 'cable', 'cable_color',
'cable_peer', 'tags', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer', 'actions',

View File

@ -1,10 +1,8 @@
import django_tables2 as tables
from django_tables2.utils import Accessor
from dcim.models import PowerFeed, PowerPanel
from utilities.tables import BaseTable, ChoiceFieldColumn, LinkedCountColumn, TagColumn, ToggleColumn
from .devices import CableTerminationTable
from .template_code import POWERFEED_CABLE, POWERFEED_CABLETERMINATION
__all__ = (
'PowerFeedTable',
@ -68,7 +66,8 @@ class PowerFeedTable(CableTerminationTable):
model = PowerFeed
fields = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
'max_utilization', 'mark_connected', 'cable', 'cable_peer', 'connection', 'available_power', 'tags',
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'available_power',
'tags',
)
default_columns = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',

View File

@ -502,10 +502,11 @@ class RackView(generic.ObjectView):
queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'location', 'role')
def get_extra_context(self, request, instance):
# Get 0U and child devices located within the rack
# Get 0U devices located within the rack
nonracked_devices = Device.objects.filter(
rack=instance,
position__isnull=True
position__isnull=True,
parent_bay__isnull=True
).prefetch_related('device_type__manufacturer')
peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site)

View File

@ -131,16 +131,17 @@
</li>
{% endif %}
{% endwith %}
{% if perms.dcim.napalm_read_device %}
{% if object.status != 'active' %}
{% include 'dcim/inc/device_napalm_tabs.html' with disabled_message='Device must be in active status' %}
{% elif not object.platform %}
{% include 'dcim/inc/device_napalm_tabs.html' with disabled_message='No platform assigned to this device' %}
{% elif not object.platform.napalm_driver %}
{% include 'dcim/inc/device_napalm_tabs.html' with disabled_message='No NAPALM driver assigned for this platform' %}
{% else %}
{% include 'dcim/inc/device_napalm_tabs.html' %}
{% endif %}
{% if perms.dcim.napalm_read_device and object.status == 'active' and object.primary_ip and object.platform.napalm_driver %}
{# NAPALM-enabled tabs #}
<li role="presentation"{% if active_tab == 'status' %} class="active"{% endif %}>
<a href="{% url 'dcim:device_status' pk=object.pk %}">Status</a>
</li>
<li role="presentation"{% if active_tab == 'lldp-neighbors' %} class="active"{% endif %}>
<a href="{% url 'dcim:device_lldp_neighbors' pk=object.pk %}">LLDP Neighbors</a>
</li>
<li role="presentation"{% if active_tab == 'config' %} class="active"{% endif %}>
<a href="{% url 'dcim:device_config' pk=object.pk %}">Configuration</a>
</li>
{% endif %}
{% if perms.extras.view_configcontext %}
<li role="presentation"{% if active_tab == 'config-context' %} class="active"{% endif %}>

View File

@ -1,15 +0,0 @@
{% if not disabled_message %}
<li role="presentation"{% if active_tab == 'status' %} class="active"{% endif %}>
<a href="{% url 'dcim:device_status' pk=object.pk %}">Status</a>
</li>
<li role="presentation"{% if active_tab == 'lldp-neighbors' %} class="active"{% endif %}>
<a href="{% url 'dcim:device_lldp_neighbors' pk=object.pk %}">LLDP Neighbors</a>
</li>
<li role="presentation"{% if active_tab == 'config' %} class="active"{% endif %}>
<a href="{% url 'dcim:device_config' pk=object.pk %}">Configuration</a>
</li>
{% else %}
<li role="presentation" class="disabled"><a href="#" title="{{ disabled_message }}">Status</a></li>
<li role="presentation" class="disabled"><a href="#" title="{{ disabled_message }}">LLDP Neighbors</a></li>
<li role="presentation" class="disabled"><a href="#" title="{{ disabled_message }}">Configuration</a></li>
{% endif %}

View File

@ -11,12 +11,8 @@
<div class="panel-heading">
<div class="pull-right noprint">
<a class="btn btn-xs btn-success copy-token" data-clipboard-target="#token_{{ token.pk }}">Copy</a>
{% if perms.users.change_token %}
<a href="{% url 'user:token_edit' pk=token.pk %}" class="btn btn-xs btn-warning">Edit</a>
{% endif %}
{% if perms.users.delete_token %}
<a href="{% url 'user:token_delete' pk=token.pk %}" class="btn btn-xs btn-danger">Delete</a>
{% endif %}
<a href="{% url 'user:token_edit' pk=token.pk %}" class="btn btn-xs btn-warning">Edit</a>
<a href="{% url 'user:token_delete' pk=token.pk %}" class="btn btn-xs btn-danger">Delete</a>
</div>
<i class="mdi mdi-key"></i>
<samp><span id="token_{{ token.pk }}">{{ token.key }}</span></samp>
@ -55,16 +51,10 @@
{% empty %}
<p>You do not have any API tokens.</p>
{% endfor %}
{% if perms.users.add_token %}
<a href="{% url 'user:token_add' %}" class="btn btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span>
Add a token
</a>
{% else %}
<div class="alert alert-info text-center" role="alert">
You do not have permission to create new API tokens. If needed, ask an administrator to enable token creation for your account or an assigned group.
</div>
{% endif %}
<a href="{% url 'user:token_add' %}" class="btn btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span>
Add a token
</a>
</div>
</div>
{% endblock %}

View File

@ -6,7 +6,7 @@ from django.contrib.auth import login as auth_login, logout as auth_logout, upda
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import update_last_login
from django.contrib.auth.signals import user_logged_in
from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.decorators import method_decorator
@ -282,13 +282,9 @@ class TokenEditView(LoginRequiredMixin, View):
def get(self, request, pk=None):
if pk is not None:
if not request.user.has_perm('users.change_token'):
return HttpResponseForbidden()
if pk:
token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
else:
if not request.user.has_perm('users.add_token'):
return HttpResponseForbidden()
token = Token(user=request.user)
form = TokenForm(instance=token)
@ -302,11 +298,11 @@ class TokenEditView(LoginRequiredMixin, View):
def post(self, request, pk=None):
if pk is not None:
if pk:
token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
form = TokenForm(request.POST, instance=token)
else:
token = Token()
token = Token(user=request.user)
form = TokenForm(request.POST)
if form.is_valid():
@ -314,7 +310,7 @@ class TokenEditView(LoginRequiredMixin, View):
token.user = request.user
token.save()
msg = "Modified token {}".format(token) if pk else "Created token {}".format(token)
msg = f"Modified token {token}" if pk else f"Created token {token}"
messages.success(request, msg)
if '_addanother' in request.POST:

View File

@ -800,6 +800,10 @@ class VMInterfaceFilterForm(forms.Form):
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
mac_address = forms.CharField(
required=False,
label='MAC address'
)
tag = TagFilterField(model)