Compare commits

...

31 Commits

Author SHA1 Message Date
Jeremy Stretch
ce6796ed9b Merge pull request #870 from digitalocean/develop
Release v1.8.4
2017-02-03 13:59:02 -05:00
Jeremy Stretch
585e08eb95 Release v1.8.4 2017-02-03 13:55:32 -05:00
Jeremy Stretch
d817990283 Fixes #865: Fix server error when attempting to delete a protected object parent (Python 3) 2017-02-01 12:09:59 -05:00
Jeremy Stretch
9905099a71 Fixes #854: Check whether object still exists before attempting to resolve its URL 2017-02-01 11:59:47 -05:00
Jeremy Stretch
0eba5a0de3 Fixes #851: Resolve encoding issues during import/export with Python 3 2017-02-01 11:49:54 -05:00
Jeremy Stretch
5eb3c1a67b Removed deprecated base_path Swagger setting 2017-02-01 10:48:36 -05:00
Jeremy Stretch
b370375414 Fixes #861: Avoid overwriting device primary IP assignment from alternate family during bulk import of IP addresses 2017-01-31 17:25:44 -05:00
Jeremy Stretch
8536f6c163 Closes #856: Strip whitespace from fields during CSV import 2017-01-31 16:54:13 -05:00
Jeremy Stretch
f4f41a5985 Fixes #859: Fix Javascript for connection status toggle button 2017-01-31 09:41:25 -05:00
Jeremy Stretch
af3c9eaec1 Fixes #854: Correct processing of get_return_url() in ObjectDeleteView 2017-01-30 12:13:24 -05:00
Jeremy Stretch
b8b2ea7ccb Post-release version bump 2017-01-26 14:00:08 -05:00
Jeremy Stretch
c90cecc2fb Merge pull request #849 from digitalocean/develop
Release v1.8.3
2017-01-26 13:58:52 -05:00
Jeremy Stretch
b2ef7bb104 Release v1.8.3 2017-01-26 13:57:00 -05:00
Jeremy Stretch
5d5d4ac714 Fixes #845: Fix missing edit/delete buttons on object tables for non-superusers 2017-01-26 13:20:56 -05:00
dav3860
b3b96e5e10 Support for comma in interfaces and ip addresses bulk creation (#833)
* Added support for comma in interfaces and ip addresses bulk creation

* fixed PEP8 style

* removed unnecessary assertions
2017-01-25 14:47:14 -05:00
Jeremy Stretch
6be520a8f9 Fixed DeviceTypeTest 2017-01-25 14:38:45 -05:00
Jeremy Stretch
f3db914e9d Fixes #844: Apply order_naturally() to API interfaces list 2017-01-25 14:34:34 -05:00
Jeremy Stretch
fbfa3cf619 Added gunicorn_config.py to .gitignore 2017-01-24 13:41:46 -05:00
Jeremy Stretch
1317c0dd8c Closes #841: Merged search and filter forms on all object lists 2017-01-24 12:05:39 -05:00
Jeremy Stretch
bbc633b004 Closes #782: Allow filtering devices list by manufacturer 2017-01-24 10:53:59 -05:00
Jeremy Stretch
ed8fdd9292 Fixes #816: Redirect back to parent prefix view after deleting child prefixes 2017-01-24 09:50:51 -05:00
Jeremy Stretch
2d9c33c34f Tweaked installation docs to include instructions for Python 2 and 3 2017-01-23 17:01:23 -05:00
Jens L
80439c495e Basic Support for Python 3 (#827)
* Rudimentary python3 support

* update docs and trigger Travis

* fix some of the tests

* fix all python3 errors

* change env calls to just python

* add @python_2_unicode_compatible decorator to models for python2 compatibility

* switch netbox.configuration to from netbox import configuration
2017-01-23 16:44:29 -05:00
Jeremy Stretch
1bddd038fe Fixes #840: Correct API path resolution for secrets when BASE_PATH is configured 2017-01-23 16:25:05 -05:00
Jeremy Stretch
d36923e47d Fixes #817: Update last_updated time of a circuit when editing a child termination 2017-01-23 15:31:41 -05:00
Jeremy Stretch
476cbf17f6 Closes #820: Add VLAN column to parent prefixes table on IP address view 2017-01-23 14:23:42 -05:00
Jeremy Stretch
91d50b9627 Closes #836: Add 'deprecated' status for IP addresses 2017-01-23 14:12:43 -05:00
Jeremy Stretch
52420945b2 Standardized naming of return_url for all object views 2017-01-23 14:07:26 -05:00
Jeremy Stretch
b70eca7661 Fixes #830: Redirect user to device view after editing a device component 2017-01-23 12:14:12 -05:00
Jeremy Stretch
39d083eae7 Re-implemented method for bulk editing/deleting all objects within a filtered queryset 2017-01-20 16:42:11 -05:00
Jeremy Stretch
3bfc1ebcea Post-release version bump 2017-01-18 16:23:52 -05:00
101 changed files with 703 additions and 494 deletions

2
.gitignore vendored
View File

@@ -1,8 +1,10 @@
*.pyc
/netbox/netbox/configuration.py
/netbox/netbox/ldap_config.py
/netbox/static
.idea
/*.sh
!upgrade.sh
fabfile.py
*.swp
gunicorn_config.py

View File

@@ -9,6 +9,9 @@ env:
language: python
python:
- "2.7"
- "3.4"
- "3.5"
- "3.6"
install:
- pip install -r requirements.txt
- pip install pep8

View File

@@ -2,12 +2,29 @@
**Debian/Ubuntu**
Python 3:
```no-highlight
# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
```
Python 2:
```no-highlight
# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
```
**CentOS/RHEL**
Python 3:
```no-highlight
# yum install -y epel-release
# yum install -y gcc python3 python3-devel python3-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel
```
Python 2:
```no-highlight
# yum install -y epel-release
# yum install -y gcc python2 python-devel python-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel

View File

@@ -0,0 +1 @@
default_app_config = 'circuits.apps.CircuitsConfig'

9
netbox/circuits/apps.py Normal file
View File

@@ -0,0 +1,9 @@
from django.apps import AppConfig
class CircuitsConfig(AppConfig):
name = "circuits"
verbose_name = "Circuits"
def ready(self):
import circuits.signals

View File

@@ -62,6 +62,7 @@ class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Provider
q = forms.CharField(required=False, label='Search')
site = FilterChoiceField(queryset=Site.objects.all(), to_field_name='slug')
@@ -126,6 +127,7 @@ class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Circuit
q = forms.CharField(required=False, label='Search')
type = FilterChoiceField(queryset=CircuitType.objects.annotate(filter_count=Count('circuits')),
to_field_name='slug')
provider = FilterChoiceField(queryset=Provider.objects.annotate(filter_count=Count('circuits')),

View File

@@ -1,6 +1,7 @@
from django.contrib.contenttypes.fields import GenericRelation
from django.core.urlresolvers import reverse
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from dcim.fields import ASNField
from extras.models import CustomFieldModel, CustomFieldValue
@@ -33,6 +34,7 @@ def humanize_speed(speed):
return '{} Kbps'.format(speed)
@python_2_unicode_compatible
class Provider(CreatedUpdatedModel, CustomFieldModel):
"""
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
@@ -51,7 +53,7 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
class Meta:
ordering = ['name']
def __unicode__(self):
def __str__(self):
return self.name
def get_absolute_url(self):
@@ -67,6 +69,7 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
])
@python_2_unicode_compatible
class CircuitType(models.Model):
"""
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
@@ -78,13 +81,14 @@ class CircuitType(models.Model):
class Meta:
ordering = ['name']
def __unicode__(self):
def __str__(self):
return self.name
def get_absolute_url(self):
return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug)
@python_2_unicode_compatible
class Circuit(CreatedUpdatedModel, CustomFieldModel):
"""
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
@@ -105,7 +109,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
ordering = ['provider', 'cid']
unique_together = ['provider', 'cid']
def __unicode__(self):
def __str__(self):
return u'{} {}'.format(self.provider, self.cid)
def get_absolute_url(self):
@@ -141,6 +145,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
commit_rate_human.admin_order_field = 'commit_rate'
@python_2_unicode_compatible
class CircuitTermination(models.Model):
circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.CASCADE)
term_side = models.CharField(max_length=1, choices=TERM_SIDE_CHOICES, verbose_name='Termination')
@@ -156,7 +161,7 @@ class CircuitTermination(models.Model):
ordering = ['circuit', 'term_side']
unique_together = ['circuit', 'term_side']
def __unicode__(self):
def __str__(self):
return u'{} (Side {})'.format(self.circuit, self.get_term_side_display())
def get_peer_termination(self):

View File

@@ -0,0 +1,13 @@
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from django.utils import timezone
from .models import Circuit, CircuitTermination
@receiver((post_save, post_delete), sender=CircuitTermination)
def update_circuit(instance, **kwargs):
"""
When a CircuitTermination has been modified, update the last_updated time of its parent Circuit.
"""
Circuit.objects.filter(pk=instance.circuit_id).update(last_updated=timezone.now())

View File

@@ -25,7 +25,6 @@ class ProviderListView(ObjectListView):
filter = filters.ProviderFilter
filter_form = forms.ProviderFilterForm
table = tables.ProviderTable
edit_permissions = ['circuits.change_provider', 'circuits.delete_provider']
template_name = 'circuits/provider_list.html'
@@ -47,7 +46,7 @@ class ProviderEditView(PermissionRequiredMixin, ObjectEditView):
model = Provider
form_class = forms.ProviderForm
template_name = 'circuits/provider_edit.html'
obj_list_url = 'circuits:provider_list'
default_return_url = 'circuits:provider_list'
class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@@ -61,21 +60,23 @@ class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.ProviderImportForm
table = tables.ProviderTable
template_name = 'circuits/provider_import.html'
obj_list_url = 'circuits:provider_list'
default_return_url = 'circuits:provider_list'
class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'circuits.change_provider'
cls = Provider
filter = filters.ProviderFilter
form = forms.ProviderBulkEditForm
template_name = 'circuits/provider_bulk_edit.html'
default_redirect_url = 'circuits:provider_list'
default_return_url = 'circuits:provider_list'
class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_provider'
cls = Provider
default_redirect_url = 'circuits:provider_list'
filter = filters.ProviderFilter
default_return_url = 'circuits:provider_list'
#
@@ -85,7 +86,6 @@ class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
class CircuitTypeListView(ObjectListView):
queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
table = tables.CircuitTypeTable
edit_permissions = ['circuits.change_circuittype', 'circuits.delete_circuittype']
template_name = 'circuits/circuittype_list.html'
@@ -101,7 +101,7 @@ class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView):
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuittype'
cls = CircuitType
default_redirect_url = 'circuits:circuittype_list'
default_return_url = 'circuits:circuittype_list'
#
@@ -113,7 +113,6 @@ class CircuitListView(ObjectListView):
filter = filters.CircuitFilter
filter_form = forms.CircuitFilterForm
table = tables.CircuitTable
edit_permissions = ['circuits.change_circuit', 'circuits.delete_circuit']
template_name = 'circuits/circuit_list.html'
@@ -136,7 +135,7 @@ class CircuitEditView(PermissionRequiredMixin, ObjectEditView):
form_class = forms.CircuitForm
fields_initial = ['provider']
template_name = 'circuits/circuit_edit.html'
obj_list_url = 'circuits:circuit_list'
default_return_url = 'circuits:circuit_list'
class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@@ -150,21 +149,23 @@ class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.CircuitImportForm
table = tables.CircuitTable
template_name = 'circuits/circuit_import.html'
obj_list_url = 'circuits:circuit_list'
default_return_url = 'circuits:circuit_list'
class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'circuits.change_circuit'
cls = Circuit
filter = filters.CircuitFilter
form = forms.CircuitBulkEditForm
template_name = 'circuits/circuit_bulk_edit.html'
default_redirect_url = 'circuits:circuit_list'
default_return_url = 'circuits:circuit_list'
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuit'
cls = Circuit
default_redirect_url = 'circuits:circuit_list'
filter = filters.CircuitFilter
default_return_url = 'circuits:circuit_list'
@permission_required('circuits.change_circuittermination')
@@ -208,7 +209,7 @@ def circuit_terminations_swap(request, pk):
'form': form,
'panel_class': 'default',
'button_class': 'primary',
'cancel_url': circuit.get_absolute_url(),
'return_url': circuit.get_absolute_url(),
})

View File

@@ -134,12 +134,13 @@ class ManufacturerNestedSerializer(ManufacturerSerializer):
class DeviceTypeSerializer(CustomFieldSerializer, serializers.ModelSerializer):
manufacturer = ManufacturerNestedSerializer()
subdevice_role = serializers.SerializerMethodField()
instance_count = serializers.IntegerField(source='instances.count', read_only=True)
class Meta:
model = DeviceType
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
'interface_ordering', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role',
'comments', 'custom_fields']
'comments', 'custom_fields', 'instance_count']
def get_subdevice_role(self, obj):
return {

View File

@@ -331,7 +331,8 @@ class InterfaceListView(generics.ListAPIView):
def get_queryset(self):
device = get_object_or_404(Device, pk=self.kwargs['pk'])
queryset = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b')
queryset = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\
.select_related('connected_as_a', 'connected_as_b', 'circuit_termination')
# Filter by type (physical or virtual)
iface_type = self.request.query_params.get('type')
@@ -489,8 +490,8 @@ class RelatedConnectionsView(APIView):
response['power-ports'].append(data)
# Interface connections
interfaces = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b',
'circuit_termination')
interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\
.select_related('connected_as_a', 'connected_as_b', 'circuit_termination')
for iface in interfaces:
data = serializers.InterfaceDetailSerializer(instance=iface).data
del(data['device'])

View File

@@ -13,7 +13,7 @@ from utilities.forms import (
SlugField,
)
from formfields import MACAddressFormField
from .formfields import MACAddressFormField
from .models import (
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
@@ -101,6 +101,7 @@ class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Site
q = forms.CharField(required=False, label='Search')
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('sites')), to_field_name='slug',
null_option=(0, 'None'))
@@ -232,6 +233,7 @@ class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Rack
q = forms.CharField(required=False, label='Search')
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks')), to_field_name='slug')
group_id = FilterChoiceField(queryset=RackGroup.objects.select_related('site')
.annotate(filter_count=Count('racks')), label='Rack group', null_option=(0, 'None'))
@@ -281,6 +283,7 @@ class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = DeviceType
q = forms.CharField(required=False, label='Search')
manufacturer = FilterChoiceField(queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
to_field_name='slug')
@@ -639,18 +642,46 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Device
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks__devices')), to_field_name='slug')
rack_group_id = FilterChoiceField(queryset=RackGroup.objects.annotate(filter_count=Count('racks__devices')),
label='Rack Group')
role = FilterChoiceField(queryset=DeviceRole.objects.annotate(filter_count=Count('devices')), to_field_name='slug')
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('devices')), to_field_name='slug',
null_option=(0, 'None'))
device_type_id = FilterChoiceField(queryset=DeviceType.objects.select_related('manufacturer')
.annotate(filter_count=Count('instances')), label='Type')
platform = FilterChoiceField(queryset=Platform.objects.annotate(filter_count=Count('devices')),
to_field_name='slug', null_option=(0, 'None'))
status = forms.NullBooleanField(required=False, widget=forms.Select(choices=FORM_STATUS_CHOICES))
mac_address = forms.CharField(required=False, label='MAC address')
q = forms.CharField(required=False, label='Search')
site = FilterChoiceField(
queryset=Site.objects.annotate(filter_count=Count('racks__devices')),
to_field_name='slug',
)
rack_group_id = FilterChoiceField(
queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__devices')),
label='Rack Group',
)
role = FilterChoiceField(
queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
to_field_name='slug',
)
tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('devices')), to_field_name='slug',
null_option=(0, 'None'),
)
manufacturer_id = FilterChoiceField(
queryset=Manufacturer.objects.all(),
label='Manufacturer',
)
device_type_id = FilterChoiceField(
queryset=DeviceType.objects.select_related('manufacturer').order_by('model').annotate(
filter_count=Count('instances'),
),
label='Model',
)
platform = FilterChoiceField(
queryset=Platform.objects.annotate(filter_count=Count('devices')),
to_field_name='slug',
null_option=(0, 'None'),
)
status = forms.NullBooleanField(
required=False,
widget=forms.Select(choices=FORM_STATUS_CHOICES),
)
mac_address = forms.CharField(
required=False,
label='MAC address',
)
#

View File

@@ -8,6 +8,7 @@ from django.core.urlresolvers import reverse
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import Count, Q, ObjectDoesNotExist
from django.utils.encoding import python_2_unicode_compatible
from circuits.models import Circuit
from extras.models import CustomFieldModel, CustomField, CustomFieldValue
@@ -199,6 +200,7 @@ class SiteManager(NaturalOrderByManager):
return self.natural_order_by('name')
@python_2_unicode_compatible
class Site(CreatedUpdatedModel, CustomFieldModel):
"""
A Site represents a geographic location within a network; typically a building or campus. The optional facility
@@ -222,7 +224,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
class Meta:
ordering = ['name']
def __unicode__(self):
def __str__(self):
return self.name
def get_absolute_url(self):
@@ -265,6 +267,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
# Racks
#
@python_2_unicode_compatible
class RackGroup(models.Model):
"""
Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For
@@ -282,13 +285,14 @@ class RackGroup(models.Model):
['site', 'slug'],
]
def __unicode__(self):
def __str__(self):
return u'{} - {}'.format(self.site.name, self.name)
def get_absolute_url(self):
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
@python_2_unicode_compatible
class RackRole(models.Model):
"""
Racks can be organized by functional role, similar to Devices.
@@ -300,7 +304,7 @@ class RackRole(models.Model):
class Meta:
ordering = ['name']
def __unicode__(self):
def __str__(self):
return self.name
def get_absolute_url(self):
@@ -313,6 +317,7 @@ class RackManager(NaturalOrderByManager):
return self.natural_order_by('site__name', 'name')
@python_2_unicode_compatible
class Rack(CreatedUpdatedModel, CustomFieldModel):
"""
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
@@ -343,7 +348,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
['site', 'facility_id'],
]
def __unicode__(self):
def __str__(self):
return self.display_name
def get_absolute_url(self):
@@ -442,7 +447,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
devices = self.devices.select_related('device_type').filter(position__gte=1).exclude(pk__in=exclude)
# Initialize the rack unit skeleton
units = range(1, self.u_height + 1)
units = list(range(1, self.u_height + 1))
# Remove units consumed by installed devices
for d in devices:
@@ -477,6 +482,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
# Device Types
#
@python_2_unicode_compatible
class Manufacturer(models.Model):
"""
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
@@ -487,13 +493,14 @@ class Manufacturer(models.Model):
class Meta:
ordering = ['name']
def __unicode__(self):
def __str__(self):
return self.name
def get_absolute_url(self):
return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug)
@python_2_unicode_compatible
class DeviceType(models.Model, CustomFieldModel):
"""
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
@@ -538,7 +545,7 @@ class DeviceType(models.Model, CustomFieldModel):
['manufacturer', 'slug'],
]
def __unicode__(self):
def __str__(self):
return self.model
def __init__(self, *args, **kwargs):
@@ -608,6 +615,7 @@ class DeviceType(models.Model, CustomFieldModel):
return bool(self.subdevice_role is False)
@python_2_unicode_compatible
class ConsolePortTemplate(models.Model):
"""
A template for a ConsolePort to be created for a new Device.
@@ -619,10 +627,11 @@ class ConsolePortTemplate(models.Model):
ordering = ['device_type', 'name']
unique_together = ['device_type', 'name']
def __unicode__(self):
def __str__(self):
return self.name
@python_2_unicode_compatible
class ConsoleServerPortTemplate(models.Model):
"""
A template for a ConsoleServerPort to be created for a new Device.
@@ -634,10 +643,11 @@ class ConsoleServerPortTemplate(models.Model):
ordering = ['device_type', 'name']
unique_together = ['device_type', 'name']
def __unicode__(self):
def __str__(self):
return self.name
@python_2_unicode_compatible
class PowerPortTemplate(models.Model):
"""
A template for a PowerPort to be created for a new Device.
@@ -649,10 +659,11 @@ class PowerPortTemplate(models.Model):
ordering = ['device_type', 'name']
unique_together = ['device_type', 'name']
def __unicode__(self):
def __str__(self):
return self.name
@python_2_unicode_compatible
class PowerOutletTemplate(models.Model):
"""
A template for a PowerOutlet to be created for a new Device.
@@ -664,7 +675,7 @@ class PowerOutletTemplate(models.Model):
ordering = ['device_type', 'name']
unique_together = ['device_type', 'name']
def __unicode__(self):
def __str__(self):
return self.name
@@ -706,6 +717,7 @@ class InterfaceManager(models.Manager):
}).order_by(*ordering)
@python_2_unicode_compatible
class InterfaceTemplate(models.Model):
"""
A template for a physical data interface on a new Device.
@@ -721,10 +733,11 @@ class InterfaceTemplate(models.Model):
ordering = ['device_type', 'name']
unique_together = ['device_type', 'name']
def __unicode__(self):
def __str__(self):
return self.name
@python_2_unicode_compatible
class DeviceBayTemplate(models.Model):
"""
A template for a DeviceBay to be created for a new parent Device.
@@ -736,7 +749,7 @@ class DeviceBayTemplate(models.Model):
ordering = ['device_type', 'name']
unique_together = ['device_type', 'name']
def __unicode__(self):
def __str__(self):
return self.name
@@ -744,6 +757,7 @@ class DeviceBayTemplate(models.Model):
# Devices
#
@python_2_unicode_compatible
class DeviceRole(models.Model):
"""
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
@@ -756,13 +770,14 @@ class DeviceRole(models.Model):
class Meta:
ordering = ['name']
def __unicode__(self):
def __str__(self):
return self.name
def get_absolute_url(self):
return "{}?role={}".format(reverse('dcim:device_list'), self.slug)
@python_2_unicode_compatible
class Platform(models.Model):
"""
Platform refers to the software or firmware running on a Device; for example, "Cisco IOS-XR" or "Juniper Junos".
@@ -776,7 +791,7 @@ class Platform(models.Model):
class Meta:
ordering = ['name']
def __unicode__(self):
def __str__(self):
return self.name
def get_absolute_url(self):
@@ -789,6 +804,7 @@ class DeviceManager(NaturalOrderByManager):
return self.natural_order_by('name')
@python_2_unicode_compatible
class Device(CreatedUpdatedModel, CustomFieldModel):
"""
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
@@ -828,7 +844,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
ordering = ['name']
unique_together = ['rack', 'position', 'face']
def __unicode__(self):
def __str__(self):
return self.display_name
def get_absolute_url(self):
@@ -968,6 +984,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
return RPC_CLIENTS.get(self.platform.rpc_client)
@python_2_unicode_compatible
class ConsolePort(models.Model):
"""
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
@@ -982,7 +999,7 @@ class ConsolePort(models.Model):
ordering = ['device', 'name']
unique_together = ['device', 'name']
def __unicode__(self):
def __str__(self):
return self.name
# Used for connections export
@@ -1011,6 +1028,7 @@ class ConsoleServerPortManager(models.Manager):
}).order_by('device', 'name_as_integer')
@python_2_unicode_compatible
class ConsoleServerPort(models.Model):
"""
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
@@ -1023,10 +1041,11 @@ class ConsoleServerPort(models.Model):
class Meta:
unique_together = ['device', 'name']
def __unicode__(self):
def __str__(self):
return self.name
@python_2_unicode_compatible
class PowerPort(models.Model):
"""
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
@@ -1041,7 +1060,7 @@ class PowerPort(models.Model):
ordering = ['device', 'name']
unique_together = ['device', 'name']
def __unicode__(self):
def __str__(self):
return self.name
# Used for connections export
@@ -1064,6 +1083,7 @@ class PowerOutletManager(models.Manager):
}).order_by('device', 'name_padded')
@python_2_unicode_compatible
class PowerOutlet(models.Model):
"""
A physical power outlet (output) within a Device which provides power to a PowerPort.
@@ -1076,10 +1096,11 @@ class PowerOutlet(models.Model):
class Meta:
unique_together = ['device', 'name']
def __unicode__(self):
def __str__(self):
return self.name
@python_2_unicode_compatible
class Interface(models.Model):
"""
A physical data interface within a Device. An Interface can connect to exactly one other Interface via the creation
@@ -1099,7 +1120,7 @@ class Interface(models.Model):
ordering = ['device', 'name']
unique_together = ['device', 'name']
def __unicode__(self):
def __str__(self):
return self.name
def clean(self):
@@ -1176,6 +1197,7 @@ class InterfaceConnection(models.Model):
])
@python_2_unicode_compatible
class DeviceBay(models.Model):
"""
An empty space within a Device which can house a child device
@@ -1189,7 +1211,7 @@ class DeviceBay(models.Model):
ordering = ['device', 'name']
unique_together = ['device', 'name']
def __unicode__(self):
def __str__(self):
return u'{} - {}'.format(self.device.name, self.name)
def clean(self):
@@ -1205,6 +1227,7 @@ class DeviceBay(models.Model):
raise ValidationError("Cannot install a device into itself.")
@python_2_unicode_compatible
class Module(models.Model):
"""
A Module represents a piece of hardware within a Device, such as a line card or power supply. Modules are used only
@@ -1223,5 +1246,5 @@ class Module(models.Model):
ordering = ['device__id', 'parent__id', 'name']
unique_together = ['device', 'parent', 'name']
def __unicode__(self):
def __str__(self):
return self.name

View File

@@ -65,7 +65,7 @@ class SiteTest(APITestCase):
def test_get_list(self, endpoint='/{}api/dcim/sites/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in content:
self.assertEqual(
@@ -75,7 +75,7 @@ class SiteTest(APITestCase):
def test_get_detail(self, endpoint='/{}api/dcim/sites/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),
@@ -84,9 +84,9 @@ class SiteTest(APITestCase):
def test_get_site_list_rack(self, endpoint='/{}api/dcim/sites/1/racks/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in json.loads(response.content):
for i in json.loads(response.content.decode('utf-8')):
self.assertEqual(
sorted(i.keys()),
sorted(self.rack_fields),
@@ -99,9 +99,9 @@ class SiteTest(APITestCase):
def test_get_site_list_graphs(self, endpoint='/{}api/dcim/sites/1/graphs/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in json.loads(response.content):
for i in json.loads(response.content.decode('utf-8')):
self.assertEqual(
sorted(i.keys()),
sorted(self.graph_fields),
@@ -159,7 +159,7 @@ class RackTest(APITestCase):
def test_get_list(self, endpoint='/{}api/dcim/racks/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in content:
self.assertEqual(
@@ -173,7 +173,7 @@ class RackTest(APITestCase):
def test_get_detail(self, endpoint='/{}api/dcim/racks/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),
@@ -202,7 +202,7 @@ class ManufacturersTest(APITestCase):
def test_get_list(self, endpoint='/{}api/dcim/manufacturers/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in content:
self.assertEqual(
@@ -212,7 +212,7 @@ class ManufacturersTest(APITestCase):
def test_get_detail(self, endpoint='/{}api/dcim/manufacturers/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),
@@ -239,6 +239,7 @@ class DeviceTypeTest(APITestCase):
'subdevice_role',
'comments',
'custom_fields',
'instance_count',
]
nested_fields = [
@@ -250,7 +251,7 @@ class DeviceTypeTest(APITestCase):
def test_get_list(self, endpoint='/{}api/dcim/device-types/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in content:
self.assertEqual(
@@ -261,7 +262,7 @@ class DeviceTypeTest(APITestCase):
def test_detail_list(self, endpoint='/{}api/dcim/device-types/1/'.format(settings.BASE_PATH)):
# TODO: details returns list view.
# response = self.client.get(endpoint)
# content = json.loads(response.content)
# content = json.loads(response.content.decode('utf-8'))
# self.assertEqual(response.status_code, status.HTTP_200_OK)
# self.assertEqual(
# sorted(content.keys()),
@@ -284,7 +285,7 @@ class DeviceRolesTest(APITestCase):
def test_get_list(self, endpoint='/{}api/dcim/device-roles/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in content:
self.assertEqual(
@@ -294,7 +295,7 @@ class DeviceRolesTest(APITestCase):
def test_get_detail(self, endpoint='/{}api/dcim/device-roles/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),
@@ -312,7 +313,7 @@ class PlatformsTest(APITestCase):
def test_get_list(self, endpoint='/{}api/dcim/platforms/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in content:
self.assertEqual(
@@ -322,7 +323,7 @@ class PlatformsTest(APITestCase):
def test_get_detail(self, endpoint='/{}api/dcim/platforms/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),
@@ -360,7 +361,7 @@ class DeviceTest(APITestCase):
def test_get_list(self, endpoint='/{}api/dcim/devices/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for device in content:
self.assertEqual(
@@ -425,7 +426,7 @@ class DeviceTest(APITestCase):
]
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
device = content[0]
self.assertEqual(
@@ -435,7 +436,7 @@ class DeviceTest(APITestCase):
def test_get_detail(self, endpoint='/{}api/dcim/devices/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),
@@ -453,7 +454,7 @@ class ConsoleServerPortsTest(APITestCase):
def test_get_list(self, endpoint='/{}api/dcim/devices/9/console-server-ports/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for console_port in content:
self.assertEqual(
@@ -475,7 +476,7 @@ class ConsolePortsTest(APITestCase):
def test_get_list(self, endpoint='/{}api/dcim/devices/1/console-ports/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for console_port in content:
self.assertEqual(
@@ -493,7 +494,7 @@ class ConsolePortsTest(APITestCase):
def test_get_detail(self, endpoint='/{}api/dcim/console-ports/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),
@@ -514,7 +515,7 @@ class PowerPortsTest(APITestCase):
def test_get_list(self, endpoint='/{}api/dcim/devices/1/power-ports/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in content:
self.assertEqual(
@@ -528,7 +529,7 @@ class PowerPortsTest(APITestCase):
def test_get_detail(self, endpoint='/{}api/dcim/power-ports/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),
@@ -549,7 +550,7 @@ class PowerOutletsTest(APITestCase):
def test_get_list(self, endpoint='/{}api/dcim/devices/11/power-outlets/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in content:
self.assertEqual(
@@ -599,7 +600,7 @@ class InterfaceTest(APITestCase):
def test_get_list(self, endpoint='/{}api/dcim/devices/1/interfaces/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in content:
self.assertEqual(
@@ -613,7 +614,7 @@ class InterfaceTest(APITestCase):
def test_get_detail(self, endpoint='/{}api/dcim/interfaces/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),
@@ -625,19 +626,19 @@ class InterfaceTest(APITestCase):
)
def test_get_graph_list(self, endpoint='/{}api/dcim/interfaces/1/graphs/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in content:
self.assertEqual(
sorted(i.keys()),
sorted(SiteTest.graph_fields),
)
response = self.client.get(endpoint)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
for i in content:
self.assertEqual(
sorted(i.keys()),
sorted(SiteTest.graph_fields),
)
def test_get_interface_connections(self, endpoint='/{}api/dcim/interface-connections/4/'
.format(settings.BASE_PATH)):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),
@@ -659,7 +660,7 @@ class RelatedConnectionsTest(APITestCase):
def test_get_list(self, endpoint=('/{}api/dcim/related-connections/?peer-device=test1-edge1&peer-interface=xe-0/0/3'
.format(settings.BASE_PATH))):
response = self.client.get(endpoint)
content = json.loads(response.content)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
sorted(content.keys()),

View File

@@ -71,7 +71,7 @@ class ComponentCreateView(View):
'parent': parent,
'component_type': self.model._meta.verbose_name,
'form': self.form(initial=request.GET),
'cancel_url': parent.get_absolute_url(),
'return_url': parent.get_absolute_url(),
})
def post(self, request, pk):
@@ -112,10 +112,22 @@ class ComponentCreateView(View):
'parent': parent,
'component_type': self.model._meta.verbose_name,
'form': form,
'cancel_url': parent.get_absolute_url(),
'return_url': parent.get_absolute_url(),
})
class ComponentEditView(ObjectEditView):
def get_return_url(self, obj):
return obj.device.get_absolute_url()
class ComponentDeleteView(ObjectDeleteView):
def get_return_url(self, obj):
return obj.device.get_absolute_url()
#
# Sites
#
@@ -125,7 +137,6 @@ class SiteListView(ObjectListView):
filter = filters.SiteFilter
filter_form = forms.SiteFilterForm
table = tables.SiteTable
edit_permissions = ['dcim.change_rack', 'dcim.delete_rack']
template_name = 'dcim/site_list.html'
@@ -157,7 +168,7 @@ class SiteEditView(PermissionRequiredMixin, ObjectEditView):
model = Site
form_class = forms.SiteForm
template_name = 'dcim/site_edit.html'
obj_list_url = 'dcim:site_list'
default_return_url = 'dcim:site_list'
class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@@ -171,15 +182,16 @@ class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.SiteImportForm
table = tables.SiteTable
template_name = 'dcim/site_import.html'
obj_list_url = 'dcim:site_list'
default_return_url = 'dcim:site_list'
class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_site'
cls = Site
filter = filters.SiteFilter
form = forms.SiteBulkEditForm
template_name = 'dcim/site_bulk_edit.html'
default_redirect_url = 'dcim:site_list'
default_return_url = 'dcim:site_list'
#
@@ -191,7 +203,6 @@ class RackGroupListView(ObjectListView):
filter = filters.RackGroupFilter
filter_form = forms.RackGroupFilterForm
table = tables.RackGroupTable
edit_permissions = ['dcim.change_rackgroup', 'dcim.delete_rackgroup']
template_name = 'dcim/rackgroup_list.html'
@@ -207,7 +218,8 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rackgroup'
cls = RackGroup
default_redirect_url = 'dcim:rackgroup_list'
filter = filters.RackGroupFilter
default_return_url = 'dcim:rackgroup_list'
#
@@ -217,7 +229,6 @@ class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
class RackRoleListView(ObjectListView):
queryset = RackRole.objects.annotate(rack_count=Count('racks'))
table = tables.RackRoleTable
edit_permissions = ['dcim.change_rackrole', 'dcim.delete_rackrole']
template_name = 'dcim/rackrole_list.html'
@@ -233,7 +244,7 @@ class RackRoleEditView(PermissionRequiredMixin, ObjectEditView):
class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rackrole'
cls = RackRole
default_redirect_url = 'dcim:rackrole_list'
default_return_url = 'dcim:rackrole_list'
#
@@ -246,7 +257,6 @@ class RackListView(ObjectListView):
filter = filters.RackFilter
filter_form = forms.RackFilterForm
table = tables.RackTable
edit_permissions = ['dcim.change_rack', 'dcim.delete_rack']
template_name = 'dcim/rack_list.html'
@@ -274,7 +284,7 @@ class RackEditView(PermissionRequiredMixin, ObjectEditView):
model = Rack
form_class = forms.RackForm
template_name = 'dcim/rack_edit.html'
obj_list_url = 'dcim:rack_list'
default_return_url = 'dcim:rack_list'
class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@@ -288,21 +298,23 @@ class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.RackImportForm
table = tables.RackImportTable
template_name = 'dcim/rack_import.html'
obj_list_url = 'dcim:rack_list'
default_return_url = 'dcim:rack_list'
class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_rack'
cls = Rack
filter = filters.RackFilter
form = forms.RackBulkEditForm
template_name = 'dcim/rack_bulk_edit.html'
default_redirect_url = 'dcim:rack_list'
default_return_url = 'dcim:rack_list'
class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rack'
cls = Rack
default_redirect_url = 'dcim:rack_list'
filter = filters.RackFilter
default_return_url = 'dcim:rack_list'
#
@@ -312,7 +324,6 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
class ManufacturerListView(ObjectListView):
queryset = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
table = tables.ManufacturerTable
edit_permissions = ['dcim.change_manufacturer', 'dcim.delete_manufacturer']
template_name = 'dcim/manufacturer_list.html'
@@ -328,7 +339,7 @@ class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView):
class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_manufacturer'
cls = Manufacturer
default_redirect_url = 'dcim:manufacturer_list'
default_return_url = 'dcim:manufacturer_list'
#
@@ -340,7 +351,6 @@ class DeviceTypeListView(ObjectListView):
filter = filters.DeviceTypeFilter
filter_form = forms.DeviceTypeFilterForm
table = tables.DeviceTypeTable
edit_permissions = ['dcim.change_devicetype', 'dcim.delete_devicetype']
template_name = 'dcim/devicetype_list.html'
@@ -398,7 +408,7 @@ class DeviceTypeEditView(PermissionRequiredMixin, ObjectEditView):
model = DeviceType
form_class = forms.DeviceTypeForm
template_name = 'dcim/devicetype_edit.html'
obj_list_url = 'dcim:devicetype_list'
default_return_url = 'dcim:devicetype_list'
class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@@ -410,15 +420,17 @@ class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_devicetype'
cls = DeviceType
filter = filters.DeviceTypeFilter
form = forms.DeviceTypeBulkEditForm
template_name = 'dcim/devicetype_bulk_edit.html'
default_redirect_url = 'dcim:devicetype_list'
default_return_url = 'dcim:devicetype_list'
class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicetype'
cls = DeviceType
default_redirect_url = 'dcim:devicetype_list'
filter = filters.DeviceTypeFilter
default_return_url = 'dcim:devicetype_list'
#
@@ -532,7 +544,6 @@ class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
class DeviceRoleListView(ObjectListView):
queryset = DeviceRole.objects.annotate(device_count=Count('devices'))
table = tables.DeviceRoleTable
edit_permissions = ['dcim.change_devicerole', 'dcim.delete_devicerole']
template_name = 'dcim/devicerole_list.html'
@@ -548,7 +559,7 @@ class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView):
class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicerole'
cls = DeviceRole
default_redirect_url = 'dcim:devicerole_list'
default_return_url = 'dcim:devicerole_list'
#
@@ -558,7 +569,6 @@ class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
class PlatformListView(ObjectListView):
queryset = Platform.objects.annotate(device_count=Count('devices'))
table = tables.PlatformTable
edit_permissions = ['dcim.change_platform', 'dcim.delete_platform']
template_name = 'dcim/platform_list.html'
@@ -574,7 +584,7 @@ class PlatformEditView(PermissionRequiredMixin, ObjectEditView):
class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_platform'
cls = Platform
default_redirect_url = 'dcim:platform_list'
default_return_url = 'dcim:platform_list'
#
@@ -587,7 +597,6 @@ class DeviceListView(ObjectListView):
filter = filters.DeviceFilter
filter_form = forms.DeviceFilterForm
table = tables.DeviceTable
edit_permissions = ['dcim.change_device', 'dcim.delete_device']
template_name = 'dcim/device_list.html'
@@ -666,7 +675,7 @@ class DeviceEditView(PermissionRequiredMixin, ObjectEditView):
form_class = forms.DeviceForm
fields_initial = ['site', 'rack', 'position', 'face', 'device_bay']
template_name = 'dcim/device_edit.html'
obj_list_url = 'dcim:device_list'
default_return_url = 'dcim:device_list'
class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@@ -680,7 +689,7 @@ class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.DeviceImportForm
table = tables.DeviceImportTable
template_name = 'dcim/device_import.html'
obj_list_url = 'dcim:device_list'
default_return_url = 'dcim:device_list'
class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
@@ -688,7 +697,7 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.ChildDeviceImportForm
table = tables.DeviceImportTable
template_name = 'dcim/device_import_child.html'
obj_list_url = 'dcim:device_list'
default_return_url = 'dcim:device_list'
def save_obj(self, obj):
# Inherent rack from parent device
@@ -703,15 +712,17 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_device'
cls = Device
filter = filters.DeviceFilter
form = forms.DeviceBulkEditForm
template_name = 'dcim/device_bulk_edit.html'
default_redirect_url = 'dcim:device_list'
default_return_url = 'dcim:device_list'
class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_device'
cls = Device
default_redirect_url = 'dcim:device_list'
filter = filters.DeviceFilter
default_return_url = 'dcim:device_list'
def device_inventory(request, pk):
@@ -729,7 +740,8 @@ def device_inventory(request, pk):
def device_lldp_neighbors(request, pk):
device = get_object_or_404(Device, pk=pk)
interfaces = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b')
interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\
.select_related('connected_as_a', 'connected_as_b')
return render(request, 'dcim/device_lldp_neighbors.html', {
'device': device,
@@ -776,7 +788,7 @@ def consoleport_connect(request, pk):
return render(request, 'dcim/consoleport_connect.html', {
'consoleport': consoleport,
'form': form,
'cancel_url': reverse('dcim:device', kwargs={'pk': consoleport.device.pk}),
'return_url': reverse('dcim:device', kwargs={'pk': consoleport.device.pk}),
})
@@ -805,17 +817,17 @@ def consoleport_disconnect(request, pk):
return render(request, 'dcim/consoleport_disconnect.html', {
'consoleport': consoleport,
'form': form,
'cancel_url': reverse('dcim:device', kwargs={'pk': consoleport.device.pk}),
'return_url': reverse('dcim:device', kwargs={'pk': consoleport.device.pk}),
})
class ConsolePortEditView(PermissionRequiredMixin, ObjectEditView):
class ConsolePortEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_consoleport'
model = ConsolePort
form_class = forms.ConsolePortForm
class ConsolePortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class ConsolePortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_consoleport'
model = ConsolePort
@@ -872,7 +884,7 @@ def consoleserverport_connect(request, pk):
return render(request, 'dcim/consoleserverport_connect.html', {
'consoleserverport': consoleserverport,
'form': form,
'cancel_url': reverse('dcim:device', kwargs={'pk': consoleserverport.device.pk}),
'return_url': reverse('dcim:device', kwargs={'pk': consoleserverport.device.pk}),
})
@@ -902,17 +914,17 @@ def consoleserverport_disconnect(request, pk):
return render(request, 'dcim/consoleserverport_disconnect.html', {
'consoleserverport': consoleserverport,
'form': form,
'cancel_url': reverse('dcim:device', kwargs={'pk': consoleserverport.device.pk}),
'return_url': reverse('dcim:device', kwargs={'pk': consoleserverport.device.pk}),
})
class ConsoleServerPortEditView(PermissionRequiredMixin, ObjectEditView):
class ConsoleServerPortEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_consoleserverport'
model = ConsoleServerPort
form_class = forms.ConsoleServerPortForm
class ConsoleServerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class ConsoleServerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_consoleserverport'
model = ConsoleServerPort
@@ -962,7 +974,7 @@ def powerport_connect(request, pk):
return render(request, 'dcim/powerport_connect.html', {
'powerport': powerport,
'form': form,
'cancel_url': reverse('dcim:device', kwargs={'pk': powerport.device.pk}),
'return_url': reverse('dcim:device', kwargs={'pk': powerport.device.pk}),
})
@@ -991,17 +1003,17 @@ def powerport_disconnect(request, pk):
return render(request, 'dcim/powerport_disconnect.html', {
'powerport': powerport,
'form': form,
'cancel_url': reverse('dcim:device', kwargs={'pk': powerport.device.pk}),
'return_url': reverse('dcim:device', kwargs={'pk': powerport.device.pk}),
})
class PowerPortEditView(PermissionRequiredMixin, ObjectEditView):
class PowerPortEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_powerport'
model = PowerPort
form_class = forms.PowerPortForm
class PowerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class PowerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_powerport'
model = PowerPort
@@ -1058,7 +1070,7 @@ def poweroutlet_connect(request, pk):
return render(request, 'dcim/poweroutlet_connect.html', {
'poweroutlet': poweroutlet,
'form': form,
'cancel_url': reverse('dcim:device', kwargs={'pk': poweroutlet.device.pk}),
'return_url': reverse('dcim:device', kwargs={'pk': poweroutlet.device.pk}),
})
@@ -1087,17 +1099,17 @@ def poweroutlet_disconnect(request, pk):
return render(request, 'dcim/poweroutlet_disconnect.html', {
'poweroutlet': poweroutlet,
'form': form,
'cancel_url': reverse('dcim:device', kwargs={'pk': poweroutlet.device.pk}),
'return_url': reverse('dcim:device', kwargs={'pk': poweroutlet.device.pk}),
})
class PowerOutletEditView(PermissionRequiredMixin, ObjectEditView):
class PowerOutletEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_poweroutlet'
model = PowerOutlet
form_class = forms.PowerOutletForm
class PowerOutletDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class PowerOutletDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_poweroutlet'
model = PowerOutlet
@@ -1121,13 +1133,13 @@ class InterfaceAddView(PermissionRequiredMixin, ComponentCreateView):
model_form = forms.InterfaceForm
class InterfaceEditView(PermissionRequiredMixin, ObjectEditView):
class InterfaceEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_interface'
model = Interface
form_class = forms.InterfaceForm
class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class InterfaceDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_interface'
model = Interface
@@ -1159,13 +1171,13 @@ class DeviceBayAddView(PermissionRequiredMixin, ComponentCreateView):
model_form = forms.DeviceBayForm
class DeviceBayEditView(PermissionRequiredMixin, ObjectEditView):
class DeviceBayEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_devicebay'
model = DeviceBay
form_class = forms.DeviceBayForm
class DeviceBayDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class DeviceBayDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_devicebay'
model = DeviceBay
@@ -1192,7 +1204,7 @@ def devicebay_populate(request, pk):
return render(request, 'dcim/devicebay_populate.html', {
'device_bay': device_bay,
'form': form,
'cancel_url': reverse('dcim:device', kwargs={'pk': device_bay.device.pk}),
'return_url': reverse('dcim:device', kwargs={'pk': device_bay.device.pk}),
})
@@ -1216,7 +1228,7 @@ def devicebay_depopulate(request, pk):
return render(request, 'dcim/devicebay_depopulate.html', {
'device_bay': device_bay,
'form': form,
'cancel_url': reverse('dcim:device', kwargs={'pk': device_bay.device.pk}),
'return_url': reverse('dcim:device', kwargs={'pk': device_bay.device.pk}),
})
@@ -1245,7 +1257,7 @@ class DeviceBulkAddComponentView(View):
# Are we editing *all* objects in the queryset or just a selected subset?
if request.POST.get('_all'):
pk_list = [int(pk) for pk in request.POST.get('pk_all').split(',') if pk]
pk_list = [obj.pk for obj in filters.DeviceFilter(request.GET, Device.objects.all())]
else:
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
@@ -1291,7 +1303,7 @@ class DeviceBulkAddComponentView(View):
'form': form,
'component_name': self.model._meta.verbose_name_plural,
'selected_devices': selected_devices,
'cancel_url': reverse('dcim:device_list'),
'return_url': reverse('dcim:device_list'),
})
@@ -1373,7 +1385,7 @@ def interfaceconnection_add(request, pk):
return render(request, 'dcim/interfaceconnection_edit.html', {
'device': device,
'form': form,
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
'return_url': reverse('dcim:device', kwargs={'pk': device.pk}),
})
@@ -1405,15 +1417,15 @@ def interfaceconnection_delete(request, pk):
# Determine where to direct user upon cancellation
if device_id:
cancel_url = reverse('dcim:device', kwargs={'pk': device_id})
return_url = reverse('dcim:device', kwargs={'pk': device_id})
else:
cancel_url = reverse('dcim:device_list')
return_url = reverse('dcim:device_list')
return render(request, 'dcim/interfaceconnection_delete.html', {
'interfaceconnection': interfaceconnection,
'device_id': device_id,
'form': form,
'cancel_url': cancel_url,
'return_url': return_url,
})
@@ -1492,7 +1504,7 @@ def ipaddress_assign(request, pk):
return render(request, 'dcim/ipaddress_assign.html', {
'device': device,
'form': form,
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
'return_url': reverse('dcim:device', kwargs={'pk': device.pk}),
})
@@ -1500,7 +1512,7 @@ def ipaddress_assign(request, pk):
# Modules
#
class ModuleEditView(PermissionRequiredMixin, ObjectEditView):
class ModuleEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_module'
model = Module
form_class = forms.ModuleForm
@@ -1510,10 +1522,7 @@ class ModuleEditView(PermissionRequiredMixin, ObjectEditView):
obj.device = get_object_or_404(Device, pk=kwargs['device'])
return obj
def get_return_url(self, obj):
return obj.device.get_absolute_url()
class ModuleDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class ModuleDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_module'
model = Module

View File

@@ -50,7 +50,7 @@ class FlatJSONRenderer(renderers.BaseRenderer):
def render(self, data, media_type=None, renderer_context=None):
def flatten(entry):
for key, val in entry.iteritems():
for key, val in entry.items():
if isinstance(val, dict):
for child_key, child_val in flatten(val):
yield "{}_{}".format(key, child_key), child_val

View File

@@ -8,6 +8,7 @@ from django.core.validators import ValidationError
from django.db import models
from django.http import HttpResponse
from django.template import Template, Context
from django.utils.encoding import python_2_unicode_compatible
from django.utils.safestring import mark_safe
@@ -93,6 +94,7 @@ class CustomFieldModel(object):
return OrderedDict([(field, None) for field in fields])
@python_2_unicode_compatible
class CustomField(models.Model):
obj_type = models.ManyToManyField(ContentType, related_name='custom_fields', verbose_name='Object(s)',
limit_choices_to={'model__in': CUSTOMFIELD_MODELS},
@@ -114,7 +116,7 @@ class CustomField(models.Model):
class Meta:
ordering = ['weight', 'name']
def __unicode__(self):
def __str__(self):
return self.label or self.name.replace('_', ' ').capitalize()
def serialize_value(self, value):
@@ -153,6 +155,7 @@ class CustomField(models.Model):
return serialized_value
@python_2_unicode_compatible
class CustomFieldValue(models.Model):
field = models.ForeignKey('CustomField', related_name='values')
obj_type = models.ForeignKey(ContentType, related_name='+', on_delete=models.PROTECT)
@@ -164,7 +167,7 @@ class CustomFieldValue(models.Model):
ordering = ['obj_type', 'obj_id']
unique_together = ['field', 'obj_type', 'obj_id']
def __unicode__(self):
def __str__(self):
return u'{} {}'.format(self.obj, self.field)
@property
@@ -183,6 +186,7 @@ class CustomFieldValue(models.Model):
super(CustomFieldValue, self).save(*args, **kwargs)
@python_2_unicode_compatible
class CustomFieldChoice(models.Model):
field = models.ForeignKey('CustomField', related_name='choices', limit_choices_to={'type': CF_TYPE_SELECT},
on_delete=models.CASCADE)
@@ -193,7 +197,7 @@ class CustomFieldChoice(models.Model):
ordering = ['field', 'weight', 'value']
unique_together = ['field', 'value']
def __unicode__(self):
def __str__(self):
return self.value
def clean(self):
@@ -207,6 +211,7 @@ class CustomFieldChoice(models.Model):
CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete()
@python_2_unicode_compatible
class Graph(models.Model):
type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
weight = models.PositiveSmallIntegerField(default=1000)
@@ -217,7 +222,7 @@ class Graph(models.Model):
class Meta:
ordering = ['type', 'weight', 'name']
def __unicode__(self):
def __str__(self):
return self.name
def embed_url(self, obj):
@@ -231,6 +236,7 @@ class Graph(models.Model):
return template.render(Context({'obj': obj}))
@python_2_unicode_compatible
class ExportTemplate(models.Model):
content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS})
name = models.CharField(max_length=100)
@@ -245,7 +251,7 @@ class ExportTemplate(models.Model):
['content_type', 'name']
]
def __unicode__(self):
def __str__(self):
return u'{}: {}'.format(self.content_type, self.name)
def to_response(self, context_dict, filename):
@@ -264,6 +270,7 @@ class ExportTemplate(models.Model):
return response
@python_2_unicode_compatible
class TopologyMap(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
@@ -278,7 +285,7 @@ class TopologyMap(models.Model):
class Meta:
ordering = ['name']
def __unicode__(self):
def __str__(self):
return self.name
@property
@@ -328,6 +335,7 @@ class UserActionManager(models.Manager):
self.log_bulk_action(user, content_type, ACTION_BULK_DELETE, message)
@python_2_unicode_compatible
class UserAction(models.Model):
"""
A record of an action (add, edit, or delete) performed on an object by a User.
@@ -344,7 +352,7 @@ class UserAction(models.Model):
class Meta:
ordering = ['-time']
def __unicode__(self):
def __str__(self):
if self.message:
return u'{} {}'.format(self.user, self.message)
return u'{} {} {}'.format(self.user, self.get_action_display(), self.content_type)

View File

@@ -1,8 +1,8 @@
#!/usr/bin/python
#!/usr/bin/env python
# This script will generate a random 50-character string suitable for use as a SECRET_KEY.
import os
import random
charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)'
random.seed = (os.urandom(2048))
print ''.join(random.choice(charset) for c in range(50))
print(''.join(random.choice(charset) for c in range(50)))

View File

@@ -63,6 +63,7 @@ class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = VRF
q = forms.CharField(required=False, label='Search')
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vrfs')), to_field_name='slug',
null_option=(0, None))
@@ -128,6 +129,7 @@ class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Aggregate
q = forms.CharField(required=False, label='Search')
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
rir = FilterChoiceField(queryset=RIR.objects.annotate(filter_count=Count('aggregates')), to_field_name='slug',
label='RIR')
@@ -256,8 +258,9 @@ def prefix_status_choices():
class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Prefix
parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
'placeholder': 'Network',
q = forms.CharField(required=False, label='Search')
parent = forms.CharField(required=False, label='Parent Prefix', widget=forms.TextInput(attrs={
'placeholder': 'Prefix',
}))
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
vrf = FilterChoiceField(queryset=VRF.objects.annotate(filter_count=Count('prefixes')), to_field_name='rd',
@@ -446,7 +449,8 @@ def ipaddress_status_choices():
class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = IPAddress
parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
q = forms.CharField(required=False, label='Search')
parent = forms.CharField(required=False, label='Parent Prefix', widget=forms.TextInput(attrs={
'placeholder': 'Prefix',
}))
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
@@ -560,6 +564,7 @@ def vlan_status_choices():
class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = VLAN
q = forms.CharField(required=False, label='Search')
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlans')), to_field_name='slug')
group_id = FilterChoiceField(queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')), label='VLAN group',
null_option=(0, 'None'))

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-23 19:10
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0013_prefix_add_is_pool'),
]
operations = [
migrations.AlterField(
model_name='ipaddress',
name='status',
field=models.PositiveSmallIntegerField(choices=[(1, b'Active'), (2, b'Reserved'), (3, b'Deprecated'), (5, b'DHCP')], default=1, verbose_name=b'Status'),
),
]

View File

@@ -7,6 +7,7 @@ from django.core.urlresolvers import reverse
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models.expressions import RawSQL
from django.utils.encoding import python_2_unicode_compatible
from dcim.models import Interface
from extras.models import CustomFieldModel, CustomFieldValue
@@ -36,10 +37,12 @@ PREFIX_STATUS_CHOICES = (
IPADDRESS_STATUS_ACTIVE = 1
IPADDRESS_STATUS_RESERVED = 2
IPADDRESS_STATUS_DEPRECATED = 3
IPADDRESS_STATUS_DHCP = 5
IPADDRESS_STATUS_CHOICES = (
(IPADDRESS_STATUS_ACTIVE, 'Active'),
(IPADDRESS_STATUS_RESERVED, 'Reserved'),
(IPADDRESS_STATUS_DEPRECATED, 'Deprecated'),
(IPADDRESS_STATUS_DHCP, 'DHCP')
)
@@ -70,6 +73,7 @@ IP_PROTOCOL_CHOICES = (
)
@python_2_unicode_compatible
class VRF(CreatedUpdatedModel, CustomFieldModel):
"""
A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
@@ -89,7 +93,7 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
verbose_name = 'VRF'
verbose_name_plural = 'VRFs'
def __unicode__(self):
def __str__(self):
return self.name
def get_absolute_url(self):
@@ -105,6 +109,7 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
])
@python_2_unicode_compatible
class RIR(models.Model):
"""
A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address
@@ -120,13 +125,14 @@ class RIR(models.Model):
verbose_name = 'RIR'
verbose_name_plural = 'RIRs'
def __unicode__(self):
def __str__(self):
return self.name
def get_absolute_url(self):
return "{}?rir={}".format(reverse('ipam:aggregate_list'), self.slug)
@python_2_unicode_compatible
class Aggregate(CreatedUpdatedModel, CustomFieldModel):
"""
An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
@@ -142,7 +148,7 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
class Meta:
ordering = ['family', 'prefix']
def __unicode__(self):
def __str__(self):
return str(self.prefix)
def get_absolute_url(self):
@@ -204,6 +210,7 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
return int(children_size / self.prefix.size * 100)
@python_2_unicode_compatible
class Role(models.Model):
"""
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
@@ -216,7 +223,7 @@ class Role(models.Model):
class Meta:
ordering = ['weight', 'name']
def __unicode__(self):
def __str__(self):
return self.name
@property
@@ -263,6 +270,7 @@ class PrefixQuerySet(NullsFirstQuerySet):
return filter(lambda p: p.depth <= limit, queryset)
@python_2_unicode_compatible
class Prefix(CreatedUpdatedModel, CustomFieldModel):
"""
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and
@@ -292,7 +300,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
ordering = ['vrf', 'family', 'prefix']
verbose_name_plural = 'prefixes'
def __unicode__(self):
def __str__(self):
return str(self.prefix)
def get_absolute_url(self):
@@ -377,6 +385,7 @@ class IPAddressManager(models.Manager):
return qs.annotate(host=RawSQL('INET(HOST(ipam_ipaddress.address))', [])).order_by('family', 'host')
@python_2_unicode_compatible
class IPAddress(CreatedUpdatedModel, CustomFieldModel):
"""
An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
@@ -409,7 +418,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
verbose_name = 'IP address'
verbose_name_plural = 'IP addresses'
def __unicode__(self):
def __str__(self):
return str(self.address)
def get_absolute_url(self):
@@ -469,6 +478,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
return STATUS_CHOICE_CLASSES[self.status]
@python_2_unicode_compatible
class VLANGroup(models.Model):
"""
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
@@ -486,13 +496,14 @@ class VLANGroup(models.Model):
verbose_name = 'VLAN group'
verbose_name_plural = 'VLAN groups'
def __unicode__(self):
def __str__(self):
return u'{} - {}'.format(self.site.name, self.name)
def get_absolute_url(self):
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
@python_2_unicode_compatible
class VLAN(CreatedUpdatedModel, CustomFieldModel):
"""
A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned
@@ -524,7 +535,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
verbose_name = 'VLAN'
verbose_name_plural = 'VLANs'
def __unicode__(self):
def __str__(self):
return self.display_name
def get_absolute_url(self):
@@ -558,6 +569,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
return STATUS_CHOICE_CLASSES[self.status]
@python_2_unicode_compatible
class Service(CreatedUpdatedModel):
"""
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device. A Service may optionally be tied
@@ -576,5 +588,5 @@ class Service(CreatedUpdatedModel):
ordering = ['device', 'protocol', 'port']
unique_together = ['device', 'protocol', 'port']
def __unicode__(self):
def __str__(self):
return u'{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())

View File

@@ -234,11 +234,12 @@ class PrefixBriefTable(BaseTable):
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN')
role = tables.Column(verbose_name='Role')
class Meta(BaseTable.Meta):
model = Prefix
fields = ('prefix', 'vrf', 'status', 'site', 'role')
fields = ('prefix', 'vrf', 'status', 'site', 'vlan', 'role')
orderable = False

View File

@@ -95,7 +95,6 @@ class VRFListView(ObjectListView):
filter = filters.VRFFilter
filter_form = forms.VRFFilterForm
table = tables.VRFTable
edit_permissions = ['ipam.change_vrf', 'ipam.delete_vrf']
template_name = 'ipam/vrf_list.html'
@@ -118,7 +117,7 @@ class VRFEditView(PermissionRequiredMixin, ObjectEditView):
model = VRF
form_class = forms.VRFForm
template_name = 'ipam/vrf_edit.html'
obj_list_url = 'ipam:vrf_list'
default_return_url = 'ipam:vrf_list'
class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@@ -132,21 +131,23 @@ class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.VRFImportForm
table = tables.VRFTable
template_name = 'ipam/vrf_import.html'
obj_list_url = 'ipam:vrf_list'
default_return_url = 'ipam:vrf_list'
class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_vrf'
cls = VRF
filter = filters.VRFFilter
form = forms.VRFBulkEditForm
template_name = 'ipam/vrf_bulk_edit.html'
default_redirect_url = 'ipam:vrf_list'
default_return_url = 'ipam:vrf_list'
class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vrf'
cls = VRF
default_redirect_url = 'ipam:vrf_list'
filter = filters.VRFFilter
default_return_url = 'ipam:vrf_list'
#
@@ -158,7 +159,6 @@ class RIRListView(ObjectListView):
filter = filters.RIRFilter
filter_form = forms.RIRFilterForm
table = tables.RIRTable
edit_permissions = ['ipam.change_rir', 'ipam.delete_rir']
template_name = 'ipam/rir_list.html'
def alter_queryset(self, request):
@@ -250,7 +250,8 @@ class RIREditView(PermissionRequiredMixin, ObjectEditView):
class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_rir'
cls = RIR
default_redirect_url = 'ipam:rir_list'
filter = filters.RIRFilter
default_return_url = 'ipam:rir_list'
#
@@ -264,7 +265,6 @@ class AggregateListView(ObjectListView):
filter = filters.AggregateFilter
filter_form = forms.AggregateFilterForm
table = tables.AggregateTable
edit_permissions = ['ipam.change_aggregate', 'ipam.delete_aggregate']
template_name = 'ipam/aggregate_list.html'
def extra_context(self):
@@ -308,7 +308,7 @@ class AggregateEditView(PermissionRequiredMixin, ObjectEditView):
model = Aggregate
form_class = forms.AggregateForm
template_name = 'ipam/aggregate_edit.html'
obj_list_url = 'ipam:aggregate_list'
default_return_url = 'ipam:aggregate_list'
class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@@ -322,21 +322,23 @@ class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.AggregateImportForm
table = tables.AggregateTable
template_name = 'ipam/aggregate_import.html'
obj_list_url = 'ipam:aggregate_list'
default_return_url = 'ipam:aggregate_list'
class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_aggregate'
cls = Aggregate
filter = filters.AggregateFilter
form = forms.AggregateBulkEditForm
template_name = 'ipam/aggregate_bulk_edit.html'
default_redirect_url = 'ipam:aggregate_list'
default_return_url = 'ipam:aggregate_list'
class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_aggregate'
cls = Aggregate
default_redirect_url = 'ipam:aggregate_list'
filter = filters.AggregateFilter
default_return_url = 'ipam:aggregate_list'
#
@@ -346,7 +348,6 @@ class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
class RoleListView(ObjectListView):
queryset = Role.objects.all()
table = tables.RoleTable
edit_permissions = ['ipam.change_role', 'ipam.delete_role']
template_name = 'ipam/role_list.html'
@@ -362,7 +363,7 @@ class RoleEditView(PermissionRequiredMixin, ObjectEditView):
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_role'
cls = Role
default_redirect_url = 'ipam:role_list'
default_return_url = 'ipam:role_list'
#
@@ -374,7 +375,6 @@ class PrefixListView(ObjectListView):
filter = filters.PrefixFilter
filter_form = forms.PrefixFilterForm
table = tables.PrefixTable
edit_permissions = ['ipam.change_prefix', 'ipam.delete_prefix']
template_name = 'ipam/prefix_list.html'
def alter_queryset(self, request):
@@ -401,11 +401,13 @@ def prefix(request, pk):
.filter(prefix__net_contains=str(prefix.prefix))\
.select_related('site', 'role').annotate_depth()
parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
parent_prefix_table.exclude = ('vrf',)
# Duplicate prefixes table
duplicate_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix=str(prefix.prefix)).exclude(pk=prefix.pk)\
.select_related('site', 'role')
duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes))
duplicate_prefix_table.exclude = ('vrf',)
# Child prefixes table
if prefix.vrf:
@@ -430,6 +432,7 @@ def prefix(request, pk):
'parent_prefix_table': parent_prefix_table,
'child_prefix_table': child_prefix_table,
'duplicate_prefix_table': duplicate_prefix_table,
'return_url': prefix.get_absolute_url(),
})
@@ -439,14 +442,14 @@ class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
form_class = forms.PrefixForm
template_name = 'ipam/prefix_edit.html'
fields_initial = ['vrf', 'tenant', 'site', 'prefix', 'vlan']
obj_list_url = 'ipam:prefix_list'
default_return_url = 'ipam:prefix_list'
class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_prefix'
model = Prefix
default_return_url = 'ipam:prefix_list'
template_name = 'ipam/prefix_delete.html'
default_return_url = 'ipam:prefix_list'
class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
@@ -454,21 +457,23 @@ class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.PrefixImportForm
table = tables.PrefixTable
template_name = 'ipam/prefix_import.html'
obj_list_url = 'ipam:prefix_list'
default_return_url = 'ipam:prefix_list'
class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_prefix'
cls = Prefix
filter = filters.PrefixFilter
form = forms.PrefixBulkEditForm
template_name = 'ipam/prefix_bulk_edit.html'
default_redirect_url = 'ipam:prefix_list'
default_return_url = 'ipam:prefix_list'
class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_prefix'
cls = Prefix
default_redirect_url = 'ipam:prefix_list'
filter = filters.PrefixFilter
default_return_url = 'ipam:prefix_list'
def prefix_ipaddresses(request, pk):
@@ -500,7 +505,6 @@ class IPAddressListView(ObjectListView):
filter = filters.IPAddressFilter
filter_form = forms.IPAddressFilterForm
table = tables.IPAddressTable
edit_permissions = ['ipam.change_ipaddress', 'ipam.delete_ipaddress']
template_name = 'ipam/ipaddress_list.html'
@@ -562,7 +566,7 @@ def ipaddress_assign(request, pk):
return render(request, 'ipam/ipaddress_assign.html', {
'ipaddress': ipaddress,
'form': form,
'cancel_url': reverse('ipam:ipaddress', kwargs={'pk': ipaddress.pk}),
'return_url': reverse('ipam:ipaddress', kwargs={'pk': ipaddress.pk}),
})
@@ -595,7 +599,7 @@ def ipaddress_remove(request, pk):
return render(request, 'ipam/ipaddress_unassign.html', {
'ipaddress': ipaddress,
'form': form,
'cancel_url': reverse('ipam:ipaddress', kwargs={'pk': ipaddress.pk}),
'return_url': reverse('ipam:ipaddress', kwargs={'pk': ipaddress.pk}),
})
@@ -605,7 +609,7 @@ class IPAddressEditView(PermissionRequiredMixin, ObjectEditView):
form_class = forms.IPAddressForm
fields_initial = ['address', 'vrf']
template_name = 'ipam/ipaddress_edit.html'
obj_list_url = 'ipam:ipaddress_list'
default_return_url = 'ipam:ipaddress_list'
class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@@ -619,7 +623,7 @@ class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
form = forms.IPAddressBulkAddForm
model = IPAddress
template_name = 'ipam/ipaddress_bulk_add.html'
redirect_url = 'ipam:ipaddress_list'
default_return_url = 'ipam:ipaddress_list'
class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
@@ -627,20 +631,18 @@ class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.IPAddressImportForm
table = tables.IPAddressTable
template_name = 'ipam/ipaddress_import.html'
obj_list_url = 'ipam:ipaddress_list'
default_return_url = 'ipam:ipaddress_list'
def save_obj(self, obj):
obj.save()
# Update primary IP for device if needed
# Update primary IP for device if needed. The Device must be updated directly in the database; otherwise we risk
# overwriting a previous IP assignment from the same import (see #861).
try:
if obj.family == 4 and obj.primary_ip4_for:
device = obj.primary_ip4_for
device.primary_ip4 = obj
device.save()
Device.objects.filter(pk=obj.primary_ip4_for.pk).update(primary_ip4=obj)
elif obj.family == 6 and obj.primary_ip6_for:
device = obj.primary_ip6_for
device.primary_ip6 = obj
device.save()
Device.objects.filter(pk=obj.primary_ip6_for.pk).update(primary_ip6=obj)
except Device.DoesNotExist:
pass
@@ -648,15 +650,17 @@ class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_ipaddress'
cls = IPAddress
filter = filters.IPAddressFilter
form = forms.IPAddressBulkEditForm
template_name = 'ipam/ipaddress_bulk_edit.html'
default_redirect_url = 'ipam:ipaddress_list'
default_return_url = 'ipam:ipaddress_list'
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_ipaddress'
cls = IPAddress
default_redirect_url = 'ipam:ipaddress_list'
filter = filters.IPAddressFilter
default_return_url = 'ipam:ipaddress_list'
#
@@ -668,7 +672,6 @@ class VLANGroupListView(ObjectListView):
filter = filters.VLANGroupFilter
filter_form = forms.VLANGroupFilterForm
table = tables.VLANGroupTable
edit_permissions = ['ipam.change_vlangroup', 'ipam.delete_vlangroup']
template_name = 'ipam/vlangroup_list.html'
@@ -684,7 +687,8 @@ class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vlangroup'
cls = VLANGroup
default_redirect_url = 'ipam:vlangroup_list'
filter = filters.VLANGroupFilter
default_return_url = 'ipam:vlangroup_list'
#
@@ -696,7 +700,6 @@ class VLANListView(ObjectListView):
filter = filters.VLANFilter
filter_form = forms.VLANFilterForm
table = tables.VLANTable
edit_permissions = ['ipam.change_vlan', 'ipam.delete_vlan']
template_name = 'ipam/vlan_list.html'
@@ -705,6 +708,7 @@ def vlan(request, pk):
vlan = get_object_or_404(VLAN.objects.select_related('site', 'role'), pk=pk)
prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
prefix_table = tables.PrefixBriefTable(list(prefixes))
prefix_table.exclude = ('vlan',)
return render(request, 'ipam/vlan.html', {
'vlan': vlan,
@@ -717,7 +721,7 @@ class VLANEditView(PermissionRequiredMixin, ObjectEditView):
model = VLAN
form_class = forms.VLANForm
template_name = 'ipam/vlan_edit.html'
obj_list_url = 'ipam:vlan_list'
default_return_url = 'ipam:vlan_list'
class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@@ -731,21 +735,23 @@ class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.VLANImportForm
table = tables.VLANTable
template_name = 'ipam/vlan_import.html'
obj_list_url = 'ipam:vlan_list'
default_return_url = 'ipam:vlan_list'
class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_vlan'
cls = VLAN
filter = filters.VLANFilter
form = forms.VLANBulkEditForm
template_name = 'ipam/vlan_bulk_edit.html'
default_redirect_url = 'ipam:vlan_list'
default_return_url = 'ipam:vlan_list'
class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vlan'
cls = VLAN
default_redirect_url = 'ipam:vlan_list'
filter = filters.VLANFilter
default_return_url = 'ipam:vlan_list'
#

View File

@@ -6,13 +6,13 @@ from django.contrib.messages import constants as messages
from django.core.exceptions import ImproperlyConfigured
try:
import configuration
from netbox import configuration
except ImportError:
raise ImproperlyConfigured("Configuration file is not present. Please define netbox/netbox/configuration.py per "
"the documentation.")
VERSION = '1.8.2'
VERSION = '1.8.4'
# Import local configuration
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
@@ -50,7 +50,7 @@ CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
# Attempt to import LDAP configuration if it has been defined
LDAP_IGNORE_CERT_ERRORS = False
try:
from ldap_config import *
from netbox.ldap_config import *
LDAP_CONFIGURED = True
except ImportError:
LDAP_CONFIGURED = False
@@ -189,11 +189,6 @@ REST_FRAMEWORK = {
if LOGIN_REQUIRED:
REST_FRAMEWORK['DEFAULT_PERMISSION_CLASSES'] = ('rest_framework.permissions.IsAuthenticated',)
# Swagger settings (API docs)
SWAGGER_SETTINGS = {
'base_path': '{}/{}api/docs'.format(ALLOWED_HOSTS[0], BASE_PATH),
}
# Django debug toolbar
INTERNAL_IPS = (
'127.0.0.1',

View File

@@ -2,7 +2,7 @@ from django.conf import settings
from django.conf.urls import include, url
from django.contrib import admin
from views import home, handle_500, trigger_500
from netbox.views import home, handle_500, trigger_500
from users.views import login, logout

View File

@@ -9,6 +9,14 @@ $(document).ready(function() {
$('#select_all').prop('checked', false);
}
});
// Enable hidden buttons when "select all" is checked
$('#select_all').click(function (event) {
if ($(this).is(':checked')) {
$('#select_all_box').find('button').prop('disabled', '');
} else {
$('#select_all_box').find('button').prop('disabled', 'disabled');
}
});
// Uncheck the "toggle all" checkbox if an item is unchecked
$('input:checkbox[name=pk]').click(function (event) {
if (!$(this).attr('checked')) {

View File

@@ -48,7 +48,7 @@ $(document).ready(function() {
$('#generate_keypair').click(function() {
$('#new_keypair_modal').modal('show');
$.ajax({
url: '/api/secrets/generate-keys/',
url: netbox_api_path + 'secrets/generate-keys/',
type: 'GET',
dataType: 'json',
success: function (response, status) {
@@ -75,7 +75,7 @@ $(document).ready(function() {
function unlock_secret(secret_id, private_key) {
var csrf_token = $('input[name=csrfmiddlewaretoken]').val();
$.ajax({
url: '/api/secrets/secrets/' + secret_id + '/',
url: netbox_api_path + 'secrets/secrets/' + secret_id + '/',
type: 'POST',
data: {
private_key: private_key

View File

@@ -100,6 +100,7 @@ class SecretBulkEditForm(BootstrapMixin, BulkEditForm):
class SecretFilterForm(BootstrapMixin, forms.Form):
q = forms.CharField(required=False, label='Search')
role = FilterChoiceField(queryset=SecretRole.objects.annotate(filter_count=Count('secrets')), to_field_name='slug')

View File

@@ -8,7 +8,7 @@ from django.contrib.auth.models import Group, User
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db import models
from django.utils.encoding import force_bytes
from django.utils.encoding import force_bytes, python_2_unicode_compatible
from dcim.models import Device
from utilities.models import CreatedUpdatedModel
@@ -51,6 +51,7 @@ class UserKeyQuerySet(models.QuerySet):
raise Exception("Bulk deletion has been disabled.")
@python_2_unicode_compatible
class UserKey(CreatedUpdatedModel):
"""
A UserKey stores a user's personal RSA (public) encryption key, which is used to generate their unique encrypted
@@ -76,7 +77,7 @@ class UserKey(CreatedUpdatedModel):
self.__initial_public_key = self.public_key
self.__initial_master_key_cipher = self.master_key_cipher
def __unicode__(self):
def __str__(self):
return self.user.username
def clean(self, *args, **kwargs):
@@ -170,6 +171,7 @@ class UserKey(CreatedUpdatedModel):
self.save()
@python_2_unicode_compatible
class SecretRole(models.Model):
"""
A SecretRole represents an arbitrary functional classification of Secrets. For example, a user might define roles
@@ -186,7 +188,7 @@ class SecretRole(models.Model):
class Meta:
ordering = ['name']
def __unicode__(self):
def __str__(self):
return self.name
def get_absolute_url(self):
@@ -201,6 +203,7 @@ class SecretRole(models.Model):
return user in self.users.all() or user.groups.filter(pk__in=self.groups.all()).exists()
@python_2_unicode_compatible
class Secret(CreatedUpdatedModel):
"""
A Secret stores an AES256-encrypted copy of sensitive data, such as passwords or secret keys. An irreversible
@@ -227,7 +230,7 @@ class Secret(CreatedUpdatedModel):
self.plaintext = kwargs.pop('plaintext', None)
super(Secret, self).__init__(*args, **kwargs)
def __unicode__(self):
def __str__(self):
if self.role and self.device:
return u'{} for {}'.format(self.role, self.device)
return u'Secret'

View File

@@ -1 +0,0 @@
from test_models import *

View File

@@ -22,7 +22,6 @@ from .models import SecretRole, Secret, UserKey
class SecretRoleListView(ObjectListView):
queryset = SecretRole.objects.annotate(secret_count=Count('secrets'))
table = tables.SecretRoleTable
edit_permissions = ['secrets.change_secretrole', 'secrets.delete_secretrole']
template_name = 'secrets/secretrole_list.html'
@@ -38,7 +37,7 @@ class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView):
class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'secrets.delete_secretrole'
cls = SecretRole
default_redirect_url = 'secrets:secretrole_list'
default_return_url = 'secrets:secretrole_list'
#
@@ -51,7 +50,6 @@ class SecretListView(ObjectListView):
filter = filters.SecretFilter
filter_form = forms.SecretFilterForm
table = tables.SecretTable
edit_permissions = ['secrets.change_secret', 'secrets.delete_secret']
template_name = 'secrets/secret_list.html'
@@ -103,7 +101,7 @@ def secret_add(request, pk):
return render(request, 'secrets/secret_edit.html', {
'secret': secret,
'form': form,
'cancel_url': device.get_absolute_url(),
'return_url': device.get_absolute_url(),
})
@@ -145,7 +143,7 @@ def secret_edit(request, pk):
return render(request, 'secrets/secret_edit.html', {
'secret': secret,
'form': form,
'cancel_url': reverse('secrets:secret', kwargs={'pk': secret.pk}),
'return_url': reverse('secrets:secret', kwargs={'pk': secret.pk}),
})
@@ -195,19 +193,21 @@ def secret_import(request):
return render(request, 'secrets/secret_import.html', {
'form': form,
'cancel_url': reverse('secrets:secret_list'),
'return_url': reverse('secrets:secret_list'),
})
class SecretBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'secrets.change_secret'
cls = Secret
filter = filters.SecretFilter
form = forms.SecretBulkEditForm
template_name = 'secrets/secret_bulk_edit.html'
default_redirect_url = 'secrets:secret_list'
default_return_url = 'secrets:secret_list'
class SecretBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'secrets.delete_secret'
cls = Secret
default_redirect_url = 'secrets:secret_list'
filter = filters.SecretFilter
default_return_url = 'secrets:secret_list'

View File

@@ -296,6 +296,9 @@
</div>
</div>
</footer>
<script type="text/javascript">
var netbox_api_path = "/{{ settings.BASE_PATH }}api/";
</script>
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
<script src="{% static 'jquery-ui-1.11.4/jquery-ui.min.js' %}"></script>
<script src="{% static 'bootstrap-3.3.6-dist/js/bootstrap.min.js' %}"></script>

View File

@@ -13,7 +13,7 @@
{% render_form form %}
<div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div>
</form>
</div>

View File

@@ -24,7 +24,6 @@
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>
{% endblock %}

View File

@@ -83,7 +83,7 @@
{% else %}
<button type="submit" name="_create" class="btn btn-primary">Create</button>
{% endif %}
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</form>

View File

@@ -13,7 +13,7 @@
{% render_form form %}
<div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div>
</form>
</div>

View File

@@ -23,7 +23,6 @@
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>
{% endblock %}

View File

@@ -19,7 +19,7 @@
{% render_table table 'table.html' %}
</div>
<div class="col-md-3">
{% include 'inc/filter_panel.html' %}
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@@ -40,7 +40,7 @@
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<button type="submit" name="_update" class="btn btn-primary">Connect</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</div>

View File

@@ -40,7 +40,7 @@
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<button type="submit" name="_update" class="btn btn-primary">Connect</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</div>

View File

@@ -548,9 +548,10 @@
{% block javascript %}
<script type="text/javascript">
function toggleConnection(elem, api_url) {
var url = netbox_api_path + api_url + elem.attr('data') + "/";
if (elem.hasClass('connected')) {
$.ajax({
url: api_url + elem.attr('data') + "/",
url: url,
method: 'PATCH',
dataType: 'json',
beforeSend: function(xhr, settings) {
@@ -569,7 +570,7 @@ function toggleConnection(elem, api_url) {
});
} else {
$.ajax({
url: api_url + elem.attr('data') + "/",
url: url,
method: 'PATCH',
dataType: 'json',
beforeSend: function(xhr, settings) {
@@ -590,13 +591,13 @@ function toggleConnection(elem, api_url) {
return false;
}
$(".consoleport-toggle").click(function() {
return toggleConnection($(this), "/{{ settings.BASE_PATH }}api/dcim/console-ports/");
return toggleConnection($(this), "dcim/console-ports/");
});
$(".powerport-toggle").click(function() {
return toggleConnection($(this), "/{{ settings.BASE_PATH }}api/dcim/power-ports/");
return toggleConnection($(this), "dcim/power-ports/");
});
$(".interface-toggle").click(function() {
return toggleConnection($(this), "/{{ settings.BASE_PATH }}api/dcim/interface-connections/");
return toggleConnection($(this), "dcim/interface-connections/");
});
</script>
<script src="{% static 'js/graphs.js' %}"></script>

View File

@@ -5,8 +5,8 @@
<h1>Add {{ component_name|title }}</h1>
<form action="." method="post" class="form form-horizontal">
{% csrf_token %}
{% if request.POST.redirect_url %}
<input type="hidden" name="redirect_url" value="{{ request.POST.redirect_url }}" />
{% if request.POST.return_url %}
<input type="hidden" name="return_url" value="{{ request.POST.return_url }}" />
{% endif %}
{% for field in form.hidden_fields %}
{{ field }}
@@ -51,7 +51,7 @@
<div class="form-group text-right">
<div class="col-md-12">
<button type="submit" name="_create" class="btn btn-primary">Create</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</div>

View File

@@ -34,7 +34,7 @@
<div class="col-md-9 col-md-offset-3">
<button type="submit" name="_create" class="btn btn-primary">Create</button>
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</div>

View File

@@ -13,7 +13,7 @@
{% render_form form %}
<div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div>
</form>
<h4>CSV Format</h4>

View File

@@ -13,7 +13,7 @@
{% render_form form %}
<div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div>
</form>
<h4>CSV Format</h4>

View File

@@ -24,7 +24,31 @@
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>
{% endblock %}
{% block javascript %}
<script type="text/javascript">
$(document).ready(function() {
var model_list = $('#id_device_type_id');
$('#id_manufacturer_id').change(function() {
model_list.empty();
var selected_manufacturers = $(this).val();
if (selected_manufacturers) {
var api_url = netbox_api_path + 'dcim/device-types/?manufacturer_id=' + selected_manufacturers.join('&manufacturer_id=');
$.ajax({
url: api_url,
dataType: 'json',
success: function (response, status) {
$.each(response, function (index, device_type) {
var option = $("<option></option>").attr("value", device_type.id).text(device_type["model"] + " (" + device_type["instance_count"] + ")");
model_list.append(option);
});
}
});
}
});
});
</script>
{% endblock %}

View File

@@ -37,7 +37,7 @@
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<button type="submit" name="_update" class="btn btn-primary">Save</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</div>

View File

@@ -33,7 +33,7 @@
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<button type="submit" name="_update" class="btn btn-primary">Save</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</div>

View File

@@ -19,7 +19,6 @@
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>
{% endblock %}

View File

@@ -50,7 +50,7 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button>
{% else %}
<a href="{% url 'dcim:consoleport_delete' pk=cp.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<a href="{% url 'dcim:consoleport_delete' pk=cp.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete port"></i>
</a>
{% endif %}

View File

@@ -49,7 +49,7 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button>
{% else %}
<a href="{% url 'dcim:consoleserverport_delete' pk=csp.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<a href="{% url 'dcim:consoleserverport_delete' pk=csp.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete port"></i>
</a>
{% endif %}

View File

@@ -7,12 +7,12 @@
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% if perms.dcim.add_consoleport %}<li><a href="{% url 'dcim:device_bulk_add_consoleport' %}" class="formaction">Console Ports</a></li>{% endif %}
{% if perms.dcim.add_consoleserverport %}<li><a href="{% url 'dcim:device_bulk_add_consoleserverport' %}" class="formaction">Console Server Ports</a></li>{% endif %}
{% if perms.dcim.add_powerport %}<li><a href="{% url 'dcim:device_bulk_add_powerport' %}" class="formaction">Power Ports</a></li>{% endif %}
{% if perms.dcim.add_poweroutlet %}<li><a href="{% url 'dcim:device_bulk_add_poweroutlet' %}" class="formaction">Power Outlets</a></li>{% endif %}
{% if perms.dcim.add_interface %}<li><a href="{% url 'dcim:device_bulk_add_interface' %}" class="formaction">Interfaces</a></li>{% endif %}
{% if perms.dcim.add_devicebay %}<li><a href="{% url 'dcim:device_bulk_add_devicebay' %}" class="formaction">Device Bays</a></li>{% endif %}
{% if perms.dcim.add_consoleport %}<li><a href="{% url 'dcim:device_bulk_add_consoleport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Console Ports</a></li>{% endif %}
{% if perms.dcim.add_consoleserverport %}<li><a href="{% url 'dcim:device_bulk_add_consoleserverport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Console Server Ports</a></li>{% endif %}
{% if perms.dcim.add_powerport %}<li><a href="{% url 'dcim:device_bulk_add_powerport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Power Ports</a></li>{% endif %}
{% if perms.dcim.add_poweroutlet %}<li><a href="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Power Outlets</a></li>{% endif %}
{% if perms.dcim.add_interface %}<li><a href="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Interfaces</a></li>{% endif %}
{% if perms.dcim.add_devicebay %}<li><a href="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Device Bays</a></li>{% endif %}
</ul>
</div>
{% endif %}

View File

@@ -40,7 +40,7 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button>
{% else %}
<a href="{% url 'dcim:devicebay_delete' pk=devicebay.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<a href="{% url 'dcim:devicebay_delete' pk=devicebay.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete device bay"></i>
</a>
{% endif %}

View File

@@ -85,7 +85,7 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button>
{% else %}
<a href="{% url 'dcim:interface_delete' pk=iface.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs" title="Delete interface">
<a href="{% url 'dcim:interface_delete' pk=iface.pk %}" class="btn btn-danger btn-xs" title="Delete interface">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</a>
{% endif %}

View File

@@ -49,7 +49,7 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button>
{% else %}
<a href="{% url 'dcim:poweroutlet_delete' pk=po.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<a href="{% url 'dcim:poweroutlet_delete' pk=po.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete outlet"></i>
</a>
{% endif %}

View File

@@ -50,7 +50,7 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button>
{% else %}
<a href="{% url 'dcim:powerport_delete' pk=pp.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<a href="{% url 'dcim:powerport_delete' pk=pp.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete port"></i>
</a>
{% endif %}

View File

@@ -19,7 +19,7 @@
{% render_table table 'table.html' %}
</div>
<div class="col-md-3">
{% include 'inc/filter_panel.html' %}
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@@ -86,7 +86,7 @@
<div class="form-group">
<button type="submit" name="_create" class="btn btn-primary">Connect</button>
<button type="submit" name="_addanother" class="btn btn-primary">Connect and Add Another</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</form>

View File

@@ -53,7 +53,7 @@
<div class="col-md-9 col-md-offset-3">
<button type="submit" name="_create" class="btn btn-primary">Create</button>
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</div>

View File

@@ -19,7 +19,7 @@
{% render_table table 'table.html' %}
</div>
<div class="col-md-3">
{% include 'inc/filter_panel.html' %}
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@@ -40,7 +40,7 @@
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<button type="submit" name="_update" class="btn btn-primary">Connect</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</div>

View File

@@ -40,7 +40,7 @@
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<button type="submit" name="_update" class="btn btn-primary">Connect</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</div>

View File

@@ -13,7 +13,7 @@
{% render_form form %}
<div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div>
</form>
</div>

View File

@@ -24,7 +24,6 @@
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>
{% endblock %}

View File

@@ -18,7 +18,7 @@
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackgroup_bulk_delete' %}
</div>
<div class="col-md-3">
{% include 'inc/filter_panel.html' %}
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@@ -13,7 +13,7 @@
{% render_form form %}
<div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div>
</form>
</div>

View File

@@ -23,7 +23,6 @@
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>
{% endblock %}

View File

@@ -1,32 +0,0 @@
{% load form_helpers %}
{% if filter_form %}
<div class="panel panel-default">
<div class="panel-heading">
<span class="fa fa-filter" aria-hidden="true"></span>
<strong>Filter</strong>
</div>
<div class="panel-body">
<form action="." method="get" class="form">
{% for field in filter_form %}
<div class="form-group">
{% if field|widget_type == 'checkboxinput' %}
<label for="{{ field.id_for_label }}">{{ field }} {{ field.label }}</label>
{% else %}
{{ field.label_tag }}
{{ field }}
{% endif %}
</div>
{% endfor %}
<div class="text-right">
<button type="submit" class="btn btn-primary">
<span class="fa fa-search" aria-hidden="true"></span> Apply
</button>
<a href="." class="btn btn-default">
<span class="fa fa-remove" aria-hidden="true"></span> Clear
</a>
</div>
</form>
</div>
</div>
{% endif %}

View File

@@ -1,18 +1,39 @@
{% load form_helpers %}
<div class="panel panel-default">
<div class="panel-heading">
<span class="fa fa-search" aria-hidden="true"></span>
<strong>Search</strong>
</div>
<div class="panel-body">
<form action="." method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Search" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
<span class="input-group-btn">
<form action="." method="get" class="form">
{% for field in filter_form %}
<div class="form-group">
{% if field.name == "q" %}
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Search" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</span>
</div>
{% elif field|widget_type == 'checkboxinput' %}
<label for="{{ field.id_for_label }}">{{ field }} {{ field.label }}</label>
{% else %}
{{ field.label_tag }}
{{ field }}
{% endif %}
</div>
{% endfor %}
<div class="text-right">
<button type="submit" class="btn btn-primary">
<span class="fa fa-search" aria-hidden="true"></span>
<span class="fa fa-search" aria-hidden="true"></span> Apply
</button>
</span>
</div>
<a href="." class="btn btn-default">
<span class="fa fa-remove" aria-hidden="true"></span> Clear
</a>
</div>
</form>
</div>
</div>

View File

@@ -13,7 +13,7 @@
{% render_form form %}
<div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div>
</form>
</div>

View File

@@ -27,7 +27,6 @@
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>
{% endblock %}

View File

@@ -59,7 +59,7 @@
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<button type="submit" name="_assign" class="btn btn-primary">Assign</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</div>

View File

@@ -13,7 +13,7 @@
{% render_form form %}
<div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div>
</form>
</div>

View File

@@ -1,5 +1,4 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% load helpers %}
{% block title %}IP Addresses{% endblock %}
@@ -25,7 +24,6 @@
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>
{% endblock %}

View File

@@ -13,7 +13,7 @@
{% render_form form %}
<div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div>
</form>
</div>

View File

@@ -34,7 +34,6 @@
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>
{% endblock %}

View File

@@ -33,7 +33,7 @@
{% endif %}
</div>
<div class="col-md-3">
{% include 'inc/filter_panel.html' %}
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@@ -13,7 +13,7 @@
{% render_form form %}
<div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div>
</form>
</div>

View File

@@ -25,7 +25,6 @@
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>
{% endblock %}

View File

@@ -18,7 +18,7 @@
{% include 'utilities/obj_table.html' with bulk_delete_url='ipam:vlangroup_bulk_delete' %}
</div>
<div class="col-md-3">
{% include 'inc/filter_panel.html' %}
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@@ -13,7 +13,7 @@
{% render_form form %}
<div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div>
</form>
</div>

View File

@@ -25,7 +25,6 @@
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>
{% endblock %}

View File

@@ -59,7 +59,7 @@
{% else %}
<button type="submit" name="_create" class="btn btn-primary">Create</button>
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add Another</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
{% endif %}
</div>
</div>

View File

@@ -22,7 +22,7 @@
{% render_form form %}
<div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</form>
</div>

View File

@@ -19,7 +19,6 @@
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>
{% endblock %}

View File

@@ -13,7 +13,7 @@
{% render_form form %}
<div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a>
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div>
</form>
</div>

View File

@@ -24,7 +24,6 @@
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>
{% endblock %}

View File

@@ -5,8 +5,8 @@
<h1>{% block title %}{% endblock %}</h1>
<form action="." method="post" class="form form-horizontal">
{% csrf_token %}
{% if request.POST.redirect_url %}
<input type="hidden" name="redirect_url" value="{{ request.POST.redirect_url }}" />
{% if request.POST.return_url %}
<input type="hidden" name="return_url" value="{{ request.POST.return_url }}" />
{% endif %}
{% for field in form.hidden_fields %}
{{ field }}
@@ -44,7 +44,7 @@
<div class="form-group text-right">
<div class="col-md-12">
<button type="submit" name="_apply" class="btn btn-primary">Apply</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</div>

View File

@@ -5,7 +5,7 @@
{% block message %}
<p>
Are you sure you want to delete these {{ obj_type_plural|default:"objects" }}{% if parent_obj %} from <a href="{{ parent_obj.get_absolute_url }}">{{ parent_obj }}</a>{% endif %}?
Are you sure you want to delete these {{ selected_objects|length }} {{ obj_type_plural|default:"objects" }}{% if parent_obj %} from <a href="{{ parent_obj.get_absolute_url }}">{{ parent_obj }}</a>{% endif %}?
</p>
<ul>
{% for obj in selected_objects %}

View File

@@ -23,7 +23,7 @@
</div>
<div class="text-right">
<button type="submit" name="_confirm" class="btn btn-{{ button_class|default:"danger" }}">Confirm</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</div>

View File

@@ -37,7 +37,7 @@
<button type="submit" name="_create" class="btn btn-primary">Create</button>
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add Another</button>
{% endif %}
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</form>

View File

@@ -1,29 +1,42 @@
{% load render_table from django_tables2 %}
{% load helpers %}
{% if table.model|user_can_change:request.user or table.model|user_can_delete:request.user %}
{% if permissions.change or permissions.delete %}
<form method="post" class="form form-horizontal">
{% csrf_token %}
<input type="hidden" name="redirect_url" value="{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" />
<input type="hidden" name="pk_all" value="{% for obj in table.data.queryset %}{{ obj.pk|default:'' }}{% if not forloop.last %},{% endif %}{% endfor %}" />
<input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
{% if table.paginator.num_pages > 1 %}
<div id="select_all_box" class="hidden alert alert-info">
<div class="checkbox-inline">
<label for="select_all">
<input type="checkbox" id="select_all" name="_all" />
Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> matching query
</label>
<div id="select_all_box" class="hidden panel panel-default">
<div class="panel-body">
<div class="checkbox-inline">
<label for="select_all">
<input type="checkbox" id="select_all" name="_all" />
Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> matching query
</label>
</div>
<div class="pull-right">
{% if bulk_edit_url and permissions.change %}
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled="disabled">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit All
</button>
{% endif %}
{% if bulk_delete_url and permissions.delete %}
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled="disabled">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete All
</button>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% render_table table table_template|default:'table.html' %}
{% block extra_actions %}{% endblock %}
{% if bulk_edit_url and table.model|user_can_change:request.user %}
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}" class="btn btn-warning btn-sm">
{% if bulk_edit_url and permissions.change %}
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Selected
</button>
{% endif %}
{% if bulk_delete_url and table.model|user_can_delete:request.user %}
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}" class="btn btn-danger btn-sm">
{% if bulk_delete_url and permissions.delete %}
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
</button>
{% endif %}

View File

@@ -55,5 +55,6 @@ class TenantBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Tenant
q = forms.CharField(required=False, label='Search')
group = FilterChoiceField(queryset=TenantGroup.objects.annotate(filter_count=Count('tenants')),
to_field_name='slug', null_option=(0, 'None'))

View File

@@ -1,12 +1,14 @@
from django.contrib.contenttypes.fields import GenericRelation
from django.core.urlresolvers import reverse
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from extras.models import CustomFieldModel, CustomFieldValue
from utilities.models import CreatedUpdatedModel
from utilities.utils import csv_format
@python_2_unicode_compatible
class TenantGroup(models.Model):
"""
An arbitrary collection of Tenants.
@@ -17,13 +19,14 @@ class TenantGroup(models.Model):
class Meta:
ordering = ['name']
def __unicode__(self):
def __str__(self):
return self.name
def get_absolute_url(self):
return "{}?group={}".format(reverse('tenancy:tenant_list'), self.slug)
@python_2_unicode_compatible
class Tenant(CreatedUpdatedModel, CustomFieldModel):
"""
A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal
@@ -39,7 +42,7 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel):
class Meta:
ordering = ['group', 'name']
def __unicode__(self):
def __str__(self):
return self.name
def get_absolute_url(self):

View File

@@ -10,7 +10,7 @@ from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
from models import Tenant, TenantGroup
from .models import Tenant, TenantGroup
from . import filters, forms, tables
@@ -21,7 +21,6 @@ from . import filters, forms, tables
class TenantGroupListView(ObjectListView):
queryset = TenantGroup.objects.annotate(tenant_count=Count('tenants'))
table = tables.TenantGroupTable
edit_permissions = ['tenancy.change_tenantgroup', 'tenancy.delete_tenantgroup']
template_name = 'tenancy/tenantgroup_list.html'
@@ -37,7 +36,7 @@ class TenantGroupEditView(PermissionRequiredMixin, ObjectEditView):
class TenantGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'tenancy.delete_tenantgroup'
cls = TenantGroup
default_redirect_url = 'tenancy:tenantgroup_list'
default_return_url = 'tenancy:tenantgroup_list'
#
@@ -49,7 +48,6 @@ class TenantListView(ObjectListView):
filter = filters.TenantFilter
filter_form = forms.TenantFilterForm
table = tables.TenantTable
edit_permissions = ['tenancy.change_tenant', 'tenancy.delete_tenant']
template_name = 'tenancy/tenant_list.html'
@@ -85,7 +83,7 @@ class TenantEditView(PermissionRequiredMixin, ObjectEditView):
form_class = forms.TenantForm
fields_initial = ['group']
template_name = 'tenancy/tenant_edit.html'
obj_list_url = 'tenancy:tenant_list'
default_return_url = 'tenancy:tenant_list'
class TenantDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@@ -99,18 +97,20 @@ class TenantBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.TenantImportForm
table = tables.TenantTable
template_name = 'tenancy/tenant_import.html'
obj_list_url = 'tenancy:tenant_list'
default_return_url = 'tenancy:tenant_list'
class TenantBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'tenancy.change_tenant'
cls = Tenant
filter = filters.TenantFilter
form = forms.TenantBulkEditForm
template_name = 'tenancy/tenant_bulk_edit.html'
default_redirect_url = 'tenancy:tenant_list'
default_return_url = 'tenancy:tenant_list'
class TenantBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'tenancy.delete_tenant'
cls = Tenant
default_redirect_url = 'tenancy:tenant_list'
filter = filters.TenantFilter
default_return_url = 'tenancy:tenant_list'

View File

@@ -5,20 +5,19 @@ def handle_protectederror(obj, request, e):
"""
Generate a user-friendly error message in response to a ProtectedError exception.
"""
dependent_objects = e[1]
try:
dep_class = dependent_objects[0]._meta.verbose_name_plural
dep_class = e.protected_objects[0]._meta.verbose_name_plural
except IndexError:
raise e
# Grammar for single versus multiple triggering objects
if type(obj) in (list, tuple):
err_message = "Unable to delete the requested {}. The following dependent {} were found: ".format(
err_message = u"Unable to delete the requested {}. The following dependent {} were found: ".format(
obj[0]._meta.verbose_name_plural,
dep_class,
)
else:
err_message = "Unable to delete {} {}. The following dependent {} were found: ".format(
err_message = u"Unable to delete {} {}. The following dependent {} were found: ".format(
obj._meta.verbose_name,
obj,
dep_class,
@@ -26,11 +25,11 @@ def handle_protectederror(obj, request, e):
# Append dependent objects to error message
dependent_objects = []
for o in e[1]:
for o in e.protected_objects:
if hasattr(o, 'get_absolute_url'):
dependent_objects.append('<a href="{}">{}</a>'.format(o.get_absolute_url(), str(o)))
dependent_objects.append(u'<a href="{}">{}</a>'.format(o.get_absolute_url(), o))
else:
dependent_objects.append(str(o))
err_message += ', '.join(dependent_objects)
err_message += u', '.join(dependent_objects)
messages.error(request, err_message)

View File

@@ -37,20 +37,38 @@ COLOR_CHOICES = (
('607d8b', 'Dark grey'),
('111111', 'Black'),
)
NUMERIC_EXPANSION_PATTERN = '\[(\d+-\d+)\]'
IP4_EXPANSION_PATTERN = '\[([0-9]{1,3}-[0-9]{1,3})\]'
IP6_EXPANSION_PATTERN = '\[([0-9a-f]{1,4}-[0-9a-f]{1,4})\]'
NUMERIC_EXPANSION_PATTERN = '\[((?:\d+[?:,-])+\d+)\]'
IP4_EXPANSION_PATTERN = '\[((?:[0-9]{1,3}[?:,-])+[0-9]{1,3})\]'
IP6_EXPANSION_PATTERN = '\[((?:[0-9a-f]{1,4}[?:,-])+[0-9a-f]{1,4})\]'
def parse_numeric_range(string, base=10):
"""
Expand a numeric range (continuous or not) into a decimal or
hexadecimal list, as specified by the base parameter
'0-3,5' => [0, 1, 2, 3, 5]
'2,8-b,d,f' => [2, 8, 9, a, b, d, f]
"""
values = list()
for dash_range in string.split(','):
try:
begin, end = dash_range.split('-')
except ValueError:
begin, end = dash_range, dash_range
begin, end = int(begin.strip()), int(end.strip(), base=base) + 1
values.extend(range(begin, end))
return list(set(values))
def expand_numeric_pattern(string):
"""
Expand a numeric pattern into a list of strings. Examples:
'ge-0/0/[0-3]' => ['ge-0/0/0', 'ge-0/0/1', 'ge-0/0/2', 'ge-0/0/3']
'xe-0/[0-3]/[0-7]' => ['xe-0/0/0', 'xe-0/0/1', 'xe-0/0/2', ... 'xe-0/3/5', 'xe-0/3/6', 'xe-0/3/7']
'ge-0/0/[0-3,5]' => ['ge-0/0/0', 'ge-0/0/1', 'ge-0/0/2', 'ge-0/0/3', 'ge-0/0/5']
'xe-0/[0,2-3]/[0-7]' => ['xe-0/0/0', 'xe-0/0/1', 'xe-0/0/2', ... 'xe-0/3/5', 'xe-0/3/6', 'xe-0/3/7']
"""
lead, pattern, remnant = re.split(NUMERIC_EXPANSION_PATTERN, string, maxsplit=1)
x, y = pattern.split('-')
for i in range(int(x), int(y) + 1):
parsed_range = parse_numeric_range(pattern)
for i in parsed_range:
if re.search(NUMERIC_EXPANSION_PATTERN, remnant):
for string in expand_numeric_pattern(remnant):
yield "{}{}{}".format(lead, i, string)
@@ -61,8 +79,8 @@ def expand_numeric_pattern(string):
def expand_ipaddress_pattern(string, family):
"""
Expand an IP address pattern into a list of strings. Examples:
'192.0.2.[1-254]/24' => ['192.0.2.1/24', '192.0.2.2/24', '192.0.2.3/24' ... '192.0.2.254/24']
'2001:db8:0:[0-ff]::/64' => ['2001:db8:0:0::/64', '2001:db8:0:1::/64', ... '2001:db8:0:ff::/64']
'192.0.2.[1,2,100-250,254]/24' => ['192.0.2.1/24', '192.0.2.2/24', '192.0.2.100/24' ... '192.0.2.250/24', '192.0.2.254/24']
'2001:db8:0:[0,fd-ff]::/64' => ['2001:db8:0:0::/64', '2001:db8:0:fd::/64', ... '2001:db8:0:ff::/64']
"""
if family not in [4, 6]:
raise Exception("Invalid IP address family: {}".format(family))
@@ -73,8 +91,8 @@ def expand_ipaddress_pattern(string, family):
regex = IP6_EXPANSION_PATTERN
base = 16
lead, pattern, remnant = re.split(regex, string, maxsplit=1)
x, y = pattern.split('-')
for i in range(int(x, base), int(y, base) + 1):
parsed_range = parse_numeric_range(pattern, base)
for i in parsed_range:
if re.search(regex, remnant):
for string in expand_ipaddress_pattern(remnant, family):
yield ''.join([lead, format(i, 'x' if family == 6 else 'd'), string])
@@ -218,14 +236,15 @@ class CSVDataField(forms.CharField):
if not self.help_text:
self.help_text = 'Enter one line per record in CSV format.'
def utf_8_encoder(self, unicode_csv_data):
for line in unicode_csv_data:
yield line.encode('utf-8')
def to_python(self, value):
# Return a list of dictionaries, each representing an individual record
"""
Return a list of dictionaries, each representing an individual record
"""
# Python 2's csv module has problems with Unicode
if not isinstance(value, str):
value = value.encode('utf-8')
records = []
reader = csv.reader(self.utf_8_encoder(value.splitlines()))
reader = csv.reader(value.splitlines())
for i, row in enumerate(reader, start=1):
if row:
if len(row) < len(self.columns):
@@ -234,6 +253,7 @@ class CSVDataField(forms.CharField):
elif len(row) > len(self.columns):
raise forms.ValidationError("Line {}: Too many fields (found {}; expected {})"
.format(i, len(row), len(self.columns)))
row = [col.strip() for col in row]
record = dict(zip(self.columns, row))
records.append(record)
return records
@@ -248,7 +268,7 @@ class ExpandableNameField(forms.CharField):
super(ExpandableNameField, self).__init__(*args, **kwargs)
if not self.help_text:
self.help_text = 'Numeric ranges are supported for bulk creation.<br />'\
'Example: <code>ge-0/0/[0-47]</code>'
'Example: <code>ge-0/0/[0-23,25,30]</code>'
def to_python(self, value):
if re.search(NUMERIC_EXPANSION_PATTERN, value):
@@ -265,7 +285,7 @@ class ExpandableIPAddressField(forms.CharField):
super(ExpandableIPAddressField, self).__init__(*args, **kwargs)
if not self.help_text:
self.help_text = 'Specify a numeric range to create multiple IPs.<br />'\
'Example: <code>192.0.2.[1-254]/24</code>'
'Example: <code>192.0.2.[1,5,100-254]/24</code>'
def to_python(self, value):
# Hackish address family detection but it's all we have to work with

View File

@@ -17,10 +17,6 @@ class BaseTable(tables.Table):
'class': 'table table-hover',
}
@property
def model(self):
return self._meta.model
class ToggleColumn(tables.CheckBoxColumn):

View File

@@ -44,24 +44,6 @@ def startswith(value, arg):
return str(value).startswith(arg)
@register.filter()
def user_can_add(model, user):
perm_name = '{}:add_{}'.format(model._meta.app_label, model.__class__.__name__.lower())
return user.has_perm(perm_name)
@register.filter()
def user_can_change(model, user):
perm_name = '{}:change_{}'.format(model._meta.app_label, model.__class__.__name__.lower())
return user.has_perm(perm_name)
@register.filter()
def user_can_delete(model, user):
perm_name = '{}:delete_{}'.format(model._meta.app_label, model.__class__.__name__.lower())
return user.has_perm(perm_name)
#
# Tags
#

View File

@@ -1,15 +1,26 @@
import six
def csv_format(data):
"""
Encapsulate any data which contains a comma within double quotes.
"""
csv = []
for d in data:
if d in [None, False]:
for value in data:
# Represent None or False with empty string
if value in [None, False]:
csv.append(u'')
elif type(d) not in (str, unicode):
csv.append(u'{}'.format(d))
elif u',' in d:
csv.append(u'"{}"'.format(d))
continue
# Force conversion to string first so we can check for any commas
if not isinstance(value, six.string_types):
value = u'{}'.format(value)
# Double-quote the value if it contains a comma
if u',' in value:
csv.append(u'"{}"'.format(value))
else:
csv.append(d)
csv.append(u'{}'.format(value))
return u','.join(csv)

Some files were not shown because too many files have changed in this diff Show More