Compare commits

..

32 Commits

Author SHA1 Message Date
Jeremy Stretch
ec0cb7a8bc Merge pull request #1789 from digitalocean/develop
Release v2.2.8
2017-12-20 15:27:22 -05:00
Jeremy Stretch
841471104b Release v2.2.8 2017-12-20 15:24:07 -05:00
Jeremy Stretch
ac71416eb9 Closes #1775: Added instructions for enabling STARTTLS for LDAP authentication 2017-12-20 14:48:42 -05:00
Jeremy Stretch
779d685335 Closes #1784: Added cluster_type filters for virtual machines 2017-12-20 14:24:12 -05:00
Jeremy Stretch
4d1e798c56 Merge pull request #1780 from explody/fix_1778
Fix for #1778.
2017-12-20 14:17:45 -05:00
Jeremy Stretch
a598035236 Closes #1774: Include a button to refine search results for all object types under global search 2017-12-20 14:09:52 -05:00
Jeremy Stretch
50395aa821 Closes #1773: Moved child prefixes table to its own view 2017-12-20 14:01:37 -05:00
Jeremy Stretch
6d9c8fd85b Fixes #1787: Added missing site field to virtualization cluster CSV export 2017-12-20 13:18:30 -05:00
Jeremy Stretch
c3599bacf2 Fixes #1785: Omit filter forms from browsable API 2017-12-19 15:30:55 -05:00
Jeremy Stretch
c10481b99d Fixes #1783: Added vm_role filter for device roles 2017-12-19 09:37:26 -05:00
Mike Culbertson
1cebc1248b Fix for #1778.
This will set initial values for visible bulk-add form fields from query args.
2017-12-16 12:28:37 -05:00
Jeremy Stretch
c97f7041a7 Closes #1772: Added position filter for devices 2017-12-14 13:12:04 -05:00
Jeremy Stretch
89bfb4f722 Closes #1771: Added name filter for racks 2017-12-14 13:05:26 -05:00
Jeremy Stretch
da3935ff36 Fixes #1766: Fixed display of "select all" button on device power outlets list 2017-12-13 15:23:35 -05:00
Jeremy Stretch
06810bff91 Fixes #1764: Fixed typos in export buttons 2017-12-13 11:55:31 -05:00
Jeremy Stretch
a9af75bbd1 Fixes #1767: Use proper template for 404 responses 2017-12-13 11:49:36 -05:00
Jeremy Stretch
be6ef15ffa Post-release version bump 2017-12-07 14:54:16 -05:00
Jeremy Stretch
e98f0c39d1 Merge pull request #1757 from digitalocean/develop
Release v2.2.7
2017-12-07 14:52:28 -05:00
Jeremy Stretch
5666079d92 Release v2.2.7 2017-12-07 14:50:44 -05:00
Jeremy Stretch
85f5ba9a25 Fixes #1756: Improved natural ordering of console server ports and power outlets 2017-12-07 13:22:48 -05:00
Jeremy Stretch
df141a48d9 Fixed typo 2017-12-06 12:17:04 -05:00
Jeremy Stretch
fed6fc131b Fixes #1751: Corrected filtering for IPv6 addresses containing letters 2017-12-05 16:10:45 -05:00
Jeremy Stretch
cf49891853 Fixes #1740: Delete session_key cookie on logout 2017-12-05 14:19:24 -05:00
Jeremy Stretch
de2a894269 Closes #1737: Added a 'contains' API filter to find all prefixes containing a given IP or prefix 2017-11-30 12:37:41 -05:00
Jeremy Stretch
34d10f8db7 Fixes #1741: Fixed Unicode support for secret plaintexts 2017-11-29 15:16:11 -05:00
Jeremy Stretch
68f76465cf Fixes #1743: Include number of instances for device types in global search 2017-11-29 14:07:41 -05:00
Jeremy Stretch
45d6955260 Fixed search field length in search view 2017-11-28 09:27:31 -05:00
Jeremy Stretch
30df060357 Closes #1722: Added VM count to site view 2017-11-27 10:59:24 -05:00
Jeremy Stretch
252be84bf0 Corrected tenant inheritance for new IP addresses created from a parent prefix 2017-11-22 13:00:48 -05:00
Jeremy Stretch
40ab272995 Fixes #1721: Differentiated child IP count from utilization percentage for prefixes 2017-11-22 12:40:58 -05:00
Jeremy Stretch
0ec3b5db8b Closes #1722: Added virtual machine count to sites list 2017-11-22 12:19:04 -05:00
Jeremy Stretch
5dc9723585 Post-release version bump 2017-11-16 12:01:09 -05:00
34 changed files with 265 additions and 143 deletions

