Skip to content

Commit

Permalink
Merge pull request #1201 from digitalocean/develop
Browse files Browse the repository at this point in the history
Release v2.0.3
  • Loading branch information
jeremystretch authored May 18, 2017
2 parents 43e1e0d + 9cf10ee commit ad95b86
Show file tree
Hide file tree
Showing 59 changed files with 215 additions and 229 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for inst

* [Docker container](https://github.com/digitalocean/netbox-docker)
* [Heroku deployment](https://heroku.com/deploy?template=https://github.com/BILDQUADRAT/netbox/tree/heroku) (via [@mraerino](https://github.com/BILDQUADRAT/netbox/tree/heroku))
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant)
4 changes: 2 additions & 2 deletions docs/installation/netbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
Python 3:

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

Python 2:

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

**CentOS/RHEL**
Expand Down
3 changes: 3 additions & 0 deletions docs/installation/web-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ Once Apache is installed, proceed with the following configuration (Be sure to m
Alias /static /opt/netbox/netbox/static
# Needed to allow token-based API authentication
WSGIPassAuthorization on
<Directory /opt/netbox/netbox/static>
Options Indexes FollowSymLinks MultiViews
AllowOverride None
Expand Down
9 changes: 9 additions & 0 deletions netbox/dcim/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,9 +581,18 @@ class Meta:
# Interfaces
#

class NestedInterfaceSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')

class Meta:
model = Interface
fields = ['id', 'url', 'name']


class InterfaceSerializer(serializers.ModelSerializer):
device = NestedDeviceSerializer()
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
lag = NestedInterfaceSerializer()
connection = serializers.SerializerMethodField(read_only=True)
connected_interface = serializers.SerializerMethodField(read_only=True)

Expand Down
5 changes: 5 additions & 0 deletions netbox/dcim/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,11 @@ class InterfaceFilter(DeviceComponentFilterSet):
method='filter_type',
label='Interface type',
)
lag_id = django_filters.ModelMultipleChoiceFilter(
name='lag',
queryset=Interface.objects.all(),
label='LAG interface (ID)',
)
mac_address = django_filters.CharFilter(
method='_mac_address',
label='MAC address',
Expand Down
26 changes: 16 additions & 10 deletions netbox/dcim/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ class BaseDeviceFromCSVForm(forms.ModelForm):
queryset=Platform.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Invalid platform.'}
)
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in STATUS_CHOICES])
status = forms.CharField()

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

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


class DeviceFromCSVForm(BaseDeviceFromCSVForm):
Expand All @@ -707,8 +711,8 @@ class DeviceFromCSVForm(BaseDeviceFromCSVForm):

class Meta(BaseDeviceFromCSVForm.Meta):
fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag',
'status_name', 'site', 'rack_name', 'position', 'face',
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
'site', 'rack_name', 'position', 'face',
]

def clean(self):
Expand Down Expand Up @@ -751,8 +755,8 @@ class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):

class Meta(BaseDeviceFromCSVForm.Meta):
fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag',
'status_name', 'parent', 'device_bay_name',
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
'parent', 'device_bay_name',
]

def clean(self):
Expand Down Expand Up @@ -817,13 +821,15 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
rack_id = FilterChoiceField(
queryset=Rack.objects.annotate(filter_count=Count('devices')),
label='Rack',
null_option=(0, 'None'),
)
role = FilterChoiceField(
queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
to_field_name='slug',
)
tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('devices')), to_field_name='slug',
queryset=Tenant.objects.annotate(filter_count=Count('devices')),
to_field_name='slug',
null_option=(0, 'None'),
)
manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer')
Expand Down Expand Up @@ -1207,7 +1213,7 @@ class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
)
power_outlet = ChainedModelChoiceField(
queryset=PowerOutlet.objects.all(),
chains={'device': 'device'},
chains={'device': 'pdu'},
label='Outlet',
widget=APISelect(
api_url='/api/dcim/power-outlets/?device_id={{pdu}}',
Expand Down Expand Up @@ -1441,7 +1447,7 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
label='Interface',
widget=APISelect(
api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical',
disabled_indicator='is_connected'
disabled_indicator='connection'
)
)

Expand Down
50 changes: 31 additions & 19 deletions netbox/ipam/forms.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Count

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

class Meta:
model = Prefix
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role', 'is_pool',
'description']
fields = [
'prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status', 'role', 'is_pool',
'description',
]

def clean(self):

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

def save(self, *args, **kwargs):

# Assign Prefix status by name
self.instance.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]

return super(PrefixFromCSVForm, self).save(*args, **kwargs)
def clean_status(self):
status_choices = {s[1].lower(): s[0] for s in PREFIX_STATUS_CHOICES}
try:
return status_choices[self.cleaned_data['status'].lower()]
except KeyError:
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))


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

class Meta:
model = IPAddress
fields = ['address', 'vrf', 'tenant', 'status_name', 'device', 'interface_name', 'is_primary', 'description']
fields = ['address', 'vrf', 'tenant', 'status', 'device', 'interface_name', 'is_primary', 'description']

def clean(self):

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

def save(self, *args, **kwargs):
def clean_status(self):
status_choices = {s[1].lower(): s[0] for s in IPADDRESS_STATUS_CHOICES}
try:
return status_choices[self.cleaned_data['status'].lower()]
except KeyError:
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))

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

# Set interface
if self.cleaned_data['device'] and self.cleaned_data['interface_name']:
Expand Down Expand Up @@ -612,6 +619,7 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
required=False,
widget=forms.Select(
attrs={'filter-for': 'group', 'nullable': 'true'}
)
Expand Down Expand Up @@ -649,15 +657,15 @@ class VLANFromCSVForm(forms.ModelForm):
Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'}
)
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in VLAN_STATUS_CHOICES])
status = forms.CharField()
role = forms.ModelChoiceField(
queryset=Role.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Invalid role.'}
)

class Meta:
model = VLAN
fields = ['site', 'group_name', 'vid', 'name', 'tenant', 'status_name', 'role', 'description']
fields = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']

def clean(self):

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

def clean_status(self):
status_choices = {s[1].lower(): s[0] for s in VLAN_STATUS_CHOICES}
try:
return status_choices[self.cleaned_data['status'].lower()]
except KeyError:
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))

def save(self, *args, **kwargs):

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

# Assign VLAN status by name
vlan.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]

if kwargs.get('commit'):
vlan.save()
return vlan
Expand Down
4 changes: 2 additions & 2 deletions netbox/ipam/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@
{% if record.pk %}
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
{% elif perms.ipam.add_ipaddress %}
<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>
<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>
{% else %}
{{ record.0 }}
{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available
{% endif %}
"""

Expand Down
1 change: 1 addition & 0 deletions netbox/ipam/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,7 @@ def prefix_ipaddresses(request, pk):
'prefix': prefix,
'ip_table': ip_table,
'permissions': permissions,
'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf or '0', prefix.prefix),
})


Expand Down
2 changes: 1 addition & 1 deletion netbox/netbox/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
)


VERSION = '2.0.2'
VERSION = '2.0.3'

# Import local configuration
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
Expand Down
Loading

0 comments on commit ad95b86

Please sign in to comment.