mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-14 07:42:18 -06:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcf22831e2 | ||
|
|
cde6e9757b | ||
|
|
f2d9a3e0a1 | ||
|
|
b917e8d3b0 | ||
|
|
3b26ce6501 | ||
|
|
1b2d3bf08b | ||
|
|
492bc9f86e | ||
|
|
a457a73826 | ||
|
|
ac36339491 | ||
|
|
dbbf7ab664 | ||
|
|
66400a98f1 | ||
|
|
aa50e2e385 | ||
|
|
118b8db209 | ||
|
|
967feb6931 | ||
|
|
e1e41a768a | ||
|
|
c333af33dc | ||
|
|
9e5b482b1d | ||
|
|
771747147c | ||
|
|
bc49979243 | ||
|
|
d46b3e2446 | ||
|
|
2804d89c5e | ||
|
|
fd32a71131 | ||
|
|
1556fd0e92 | ||
|
|
5dce7c4e48 | ||
|
|
4bfc32ec99 | ||
|
|
ff65f7fd7b | ||
|
|
cd2aee3053 | ||
|
|
f224ad2959 | ||
|
|
9d9318f38a | ||
|
|
f43d861b50 | ||
|
|
17714b0c12 | ||
|
|
9914576eaa | ||
|
|
bf8eff11ea | ||
|
|
a6c78b99c4 | ||
|
|
6a56ffc650 | ||
|
|
05059606c5 | ||
|
|
a2ff21fab9 | ||
|
|
134370f48d | ||
|
|
c7fa610842 | ||
|
|
242cb7c7cb | ||
|
|
edb49c7f0a | ||
|
|
3e0a7e7f8a | ||
|
|
cfab9a6a0a | ||
|
|
91b5f6d799 | ||
|
|
d5488ca7da | ||
|
|
f9911bff0d | ||
|
|
d5239191fe | ||
|
|
db7148350e | ||
|
|
c51c20a301 | ||
|
|
f4485dc72a | ||
|
|
f59682a7c9 | ||
|
|
507a023f41 |
1654
CHANGELOG.md
Normal file
1654
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,10 +7,18 @@ NetBox uses [PostgreSQL](https://www.postgresql.org/) for its database, so gener
|
|||||||
|
|
||||||
## Export the Database
|
## Export the Database
|
||||||
|
|
||||||
|
Use the `pg_dump` utility to export the entire database to a file:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
pg_dump netbox > netbox.sql
|
pg_dump netbox > netbox.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When replicating a production database for development purposes, you may find it convenient to exclude changelog data, which can easily account for the bulk of a database's size. To do this, exclude the `extras_objectchange` table data from the export. The table will still be included in the output file, but will not be populated with any data.
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
pg_dump --exclude-table-data=extras_objectchange netbox > netbox.sql
|
||||||
|
```
|
||||||
|
|
||||||
## Load an Exported Database
|
## Load an Exported Database
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|||||||
@@ -12,5 +12,5 @@ While NetBox has many configuration settings, only a few of them must be defined
|
|||||||
Configuration settings may be changed at any time. However, the NetBox service must be restarted before the changes will take effect:
|
Configuration settings may be changed at any time. However, the NetBox service must be restarted before the changes will take effect:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# sudo supervsiorctl restart netbox
|
# sudo supervisorctl restart netbox
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ Close the release milestone on GitHub. Ensure that there are no remaining open i
|
|||||||
|
|
||||||
Ensure that continuous integration testing on the `develop` branch is completing successfully.
|
Ensure that continuous integration testing on the `develop` branch is completing successfully.
|
||||||
|
|
||||||
## Update VERSION
|
## Update Version and Changelog
|
||||||
|
|
||||||
Update the `VERSION` constant in `settings.py` to the new release.
|
Update the `VERSION` constant in `settings.py` to the new release version and add the current date to the release notes in `CHANGELOG.md`.
|
||||||
|
|
||||||
## Submit a Pull Request
|
## Submit a Pull Request
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https:
|
|||||||
## Option B: Apache
|
## Option B: Apache
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# apt-get install -y apache2
|
# apt-get install -y apache2 libapache2-mod-wsgi-py3
|
||||||
```
|
```
|
||||||
|
|
||||||
Once Apache is installed, proceed with the following configuration (Be sure to modify the `ServerName` appropriately):
|
Once Apache is installed, proceed with the following configuration (Be sure to modify the `ServerName` appropriately):
|
||||||
|
|||||||
Submodule netbox/_reports deleted from b3a4494377
@@ -120,10 +120,10 @@ class NestedRackRoleSerializer(WritableNestedSerializer):
|
|||||||
|
|
||||||
class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
group = NestedRackGroupSerializer(required=False, allow_null=True)
|
group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
||||||
type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False)
|
type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False, allow_null=True)
|
||||||
width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False)
|
width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False)
|
||||||
tags = TagListSerializerField(required=False)
|
tags = TagListSerializerField(required=False)
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ class NestedManufacturerSerializer(WritableNestedSerializer):
|
|||||||
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||||
manufacturer = NestedManufacturerSerializer()
|
manufacturer = NestedManufacturerSerializer()
|
||||||
interface_ordering = ChoiceField(choices=IFACE_ORDERING_CHOICES, required=False)
|
interface_ordering = ChoiceField(choices=IFACE_ORDERING_CHOICES, required=False)
|
||||||
subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False)
|
subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False, allow_null=True)
|
||||||
instance_count = serializers.IntegerField(source='instances.count', read_only=True)
|
instance_count = serializers.IntegerField(source='instances.count', read_only=True)
|
||||||
tags = TagListSerializerField(required=False)
|
tags = TagListSerializerField(required=False)
|
||||||
|
|
||||||
@@ -396,7 +396,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
|||||||
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
rack = NestedRackSerializer(required=False, allow_null=True)
|
rack = NestedRackSerializer(required=False, allow_null=True)
|
||||||
face = ChoiceField(choices=RACK_FACE_CHOICES, required=False)
|
face = ChoiceField(choices=RACK_FACE_CHOICES, required=False, allow_null=True)
|
||||||
status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False)
|
status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False)
|
||||||
primary_ip = DeviceIPAddressSerializer(read_only=True)
|
primary_ip = DeviceIPAddressSerializer(read_only=True)
|
||||||
primary_ip4 = DeviceIPAddressSerializer(required=False, allow_null=True)
|
primary_ip4 = DeviceIPAddressSerializer(required=False, allow_null=True)
|
||||||
@@ -576,7 +576,7 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
|||||||
is_connected = serializers.SerializerMethodField(read_only=True)
|
is_connected = serializers.SerializerMethodField(read_only=True)
|
||||||
interface_connection = serializers.SerializerMethodField(read_only=True)
|
interface_connection = serializers.SerializerMethodField(read_only=True)
|
||||||
circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True)
|
circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True)
|
||||||
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False)
|
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
|
||||||
untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
|
untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
|
||||||
tagged_vlans = SerializedPKRelatedField(
|
tagged_vlans = SerializedPKRelatedField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
@@ -666,7 +666,7 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
|
|||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
# Provide a default value to satisfy UniqueTogetherValidator
|
# Provide a default value to satisfy UniqueTogetherValidator
|
||||||
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
||||||
manufacturer = NestedManufacturerSerializer()
|
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
|
||||||
tags = TagListSerializerField(required=False)
|
tags = TagListSerializerField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -93,6 +93,11 @@ IFACE_FF_STACKWISE_PLUS = 5050
|
|||||||
IFACE_FF_FLEXSTACK = 5100
|
IFACE_FF_FLEXSTACK = 5100
|
||||||
IFACE_FF_FLEXSTACK_PLUS = 5150
|
IFACE_FF_FLEXSTACK_PLUS = 5150
|
||||||
IFACE_FF_JUNIPER_VCP = 5200
|
IFACE_FF_JUNIPER_VCP = 5200
|
||||||
|
IFACE_FF_SUMMITSTACK = 5300
|
||||||
|
IFACE_FF_SUMMITSTACK128 = 5310
|
||||||
|
IFACE_FF_SUMMITSTACK256 = 5320
|
||||||
|
IFACE_FF_SUMMITSTACK512 = 5330
|
||||||
|
|
||||||
# Other
|
# Other
|
||||||
IFACE_FF_OTHER = 32767
|
IFACE_FF_OTHER = 32767
|
||||||
|
|
||||||
@@ -168,6 +173,10 @@ IFACE_FF_CHOICES = [
|
|||||||
[IFACE_FF_FLEXSTACK, 'Cisco FlexStack'],
|
[IFACE_FF_FLEXSTACK, 'Cisco FlexStack'],
|
||||||
[IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
|
[IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
|
||||||
[IFACE_FF_JUNIPER_VCP, 'Juniper VCP'],
|
[IFACE_FF_JUNIPER_VCP, 'Juniper VCP'],
|
||||||
|
[IFACE_FF_SUMMITSTACK, 'Extreme SummitStack'],
|
||||||
|
[IFACE_FF_SUMMITSTACK128, 'Extreme SummitStack-128'],
|
||||||
|
[IFACE_FF_SUMMITSTACK256, 'Extreme SummitStack-256'],
|
||||||
|
[IFACE_FF_SUMMITSTACK512, 'Extreme SummitStack-512'],
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -112,6 +112,10 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class RackGroupFilter(django_filters.FilterSet):
|
class RackGroupFilter(django_filters.FilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
@@ -127,6 +131,15 @@ class RackGroupFilter(django_filters.FilterSet):
|
|||||||
model = RackGroup
|
model = RackGroup
|
||||||
fields = ['site_id', 'name', 'slug']
|
fields = ['site_id', 'name', 'slug']
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
qs_filter = (
|
||||||
|
Q(name__icontains=value) |
|
||||||
|
Q(slug__icontains=value)
|
||||||
|
)
|
||||||
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
class RackRoleFilter(django_filters.FilterSet):
|
class RackRoleFilter(django_filters.FilterSet):
|
||||||
|
|
||||||
|
|||||||
@@ -1795,7 +1795,7 @@ class InterfaceAssignVLANsForm(BootstrapMixin, forms.ModelForm):
|
|||||||
# Compile VLAN choices
|
# Compile VLAN choices
|
||||||
vlan_choices = []
|
vlan_choices = []
|
||||||
|
|
||||||
# Add global VLANs
|
# Add non-grouped global VLANs
|
||||||
global_vlans = VLAN.objects.filter(site=None, group=None).exclude(pk__in=assigned_vlans)
|
global_vlans = VLAN.objects.filter(site=None, group=None).exclude(pk__in=assigned_vlans)
|
||||||
vlan_choices.append((
|
vlan_choices.append((
|
||||||
'Global', [(vlan.pk, vlan) for vlan in global_vlans])
|
'Global', [(vlan.pk, vlan) for vlan in global_vlans])
|
||||||
@@ -1808,16 +1808,15 @@ class InterfaceAssignVLANsForm(BootstrapMixin, forms.ModelForm):
|
|||||||
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
|
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
|
||||||
)
|
)
|
||||||
|
|
||||||
parent = self.instance.parent
|
site = getattr(self.instance.parent, 'site', None)
|
||||||
if parent is not None:
|
if site is not None:
|
||||||
|
|
||||||
# Add site VLANs
|
# Add non-grouped site VLANs
|
||||||
if parent.site:
|
site_vlans = VLAN.objects.filter(site=site, group=None).exclude(pk__in=assigned_vlans)
|
||||||
site_vlans = VLAN.objects.filter(site=parent.site, group=None).exclude(pk__in=assigned_vlans)
|
vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
|
||||||
vlan_choices.append((parent.site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
|
|
||||||
|
|
||||||
# Add grouped site VLANs
|
# Add grouped site VLANs
|
||||||
for group in VLANGroup.objects.filter(site=parent.site):
|
for group in VLANGroup.objects.filter(site=site):
|
||||||
site_group_vlans = VLAN.objects.filter(group=group).exclude(pk__in=assigned_vlans)
|
site_group_vlans = VLAN.objects.filter(group=group).exclude(pk__in=assigned_vlans)
|
||||||
vlan_choices.append((
|
vlan_choices.append((
|
||||||
'{} / {}'.format(group.site.name, group.name),
|
'{} / {}'.format(group.site.name, group.name),
|
||||||
|
|||||||
29
netbox/dcim/migrations/0062_interface_mtu.py
Normal file
29
netbox/dcim/migrations/0062_interface_mtu.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 2.0.8 on 2018-08-22 14:23
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0061_platform_napalm_args'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interface',
|
||||||
|
name='mtu',
|
||||||
|
field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65536)], verbose_name='MTU'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interface',
|
||||||
|
name='form_factor',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interfacetemplate',
|
||||||
|
name='form_factor',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -7,7 +7,7 @@ from django.conf import settings
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.contrib.postgres.fields import ArrayField, JSONField
|
from django.contrib.postgres.fields import ArrayField, JSONField
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Count, Q, ObjectDoesNotExist
|
from django.db.models import Count, Q, ObjectDoesNotExist
|
||||||
@@ -1809,9 +1809,10 @@ class Interface(ComponentModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
verbose_name='MAC Address'
|
verbose_name='MAC Address'
|
||||||
)
|
)
|
||||||
mtu = models.PositiveSmallIntegerField(
|
mtu = models.PositiveIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
|
validators=[MinValueValidator(1), MaxValueValidator(65536)],
|
||||||
verbose_name='MTU'
|
verbose_name='MTU'
|
||||||
)
|
)
|
||||||
mgmt_only = models.BooleanField(
|
mgmt_only = models.BooleanField(
|
||||||
@@ -1933,11 +1934,20 @@ class Interface(ComponentModel):
|
|||||||
"""
|
"""
|
||||||
Include the connected Interface (if any).
|
Include the connected Interface (if any).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# It's possible that an Interface can be deleted _after_ its parent Device/VM, in which case trying to resolve
|
||||||
|
# the component parent will raise DoesNotExist. For more discussion, see
|
||||||
|
# https://github.com/digitalocean/netbox/issues/2323
|
||||||
|
try:
|
||||||
|
parent_obj = self.get_component_parent()
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
parent_obj = None
|
||||||
|
|
||||||
ObjectChange(
|
ObjectChange(
|
||||||
user=user,
|
user=user,
|
||||||
request_id=request_id,
|
request_id=request_id,
|
||||||
changed_object=self,
|
changed_object=self,
|
||||||
related_object=self.get_component_parent(),
|
related_object=parent_obj,
|
||||||
action=action,
|
action=action,
|
||||||
object_data=serialize_object(self, extra={
|
object_data=serialize_object(self, extra={
|
||||||
'connected_interface': self.connected_interface.pk if self.connection else None,
|
'connected_interface': self.connected_interface.pk if self.connection else None,
|
||||||
@@ -2062,6 +2072,7 @@ class InterfaceConnection(models.Model):
|
|||||||
(self.interface_a, self.interface_b),
|
(self.interface_a, self.interface_b),
|
||||||
(self.interface_b, self.interface_a),
|
(self.interface_b, self.interface_a),
|
||||||
)
|
)
|
||||||
|
|
||||||
for interface, peer_interface in interfaces:
|
for interface, peer_interface in interfaces:
|
||||||
if action == OBJECTCHANGE_ACTION_DELETE:
|
if action == OBJECTCHANGE_ACTION_DELETE:
|
||||||
connection_data = {
|
connection_data = {
|
||||||
@@ -2072,11 +2083,17 @@ class InterfaceConnection(models.Model):
|
|||||||
'connected_interface': peer_interface.pk,
|
'connected_interface': peer_interface.pk,
|
||||||
'connection_status': self.connection_status
|
'connection_status': self.connection_status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
parent_obj = interface.parent
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
parent_obj = None
|
||||||
|
|
||||||
ObjectChange(
|
ObjectChange(
|
||||||
user=user,
|
user=user,
|
||||||
request_id=request_id,
|
request_id=request_id,
|
||||||
changed_object=interface,
|
changed_object=interface,
|
||||||
related_object=interface.parent,
|
related_object=parent_obj,
|
||||||
action=OBJECTCHANGE_ACTION_UPDATE,
|
action=OBJECTCHANGE_ACTION_UPDATE,
|
||||||
object_data=serialize_object(interface, extra=connection_data)
|
object_data=serialize_object(interface, extra=connection_data)
|
||||||
).save()
|
).save()
|
||||||
|
|||||||
@@ -4,12 +4,9 @@ from django import forms
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
from netbox.admin import admin_site
|
||||||
from utilities.forms import LaxURLField
|
from utilities.forms import LaxURLField
|
||||||
from .constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
|
from .models import CustomField, CustomFieldChoice, Graph, ExportTemplate, TopologyMap, UserAction, Webhook
|
||||||
from .models import (
|
|
||||||
ConfigContext, CustomField, CustomFieldChoice, Graph, ExportTemplate, ObjectChange, TopologyMap, UserAction,
|
|
||||||
Webhook,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def order_content_types(field):
|
def order_content_types(field):
|
||||||
@@ -39,7 +36,7 @@ class WebhookForm(forms.ModelForm):
|
|||||||
order_content_types(self.fields['obj_type'])
|
order_content_types(self.fields['obj_type'])
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Webhook)
|
@admin.register(Webhook, site=admin_site)
|
||||||
class WebhookAdmin(admin.ModelAdmin):
|
class WebhookAdmin(admin.ModelAdmin):
|
||||||
list_display = [
|
list_display = [
|
||||||
'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update',
|
'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update',
|
||||||
@@ -72,7 +69,7 @@ class CustomFieldChoiceAdmin(admin.TabularInline):
|
|||||||
extra = 5
|
extra = 5
|
||||||
|
|
||||||
|
|
||||||
@admin.register(CustomField)
|
@admin.register(CustomField, site=admin_site)
|
||||||
class CustomFieldAdmin(admin.ModelAdmin):
|
class CustomFieldAdmin(admin.ModelAdmin):
|
||||||
inlines = [CustomFieldChoiceAdmin]
|
inlines = [CustomFieldChoiceAdmin]
|
||||||
list_display = ['name', 'models', 'type', 'required', 'filter_logic', 'default', 'weight', 'description']
|
list_display = ['name', 'models', 'type', 'required', 'filter_logic', 'default', 'weight', 'description']
|
||||||
@@ -86,7 +83,7 @@ class CustomFieldAdmin(admin.ModelAdmin):
|
|||||||
# Graphs
|
# Graphs
|
||||||
#
|
#
|
||||||
|
|
||||||
@admin.register(Graph)
|
@admin.register(Graph, site=admin_site)
|
||||||
class GraphAdmin(admin.ModelAdmin):
|
class GraphAdmin(admin.ModelAdmin):
|
||||||
list_display = ['name', 'type', 'weight', 'source']
|
list_display = ['name', 'type', 'weight', 'source']
|
||||||
|
|
||||||
@@ -109,7 +106,7 @@ class ExportTemplateForm(forms.ModelForm):
|
|||||||
self.fields['content_type'].choices.insert(0, ('', '---------'))
|
self.fields['content_type'].choices.insert(0, ('', '---------'))
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ExportTemplate)
|
@admin.register(ExportTemplate, site=admin_site)
|
||||||
class ExportTemplateAdmin(admin.ModelAdmin):
|
class ExportTemplateAdmin(admin.ModelAdmin):
|
||||||
list_display = ['name', 'content_type', 'description', 'mime_type', 'file_extension']
|
list_display = ['name', 'content_type', 'description', 'mime_type', 'file_extension']
|
||||||
form = ExportTemplateForm
|
form = ExportTemplateForm
|
||||||
@@ -119,7 +116,7 @@ class ExportTemplateAdmin(admin.ModelAdmin):
|
|||||||
# Topology maps
|
# Topology maps
|
||||||
#
|
#
|
||||||
|
|
||||||
@admin.register(TopologyMap)
|
@admin.register(TopologyMap, site=admin_site)
|
||||||
class TopologyMapAdmin(admin.ModelAdmin):
|
class TopologyMapAdmin(admin.ModelAdmin):
|
||||||
list_display = ['name', 'slug', 'site']
|
list_display = ['name', 'slug', 'site']
|
||||||
prepopulated_fields = {
|
prepopulated_fields = {
|
||||||
@@ -131,7 +128,7 @@ class TopologyMapAdmin(admin.ModelAdmin):
|
|||||||
# User actions
|
# User actions
|
||||||
#
|
#
|
||||||
|
|
||||||
@admin.register(UserAction)
|
@admin.register(UserAction, site=admin_site)
|
||||||
class UserActionAdmin(admin.ModelAdmin):
|
class UserActionAdmin(admin.ModelAdmin):
|
||||||
actions = None
|
actions = None
|
||||||
list_display = ['user', 'action', 'content_type', 'object_id', '_message']
|
list_display = ['user', 'action', 'content_type', 'object_id', '_message']
|
||||||
|
|||||||
@@ -138,8 +138,11 @@ class ImageAttachmentViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ConfigContextViewSet(ModelViewSet):
|
class ConfigContextViewSet(ModelViewSet):
|
||||||
queryset = ConfigContext.objects.prefetch_related('regions', 'sites', 'roles', 'platforms', 'tenants')
|
queryset = ConfigContext.objects.prefetch_related(
|
||||||
|
'regions', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
|
||||||
|
)
|
||||||
serializer_class = serializers.ConfigContextSerializer
|
serializer_class = serializers.ConfigContextSerializer
|
||||||
|
filter_class = filters.ConfigContextFilter
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from taggit.models import Tag
|
from taggit.models import Tag
|
||||||
|
|
||||||
from dcim.models import Site
|
from dcim.models import DeviceRole, Platform, Region, Site
|
||||||
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
|
from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
|
||||||
from .models import CustomField, Graph, ExportTemplate, ObjectChange, TopologyMap, UserAction
|
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, TopologyMap, UserAction
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldFilter(django_filters.Filter):
|
class CustomFieldFilter(django_filters.Filter):
|
||||||
@@ -124,6 +125,92 @@ class TopologyMapFilter(django_filters.FilterSet):
|
|||||||
fields = ['name', 'slug']
|
fields = ['name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigContextFilter(django_filters.FilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
|
region_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='regions',
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
label='Region',
|
||||||
|
)
|
||||||
|
region = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='regions__slug',
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Region (slug)',
|
||||||
|
)
|
||||||
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='sites',
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
label='Site',
|
||||||
|
)
|
||||||
|
site = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='sites__slug',
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Site (slug)',
|
||||||
|
)
|
||||||
|
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='roles',
|
||||||
|
queryset=DeviceRole.objects.all(),
|
||||||
|
label='Role',
|
||||||
|
)
|
||||||
|
role = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='roles__slug',
|
||||||
|
queryset=DeviceRole.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Role (slug)',
|
||||||
|
)
|
||||||
|
platform_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='platforms',
|
||||||
|
queryset=Platform.objects.all(),
|
||||||
|
label='Platform',
|
||||||
|
)
|
||||||
|
platform = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='platforms__slug',
|
||||||
|
queryset=Platform.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Platform (slug)',
|
||||||
|
)
|
||||||
|
tenant_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='tenant_groups',
|
||||||
|
queryset=TenantGroup.objects.all(),
|
||||||
|
label='Tenant group',
|
||||||
|
)
|
||||||
|
tenant_group = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='tenant_groups__slug',
|
||||||
|
queryset=TenantGroup.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Tenant group (slug)',
|
||||||
|
)
|
||||||
|
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='tenants',
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
label='Tenant',
|
||||||
|
)
|
||||||
|
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='tenants__slug',
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Tenant (slug)',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ConfigContext
|
||||||
|
fields = ['name', 'is_active']
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(name__icontains=value) |
|
||||||
|
Q(description__icontains=value) |
|
||||||
|
Q(data__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ObjectChangeFilter(django_filters.FilterSet):
|
class ObjectChangeFilter(django_filters.FilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
|
|||||||
@@ -10,8 +10,12 @@ from mptt.forms import TreeNodeMultipleChoiceField
|
|||||||
from taggit.forms import TagField
|
from taggit.forms import TagField
|
||||||
from taggit.models import Tag
|
from taggit.models import Tag
|
||||||
|
|
||||||
from dcim.models import Region
|
from dcim.models import DeviceRole, Platform, Region, Site
|
||||||
from utilities.forms import add_blank_choice, BootstrapMixin, BulkEditForm, LaxURLField, JSONField, SlugField
|
from tenancy.models import Tenant, TenantGroup
|
||||||
|
from utilities.forms import (
|
||||||
|
add_blank_choice, BootstrapMixin, BulkEditForm, FilterChoiceField, FilterTreeNodeMultipleChoiceField, LaxURLField,
|
||||||
|
JSONField, SlugField,
|
||||||
|
)
|
||||||
from .constants import (
|
from .constants import (
|
||||||
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
|
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
|
||||||
OBJECTCHANGE_ACTION_CHOICES,
|
OBJECTCHANGE_ACTION_CHOICES,
|
||||||
@@ -223,6 +227,37 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||||
|
q = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label='Search'
|
||||||
|
)
|
||||||
|
region = FilterTreeNodeMultipleChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
to_field_name='slug'
|
||||||
|
)
|
||||||
|
site = FilterChoiceField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
to_field_name='slug'
|
||||||
|
)
|
||||||
|
role = FilterChoiceField(
|
||||||
|
queryset=DeviceRole.objects.all(),
|
||||||
|
to_field_name='slug'
|
||||||
|
)
|
||||||
|
platform = FilterChoiceField(
|
||||||
|
queryset=Platform.objects.all(),
|
||||||
|
to_field_name='slug'
|
||||||
|
)
|
||||||
|
tenant_group = FilterChoiceField(
|
||||||
|
queryset=TenantGroup.objects.all(),
|
||||||
|
to_field_name='slug'
|
||||||
|
)
|
||||||
|
tenant = FilterChoiceField(
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
to_field_name='slug'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Image attachments
|
# Image attachments
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
import importlib
|
import importlib
|
||||||
import inspect
|
import inspect
|
||||||
import pkgutil
|
import pkgutil
|
||||||
from collections import OrderedDict
|
import sys
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -23,10 +24,29 @@ def get_report(module_name, report_name):
|
|||||||
"""
|
"""
|
||||||
Return a specific report from within a module.
|
Return a specific report from within a module.
|
||||||
"""
|
"""
|
||||||
module = importlib.import_module(module_name)
|
file_path = '{}/{}.py'.format(settings.REPORTS_ROOT, module_name)
|
||||||
|
|
||||||
|
# Python 3.5+
|
||||||
|
if sys.version_info >= (3, 5):
|
||||||
|
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
try:
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Python 2.7
|
||||||
|
else:
|
||||||
|
import imp
|
||||||
|
try:
|
||||||
|
module = imp.load_source(module_name, file_path)
|
||||||
|
except IOError:
|
||||||
|
return None
|
||||||
|
|
||||||
report = getattr(module, report_name, None)
|
report = getattr(module, report_name, None)
|
||||||
if report is None:
|
if report is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return report()
|
return report()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -72,15 +72,10 @@ class ConfigContextTable(BaseTable):
|
|||||||
is_active = BooleanColumn(
|
is_active = BooleanColumn(
|
||||||
verbose_name='Active'
|
verbose_name='Active'
|
||||||
)
|
)
|
||||||
actions = tables.TemplateColumn(
|
|
||||||
template_code=CONFIGCONTEXT_ACTIONS,
|
|
||||||
attrs={'td': {'class': 'text-right'}},
|
|
||||||
verbose_name=''
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = ConfigContext
|
model = ConfigContext
|
||||||
fields = ('pk', 'name', 'weight', 'is_active', 'description', 'actions')
|
fields = ('pk', 'name', 'weight', 'is_active', 'description')
|
||||||
|
|
||||||
|
|
||||||
class ObjectChangeTable(BaseTable):
|
class ObjectChangeTable(BaseTable):
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from taggit.models import Tag
|
|||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.views import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView
|
from utilities.views import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView
|
||||||
from . import filters
|
from . import filters
|
||||||
from .forms import ConfigContextForm, ImageAttachmentForm, ObjectChangeFilterForm, TagForm
|
from .forms import ConfigContextForm, ConfigContextFilterForm, ImageAttachmentForm, ObjectChangeFilterForm, TagForm
|
||||||
from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult
|
from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult
|
||||||
from .reports import get_report, get_reports
|
from .reports import get_report, get_reports
|
||||||
from .tables import ConfigContextTable, ObjectChangeTable, TagTable
|
from .tables import ConfigContextTable, ObjectChangeTable, TagTable
|
||||||
@@ -56,6 +56,8 @@ class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
|
|
||||||
class ConfigContextListView(ObjectListView):
|
class ConfigContextListView(ObjectListView):
|
||||||
queryset = ConfigContext.objects.all()
|
queryset = ConfigContext.objects.all()
|
||||||
|
filter = filters.ConfigContextFilter
|
||||||
|
filter_form = ConfigContextFilterForm
|
||||||
table = ConfigContextTable
|
table = ConfigContextTable
|
||||||
template_name = 'extras/configcontext_list.html'
|
template_name = 'extras/configcontext_list.html'
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import datetime
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Q
|
|
||||||
|
|
||||||
from extras.models import Webhook
|
from extras.models import Webhook
|
||||||
from extras.constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
|
from extras.constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
|
||||||
@@ -18,23 +17,16 @@ def enqueue_webhooks(instance, action):
|
|||||||
if not settings.WEBHOOKS_ENABLED or instance._meta.model_name not in WEBHOOK_MODELS:
|
if not settings.WEBHOOKS_ENABLED or instance._meta.model_name not in WEBHOOK_MODELS:
|
||||||
return
|
return
|
||||||
|
|
||||||
type_create = action == OBJECTCHANGE_ACTION_CREATE
|
# Retrieve any applicable Webhooks
|
||||||
type_update = action == OBJECTCHANGE_ACTION_UPDATE
|
action_flag = {
|
||||||
type_delete = action == OBJECTCHANGE_ACTION_DELETE
|
OBJECTCHANGE_ACTION_CREATE: 'type_create',
|
||||||
|
OBJECTCHANGE_ACTION_UPDATE: 'type_update',
|
||||||
# Find assigned webhooks
|
OBJECTCHANGE_ACTION_DELETE: 'type_delete',
|
||||||
|
}[action]
|
||||||
obj_type = ContentType.objects.get_for_model(instance.__class__)
|
obj_type = ContentType.objects.get_for_model(instance.__class__)
|
||||||
webhooks = Webhook.objects.filter(
|
webhooks = Webhook.objects.filter(obj_type=obj_type, enabled=True, **{action_flag: True})
|
||||||
Q(enabled=True) &
|
|
||||||
(
|
|
||||||
Q(type_create=type_create) |
|
|
||||||
Q(type_update=type_update) |
|
|
||||||
Q(type_delete=type_delete)
|
|
||||||
) &
|
|
||||||
Q(obj_type=obj_type)
|
|
||||||
)
|
|
||||||
|
|
||||||
if webhooks:
|
if webhooks.exists():
|
||||||
# Get the Model's API serializer class and serialize the object
|
# Get the Model's API serializer class and serialize the object
|
||||||
serializer_class = get_serializer_for_model(instance.__class__)
|
serializer_class = get_serializer_for_model(instance.__class__)
|
||||||
serializer_context = {
|
serializer_context = {
|
||||||
|
|||||||
@@ -37,8 +37,12 @@ def process_webhook(webhook, data, model_class, event, timestamp):
|
|||||||
prepared_request = requests.Request(**params).prepare()
|
prepared_request = requests.Request(**params).prepare()
|
||||||
|
|
||||||
if webhook.secret != '':
|
if webhook.secret != '':
|
||||||
# sign the request with the secret
|
# Sign the request with a hash of the secret key and its content.
|
||||||
hmac_prep = hmac.new(bytearray(webhook.secret, 'utf8'), prepared_request.body, digestmod=hashlib.sha512)
|
hmac_prep = hmac.new(
|
||||||
|
key=webhook.secret.encode('utf8'),
|
||||||
|
msg=prepared_request.body.encode('utf8'),
|
||||||
|
digestmod=hashlib.sha512
|
||||||
|
)
|
||||||
prepared_request.headers['X-Hook-Signature'] = hmac_prep.hexdigest()
|
prepared_request.headers['X-Hook-Signature'] = hmac_prep.hexdigest()
|
||||||
|
|
||||||
with requests.Session() as session:
|
with requests.Session() as session:
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
|||||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
status = ChoiceField(choices=IPADDRESS_STATUS_CHOICES, required=False)
|
status = ChoiceField(choices=IPADDRESS_STATUS_CHOICES, required=False)
|
||||||
role = ChoiceField(choices=IPADDRESS_ROLE_CHOICES, required=False)
|
role = ChoiceField(choices=IPADDRESS_ROLE_CHOICES, required=False, allow_null=True)
|
||||||
interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
|
interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
|
||||||
tags = TagListSerializerField(required=False)
|
tags = TagListSerializerField(required=False)
|
||||||
|
|
||||||
|
|||||||
@@ -140,10 +140,11 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
|||||||
available_prefixes.remove(allocated_prefix)
|
available_prefixes.remove(allocated_prefix)
|
||||||
|
|
||||||
# Initialize the serializer with a list or a single object depending on what was requested
|
# Initialize the serializer with a list or a single object depending on what was requested
|
||||||
|
context = {'request': request}
|
||||||
if isinstance(request.data, list):
|
if isinstance(request.data, list):
|
||||||
serializer = serializers.PrefixSerializer(data=requested_prefixes, many=True)
|
serializer = serializers.PrefixSerializer(data=requested_prefixes, many=True, context=context)
|
||||||
else:
|
else:
|
||||||
serializer = serializers.PrefixSerializer(data=requested_prefixes[0])
|
serializer = serializers.PrefixSerializer(data=requested_prefixes[0], context=context)
|
||||||
|
|
||||||
# Create the new Prefix(es)
|
# Create the new Prefix(es)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
@@ -199,10 +200,11 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
|||||||
requested_ip['vrf'] = prefix.vrf.pk if prefix.vrf else None
|
requested_ip['vrf'] = prefix.vrf.pk if prefix.vrf else None
|
||||||
|
|
||||||
# Initialize the serializer with a list or a single object depending on what was requested
|
# Initialize the serializer with a list or a single object depending on what was requested
|
||||||
|
context = {'request': request}
|
||||||
if isinstance(request.data, list):
|
if isinstance(request.data, list):
|
||||||
serializer = serializers.IPAddressSerializer(data=requested_ips, many=True)
|
serializer = serializers.IPAddressSerializer(data=requested_ips, many=True, context=context)
|
||||||
else:
|
else:
|
||||||
serializer = serializers.IPAddressSerializer(data=requested_ips[0])
|
serializer = serializers.IPAddressSerializer(data=requested_ips[0], context=context)
|
||||||
|
|
||||||
# Create the new IP address(es)
|
# Create the new IP address(es)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
|
|||||||
@@ -494,7 +494,8 @@ class PrefixTest(APITestCase):
|
|||||||
|
|
||||||
def test_create_single_available_prefix(self):
|
def test_create_single_available_prefix(self):
|
||||||
|
|
||||||
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/28'), is_pool=True)
|
vrf = VRF.objects.create(name='Test VRF 1', rd='1234')
|
||||||
|
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/28'), vrf=vrf, is_pool=True)
|
||||||
url = reverse('ipam-api:prefix-available-prefixes', kwargs={'pk': prefix.pk})
|
url = reverse('ipam-api:prefix-available-prefixes', kwargs={'pk': prefix.pk})
|
||||||
|
|
||||||
# Create four available prefixes with individual requests
|
# Create four available prefixes with individual requests
|
||||||
@@ -512,6 +513,7 @@ class PrefixTest(APITestCase):
|
|||||||
response = self.client.post(url, data, format='json', **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(response.data['prefix'], prefixes_to_be_created[i])
|
self.assertEqual(response.data['prefix'], prefixes_to_be_created[i])
|
||||||
|
self.assertEqual(response.data['vrf']['id'], vrf.pk)
|
||||||
self.assertEqual(response.data['description'], data['description'])
|
self.assertEqual(response.data['description'], data['description'])
|
||||||
|
|
||||||
# Try to create one more prefix
|
# Try to create one more prefix
|
||||||
@@ -562,7 +564,8 @@ class PrefixTest(APITestCase):
|
|||||||
|
|
||||||
def test_create_single_available_ip(self):
|
def test_create_single_available_ip(self):
|
||||||
|
|
||||||
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/30'), is_pool=True)
|
vrf = VRF.objects.create(name='Test VRF 1', rd='1234')
|
||||||
|
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/30'), vrf=vrf, is_pool=True)
|
||||||
url = reverse('ipam-api:prefix-available-ips', kwargs={'pk': prefix.pk})
|
url = reverse('ipam-api:prefix-available-ips', kwargs={'pk': prefix.pk})
|
||||||
|
|
||||||
# Create all four available IPs with individual requests
|
# Create all four available IPs with individual requests
|
||||||
@@ -572,6 +575,7 @@ class PrefixTest(APITestCase):
|
|||||||
}
|
}
|
||||||
response = self.client.post(url, data, format='json', **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(response.data['vrf']['id'], vrf.pk)
|
||||||
self.assertEqual(response.data['description'], data['description'])
|
self.assertEqual(response.data['description'], data['description'])
|
||||||
|
|
||||||
# Try to create one more IP
|
# Try to create one more IP
|
||||||
|
|||||||
30
netbox/netbox/admin.py
Normal file
30
netbox/netbox/admin.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.admin import AdminSite
|
||||||
|
from django.contrib.auth.models import Group, User
|
||||||
|
from django.contrib.auth.admin import GroupAdmin, UserAdmin
|
||||||
|
from taggit.admin import TagAdmin
|
||||||
|
from taggit.models import Tag
|
||||||
|
|
||||||
|
|
||||||
|
class NetBoxAdminSite(AdminSite):
|
||||||
|
"""
|
||||||
|
Custom admin site
|
||||||
|
"""
|
||||||
|
site_header = 'NetBox Administration'
|
||||||
|
site_title = 'NetBox'
|
||||||
|
site_url = '/{}'.format(settings.BASE_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
admin_site = NetBoxAdminSite(name='admin')
|
||||||
|
|
||||||
|
# Register external models
|
||||||
|
admin_site.register(Group, GroupAdmin)
|
||||||
|
admin_site.register(User, UserAdmin)
|
||||||
|
admin_site.register(Tag, TagAdmin)
|
||||||
|
|
||||||
|
# Modify the template to include an RQ link if django_rq is installed (see RQ_SHOW_ADMIN_LINK)
|
||||||
|
try:
|
||||||
|
import django_rq
|
||||||
|
admin_site.index_template = 'django_rq/index.html'
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
@@ -13,6 +13,7 @@ OBJ_TYPE_CHOICES = (
|
|||||||
('DCIM', (
|
('DCIM', (
|
||||||
('site', 'Sites'),
|
('site', 'Sites'),
|
||||||
('rack', 'Racks'),
|
('rack', 'Racks'),
|
||||||
|
('rackgroup', 'Rack Groups'),
|
||||||
('devicetype', 'Device types'),
|
('devicetype', 'Device types'),
|
||||||
('device', 'Devices'),
|
('device', 'Devices'),
|
||||||
('virtualchassis', 'Virtual Chassis'),
|
('virtualchassis', 'Virtual Chassis'),
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ if sys.version_info[0] < 3:
|
|||||||
DeprecationWarning
|
DeprecationWarning
|
||||||
)
|
)
|
||||||
|
|
||||||
VERSION = '2.4.1'
|
VERSION = '2.4.4'
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
@@ -272,7 +272,6 @@ RQ_QUEUES = {
|
|||||||
'DEFAULT_TIMEOUT': REDIS_DEFAULT_TIMEOUT,
|
'DEFAULT_TIMEOUT': REDIS_DEFAULT_TIMEOUT,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RQ_SHOW_ADMIN_LINK = True
|
|
||||||
|
|
||||||
# drf_yasg settings for Swagger
|
# drf_yasg settings for Swagger
|
||||||
SWAGGER_SETTINGS = {
|
SWAGGER_SETTINGS = {
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from django.contrib import admin
|
|
||||||
from django.views.static import serve
|
from django.views.static import serve
|
||||||
from drf_yasg.views import get_schema_view
|
from drf_yasg.views import get_schema_view
|
||||||
from drf_yasg import openapi
|
from drf_yasg import openapi
|
||||||
|
|
||||||
from netbox.views import APIRootView, HomeView, SearchView
|
from netbox.views import APIRootView, HomeView, SearchView
|
||||||
from users.views import LoginView, LogoutView
|
from users.views import LoginView, LogoutView
|
||||||
|
from .admin import admin_site
|
||||||
|
|
||||||
schema_view = get_schema_view(
|
schema_view = get_schema_view(
|
||||||
openapi.Info(
|
openapi.Info(
|
||||||
@@ -60,7 +60,7 @@ _patterns = [
|
|||||||
url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
|
url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
|
||||||
|
|
||||||
# Admin
|
# Admin
|
||||||
url(r'^admin/', admin.site.urls),
|
url(r'^admin/', admin_site.urls),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -69,7 +69,6 @@ if settings.WEBHOOKS_ENABLED:
|
|||||||
url(r'^admin/webhook-backend-status/', include('django_rq.urls')),
|
url(r'^admin/webhook-backend-status/', include('django_rq.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
import debug_toolbar
|
import debug_toolbar
|
||||||
_patterns += [
|
_patterns += [
|
||||||
|
|||||||
@@ -12,9 +12,16 @@ from rest_framework.views import APIView
|
|||||||
from circuits.filters import CircuitFilter, ProviderFilter
|
from circuits.filters import CircuitFilter, ProviderFilter
|
||||||
from circuits.models import Circuit, Provider
|
from circuits.models import Circuit, Provider
|
||||||
from circuits.tables import CircuitTable, ProviderTable
|
from circuits.tables import CircuitTable, ProviderTable
|
||||||
from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter, VirtualChassisFilter
|
from dcim.filters import (
|
||||||
from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site, VirtualChassis
|
DeviceFilter, DeviceTypeFilter, RackFilter, RackGroupFilter, SiteFilter, VirtualChassisFilter
|
||||||
from dcim.tables import DeviceDetailTable, DeviceTypeTable, RackTable, SiteTable, VirtualChassisTable
|
)
|
||||||
|
from dcim.models import (
|
||||||
|
ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, RackGroup, Site,
|
||||||
|
VirtualChassis
|
||||||
|
)
|
||||||
|
from dcim.tables import (
|
||||||
|
DeviceDetailTable, DeviceTypeTable, RackTable, RackGroupTable, SiteTable, VirtualChassisTable
|
||||||
|
)
|
||||||
from extras.models import ObjectChange, ReportResult, TopologyMap
|
from extras.models import ObjectChange, ReportResult, TopologyMap
|
||||||
from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
|
from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
|
||||||
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
|
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
|
||||||
@@ -58,6 +65,12 @@ SEARCH_TYPES = OrderedDict((
|
|||||||
'table': RackTable,
|
'table': RackTable,
|
||||||
'url': 'dcim:rack_list',
|
'url': 'dcim:rack_list',
|
||||||
}),
|
}),
|
||||||
|
('rackgroup', {
|
||||||
|
'queryset': RackGroup.objects.select_related('site').annotate(rack_count=Count('racks')),
|
||||||
|
'filter': RackGroupFilter,
|
||||||
|
'table': RackGroupTable,
|
||||||
|
'url': 'dcim:rackgroup_list',
|
||||||
|
}),
|
||||||
('devicetype', {
|
('devicetype', {
|
||||||
'queryset': DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances')),
|
'queryset': DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances')),
|
||||||
'filter': DeviceTypeFilter,
|
'filter': DeviceTypeFilter,
|
||||||
|
|||||||
0
netbox/reports/__init__.py
Normal file
0
netbox/reports/__init__.py
Normal file
@@ -3,11 +3,12 @@ from __future__ import unicode_literals
|
|||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
|
|
||||||
|
from netbox.admin import admin_site
|
||||||
from .forms import ActivateUserKeyForm
|
from .forms import ActivateUserKeyForm
|
||||||
from .models import UserKey
|
from .models import UserKey
|
||||||
|
|
||||||
|
|
||||||
@admin.register(UserKey)
|
@admin.register(UserKey, site=admin_site)
|
||||||
class UserKeyAdmin(admin.ModelAdmin):
|
class UserKeyAdmin(admin.ModelAdmin):
|
||||||
actions = ['activate_selected']
|
actions = ['activate_selected']
|
||||||
list_display = ['user', 'is_filled', 'is_active', 'created']
|
list_display = ['user', 'is_filled', 'is_active', 'created']
|
||||||
|
|||||||
@@ -54,7 +54,9 @@
|
|||||||
<a href="{% url 'dcim:device' pk=device.pk %}">Device</a>
|
<a href="{% url 'dcim:device' pk=device.pk %}">Device</a>
|
||||||
</li>
|
</li>
|
||||||
<li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}>
|
<li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}>
|
||||||
<a href="{% url 'dcim:device_inventory' pk=device.pk %}">Inventory</a>
|
<a href="{% url 'dcim:device_inventory' pk=device.pk %}">
|
||||||
|
Inventory <span class="badge">{{ device.inventory_items.count }}</span>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% if perms.dcim.napalm_read %}
|
{% if perms.dcim.napalm_read %}
|
||||||
{% if device.status != 1 %}
|
{% if device.status != 1 %}
|
||||||
@@ -445,7 +447,7 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
{% if device_bays or device.device_type.is_parent_device %}
|
{% if device_bays or device.device_type.is_parent_device %}
|
||||||
{% if perms.dcim.delete_devicebay %}
|
{% if perms.dcim.delete_devicebay %}
|
||||||
<form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
@@ -481,7 +483,7 @@
|
|||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if device_bays and perms.dcim.delete_devicebay %}
|
{% if device_bays and perms.dcim.delete_devicebay %}
|
||||||
<button type="submit" class="btn btn-danger btn-xs">
|
<button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -573,7 +575,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if cs_ports or device.device_type.is_console_server %}
|
{% if cs_ports or device.device_type.is_console_server %}
|
||||||
{% if perms.dcim.delete_consoleserverport %}
|
{% if perms.dcim.delete_consoleserverport %}
|
||||||
<form method="post" action="{% url 'dcim:consoleserverport_bulk_delete' pk=device.pk %}">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
@@ -606,12 +608,12 @@
|
|||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if cs_ports and perms.dcim.delete_consoleserverport %}
|
{% if cs_ports and perms.dcim.delete_consoleserverport %}
|
||||||
<button type="submit" class="btn btn-danger btn-xs">
|
<button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -631,7 +633,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if power_outlets or device.device_type.is_pdu %}
|
{% if power_outlets or device.device_type.is_pdu %}
|
||||||
{% if perms.dcim.delete_poweroutlet %}
|
{% if perms.dcim.delete_poweroutlet %}
|
||||||
<form method="post" action="{% url 'dcim:poweroutlet_bulk_delete' pk=device.pk %}">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
@@ -664,12 +666,12 @@
|
|||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
<button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
|
<button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if power_outlets and perms.dcim.delete_poweroutlet %}
|
{% if power_outlets and perms.dcim.delete_poweroutlet %}
|
||||||
<button type="submit" class="btn btn-danger btn-xs">
|
<button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -17,12 +17,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
{% if perms.dcim.change_interface %}
|
{% if perms.dcim.change_interface %}
|
||||||
<a href="{% url 'dcim:interface_edit' pk=interface.pk %}" class="btn btn-warning">
|
<a href="{% if interface.device %}{% url 'dcim:interface_edit' pk=interface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=interface.pk %}{% endif %}" class="btn btn-warning">
|
||||||
<span class="fa fa-pencil" aria-hidden="true"></span> Edit this interface
|
<span class="fa fa-pencil" aria-hidden="true"></span> Edit this interface
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_interface %}
|
{% if perms.dcim.delete_interface %}
|
||||||
<a href="{% url 'dcim:interface_delete' pk=interface.pk %}" class="btn btn-danger">
|
<a href="{% if interface.device %}{% url 'dcim:interface_delete' pk=interface.pk %}{% else %}{% url 'virtualization:interface_delete' pk=interface.pk %}{% endif %}" class="btn btn-danger">
|
||||||
<span class="fa fa-trash" aria-hidden="true"></span> Delete this interface
|
<span class="fa fa-trash" aria-hidden="true"></span> Delete this interface
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -140,6 +140,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tenant Groups</td>
|
||||||
|
<td>
|
||||||
|
{% if configcontext.tenant_groups.all %}
|
||||||
|
<ul>
|
||||||
|
{% for tenant_group in configcontext.tenant_groups.all %}
|
||||||
|
<li><a href="{{ tenant_group.get_absolute_url }}">{{ tenant_group }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Tenants</td>
|
<td>Tenants</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -9,8 +9,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<h1>{% block title %}Config Contexts{% endblock %}</h1>
|
<h1>{% block title %}Config Contexts{% endblock %}</h1>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-9">
|
||||||
{% include 'utilities/obj_table.html' with bulk_delete_url='extras:configcontext_bulk_delete' %}
|
{% include 'utilities/obj_table.html' with bulk_delete_url='extras:configcontext_bulk_delete' %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
{% include 'inc/search_panel.html' %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -194,10 +194,13 @@
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% empty %}
|
{% if forloop.last %}
|
||||||
<div class="list-group-item">
|
<div class="list-group-item text-right">
|
||||||
Welcome to NetBox! {% if perms.add_site %} <a href="{% url 'dcim:site_add' %}">Add a site</a> to get started.{% endif %}
|
<a href="{% url 'extras:objectchange_list' %}">View All Changes</a>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% empty %}
|
||||||
|
<div class="list-group-item text-muted">No change history found</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
{% include 'inc/created_updated.html' with obj=vrf %}
|
{% include 'inc/created_updated.html' with obj=vrf %}
|
||||||
<ul class="nav nav-tabs">
|
<ul class="nav nav-tabs">
|
||||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||||
<a href="{{ aggregate.get_absolute_url }}">VRF</a>
|
<a href="{{ vrf.get_absolute_url }}">VRF</a>
|
||||||
</li>
|
</li>
|
||||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||||
<a href="{% url 'ipam:vrf_changelog' pk=vrf.pk %}">Changelog</a>
|
<a href="{% url 'ipam:vrf_changelog' pk=vrf.pk %}">Changelog</a>
|
||||||
|
|||||||
@@ -315,9 +315,9 @@
|
|||||||
$('button.toggle-ips').click(function() {
|
$('button.toggle-ips').click(function() {
|
||||||
var selected = $(this).attr('selected');
|
var selected = $(this).attr('selected');
|
||||||
if (selected) {
|
if (selected) {
|
||||||
$('#interfaces_table tr.ipaddress').hide();
|
$('#interfaces_table tr.ipaddresses').hide();
|
||||||
} else {
|
} else {
|
||||||
$('#interfaces_table tr.ipaddress').show();
|
$('#interfaces_table tr.ipaddresses').show();
|
||||||
}
|
}
|
||||||
$(this).attr('selected', !selected);
|
$(this).attr('selected', !selected);
|
||||||
$(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');
|
$(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from netbox.admin import admin_site
|
||||||
from .models import Token
|
from .models import Token
|
||||||
|
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ class TokenAdminForm(forms.ModelForm):
|
|||||||
model = Token
|
model = Token
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Token)
|
@admin.register(Token, site=admin_site)
|
||||||
class TokenAdmin(admin.ModelAdmin):
|
class TokenAdmin(admin.ModelAdmin):
|
||||||
form = TokenAdminForm
|
form = TokenAdminForm
|
||||||
list_display = ['key', 'user', 'created', 'expires', 'write_enabled', 'description']
|
list_display = ['key', 'user', 'created', 'expires', 'write_enabled', 'description']
|
||||||
|
|||||||
@@ -74,6 +74,12 @@ class ChoiceField(Field):
|
|||||||
return {'value': obj, 'label': self._choices[obj]}
|
return {'value': obj, 'label': self._choices[obj]}
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
|
# Hotwiring boolean values
|
||||||
|
if hasattr(data, 'lower'):
|
||||||
|
if data.lower() == 'true':
|
||||||
|
return True
|
||||||
|
if data.lower() == 'false':
|
||||||
|
return False
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -102,10 +108,9 @@ class TimeZoneField(Field):
|
|||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
if not data:
|
if not data:
|
||||||
return ""
|
return ""
|
||||||
try:
|
if data not in pytz.common_timezones:
|
||||||
return pytz.timezone(str(data))
|
raise ValidationError('Unknown time zone "{}" (see pytz.common_timezones for all options)'.format(data))
|
||||||
except pytz.exceptions.UnknownTimeZoneError:
|
return pytz.timezone(data)
|
||||||
raise ValidationError('Invalid time zone "{}"'.format(data))
|
|
||||||
|
|
||||||
|
|
||||||
class SerializedPKRelatedField(PrimaryKeyRelatedField):
|
class SerializedPKRelatedField(PrimaryKeyRelatedField):
|
||||||
@@ -164,7 +169,9 @@ class WritableNestedSerializer(ModelSerializer):
|
|||||||
if data is None:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
return self.Meta.model.objects.get(pk=data)
|
return self.Meta.model.objects.get(pk=int(data))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise ValidationError("Primary key must be an integer")
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise ValidationError("Invalid ID")
|
raise ValidationError("Invalid ID")
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ class VirtualMachineIPAddressSerializer(WritableNestedSerializer):
|
|||||||
|
|
||||||
class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||||
status = ChoiceField(choices=VM_STATUS_CHOICES, required=False)
|
status = ChoiceField(choices=VM_STATUS_CHOICES, required=False)
|
||||||
|
site = NestedSiteSerializer(read_only=True)
|
||||||
cluster = NestedClusterSerializer(required=False, allow_null=True)
|
cluster = NestedClusterSerializer(required=False, allow_null=True)
|
||||||
role = NestedDeviceRoleSerializer(required=False, allow_null=True)
|
role = NestedDeviceRoleSerializer(required=False, allow_null=True)
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
@@ -104,8 +105,8 @@ class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6',
|
'id', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4',
|
||||||
'vcpus', 'memory', 'disk', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -146,7 +147,7 @@ class InterfaceVLANSerializer(WritableNestedSerializer):
|
|||||||
class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||||
virtual_machine = NestedVirtualMachineSerializer()
|
virtual_machine = NestedVirtualMachineSerializer()
|
||||||
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, default=IFACE_FF_VIRTUAL, required=False)
|
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, default=IFACE_FF_VIRTUAL, required=False)
|
||||||
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False)
|
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
|
||||||
untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
|
untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
|
||||||
tagged_vlans = SerializedPKRelatedField(
|
tagged_vlans = SerializedPKRelatedField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ class ClusterViewSet(CustomFieldModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VirtualMachineViewSet(CustomFieldModelViewSet):
|
class VirtualMachineViewSet(CustomFieldModelViewSet):
|
||||||
queryset = VirtualMachine.objects.all()
|
queryset = VirtualMachine.objects.select_related(
|
||||||
|
'cluster__site', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6'
|
||||||
|
)
|
||||||
filter_class = filters.VirtualMachineFilter
|
filter_class = filters.VirtualMachineFilter
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
|
|||||||
@@ -260,6 +260,22 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('virtualization:virtualmachine', args=[self.pk])
|
return reverse('virtualization:virtualmachine', args=[self.pk])
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
# Validate primary IP addresses
|
||||||
|
interfaces = self.interfaces.all()
|
||||||
|
for field in ['primary_ip4', 'primary_ip6']:
|
||||||
|
ip = getattr(self, field)
|
||||||
|
if ip is not None:
|
||||||
|
if ip.interface in interfaces:
|
||||||
|
pass
|
||||||
|
elif self.primary_ip4.nat_inside is not None and self.primary_ip4.nat_inside.interface in interfaces:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise ValidationError({
|
||||||
|
field: "The specified IP address ({}) is not assigned to this VM.".format(ip),
|
||||||
|
})
|
||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return (
|
return (
|
||||||
self.name,
|
self.name,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
|
from django.db import transaction
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@@ -183,17 +184,21 @@ class ClusterAddDevicesView(PermissionRequiredMixin, View):
|
|||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
|
||||||
|
device_pks = form.cleaned_data['devices']
|
||||||
|
with transaction.atomic():
|
||||||
|
|
||||||
# Assign the selected Devices to the Cluster
|
# Assign the selected Devices to the Cluster
|
||||||
devices = form.cleaned_data['devices']
|
for device in Device.objects.filter(pk__in=device_pks):
|
||||||
Device.objects.filter(pk__in=devices).update(cluster=cluster)
|
device.cluster = cluster
|
||||||
|
device.save()
|
||||||
|
|
||||||
messages.success(request, "Added {} devices to cluster {}".format(
|
messages.success(request, "Added {} devices to cluster {}".format(
|
||||||
len(devices), cluster
|
len(device_pks), cluster
|
||||||
))
|
))
|
||||||
return redirect(cluster.get_absolute_url())
|
return redirect(cluster.get_absolute_url())
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'cluser': cluster,
|
'cluster': cluster,
|
||||||
'form': form,
|
'form': form,
|
||||||
'return_url': cluster.get_absolute_url(),
|
'return_url': cluster.get_absolute_url(),
|
||||||
})
|
})
|
||||||
@@ -212,12 +217,16 @@ class ClusterRemoveDevicesView(PermissionRequiredMixin, View):
|
|||||||
form = self.form(request.POST)
|
form = self.form(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
|
||||||
|
device_pks = form.cleaned_data['pk']
|
||||||
|
with transaction.atomic():
|
||||||
|
|
||||||
# Remove the selected Devices from the Cluster
|
# Remove the selected Devices from the Cluster
|
||||||
devices = form.cleaned_data['pk']
|
for device in Device.objects.filter(pk__in=device_pks):
|
||||||
Device.objects.filter(pk__in=devices).update(cluster=None)
|
device.cluster = None
|
||||||
|
device.save()
|
||||||
|
|
||||||
messages.success(request, "Removed {} devices from cluster {}".format(
|
messages.success(request, "Removed {} devices from cluster {}".format(
|
||||||
len(devices), cluster
|
len(device_pks), cluster
|
||||||
))
|
))
|
||||||
return redirect(cluster.get_absolute_url())
|
return redirect(cluster.get_absolute_url())
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user