View File

@@ -123,7 +123,7 @@ $ curl -X PATCH -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc
Send an authenticated `DELETE` request to the site detail endpoint. Send an authenticated `DELETE` request to the site detail endpoint.
``` ```
$ curl -v X DELETE -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/16/ $ curl -v -X DELETE -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/16/
* Connected to localhost (127.0.0.1) port 8000 (#0) * Connected to localhost (127.0.0.1) port 8000 (#0)
> DELETE /api/dcim/sites/16/ HTTP/1.1 > DELETE /api/dcim/sites/16/ HTTP/1.1
> User-Agent: curl/7.35.0 > User-Agent: curl/7.35.0

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,7 +80,7 @@ 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 `GroupOfNamesType()` instead of `NestedGroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`.
```python ```python

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():
@@ -330,7 +330,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):
@@ -455,7 +455,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

@@ -143,6 +143,11 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
def count_circuits(self): def count_circuits(self):
return Circuit.objects.filter(terminations__site=self).count() return Circuit.objects.filter(terminations__site=self).count()
@property
def count_vms(self):
from virtualization.models import VirtualMachine
return VirtualMachine.objects.filter(cluster__site=self).count()
# #
# Racks # Racks
@@ -1089,16 +1094,11 @@ class ConsolePort(models.Model):
class ConsoleServerPortManager(models.Manager): class ConsoleServerPortManager(models.Manager):
def get_queryset(self): def get_queryset(self):
""" # Pad any trailing digits to effect natural sorting
Include the trailing numeric portion of each port name to allow for proper ordering.
For example:
Port 1, Port 2, Port 3 ... Port 9, Port 10, Port 11 ...
Instead of:
Port 1, Port 10, Port 11 ... Port 19, Port 2, Port 20 ...
"""
return super(ConsoleServerPortManager, self).get_queryset().extra(select={ return super(ConsoleServerPortManager, self).get_queryset().extra(select={
'name_as_integer': "CAST(substring(dcim_consoleserverport.name FROM '[0-9]+$') AS INTEGER)", 'name_padded': "CONCAT(REGEXP_REPLACE(dcim_consoleserverport.name, '\d+$', ''), "
}).order_by('device', 'name_as_integer') "LPAD(SUBSTRING(dcim_consoleserverport.name FROM '\d+$'), 8, '0'))",
}).order_by('device', 'name_padded')
@python_2_unicode_compatible @python_2_unicode_compatible
@@ -1171,9 +1171,10 @@ class PowerPort(models.Model):
class PowerOutletManager(models.Manager): class PowerOutletManager(models.Manager):
def get_queryset(self): def get_queryset(self):
# Pad any trailing digits to effect natural sorting
return super(PowerOutletManager, self).get_queryset().extra(select={ return super(PowerOutletManager, self).get_queryset().extra(select={
'name_padded': "CONCAT(SUBSTRING(dcim_poweroutlet.name FROM '^[^0-9]+'), " 'name_padded': "CONCAT(REGEXP_REPLACE(dcim_poweroutlet.name, '\d+$', ''), "
"LPAD(SUBSTRING(dcim_poweroutlet.name FROM '[0-9\/]+$'), 8, '0'))", "LPAD(SUBSTRING(dcim_poweroutlet.name FROM '\d+$'), 8, '0'))",
}).order_by('device', 'name_padded') }).order_by('device', 'name_padded')

View File

@@ -153,11 +153,12 @@ class SiteDetailTable(SiteTable):
prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes') prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes')
vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs') vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs')
circuit_count = tables.Column(accessor=Accessor('count_circuits'), orderable=False, verbose_name='Circuits') circuit_count = tables.Column(accessor=Accessor('count_circuits'), orderable=False, verbose_name='Circuits')
vm_count = tables.Column(accessor=Accessor('count_vms'), orderable=False, verbose_name='VMs')
class Meta(SiteTable.Meta): class Meta(SiteTable.Meta):
fields = ( fields = (
'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count', 'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
'vlan_count', 'circuit_count', 'vlan_count', 'circuit_count', 'vm_count',
) )

