Skip to content

Commit ad95b86

Browse files
Merge pull request #1201 from digitalocean/develop
Release v2.0.3
2 parents 43e1e0d + 9cf10ee commit ad95b86

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+215
-229
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for inst
3333

3434
* [Docker container](https://github.com/digitalocean/netbox-docker)
3535
* [Heroku deployment](https://heroku.com/deploy?template=https://github.com/BILDQUADRAT/netbox/tree/heroku) (via [@mraerino](https://github.com/BILDQUADRAT/netbox/tree/heroku))
36+
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant)

docs/installation/netbox.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
Python 3:
66

77
```no-highlight
8-
# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
8+
# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
99
# update-alternatives --install /usr/bin/python python /usr/bin/python3 1
1010
```
1111

1212
Python 2:
1313

1414
```no-highlight
15-
# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
15+
# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
1616
```
1717

1818
**CentOS/RHEL**

docs/installation/web-server.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ Once Apache is installed, proceed with the following configuration (Be sure to m
7373
7474
Alias /static /opt/netbox/netbox/static
7575
76+
# Needed to allow token-based API authentication
77+
WSGIPassAuthorization on
78+
7679
<Directory /opt/netbox/netbox/static>
7780
Options Indexes FollowSymLinks MultiViews
7881
AllowOverride None

netbox/dcim/api/serializers.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,9 +581,18 @@ class Meta:
581581
# Interfaces
582582
#
583583

584+
class NestedInterfaceSerializer(serializers.ModelSerializer):
585+
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
586+
587+
class Meta:
588+
model = Interface
589+
fields = ['id', 'url', 'name']
590+
591+
584592
class InterfaceSerializer(serializers.ModelSerializer):
585593
device = NestedDeviceSerializer()
586594
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
595+
lag = NestedInterfaceSerializer()
587596
connection = serializers.SerializerMethodField(read_only=True)
588597
connected_interface = serializers.SerializerMethodField(read_only=True)
589598

netbox/dcim/filters.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,11 @@ class InterfaceFilter(DeviceComponentFilterSet):
477477
method='filter_type',
478478
label='Interface type',
479479
)
480+
lag_id = django_filters.ModelMultipleChoiceFilter(
481+
name='lag',
482+
queryset=Interface.objects.all(),
483+
label='LAG interface (ID)',
484+
)
480485
mac_address = django_filters.CharFilter(
481486
method='_mac_address',
482487
label='MAC address',

netbox/dcim/forms.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -674,7 +674,7 @@ class BaseDeviceFromCSVForm(forms.ModelForm):
674674
queryset=Platform.objects.all(), required=False, to_field_name='name',
675675
error_messages={'invalid_choice': 'Invalid platform.'}
676676
)
677-
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in STATUS_CHOICES])
677+
status = forms.CharField()
678678

679679
class Meta:
680680
fields = []
@@ -692,8 +692,12 @@ def clean(self):
692692
except DeviceType.DoesNotExist:
693693
self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name))
694694

695-
def clean_status_name(self):
696-
return dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
695+
def clean_status(self):
696+
status_choices = {s[1].lower(): s[0] for s in STATUS_CHOICES}
697+
try:
698+
return status_choices[self.cleaned_data['status'].lower()]
699+
except KeyError:
700+
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
697701

698702

699703
class DeviceFromCSVForm(BaseDeviceFromCSVForm):
@@ -707,8 +711,8 @@ class DeviceFromCSVForm(BaseDeviceFromCSVForm):
707711

708712
class Meta(BaseDeviceFromCSVForm.Meta):
709713
fields = [
710-
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag',
711-
'status_name', 'site', 'rack_name', 'position', 'face',
714+
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
715+
'site', 'rack_name', 'position', 'face',
712716
]
713717

714718
def clean(self):
@@ -751,8 +755,8 @@ class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
751755

752756
class Meta(BaseDeviceFromCSVForm.Meta):
753757
fields = [
754-
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag',
755-
'status_name', 'parent', 'device_bay_name',
758+
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
759+
'parent', 'device_bay_name',
756760
]
757761

758762
def clean(self):
@@ -817,13 +821,15 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
817821
rack_id = FilterChoiceField(
818822
queryset=Rack.objects.annotate(filter_count=Count('devices')),
819823
label='Rack',
824+
null_option=(0, 'None'),
820825
)
821826
role = FilterChoiceField(
822827
queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
823828
to_field_name='slug',
824829
)
825830
tenant = FilterChoiceField(
826-
queryset=Tenant.objects.annotate(filter_count=Count('devices')), to_field_name='slug',
831+
queryset=Tenant.objects.annotate(filter_count=Count('devices')),
832+
to_field_name='slug',
827833
null_option=(0, 'None'),
828834
)
829835
manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer')
@@ -1207,7 +1213,7 @@ class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
12071213
)
12081214
power_outlet = ChainedModelChoiceField(
12091215
queryset=PowerOutlet.objects.all(),
1210-
chains={'device': 'device'},
1216+
chains={'device': 'pdu'},
12111217
label='Outlet',
12121218
widget=APISelect(
12131219
api_url='/api/dcim/power-outlets/?device_id={{pdu}}',
@@ -1441,7 +1447,7 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
14411447
label='Interface',
14421448
widget=APISelect(
14431449
api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical',
1444-
disabled_indicator='is_connected'
1450+
disabled_indicator='connection'
14451451
)
14461452
)
14471453

netbox/ipam/forms.py

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django import forms
2+
from django.core.exceptions import ValidationError
23
from django.db.models import Count
34

45
from dcim.models import Site, Rack, Device, Interface
@@ -195,14 +196,16 @@ class PrefixFromCSVForm(forms.ModelForm):
195196
error_messages={'invalid_choice': 'Site not found.'})
196197
vlan_group_name = forms.CharField(required=False)
197198
vlan_vid = forms.IntegerField(required=False)
198-
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in PREFIX_STATUS_CHOICES])
199+
status = forms.CharField()
199200
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
200201
error_messages={'invalid_choice': 'Invalid role.'})
201202

