Merge pull request #2922 from digitalocean/develop

Release v2.5.7
This commit is contained in:
Jeremy Stretch 2019-02-21 14:44:48 -05:00 committed by GitHub
commit ac1e4b8e8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1417 additions and 45 deletions

View File

@ -1,3 +1,29 @@
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

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

@ -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

@ -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
@ -270,11 +276,14 @@ 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',
@ -288,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'],
]
]
@ -355,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 = (
@ -377,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

@ -543,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)',
@ -602,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():

View File

@ -2362,7 +2362,7 @@ class FrontPortCreateForm(ComponentForm):
class FrontPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Interface.objects.all(),
queryset=FrontPort.objects.all(),
widget=forms.MultipleHiddenInput()
)
type = forms.ChoiceField(
@ -2436,7 +2436,7 @@ class RearPortCreateForm(ComponentForm):
class RearPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Interface.objects.all(),
queryset=RearPort.objects.all(),
widget=forms.MultipleHiddenInput()
)
type = forms.ChoiceField(
@ -2753,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
@ -217,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())
@ -470,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,
@ -511,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,
@ -1354,7 +1353,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
blank=True,
null=True
)
name = NullableCharField(
name = models.CharField(
max_length=64,
blank=True,
null=True,
@ -1365,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,
@ -2389,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,
@ -2652,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

@ -647,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

@ -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

@ -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

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

@ -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

@ -22,7 +22,7 @@ except ImportError:
)
VERSION = '2.5.6'
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

@ -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

@ -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

@ -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,7 +25,7 @@
{% 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"
<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 %}

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

@ -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

@ -87,7 +87,7 @@
<tr>
<td>Prefixes</td>
<td>
<a href="{% url 'ipam:prefix_list' %}?vrf={{ vrf.rd }}">{{ prefix_count }}</a>
<a href="{% url 'ipam:prefix_list' %}?vrf_id={{ vrf.pk }}">{{ prefix_count }}</a>
</td>
</tr>
</table>

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

@ -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

@ -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,5 @@
import django_filters
from django.conf import settings
from django.db.models import Q
from taggit.models import Tag
@ -14,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

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)