View File

@@ -25,6 +25,7 @@ from utilities.views import (
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView, BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView,
ComponentEditView, ObjectDeleteView, ObjectEditView, ObjectListView, ComponentEditView, ObjectDeleteView, ObjectEditView, ObjectListView,
) )
from virtualization.models import VirtualMachine
from . import filters, forms, tables from . import filters, forms, tables
from .constants import CONNECTION_STATUS_CONNECTED from .constants import CONNECTION_STATUS_CONNECTED
from .models import ( from .models import (
@@ -134,6 +135,7 @@ class SiteView(View):
'prefix_count': Prefix.objects.filter(site=site).count(), 'prefix_count': Prefix.objects.filter(site=site).count(),
'vlan_count': VLAN.objects.filter(site=site).count(), 'vlan_count': VLAN.objects.filter(site=site).count(),
'circuit_count': Circuit.objects.filter(terminations__site=site).count(), 'circuit_count': Circuit.objects.filter(terminations__site=site).count(),
'vm_count': VirtualMachine.objects.filter(cluster__site=site).count(),
} }
rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks')) rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
topology_maps = TopologyMap.objects.filter(site=site) topology_maps = TopologyMap.objects.filter(site=site)
@@ -808,15 +810,11 @@ class DeviceView(View):
console_ports = natsorted( console_ports = natsorted(
ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name') ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name')
) )
cs_ports = natsorted( cs_ports = ConsoleServerPort.objects.filter(device=device).select_related('connected_console')
ConsoleServerPort.objects.filter(device=device).select_related('connected_console'), key=attrgetter('name')
)
power_ports = natsorted( power_ports = natsorted(
PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name') PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name')
) )
power_outlets = natsorted( power_outlets = PowerOutlet.objects.filter(device=device).select_related('connected_port')
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
)
interfaces = Interface.objects.order_naturally( interfaces = Interface.objects.order_naturally(
device.device_type.interface_ordering device.device_type.interface_ordering
).filter( ).filter(

View File

@@ -5,10 +5,7 @@ from django.db import models
from netaddr import IPNetwork from netaddr import IPNetwork
from .formfields import IPFormField from .formfields import IPFormField
from .lookups import ( from . import lookups
EndsWith, IEndsWith, IRegex, IStartsWith, NetContained, NetContainedOrEqual, NetContains, NetContainsOrEquals,
NetHost, NetHostContained, NetMaskLength, Regex, StartsWith,
)
def prefix_validator(prefix): def prefix_validator(prefix):
@@ -57,17 +54,18 @@ class IPNetworkField(BaseIPField):
return 'cidr' return 'cidr'
IPNetworkField.register_lookup(EndsWith) IPNetworkField.register_lookup(lookups.IExact)
IPNetworkField.register_lookup(IEndsWith) IPNetworkField.register_lookup(lookups.EndsWith)
IPNetworkField.register_lookup(StartsWith) IPNetworkField.register_lookup(lookups.IEndsWith)
IPNetworkField.register_lookup(IStartsWith) IPNetworkField.register_lookup(lookups.StartsWith)
IPNetworkField.register_lookup(Regex) IPNetworkField.register_lookup(lookups.IStartsWith)
IPNetworkField.register_lookup(IRegex) IPNetworkField.register_lookup(lookups.Regex)
IPNetworkField.register_lookup(NetContained) IPNetworkField.register_lookup(lookups.IRegex)
IPNetworkField.register_lookup(NetContainedOrEqual) IPNetworkField.register_lookup(lookups.NetContained)
IPNetworkField.register_lookup(NetContains) IPNetworkField.register_lookup(lookups.NetContainedOrEqual)
IPNetworkField.register_lookup(NetContainsOrEquals) IPNetworkField.register_lookup(lookups.NetContains)
IPNetworkField.register_lookup(NetMaskLength) IPNetworkField.register_lookup(lookups.NetContainsOrEquals)
IPNetworkField.register_lookup(lookups.NetMaskLength)
class IPAddressField(BaseIPField): class IPAddressField(BaseIPField):
@@ -80,16 +78,17 @@ class IPAddressField(BaseIPField):
return 'inet' return 'inet'
IPAddressField.register_lookup(EndsWith) IPAddressField.register_lookup(lookups.IExact)
IPAddressField.register_lookup(IEndsWith) IPAddressField.register_lookup(lookups.EndsWith)
IPAddressField.register_lookup(StartsWith) IPAddressField.register_lookup(lookups.IEndsWith)
IPAddressField.register_lookup(IStartsWith) IPAddressField.register_lookup(lookups.StartsWith)
IPAddressField.register_lookup(Regex) IPAddressField.register_lookup(lookups.IStartsWith)
IPAddressField.register_lookup(IRegex) IPAddressField.register_lookup(lookups.Regex)
IPAddressField.register_lookup(NetContained) IPAddressField.register_lookup(lookups.IRegex)
IPAddressField.register_lookup(NetContainedOrEqual) IPAddressField.register_lookup(lookups.NetContained)
IPAddressField.register_lookup(NetContains) IPAddressField.register_lookup(lookups.NetContainedOrEqual)
IPAddressField.register_lookup(NetContainsOrEquals) IPAddressField.register_lookup(lookups.NetContains)
IPAddressField.register_lookup(NetHost) IPAddressField.register_lookup(lookups.NetContainsOrEquals)
IPAddressField.register_lookup(NetHostContained) IPAddressField.register_lookup(lookups.NetHost)
IPAddressField.register_lookup(NetMaskLength) IPAddressField.register_lookup(lookups.NetHostContained)
IPAddressField.register_lookup(lookups.NetMaskLength)

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
import django_filters import django_filters
from django.db.models import Q from django.db.models import Q
from netaddr import IPNetwork import netaddr
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
from dcim.models import Site, Device, Interface from dcim.models import Site, Device, Interface
@@ -79,7 +79,7 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset return queryset
qs_filter = Q(description__icontains=value) qs_filter = Q(description__icontains=value)
try: try:
prefix = str(IPNetwork(value.strip()).cidr) prefix = str(netaddr.IPNetwork(value.strip()).cidr)
qs_filter |= Q(prefix__net_contains_or_equals=prefix) qs_filter |= Q(prefix__net_contains_or_equals=prefix)
except (AddrFormatError, ValueError): except (AddrFormatError, ValueError):
pass pass
@@ -112,6 +112,10 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search_within_include', method='search_within_include',
label='Within and including prefix', label='Within and including prefix',
) )
contains = django_filters.CharFilter(
method='search_contains',
label='Prefixes which contain this prefix or IP',
)
mask_length = django_filters.NumberFilter( mask_length = django_filters.NumberFilter(
method='filter_mask_length', method='filter_mask_length',
label='Mask length', label='Mask length',
@@ -178,7 +182,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset return queryset
qs_filter = Q(description__icontains=value) qs_filter = Q(description__icontains=value)
try: try:
prefix = str(IPNetwork(value.strip()).cidr) prefix = str(netaddr.IPNetwork(value.strip()).cidr)
qs_filter |= Q(prefix__net_contains_or_equals=prefix) qs_filter |= Q(prefix__net_contains_or_equals=prefix)
except (AddrFormatError, ValueError): except (AddrFormatError, ValueError):
pass pass
@@ -189,7 +193,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
if not value: if not value:
return queryset return queryset
try: try:
query = str(IPNetwork(value).cidr) query = str(netaddr.IPNetwork(value).cidr)
return queryset.filter(prefix__net_contained=query) return queryset.filter(prefix__net_contained=query)
except (AddrFormatError, ValueError): except (AddrFormatError, ValueError):
return queryset.none() return queryset.none()
@@ -199,11 +203,25 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
if not value: if not value:
return queryset return queryset
try: try:
query = str(IPNetwork(value).cidr) query = str(netaddr.IPNetwork(value).cidr)
return queryset.filter(prefix__net_contained_or_equal=query) return queryset.filter(prefix__net_contained_or_equal=query)
except (AddrFormatError, ValueError): except (AddrFormatError, ValueError):
return queryset.none() return queryset.none()
def search_contains(self, queryset, name, value):
value = value.strip()
if not value:
return queryset
try:
# Searching by prefix
if '/' in value:
return queryset.filter(prefix__net_contains_or_equals=str(netaddr.IPNetwork(value).cidr))
# Searching by IP address
else:
return queryset.filter(prefix__net_contains=str(netaddr.IPAddress(value)))
except (AddrFormatError, ValueError):
return queryset.none()
def filter_mask_length(self, queryset, name, value): def filter_mask_length(self, queryset, name, value):
if not value: if not value:
return queryset return queryset
@@ -296,7 +314,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
if not value: if not value:
return queryset return queryset
try: try:
query = str(IPNetwork(value.strip()).cidr) query = str(netaddr.IPNetwork(value.strip()).cidr)
return queryset.filter(address__net_host_contained=query) return queryset.filter(address__net_host_contained=query)
except (AddrFormatError, ValueError): except (AddrFormatError, ValueError):
return queryset.none() return queryset.none()

View File

@@ -13,12 +13,21 @@ class NetFieldDecoratorMixin(object):
return lhs_string, lhs_params return lhs_string, lhs_params
class IExact(NetFieldDecoratorMixin, lookups.IExact):
def get_rhs_op(self, connection, rhs):
return '= LOWER(%s)' % rhs
class EndsWith(NetFieldDecoratorMixin, lookups.EndsWith): class EndsWith(NetFieldDecoratorMixin, lookups.EndsWith):
lookup_name = 'endswith' pass
class IEndsWith(NetFieldDecoratorMixin, lookups.IEndsWith): class IEndsWith(NetFieldDecoratorMixin, lookups.IEndsWith):
lookup_name = 'iendswith' pass
def get_rhs_op(self, connection, rhs):
return 'LIKE LOWER(%s)' % rhs
class StartsWith(NetFieldDecoratorMixin, lookups.StartsWith): class StartsWith(NetFieldDecoratorMixin, lookups.StartsWith):
@@ -26,15 +35,18 @@ class StartsWith(NetFieldDecoratorMixin, lookups.StartsWith):
class IStartsWith(NetFieldDecoratorMixin, lookups.IStartsWith): class IStartsWith(NetFieldDecoratorMixin, lookups.IStartsWith):
lookup_name = 'istartswith' pass
def get_rhs_op(self, connection, rhs):
return 'LIKE LOWER(%s)' % rhs
class Regex(NetFieldDecoratorMixin, lookups.Regex): class Regex(NetFieldDecoratorMixin, lookups.Regex):
lookup_name = 'regex' pass
class IRegex(NetFieldDecoratorMixin, lookups.IRegex): class IRegex(NetFieldDecoratorMixin, lookups.IRegex):
lookup_name = 'iregex' pass
class NetContainsOrEquals(Lookup): class NetContainsOrEquals(Lookup):

View File

@@ -281,12 +281,28 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
def get_duplicates(self): def get_duplicates(self):
return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk) return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk)
def get_child_prefixes(self):
"""
Return all Prefixes within this Prefix and VRF.
"""
return Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf)
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.
@@ -304,15 +320,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):
""" """
@@ -330,17 +354,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