202203
class Meta:
203204
model = Prefix
204-
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role', 'is_pool',
205-
'description']
205+
fields = [
206+
'prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status', 'role', 'is_pool',
207+
'description',
208+
]
206209

207210
def clean(self):
208211

@@ -237,12 +240,12 @@ def clean(self):
237240
except VLAN.MultipleObjectsReturned:
238241
self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
239242

240-
def save(self, *args, **kwargs):
241-
242-
# Assign Prefix status by name
243-
self.instance.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
244-
245-
return super(PrefixFromCSVForm, self).save(*args, **kwargs)
243+
def clean_status(self):
244+
status_choices = {s[1].lower(): s[0] for s in PREFIX_STATUS_CHOICES}
245+
try:
246+
return status_choices[self.cleaned_data['status'].lower()]
247+
except KeyError:
248+
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
246249

247250

248251
class PrefixImportForm(BootstrapMixin, BulkImportForm):
@@ -491,15 +494,15 @@ class IPAddressFromCSVForm(forms.ModelForm):
491494
error_messages={'invalid_choice': 'VRF not found.'})
492495
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
493496
error_messages={'invalid_choice': 'Tenant not found.'})
494-
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in IPADDRESS_STATUS_CHOICES])
497+
status = forms.CharField()
495498
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
496499
error_messages={'invalid_choice': 'Device not found.'})
497500
interface_name = forms.CharField(required=False)
498501
is_primary = forms.BooleanField(required=False)
499502

500503
class Meta:
501504
model = IPAddress
502-
fields = ['address', 'vrf', 'tenant', 'status_name', 'device', 'interface_name', 'is_primary', 'description']
505+
fields = ['address', 'vrf', 'tenant', 'status', 'device', 'interface_name', 'is_primary', 'description']
503506

504507
def clean(self):
505508

@@ -522,10 +525,14 @@ def clean(self):
522525
if is_primary and not device:
523526
self.add_error('is_primary', "No device specified; cannot set as primary IP")
524527

525-
def save(self, *args, **kwargs):
528+
def clean_status(self):
529+
status_choices = {s[1].lower(): s[0] for s in IPADDRESS_STATUS_CHOICES}
530+
try:
531+
return status_choices[self.cleaned_data['status'].lower()]
532+
except KeyError:
533+
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
526534

527-
# Assign status by name
528-
self.instance.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
535+
def save(self, *args, **kwargs):
529536

530537
# Set interface
531538
if self.cleaned_data['device'] and self.cleaned_data['interface_name']:
@@ -612,6 +619,7 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
612619
class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
613620
site = forms.ModelChoiceField(
614621
queryset=Site.objects.all(),
622+
required=False,
615623
widget=forms.Select(
616624
attrs={'filter-for': 'group', 'nullable': 'true'}
617625
)
@@ -649,15 +657,15 @@ class VLANFromCSVForm(forms.ModelForm):
649657
Tenant.objects.all(), to_field_name='name', required=False,
650658
error_messages={'invalid_choice': 'Tenant not found.'}
651659
)
652-
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in VLAN_STATUS_CHOICES])
660+
status = forms.CharField()
653661
role = forms.ModelChoiceField(
654662
queryset=Role.objects.all(), required=False, to_field_name='name',
655663
error_messages={'invalid_choice': 'Invalid role.'}
656664
)
657665

658666
class Meta:
659667
model = VLAN
660-
fields = ['site', 'group_name', 'vid', 'name', 'tenant', 'status_name', 'role', 'description']
668+
fields = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
661669

662670
def clean(self):
663671

@@ -671,6 +679,13 @@ def clean(self):
671679
except VLANGroup.DoesNotExist:
672680
self.add_error('group_name', "Invalid VLAN group {}.".format(group_name))
673681

682+
def clean_status(self):
683+
status_choices = {s[1].lower(): s[0] for s in VLAN_STATUS_CHOICES}
684+
try:
685+
return status_choices[self.cleaned_data['status'].lower()]
686+
except KeyError:
687+
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
688+
674689
def save(self, *args, **kwargs):
675690

676691
vlan = super(VLANFromCSVForm, self).save(commit=False)
@@ -679,9 +694,6 @@ def save(self, *args, **kwargs):
679694
if self.cleaned_data['group_name']:
680695
vlan.group = VLANGroup.objects.get(site=self.cleaned_data['site'], name=self.cleaned_data['group_name'])
681696

682-
# Assign VLAN status by name
683-
vlan.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
684-
685697
if kwargs.get('commit'):
686698
vlan.save()
687699
return vlan

netbox/ipam/tables.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,9 @@
7070
{% if record.pk %}
7171
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
7272
{% elif perms.ipam.add_ipaddress %}
73-
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Lots of{% endif %} free IP{{ record.0|pluralize }}</a>
73+
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available</a>
7474
{% else %}
75-
{{ record.0 }}
75+
{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available
7676
{% endif %}
7777
"""
7878

netbox/ipam/views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,7 @@ def prefix_ipaddresses(request, pk):
525525
'prefix': prefix,
526526
'ip_table': ip_table,
527527
'permissions': permissions,
528+
'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf or '0', prefix.prefix),
528529
})
529530

530531

netbox/netbox/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
)
1414

1515

16-
VERSION = '2.0.2'
16+
VERSION = '2.0.3'
1717

1818
# Import local configuration
1919
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None

0 commit comments

Comments
 (0)