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

Merge branch 'develop' into develop-2.5 (v2.4.4 release)

This commit is contained in:
Jeremy Stretch
2018-08-22 12:10:44 -04:00
17 changed files with 122 additions and 32 deletions

View File

@ -19,16 +19,25 @@ v2.5.0 (FUTURE)
--- ---
v2.4.4 (FUTURE) v2.4.4 (2018-08-22)
## Enhancements ## Enhancements
* [#2168](https://github.com/digitalocean/netbox/issues/2168) - Added Extreme SummitStack interface form factors
* [#2356](https://github.com/digitalocean/netbox/issues/2356) - Include cluster site as read-only field in VirtualMachine serializer * [#2356](https://github.com/digitalocean/netbox/issues/2356) - Include cluster site as read-only field in VirtualMachine serializer
* [#2362](https://github.com/digitalocean/netbox/issues/2362) - Implemented custom admin site to properly handle BASE_PATH * [#2362](https://github.com/digitalocean/netbox/issues/2362) - Implemented custom admin site to properly handle BASE_PATH
* [#2254](https://github.com/digitalocean/netbox/issues/2254) - Implemented searchability for Rack Groups
## Bug Fixes ## Bug Fixes
* [#2353](https://github.com/digitalocean/netbox/issues/2353) - Handle `DoesNotExist` exception when deleting a device with connected interfaces
* [#2354](https://github.com/digitalocean/netbox/issues/2354) - Increased maximum MTU for interfaces to 65536 bytes
* [#2355](https://github.com/digitalocean/netbox/issues/2355) - Added item count to inventory tab on device view * [#2355](https://github.com/digitalocean/netbox/issues/2355) - Added item count to inventory tab on device view
* [#2368](https://github.com/digitalocean/netbox/issues/2368) - Record change in device changelog when altering cluster assignment
* [#2369](https://github.com/digitalocean/netbox/issues/2369) - Corrected time zone validation on site API serializer
* [#2370](https://github.com/digitalocean/netbox/issues/2370) - Redirect to parent device after deleting device bays
* [#2374](https://github.com/digitalocean/netbox/issues/2374) - Fix toggling display of IP addresses in virtual machine interfaces list
* [#2378](https://github.com/digitalocean/netbox/issues/2378) - Corrected "edit" link for virtual machine interfaces
--- ---

View File

@ -12,5 +12,5 @@ While NetBox has many configuration settings, only a few of them must be defined
Configuration settings may be changed at any time. However, the NetBox service must be restarted before the changes will take effect: Configuration settings may be changed at any time. However, the NetBox service must be restarted before the changes will take effect:
```no-highlight ```no-highlight
# sudo supervsiorctl restart netbox # sudo supervisorctl restart netbox
``` ```

View File

@ -48,9 +48,9 @@ Close the release milestone on GitHub. Ensure that there are no remaining open i
Ensure that continuous integration testing on the `develop` branch is completing successfully. Ensure that continuous integration testing on the `develop` branch is completing successfully.
## Update VERSION ## Update Version and Changelog
Update the `VERSION` constant in `settings.py` to the new release. Update the `VERSION` constant in `settings.py` to the new release version and add the current date to the release notes in `CHANGELOG.md`.
## Submit a Pull Request ## Submit a Pull Request

View File

@ -56,7 +56,7 @@ To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https:
## Option B: Apache ## Option B: Apache
```no-highlight ```no-highlight
# apt-get install -y apache2 # apt-get install -y apache2 libapache2-mod-wsgi-py3
``` ```
Once Apache is installed, proceed with the following configuration (Be sure to modify the `ServerName` appropriately): Once Apache is installed, proceed with the following configuration (Be sure to modify the `ServerName` appropriately):

View File

@ -91,6 +91,11 @@ IFACE_FF_STACKWISE_PLUS = 5050
IFACE_FF_FLEXSTACK = 5100 IFACE_FF_FLEXSTACK = 5100
IFACE_FF_FLEXSTACK_PLUS = 5150 IFACE_FF_FLEXSTACK_PLUS = 5150
IFACE_FF_JUNIPER_VCP = 5200 IFACE_FF_JUNIPER_VCP = 5200
IFACE_FF_SUMMITSTACK = 5300
IFACE_FF_SUMMITSTACK128 = 5310
IFACE_FF_SUMMITSTACK256 = 5320
IFACE_FF_SUMMITSTACK512 = 5330
# Other # Other
IFACE_FF_OTHER = 32767 IFACE_FF_OTHER = 32767
@ -166,6 +171,10 @@ IFACE_FF_CHOICES = [
[IFACE_FF_FLEXSTACK, 'Cisco FlexStack'], [IFACE_FF_FLEXSTACK, 'Cisco FlexStack'],
[IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'], [IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
[IFACE_FF_JUNIPER_VCP, 'Juniper VCP'], [IFACE_FF_JUNIPER_VCP, 'Juniper VCP'],
[IFACE_FF_SUMMITSTACK, 'Extreme SummitStack'],
[IFACE_FF_SUMMITSTACK128, 'Extreme SummitStack-128'],
[IFACE_FF_SUMMITSTACK256, 'Extreme SummitStack-256'],
[IFACE_FF_SUMMITSTACK512, 'Extreme SummitStack-512'],
] ]
], ],
[ [

View File

@ -110,6 +110,10 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
class RackGroupFilter(django_filters.FilterSet): class RackGroupFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
@ -125,6 +129,15 @@ class RackGroupFilter(django_filters.FilterSet):
model = RackGroup model = RackGroup
fields = ['site_id', 'name', 'slug'] fields = ['site_id', 'name', 'slug']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value) |
Q(slug__icontains=value)
)
return queryset.filter(qs_filter)
class RackRoleFilter(django_filters.FilterSet): class RackRoleFilter(django_filters.FilterSet):

View File

@ -0,0 +1,29 @@
# Generated by Django 2.0.8 on 2018-08-22 14:23
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0061_platform_napalm_args'),
]
operations = [
migrations.AlterField(
model_name='interface',
name='mtu',
field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65536)], verbose_name='MTU'),
),
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200),
),
]

View File

@ -1,4 +1,4 @@
# Generated by Django 2.0.8 on 2018-08-16 16:17 # Generated by Django 2.0.8 on 2018-08-22 16:09
from django.db import migrations from django.db import migrations
@ -6,7 +6,7 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('dcim', '0061_platform_napalm_args'), ('dcim', '0062_interface_mtu'),
] ]
operations = [ operations = [

View File

@ -1754,9 +1754,10 @@ class Interface(ComponentModel):
blank=True, blank=True,
verbose_name='MAC Address' verbose_name='MAC Address'
) )
mtu = models.PositiveSmallIntegerField( mtu = models.PositiveIntegerField(
blank=True, blank=True,
null=True, null=True,
validators=[MinValueValidator(1), MaxValueValidator(65536)],
verbose_name='MTU' verbose_name='MTU'
) )
mgmt_only = models.BooleanField( mgmt_only = models.BooleanField(
@ -2012,6 +2013,7 @@ class InterfaceConnection(models.Model):
(self.interface_a, self.interface_b), (self.interface_a, self.interface_b),
(self.interface_b, self.interface_a), (self.interface_b, self.interface_a),
) )
for interface, peer_interface in interfaces: for interface, peer_interface in interfaces:
if action == OBJECTCHANGE_ACTION_DELETE: if action == OBJECTCHANGE_ACTION_DELETE:
connection_data = { connection_data = {
@ -2022,11 +2024,17 @@ class InterfaceConnection(models.Model):
'connected_interface': peer_interface.pk, 'connected_interface': peer_interface.pk,
'connection_status': self.connection_status 'connection_status': self.connection_status
} }
try:
parent_obj = interface.parent
except ObjectDoesNotExist:
parent_obj = None
ObjectChange( ObjectChange(
user=user, user=user,
request_id=request_id, request_id=request_id,
changed_object=interface, changed_object=interface,
related_object=interface.parent, related_object=parent_obj,
action=OBJECTCHANGE_ACTION_UPDATE, action=OBJECTCHANGE_ACTION_UPDATE,
object_data=serialize_object(interface, extra=connection_data) object_data=serialize_object(interface, extra=connection_data)
).save() ).save()

View File

@ -11,6 +11,7 @@ OBJ_TYPE_CHOICES = (
('DCIM', ( ('DCIM', (
('site', 'Sites'), ('site', 'Sites'),
('rack', 'Racks'), ('rack', 'Racks'),
('rackgroup', 'Rack Groups'),
('devicetype', 'Device types'), ('devicetype', 'Device types'),
('device', 'Devices'), ('device', 'Devices'),
('virtualchassis', 'Virtual Chassis'), ('virtualchassis', 'Virtual Chassis'),

View File

@ -21,7 +21,7 @@ except ImportError:
"Configuration file is not present. Please define netbox/netbox/configuration.py per the documentation." "Configuration file is not present. Please define netbox/netbox/configuration.py per the documentation."
) )
VERSION = '2.4.4-dev' VERSION = '2.5.0-dev'
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

View File

@ -10,9 +10,16 @@ from rest_framework.views import APIView
from circuits.filters import CircuitFilter, ProviderFilter from circuits.filters import CircuitFilter, ProviderFilter
from circuits.models import Circuit, Provider from circuits.models import Circuit, Provider
from circuits.tables import CircuitTable, ProviderTable from circuits.tables import CircuitTable, ProviderTable
from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter, VirtualChassisFilter from dcim.filters import (
from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site, VirtualChassis DeviceFilter, DeviceTypeFilter, RackFilter, RackGroupFilter, SiteFilter, VirtualChassisFilter
from dcim.tables import DeviceDetailTable, DeviceTypeTable, RackTable, SiteTable, VirtualChassisTable )
from dcim.models import (
ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, RackGroup, Site,
VirtualChassis
)
from dcim.tables import (
DeviceDetailTable, DeviceTypeTable, RackTable, RackGroupTable, SiteTable, VirtualChassisTable
)
from extras.models import ObjectChange, ReportResult, TopologyMap from extras.models import ObjectChange, ReportResult, TopologyMap
from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
@ -56,6 +63,12 @@ SEARCH_TYPES = OrderedDict((
'table': RackTable, 'table': RackTable,
'url': 'dcim:rack_list', 'url': 'dcim:rack_list',
}), }),
('rackgroup', {
'queryset': RackGroup.objects.select_related('site').annotate(rack_count=Count('racks')),
'filter': RackGroupFilter,
'table': RackGroupTable,
'url': 'dcim:rackgroup_list',
}),
('devicetype', { ('devicetype', {
'queryset': DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances')), 'queryset': DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances')),
'filter': DeviceTypeFilter, 'filter': DeviceTypeFilter,

View File

@ -447,7 +447,7 @@
<div class="col-md-12"> <div class="col-md-12">
{% if device_bays or device.device_type.is_parent_device %} {% if device_bays or device.device_type.is_parent_device %}
{% if perms.dcim.delete_devicebay %} {% if perms.dcim.delete_devicebay %}
<form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% endif %} {% endif %}
<div class="panel panel-default"> <div class="panel panel-default">
@ -483,7 +483,7 @@
</button> </button>
{% endif %} {% endif %}
{% if device_bays and perms.dcim.delete_devicebay %} {% if device_bays and perms.dcim.delete_devicebay %}
<button type="submit" class="btn btn-danger btn-xs"> <button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
</button> </button>
{% endif %} {% endif %}

View File

@ -17,12 +17,12 @@
</div> </div>
<div class="pull-right"> <div class="pull-right">
{% if perms.dcim.change_interface %} {% if perms.dcim.change_interface %}
<a href="{% url 'dcim:interface_edit' pk=interface.pk %}" class="btn btn-warning"> <a href="{% if interface.device %}{% url 'dcim:interface_edit' pk=interface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=interface.pk %}{% endif %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span> Edit this interface <span class="fa fa-pencil" aria-hidden="true"></span> Edit this interface
</a> </a>
{% endif %} {% endif %}
{% if perms.dcim.delete_interface %} {% if perms.dcim.delete_interface %}
<a href="{% url 'dcim:interface_delete' pk=interface.pk %}" class="btn btn-danger"> <a href="{% if interface.device %}{% url 'dcim:interface_delete' pk=interface.pk %}{% else %}{% url 'virtualization:interface_delete' pk=interface.pk %}{% endif %}" class="btn btn-danger">
<span class="fa fa-trash" aria-hidden="true"></span> Delete this interface <span class="fa fa-trash" aria-hidden="true"></span> Delete this interface
</a> </a>
{% endif %} {% endif %}

View File

@ -315,9 +315,9 @@
$('button.toggle-ips').click(function() { $('button.toggle-ips').click(function() {
var selected = $(this).attr('selected'); var selected = $(this).attr('selected');
if (selected) { if (selected) {
$('#interfaces_table tr.ipaddress').hide(); $('#interfaces_table tr.ipaddresses').hide();
} else { } else {
$('#interfaces_table tr.ipaddress').show(); $('#interfaces_table tr.ipaddresses').show();
} }
$(this).attr('selected', !selected); $(this).attr('selected', !selected);
$(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked'); $(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');

View File

@ -106,10 +106,9 @@ class TimeZoneField(Field):
def to_internal_value(self, data): def to_internal_value(self, data):
if not data: if not data:
return "" return ""
try: if data not in pytz.common_timezones:
return pytz.timezone(str(data)) raise ValidationError('Unknown time zone "{}" (see pytz.common_timezones for all options)'.format(data))
except pytz.exceptions.UnknownTimeZoneError: return pytz.timezone(data)
raise ValidationError('Invalid time zone "{}"'.format(data))
class SerializedPKRelatedField(PrimaryKeyRelatedField): class SerializedPKRelatedField(PrimaryKeyRelatedField):

View File

@ -1,5 +1,6 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db import transaction
from django.db.models import Count from django.db.models import Count
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
@ -181,17 +182,21 @@ class ClusterAddDevicesView(PermissionRequiredMixin, View):
if form.is_valid(): if form.is_valid():
# Assign the selected Devices to the Cluster device_pks = form.cleaned_data['devices']
devices = form.cleaned_data['devices'] with transaction.atomic():
Device.objects.filter(pk__in=devices).update(cluster=cluster)
# Assign the selected Devices to the Cluster
for device in Device.objects.filter(pk__in=device_pks):
device.cluster = cluster
device.save()
messages.success(request, "Added {} devices to cluster {}".format( messages.success(request, "Added {} devices to cluster {}".format(
len(devices), cluster len(device_pks), cluster
)) ))
return redirect(cluster.get_absolute_url()) return redirect(cluster.get_absolute_url())
return render(request, self.template_name, { return render(request, self.template_name, {
'cluser': cluster, 'cluster': cluster,
'form': form, 'form': form,
'return_url': cluster.get_absolute_url(), 'return_url': cluster.get_absolute_url(),
}) })
@ -210,12 +215,16 @@ class ClusterRemoveDevicesView(PermissionRequiredMixin, View):
form = self.form(request.POST) form = self.form(request.POST)
if form.is_valid(): if form.is_valid():
# Remove the selected Devices from the Cluster device_pks = form.cleaned_data['pk']
devices = form.cleaned_data['pk'] with transaction.atomic():
Device.objects.filter(pk__in=devices).update(cluster=None)
# Remove the selected Devices from the Cluster
for device in Device.objects.filter(pk__in=device_pks):
device.cluster = None
device.save()
messages.success(request, "Removed {} devices from cluster {}".format( messages.success(request, "Removed {} devices from cluster {}".format(
len(devices), cluster len(device_pks), cluster
)) ))
return redirect(cluster.get_absolute_url()) return redirect(cluster.get_absolute_url())