@@ -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

@@ -454,9 +454,6 @@ class PrefixView(View):
except Aggregate.DoesNotExist: except Aggregate.DoesNotExist:
aggregate = None aggregate = None
# Count child IP addresses
ipaddress_count = prefix.get_child_ips().count()
# Parent prefixes table # Parent prefixes table
parent_prefixes = Prefix.objects.filter( parent_prefixes = Prefix.objects.filter(
Q(vrf=prefix.vrf) | Q(vrf__isnull=True) Q(vrf=prefix.vrf) | Q(vrf__isnull=True)
@@ -479,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)
@@ -487,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 = {
@@ -504,16 +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(),
'ipaddress_count': ipaddress_count, 'prefix_table': prefix_table,
'parent_prefix_table': parent_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),
}) })
@@ -548,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

@@ -38,7 +38,7 @@ OBJ_TYPE_CHOICES = (
class SearchForm(BootstrapMixin, forms.Form): class SearchForm(BootstrapMixin, forms.Form):
q = forms.CharField( q = forms.CharField(
label='Search', widget=forms.TextInput(attrs={'style': 'width: 350px'}) label='Search'
) )
obj_type = forms.ChoiceField( obj_type = forms.ChoiceField(
choices=OBJ_TYPE_CHOICES, required=False, label='Type' choices=OBJ_TYPE_CHOICES, required=False, label='Type'

View File

@@ -13,7 +13,7 @@ except ImportError:
) )
VERSION = '2.2.6' VERSION = '2.2.8'
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

@@ -2,6 +2,7 @@ from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from django.db.models import Count
from django.shortcuts import render from django.shortcuts import render
from django.views.generic import View from django.views.generic import View
from rest_framework.response import Response from rest_framework.response import Response
@@ -58,7 +59,7 @@ SEARCH_TYPES = OrderedDict((
'url': 'dcim:rack_list', 'url': 'dcim:rack_list',
}), }),
('devicetype', { ('devicetype', {
'queryset': DeviceType.objects.select_related('manufacturer'), 'queryset': DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances')),
'filter': DeviceTypeFilter, 'filter': DeviceTypeFilter,
'table': DeviceTypeTable, 'table': DeviceTypeTable,
'url': 'dcim:devicetype_list', 'url': 'dcim:devicetype_list',

View File

@@ -303,6 +303,7 @@ class Secret(CreatedUpdatedModel):
|LL|MySecret|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| |LL|MySecret|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|
+--+--------+-------------------------------------------+ +--+--------+-------------------------------------------+
""" """
s = s.encode('utf8')
if len(s) > 65535: if len(s) > 65535:
raise ValueError("Maximum plaintext size is 65535 bytes.") raise ValueError("Maximum plaintext size is 65535 bytes.")
# Minimum ciphertext size is 64 bytes to conceal the length of short secrets. # Minimum ciphertext size is 64 bytes to conceal the length of short secrets.
@@ -315,7 +316,7 @@ class Secret(CreatedUpdatedModel):
return ( return (
chr(len(s) >> 8).encode() + chr(len(s) >> 8).encode() +
chr(len(s) % 256).encode() + chr(len(s) % 256).encode() +
s.encode() + s +
os.urandom(pad_length) os.urandom(pad_length)
) )
@@ -324,11 +325,11 @@ class Secret(CreatedUpdatedModel):
Consume the first two bytes of s as a plaintext length indicator and return only that many bytes as the Consume the first two bytes of s as a plaintext length indicator and return only that many bytes as the
plaintext. plaintext.
""" """
if isinstance(s[0], int): if isinstance(s[0], str):
plaintext_length = (s[0] << 8) + s[1]
elif isinstance(s[0], str):
plaintext_length = (ord(s[0]) << 8) + ord(s[1]) plaintext_length = (ord(s[0]) << 8) + ord(s[1])
return s[2:plaintext_length + 2].decode() else:
plaintext_length = (s[0] << 8) + s[1]
return s[2:plaintext_length + 2].decode('utf8')
def encrypt(self, secret_key): def encrypt(self, secret_key):
""" """

View File

@@ -166,7 +166,7 @@ def secret_edit(request, pk):
# Create and encrypt the new Secret # Create and encrypt the new Secret
if master_key is not None: if master_key is not None:
secret = form.save(commit=False) secret = form.save(commit=False)
secret.plaintext = str(form.cleaned_data['plaintext']) secret.plaintext = form.cleaned_data['plaintext']
secret.encrypt(master_key) secret.encrypt(master_key)
secret.save() secret.save()
messages.success(request, "Modified secret {}.".format(secret)) messages.success(request, "Modified secret {}.".format(secret))

View File

@@ -535,7 +535,7 @@
<div class="panel-heading"> <div class="panel-heading">
<strong>Power Outlets</strong> <strong>Power Outlets</strong>
<div class="pull-right"> <div class="pull-right">
{% if perms.dcim.change_poweroutlet and cs_ports|length > 1 %} {% if perms.dcim.change_poweroutlet and power_outlets|length > 1 %}
<button class="btn btn-default btn-xs toggle"> <button class="btn btn-default btn-xs toggle">
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all <span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
</button> </button>

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

@@ -211,6 +211,10 @@
<h2><a href="{% url 'circuits:circuit_list' %}?site={{ site.slug }}" class="btn {% if stats.circuit_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.circuit_count }}</a></h2> <h2><a href="{% url 'circuits:circuit_list' %}?site={{ site.slug }}" class="btn {% if stats.circuit_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.circuit_count }}</a></h2>
<p>Circuits</p> <p>Circuits</p>
</div> </div>
<div class="col-md-4 text-center">
<h2><a href="{% url 'virtualization:virtualmachine_list' %}?site={{ site.slug }}" class="btn {% if stats.vm_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.vm_count }}</a></h2>
<p>Virtual Machines</p>
</div>
</div> </div>
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">

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 }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}{% if prefix.tenant %}&tenant={{ prefix.tenant.pk }}{% endif %}" 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 == 'ip-addresses' %} class="active"{% endif %}><a href="{% url 'ipam:prefix_ipaddresses' pk=prefix.pk %}">IP Addresses</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>
</ul> </ul>

View File

@@ -1,4 +1,5 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% load helpers %}
{% block title %}{{ prefix }}{% endblock %} {% block title %}{{ prefix }}{% endblock %}
@@ -100,16 +101,6 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<tr>
<td>Is a pool</td>
<td>
{% if prefix.is_pool %}
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
{% else %}
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
{% endif %}
</td>
</tr>
<tr> <tr>
<td>Description</td> <td>Description</td>
<td> <td>
@@ -120,9 +111,19 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<tr>
<td>Is a pool</td>
<td>
{% if prefix.is_pool %}
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
{% else %}
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
{% endif %}
</td>
</tr>
<tr> <tr>
<td>Utilization</td> <td>Utilization</td>
<td><a href="{% url 'ipam:prefix_ipaddresses' pk=prefix.pk %}">{{ ipaddress_count }} IP addresses</a> ({{ prefix.get_utilization }}%)</td> <td>{% utilization_graph prefix.get_utilization %}</td>
</tr> </tr>
</table> </table>
</div> </div>
@@ -138,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' %}
</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

