diff --git a/.github/stale.yml b/.github/stale.yml index fdfb1d590..92da07e6a 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -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 diff --git a/docs/additional-features/prometheus-metrics.md b/docs/additional-features/prometheus-metrics.md index 048eb68b3..56365e336 100644 --- a/docs/additional-features/prometheus-metrics.md +++ b/docs/additional-features/prometheus-metrics.md @@ -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). \ No newline at end of file + 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). diff --git a/docs/additional-features/reports.md b/docs/additional-features/reports.md index 9d60f797e..1266c22ec 100644 --- a/docs/additional-features/reports.md +++ b/docs/additional-features/reports.md @@ -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) diff --git a/docs/installation/6-ldap.md b/docs/installation/6-ldap.md index 25f9c8f2b..1c5424595 100644 --- a/docs/installation/6-ldap.md +++ b/docs/installation/6-ldap.md @@ -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`: diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index f5e0b4fa2..bc3d0e1a5 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -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 --- diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 88890bf95..12ec9ba7f 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -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): diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 03deca2a4..41363c261 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -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', diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index ee1dc091b..7a52b85b0 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -95,6 +95,11 @@ CONSOLEPORT_BUTTONS = """ {% if record.cable %} {% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %} + {% if perms.dcim.delete_cable %} + + + + {% endif %} {% elif perms.dcim.add_cable %} @@ -115,6 +120,11 @@ CONSOLESERVERPORT_BUTTONS = """ {% if record.cable %} {% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %} + {% if perms.dcim.delete_cable %} + + + + {% endif %} {% elif perms.dcim.add_cable %} @@ -135,6 +145,11 @@ POWERPORT_BUTTONS = """ {% if record.cable %} {% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %} + {% if perms.dcim.delete_cable %} + + + + {% endif %} {% elif perms.dcim.add_cable %} @@ -154,6 +169,11 @@ POWEROUTLET_BUTTONS = """ {% if record.cable %} {% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %} + {% if perms.dcim.delete_cable %} + + + + {% endif %} {% elif perms.dcim.add_cable %} @@ -172,6 +192,11 @@ INTERFACE_BUTTONS = """ {% if record.cable %} {% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %} + {% if perms.dcim.delete_cable %} + + + + {% endif %} {% elif record.is_connectable and perms.dcim.add_cable %} @@ -193,6 +218,11 @@ FRONTPORT_BUTTONS = """ {% if record.cable %} {% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %} + {% if perms.dcim.delete_cable %} + + + + {% endif %} {% elif perms.dcim.add_cable %} @@ -216,6 +246,11 @@ REARPORT_BUTTONS = """ {% if record.cable %} {% include 'dcim/inc/cable_toggle_buttons.html' with cable=record.cable %} + {% if perms.dcim.delete_cable %} + + + + {% endif %} {% elif perms.dcim.add_cable %} diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 0ae9db7b2..e8b3dff0a 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -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", diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py index 6091aa70e..a47862165 100644 --- a/netbox/ipam/tests/test_models.py +++ b/netbox/ipam/tests/test_models.py @@ -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) diff --git a/netbox/netbox/views/generic.py b/netbox/netbox/views/generic.py index bd21d469c..216b50759 100644 --- a/netbox/netbox/views/generic.py +++ b/netbox/netbox/views/generic.py @@ -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 diff --git a/netbox/templates/dcim/inc/cable_toggle_buttons.html b/netbox/templates/dcim/inc/cable_toggle_buttons.html index 98e4efd94..e3eb318ac 100644 --- a/netbox/templates/dcim/inc/cable_toggle_buttons.html +++ b/netbox/templates/dcim/inc/cable_toggle_buttons.html @@ -9,8 +9,3 @@ {% endif %} {% endif %} -{% if perms.dcim.delete_cable %} - - - -{% endif %} diff --git a/netbox/utilities/forms/widgets.py b/netbox/utilities/forms/widgets.py index cd6fb0fbb..1c456c74c 100644 --- a/netbox/utilities/forms/widgets.py +++ b/netbox/utilities/forms/widgets.py @@ -114,7 +114,10 @@ class ContentTypeSelect(StaticSelect2): class NumericArrayField(SimpleArrayField): def to_python(self, value): - value = ','.join([str(n) for n in parse_numeric_range(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) diff --git a/upgrade.sh b/upgrade.sh index 2e35a02f9..648f825a0 100755 --- a/upgrade.sh +++ b/upgrade.sh @@ -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