mirror of
https://github.com/netbox-community/netbox.git
synced 2026-02-09 10:27:43 +01:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be393a9d10 | ||
|
|
ef59f38ec4 | ||
|
|
47120fae01 | ||
|
|
c0417c1989 | ||
|
|
fb6cfa45fd | ||
|
|
b875cea10d | ||
|
|
32bf17c076 | ||
|
|
66a6a8f33c | ||
|
|
05b71564d8 | ||
|
|
1682de59df | ||
|
|
f26253ec49 | ||
|
|
f2dc287f14 | ||
|
|
3fe3151af7 | ||
|
|
1c1fd8f210 | ||
|
|
3ce2f0d100 | ||
|
|
92d726bbd4 | ||
|
|
e2ef0bc3a6 | ||
|
|
13c29cb7a9 | ||
|
|
27eefd8705 | ||
|
|
b22c6a0078 | ||
|
|
f4784412de | ||
|
|
33c5ea1f4e | ||
|
|
d9f1bcbf15 | ||
|
|
4b7af8d3b4 | ||
|
|
f3fd82a24a | ||
|
|
cd97b2fb96 | ||
|
|
f661c233be | ||
|
|
6a2a2d5d11 | ||
|
|
87ff433ef8 | ||
|
|
d68b34cefe | ||
|
|
70a05b4280 |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.sh text eol=lf
|
||||
@@ -9,9 +9,7 @@ env:
|
||||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
- pip install pep8
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
**The [2017 NetBox User Survey](https://goo.gl/forms/75HnNS2iE0Y1hVFH3) is open!** Please consider taking a moment to respond. Your feedback helps shape the pace and focus of NetBox development. The survey will remain open until 2017-03-31. Results will be published on the mailing list.
|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
NetBox is an IP address management (IPAM) and data center infrastructure management (DCIM) tool. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers.
|
||||
|
||||
@@ -90,6 +90,22 @@ NetBox does not have the ability to generate graphs natively, but this feature a
|
||||
* **Source URL:** The source of the image to be embedded. The associated object will be available as a template variable named `obj`.
|
||||
* **Link URL (optional):** A URL to which the graph will be linked. The associated object will be available as a template variable named `obj`.
|
||||
|
||||
## Examples
|
||||
|
||||
You only need to define one graph object for each graph you want to include when viewing an object. For example, if you want to include a graph of traffic through an interface over the past five minutes, your graph source might looks like this:
|
||||
|
||||
```
|
||||
https://my.nms.local/graphs/?node={{ obj.device.name }}&interface={{ obj.name }}&duration=5m
|
||||
```
|
||||
|
||||
You can define several graphs to provide multiple contexts when viewing an object. For example:
|
||||
|
||||
```
|
||||
https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
|
||||
https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=24h
|
||||
https://my.nms.local/graphs/?type=errors&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
|
||||
```
|
||||
|
||||
# Topology Maps
|
||||
|
||||
NetBox can generate simple topology maps from the physical network connections recorded in its database. First, you'll need to create a topology map definition under the admin UI at Extras > Topology Maps.
|
||||
|
||||
@@ -5,12 +5,13 @@ from django.db.models import Q
|
||||
from dcim.models import Site
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
||||
|
||||
from .models import Provider, Circuit, CircuitType
|
||||
|
||||
|
||||
class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -42,6 +43,7 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
|
||||
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.db.models import Q
|
||||
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
||||
from .models import (
|
||||
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection,
|
||||
Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Region, Site,
|
||||
@@ -14,6 +14,7 @@ from .models import (
|
||||
|
||||
|
||||
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -81,6 +82,7 @@ class RackGroupFilter(django_filters.FilterSet):
|
||||
|
||||
|
||||
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -157,6 +159,7 @@ class RackReservationFilter(django_filters.FilterSet):
|
||||
|
||||
|
||||
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -191,6 +194,7 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
|
||||
class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -405,6 +409,10 @@ class InterfaceFilter(django_filters.FilterSet):
|
||||
method='filter_type',
|
||||
label='Interface type',
|
||||
)
|
||||
mac_address = django_filters.CharFilter(
|
||||
method='_mac_address',
|
||||
label='MAC address',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
@@ -420,48 +428,73 @@ class InterfaceFilter(django_filters.FilterSet):
|
||||
return queryset.filter(form_factor=IFACE_FF_LAG)
|
||||
return queryset
|
||||
|
||||
def _mac_address(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return queryset
|
||||
try:
|
||||
return queryset.filter(mac_address=value)
|
||||
except AddrFormatError:
|
||||
return queryset.none()
|
||||
|
||||
|
||||
class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = []
|
||||
device = django_filters.CharFilter(
|
||||
method='filter_device',
|
||||
label='Device',
|
||||
)
|
||||
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(cs_port__device__site__slug=value)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(device__name__icontains=value) |
|
||||
Q(cs_port__device__name__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class PowerConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = []
|
||||
device = django_filters.CharFilter(
|
||||
method='filter_device',
|
||||
label='Device',
|
||||
)
|
||||
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(power_outlet__device__site__slug=value)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(device__name__icontains=value) |
|
||||
Q(power_outlet__device__name__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class InterfaceConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
fields = []
|
||||
device = django_filters.CharFilter(
|
||||
method='filter_device',
|
||||
label='Device',
|
||||
)
|
||||
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -470,3 +503,11 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
|
||||
Q(interface_a__device__site__slug=value) |
|
||||
Q(interface_b__device__site__slug=value)
|
||||
)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(interface_a__device__name__icontains=value) |
|
||||
Q(interface_b__device__name__icontains=value)
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ from .models import (
|
||||
Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
|
||||
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
|
||||
RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD,
|
||||
VIRTUAL_IFACE_TYPES
|
||||
SUBDEVICE_ROLE_PARENT, VIRTUAL_IFACE_TYPES
|
||||
)
|
||||
|
||||
|
||||
@@ -375,6 +375,21 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
|
||||
to_field_name='slug'
|
||||
)
|
||||
is_console_server = forms.BooleanField(
|
||||
required=False, label='Is a console server', widget=forms.CheckboxInput(attrs={'value': 'True'}))
|
||||
is_pdu = forms.BooleanField(
|
||||
required=False, label='Is a PDU', widget=forms.CheckboxInput(attrs={'value': 'True'})
|
||||
)
|
||||
is_network_device = forms.BooleanField(
|
||||
required=False, label='Is a network device', widget=forms.CheckboxInput(attrs={'value': 'True'})
|
||||
)
|
||||
subdevice_role = forms.NullBooleanField(
|
||||
required=False, label='Subdevice role', widget=forms.Select(choices=(
|
||||
('', '---------'),
|
||||
(SUBDEVICE_ROLE_PARENT, 'Parent'),
|
||||
(SUBDEVICE_ROLE_CHILD, 'Child'),
|
||||
))
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -680,13 +695,21 @@ class DeviceFromCSVForm(BaseDeviceFromCSVForm):
|
||||
|
||||
|
||||
class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
|
||||
parent = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Parent device not found.'})
|
||||
parent = FlexibleModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
error_messages={
|
||||
'invalid_choice': 'Parent device not found.'
|
||||
}
|
||||
)
|
||||
device_bay_name = forms.CharField(required=False)
|
||||
|
||||
class Meta(BaseDeviceFromCSVForm.Meta):
|
||||
fields = ['name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag',
|
||||
'parent', 'device_bay_name']
|
||||
fields = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'parent',
|
||||
'device_bay_name',
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -733,7 +756,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Device
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.annotate(filter_count=Count('racks__devices')),
|
||||
queryset=Site.objects.annotate(filter_count=Count('devices')),
|
||||
to_field_name='slug',
|
||||
)
|
||||
rack_group_id = FilterChoiceField(
|
||||
@@ -1610,20 +1633,23 @@ class DeviceBayCreateForm(DeviceComponentForm):
|
||||
|
||||
|
||||
class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
|
||||
installed_device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Child Device',
|
||||
help_text="Child devices must first be created within the rack occupied "
|
||||
"by the parent device. Then they can be assigned to a bay.")
|
||||
installed_device = forms.ModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
label='Child Device',
|
||||
help_text="Child devices must first be created and assigned to the site/rack of the parent device."
|
||||
)
|
||||
|
||||
def __init__(self, device_bay, *args, **kwargs):
|
||||
|
||||
super(PopulateDeviceBayForm, self).__init__(*args, **kwargs)
|
||||
|
||||
children_queryset = Device.objects.filter(rack=device_bay.device.rack,
|
||||
parent_bay__isnull=True,
|
||||
device_type__u_height=0,
|
||||
device_type__subdevice_role=SUBDEVICE_ROLE_CHILD)\
|
||||
.exclude(pk=device_bay.device.pk)
|
||||
self.fields['installed_device'].queryset = children_queryset
|
||||
self.fields['installed_device'].queryset = Device.objects.filter(
|
||||
site=device_bay.device.site,
|
||||
rack=device_bay.device.rack,
|
||||
parent_bay__isnull=True,
|
||||
device_type__u_height=0,
|
||||
device_type__subdevice_role=SUBDEVICE_ROLE_CHILD
|
||||
).exclude(pk=device_bay.device.pk)
|
||||
|
||||
|
||||
#
|
||||
@@ -1632,14 +1658,17 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
|
||||
|
||||
class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
||||
device = forms.CharField(required=False, label='Device name')
|
||||
|
||||
|
||||
class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
||||
device = forms.CharField(required=False, label='Device name')
|
||||
|
||||
|
||||
class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
||||
device = forms.CharField(required=False, label='Device name')
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -975,25 +975,26 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
# Child devices cannot be assigned to a rack face/unit
|
||||
if self.device_type.is_child_device and self.face is not None:
|
||||
raise ValidationError({
|
||||
'face': "Child device types cannot be assigned to a rack face. This is an attribute of the parent "
|
||||
"device."
|
||||
'face': "Child device types cannot be assigned to a rack face. This is an attribute of the "
|
||||
"parent device."
|
||||
})
|
||||
if self.device_type.is_child_device and self.position:
|
||||
raise ValidationError({
|
||||
'position': "Child device types cannot be assigned to a rack position. This is an attribute of the "
|
||||
"parent device."
|
||||
'position': "Child device types cannot be assigned to a rack position. This is an attribute of "
|
||||
"the parent device."
|
||||
})
|
||||
|
||||
# Validate rack space
|
||||
rack_face = self.face if not self.device_type.is_full_depth else None
|
||||
exclude_list = [self.pk] if self.pk else []
|
||||
try:
|
||||
available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face,
|
||||
exclude=exclude_list)
|
||||
available_units = self.rack.get_available_units(
|
||||
u_height=self.device_type.u_height, rack_face=rack_face, exclude=exclude_list
|
||||
)
|
||||
if self.position and self.position not in available_units:
|
||||
raise ValidationError({
|
||||
'position': "U{} is already occupied or does not have sufficient space to accommodate a(n) {} "
|
||||
"({}U).".format(self.position, self.device_type, self.device_type.u_height)
|
||||
'position': "U{} is already occupied or does not have sufficient space to accommodate a(n) "
|
||||
"{} ({}U).".format(self.position, self.device_type, self.device_type.u_height)
|
||||
})
|
||||
except Rack.DoesNotExist:
|
||||
pass
|
||||
@@ -1034,8 +1035,8 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
self.device_type.device_bay_templates.all()]
|
||||
)
|
||||
|
||||
# Update Rack assignment for any child Devices
|
||||
Device.objects.filter(parent_bay__device=self).update(rack=self.rack)
|
||||
# Update Site and Rack assignment for any child Devices
|
||||
Device.objects.filter(parent_bay__device=self).update(site=self.site, rack=self.rack)
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
@@ -1059,8 +1060,10 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
return self.name
|
||||
elif self.position:
|
||||
return u"{} ({} U{})".format(self.device_type, self.rack.name, self.position)
|
||||
else:
|
||||
elif self.rack:
|
||||
return u"{} ({})".format(self.device_type, self.rack.name)
|
||||
else:
|
||||
return u"{} ({})".format(self.device_type, self.site.name)
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
|
||||
@@ -100,6 +100,10 @@ DEVICE_PRIMARY_IP = """
|
||||
{{ record.primary_ip4.address.ip|default:"" }}
|
||||
"""
|
||||
|
||||
SUBDEVICE_ROLE_TEMPLATE = """
|
||||
{% if record.subdevice_role == True %}Parent{% elif record.subdevice_role == False %}Child{% else %}—{% endif %}
|
||||
"""
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% load helpers %}
|
||||
{% utilization_graph value %}
|
||||
@@ -249,11 +253,18 @@ class DeviceTypeTable(BaseTable):
|
||||
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
|
||||
part_number = tables.Column(verbose_name='Part Number')
|
||||
is_full_depth = tables.BooleanColumn(verbose_name='Full Depth')
|
||||
is_console_server = tables.BooleanColumn(verbose_name='CS')
|
||||
is_pdu = tables.BooleanColumn(verbose_name='PDU')
|
||||
is_network_device = tables.BooleanColumn(verbose_name='Net')
|
||||
subdevice_role = tables.TemplateColumn(SUBDEVICE_ROLE_TEMPLATE, verbose_name='Subdevice Role')
|
||||
instance_count = tables.Column(verbose_name='Instances')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceType
|
||||
fields = ('pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count')
|
||||
fields = (
|
||||
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
||||
'is_network_device', 'subdevice_role', 'instance_count'
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -90,7 +90,12 @@ class ComponentCreateView(View):
|
||||
self.parent_field: parent.pk,
|
||||
'name': name,
|
||||
}
|
||||
component_data.update(data)
|
||||
# Replace objects with their primary key to keep component_form.clean() happy
|
||||
for k, v in data.items():
|
||||
if hasattr(v, 'pk'):
|
||||
component_data[k] = v.pk
|
||||
else:
|
||||
component_data[k] = v
|
||||
component_form = self.model_form(component_data)
|
||||
if component_form.is_valid():
|
||||
new_components.append(component_form.save(commit=False))
|
||||
@@ -763,9 +768,12 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
def save_obj(self, obj):
|
||||
# Inherent rack from parent device
|
||||
|
||||
# Inherit site and rack from parent device
|
||||
obj.site = obj.parent_bay.device.site
|
||||
obj.rack = obj.parent_bay.device.rack
|
||||
obj.save()
|
||||
|
||||
# Save the reverse relation
|
||||
device_bay = obj.parent_bay
|
||||
device_bay.installed_device = obj
|
||||
|
||||
@@ -7,12 +7,13 @@ from django.db.models import Q
|
||||
from dcim.models import Site, Device, Interface
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
||||
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
|
||||
|
||||
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -44,6 +45,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
|
||||
class RIRFilter(django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
|
||||
class Meta:
|
||||
model = RIR
|
||||
@@ -51,6 +53,7 @@ class RIRFilter(django_filters.FilterSet):
|
||||
|
||||
|
||||
class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -84,6 +87,7 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
|
||||
class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -182,6 +186,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
|
||||
class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -283,6 +288,7 @@ class VLANGroupFilter(django_filters.FilterSet):
|
||||
|
||||
|
||||
class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
|
||||
@@ -12,7 +12,7 @@ except ImportError:
|
||||
"the documentation.")
|
||||
|
||||
|
||||
VERSION = '1.9.1'
|
||||
VERSION = '1.9.3'
|
||||
|
||||
# Import local configuration
|
||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||
|
||||
@@ -23,7 +23,7 @@ _patterns = [
|
||||
url(r'^ipam/', include('ipam.urls', namespace='ipam')),
|
||||
url(r'^secrets/', include('secrets.urls', namespace='secrets')),
|
||||
url(r'^tenancy/', include('tenancy.urls', namespace='tenancy')),
|
||||
url(r'^profile/', include('users.urls', namespace='users')),
|
||||
url(r'^user/', include('users.urls', namespace='user')),
|
||||
|
||||
# API
|
||||
url(r'^api/circuits/', include('circuits.api.urls', namespace='circuits-api')),
|
||||
|
||||
@@ -15,10 +15,10 @@ def userkey_required():
|
||||
uk = UserKey.objects.get(user=request.user)
|
||||
except UserKey.DoesNotExist:
|
||||
messages.warning(request, u"This operation requires an active user key, but you don't have one.")
|
||||
return redirect('users:userkey')
|
||||
return redirect('user:userkey')
|
||||
if not uk.is_active():
|
||||
messages.warning(request, u"This operation is not available. Your user key has not been activated.")
|
||||
return redirect('users:userkey')
|
||||
return redirect('user:userkey')
|
||||
return view(request, *args, **kwargs)
|
||||
return wrapped_view
|
||||
return _decorator
|
||||
|
||||
@@ -4,9 +4,11 @@ from django.db.models import Q
|
||||
|
||||
from .models import Secret, SecretRole
|
||||
from dcim.models import Device
|
||||
from utilities.filters import NumericInFilter
|
||||
|
||||
|
||||
class SecretFilter(django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
|
||||
@@ -245,7 +245,7 @@
|
||||
{% if request.user.is_staff %}
|
||||
<li><a href="{% url 'admin:index' %}"><i class="fa fa-cogs" aria-hidden="true"></i> Admin</a></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'users:profile' %}"><i class="fa fa-user" aria-hidden="true"></i> {{ request.user }}</a></li>
|
||||
<li><a href="{% url 'user:profile' %}"><i class="fa fa-user" aria-hidden="true"></i> {{ request.user }}</a></li>
|
||||
<li><a href="{% url 'logout' %}"><i class="fa fa-sign-out" aria-hidden="true"></i> Log out</a></li>
|
||||
{% else %}
|
||||
<li><a href="{% url 'login' %}?next={{ request.path }}"><i class="fa fa-sign-in" aria-hidden="true"></i> Log in</a></li>
|
||||
@@ -295,7 +295,8 @@
|
||||
<p class="text-muted">
|
||||
<i class="fa fa-fw fa-book text-primary"></i> <a href="http://netbox.readthedocs.io/" target="_blank">Docs</a> ·
|
||||
<i class="fa fa-fw fa-cloud text-primary"></i> <a href="{% url 'django.swagger.base.view' %}">API</a> ·
|
||||
<i class="fa fa-fw fa-code text-primary"></i> <a href="https://github.com/digitalocean/netbox">Code</a>
|
||||
<i class="fa fa-fw fa-code text-primary"></i> <a href="https://github.com/digitalocean/netbox">Code</a> ·
|
||||
<i class="fa fa-fw fa-support text-primary"></i> <a href="https://github.com/digitalocean/netbox/wiki">Help</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -43,8 +43,10 @@
|
||||
<td>
|
||||
{% if device.parent_bay %}
|
||||
{% with device.parent_bay.device as parent %}
|
||||
<span>U{{ parent.position }} / {{ parent.get_face_display }}
|
||||
(<a href="{{ parent.get_absolute_url }}">{{ parent }}</a> - {{ device.parent_bay.name }})</span>
|
||||
<a href="{{ parent.get_absolute_url }}">{{ parent }}</a> <i class="fa fa-angle-right"></i> {{ device.parent_bay.name }}
|
||||
{% if parent.position %}
|
||||
(U{{ parent.position }} / {{ parent.get_face_display }})
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% elif device.rack and device.position %}
|
||||
<span>U{{ device.position }} / {{ device.get_face_display }}</span>
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rack</td>
|
||||
<td>Rack name</td>
|
||||
<td>Rack name (optional)</td>
|
||||
<td>R101</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -35,7 +35,13 @@
|
||||
<td colspan="2">
|
||||
<i class="fa fa-fw fa-globe" title="Circuit"></i>
|
||||
{% if peer_termination %}
|
||||
<a href="{% url 'dcim:site' slug=peer_termination.site.slug %}">{{ peer_termination.site }}</a> via
|
||||
{% if peer_termination.interface %}
|
||||
<a href="{% url 'dcim:device' pk=peer_termination.interface.device.pk %}">{{ peer_termination.interface.device }}</a>
|
||||
(<a href="{% url 'dcim:site' slug=peer_termination.site.slug %}">{{ peer_termination.site }}</a>)
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:site' slug=peer_termination.site.slug %}">{{ peer_termination.site }}</a>
|
||||
{% endif %}
|
||||
via
|
||||
{% endif %}
|
||||
<a href="{% url 'circuits:circuit' pk=iface.circuit_termination.circuit_id %}">{{ iface.circuit_termination.circuit }}</a>
|
||||
</td>
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
<div class="row">
|
||||
<div class="col-sm-3 col-md-2 col-md-offset-2">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li{% ifequal active_tab "profile" %} class="active"{% endifequal %}><a href="{% url 'users:profile' %}">Profile</a></li>
|
||||
<li{% ifequal active_tab "change_password" %} class="active"{% endifequal %}><a href="{% url 'users:change_password' %}">Change Password</a></li>
|
||||
<li{% ifequal active_tab "userkey" %} class="active"{% endifequal %}><a href="{% url 'users:userkey' %}">User Key</a></li>
|
||||
<li{% ifequal active_tab "recent_activity" %} class="active"{% endifequal %}><a href="{% url 'users:recent_activity' %}">Recent Activity</a></li>
|
||||
<li{% ifequal active_tab "profile" %} class="active"{% endifequal %}><a href="{% url 'user:profile' %}">Profile</a></li>
|
||||
<li{% ifequal active_tab "change_password" %} class="active"{% endifequal %}><a href="{% url 'user:change_password' %}">Change Password</a></li>
|
||||
<li{% ifequal active_tab "userkey" %} class="active"{% endifequal %}><a href="{% url 'user:userkey' %}">User Key</a></li>
|
||||
<li{% ifequal active_tab "recent_activity" %} class="active"{% endifequal %}><a href="{% url 'user:recent_activity' %}">Recent Activity</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-sm-9 col-md-6">
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button type="submit" name="_update" class="btn btn-primary">Update</button>
|
||||
<a href="{% url 'users:profile' %}" class="btn btn-default">Cancel</a>
|
||||
<a href="{% url 'user:profile' %}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<p>Your public key is below.</p>
|
||||
<pre>{{ userkey.public_key }}</pre>
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'users:userkey_edit' %}" class="btn btn-warning">
|
||||
<a href="{% url 'user:userkey_edit' %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
Edit user key
|
||||
</a>
|
||||
@@ -24,7 +24,7 @@
|
||||
{% else %}
|
||||
<p>You don't have a user key on file.</p>
|
||||
<p>
|
||||
<a href="{% url 'users:userkey_edit' %}" class="btn btn-primary">
|
||||
<a href="{% url 'user:userkey_edit' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Create a User Key
|
||||
</a>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-6 text-right">
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
<a href="{% url 'users:userkey' %}" class="btn btn-default">Cancel</a>
|
||||
<a href="{% url 'user:userkey' %}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,11 +3,12 @@ import django_filters
|
||||
from django.db.models import Q
|
||||
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
||||
from .models import Tenant, TenantGroup
|
||||
|
||||
|
||||
class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
|
||||
@@ -7,9 +7,9 @@ urlpatterns = [
|
||||
|
||||
# User profiles
|
||||
url(r'^profile/$', views.profile, name='profile'),
|
||||
url(r'^profile/password/$', views.change_password, name='change_password'),
|
||||
url(r'^profile/user-key/$', views.userkey, name='userkey'),
|
||||
url(r'^profile/user-key/edit/$', views.userkey_edit, name='userkey_edit'),
|
||||
url(r'^profile/recent-activity/$', views.recent_activity, name='recent_activity'),
|
||||
url(r'^password/$', views.change_password, name='change_password'),
|
||||
url(r'^user-key/$', views.userkey, name='userkey'),
|
||||
url(r'^user-key/edit/$', views.userkey_edit, name='userkey_edit'),
|
||||
url(r'^recent-activity/$', views.recent_activity, name='recent_activity'),
|
||||
|
||||
]
|
||||
|
||||
@@ -69,7 +69,7 @@ def change_password(request):
|
||||
form.save()
|
||||
update_session_auth_hash(request, form.user)
|
||||
messages.success(request, u"Your password has been changed successfully.")
|
||||
return redirect('users:profile')
|
||||
return redirect('user:profile')
|
||||
|
||||
else:
|
||||
form = PasswordChangeForm(user=request.user)
|
||||
@@ -109,7 +109,7 @@ def userkey_edit(request):
|
||||
uk.user = request.user
|
||||
uk.save()
|
||||
messages.success(request, u"Your user key has been saved.")
|
||||
return redirect('users:userkey')
|
||||
return redirect('user:userkey')
|
||||
|
||||
else:
|
||||
form = UserKeyForm(instance=userkey)
|
||||
|
||||
@@ -6,6 +6,17 @@ from django.db.models import Q
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
|
||||
#
|
||||
# Filters
|
||||
#
|
||||
|
||||
class NumericInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
|
||||
"""
|
||||
Filters for a set of numeric values. Example: id__in=100,200,300
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class NullableModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
||||
"""
|
||||
This field operates like a normal ModelMultipleChoiceField except that it allows for one additional choice which is
|
||||
|
||||
@@ -57,7 +57,7 @@ def parse_numeric_range(string, base=10):
|
||||
begin, end = dash_range.split('-')
|
||||
except ValueError:
|
||||
begin, end = dash_range, dash_range
|
||||
begin, end = int(begin.strip()), int(end.strip(), base=base) + 1
|
||||
begin, end = int(begin.strip(), base=base), int(end.strip(), base=base) + 1
|
||||
values.extend(range(begin, end))
|
||||
return list(set(values))
|
||||
|
||||
|
||||
@@ -434,7 +434,7 @@ class BulkEditView(View):
|
||||
|
||||
# Are we editing *all* objects in the queryset or just a selected subset?
|
||||
if request.POST.get('_all') and self.filter is not None:
|
||||
pk_list = [obj.pk for obj in self.filter(request.GET, self.cls.objects.only('pk'))]
|
||||
pk_list = [obj.pk for obj in self.filter(request.GET, self.cls.objects.only('pk')).qs]
|
||||
else:
|
||||
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
|
||||
|
||||
@@ -572,7 +572,7 @@ class BulkDeleteView(View):
|
||||
|
||||
# Are we deleting *all* objects in the queryset or just a selected subset?
|
||||
if request.POST.get('_all') and self.filter is not None:
|
||||
pk_list = [obj.pk for obj in self.filter(request.GET, self.cls.objects.only('pk'))]
|
||||
pk_list = [obj.pk for obj in self.filter(request.GET, self.cls.objects.only('pk')).qs]
|
||||
else:
|
||||
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user