Compare commits

..

24 Commits

Author SHA1 Message Date
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
Jeremy Stretch
50a451eddc Merge pull request #1720 from digitalocean/develop
Release v2.2.6
2017-11-16 12:00:34 -05:00
Jeremy Stretch
3f8350b78f Release v2.2.6 2017-11-16 11:57:43 -05:00
Jeremy Stretch
500a56b869 Fixes #1718: Set empty label to 'Global' or VRF field in IP assignment form 2017-11-16 11:54:23 -05:00
Jeremy Stretch
e50b7174bf Closes #1669: Clicking "add an IP" from the prefix view will default to the first available IP within the prefix 2017-11-15 15:26:00 -05:00
Jeremy Stretch
8299c735b1 Fixes #1599: Reduce mobile cut-off for navigation menu to 960px 2017-11-15 14:57:56 -05:00
Jeremy Stretch
124878ed22 Fixes #1599: Display global search in navigation menu unless display is less than 1200px wide 2017-11-15 14:44:33 -05:00
Jeremy Stretch
d888aa67f9 Fixes #1715: Added missing import buttons on object lists 2017-11-15 12:52:21 -05:00
Jeremy Stretch
0cb3e1749b Fixes #1717: Fixed inteface validation for virtual machines 2017-11-15 12:37:08 -05:00
Jeremy Stretch
8ff10d5995 Post-release version bump 2017-11-14 13:29:46 -05:00
35 changed files with 225 additions and 100 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

