Merge branch 'develop' into develop-2.3

This commit is contained in:
Jeremy Stretch 2018-01-19 10:54:26 -05:00
commit 0714a40509
29 changed files with 186 additions and 113 deletions

View File

@ -24,7 +24,7 @@ sudo pip install django-auth-ldap
# Configuration # Configuration
Create a file in the same directory as `configuration.py` (typically `netbox/netbox/`) named `ldap_config.py`. Define all of the parameters required below in `ldap_config.py`. Create a file in the same directory as `configuration.py` (typically `netbox/netbox/`) named `ldap_config.py`. Define all of the parameters required below in `ldap_config.py`. Complete documentation of all `django-auth-ldap` configuration options is included in the project's [official documentation](http://django-auth-ldap.readthedocs.io/).
## General Server Configuration ## General Server Configuration
@ -52,6 +52,8 @@ AUTH_LDAP_BIND_PASSWORD = "demo"
LDAP_IGNORE_CERT_ERRORS = True LDAP_IGNORE_CERT_ERRORS = True
``` ```
STARTTLS can be configured by setting `AUTH_LDAP_START_TLS = True` and using the `ldap://` URI scheme.
## User Authentication ## User Authentication
!!! info !!! info
@ -78,8 +80,8 @@ AUTH_LDAP_USER_ATTR_MAP = {
``` ```
# User Groups for Permissions # User Groups for Permissions
!!! Info !!! info
When using Microsoft Active Directory, Support for nested Groups can be activated by using `GroupOfNamesType()` instead of `NestedGroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`. When using Microsoft Active Directory, support for nested groups can be activated by using `NestedGroupOfNamesType()` instead of `GroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`.
```python ```python
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType from django_auth_ldap.config import LDAPSearch, GroupOfNamesType

View File

@ -174,7 +174,7 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('circuits')), queryset=Tenant.objects.annotate(filter_count=Count('circuits')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
site = FilterChoiceField( site = FilterChoiceField(
queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')), queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),

View File

@ -776,6 +776,8 @@ class InventoryItemSerializer(serializers.ModelSerializer):
class WritableInventoryItemSerializer(ValidatedModelSerializer): class WritableInventoryItemSerializer(ValidatedModelSerializer):
# Provide a default value to satisfy UniqueTogetherValidator
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
class Meta: class Meta:
model = InventoryItem model = InventoryItem

View File

@ -163,7 +163,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = Rack model = Rack
fields = ['serial', 'type', 'width', 'u_height', 'desc_units'] fields = ['name', 'serial', 'type', 'width', 'u_height', 'desc_units']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -340,7 +340,7 @@ class DeviceRoleFilter(django_filters.FilterSet):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = ['name', 'slug', 'color'] fields = ['name', 'slug', 'color', 'vm_role']
class PlatformFilter(django_filters.FilterSet): class PlatformFilter(django_filters.FilterSet):
@ -476,7 +476,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = Device model = Device
fields = ['serial'] fields = ['serial', 'position']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

View File

@ -175,7 +175,7 @@ class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('sites')), queryset=Tenant.objects.annotate(filter_count=Count('sites')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
@ -371,17 +371,17 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
group_id = FilterChoiceField( group_id = FilterChoiceField(
queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks')), queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks')),
label='Rack group', label='Rack group',
null_option=(0, 'None') null_label='-- None --'
) )
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('racks')), queryset=Tenant.objects.annotate(filter_count=Count('racks')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
role = FilterChoiceField( role = FilterChoiceField(
queryset=RackRole.objects.annotate(filter_count=Count('racks')), queryset=RackRole.objects.annotate(filter_count=Count('racks')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
@ -423,12 +423,12 @@ class RackReservationFilterForm(BootstrapMixin, forms.Form):
group_id = FilterChoiceField( group_id = FilterChoiceField(
queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__reservations')), queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__reservations')),
label='Rack group', label='Rack group',
null_option=(0, 'None') null_label='-- None --'
) )
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('rackreservations')), queryset=Tenant.objects.annotate(filter_count=Count('rackreservations')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
@ -1053,7 +1053,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
rack_id = FilterChoiceField( rack_id = FilterChoiceField(
queryset=Rack.objects.annotate(filter_count=Count('devices')), queryset=Rack.objects.annotate(filter_count=Count('devices')),
label='Rack', label='Rack',
null_option=(0, 'None'), null_label='-- None --',
) )
role = FilterChoiceField( role = FilterChoiceField(
queryset=DeviceRole.objects.annotate(filter_count=Count('devices')), queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
@ -1062,7 +1062,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('devices')), queryset=Tenant.objects.annotate(filter_count=Count('devices')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None'), null_label='-- None --',
) )
manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer') manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer')
device_type_id = FilterChoiceField( device_type_id = FilterChoiceField(
@ -1074,7 +1074,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
platform = FilterChoiceField( platform = FilterChoiceField(
queryset=Platform.objects.annotate(filter_count=Count('devices')), queryset=Platform.objects.annotate(filter_count=Count('devices')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None'), null_label='-- None --',
) )
status = forms.MultipleChoiceField(choices=device_status_choices, required=False) status = forms.MultipleChoiceField(choices=device_status_choices, required=False)
mac_address = forms.CharField(required=False, label='MAC address') mac_address = forms.CharField(required=False, label='MAC address')

View File

@ -389,6 +389,7 @@ class PlatformTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name') name = tables.LinkColumn(verbose_name='Name')
device_count = tables.Column(verbose_name='Devices') device_count = tables.Column(verbose_name='Devices')
vm_count = tables.Column(verbose_name='VMs')
slug = tables.Column(verbose_name='Slug') slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=PLATFORM_ACTIONS, template_code=PLATFORM_ACTIONS,
@ -398,7 +399,7 @@ class PlatformTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Platform model = Platform
fields = ('pk', 'name', 'manufacturer', 'device_count', 'slug', 'napalm_driver', 'actions') fields = ('pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'actions')
# #

View File

@ -802,7 +802,10 @@ class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# #
class PlatformListView(ObjectListView): class PlatformListView(ObjectListView):
queryset = Platform.objects.annotate(device_count=Count('devices')) queryset = Platform.objects.annotate(
device_count=Count('devices', distinct=True),
vm_count=Count('virtual_machines', distinct=True)
)
table = tables.PlatformTable table = tables.PlatformTable
template_name = 'dcim/platform_list.html' template_name = 'dcim/platform_list.html'

View File

@ -78,8 +78,11 @@ class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm): class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = VRF model = VRF
q = forms.CharField(required=False, label='Search') q = forms.CharField(required=False, label='Search')
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vrfs')), to_field_name='slug', tenant = FilterChoiceField(
null_option=(0, None)) queryset=Tenant.objects.annotate(filter_count=Count('vrfs')),
to_field_name='slug',
null_label='-- None --'
)
# #
@ -368,23 +371,23 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
queryset=VRF.objects.annotate(filter_count=Count('prefixes')), queryset=VRF.objects.annotate(filter_count=Count('prefixes')),
to_field_name='rd', to_field_name='rd',
label='VRF', label='VRF',
null_option=(0, 'Global') null_label='-- Global --'
) )
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('prefixes')), queryset=Tenant.objects.annotate(filter_count=Count('prefixes')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
status = forms.MultipleChoiceField(choices=prefix_status_choices, required=False) status = forms.MultipleChoiceField(choices=prefix_status_choices, required=False)
site = FilterChoiceField( site = FilterChoiceField(
queryset=Site.objects.annotate(filter_count=Count('prefixes')), queryset=Site.objects.annotate(filter_count=Count('prefixes')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
role = FilterChoiceField( role = FilterChoiceField(
queryset=Role.objects.annotate(filter_count=Count('prefixes')), queryset=Role.objects.annotate(filter_count=Count('prefixes')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
expand = forms.BooleanField(required=False, label='Expand prefix hierarchy') expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
@ -719,12 +722,12 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
queryset=VRF.objects.annotate(filter_count=Count('ip_addresses')), queryset=VRF.objects.annotate(filter_count=Count('ip_addresses')),
to_field_name='rd', to_field_name='rd',
label='VRF', label='VRF',
null_option=(0, 'Global') null_label='-- Global --'
) )
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')), queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False) status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False)
role = forms.MultipleChoiceField(choices=ipaddress_role_choices, required=False) role = forms.MultipleChoiceField(choices=ipaddress_role_choices, required=False)
@ -766,7 +769,7 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
site = FilterChoiceField( site = FilterChoiceField(
queryset=Site.objects.annotate(filter_count=Count('vlan_groups')), queryset=Site.objects.annotate(filter_count=Count('vlan_groups')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'Global') null_label='-- Global --'
) )
@ -896,23 +899,23 @@ class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
site = FilterChoiceField( site = FilterChoiceField(
queryset=Site.objects.annotate(filter_count=Count('vlans')), queryset=Site.objects.annotate(filter_count=Count('vlans')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'Global') null_label='-- Global --'
) )
group_id = FilterChoiceField( group_id = FilterChoiceField(
queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')), queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')),
label='VLAN group', label='VLAN group',
null_option=(0, 'None') null_label='-- None --'
) )
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('vlans')), queryset=Tenant.objects.annotate(filter_count=Count('vlans')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
status = forms.MultipleChoiceField(choices=vlan_status_choices, required=False) status = forms.MultipleChoiceField(choices=vlan_status_choices, required=False)
role = FilterChoiceField( role = FilterChoiceField(
queryset=Role.objects.annotate(filter_count=Count('vlans')), queryset=Role.objects.annotate(filter_count=Count('vlans')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )

View File

@ -298,10 +298,20 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
def get_child_ips(self): def get_child_ips(self):
""" """
Return all IPAddresses within this Prefix. Return all IPAddresses within this Prefix and VRF.
""" """
return IPAddress.objects.filter(address__net_host_contained=str(self.prefix), vrf=self.vrf) return IPAddress.objects.filter(address__net_host_contained=str(self.prefix), vrf=self.vrf)
def get_available_prefixes(self):
"""
Return all available Prefixes within this prefix as an IPSet.
"""
prefix = netaddr.IPSet(self.prefix)
child_prefixes = netaddr.IPSet([child.prefix for child in self.get_child_prefixes()])
available_prefixes = prefix - child_prefixes
return available_prefixes
def get_available_ips(self): def get_available_ips(self):
""" """
Return all available IPs within this prefix as an IPSet. Return all available IPs within this prefix as an IPSet.
@ -319,15 +329,23 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
return available_ips return available_ips
def get_first_available_prefix(self):
"""
Return the first available child prefix within the prefix (or None).
"""
available_prefixes = self.get_available_prefixes()
if not available_prefixes:
return None
return available_prefixes.iter_cidrs()[0]
def get_first_available_ip(self): def get_first_available_ip(self):
""" """
Return the first available IP within the prefix (or None). Return the first available IP within the prefix (or None).
""" """
available_ips = self.get_available_ips() available_ips = self.get_available_ips()
if available_ips: if not available_ips:
return '{}/{}'.format(next(available_ips.__iter__()), self.prefix.prefixlen)
else:
return None return None
return '{}/{}'.format(next(available_ips.__iter__()), self.prefix.prefixlen)
def get_utilization(self): def get_utilization(self):
""" """
@ -345,17 +363,6 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
prefix_size -= 2 prefix_size -= 2
return int(float(child_count) / prefix_size * 100) return int(float(child_count) / prefix_size * 100)
@property
def new_subnet(self):
if self.family == 4:
if self.prefix.prefixlen <= 30:
return netaddr.IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
return None
if self.family == 6:
if self.prefix.prefixlen <= 126:
return netaddr.IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
return None
class IPAddressManager(models.Manager): class IPAddressManager(models.Manager):

View File

@ -48,13 +48,7 @@ PREFIX_LINK = """
{% else %} {% else %}
<span class="text-nowrap" style="padding-left: {{ record.depth }}9px"> <span class="text-nowrap" style="padding-left: {{ record.depth }}9px">
{% endif %} {% endif %}
<a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if parent.vrf %}&vrf={{ parent.vrf.pk }}{% endif %}{% if parent.site %}&site={{ parent.site.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a> <a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if parent.vrf %}&vrf={{ parent.vrf.pk }}{% endif %}{% if parent.site %}&site={{ parent.site.pk }}{% endif %}{% if parent.tenant %}&tenant_group={{ parent.tenant.group.pk }}&tenant={{ parent.tenant.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a>
</span>
"""
PREFIX_LINK_BRIEF = """
<span style="padding-left: {{ record.depth }}0px">
<a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if parent.vrf %}&vrf={{ parent.vrf.pk }}{% endif %}{% if parent.site %}&site={{ parent.site.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a>
</span> </span>
""" """

View File

@ -51,6 +51,7 @@ urlpatterns = [
url(r'^prefixes/(?P<pk>\d+)/$', views.PrefixView.as_view(), name='prefix'), url(r'^prefixes/(?P<pk>\d+)/$', views.PrefixView.as_view(), name='prefix'),
url(r'^prefixes/(?P<pk>\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'), url(r'^prefixes/(?P<pk>\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'),
url(r'^prefixes/(?P<pk>\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'), url(r'^prefixes/(?P<pk>\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'),
url(r'^prefixes/(?P<pk>\d+)/prefixes/$', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'),
url(r'^prefixes/(?P<pk>\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'), url(r'^prefixes/(?P<pk>\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
# IP addresses # IP addresses

View File

@ -476,6 +476,20 @@ class PrefixView(View):
duplicate_prefix_table = tables.PrefixTable(list(duplicate_prefixes), orderable=False) duplicate_prefix_table = tables.PrefixTable(list(duplicate_prefixes), orderable=False)
duplicate_prefix_table.exclude = ('vrf',) duplicate_prefix_table.exclude = ('vrf',)
return render(request, 'ipam/prefix.html', {
'prefix': prefix,
'aggregate': aggregate,
'parent_prefix_table': parent_prefix_table,
'duplicate_prefix_table': duplicate_prefix_table,
})
class PrefixPrefixesView(View):
def get(self, request, pk):
prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
# Child prefixes table # Child prefixes table
child_prefixes = Prefix.objects.filter( child_prefixes = Prefix.objects.filter(
vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix) vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix)
@ -484,15 +498,16 @@ class PrefixView(View):
).annotate_depth(limit=0) ).annotate_depth(limit=0)
if child_prefixes: if child_prefixes:
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes) child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
child_prefix_table = tables.PrefixDetailTable(child_prefixes)
prefix_table = tables.PrefixDetailTable(child_prefixes)
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'): if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
child_prefix_table.columns.show('pk') prefix_table.columns.show('pk')
paginate = { paginate = {
'klass': EnhancedPaginator, 'klass': EnhancedPaginator,
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT) 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
} }
RequestConfig(request, paginate).configure(child_prefix_table) RequestConfig(request, paginate).configure(prefix_table)
# Compile permissions list for rendering the object table # Compile permissions list for rendering the object table
permissions = { permissions = {
@ -501,15 +516,12 @@ class PrefixView(View):
'delete': request.user.has_perm('ipam.delete_prefix'), 'delete': request.user.has_perm('ipam.delete_prefix'),
} }
return render(request, 'ipam/prefix.html', { return render(request, 'ipam/prefix_prefixes.html', {
'prefix': prefix, 'prefix': prefix,
'aggregate': aggregate, 'first_available_prefix': prefix.get_first_available_prefix(),
'parent_prefix_table': parent_prefix_table, 'prefix_table': prefix_table,
'child_prefix_table': child_prefix_table,
'duplicate_prefix_table': duplicate_prefix_table,
'bulk_querystring': 'vrf_id={}&within={}'.format(prefix.vrf or '0', prefix.prefix),
'permissions': permissions, 'permissions': permissions,
'return_url': prefix.get_absolute_url(), 'bulk_querystring': 'vrf_id={}&within={}'.format(prefix.vrf.pk if prefix.vrf else '0', prefix.prefix),
}) })
@ -544,6 +556,7 @@ class PrefixIPAddressesView(View):
return render(request, 'ipam/prefix_ipaddresses.html', { return render(request, 'ipam/prefix_ipaddresses.html', {
'prefix': prefix, 'prefix': prefix,
'first_available_ip': prefix.get_first_available_ip(),
'ip_table': ip_table, 'ip_table': ip_table,
'permissions': permissions, 'permissions': permissions,
'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf.pk if prefix.vrf else '0', prefix.prefix), 'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf.pk if prefix.vrf else '0', prefix.prefix),

View File

@ -20,6 +20,9 @@ class FormlessBrowsableAPIRenderer(BrowsableAPIRenderer):
def show_form_for_method(self, *args, **kwargs): def show_form_for_method(self, *args, **kwargs):
return False return False
def get_filter_form(self, data, view, request):
return None
# #
# Authentication # Authentication

View File

@ -58,9 +58,10 @@ $(document).ready(function() {
// Glean configured hostnames/interfaces from the DOM // Glean configured hostnames/interfaces from the DOM
var configured_device = row.children('td.configured_device').attr('data'); var configured_device = row.children('td.configured_device').attr('data');
var configured_interface = row.children('td.configured_interface').attr('data'); var configured_interface = row.children('td.configured_interface').attr('data');
var configured_interface_short = null;
if (configured_interface) { if (configured_interface) {
// Match long-form IOS names against short ones (e.g. Gi0/1 == GigabitEthernet0/1). // Match long-form IOS names against short ones (e.g. Gi0/1 == GigabitEthernet0/1).
configured_interface = configured_interface.replace(/^([A-Z][a-z])[^0-9]*([0-9\/]+)$/, "$1$2"); configured_interface_short = configured_interface.replace(/^([A-Z][a-z])[^0-9]*([0-9\/]+)$/, "$1$2");
} }
// Clean up hostnames/interfaces learned via LLDP // Clean up hostnames/interfaces learned via LLDP
@ -76,6 +77,8 @@ $(document).ready(function() {
row.addClass('info'); row.addClass('info');
} else if (configured_device == lldp_device && configured_interface == lldp_interface) { } else if (configured_device == lldp_device && configured_interface == lldp_interface) {
row.addClass('success'); row.addClass('success');
} else if (configured_device == lldp_device && configured_interface_short == lldp_interface) {
row.addClass('success');
} else { } else {
row.addClass('danger'); row.addClass('danger');
} }

View File

@ -13,7 +13,7 @@
Import device types Import device types
</a> </a>
{% endif %} {% endif %}
{% include 'inc/export_button.html' with obj_type='devicetypes' %} {% include 'inc/export_button.html' with obj_type='device types' %}
</div> </div>
<h1>{% block title %}Device Types{% endblock %}</h1> <h1>{% block title %}Device Types{% endblock %}</h1>
<div class="row"> <div class="row">

View File

@ -13,7 +13,7 @@
Import rack groups Import rack groups
</a> </a>
{% endif %} {% endif %}
{% include 'inc/export_button.html' with obj_type='rackgroups' %} {% include 'inc/export_button.html' with obj_type='rack groups' %}
</div> </div>
<h1>{% block title %}Rack Groups{% endblock %}</h1> <h1>{% block title %}Rack Groups{% endblock %}</h1>
<div class="row"> <div class="row">

View File

@ -22,8 +22,13 @@
</div> </div>
</div> </div>
<div class="pull-right"> <div class="pull-right">
{% if perms.ipam.add_ipaddress %} {% if perms.ipam.add_prefix and active_tab == 'prefixes' and first_available_prefix %}
<a href="{% url 'ipam:ipaddress_add' %}?address={{ prefix.get_first_available_ip }}&vrf={{ prefix.vrf.pk }}&tenant_group={{ prefix.tenant.group.pk }}&tenant={{ prefix.tenant.pk }}" class="btn btn-success"> <a href="{% url 'ipam:prefix_add' %}?prefix={{ first_available_prefix }}&vrf={{ prefix.vrf.pk }}&site={{ prefix.site.pk }}&tenant_group={{ prefix.tenant.group.pk }}&tenant={{ prefix.tenant.pk }}" class="btn btn-success">
<i class="fa fa-plus" aria-hidden="true"></i> Add Child Prefix
</a>
{% endif %}
{% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and first_available_ip %}
<a href="{% url 'ipam:ipaddress_add' %}?address={{ first_available_ip }}&vrf={{ prefix.vrf.pk }}&tenant_group={{ prefix.tenant.group.pk }}&tenant={{ prefix.tenant.pk }}" class="btn btn-success">
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
Add an IP Address Add an IP Address
</a> </a>
@ -45,5 +50,6 @@
{% include 'inc/created_updated.html' with obj=prefix %} {% include 'inc/created_updated.html' with obj=prefix %}
<ul class="nav nav-tabs" style="margin-bottom: 20px"> <ul class="nav nav-tabs" style="margin-bottom: 20px">
<li role="presentation"{% if active_tab == 'prefix' %} class="active"{% endif %}><a href="{% url 'ipam:prefix' pk=prefix.pk %}">Prefix</a></li> <li role="presentation"{% if active_tab == 'prefix' %} class="active"{% endif %}><a href="{% url 'ipam:prefix' pk=prefix.pk %}">Prefix</a></li>
<li role="presentation"{% if active_tab == 'prefixes' %} class="active"{% endif %}><a href="{% url 'ipam:prefix_prefixes' pk=prefix.pk %}">Child Prefixes <span class="badge">{{ prefix.get_child_prefixes.count }}</span></a></li>
<li role="presentation"{% if active_tab == 'ip-addresses' %} class="active"{% endif %}><a href="{% url 'ipam:prefix_ipaddresses' pk=prefix.pk %}">IP Addresses <span class="badge">{{ prefix.get_child_ips.count }}</span></a></li> <li role="presentation"{% if active_tab == 'ip-addresses' %} class="active"{% endif %}><a href="{% url 'ipam:prefix_ipaddresses' pk=prefix.pk %}">IP Addresses <span class="badge">{{ prefix.get_child_ips.count }}</span></a></li>
</ul> </ul>

View File

@ -139,15 +139,4 @@
{% include 'panel_table.html' with table=parent_prefix_table heading='Parent Prefixes' %} {% include 'panel_table.html' with table=parent_prefix_table heading='Parent Prefixes' %}
</div> </div>
</div> </div>
<div class="row">
<div class="col-md-12">
{% if child_prefix_table.rows %}
{% include 'utilities/obj_table.html' with table=child_prefix_table table_template='panel_table.html' heading='Child Prefixes' parent=prefix bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %}
{% elif prefix.new_subnet %}
<a href="{% url 'ipam:prefix_add' %}?prefix={{ prefix.new_subnet }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}{% if prefix.site %}&site={{ prefix.site.pk }}{% endif %}" class="btn btn-success">
<i class="fa fa-plus" aria-hidden="true"></i> Add Child Prefix
</a>
{% endif %}
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -3,10 +3,10 @@
{% block title %}{{ prefix }} - IP Addresses{% endblock %} {% block title %}{{ prefix }} - IP Addresses{% endblock %}
{% block content %} {% block content %}
{% include 'ipam/inc/prefix_header.html' with active_tab='ip-addresses' %} {% include 'ipam/inc/prefix_header.html' with active_tab='ip-addresses' %}
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
{% include 'utilities/obj_table.html' with table=ip_table table_template='panel_table.html' heading='IP Addresses' bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %} {% include 'utilities/obj_table.html' with table=ip_table table_template='panel_table.html' heading='IP Addresses' bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %}
</div>
</div> </div>
</div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,12 @@
{% extends '_base.html' %}
{% block title %}{{ prefix }} - Prefixes{% endblock %}
{% block content %}
{% include 'ipam/inc/prefix_header.html' with active_tab='prefixes' %}
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with table=prefix_table table_template='panel_table.html' heading='Child Prefixes' bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' parent=prefix %}
</div>
</div>
{% endblock %}

View File

@ -13,12 +13,14 @@
{% for obj_type in results %} {% for obj_type in results %}
<h3 id="{{ obj_type.name|lower }}">{{ obj_type.name|bettertitle }}</h3> <h3 id="{{ obj_type.name|lower }}">{{ obj_type.name|bettertitle }}</h3>
{% include 'panel_table.html' with table=obj_type.table hide_paginator=True %} {% include 'panel_table.html' with table=obj_type.table hide_paginator=True %}
{% if obj_type.table.page.has_next %} <a href="{{ obj_type.url }}" class="btn btn-primary pull-right">
<a href="{{ obj_type.url }}" class="btn btn-primary pull-right"> <span class="fa fa-arrow-right" aria-hidden="true"></span>
<span class="fa fa-arrow-right" aria-hidden="true"></span> {% if obj_type.table.page.has_next %}
See all {{ obj_type.table.page.paginator.count }} results See all {{ obj_type.table.page.paginator.count }} results
</a> {% else %}
{% endif %} Refine search
{% endif %}
</a>
<div class="clearfix"></div> <div class="clearfix"></div>
{% endfor %} {% endfor %}
</div> </div>

View File

@ -81,7 +81,7 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
group = FilterChoiceField( group = FilterChoiceField(
queryset=TenantGroup.objects.annotate(filter_count=Count('tenants')), queryset=TenantGroup.objects.annotate(filter_count=Count('tenants')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )

View File

@ -42,7 +42,7 @@ class NullableModelMultipleChoiceField(forms.ModelMultipleChoiceField):
""" """
iterator = forms.models.ModelChoiceIterator iterator = forms.models.ModelChoiceIterator
def __init__(self, null_value=0, null_label='None', *args, **kwargs): def __init__(self, null_value=0, null_label='-- None --', *args, **kwargs):
self.null_value = null_value self.null_value = null_value
self.null_label = null_label self.null_label = null_label
super(NullableModelMultipleChoiceField, self).__init__(*args, **kwargs) super(NullableModelMultipleChoiceField, self).__init__(*args, **kwargs)

View File

@ -407,11 +407,25 @@ class SlugField(forms.SlugField):
self.widget.attrs['slug-source'] = slug_source self.widget.attrs['slug-source'] = slug_source
class FilterChoiceFieldMixin(object): class FilterChoiceIterator(forms.models.ModelChoiceIterator):
iterator = forms.models.ModelChoiceIterator
def __init__(self, null_option=None, *args, **kwargs): def __iter__(self):
self.null_option = null_option # Filter on "empty" choice using FILTERS_NULL_CHOICE_VALUE (instead of an empty string)
if self.field.null_label is not None:
yield (settings.FILTERS_NULL_CHOICE_VALUE, self.field.null_label)
queryset = self.queryset.all()
# Can't use iterator() when queryset uses prefetch_related()
if not queryset._prefetch_related_lookups:
queryset = queryset.iterator()
for obj in queryset:
yield self.choice(obj)
class FilterChoiceFieldMixin(object):
iterator = FilterChoiceIterator
def __init__(self, null_label=None, *args, **kwargs):
self.null_label = null_label
if 'required' not in kwargs: if 'required' not in kwargs:
kwargs['required'] = False kwargs['required'] = False
if 'widget' not in kwargs: if 'widget' not in kwargs:
@ -424,15 +438,6 @@ class FilterChoiceFieldMixin(object):
return '{} ({})'.format(label, obj.filter_count) return '{} ({})'.format(label, obj.filter_count)
return label return label
def _get_choices(self):
if hasattr(self, '_choices'):
return self._choices
if self.null_option is not None:
return itertools.chain([self.null_option], self.iterator(self))
return self.iterator(self)
choices = property(_get_choices, forms.ChoiceField._set_choices)
class FilterChoiceField(FilterChoiceFieldMixin, forms.ModelMultipleChoiceField): class FilterChoiceField(FilterChoiceFieldMixin, forms.ModelMultipleChoiceField):
pass pass

View File

@ -4,7 +4,7 @@ import sys
from django.conf import settings from django.conf import settings
from django.db import ProgrammingError from django.db import ProgrammingError
from django.http import HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse from django.urls import reverse
@ -61,6 +61,10 @@ class ExceptionHandlingMiddleware(object):
if settings.DEBUG: if settings.DEBUG:
return return
# Ignore Http404s (defer to Django's built-in 404 handling)
if isinstance(exception, Http404):
return
# Determine the type of exception # Determine the type of exception
if isinstance(exception, ProgrammingError): if isinstance(exception, ProgrammingError):
template_name = 'exceptions/programming_error.html' template_name = 'exceptions/programming_error.html'

View File

@ -309,8 +309,14 @@ class BulkCreateView(View):
def get(self, request): def get(self, request):
# Set initial values for visible form fields from query args
initial = {}
for field in getattr(self.model_form._meta, 'fields', []):
if request.GET.get(field):
initial[field] = request.GET[field]
form = self.form() form = self.form()
model_form = self.model_form() model_form = self.model_form(initial=initial)
return render(request, self.template_name, { return render(request, self.template_name, {
'obj_type': self.model_form._meta.model._meta.verbose_name, 'obj_type': self.model_form._meta.model._meta.verbose_name,

View File

@ -84,6 +84,17 @@ class VirtualMachineFilter(CustomFieldFilterSet):
to_field_name='slug', to_field_name='slug',
label='Cluster group (slug)', label='Cluster group (slug)',
) )
cluster_type_id = django_filters.ModelMultipleChoiceFilter(
name='cluster__type',
queryset=ClusterType.objects.all(),
label='Cluster type (ID)',
)
cluster_type = django_filters.ModelMultipleChoiceFilter(
name='cluster__type__slug',
queryset=ClusterType.objects.all(),
to_field_name='slug',
label='Cluster type (slug)',
)
cluster_id = django_filters.ModelMultipleChoiceFilter( cluster_id = django_filters.ModelMultipleChoiceFilter(
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
label='Cluster (ID)', label='Cluster (ID)',

View File

@ -137,13 +137,13 @@ class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm):
group = FilterChoiceField( group = FilterChoiceField(
queryset=ClusterGroup.objects.annotate(filter_count=Count('clusters')), queryset=ClusterGroup.objects.annotate(filter_count=Count('clusters')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None'), null_label='-- None --',
required=False, required=False,
) )
site = FilterChoiceField( site = FilterChoiceField(
queryset=Site.objects.annotate(filter_count=Count('clusters')), queryset=Site.objects.annotate(filter_count=Count('clusters')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None'), null_label='-- None --',
required=False, required=False,
) )
@ -338,7 +338,12 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm):
cluster_group = FilterChoiceField( cluster_group = FilterChoiceField(
queryset=ClusterGroup.objects.all(), queryset=ClusterGroup.objects.all(),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
)
cluster_type = FilterChoiceField(
queryset=ClusterType.objects.all(),
to_field_name='slug',
null_label='-- None --'
) )
cluster_id = FilterChoiceField( cluster_id = FilterChoiceField(
queryset=Cluster.objects.annotate(filter_count=Count('virtual_machines')), queryset=Cluster.objects.annotate(filter_count=Count('virtual_machines')),
@ -347,23 +352,23 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm):
site = FilterChoiceField( site = FilterChoiceField(
queryset=Site.objects.annotate(filter_count=Count('clusters__virtual_machines')), queryset=Site.objects.annotate(filter_count=Count('clusters__virtual_machines')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
role = FilterChoiceField( role = FilterChoiceField(
queryset=DeviceRole.objects.filter(vm_role=True).annotate(filter_count=Count('virtual_machines')), queryset=DeviceRole.objects.filter(vm_role=True).annotate(filter_count=Count('virtual_machines')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
status = forms.MultipleChoiceField(choices=vm_status_choices, required=False) status = forms.MultipleChoiceField(choices=vm_status_choices, required=False)
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('virtual_machines')), queryset=Tenant.objects.annotate(filter_count=Count('virtual_machines')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )
platform = FilterChoiceField( platform = FilterChoiceField(
queryset=Platform.objects.annotate(filter_count=Count('virtual_machines')), queryset=Platform.objects.annotate(filter_count=Count('virtual_machines')),
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_label='-- None --'
) )

View File

@ -139,6 +139,7 @@ class Cluster(CreatedUpdatedModel, CustomFieldModel):
self.name, self.name,
self.type.name, self.type.name,
self.group.name if self.group else None, self.group.name if self.group else None,
self.site.name if self.site else None,
self.comments, self.comments,
]) ])