diff --git a/docs/api/examples.md b/docs/api/examples.md index 4ec2f0f33..0291f6012 100644 --- a/docs/api/examples.md +++ b/docs/api/examples.md @@ -123,7 +123,7 @@ $ curl -X PATCH -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc 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) > DELETE /api/dcim/sites/16/ HTTP/1.1 > User-Agent: curl/7.35.0 diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 6324b785a..3ca67c311 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -143,6 +143,11 @@ class Site(CreatedUpdatedModel, CustomFieldModel): def count_circuits(self): 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 @@ -1090,16 +1095,11 @@ class ConsolePort(models.Model): class ConsoleServerPortManager(models.Manager): def get_queryset(self): - """ - 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 ... - """ + # Pad any trailing digits to effect natural sorting return super(ConsoleServerPortManager, self).get_queryset().extra(select={ - 'name_as_integer': "CAST(substring(dcim_consoleserverport.name FROM '[0-9]+$') AS INTEGER)", - }).order_by('device', 'name_as_integer') + 'name_padded': "CONCAT(REGEXP_REPLACE(dcim_consoleserverport.name, '\d+$', ''), " + "LPAD(SUBSTRING(dcim_consoleserverport.name FROM '\d+$'), 8, '0'))", + }).order_by('device', 'name_padded') @python_2_unicode_compatible @@ -1172,9 +1172,10 @@ class PowerPort(models.Model): class PowerOutletManager(models.Manager): def get_queryset(self): + # Pad any trailing digits to effect natural sorting return super(PowerOutletManager, self).get_queryset().extra(select={ - 'name_padded': "CONCAT(SUBSTRING(dcim_poweroutlet.name FROM '^[^0-9]+'), " - "LPAD(SUBSTRING(dcim_poweroutlet.name FROM '[0-9\/]+$'), 8, '0'))", + 'name_padded': "CONCAT(REGEXP_REPLACE(dcim_poweroutlet.name, '\d+$', ''), " + "LPAD(SUBSTRING(dcim_poweroutlet.name FROM '\d+$'), 8, '0'))", }).order_by('device', 'name_padded') diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 061b3bd9d..d238372e8 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -153,11 +153,12 @@ class SiteDetailTable(SiteTable): 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') 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): fields = ( 'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count', - 'vlan_count', 'circuit_count', + 'vlan_count', 'circuit_count', 'vm_count', ) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 2f55d073d..0640885de 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -25,6 +25,7 @@ from utilities.views import ( BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView, ComponentEditView, ObjectDeleteView, ObjectEditView, ObjectListView, ) +from virtualization.models import VirtualMachine from . import filters, forms, tables from .constants import CONNECTION_STATUS_CONNECTED from .models import ( @@ -134,6 +135,7 @@ class SiteView(View): 'prefix_count': Prefix.objects.filter(site=site).count(), 'vlan_count': VLAN.objects.filter(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')) topology_maps = TopologyMap.objects.filter(site=site) @@ -808,15 +810,11 @@ class DeviceView(View): console_ports = natsorted( ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name') ) - cs_ports = natsorted( - ConsoleServerPort.objects.filter(device=device).select_related('connected_console'), key=attrgetter('name') - ) + cs_ports = ConsoleServerPort.objects.filter(device=device).select_related('connected_console') power_ports = natsorted( PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name') ) - power_outlets = natsorted( - PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name') - ) + power_outlets = PowerOutlet.objects.filter(device=device).select_related('connected_port') interfaces = Interface.objects.order_naturally( device.device_type.interface_ordering ).filter( diff --git a/netbox/ipam/fields.py b/netbox/ipam/fields.py index 39895b52a..e0842d10b 100644 --- a/netbox/ipam/fields.py +++ b/netbox/ipam/fields.py @@ -5,10 +5,7 @@ from django.db import models from netaddr import IPNetwork from .formfields import IPFormField -from .lookups import ( - EndsWith, IEndsWith, IRegex, IStartsWith, NetContained, NetContainedOrEqual, NetContains, NetContainsOrEquals, - NetHost, NetHostContained, NetMaskLength, Regex, StartsWith, -) +from . import lookups def prefix_validator(prefix): @@ -57,17 +54,18 @@ class IPNetworkField(BaseIPField): return 'cidr' -IPNetworkField.register_lookup(EndsWith) -IPNetworkField.register_lookup(IEndsWith) -IPNetworkField.register_lookup(StartsWith) -IPNetworkField.register_lookup(IStartsWith) -IPNetworkField.register_lookup(Regex) -IPNetworkField.register_lookup(IRegex) -IPNetworkField.register_lookup(NetContained) -IPNetworkField.register_lookup(NetContainedOrEqual) -IPNetworkField.register_lookup(NetContains) -IPNetworkField.register_lookup(NetContainsOrEquals) -IPNetworkField.register_lookup(NetMaskLength) +IPNetworkField.register_lookup(lookups.IExact) +IPNetworkField.register_lookup(lookups.EndsWith) +IPNetworkField.register_lookup(lookups.IEndsWith) +IPNetworkField.register_lookup(lookups.StartsWith) +IPNetworkField.register_lookup(lookups.IStartsWith) +IPNetworkField.register_lookup(lookups.Regex) +IPNetworkField.register_lookup(lookups.IRegex) +IPNetworkField.register_lookup(lookups.NetContained) +IPNetworkField.register_lookup(lookups.NetContainedOrEqual) +IPNetworkField.register_lookup(lookups.NetContains) +IPNetworkField.register_lookup(lookups.NetContainsOrEquals) +IPNetworkField.register_lookup(lookups.NetMaskLength) class IPAddressField(BaseIPField): @@ -80,16 +78,17 @@ class IPAddressField(BaseIPField): return 'inet' -IPAddressField.register_lookup(EndsWith) -IPAddressField.register_lookup(IEndsWith) -IPAddressField.register_lookup(StartsWith) -IPAddressField.register_lookup(IStartsWith) -IPAddressField.register_lookup(Regex) -IPAddressField.register_lookup(IRegex) -IPAddressField.register_lookup(NetContained) -IPAddressField.register_lookup(NetContainedOrEqual) -IPAddressField.register_lookup(NetContains) -IPAddressField.register_lookup(NetContainsOrEquals) -IPAddressField.register_lookup(NetHost) -IPAddressField.register_lookup(NetHostContained) -IPAddressField.register_lookup(NetMaskLength) +IPAddressField.register_lookup(lookups.IExact) +IPAddressField.register_lookup(lookups.EndsWith) +IPAddressField.register_lookup(lookups.IEndsWith) +IPAddressField.register_lookup(lookups.StartsWith) +IPAddressField.register_lookup(lookups.IStartsWith) +IPAddressField.register_lookup(lookups.Regex) +IPAddressField.register_lookup(lookups.IRegex) +IPAddressField.register_lookup(lookups.NetContained) +IPAddressField.register_lookup(lookups.NetContainedOrEqual) +IPAddressField.register_lookup(lookups.NetContains) +IPAddressField.register_lookup(lookups.NetContainsOrEquals) +IPAddressField.register_lookup(lookups.NetHost) +IPAddressField.register_lookup(lookups.NetHostContained) +IPAddressField.register_lookup(lookups.NetMaskLength) diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 40933c43d..7cd00cc2c 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import django_filters from django.db.models import Q -from netaddr import IPNetwork +import netaddr from netaddr.core import AddrFormatError from dcim.models import Site, Device, Interface @@ -79,7 +79,7 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet): return queryset qs_filter = Q(description__icontains=value) try: - prefix = str(IPNetwork(value.strip()).cidr) + prefix = str(netaddr.IPNetwork(value.strip()).cidr) qs_filter |= Q(prefix__net_contains_or_equals=prefix) except (AddrFormatError, ValueError): pass @@ -107,6 +107,10 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): method='search_within_include', 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( method='filter_mask_length', label='Mask length', @@ -173,7 +177,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): return queryset qs_filter = Q(description__icontains=value) try: - prefix = str(IPNetwork(value.strip()).cidr) + prefix = str(netaddr.IPNetwork(value.strip()).cidr) qs_filter |= Q(prefix__net_contains_or_equals=prefix) except (AddrFormatError, ValueError): pass @@ -184,7 +188,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): if not value: return queryset try: - query = str(IPNetwork(value).cidr) + query = str(netaddr.IPNetwork(value).cidr) return queryset.filter(prefix__net_contained=query) except (AddrFormatError, ValueError): return queryset.none() @@ -194,11 +198,25 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): if not value: return queryset try: - query = str(IPNetwork(value).cidr) + query = str(netaddr.IPNetwork(value).cidr) return queryset.filter(prefix__net_contained_or_equal=query) except (AddrFormatError, ValueError): 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): if not value: return queryset @@ -291,7 +309,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet): if not value: return queryset try: - query = str(IPNetwork(value.strip()).cidr) + query = str(netaddr.IPNetwork(value.strip()).cidr) return queryset.filter(address__net_host_contained=query) except (AddrFormatError, ValueError): return queryset.none() diff --git a/netbox/ipam/lookups.py b/netbox/ipam/lookups.py index 59cd91ed8..9aca3c03b 100644 --- a/netbox/ipam/lookups.py +++ b/netbox/ipam/lookups.py @@ -13,12 +13,21 @@ class NetFieldDecoratorMixin(object): 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): - lookup_name = 'endswith' + pass 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): @@ -26,15 +35,18 @@ class StartsWith(NetFieldDecoratorMixin, lookups.StartsWith): 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): - lookup_name = 'regex' + pass class IRegex(NetFieldDecoratorMixin, lookups.IRegex): - lookup_name = 'iregex' + pass class NetContainsOrEquals(Lookup): diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 22335da8f..e36162938 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -454,9 +454,6 @@ class PrefixView(View): except Aggregate.DoesNotExist: aggregate = None - # Count child IP addresses - ipaddress_count = prefix.get_child_ips().count() - # Parent prefixes table parent_prefixes = Prefix.objects.filter( Q(vrf=prefix.vrf) | Q(vrf__isnull=True) @@ -507,7 +504,6 @@ class PrefixView(View): return render(request, 'ipam/prefix.html', { 'prefix': prefix, 'aggregate': aggregate, - 'ipaddress_count': ipaddress_count, 'parent_prefix_table': parent_prefix_table, 'child_prefix_table': child_prefix_table, 'duplicate_prefix_table': duplicate_prefix_table, diff --git a/netbox/netbox/forms.py b/netbox/netbox/forms.py index 69db4fba5..caa074e18 100644 --- a/netbox/netbox/forms.py +++ b/netbox/netbox/forms.py @@ -38,7 +38,7 @@ OBJ_TYPE_CHOICES = ( class SearchForm(BootstrapMixin, forms.Form): q = forms.CharField( - label='Search', widget=forms.TextInput(attrs={'style': 'width: 350px'}) + label='Search' ) obj_type = forms.ChoiceField( choices=OBJ_TYPE_CHOICES, required=False, label='Type' diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index 3be9e9df9..1c21e58b7 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from collections import OrderedDict +from django.db.models import Count from django.shortcuts import render from django.views.generic import View from rest_framework.response import Response @@ -58,7 +59,7 @@ SEARCH_TYPES = OrderedDict(( 'url': 'dcim:rack_list', }), ('devicetype', { - 'queryset': DeviceType.objects.select_related('manufacturer'), + 'queryset': DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances')), 'filter': DeviceTypeFilter, 'table': DeviceTypeTable, 'url': 'dcim:devicetype_list', diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py index f673af2f0..d4e9874b3 100644 --- a/netbox/secrets/models.py +++ b/netbox/secrets/models.py @@ -303,6 +303,7 @@ class Secret(CreatedUpdatedModel): |LL|MySecret|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| +--+--------+-------------------------------------------+ """ + s = s.encode('utf8') if len(s) > 65535: raise ValueError("Maximum plaintext size is 65535 bytes.") # Minimum ciphertext size is 64 bytes to conceal the length of short secrets. @@ -315,7 +316,7 @@ class Secret(CreatedUpdatedModel): return ( chr(len(s) >> 8).encode() + chr(len(s) % 256).encode() + - s.encode() + + s + 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 plaintext. """ - if isinstance(s[0], int): - plaintext_length = (s[0] << 8) + s[1] - elif isinstance(s[0], str): + if isinstance(s[0], str): 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): """ diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index 250559139..dfde0a662 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -166,7 +166,7 @@ def secret_edit(request, pk): # Create and encrypt the new Secret if master_key is not None: secret = form.save(commit=False) - secret.plaintext = str(form.cleaned_data['plaintext']) + secret.plaintext = form.cleaned_data['plaintext'] secret.encrypt(master_key) secret.save() messages.success(request, "Modified secret {}.".format(secret)) diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index 228eec4c8..efc98c3d0 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -211,6 +211,10 @@
Circuits
+Virtual Machines
+