@@ -1,7 +1,7 @@
<div class="row" style="padding-bottom: 20px"> <div class="row" style="padding-bottom: 20px">
<div class="col-md-12 text-center"> <div class="col-md-12 text-center">
<form action="{% url 'search' %}" method="get" class="form-inline"> <form action="{% url 'search' %}" method="get" class="form-inline">
{{ search_form.q }} <input type="text" name="q" value="{{ request.GET.q }}" placeholder="Search" id="id_q" class="form-control" style="width: 350px" />
{{ search_form.obj_type }} {{ search_form.obj_type }}
<button type="submit" class="btn btn-primary">Search</button> <button type="submit" class="btn btn-primary">Search</button>
</form> </form>

View File

@@ -55,10 +55,16 @@ class LoginView(View):
class LogoutView(View): class LogoutView(View):
def get(self, request): def get(self, request):
# Log out the user
auth_logout(request) auth_logout(request)
messages.info(request, "You have logged out.") messages.info(request, "You have logged out.")
return HttpResponseRedirect(reverse('home')) # Delete session key cookie (if set) upon logout
response = HttpResponseRedirect(reverse('home'))
response.delete_cookie('session_key')
return response
# #

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

@@ -308,8 +308,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,10 +84,32 @@ 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)',
) )
site_id = django_filters.ModelMultipleChoiceFilter(
name='cluster__site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='cluster__site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
role_id = django_filters.ModelMultipleChoiceFilter( role_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
label='Role (ID)', label='Role (ID)',

View File

@@ -340,10 +340,20 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm):
to_field_name='slug', to_field_name='slug',
null_option=(0, 'None') null_option=(0, 'None')
) )
cluster_type = FilterChoiceField(
queryset=ClusterType.objects.all(),
to_field_name='slug',
null_option=(0, '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')),
label='Cluster' label='Cluster'
) )
site = FilterChoiceField(
queryset=Site.objects.annotate(filter_count=Count('clusters__virtual_machines')),
to_field_name='slug',
null_option=(0, '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',

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,
]) ])