Merge branch 'develop' into develop-2.8

This commit is contained in:
Jeremy Stretch
2020-03-03 13:20:00 -05:00
77 changed files with 1331 additions and 565 deletions

View File

@@ -3,8 +3,8 @@ from rest_framework import serializers
from dcim.constants import CONNECTION_STATUS_CHOICES
from dcim.models import (
Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceType, DeviceRole, FrontPort, FrontPortTemplate,
Interface, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerPanel, PowerPort, Rack, RackGroup, RackRole,
RearPort, RearPortTemplate, Region, Site, VirtualChassis,
Interface, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerPanel, PowerPort, PowerPortTemplate, Rack,
RackGroup, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
)
from utilities.api import ChoiceField, WritableNestedSerializer
@@ -25,6 +25,7 @@ __all__ = [
'NestedPowerOutletSerializer',
'NestedPowerPanelSerializer',
'NestedPowerPortSerializer',
'NestedPowerPortTemplateSerializer',
'NestedRackGroupSerializer',
'NestedRackRoleSerializer',
'NestedRackSerializer',
@@ -111,6 +112,14 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'manufacturer', 'model', 'slug', 'display_name', 'device_count']
class NestedPowerPortTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail')
class Meta:
model = PowerPortTemplate
fields = ['id', 'url', 'name']
class NestedRearPortTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')

View File

@@ -172,6 +172,10 @@ class RackReservationSerializer(ValidatedModelSerializer):
class RackElevationDetailFilterSerializer(serializers.Serializer):
q = serializers.CharField(
required=False,
default=None
)
face = serializers.ChoiceField(
choices=DeviceFaceChoices,
default=DeviceFaceChoices.FACE_FRONT
@@ -278,7 +282,7 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
allow_blank=True,
required=False
)
power_port = PowerPortTemplateSerializer(
power_port = NestedPowerPortTemplateSerializer(
required=False
)
feed_leg = ChoiceField(
@@ -294,7 +298,7 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
class InterfaceTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(choices=InterfaceTypeChoices, required=False)
type = ChoiceField(choices=InterfaceTypeChoices)
class Meta:
model = InterfaceTemplate
@@ -514,7 +518,7 @@ class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer()
type = ChoiceField(choices=InterfaceTypeChoices, required=False)
type = ChoiceField(choices=InterfaceTypeChoices)
lag = NestedInterfaceSerializer(required=False, allow_null=True)
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)

View File

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

View File

@@ -55,10 +55,12 @@ class RackTypeChoices(ChoiceSet):
class RackWidthChoices(ChoiceSet):
WIDTH_10IN = 10
WIDTH_19IN = 19
WIDTH_23IN = 23
CHOICES = (
(WIDTH_10IN, '10 inches'),
(WIDTH_19IN, '19 inches'),
(WIDTH_23IN, '23 inches'),
)
@@ -836,6 +838,7 @@ class PortTypeChoices(ChoiceSet):
TYPE_8P8C = '8p8c'
TYPE_110_PUNCH = '110-punch'
TYPE_BNC = 'bnc'
TYPE_MRJ21 = 'mrj21'
TYPE_ST = 'st'
TYPE_SC = 'sc'
TYPE_SC_APC = 'sc-apc'
@@ -854,6 +857,7 @@ class PortTypeChoices(ChoiceSet):
(TYPE_8P8C, '8P8C'),
(TYPE_110_PUNCH, '110 Punch'),
(TYPE_BNC, 'BNC'),
(TYPE_MRJ21, 'MRJ21'),
),
),
(
@@ -904,6 +908,7 @@ class CableTypeChoices(ChoiceSet):
TYPE_CAT7 = 'cat7'
TYPE_DAC_ACTIVE = 'dac-active'
TYPE_DAC_PASSIVE = 'dac-passive'
TYPE_MRJ21_TRUNK = 'mrj21-trunk'
TYPE_COAXIAL = 'coaxial'
TYPE_MMF = 'mmf'
TYPE_MMF_OM1 = 'mmf-om1'
@@ -927,6 +932,7 @@ class CableTypeChoices(ChoiceSet):
(TYPE_CAT7, 'CAT7'),
(TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'),
(TYPE_DAC_PASSIVE, 'Direct Attach Copper (Passive)'),
(TYPE_MRJ21_TRUNK, 'MRJ21 Trunk'),
(TYPE_COAXIAL, 'Coaxial'),
),
),
@@ -973,10 +979,12 @@ class CableStatusChoices(ChoiceSet):
STATUS_CONNECTED = 'connected'
STATUS_PLANNED = 'planned'
STATUS_DECOMMISSIONING = 'decommissioning'
CHOICES = (
(STATUS_CONNECTED, 'Connected'),
(STATUS_PLANNED, 'Planned'),
(STATUS_DECOMMISSIONING, 'Decommissioning'),
)
LEGACY_MAP = {

View File

@@ -61,13 +61,10 @@ POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage
# Cabling and connections
#
# TODO: Replace with CableStatusChoices?
# Console/power/interface connection statuses
CONNECTION_STATUS_PLANNED = False
CONNECTION_STATUS_CONNECTED = True
CONNECTION_STATUS_CHOICES = [
[CONNECTION_STATUS_PLANNED, 'Planned'],
[CONNECTION_STATUS_CONNECTED, 'Connected'],
[False, 'Not Connected'],
[True, 'Connected'],
]
# Cable endpoint types

View File

@@ -20,6 +20,16 @@ class RackElevationSVG:
self.rack = rack
self.include_images = include_images
def _get_device_description(self, device):
return '{} ({}) — {} ({}U) {} {}'.format(
device.name,
device.device_role,
device.device_type.display_name,
device.device_type.u_height,
device.asset_tag or '',
device.serial or ''
)
@staticmethod
def _add_gradient(drawing, id_, color):
gradient = drawing.linearGradient(
@@ -64,10 +74,7 @@ class RackElevationSVG:
fill='black'
)
)
link.set_desc('{}{} ({}U) {} {}'.format(
device.device_role, device.device_type.display_name,
device.device_type.u_height, device.asset_tag or '', device.serial or ''
))
link.set_desc(self._get_device_description(device))
link.add(drawing.rect(start, end, style='fill: #{}'.format(color), class_='slot'))
hex_color = '#{}'.format(foreground_color(color))
link.add(drawing.text(str(name), insert=text, fill=hex_color))
@@ -81,10 +88,7 @@ class RackElevationSVG:
def _draw_device_rear(self, drawing, device, start, end, text):
rect = drawing.rect(start, end, class_="slot blocked")
rect.set_desc('{}{} ({}U) {} {}'.format(
device.device_role, device.device_type.display_name,
device.device_type.u_height, device.asset_tag or '', device.serial or ''
))
rect.set_desc(self._get_device_description(device))
drawing.add(rect)
drawing.add(drawing.text(str(device), insert=text))

