Merge pull request #2653 from digitalocean/develop

Release v2.4.9
This commit is contained in:
Jeremy Stretch 2018-12-07 10:25:46 -05:00 committed by GitHub
commit bf0083552d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 297 additions and 48 deletions

View File

@ -1,3 +1,24 @@
v2.4.9 (2018-12-07)
## Enhancements
* [#2089](https://github.com/digitalocean/netbox/issues/2089) - Add SONET interface form factors
* [#2495](https://github.com/digitalocean/netbox/issues/2495) - Enable deep-merging of config context data
* [#2597](https://github.com/digitalocean/netbox/issues/2597) - Add FibreChannel SFP28 (32GFC) interface form factor
## Bug Fixes
* [#2400](https://github.com/digitalocean/netbox/issues/2400) - Correct representation of nested object assignment in API docs
* [#2576](https://github.com/digitalocean/netbox/issues/2576) - Correct type for count_* fields in site API representation
* [#2606](https://github.com/digitalocean/netbox/issues/2606) - Fixed filtering for interfaces with a virtual form factor
* [#2611](https://github.com/digitalocean/netbox/issues/2611) - Fix error handling when assigning a clustered device to a different site
* [#2613](https://github.com/digitalocean/netbox/issues/2613) - Decrease live search minimum characters to three
* [#2615](https://github.com/digitalocean/netbox/issues/2615) - Tweak live search widget to use brief format for API requests
* [#2623](https://github.com/digitalocean/netbox/issues/2623) - Removed the need to pass the model class to the rqworker process for webhooks
* [#2634](https://github.com/digitalocean/netbox/issues/2634) - Enforce consistent representation of unnamed devices in rack view
---
v2.4.8 (2018-11-20) v2.4.8 (2018-11-20)
## Enhancements ## Enhancements

View File

@ -44,7 +44,11 @@ If you're adding a relational field (e.g. `ForeignKey`) and intend to include th
Extend the model's API serializer in `<app>.api.serializers` to include the new field. In most cases, it will not be necessary to also extend the nested serializer, which produces a minimal represenation of the model. Extend the model's API serializer in `<app>.api.serializers` to include the new field. In most cases, it will not be necessary to also extend the nested serializer, which produces a minimal represenation of the model.
### 6. Add field to forms ### 6. Add choices to API view
If the new field has static choices, add it to the `FieldChoicesViewSet` for the app.
### 7. Add field to forms
Extend any forms to include the new field as appropriate. Common forms include: Extend any forms to include the new field as appropriate. Common forms include:
@ -53,18 +57,18 @@ Extend any forms to include the new field as appropriate. Common forms include:
* **CSV import** - The form used when bulk importing objects in CSV format * **CSV import** - The form used when bulk importing objects in CSV format
* **Filter** - Displays the options available for filtering a list of objects (both UI and API) * **Filter** - Displays the options available for filtering a list of objects (both UI and API)
### 7. Extend object filter set ### 8. Extend object filter set
If the new field should be filterable, add it to the `FilterSet` for the model. If the field should be searchable, remember to reference it in the FilterSet's `search()` method. If the new field should be filterable, add it to the `FilterSet` for the model. If the field should be searchable, remember to reference it in the FilterSet's `search()` method.
### 8. Add column to object table ### 9. Add column to object table
If the new field will be included in the object list view, add a column to the model's table. For simple fields, adding the field name to `Meta.fields` will be sufficient. More complex fields may require explicitly declaring a new column. If the new field will be included in the object list view, add a column to the model's table. For simple fields, adding the field name to `Meta.fields` will be sufficient. More complex fields may require explicitly declaring a new column.
### 9. Update the UI templates ### 10. Update the UI templates
Edit the object's view template to display the new field. There may also be a custom add/edit form template that needs to be updated. Edit the object's view template to display the new field. There may also be a custom add/edit form template that needs to be updated.
### 10. Adjust API and model tests ### 11. Adjust API and model tests
Extend the model and/or API tests to verify that the new field and any accompanying validation logic perform as expected. This is especially important for relational fields. Extend the model and/or API tests to verify that the new field and any accompanying validation logic perform as expected. This is especially important for relational fields.

View File

@ -28,6 +28,19 @@ To invoke `pycodestyle` manually, run:
pycodestyle --ignore=W504,E501 netbox/ pycodestyle --ignore=W504,E501 netbox/
``` ```
## Introducing New Dependencies
The introduction of a new dependency is best avoided unless it is absolutely necessary. For small features, it's generally preferable to replicate functionality within the NetBox code base rather than to introduce reliance on an external project. This reduces both the burden of tracking new releases and our exposure to outside bugs and attacks.
If there's a strong case for introducing a new depdency, it must meet the following criteria:
* Its complete source code must be published and freely accessible without registration.
* Its license must be conducive to inclusion in an open source project.
* It must be actively maintained, with no longer than one year between releases.
* It must be available via the [Python Package Index](https://pypi.org/) (PyPI).
When adding a new dependency, a short description of the package and the URL of its code repository must be added to `base_requirements.txt`. Additionally, a line specifying the package name pinned to the current stable release must be added to `requirements.txt`. This ensures that NetBox will install only the known-good release and simplify support efforts.
## General Guidance ## General Guidance
* When in doubt, remain consistent: It is better to be consistently incorrect than inconsistently correct. If you notice in the course of unrelated work a pattern that should be corrected, continue to follow the pattern for now and open a bug so that the entire code base can be evaluated at a later point. * When in doubt, remain consistent: It is better to be consistently incorrect than inconsistently correct. If you notice in the course of unrelated work a pattern that should be corrected, continue to follow the pattern for now and open a bug so that the entire code base can be evaluated at a later point.

View File

@ -246,13 +246,13 @@ At this point, NetBox should be able to run. We can verify this by starting a de
Performing system checks... Performing system checks...
System check identified no issues (0 silenced). System check identified no issues (0 silenced).
June 17, 2016 - 16:17:36 November 28, 2018 - 09:33:45
Django version 1.9.7, using settings 'netbox.settings' Django version 2.0.9, using settings 'netbox.settings'
Starting development server at http://0.0.0.0:8000/ Starting development server at http://0.0.0.0:8000/
Quit the server with CONTROL-C. Quit the server with CONTROL-C.
``` ```
Now if we navigate to the name or IP of the server (as defined in `ALLOWED_HOSTS`) we should be greeted with the NetBox home page. Note that this built-in web service is for development and testing purposes only. **It is not suited for production use.** Next, connect to the name or IP of the server (as defined in `ALLOWED_HOSTS`) on port 8000; for example, <http://127.0.0.1:8000/>. You should be greeted with the NetBox home page. Note that this built-in web service is for development and testing purposes only. **It is not suited for production use.**
!!! warning !!! warning
If the test service does not run, or you cannot reach the NetBox home page, something has gone wrong. Do not proceed with the rest of this guide until the installation has been corrected. If the test service does not run, or you cannot reach the NetBox home page, something has gone wrong. Do not proceed with the rest of this guide until the installation has been corrected.

View File

@ -36,3 +36,9 @@ If using LDAP authentication, install the `django-auth-ldap` package:
```no-highlight ```no-highlight
# pip3 install django-auth-ldap # pip3 install django-auth-ldap
``` ```
If using Webhooks, install the `django-rq` package:
```no-highlight
# pip3 install django-rq
```

View File

@ -38,6 +38,7 @@ pages:
- Change Logging: 'additional-features/change-logging.md' - Change Logging: 'additional-features/change-logging.md'
- Administration: - Administration:
- Replicating NetBox: 'administration/replicating-netbox.md' - Replicating NetBox: 'administration/replicating-netbox.md'
- NetBox Shell: 'administration/netbox-shell.md'
- API: - API:
- Overview: 'api/overview.md' - Overview: 'api/overview.md'
- Authentication: 'api/authentication.md' - Authentication: 'api/authentication.md'

View File

@ -56,6 +56,11 @@ class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
time_zone = TimeZoneField(required=False) time_zone = TimeZoneField(required=False)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
count_prefixes = serializers.IntegerField(read_only=True)
count_vlans = serializers.IntegerField(read_only=True)
count_racks = serializers.IntegerField(read_only=True)
count_devices = serializers.IntegerField(read_only=True)
count_circuits = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = Site model = Site

View File

@ -76,12 +76,21 @@ IFACE_FF_80211G = 2610
IFACE_FF_80211N = 2620 IFACE_FF_80211N = 2620
IFACE_FF_80211AC = 2630 IFACE_FF_80211AC = 2630
IFACE_FF_80211AD = 2640 IFACE_FF_80211AD = 2640
# SONET
IFACE_FF_SONET_OC3 = 6100
IFACE_FF_SONET_OC12 = 6200
IFACE_FF_SONET_OC48 = 6300
IFACE_FF_SONET_OC192 = 6400
IFACE_FF_SONET_OC768 = 6500
IFACE_FF_SONET_OC1920 = 6600
IFACE_FF_SONET_OC3840 = 6700
# Fibrechannel # Fibrechannel
IFACE_FF_1GFC_SFP = 3010 IFACE_FF_1GFC_SFP = 3010
IFACE_FF_2GFC_SFP = 3020 IFACE_FF_2GFC_SFP = 3020
IFACE_FF_4GFC_SFP = 3040 IFACE_FF_4GFC_SFP = 3040
IFACE_FF_8GFC_SFP_PLUS = 3080 IFACE_FF_8GFC_SFP_PLUS = 3080
IFACE_FF_16GFC_SFP_PLUS = 3160 IFACE_FF_16GFC_SFP_PLUS = 3160
IFACE_FF_32GFC_SFP28 = 3320
# Serial # Serial
IFACE_FF_T1 = 4000 IFACE_FF_T1 = 4000
IFACE_FF_E1 = 4010 IFACE_FF_E1 = 4010
@ -146,6 +155,18 @@ IFACE_FF_CHOICES = [
[IFACE_FF_80211AD, 'IEEE 802.11ad'], [IFACE_FF_80211AD, 'IEEE 802.11ad'],
] ]
], ],
[
'SONET',
[
[IFACE_FF_SONET_OC3, 'OC-3/STM-1'],
[IFACE_FF_SONET_OC12, 'OC-12/STM-4'],
[IFACE_FF_SONET_OC48, 'OC-48/STM-16'],
[IFACE_FF_SONET_OC192, 'OC-192/STM-64'],
[IFACE_FF_SONET_OC768, 'OC-768/STM-256'],
[IFACE_FF_SONET_OC1920, 'OC-1920/STM-640'],
[IFACE_FF_SONET_OC3840, 'OC-3840/STM-1234'],
]
],
[ [
'FibreChannel', 'FibreChannel',
[ [
@ -154,6 +175,7 @@ IFACE_FF_CHOICES = [
[IFACE_FF_4GFC_SFP, 'SFP (4GFC)'], [IFACE_FF_4GFC_SFP, 'SFP (4GFC)'],
[IFACE_FF_8GFC_SFP_PLUS, 'SFP+ (8GFC)'], [IFACE_FF_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
[IFACE_FF_16GFC_SFP_PLUS, 'SFP+ (16GFC)'], [IFACE_FF_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
[IFACE_FF_32GFC_SFP28, 'SFP28 (32GFC)'],
] ]
], ],
[ [

View File

@ -13,7 +13,7 @@ from utilities.filters import NullableCharFieldFilter, NumericInFilter, TagFilte
from virtualization.models import Cluster from virtualization.models import Cluster
from .constants import ( from .constants import (
DEVICE_STATUS_CHOICES, IFACE_FF_LAG, NONCONNECTABLE_IFACE_TYPES, SITE_STATUS_CHOICES, VIRTUAL_IFACE_TYPES, DEVICE_STATUS_CHOICES, IFACE_FF_LAG, NONCONNECTABLE_IFACE_TYPES, SITE_STATUS_CHOICES, VIRTUAL_IFACE_TYPES,
WIRELESS_IFACE_TYPES, WIRELESS_IFACE_TYPES, IFACE_FF_CHOICES,
) )
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
@ -652,10 +652,14 @@ class InterfaceFilter(django_filters.FilterSet):
method='filter_vlan', method='filter_vlan',
label='Assigned VID' label='Assigned VID'
) )
form_factor = django_filters.MultipleChoiceFilter(
choices=IFACE_FF_CHOICES,
null_value=None
)
class Meta: class Meta:
model = Interface model = Interface
fields = ['name', 'form_factor', 'enabled', 'mtu', 'mgmt_only'] fields = ['name', 'enabled', 'mtu', 'mgmt_only']
def filter_device(self, queryset, name, value): def filter_device(self, queryset, name, value):
try: try:

View File

@ -20,7 +20,7 @@ from utilities.forms import (
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FilterTreeNodeMultipleChoiceField, ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FilterTreeNodeMultipleChoiceField,
FlexibleModelChoiceField, JSONField, Livesearch, SelectWithDisabled, SelectWithPK, SmallTextarea, SlugField, FlexibleModelChoiceField, JSONField, Livesearch, SelectWithDisabled, SelectWithPK, SmallTextarea, SlugField,
) )
from virtualization.models import Cluster from virtualization.models import Cluster, ClusterGroup
from .constants import ( from .constants import (
CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, DEVICE_STATUS_CHOICES, IFACE_FF_CHOICES, IFACE_FF_LAG, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, DEVICE_STATUS_CHOICES, IFACE_FF_CHOICES, IFACE_FF_LAG,
IFACE_MODE_ACCESS, IFACE_MODE_CHOICES, IFACE_MODE_TAGGED_ALL, IFACE_ORDERING_CHOICES, RACK_FACE_CHOICES, IFACE_MODE_ACCESS, IFACE_MODE_CHOICES, IFACE_MODE_TAGGED_ALL, IFACE_ORDERING_CHOICES, RACK_FACE_CHOICES,
@ -820,6 +820,23 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
display_field='model' display_field='model'
) )
) )
cluster_group = forms.ModelChoiceField(
queryset=ClusterGroup.objects.all(),
required=False,
widget=forms.Select(
attrs={'filter-for': 'cluster', 'nullable': 'true'}
)
)
cluster = ChainedModelChoiceField(
queryset=Cluster.objects.all(),
chains=(
('group', 'cluster_group'),
),
required=False,
widget=APISelect(
api_url='/api/virtualization/clusters/?group_id={{cluster_group}}',
)
)
comments = CommentField() comments = CommentField()
tags = TagField(required=False) tags = TagField(required=False)
local_context_data = JSONField(required=False) local_context_data = JSONField(required=False)
@ -828,8 +845,8 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
model = Device model = Device
fields = [ fields = [
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face', 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face',
'status', 'platform', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments', 'tags', 'status', 'platform', 'primary_ip4', 'primary_ip6', 'cluster_group', 'cluster', 'tenant_group', 'tenant',
'local_context_data' 'comments', 'tags', 'local_context_data'
] ]
help_texts = { help_texts = {
'device_role': "The function this device serves", 'device_role': "The function this device serves",

View File

@ -19,11 +19,11 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='interface', model_name='interface',
name='form_factor', 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), 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']]], ['SONET', [[6100, 'OC-3/STM-1'], [6200, 'OC-12/STM-4'], [6300, 'OC-48/STM-16'], [6400, 'OC-192/STM-64'], [6500, 'OC-768/STM-256'], [6600, 'OC-1920/STM-640'], [6700, 'OC-3840/STM-1234']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)'], [3320, 'SFP28 (32GFC)']]], ['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( migrations.AlterField(
model_name='interfacetemplate', model_name='interfacetemplate',
name='form_factor', 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), 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']]], ['SONET', [[6100, 'OC-3/STM-1'], [6200, 'OC-12/STM-4'], [6300, 'OC-48/STM-16'], [6400, 'OC-192/STM-64'], [6500, 'OC-768/STM-256'], [6600, 'OC-1920/STM-640'], [6700, 'OC-3840/STM-1234']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)'], [3320, 'SFP28 (32GFC)']]], ['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),
), ),
] ]

View File

@ -2,9 +2,6 @@
# Generated by Django 1.11.14 on 2018-07-31 02:19 # Generated by Django 1.11.14 on 2018-07-31 02:19
from __future__ import unicode_literals from __future__ import unicode_literals
import re
from distutils.version import StrictVersion
from django.conf import settings from django.conf import settings
import django.contrib.postgres.fields.jsonb import django.contrib.postgres.fields.jsonb
from django.db import connection, migrations, models from django.db import connection, migrations, models
@ -19,13 +16,14 @@ def verify_postgresql_version(apps, schema_editor):
""" """
Verify that PostgreSQL is version 9.4 or higher. Verify that PostgreSQL is version 9.4 or higher.
""" """
# https://www.postgresql.org/docs/current/libpq-status.html#LIBPQ-PQSERVERVERSION
DB_MINIMUM_VERSION = 90400 # 9.4.0
try: try:
with connection.cursor() as cursor: pg_version = connection.pg_version
cursor.execute("SELECT VERSION()")
row = cursor.fetchone() if pg_version < DB_MINIMUM_VERSION:
pg_version = re.match(r'^PostgreSQL (\d+\.\d+(\.\d+)?)', row[0]).group(1) raise Exception("PostgreSQL 9.4.0 ({}) or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(DB_MINIMUM_VERSION, pg_version))
if StrictVersion(pg_version) < StrictVersion('9.4.0'):
raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version))
# Skip if the database is missing (e.g. for CI testing) or misconfigured. # Skip if the database is missing (e.g. for CI testing) or misconfigured.
except OperationalError: except OperationalError:

View File

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-26 21:25 # Generated by Django 1.11.4 on 2017-09-26 21:25
from __future__ import unicode_literals from __future__ import unicode_literals
from distutils.version import StrictVersion
import re
from django.conf import settings from django.conf import settings
import django.contrib.postgres.fields.jsonb import django.contrib.postgres.fields.jsonb
@ -15,13 +13,14 @@ def verify_postgresql_version(apps, schema_editor):
""" """
Verify that PostgreSQL is version 9.4 or higher. Verify that PostgreSQL is version 9.4 or higher.
""" """
# https://www.postgresql.org/docs/current/libpq-status.html#LIBPQ-PQSERVERVERSION
DB_MINIMUM_VERSION = 90400 # 9.4.0
try: try:
with connection.cursor() as cursor: pg_version = connection.pg_version
cursor.execute("SELECT VERSION()")
row = cursor.fetchone() if pg_version < DB_MINIMUM_VERSION:
pg_version = re.match(r'^PostgreSQL (\d+\.\d+(\.\d+)?)', row[0]).group(1) raise Exception("PostgreSQL 9.4.0 ({}) or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(DB_MINIMUM_VERSION, pg_version))
if StrictVersion(pg_version) < StrictVersion('9.4.0'):
raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version))
# Skip if the database is missing (e.g. for CI testing) or misconfigured. # Skip if the database is missing (e.g. for CI testing) or misconfigured.
except OperationalError: except OperationalError:

View File

@ -18,7 +18,7 @@ from django.utils.encoding import python_2_unicode_compatible
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from dcim.constants import CONNECTION_STATUS_CONNECTED from dcim.constants import CONNECTION_STATUS_CONNECTED
from utilities.utils import foreground_color from utilities.utils import deepmerge, foreground_color
from .constants import * from .constants import *
from .querysets import ConfigContextQuerySet from .querysets import ConfigContextQuerySet
@ -727,11 +727,11 @@ class ConfigContextModel(models.Model):
# Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs # Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
data = OrderedDict() data = OrderedDict()
for context in ConfigContext.objects.get_for_object(self): for context in ConfigContext.objects.get_for_object(self):
data.update(context.data) data = deepmerge(data, context.data)
# If the object has local config context data defined, that data overwrites all rendered data # 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 is not None:
data.update(self.local_context_data) data = deepmerge(data, self.local_context_data)
return data return data

View File

@ -45,7 +45,7 @@ def enqueue_webhooks(instance, action):
"extras.webhooks_worker.process_webhook", "extras.webhooks_worker.process_webhook",
webhook, webhook,
serializer.data, serializer.data,
instance.__class__, instance._meta.model_name,
action, action,
str(datetime.datetime.now()) str(datetime.datetime.now())
) )

View File

@ -10,14 +10,14 @@ from extras.constants import WEBHOOK_CT_JSON, WEBHOOK_CT_X_WWW_FORM_ENCODED, OBJ
@job('default') @job('default')
def process_webhook(webhook, data, model_class, event, timestamp): def process_webhook(webhook, data, model_name, event, timestamp):
""" """
Make a POST request to the defined Webhook Make a POST request to the defined Webhook
""" """
payload = { payload = {
'event': dict(OBJECTCHANGE_ACTION_CHOICES)[event].lower(), 'event': dict(OBJECTCHANGE_ACTION_CHOICES)[event].lower(),
'timestamp': timestamp, 'timestamp': timestamp,
'model': model_class._meta.model_name, 'model': model_name,
'data': data 'data': data
} }
headers = { headers = {

View File

@ -22,7 +22,7 @@ if sys.version_info[0] < 3:
DeprecationWarning DeprecationWarning
) )
VERSION = '2.4.8' VERSION = '2.4.9'
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -275,9 +275,12 @@ RQ_QUEUES = {
# drf_yasg settings for Swagger # drf_yasg settings for Swagger
SWAGGER_SETTINGS = { SWAGGER_SETTINGS = {
'DEFAULT_AUTO_SCHEMA_CLASS': 'utilities.custom_inspectors.NetBoxSwaggerAutoSchema',
'DEFAULT_FIELD_INSPECTORS': [ 'DEFAULT_FIELD_INSPECTORS': [
'utilities.custom_inspectors.NullableBooleanFieldInspector', 'utilities.custom_inspectors.NullableBooleanFieldInspector',
'utilities.custom_inspectors.CustomChoiceFieldInspector', 'utilities.custom_inspectors.CustomChoiceFieldInspector',
'utilities.custom_inspectors.TagListFieldInspector',
'utilities.custom_inspectors.SerializedPKRelatedFieldInspector',
'drf_yasg.inspectors.CamelCaseJSONFilter', 'drf_yasg.inspectors.CamelCaseJSONFilter',
'drf_yasg.inspectors.ReferencingSerializerInspector', 'drf_yasg.inspectors.ReferencingSerializerInspector',
'drf_yasg.inspectors.RelatedFieldInspector', 'drf_yasg.inspectors.RelatedFieldInspector',

View File

@ -24,7 +24,7 @@ $(document).ready(function() {
source: function(request, response) { source: function(request, response) {
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: search_field.attr('data-source'), url: search_field.attr('data-source') + '?brief=1',
data: search_key + '=' + request.term, data: search_key + '=' + request.term,
success: function(data) { success: function(data) {
var choices = []; var choices = [];
@ -49,7 +49,7 @@ $(document).ready(function() {
// Disable parent selection fields // Disable parent selection fields
// $('select[filter-for="' + real_field.attr('name') + '"]').val(''); // $('select[filter-for="' + real_field.attr('name') + '"]').val('');
}, },
minLength: 4, minLength: 3,
delay: 500 delay: 500
}); });

View File

@ -54,7 +54,7 @@
</div> </div>
<div class="col-xs-4 text-right"> <div class="col-xs-4 text-right">
<p class="text-muted"> <p class="text-muted">
<i class="fa fa-fw fa-book text-primary"></i> <a href="http://netbox.readthedocs.io/" target="_blank">Docs</a> &middot; <i class="fa fa-fw fa-book text-primary"></i> <a href="http://netbox.readthedocs.io/">Docs</a> &middot;
<i class="fa fa-fw fa-cloud text-primary"></i> <a href="{% url 'api_docs' %}">API</a> &middot; <i class="fa fa-fw fa-cloud text-primary"></i> <a href="{% url 'api_docs' %}">API</a> &middot;
<i class="fa fa-fw fa-code text-primary"></i> <a href="https://github.com/digitalocean/netbox">Code</a> &middot; <i class="fa fa-fw fa-code text-primary"></i> <a href="https://github.com/digitalocean/netbox">Code</a> &middot;
<i class="fa fa-fw fa-support text-primary"></i> <a href="https://github.com/digitalocean/netbox/wiki">Help</a> <i class="fa fa-fw fa-support text-primary"></i> <a href="https://github.com/digitalocean/netbox/wiki">Help</a>

View File

@ -62,6 +62,13 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Virtualization</strong></div>
<div class="panel-body">
{% render_field form.cluster_group %}
{% render_field form.cluster %}
</div>
</div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><strong>Tenancy</strong></div> <div class="panel-heading"><strong>Tenancy</strong></div>
<div class="panel-body"> <div class="panel-body">

View File

@ -55,7 +55,10 @@
</tr> </tr>
<tr> <tr>
<td>Model Name</td> <td>Model Name</td>
<td>{{ devicetype.model }}</td> <td>
{{ devicetype.model }}<br/>
<small class="text-muted">{{ devicetype.slug }}</small>
</td>
</tr> </tr>
<tr> <tr>
<td>Part Number</td> <td>Part Number</td>

View File

@ -27,13 +27,13 @@
{% ifequal u.device.face face_id %} {% ifequal u.device.face face_id %}
<a href="{% url 'dcim:device' pk=u.device.pk %}" data-toggle="popover" data-trigger="hover" data-container="body" data-html="true" <a href="{% url 'dcim:device' pk=u.device.pk %}" data-toggle="popover" data-trigger="hover" data-container="body" data-html="true"
data-content="{{ u.device.device_role }}<br />{{ u.device.device_type.full_name }} ({{ u.device.device_type.u_height }}U){% if u.device.asset_tag %}<br />{{ u.device.asset_tag }}{% endif %}{% if u.device.serial %}<br />{{ u.device.serial }}{% endif %}"> data-content="{{ u.device.device_role }}<br />{{ u.device.device_type.full_name }} ({{ u.device.device_type.u_height }}U){% if u.device.asset_tag %}<br />{{ u.device.asset_tag }}{% endif %}{% if u.device.serial %}<br />{{ u.device.serial }}{% endif %}">
{{ u.device.name|default:u.device.device_role }} {{ u.device }}
{% if u.device.devicebay_count %} {% if u.device.devicebay_count %}
({{ u.device.get_children.count }}/{{ u.device.devicebay_count }}) ({{ u.device.get_children.count }}/{{ u.device.devicebay_count }})
{% endif %} {% endif %}
</a> </a>
{% else %} {% else %}
<span>{{ u.device.name|default:u.device.device_role }}</span> <span>{{ u.device }}</span>
{% endifequal %} {% endifequal %}
</li> </li>
{% else %} {% else %}

View File

@ -1,9 +1,52 @@
from drf_yasg import openapi from drf_yasg import openapi
from drf_yasg.inspectors import FieldInspector, NotHandled, PaginatorInspector, FilterInspector from drf_yasg.inspectors import FieldInspector, NotHandled, PaginatorInspector, FilterInspector, SwaggerAutoSchema
from rest_framework.fields import ChoiceField from rest_framework.fields import ChoiceField
from rest_framework.relations import ManyRelatedField
from taggit_serializer.serializers import TagListSerializerField
from extras.api.customfields import CustomFieldsSerializer from extras.api.customfields import CustomFieldsSerializer
from utilities.api import ChoiceField from utilities.api import ChoiceField, SerializedPKRelatedField, WritableNestedSerializer
class NetBoxSwaggerAutoSchema(SwaggerAutoSchema):
def get_request_serializer(self):
serializer = super().get_request_serializer()
if serializer is not None and self.method in self.implicit_body_methods:
properties = {}
for child_name, child in serializer.fields.items():
if isinstance(child, (ChoiceField, WritableNestedSerializer)):
properties[child_name] = None
elif isinstance(child, ManyRelatedField) and isinstance(child.child_relation, SerializedPKRelatedField):
properties[child_name] = None
if properties:
writable_class = type('Writable' + type(serializer).__name__, (type(serializer),), properties)
serializer = writable_class()
return serializer
class SerializedPKRelatedFieldInspector(FieldInspector):
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
if isinstance(field, SerializedPKRelatedField):
return self.probe_field_inspectors(field.serializer(), ChildSwaggerType, use_references)
return NotHandled
class TagListFieldInspector(FieldInspector):
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
if isinstance(field, TagListSerializerField):
child_schema = self.probe_field_inspectors(field.child, ChildSwaggerType, use_references)
return SwaggerType(
type=openapi.TYPE_ARRAY,
items=child_schema,
)
return NotHandled
class CustomChoiceFieldInspector(FieldInspector): class CustomChoiceFieldInspector(FieldInspector):

View File

@ -0,0 +1,89 @@
from django.test import TestCase
from utilities.utils import deepmerge
class DeepMergeTest(TestCase):
"""
Validate the behavior of the deepmerge() utility.
"""
def setUp(self):
return
def test_deepmerge(self):
dict1 = {
'active': True,
'foo': 123,
'fruits': {
'orange': 1,
'apple': 2,
'pear': 3,
},
'vegetables': None,
'dairy': {
'milk': 1,
'cheese': 2,
},
'deepnesting': {
'foo': {
'a': 10,
'b': 20,
'c': 30,
},
},
}
dict2 = {
'active': False,
'bar': 456,
'fruits': {
'banana': 4,
'grape': 5,
},
'vegetables': {
'celery': 1,
'carrots': 2,
'corn': 3,
},
'dairy': None,
'deepnesting': {
'foo': {
'a': 100,
'd': 40,
},
},
}
merged = {
'active': False,
'foo': 123,
'bar': 456,
'fruits': {
'orange': 1,
'apple': 2,
'pear': 3,
'banana': 4,
'grape': 5,
},
'vegetables': {
'celery': 1,
'carrots': 2,
'corn': 3,
},
'dairy': None,
'deepnesting': {
'foo': {
'a': 100,
'b': 20,
'c': 30,
'd': 40,
},
},
}
self.assertEqual(
deepmerge(dict1, dict2),
merged
)

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict
import datetime import datetime
import json import json
import six import six
@ -109,3 +110,16 @@ def serialize_object(obj, extra=None):
data.update(extra) data.update(extra)
return data return data
def deepmerge(original, new):
"""
Deep merge two dictionaries (new into original) and return a new dict
"""
merged = OrderedDict(original)
for key, val in new.items():
if key in original and isinstance(original[key], dict) and isinstance(val, dict):
merged[key] = deepmerge(original[key], val)
else:
merged[key] = val
return merged