Merge branch 'develop' into 5315-site_unassignment_vlan

This commit is contained in:
Daniel Sheppard 2021-02-12 10:52:45 -06:00 committed by GitHub
commit 20661b2444
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 80 additions and 26 deletions

3
.github/stale.yml vendored
View File

@ -1,8 +1,5 @@
# Configuration for Stale (https://github.com/apps/stale)
# Pull requests are exempt from being marked as stale
only: issues
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 45

View File

@ -26,4 +26,4 @@ For the exhaustive list of exposed metrics, visit the `/metrics` endpoint on you
When deploying NetBox in a multiprocess manner (e.g. running multiple Gunicorn workers) the Prometheus client library requires the use of a shared directory to collect metrics from all worker processes. To configure this, first create or designate a local directory to which the worker processes have read and write access, and then configure your WSGI service (e.g. Gunicorn) to define this path as the `prometheus_multiproc_dir` environment variable.
!!! warning
If having accurate long-term metrics in a multiprocess environment is crucial to your deployment, it's recommended you use the `uwsgi` library instead of `gunicorn`. The issue lies in the way `gunicorn` tracks worker processes (vs `uwsgi`) which helps manage the metrics files created by the above configurations. If you're using Netbox with gunicorn in a containerized enviroment following the one-process-per-container methodology, then you will likely not need to change to `uwsgi`. More details can be found in [issue #3779](https://github.com/netbox-community/netbox/issues/3779#issuecomment-590547562).
If having accurate long-term metrics in a multiprocess environment is crucial to your deployment, it's recommended you use the `uwsgi` library instead of `gunicorn`. The issue lies in the way `gunicorn` tracks worker processes (vs `uwsgi`) which helps manage the metrics files created by the above configurations. If you're using NetBox with gunicorn in a containerized enviroment following the one-process-per-container methodology, then you will likely not need to change to `uwsgi`. More details can be found in [issue #3779](https://github.com/netbox-community/netbox/issues/3779#issuecomment-590547562).

View File

@ -66,7 +66,7 @@ class DeviceConnectionsReport(Report):
for power_port in PowerPort.objects.filter(device=device):
if power_port.connected_endpoint is not None:
connected_ports += 1
if not power_port.connection_status:
if not power_port.path.is_active:
self.log_warning(
device,
"Power connection for {} marked as planned".format(power_port.name)

View File

@ -140,7 +140,7 @@ AUTH_LDAP_CACHE_TIMEOUT = 3600
## Troubleshooting LDAP
`systemctl restart netbox` restarts the Netbox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/messages`.
`systemctl restart netbox` restarts the NetBox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/messages`.
For troubleshooting LDAP user/group queries, add or merge the following [logging](/configuration/optional-settings.md#logging) configuration to `configuration.py`:

View File

@ -5,6 +5,11 @@
### Bug Fixes
* [#5315](https://github.com/netbox-community/netbox/issues/5315) - Fix site unassignment from VLAN when using "None" option
* [#5626](https://github.com/netbox-community/netbox/issues/5626) - Fix REST API representation for circuit terminations connected to non-interface endpoints
* [#5716](https://github.com/netbox-community/netbox/issues/5716) - Fix filtering rack reservations by custom field
* [#5718](https://github.com/netbox-community/netbox/issues/5718) - Fix bulk editing of services when no port(s) are defined
* [#5735](https://github.com/netbox-community/netbox/issues/5735) - Ensure consistent treatment of duplicate IP addresses
* [#5738](https://github.com/netbox-community/netbox/issues/5738) - Fix redirect to device components view after disconnecting a cable
---

View File

@ -40,14 +40,16 @@ class CircuitTypeSerializer(ValidatedModelSerializer):
fields = ['id', 'url', 'name', 'slug', 'description', 'circuit_count']
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
class CircuitCircuitTerminationSerializer(WritableNestedSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
site = NestedSiteSerializer()
connected_endpoint = NestedInterfaceSerializer()
class Meta:
model = CircuitTermination
fields = ['id', 'url', 'site', 'connected_endpoint', 'port_speed', 'upstream_speed', 'xconnect_id']
fields = [
'id', 'url', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'connected_endpoint',
'connected_endpoint_type', 'connected_endpoint_reachable',
]
class CircuitSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):

View File

@ -264,7 +264,7 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
)
class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet):
class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',

View File

@ -95,6 +95,11 @@ CONSOLEPORT_BUTTONS = """
{% if record.cable %}
<a href="{% url 'dcim:consoleport_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% endif %}
{% elif perms.dcim.add_cable %}
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
@ -115,6 +120,11 @@ CONSOLESERVERPORT_BUTTONS = """
{% if record.cable %}
<a href="{% url 'dcim:consoleserverport_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% endif %}
{% elif perms.dcim.add_cable %}
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
@ -135,6 +145,11 @@ POWERPORT_BUTTONS = """
{% if record.cable %}
<a href="{% url 'dcim:powerport_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% endif %}
{% elif perms.dcim.add_cable %}
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
@ -154,6 +169,11 @@ POWEROUTLET_BUTTONS = """
{% if record.cable %}
<a href="{% url 'dcim:poweroutlet_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% endif %}
{% elif perms.dcim.add_cable %}
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
@ -172,6 +192,11 @@ INTERFACE_BUTTONS = """
{% if record.cable %}
<a href="{% url 'dcim:interface_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% endif %}
{% elif record.is_connectable and perms.dcim.add_cable %}
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
@ -193,6 +218,11 @@ FRONTPORT_BUTTONS = """
{% if record.cable %}
<a href="{% url 'dcim:frontport_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% endif %}
{% elif perms.dcim.add_cable %}
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
@ -216,6 +246,11 @@ REARPORT_BUTTONS = """
{% if record.cable %}
<a href="{% url 'dcim:rearport_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace"><i class="mdi mdi-transit-connection-variant"></i></a>
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=record.cable.pk %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" title="Remove cable" class="btn btn-danger btn-xs">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% endif %}
{% elif perms.dcim.add_cable %}
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
<a href="#" class="btn btn-default btn-xs disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>

View File

@ -734,13 +734,12 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
})
# Enforce unique IP space (if applicable)
if self.role not in IPADDRESS_ROLES_NONUNIQUE and ((
self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE
) or (
self.vrf and self.vrf.enforce_unique
)):
if (self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique):
duplicate_ips = self.get_duplicates()
if duplicate_ips:
if duplicate_ips and (
self.role not in IPADDRESS_ROLES_NONUNIQUE or
any(dip.role not in IPADDRESS_ROLES_NONUNIQUE for dip in duplicate_ips)
):
raise ValidationError({
'address': "Duplicate IP address found in {}: {}".format(
"VRF {}".format(self.vrf) if self.vrf else "global table",

View File

@ -259,6 +259,18 @@ class TestIPAddress(TestCase):
duplicate_ip = IPAddress(vrf=vrf, address=netaddr.IPNetwork('192.0.2.1/24'))
self.assertRaises(ValidationError, duplicate_ip.clean)
@override_settings(ENFORCE_GLOBAL_UNIQUE=True)
def test_duplicate_nonunique_nonrole_role(self):
IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'))
duplicate_ip = IPAddress(address=netaddr.IPNetwork('192.0.2.1/24'), role=IPAddressRoleChoices.ROLE_VIP)
self.assertRaises(ValidationError, duplicate_ip.clean)
@override_settings(ENFORCE_GLOBAL_UNIQUE=True)
def test_duplicate_nonunique_role_nonrole(self):
IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'), role=IPAddressRoleChoices.ROLE_VIP)
duplicate_ip = IPAddress(address=netaddr.IPNetwork('192.0.2.1/24'))
self.assertRaises(ValidationError, duplicate_ip.clean)
@override_settings(ENFORCE_GLOBAL_UNIQUE=True)
def test_duplicate_nonunique_role(self):
IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'), role=IPAddressRoleChoices.ROLE_VIP)

View File

@ -792,7 +792,7 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
if form.cleaned_data[name]:
getattr(obj, name).set(form.cleaned_data[name])
# Normal fields
elif form.cleaned_data[name] not in (None, ''):
elif form.cleaned_data[name] not in (None, '', []):
setattr(obj, name, form.cleaned_data[name])
# Update custom fields

View File

@ -9,8 +9,3 @@
</a>
{% endif %}
{% endif %}
{% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=cable.pk %}?return_url={{ object.get_absolute_url }}" title="Remove cable" class="btn btn-danger btn-xs">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i>
</a>
{% endif %}

View File

@ -114,6 +114,9 @@ class ContentTypeSelect(StaticSelect2):
class NumericArrayField(SimpleArrayField):
def to_python(self, value):
if not value:
return []
if isinstance(value, str):
value = ','.join([str(n) for n in parse_numeric_range(value)])
return super().to_python(value)

View File

@ -29,19 +29,25 @@ eval $COMMAND || {
# Activate the virtual environment
source "${VIRTUALENV}/bin/activate"
# Upgrade pip
COMMAND="pip install --upgrade pip"
echo "Updating pip ($COMMAND)..."
eval $COMMAND || exit 1
pip -V
# Install necessary system packages
COMMAND="pip3 install wheel"
COMMAND="pip install wheel"
echo "Installing Python system packages ($COMMAND)..."
eval $COMMAND || exit 1
# Install required Python packages
COMMAND="pip3 install -r requirements.txt"
COMMAND="pip install -r requirements.txt"
echo "Installing core dependencies ($COMMAND)..."
eval $COMMAND || exit 1
# Install optional packages (if any)
if [ -s "local_requirements.txt" ]; then
COMMAND="pip3 install -r local_requirements.txt"
COMMAND="pip install -r local_requirements.txt"
echo "Installing local dependencies ($COMMAND)..."
eval $COMMAND || exit 1
elif [ -f "local_requirements.txt" ]; then