mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-11 06:12:16 -06:00
Merge branch 'develop' into develop-2.8
This commit is contained in:
@@ -823,6 +823,13 @@ class RackElevationFilterForm(RackFilterForm):
|
||||
#
|
||||
|
||||
class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
|
||||
rack = forms.ModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
widget=forms.HiddenInput()
|
||||
)
|
||||
# TODO: Change this to an API-backed form field. We can't do this currently because we want to retain
|
||||
# the multi-line <select> widget for easy selection of multiple rack units.
|
||||
units = SimpleArrayField(
|
||||
base_field=forms.IntegerField(),
|
||||
widget=ArrayFieldSelectMultiple(
|
||||
@@ -841,7 +848,7 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
fields = [
|
||||
'units', 'user', 'tenant_group', 'tenant', 'description',
|
||||
'rack', 'units', 'user', 'tenant_group', 'tenant', 'description',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -849,7 +856,8 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Populate rack unit choices
|
||||
self.fields['units'].widget.choices = self._get_unit_choices()
|
||||
if hasattr(self.instance, 'rack'):
|
||||
self.fields['units'].widget.choices = self._get_unit_choices()
|
||||
|
||||
def _get_unit_choices(self):
|
||||
rack = self.instance.rack
|
||||
|
||||
@@ -21,6 +21,7 @@ from dcim.constants import *
|
||||
from dcim.fields import ASNField
|
||||
from dcim.elevations import RackElevationSVG
|
||||
from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from utilities.fields import ColorField, NaturalOrderingField
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.utils import serialize_object, to_meters
|
||||
@@ -75,6 +76,7 @@ __all__ = (
|
||||
# Regions
|
||||
#
|
||||
|
||||
@extras_features('export_templates', 'webhooks')
|
||||
class Region(MPTTModel, ChangeLoggedModel):
|
||||
"""
|
||||
Sites can be grouped within geographic Regions.
|
||||
@@ -138,6 +140,7 @@ class Region(MPTTModel, ChangeLoggedModel):
|
||||
# Sites
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'graphs', 'export_templates', 'webhooks')
|
||||
class Site(ChangeLoggedModel, CustomFieldModel):
|
||||
"""
|
||||
A Site represents a geographic location within a network; typically a building or campus. The optional facility
|
||||
@@ -288,6 +291,7 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
||||
# Racks
|
||||
#
|
||||
|
||||
@extras_features('export_templates')
|
||||
class RackGroup(MPTTModel, ChangeLoggedModel):
|
||||
"""
|
||||
Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For
|
||||
@@ -396,6 +400,7 @@ class RackRole(ChangeLoggedModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
"""
|
||||
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
||||
@@ -806,6 +811,9 @@ class RackReservation(ChangeLoggedModel):
|
||||
def __str__(self):
|
||||
return "Reservation for rack {}".format(self.rack)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:rackreservation', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
|
||||
if self.units:
|
||||
@@ -857,6 +865,7 @@ class RackReservation(ChangeLoggedModel):
|
||||
# Device Types
|
||||
#
|
||||
|
||||
@extras_features('export_templates', 'webhooks')
|
||||
class Manufacturer(ChangeLoggedModel):
|
||||
"""
|
||||
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
|
||||
@@ -892,6 +901,7 @@ class Manufacturer(ChangeLoggedModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
"""
|
||||
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
|
||||
@@ -1240,6 +1250,7 @@ class Platform(ChangeLoggedModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'graphs', 'export_templates', 'webhooks')
|
||||
class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
"""
|
||||
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
|
||||
@@ -1675,6 +1686,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
@extras_features('export_templates', 'webhooks')
|
||||
class VirtualChassis(ChangeLoggedModel):
|
||||
"""
|
||||
A collection of Devices which operate with a shared control plane (e.g. a switch stack).
|
||||
@@ -1741,6 +1753,7 @@ class VirtualChassis(ChangeLoggedModel):
|
||||
# Power
|
||||
#
|
||||
|
||||
@extras_features('custom_links', 'export_templates', 'webhooks')
|
||||
class PowerPanel(ChangeLoggedModel):
|
||||
"""
|
||||
A distribution point for electrical power; e.g. a data center RPP.
|
||||
@@ -1787,6 +1800,7 @@ class PowerPanel(ChangeLoggedModel):
|
||||
))
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
|
||||
"""
|
||||
An electrical circuit delivered from a PowerPanel.
|
||||
@@ -1948,6 +1962,7 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
|
||||
# Cables
|
||||
#
|
||||
|
||||
@extras_features('custom_links', 'export_templates', 'webhooks')
|
||||
class Cable(ChangeLoggedModel):
|
||||
"""
|
||||
A physical connection between two endpoints.
|
||||
|
||||
@@ -11,6 +11,7 @@ from dcim.constants import *
|
||||
from dcim.exceptions import LoopDetected
|
||||
from dcim.fields import MACAddressField
|
||||
from extras.models import ObjectChange, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from utilities.fields import NaturalOrderingField
|
||||
from utilities.ordering import naturalize_interface
|
||||
from utilities.utils import serialize_object
|
||||
@@ -169,6 +170,7 @@ class CableTermination(models.Model):
|
||||
# Console ports
|
||||
#
|
||||
|
||||
@extras_features('export_templates', 'webhooks')
|
||||
class ConsolePort(CableTermination, ComponentModel):
|
||||
"""
|
||||
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
||||
@@ -229,6 +231,7 @@ class ConsolePort(CableTermination, ComponentModel):
|
||||
# Console server ports
|
||||
#
|
||||
|
||||
@extras_features('webhooks')
|
||||
class ConsoleServerPort(CableTermination, ComponentModel):
|
||||
"""
|
||||
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
||||
@@ -282,6 +285,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
|
||||
# Power ports
|
||||
#
|
||||
|
||||
@extras_features('export_templates', 'webhooks')
|
||||
class PowerPort(CableTermination, ComponentModel):
|
||||
"""
|
||||
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
||||
@@ -443,6 +447,7 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
# Power outlets
|
||||
#
|
||||
|
||||
@extras_features('webhooks')
|
||||
class PowerOutlet(CableTermination, ComponentModel):
|
||||
"""
|
||||
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
||||
@@ -519,6 +524,7 @@ class PowerOutlet(CableTermination, ComponentModel):
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
@extras_features('graphs', 'export_templates', 'webhooks')
|
||||
class Interface(CableTermination, ComponentModel):
|
||||
"""
|
||||
A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other
|
||||
@@ -792,6 +798,7 @@ class Interface(CableTermination, ComponentModel):
|
||||
# Pass-through ports
|
||||
#
|
||||
|
||||
@extras_features('webhooks')
|
||||
class FrontPort(CableTermination, ComponentModel):
|
||||
"""
|
||||
A pass-through port on the front of a Device.
|
||||
@@ -864,6 +871,7 @@ class FrontPort(CableTermination, ComponentModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('webhooks')
|
||||
class RearPort(CableTermination, ComponentModel):
|
||||
"""
|
||||
A pass-through port on the rear of a Device.
|
||||
@@ -915,6 +923,7 @@ class RearPort(CableTermination, ComponentModel):
|
||||
# Device bays
|
||||
#
|
||||
|
||||
@extras_features('webhooks')
|
||||
class DeviceBay(ComponentModel):
|
||||
"""
|
||||
An empty space within a Device which can house a child device
|
||||
@@ -989,6 +998,7 @@ class DeviceBay(ComponentModel):
|
||||
# Inventory items
|
||||
#
|
||||
|
||||
@extras_features('export_templates', 'webhooks')
|
||||
class InventoryItem(ComponentModel):
|
||||
"""
|
||||
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
|
||||
|
||||
@@ -341,21 +341,38 @@ class RackDetailTable(RackTable):
|
||||
|
||||
class RackReservationTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
reservation = tables.LinkColumn(
|
||||
viewname='dcim:rackreservation',
|
||||
args=[Accessor('pk')],
|
||||
accessor='pk'
|
||||
)
|
||||
site = tables.LinkColumn(
|
||||
viewname='dcim:site',
|
||||
accessor=Accessor('rack.site'),
|
||||
args=[Accessor('rack.site.slug')],
|
||||
)
|
||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
||||
unit_list = tables.Column(orderable=False, verbose_name='Units')
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
rack = tables.LinkColumn(
|
||||
viewname='dcim:rack',
|
||||
args=[Accessor('rack.pk')]
|
||||
)
|
||||
unit_list = tables.Column(
|
||||
orderable=False,
|
||||
verbose_name='Units'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=RACKRESERVATION_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name=''
|
||||
template_code=RACKRESERVATION_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackReservation
|
||||
fields = ('pk', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'actions')
|
||||
fields = (
|
||||
'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'actions',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -2017,6 +2017,20 @@ class DeviceTest(APITestCase):
|
||||
|
||||
self.assertFalse('config_context' in response.data['results'][0])
|
||||
|
||||
def test_unique_name_per_site_constraint(self):
|
||||
|
||||
data = {
|
||||
'device_type': self.devicetype1.pk,
|
||||
'device_role': self.devicerole1.pk,
|
||||
'name': 'Test Device 1',
|
||||
'site': self.site1.pk,
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:device-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class ConsolePortTest(APITestCase):
|
||||
|
||||
|
||||
@@ -176,10 +176,6 @@ class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = RackReservation
|
||||
|
||||
# Disable inapplicable tests
|
||||
test_get_object = None
|
||||
test_create_object = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ from django.urls import path
|
||||
|
||||
from extras.views import ObjectChangeLogView, ImageAttachmentEditView
|
||||
from ipam.views import ServiceCreateView
|
||||
from secrets.views import secret_add
|
||||
from . import views
|
||||
from .models import (
|
||||
Cable, ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, FrontPort, Interface, Manufacturer, Platform,
|
||||
@@ -51,9 +50,11 @@ urlpatterns = [
|
||||
|
||||
# Rack reservations
|
||||
path('rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'),
|
||||
path('rack-reservations/add/', views.RackReservationCreateView.as_view(), name='rackreservation_add'),
|
||||
path('rack-reservations/import/', views.RackReservationImportView.as_view(), name='rackreservation_import'),
|
||||
path('rack-reservations/edit/', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'),
|
||||
path('rack-reservations/delete/', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
|
||||
path('rack-reservations/<int:pk>/', views.RackReservationView.as_view(), name='rackreservation'),
|
||||
path('rack-reservations/<int:pk>/edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
|
||||
path('rack-reservations/<int:pk>/delete/', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
|
||||
path('rack-reservations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}),
|
||||
@@ -69,7 +70,6 @@ urlpatterns = [
|
||||
path('racks/<int:pk>/edit/', views.RackEditView.as_view(), name='rack_edit'),
|
||||
path('racks/<int:pk>/delete/', views.RackDeleteView.as_view(), name='rack_delete'),
|
||||
path('racks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}),
|
||||
path('racks/<int:rack>/reservations/add/', views.RackReservationCreateView.as_view(), name='rack_add_reservation'),
|
||||
path('racks/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
|
||||
|
||||
# Manufacturers
|
||||
@@ -179,7 +179,6 @@ urlpatterns = [
|
||||
path('devices/<int:pk>/status/', views.DeviceStatusView.as_view(), name='device_status'),
|
||||
path('devices/<int:pk>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
|
||||
path('devices/<int:pk>/config/', views.DeviceConfigView.as_view(), name='device_config'),
|
||||
path('devices/<int:pk>/add-secret/', secret_add, name='device_addsecret'),
|
||||
path('devices/<int:device>/services/assign/', ServiceCreateView.as_view(), name='device_service_assign'),
|
||||
path('devices/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
|
||||
|
||||
|
||||
@@ -479,20 +479,32 @@ class RackReservationListView(PermissionRequiredMixin, ObjectListView):
|
||||
action_buttons = ('export',)
|
||||
|
||||
|
||||
class RackReservationView(PermissionRequiredMixin, View):
|
||||
permission_required = 'dcim.view_rackreservation'
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
rackreservation = get_object_or_404(RackReservation.objects.prefetch_related('rack'), pk=pk)
|
||||
|
||||
return render(request, 'dcim/rackreservation.html', {
|
||||
'rackreservation': rackreservation,
|
||||
})
|
||||
|
||||
|
||||
class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.add_rackreservation'
|
||||
model = RackReservation
|
||||
model_form = forms.RackReservationForm
|
||||
template_name = 'dcim/rackreservation_edit.html'
|
||||
default_return_url = 'dcim:rackreservation_list'
|
||||
|
||||
def alter_obj(self, obj, request, args, kwargs):
|
||||
if not obj.pk:
|
||||
obj.rack = get_object_or_404(Rack, pk=kwargs['rack'])
|
||||
if 'rack' in request.GET:
|
||||
obj.rack = get_object_or_404(Rack, pk=request.GET.get('rack'))
|
||||
obj.user = request.user
|
||||
return obj
|
||||
|
||||
def get_return_url(self, request, obj):
|
||||
return obj.rack.get_absolute_url()
|
||||
|
||||
|
||||
class RackReservationEditView(RackReservationCreateView):
|
||||
permission_required = 'dcim.change_rackreservation'
|
||||
@@ -501,9 +513,7 @@ class RackReservationEditView(RackReservationCreateView):
|
||||
class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_rackreservation'
|
||||
model = RackReservation
|
||||
|
||||
def get_return_url(self, request, obj):
|
||||
return obj.rack.get_absolute_url()
|
||||
default_return_url = 'dcim:rackreservation_list'
|
||||
|
||||
|
||||
class RackReservationImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
Reference in New Issue
Block a user