@@ -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
@@ -1120,6 +1120,8 @@ class ConsoleServerPort(models.Model):
def clean(self): def clean(self):
# Check that the parent device's DeviceType is a console server # Check that the parent device's DeviceType is a console server
if self.device is None:
raise ValidationError("Console server ports must be assigned to devices.")
device_type = self.device.device_type device_type = self.device.device_type
if not device_type.is_console_server: if not device_type.is_console_server:
raise ValidationError("The {} {} device type not support assignment of console server ports.".format( raise ValidationError("The {} {} device type not support assignment of console server ports.".format(
@@ -1169,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')
@@ -1194,6 +1197,8 @@ class PowerOutlet(models.Model):
def clean(self): def clean(self):
# Check that the parent device's DeviceType is a PDU # Check that the parent device's DeviceType is a PDU
if self.device is None:
raise ValidationError("Power outlets must be assigned to devices.")
device_type = self.device.device_type device_type = self.device.device_type
if not device_type.is_pdu: if not device_type.is_pdu:
raise ValidationError("The {} {} device type not support assignment of power outlets.".format( raise ValidationError("The {} {} device type not support assignment of power outlets.".format(
@@ -1257,6 +1262,7 @@ class Interface(models.Model):
def clean(self): def clean(self):
# Check that the parent device's DeviceType is a network device # Check that the parent device's DeviceType is a network device
if self.device is not None:
device_type = self.device.device_type device_type = self.device.device_type
if not device_type.is_network_device: if not device_type.is_network_device:
raise ValidationError("The {} {} device type not support assignment of network interfaces.".format( raise ValidationError("The {} {} device type not support assignment of network interfaces.".format(

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

@@ -689,7 +689,7 @@ class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class IPAddressAssignForm(BootstrapMixin, forms.Form): class IPAddressAssignForm(BootstrapMixin, forms.Form):
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF') vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF', empty_label='Global')
address = forms.CharField(label='IP Address') address = forms.CharField(label='IP Address')

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

@@ -304,6 +304,16 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
return available_ips return available_ips
def get_first_available_ip(self):
"""
Return the first available IP within the prefix (or None).
"""
available_ips = self.get_available_ips()
if available_ips:
return '{}/{}'.format(next(available_ips.__iter__()), self.prefix.prefixlen)
else:
return None
def get_utilization(self): def get_utilization(self):
""" """
Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of

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)
@@ -507,7 +504,6 @@ class PrefixView(View):
return render(request, 'ipam/prefix.html', { return render(request, 'ipam/prefix.html', {
'prefix': prefix, 'prefix': prefix,
'aggregate': aggregate, 'aggregate': aggregate,
'ipaddress_count': ipaddress_count,
'parent_prefix_table': parent_prefix_table, 'parent_prefix_table': parent_prefix_table,
'child_prefix_table': child_prefix_table, 'child_prefix_table': child_prefix_table,
'duplicate_prefix_table': duplicate_prefix_table, 'duplicate_prefix_table': duplicate_prefix_table,

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.5' VERSION = '2.2.7'
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

@@ -35,8 +35,22 @@ footer p {
margin: 20px 0; margin: 20px 0;
} }
/* Collapse the nav menu on displays less than 1200px wide */ /* Hide the username in the navigation menu on displays less than 1400px wide */
@media (max-width: 1399px) {
#navbar_user {
display: none;
}
}
/* Hide the search bar in the navigation menu on displays less than 1200px wide */
@media (max-width: 1199px) { @media (max-width: 1199px) {
#navbar_search {
display: none;
}
}
/* Collapse the nav menu on displays less than 960px wide */
@media (max-width: 959px) {
.navbar-header { .navbar-header {
float: none; float: none;
} }
@@ -72,12 +86,8 @@ footer p {
.collapse.in { .collapse.in {
display:block !important; display:block !important;
} }
} #navbar_user {
display: inline;
/* Hide the nav search bar on displays less than 1600px wide */
@media (max-width: 1599px) {
#navbar_search {
display: none;
} }
} }

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

@@ -8,6 +8,10 @@
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
Add a circuit type Add a circuit type
</a> </a>
<a href="{% url 'circuits:circuittype_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import circuit types
</a>
{% endif %} {% endif %}
</div> </div>
<h1>{% block title %}Circuit Types{% endblock %}</h1> <h1>{% block title %}Circuit Types{% endblock %}</h1>

View File

@@ -8,6 +8,10 @@
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
Add a device role Add a device role
</a> </a>
<a href="{% url 'dcim:devicerole_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import device roles
</a>
{% endif %} {% endif %}
</div> </div>
<h1>{% block title %}Device Roles{% endblock %}</h1> <h1>{% block title %}Device Roles{% endblock %}</h1>

View File

@@ -8,6 +8,10 @@
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
Add a platform Add a platform
</a> </a>
<a href="{% url 'dcim:platform_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import platforms
</a>
{% endif %} {% endif %}
</div> </div>
<h1>{% block title %}Platforms{% endblock %}</h1> <h1>{% block title %}Platforms{% endblock %}</h1>

View File

@@ -8,6 +8,10 @@
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
Add a rack role Add a rack role
</a> </a>
<a href="{% url 'dcim:rackrole_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import rack roles
</a>
{% endif %} {% endif %}
</div> </div>
<h1>{% block title %}Rack Roles{% endblock %}</h1> <h1>{% block title %}Rack Roles{% endblock %}</h1>

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

@@ -379,7 +379,9 @@
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" title="{{ request.user }}" role="button" aria-haspopup="true" aria-expanded="false"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" title="{{ request.user }}" role="button" aria-haspopup="true" aria-expanded="false">
{{ request.user|truncatechars:"30" }} <span class="caret"></span> <i class="fa fa-user"></i>
<span id="navbar_user">{{ request.user|truncatechars:"30" }}</span>
<span class="caret"></span>
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{% url 'user:profile' %}"><i class="fa fa-user"></i> Profile</a></li> <li><a href="{% url 'user:profile' %}"><i class="fa fa-user"></i> Profile</a></li>

View File

@@ -23,7 +23,7 @@
</div> </div>
<div class="pull-right"> <div class="pull-right">
{% if perms.ipam.add_ipaddress %} {% if perms.ipam.add_ipaddress %}
<a href="{% url 'ipam:ipaddress_add' %}?address={{ prefix.prefix }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}{% if prefix.tenant %}&tenant={{ prefix.tenant.pk }}{% endif %}" class="btn btn-success"> <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">
<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 +45,5 @@
{% 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 == '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>

View File

@@ -20,6 +20,10 @@
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
Add a RIR Add a RIR
</a> </a>
<a href="{% url 'ipam:rir_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import RIRs
</a>
{% endif %} {% endif %}
</div> </div>
<h1>{% block title %}RIRs{% endblock %}</h1> <h1>{% block title %}RIRs{% endblock %}</h1>

View File

@@ -8,6 +8,10 @@
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
Add a role Add a role
</a> </a>
<a href="{% url 'ipam:role_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import roles
</a>
{% endif %} {% endif %}
</div> </div>
<h1>{% block title %}Prefix/VLAN Roles{% endblock %}</h1> <h1>{% block title %}Prefix/VLAN Roles{% endblock %}</h1>

View File

@@ -8,6 +8,10 @@
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
Add a VLAN group Add a VLAN group
</a> </a>
<a href="{% url 'ipam:vlangroup_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import VLAN groups
</a>
{% endif %} {% endif %}
</div> </div>
<h1>{% block title %}VLAN Groups{% endblock %}</h1> <h1>{% block title %}VLAN Groups{% endblock %}</h1>

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

@@ -8,6 +8,10 @@
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
Add a secret role Add a secret role
</a> </a>
<a href="{% url 'secrets:secretrole_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import secret roles
</a>
{% endif %} {% endif %}
</div> </div>
<h1>{% block title %}Secret Roles{% endblock %}</h1> <h1>{% block title %}Secret Roles{% endblock %}</h1>

View File

@@ -8,6 +8,10 @@
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
Add a tenant group Add a tenant group
</a> </a>
<a href="{% url 'tenancy:tenantgroup_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import tenant groups
</a>
{% endif %} {% endif %}
</div> </div>
<h1>{% block title %}Tenant Groups{% endblock %}</h1> <h1>{% block title %}Tenant Groups{% endblock %}</h1>

View File

@@ -8,6 +8,10 @@
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
Add a cluster group Add a cluster group
</a> </a>
<a href="{% url 'virtualization:clustergroup_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import cluster groups
</a>
{% endif %} {% endif %}
</div> </div>
<h1>{% block title %}Cluster Groups{% endblock %}</h1> <h1>{% block title %}Cluster Groups{% endblock %}</h1>

View File

@@ -8,6 +8,10 @@
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
Add a cluster type Add a cluster type
</a> </a>
<a href="{% url 'virtualization:clustertype_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import cluster types
</a>
{% endif %} {% endif %}
</div> </div>
<h1>{% block title %}Cluster Types{% endblock %}</h1> <h1>{% block title %}Cluster Types{% endblock %}</h1>

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

@@ -88,6 +88,17 @@ class VirtualMachineFilter(CustomFieldFilterSet):
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

@@ -344,6 +344,11 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm):
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',