Compare commits

...

66 Commits

Author SHA1 Message Date
Jeremy Stretch
ac1e4b8e8f Merge pull request #2922 from digitalocean/develop
Release v2.5.7
2019-02-21 14:44:48 -05:00
Jeremy Stretch
36101f50c3 Release v2.5.7 2019-02-21 14:42:44 -05:00
Jeremy Stretch
08dad7f7af Changelog for #2638 2019-02-21 13:30:57 -05:00
hSaria
298152bc50 Closes #2638: Copy Button for Secrets (#2860)
* Added clipboard.min.js

* Added copy button to secrets

* renamed clipboard JS library
2019-02-21 13:26:19 -05:00
Jeremy Stretch
993e94c00c Fixes #2852: Allow filtering devices by null rack position 2019-02-21 10:26:31 -05:00
Jeremy Stretch
4c3a7b84c1 Merge pull request #2881 from candlerb/candlerb/doc-reports
Clarify use of "runreport" CLI
2019-02-20 12:04:35 -05:00
Jeremy Stretch
8961a54a03 Chnagelog for #2890 2019-02-20 12:01:37 -05:00
Jeremy Stretch
2184a9402f Merge pull request #2891 from gdelafond/add-port-types
Fixes #2890: add port type LC/APC, SC/APC, LSH/APC
2019-02-20 12:00:39 -05:00
Jeremy Stretch
b9f1d14d4e Closes #2870: Add Markdown rendering for provider NOC/admin contact fields 2019-02-20 11:48:11 -05:00
Jeremy Stretch
fba154386e Closes #2898: Enable filtering cables list by connection status 2019-02-20 11:36:55 -05:00
Jeremy Stretch
c8366eff8f Fixes #2915: Fix bulk editing of pass-through ports 2019-02-20 11:13:43 -05:00
Jeremy Stretch
161d9ed512 Fixes #2912: Cable type in filter form should be blank by default 2019-02-20 10:21:34 -05:00
Jeremy Stretch
fadc9521f0 Fixes #2913: Fix assigned prefixes link on VRF view 2019-02-20 10:17:36 -05:00
Jeremy Stretch
40cb47868f Fixes #2914: Fix empty connected circuit link on device interfaces list 2019-02-20 10:13:36 -05:00
John Anderson
9d055ff4fd fixed #2910 - config context list and edit views not using select2 2019-02-18 21:10:15 -05:00
Jeremy Stretch
9d69f14faa Implement basic view testing for all models (#2909) 2019-02-15 17:02:18 -05:00
Jeremy Stretch
8e3ea6c878 Closes #2878: Add cable types for OS1/OS2 singlemode fiber 2019-02-15 14:42:03 -05:00
Jeremy Stretch
971f3cd63c Closes #2357: Enable filtering of devices by rack face 2019-02-15 11:31:57 -05:00
Jeremy Stretch
a9d7a7e306 Closes #2903: Clarify purpose of tags field on interface edit form 2019-02-15 10:23:46 -05:00
Jeremy Stretch
8797298a71 Fixes #2884: Don't display connect button for wireless interfaces 2019-02-15 10:14:44 -05:00
Jeremy Stretch
3bc182e453 Fixes #2901: Fix ordering regions by site count 2019-02-15 10:10:26 -05:00
Jeremy Stretch
7a0ab3aa15 Fixes #2895: Fix filtering of nullable character fields 2019-02-14 09:43:56 -05:00
Jeremy Stretch
fdbef8ee71 Closes #2836: Deprecate NullableCharField 2019-02-14 09:39:04 -05:00
Jeremy Stretch
d95b127378 Changelog for #2888 and #2893 2019-02-14 09:20:27 -05:00
Jeremy Stretch
008138cd02 Merge pull request #2889 from stuntguy3000/patch-1
Fixes #2888 - Render correct foreground colour for rack devices
2019-02-14 09:14:36 -05:00
Jeremy Stretch
2a1630e068 Merge pull request #2894 from TakeMeNL/bug/duplicate-vrf
Remove duplicate VRF RD on ip address view
2019-02-14 09:09:32 -05:00
TakeMeNL
31611203eb ipaddress.vrf already includes ipaddress.vrf.rd 2019-02-14 14:48:25 +01:00
Guillaume de Lafond
1ee6d16d78 add port type LC/APC, SC/APC, LSH/APC 2019-02-14 10:17:25 +01:00
Luke Anderson
0e8c3a8efe Render correct foreground colour for rack devices 2019-02-14 16:37:27 +10:30
Jeremy Stretch
d084d19675 Post-release version bump 2019-02-13 17:11:51 -05:00
Jeremy Stretch
77954a3796 Merge pull request #2886 from digitalocean/develop
Release v2.5.6
2019-02-13 17:11:26 -05:00
Jeremy Stretch
8152dc4b04 Release v2.5.6 2019-02-13 17:07:15 -05:00
Jeremy Stretch
109b233e14 Closes #2758: Add cable trace button to pass-through ports 2019-02-13 17:05:02 -05:00
Jeremy Stretch
cc3b26998b Fixes #2880: Sanitize user password if an exception is raised during login 2019-02-13 11:34:16 -05:00
Jeremy Stretch
95dea1faaa Closes #2866: Add cellular interface types (GSM/CDMA/LTE) 2019-02-13 10:45:42 -05:00
Jeremy Stretch
57fecdbf17 Closes #2851: Include circuit provider in pass-through port connection details 2019-02-13 10:26:54 -05:00
Jeremy Stretch
3b4bcc881f Fix broken test 2019-02-13 10:09:33 -05:00
Jeremy Stretch
dfa4dfa4a4 Fixes #2877: Fixed device role label display on light background color 2019-02-13 10:06:56 -05:00
Brian Candler
8d86e97247 Clarify use of "runreport" CLI 2019-02-13 08:54:04 +00:00
Jeremy Stretch
5da9d6b46b Closes #2839: Add "110 punch" type for pass-through ports 2019-02-08 09:57:04 -05:00
Jeremy Stretch
100809f11a Closes #2854: Enable bulk editing of pass-through ports 2019-02-08 09:31:10 -05:00
Jeremy Stretch
5256077a3c Fixes #2862: Follow return URL when connecting a cable 2019-02-08 09:10:31 -05:00
Jeremy Stretch
375e66047d Fixes #2864: Correct display of VRF name when no RD is assigned 2019-02-08 09:04:43 -05:00
Jeremy Stretch
42d1d6e1b0 Fixes #2841: Fix filtering by VRF for prefix and IP address lists 2019-02-06 10:48:14 -05:00
Jeremy Stretch
ca51fab4d8 Fixes #2845: Enable filtering of rack unit list by unit ID 2019-02-06 10:44:05 -05:00
Jeremy Stretch
73c983516d Fixes #2856: Fix navigation links between LAG interfaces and their members on device view 2019-02-06 10:28:25 -05:00
Jeremy Stretch
f733d5a4da Fixes #2857: Add display_name to DeviceType API serializer; fix DeviceType list for bulk device edit 2019-02-06 10:23:30 -05:00
Jeremy Stretch
3d2948daf3 Merge pull request #2793 from candlerb/candlerb/doc-inventory
Clarify how chassis-based switches/routers are supposed to be modelled
2019-02-06 10:02:05 -05:00
Jeremy Stretch
69a5d3644a Closes #2844: Correct display of far cable end for pass-through ports 2019-02-01 09:12:48 -05:00
Jeremy Stretch
2f1018c742 Post-release version bump 2019-01-31 16:12:00 -05:00
Jeremy Stretch
d5fc37282f Merge pull request #2838 from digitalocean/develop
Release v2.5.5
2019-01-31 16:10:32 -05:00
Jeremy Stretch
525ed359cd Release v2.5.5 2019-01-31 16:04:14 -05:00
John Anderson
fe00db62d6 fixes #2837 - select2 nullable filter fields add multiple null_option elements when paging 2019-01-31 13:56:36 -05:00
Jeremy Stretch
bcfa760cf9 Closes #2805: Allow null route distinguisher for VRFs 2019-01-31 13:47:24 -05:00
John Anderson
613e8f05c2 fixes #2835 - certain model filters did not support the q query param 2019-01-31 13:36:30 -05:00
Jeremy Stretch
59f8f0c7ea Closes #2825: Include directly connected device for front/rear ports 2019-01-31 12:21:43 -05:00
Jeremy Stretch
ae0c8deec2 Closes #2809: Remove VRF child prefixes table; link to main prefixes view 2019-01-31 10:06:08 -05:00
Jeremy Stretch
b508415983 Fixes #2833: Fix form widget for front port template creation 2019-01-31 09:19:53 -05:00
Jeremy Stretch
a98d014763 Post-release version bump 2019-01-31 09:06:49 -05:00
John Anderson
51e5e49d3b Merge pull request #2832 from cimnine/patch-1
Updated link to netbox-docker, again
2019-01-31 03:06:29 -05:00
Christian Mäder
0eced489da Updated link to netbox-docker, again
After some feedback, that `netbox-community/docker` is not an ideal name, I've renamed the repo back to `netbox-docker`. Hence one more PR to update that link.
2019-01-31 09:02:12 +01:00
TheTrafficNetwork
5138d12942 Typo (#2822) 2019-01-30 14:20:40 -05:00
Jeremy Stretch
fe30276db2 Merge pull request #2828 from cimnine/patch-1
Updated link after the move of `netbox-docker`
2019-01-30 14:19:57 -05:00
Christian Mäder
e51e8b5c8a Updated link after the move of netbox-docker 2019-01-30 16:17:37 +01:00
Jeremy Stretch
170900e80f Fixes #2824: Fix template exception when viewing rack elevations list 2019-01-30 09:59:19 -05:00
Brian Candler
9991985170 Clarify how chassis-based switches and routers are supposed to be modelled 2019-01-24 13:35:37 +00:00
62 changed files with 1769 additions and 156 deletions

View File

@@ -1,3 +1,69 @@
v2.5.7 (2019-02-21)
## Enhancements
* [#2357](https://github.com/digitalocean/netbox/issues/2357) - Enable filtering of devices by rack face
* [#2638](https://github.com/digitalocean/netbox/issues/2638) - Add button to copy unlocked secret to clipboard
* [#2870](https://github.com/digitalocean/netbox/issues/2870) - Add Markdown rendering for provider NOC/admin contact fields
* [#2878](https://github.com/digitalocean/netbox/issues/2878) - Add cable types for OS1/OS2 singlemode fiber
* [#2890](https://github.com/digitalocean/netbox/issues/2890) - Add port types for APC fiber
* [#2898](https://github.com/digitalocean/netbox/issues/2898) - Enable filtering cables list by connection status
* [#2903](https://github.com/digitalocean/netbox/issues/2903) - Clarify purpose of tags field on interface edit form
## Bug Fixes
* [#2852](https://github.com/digitalocean/netbox/issues/2852) - Allow filtering devices by null rack position
* [#2884](https://github.com/digitalocean/netbox/issues/2884) - Don't display connect button for wireless interfaces
* [#2888](https://github.com/digitalocean/netbox/issues/2888) - Correct foreground color of device roles in rack elevations
* [#2893](https://github.com/digitalocean/netbox/issues/2893) - Remove duplicate display of VRF RD on IP address view
* [#2895](https://github.com/digitalocean/netbox/issues/2895) - Fix filtering of nullable character fields
* [#2901](https://github.com/digitalocean/netbox/issues/2901) - Fix ordering regions by site count
* [#2910](https://github.com/digitalocean/netbox/issues/2910) - Fix config context list and edit forms to use Select2 elements
* [#2912](https://github.com/digitalocean/netbox/issues/2912) - Cable type in filter form should be blank by default
* [#2913](https://github.com/digitalocean/netbox/issues/2913) - Fix assigned prefixes link on VRF view
* [#2914](https://github.com/digitalocean/netbox/issues/2914) - Fix empty connected circuit link on device interfaces list
* [#2915](https://github.com/digitalocean/netbox/issues/2915) - Fix bulk editing of pass-through ports
v2.5.6 (2019-02-13)
## Enhancements
* [#2758](https://github.com/digitalocean/netbox/issues/2758) - Add cable trace button to pass-through ports
* [#2839](https://github.com/digitalocean/netbox/issues/2839) - Add "110 punch" type for pass-through ports
* [#2854](https://github.com/digitalocean/netbox/issues/2854) - Enable bulk editing of pass-through ports
* [#2866](https://github.com/digitalocean/netbox/issues/2866) - Add cellular interface types (GSM/CDMA/LTE)
## Bug Fixes
* [#2841](https://github.com/digitalocean/netbox/issues/2841) - Fix filtering by VRF for prefix and IP address lists
* [#2844](https://github.com/digitalocean/netbox/issues/2844) - Correct display of far cable end for pass-through ports
* [#2845](https://github.com/digitalocean/netbox/issues/2845) - Enable filtering of rack unit list by unit ID
* [#2856](https://github.com/digitalocean/netbox/issues/2856) - Fix navigation links between LAG interfaces and their members on device view
* [#2857](https://github.com/digitalocean/netbox/issues/2857) - Add `display_name` to DeviceType API serializer; fix DeviceType list for bulk device edit
* [#2862](https://github.com/digitalocean/netbox/issues/2862) - Follow return URL when connecting a cable
* [#2864](https://github.com/digitalocean/netbox/issues/2864) - Correct display of VRF name when no RD is assigned
* [#2877](https://github.com/digitalocean/netbox/issues/2877) - Fixed device role label display on light background color
* [#2880](https://github.com/digitalocean/netbox/issues/2880) - Sanitize user password if an exception is raised during login
---
v2.5.5 (2019-01-31)
## Enhancements
* [#2805](https://github.com/digitalocean/netbox/issues/2805) - Allow null route distinguisher for VRFs
* [#2809](https://github.com/digitalocean/netbox/issues/2809) - Remove VRF child prefixes table; link to main prefixes view
* [#2825](https://github.com/digitalocean/netbox/issues/2825) - Include directly connected device for front/rear ports
## Bug Fixes
* [#2824](https://github.com/digitalocean/netbox/issues/2824) - Fix template exception when viewing rack elevations list
* [#2833](https://github.com/digitalocean/netbox/issues/2833) - Fix form widget for front port template creation
* [#2835](https://github.com/digitalocean/netbox/issues/2835) - Fix certain model filters did not support the `q` query param
* [#2837](https://github.com/digitalocean/netbox/issues/2837) - Fix select2 nullable filter fields add multiple null_option elements when paging
---
v2.5.4 (2019-01-29)
## Enhancements

View File

@@ -37,7 +37,7 @@ and run `upgrade.sh`.
## Alternative Installations
* [Docker container](https://github.com/ninech/netbox-docker) (via [@cimnine](https://github.com/cimnine))
* [Docker container](https://github.com/netbox-community/netbox-docker) (via [@cimnine](https://github.com/cimnine))
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant) (via [@ryanmerolle](https://github.com/ryanmerolle))
* [Ansible deployment](https://github.com/lae/ansible-role-netbox) (via [@lae](https://github.com/lae))

View File

@@ -128,4 +128,4 @@ Reports can be run on the CLI by invoking the management command:
python3 manage.py runreport <module>
```
One or more report modules may be specified.
where ``<module>`` is the name of the python file in the ``reports`` directory without the ``.py`` extension. One or more report modules may be specified.

View File

@@ -1,6 +1,6 @@
# Tags
Tags are free-form text labels which can be applied to a variety of objects within NetBox. Tags are created on-demand as they are assigned to objects. Use commas to separate tags when adding multiple tags to an object 9for example: `Inventoried, Monitored`). Use double quotes around a multi-word tag when adding only one tag, e.g. `"Core Switch"`.
Tags are free-form text labels which can be applied to a variety of objects within NetBox. Tags are created on-demand as they are assigned to objects. Use commas to separate tags when adding multiple tags to an object (for example: `Inventoried, Monitored`). Use double quotes around a multi-word tag when adding only one tag, e.g. `"Core Switch"`.
Each tag has a label and a URL-friendly slug. For example, the slug for a tag named "Dunder Mifflin, Inc." would be `dunder-mifflin-inc`. The slug is generated automatically and makes tags easier to work with as URL parameters.

View File

@@ -13,6 +13,10 @@ Some devices house child devices which share physical resources, like space and
!!! note
This parent/child relationship is **not** suitable for modeling chassis-based devices, wherein child members share a common control plane.
For that application you should create a single Device for the chassis, and add Interfaces directly to it. Interfaces can be created in bulk using range patterns, e.g. "Gi1/[1-24]".
Add Inventory Items if you want to record the line cards themselves as separate entities. There is no explicit relationship between each interface and its line card, but it may be implied by the naming (e.g. interfaces "Gi1/x" are on line card 1)
## Manufacturers
Each device type must be assigned to a manufacturer. The model number of a device type must be unique to its manufacturer.
@@ -93,6 +97,10 @@ Pass-through ports can also be used to model "bump in the wire" devices, such as
Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear within rack elevations, but they are included in the "Non-Racked Devices" list within the rack view.
Child devices are first-class Devices in their own right: that is, fully independent managed entities which don't share any control plane with the parent. Just like normal devices, child devices have their own platform (OS), role, tags, and interfaces. You cannot create a LAG between interfaces in different child devices.
Therefore, Device bays are **not** suitable for modeling chassis-based switches and routers. These should instead be modeled as a single Device, with the line cards as Inventory Items.
## Device Roles
Devices can be organized by functional roles. These roles are fully customizable. For example, you might create roles for core switches, distribution switches, and access switches.
@@ -111,7 +119,7 @@ The assignment of platforms to devices is an optional feature, and may be disreg
# Inventory Items
Inventory items represent hardware components installed within a device, such as a power supply or CPU. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Like device types, each item can optionally be assigned a manufacturer.
Inventory items represent hardware components installed within a device, such as a power supply or CPU or line card. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Like device types, each item can optionally be assigned a manufacturer.
---

View File

@@ -83,7 +83,7 @@ An IP address can be designated as the network address translation (NAT) inside
A VRF object in NetBox represents a virtual routing and forwarding (VRF) domain. Each VRF is essentially a separate routing table. VRFs are commonly used to isolate customers or organizations from one another within a network, or to route overlapping address space (e.g. multiple instances of the 10.0.0.0/8 space).
Each VRF is assigned a unique name and route distinguisher (RD). The RD is expected to take one of the forms prescribed in [RFC 4364](https://tools.ietf.org/html/rfc4364#section-4.2), however its formatting is not strictly enforced.
Each VRF is assigned a unique name and an optional route distinguisher (RD). The RD is expected to take one of the forms prescribed in [RFC 4364](https://tools.ietf.org/html/rfc4364#section-4.2), however its formatting is not strictly enforced.
Each prefix and IP address may be assigned to one (and only one) VRF. If you have a prefix or IP address which exists in multiple VRFs, you will need to create a separate instance of it in NetBox for each VRF. Any IP prefix or address not assigned to a VRF is said to belong to the "global" table.

View File

@@ -4,7 +4,7 @@ from django.db.models import Q
from dcim.models import Site
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NumericInFilter, TagFilter
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from .constants import CIRCUIT_STATUS_CHOICES
from .models import Provider, Circuit, CircuitTermination, CircuitType
@@ -47,7 +47,7 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
)
class CircuitTypeFilter(django_filters.FilterSet):
class CircuitTypeFilter(NameSlugSearchFilterSet):
class Meta:
model = CircuitType

View File

@@ -3,7 +3,7 @@ from django.db import models
from django.urls import reverse
from taggit.managers import TaggableManager
from dcim.constants import CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, STATUS_CLASSES
from dcim.constants import CONNECTION_STATUS_CHOICES, STATUS_CLASSES
from dcim.fields import ASNField
from dcim.models import CableTermination
from extras.models import CustomFieldModel, ObjectChange
@@ -283,6 +283,10 @@ class CircuitTermination(CableTermination):
object_data=serialize_object(self)
).save()
@property
def parent(self):
return self.circuit
def get_peer_termination(self):
peer_side = 'Z' if self.term_side == 'A' else 'A'
try:

View File

@@ -0,0 +1,91 @@
import urllib.parse
from django.test import Client, TestCase
from django.urls import reverse
from circuits.models import Circuit, CircuitType, Provider
class ProviderTestCase(TestCase):
def setUp(self):
self.client = Client()
Provider.objects.bulk_create([
Provider(name='Provider 1', slug='provider-1', asn=65001),
Provider(name='Provider 2', slug='provider-2', asn=65002),
Provider(name='Provider 3', slug='provider-3', asn=65003),
])
def test_provider_list(self):
url = reverse('circuits:provider_list')
params = {
"q": "test",
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_provider(self):
provider = Provider.objects.first()
response = self.client.get(provider.get_absolute_url())
self.assertEqual(response.status_code, 200)
class CircuitTypeTestCase(TestCase):
def setUp(self):
self.client = Client()
CircuitType.objects.bulk_create([
CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
CircuitType(name='Circuit Type 2', slug='circuit-type-2'),
CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
])
def test_circuittype_list(self):
url = reverse('circuits:circuittype_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class CircuitTestCase(TestCase):
def setUp(self):
self.client = Client()
provider = Provider(name='Provider 1', slug='provider-1', asn=65001)
provider.save()
circuittype = CircuitType(name='Circuit Type 1', slug='circuit-type-1')
circuittype.save()
Circuit.objects.bulk_create([
Circuit(cid='Circuit 1', provider=provider, type=circuittype),
Circuit(cid='Circuit 2', provider=provider, type=circuittype),
Circuit(cid='Circuit 3', provider=provider, type=circuittype),
])
def test_circuit_list(self):
url = reverse('circuits:circuit_list')
params = {
"provider": Provider.objects.first().slug,
"type": CircuitType.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_provider(self):
provider = Provider.objects.first()
response = self.client.get(provider.get_absolute_url())
self.assertEqual(response.status_code, 200)

View File

@@ -100,7 +100,7 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
class Meta:
model = DeviceType
fields = ['id', 'url', 'manufacturer', 'model', 'slug']
fields = ['id', 'url', 'manufacturer', 'model', 'slug', 'display_name']
class NestedRearPortTemplateSerializer(WritableNestedSerializer):

View File

@@ -180,8 +180,8 @@ class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
class Meta:
model = DeviceType
fields = [
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'instance_count',
'id', 'manufacturer', 'model', 'slug', 'display_name', 'part_number', 'u_height', 'is_full_depth',
'subdevice_role', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'instance_count',
]

View File

@@ -159,6 +159,11 @@ class RackViewSet(CustomFieldModelViewSet):
exclude_pk = None
elevation = rack.get_rack_units(face, exclude_pk)
# Enable filtering rack units by ID
q = request.GET.get('q', None)
if q:
elevation = [u for u in elevation if q in str(u['id'])]
page = self.paginate_queryset(elevation)
if page is not None:
rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})

View File

@@ -43,6 +43,12 @@ RACK_STATUS_CHOICES = [
[RACK_STATUS_DEPRECATED, 'Deprecated'],
]
# Device rack position
DEVICE_POSITION_CHOICES = [
# Rack.u_height is limited to 100
(i, 'Unit {}'.format(i)) for i in range(1, 101)
]
# Parent/child device roles
SUBDEVICE_ROLE_PARENT = True
SUBDEVICE_ROLE_CHILD = False
@@ -91,6 +97,10 @@ IFACE_FF_80211G = 2610
IFACE_FF_80211N = 2620
IFACE_FF_80211AC = 2630
IFACE_FF_80211AD = 2640
# Cellular
IFACE_FF_GSM = 2810
IFACE_FF_CDMA = 2820
IFACE_FF_LTE = 2830
# SONET
IFACE_FF_SONET_OC3 = 6100
IFACE_FF_SONET_OC12 = 6200
@@ -174,6 +184,14 @@ IFACE_FF_CHOICES = [
[IFACE_FF_80211AD, 'IEEE 802.11ad'],
]
],
[
'Cellular',
[
[IFACE_FF_GSM, 'GSM'],
[IFACE_FF_CDMA, 'CDMA'],
[IFACE_FF_LTE, 'LTE'],
]
],
[
'SONET',
[
@@ -255,18 +273,23 @@ IFACE_MODE_CHOICES = [
# Pass-through port types
PORT_TYPE_8P8C = 1000
PORT_TYPE_110_PUNCH = 1100
PORT_TYPE_ST = 2000
PORT_TYPE_SC = 2100
PORT_TYPE_SC_APC = 2110
PORT_TYPE_FC = 2200
PORT_TYPE_LC = 2300
PORT_TYPE_LC_APC = 2310
PORT_TYPE_MTRJ = 2400
PORT_TYPE_MPO = 2500
PORT_TYPE_LSH = 2600
PORT_TYPE_LSH_APC = 2610
PORT_TYPE_CHOICES = [
[
'Copper',
[
[PORT_TYPE_8P8C, '8P8C'],
[PORT_TYPE_110_PUNCH, '110 Punch'],
],
],
[
@@ -274,10 +297,13 @@ PORT_TYPE_CHOICES = [
[
[PORT_TYPE_FC, 'FC'],
[PORT_TYPE_LC, 'LC'],
[PORT_TYPE_LC_APC, 'LC/APC'],
[PORT_TYPE_LSH, 'LSH'],
[PORT_TYPE_LSH_APC, 'LSH/APC'],
[PORT_TYPE_MPO, 'MPO'],
[PORT_TYPE_MTRJ, 'MTRJ'],
[PORT_TYPE_SC, 'SC'],
[PORT_TYPE_SC_APC, 'SC/APC'],
[PORT_TYPE_ST, 'ST'],
]
]
@@ -341,11 +367,14 @@ CABLE_TYPE_CAT6A = 1610
CABLE_TYPE_CAT7 = 1700
CABLE_TYPE_DAC_ACTIVE = 1800
CABLE_TYPE_DAC_PASSIVE = 1810
CABLE_TYPE_MMF = 3000
CABLE_TYPE_MMF_OM1 = 3010
CABLE_TYPE_MMF_OM2 = 3020
CABLE_TYPE_MMF_OM3 = 3030
CABLE_TYPE_MMF_OM4 = 3040
CABLE_TYPE_SMF = 3500
CABLE_TYPE_SMF_OS1 = 3510
CABLE_TYPE_SMF_OS2 = 3520
CABLE_TYPE_AOC = 3800
CABLE_TYPE_POWER = 5000
CABLE_TYPE_CHOICES = (
@@ -363,11 +392,14 @@ CABLE_TYPE_CHOICES = (
),
(
'Fiber', (
(CABLE_TYPE_MMF, 'Multimode Fiber'),
(CABLE_TYPE_MMF_OM1, 'Multimode Fiber (OM1)'),
(CABLE_TYPE_MMF_OM2, 'Multimode Fiber (OM2)'),
(CABLE_TYPE_MMF_OM3, 'Multimode Fiber (OM3)'),
(CABLE_TYPE_MMF_OM4, 'Multimode Fiber (OM4)'),
(CABLE_TYPE_SMF, 'Singlemode Fiber'),
(CABLE_TYPE_SMF_OS1, 'Singlemode Fiber (OS1)'),
(CABLE_TYPE_SMF_OS2, 'Singlemode Fiber (OS2)'),
(CABLE_TYPE_AOC, 'Active Optical Cabling (AOC)'),
),
),

View File

@@ -8,7 +8,7 @@ from netaddr.core import AddrFormatError
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.constants import COLOR_CHOICES
from utilities.filters import NullableCharFieldFilter, NumericInFilter, TagFilter
from utilities.filters import NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter
from virtualization.models import Cluster
from .constants import *
from .models import (
@@ -19,11 +19,7 @@ from .models import (
)
class RegionFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
class RegionFilter(NameSlugSearchFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(),
label='Parent region (ID)',
@@ -39,15 +35,6 @@ class RegionFilter(django_filters.FilterSet):
model = Region
fields = ['name', 'slug']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value) |
Q(slug__icontains=value)
)
return queryset.filter(qs_filter)
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(
@@ -119,11 +106,7 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
)
class RackGroupFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
class RackGroupFilter(NameSlugSearchFilterSet):
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
@@ -139,17 +122,8 @@ class RackGroupFilter(django_filters.FilterSet):
model = RackGroup
fields = ['site_id', 'name', 'slug']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value) |
Q(slug__icontains=value)
)
return queryset.filter(qs_filter)
class RackRoleFilter(django_filters.FilterSet):
class RackRoleFilter(NameSlugSearchFilterSet):
class Meta:
model = RackRole
@@ -303,7 +277,7 @@ class RackReservationFilter(django_filters.FilterSet):
)
class ManufacturerFilter(django_filters.FilterSet):
class ManufacturerFilter(NameSlugSearchFilterSet):
class Meta:
model = Manufacturer
@@ -393,7 +367,7 @@ class DeviceTypeFilter(CustomFieldFilterSet):
)
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet):
devicetype_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(),
field_name='device_type_id',
@@ -457,14 +431,14 @@ class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
fields = ['name']
class DeviceRoleFilter(django_filters.FilterSet):
class DeviceRoleFilter(NameSlugSearchFilterSet):
class Meta:
model = DeviceRole
fields = ['name', 'slug', 'color', 'vm_role']
class PlatformFilter(django_filters.FilterSet):
class PlatformFilter(NameSlugSearchFilterSet):
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
field_name='manufacturer',
queryset=Manufacturer.objects.all(),
@@ -569,6 +543,10 @@ class DeviceFilter(CustomFieldFilterSet):
queryset=Rack.objects.all(),
label='Rack (ID)',
)
position = django_filters.ChoiceFilter(
choices=DEVICE_POSITION_CHOICES,
null_label='Non-racked'
)
cluster_id = django_filters.ModelMultipleChoiceFilter(
queryset=Cluster.objects.all(),
label='VM cluster (ID)',
@@ -628,7 +606,7 @@ class DeviceFilter(CustomFieldFilterSet):
class Meta:
model = Device
fields = ['serial', 'position']
fields = ['serial', 'face']
def search(self, queryset, name, value):
if not value.strip():
@@ -696,6 +674,10 @@ class DeviceFilter(CustomFieldFilterSet):
class DeviceComponentFilterSet(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
device_id = django_filters.ModelChoiceFilter(
queryset=Device.objects.all(),
label='Device (ID)',
@@ -707,6 +689,13 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
)
tag = TagFilter()
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value)
)
class ConsolePortFilter(DeviceComponentFilterSet):
cabled = django_filters.BooleanFilter(

View File

@@ -1066,7 +1066,6 @@ class FrontPortTemplateCreateForm(ComponentForm):
choices=[],
label='Rear ports',
help_text='Select one rear port assignment for each front port being created.',
widget=StaticSelect2(),
)
def __init__(self, *args, **kwargs):
@@ -1600,7 +1599,8 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
required=False,
label='Type',
widget=APISelect(
api_url="/api/dcim/device-types/"
api_url="/api/dcim/device-types/",
display_field='display_name'
)
)
device_role = forms.ModelChoiceField(
@@ -2360,6 +2360,27 @@ class FrontPortCreateForm(ComponentForm):
}
class FrontPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=FrontPort.objects.all(),
widget=forms.MultipleHiddenInput()
)
type = forms.ChoiceField(
choices=add_blank_choice(PORT_TYPE_CHOICES),
required=False,
widget=StaticSelect2()
)
description = forms.CharField(
max_length=100,
required=False
)
class Meta:
nullable_fields = [
'description',
]
class FrontPortBulkRenameForm(BulkRenameForm):
pk = forms.ModelMultipleChoiceField(
queryset=FrontPort.objects.all(),
@@ -2413,6 +2434,27 @@ class RearPortCreateForm(ComponentForm):
)
class RearPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=RearPort.objects.all(),
widget=forms.MultipleHiddenInput()
)
type = forms.ChoiceField(
choices=add_blank_choice(PORT_TYPE_CHOICES),
required=False,
widget=StaticSelect2()
)
description = forms.CharField(
max_length=100,
required=False
)
class Meta:
nullable_fields = [
'description',
]
class RearPortBulkRenameForm(BulkRenameForm):
pk = forms.ModelMultipleChoiceField(
queryset=RearPort.objects.all(),
@@ -2711,10 +2753,15 @@ class CableFilterForm(BootstrapMixin, forms.Form):
label='Search'
)
type = forms.MultipleChoiceField(
choices=CABLE_TYPE_CHOICES,
choices=add_blank_choice(CABLE_TYPE_CHOICES),
required=False,
widget=StaticSelect2()
)
status = forms.ChoiceField(
required=False,
choices=add_blank_choice(CONNECTION_STATUS_CHOICES),
widget=StaticSelect2()
)
color = forms.CharField(
max_length=6,
required=False,

View File

@@ -0,0 +1,38 @@
# Generated by Django 2.1.5 on 2019-02-14 14:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0068_rack_new_fields'),
]
operations = [
migrations.AlterField(
model_name='device',
name='asset_tag',
field=models.CharField(blank=True, max_length=50, null=True, unique=True),
),
migrations.AlterField(
model_name='device',
name='name',
field=models.CharField(blank=True, max_length=64, null=True, unique=True),
),
migrations.AlterField(
model_name='inventoryitem',
name='asset_tag',
field=models.CharField(blank=True, max_length=50, null=True, unique=True),
),
migrations.AlterField(
model_name='rack',
name='asset_tag',
field=models.CharField(blank=True, max_length=50, null=True, unique=True),
),
migrations.AlterField(
model_name='rack',
name='facility_id',
field=models.CharField(blank=True, max_length=50, null=True),
),
]

View File

@@ -16,7 +16,7 @@ from taggit.managers import TaggableManager
from timezone_field import TimeZoneField
from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange
from utilities.fields import ColorField, NullableCharField
from utilities.fields import ColorField
from utilities.managers import NaturalOrderingManager
from utilities.models import ChangeLoggedModel
from utilities.utils import serialize_object, to_meters
@@ -68,6 +68,10 @@ class ComponentModel(models.Model):
object_data=serialize_object(self)
).save()
@property
def parent(self):
return getattr(self, 'device', None)
class CableTermination(models.Model):
cable = models.ForeignKey(
@@ -162,6 +166,14 @@ class CableTermination(models.Model):
return path + next_segment
def get_cable_peer(self):
if self.cable is None:
return None
if self._cabled_as_a.exists():
return self.cable.termination_b
if self._cabled_as_b.exists():
return self.cable.termination_a
#
# Regions
@@ -205,8 +217,7 @@ class Region(MPTTModel, ChangeLoggedModel):
self.parent.name if self.parent else None,
)
@property
def site_count(self):
def get_site_count(self):
return Site.objects.filter(
Q(region=self) |
Q(region__in=self.get_descendants())
@@ -458,7 +469,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
name = models.CharField(
max_length=50
)
facility_id = NullableCharField(
facility_id = models.CharField(
max_length=50,
blank=True,
null=True,
@@ -499,7 +510,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
blank=True,
verbose_name='Serial number'
)
asset_tag = NullableCharField(
asset_tag = models.CharField(
max_length=50,
blank=True,
null=True,
@@ -968,7 +979,7 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
})
@property
def full_name(self):
def display_name(self):
return '{} {}'.format(self.manufacturer.name, self.model)
@property
@@ -1342,7 +1353,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
blank=True,
null=True
)
name = NullableCharField(
name = models.CharField(
max_length=64,
blank=True,
null=True,
@@ -1353,7 +1364,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
blank=True,
verbose_name='Serial number'
)
asset_tag = NullableCharField(
asset_tag = models.CharField(
max_length=50,
blank=True,
null=True,
@@ -2377,7 +2388,7 @@ class InventoryItem(ComponentModel):
verbose_name='Serial number',
blank=True
)
asset_tag = NullableCharField(
asset_tag = models.CharField(
max_length=50,
unique=True,
blank=True,
@@ -2640,6 +2651,9 @@ class Cable(ChangeLoggedModel):
self.length_unit,
)
def get_status_class(self):
return 'success' if self.status else 'info'
def get_path_endpoints(self):
"""
Traverse both ends of a cable path and return its connected endpoints. Note that one or both endpoints may be

View File

@@ -136,7 +136,8 @@ PLATFORM_ACTIONS = """
"""
DEVICE_ROLE = """
<label class="label" style="background-color: #{{ record.device_role.color }}">{{ value }}</label>
{% load helpers %}
<label class="label" style="color: {{ record.device_role.color|fgcolor }}; background-color: #{{ record.device_role.color }}">{{ value }}</label>
"""
STATUS_LABEL = """
@@ -517,7 +518,7 @@ class DeviceTable(BaseTable):
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
device_type = tables.LinkColumn(
'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
text=lambda record: record.device_type.full_name
text=lambda record: record.device_type.display_name
)
class Meta(BaseTable.Meta):
@@ -646,6 +647,9 @@ class CableTable(BaseTable):
orderable=False,
verbose_name=''
)
status = tables.TemplateColumn(
template_code=STATUS_LABEL
)
length = tables.TemplateColumn(
template_code=CABLE_LENGTH,
order_by='_abs_length'

View File

@@ -855,7 +855,7 @@ class DeviceTypeTest(APITestCase):
self.assertEqual(
sorted(response.data['results'][0]),
['id', 'manufacturer', 'model', 'slug', 'url']
['display_name', 'id', 'manufacturer', 'model', 'slug', 'url']
)
def test_create_devicetype(self):

View File

@@ -0,0 +1,458 @@
import urllib.parse
from django.contrib.auth import get_user_model
from django.test import Client, TestCase
from django.urls import reverse
from dcim.constants import CABLE_TYPE_CAT6, IFACE_FF_1GE_FIXED
from dcim.models import (
Cable, Device, DeviceRole, DeviceType, Interface, InventoryItem, Manufacturer, Platform, Rack, RackGroup,
RackReservation, RackRole, Site, Region, VirtualChassis,
)
class RegionTestCase(TestCase):
def setUp(self):
self.client = Client()
# Create three Regions
for i in range(1, 4):
Region(name='Region {}'.format(i), slug='region-{}'.format(i)).save()
def test_region_list(self):
url = reverse('dcim:region_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class SiteTestCase(TestCase):
def setUp(self):
self.client = Client()
region = Region(name='Region 1', slug='region-1')
region.save()
Site.objects.bulk_create([
Site(name='Site 1', slug='site-1', region=region),
Site(name='Site 2', slug='site-2', region=region),
Site(name='Site 3', slug='site-3', region=region),
])
def test_site_list(self):
url = reverse('dcim:site_list')
params = {
"region": Region.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_site(self):
site = Site.objects.first()
response = self.client.get(site.get_absolute_url())
self.assertEqual(response.status_code, 200)
class RackGroupTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site(name='Site 1', slug='site-1')
site.save()
RackGroup.objects.bulk_create([
RackGroup(name='Rack Group 1', slug='rack-group-1', site=site),
RackGroup(name='Rack Group 2', slug='rack-group-2', site=site),
RackGroup(name='Rack Group 3', slug='rack-group-3', site=site),
])
def test_rackgroup_list(self):
url = reverse('dcim:rackgroup_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class RackTypeTestCase(TestCase):
def setUp(self):
self.client = Client()
RackRole.objects.bulk_create([
RackRole(name='Rack Role 1', slug='rack-role-1'),
RackRole(name='Rack Role 2', slug='rack-role-2'),
RackRole(name='Rack Role 3', slug='rack-role-3'),
])
def test_rackrole_list(self):
url = reverse('dcim:rackrole_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class RackReservationTestCase(TestCase):
def setUp(self):
self.client = Client()
User = get_user_model()
user = User(username='testuser', email='testuser@example.com')
user.save()
site = Site(name='Site 1', slug='site-1')
site.save()
rack = Rack(name='Rack 1', site=site)
rack.save()
RackReservation.objects.bulk_create([
RackReservation(rack=rack, user=user, units=[1, 2, 3], description='Reservation 1'),
RackReservation(rack=rack, user=user, units=[4, 5, 6], description='Reservation 2'),
RackReservation(rack=rack, user=user, units=[7, 8, 9], description='Reservation 3'),
])
def test_rackreservation_list(self):
url = reverse('dcim:rackreservation_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class RackTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site(name='Site 1', slug='site-1')
site.save()
Rack.objects.bulk_create([
Rack(name='Rack 1', site=site),
Rack(name='Rack 2', site=site),
Rack(name='Rack 3', site=site),
])
def test_rack_list(self):
url = reverse('dcim:rack_list')
params = {
"site": Site.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_rack(self):
rack = Rack.objects.first()
response = self.client.get(rack.get_absolute_url())
self.assertEqual(response.status_code, 200)
class ManufacturerTypeTestCase(TestCase):
def setUp(self):
self.client = Client()
Manufacturer.objects.bulk_create([
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
])
def test_manufacturer_list(self):
url = reverse('dcim:manufacturer_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class DeviceTypeTestCase(TestCase):
def setUp(self):
self.client = Client()
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
manufacturer.save()
DeviceType.objects.bulk_create([
DeviceType(model='Device Type 1', slug='device-type-1', manufacturer=manufacturer),
DeviceType(model='Device Type 2', slug='device-type-2', manufacturer=manufacturer),
DeviceType(model='Device Type 3', slug='device-type-3', manufacturer=manufacturer),
])
def test_devicetype_list(self):
url = reverse('dcim:devicetype_list')
params = {
"manufacturer": Manufacturer.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_devicetype(self):
devicetype = DeviceType.objects.first()
response = self.client.get(devicetype.get_absolute_url())
self.assertEqual(response.status_code, 200)
class DeviceRoleTestCase(TestCase):
def setUp(self):
self.client = Client()
DeviceRole.objects.bulk_create([
DeviceRole(name='Device Role 1', slug='device-role-1'),
DeviceRole(name='Device Role 2', slug='device-role-2'),
DeviceRole(name='Device Role 3', slug='device-role-3'),
])
def test_devicerole_list(self):
url = reverse('dcim:devicerole_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class PlatformTestCase(TestCase):
def setUp(self):
self.client = Client()
Platform.objects.bulk_create([
Platform(name='Platform 1', slug='platform-1'),
Platform(name='Platform 2', slug='platform-2'),
Platform(name='Platform 3', slug='platform-3'),
])
def test_platform_list(self):
url = reverse('dcim:platform_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class DeviceTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site(name='Site 1', slug='site-1')
site.save()
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
manufacturer.save()
devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer)
devicetype.save()
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
devicerole.save()
Device.objects.bulk_create([
Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole),
Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole),
Device(name='Device 3', site=site, device_type=devicetype, device_role=devicerole),
])
def test_device_list(self):
url = reverse('dcim:device_list')
params = {
"device_type_id": DeviceType.objects.first().pk,
"role": DeviceRole.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_device(self):
device = Device.objects.first()
response = self.client.get(device.get_absolute_url())
self.assertEqual(response.status_code, 200)
class InventoryItemTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site(name='Site 1', slug='site-1')
site.save()
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
manufacturer.save()
devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer)
devicetype.save()
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
devicerole.save()
device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
device.save()
InventoryItem.objects.bulk_create([
InventoryItem(device=device, name='Inventory Item 1'),
InventoryItem(device=device, name='Inventory Item 2'),
InventoryItem(device=device, name='Inventory Item 3'),
])
def test_inventoryitem_list(self):
url = reverse('dcim:inventoryitem_list')
params = {
"device_id": Device.objects.first().pk,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_inventoryitem(self):
inventoryitem = InventoryItem.objects.first()
response = self.client.get(inventoryitem.get_absolute_url())
self.assertEqual(response.status_code, 200)
class CableTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site(name='Site 1', slug='site-1')
site.save()
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
manufacturer.save()
devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer)
devicetype.save()
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
devicerole.save()
device1 = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
device1.save()
device2 = Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole)
device2.save()
iface1 = Interface(device=device1, name='Interface 1', form_factor=IFACE_FF_1GE_FIXED)
iface1.save()
iface2 = Interface(device=device1, name='Interface 2', form_factor=IFACE_FF_1GE_FIXED)
iface2.save()
iface3 = Interface(device=device1, name='Interface 3', form_factor=IFACE_FF_1GE_FIXED)
iface3.save()
iface4 = Interface(device=device2, name='Interface 1', form_factor=IFACE_FF_1GE_FIXED)
iface4.save()
iface5 = Interface(device=device2, name='Interface 2', form_factor=IFACE_FF_1GE_FIXED)
iface5.save()
iface6 = Interface(device=device2, name='Interface 3', form_factor=IFACE_FF_1GE_FIXED)
iface6.save()
Cable(termination_a=iface1, termination_b=iface4, type=CABLE_TYPE_CAT6).save()
Cable(termination_a=iface2, termination_b=iface5, type=CABLE_TYPE_CAT6).save()
Cable(termination_a=iface3, termination_b=iface6, type=CABLE_TYPE_CAT6).save()
def test_cable_list(self):
url = reverse('dcim:cable_list')
params = {
"type": CABLE_TYPE_CAT6,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_cable(self):
cable = Cable.objects.first()
response = self.client.get(cable.get_absolute_url())
self.assertEqual(response.status_code, 200)
class VirtualMachineTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site.objects.create(name='Site 1', slug='site-1')
manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer-1')
device_type = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
)
device_role = DeviceRole.objects.create(
name='Device Role', slug='device-role-1'
)
# Create 9 member Devices
device1 = Device.objects.create(
device_type=device_type, device_role=device_role, name='Device 1', site=site
)
device2 = Device.objects.create(
device_type=device_type, device_role=device_role, name='Device 2', site=site
)
device3 = Device.objects.create(
device_type=device_type, device_role=device_role, name='Device 3', site=site
)
device4 = Device.objects.create(
device_type=device_type, device_role=device_role, name='Device 4', site=site
)
device5 = Device.objects.create(
device_type=device_type, device_role=device_role, name='Device 5', site=site
)
device6 = Device.objects.create(
device_type=device_type, device_role=device_role, name='Device 6', site=site
)
# Create three VirtualChassis with two members each
vc1 = VirtualChassis.objects.create(master=device1, domain='test-domain-1')
Device.objects.filter(pk=device2.pk).update(virtual_chassis=vc1, vc_position=2)
vc2 = VirtualChassis.objects.create(master=device3, domain='test-domain-2')
Device.objects.filter(pk=device4.pk).update(virtual_chassis=vc2, vc_position=2)
vc3 = VirtualChassis.objects.create(master=device5, domain='test-domain-3')
Device.objects.filter(pk=device6.pk).update(virtual_chassis=vc3, vc_position=2)
def test_virtualchassis_list(self):
url = reverse('dcim:virtualchassis_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_virtualchassis(self):
virtualchassis = VirtualChassis.objects.first()
response = self.client.get(virtualchassis.get_absolute_url())
self.assertEqual(response.status_code, 200)

View File

@@ -215,6 +215,7 @@ urlpatterns = [
# Front ports
# url(r'^devices/front-ports/add/$', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
url(r'^devices/(?P<pk>\d+)/front-ports/add/$', views.FrontPortCreateView.as_view(), name='frontport_add'),
url(r'^devices/(?P<pk>\d+)/front-ports/edit/$', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'),
url(r'^devices/(?P<pk>\d+)/front-ports/delete/$', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'),
url(r'^front-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
url(r'^front-ports/(?P<pk>\d+)/edit/$', views.FrontPortEditView.as_view(), name='frontport_edit'),
@@ -226,6 +227,7 @@ urlpatterns = [
# Rear ports
# url(r'^devices/rear-ports/add/$', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
url(r'^devices/(?P<pk>\d+)/rear-ports/add/$', views.RearPortCreateView.as_view(), name='rearport_add'),
url(r'^devices/(?P<pk>\d+)/rear-ports/edit/$', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'),
url(r'^devices/(?P<pk>\d+)/rear-ports/delete/$', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'),
url(r'^rear-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
url(r'^rear-ports/(?P<pk>\d+)/edit/$', views.RearPortEditView.as_view(), name='rearport_edit'),

View File

@@ -135,7 +135,13 @@ class BulkDisconnectView(GetReturnURLMixin, View):
#
class RegionListView(ObjectListView):
queryset = Region.objects.all()
queryset = Region.objects.add_related_count(
Region.objects.all(),
Site,
'region',
'site_count',
cumulative=True
)
filter = filters.RegionFilter
filter_form = forms.RegionFilterForm
table = tables.RegionTable
@@ -1360,6 +1366,14 @@ class FrontPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = FrontPort
class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_frontport'
queryset = FrontPort.objects.all()
parent_model = Device
table = tables.FrontPortTable
form = forms.FrontPortBulkEditForm
class FrontPortBulkRenameView(PermissionRequiredMixin, BulkRenameView):
permission_required = 'dcim.change_frontport'
queryset = FrontPort.objects.all()
@@ -1404,6 +1418,14 @@ class RearPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = RearPort
class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_rearport'
queryset = RearPort.objects.all()
parent_model = Device
table = tables.RearPortTable
form = forms.RearPortBulkEditForm
class RearPortBulkRenameView(PermissionRequiredMixin, BulkRenameView):
permission_required = 'dcim.change_rearport'
queryset = RearPort.objects.all()

View File

@@ -11,8 +11,8 @@ from taggit.models import Tag
from dcim.models import DeviceRole, Platform, Region, Site
from tenancy.models import Tenant, TenantGroup
from utilities.forms import (
add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect, FilterChoiceField,
FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField,
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect,
FilterChoiceField, FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField,
)
from .constants import (
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
@@ -221,10 +221,6 @@ class TagFilterForm(BootstrapMixin, forms.Form):
#
class ConfigContextForm(BootstrapMixin, forms.ModelForm):
regions = TreeNodeMultipleChoiceField(
queryset=Region.objects.all(),
required=False
)
data = JSONField()
class Meta:
@@ -233,6 +229,26 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenant_groups',
'tenants', 'data',
]
widgets = {
'regions': APISelectMultiple(
api_url="/api/dcim/regions/"
),
'sites': APISelectMultiple(
api_url="/api/dcim/sites/"
),
'roles': APISelectMultiple(
api_url="/api/dcim/device-roles/"
),
'platforms': APISelectMultiple(
api_url="/api/dcim/platforms/"
),
'tenant_groups': APISelectMultiple(
api_url="/api/tenancy/tenant-groups/"
),
'tenants': APISelectMultiple(
api_url="/api/tenancy/tenants/"
)
}
class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
@@ -264,29 +280,53 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
required=False,
label='Search'
)
region = FilterTreeNodeMultipleChoiceField(
region = FilterChoiceField(
queryset=Region.objects.all(),
to_field_name='slug'
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/regions/",
value_field="slug",
)
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug'
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
)
)
role = FilterChoiceField(
queryset=DeviceRole.objects.all(),
to_field_name='slug'
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/device-roles/",
value_field="slug",
)
)
platform = FilterChoiceField(
queryset=Platform.objects.all(),
to_field_name='slug'
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/platforms/",
value_field="slug",
)
)
tenant_group = FilterChoiceField(
queryset=TenantGroup.objects.all(),
to_field_name='slug'
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/tenancy/tenant-groups/",
value_field="slug",
)
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug'
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
)
)

View File

@@ -0,0 +1,105 @@
import urllib.parse
import uuid
from django.contrib.auth.models import User
from django.test import Client, TestCase
from django.urls import reverse
from taggit.models import Tag
from dcim.models import Site
from extras.models import ConfigContext, ObjectChange
class TagTestCase(TestCase):
def setUp(self):
self.client = Client()
Tag.objects.bulk_create([
Tag(name='Tag 1', slug='tag-1'),
Tag(name='Tag 2', slug='tag-2'),
Tag(name='Tag 3', slug='tag-3'),
])
def test_tag_list(self):
url = reverse('extras:tag_list')
params = {
"q": "tag",
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
class ConfigContextTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site(name='Site 1', slug='site-1')
site.save()
# Create three ConfigContexts
for i in range(1, 4):
configcontext = ConfigContext(
name='Config Context {}'.format(i),
data='{{"foo": {}}}'.format(i)
)
configcontext.save()
configcontext.sites.add(site)
def test_configcontext_list(self):
url = reverse('extras:configcontext_list')
params = {
"q": "foo",
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_configcontext(self):
configcontext = ConfigContext.objects.first()
response = self.client.get(configcontext.get_absolute_url())
self.assertEqual(response.status_code, 200)
class ObjectChangeTestCase(TestCase):
def setUp(self):
self.client = Client()
user = User(username='testuser', email='testuser@example.com')
user.save()
site = Site(name='Site 1', slug='site-1')
site.save()
# Create three ObjectChanges
for i in range(1, 4):
site.log_change(
user=user,
request_id=uuid.uuid4(),
action=2
)
def test_objectchange_list(self):
url = reverse('extras:objectchange_list')
params = {
"user": User.objects.first(),
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_objectchange(self):
objectchange = ObjectChange.objects.first()
response = self.client.get(objectchange.get_absolute_url())
self.assertEqual(response.status_code, 200)

View File

@@ -7,7 +7,7 @@ from netaddr.core import AddrFormatError
from dcim.models import Site, Device, Interface
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NumericInFilter, TagFilter
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from virtualization.models import VirtualMachine
from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
@@ -48,7 +48,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
fields = ['name', 'rd', 'enforce_unique']
class RIRFilter(django_filters.FilterSet):
class RIRFilter(NameSlugSearchFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
@@ -96,7 +96,11 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset.filter(qs_filter)
class RoleFilter(django_filters.FilterSet):
class RoleFilter(NameSlugSearchFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
class Meta:
model = Role
@@ -373,7 +377,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset.none()
class VLANGroupFilter(django_filters.FilterSet):
class VLANGroupFilter(NameSlugSearchFilterSet):
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',

View File

@@ -531,7 +531,7 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
null_label='-- Global --',
widget=APISelectMultiple(
api_url="/api/ipam/vrfs/",
value_field="slug",
value_field="rd",
null_option=True,
)
)
@@ -980,7 +980,7 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
null_label='-- Global --',
widget=APISelectMultiple(
api_url="/api/ipam/vrfs/",
value_field="slug",
value_field="rd",
null_option=True,
)
)

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.1.5 on 2019-01-31 18:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0023_change_logging'),
]
operations = [
migrations.AlterField(
model_name='vrf',
name='rd',
field=models.CharField(blank=True, max_length=21, null=True, unique=True),
),
]

View File

@@ -29,6 +29,8 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
rd = models.CharField(
max_length=21,
unique=True,
blank=True,
null=True,
verbose_name='Route distinguisher'
)
tenant = models.ForeignKey(
@@ -79,9 +81,9 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
@property
def display_name(self):
if self.name and self.rd:
if self.rd:
return "{} ({})".format(self.name, self.rd)
return None
return self.name
class RIR(ChangeLoggedModel):

View File

@@ -16,7 +16,7 @@ class VRFTest(APITestCase):
self.vrf1 = VRF.objects.create(name='Test VRF 1', rd='65000:1')
self.vrf2 = VRF.objects.create(name='Test VRF 2', rd='65000:2')
self.vrf3 = VRF.objects.create(name='Test VRF 3', rd='65000:3')
self.vrf3 = VRF.objects.create(name='Test VRF 3') # No RD
def test_get_vrf(self):
@@ -44,19 +44,26 @@ class VRFTest(APITestCase):
def test_create_vrf(self):
data = {
'name': 'Test VRF 4',
'rd': '65000:4',
}
data_list = [
# VRF with RD
{
'name': 'Test VRF 4',
'rd': '65000:4',
},
# VRF without RD
{
'name': 'Test VRF 5',
}
]
url = reverse('ipam-api:vrf-list')
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(VRF.objects.count(), 4)
vrf4 = VRF.objects.get(pk=response.data['id'])
self.assertEqual(vrf4.name, data['name'])
self.assertEqual(vrf4.rd, data['rd'])
for data in data_list:
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
vrf = VRF.objects.get(pk=response.data['id'])
self.assertEqual(vrf.name, data['name'])
self.assertEqual(vrf.rd, data['rd'] if 'rd' in data else None)
def test_create_vrf_bulk(self):

View File

@@ -0,0 +1,282 @@
from netaddr import IPNetwork
import urllib.parse
from django.test import Client, TestCase
from django.urls import reverse
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from ipam.constants import IP_PROTOCOL_TCP
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
class VRFTestCase(TestCase):
def setUp(self):
self.client = Client()
VRF.objects.bulk_create([
VRF(name='VRF 1', rd='65000:1'),
VRF(name='VRF 2', rd='65000:2'),
VRF(name='VRF 3', rd='65000:3'),
])
def test_vrf_list(self):
url = reverse('ipam:vrf_list')
params = {
"q": "65000",
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_configcontext(self):
vrf = VRF.objects.first()
response = self.client.get(vrf.get_absolute_url())
self.assertEqual(response.status_code, 200)
class RIRTestCase(TestCase):
def setUp(self):
self.client = Client()
RIR.objects.bulk_create([
RIR(name='RIR 1', slug='rir-1'),
RIR(name='RIR 2', slug='rir-2'),
RIR(name='RIR 3', slug='rir-3'),
])
def test_rir_list(self):
url = reverse('ipam:rir_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_rir(self):
rir = RIR.objects.first()
response = self.client.get(rir.get_absolute_url())
self.assertEqual(response.status_code, 200)
class AggregateTestCase(TestCase):
def setUp(self):
self.client = Client()
rir = RIR(name='RIR 1', slug='rir-1')
rir.save()
Aggregate.objects.bulk_create([
Aggregate(family=4, prefix=IPNetwork('10.1.0.0/16'), rir=rir),
Aggregate(family=4, prefix=IPNetwork('10.2.0.0/16'), rir=rir),
Aggregate(family=4, prefix=IPNetwork('10.3.0.0/16'), rir=rir),
])
def test_aggregate_list(self):
url = reverse('ipam:aggregate_list')
params = {
"rir": RIR.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_aggregate(self):
aggregate = Aggregate.objects.first()
response = self.client.get(aggregate.get_absolute_url())
self.assertEqual(response.status_code, 200)
class RoleTestCase(TestCase):
def setUp(self):
self.client = Client()
Role.objects.bulk_create([
Role(name='Role 1', slug='role-1'),
Role(name='Role 2', slug='role-2'),
Role(name='Role 3', slug='role-3'),
])
def test_role_list(self):
url = reverse('ipam:role_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class PrefixTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site(name='Site 1', slug='site-1')
site.save()
Prefix.objects.bulk_create([
Prefix(family=4, prefix=IPNetwork('10.1.0.0/16'), site=site),
Prefix(family=4, prefix=IPNetwork('10.2.0.0/16'), site=site),
Prefix(family=4, prefix=IPNetwork('10.3.0.0/16'), site=site),
])
def test_prefix_list(self):
url = reverse('ipam:prefix_list')
params = {
"site": Site.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_prefix(self):
prefix = Prefix.objects.first()
response = self.client.get(prefix.get_absolute_url())
self.assertEqual(response.status_code, 200)
class IPAddressTestCase(TestCase):
def setUp(self):
self.client = Client()
vrf = VRF(name='VRF 1', rd='65000:1')
vrf.save()
IPAddress.objects.bulk_create([
IPAddress(family=4, address=IPNetwork('10.1.0.0/16'), vrf=vrf),
IPAddress(family=4, address=IPNetwork('10.2.0.0/16'), vrf=vrf),
IPAddress(family=4, address=IPNetwork('10.3.0.0/16'), vrf=vrf),
])
def test_ipaddress_list(self):
url = reverse('ipam:ipaddress_list')
params = {
"vrf": VRF.objects.first().rd,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_ipaddress(self):
ipaddress = IPAddress.objects.first()
response = self.client.get(ipaddress.get_absolute_url())
self.assertEqual(response.status_code, 200)
class VLANGroupTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site(name='Site 1', slug='site-1')
site.save()
VLANGroup.objects.bulk_create([
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=site),
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', site=site),
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', site=site),
])
def test_vlangroup_list(self):
url = reverse('ipam:vlangroup_list')
params = {
"site": Site.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
class VLANTestCase(TestCase):
def setUp(self):
self.client = Client()
vlangroup = VLANGroup(name='VLAN Group 1', slug='vlan-group-1')
vlangroup.save()
VLAN.objects.bulk_create([
VLAN(group=vlangroup, vid=101, name='VLAN101'),
VLAN(group=vlangroup, vid=102, name='VLAN102'),
VLAN(group=vlangroup, vid=103, name='VLAN103'),
])
def test_vlan_list(self):
url = reverse('ipam:vlan_list')
params = {
"group": VLANGroup.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_vlan(self):
vlan = VLAN.objects.first()
response = self.client.get(vlan.get_absolute_url())
self.assertEqual(response.status_code, 200)
class ServiceTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site(name='Site 1', slug='site-1')
site.save()
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
manufacturer.save()
devicetype = DeviceType(manufacturer=manufacturer, model='Device Type 1')
devicetype.save()
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
devicerole.save()
device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
device.save()
Service.objects.bulk_create([
Service(device=device, name='Service 1', protocol=IP_PROTOCOL_TCP, port=101),
Service(device=device, name='Service 2', protocol=IP_PROTOCOL_TCP, port=102),
Service(device=device, name='Service 3', protocol=IP_PROTOCOL_TCP, port=103),
])
def test_service_list(self):
url = reverse('ipam:service_list')
params = {
"device_id": Device.objects.first(),
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_service(self):
service = Service.objects.first()
response = self.client.get(service.get_absolute_url())
self.assertEqual(response.status_code, 200)

View File

@@ -126,14 +126,11 @@ class VRFView(View):
def get(self, request, pk):
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
prefix_table = tables.PrefixTable(
list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role')), orderable=False
)
prefix_table.exclude = ('vrf',)
prefix_count = Prefix.objects.filter(vrf=vrf).count()
return render(request, 'ipam/vrf.html', {
'vrf': vrf,
'prefix_table': prefix_table,
'prefix_count': prefix_count,
})

View File

@@ -22,7 +22,7 @@ except ImportError:
)
VERSION = '2.5.4'
VERSION = '2.5.7'
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

File diff suppressed because one or more lines are too long

View File

@@ -197,8 +197,8 @@ $(document).ready(function() {
return obj;
});
// Handle the null option
if (element.getAttribute('data-null-option')) {
// Handle the null option, but only add it once
if (element.getAttribute('data-null-option') && data.previous === null) {
var null_option = $(element).children()[0]
results.unshift({
id: null_option.value,

View File

@@ -1,4 +1,6 @@
$(document).ready(function() {
// Instantiate ClipboardJS on all copy buttons
new ClipboardJS('button.copy-secret');
// Unlocking a secret
$('button.unlock-secret').click(function(event) {
@@ -45,6 +47,7 @@ $(document).ready(function() {
console.log("Secret retrieved successfully");
$('#secret_' + secret_id).text(response.plaintext);
$('button.unlock-secret[secret-id=' + secret_id + ']').hide();
$('button.copy-secret[secret-id=' + secret_id + ']').show();
$('button.lock-secret[secret-id=' + secret_id + ']').show();
} else {
console.log("Secret was not decrypted. Prompt user for private key.");
@@ -67,6 +70,7 @@ $(document).ready(function() {
var secret_div = $('#secret_' + secret_id);
secret_div.html('********');
$('button.lock-secret[secret-id=' + secret_id + ']').hide();
$('button.copy-secret[secret-id=' + secret_id + ']').hide();
$('button.unlock-secret[secret-id=' + secret_id + ']').show();
}

View File

@@ -3,11 +3,11 @@ from django.db.models import Q
from dcim.models import Device
from extras.filters import CustomFieldFilterSet
from utilities.filters import NumericInFilter, TagFilter
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from .models import Secret, SecretRole
class SecretRoleFilter(django_filters.FilterSet):
class SecretRoleFilter(NameSlugSearchFilterSet):
class Meta:
model = SecretRole

View File

@@ -0,0 +1,82 @@
import urllib.parse
from django.contrib.auth import get_user_model
from django.test import Client, TestCase
from django.urls import reverse
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from secrets.models import Secret, SecretRole
class SecretRoleTestCase(TestCase):
def setUp(self):
TEST_USERNAME = 'testuser'
TEST_PASSWORD = 'testpassword'
User = get_user_model()
User.objects.create(username=TEST_USERNAME, email='testuser@example.com', password=TEST_PASSWORD)
self.client = Client()
self.client.login(username=TEST_USERNAME, password=TEST_PASSWORD)
SecretRole.objects.bulk_create([
SecretRole(name='Secret Role 1', slug='secret-role-1'),
SecretRole(name='Secret Role 2', slug='secret-role-2'),
SecretRole(name='Secret Role 3', slug='secret-role-3'),
])
def test_secretrole_list(self):
url = reverse('secrets:secret_list')
response = self.client.get(url, follow=True)
self.assertEqual(response.status_code, 200)
class SecretTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site(name='Site 1', slug='site-1')
site.save()
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
manufacturer.save()
devicetype = DeviceType(manufacturer=manufacturer, model='Device Type 1')
devicetype.save()
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
devicerole.save()
device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
device.save()
secretrole = SecretRole(name='Secret Role 1', slug='secret-role-1')
secretrole.save()
Secret.objects.bulk_create([
Secret(device=device, role=secretrole, name='Secret 1', ciphertext=b'1234567890'),
Secret(device=device, role=secretrole, name='Secret 2', ciphertext=b'1234567890'),
Secret(device=device, role=secretrole, name='Secret 3', ciphertext=b'1234567890'),
])
def test_secret_list(self):
url = reverse('secrets:secret_list')
params = {
"role": SecretRole.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)), follow=True)
self.assertEqual(response.status_code, 200)
def test_configcontext(self):
secret = Secret.objects.first()
response = self.client.get(secret.get_absolute_url(), follow=True)
self.assertEqual(response.status_code, 200)

View File

@@ -69,6 +69,7 @@
<script src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"></script>
<script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.min.js' %}"></script>
<script src="{% static 'select2-4.0.5/js/select2.min.js' %}"></script>
<script src="{% static 'clipboard-2.0.4.min.js' %}"></script>
<script src="{% static 'js/forms.js' %}?v{{ settings.VERSION }}"></script>
<script type="text/javascript">
var netbox_api_path = "/{{ settings.BASE_PATH }}api/";

View File

@@ -85,11 +85,11 @@
</tr>
<tr>
<td>NOC Contact</td>
<td>{{ provider.noc_contact|linebreaksbr|placeholder }}</td>
<td class="rendered-markdown">{{ provider.noc_contact|gfm|placeholder }}</td>
</tr>
<tr>
<td>Admin Contact</td>
<td>{{ provider.admin_contact|linebreaksbr|placeholder }}</td>
<td class="rendered-markdown">{{ provider.admin_contact|gfm|placeholder }}</td>
</tr>
<tr>
<td>Circuits</td>

View File

@@ -4,7 +4,7 @@
{% load form_helpers %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
<form method="post" class="form form-horizontal">
{% csrf_token %}
{% for field in form.hidden_fields %}
{{ field }}

View File

@@ -163,7 +163,7 @@
<tr>
<td>Device Type</td>
<td>
<span><a href="{% url 'dcim:devicetype' pk=device.device_type.pk %}">{{ device.device_type.full_name }}</a> ({{ device.device_type.u_height }}U)</span>
<span><a href="{% url 'dcim:devicetype' pk=device.device_type.pk %}">{{ device.device_type.display_name }}</a> ({{ device.device_type.u_height }}U)</span>
</td>
</tr>
<tr>
@@ -416,7 +416,7 @@
<span class="text-muted">&mdash;</span>
{% endif %}
</td>
<td>{{ rd.device_type.full_name }}</td>
<td>{{ rd.device_type.display_name }}</td>
</tr>
{% endfor %}
</table>
@@ -682,7 +682,8 @@
<th>Rear Port</th>
<th>Position</th>
<th>Description</th>
<th>Connected Cable</th>
<th>Cable</th>
<th colspan="2">Connection</th>
<th></th>
</tr>
</thead>
@@ -697,6 +698,9 @@
<button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
</button>
<button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
</button>
<button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
</button>
@@ -735,7 +739,8 @@
<th>Type</th>
<th>Positions</th>
<th>Description</th>
<th>Connected Cable</th>
<th>Cable</th>
<th colspan="2">Connection</th>
<th></th>
</tr>
</thead>
@@ -750,6 +755,9 @@
<button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
</button>
<button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
</button>
<button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
</button>

View File

@@ -13,7 +13,7 @@
<table class="table table-hover panel-body attr-table">
<tr>
<td>Model</td>
<td>{{ device.device_type.full_name }}</td>
<td>{{ device.device_type.display_name }}</td>
</tr>
<tr>
<td>Serial Number</td>

View File

@@ -15,7 +15,7 @@
<a href="{% url 'dcim:device' pk=devicebay.installed_device.pk %}">{{ devicebay.installed_device }}</a>
</td>
<td>
<span>{{ devicebay.installed_device.device_type.full_name }}</span>
<span>{{ devicebay.installed_device.device_type.display_name }}</span>
</td>
{% else %}
<td></td>

View File

@@ -23,14 +23,35 @@
{# Description #}
<td>{{ frontport.description|placeholder }}</td>
{# Cable #}
<td>
{% if frontport.cable %}
{# Cable/connection #}
{% if frontport.cable %}
<td>
<a href="{{ frontport.cable.get_absolute_url }}">{{ frontport.cable }}</a>
{% else %}
<a href="{% url 'dcim:frontport_trace' pk=frontport.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i>
</a>
</td>
{% with far_end=frontport.get_cable_peer %}
<td>
{% if far_end.parent.provider %}
<i class="fa fa-fw fa-globe" title="Circuit"></i>
<a href="{{ far_end.parent.get_absolute_url }}">
{{ far_end.parent.provider }}
{{ far_end.parent }}
</a>
{% else %}
<a href="{{ far_end.parent.get_absolute_url }}">
{{ far_end.parent }}
</a>
{% endif %}
</td>
<td>{{ far_end }}</td>
{% endwith %}
{% else %}
<td colspan="3">
<span class="text-muted">Not connected</span>
{% endif %}
</td>
</td>
{% endif %}
{# Actions #}
<td class="text-right">

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="iface_{{ iface.name }}">
<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 }}">
{# Checkbox #}
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
@@ -96,7 +96,7 @@
{{ peer_termination.connected_endpoint.device }}
</a><br/>
<small>via <i class="fa fa-fw fa-globe" title="Circuit"></i>
<a href="{{ iface.connected_endpoint.circuit.get_absolure_url }}">
<a href="{{ iface.connected_endpoint.circuit.get_absolute_url }}">
{{ iface.connected_endpoint.circuit.provider }}
{{ iface.connected_endpoint.circuit }}
</a>
@@ -150,7 +150,7 @@
{% if perms.dcim.change_interface %}
{% if iface.cable %}
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=iface.cable %}
{% elif not iface.is_virtual and perms.dcim.add_cable %}
{% elif iface.is_connectable and perms.dcim.add_cable %}
<a href="{% url 'dcim:interface_connect' termination_a_id=iface.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-success btn-xs" title="Connect">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
</a>

View File

@@ -25,8 +25,8 @@
{% if u.device %}
<li class="occupied h{{ u.device.device_type.u_height }}u"{% ifequal u.device.face face_id %} style="background-color: #{{ u.device.device_role.color }}"{% endifequal %}>
{% 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"
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 %}">
<a href="{% url 'dcim:device' pk=u.device.pk %}" style="color: {{ u.device.device_role.color|fgcolor }}" data-toggle="popover" data-trigger="hover" data-container="body" data-html="true"
data-content="{{ u.device.device_role }}<br />{{ u.device.device_type.display_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 }}
{% if u.device.devicebay_count %}
({{ u.device.get_children.count }}/{{ u.device.devicebay_count }})

View File

@@ -22,14 +22,35 @@
{# Description #}
<td>{{ rearport.description|placeholder }}</td>
{# Cable #}
<td>
{% if rearport.cable %}
{# Cable/connection #}
{% if rearport.cable %}
<td>
<a href="{{ rearport.cable.get_absolute_url }}">{{ rearport.cable }}</a>
{% else %}
<a href="{% url 'dcim:rearport_trace' pk=rearport.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i>
</a>
</td>
{% with far_end=rearport.get_cable_peer %}
<td>
{% if far_end.parent.provider %}
<i class="fa fa-fw fa-globe" title="Circuit"></i>
<a href="{{ far_end.parent.get_absolute_url }}">
{{ far_end.parent.provider }}
{{ far_end.parent }}
</a>
{% else %}
<a href="{{ far_end.parent.get_absolute_url }}">
{{ far_end.parent }}
</a>
{% endif %}
</td>
<td>{{ far_end }}</td>
{% endwith %}
{% else %}
<td colspan="3">
<span class="text-muted">Not connected</span>
{% endif %}
</td>
</td>
{% endif %}
{# Actions #}
<td class="text-right">

View File

@@ -14,20 +14,29 @@
{% render_field form.mgmt_only %}
{% render_field form.description %}
{% render_field form.mode %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Tags</strong></div>
<div class="panel-body">
{% render_field form.tags %}
</div>
</div>
{% if obj.mode %}
<div class="panel panel-default" id="vlans_panel">
<div class="panel-heading"><strong>802.1Q VLANs</strong></div>
<div class="panel panel-default" id="vlans_panel">
<div class="panel-heading"><strong>802.1Q VLANs</strong></div>
{% if obj.mode %}
{% include 'dcim/inc/interface_vlans_table.html' %}
<div class="panel-footer text-right">
<a href="{% url 'dcim:interface_assign_vlans' pk=obj.pk %}?return_url={% url 'dcim:interface_edit' pk=obj.pk %}" class="btn btn-primary btn-xs{% if form.instance.mode == 100 and form.instance.untagged_vlan %} disabled{% endif %}">
<i class="glyphicon glyphicon-plus"></i> Add VLANs
</a>
</div>
</div>
{% endif %}
{% else %}
<div class="panel-body text-center text-muted">
<p>802.1Q mode not set</p>
</div>
{% endif %}
</div>
{% endblock %}
{% block buttons %}

View File

@@ -208,7 +208,7 @@
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
</td>
<td>{{ device.device_role }}</td>
<td>{{ device.device_type.full_name }}</td>
<td>{{ device.device_type.display_name }}</td>
<td>
{% if device.parent_bay %}
<a href="{{ device.parent_bay.device.get_absolute_url }}">{{ device.parent_bay }}</a>

View File

@@ -45,7 +45,6 @@
{% endblock %}
{% block javascript %}
{% include 'dcim/inc/filter_rack_group.html' %}
<script type="text/javascript">
$(function() {
$('[data-toggle="popover"]').popover()

View File

@@ -67,7 +67,7 @@
<td>VRF</td>
<td>
{% if ipaddress.vrf %}
<a href="{% url 'ipam:vrf' pk=ipaddress.vrf.pk %}">{{ ipaddress.vrf }}</a> ({{ ipaddress.vrf.rd }})
<a href="{% url 'ipam:vrf' pk=ipaddress.vrf.pk %}">{{ ipaddress.vrf }}</a>
{% else %}
<span>Global</span>
{% endif %}

View File

@@ -83,19 +83,19 @@
<tr>
<td>Description</td>
<td>{{ vrf.description|placeholder }}</td>
</tr>
<tr>
<td>Prefixes</td>
<td>
<a href="{% url 'ipam:prefix_list' %}?vrf_id={{ vrf.pk }}">{{ prefix_count }}</a>
</td>
</tr>
</table>
</div>
{% include 'inc/custom_fields_panel.html' with obj=vrf %}
{% include 'extras/inc/tags_panel.html' with tags=vrf.tags.all url='ipam:vrf_list' %}
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Prefixes</strong>
</div>
{% include 'responsive_table.html' with table=prefix_table %}
</div>
</div>
{% include 'inc/custom_fields_panel.html' with obj=vrf %}
</div>
</div>
{% endblock %}

View File

@@ -8,6 +8,9 @@
<button class="btn btn-xs btn-success unlock-secret" secret-id="{{ secret.pk }}">
<i class="fa fa-lock"></i> Unlock
</button>
<button class="btn btn-xs btn-default copy-secret collapse" secret-id="{{ secret.pk }}" data-clipboard-target="#secret_{{ secret.pk }}">
<i class="fa fa-copy"></i> Copy
</button>
<button class="btn btn-xs btn-danger lock-secret collapse" secret-id="{{ secret.pk }}">
<i class="fa fa-unlock-alt"></i> Lock
</button>

View File

@@ -77,11 +77,14 @@
</form>
<div class="row">
<div class="col-md-2">Secret</div>
<div class="col-md-8" id="secret_{{ secret.pk }}">********</div>
<div class="col-md-2 text-right">
<div class="col-md-6" id="secret_{{ secret.pk }}">********</div>
<div class="col-md-4 text-right">
<button class="btn btn-xs btn-success unlock-secret" secret-id="{{ secret.pk }}">
<i class="fa fa-lock"></i> Unlock
</button>
<button class="btn btn-xs btn-default copy-secret collapse" secret-id="{{ secret.pk }}" data-clipboard-target="#secret_{{ secret.pk }}">
<i class="fa fa-copy"></i> Copy
</button>
<button class="btn btn-xs btn-danger lock-secret collapse" secret-id="{{ secret.pk }}">
<i class="fa fa-unlock-alt"></i> Lock
</button>

View File

@@ -4,6 +4,14 @@
{% block content %}
<h1>{% block title %}Editing {{ table.rows|length }} {{ obj_type_plural|bettertitle }}{% endblock %}</h1>
{% if form.errors %}
<div class="panel panel-danger">
<div class="panel-heading"><strong>Errors</strong></div>
<div class="panel-body">
{{ form.errors }}
</div>
</div>
{% endif %}
<form action="" method="post" class="form form-horizontal">
{% csrf_token %}
{% if request.POST.return_url %}

View File

@@ -2,11 +2,11 @@ import django_filters
from django.db.models import Q
from extras.filters import CustomFieldFilterSet
from utilities.filters import NumericInFilter, TagFilter
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from .models import Tenant, TenantGroup
class TenantGroupFilter(django_filters.FilterSet):
class TenantGroupFilter(NameSlugSearchFilterSet):
class Meta:
model = TenantGroup

View File

@@ -0,0 +1,58 @@
import urllib.parse
from django.test import Client, TestCase
from django.urls import reverse
from tenancy.models import Tenant, TenantGroup
class TenantGroupTestCase(TestCase):
def setUp(self):
self.client = Client()
TenantGroup.objects.bulk_create([
TenantGroup(name='Tenant Group 1', slug='tenant-group-1'),
TenantGroup(name='Tenant Group 2', slug='tenant-group-2'),
TenantGroup(name='Tenant Group 3', slug='tenant-group-3'),
])
def test_tenantgroup_list(self):
url = reverse('tenancy:tenantgroup_list')
response = self.client.get(url, follow=True)
self.assertEqual(response.status_code, 200)
class TenantTestCase(TestCase):
def setUp(self):
self.client = Client()
tenantgroup = TenantGroup(name='Tenant Group 1', slug='tenant-group-1')
tenantgroup.save()
Tenant.objects.bulk_create([
Tenant(name='Tenant 1', slug='tenant-1', group=tenantgroup),
Tenant(name='Tenant 2', slug='tenant-2', group=tenantgroup),
Tenant(name='Tenant 3', slug='tenant-3', group=tenantgroup),
])
def test_tenant_list(self):
url = reverse('tenancy:tenant_list')
params = {
"group": TenantGroup.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)), follow=True)
self.assertEqual(response.status_code, 200)
def test_tenant(self):
tenant = Tenant.objects.first()
response = self.client.get(tenant.get_absolute_url(), follow=True)
self.assertEqual(response.status_code, 200)

View File

@@ -7,6 +7,7 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import is_safe_url
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import View
from secrets.forms import UserKeyForm
@@ -23,6 +24,10 @@ from .models import Token
class LoginView(View):
template_name = 'login.html'
@method_decorator(sensitive_post_parameters('password'))
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
def get(self, request):
form = LoginForm(request)

View File

@@ -10,6 +10,8 @@ ColorValidator = RegexValidator(
)
# Deprecated: Retained only to ensure successful migration from early releases
# Use models.CharField(null=True) instead
class NullableCharField(models.CharField):
description = "Stores empty values as NULL rather than ''"

View File

@@ -1,4 +1,6 @@
import django_filters
from django.conf import settings
from django.db.models import Q
from taggit.models import Tag
@@ -13,12 +15,11 @@ class NullableCharFieldFilter(django_filters.CharFilter):
"""
Allow matching on null field values by passing a special string used to signify NULL.
"""
null_value = 'NULL'
def filter(self, qs, value):
if value != self.null_value:
if value != settings.FILTERS_NULL_CHOICE_VALUE:
return super().filter(qs, value)
qs = self.get_method(qs)(**{'{}__isnull'.format(self.name): True})
qs = self.get_method(qs)(**{'{}__isnull'.format(self.field_name): True})
return qs.distinct() if self.distinct else qs
@@ -35,3 +36,21 @@ class TagFilter(django_filters.ModelMultipleChoiceFilter):
kwargs.setdefault('queryset', Tag.objects.all())
super().__init__(*args, **kwargs)
class NameSlugSearchFilterSet(django_filters.FilterSet):
"""
A base class for adding the search method to models which only expose the `name` and `slug` fields
"""
q = django_filters.CharFilter(
method='search',
label='Search',
)
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(slug__icontains=value)
)

View File

@@ -7,19 +7,19 @@ from netaddr.core import AddrFormatError
from dcim.models import DeviceRole, Interface, Platform, Region, Site
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NumericInFilter, TagFilter
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from .constants import VM_STATUS_CHOICES
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
class ClusterTypeFilter(django_filters.FilterSet):
class ClusterTypeFilter(NameSlugSearchFilterSet):
class Meta:
model = ClusterType
fields = ['name', 'slug']
class ClusterGroupFilter(django_filters.FilterSet):
class ClusterGroupFilter(NameSlugSearchFilterSet):
class Meta:
model = ClusterGroup
@@ -196,6 +196,10 @@ class VirtualMachineFilter(CustomFieldFilterSet):
class InterfaceFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
field_name='virtual_machine',
queryset=VirtualMachine.objects.all(),
@@ -225,3 +229,10 @@ class InterfaceFilter(django_filters.FilterSet):
return queryset.filter(mac_address=mac)
except AddrFormatError:
return queryset.none()
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value)
)

View File

@@ -0,0 +1,117 @@
import urllib.parse
from django.test import Client, TestCase
from django.urls import reverse
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
class ClusterGroupTestCase(TestCase):
def setUp(self):
self.client = Client()
ClusterGroup.objects.bulk_create([
ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'),
ClusterGroup(name='Cluster Group 2', slug='cluster-group-2'),
ClusterGroup(name='Cluster Group 3', slug='cluster-group-3'),
])
def test_clustergroup_list(self):
url = reverse('virtualization:clustergroup_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class ClusterTypeTestCase(TestCase):
def setUp(self):
self.client = Client()
ClusterType.objects.bulk_create([
ClusterType(name='Cluster Type 1', slug='cluster-type-1'),
ClusterType(name='Cluster Type 2', slug='cluster-type-2'),
ClusterType(name='Cluster Type 3', slug='cluster-type-3'),
])
def test_clustertype_list(self):
url = reverse('virtualization:clustertype_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class ClusterTestCase(TestCase):
def setUp(self):
self.client = Client()
clustergroup = ClusterGroup(name='Cluster Group 1', slug='cluster-group-1')
clustergroup.save()
clustertype = ClusterType(name='Cluster Type 1', slug='cluster-type-1')
clustertype.save()
Cluster.objects.bulk_create([
Cluster(name='Cluster 1', group=clustergroup, type=clustertype),
Cluster(name='Cluster 2', group=clustergroup, type=clustertype),
Cluster(name='Cluster 3', group=clustergroup, type=clustertype),
])
def test_cluster_list(self):
url = reverse('virtualization:cluster_list')
params = {
"group": ClusterGroup.objects.first().slug,
"type": ClusterType.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_cluster(self):
cluster = Cluster.objects.first()
response = self.client.get(cluster.get_absolute_url())
self.assertEqual(response.status_code, 200)
class VirtualMachineTestCase(TestCase):
def setUp(self):
self.client = Client()
clustertype = ClusterType(name='Cluster Type 1', slug='cluster-type-1')
clustertype.save()
cluster = Cluster(name='Cluster 1', type=clustertype)
cluster.save()
VirtualMachine.objects.bulk_create([
VirtualMachine(name='Virtual Machine 1', cluster=cluster),
VirtualMachine(name='Virtual Machine 2', cluster=cluster),
VirtualMachine(name='Virtual Machine 3', cluster=cluster),
])
def test_virtualmachine_list(self):
url = reverse('virtualization:virtualmachine_list')
params = {
"cluster_id": Cluster.objects.first().pk,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_virtualmachine(self):
virtualmachine = VirtualMachine.objects.first()
response = self.client.get(virtualmachine.get_absolute_url())
self.assertEqual(response.status_code, 200)