View File

@@ -2344,6 +2344,11 @@ class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm):
class ConsolePortFilterForm(DeviceComponentFilterForm):
model = ConsolePort
type = forms.MultipleChoiceField(
choices=ConsolePortTypeChoices,
required=False,
widget=StaticSelect2Multiple()
)
tag = TagFilterField(model)
@@ -2429,6 +2434,11 @@ class ConsolePortCSVForm(forms.ModelForm):
class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
model = ConsoleServerPort
type = forms.MultipleChoiceField(
choices=ConsolePortTypeChoices,
required=False,
widget=StaticSelect2Multiple()
)
tag = TagFilterField(model)
@@ -2528,6 +2538,11 @@ class ConsoleServerPortCSVForm(forms.ModelForm):
class PowerPortFilterForm(DeviceComponentFilterForm):
model = PowerPort
type = forms.MultipleChoiceField(
choices=PowerPortTypeChoices,
required=False,
widget=StaticSelect2Multiple()
)
tag = TagFilterField(model)
@@ -2633,6 +2648,11 @@ class PowerPortCSVForm(forms.ModelForm):
class PowerOutletFilterForm(DeviceComponentFilterForm):
model = PowerOutlet
type = forms.MultipleChoiceField(
choices=PowerOutletTypeChoices,
required=False,
widget=StaticSelect2Multiple()
)
tag = TagFilterField(model)
@@ -2821,6 +2841,11 @@ class PowerOutletBulkDisconnectForm(ConfirmationForm):
class InterfaceFilterForm(DeviceComponentFilterForm):
model = Interface
type = forms.MultipleChoiceField(
choices=InterfaceTypeChoices,
required=False,
widget=StaticSelect2Multiple()
)
enabled = forms.NullBooleanField(
required=False,
widget=StaticSelect2(
@@ -3190,6 +3215,11 @@ class InterfaceBulkDisconnectForm(ConfirmationForm):
class FrontPortFilterForm(DeviceComponentFilterForm):
model = FrontPort
type = forms.MultipleChoiceField(
choices=PortTypeChoices,
required=False,
widget=StaticSelect2Multiple()
)
tag = TagFilterField(model)
@@ -3379,6 +3409,11 @@ class FrontPortBulkDisconnectForm(ConfirmationForm):
class RearPortFilterForm(DeviceComponentFilterForm):
model = RearPort
type = forms.MultipleChoiceField(
choices=PortTypeChoices,
required=False,
widget=StaticSelect2Multiple()
)
tag = TagFilterField(model)

View File

@@ -0,0 +1,19 @@
# Generated by Django 2.2.10 on 2020-03-03 16:59
from django.db import migrations, models
import utilities.validators
class Migration(migrations.Migration):
dependencies = [
('dcim', '0098_devicetype_images'),
]
operations = [
migrations.AlterField(
model_name='powerfeed',
name='voltage',
field=models.SmallIntegerField(default=120, validators=[utilities.validators.ExclusionValidator([0])]),
),
]

View File

@@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0098_devicetype_images'),
('dcim', '0099_powerfeed_negative_voltage'),
]
operations = [

View File

@@ -20,10 +20,11 @@ from dcim.choices import *
from dcim.constants import *
from dcim.fields import ASNField
from dcim.elevations import RackElevationSVG
from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem
from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem
from utilities.fields import ColorField, NaturalOrderingField
from utilities.models import ChangeLoggedModel
from utilities.utils import to_meters
from utilities.utils import serialize_object, to_meters
from utilities.validators import ExclusionValidator
from .device_component_templates import (
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate,
PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
@@ -118,6 +119,15 @@ class Region(MPTTModel, ChangeLoggedModel):
Q(region__in=self.get_descendants())
).count()
def to_objectchange(self, action):
# Remove MPTT-internal fields
return ObjectChange(
changed_object=self,
object_repr=str(self),
action=action,
object_data=serialize_object(self, exclude=['level', 'lft', 'rght', 'tree_id'])
)
#
# Sites
@@ -1766,9 +1776,9 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
choices=PowerFeedPhaseChoices,
default=PowerFeedPhaseChoices.PHASE_SINGLE
)
voltage = models.PositiveSmallIntegerField(
validators=[MinValueValidator(1)],
default=POWERFEED_VOLTAGE_DEFAULT
voltage = models.SmallIntegerField(
default=POWERFEED_VOLTAGE_DEFAULT,
validators=[ExclusionValidator([0])]
)
amperage = models.PositiveSmallIntegerField(
validators=[MinValueValidator(1)],
@@ -1850,10 +1860,16 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
self.rack, self.rack.site, self.power_panel, self.power_panel.site
))
# AC voltage cannot be negative
if self.voltage < 0 and self.supply == PowerFeedSupplyChoices.SUPPLY_AC:
raise ValidationError({
"voltage": "Voltage cannot be negative for AC supply"
})
def save(self, *args, **kwargs):
# Cache the available_power property on the instance
kva = self.voltage * self.amperage * (self.max_utilization / 100)
kva = abs(self.voltage) * self.amperage * (self.max_utilization / 100)
if self.phase == PowerFeedPhaseChoices.PHASE_3PHASE:
self.available_power = round(kva * 1.732)
else:
@@ -1956,6 +1972,7 @@ class Cable(ChangeLoggedModel):
STATUS_CLASS_MAP = {
CableStatusChoices.STATUS_CONNECTED: 'success',
CableStatusChoices.STATUS_PLANNED: 'info',
CableStatusChoices.STATUS_DECOMMISSIONING: 'warning',
}
class Meta:
@@ -2116,14 +2133,14 @@ class Cable(ChangeLoggedModel):
b_path = self.termination_a.trace()
# Determine overall path status (connected or planned)
if self.status == CableStatusChoices.STATUS_PLANNED:
path_status = CONNECTION_STATUS_PLANNED
else:
path_status = CONNECTION_STATUS_CONNECTED
if self.status == CableStatusChoices.STATUS_CONNECTED:
path_status = True
for segment in a_path[1:] + b_path[1:]:
if segment[1] is None or segment[1].status == CableStatusChoices.STATUS_PLANNED:
path_status = CONNECTION_STATUS_PLANNED
if segment[1] is None or segment[1].status != CableStatusChoices.STATUS_CONNECTED:
path_status = False
break
else:
path_status = False
a_endpoint = a_path[-1][2]
b_endpoint = b_path[-1][2]

View File

@@ -1,4 +1,4 @@
from django.core.exceptions import ValidationError
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
@@ -37,11 +37,17 @@ class ComponentTemplateModel(models.Model):
raise NotImplementedError()
def to_objectchange(self, action):
# Annotate the parent DeviceType
try:
device_type = self.device_type
except ObjectDoesNotExist:
# The parent DeviceType has already been deleted
device_type = None
return ObjectChange(
changed_object=self,
object_repr=str(self),
action=action,
related_object=self.device_type,
related_object=device_type,
object_data=serialize_object(self)
)

View File

@@ -360,9 +360,21 @@ class PowerPort(CableTermination, ComponentModel):
@property
def connected_endpoint(self):
if self._connected_poweroutlet:
return self._connected_poweroutlet
return self._connected_powerfeed
"""
Return the connected PowerOutlet, if it exists, or the connected PowerFeed, if it exists. We have to check for
ObjectDoesNotExist in case the referenced object has been deleted from the database.
"""
try:
if self._connected_poweroutlet:
return self._connected_poweroutlet
except ObjectDoesNotExist:
pass
try:
if self._connected_powerfeed:
return self._connected_powerfeed
except ObjectDoesNotExist:
pass
return None
@connected_endpoint.setter
def connected_endpoint(self, value):
@@ -717,9 +729,21 @@ class Interface(CableTermination, ComponentModel):
@property
def connected_endpoint(self):
if self._connected_interface:
return self._connected_interface
return self._connected_circuittermination
"""
Return the connected Interface, if it exists, or the connected CircuitTermination, if it exists. We have to
check for ObjectDoesNotExist in case the referenced object has been deleted from the database.
"""
try:
if self._connected_interface:
return self._connected_interface
except ObjectDoesNotExist:
pass
try:
if self._connected_circuittermination:
return self._connected_circuittermination
except ObjectDoesNotExist:
pass
return None
@connected_endpoint.setter
def connected_endpoint(self, value):

View File

@@ -4,7 +4,6 @@ from netaddr import IPNetwork
from rest_framework import status
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from dcim.api import serializers
from dcim.choices import *
from dcim.constants import *
from dcim.models import (
@@ -589,6 +588,28 @@ class RackTest(APITestCase):
self.assertEqual(response.data['name'], self.rack1.name)
def test_get_elevation_rack_units(self):
url = '{}?q=3'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk}))
response = self.client.get(url, **self.header)
self.assertEqual(response.data['count'], 13)
url = '{}?q=U3'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk}))
response = self.client.get(url, **self.header)
self.assertEqual(response.data['count'], 11)
url = '{}?q=10'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk}))
response = self.client.get(url, **self.header)
self.assertEqual(response.data['count'], 1)
url = '{}?q=U20'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk}))
response = self.client.get(url, **self.header)
self.assertEqual(response.data['count'], 1)
def test_get_rack_elevation(self):
url = reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk})
@@ -1441,13 +1462,13 @@ class InterfaceTemplateTest(APITestCase):
manufacturer=self.manufacturer, model='Test Device Type 1', slug='test-device-type-1'
)
self.interfacetemplate1 = InterfaceTemplate.objects.create(
device_type=self.devicetype, name='Test Interface Template 1'
device_type=self.devicetype, name='Test Interface Template 1', type='1000base-t'
)
self.interfacetemplate2 = InterfaceTemplate.objects.create(
device_type=self.devicetype, name='Test Interface Template 2'
device_type=self.devicetype, name='Test Interface Template 2', type='1000base-t'
)
self.interfacetemplate3 = InterfaceTemplate.objects.create(
device_type=self.devicetype, name='Test Interface Template 3'
device_type=self.devicetype, name='Test Interface Template 3', type='1000base-t'
)
def test_get_interfacetemplate(self):
@@ -1469,6 +1490,7 @@ class InterfaceTemplateTest(APITestCase):
data = {
'device_type': self.devicetype.pk,
'name': 'Test Interface Template 4',
'type': '1000base-t',
}
url = reverse('dcim-api:interfacetemplate-list')
@@ -1486,14 +1508,17 @@ class InterfaceTemplateTest(APITestCase):
{
'device_type': self.devicetype.pk,
'name': 'Test Interface Template 4',
'type': '1000base-t',
},
{
'device_type': self.devicetype.pk,
'name': 'Test Interface Template 5',
'type': '1000base-t',
},
{
'device_type': self.devicetype.pk,
'name': 'Test Interface Template 6',
'type': '1000base-t',
},
]
@@ -1511,6 +1536,7 @@ class InterfaceTemplateTest(APITestCase):
data = {
'device_type': self.devicetype.pk,
'name': 'Test Interface Template X',
'type': '1000base-x-gbic',
}
url = reverse('dcim-api:interfacetemplate-detail', kwargs={'pk': self.interfacetemplate1.pk})
@@ -2621,9 +2647,9 @@ class InterfaceTest(APITestCase):
self.device = Device.objects.create(
device_type=devicetype, device_role=devicerole, name='Test Device 1', site=site
)
self.interface1 = Interface.objects.create(device=self.device, name='Test Interface 1')
self.interface2 = Interface.objects.create(device=self.device, name='Test Interface 2')
self.interface3 = Interface.objects.create(device=self.device, name='Test Interface 3')
self.interface1 = Interface.objects.create(device=self.device, name='Test Interface 1', type='1000base-t')
self.interface2 = Interface.objects.create(device=self.device, name='Test Interface 2', type='1000base-t')
self.interface3 = Interface.objects.create(device=self.device, name='Test Interface 3', type='1000base-t')
self.vlan1 = VLAN.objects.create(name="Test VLAN 1", vid=1)
self.vlan2 = VLAN.objects.create(name="Test VLAN 2", vid=2)
@@ -2684,6 +2710,7 @@ class InterfaceTest(APITestCase):
data = {
'device': self.device.pk,
'name': 'Test Interface 4',
'type': '1000base-t',
}
url = reverse('dcim-api:interface-list')
@@ -2700,6 +2727,7 @@ class InterfaceTest(APITestCase):
data = {
'device': self.device.pk,
'name': 'Test Interface 4',
'type': '1000base-t',
'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': self.vlan3.id,
'tagged_vlans': [self.vlan1.id, self.vlan2.id],
@@ -2721,14 +2749,17 @@ class InterfaceTest(APITestCase):
{
'device': self.device.pk,
'name': 'Test Interface 4',
'type': '1000base-t',
},
{
'device': self.device.pk,
'name': 'Test Interface 5',
'type': '1000base-t',
},
{
'device': self.device.pk,
'name': 'Test Interface 6',
'type': '1000base-t',
},
]
@@ -2747,6 +2778,7 @@ class InterfaceTest(APITestCase):
{
'device': self.device.pk,
'name': 'Test Interface 4',
'type': '1000base-t',
'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': self.vlan2.id,
'tagged_vlans': [self.vlan1.id],
@@ -2754,6 +2786,7 @@ class InterfaceTest(APITestCase):
{
'device': self.device.pk,
'name': 'Test Interface 5',
'type': '1000base-t',
'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': self.vlan2.id,
'tagged_vlans': [self.vlan1.id],
@@ -2761,6 +2794,7 @@ class InterfaceTest(APITestCase):
{
'device': self.device.pk,
'name': 'Test Interface 6',
'type': '1000base-t',
'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': self.vlan2.id,
'tagged_vlans': [self.vlan1.id],
@@ -2786,6 +2820,7 @@ class InterfaceTest(APITestCase):
data = {
'device': self.device.pk,
'name': 'Test Interface X',
'type': '1000base-x-gbic',
'lag': lag_interface.pk,
}

View File

@@ -11,7 +11,7 @@ from dcim.models import (
VirtualChassis,
)
from ipam.models import IPAddress
from tenancy.models import Tenant
from tenancy.models import Tenant, TenantGroup
from virtualization.models import Cluster, ClusterType
@@ -76,10 +76,24 @@ class SiteTestCase(TestCase):
for region in regions:
region.save()
tenant_groups = (
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'),
)
TenantGroup.objects.bulk_create(tenant_groups)
tenants = (
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]),
Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]),
)
Tenant.objects.bulk_create(tenants)
sites = (
Site(name='Site 1', slug='site-1', region=regions[0], status=SiteStatusChoices.STATUS_ACTIVE, facility='Facility 1', asn=65001, latitude=10, longitude=10, contact_name='Contact 1', contact_phone='123-555-0001', contact_email='contact1@example.com'),
Site(name='Site 2', slug='site-2', region=regions[1], status=SiteStatusChoices.STATUS_PLANNED, facility='Facility 2', asn=65002, latitude=20, longitude=20, contact_name='Contact 2', contact_phone='123-555-0002', contact_email='contact2@example.com'),
Site(name='Site 3', slug='site-3', region=regions[2], status=SiteStatusChoices.STATUS_RETIRED, facility='Facility 3', asn=65003, latitude=30, longitude=30, contact_name='Contact 3', contact_phone='123-555-0003', contact_email='contact3@example.com'),
Site(name='Site 1', slug='site-1', region=regions[0], tenant=tenants[0], status=SiteStatusChoices.STATUS_ACTIVE, facility='Facility 1', asn=65001, latitude=10, longitude=10, contact_name='Contact 1', contact_phone='123-555-0001', contact_email='contact1@example.com'),
Site(name='Site 2', slug='site-2', region=regions[1], tenant=tenants[1], status=SiteStatusChoices.STATUS_PLANNED, facility='Facility 2', asn=65002, latitude=20, longitude=20, contact_name='Contact 2', contact_phone='123-555-0002', contact_email='contact2@example.com'),
Site(name='Site 3', slug='site-3', region=regions[2], tenant=tenants[2], status=SiteStatusChoices.STATUS_RETIRED, facility='Facility 3', asn=65003, latitude=30, longitude=30, contact_name='Contact 3', contact_phone='123-555-0003', contact_email='contact3@example.com'),
)
Site.objects.bulk_create(sites)
@@ -140,6 +154,20 @@ class SiteTestCase(TestCase):
params = {'region': [regions[0].slug, regions[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_tenant(self):
tenants = Tenant.objects.all()[:2]
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_tenant_group(self):
tenant_groups = TenantGroup.objects.all()[:2]
params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class RackGroupTestCase(TestCase):
queryset = RackGroup.objects.all()
@@ -266,10 +294,24 @@ class RackTestCase(TestCase):
)
RackRole.objects.bulk_create(rack_roles)
tenant_groups = (
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'),
)
TenantGroup.objects.bulk_create(tenant_groups)
tenants = (
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]),
Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]),
)
Tenant.objects.bulk_create(tenants)
racks = (
Rack(name='Rack 1', facility_id='rack-1', site=sites[0], group=rack_groups[0], status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
Rack(name='Rack 2', facility_id='rack-2', site=sites[1], group=rack_groups[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_19IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
Rack(name='Rack 3', facility_id='rack-3', site=sites[2], group=rack_groups[2], status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH),
Rack(name='Rack 1', facility_id='rack-1', site=sites[0], group=rack_groups[0], tenant=tenants[0], status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
Rack(name='Rack 2', facility_id='rack-2', site=sites[1], group=rack_groups[1], tenant=tenants[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_19IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
Rack(name='Rack 3', facility_id='rack-3', site=sites[2], group=rack_groups[2], tenant=tenants[2], status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH),
)
Rack.objects.bulk_create(racks)
@@ -366,6 +408,20 @@ class RackTestCase(TestCase):
params = {'serial': 'abc'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_tenant(self):
tenants = Tenant.objects.all()[:2]
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_tenant_group(self):
tenant_groups = TenantGroup.objects.all()[:2]
params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class RackReservationTestCase(TestCase):
queryset = RackReservation.objects.all()
@@ -402,10 +458,24 @@ class RackReservationTestCase(TestCase):
)
User.objects.bulk_create(users)
tenant_groups = (
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'),
)
TenantGroup.objects.bulk_create(tenant_groups)
tenants = (
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]),
Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]),
)
Tenant.objects.bulk_create(tenants)
reservations = (
RackReservation(rack=racks[0], units=[1, 2, 3], user=users[0]),
RackReservation(rack=racks[1], units=[4, 5, 6], user=users[1]),
RackReservation(rack=racks[2], units=[7, 8, 9], user=users[2]),
RackReservation(rack=racks[0], units=[1, 2, 3], user=users[0], tenant=tenants[0]),
RackReservation(rack=racks[1], units=[4, 5, 6], user=users[1], tenant=tenants[1]),
RackReservation(rack=racks[2], units=[7, 8, 9], user=users[2], tenant=tenants[2]),
)
RackReservation.objects.bulk_create(reservations)
@@ -436,6 +506,20 @@ class RackReservationTestCase(TestCase):
# params = {'user': [users[0].username, users[1].username]}
# self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_tenant(self):
tenants = Tenant.objects.all()[:2]
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_tenant_group(self):
tenant_groups = TenantGroup.objects.all()[:2]
params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class ManufacturerTestCase(TestCase):
queryset = Manufacturer.objects.all()
@@ -1099,10 +1183,24 @@ class DeviceTestCase(TestCase):
)
Cluster.objects.bulk_create(clusters)
tenant_groups = (
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'),
)
TenantGroup.objects.bulk_create(tenant_groups)
tenants = (
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]),
Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]),
)
Tenant.objects.bulk_create(tenants)
devices = (
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], serial='ABC', asset_tag='1001', site=sites[0], rack=racks[0], position=1, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_ACTIVE, cluster=clusters[0], local_context_data={"foo": 123}),
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], platform=platforms[1], serial='DEF', asset_tag='1002', site=sites[1], rack=racks[1], position=2, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_STAGED, cluster=clusters[1]),
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], platform=platforms[2], serial='GHI', asset_tag='1003', site=sites[2], rack=racks[2], position=3, face=DeviceFaceChoices.FACE_REAR, status=DeviceStatusChoices.STATUS_FAILED, cluster=clusters[2]),
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], tenant=tenants[0], serial='ABC', asset_tag='1001', site=sites[0], rack=racks[0], position=1, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_ACTIVE, cluster=clusters[0], local_context_data={"foo": 123}),
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], platform=platforms[1], tenant=tenants[1], serial='DEF', asset_tag='1002', site=sites[1], rack=racks[1], position=2, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_STAGED, cluster=clusters[1]),
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], platform=platforms[2], tenant=tenants[2], serial='GHI', asset_tag='1003', site=sites[2], rack=racks[2], position=3, face=DeviceFaceChoices.FACE_REAR, status=DeviceStatusChoices.STATUS_FAILED, cluster=clusters[2]),
)
Device.objects.bulk_create(devices)
@@ -1333,6 +1431,20 @@ class DeviceTestCase(TestCase):
params = {'local_context_data': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_tenant(self):
tenants = Tenant.objects.all()[:2]
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_tenant_group(self):
tenant_groups = TenantGroup.objects.all()[:2]
params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class ConsolePortTestCase(TestCase):
queryset = ConsolePort.objects.all()

View File

@@ -2,7 +2,6 @@ from django.core.exceptions import ValidationError
from django.test import TestCase
from dcim.choices import *
from dcim.constants import CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_PLANNED
from dcim.models import *
from tenancy.models import Tenant
@@ -522,14 +521,14 @@ class CablePathTestCase(TestCase):
cable3.save()
interface1 = Interface.objects.get(pk=self.interface1.pk)
self.assertEqual(interface1.connected_endpoint, self.interface2)
self.assertEqual(interface1.connection_status, CONNECTION_STATUS_PLANNED)
self.assertFalse(interface1.connection_status)
# Switch third segment from planned to connected
cable3.status = CableStatusChoices.STATUS_CONNECTED
cable3.save()
interface1 = Interface.objects.get(pk=self.interface1.pk)
self.assertEqual(interface1.connected_endpoint, self.interface2)
self.assertEqual(interface1.connection_status, CONNECTION_STATUS_CONNECTED)
self.assertTrue(interface1.connection_status)
def test_path_teardown(self):
@@ -542,7 +541,7 @@ class CablePathTestCase(TestCase):
cable3.save()
interface1 = Interface.objects.get(pk=self.interface1.pk)
self.assertEqual(interface1.connected_endpoint, self.interface2)
self.assertEqual(interface1.connection_status, CONNECTION_STATUS_CONNECTED)
self.assertTrue(interface1.connection_status)
# Remove a cable
cable2.delete()

View File

@@ -357,7 +357,7 @@ class RackElevationListView(PermissionRequiredMixin, View):
def get(self, request):
racks = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role', 'devices__device_type')
racks = Rack.objects.prefetch_related('role')
racks = filters.RackFilterSet(request.GET, racks).qs
total_count = racks.count()