mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'develop' into develop-2.6
This commit is contained in:
25
CHANGELOG.md
25
CHANGELOG.md
@ -30,6 +30,31 @@ to now use "Extras | Tag."
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
v2.5.9 (2019-04-01)
|
||||||
|
|
||||||
|
## Enhancements
|
||||||
|
|
||||||
|
* [#2933](https://github.com/digitalocean/netbox/issues/2933) - Add username to outbound webhook requests
|
||||||
|
* [#3011](https://github.com/digitalocean/netbox/issues/3011) - Add SSL support for django-rq (requires django-rq v1.3.1+)
|
||||||
|
* [#3025](https://github.com/digitalocean/netbox/issues/3025) - Add request ID to outbound webhook requests (for correlating all changes part of a single request)
|
||||||
|
|
||||||
|
## Bug Fixes
|
||||||
|
|
||||||
|
* [#2207](https://github.com/digitalocean/netbox/issues/2207) - Fixes deterministic ordering of interfaces
|
||||||
|
* [#2577](https://github.com/digitalocean/netbox/issues/2577) - Clarification of wording in API regarding filtering
|
||||||
|
* [#2924](https://github.com/digitalocean/netbox/issues/2924) - Add interface type for QSFP28 50GE
|
||||||
|
* [#2936](https://github.com/digitalocean/netbox/issues/2936) - Fix device role selection showing duplicate first entry
|
||||||
|
* [#2998](https://github.com/digitalocean/netbox/issues/2998) - Limit device query to non-racked devices if no rack selected when creating a cable
|
||||||
|
* [#3001](https://github.com/digitalocean/netbox/issues/3001) - Fix API representation of ObjectChange `action` and add `changed_object_type`
|
||||||
|
* [#3014](https://github.com/digitalocean/netbox/issues/3014) - Fixes VM Role filtering
|
||||||
|
* [#3019](https://github.com/digitalocean/netbox/issues/3019) - Fix tag population when running NetBox within a path
|
||||||
|
* [#3022](https://github.com/digitalocean/netbox/issues/3022) - Add missing cable termination types to DCIM `_choices` endpoint
|
||||||
|
* [#3026](https://github.com/digitalocean/netbox/issues/3026) - Tweak prefix/IP filter forms to filter using VRF ID rather than route distinguisher
|
||||||
|
* [#3027](https://github.com/digitalocean/netbox/issues/3027) - Ignore empty local context data when rendering config contexts
|
||||||
|
* [#3032](https://github.com/digitalocean/netbox/issues/3032) - Save assigned tags when creating a new secret
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
v2.5.8 (2019-03-11)
|
v2.5.8 (2019-03-11)
|
||||||
|
|
||||||
## Enhancements
|
## Enhancements
|
||||||
|
@ -45,13 +45,13 @@ and run `upgrade.sh`.
|
|||||||
|
|
||||||
## Supported SDK
|
## Supported SDK
|
||||||
|
|
||||||
- [pynetbox](https://github.com/digitalocean/pynetbox) Python API client library for Netbox.
|
- [pynetbox](https://github.com/digitalocean/pynetbox) - A Python API client library for Netbox
|
||||||
|
|
||||||
## Community SDK
|
## Community SDK
|
||||||
|
|
||||||
- [netbox-client-ruby](https://github.com/ninech/netbox-client-ruby) A ruby client library for Netbox v2.
|
- [netbox-client-ruby](https://github.com/ninech/netbox-client-ruby) - A Ruby client library for Netbox
|
||||||
|
- [powerbox](https://github.com/BatmanAMA/powerbox) - A PowerShell library for Netbox
|
||||||
|
|
||||||
## Ansible Inventory
|
## Ansible Inventory
|
||||||
|
|
||||||
- [netbox-as-ansible-inventory](https://github.com/AAbouZaid/netbox-as-ansible-inventory) Ansible dynamic inventory script for Netbox.
|
- [netbox-as-ansible-inventory](https://github.com/AAbouZaid/netbox-as-ansible-inventory) - Ansible dynamic inventory script for Netbox
|
||||||
|
|
||||||
|
@ -261,7 +261,7 @@ A list of objects retrieved via the API can be filtered by passing one or more q
|
|||||||
GET /api/ipam/prefixes/?status=1
|
GET /api/ipam/prefixes/?status=1
|
||||||
```
|
```
|
||||||
|
|
||||||
The same filter can be incldued multiple times. These will effect a logical OR and return objects matching any of the given values. For example, the following will return all active and reserved prefixes:
|
Certain filters can be included multiple times within a single request. These will effect a logical OR and return objects matching any of the given values. For example, the following will return all active and reserved prefixes:
|
||||||
|
|
||||||
```
|
```
|
||||||
GET /api/ipam/prefixes/?status=1&status=2
|
GET /api/ipam/prefixes/?status=1&status=2
|
||||||
|
@ -283,6 +283,7 @@ REDIS = {
|
|||||||
'PASSWORD': '',
|
'PASSWORD': '',
|
||||||
'DATABASE': 0,
|
'DATABASE': 0,
|
||||||
'DEFAULT_TIMEOUT': 300,
|
'DEFAULT_TIMEOUT': 300,
|
||||||
|
'SSL': False,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -315,3 +316,9 @@ The TCP port to use when connecting to the Redis server.
|
|||||||
Default: None
|
Default: None
|
||||||
|
|
||||||
The password to use when authenticating to the Redis server (optional).
|
The password to use when authenticating to the Redis server (optional).
|
||||||
|
|
||||||
|
### SSL
|
||||||
|
|
||||||
|
Default: False
|
||||||
|
|
||||||
|
Use secure sockets layer to encrypt the connections to the Redis server.
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from drf_yasg.utils import swagger_serializer_method
|
from drf_yasg.utils import swagger_serializer_method
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.validators import UniqueTogetherValidator
|
from rest_framework.validators import UniqueTogetherValidator
|
||||||
@ -506,8 +507,12 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CableSerializer(ValidatedModelSerializer):
|
class CableSerializer(ValidatedModelSerializer):
|
||||||
termination_a_type = ContentTypeField()
|
termination_a_type = ContentTypeField(
|
||||||
termination_b_type = ContentTypeField()
|
queryset=ContentType.objects.filter(model__in=CABLE_TERMINATION_TYPES)
|
||||||
|
)
|
||||||
|
termination_b_type = ContentTypeField(
|
||||||
|
queryset=ContentType.objects.filter(model__in=CABLE_TERMINATION_TYPES)
|
||||||
|
)
|
||||||
termination_a = serializers.SerializerMethodField(read_only=True)
|
termination_a = serializers.SerializerMethodField(read_only=True)
|
||||||
termination_b = serializers.SerializerMethodField(read_only=True)
|
termination_b = serializers.SerializerMethodField(read_only=True)
|
||||||
status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False)
|
status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import F, Q
|
from django.db.models import F
|
||||||
from django.http import HttpResponseForbidden
|
from django.http import HttpResponseForbidden
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from drf_yasg import openapi
|
from drf_yasg import openapi
|
||||||
@ -35,7 +35,7 @@ from .exceptions import MissingFilterException
|
|||||||
|
|
||||||
class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
|
class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
|
||||||
fields = (
|
fields = (
|
||||||
(Cable, ['length_unit', 'status', 'type']),
|
(Cable, ['length_unit', 'status', 'termination_a_type', 'termination_b_type', 'type']),
|
||||||
(ConsolePort, ['connection_status']),
|
(ConsolePort, ['connection_status']),
|
||||||
(Device, ['face', 'status']),
|
(Device, ['face', 'status']),
|
||||||
(DeviceType, ['subdevice_role']),
|
(DeviceType, ['subdevice_role']),
|
||||||
|
@ -83,6 +83,7 @@ IFACE_FF_10GE_XENPAK = 1310
|
|||||||
IFACE_FF_10GE_X2 = 1320
|
IFACE_FF_10GE_X2 = 1320
|
||||||
IFACE_FF_25GE_SFP28 = 1350
|
IFACE_FF_25GE_SFP28 = 1350
|
||||||
IFACE_FF_40GE_QSFP_PLUS = 1400
|
IFACE_FF_40GE_QSFP_PLUS = 1400
|
||||||
|
IFACE_FF_50GE_QSFP28 = 1420
|
||||||
IFACE_FF_100GE_CFP = 1500
|
IFACE_FF_100GE_CFP = 1500
|
||||||
IFACE_FF_100GE_CFP2 = 1510
|
IFACE_FF_100GE_CFP2 = 1510
|
||||||
IFACE_FF_100GE_CFP4 = 1520
|
IFACE_FF_100GE_CFP4 = 1520
|
||||||
@ -164,6 +165,7 @@ IFACE_FF_CHOICES = [
|
|||||||
[IFACE_FF_10GE_X2, 'X2 (10GE)'],
|
[IFACE_FF_10GE_X2, 'X2 (10GE)'],
|
||||||
[IFACE_FF_25GE_SFP28, 'SFP28 (25GE)'],
|
[IFACE_FF_25GE_SFP28, 'SFP28 (25GE)'],
|
||||||
[IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
|
[IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
|
||||||
|
[IFACE_FF_50GE_QSFP28, 'QSFP28 (50GE)'],
|
||||||
[IFACE_FF_100GE_CFP, 'CFP (100GE)'],
|
[IFACE_FF_100GE_CFP, 'CFP (100GE)'],
|
||||||
[IFACE_FF_100GE_CFP2, 'CFP2 (100GE)'],
|
[IFACE_FF_100GE_CFP2, 'CFP2 (100GE)'],
|
||||||
[IFACE_FF_200GE_CFP2, 'CFP2 (200GE)'],
|
[IFACE_FF_200GE_CFP2, 'CFP2 (200GE)'],
|
||||||
|
@ -1700,7 +1700,6 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/dcim/device-roles/",
|
api_url="/api/dcim/device-roles/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
null_option=True,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
tenant = FilterChoiceField(
|
tenant = FilterChoiceField(
|
||||||
|
@ -64,11 +64,15 @@ class InterfaceManager(Manager):
|
|||||||
|
|
||||||
The original `name` field is considered in its entirety to serve as a fallback in the event interfaces do not
|
The original `name` field is considered in its entirety to serve as a fallback in the event interfaces do not
|
||||||
match any of the prescribed fields.
|
match any of the prescribed fields.
|
||||||
|
|
||||||
|
The `id` field is included to enforce deterministic ordering of interfaces in similar vein of other device
|
||||||
|
components.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sql_col = '{}.name'.format(self.model._meta.db_table)
|
sql_col = '{}.name'.format(self.model._meta.db_table)
|
||||||
ordering = [
|
ordering = [
|
||||||
'_slot', '_subslot', '_position', '_subposition', '_type', '_id', '_channel', '_vc', 'name',
|
'_slot', '_subslot', '_position', '_subposition', '_type', '_id', '_channel', '_vc', 'name', 'pk'
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
fields = {
|
fields = {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from drf_yasg.utils import swagger_serializer_method
|
from drf_yasg.utils import swagger_serializer_method
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
@ -89,7 +90,9 @@ class TagSerializer(ValidatedModelSerializer):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ImageAttachmentSerializer(ValidatedModelSerializer):
|
class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||||
content_type = ContentTypeField()
|
content_type = ContentTypeField(
|
||||||
|
queryset=ContentType.objects.all()
|
||||||
|
)
|
||||||
parent = serializers.SerializerMethodField(read_only=True)
|
parent = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -207,14 +210,25 @@ class ReportDetailSerializer(ReportSerializer):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class ObjectChangeSerializer(serializers.ModelSerializer):
|
class ObjectChangeSerializer(serializers.ModelSerializer):
|
||||||
user = NestedUserSerializer(read_only=True)
|
user = NestedUserSerializer(
|
||||||
content_type = ContentTypeField(read_only=True)
|
read_only=True
|
||||||
changed_object = serializers.SerializerMethodField(read_only=True)
|
)
|
||||||
|
action = ChoiceField(
|
||||||
|
choices=OBJECTCHANGE_ACTION_CHOICES,
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
changed_object_type = ContentTypeField(
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
changed_object = serializers.SerializerMethodField(
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ObjectChange
|
model = ObjectChange
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'time', 'user', 'user_name', 'request_id', 'action', 'content_type', 'changed_object', 'object_data',
|
'id', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object',
|
||||||
|
'object_data',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||||
|
@ -11,7 +11,7 @@ from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
|
|||||||
|
|
||||||
from extras import filters
|
from extras import filters
|
||||||
from extras.models import (
|
from extras.models import (
|
||||||
ConfigContext, CustomField, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
|
ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
|
||||||
Tag,
|
Tag,
|
||||||
)
|
)
|
||||||
from extras.reports import get_report, get_reports
|
from extras.reports import get_report, get_reports
|
||||||
@ -25,8 +25,8 @@ from . import serializers
|
|||||||
|
|
||||||
class ExtrasFieldChoicesViewSet(FieldChoicesViewSet):
|
class ExtrasFieldChoicesViewSet(FieldChoicesViewSet):
|
||||||
fields = (
|
fields = (
|
||||||
(CustomField, ['type']),
|
|
||||||
(Graph, ['type']),
|
(Graph, ['type']),
|
||||||
|
(ObjectChange, ['action']),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ class ExtrasConfig(AppConfig):
|
|||||||
port=settings.REDIS_PORT,
|
port=settings.REDIS_PORT,
|
||||||
db=settings.REDIS_DATABASE,
|
db=settings.REDIS_DATABASE,
|
||||||
password=settings.REDIS_PASSWORD or None,
|
password=settings.REDIS_PASSWORD or None,
|
||||||
|
ssl=settings.REDIS_SSL,
|
||||||
)
|
)
|
||||||
rs.ping()
|
rs.ping()
|
||||||
except redis.exceptions.ConnectionError:
|
except redis.exceptions.ConnectionError:
|
||||||
|
@ -37,7 +37,7 @@ def _record_object_deleted(request, instance, **kwargs):
|
|||||||
if hasattr(instance, 'log_change'):
|
if hasattr(instance, 'log_change'):
|
||||||
instance.log_change(request.user, request.id, OBJECTCHANGE_ACTION_DELETE)
|
instance.log_change(request.user, request.id, OBJECTCHANGE_ACTION_DELETE)
|
||||||
|
|
||||||
enqueue_webhooks(instance, OBJECTCHANGE_ACTION_DELETE)
|
enqueue_webhooks(instance, request.user, request.id, OBJECTCHANGE_ACTION_DELETE)
|
||||||
|
|
||||||
|
|
||||||
class ObjectChangeMiddleware(object):
|
class ObjectChangeMiddleware(object):
|
||||||
@ -83,7 +83,7 @@ class ObjectChangeMiddleware(object):
|
|||||||
obj.log_change(request.user, request.id, action)
|
obj.log_change(request.user, request.id, action)
|
||||||
|
|
||||||
# Enqueue webhooks
|
# Enqueue webhooks
|
||||||
enqueue_webhooks(obj, action)
|
enqueue_webhooks(obj, request.user, request.id, action)
|
||||||
|
|
||||||
# Housekeeping: 1% chance of clearing out expired ObjectChanges
|
# Housekeeping: 1% chance of clearing out expired ObjectChanges
|
||||||
if _thread_locals.changed_objects and settings.CHANGELOG_RETENTION and random.randint(1, 100) == 1:
|
if _thread_locals.changed_objects and settings.CHANGELOG_RETENTION and random.randint(1, 100) == 1:
|
||||||
|
@ -722,7 +722,7 @@ class ConfigContextModel(models.Model):
|
|||||||
data = deepmerge(data, context.data)
|
data = deepmerge(data, context.data)
|
||||||
|
|
||||||
# If the object has local config context data defined, merge it last
|
# If the object has local config context data defined, merge it last
|
||||||
if self.local_context_data is not None:
|
if self.local_context_data:
|
||||||
data = deepmerge(data, self.local_context_data)
|
data = deepmerge(data, self.local_context_data)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
@ -9,7 +9,7 @@ from utilities.api import get_serializer_for_model
|
|||||||
from .constants import WEBHOOK_MODELS
|
from .constants import WEBHOOK_MODELS
|
||||||
|
|
||||||
|
|
||||||
def enqueue_webhooks(instance, action):
|
def enqueue_webhooks(instance, user, request_id, action):
|
||||||
"""
|
"""
|
||||||
Find Webhook(s) assigned to this instance + action and enqueue them
|
Find Webhook(s) assigned to this instance + action and enqueue them
|
||||||
to be processed
|
to be processed
|
||||||
@ -47,5 +47,7 @@ def enqueue_webhooks(instance, action):
|
|||||||
serializer.data,
|
serializer.data,
|
||||||
instance._meta.model_name,
|
instance._meta.model_name,
|
||||||
action,
|
action,
|
||||||
str(datetime.datetime.now())
|
str(datetime.datetime.now()),
|
||||||
|
user.username,
|
||||||
|
request_id
|
||||||
)
|
)
|
||||||
|
@ -10,7 +10,7 @@ from extras.constants import WEBHOOK_CT_JSON, WEBHOOK_CT_X_WWW_FORM_ENCODED, OBJ
|
|||||||
|
|
||||||
|
|
||||||
@job('default')
|
@job('default')
|
||||||
def process_webhook(webhook, data, model_name, event, timestamp):
|
def process_webhook(webhook, data, model_name, event, timestamp, username, request_id):
|
||||||
"""
|
"""
|
||||||
Make a POST request to the defined Webhook
|
Make a POST request to the defined Webhook
|
||||||
"""
|
"""
|
||||||
@ -18,6 +18,8 @@ def process_webhook(webhook, data, model_name, event, timestamp):
|
|||||||
'event': dict(OBJECTCHANGE_ACTION_CHOICES)[event].lower(),
|
'event': dict(OBJECTCHANGE_ACTION_CHOICES)[event].lower(),
|
||||||
'timestamp': timestamp,
|
'timestamp': timestamp,
|
||||||
'model': model_name,
|
'model': model_name,
|
||||||
|
'username': username,
|
||||||
|
'request_id': request_id,
|
||||||
'data': data
|
'data': data
|
||||||
}
|
}
|
||||||
headers = {
|
headers = {
|
||||||
|
@ -128,6 +128,7 @@ class VLANSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class PrefixSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
class PrefixSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||||
|
family = ChoiceField(choices=AF_CHOICES, read_only=True)
|
||||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||||
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)
|
||||||
@ -189,6 +190,7 @@ class IPAddressInterfaceSerializer(WritableNestedSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||||
|
family = ChoiceField(choices=AF_CHOICES, read_only=True)
|
||||||
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)
|
||||||
|
@ -524,14 +524,12 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
label='Mask length',
|
label='Mask length',
|
||||||
widget=StaticSelect2()
|
widget=StaticSelect2()
|
||||||
)
|
)
|
||||||
vrf = FilterChoiceField(
|
vrf_id = FilterChoiceField(
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
to_field_name='rd',
|
|
||||||
label='VRF',
|
label='VRF',
|
||||||
null_label='-- Global --',
|
null_label='-- Global --',
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/ipam/vrfs/",
|
api_url="/api/ipam/vrfs/",
|
||||||
value_field="rd",
|
|
||||||
null_option=True,
|
null_option=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -973,14 +971,12 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
label='Mask length',
|
label='Mask length',
|
||||||
widget=StaticSelect2()
|
widget=StaticSelect2()
|
||||||
)
|
)
|
||||||
vrf = FilterChoiceField(
|
vrf_id = FilterChoiceField(
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
to_field_name='rd',
|
|
||||||
label='VRF',
|
label='VRF',
|
||||||
null_label='-- Global --',
|
null_label='-- Global --',
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/ipam/vrfs/",
|
api_url="/api/ipam/vrfs/",
|
||||||
value_field="rd",
|
|
||||||
null_option=True,
|
null_option=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -132,6 +132,7 @@ REDIS = {
|
|||||||
'PASSWORD': '',
|
'PASSWORD': '',
|
||||||
'DATABASE': 0,
|
'DATABASE': 0,
|
||||||
'DEFAULT_TIMEOUT': 300,
|
'DEFAULT_TIMEOUT': 300,
|
||||||
|
'SSL': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
# The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of
|
# The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of
|
||||||
|
@ -131,6 +131,7 @@ REDIS_PORT = REDIS.get('PORT', 6379)
|
|||||||
REDIS_PASSWORD = REDIS.get('PASSWORD', '')
|
REDIS_PASSWORD = REDIS.get('PASSWORD', '')
|
||||||
REDIS_DATABASE = REDIS.get('DATABASE', 0)
|
REDIS_DATABASE = REDIS.get('DATABASE', 0)
|
||||||
REDIS_DEFAULT_TIMEOUT = REDIS.get('DEFAULT_TIMEOUT', 300)
|
REDIS_DEFAULT_TIMEOUT = REDIS.get('DEFAULT_TIMEOUT', 300)
|
||||||
|
REDIS_SSL = REDIS.get('SSL', False)
|
||||||
|
|
||||||
# Email
|
# Email
|
||||||
EMAIL_HOST = EMAIL.get('SERVER')
|
EMAIL_HOST = EMAIL.get('SERVER')
|
||||||
@ -291,6 +292,7 @@ RQ_QUEUES = {
|
|||||||
'DB': REDIS_DATABASE,
|
'DB': REDIS_DATABASE,
|
||||||
'PASSWORD': REDIS_PASSWORD,
|
'PASSWORD': REDIS_PASSWORD,
|
||||||
'DEFAULT_TIMEOUT': REDIS_DEFAULT_TIMEOUT,
|
'DEFAULT_TIMEOUT': REDIS_DEFAULT_TIMEOUT,
|
||||||
|
'SSL': REDIS_SSL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,10 +155,13 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
filter_for_elements.each(function(index, filter_for_element) {
|
filter_for_elements.each(function(index, filter_for_element) {
|
||||||
var param_name = $(filter_for_element).attr(attr_name);
|
var param_name = $(filter_for_element).attr(attr_name);
|
||||||
|
var is_nullable = $(filter_for_element).attr("nullable");
|
||||||
var value = $(filter_for_element).val();
|
var value = $(filter_for_element).val();
|
||||||
|
|
||||||
if (param_name && value) {
|
if (param_name && value) {
|
||||||
parameters[param_name] = value;
|
parameters[param_name] = value;
|
||||||
|
} else if (param_name && is_nullable) {
|
||||||
|
parameters[param_name] = "null";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -247,7 +250,7 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
ajax: {
|
ajax: {
|
||||||
delay: 250,
|
delay: 250,
|
||||||
url: "/api/extras/tags/",
|
url: netbox_api_path + "extras/tags/",
|
||||||
|
|
||||||
data: function(params) {
|
data: function(params) {
|
||||||
// Paging. Note that `params.page` indexes at 1
|
// Paging. Note that `params.page` indexes at 1
|
||||||
|
@ -120,6 +120,8 @@ def secret_add(request, pk):
|
|||||||
secret.plaintext = str(form.cleaned_data['plaintext'])
|
secret.plaintext = str(form.cleaned_data['plaintext'])
|
||||||
secret.encrypt(master_key)
|
secret.encrypt(master_key)
|
||||||
secret.save()
|
secret.save()
|
||||||
|
form.save_m2m()
|
||||||
|
|
||||||
messages.success(request, "Added new secret: {}.".format(secret))
|
messages.success(request, "Added new secret: {}.".format(secret))
|
||||||
if '_addanother' in request.POST:
|
if '_addanother' in request.POST:
|
||||||
return redirect('dcim:device_addsecret', pk=device.pk)
|
return redirect('dcim:device_addsecret', pk=device.pk)
|
||||||
|
@ -21,6 +21,10 @@ class ServiceUnavailable(APIException):
|
|||||||
default_detail = "Service temporarily unavailable, please try again later."
|
default_detail = "Service temporarily unavailable, please try again later."
|
||||||
|
|
||||||
|
|
||||||
|
class SerializerNotFound(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_serializer_for_model(model, prefix=''):
|
def get_serializer_for_model(model, prefix=''):
|
||||||
"""
|
"""
|
||||||
Dynamically resolve and return the appropriate serializer for a model.
|
Dynamically resolve and return the appropriate serializer for a model.
|
||||||
@ -32,7 +36,9 @@ def get_serializer_for_model(model, prefix=''):
|
|||||||
try:
|
try:
|
||||||
return dynamic_import(serializer_name)
|
return dynamic_import(serializer_name)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return None
|
raise SerializerNotFound(
|
||||||
|
"Could not determine serializer for {}.{} with prefix '{}'".format(app_name, model_name, prefix)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -100,6 +106,10 @@ class ChoiceField(Field):
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def choices(self):
|
||||||
|
return self._choices
|
||||||
|
|
||||||
|
|
||||||
class ContentTypeField(RelatedField):
|
class ContentTypeField(RelatedField):
|
||||||
"""
|
"""
|
||||||
@ -110,10 +120,6 @@ class ContentTypeField(RelatedField):
|
|||||||
"invalid": "Invalid value. Specify a content type as '<app_label>.<model_name>'.",
|
"invalid": "Invalid value. Specify a content type as '<app_label>.<model_name>'.",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Can't set this as an attribute because it raises an exception when the field is read-only
|
|
||||||
def get_queryset(self):
|
|
||||||
return ContentType.objects.all()
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
try:
|
try:
|
||||||
app_label, model = data.split('.')
|
app_label, model = data.split('.')
|
||||||
@ -234,9 +240,10 @@ class ModelViewSet(_ModelViewSet):
|
|||||||
# exists
|
# exists
|
||||||
request = self.get_serializer_context()['request']
|
request = self.get_serializer_context()['request']
|
||||||
if request.query_params.get('brief', False):
|
if request.query_params.get('brief', False):
|
||||||
serializer_class = get_serializer_for_model(self.queryset.model, prefix='Nested')
|
try:
|
||||||
if serializer_class is not None:
|
return get_serializer_for_model(self.queryset.model, prefix='Nested')
|
||||||
return serializer_class
|
except SerializerNotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
# Fall back to the hard-coded serializer class
|
# Fall back to the hard-coded serializer class
|
||||||
return self.serializer_class
|
return self.serializer_class
|
||||||
@ -256,10 +263,14 @@ class FieldChoicesViewSet(ViewSet):
|
|||||||
self._fields = OrderedDict()
|
self._fields = OrderedDict()
|
||||||
for cls, field_list in self.fields:
|
for cls, field_list in self.fields:
|
||||||
for field_name in field_list:
|
for field_name in field_list:
|
||||||
|
|
||||||
model_name = cls._meta.verbose_name.lower().replace(' ', '-')
|
model_name = cls._meta.verbose_name.lower().replace(' ', '-')
|
||||||
key = ':'.join([model_name, field_name])
|
key = ':'.join([model_name, field_name])
|
||||||
|
|
||||||
|
serializer = get_serializer_for_model(cls)()
|
||||||
choices = []
|
choices = []
|
||||||
for k, v in cls._meta.get_field(field_name).choices:
|
|
||||||
|
for k, v in serializer.get_fields()[field_name].choices.items():
|
||||||
if type(v) in [list, tuple]:
|
if type(v) in [list, tuple]:
|
||||||
for k2, v2 in v:
|
for k2, v2 in v:
|
||||||
choices.append({
|
choices.append({
|
||||||
|
@ -348,7 +348,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
"role": APISelect(
|
"role": APISelect(
|
||||||
api_url="/api/dcim/device-roles/",
|
api_url="/api/dcim/device-roles/",
|
||||||
additional_query_params={
|
additional_query_params={
|
||||||
"vm_role": "true"
|
"vm_role": "True"
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
'primary_ip4': StaticSelect2(),
|
'primary_ip4': StaticSelect2(),
|
||||||
@ -480,7 +480,7 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB
|
|||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url="/api/dcim/device-roles/",
|
api_url="/api/dcim/device-roles/",
|
||||||
additional_query_params={
|
additional_query_params={
|
||||||
"vm_role": "true"
|
"vm_role": "True"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -582,7 +582,7 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
value_field="slug",
|
value_field="slug",
|
||||||
null_option=True,
|
null_option=True,
|
||||||
additional_query_params={
|
additional_query_params={
|
||||||
'vm_role': 'true'
|
'vm_role': "True"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
Reference in New Issue
Block a user