1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merge branch 'develop' into 2921-tags-select2

This commit is contained in:
Saria Hajjar
2020-01-30 17:51:55 +00:00
38 changed files with 250 additions and 6429 deletions

View File

@ -29,7 +29,7 @@ server {
location / {
proxy_pass http://127.0.0.1:8001;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}

View File

@ -110,8 +110,8 @@ AUTH_LDAP_USER_FLAGS_BY_GROUP = {
AUTH_LDAP_FIND_GROUP_PERMS = True
# Cache groups for one hour to reduce LDAP traffic
AUTH_LDAP_CACHE_GROUPS = True
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
AUTH_LDAP_CACHE_TIMEOUT = 3600
```
* `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in.

View File

@ -1,10 +1,22 @@
# v2.7.3 (FUTURE)
# v2.7.4 (FUTURE)
## Bug Fixes
* [#4043](https://github.com/netbox-community/netbox/issues/4043) - Fix toggling of required fields in custom scripts
* [#4049](https://github.com/netbox-community/netbox/issues/4049) - Restore missing `tags` field in IPAM service serializer
* [#4056](https://github.com/netbox-community/netbox/issues/4056) - Repair schema migration for Rack.outer_unit (from #3569)
---
# v2.7.3 (2020-01-28)
## Enhancements
* [#2921](https://github.com/netbox-community/netbox/issues/2921) - Replace tags filter with Select2 widget
* [#3310](https://github.com/netbox-community/netbox/issues/3310) - Pre-select site/rack for B side when creating a new cable
* [#3338](https://github.com/netbox-community/netbox/issues/3338) - Include circuit terminations in API representation of circuits
* [#3509](https://github.com/netbox-community/netbox/issues/3509) - Add IP address variables for custom scripts
* [#3978](https://github.com/netbox-community/netbox/issues/3978) - Add VRF filtering to search NAT IP
* [#4005](https://github.com/netbox-community/netbox/issues/4005) - Include timezone context in webhook timestamps
## Bug Fixes
@ -15,6 +27,14 @@
* [#3989](https://github.com/netbox-community/netbox/issues/3989) - Correct HTTP content type assignment for webhooks
* [#3999](https://github.com/netbox-community/netbox/issues/3999) - Do not filter child results by null if non-required parent fields are blank
* [#4008](https://github.com/netbox-community/netbox/issues/4008) - Toggle rack elevation face using front/rear strings
* [#4017](https://github.com/netbox-community/netbox/issues/4017) - Remove redundant tenant field from cluster form
* [#4019](https://github.com/netbox-community/netbox/issues/4019) - Restore border around background devices in rack elevations
* [#4022](https://github.com/netbox-community/netbox/issues/4022) - Fix display of assigned IPs when filtering device interfaces
* [#4025](https://github.com/netbox-community/netbox/issues/4025) - Correct display of cable status (various places)
* [#4027](https://github.com/netbox-community/netbox/issues/4027) - Repair schema migration for #3569 to convert IP addresses with DHCP status
* [#4028](https://github.com/netbox-community/netbox/issues/4028) - Correct URL patterns to match Unicode characters in tag slugs
* [#4030](https://github.com/netbox-community/netbox/issues/4030) - Fix exception when setting interfaces to tagged mode in bulk
* [#4033](https://github.com/netbox-community/netbox/issues/4033) - Restore missing comments field label of various bulk edit forms
---

View File

@ -3,11 +3,11 @@ from taggit_serializer.serializers import TaggitSerializer, TagListSerializerFie
from circuits.choices import CircuitStatusChoices
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
from dcim.api.nested_serializers import NestedCableSerializer, NestedInterfaceSerializer, NestedSiteSerializer
from dcim.api.serializers import ConnectedEndpointSerializer
from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer
from utilities.api import ChoiceField, ValidatedModelSerializer
from utilities.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer
from .nested_serializers import *
@ -39,18 +39,30 @@ class CircuitTypeSerializer(ValidatedModelSerializer):
fields = ['id', 'name', 'slug', 'description', 'circuit_count']
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
site = NestedSiteSerializer()
connected_endpoint = NestedInterfaceSerializer()
class Meta:
model = CircuitTermination
fields = ['id', 'url', 'site', 'connected_endpoint', 'port_speed', 'upstream_speed', 'xconnect_id']
class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
provider = NestedProviderSerializer()
status = ChoiceField(choices=CircuitStatusChoices, required=False)
type = NestedCircuitTypeSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True)
termination_a = CircuitCircuitTerminationSerializer(read_only=True)
termination_z = CircuitCircuitTerminationSerializer(read_only=True)
tags = TagListSerializerField(required=False)
class Meta:
model = Circuit
fields = [
'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]

View File

@ -62,7 +62,9 @@ class CircuitTypeViewSet(ModelViewSet):
#
class CircuitViewSet(CustomFieldModelViewSet):
queryset = Circuit.objects.prefetch_related('type', 'tenant', 'provider').prefetch_related('tags')
queryset = Circuit.objects.prefetch_related(
'type', 'tenant', 'provider', 'terminations__site', 'terminations__connected_endpoint__device'
).prefetch_related('tags')
serializer_class = serializers.CircuitSerializer
filterset_class = filters.CircuitFilterSet

View File

@ -89,7 +89,8 @@ class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdi
label='Admin contact'
)
comments = CommentField(
widget=SmallTextarea()
widget=SmallTextarea,
label='Comments'
)
class Meta:

File diff suppressed because it is too large Load Diff

View File

@ -678,7 +678,8 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
widget=StaticSelect2()
)
comments = CommentField(
widget=SmallTextarea
widget=SmallTextarea,
label='Comments'
)
class Meta:
@ -2758,7 +2759,7 @@ class InterfaceCSVForm(forms.ModelForm):
return self.cleaned_data['enabled']
class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Interface.objects.all(),
widget=forms.MultipleHiddenInput()
@ -2839,6 +2840,18 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo
else:
self.fields['lag'].choices = []
def clean(self):
# Untagged interfaces cannot be assigned tagged VLANs
if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and self.cleaned_data['tagged_vlans']:
raise forms.ValidationError({
'mode': "An access interface cannot have tagged VLANs assigned."
})
# Remove all tagged VLAN assignments from "tagged all" interfaces
elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL:
self.cleaned_data['tagged_vlans'] = []
class InterfaceBulkRenameForm(BulkRenameForm):
pk = forms.ModelMultipleChoiceField(
@ -4421,8 +4434,9 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
max_utilization = forms.IntegerField(
required=False
)
comments = forms.CharField(
required=False
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
class Meta:

View File

@ -37,7 +37,7 @@ def rack_status_to_slug(apps, schema_editor):
def rack_outer_unit_to_slug(apps, schema_editor):
Rack = apps.get_model('dcim', 'Rack')
for id, slug in RACK_DIMENSION_CHOICES:
Rack.objects.filter(status=str(id)).update(status=slug)
Rack.objects.filter(outer_unit=str(id)).update(outer_unit=slug)
class Migration(migrations.Migration):

View File

@ -0,0 +1,27 @@
from django.db import migrations
RACK_DIMENSION_CHOICES = (
(1000, 'mm'),
(2000, 'in'),
)
def rack_outer_unit_to_slug(apps, schema_editor):
Rack = apps.get_model('dcim', 'Rack')
for id, slug in RACK_DIMENSION_CHOICES:
Rack.objects.filter(outer_unit=str(id)).update(outer_unit=slug)
class Migration(migrations.Migration):
dependencies = [
('dcim', '0091_interface_type_other'),
]
operations = [
# Fixes a missed field migration from #3569; see bug #4056. The original migration has also been fixed,
# so this can be omitted when squashing in the future.
migrations.RunPython(
code=rack_outer_unit_to_slug
),
]

View File

@ -405,7 +405,7 @@ class RackElevationHelperMixin:
@staticmethod
def _draw_device_rear(drawing, device, start, end, text):
rect = drawing.rect(start, end, class_="blocked")
rect = drawing.rect(start, end, class_="slot blocked")
rect.set_desc('{}{} ({}U) {} {}'.format(
device.device_role, device.device_type.display_name,
device.device_type.u_height, device.asset_tag or '', device.serial or ''

View File

@ -2,6 +2,7 @@ from django.test import TestCase
from dcim.forms import *
from dcim.models import *
from virtualization.models import Cluster, ClusterGroup, ClusterType
def get_id(model, slug):
@ -10,83 +11,108 @@ def get_id(model, slug):
class DeviceTestCase(TestCase):
fixtures = ['dcim', 'ipam', 'virtualization']
@classmethod
def setUpTestData(cls):
site = Site.objects.create(name='Site 1', slug='site-1')
rack = Rack.objects.create(name='Rack 1', site=site)
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
device_type = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', u_height=1
)
device_role = DeviceRole.objects.create(
name='Device Role 1', slug='device-role-1', color='ff0000'
)
Platform.objects.create(name='Platform 1', slug='platform-1')
Device.objects.create(
name='Device 1', device_type=device_type, device_role=device_role, site=site, rack=rack, position=1
)
cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
cluster_group = ClusterGroup.objects.create(name='Cluster Group 1', slug='cluster-group-1')
Cluster.objects.create(name='Cluster 1', type=cluster_type, group=cluster_group)
def test_racked_device(self):
test = DeviceForm(data={
'name': 'test',
'device_role': get_id(DeviceRole, 'leaf-switch'),
form = DeviceForm(data={
'name': 'New Device',
'device_role': DeviceRole.objects.first().pk,
'tenant': None,
'manufacturer': get_id(Manufacturer, 'juniper'),
'device_type': get_id(DeviceType, 'qfx5100-48s'),
'site': get_id(Site, 'test1'),
'rack': '1',
'manufacturer': Manufacturer.objects.first().pk,
'device_type': DeviceType.objects.first().pk,
'site': Site.objects.first().pk,
'rack': Rack.objects.first().pk,
'face': DeviceFaceChoices.FACE_FRONT,
'position': 41,
'platform': get_id(Platform, 'juniper-junos'),
'position': 2,
'platform': Platform.objects.first().pk,
'status': DeviceStatusChoices.STATUS_ACTIVE,
})
self.assertTrue(test.is_valid(), test.fields['position'].choices)
self.assertTrue(test.save())
self.assertTrue(form.is_valid())
self.assertTrue(form.save())
def test_racked_device_occupied(self):
test = DeviceForm(data={
form = DeviceForm(data={
'name': 'test',
'device_role': get_id(DeviceRole, 'leaf-switch'),
'device_role': DeviceRole.objects.first().pk,
'tenant': None,
'manufacturer': get_id(Manufacturer, 'juniper'),
'device_type': get_id(DeviceType, 'qfx5100-48s'),
'site': get_id(Site, 'test1'),
'rack': '1',
'manufacturer': Manufacturer.objects.first().pk,
'device_type': DeviceType.objects.first().pk,
'site': Site.objects.first().pk,
'rack': Rack.objects.first().pk,
'face': DeviceFaceChoices.FACE_FRONT,
'position': 1,
'platform': get_id(Platform, 'juniper-junos'),
'platform': Platform.objects.first().pk,
'status': DeviceStatusChoices.STATUS_ACTIVE,
})
self.assertFalse(test.is_valid())
self.assertFalse(form.is_valid())
self.assertIn('position', form.errors)
def test_non_racked_device(self):
test = DeviceForm(data={
'name': 'test',
'device_role': get_id(DeviceRole, 'pdu'),
form = DeviceForm(data={
'name': 'New Device',
'device_role': DeviceRole.objects.first().pk,
'tenant': None,
'manufacturer': get_id(Manufacturer, 'servertech'),
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
'site': get_id(Site, 'test1'),
'rack': '1',
'face': '',
'manufacturer': Manufacturer.objects.first().pk,
'device_type': DeviceType.objects.first().pk,
'site': Site.objects.first().pk,
'rack': None,
'face': None,
'position': None,
'platform': None,
'platform': Platform.objects.first().pk,
'status': DeviceStatusChoices.STATUS_ACTIVE,
})
self.assertTrue(test.is_valid())
self.assertTrue(test.save())
self.assertTrue(form.is_valid())
self.assertTrue(form.save())
def test_non_racked_device_with_face(self):
test = DeviceForm(data={
'name': 'test',
'device_role': get_id(DeviceRole, 'pdu'),
def test_non_racked_device_with_face_position(self):
form = DeviceForm(data={
'name': 'New Device',
'device_role': DeviceRole.objects.first().pk,
'tenant': None,
'manufacturer': get_id(Manufacturer, 'servertech'),
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
'site': get_id(Site, 'test1'),
'rack': '1',
'manufacturer': Manufacturer.objects.first().pk,
'device_type': DeviceType.objects.first().pk,
'site': Site.objects.first().pk,
'rack': None,
'face': DeviceFaceChoices.FACE_REAR,
'position': None,
'position': 10,
'platform': None,
'status': DeviceStatusChoices.STATUS_ACTIVE,
})
self.assertTrue(test.is_valid())
self.assertTrue(test.save())
self.assertFalse(form.is_valid())
self.assertIn('face', form.errors)
self.assertIn('position', form.errors)
def test_cloned_cluster_device_initial_data(self):
def test_initial_data_population(self):
device_type = DeviceType.objects.first()
cluster = Cluster.objects.first()
test = DeviceForm(initial={
'device_type': get_id(DeviceType, 'poweredge-r640'),
'device_role': get_id(DeviceRole, 'server'),
'device_type': device_type.pk,
'device_role': DeviceRole.objects.first().pk,
'status': DeviceStatusChoices.STATUS_ACTIVE,
'site': get_id(Site, 'test1'),
"cluster": Cluster.objects.get(id=4).id,
'site': Site.objects.first().pk,
'cluster': cluster.pk,
})
self.assertEqual(test.initial['manufacturer'], get_id(Manufacturer, 'dell'))
self.assertIn('cluster_group', test.initial)
self.assertEqual(test.initial['cluster_group'], get_id(ClusterGroup, 'vm-host'))
# Check that the initial value for the manufacturer is set automatically when assigning the device type
self.assertEqual(test.initial['manufacturer'], device_type.manufacturer.pk)
# Check that the initial value for the cluster group is set automatically when assigning the cluster
self.assertEqual(test.initial['cluster_group'], cluster.group.pk)

View File

@ -1,35 +0,0 @@
[
{
"model": "extras.graph",
"pk": 1,
"fields": {
"type": 300,
"weight": 1000,
"name": "Site Test Graph",
"source": "http://localhost/na.png",
"link": ""
}
},
{
"model": "extras.graph",
"pk": 2,
"fields": {
"type": 200,
"weight": 1000,
"name": "Provider Test Graph",
"source": "http://localhost/provider_graph.png",
"link": ""
}
},
{
"model": "extras.graph",
"pk": 3,
"fields": {
"type": 100,
"weight": 1000,
"name": "Interface Test Graph",
"source": "http://localhost/interface_graph.png",
"link": ""
}
}
]

View File

@ -53,14 +53,15 @@ class ScriptVariable:
# Initialize field attributes
if not hasattr(self, 'field_attrs'):
self.field_attrs = {}
if description:
self.field_attrs['help_text'] = description
if label:
self.field_attrs['label'] = label
if description:
self.field_attrs['help_text'] = description
if default:
self.field_attrs['initial'] = default
if required:
self.field_attrs['required'] = True
self.field_attrs['required'] = required
# Initialize the list of optional validators if none have already been defined
if 'validators' not in self.field_attrs:
self.field_attrs['validators'] = []

View File

@ -11,10 +11,10 @@ urlpatterns = [
path(r'tags/', views.TagListView.as_view(), name='tag_list'),
path(r'tags/edit/', views.TagBulkEditView.as_view(), name='tag_bulk_edit'),
path(r'tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
path(r'tags/<slug:slug>/', views.TagView.as_view(), name='tag'),
path(r'tags/<slug:slug>/edit/', views.TagEditView.as_view(), name='tag_edit'),
path(r'tags/<slug:slug>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
path(r'tags/<slug:slug>/changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}),
path(r'tags/<str:slug>/', views.TagView.as_view(), name='tag'),
path(r'tags/<str:slug>/edit/', views.TagEditView.as_view(), name='tag_edit'),
path(r'tags/<str:slug>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
path(r'tags/<str:slug>/changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}),
# Config contexts
path(r'config-contexts/', views.ConfigContextListView.as_view(), name='configcontext_list'),

View File

@ -237,7 +237,7 @@ class AvailableIPSerializer(serializers.Serializer):
# Services
#
class ServiceSerializer(CustomFieldModelSerializer):
class ServiceSerializer(TaggitSerializer, CustomFieldModelSerializer):
device = NestedDeviceSerializer(required=False, allow_null=True)
virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
protocol = ChoiceField(choices=ServiceProtocolChoices)
@ -247,10 +247,11 @@ class ServiceSerializer(CustomFieldModelSerializer):
required=False,
many=True
)
tags = TagListSerializerField(required=False)
class Meta:
model = Service
fields = [
'id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description',
'id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description', 'tags',
'custom_fields', 'created', 'last_updated',
]

View File

@ -1,329 +0,0 @@
[
{
"model": "ipam.rir",
"pk": 1,
"fields": {
"name": "RFC1918",
"slug": "rfc1918"
}
},
{
"model": "ipam.aggregate",
"pk": 1,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"family": 4,
"prefix": "10.0.0.0/8",
"rir": 1,
"date_added": null,
"description": ""
}
},
{
"model": "ipam.role",
"pk": 1,
"fields": {
"name": "Lab Network",
"slug": "lab-network",
"weight": 1000
}
},
{
"model": "ipam.prefix",
"pk": 1,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"family": 4,
"prefix": "10.1.1.0/24",
"site": 1,
"vrf": null,
"vlan": null,
"status": "active",
"role": 1,
"description": ""
}
},
{
"model": "ipam.prefix",
"pk": 2,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"family": 4,
"prefix": "10.0.255.0/24",
"site": 1,
"vrf": null,
"vlan": null,
"status": "active",
"role": 1,
"description": ""
}
},
{
"model": "ipam.ipaddress",
"pk": 1,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"family": 4,
"address": "10.0.255.1/32",
"vrf": null,
"interface_id": 3,
"nat_inside": null,
"description": ""
}
},
{
"model": "ipam.ipaddress",
"pk": 2,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"family": 4,
"address": "169.254.254.1/31",
"vrf": null,
"interface_id": 4,
"nat_inside": null,
"description": ""
}
},
{
"model": "ipam.ipaddress",
"pk": 3,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"family": 4,
"address": "10.0.255.2/32",
"vrf": null,
"interface_id": 185,
"nat_inside": null,
"description": ""
}
},
{
"model": "ipam.ipaddress",
"pk": 4,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"family": 4,
"address": "169.254.1.1/31",
"vrf": null,
"interface_id": 213,
"nat_inside": null,
"description": ""
}
},
{
"model": "ipam.ipaddress",
"pk": 5,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"family": 4,
"address": "10.0.254.1/24",
"vrf": null,
"interface_id": 12,
"nat_inside": null,
"description": ""
}
},
{
"model": "ipam.ipaddress",
"pk": 8,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"family": 4,
"address": "10.15.21.1/31",
"vrf": null,
"interface_id": 218,
"nat_inside": null,
"description": ""
}
},
{
"model": "ipam.ipaddress",
"pk": 9,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"family": 4,
"address": "10.15.21.2/31",
"vrf": null,
"interface_id": 9,
"nat_inside": null,
"description": ""
}
},
{
"model": "ipam.ipaddress",
"pk": 10,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"family": 4,
"address": "10.15.22.1/31",
"vrf": null,
"interface_id": 8,
"nat_inside": null,
"description": ""
}
},
{
"model": "ipam.ipaddress",
"pk": 11,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"family": 4,
"address": "10.15.20.1/31",
"vrf": null,
"interface_id": 7,
"nat_inside": null,
"description": ""
}
},
{
"model": "ipam.ipaddress",
"pk": 12,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"family": 4,
"address": "10.16.20.1/31",
"vrf": null,
"interface_id": 216,
"nat_inside": null,
"description": ""
}
},
{
"model": "ipam.ipaddress",
"pk": 13,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"family": 4,
"address": "10.15.22.2/31",
"vrf": null,
"interface_id": 206,
"nat_inside": null,
"description": ""
}
},
{
"model": "ipam.ipaddress",
"pk": 14,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"family": 4,
"address": "10.16.22.1/31",
"vrf": null,
"interface_id": 217,
"nat_inside": null,
"description": ""
}
},
{
"model": "ipam.ipaddress",
"pk": 15,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"family": 4,
"address": "10.16.22.2/31",
"vrf": null,
"interface_id": 205,
"nat_inside": null,
"description": ""
}
},
{
"model": "ipam.ipaddress",
"pk": 16,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"family": 4,
"address": "10.16.20.2/31",
"vrf": null,
"interface_id": 211,
"nat_inside": null,
"description": ""
}
},
{
"model": "ipam.ipaddress",
"pk": 17,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"family": 4,
"address": "10.15.22.2/31",
"vrf": null,
"interface_id": 212,
"nat_inside": null,
"description": ""
}
},
{
"model": "ipam.ipaddress",
"pk": 19,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"family": 4,
"address": "10.0.254.2/32",
"vrf": null,
"interface_id": 188,
"nat_inside": null,
"description": ""
}
},
{
"model": "ipam.ipaddress",
"pk": 20,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"family": 4,
"address": "169.254.1.1/31",
"vrf": null,
"interface_id": 200,
"nat_inside": null,
"description": ""
}
},
{
"model": "ipam.ipaddress",
"pk": 21,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"family": 4,
"address": "169.254.1.2/31",
"vrf": null,
"interface_id": 194,
"nat_inside": null,
"description": ""
}
},
{
"model": "ipam.vlan",
"pk": 1,
"fields": {
"created": "2016-06-23",
"last_updated": "2016-06-23T03:19:56.521Z",
"site": 1,
"vid": 999,
"name": "TEST",
"status": "active",
"role": 1
}
}
]

View File

@ -638,6 +638,17 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
}
)
)
nat_vrf = forms.ModelChoiceField(
queryset=VRF.objects.all(),
required=False,
label='VRF',
widget=APISelect(
api_url="/api/ipam/vrfs/",
filter_for={
'nat_inside': 'vrf_id'
}
)
)
nat_inside = ChainedModelChoiceField(
queryset=IPAddress.objects.all(),
chains=(

View File

@ -2,10 +2,10 @@ from django.db import migrations, models
IPADDRESS_STATUS_CHOICES = (
(0, 'container'),
(1, 'active'),
(2, 'reserved'),
(3, 'deprecated'),
(5, 'dhcp'),
)
IPADDRESS_ROLE_CHOICES = (

View File

@ -0,0 +1,21 @@
from django.db import migrations
def ipaddress_status_dhcp_to_slug(apps, schema_editor):
IPAddress = apps.get_model('ipam', 'IPAddress')
IPAddress.objects.filter(status='5').update(status='dhcp')
class Migration(migrations.Migration):
dependencies = [
('ipam', '0033_deterministic_ordering'),
]
operations = [
# Fixes a missed integer substitution from #3569; see bug #4027. The original migration has also been fixed,
# so this can be omitted when squashing in the future.
migrations.RunPython(
code=ipaddress_status_dhcp_to_slug
),
]

View File

@ -1064,6 +1064,7 @@ class ServiceTest(APITestCase):
'name': 'Test Service 4',
'protocol': ServiceProtocolChoices.PROTOCOL_TCP,
'port': 4,
'tags': ['Foo', 'Bar'],
}
url = reverse('ipam-api:service-list')
@ -1076,6 +1077,8 @@ class ServiceTest(APITestCase):
self.assertEqual(service4.name, data['name'])
self.assertEqual(service4.protocol, data['protocol'])
self.assertEqual(service4.port, data['port'])
tags = [tag.name for tag in service4.tags.all()]
self.assertEqual(sorted(tags), sorted(data['tags']))
def test_create_service_bulk(self):

View File

@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured
# Environment setup
#
VERSION = '2.7.3-dev'
VERSION = '2.7.4-dev'
# Hostname
HOSTNAME = platform.node()
@ -503,6 +503,7 @@ SWAGGER_SETTINGS = {
'utilities.custom_inspectors.IdInFilterInspector',
'drf_yasg.inspectors.CoreAPICompatInspector',
],
'DEFAULT_INFO': 'netbox.urls.openapi_info',
'DEFAULT_MODEL_DEPTH': 1,
'DEFAULT_PAGINATOR_INSPECTORS': [
'utilities.custom_inspectors.NullablePaginatorInspector',

View File

@ -9,14 +9,16 @@ from netbox.views import APIRootView, HomeView, SearchView
from users.views import LoginView, LogoutView
from .admin import admin_site
openapi_info = openapi.Info(
title="NetBox API",
default_version='v2',
description="API to access NetBox",
terms_of_service="https://github.com/netbox-community/netbox",
license=openapi.License(name="Apache v2 License"),
)
schema_view = get_schema_view(
openapi.Info(
title="NetBox API",
default_version='v2',
description="API to access NetBox",
terms_of_service="https://github.com/netbox-community/netbox",
license=openapi.License(name="Apache v2 License"),
),
openapi_info,
validators=['flex', 'ssv'],
public=True,
)

View File

@ -2,9 +2,9 @@
$('button.toggle-ips').click(function() {
var selected = $(this).attr('selected');
if (selected) {
$('#interfaces_table tr.ipaddresses').hide();
$('#interfaces_table tr.interface:visible + tr.ipaddresses').hide();
} else {
$('#interfaces_table tr.ipaddresses').show();
$('#interfaces_table tr.interface:visible + tr.ipaddresses').show();
}
$(this).attr('selected', !selected);
$(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');
@ -14,17 +14,22 @@ $('button.toggle-ips').click(function() {
// Inteface filtering
$('input.interface-filter').on('input', function() {
var filter = new RegExp(this.value);
var interface;
for (interface of $(this).closest('div.panel').find('tbody > tr')) {
for (interface of $('#interfaces_table > tbody > tr.interface')) {
// Slice off 'interface_' at the start of the ID
if (filter && filter.test(interface.id.slice(10))) {
if (filter.test(interface.id.slice(10))) {
// Match the toggle in case the filter now matches the interface
$(interface).find('input:checkbox[name=pk]').prop('checked', $('input.toggle').prop('checked'));
$(interface).show();
if ($('button.toggle-ips').attr('selected')) {
$(interface).next('tr.ipaddresses').show();
}
} else {
// Uncheck to prevent actions from including it when it doesn't match
$(interface).find('input:checkbox[name=pk]').prop('checked', false);
$(interface).hide();
$(interface).next('tr.ipaddresses').hide();
}
}
});

View File

@ -32,7 +32,7 @@
{% if cable.label %}<code>{{ cable.label }}</code>{% else %}Cable #{{ cable.pk }}{% endif %}
</a>
</h4>
<p><span class="label label-{% if cable.status %}success{% else %}info{% endif %}">{{ cable.get_status_display }}</span></p>
<p><span class="label label-{{ cable.get_status_class }}">{{ cable.get_status_display }}</span></p>
<p>{{ cable.get_type_display|default:"" }}</p>
{% if cable.length %}{{ cable.length }} {{ cable.get_length_unit_display }}{% endif %}
{% if cable.color %}

View File

@ -1,5 +1,5 @@
{% if perms.dcim.change_cable %}
{% if cable.status %}
{% if cable.status == 'connected' %}
<a href="#" class="btn btn-warning btn-xs cable-toggle connected" title="Mark planned" data="{{ cable.pk }}">
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
</a>

View File

@ -1,4 +1,4 @@
<tr class="consoleport{% if cp.cable.status %} success{% elif cp.cable %} info{% endif %}">
<tr class="consoleport{% if cp.cable %} {{ cp.cable.get_status_class }}{% endif %}">
{# Name #}
<td>

View File

@ -1,6 +1,6 @@
{% load helpers %}
<tr class="consoleserverport{% if csp.cable.status %} success{% elif csp.cable %} info{% endif %}">
<tr class="consoleserverport{% if csp.cable %} {{ csp.cable.get_status_class }}{% endif %}">
{# Checkbox #}
{% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}

View File

@ -1,5 +1,5 @@
{% load helpers %}
<tr class="frontport{% if frontport.cable.status %} success{% elif frontport.cable %} info{% endif %}">
<tr class="frontport{% if frontport.cable %} {{ frontport.cable.get_status_class }}{% endif %}">
{# Checkbox #}
{% if perms.dcim.change_frontport or perms.dcim.delete_frontport %}

View File

@ -1,5 +1,5 @@
{% load helpers %}
<tr class="interface{% if not iface.enabled %} danger{% elif iface.cable.status %} success{% elif iface.cable %} info{% elif iface.is_virtual %} warning{% endif %}" id="interface_{{ iface.name }}">
<tr class="interface{% if not iface.enabled %} danger{% elif iface.cable %} {{ iface.cable.get_status_class }}{% elif iface.is_virtual %} warning{% endif %}" id="interface_{{ iface.name }}">
{# Checkbox #}
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}

View File

@ -1,6 +1,6 @@
{% load helpers %}
<tr class="poweroutlet{% if po.cable.status %} success{% elif po.cable %} info{% endif %}">
<tr class="poweroutlet{% if po.cable %} {{ po.cable.get_status_class }}{% endif %}">
{# Checkbox #}
{% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}

View File

@ -1,4 +1,4 @@
<tr class="powerport{% if pp.cable.status %} success{% elif pp.cable %} info{% endif %}">
<tr class="powerport{% if pp.cable %} {{ pp.cable.get_status_class }}{% endif %}">
{# Name #}
<td>

View File

@ -1,5 +1,5 @@
{% load helpers %}
<tr class="rearport{% if rearport.cable.status %} success{% elif rearport.cable %} info{% endif %}">
<tr class="rearport{% if rearport.cable %} {{ rearport.cable.get_status_class }}{% endif %}">
{# Checkbox #}
{% if perms.dcim.change_rearport or perms.dcim.delete_rearport %}

View File

@ -61,7 +61,7 @@
{% render_field form.nat_device %}
</div>
<div class="tab-pane" id="search">
&nbsp;
{% render_field form.nat_vrf %}
</div>
</div>
{% render_field form.nat_inside %}

View File

@ -8,7 +8,6 @@
{% render_field form.name %}
{% render_field form.type %}
{% render_field form.group %}
{% render_field form.tenant %}
{% render_field form.site %}
</div>
</div>

View File

@ -7,9 +7,6 @@ from django.urls import reverse
from .views import server_error
BASE_PATH = getattr(settings, 'BASE_PATH', False)
LOGIN_REQUIRED = getattr(settings, 'LOGIN_REQUIRED', False)
class LoginRequiredMiddleware(object):
"""
@ -19,7 +16,7 @@ class LoginRequiredMiddleware(object):
self.get_response = get_response
def __call__(self, request):
if LOGIN_REQUIRED and not request.user.is_authenticated:
if settings.LOGIN_REQUIRED and not request.user.is_authenticated:
# Redirect unauthenticated requests to the login page. API requests are exempt from redirection as the API
# performs its own authentication. Also metrics can be read without login.
api_path = reverse('api-root')

View File

@ -1,170 +0,0 @@
[
{
"model": "virtualization.clustertype",
"pk": 1,
"fields": {
"created": "2016-08-01",
"last_updated": "2016-08-01T15:22:42.289Z",
"name": "Public Cloud",
"slug": "public-cloud"
}
},
{
"model": "virtualization.clustertype",
"pk": 2,
"fields": {
"created": "2016-08-01",
"last_updated": "2016-08-01T15:22:42.289Z",
"name": "vSphere",
"slug": "vsphere"
}
},
{
"model": "virtualization.clustertype",
"pk": 3,
"fields": {
"created": "2016-08-01",
"last_updated": "2016-08-01T15:22:42.289Z",
"name": "Hyper-V",
"slug": "hyper-v"
}
},
{
"model": "virtualization.clustertype",
"pk": 4,
"fields": {
"created": "2016-08-01",
"last_updated": "2016-08-01T15:22:42.289Z",
"name": "libvirt",
"slug": "libvirt"
}
},
{
"model": "virtualization.clustertype",
"pk": 5,
"fields": {
"created": "2016-08-01",
"last_updated": "2016-08-01T15:22:42.289Z",
"name": "LXD",
"slug": "lxd"
}
},
{
"model": "virtualization.clustertype",
"pk": 6,
"fields": {
"created": "2016-08-01",
"last_updated": "2016-08-01T15:22:42.289Z",
"name": "Docker",
"slug": "docker"
}
},
{
"model": "virtualization.clustergroup",
"pk": 1,
"fields": {
"created": "2016-08-01",
"last_updated": "2016-08-01T15:22:42.289Z",
"name": "VM Host",
"slug": "vm-host"
}
},
{
"model": "virtualization.cluster",
"pk": 1,
"fields": {
"created": "2016-08-01",
"last_updated": "2016-08-01T15:22:42.289Z",
"name": "Digital Ocean",
"type": 1,
"group": 1,
"tenant": null,
"site": null,
"comments": ""
}
},
{
"model": "virtualization.cluster",
"pk": 2,
"fields": {
"created": "2016-08-01",
"last_updated": "2016-08-01T15:22:42.289Z",
"name": "Amazon EC2",
"type": 1,
"group": 1,
"tenant": null,
"site": null,
"comments": ""
}
},
{
"model": "virtualization.cluster",
"pk": 3,
"fields": {
"created": "2016-08-01",
"last_updated": "2016-08-01T15:22:42.289Z",
"name": "Microsoft Azure",
"type": 1,
"group": 1,
"tenant": null,
"site": null,
"comments": ""
}
},
{
"model": "virtualization.cluster",
"pk": 4,
"fields": {
"created": "2016-08-01",
"last_updated": "2016-08-01T15:22:42.289Z",
"name": "vSphere Cluster",
"type": 2,
"group": 1,
"tenant": null,
"site": null,
"comments": ""
}
},
{
"model": "virtualization.virtualmachine",
"pk": 1,
"fields": {
"local_context_data": null,
"created": "2019-12-19",
"last_updated": "2019-12-19T05:24:19.146Z",
"cluster": 2,
"tenant": null,
"platform": null,
"name": "vm1",
"status": "active",
"role": null,
"primary_ip4": null,
"primary_ip6": null,
"vcpus": null,
"memory": null,
"disk": null,
"comments": ""
}
},
{
"model": "virtualization.virtualmachine",
"pk": 2,
"fields": {
"local_context_data": null,
"created": "2019-12-19",
"last_updated": "2019-12-19T05:24:41.478Z",
"cluster": 1,
"tenant": null,
"platform": null,
"name": "vm2",
"status": "active",
"role": null,
"primary_ip4": null,
"primary_ip6": null,
"vcpus": null,
"memory": null,
"disk": null,
"comments": ""
}
}
]

View File

@ -171,7 +171,8 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
)
)
comments = CommentField(
widget=SmallTextarea()
widget=SmallTextarea,
label='Comments'
)
class Meta:
@ -536,7 +537,8 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB
label='Disk (GB)'
)
comments = CommentField(
widget=SmallTextarea()
widget=SmallTextarea,
label='Comments'
)
class Meta: