mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
commit
8cb38de7d5
@ -12,8 +12,9 @@ possible that the bug has already been fixed.
|
||||
reported. If you think you may be experiencing a reported issue, please add a quick comment to it with a "+1" and a
|
||||
quick description of how it's affecting your installation.
|
||||
|
||||
* If you're unsure whether the behavior you're seeing is expected, you can join #netbox on irc.freenode.net and ask
|
||||
before going through the trouble of submitting an issue report.
|
||||
* If you're having trouble installing NetBox, please join #netbox on irc.freenode.net and ask for help before creating
|
||||
an issue on GitHub. Many installation problems are simple fixes. The issues list should be reserved for bug reports and
|
||||
feature requests.
|
||||
|
||||
* When submitting an issue, please be as descriptive as possible. Be sure to describe:
|
||||
|
||||
@ -40,12 +41,15 @@ feature creep. For example, the following features would be firmly out of scope
|
||||
* Acting as a DNS server
|
||||
* Acting as an authentication server
|
||||
|
||||
* Feature requests must be very narrowly defined. The more effort you put into writing a feature request, the better its
|
||||
chances are of being implemented. Overly broad feature requests will be closed.
|
||||
|
||||
* If you're not sure whether the feature you want is a good fit for NetBox, please ask in #netbox on irc.freenode.net.
|
||||
Even if it's not quite right for NetBox, we may be able to point you to a tool better suited for the job.
|
||||
|
||||
* When submitting a feature request, be sure to include the following:
|
||||
|
||||
* A brief description of the functionality
|
||||
* A detailed description of the functionality
|
||||
* A use case for the feature; who would use it and what value it would add to NetBox
|
||||
* A rough description of any changes necessary to the database schema (if applicable)
|
||||
* Any third-party libraries or other resources which would be involved
|
||||
|
@ -197,17 +197,17 @@ class CircuitBulkDeleteForm(ConfirmationForm):
|
||||
|
||||
def circuit_type_choices():
|
||||
type_choices = CircuitType.objects.annotate(circuit_count=Count('circuits'))
|
||||
return [(t.slug, '{} ({})'.format(t.name, t.circuit_count)) for t in type_choices]
|
||||
return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in type_choices]
|
||||
|
||||
|
||||
def circuit_provider_choices():
|
||||
provider_choices = Provider.objects.annotate(circuit_count=Count('circuits'))
|
||||
return [(p.slug, '{} ({})'.format(p.name, p.circuit_count)) for p in provider_choices]
|
||||
return [(p.slug, u'{} ({})'.format(p.name, p.circuit_count)) for p in provider_choices]
|
||||
|
||||
|
||||
def circuit_site_choices():
|
||||
site_choices = Site.objects.annotate(circuit_count=Count('circuits'))
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.circuit_count)) for s in site_choices]
|
||||
return [(s.slug, u'{} ({})'.format(s.name, s.circuit_count)) for s in site_choices]
|
||||
|
||||
|
||||
class CircuitFilterForm(forms.Form, BootstrapMixin):
|
||||
|
@ -80,7 +80,7 @@ class Circuit(CreatedUpdatedModel):
|
||||
unique_together = ['provider', 'cid']
|
||||
|
||||
def __unicode__(self):
|
||||
return "{0} {1}".format(self.provider, self.cid)
|
||||
return u'{} {}'.format(self.provider, self.cid)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuit', args=[self.pk])
|
||||
|
@ -61,7 +61,8 @@ urlpatterns = [
|
||||
url(r'^interfaces/(?P<pk>\d+)/$', InterfaceDetailView.as_view(), name='interface_detail'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_INTERFACE},
|
||||
name='interface_graphs'),
|
||||
url(r'^interface-connections/(?P<pk>\d+)/$', InterfaceConnectionView.as_view(), name='interfaceconnection'),
|
||||
url(r'^interface-connections/$', InterfaceConnectionListView.as_view(), name='interfaceconnection_list'),
|
||||
url(r'^interface-connections/(?P<pk>\d+)/$', InterfaceConnectionView.as_view(), name='interfaceconnection_detail'),
|
||||
|
||||
# Miscellaneous
|
||||
url(r'^related-connections/$', RelatedConnectionsView.as_view(), name='related_connections'),
|
||||
|
@ -326,6 +326,14 @@ class InterfaceConnectionView(generics.RetrieveUpdateDestroyAPIView):
|
||||
queryset = InterfaceConnection.objects.all()
|
||||
|
||||
|
||||
class InterfaceConnectionListView(generics.ListAPIView):
|
||||
"""
|
||||
Retrieve a list of all interface connections
|
||||
"""
|
||||
serializer_class = serializers.InterfaceConnectionSerializer
|
||||
queryset = InterfaceConnection.objects.all()
|
||||
|
||||
|
||||
#
|
||||
# Device bays
|
||||
#
|
||||
|
@ -91,7 +91,7 @@ class RackGroupBulkDeleteForm(ConfirmationForm):
|
||||
|
||||
def rackgroup_site_choices():
|
||||
site_choices = Site.objects.annotate(rack_count=Count('rack_groups'))
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
|
||||
return [(s.slug, u'{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
|
||||
|
||||
|
||||
class RackGroupFilterForm(forms.Form, BootstrapMixin):
|
||||
@ -175,12 +175,12 @@ class RackBulkDeleteForm(ConfirmationForm):
|
||||
|
||||
def rack_site_choices():
|
||||
site_choices = Site.objects.annotate(rack_count=Count('racks'))
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
|
||||
return [(s.slug, u'{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
|
||||
|
||||
|
||||
def rack_group_choices():
|
||||
group_choices = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
|
||||
return [(g.pk, '{} ({})'.format(g, g.rack_count)) for g in group_choices]
|
||||
return [(g.pk, u'{} ({})'.format(g, g.rack_count)) for g in group_choices]
|
||||
|
||||
|
||||
class RackFilterForm(forms.Form, BootstrapMixin):
|
||||
@ -231,7 +231,7 @@ class DeviceTypeBulkDeleteForm(ConfirmationForm):
|
||||
|
||||
def devicetype_manufacturer_choices():
|
||||
manufacturer_choices = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
|
||||
return [(m.slug, '{} ({})'.format(m.name, m.devicetype_count)) for m in manufacturer_choices]
|
||||
return [(m.slug, u'{} ({})'.format(m.name, m.devicetype_count)) for m in manufacturer_choices]
|
||||
|
||||
|
||||
class DeviceTypeFilterForm(forms.Form, BootstrapMixin):
|
||||
@ -373,10 +373,10 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
|
||||
for family in [4, 6]:
|
||||
ip_choices = []
|
||||
interface_ips = IPAddress.objects.filter(family=family, interface__device=self.instance)
|
||||
ip_choices += [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
|
||||
ip_choices += [(ip.id, u'{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
|
||||
nat_ips = IPAddress.objects.filter(family=family, nat_inside__interface__device=self.instance)\
|
||||
.select_related('nat_inside__interface')
|
||||
ip_choices += [(ip.id, '{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
|
||||
ip_choices += [(ip.id, u'{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
|
||||
self.fields['primary_ip{}'.format(family)].choices = [(None, '---------')] + ip_choices
|
||||
|
||||
else:
|
||||
@ -396,8 +396,8 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
|
||||
self.fields['rack'].choices = []
|
||||
|
||||
# Rack position
|
||||
pk = self.instance.pk if self.instance.pk else None
|
||||
try:
|
||||
pk = self.instance.pk if self.instance.pk else None
|
||||
if self.is_bound and self.data.get('rack') and str(self.data.get('face')):
|
||||
position_choices = Rack.objects.get(pk=self.data['rack'])\
|
||||
.get_rack_units(face=self.data.get('face'), exclude=pk)
|
||||
@ -425,6 +425,11 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
|
||||
else:
|
||||
self.fields['device_type'].choices = []
|
||||
|
||||
# Disable rack assignment if this is a child device installed in a parent device
|
||||
if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
|
||||
self.fields['site'].disabled = True
|
||||
self.fields['rack'].disabled = True
|
||||
|
||||
|
||||
class BaseDeviceFromCSVForm(forms.ModelForm):
|
||||
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), to_field_name='name',
|
||||
@ -543,27 +548,27 @@ class DeviceBulkDeleteForm(ConfirmationForm):
|
||||
|
||||
def device_site_choices():
|
||||
site_choices = Site.objects.annotate(device_count=Count('racks__devices'))
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.device_count)) for s in site_choices]
|
||||
return [(s.slug, u'{} ({})'.format(s.name, s.device_count)) for s in site_choices]
|
||||
|
||||
|
||||
def device_rack_group_choices():
|
||||
group_choices = RackGroup.objects.select_related('site').annotate(device_count=Count('racks__devices'))
|
||||
return [(g.pk, '{} ({})'.format(g, g.device_count)) for g in group_choices]
|
||||
return [(g.pk, u'{} ({})'.format(g, g.device_count)) for g in group_choices]
|
||||
|
||||
|
||||
def device_role_choices():
|
||||
role_choices = DeviceRole.objects.annotate(device_count=Count('devices'))
|
||||
return [(r.slug, '{} ({})'.format(r.name, r.device_count)) for r in role_choices]
|
||||
return [(r.slug, u'{} ({})'.format(r.name, r.device_count)) for r in role_choices]
|
||||
|
||||
|
||||
def device_type_choices():
|
||||
type_choices = DeviceType.objects.select_related('manufacturer').annotate(device_count=Count('instances'))
|
||||
return [(t.pk, '{} ({})'.format(t, t.device_count)) for t in type_choices]
|
||||
return [(t.pk, u'{} ({})'.format(t, t.device_count)) for t in type_choices]
|
||||
|
||||
|
||||
def device_platform_choices():
|
||||
platform_choices = Platform.objects.annotate(device_count=Count('devices'))
|
||||
return [(p.slug, '{} ({})'.format(p.name, p.device_count)) for p in platform_choices]
|
||||
return [(p.slug, u'{} ({})'.format(p.name, p.device_count)) for p in platform_choices]
|
||||
|
||||
|
||||
class DeviceFilterForm(forms.Form, BootstrapMixin):
|
||||
|
@ -1,7 +1,7 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.exceptions import MultipleObjectsReturned, ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models
|
||||
@ -9,10 +9,12 @@ from django.db.models import Count, Q, ObjectDoesNotExist
|
||||
|
||||
from extras.rpc import RPC_CLIENTS
|
||||
from utilities.fields import NullableCharField
|
||||
from utilities.managers import NaturalOrderByManager
|
||||
from utilities.models import CreatedUpdatedModel
|
||||
|
||||
from .fields import ASNField, MACAddressField
|
||||
|
||||
|
||||
RACK_FACE_FRONT = 0
|
||||
RACK_FACE_REAR = 1
|
||||
RACK_FACE_CHOICES = [
|
||||
@ -137,6 +139,12 @@ def order_interfaces(queryset, sql_col, primary_ordering=tuple()):
|
||||
}).order_by(*ordering)
|
||||
|
||||
|
||||
class SiteManager(NaturalOrderByManager):
|
||||
|
||||
def get_queryset(self):
|
||||
return self.natural_order_by('name')
|
||||
|
||||
|
||||
class Site(CreatedUpdatedModel):
|
||||
"""
|
||||
A Site represents a geographic location within a network; typically a building or campus. The optional facility
|
||||
@ -150,6 +158,8 @@ class Site(CreatedUpdatedModel):
|
||||
shipping_address = models.CharField(max_length=200, blank=True)
|
||||
comments = models.TextField(blank=True)
|
||||
|
||||
objects = SiteManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@ -206,12 +216,18 @@ class RackGroup(models.Model):
|
||||
]
|
||||
|
||||
def __unicode__(self):
|
||||
return '{} - {}'.format(self.site.name, self.name)
|
||||
return u'{} - {}'.format(self.site.name, self.name)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
|
||||
|
||||
|
||||
class RackManager(NaturalOrderByManager):
|
||||
|
||||
def get_queryset(self):
|
||||
return self.natural_order_by('site__name', 'name')
|
||||
|
||||
|
||||
class Rack(CreatedUpdatedModel):
|
||||
"""
|
||||
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
||||
@ -224,6 +240,8 @@ class Rack(CreatedUpdatedModel):
|
||||
u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)')
|
||||
comments = models.TextField(blank=True)
|
||||
|
||||
objects = RackManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
unique_together = [
|
||||
@ -342,6 +360,15 @@ class Rack(CreatedUpdatedModel):
|
||||
def get_0u_devices(self):
|
||||
return self.devices.filter(position=0)
|
||||
|
||||
def get_utilization(self):
|
||||
"""
|
||||
Determine the utilization rate of the rack and return it as a percentage.
|
||||
"""
|
||||
if self.u_consumed is None:
|
||||
self.u_consumed = 0
|
||||
u_available = self.u_height - self.u_consumed
|
||||
return int(float(self.u_height - u_available) / self.u_height * 100)
|
||||
|
||||
|
||||
#
|
||||
# Device Types
|
||||
@ -404,7 +431,7 @@ class DeviceType(models.Model):
|
||||
]
|
||||
|
||||
def __unicode__(self):
|
||||
return "{} {}".format(self.manufacturer, self.model)
|
||||
return u'{} {}'.format(self.manufacturer, self.model)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:devicetype', args=[self.pk])
|
||||
@ -583,6 +610,12 @@ class Platform(models.Model):
|
||||
return "{}?platform={}".format(reverse('dcim:device_list'), self.slug)
|
||||
|
||||
|
||||
class DeviceManager(NaturalOrderByManager):
|
||||
|
||||
def get_queryset(self):
|
||||
return self.natural_order_by('name')
|
||||
|
||||
|
||||
class Device(CreatedUpdatedModel):
|
||||
"""
|
||||
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
|
||||
@ -612,6 +645,8 @@ class Device(CreatedUpdatedModel):
|
||||
blank=True, null=True, verbose_name='Primary IPv6')
|
||||
comments = models.TextField(blank=True)
|
||||
|
||||
objects = DeviceManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
unique_together = ['rack', 'position', 'face']
|
||||
@ -922,8 +957,8 @@ class Interface(models.Model):
|
||||
return connection.interface_a
|
||||
except InterfaceConnection.DoesNotExist:
|
||||
return None
|
||||
except InterfaceConnection.MultipleObjectsReturned as e:
|
||||
raise e("Multiple connections found for {0} interface {1}!".format(self.device, self))
|
||||
except InterfaceConnection.MultipleObjectsReturned:
|
||||
raise MultipleObjectsReturned("Multiple connections found for {} interface {}!".format(self.device, self))
|
||||
|
||||
|
||||
class InterfaceConnection(models.Model):
|
||||
@ -965,7 +1000,7 @@ class DeviceBay(models.Model):
|
||||
unique_together = ['device', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return '{} - {}'.format(self.device.name, self.name)
|
||||
return u'{} - {}'.format(self.device.name, self.name)
|
||||
|
||||
def clean(self):
|
||||
|
||||
|
@ -48,6 +48,11 @@ STATUS_ICON = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% load helpers %}
|
||||
{% utilization_graph record.get_utilization %}
|
||||
"""
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
@ -97,6 +102,8 @@ class RackTable(BaseTable):
|
||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||
facility_id = tables.Column(verbose_name='Facility ID')
|
||||
u_height = tables.Column(verbose_name='Height (U)')
|
||||
u_consumed = tables.Column(accessor=Accessor('u_consumed'), verbose_name='Used (U)')
|
||||
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
|
@ -7,7 +7,7 @@ from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db.models import Count, ProtectedError
|
||||
from django.db.models import Count, ProtectedError, Sum
|
||||
from django.forms import ModelMultipleChoiceField, MultipleHiddenInput
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
@ -144,7 +144,7 @@ class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
#
|
||||
|
||||
class RackListView(ObjectListView):
|
||||
queryset = Rack.objects.select_related('site').annotate(device_count=Count('devices', distinct=True))
|
||||
queryset = Rack.objects.select_related('site').prefetch_related('devices__device_type').annotate(device_count=Count('devices', distinct=True), u_consumed=Sum('devices__device_type__u_height'))
|
||||
filter = filters.RackFilter
|
||||
filter_form = forms.RackFilterForm
|
||||
table = tables.RackTable
|
||||
|
@ -112,7 +112,7 @@ class AggregateBulkDeleteForm(ConfirmationForm):
|
||||
|
||||
def aggregate_rir_choices():
|
||||
rir_choices = RIR.objects.annotate(aggregate_count=Count('aggregates'))
|
||||
return [(r.slug, '{} ({})'.format(r.name, r.aggregate_count)) for r in rir_choices]
|
||||
return [(r.slug, u'{} ({})'.format(r.name, r.aggregate_count)) for r in rir_choices]
|
||||
|
||||
|
||||
class AggregateFilterForm(forms.Form, BootstrapMixin):
|
||||
@ -266,19 +266,19 @@ def prefix_vrf_choices():
|
||||
|
||||
def prefix_site_choices():
|
||||
site_choices = Site.objects.annotate(prefix_count=Count('prefixes'))
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.prefix_count)) for s in site_choices]
|
||||
return [(s.slug, u'{} ({})'.format(s.name, s.prefix_count)) for s in site_choices]
|
||||
|
||||
|
||||
def prefix_status_choices():
|
||||
status_counts = {}
|
||||
for status in Prefix.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
||||
status_counts[status['status']] = status['count']
|
||||
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES]
|
||||
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES]
|
||||
|
||||
|
||||
def prefix_role_choices():
|
||||
role_choices = Role.objects.annotate(prefix_count=Count('prefixes'))
|
||||
return [(r.slug, '{} ({})'.format(r.name, r.prefix_count)) for r in role_choices]
|
||||
return [(r.slug, u'{} ({})'.format(r.name, r.prefix_count)) for r in role_choices]
|
||||
|
||||
|
||||
class PrefixFilterForm(forms.Form, BootstrapMixin):
|
||||
@ -455,7 +455,7 @@ class VLANGroupBulkDeleteForm(ConfirmationForm):
|
||||
|
||||
def vlangroup_site_choices():
|
||||
site_choices = Site.objects.annotate(vlangroup_count=Count('vlan_groups'))
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.vlangroup_count)) for s in site_choices]
|
||||
return [(s.slug, u'{} ({})'.format(s.name, s.vlangroup_count)) for s in site_choices]
|
||||
|
||||
|
||||
class VLANGroupFilterForm(forms.Form, BootstrapMixin):
|
||||
@ -529,6 +529,7 @@ class VLANImportForm(BulkImportForm, BootstrapMixin):
|
||||
class VLANBulkEditForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
||||
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False)
|
||||
status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False)
|
||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
||||
|
||||
@ -539,24 +540,24 @@ class VLANBulkDeleteForm(ConfirmationForm):
|
||||
|
||||
def vlan_site_choices():
|
||||
site_choices = Site.objects.annotate(vlan_count=Count('vlans'))
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.vlan_count)) for s in site_choices]
|
||||
return [(s.slug, u'{} ({})'.format(s.name, s.vlan_count)) for s in site_choices]
|
||||
|
||||
|
||||
def vlan_group_choices():
|
||||
group_choices = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
|
||||
return [(g.pk, '{} ({})'.format(g, g.vlan_count)) for g in group_choices]
|
||||
return [(g.pk, u'{} ({})'.format(g, g.vlan_count)) for g in group_choices]
|
||||
|
||||
|
||||
def vlan_status_choices():
|
||||
status_counts = {}
|
||||
for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
||||
status_counts[status['status']] = status['count']
|
||||
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES]
|
||||
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES]
|
||||
|
||||
|
||||
def vlan_role_choices():
|
||||
role_choices = Role.objects.annotate(vlan_count=Count('vlans'))
|
||||
return [(r.slug, '{} ({})'.format(r.name, r.vlan_count)) for r in role_choices]
|
||||
return [(r.slug, u'{} ({})'.format(r.name, r.vlan_count)) for r in role_choices]
|
||||
|
||||
|
||||
class VLANFilterForm(forms.Form, BootstrapMixin):
|
||||
|
@ -385,7 +385,7 @@ class VLANGroup(models.Model):
|
||||
verbose_name_plural = 'VLAN groups'
|
||||
|
||||
def __unicode__(self):
|
||||
return '{} - {}'.format(self.site.name, self.name)
|
||||
return u'{} - {}'.format(self.site.name, self.name)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
|
||||
@ -442,7 +442,7 @@ class VLAN(CreatedUpdatedModel):
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
return u"{} ({})".format(self.vid, self.name)
|
||||
return u'{} ({})'.format(self.vid, self.name)
|
||||
|
||||
def get_status_class(self):
|
||||
return STATUS_CHOICE_CLASSES[self.status]
|
||||
|
@ -11,15 +11,8 @@ RIR_EDIT_LINK = """
|
||||
"""
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% with record.get_utilization as percentage %}
|
||||
<div class="progress text-center">
|
||||
{% if percentage < 15 %}<span style="font-size: 12px;">{{ percentage }}%</span>{% endif %}
|
||||
<div class="progress-bar progress-bar-{% if percentage >= 90 %}danger{% elif percentage >= 75 %}warning{% else %}success{% endif %}"
|
||||
role="progressbar" aria-valuenow="{{ percentage }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ percentage }}%">
|
||||
{% if percentage >= 15 %}{{ percentage }}%{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% load helpers %}
|
||||
{% utilization_graph record.get_utilization %}
|
||||
"""
|
||||
|
||||
ROLE_EDIT_LINK = """
|
||||
|
@ -565,7 +565,7 @@ class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
def update_objects(self, pk_list, form):
|
||||
|
||||
fields_to_update = {}
|
||||
for field in ['site', 'status', 'role']:
|
||||
for field in ['site', 'group', 'status', 'role']:
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
|
@ -12,7 +12,7 @@ except ImportError:
|
||||
"the documentation.")
|
||||
|
||||
|
||||
VERSION = '1.3.0'
|
||||
VERSION = '1.3.1'
|
||||
|
||||
# Import local configuration
|
||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||
|
@ -28,6 +28,7 @@ class SecretRoleListView(generics.ListAPIView):
|
||||
"""
|
||||
queryset = SecretRole.objects.all()
|
||||
serializer_class = serializers.SecretRoleSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
|
||||
class SecretRoleDetailView(generics.RetrieveAPIView):
|
||||
@ -36,6 +37,7 @@ class SecretRoleDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
queryset = SecretRole.objects.all()
|
||||
serializer_class = serializers.SecretRoleSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
|
||||
class SecretListView(generics.GenericAPIView):
|
||||
@ -47,6 +49,7 @@ class SecretListView(generics.GenericAPIView):
|
||||
serializer_class = serializers.SecretSerializer
|
||||
filter_class = SecretFilter
|
||||
renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, private_key=None):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
@ -91,6 +94,7 @@ class SecretDetailView(generics.GenericAPIView):
|
||||
.prefetch_related('role__users', 'role__groups')
|
||||
serializer_class = serializers.SecretSerializer
|
||||
renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, pk, private_key=None):
|
||||
secret = get_object_or_404(Secret, pk=pk)
|
||||
|
@ -1,6 +1,7 @@
|
||||
import django_filters
|
||||
|
||||
from .models import Secret, SecretRole
|
||||
from dcim.models import Device
|
||||
|
||||
|
||||
class SecretFilter(django_filters.FilterSet):
|
||||
@ -15,7 +16,13 @@ class SecretFilter(django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (Name)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Secret
|
||||
fields = ['name', 'role_id', 'role']
|
||||
fields = ['name', 'role_id', 'role', 'device']
|
||||
|
@ -103,7 +103,7 @@ class SecretBulkDeleteForm(ConfirmationForm):
|
||||
|
||||
def secret_role_choices():
|
||||
role_choices = SecretRole.objects.annotate(secret_count=Count('secrets'))
|
||||
return [(r.slug, '{} ({})'.format(r.name, r.secret_count)) for r in role_choices]
|
||||
return [(r.slug, u'{} ({})'.format(r.name, r.secret_count)) for r in role_choices]
|
||||
|
||||
|
||||
class SecretFilterForm(forms.Form, BootstrapMixin):
|
||||
|
@ -219,8 +219,8 @@ class Secret(CreatedUpdatedModel):
|
||||
|
||||
def __unicode__(self):
|
||||
if self.role and self.device:
|
||||
return "{} for {}".format(self.role, self.device)
|
||||
return "Secret"
|
||||
return u'{} for {}'.format(self.role, self.device)
|
||||
return u'Secret'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('secrets:secret', args=[self.pk])
|
||||
|
@ -22,8 +22,32 @@
|
||||
<div class="panel-body">
|
||||
{% render_field form.site %}
|
||||
{% render_field form.rack %}
|
||||
{% render_field form.face %}
|
||||
{% render_field form.position %}
|
||||
{% if obj.device_type.is_child_device and obj.parent_bay %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Parent device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">
|
||||
<a href="{% url 'dcim:device' pk=obj.parent_bay.device.pk %}">{{ obj.parent_bay.device }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Parent bay</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">
|
||||
{{ obj.parent_bay.name }}
|
||||
{% if perms.dcim.change_devicebay %}
|
||||
<a href="{% url 'dcim:devicebay_depopulate' pk=obj.parent_bay.pk %}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-remove" aria-hidden="true" title="Remove device"></i> Remove
|
||||
</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% elif not obj.device_type.is_child_device %}
|
||||
{% render_field form.face %}
|
||||
{% render_field form.position %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
|
@ -0,0 +1,7 @@
|
||||
<div class="progress text-center">
|
||||
{% if utilization < 30 %}<span style="font-size: 12px;">{{ utilization }}%</span>{% endif %}
|
||||
<div class="progress-bar progress-bar-{% if utilization >= danger_threshold %}danger{% elif utilization >= warning_threshold %}warning{% else %}success{% endif %}"
|
||||
role="progressbar" aria-valuenow="{{ utilization }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ utilization }}%">
|
||||
{% if utilization >= 30 %}{{ utilization }}%{% endif %}
|
||||
</div>
|
||||
</div>
|
@ -60,7 +60,7 @@ class SelectWithDisabled(forms.Select):
|
||||
option_label = option_label['label']
|
||||
disabled_html = ' disabled="disabled"' if option_disabled else ''
|
||||
|
||||
return format_html('<option value="{}"{}{}>{}</option>',
|
||||
return format_html(u'<option value="{}"{}{}>{}</option>',
|
||||
option_value,
|
||||
selected_html,
|
||||
disabled_html,
|
||||
|
30
netbox/utilities/managers.py
Normal file
30
netbox/utilities/managers.py
Normal file
@ -0,0 +1,30 @@
|
||||
from django.db.models import Manager
|
||||
|
||||
|
||||
class NaturalOrderByManager(Manager):
|
||||
|
||||
def natural_order_by(self, *fields):
|
||||
"""
|
||||
Attempt to order records naturally by segmenting a field into three parts:
|
||||
|
||||
1. Leading integer (if any)
|
||||
2. Middle portion
|
||||
3. Trailing integer (if any)
|
||||
|
||||
:param fields: The fields on which to order the queryset. The last field in the list will be ordered naturally.
|
||||
"""
|
||||
db_table = self.model._meta.db_table
|
||||
primary_field = fields[-1]
|
||||
|
||||
id1 = '_{}_{}1'.format(db_table, primary_field)
|
||||
id2 = '_{}_{}2'.format(db_table, primary_field)
|
||||
id3 = '_{}_{}3'.format(db_table, primary_field)
|
||||
|
||||
queryset = super(NaturalOrderByManager, self).get_queryset().extra(select={
|
||||
id1: "CAST(SUBSTRING({}.{} FROM '^(\d+)') AS integer)".format(db_table, primary_field),
|
||||
id2: "SUBSTRING({}.{} FROM '^\d*(.*?)\d*$')".format(db_table, primary_field),
|
||||
id3: "CAST(SUBSTRING({}.{} FROM '(\d+)$') AS integer)".format(db_table, primary_field),
|
||||
})
|
||||
ordering = fields[0:-1] + (id1, id2, id3)
|
||||
|
||||
return queryset.order_by(*ordering)
|
@ -95,3 +95,15 @@ def querystring_toggle(request, multi=True, page_key='page', **kwargs):
|
||||
return '?' + querystring
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
@register.inclusion_tag('utilities/templatetags/utilization_graph.html')
|
||||
def utilization_graph(utilization, warning_threshold=75, danger_threshold=90):
|
||||
"""
|
||||
Display a horizontal bar graph indicating a percentage of utilization.
|
||||
"""
|
||||
return {
|
||||
'utilization': utilization,
|
||||
'warning_threshold': warning_threshold,
|
||||
'danger_threshold': danger_threshold,
|
||||
}
|
||||
|
@ -134,12 +134,12 @@ class ObjectEditView(View):
|
||||
obj_created = not obj.pk
|
||||
obj.save()
|
||||
|
||||
msg = 'Created ' if obj_created else 'Modified '
|
||||
msg = u'Created ' if obj_created else u'Modified '
|
||||
msg += self.model._meta.verbose_name
|
||||
if hasattr(obj, 'get_absolute_url'):
|
||||
msg = '{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), obj)
|
||||
msg = u'{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), obj)
|
||||
else:
|
||||
msg = '{} {}'.format(msg, obj)
|
||||
msg = u'{} {}'.format(msg, obj)
|
||||
messages.success(request, msg)
|
||||
if obj_created:
|
||||
UserAction.objects.log_create(request.user, obj, msg)
|
||||
@ -192,7 +192,7 @@ class ObjectDeleteView(View):
|
||||
if form.is_valid():
|
||||
try:
|
||||
obj.delete()
|
||||
msg = 'Deleted {} {}'.format(self.model._meta.verbose_name, obj)
|
||||
msg = u'Deleted {} {}'.format(self.model._meta.verbose_name, obj)
|
||||
messages.success(request, msg)
|
||||
UserAction.objects.log_delete(request.user, obj, msg)
|
||||
return redirect(self.redirect_url)
|
||||
@ -234,7 +234,7 @@ class BulkImportView(View):
|
||||
|
||||
obj_table = self.table(new_objs)
|
||||
if new_objs:
|
||||
msg = 'Imported {} {}'.format(len(new_objs), new_objs[0]._meta.verbose_name_plural)
|
||||
msg = u'Imported {} {}'.format(len(new_objs), new_objs[0]._meta.verbose_name_plural)
|
||||
messages.success(request, msg)
|
||||
UserAction.objects.log_import(request.user, ContentType.objects.get_for_model(new_objs[0]), msg)
|
||||
|
||||
@ -281,7 +281,7 @@ class BulkEditView(View):
|
||||
if form.is_valid():
|
||||
updated_count = self.update_objects(pk_list, form)
|
||||
if updated_count:
|
||||
msg = 'Updated {} {}'.format(updated_count, self.cls._meta.verbose_name_plural)
|
||||
msg = u'Updated {} {}'.format(updated_count, self.cls._meta.verbose_name_plural)
|
||||
messages.success(self.request, msg)
|
||||
UserAction.objects.log_bulk_edit(request.user, ContentType.objects.get_for_model(self.cls), msg)
|
||||
return redirect(redirect_url)
|
||||
@ -345,7 +345,7 @@ class BulkDeleteView(View):
|
||||
handle_protectederror(list(queryset), request, e)
|
||||
return redirect(redirect_url)
|
||||
|
||||
msg = 'Deleted {} {}'.format(deleted_count, self.cls._meta.verbose_name_plural)
|
||||
msg = u'Deleted {} {}'.format(deleted_count, self.cls._meta.verbose_name_plural)
|
||||
messages.success(request, msg)
|
||||
UserAction.objects.log_bulk_delete(request.user, ContentType.objects.get_for_model(self.cls), msg)
|
||||
return redirect(redirect_url)
|
||||
|
@ -1,5 +1,5 @@
|
||||
cryptography==1.4
|
||||
Django==1.9.7
|
||||
Django==1.9.8
|
||||
django-debug-toolbar==1.4
|
||||
django-filter==0.13.0
|
||||
django-rest-swagger==0.3.7
|
||||
|
Loading…
Reference in New Issue
Block a user