mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Merge branch 'develop-2.9' into 2006-scripts-reports-background
This commit is contained in:
commit
f98fa364c0
@ -156,9 +156,13 @@ direction = ChoiceVar(choices=CHOICES)
|
||||
|
||||
### ObjectVar
|
||||
|
||||
A NetBox object. The list of available objects is defined by the queryset parameter. Each instance of this variable is limited to a single object type.
|
||||
A NetBox object of a particular type, identified by the associated queryset. Most models will utilize the REST API to retrieve available options: Note that any filtering on the queryset in this case has no effect.
|
||||
|
||||
* `queryset` - A [Django queryset](https://docs.djangoproject.com/en/stable/topics/db/queries/)
|
||||
* `queryset` - The base [Django queryset](https://docs.djangoproject.com/en/stable/topics/db/queries/) for the model
|
||||
|
||||
### MultiObjectVar
|
||||
|
||||
Similar to `ObjectVar`, but allows for the selection of multiple objects.
|
||||
|
||||
### FileVar
|
||||
|
||||
@ -222,10 +226,7 @@ class NewBranchScript(Script):
|
||||
)
|
||||
switch_model = ObjectVar(
|
||||
description="Access switch model",
|
||||
queryset = DeviceType.objects.filter(
|
||||
manufacturer__name='Cisco',
|
||||
model__in=['Catalyst 3560X-48T', 'Catalyst 3750X-48T']
|
||||
)
|
||||
queryset = DeviceType.objects.all()
|
||||
)
|
||||
|
||||
def run(self, data, commit):
|
||||
|
@ -382,6 +382,22 @@ When determining the primary IP address for a device, IPv6 is preferred over IPv
|
||||
|
||||
---
|
||||
|
||||
## RACK_ELEVATION_DEFAULT_UNIT_HEIGHT
|
||||
|
||||
Default: 22
|
||||
|
||||
Default height (in pixels) of a unit within a rack elevation. For best results, this should be approximately one tenth of `RACK_ELEVATION_DEFAULT_UNIT_WIDTH`.
|
||||
|
||||
---
|
||||
|
||||
## RACK_ELEVATION_DEFAULT_UNIT_WIDTH
|
||||
|
||||
Default: 220
|
||||
|
||||
Default width (in pixels) of a unit within a rack elevation.
|
||||
|
||||
---
|
||||
|
||||
## REMOTE_AUTH_ENABLED
|
||||
|
||||
Default: `False`
|
||||
|
@ -44,11 +44,7 @@ If you're adding a relational field (e.g. `ForeignKey`) and intend to include th
|
||||
|
||||
Extend the model's API serializer in `<app>.api.serializers` to include the new field. In most cases, it will not be necessary to also extend the nested serializer, which produces a minimal represenation of the model.
|
||||
|
||||
## 6. Add choices to API view
|
||||
|
||||
If the new field has static choices, add it to the `FieldChoicesViewSet` for the app.
|
||||
|
||||
## 7. Add field to forms
|
||||
## 6. Add field to forms
|
||||
|
||||
Extend any forms to include the new field as appropriate. Common forms include:
|
||||
|
||||
@ -57,19 +53,19 @@ Extend any forms to include the new field as appropriate. Common forms include:
|
||||
* **CSV import** - The form used when bulk importing objects in CSV format
|
||||
* **Filter** - Displays the options available for filtering a list of objects (both UI and API)
|
||||
|
||||
## 8. Extend object filter set
|
||||
## 7. Extend object filter set
|
||||
|
||||
If the new field should be filterable, add it to the `FilterSet` for the model. If the field should be searchable, remember to reference it in the FilterSet's `search()` method.
|
||||
|
||||
## 9. Add column to object table
|
||||
## 8. Add column to object table
|
||||
|
||||
If the new field will be included in the object list view, add a column to the model's table. For simple fields, adding the field name to `Meta.fields` will be sufficient. More complex fields may require explicitly declaring a new column.
|
||||
|
||||
## 10. Update the UI templates
|
||||
## 9. Update the UI templates
|
||||
|
||||
Edit the object's view template to display the new field. There may also be a custom add/edit form template that needs to be updated.
|
||||
|
||||
## 11. Create/extend test cases
|
||||
## 10. Create/extend test cases
|
||||
|
||||
Create or extend the relevant test cases to verify that the new field and any accompanying validation logic perform as expected. This is especially important for relational fields. NetBox incorporates various test suites, including:
|
||||
|
||||
|
@ -1,11 +1,20 @@
|
||||
# NetBox v2.8
|
||||
|
||||
## v2.8.7 (FUTURE)
|
||||
## v2.8.7 (2020-07-02)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#4796](https://github.com/netbox-community/netbox/issues/4796) - Introduce configuration parameters for default rack elevation size
|
||||
* [#4802](https://github.com/netbox-community/netbox/issues/4802) - Allow changing page size when displaying only a single page of results
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#4695](https://github.com/netbox-community/netbox/issues/4695) - Expose cable termination type choices in OpenAPI spec
|
||||
* [#4708](https://github.com/netbox-community/netbox/issues/4708) - Relax connection constraints for multi-position rear ports
|
||||
* [#4766](https://github.com/netbox-community/netbox/issues/4766) - Fix redirect after login when `next` is not specified
|
||||
* [#4771](https://github.com/netbox-community/netbox/issues/4771) - Fix add/remove tag population when bulk editing objects
|
||||
* [#4772](https://github.com/netbox-community/netbox/issues/4772) - Fix "brief" format for the secrets REST API endpoint
|
||||
* [#4774](https://github.com/netbox-community/netbox/issues/4774) - Fix exception when deleting a device with device bays
|
||||
* [#4775](https://github.com/netbox-community/netbox/issues/4775) - Allow selecting an alternate device type when creating component templates
|
||||
|
||||
---
|
||||
|
@ -1,4 +1,4 @@
|
||||
# NetBox v2.8
|
||||
# NetBox v2.9
|
||||
|
||||
## v2.9.0 (FUTURE)
|
||||
|
||||
@ -10,9 +10,15 @@ NetBox v2.9 replaces Django's built-in permissions framework with one that suppo
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#2018](https://github.com/netbox-community/netbox/issues/2018) - Add `name` field to virtual chassis model
|
||||
* [#3703](https://github.com/netbox-community/netbox/issues/3703) - Tags must be created administratively before being assigned to an object
|
||||
* [#4615](https://github.com/netbox-community/netbox/issues/4615) - Add `label` field for all device components
|
||||
* [#4742](https://github.com/netbox-community/netbox/issues/4742) - Add tagging for cables, power panels, and rack reservations
|
||||
* [#4788](https://github.com/netbox-community/netbox/issues/4788) - Add dedicated views for all device components
|
||||
* [#4792](https://github.com/netbox-community/netbox/issues/4792) - Add bulk rename capability for console and power ports
|
||||
* [#4793](https://github.com/netbox-community/netbox/issues/4793) - Add `description` field to device component templates
|
||||
* [#4795](https://github.com/netbox-community/netbox/issues/4795) - Add bulk disconnect capability for console and power ports
|
||||
* [#4807](https://github.com/netbox-community/netbox/issues/4807) - Add bulk edit ability for device bay templates
|
||||
|
||||
### Configuration Changes
|
||||
|
||||
@ -33,10 +39,17 @@ NetBox v2.9 replaces Django's built-in permissions framework with one that suppo
|
||||
|
||||
* The `tags` field of an object now includes a more complete representation of each tag, rather than just its name.
|
||||
* A `label` field has been added to all device components and component templates.
|
||||
* The IP address model now uses a generic foreign key to refer to the assigned interface. The `interface` field on the serializer has been replaced with `assigned_object_type` and `assigned_object_id` for write operations. If one exists, the assigned interface is available as `assigned_object`.
|
||||
* The serialized representation of a virtual machine interface now includes only relevant fields: `type`, `lag`, `mgmt_only`, `connected_endpoint_type`, `connected_endpoint`, and `cable` are no longer included.
|
||||
* dcim.VirtualChassis: Added a mandatory `name` field
|
||||
* An optional `description` field has been added to all device component templates
|
||||
|
||||
### Other Changes
|
||||
|
||||
* A new model, `VMInterface` has been introduced to represent interfaces assigned to VirtualMachine instances. Previously, these interfaces utilized the DCIM model `Interface`. Instances will be replicated automatically upon upgrade, however any custom code which references or manipulates virtual machine interfaces will need to be updated accordingly.
|
||||
* The `secrets.activate_userkey` permission no longer exists. Instead, `secrets.change_userkey` is checked to determine whether a user has the ability to activate a UserKey.
|
||||
* The `users.delete_token` permission is no longer enforced. All users are permitted to delete their own API tokens.
|
||||
* Dropped backward compatibility for the `webhooks` Redis queue configuration (use `tasks` instead).
|
||||
* Dropped backward compatibility for the `/admin/webhook-backend-status` URL (moved to `/admin/background-tasks/`).
|
||||
* Virtual chassis are now created by navigating to `/dcim/virtual-chassis/add` rather than via the devices list.
|
||||
* A name is required when creating a virtual chassis.
|
||||
|
@ -1,4 +1,4 @@
|
||||
from django.db.models import Count
|
||||
from django.db.models import Count, Prefetch
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
@ -28,8 +28,8 @@ class ProviderViewSet(CustomFieldModelViewSet):
|
||||
"""
|
||||
A convenience method for rendering graphs for a particular provider.
|
||||
"""
|
||||
provider = get_object_or_404(Provider, pk=pk)
|
||||
queryset = Graph.objects.filter(type__model='provider')
|
||||
provider = get_object_or_404(self.queryset, pk=pk)
|
||||
queryset = Graph.objects.restrict(request.user).filter(type__model='provider')
|
||||
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': provider})
|
||||
return Response(serializer.data)
|
||||
|
||||
@ -52,7 +52,10 @@ class CircuitTypeViewSet(ModelViewSet):
|
||||
|
||||
class CircuitViewSet(CustomFieldModelViewSet):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'type', 'tenant', 'provider', 'terminations__site', 'terminations__connected_endpoint__device'
|
||||
Prefetch('terminations', queryset=CircuitTermination.objects.unrestricted().prefetch_related(
|
||||
'site', 'connected_endpoint__device'
|
||||
)),
|
||||
'type', 'tenant', 'provider',
|
||||
).prefetch_related('tags')
|
||||
serializer_class = serializers.CircuitSerializer
|
||||
filterset_class = filters.CircuitFilterSet
|
||||
|
@ -239,7 +239,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
|
||||
return self.STATUS_CLASS_MAP.get(self.status)
|
||||
|
||||
def _get_termination(self, side):
|
||||
for ct in self.terminations.all():
|
||||
for ct in self.terminations.unrestricted():
|
||||
if ct.term_side == side:
|
||||
return ct
|
||||
return None
|
||||
|
@ -10,7 +10,7 @@ def update_circuit(instance, **kwargs):
|
||||
"""
|
||||
When a CircuitTermination has been modified, update the last_updated time of its parent Circuit.
|
||||
"""
|
||||
circuits = Circuit.objects.filter(pk=instance.circuit_id)
|
||||
circuits = Circuit.objects.unrestricted().filter(pk=instance.circuit_id)
|
||||
time = timezone.now()
|
||||
for circuit in circuits:
|
||||
circuit.last_updated = time
|
||||
|
@ -2,19 +2,9 @@ import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from tenancy.tables import COL_TENANT
|
||||
from utilities.tables import BaseTable, TagColumn, ToggleColumn
|
||||
from utilities.tables import BaseTable, ButtonsColumn, TagColumn, ToggleColumn
|
||||
from .models import Circuit, CircuitType, Provider
|
||||
|
||||
CIRCUITTYPE_ACTIONS = """
|
||||
<a href="{% url 'circuits:circuittype_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.circuit.change_circuittype %}
|
||||
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}?return_url={{ request.path }}"
|
||||
class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
STATUS_LABEL = """
|
||||
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
|
||||
"""
|
||||
@ -53,11 +43,7 @@ class CircuitTypeTable(BaseTable):
|
||||
circuit_count = tables.Column(
|
||||
verbose_name='Circuits'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=CIRCUITTYPE_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
actions = ButtonsColumn(CircuitType, pk_field='slug')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = CircuitType
|
||||
|
@ -49,7 +49,7 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
|
||||
"""
|
||||
Test retrieval of Graphs assigned to Providers.
|
||||
"""
|
||||
provider = self.model.objects.first()
|
||||
provider = self.model.objects.unrestricted().first()
|
||||
ct = ContentType.objects.get(app_label='circuits', model='provider')
|
||||
graphs = (
|
||||
Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=1'),
|
||||
|
@ -25,6 +25,7 @@ urlpatterns = [
|
||||
path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
|
||||
path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
|
||||
path('circuit-types/<slug:slug>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
|
||||
path('circuit-types/<slug:slug>/delete/', views.CircuitTypeDeleteView.as_view(), name='circuittype_delete'),
|
||||
path('circuit-types/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
|
||||
|
||||
# Circuits
|
||||
|
@ -60,19 +60,16 @@ class ProviderEditView(ObjectEditView):
|
||||
queryset = Provider.objects.all()
|
||||
model_form = forms.ProviderForm
|
||||
template_name = 'circuits/provider_edit.html'
|
||||
default_return_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
class ProviderDeleteView(ObjectDeleteView):
|
||||
queryset = Provider.objects.all()
|
||||
default_return_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
class ProviderBulkImportView(BulkImportView):
|
||||
queryset = Provider.objects.all()
|
||||
model_form = forms.ProviderCSVForm
|
||||
table = tables.ProviderTable
|
||||
default_return_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
class ProviderBulkEditView(BulkEditView):
|
||||
@ -80,14 +77,12 @@ class ProviderBulkEditView(BulkEditView):
|
||||
filterset = filters.ProviderFilterSet
|
||||
table = tables.ProviderTable
|
||||
form = forms.ProviderBulkEditForm
|
||||
default_return_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
class ProviderBulkDeleteView(BulkDeleteView):
|
||||
queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
|
||||
filterset = filters.ProviderFilterSet
|
||||
table = tables.ProviderTable
|
||||
default_return_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
#
|
||||
@ -102,20 +97,21 @@ class CircuitTypeListView(ObjectListView):
|
||||
class CircuitTypeEditView(ObjectEditView):
|
||||
queryset = CircuitType.objects.all()
|
||||
model_form = forms.CircuitTypeForm
|
||||
default_return_url = 'circuits:circuittype_list'
|
||||
|
||||
|
||||
class CircuitTypeDeleteView(ObjectDeleteView):
|
||||
queryset = CircuitType.objects.all()
|
||||
|
||||
|
||||
class CircuitTypeBulkImportView(BulkImportView):
|
||||
queryset = CircuitType.objects.all()
|
||||
model_form = forms.CircuitTypeCSVForm
|
||||
table = tables.CircuitTypeTable
|
||||
default_return_url = 'circuits:circuittype_list'
|
||||
|
||||
|
||||
class CircuitTypeBulkDeleteView(BulkDeleteView):
|
||||
queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
|
||||
table = tables.CircuitTypeTable
|
||||
default_return_url = 'circuits:circuittype_list'
|
||||
|
||||
|
||||
#
|
||||
@ -165,19 +161,16 @@ class CircuitEditView(ObjectEditView):
|
||||
queryset = Circuit.objects.all()
|
||||
model_form = forms.CircuitForm
|
||||
template_name = 'circuits/circuit_edit.html'
|
||||
default_return_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
class CircuitDeleteView(ObjectDeleteView):
|
||||
queryset = Circuit.objects.all()
|
||||
default_return_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
class CircuitBulkImportView(BulkImportView):
|
||||
queryset = Circuit.objects.all()
|
||||
model_form = forms.CircuitCSVForm
|
||||
table = tables.CircuitTable
|
||||
default_return_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
class CircuitBulkEditView(BulkEditView):
|
||||
@ -185,14 +178,12 @@ class CircuitBulkEditView(BulkEditView):
|
||||
filterset = filters.CircuitFilterSet
|
||||
table = tables.CircuitTable
|
||||
form = forms.CircuitBulkEditForm
|
||||
default_return_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
class CircuitBulkDeleteView(BulkDeleteView):
|
||||
queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
|
||||
filterset = filters.CircuitFilterSet
|
||||
table = tables.CircuitTable
|
||||
default_return_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
class CircuitSwapTerminations(ObjectEditView):
|
||||
|
@ -332,7 +332,7 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.VirtualChassis
|
||||
fields = ['id', 'url', 'master', 'member_count']
|
||||
fields = ['id', 'name', 'url', 'master', 'member_count']
|
||||
|
||||
|
||||
#
|
||||
|
@ -1,3 +1,4 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from drf_yasg.utils import swagger_serializer_method
|
||||
from rest_framework import serializers
|
||||
@ -183,10 +184,10 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
|
||||
default=RackElevationDetailRenderChoices.RENDER_JSON
|
||||
)
|
||||
unit_width = serializers.IntegerField(
|
||||
default=RACK_ELEVATION_UNIT_WIDTH_DEFAULT
|
||||
default=settings.RACK_ELEVATION_DEFAULT_UNIT_WIDTH
|
||||
)
|
||||
unit_height = serializers.IntegerField(
|
||||
default=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT
|
||||
default=settings.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT
|
||||
)
|
||||
legend_width = serializers.IntegerField(
|
||||
default=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT
|
||||
@ -245,7 +246,7 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type']
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type', 'description']
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
@ -258,7 +259,7 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type']
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type', 'description']
|
||||
|
||||
|
||||
class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
@ -271,7 +272,7 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw']
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description']
|
||||
|
||||
|
||||
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
@ -292,7 +293,7 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg']
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description']
|
||||
|
||||
|
||||
class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
@ -301,7 +302,7 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type', 'mgmt_only']
|
||||
fields = ['id', 'device_type', 'name', 'label', 'type', 'mgmt_only', 'description']
|
||||
|
||||
|
||||
class RearPortTemplateSerializer(ValidatedModelSerializer):
|
||||
@ -310,7 +311,7 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = ['id', 'device_type', 'name', 'type', 'positions']
|
||||
fields = ['id', 'device_type', 'name', 'type', 'positions', 'description']
|
||||
|
||||
|
||||
class FrontPortTemplateSerializer(ValidatedModelSerializer):
|
||||
@ -320,7 +321,7 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = ['id', 'device_type', 'name', 'type', 'rear_port', 'rear_port_position']
|
||||
fields = ['id', 'device_type', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
|
||||
|
||||
|
||||
class DeviceBayTemplateSerializer(ValidatedModelSerializer):
|
||||
@ -328,7 +329,7 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = ['id', 'device_type', 'name', 'label']
|
||||
fields = ['id', 'device_type', 'name', 'label', 'description']
|
||||
|
||||
|
||||
#
|
||||
@ -694,12 +695,12 @@ class InterfaceConnectionSerializer(ValidatedModelSerializer):
|
||||
#
|
||||
|
||||
class VirtualChassisSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||
master = NestedDeviceSerializer()
|
||||
master = NestedDeviceSerializer(required=False)
|
||||
member_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = ['id', 'master', 'domain', 'tags', 'member_count']
|
||||
fields = ['id', 'name', 'domain', 'master', 'tags', 'member_count']
|
||||
|
||||
|
||||
#
|
||||
|
@ -29,6 +29,7 @@ from utilities.api import (
|
||||
get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, ModelViewSet, ServiceUnavailable,
|
||||
)
|
||||
from utilities.utils import get_subquery
|
||||
from utilities.metadata import ContentTypeMetadata
|
||||
from virtualization.models import VirtualMachine
|
||||
from . import serializers
|
||||
from .exceptions import MissingFilterException
|
||||
@ -43,7 +44,7 @@ class CableTraceMixin(object):
|
||||
"""
|
||||
Trace a complete cable path and return each segment as a three-tuple of (termination, cable, termination).
|
||||
"""
|
||||
obj = get_object_or_404(self.queryset.model, pk=pk)
|
||||
obj = get_object_or_404(self.queryset, pk=pk)
|
||||
|
||||
# Initialize the path array
|
||||
path = []
|
||||
@ -103,8 +104,8 @@ class SiteViewSet(CustomFieldModelViewSet):
|
||||
"""
|
||||
A convenience method for rendering graphs for a particular site.
|
||||
"""
|
||||
site = get_object_or_404(Site, pk=pk)
|
||||
queryset = Graph.objects.filter(type__model='site')
|
||||
site = get_object_or_404(self.queryset, pk=pk)
|
||||
queryset = Graph.objects.restrict(request.user).filter(type__model='site')
|
||||
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': site})
|
||||
return Response(serializer.data)
|
||||
|
||||
@ -156,7 +157,7 @@ class RackViewSet(CustomFieldModelViewSet):
|
||||
"""
|
||||
Rack elevation representing the list of rack units. Also supports rendering the elevation as an SVG.
|
||||
"""
|
||||
rack = get_object_or_404(Rack, pk=pk)
|
||||
rack = get_object_or_404(self.queryset, pk=pk)
|
||||
serializer = serializers.RackElevationDetailFilterSerializer(data=request.GET)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, 400)
|
||||
@ -226,7 +227,7 @@ class ManufacturerViewSet(ModelViewSet):
|
||||
#
|
||||
|
||||
class DeviceTypeViewSet(CustomFieldModelViewSet):
|
||||
queryset = DeviceType.objects.prefetch_related('manufacturer').prefetch_related('tags').annotate(
|
||||
queryset = DeviceType.objects.prefetch_related('manufacturer', 'tags').annotate(
|
||||
device_count=Count('instances')
|
||||
)
|
||||
serializer_class = serializers.DeviceTypeSerializer
|
||||
@ -347,8 +348,8 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
||||
"""
|
||||
A convenience method for rendering graphs for a particular Device.
|
||||
"""
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
queryset = Graph.objects.filter(type__model='device')
|
||||
device = get_object_or_404(self.queryset, pk=pk)
|
||||
queryset = Graph.objects.restrict(request.user).filter(type__model='device')
|
||||
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': device})
|
||||
|
||||
return Response(serializer.data)
|
||||
@ -369,7 +370,7 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
||||
"""
|
||||
Execute a NAPALM method on a Device
|
||||
"""
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
device = get_object_or_404(self.queryset, pk=pk)
|
||||
if not device.primary_ip:
|
||||
raise ServiceUnavailable("This device does not have a primary IP address configured.")
|
||||
if device.platform is None:
|
||||
@ -496,8 +497,8 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet):
|
||||
"""
|
||||
A convenience method for rendering graphs for a particular interface.
|
||||
"""
|
||||
interface = get_object_or_404(Interface, pk=pk)
|
||||
queryset = Graph.objects.filter(type__model='interface')
|
||||
interface = get_object_or_404(self.queryset, pk=pk)
|
||||
queryset = Graph.objects.restrict(request.user).filter(type__model='interface')
|
||||
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': interface})
|
||||
return Response(serializer.data)
|
||||
|
||||
@ -567,6 +568,7 @@ class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
#
|
||||
|
||||
class CableViewSet(ModelViewSet):
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = Cable.objects.prefetch_related(
|
||||
'termination_a', 'termination_b'
|
||||
)
|
||||
@ -655,7 +657,11 @@ class ConnectedDeviceViewSet(ViewSet):
|
||||
raise MissingFilterException(detail='Request must include "peer_device" and "peer_interface" filters.')
|
||||
|
||||
# Determine local interface from peer interface's connection
|
||||
peer_interface = get_object_or_404(Interface, device__name=peer_device_name, name=peer_interface_name)
|
||||
peer_interface = get_object_or_404(
|
||||
Interface.objects.unrestricted(),
|
||||
device__name=peer_device_name,
|
||||
name=peer_interface_name
|
||||
)
|
||||
local_interface = peer_interface._connected_interface
|
||||
|
||||
if local_interface is None:
|
||||
|
@ -260,6 +260,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h'
|
||||
TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h'
|
||||
# NEMA non-locking
|
||||
TYPE_NEMA_115P = 'nema-1-15p'
|
||||
TYPE_NEMA_515P = 'nema-5-15p'
|
||||
TYPE_NEMA_520P = 'nema-5-20p'
|
||||
TYPE_NEMA_530P = 'nema-5-30p'
|
||||
@ -268,16 +269,27 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
TYPE_NEMA_620P = 'nema-6-20p'
|
||||
TYPE_NEMA_630P = 'nema-6-30p'
|
||||
TYPE_NEMA_650P = 'nema-6-50p'
|
||||
TYPE_NEMA_1030P = 'nema-10-30p'
|
||||
TYPE_NEMA_1050P = 'nema-10-50p'
|
||||
TYPE_NEMA_1420P = 'nema-14-20p'
|
||||
TYPE_NEMA_1430P = 'nema-14-30p'
|
||||
TYPE_NEMA_1450P = 'nema-14-50p'
|
||||
TYPE_NEMA_1460P = 'nema-14-60p'
|
||||
# NEMA locking
|
||||
TYPE_NEMA_L115P = 'nema-l1-15p'
|
||||
TYPE_NEMA_L515P = 'nema-l5-15p'
|
||||
TYPE_NEMA_L520P = 'nema-l5-20p'
|
||||
TYPE_NEMA_L530P = 'nema-l5-30p'
|
||||
TYPE_NEMA_L615P = 'nema-l5-50p'
|
||||
TYPE_NEMA_L550P = 'nema-l5-50p'
|
||||
TYPE_NEMA_L615P = 'nema-l6-15p'
|
||||
TYPE_NEMA_L620P = 'nema-l6-20p'
|
||||
TYPE_NEMA_L630P = 'nema-l6-30p'
|
||||
TYPE_NEMA_L650P = 'nema-l6-50p'
|
||||
TYPE_NEMA_L1030P = 'nema-l10-30p'
|
||||
TYPE_NEMA_L1420P = 'nema-l14-20p'
|
||||
TYPE_NEMA_L1430P = 'nema-l14-30p'
|
||||
TYPE_NEMA_L1450P = 'nema-l14-50p'
|
||||
TYPE_NEMA_L1460P = 'nema-l14-60p'
|
||||
TYPE_NEMA_L2120P = 'nema-l21-20p'
|
||||
TYPE_NEMA_L2130P = 'nema-l21-30p'
|
||||
# California style
|
||||
@ -324,6 +336,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
(TYPE_IEC_3PNE9H, '3P+N+E 9H'),
|
||||
)),
|
||||
('NEMA (Non-locking)', (
|
||||
(TYPE_NEMA_115P, 'NEMA 1-15P'),
|
||||
(TYPE_NEMA_515P, 'NEMA 5-15P'),
|
||||
(TYPE_NEMA_520P, 'NEMA 5-20P'),
|
||||
(TYPE_NEMA_530P, 'NEMA 5-30P'),
|
||||
@ -332,17 +345,28 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
(TYPE_NEMA_620P, 'NEMA 6-20P'),
|
||||
(TYPE_NEMA_630P, 'NEMA 6-30P'),
|
||||
(TYPE_NEMA_650P, 'NEMA 6-50P'),
|
||||
(TYPE_NEMA_1030P, 'NEMA 10-30P'),
|
||||
(TYPE_NEMA_1050P, 'NEMA 10-50P'),
|
||||
(TYPE_NEMA_1420P, 'NEMA 14-20P'),
|
||||
(TYPE_NEMA_1430P, 'NEMA 14-30P'),
|
||||
(TYPE_NEMA_1450P, 'NEMA 14-50P'),
|
||||
(TYPE_NEMA_1460P, 'NEMA 14-60P'),
|
||||
)),
|
||||
('NEMA (Locking)', (
|
||||
(TYPE_NEMA_L115P, 'NEMA L1-15P'),
|
||||
(TYPE_NEMA_L515P, 'NEMA L5-15P'),
|
||||
(TYPE_NEMA_L520P, 'NEMA L5-20P'),
|
||||
(TYPE_NEMA_L530P, 'NEMA L5-30P'),
|
||||
(TYPE_NEMA_L550P, 'NEMA L5-50P'),
|
||||
(TYPE_NEMA_L615P, 'NEMA L6-15P'),
|
||||
(TYPE_NEMA_L620P, 'NEMA L6-20P'),
|
||||
(TYPE_NEMA_L630P, 'NEMA L6-30P'),
|
||||
(TYPE_NEMA_L650P, 'NEMA L6-50P'),
|
||||
(TYPE_NEMA_L1030P, 'NEMA L10-30P'),
|
||||
(TYPE_NEMA_L1420P, 'NEMA L14-20P'),
|
||||
(TYPE_NEMA_L1430P, 'NEMA L14-30P'),
|
||||
(TYPE_NEMA_L1450P, 'NEMA L14-50P'),
|
||||
(TYPE_NEMA_L1460P, 'NEMA L14-60P'),
|
||||
(TYPE_NEMA_L2120P, 'NEMA L21-20P'),
|
||||
(TYPE_NEMA_L2130P, 'NEMA L21-30P'),
|
||||
)),
|
||||
@ -397,6 +421,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h'
|
||||
TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h'
|
||||
# NEMA non-locking
|
||||
TYPE_NEMA_115R = 'nema-1-15r'
|
||||
TYPE_NEMA_515R = 'nema-5-15r'
|
||||
TYPE_NEMA_520R = 'nema-5-20r'
|
||||
TYPE_NEMA_530R = 'nema-5-30r'
|
||||
@ -405,16 +430,27 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
TYPE_NEMA_620R = 'nema-6-20r'
|
||||
TYPE_NEMA_630R = 'nema-6-30r'
|
||||
TYPE_NEMA_650R = 'nema-6-50r'
|
||||
TYPE_NEMA_1030R = 'nema-10-30r'
|
||||
TYPE_NEMA_1050R = 'nema-10-50r'
|
||||
TYPE_NEMA_1420R = 'nema-14-20r'
|
||||
TYPE_NEMA_1430R = 'nema-14-30r'
|
||||
TYPE_NEMA_1450R = 'nema-14-50r'
|
||||
TYPE_NEMA_1460R = 'nema-14-60r'
|
||||
# NEMA locking
|
||||
TYPE_NEMA_L115R = 'nema-l1-15r'
|
||||
TYPE_NEMA_L515R = 'nema-l5-15r'
|
||||
TYPE_NEMA_L520R = 'nema-l5-20r'
|
||||
TYPE_NEMA_L530R = 'nema-l5-30r'
|
||||
TYPE_NEMA_L615R = 'nema-l5-50r'
|
||||
TYPE_NEMA_L550R = 'nema-l5-50r'
|
||||
TYPE_NEMA_L615R = 'nema-l6-15r'
|
||||
TYPE_NEMA_L620R = 'nema-l6-20r'
|
||||
TYPE_NEMA_L630R = 'nema-l6-30r'
|
||||
TYPE_NEMA_L650R = 'nema-l6-50r'
|
||||
TYPE_NEMA_L1030R = 'nema-l10-30r'
|
||||
TYPE_NEMA_L1420R = 'nema-l14-20r'
|
||||
TYPE_NEMA_L1430R = 'nema-l14-30r'
|
||||
TYPE_NEMA_L1450R = 'nema-l14-50r'
|
||||
TYPE_NEMA_L1460R = 'nema-l14-60r'
|
||||
TYPE_NEMA_L2120R = 'nema-l21-20r'
|
||||
TYPE_NEMA_L2130R = 'nema-l21-30r'
|
||||
# California style
|
||||
@ -462,6 +498,7 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
(TYPE_IEC_3PNE9H, '3P+N+E 9H'),
|
||||
)),
|
||||
('NEMA (Non-locking)', (
|
||||
(TYPE_NEMA_115R, 'NEMA 1-15R'),
|
||||
(TYPE_NEMA_515R, 'NEMA 5-15R'),
|
||||
(TYPE_NEMA_520R, 'NEMA 5-20R'),
|
||||
(TYPE_NEMA_530R, 'NEMA 5-30R'),
|
||||
@ -470,17 +507,28 @@ class PowerOutletTypeChoices(ChoiceSet):
|
||||
(TYPE_NEMA_620R, 'NEMA 6-20R'),
|
||||
(TYPE_NEMA_630R, 'NEMA 6-30R'),
|
||||
(TYPE_NEMA_650R, 'NEMA 6-50R'),
|
||||
(TYPE_NEMA_1030R, 'NEMA 10-30R'),
|
||||
(TYPE_NEMA_1050R, 'NEMA 10-50R'),
|
||||
(TYPE_NEMA_1420R, 'NEMA 14-20R'),
|
||||
(TYPE_NEMA_1430R, 'NEMA 14-30R'),
|
||||
(TYPE_NEMA_1450R, 'NEMA 14-50R'),
|
||||
(TYPE_NEMA_1460R, 'NEMA 14-60R'),
|
||||
)),
|
||||
('NEMA (Locking)', (
|
||||
(TYPE_NEMA_L115R, 'NEMA L1-15R'),
|
||||
(TYPE_NEMA_L515R, 'NEMA L5-15R'),
|
||||
(TYPE_NEMA_L520R, 'NEMA L5-20R'),
|
||||
(TYPE_NEMA_L530R, 'NEMA L5-30R'),
|
||||
(TYPE_NEMA_L550R, 'NEMA L5-50R'),
|
||||
(TYPE_NEMA_L615R, 'NEMA L6-15R'),
|
||||
(TYPE_NEMA_L620R, 'NEMA L6-20R'),
|
||||
(TYPE_NEMA_L630R, 'NEMA L6-30R'),
|
||||
(TYPE_NEMA_L650R, 'NEMA L6-50R'),
|
||||
(TYPE_NEMA_L1030R, 'NEMA L10-30R'),
|
||||
(TYPE_NEMA_L1420R, 'NEMA L14-20R'),
|
||||
(TYPE_NEMA_L1430R, 'NEMA L14-30R'),
|
||||
(TYPE_NEMA_L1450R, 'NEMA L14-50R'),
|
||||
(TYPE_NEMA_L1460R, 'NEMA L14-60R'),
|
||||
(TYPE_NEMA_L2120R, 'NEMA L21-20R'),
|
||||
(TYPE_NEMA_L2130R, 'NEMA L21-30R'),
|
||||
)),
|
||||
|
@ -11,8 +11,6 @@ RACK_U_HEIGHT_DEFAULT = 42
|
||||
|
||||
RACK_ELEVATION_BORDER_WIDTH = 2
|
||||
RACK_ELEVATION_LEGEND_WIDTH_DEFAULT = 30
|
||||
RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 220
|
||||
RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 22
|
||||
|
||||
|
||||
#
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -79,42 +79,42 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverport',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicebay',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontport',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventoryitem',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rearport',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_consoleports,
|
||||
|
@ -75,37 +75,37 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='consoleporttemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverporttemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicebaytemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontporttemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerporttemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rearporttemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_consoleporttemplates,
|
||||
|
@ -43,17 +43,17 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize, null=True),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_sites,
|
||||
|
@ -35,12 +35,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize_interface),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize_interface),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interfacetemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize_interface),
|
||||
field=utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize_interface),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_interfacetemplates,
|
||||
|
18
netbox/dcim/migrations/0109_interface_remove_vm.py
Normal file
18
netbox/dcim/migrations/0109_interface_remove_vm.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.6 on 2020-06-22 16:03
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0108_add_tags'),
|
||||
('virtualization', '0016_replicate_interfaces'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='interface',
|
||||
name='virtual_machine',
|
||||
),
|
||||
]
|
46
netbox/dcim/migrations/0110_virtualchassis_name.py
Normal file
46
netbox/dcim/migrations/0110_virtualchassis_name.py
Normal file
@ -0,0 +1,46 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def copy_master_name(apps, schema_editor):
|
||||
"""
|
||||
Copy the master device's name to the VirtualChassis.
|
||||
"""
|
||||
VirtualChassis = apps.get_model('dcim', 'VirtualChassis')
|
||||
|
||||
for vc in VirtualChassis.objects.prefetch_related('master'):
|
||||
name = vc.master.name if vc.master.name else f'Unnamed VC #{vc.pk}'
|
||||
VirtualChassis.objects.filter(pk=vc.pk).update(name=name)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0109_interface_remove_vm'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='virtualchassis',
|
||||
options={'ordering': ['name'], 'verbose_name_plural': 'virtual chassis'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='virtualchassis',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, max_length=64),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='virtualchassis',
|
||||
name='master',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vc_master_for', to='dcim.Device'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=copy_master_name,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='virtualchassis',
|
||||
name='name',
|
||||
field=models.CharField(max_length=64),
|
||||
),
|
||||
]
|
@ -0,0 +1,53 @@
|
||||
# Generated by Django 3.0.6 on 2020-06-30 18:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0110_virtualchassis_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='consoleporttemplate',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverporttemplate',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicebaytemplate',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontporttemplate',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interfacetemplate',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerporttemplate',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rearporttemplate',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
]
|
@ -35,11 +35,12 @@ from .device_component_templates import (
|
||||
PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
|
||||
)
|
||||
from .device_components import (
|
||||
CableTermination, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, PowerOutlet,
|
||||
PowerPort, RearPort,
|
||||
BaseInterface, CableTermination, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem,
|
||||
PowerOutlet, PowerPort, RearPort,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
'BaseInterface',
|
||||
'Cable',
|
||||
'CableTermination',
|
||||
'ConsolePort',
|
||||
@ -579,7 +580,11 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
if self.pk:
|
||||
# Validate that Rack is tall enough to house the installed Devices
|
||||
top_device = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('-position').first()
|
||||
top_device = Device.objects.unrestricted().filter(
|
||||
rack=self
|
||||
).exclude(
|
||||
position__isnull=True
|
||||
).order_by('-position').first()
|
||||
if top_device:
|
||||
min_height = top_device.position + top_device.device_type.u_height - 1
|
||||
if self.u_height < min_height:
|
||||
@ -600,13 +605,13 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
# Record the original site assignment for this rack.
|
||||
_site_id = None
|
||||
if self.pk:
|
||||
_site_id = Rack.objects.get(pk=self.pk).site_id
|
||||
_site_id = Rack.objects.unrestricted().get(pk=self.pk).site_id
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# Update racked devices if the assigned Site has been changed.
|
||||
if _site_id is not None and self.site_id != _site_id:
|
||||
devices = Device.objects.filter(rack=self)
|
||||
devices = Device.objects.unrestricted().filter(rack=self)
|
||||
for device in devices:
|
||||
device.site = self.site
|
||||
device.save()
|
||||
@ -668,7 +673,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
# Add devices to rack units list
|
||||
if self.pk:
|
||||
queryset = Device.objects.prefetch_related(
|
||||
queryset = Device.objects.unrestricted().prefetch_related(
|
||||
'device_type',
|
||||
'device_type__manufacturer',
|
||||
'device_role'
|
||||
@ -744,8 +749,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
def get_elevation_svg(
|
||||
self,
|
||||
face=DeviceFaceChoices.FACE_FRONT,
|
||||
unit_width=RACK_ELEVATION_UNIT_WIDTH_DEFAULT,
|
||||
unit_height=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT,
|
||||
unit_width=settings.RACK_ELEVATION_DEFAULT_UNIT_WIDTH,
|
||||
unit_height=settings.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT,
|
||||
legend_width=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT,
|
||||
include_images=True,
|
||||
base_url=None
|
||||
@ -1124,7 +1129,7 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
# room to expand within their racks. This validation will impose a very high performance penalty when there are
|
||||
# many instances to check, but increasing the u_height of a DeviceType should be a very rare occurrence.
|
||||
if self.pk and self.u_height > self._original_u_height:
|
||||
for d in Device.objects.filter(device_type=self, position__isnull=False):
|
||||
for d in Device.objects.unrestricted().filter(device_type=self, position__isnull=False):
|
||||
face_required = None if self.is_full_depth else d.face
|
||||
u_available = d.rack.get_available_units(
|
||||
u_height=self.u_height,
|
||||
@ -1139,7 +1144,10 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
# If modifying the height of an existing DeviceType to 0U, check for any instances assigned to a rack position.
|
||||
elif self.pk and self._original_u_height > 0 and self.u_height == 0:
|
||||
racked_instance_count = Device.objects.filter(device_type=self, position__isnull=False).count()
|
||||
racked_instance_count = Device.objects.unrestricted().filter(
|
||||
device_type=self,
|
||||
position__isnull=False
|
||||
).count()
|
||||
if racked_instance_count:
|
||||
url = f"{reverse('dcim:device_list')}?manufactuer_id={self.manufacturer_id}&device_type_id={self.pk}"
|
||||
raise ValidationError({
|
||||
@ -1492,7 +1500,11 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
# because Django does not consider two NULL fields to be equal, and thus will not trigger a violation
|
||||
# of the uniqueness constraint without manual intervention.
|
||||
if self.name and self.tenant is None:
|
||||
if Device.objects.exclude(pk=self.pk).filter(name=self.name, site=self.site, tenant__isnull=True):
|
||||
if Device.objects.unrestricted().exclude(pk=self.pk).filter(
|
||||
name=self.name,
|
||||
site=self.site,
|
||||
tenant__isnull=True
|
||||
):
|
||||
raise ValidationError({
|
||||
'name': 'A device with this name already exists.'
|
||||
})
|
||||
@ -1571,9 +1583,9 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
raise ValidationError({
|
||||
'primary_ip4': f"{self.primary_ip4} is not an IPv4 address."
|
||||
})
|
||||
if self.primary_ip4.interface in vc_interfaces:
|
||||
if self.primary_ip4.assigned_object in vc_interfaces:
|
||||
pass
|
||||
elif self.primary_ip4.nat_inside is not None and self.primary_ip4.nat_inside.interface in vc_interfaces:
|
||||
elif self.primary_ip4.nat_inside is not None and self.primary_ip4.nat_inside.assigned_object in vc_interfaces:
|
||||
pass
|
||||
else:
|
||||
raise ValidationError({
|
||||
@ -1584,9 +1596,9 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
raise ValidationError({
|
||||
'primary_ip6': f"{self.primary_ip6} is not an IPv6 address."
|
||||
})
|
||||
if self.primary_ip6.interface in vc_interfaces:
|
||||
if self.primary_ip6.assigned_object in vc_interfaces:
|
||||
pass
|
||||
elif self.primary_ip6.nat_inside is not None and self.primary_ip6.nat_inside.interface in vc_interfaces:
|
||||
elif self.primary_ip6.nat_inside is not None and self.primary_ip6.nat_inside.assigned_object in vc_interfaces:
|
||||
pass
|
||||
else:
|
||||
raise ValidationError({
|
||||
@ -1622,32 +1634,32 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
# If this is a new Device, instantiate all of the related components per the DeviceType definition
|
||||
if is_new:
|
||||
ConsolePort.objects.bulk_create(
|
||||
[x.instantiate(self) for x in self.device_type.consoleport_templates.all()]
|
||||
[x.instantiate(self) for x in self.device_type.consoleport_templates.unrestricted()]
|
||||
)
|
||||
ConsoleServerPort.objects.bulk_create(
|
||||
[x.instantiate(self) for x in self.device_type.consoleserverport_templates.all()]
|
||||
[x.instantiate(self) for x in self.device_type.consoleserverport_templates.unrestricted()]
|
||||
)
|
||||
PowerPort.objects.bulk_create(
|
||||
[x.instantiate(self) for x in self.device_type.powerport_templates.all()]
|
||||
[x.instantiate(self) for x in self.device_type.powerport_templates.unrestricted()]
|
||||
)
|
||||
PowerOutlet.objects.bulk_create(
|
||||
[x.instantiate(self) for x in self.device_type.poweroutlet_templates.all()]
|
||||
[x.instantiate(self) for x in self.device_type.poweroutlet_templates.unrestricted()]
|
||||
)
|
||||
Interface.objects.bulk_create(
|
||||
[x.instantiate(self) for x in self.device_type.interface_templates.all()]
|
||||
[x.instantiate(self) for x in self.device_type.interface_templates.unrestricted()]
|
||||
)
|
||||
RearPort.objects.bulk_create(
|
||||
[x.instantiate(self) for x in self.device_type.rearport_templates.all()]
|
||||
[x.instantiate(self) for x in self.device_type.rearport_templates.unrestricted()]
|
||||
)
|
||||
FrontPort.objects.bulk_create(
|
||||
[x.instantiate(self) for x in self.device_type.frontport_templates.all()]
|
||||
[x.instantiate(self) for x in self.device_type.frontport_templates.unrestricted()]
|
||||
)
|
||||
DeviceBay.objects.bulk_create(
|
||||
[x.instantiate(self) for x in self.device_type.device_bay_templates.all()]
|
||||
[x.instantiate(self) for x in self.device_type.device_bay_templates.unrestricted()]
|
||||
)
|
||||
|
||||
# Update Site and Rack assignment for any child Devices
|
||||
devices = Device.objects.filter(parent_bay__device=self)
|
||||
devices = Device.objects.unrestricted().filter(parent_bay__device=self)
|
||||
for device in devices:
|
||||
device.site = self.site
|
||||
device.rack = self.rack
|
||||
@ -1738,7 +1750,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
"""
|
||||
Return the set of child Devices installed in DeviceBays within this Device.
|
||||
"""
|
||||
return Device.objects.filter(parent_bay__device=self.pk)
|
||||
return Device.objects.unrestricted().filter(parent_bay__device=self.pk)
|
||||
|
||||
def get_status_class(self):
|
||||
return self.STATUS_CLASS_MAP.get(self.status)
|
||||
@ -1756,7 +1768,12 @@ class VirtualChassis(ChangeLoggedModel):
|
||||
master = models.OneToOneField(
|
||||
to='Device',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='vc_master_for'
|
||||
related_name='vc_master_for',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=64
|
||||
)
|
||||
domain = models.CharField(
|
||||
max_length=30,
|
||||
@ -1766,14 +1783,14 @@ class VirtualChassis(ChangeLoggedModel):
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['master', 'domain']
|
||||
csv_headers = ['name', 'domain', 'master']
|
||||
|
||||
class Meta:
|
||||
ordering = ['master']
|
||||
ordering = ['name']
|
||||
verbose_name_plural = 'virtual chassis'
|
||||
|
||||
def __str__(self):
|
||||
return str(self.master) if hasattr(self, 'master') else 'New Virtual Chassis'
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:virtualchassis', kwargs={'pk': self.pk})
|
||||
@ -1782,15 +1799,15 @@ class VirtualChassis(ChangeLoggedModel):
|
||||
|
||||
# Verify that the selected master device has been assigned to this VirtualChassis. (Skip when creating a new
|
||||
# VirtualChassis.)
|
||||
if self.pk and self.master not in self.members.all():
|
||||
if self.pk and self.master and self.master not in self.members.all():
|
||||
raise ValidationError({
|
||||
'master': "The selected master is not assigned to this virtual chassis."
|
||||
'master': f"The selected master ({self.master}) is not assigned to this virtual chassis."
|
||||
})
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
|
||||
# Check for LAG interfaces split across member chassis
|
||||
interfaces = Interface.objects.filter(
|
||||
interfaces = Interface.objects.unrestricted().filter(
|
||||
device__in=self.members.all(),
|
||||
lag__isnull=False
|
||||
).exclude(
|
||||
@ -1798,8 +1815,7 @@ class VirtualChassis(ChangeLoggedModel):
|
||||
)
|
||||
if interfaces:
|
||||
raise ProtectedError(
|
||||
"Unable to delete virtual chassis {}. There are member interfaces which form a cross-chassis "
|
||||
"LAG".format(self),
|
||||
f"Unable to delete virtual chassis {self}. There are member interfaces which form a cross-chassis LAG",
|
||||
interfaces
|
||||
)
|
||||
|
||||
@ -1807,8 +1823,9 @@ class VirtualChassis(ChangeLoggedModel):
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.master,
|
||||
self.name,
|
||||
self.domain,
|
||||
self.master.name if self.master else None,
|
||||
)
|
||||
|
||||
|
||||
@ -2158,12 +2175,13 @@ class Cable(ChangeLoggedModel):
|
||||
return reverse('dcim:cable', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
from circuits.models import CircuitTermination
|
||||
|
||||
# Validate that termination A exists
|
||||
if not hasattr(self, 'termination_a_type'):
|
||||
raise ValidationError('Termination A type has not been specified')
|
||||
try:
|
||||
self.termination_a_type.model_class().objects.get(pk=self.termination_a_id)
|
||||
self.termination_a_type.model_class().objects.unrestricted().get(pk=self.termination_a_id)
|
||||
except ObjectDoesNotExist:
|
||||
raise ValidationError({
|
||||
'termination_a': 'Invalid ID for type {}'.format(self.termination_a_type)
|
||||
@ -2173,7 +2191,7 @@ class Cable(ChangeLoggedModel):
|
||||
if not hasattr(self, 'termination_b_type'):
|
||||
raise ValidationError('Termination B type has not been specified')
|
||||
try:
|
||||
self.termination_b_type.model_class().objects.get(pk=self.termination_b_id)
|
||||
self.termination_b_type.model_class().objects.unrestricted().get(pk=self.termination_b_id)
|
||||
except ObjectDoesNotExist:
|
||||
raise ValidationError({
|
||||
'termination_b': 'Invalid ID for type {}'.format(self.termination_b_type)
|
||||
@ -2220,19 +2238,21 @@ class Cable(ChangeLoggedModel):
|
||||
f"Incompatible termination types: {self.termination_a_type} and {self.termination_b_type}"
|
||||
)
|
||||
|
||||
# A RearPort with multiple positions must be connected to a RearPort with an equal number of positions
|
||||
# Check that a RearPort with multiple positions isn't connected to an endpoint
|
||||
# or a RearPort with a different number of positions.
|
||||
for term_a, term_b in [
|
||||
(self.termination_a, self.termination_b),
|
||||
(self.termination_b, self.termination_a)
|
||||
]:
|
||||
if isinstance(term_a, RearPort) and term_a.positions > 1:
|
||||
if not isinstance(term_b, RearPort):
|
||||
if not isinstance(term_b, (FrontPort, RearPort, CircuitTermination)):
|
||||
raise ValidationError(
|
||||
"Rear ports with multiple positions may only be connected to other rear ports"
|
||||
"Rear ports with multiple positions may only be connected to other pass-through ports"
|
||||
)
|
||||
elif term_a.positions != term_b.positions:
|
||||
if isinstance(term_b, RearPort) and term_b.positions > 1 and term_a.positions != term_b.positions:
|
||||
raise ValidationError(
|
||||
f"{term_a} has {term_a.positions} position(s) but {term_b} has {term_b.positions}. "
|
||||
f"{term_a} of {term_a.device} has {term_a.positions} position(s) but "
|
||||
f"{term_b} of {term_b.device} has {term_b.positions}. "
|
||||
f"Both terminations must have the same number of positions."
|
||||
)
|
||||
|
||||
|
@ -27,6 +27,11 @@ __all__ = (
|
||||
|
||||
|
||||
class ComponentTemplateModel(models.Model):
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
|
@ -19,7 +19,6 @@ from utilities.ordering import naturalize_interface
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.query_functions import CollateAsChar
|
||||
from utilities.utils import serialize_object
|
||||
from virtualization.choices import VMInterfaceTypeChoices
|
||||
|
||||
|
||||
__all__ = (
|
||||
@ -53,18 +52,12 @@ class ComponentModel(models.Model):
|
||||
return self.name
|
||||
|
||||
def to_objectchange(self, action):
|
||||
# Annotate the parent Device/VM
|
||||
try:
|
||||
parent = getattr(self, 'device', None) or getattr(self, 'virtual_machine', None)
|
||||
except ObjectDoesNotExist:
|
||||
# The parent device/VM has already been deleted
|
||||
parent = None
|
||||
|
||||
# Annotate the parent Device
|
||||
return ObjectChange(
|
||||
changed_object=self,
|
||||
object_repr=str(self),
|
||||
action=action,
|
||||
related_object=parent,
|
||||
related_object=self.device,
|
||||
object_data=serialize_object(self)
|
||||
)
|
||||
|
||||
@ -94,16 +87,16 @@ class CableTermination(models.Model):
|
||||
object_id_field='termination_b_id'
|
||||
)
|
||||
|
||||
is_path_endpoint = True
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def trace(self):
|
||||
"""
|
||||
Return two items: the traceable portion of a cable path, and the termination points where it splits (if any).
|
||||
This occurs when the trace is initiated from a midpoint along a path which traverses a RearPort. In cases where
|
||||
the originating endpoint is unknown, it is not possible to know which corresponding FrontPort to follow.
|
||||
Return three items: the traceable portion of a cable path, the termination points where it splits (if any), and
|
||||
the remaining positions on the position stack (if any). Splits occur when the trace is initiated from a midpoint
|
||||
along a path which traverses a RearPort. In cases where the originating endpoint is unknown, it is not possible
|
||||
to know which corresponding FrontPort to follow. Remaining positions occur when tracing a path that traverses
|
||||
a FrontPort without traversing a RearPort again.
|
||||
|
||||
The path is a list representing a complete cable path, with each individual segment represented as a
|
||||
three-tuple:
|
||||
@ -123,26 +116,35 @@ class CableTermination(models.Model):
|
||||
|
||||
# Map a front port to its corresponding rear port
|
||||
if isinstance(termination, FrontPort):
|
||||
position_stack.append(termination.rear_port_position)
|
||||
# Retrieve the corresponding RearPort from database to ensure we have an up-to-date instance
|
||||
peer_port = RearPort.objects.get(pk=termination.rear_port.pk)
|
||||
|
||||
# Don't use the stack for RearPorts with a single position. Only remember the position at
|
||||
# many-to-one points so we can select the correct FrontPort when we reach the corresponding
|
||||
# one-to-many point.
|
||||
if peer_port.positions > 1:
|
||||
position_stack.append(termination)
|
||||
|
||||
return peer_port
|
||||
|
||||
# Map a rear port/position to its corresponding front port
|
||||
elif isinstance(termination, RearPort):
|
||||
|
||||
if termination.positions > 1:
|
||||
# Can't map to a FrontPort without a position if there are multiple options
|
||||
if termination.positions > 1 and not position_stack:
|
||||
if not position_stack:
|
||||
raise CableTraceSplit(termination)
|
||||
|
||||
# We can assume position 1 if the RearPort has only one position
|
||||
position = position_stack.pop() if position_stack else 1
|
||||
front_port = position_stack.pop()
|
||||
position = front_port.rear_port_position
|
||||
|
||||
# Validate the position
|
||||
if position not in range(1, termination.positions + 1):
|
||||
raise Exception("Invalid position for {} ({} positions): {})".format(
|
||||
termination, termination.positions, position
|
||||
))
|
||||
else:
|
||||
# Don't use the stack for RearPorts with a single position. The only possible position is 1.
|
||||
position = 1
|
||||
|
||||
try:
|
||||
peer_port = FrontPort.objects.get(
|
||||
@ -173,12 +175,12 @@ class CableTermination(models.Model):
|
||||
if not endpoint.cable:
|
||||
path.append((endpoint, None, None))
|
||||
logger.debug("No cable connected")
|
||||
return path, None
|
||||
return path, None, position_stack
|
||||
|
||||
# Check for loops
|
||||
if endpoint.cable in [segment[1] for segment in path]:
|
||||
logger.debug("Loop detected!")
|
||||
return path, None
|
||||
return path, None, position_stack
|
||||
|
||||
# Record the current segment in the path
|
||||
far_end = endpoint.get_cable_peer()
|
||||
@ -191,10 +193,10 @@ class CableTermination(models.Model):
|
||||
try:
|
||||
endpoint = get_peer_port(far_end)
|
||||
except CableTraceSplit as e:
|
||||
return path, e.termination.frontports.all()
|
||||
return path, e.termination.frontports.all(), position_stack
|
||||
|
||||
if endpoint is None:
|
||||
return path, None
|
||||
return path, None, position_stack
|
||||
|
||||
def get_cable_peer(self):
|
||||
if self.cable is None:
|
||||
@ -211,7 +213,7 @@ class CableTermination(models.Model):
|
||||
endpoints = []
|
||||
|
||||
# Get the far end of the last path segment
|
||||
path, split_ends = self.trace()
|
||||
path, split_ends, position_stack = self.trace()
|
||||
endpoint = path[-1][2]
|
||||
if split_ends is not None:
|
||||
for termination in split_ends:
|
||||
@ -275,7 +277,7 @@ class ConsolePort(CableTermination, ComponentModel):
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
return reverse('dcim:consoleport', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
@ -332,7 +334,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
return reverse('dcim:consoleserverport', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
@ -415,7 +417,7 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
return reverse('dcim:powerport', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
@ -567,7 +569,7 @@ class PowerOutlet(CableTermination, ComponentModel):
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
return reverse('dcim:poweroutlet', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
@ -592,26 +594,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
|
||||
Interface.
|
||||
"""
|
||||
device = models.ForeignKey(
|
||||
to='Device',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='interfaces',
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
virtual_machine = models.ForeignKey(
|
||||
to='virtualization.VirtualMachine',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='interfaces',
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
class BaseInterface(models.Model):
|
||||
name = models.CharField(
|
||||
max_length=64
|
||||
)
|
||||
@ -621,6 +604,42 @@ class Interface(CableTermination, ComponentModel):
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
enabled = models.BooleanField(
|
||||
default=True
|
||||
)
|
||||
mac_address = MACAddressField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name='MAC Address'
|
||||
)
|
||||
mtu = models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinValueValidator(1), MaxValueValidator(65536)],
|
||||
verbose_name='MTU'
|
||||
)
|
||||
mode = models.CharField(
|
||||
max_length=50,
|
||||
choices=InterfaceModeChoices,
|
||||
blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
@extras_features('graphs', 'export_templates', 'webhooks')
|
||||
class Interface(CableTermination, ComponentModel, BaseInterface):
|
||||
"""
|
||||
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
||||
"""
|
||||
device = models.ForeignKey(
|
||||
to='Device',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='interfaces',
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
label = models.CharField(
|
||||
max_length=64,
|
||||
blank=True,
|
||||
@ -656,30 +675,11 @@ class Interface(CableTermination, ComponentModel):
|
||||
max_length=50,
|
||||
choices=InterfaceTypeChoices
|
||||
)
|
||||
enabled = models.BooleanField(
|
||||
default=True
|
||||
)
|
||||
mac_address = MACAddressField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name='MAC Address'
|
||||
)
|
||||
mtu = models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinValueValidator(1), MaxValueValidator(65536)],
|
||||
verbose_name='MTU'
|
||||
)
|
||||
mgmt_only = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name='OOB Management',
|
||||
help_text='This interface is used only for out-of-band management'
|
||||
)
|
||||
mode = models.CharField(
|
||||
max_length=50,
|
||||
choices=InterfaceModeChoices,
|
||||
blank=True
|
||||
)
|
||||
untagged_vlan = models.ForeignKey(
|
||||
to='ipam.VLAN',
|
||||
on_delete=models.SET_NULL,
|
||||
@ -694,15 +694,19 @@ class Interface(CableTermination, ComponentModel):
|
||||
blank=True,
|
||||
verbose_name='Tagged VLANs'
|
||||
)
|
||||
ip_addresses = GenericRelation(
|
||||
to='ipam.IPAddress',
|
||||
content_type_field='assigned_object_type',
|
||||
object_id_field='assigned_object_id',
|
||||
related_query_name='interface'
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = [
|
||||
'device', 'virtual_machine', 'name', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only',
|
||||
'description', 'mode',
|
||||
'device', 'name', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'description', 'mode',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
# TODO: ordering and unique_together should include virtual_machine
|
||||
ordering = ('device', CollateAsChar('_name'))
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
@ -712,7 +716,6 @@ class Interface(CableTermination, ComponentModel):
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.identifier if self.device else None,
|
||||
self.virtual_machine.name if self.virtual_machine else None,
|
||||
self.name,
|
||||
self.lag.name if self.lag else None,
|
||||
self.get_type_display(),
|
||||
@ -726,18 +729,6 @@ class Interface(CableTermination, ComponentModel):
|
||||
|
||||
def clean(self):
|
||||
|
||||
# An Interface must belong to a Device *or* to a VirtualMachine
|
||||
if self.device and self.virtual_machine:
|
||||
raise ValidationError("An interface cannot belong to both a device and a virtual machine.")
|
||||
if not self.device and not self.virtual_machine:
|
||||
raise ValidationError("An interface must belong to either a device or a virtual machine.")
|
||||
|
||||
# VM interfaces must be virtual
|
||||
if self.virtual_machine and self.type not in VMInterfaceTypeChoices.values():
|
||||
raise ValidationError({
|
||||
'type': "Invalid interface type for a virtual machine: {}".format(self.type)
|
||||
})
|
||||
|
||||
# Virtual interfaces cannot be connected
|
||||
if self.type in NONCONNECTABLE_IFACE_TYPES and (
|
||||
self.cable or getattr(self, 'circuit_termination', False)
|
||||
@ -773,7 +764,7 @@ class Interface(CableTermination, ComponentModel):
|
||||
if self.untagged_vlan and self.untagged_vlan.site not in [self.parent.site, None]:
|
||||
raise ValidationError({
|
||||
'untagged_vlan': "The untagged VLAN ({}) must belong to the same site as the interface's parent "
|
||||
"device/VM, or it must be global".format(self.untagged_vlan)
|
||||
"device, or it must be global".format(self.untagged_vlan)
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@ -788,21 +779,6 @@ class Interface(CableTermination, ComponentModel):
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def to_objectchange(self, action):
|
||||
# Annotate the parent Device/VM
|
||||
try:
|
||||
parent_obj = self.device or self.virtual_machine
|
||||
except ObjectDoesNotExist:
|
||||
parent_obj = None
|
||||
|
||||
return ObjectChange(
|
||||
changed_object=self,
|
||||
object_repr=str(self),
|
||||
action=action,
|
||||
related_object=parent_obj,
|
||||
object_data=serialize_object(self)
|
||||
)
|
||||
|
||||
@property
|
||||
def connected_endpoint(self):
|
||||
"""
|
||||
@ -841,7 +817,7 @@ class Interface(CableTermination, ComponentModel):
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
return self.device or self.virtual_machine
|
||||
return self.device
|
||||
|
||||
@property
|
||||
def is_connectable(self):
|
||||
@ -902,7 +878,6 @@ class FrontPort(CableTermination, ComponentModel):
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
|
||||
is_path_endpoint = False
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', '_name')
|
||||
@ -914,6 +889,9 @@ class FrontPort(CableTermination, ComponentModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:frontport', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.identifier,
|
||||
@ -970,7 +948,6 @@ class RearPort(CableTermination, ComponentModel):
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'type', 'positions', 'description']
|
||||
is_path_endpoint = False
|
||||
|
||||
class Meta:
|
||||
ordering = ('device', '_name')
|
||||
@ -979,6 +956,9 @@ class RearPort(CableTermination, ComponentModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:rearport', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.device.identifier,
|
||||
@ -1038,7 +1018,7 @@ class DeviceBay(ComponentModel):
|
||||
return '{} - {}'.format(self.device.name, self.name)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
return reverse('dcim:devicebay', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
@ -1147,7 +1127,7 @@ class InventoryItem(ComponentModel):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:device_inventory', kwargs={'pk': self.device.pk})
|
||||
return reverse('dcim:inventoryitem', kwargs={'pk': self.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
|
@ -4,20 +4,19 @@ from django.db.models.signals import post_save, pre_delete
|
||||
from django.dispatch import receiver
|
||||
|
||||
from .choices import CableStatusChoices
|
||||
from .models import Cable, Device, VirtualChassis
|
||||
from .models import Cable, CableTermination, Device, FrontPort, RearPort, VirtualChassis
|
||||
|
||||
|
||||
@receiver(post_save, sender=VirtualChassis)
|
||||
def assign_virtualchassis_master(instance, created, **kwargs):
|
||||
"""
|
||||
When a VirtualChassis is created, automatically assign its master device to the VC.
|
||||
When a VirtualChassis is created, automatically assign its master device (if any) to the VC.
|
||||
"""
|
||||
if created:
|
||||
devices = Device.objects.filter(pk=instance.master.pk)
|
||||
for device in devices:
|
||||
device.virtual_chassis = instance
|
||||
device.vc_position = None
|
||||
device.save()
|
||||
if created and instance.master:
|
||||
master = Device.objects.get(pk=instance.master.pk)
|
||||
master.virtual_chassis = instance
|
||||
master.vc_position = 1
|
||||
master.save()
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=VirtualChassis)
|
||||
@ -52,7 +51,7 @@ def update_connected_endpoints(instance, **kwargs):
|
||||
# Update any endpoints for this Cable.
|
||||
endpoints = instance.termination_a.get_path_endpoints() + instance.termination_b.get_path_endpoints()
|
||||
for endpoint in endpoints:
|
||||
path, split_ends = endpoint.trace()
|
||||
path, split_ends, position_stack = endpoint.trace()
|
||||
# Determine overall path status (connected or planned)
|
||||
path_status = True
|
||||
for segment in path:
|
||||
@ -61,9 +60,11 @@ def update_connected_endpoints(instance, **kwargs):
|
||||
break
|
||||
|
||||
endpoint_a = path[0][0]
|
||||
endpoint_b = path[-1][2]
|
||||
endpoint_b = path[-1][2] if not split_ends and not position_stack else None
|
||||
|
||||
if getattr(endpoint_a, 'is_path_endpoint', False) and getattr(endpoint_b, 'is_path_endpoint', False):
|
||||
# Patch panel ports are not connected endpoints, all other cable terminations are
|
||||
if isinstance(endpoint_a, CableTermination) and not isinstance(endpoint_a, (FrontPort, RearPort)) and \
|
||||
isinstance(endpoint_b, CableTermination) and not isinstance(endpoint_b, (FrontPort, RearPort)):
|
||||
logger.debug("Updating path endpoints: {} <---> {}".format(endpoint_a, endpoint_b))
|
||||
endpoint_a.connected_endpoint = endpoint_b
|
||||
endpoint_a.connection_status = path_status
|
||||
|
@ -2,7 +2,9 @@ import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from tenancy.tables import COL_TENANT
|
||||
from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ColoredLabelColumn, TagColumn, ToggleColumn
|
||||
from utilities.tables import (
|
||||
BaseTable, BooleanColumn, ButtonsColumn, ColorColumn, ColoredLabelColumn, TagColumn, ToggleColumn,
|
||||
)
|
||||
from .models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
@ -40,69 +42,16 @@ DEVICE_LINK = """
|
||||
</a>
|
||||
"""
|
||||
|
||||
REGION_ACTIONS = """
|
||||
<a href="{% url 'dcim:region_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_region %}
|
||||
<a href="{% url 'dcim:region_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
RACKGROUP_ACTIONS = """
|
||||
<a href="{% url 'dcim:rackgroup_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
RACKGROUP_ELEVATIONS = """
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&group_id={{ record.pk }}" class="btn btn-xs btn-primary" title="View elevations">
|
||||
<i class="fa fa-eye"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_rackgroup %}
|
||||
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning" title="Edit">
|
||||
<i class="glyphicon glyphicon-pencil"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
RACKROLE_ACTIONS = """
|
||||
<a href="{% url 'dcim:rackrole_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_rackrole %}
|
||||
<a href="{% url 'dcim:rackrole_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
RACK_DEVICE_COUNT = """
|
||||
<a href="{% url 'dcim:device_list' %}?rack_id={{ record.pk }}">{{ value }}</a>
|
||||
"""
|
||||
|
||||
RACKRESERVATION_ACTIONS = """
|
||||
<a href="{% url 'dcim:rackreservation_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_rackreservation %}
|
||||
<a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
MANUFACTURER_ACTIONS = """
|
||||
<a href="{% url 'dcim:manufacturer_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_manufacturer %}
|
||||
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
DEVICEROLE_ACTIONS = """
|
||||
<a href="{% url 'dcim:devicerole_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_devicerole %}
|
||||
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
DEVICEROLE_DEVICE_COUNT = """
|
||||
<a href="{% url 'dcim:device_list' %}?role={{ record.slug }}">{{ value }}</a>
|
||||
"""
|
||||
@ -119,15 +68,6 @@ PLATFORM_VM_COUNT = """
|
||||
<a href="{% url 'virtualization:virtualmachine_list' %}?platform={{ record.slug }}">{{ value }}</a>
|
||||
"""
|
||||
|
||||
PLATFORM_ACTIONS = """
|
||||
<a href="{% url 'dcim:platform_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_platform %}
|
||||
<a href="{% url 'dcim:platform_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
STATUS_LABEL = """
|
||||
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
|
||||
"""
|
||||
@ -198,11 +138,7 @@ class RegionTable(BaseTable):
|
||||
site_count = tables.Column(
|
||||
verbose_name='Sites'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=REGION_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
actions = ButtonsColumn(Region)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Region
|
||||
@ -260,10 +196,9 @@ class RackGroupTable(BaseTable):
|
||||
rack_count = tables.Column(
|
||||
verbose_name='Racks'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=RACKGROUP_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
actions = ButtonsColumn(
|
||||
model=RackGroup,
|
||||
prepend_template=RACKGROUP_ELEVATIONS
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
@ -280,11 +215,7 @@ class RackRoleTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
rack_count = tables.Column(verbose_name='Racks')
|
||||
color = tables.TemplateColumn(COLOR_LABEL)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=RACKROLE_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
actions = ButtonsColumn(RackRole)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackRole
|
||||
@ -386,11 +317,7 @@ class RackReservationTable(BaseTable):
|
||||
tags = TagColumn(
|
||||
url_name='dcim:rackreservation_list'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=RACKRESERVATION_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
actions = ButtonsColumn(RackReservation)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackReservation
|
||||
@ -420,11 +347,7 @@ class ManufacturerTable(BaseTable):
|
||||
verbose_name='Platforms'
|
||||
)
|
||||
slug = tables.Column()
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=MANUFACTURER_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
actions = ButtonsColumn(Manufacturer, pk_field='slug')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Manufacturer
|
||||
@ -486,22 +409,10 @@ class ConsolePortTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'actions')
|
||||
fields = ('pk', 'name', 'label', 'type', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class ConsolePortImportTable(BaseTable):
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = ('device', 'name', 'description')
|
||||
empty_text = False
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateTable(ComponentTemplateTable):
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('consoleserverporttemplate'),
|
||||
@ -511,22 +422,10 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'actions')
|
||||
fields = ('pk', 'name', 'label', 'type', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class ConsoleServerPortImportTable(BaseTable):
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsoleServerPort
|
||||
fields = ('device', 'name', 'description')
|
||||
empty_text = False
|
||||
|
||||
|
||||
class PowerPortTemplateTable(ComponentTemplateTable):
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('powerporttemplate'),
|
||||
@ -536,22 +435,10 @@ class PowerPortTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'actions')
|
||||
fields = ('pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class PowerPortImportTable(BaseTable):
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPort
|
||||
fields = ('device', 'name', 'description', 'maximum_draw', 'allocated_draw')
|
||||
empty_text = False
|
||||
|
||||
|
||||
class PowerOutletTemplateTable(ComponentTemplateTable):
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('poweroutlettemplate'),
|
||||
@ -561,22 +448,10 @@ class PowerOutletTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerOutletTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'actions')
|
||||
fields = ('pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class PowerOutletImportTable(BaseTable):
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerOutlet
|
||||
fields = ('device', 'name', 'description', 'power_port', 'feed_leg')
|
||||
empty_text = False
|
||||
|
||||
|
||||
class InterfaceTemplateTable(ComponentTemplateTable):
|
||||
mgmt_only = BooleanColumn(
|
||||
verbose_name='Management Only'
|
||||
@ -589,30 +464,10 @@ class InterfaceTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = InterfaceTemplate
|
||||
fields = ('pk', 'name', 'label', 'mgmt_only', 'type', 'actions')
|
||||
fields = ('pk', 'name', 'label', 'mgmt_only', 'type', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class InterfaceImportTable(BaseTable):
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
virtual_machine = tables.LinkColumn(
|
||||
viewname='virtualization:virtualmachine',
|
||||
args=[Accessor('virtual_machine.pk')],
|
||||
verbose_name='Virtual Machine'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
fields = (
|
||||
'device', 'virtual_machine', 'name', 'description', 'lag', 'type', 'enabled', 'mac_address', 'mtu',
|
||||
'mgmt_only', 'mode',
|
||||
)
|
||||
empty_text = False
|
||||
|
||||
|
||||
class FrontPortTemplateTable(ComponentTemplateTable):
|
||||
rear_port_position = tables.Column(
|
||||
verbose_name='Position'
|
||||
@ -625,22 +480,10 @@ class FrontPortTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = FrontPortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'actions')
|
||||
fields = ('pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class FrontPortImportTable(BaseTable):
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = FrontPort
|
||||
fields = ('device', 'name', 'description', 'type', 'rear_port', 'rear_port_position')
|
||||
empty_text = False
|
||||
|
||||
|
||||
class RearPortTemplateTable(ComponentTemplateTable):
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('rearporttemplate'),
|
||||
@ -650,22 +493,10 @@ class RearPortTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RearPortTemplate
|
||||
fields = ('pk', 'name', 'label', 'type', 'positions', 'actions')
|
||||
fields = ('pk', 'name', 'label', 'type', 'positions', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class RearPortImportTable(BaseTable):
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RearPort
|
||||
fields = ('device', 'name', 'description', 'type', 'position')
|
||||
empty_text = False
|
||||
|
||||
|
||||
class DeviceBayTemplateTable(ComponentTemplateTable):
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('devicebaytemplate'),
|
||||
@ -675,7 +506,7 @@ class DeviceBayTemplateTable(ComponentTemplateTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceBayTemplate
|
||||
fields = ('pk', 'name', 'label', 'actions')
|
||||
fields = ('pk', 'name', 'label', 'description', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
@ -701,11 +532,8 @@ class DeviceRoleTable(BaseTable):
|
||||
template_code=COLOR_LABEL,
|
||||
verbose_name='Label'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=DEVICEROLE_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
vm_role = BooleanColumn()
|
||||
actions = ButtonsColumn(DeviceRole, pk_field='slug')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceRole
|
||||
@ -731,11 +559,7 @@ class PlatformTable(BaseTable):
|
||||
orderable=False,
|
||||
verbose_name='VMs'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=PLATFORM_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
actions = ButtonsColumn(Platform, pk_field='slug')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Platform
|
||||
@ -861,153 +685,108 @@ class DeviceImportTable(BaseTable):
|
||||
# Device components
|
||||
#
|
||||
|
||||
class DeviceComponentDetailTable(BaseTable):
|
||||
class DeviceComponentTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(order_by=('_name',))
|
||||
cable = tables.LinkColumn()
|
||||
device = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
name = tables.Column(
|
||||
linkify=True,
|
||||
order_by=('_name',)
|
||||
)
|
||||
cable = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
order_by = ('device', 'name')
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable')
|
||||
sequence = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable')
|
||||
|
||||
|
||||
class ConsolePortTable(BaseTable):
|
||||
name = tables.Column(order_by=('_name',))
|
||||
class ConsolePortTable(DeviceComponentTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = ('name', 'label', 'type')
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
||||
|
||||
|
||||
class ConsolePortDetailTable(DeviceComponentDetailTable):
|
||||
device = tables.LinkColumn()
|
||||
class ConsoleServerPortTable(DeviceComponentTable):
|
||||
|
||||
class Meta(DeviceComponentDetailTable.Meta, ConsolePortTable.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class ConsoleServerPortTable(BaseTable):
|
||||
name = tables.Column(order_by=('_name',))
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsoleServerPort
|
||||
fields = ('name', 'label', 'description')
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
||||
|
||||
|
||||
class ConsoleServerPortDetailTable(DeviceComponentDetailTable):
|
||||
device = tables.LinkColumn()
|
||||
class PowerPortTable(DeviceComponentTable):
|
||||
|
||||
class Meta(DeviceComponentDetailTable.Meta, ConsoleServerPortTable.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class PowerPortTable(BaseTable):
|
||||
name = tables.Column(order_by=('_name',))
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = PowerPort
|
||||
fields = ('name', 'label', 'type')
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'maximum_draw', 'allocated_draw', 'cable')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
|
||||
|
||||
|
||||
class PowerPortDetailTable(DeviceComponentDetailTable):
|
||||
device = tables.LinkColumn()
|
||||
class PowerOutletTable(DeviceComponentTable):
|
||||
|
||||
class Meta(DeviceComponentDetailTable.Meta, PowerPortTable.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class PowerOutletTable(BaseTable):
|
||||
name = tables.Column(order_by=('_name',))
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = PowerOutlet
|
||||
fields = ('name', 'label', 'type', 'description')
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'power_port', 'feed_leg', 'cable')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')
|
||||
|
||||
|
||||
class PowerOutletDetailTable(DeviceComponentDetailTable):
|
||||
device = tables.LinkColumn()
|
||||
|
||||
class Meta(DeviceComponentDetailTable.Meta, PowerOutletTable.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class InterfaceTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
fields = ('name', 'label', 'type', 'lag', 'enabled', 'mgmt_only', 'description')
|
||||
|
||||
|
||||
class InterfaceDetailTable(DeviceComponentDetailTable):
|
||||
parent = tables.LinkColumn(order_by=('device', 'virtual_machine'))
|
||||
name = tables.LinkColumn()
|
||||
class InterfaceTable(DeviceComponentTable):
|
||||
enabled = BooleanColumn()
|
||||
|
||||
class Meta(InterfaceTable.Meta):
|
||||
order_by = ('parent', 'name')
|
||||
fields = ('pk', 'parent', 'name', 'label', 'enabled', 'type', 'description', 'cable')
|
||||
sequence = ('pk', 'parent', 'name', 'label', 'enabled', 'type', 'description', 'cable')
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = Interface
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'description', 'cable',
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description')
|
||||
|
||||
|
||||
class FrontPortTable(BaseTable):
|
||||
name = tables.Column(order_by=('_name',))
|
||||
class FrontPortTable(DeviceComponentTable):
|
||||
rear_port_position = tables.Column(
|
||||
verbose_name='Position'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = FrontPort
|
||||
fields = ('name', 'label', 'type', 'rear_port', 'rear_port_position', 'description')
|
||||
empty_text = "None"
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description')
|
||||
|
||||
|
||||
class FrontPortDetailTable(DeviceComponentDetailTable):
|
||||
device = tables.LinkColumn()
|
||||
class RearPortTable(DeviceComponentTable):
|
||||
|
||||
class Meta(DeviceComponentDetailTable.Meta, FrontPortTable.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class RearPortTable(BaseTable):
|
||||
name = tables.Column(order_by=('_name',))
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = RearPort
|
||||
fields = ('name', 'label', 'type', 'positions', 'description')
|
||||
empty_text = "None"
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
||||
|
||||
|
||||
class RearPortDetailTable(DeviceComponentDetailTable):
|
||||
device = tables.LinkColumn()
|
||||
class DeviceBayTable(DeviceComponentTable):
|
||||
installed_device = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
|
||||
class Meta(DeviceComponentDetailTable.Meta, RearPortTable.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class DeviceBayTable(BaseTable):
|
||||
name = tables.Column(order_by=('_name',))
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = DeviceBay
|
||||
fields = ('name', 'label', 'description')
|
||||
|
||||
|
||||
class DeviceBayDetailTable(DeviceComponentDetailTable):
|
||||
device = tables.LinkColumn()
|
||||
installed_device = tables.LinkColumn()
|
||||
|
||||
class Meta(DeviceBayTable.Meta):
|
||||
fields = ('pk', 'device', 'name', 'label', 'installed_device', 'description')
|
||||
sequence = ('pk', 'device', 'name', 'label', 'installed_device', 'description')
|
||||
exclude = ('cable',)
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'installed_device', 'description')
|
||||
|
||||
|
||||
class DeviceBayImportTable(BaseTable):
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
installed_device = tables.LinkColumn('dcim:device', args=[Accessor('installed_device.pk')], verbose_name='Installed Device')
|
||||
class InventoryItemTable(DeviceComponentTable):
|
||||
manufacturer = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
discovered = BooleanColumn()
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceBay
|
||||
fields = ('device', 'name', 'installed_device', 'description')
|
||||
empty_text = False
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = InventoryItem
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered'
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag')
|
||||
|
||||
|
||||
#
|
||||
@ -1152,29 +931,6 @@ class InterfaceConnectionTable(BaseTable):
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# InventoryItems
|
||||
#
|
||||
|
||||
class InventoryItemTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
device = tables.LinkColumn(
|
||||
viewname='dcim:device_inventory',
|
||||
args=[Accessor('device.pk')]
|
||||
)
|
||||
manufacturer = tables.Column(
|
||||
accessor=Accessor('manufacturer')
|
||||
)
|
||||
discovered = BooleanColumn()
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = InventoryItem
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered'
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag')
|
||||
|
||||
|
||||
#
|
||||
# Virtual chassis
|
||||
#
|
||||
@ -1182,7 +938,9 @@ class InventoryItemTable(BaseTable):
|
||||
class VirtualChassisTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(
|
||||
accessor=Accessor('master__name'),
|
||||
linkify=True
|
||||
)
|
||||
master = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
member_count = tables.Column(
|
||||
@ -1194,8 +952,8 @@ class VirtualChassisTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VirtualChassis
|
||||
fields = ('pk', 'name', 'domain', 'member_count', 'tags')
|
||||
default_columns = ('pk', 'name', 'domain', 'member_count')
|
||||
fields = ('pk', 'name', 'domain', 'master', 'member_count', 'tags')
|
||||
default_columns = ('pk', 'name', 'domain', 'master', 'member_count')
|
||||
|
||||
|
||||
#
|
||||
|
@ -28,6 +28,43 @@ class AppTest(APITestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class Mixins:
|
||||
|
||||
class ComponentTraceMixin(APITestCase):
|
||||
peer_termination_type = None
|
||||
|
||||
def test_trace(self):
|
||||
"""
|
||||
Test tracing a device component's attached cable.
|
||||
"""
|
||||
obj = self.model.objects.unrestricted().first()
|
||||
peer_device = Device.objects.create(
|
||||
site=Site.objects.unrestricted().first(),
|
||||
device_type=DeviceType.objects.unrestricted().first(),
|
||||
device_role=DeviceRole.objects.unrestricted().first(),
|
||||
name='Peer Device'
|
||||
)
|
||||
if self.peer_termination_type is None:
|
||||
raise NotImplementedError("Test case must set peer_termination_type")
|
||||
peer_obj = self.peer_termination_type.objects.create(
|
||||
device=peer_device,
|
||||
name='Peer Termination'
|
||||
)
|
||||
cable = Cable(termination_a=obj, termination_b=peer_obj, label='Cable 1')
|
||||
cable.save()
|
||||
|
||||
self.add_permissions(f'dcim.view_{self.model._meta.model_name}')
|
||||
url = reverse(f'dcim-api:{self.model._meta.model_name}-trace', kwargs={'pk': obj.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
segment1 = response.data[0]
|
||||
self.assertEqual(segment1[0]['name'], obj.name)
|
||||
self.assertEqual(segment1[1]['label'], cable.label)
|
||||
self.assertEqual(segment1[2]['name'], peer_obj.name)
|
||||
|
||||
|
||||
class RegionTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Region
|
||||
brief_fields = ['id', 'name', 'site_count', 'slug', 'url']
|
||||
@ -107,7 +144,7 @@ class SiteTest(APIViewTestCases.APIViewTestCase):
|
||||
Graph.objects.bulk_create(graphs)
|
||||
|
||||
self.add_permissions('dcim.view_site')
|
||||
url = reverse('dcim-api:site-graphs', kwargs={'pk': Site.objects.first().pk})
|
||||
url = reverse('dcim-api:site-graphs', kwargs={'pk': Site.objects.unrestricted().unrestricted().first().pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(len(response.data), 3)
|
||||
@ -246,7 +283,7 @@ class RackTest(APIViewTestCases.APIViewTestCase):
|
||||
"""
|
||||
GET a single rack elevation.
|
||||
"""
|
||||
rack = Rack.objects.first()
|
||||
rack = Rack.objects.unrestricted().first()
|
||||
self.add_permissions('dcim.view_rack')
|
||||
url = reverse('dcim-api:rack-elevation', kwargs={'pk': rack.pk})
|
||||
|
||||
@ -266,7 +303,7 @@ class RackTest(APIViewTestCases.APIViewTestCase):
|
||||
"""
|
||||
GET a single rack elevation in SVG format.
|
||||
"""
|
||||
rack = Rack.objects.first()
|
||||
rack = Rack.objects.unrestricted().first()
|
||||
self.add_permissions('dcim.view_rack')
|
||||
url = '{}?render=svg'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': rack.pk}))
|
||||
|
||||
@ -281,9 +318,7 @@ class RackReservationTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
user = User.objects.create(username='user1', is_active=True)
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
|
||||
cls.racks = (
|
||||
@ -878,7 +913,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
||||
Graph.objects.bulk_create(graphs)
|
||||
|
||||
self.add_permissions('dcim.view_device')
|
||||
url = reverse('dcim-api:device-graphs', kwargs={'pk': Device.objects.first().pk})
|
||||
url = reverse('dcim-api:device-graphs', kwargs={'pk': Device.objects.unrestricted().first().pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(len(response.data), 3)
|
||||
@ -908,7 +943,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
||||
"""
|
||||
Check that creating a device with a duplicate name within a site fails.
|
||||
"""
|
||||
device = Device.objects.first()
|
||||
device = Device.objects.unrestricted().first()
|
||||
data = {
|
||||
'device_type': device.device_type.pk,
|
||||
'device_role': device.device_role.pk,
|
||||
@ -923,9 +958,10 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class ConsolePortTest(APIViewTestCases.APIViewTestCase):
|
||||
class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = ConsolePort
|
||||
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url']
|
||||
peer_termination_type = ConsoleServerPort
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -957,39 +993,11 @@ class ConsolePortTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
]
|
||||
|
||||
def test_trace_consoleport(self):
|
||||
"""
|
||||
Test tracing a ConsolePort cable.
|
||||
"""
|
||||
consoleport = ConsolePort.objects.first()
|
||||
peer_device = Device.objects.create(
|
||||
site=Site.objects.first(),
|
||||
device_type=DeviceType.objects.first(),
|
||||
device_role=DeviceRole.objects.first(),
|
||||
name='Peer Device'
|
||||
)
|
||||
consoleserverport = ConsoleServerPort.objects.create(
|
||||
device=peer_device,
|
||||
name='Console Server Port 1'
|
||||
)
|
||||
cable = Cable(termination_a=consoleport, termination_b=consoleserverport, label='Cable 1')
|
||||
cable.save()
|
||||
|
||||
self.add_permissions('dcim.view_consoleport')
|
||||
url = reverse('dcim-api:consoleport-trace', kwargs={'pk': consoleport.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
segment1 = response.data[0]
|
||||
self.assertEqual(segment1[0]['name'], consoleport.name)
|
||||
self.assertEqual(segment1[1]['label'], cable.label)
|
||||
self.assertEqual(segment1[2]['name'], consoleserverport.name)
|
||||
|
||||
|
||||
class ConsoleServerPortTest(APIViewTestCases.APIViewTestCase):
|
||||
class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = ConsoleServerPort
|
||||
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url']
|
||||
peer_termination_type = ConsolePort
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -1021,39 +1029,11 @@ class ConsoleServerPortTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
]
|
||||
|
||||
def test_trace_consoleserverport(self):
|
||||
"""
|
||||
Test tracing a ConsoleServerPort cable.
|
||||
"""
|
||||
consoleserverport = ConsoleServerPort.objects.first()
|
||||
peer_device = Device.objects.create(
|
||||
site=Site.objects.first(),
|
||||
device_type=DeviceType.objects.first(),
|
||||
device_role=DeviceRole.objects.first(),
|
||||
name='Peer Device'
|
||||
)
|
||||
consoleport = ConsolePort.objects.create(
|
||||
device=peer_device,
|
||||
name='Console Port 1'
|
||||
)
|
||||
cable = Cable(termination_a=consoleserverport, termination_b=consoleport, label='Cable 1')
|
||||
cable.save()
|
||||
|
||||
self.add_permissions('dcim.view_consoleserverport')
|
||||
url = reverse('dcim-api:consoleserverport-trace', kwargs={'pk': consoleserverport.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
segment1 = response.data[0]
|
||||
self.assertEqual(segment1[0]['name'], consoleserverport.name)
|
||||
self.assertEqual(segment1[1]['label'], cable.label)
|
||||
self.assertEqual(segment1[2]['name'], consoleport.name)
|
||||
|
||||
|
||||
class PowerPortTest(APIViewTestCases.APIViewTestCase):
|
||||
class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = PowerPort
|
||||
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url']
|
||||
peer_termination_type = PowerOutlet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -1085,39 +1065,11 @@ class PowerPortTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
]
|
||||
|
||||
def test_trace_powerport(self):
|
||||
"""
|
||||
Test tracing a PowerPort cable.
|
||||
"""
|
||||
powerport = PowerPort.objects.first()
|
||||
peer_device = Device.objects.create(
|
||||
site=Site.objects.first(),
|
||||
device_type=DeviceType.objects.first(),
|
||||
device_role=DeviceRole.objects.first(),
|
||||
name='Peer Device'
|
||||
)
|
||||
poweroutlet = PowerOutlet.objects.create(
|
||||
device=peer_device,
|
||||
name='Power Outlet 1'
|
||||
)
|
||||
cable = Cable(termination_a=powerport, termination_b=poweroutlet, label='Cable 1')
|
||||
cable.save()
|
||||
|
||||
self.add_permissions('dcim.view_powerport')
|
||||
url = reverse('dcim-api:powerport-trace', kwargs={'pk': powerport.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
segment1 = response.data[0]
|
||||
self.assertEqual(segment1[0]['name'], powerport.name)
|
||||
self.assertEqual(segment1[1]['label'], cable.label)
|
||||
self.assertEqual(segment1[2]['name'], poweroutlet.name)
|
||||
|
||||
|
||||
class PowerOutletTest(APIViewTestCases.APIViewTestCase):
|
||||
class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = PowerOutlet
|
||||
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url']
|
||||
peer_termination_type = PowerPort
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -1149,39 +1101,11 @@ class PowerOutletTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
]
|
||||
|
||||
def test_trace_poweroutlet(self):
|
||||
"""
|
||||
Test tracing a PowerOutlet cable.
|
||||
"""
|
||||
poweroutlet = PowerOutlet.objects.first()
|
||||
peer_device = Device.objects.create(
|
||||
site=Site.objects.first(),
|
||||
device_type=DeviceType.objects.first(),
|
||||
device_role=DeviceRole.objects.first(),
|
||||
name='Peer Device'
|
||||
)
|
||||
powerport = PowerPort.objects.create(
|
||||
device=peer_device,
|
||||
name='Power Port 1'
|
||||
)
|
||||
cable = Cable(termination_a=poweroutlet, termination_b=powerport, label='Cable 1')
|
||||
cable.save()
|
||||
|
||||
self.add_permissions('dcim.view_poweroutlet')
|
||||
url = reverse('dcim-api:poweroutlet-trace', kwargs={'pk': poweroutlet.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
segment1 = response.data[0]
|
||||
self.assertEqual(segment1[0]['name'], poweroutlet.name)
|
||||
self.assertEqual(segment1[1]['label'], cable.label)
|
||||
self.assertEqual(segment1[2]['name'], powerport.name)
|
||||
|
||||
|
||||
class InterfaceTest(APIViewTestCases.APIViewTestCase):
|
||||
class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = Interface
|
||||
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url']
|
||||
peer_termination_type = Interface
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -1245,45 +1169,17 @@ class InterfaceTest(APIViewTestCases.APIViewTestCase):
|
||||
Graph.objects.bulk_create(graphs)
|
||||
|
||||
self.add_permissions('dcim.view_interface')
|
||||
url = reverse('dcim-api:interface-graphs', kwargs={'pk': Interface.objects.first().pk})
|
||||
url = reverse('dcim-api:interface-graphs', kwargs={'pk': Interface.objects.unrestricted().first().pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(len(response.data), 3)
|
||||
self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?interface=Interface 1&foo=1')
|
||||
|
||||
def test_trace_interface(self):
|
||||
"""
|
||||
Test tracing an Interface cable.
|
||||
"""
|
||||
interface_a = Interface.objects.first()
|
||||
peer_device = Device.objects.create(
|
||||
site=Site.objects.first(),
|
||||
device_type=DeviceType.objects.first(),
|
||||
device_role=DeviceRole.objects.first(),
|
||||
name='Peer Device'
|
||||
)
|
||||
interface_b = Interface.objects.create(
|
||||
device=peer_device,
|
||||
name='Interface X'
|
||||
)
|
||||
cable = Cable(termination_a=interface_a, termination_b=interface_b, label='Cable 1')
|
||||
cable.save()
|
||||
|
||||
self.add_permissions('dcim.view_interface')
|
||||
url = reverse('dcim-api:interface-trace', kwargs={'pk': interface_a.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
segment1 = response.data[0]
|
||||
self.assertEqual(segment1[0]['name'], interface_a.name)
|
||||
self.assertEqual(segment1[1]['label'], cable.label)
|
||||
self.assertEqual(segment1[2]['name'], interface_b.name)
|
||||
|
||||
|
||||
class FrontPortTest(APIViewTestCases.APIViewTestCase):
|
||||
class FrontPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = FrontPort
|
||||
brief_fields = ['cable', 'device', 'id', 'name', 'url']
|
||||
peer_termination_type = Interface
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -1334,39 +1230,11 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
]
|
||||
|
||||
def test_trace_frontport(self):
|
||||
"""
|
||||
Test tracing a FrontPort cable.
|
||||
"""
|
||||
frontport = FrontPort.objects.first()
|
||||
peer_device = Device.objects.create(
|
||||
site=Site.objects.first(),
|
||||
device_type=DeviceType.objects.first(),
|
||||
device_role=DeviceRole.objects.first(),
|
||||
name='Peer Device'
|
||||
)
|
||||
interface = Interface.objects.create(
|
||||
device=peer_device,
|
||||
name='Interface X'
|
||||
)
|
||||
cable = Cable(termination_a=frontport, termination_b=interface, label='Cable 1')
|
||||
cable.save()
|
||||
|
||||
self.add_permissions('dcim.view_frontport')
|
||||
url = reverse('dcim-api:frontport-trace', kwargs={'pk': frontport.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
segment1 = response.data[0]
|
||||
self.assertEqual(segment1[0]['name'], frontport.name)
|
||||
self.assertEqual(segment1[1]['label'], cable.label)
|
||||
self.assertEqual(segment1[2]['name'], interface.name)
|
||||
|
||||
|
||||
class RearPortTest(APIViewTestCases.APIViewTestCase):
|
||||
class RearPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = RearPort
|
||||
brief_fields = ['cable', 'device', 'id', 'name', 'url']
|
||||
peer_termination_type = Interface
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -1401,35 +1269,6 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
]
|
||||
|
||||
def test_trace_rearport(self):
|
||||
"""
|
||||
Test tracing a RearPort cable.
|
||||
"""
|
||||
rearport = RearPort.objects.first()
|
||||
peer_device = Device.objects.create(
|
||||
site=Site.objects.first(),
|
||||
device_type=DeviceType.objects.first(),
|
||||
device_role=DeviceRole.objects.first(),
|
||||
name='Peer Device'
|
||||
)
|
||||
interface = Interface.objects.create(
|
||||
device=peer_device,
|
||||
name='Interface X'
|
||||
)
|
||||
cable = Cable(termination_a=rearport, termination_b=interface, label='Cable 1')
|
||||
cable.save()
|
||||
|
||||
self.add_permissions('dcim.view_rearport')
|
||||
url = reverse('dcim-api:rearport-trace', kwargs={'pk': rearport.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
segment1 = response.data[0]
|
||||
self.assertEqual(segment1[0]['name'], rearport.name)
|
||||
self.assertEqual(segment1[1]['label'], cable.label)
|
||||
self.assertEqual(segment1[2]['name'], interface.name)
|
||||
|
||||
|
||||
class DeviceBayTest(APIViewTestCases.APIViewTestCase):
|
||||
model = DeviceBay
|
||||
@ -1640,11 +1479,11 @@ class ConnectionTest(APITestCase):
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(Cable.objects.count(), 1)
|
||||
self.assertEqual(Cable.objects.unrestricted().count(), 1)
|
||||
|
||||
cable = Cable.objects.get(pk=response.data['id'])
|
||||
consoleport1 = ConsolePort.objects.get(pk=consoleport1.pk)
|
||||
consoleserverport1 = ConsoleServerPort.objects.get(pk=consoleserverport1.pk)
|
||||
cable = Cable.objects.unrestricted().get(pk=response.data['id'])
|
||||
consoleport1 = ConsolePort.objects.unrestricted().get(pk=consoleport1.pk)
|
||||
consoleserverport1 = ConsoleServerPort.objects.unrestricted().get(pk=consoleserverport1.pk)
|
||||
|
||||
self.assertEqual(cable.termination_a, consoleport1)
|
||||
self.assertEqual(cable.termination_b, consoleserverport1)
|
||||
@ -1705,12 +1544,12 @@ class ConnectionTest(APITestCase):
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
|
||||
cable = Cable.objects.get(pk=response.data['id'])
|
||||
cable = Cable.objects.unrestricted().get(pk=response.data['id'])
|
||||
self.assertEqual(cable.termination_a.cable, cable)
|
||||
self.assertEqual(cable.termination_b.cable, cable)
|
||||
|
||||
consoleport1 = ConsolePort.objects.get(pk=consoleport1.pk)
|
||||
consoleserverport1 = ConsoleServerPort.objects.get(pk=consoleserverport1.pk)
|
||||
consoleport1 = ConsolePort.objects.unrestricted().get(pk=consoleport1.pk)
|
||||
consoleserverport1 = ConsoleServerPort.objects.unrestricted().get(pk=consoleserverport1.pk)
|
||||
self.assertEqual(consoleport1.connected_endpoint, consoleserverport1)
|
||||
self.assertEqual(consoleserverport1.connected_endpoint, consoleport1)
|
||||
|
||||
@ -1735,11 +1574,11 @@ class ConnectionTest(APITestCase):
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(Cable.objects.count(), 1)
|
||||
self.assertEqual(Cable.objects.unrestricted().count(), 1)
|
||||
|
||||
cable = Cable.objects.get(pk=response.data['id'])
|
||||
powerport1 = PowerPort.objects.get(pk=powerport1.pk)
|
||||
poweroutlet1 = PowerOutlet.objects.get(pk=poweroutlet1.pk)
|
||||
cable = Cable.objects.unrestricted().get(pk=response.data['id'])
|
||||
powerport1 = PowerPort.objects.unrestricted().get(pk=powerport1.pk)
|
||||
poweroutlet1 = PowerOutlet.objects.unrestricted().get(pk=poweroutlet1.pk)
|
||||
|
||||
self.assertEqual(cable.termination_a, powerport1)
|
||||
self.assertEqual(cable.termination_b, poweroutlet1)
|
||||
@ -1771,11 +1610,11 @@ class ConnectionTest(APITestCase):
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(Cable.objects.count(), 1)
|
||||
self.assertEqual(Cable.objects.unrestricted().count(), 1)
|
||||
|
||||
cable = Cable.objects.get(pk=response.data['id'])
|
||||
interface1 = Interface.objects.get(pk=interface1.pk)
|
||||
interface2 = Interface.objects.get(pk=interface2.pk)
|
||||
cable = Cable.objects.unrestricted().get(pk=response.data['id'])
|
||||
interface1 = Interface.objects.unrestricted().get(pk=interface1.pk)
|
||||
interface2 = Interface.objects.unrestricted().get(pk=interface2.pk)
|
||||
|
||||
self.assertEqual(cable.termination_a, interface1)
|
||||
self.assertEqual(cable.termination_b, interface2)
|
||||
@ -1836,12 +1675,12 @@ class ConnectionTest(APITestCase):
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
|
||||
cable = Cable.objects.get(pk=response.data['id'])
|
||||
cable = Cable.objects.unrestricted().get(pk=response.data['id'])
|
||||
self.assertEqual(cable.termination_a.cable, cable)
|
||||
self.assertEqual(cable.termination_b.cable, cable)
|
||||
|
||||
interface1 = Interface.objects.get(pk=interface1.pk)
|
||||
interface2 = Interface.objects.get(pk=interface2.pk)
|
||||
interface1 = Interface.objects.unrestricted().get(pk=interface1.pk)
|
||||
interface2 = Interface.objects.unrestricted().get(pk=interface2.pk)
|
||||
self.assertEqual(interface1.connected_endpoint, interface2)
|
||||
self.assertEqual(interface2.connected_endpoint, interface1)
|
||||
|
||||
@ -1875,11 +1714,11 @@ class ConnectionTest(APITestCase):
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(Cable.objects.count(), 1)
|
||||
self.assertEqual(Cable.objects.unrestricted().count(), 1)
|
||||
|
||||
cable = Cable.objects.get(pk=response.data['id'])
|
||||
interface1 = Interface.objects.get(pk=interface1.pk)
|
||||
circuittermination1 = CircuitTermination.objects.get(pk=circuittermination1.pk)
|
||||
cable = Cable.objects.unrestricted().get(pk=response.data['id'])
|
||||
interface1 = Interface.objects.unrestricted().get(pk=interface1.pk)
|
||||
circuittermination1 = CircuitTermination.objects.unrestricted().get(pk=circuittermination1.pk)
|
||||
|
||||
self.assertEqual(cable.termination_a, interface1)
|
||||
self.assertEqual(cable.termination_b, circuittermination1)
|
||||
@ -1949,12 +1788,12 @@ class ConnectionTest(APITestCase):
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
|
||||
cable = Cable.objects.get(pk=response.data['id'])
|
||||
cable = Cable.objects.unrestricted().get(pk=response.data['id'])
|
||||
self.assertEqual(cable.termination_a.cable, cable)
|
||||
self.assertEqual(cable.termination_b.cable, cable)
|
||||
|
||||
interface1 = Interface.objects.get(pk=interface1.pk)
|
||||
circuittermination1 = CircuitTermination.objects.get(pk=circuittermination1.pk)
|
||||
interface1 = Interface.objects.unrestricted().get(pk=interface1.pk)
|
||||
circuittermination1 = CircuitTermination.objects.unrestricted().get(pk=circuittermination1.pk)
|
||||
self.assertEqual(interface1.connected_endpoint, circuittermination1)
|
||||
self.assertEqual(circuittermination1.connected_endpoint, interface1)
|
||||
|
||||
@ -2003,7 +1842,7 @@ class ConnectedDeviceTest(APITestCase):
|
||||
|
||||
class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
|
||||
model = VirtualChassis
|
||||
brief_fields = ['id', 'master', 'member_count', 'url']
|
||||
brief_fields = ['id', 'master', 'member_count', 'name', 'url']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -2040,34 +1879,35 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
# Create three VirtualChassis with three members each
|
||||
virtual_chassis = (
|
||||
VirtualChassis(master=devices[0], domain='domain-1'),
|
||||
VirtualChassis(master=devices[3], domain='domain-2'),
|
||||
VirtualChassis(master=devices[6], domain='domain-3'),
|
||||
VirtualChassis(name='Virtual Chassis 1', master=devices[0], domain='domain-1'),
|
||||
VirtualChassis(name='Virtual Chassis 2', master=devices[3], domain='domain-2'),
|
||||
VirtualChassis(name='Virtual Chassis 3', master=devices[6], domain='domain-3'),
|
||||
)
|
||||
VirtualChassis.objects.bulk_create(virtual_chassis)
|
||||
Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis[0], vc_position=2)
|
||||
Device.objects.filter(pk=devices[2].pk).update(virtual_chassis=virtual_chassis[0], vc_position=3)
|
||||
Device.objects.filter(pk=devices[4].pk).update(virtual_chassis=virtual_chassis[1], vc_position=2)
|
||||
Device.objects.filter(pk=devices[5].pk).update(virtual_chassis=virtual_chassis[1], vc_position=3)
|
||||
Device.objects.filter(pk=devices[7].pk).update(virtual_chassis=virtual_chassis[2], vc_position=2)
|
||||
Device.objects.filter(pk=devices[8].pk).update(virtual_chassis=virtual_chassis[2], vc_position=3)
|
||||
Device.objects.unrestricted().filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis[0], vc_position=2)
|
||||
Device.objects.unrestricted().filter(pk=devices[2].pk).update(virtual_chassis=virtual_chassis[0], vc_position=3)
|
||||
Device.objects.unrestricted().filter(pk=devices[4].pk).update(virtual_chassis=virtual_chassis[1], vc_position=2)
|
||||
Device.objects.unrestricted().filter(pk=devices[5].pk).update(virtual_chassis=virtual_chassis[1], vc_position=3)
|
||||
Device.objects.unrestricted().filter(pk=devices[7].pk).update(virtual_chassis=virtual_chassis[2], vc_position=2)
|
||||
Device.objects.unrestricted().filter(pk=devices[8].pk).update(virtual_chassis=virtual_chassis[2], vc_position=3)
|
||||
|
||||
cls.update_data = {
|
||||
'master': devices[1].pk,
|
||||
'name': 'Virtual Chassis X',
|
||||
'domain': 'domain-x',
|
||||
'master': devices[1].pk,
|
||||
}
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'master': devices[9].pk,
|
||||
'name': 'Virtual Chassis 4',
|
||||
'domain': 'domain-4',
|
||||
},
|
||||
{
|
||||
'master': devices[10].pk,
|
||||
'name': 'Virtual Chassis 5',
|
||||
'domain': 'domain-5',
|
||||
},
|
||||
{
|
||||
'master': devices[11].pk,
|
||||
'name': 'Virtual Chassis 6',
|
||||
'domain': 'domain-6',
|
||||
},
|
||||
]
|
||||
|
@ -1254,8 +1254,8 @@ class DeviceTestCase(TestCase):
|
||||
|
||||
# Assign primary IPs for filtering
|
||||
ipaddresses = (
|
||||
IPAddress(address='192.0.2.1/24', interface=interfaces[0]),
|
||||
IPAddress(address='192.0.2.2/24', interface=interfaces[1]),
|
||||
IPAddress(address='192.0.2.1/24', assigned_object=interfaces[0]),
|
||||
IPAddress(address='192.0.2.2/24', assigned_object=interfaces[1]),
|
||||
)
|
||||
IPAddress.objects.bulk_create(ipaddresses)
|
||||
Device.objects.filter(pk=devices[0].pk).update(primary_ip4=ipaddresses[0])
|
||||
|
@ -363,6 +363,7 @@ class CableTestCase(TestCase):
|
||||
)
|
||||
self.interface1 = Interface.objects.create(device=self.device1, name='eth0')
|
||||
self.interface2 = Interface.objects.create(device=self.device2, name='eth0')
|
||||
self.interface3 = Interface.objects.create(device=self.device2, name='eth1')
|
||||
self.cable = Cable(termination_a=self.interface1, termination_b=self.interface2)
|
||||
self.cable.save()
|
||||
|
||||
@ -370,10 +371,27 @@ class CableTestCase(TestCase):
|
||||
self.patch_pannel = Device.objects.create(
|
||||
device_type=devicetype, device_role=devicerole, name='TestPatchPannel', site=site
|
||||
)
|
||||
self.rear_port = RearPort.objects.create(device=self.patch_pannel, name='R1', type=1000)
|
||||
self.front_port = FrontPort.objects.create(
|
||||
device=self.patch_pannel, name='F1', type=1000, rear_port=self.rear_port
|
||||
self.rear_port1 = RearPort.objects.create(device=self.patch_pannel, name='RP1', type='8p8c')
|
||||
self.front_port1 = FrontPort.objects.create(
|
||||
device=self.patch_pannel, name='FP1', type='8p8c', rear_port=self.rear_port1, rear_port_position=1
|
||||
)
|
||||
self.rear_port2 = RearPort.objects.create(device=self.patch_pannel, name='RP2', type='8p8c', positions=2)
|
||||
self.front_port2 = FrontPort.objects.create(
|
||||
device=self.patch_pannel, name='FP2', type='8p8c', rear_port=self.rear_port2, rear_port_position=1
|
||||
)
|
||||
self.rear_port3 = RearPort.objects.create(device=self.patch_pannel, name='RP3', type='8p8c', positions=3)
|
||||
self.front_port3 = FrontPort.objects.create(
|
||||
device=self.patch_pannel, name='FP3', type='8p8c', rear_port=self.rear_port3, rear_port_position=1
|
||||
)
|
||||
self.rear_port4 = RearPort.objects.create(device=self.patch_pannel, name='RP4', type='8p8c', positions=3)
|
||||
self.front_port4 = FrontPort.objects.create(
|
||||
device=self.patch_pannel, name='FP4', type='8p8c', rear_port=self.rear_port4, rear_port_position=1
|
||||
)
|
||||
self.provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
self.circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
self.circuit = Circuit.objects.create(provider=self.provider, type=self.circuittype, cid='1')
|
||||
self.circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=site, term_side='A', port_speed=1000)
|
||||
self.circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=site, term_side='Z', port_speed=1000)
|
||||
|
||||
def test_cable_creation(self):
|
||||
"""
|
||||
@ -405,7 +423,7 @@ class CableTestCase(TestCase):
|
||||
cable = Cable.objects.filter(pk=self.cable.pk).first()
|
||||
self.assertIsNone(cable)
|
||||
|
||||
def test_cable_validates_compatibale_types(self):
|
||||
def test_cable_validates_compatible_types(self):
|
||||
"""
|
||||
The clean method should have a check to ensure only compatible port types can be connected by a cable
|
||||
"""
|
||||
@ -426,7 +444,7 @@ class CableTestCase(TestCase):
|
||||
"""
|
||||
A cable cannot connect a front port to its corresponding rear port
|
||||
"""
|
||||
cable = Cable(termination_a=self.front_port, termination_b=self.rear_port)
|
||||
cable = Cable(termination_a=self.front_port1, termination_b=self.rear_port1)
|
||||
with self.assertRaises(ValidationError):
|
||||
cable.clean()
|
||||
|
||||
@ -439,7 +457,94 @@ class CableTestCase(TestCase):
|
||||
with self.assertRaises(ValidationError):
|
||||
cable.clean()
|
||||
|
||||
def test_cable_cannot_terminate_to_a_virtual_inteface(self):
|
||||
def test_connection_via_single_position_rearport(self):
|
||||
"""
|
||||
A RearPort with one position can be connected to anything.
|
||||
|
||||
[CableTermination X]---[RP(pos=1) FP]---[CableTermination Y]
|
||||
|
||||
is allowed anywhere
|
||||
|
||||
[CableTermination X]---[CableTermination Y]
|
||||
|
||||
is allowed.
|
||||
|
||||
A RearPort with multiple positions may not be directly connected to a path endpoint or another RearPort
|
||||
with a different number of positions. RearPorts with a single position on the other hand may be connected
|
||||
to such CableTerminations. Check that this is indeed allowed.
|
||||
"""
|
||||
# Connecting a single-position RearPort to a multi-position RearPort is ok
|
||||
Cable(termination_a=self.rear_port1, termination_b=self.rear_port2).full_clean()
|
||||
|
||||
# Connecting a single-position RearPort to an Interface is ok
|
||||
Cable(termination_a=self.rear_port1, termination_b=self.interface3).full_clean()
|
||||
|
||||
# Connecting a single-position RearPort to a CircuitTermination is ok
|
||||
Cable(termination_a=self.rear_port1, termination_b=self.circuittermination1).full_clean()
|
||||
|
||||
def test_connection_via_multi_position_rearport(self):
|
||||
"""
|
||||
A RearPort with multiple positions may not be directly connected to a path endpoint or another RearPort
|
||||
with a different number of positions.
|
||||
|
||||
The following scenario's are allowed (with x>1):
|
||||
|
||||
~----------+ +---------~
|
||||
| |
|
||||
RP2(pos=x)|---|RP(pos=x)
|
||||
| |
|
||||
~----------+ +---------~
|
||||
|
||||
~----------+ +---------~
|
||||
| |
|
||||
RP2(pos=x)|---|RP(pos=1)
|
||||
| |
|
||||
~----------+ +---------~
|
||||
|
||||
~----------+ +------------------~
|
||||
| |
|
||||
RP2(pos=x)|---|CircuitTermination
|
||||
| |
|
||||
~----------+ +------------------~
|
||||
|
||||
These scenarios are NOT allowed (with x>1):
|
||||
|
||||
~----------+ +----------~
|
||||
| |
|
||||
RP2(pos=x)|---|RP(pos!=x)
|
||||
| |
|
||||
~----------+ +----------~
|
||||
|
||||
~----------+ +----------~
|
||||
| |
|
||||
RP2(pos=x)|---|Interface
|
||||
| |
|
||||
~----------+ +----------~
|
||||
|
||||
These scenarios are tested in this order below.
|
||||
"""
|
||||
# Connecting a multi-position RearPort to another RearPort with the same number of positions is ok
|
||||
Cable(termination_a=self.rear_port3, termination_b=self.rear_port4).full_clean()
|
||||
|
||||
# Connecting a multi-position RearPort to a single-position RearPort is ok
|
||||
Cable(termination_a=self.rear_port2, termination_b=self.rear_port1).full_clean()
|
||||
|
||||
# Connecting a multi-position RearPort to a CircuitTermination is ok
|
||||
Cable(termination_a=self.rear_port2, termination_b=self.circuittermination1).full_clean()
|
||||
|
||||
with self.assertRaises(
|
||||
ValidationError,
|
||||
msg='Connecting a 2-position RearPort to a 3-position RearPort should fail'
|
||||
):
|
||||
Cable(termination_a=self.rear_port2, termination_b=self.rear_port3).full_clean()
|
||||
|
||||
with self.assertRaises(
|
||||
ValidationError,
|
||||
msg='Connecting a multi-position RearPort to an Interface should fail'
|
||||
):
|
||||
Cable(termination_a=self.rear_port2, termination_b=self.interface3).full_clean()
|
||||
|
||||
def test_cable_cannot_terminate_to_a_virtual_interface(self):
|
||||
"""
|
||||
A cable cannot terminate to a virtual interface
|
||||
"""
|
||||
@ -448,7 +553,7 @@ class CableTestCase(TestCase):
|
||||
with self.assertRaises(ValidationError):
|
||||
cable.clean()
|
||||
|
||||
def test_cable_cannot_terminate_to_a_wireless_inteface(self):
|
||||
def test_cable_cannot_terminate_to_a_wireless_interface(self):
|
||||
"""
|
||||
A cable cannot terminate to a wireless interface
|
||||
"""
|
||||
@ -501,9 +606,13 @@ class CablePathTestCase(TestCase):
|
||||
Device(device_type=devicetype, device_role=devicerole, name='Panel 2', site=site),
|
||||
Device(device_type=devicetype, device_role=devicerole, name='Panel 3', site=site),
|
||||
Device(device_type=devicetype, device_role=devicerole, name='Panel 4', site=site),
|
||||
Device(device_type=devicetype, device_role=devicerole, name='Panel 5', site=site),
|
||||
Device(device_type=devicetype, device_role=devicerole, name='Panel 6', site=site),
|
||||
)
|
||||
Device.objects.bulk_create(patch_panels)
|
||||
for patch_panel in patch_panels:
|
||||
|
||||
# Create patch panels with 4 positions
|
||||
for patch_panel in patch_panels[:4]:
|
||||
rearport = RearPort.objects.create(device=patch_panel, name='Rear Port 1', positions=4, type=PortTypeChoices.TYPE_8P8C)
|
||||
FrontPort.objects.bulk_create((
|
||||
FrontPort(device=patch_panel, name='Front Port 1', rear_port=rearport, rear_port_position=1, type=PortTypeChoices.TYPE_8P8C),
|
||||
@ -512,6 +621,11 @@ class CablePathTestCase(TestCase):
|
||||
FrontPort(device=patch_panel, name='Front Port 4', rear_port=rearport, rear_port_position=4, type=PortTypeChoices.TYPE_8P8C),
|
||||
))
|
||||
|
||||
# Create 1-on-1 patch panels
|
||||
for patch_panel in patch_panels[4:]:
|
||||
rearport = RearPort.objects.create(device=patch_panel, name='Rear Port 1', positions=1, type=PortTypeChoices.TYPE_8P8C)
|
||||
FrontPort.objects.create(device=patch_panel, name='Front Port 1', rear_port=rearport, rear_port_position=1, type=PortTypeChoices.TYPE_8P8C)
|
||||
|
||||
def test_direct_connection(self):
|
||||
"""
|
||||
Test a direct connection between two interfaces.
|
||||
@ -524,6 +638,7 @@ class CablePathTestCase(TestCase):
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
)
|
||||
cable.full_clean()
|
||||
cable.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
@ -551,22 +666,25 @@ class CablePathTestCase(TestCase):
|
||||
|
||||
def test_connection_via_single_rear_port(self):
|
||||
"""
|
||||
Test a connection which passes through a single front/rear port pair.
|
||||
Test a connection which passes through a rear port with exactly one front port.
|
||||
|
||||
1 2
|
||||
[Device 1] ----- [Panel 1] ----- [Device 2]
|
||||
[Device 1] ----- [Panel 5] ----- [Device 2]
|
||||
Iface1 FP1 RP1 Iface1
|
||||
"""
|
||||
# Create cables
|
||||
# Create cables (FP first, RP second)
|
||||
cable1 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 5', name='Front Port 1')
|
||||
)
|
||||
cable1.full_clean()
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
termination_b=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
|
||||
termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
termination_a=RearPort.objects.get(device__name='Panel 5', name='Rear Port 1'),
|
||||
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
)
|
||||
self.assertEqual(cable2.termination_a.positions, 1) # Sanity check
|
||||
cable2.full_clean()
|
||||
cable2.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
@ -592,6 +710,97 @@ class CablePathTestCase(TestCase):
|
||||
self.assertIsNone(endpoint_a.connection_status)
|
||||
self.assertIsNone(endpoint_b.connection_status)
|
||||
|
||||
def test_connections_via_nested_single_position_rearport(self):
|
||||
"""
|
||||
Test a connection which passes through a single front/rear port pair between two multi-position rear ports.
|
||||
|
||||
Test two connections via patched rear ports:
|
||||
Device 1 <---> Device 2
|
||||
Device 3 <---> Device 4
|
||||
|
||||
1 2
|
||||
[Device 1] -----------+ +----------- [Device 2]
|
||||
Iface1 | | Iface1
|
||||
FP1 | 3 4 | FP1
|
||||
[Panel 1] ----- [Panel 5] ----- [Panel 2]
|
||||
FP2 | RP1 RP1 FP1 RP1 | FP2
|
||||
Iface1 | | Iface1
|
||||
[Device 3] -----------+ +----------- [Device 4]
|
||||
5 6
|
||||
"""
|
||||
# Create cables (Panel 5 RP first, FP second)
|
||||
cable1 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
|
||||
)
|
||||
cable1.full_clean()
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1'),
|
||||
termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
)
|
||||
cable2.full_clean()
|
||||
cable2.save()
|
||||
cable3 = Cable(
|
||||
termination_b=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
|
||||
termination_a=RearPort.objects.get(device__name='Panel 5', name='Rear Port 1')
|
||||
)
|
||||
cable3.full_clean()
|
||||
cable3.save()
|
||||
cable4 = Cable(
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 5', name='Front Port 1'),
|
||||
termination_a=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
|
||||
)
|
||||
cable4.full_clean()
|
||||
cable4.save()
|
||||
cable5 = Cable(
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2'),
|
||||
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1')
|
||||
)
|
||||
cable5.full_clean()
|
||||
cable5.save()
|
||||
cable6 = Cable(
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2'),
|
||||
termination_a=Interface.objects.get(device__name='Device 4', name='Interface 1')
|
||||
)
|
||||
cable6.full_clean()
|
||||
cable6.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
|
||||
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
endpoint_c = Interface.objects.get(device__name='Device 3', name='Interface 1')
|
||||
endpoint_d = Interface.objects.get(device__name='Device 4', name='Interface 1')
|
||||
|
||||
# Validate connections
|
||||
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
|
||||
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
|
||||
self.assertEqual(endpoint_c.connected_endpoint, endpoint_d)
|
||||
self.assertEqual(endpoint_d.connected_endpoint, endpoint_c)
|
||||
self.assertTrue(endpoint_a.connection_status)
|
||||
self.assertTrue(endpoint_b.connection_status)
|
||||
self.assertTrue(endpoint_c.connection_status)
|
||||
self.assertTrue(endpoint_d.connection_status)
|
||||
|
||||
# Delete cable 3
|
||||
cable3.delete()
|
||||
|
||||
# Refresh endpoints
|
||||
endpoint_a.refresh_from_db()
|
||||
endpoint_b.refresh_from_db()
|
||||
endpoint_c.refresh_from_db()
|
||||
endpoint_d.refresh_from_db()
|
||||
|
||||
# Check that connections have been nullified
|
||||
self.assertIsNone(endpoint_a.connected_endpoint)
|
||||
self.assertIsNone(endpoint_b.connected_endpoint)
|
||||
self.assertIsNone(endpoint_c.connected_endpoint)
|
||||
self.assertIsNone(endpoint_d.connected_endpoint)
|
||||
self.assertIsNone(endpoint_a.connection_status)
|
||||
self.assertIsNone(endpoint_b.connection_status)
|
||||
self.assertIsNone(endpoint_c.connection_status)
|
||||
self.assertIsNone(endpoint_d.connection_status)
|
||||
|
||||
def test_connections_via_patch(self):
|
||||
"""
|
||||
Test two connections via patched rear ports:
|
||||
@ -613,28 +822,33 @@ class CablePathTestCase(TestCase):
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
|
||||
)
|
||||
cable1.full_clean()
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1')
|
||||
)
|
||||
cable2.full_clean()
|
||||
cable2.save()
|
||||
|
||||
cable3 = Cable(
|
||||
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
|
||||
termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
|
||||
)
|
||||
cable3.full_clean()
|
||||
cable3.save()
|
||||
|
||||
cable4 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2')
|
||||
)
|
||||
cable4.full_clean()
|
||||
cable4.save()
|
||||
cable5 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 4', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2')
|
||||
)
|
||||
cable5.full_clean()
|
||||
cable5.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
@ -693,43 +907,51 @@ class CablePathTestCase(TestCase):
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
|
||||
)
|
||||
cable1.full_clean()
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 3', name='Front Port 1')
|
||||
)
|
||||
cable2.full_clean()
|
||||
cable2.save()
|
||||
cable3 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 1'),
|
||||
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
)
|
||||
cable3.full_clean()
|
||||
cable3.save()
|
||||
|
||||
cable4 = Cable(
|
||||
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
|
||||
termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
|
||||
)
|
||||
cable4.full_clean()
|
||||
cable4.save()
|
||||
cable5 = Cable(
|
||||
termination_a=RearPort.objects.get(device__name='Panel 3', name='Rear Port 1'),
|
||||
termination_b=RearPort.objects.get(device__name='Panel 4', name='Rear Port 1')
|
||||
)
|
||||
cable5.full_clean()
|
||||
cable5.save()
|
||||
|
||||
cable6 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2')
|
||||
)
|
||||
cable6.full_clean()
|
||||
cable6.save()
|
||||
cable7 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 3', name='Front Port 2')
|
||||
)
|
||||
cable7.full_clean()
|
||||
cable7.save()
|
||||
cable8 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 2'),
|
||||
termination_b=Interface.objects.get(device__name='Device 4', name='Interface 1')
|
||||
)
|
||||
cable8.full_clean()
|
||||
cable8.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
@ -789,38 +1011,45 @@ class CablePathTestCase(TestCase):
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
|
||||
)
|
||||
cable1.full_clean()
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 1'),
|
||||
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
)
|
||||
cable2.full_clean()
|
||||
cable2.save()
|
||||
|
||||
cable3 = Cable(
|
||||
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1')
|
||||
)
|
||||
cable3.full_clean()
|
||||
cable3.save()
|
||||
cable4 = Cable(
|
||||
termination_a=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1'),
|
||||
termination_b=RearPort.objects.get(device__name='Panel 3', name='Rear Port 1')
|
||||
)
|
||||
cable4.full_clean()
|
||||
cable4.save()
|
||||
cable5 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 3', name='Front Port 1'),
|
||||
termination_b=RearPort.objects.get(device__name='Panel 4', name='Rear Port 1')
|
||||
)
|
||||
cable5.full_clean()
|
||||
cable5.save()
|
||||
|
||||
cable6 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2')
|
||||
)
|
||||
cable6.full_clean()
|
||||
cable6.save()
|
||||
cable7 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 2'),
|
||||
termination_b=Interface.objects.get(device__name='Device 4', name='Interface 1')
|
||||
)
|
||||
cable7.full_clean()
|
||||
cable7.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
@ -870,11 +1099,13 @@ class CablePathTestCase(TestCase):
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=CircuitTermination.objects.get(term_side='A')
|
||||
)
|
||||
cable1.full_clean()
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
termination_a=CircuitTermination.objects.get(term_side='Z'),
|
||||
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
)
|
||||
cable2.full_clean()
|
||||
cable2.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
@ -903,30 +1134,34 @@ class CablePathTestCase(TestCase):
|
||||
def test_connection_via_patched_circuit(self):
|
||||
"""
|
||||
1 2 3 4
|
||||
[Device 1] ----- [Panel 1] ----- [Circuit] ----- [Panel 2] ----- [Device 2]
|
||||
[Device 1] ----- [Panel 5] ----- [Circuit] ----- [Panel 6] ----- [Device 2]
|
||||
Iface1 FP1 RP1 A Z RP1 FP1 Iface1
|
||||
|
||||
"""
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
|
||||
termination_b=FrontPort.objects.get(device__name='Panel 5', name='Front Port 1')
|
||||
)
|
||||
cable1.full_clean()
|
||||
cable1.save()
|
||||
cable2 = Cable(
|
||||
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
|
||||
termination_a=RearPort.objects.get(device__name='Panel 5', name='Rear Port 1'),
|
||||
termination_b=CircuitTermination.objects.get(term_side='A')
|
||||
)
|
||||
cable2.full_clean()
|
||||
cable2.save()
|
||||
cable3 = Cable(
|
||||
termination_a=CircuitTermination.objects.get(term_side='Z'),
|
||||
termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
|
||||
termination_b=RearPort.objects.get(device__name='Panel 6', name='Rear Port 1')
|
||||
)
|
||||
cable3.full_clean()
|
||||
cable3.save()
|
||||
cable4 = Cable(
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1'),
|
||||
termination_a=FrontPort.objects.get(device__name='Panel 6', name='Front Port 1'),
|
||||
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
|
||||
)
|
||||
cable4.full_clean()
|
||||
cable4.save()
|
||||
|
||||
# Retrieve endpoints
|
||||
|
@ -813,14 +813,7 @@ class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase
|
||||
}
|
||||
|
||||
|
||||
# TODO: Change base class to DeviceComponentTemplateViewTestCase
|
||||
# Blocked by absence of bulk edit view for DeviceBays
|
||||
class DeviceBayTemplateTestCase(
|
||||
ViewTestCases.EditObjectViewTestCase,
|
||||
ViewTestCases.DeleteObjectViewTestCase,
|
||||
ViewTestCases.BulkCreateObjectsViewTestCase,
|
||||
ViewTestCases.BulkDeleteObjectsViewTestCase
|
||||
):
|
||||
class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
|
||||
model = DeviceBayTemplate
|
||||
|
||||
@classmethod
|
||||
@ -848,6 +841,10 @@ class DeviceBayTemplateTestCase(
|
||||
'name_pattern': 'Device Bay Template [4-6]',
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'Foo bar',
|
||||
}
|
||||
|
||||
|
||||
class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = DeviceRole
|
||||
@ -1194,10 +1191,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
)
|
||||
|
||||
|
||||
class InterfaceTestCase(
|
||||
ViewTestCases.GetObjectViewTestCase,
|
||||
ViewTestCases.DeviceComponentViewTestCase,
|
||||
):
|
||||
class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = Interface
|
||||
|
||||
@classmethod
|
||||
@ -1563,16 +1557,7 @@ class CableTestCase(
|
||||
}
|
||||
|
||||
|
||||
# TODO: Change base class to PrimaryObjectViewTestCase
|
||||
# Blocked by standard creation, bulk creation views for VirtualChassis (member devices must be selected in bulk)
|
||||
class VirtualChassisTestCase(
|
||||
ViewTestCases.GetObjectViewTestCase,
|
||||
ViewTestCases.EditObjectViewTestCase,
|
||||
ViewTestCases.DeleteObjectViewTestCase,
|
||||
ViewTestCases.ListObjectsViewTestCase,
|
||||
ViewTestCases.BulkEditObjectsViewTestCase,
|
||||
ViewTestCases.BulkDeleteObjectsViewTestCase
|
||||
):
|
||||
class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = VirtualChassis
|
||||
|
||||
@classmethod
|
||||
@ -1587,7 +1572,6 @@ class VirtualChassisTestCase(
|
||||
name='Device Role', slug='device-role-1'
|
||||
)
|
||||
|
||||
# Create 9 member Devices
|
||||
devices = (
|
||||
Device(device_type=device_type, device_role=device_role, name='Device 1', site=site),
|
||||
Device(device_type=device_type, device_role=device_role, name='Device 2', site=site),
|
||||
@ -1598,23 +1582,29 @@ class VirtualChassisTestCase(
|
||||
Device(device_type=device_type, device_role=device_role, name='Device 7', site=site),
|
||||
Device(device_type=device_type, device_role=device_role, name='Device 8', site=site),
|
||||
Device(device_type=device_type, device_role=device_role, name='Device 9', site=site),
|
||||
Device(device_type=device_type, device_role=device_role, name='Device 10', site=site),
|
||||
Device(device_type=device_type, device_role=device_role, name='Device 11', site=site),
|
||||
Device(device_type=device_type, device_role=device_role, name='Device 12', site=site),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
# Create three VirtualChassis with two members each
|
||||
vc1 = VirtualChassis.objects.create(master=devices[0], domain='domain-1')
|
||||
# Create three VirtualChassis with three members each
|
||||
vc1 = VirtualChassis.objects.create(name='VC1', master=devices[0], domain='domain-1')
|
||||
Device.objects.filter(pk=devices[0].pk).update(virtual_chassis=vc1, vc_position=1)
|
||||
Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=vc1, vc_position=2)
|
||||
Device.objects.filter(pk=devices[2].pk).update(virtual_chassis=vc1, vc_position=3)
|
||||
vc2 = VirtualChassis.objects.create(master=devices[3], domain='domain-2')
|
||||
vc2 = VirtualChassis.objects.create(name='VC2', master=devices[3], domain='domain-2')
|
||||
Device.objects.filter(pk=devices[3].pk).update(virtual_chassis=vc2, vc_position=1)
|
||||
Device.objects.filter(pk=devices[4].pk).update(virtual_chassis=vc2, vc_position=2)
|
||||
Device.objects.filter(pk=devices[5].pk).update(virtual_chassis=vc2, vc_position=3)
|
||||
vc3 = VirtualChassis.objects.create(master=devices[6], domain='domain-3')
|
||||
vc3 = VirtualChassis.objects.create(name='VC3', master=devices[6], domain='domain-3')
|
||||
Device.objects.filter(pk=devices[6].pk).update(virtual_chassis=vc3, vc_position=1)
|
||||
Device.objects.filter(pk=devices[7].pk).update(virtual_chassis=vc3, vc_position=2)
|
||||
Device.objects.filter(pk=devices[8].pk).update(virtual_chassis=vc3, vc_position=3)
|
||||
|
||||
cls.form_data = {
|
||||
'master': devices[1].pk,
|
||||
'domain': 'domain-x',
|
||||
'name': 'VC4',
|
||||
'domain': 'domain-4',
|
||||
# Management form data for VC members
|
||||
'form-TOTAL_FORMS': 0,
|
||||
'form-INITIAL_FORMS': 3,
|
||||
@ -1622,6 +1612,13 @@ class VirtualChassisTestCase(
|
||||
'form-MAX_NUM_FORMS': 1000,
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,domain,master",
|
||||
"VC4,Domain 4,Device 10",
|
||||
"VC5,Domain 5,Device 11",
|
||||
"VC6,Domain 6,Device 12",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'domain': 'domain-x',
|
||||
}
|
||||
|
@ -4,9 +4,9 @@ from extras.views import ObjectChangeLogView, ImageAttachmentEditView
|
||||
from ipam.views import ServiceEditView
|
||||
from . import views
|
||||
from .models import (
|
||||
Cable, ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, FrontPort, Interface, Manufacturer, Platform,
|
||||
PowerFeed, PowerPanel, PowerPort, PowerOutlet, Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site,
|
||||
VirtualChassis,
|
||||
Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, FrontPort, Interface,
|
||||
InventoryItem, Manufacturer, Platform, PowerFeed, PowerPanel, PowerPort, PowerOutlet, Rack, RackGroup,
|
||||
RackReservation, RackRole, RearPort, Region, Site, VirtualChassis,
|
||||
)
|
||||
|
||||
app_name = 'dcim'
|
||||
@ -18,6 +18,7 @@ urlpatterns = [
|
||||
path('regions/import/', views.RegionBulkImportView.as_view(), name='region_import'),
|
||||
path('regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
|
||||
path('regions/<int:pk>/edit/', views.RegionEditView.as_view(), name='region_edit'),
|
||||
path('regions/<int:pk>/delete/', views.RegionDeleteView.as_view(), name='region_delete'),
|
||||
path('regions/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}),
|
||||
|
||||
# Sites
|
||||
@ -38,6 +39,7 @@ urlpatterns = [
|
||||
path('rack-groups/import/', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'),
|
||||
path('rack-groups/delete/', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
|
||||
path('rack-groups/<int:pk>/edit/', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
|
||||
path('rack-groups/<int:pk>/delete/', views.RackGroupDeleteView.as_view(), name='rackgroup_delete'),
|
||||
path('rack-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}),
|
||||
|
||||
# Rack roles
|
||||
@ -46,6 +48,7 @@ urlpatterns = [
|
||||
path('rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
|
||||
path('rack-roles/delete/', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
|
||||
path('rack-roles/<int:pk>/edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
||||
path('rack-roles/<int:pk>/delete/', views.RackRoleDeleteView.as_view(), name='rackrole_delete'),
|
||||
path('rack-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}),
|
||||
|
||||
# Rack reservations
|
||||
@ -78,6 +81,7 @@ urlpatterns = [
|
||||
path('manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
|
||||
path('manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
|
||||
path('manufacturers/<slug:slug>/edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
|
||||
path('manufacturers/<slug:slug>/delete/', views.ManufacturerDeleteView.as_view(), name='manufacturer_delete'),
|
||||
path('manufacturers/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}),
|
||||
|
||||
# Device types
|
||||
@ -142,7 +146,7 @@ urlpatterns = [
|
||||
|
||||
# Device bay templates
|
||||
path('device-bay-templates/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicebaytemplate_add'),
|
||||
# path('device-bay-templates/edit/', views.DeviceBayTemplateBulkEditView.as_view(), name='devicebaytemplate_bulk_edit'),
|
||||
path('device-bay-templates/edit/', views.DeviceBayTemplateBulkEditView.as_view(), name='devicebaytemplate_bulk_edit'),
|
||||
path('device-bay-templates/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicebaytemplate_bulk_delete'),
|
||||
path('device-bay-templates/<int:pk>/edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'),
|
||||
path('device-bay-templates/<int:pk>/delete/', views.DeviceBayTemplateDeleteView.as_view(), name='devicebaytemplate_delete'),
|
||||
@ -153,6 +157,7 @@ urlpatterns = [
|
||||
path('device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
|
||||
path('device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
|
||||
path('device-roles/<slug:slug>/edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
|
||||
path('device-roles/<slug:slug>/delete/', views.DeviceRoleDeleteView.as_view(), name='devicerole_delete'),
|
||||
path('device-roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
|
||||
|
||||
# Platforms
|
||||
@ -161,6 +166,7 @@ urlpatterns = [
|
||||
path('platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'),
|
||||
path('platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
|
||||
path('platforms/<slug:slug>/edit/', views.PlatformEditView.as_view(), name='platform_edit'),
|
||||
path('platforms/<slug:slug>/delete/', views.PlatformDeleteView.as_view(), name='platform_delete'),
|
||||
path('platforms/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}),
|
||||
|
||||
# Devices
|
||||
@ -187,12 +193,15 @@ urlpatterns = [
|
||||
path('console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
|
||||
path('console-ports/import/', views.ConsolePortBulkImportView.as_view(), name='consoleport_import'),
|
||||
path('console-ports/edit/', views.ConsolePortBulkEditView.as_view(), name='consoleport_bulk_edit'),
|
||||
# TODO: Bulk rename, disconnect views for ConsolePorts
|
||||
path('console-ports/rename/', views.ConsolePortBulkRenameView.as_view(), name='consoleport_bulk_rename'),
|
||||
path('console-ports/disconnect/', views.ConsolePortBulkDisconnectView.as_view(), name='consoleport_bulk_disconnect'),
|
||||
path('console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
|
||||
path('console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
|
||||
path('console-ports/<int:pk>/', views.ConsolePortView.as_view(), name='consoleport'),
|
||||
path('console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
|
||||
path('console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
|
||||
path('console-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleport_changelog', kwargs={'model': ConsolePort}),
|
||||
path('console-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
|
||||
path('console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
|
||||
path('devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
|
||||
|
||||
# Console server ports
|
||||
@ -203,10 +212,12 @@ urlpatterns = [
|
||||
path('console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
|
||||
path('console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
|
||||
path('console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
|
||||
path('console-server-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
|
||||
path('console-server-ports/<int:pk>/', views.ConsoleServerPortView.as_view(), name='consoleserverport'),
|
||||
path('console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
|
||||
path('console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
|
||||
path('console-server-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleserverport_changelog', kwargs={'model': ConsoleServerPort}),
|
||||
path('console-server-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
|
||||
path('console-server-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
|
||||
path('devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
|
||||
|
||||
# Power ports
|
||||
@ -214,12 +225,15 @@ urlpatterns = [
|
||||
path('power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'),
|
||||
path('power-ports/import/', views.PowerPortBulkImportView.as_view(), name='powerport_import'),
|
||||
path('power-ports/edit/', views.PowerPortBulkEditView.as_view(), name='powerport_bulk_edit'),
|
||||
# TODO: Bulk rename, disconnect views for PowerPorts
|
||||
path('power-ports/rename/', views.PowerPortBulkRenameView.as_view(), name='powerport_bulk_rename'),
|
||||
path('power-ports/disconnect/', views.PowerPortBulkDisconnectView.as_view(), name='powerport_bulk_disconnect'),
|
||||
path('power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
|
||||
path('power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
|
||||
path('power-ports/<int:pk>/', views.PowerPortView.as_view(), name='powerport'),
|
||||
path('power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
|
||||
path('power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
|
||||
path('power-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerport_changelog', kwargs={'model': PowerPort}),
|
||||
path('power-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
|
||||
path('power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
|
||||
path('devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
|
||||
|
||||
# Power outlets
|
||||
@ -230,10 +244,12 @@ urlpatterns = [
|
||||
path('power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
|
||||
path('power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
|
||||
path('power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
|
||||
path('power-outlets/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
|
||||
path('power-outlets/<int:pk>/', views.PowerOutletView.as_view(), name='poweroutlet'),
|
||||
path('power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
|
||||
path('power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
|
||||
path('power-outlets/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='poweroutlet_changelog', kwargs={'model': PowerOutlet}),
|
||||
path('power-outlets/<int:pk>/trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
|
||||
path('power-outlets/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
|
||||
path('devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
|
||||
|
||||
# Interfaces
|
||||
@ -244,12 +260,12 @@ urlpatterns = [
|
||||
path('interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
|
||||
path('interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
|
||||
path('interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
||||
path('interfaces/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
|
||||
path('interfaces/<int:pk>/', views.InterfaceView.as_view(), name='interface'),
|
||||
path('interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
|
||||
path('interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
|
||||
path('interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
|
||||
path('interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
|
||||
path('interfaces/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
|
||||
path('devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
|
||||
|
||||
# Front ports
|
||||
@ -260,10 +276,12 @@ urlpatterns = [
|
||||
path('front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'),
|
||||
path('front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'),
|
||||
path('front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'),
|
||||
path('front-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
|
||||
path('front-ports/<int:pk>/', views.FrontPortView.as_view(), name='frontport'),
|
||||
path('front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'),
|
||||
path('front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
|
||||
path('front-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='frontport_changelog', kwargs={'model': FrontPort}),
|
||||
path('front-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
|
||||
path('front-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
|
||||
# path('devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
|
||||
|
||||
# Rear ports
|
||||
@ -274,10 +292,12 @@ urlpatterns = [
|
||||
path('rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'),
|
||||
path('rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'),
|
||||
path('rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'),
|
||||
path('rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
|
||||
path('rear-ports/<int:pk>/', views.RearPortView.as_view(), name='rearport'),
|
||||
path('rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
|
||||
path('rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
|
||||
path('rear-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rearport_changelog', kwargs={'model': RearPort}),
|
||||
path('rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
|
||||
path('rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
|
||||
path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
|
||||
|
||||
# Device bays
|
||||
@ -287,8 +307,10 @@ urlpatterns = [
|
||||
path('device-bays/edit/', views.DeviceBayBulkEditView.as_view(), name='devicebay_bulk_edit'),
|
||||
path('device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
|
||||
path('device-bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
|
||||
path('device-bays/<int:pk>/', views.DeviceBayView.as_view(), name='devicebay'),
|
||||
path('device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
|
||||
path('device-bays/<int:pk>/delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
|
||||
path('device-bays/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicebay_changelog', kwargs={'model': DeviceBay}),
|
||||
path('device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
|
||||
path('device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'),
|
||||
path('devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
|
||||
@ -298,10 +320,13 @@ urlpatterns = [
|
||||
path('inventory-items/add/', views.InventoryItemCreateView.as_view(), name='inventoryitem_add'),
|
||||
path('inventory-items/import/', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'),
|
||||
path('inventory-items/edit/', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'),
|
||||
# TODO: Bulk rename view for InventoryItems
|
||||
path('inventory-items/rename/', views.InventoryItemBulkRenameView.as_view(), name='inventoryitem_bulk_rename'),
|
||||
path('inventory-items/delete/', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'),
|
||||
path('inventory-items/<int:pk>/', views.InventoryItemView.as_view(), name='inventoryitem'),
|
||||
path('inventory-items/<int:pk>/edit/', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
|
||||
path('inventory-items/<int:pk>/delete/', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
|
||||
path('inventory-items/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='inventoryitem_changelog', kwargs={'model': InventoryItem}),
|
||||
path('devices/inventory-items/add/', views.DeviceBulkAddInventoryItemView.as_view(), name='device_bulk_add_inventoryitem'),
|
||||
|
||||
# Cables
|
||||
path('cables/', views.CableListView.as_view(), name='cable_list'),
|
||||
@ -321,6 +346,7 @@ urlpatterns = [
|
||||
# Virtual chassis
|
||||
path('virtual-chassis/', views.VirtualChassisListView.as_view(), name='virtualchassis_list'),
|
||||
path('virtual-chassis/add/', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'),
|
||||
path('virtual-chassis/import/', views.VirtualChassisBulkImportView.as_view(), name='virtualchassis_import'),
|
||||
path('virtual-chassis/edit/', views.VirtualChassisBulkEditView.as_view(), name='virtualchassis_bulk_edit'),
|
||||
path('virtual-chassis/delete/', views.VirtualChassisBulkDeleteView.as_view(), name='virtualchassis_bulk_delete'),
|
||||
path('virtual-chassis/<int:pk>/', views.VirtualChassisView.as_view(), name='virtualchassis'),
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -110,7 +110,7 @@ class ExportTemplateViewSet(ModelViewSet):
|
||||
#
|
||||
|
||||
class TagViewSet(ModelViewSet):
|
||||
queryset = Tag.restricted.annotate(
|
||||
queryset = Tag.objects.annotate(
|
||||
tagged_items=Count('extras_taggeditem_items', distinct=True)
|
||||
)
|
||||
serializer_class = serializers.TagSerializer
|
||||
|
@ -6,6 +6,7 @@ from django import get_version
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users', 'virtualization']
|
||||
@ -52,6 +53,7 @@ class Command(BaseCommand):
|
||||
pass
|
||||
|
||||
# Additional objects to include
|
||||
namespace['ContentType'] = ContentType
|
||||
namespace['User'] = User
|
||||
|
||||
# Load convenience commands
|
||||
|
@ -540,7 +540,7 @@ class ConfigContextModel(models.Model):
|
||||
|
||||
# Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
|
||||
data = OrderedDict()
|
||||
for context in ConfigContext.objects.get_for_object(self):
|
||||
for context in ConfigContext.objects.unrestricted().get_for_object(self):
|
||||
data = deepmerge(data, context.data)
|
||||
|
||||
# If the object has local config context data defined, merge it last
|
||||
|
@ -22,14 +22,10 @@ class Tag(TagBase, ChangeLoggedModel):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
objects = models.Manager()
|
||||
restricted = RestrictedQuerySet.as_manager()
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'color', 'description']
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:tag', args=[self.slug])
|
||||
|
||||
def slugify(self, tag, i=None):
|
||||
# Allow Unicode in Tag slugs (avoids empty slugs for Tags with all-Unicode names)
|
||||
slug = slugify(tag, allow_unicode=True)
|
||||
|
@ -173,7 +173,7 @@ class ChoiceVar(ScriptVariable):
|
||||
|
||||
class ObjectVar(ScriptVariable):
|
||||
"""
|
||||
NetBox object representation. The provided QuerySet will determine the choices available.
|
||||
A single object within NetBox.
|
||||
"""
|
||||
form_field = DynamicModelChoiceField
|
||||
|
||||
|
@ -1,21 +1,9 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn
|
||||
from utilities.tables import BaseTable, BooleanColumn, ButtonsColumn, ColorColumn, ToggleColumn
|
||||
from .models import ConfigContext, ObjectChange, JobResult, Tag, TaggedItem
|
||||
|
||||
TAG_ACTIONS = """
|
||||
<a href="{% url 'extras:tag_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.taggit.change_tag %}
|
||||
<a href="{% url 'extras:tag_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
{% if perms.taggit.delete_tag %}
|
||||
<a href="{% url 'extras:tag_delete' slug=record.slug %}" class="btn btn-xs btn-danger"><i class="glyphicon glyphicon-trash" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
TAGGED_ITEM = """
|
||||
{% if value.get_absolute_url %}
|
||||
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
||||
@ -64,16 +52,8 @@ OBJECTCHANGE_REQUEST_ID = """
|
||||
|
||||
class TagTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(
|
||||
viewname='extras:tag',
|
||||
args=[Accessor('slug')]
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=TAG_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
color = ColorColumn()
|
||||
actions = ButtonsColumn(Tag, pk_field='slug')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Tag
|
||||
|
@ -10,7 +10,7 @@ from extras.models import ConfigContext, ObjectChange, Tag
|
||||
from utilities.testing import ViewTestCases, TestCase
|
||||
|
||||
|
||||
class TagTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
class TagTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = Tag
|
||||
|
||||
@classmethod
|
||||
|
@ -13,7 +13,6 @@ urlpatterns = [
|
||||
path('tags/import/', views.TagBulkImportView.as_view(), name='tag_import'),
|
||||
path('tags/edit/', views.TagBulkEditView.as_view(), name='tag_bulk_edit'),
|
||||
path('tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
|
||||
path('tags/<str:slug>/', views.TagView.as_view(), name='tag'),
|
||||
path('tags/<str:slug>/edit/', views.TagEditView.as_view(), name='tag_edit'),
|
||||
path('tags/<str:slug>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
|
||||
path('tags/<str:slug>/changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}),
|
||||
|
@ -4,13 +4,15 @@ from django import template
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Count, Q
|
||||
from django.db.models import Count, Prefetch, Q
|
||||
from django.http import Http404, HttpResponseForbidden
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.views.generic import View
|
||||
from django_tables2 import RequestConfig
|
||||
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
from utilities.utils import copy_safe_request, shallow_compare_dict
|
||||
@ -18,6 +20,7 @@ from utilities.views import (
|
||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
ContentTypePermissionRequiredMixin,
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from . import filters, forms, tables
|
||||
from .choices import JobResultStatusChoices
|
||||
from .models import ConfigContext, ImageAttachment, ObjectChange, Report, JobResult, Script, Tag, TaggedItem
|
||||
@ -30,7 +33,7 @@ from .scripts import get_scripts, run_script
|
||||
#
|
||||
|
||||
class TagListView(ObjectListView):
|
||||
queryset = Tag.restricted.annotate(
|
||||
queryset = Tag.objects.annotate(
|
||||
items=Count('extras_taggeditem_items', distinct=True)
|
||||
).order_by(
|
||||
'name'
|
||||
@ -40,71 +43,39 @@ class TagListView(ObjectListView):
|
||||
table = tables.TagTable
|
||||
|
||||
|
||||
class TagView(ObjectView):
|
||||
queryset = Tag.restricted.all()
|
||||
|
||||
def get(self, request, slug):
|
||||
|
||||
tag = get_object_or_404(self.queryset, slug=slug)
|
||||
tagged_items = TaggedItem.objects.filter(
|
||||
tag=tag
|
||||
).prefetch_related(
|
||||
'content_type', 'content_object'
|
||||
)
|
||||
|
||||
# Generate a table of all items tagged with this Tag
|
||||
items_table = tables.TaggedItemTable(tagged_items)
|
||||
paginate = {
|
||||
'paginator_class': EnhancedPaginator,
|
||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||
}
|
||||
RequestConfig(request, paginate).configure(items_table)
|
||||
|
||||
return render(request, 'extras/tag.html', {
|
||||
'tag': tag,
|
||||
'items_count': tagged_items.count(),
|
||||
'items_table': items_table,
|
||||
})
|
||||
|
||||
|
||||
class TagEditView(ObjectEditView):
|
||||
queryset = Tag.restricted.all()
|
||||
queryset = Tag.objects.all()
|
||||
model_form = forms.TagForm
|
||||
default_return_url = 'extras:tag_list'
|
||||
template_name = 'extras/tag_edit.html'
|
||||
|
||||
|
||||
class TagDeleteView(ObjectDeleteView):
|
||||
queryset = Tag.restricted.all()
|
||||
default_return_url = 'extras:tag_list'
|
||||
queryset = Tag.objects.all()
|
||||
|
||||
|
||||
class TagBulkImportView(BulkImportView):
|
||||
queryset = Tag.restricted.all()
|
||||
queryset = Tag.objects.all()
|
||||
model_form = forms.TagCSVForm
|
||||
table = tables.TagTable
|
||||
default_return_url = 'extras:tag_list'
|
||||
|
||||
|
||||
class TagBulkEditView(BulkEditView):
|
||||
queryset = Tag.restricted.annotate(
|
||||
queryset = Tag.objects.annotate(
|
||||
items=Count('extras_taggeditem_items', distinct=True)
|
||||
).order_by(
|
||||
'name'
|
||||
)
|
||||
table = tables.TagTable
|
||||
form = forms.TagBulkEditForm
|
||||
default_return_url = 'extras:tag_list'
|
||||
|
||||
|
||||
class TagBulkDeleteView(BulkDeleteView):
|
||||
queryset = Tag.restricted.annotate(
|
||||
queryset = Tag.objects.annotate(
|
||||
items=Count('extras_taggeditem_items')
|
||||
).order_by(
|
||||
'name'
|
||||
)
|
||||
table = tables.TagTable
|
||||
default_return_url = 'extras:tag_list'
|
||||
|
||||
|
||||
#
|
||||
@ -123,6 +94,18 @@ class ConfigContextView(ObjectView):
|
||||
queryset = ConfigContext.objects.all()
|
||||
|
||||
def get(self, request, pk):
|
||||
# Extend queryset to prefetch related objects
|
||||
self.queryset = self.queryset.prefetch_related(
|
||||
Prefetch('regions', queryset=Region.objects.restrict(request.user)),
|
||||
Prefetch('sites', queryset=Site.objects.restrict(request.user)),
|
||||
Prefetch('roles', queryset=DeviceRole.objects.restrict(request.user)),
|
||||
Prefetch('platforms', queryset=Platform.objects.restrict(request.user)),
|
||||
Prefetch('clusters', queryset=Cluster.objects.restrict(request.user)),
|
||||
Prefetch('cluster_groups', queryset=ClusterGroup.objects.restrict(request.user)),
|
||||
Prefetch('tenants', queryset=Tenant.objects.restrict(request.user)),
|
||||
Prefetch('tenant_groups', queryset=TenantGroup.objects.restrict(request.user)),
|
||||
)
|
||||
|
||||
configcontext = get_object_or_404(self.queryset, pk=pk)
|
||||
|
||||
# Determine user's preferred output format
|
||||
@ -144,7 +127,6 @@ class ConfigContextView(ObjectView):
|
||||
class ConfigContextEditView(ObjectEditView):
|
||||
queryset = ConfigContext.objects.all()
|
||||
model_form = forms.ConfigContextForm
|
||||
default_return_url = 'extras:configcontext_list'
|
||||
template_name = 'extras/configcontext_edit.html'
|
||||
|
||||
|
||||
@ -153,18 +135,15 @@ class ConfigContextBulkEditView(BulkEditView):
|
||||
filterset = filters.ConfigContextFilterSet
|
||||
table = tables.ConfigContextTable
|
||||
form = forms.ConfigContextBulkEditForm
|
||||
default_return_url = 'extras:configcontext_list'
|
||||
|
||||
|
||||
class ConfigContextDeleteView(ObjectDeleteView):
|
||||
queryset = ConfigContext.objects.all()
|
||||
default_return_url = 'extras:configcontext_list'
|
||||
|
||||
|
||||
class ConfigContextBulkDeleteView(BulkDeleteView):
|
||||
queryset = ConfigContext.objects.all()
|
||||
table = tables.ConfigContextTable
|
||||
default_return_url = 'extras:configcontext_list'
|
||||
|
||||
|
||||
class ObjectConfigContextView(ObjectView):
|
||||
@ -264,9 +243,11 @@ class ObjectChangeLogView(View):
|
||||
|
||||
def get(self, request, model, **kwargs):
|
||||
|
||||
# Get object my model and kwargs (e.g. slug='foo')
|
||||
queryset = model.objects.restrict(request.user, 'view')
|
||||
obj = get_object_or_404(queryset, **kwargs)
|
||||
# Handle QuerySet restriction of parent object if needed
|
||||
if hasattr(model.objects, 'restrict'):
|
||||
obj = get_object_or_404(model.objects.restrict(request.user, 'view'), **kwargs)
|
||||
else:
|
||||
obj = get_object_or_404(model, **kwargs)
|
||||
|
||||
# Gather all changes for this object (and its related objects)
|
||||
content_type = ContentType.objects.get_for_model(model)
|
||||
@ -299,6 +280,7 @@ class ObjectChangeLogView(View):
|
||||
|
||||
return render(request, 'extras/object_changelog.html', {
|
||||
object_var: obj,
|
||||
'instance': obj, # We'll eventually standardize on 'instance` for the object variable name
|
||||
'table': objectchanges_table,
|
||||
'base_template': base_template,
|
||||
'active_tab': 'changelog',
|
||||
|
@ -1,5 +1,7 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from drf_yasg.utils import swagger_serializer_method
|
||||
from rest_framework import serializers
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
@ -9,10 +11,12 @@ from dcim.models import Interface
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from extras.api.serializers import TaggedObjectSerializer
|
||||
from ipam.choices import *
|
||||
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from utilities.api import (
|
||||
ChoiceField, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer,
|
||||
ChoiceField, ContentTypeField, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer,
|
||||
get_serializer_for_model,
|
||||
)
|
||||
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
|
||||
from .nested_serializers import *
|
||||
@ -228,18 +232,31 @@ class IPAddressSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
status = ChoiceField(choices=IPAddressStatusChoices, required=False)
|
||||
role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False)
|
||||
interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
|
||||
assigned_object_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(IPADDRESS_ASSIGNMENT_MODELS),
|
||||
required=False
|
||||
)
|
||||
assigned_object = serializers.SerializerMethodField(read_only=True)
|
||||
nat_inside = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
nat_outside = NestedIPAddressSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = [
|
||||
'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'nat_inside',
|
||||
'nat_outside', 'dns_name', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'assigned_object_type', 'assigned_object_id',
|
||||
'assigned_object', 'nat_inside', 'nat_outside', 'dns_name', 'description', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
read_only_fields = ['family']
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||
def get_assigned_object(self, obj):
|
||||
if obj.assigned_object is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.assigned_object, prefix='Nested')
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.assigned_object, context=context).data
|
||||
|
||||
|
||||
class AvailableIPSerializer(serializers.Serializer):
|
||||
"""
|
||||
|
@ -1,5 +1,5 @@
|
||||
from django.conf import settings
|
||||
from django.db.models import Count
|
||||
from django.db.models import Count, Prefetch
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django_pglocks import advisory_lock
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
@ -233,8 +233,7 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
||||
|
||||
class IPAddressViewSet(CustomFieldModelViewSet):
|
||||
queryset = IPAddress.objects.prefetch_related(
|
||||
'vrf__tenant', 'tenant', 'nat_inside', 'interface__device__device_type', 'interface__virtual_machine',
|
||||
'nat_outside', 'tags',
|
||||
'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags',
|
||||
)
|
||||
serializer_class = serializers.IPAddressSerializer
|
||||
filterset_class = filters.IPAddressFilterSet
|
||||
@ -271,6 +270,9 @@ class VLANViewSet(CustomFieldModelViewSet):
|
||||
#
|
||||
|
||||
class ServiceViewSet(ModelViewSet):
|
||||
queryset = Service.objects.prefetch_related('device').prefetch_related('tags')
|
||||
queryset = Service.objects.prefetch_related(
|
||||
Prefetch('ipaddresses', queryset=IPAddress.objects.unrestricted()),
|
||||
'device', 'virtual_machine', 'tags'
|
||||
)
|
||||
serializer_class = serializers.ServiceSerializer
|
||||
filterset_class = filters.ServiceFilterSet
|
||||
|
@ -1,3 +1,5 @@
|
||||
from django.db.models import Q
|
||||
|
||||
from .choices import IPAddressRoleChoices
|
||||
|
||||
# BGP ASN bounds
|
||||
@ -29,6 +31,11 @@ PREFIX_LENGTH_MAX = 127 # IPv6
|
||||
# IPAddresses
|
||||
#
|
||||
|
||||
IPADDRESS_ASSIGNMENT_MODELS = Q(
|
||||
Q(app_label='dcim', model='interface') |
|
||||
Q(app_label='virtualization', model='vminterface')
|
||||
)
|
||||
|
||||
IPADDRESS_MASK_LENGTH_MIN = 1
|
||||
IPADDRESS_MASK_LENGTH_MAX = 128 # IPv6
|
||||
|
||||
|
@ -11,7 +11,7 @@ from utilities.filters import (
|
||||
BaseFilterSet, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, TagFilter,
|
||||
TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
from .choices import *
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
|
||||
@ -309,27 +309,38 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet,
|
||||
field_name='pk',
|
||||
label='Device (ID)',
|
||||
)
|
||||
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='interface__virtual_machine',
|
||||
queryset=VirtualMachine.objects.unrestricted(),
|
||||
label='Virtual machine (ID)',
|
||||
)
|
||||
virtual_machine = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='interface__virtual_machine__name',
|
||||
queryset=VirtualMachine.objects.unrestricted(),
|
||||
to_field_name='name',
|
||||
virtual_machine = MultiValueCharFilter(
|
||||
method='filter_virtual_machine',
|
||||
field_name='name',
|
||||
label='Virtual machine (name)',
|
||||
)
|
||||
virtual_machine_id = MultiValueNumberFilter(
|
||||
method='filter_virtual_machine',
|
||||
field_name='pk',
|
||||
label='Virtual machine (ID)',
|
||||
)
|
||||
interface = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='interface__name',
|
||||
queryset=Interface.objects.unrestricted(),
|
||||
to_field_name='name',
|
||||
label='Interface (ID)',
|
||||
label='Interface (name)',
|
||||
)
|
||||
interface_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='interface',
|
||||
queryset=Interface.objects.unrestricted(),
|
||||
label='Interface (ID)',
|
||||
)
|
||||
vminterface = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vminterface__name',
|
||||
queryset=VMInterface.objects.unrestricted(),
|
||||
to_field_name='name',
|
||||
label='VM interface (name)',
|
||||
)
|
||||
vminterface_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vminterface',
|
||||
queryset=VMInterface.objects.unrestricted(),
|
||||
label='VM interface (ID)',
|
||||
)
|
||||
assigned_to_interface = django_filters.BooleanFilter(
|
||||
method='_assigned_to_interface',
|
||||
label='Is assigned to an interface',
|
||||
@ -379,17 +390,29 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet,
|
||||
return queryset.filter(address__net_mask_length=value)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
try:
|
||||
devices = Device.objects.prefetch_related('device_type').filter(**{'{}__in'.format(name): value})
|
||||
vc_interface_ids = []
|
||||
for device in devices:
|
||||
vc_interface_ids.extend([i['id'] for i in device.vc_interfaces.values('id')])
|
||||
return queryset.filter(interface_id__in=vc_interface_ids)
|
||||
except Device.DoesNotExist:
|
||||
devices = Device.objects.filter(**{'{}__in'.format(name): value})
|
||||
if not devices.exists():
|
||||
return queryset.none()
|
||||
interface_ids = []
|
||||
for device in devices:
|
||||
interface_ids.extend(device.vc_interfaces.values_list('id', flat=True))
|
||||
return queryset.filter(
|
||||
interface__in=interface_ids
|
||||
)
|
||||
|
||||
def filter_virtual_machine(self, queryset, name, value):
|
||||
virtual_machines = VirtualMachine.objects.filter(**{'{}__in'.format(name): value})
|
||||
if not virtual_machines.exists():
|
||||
return queryset.none()
|
||||
interface_ids = []
|
||||
for vm in virtual_machines:
|
||||
interface_ids.extend(vm.interfaces.values_list('id', flat=True))
|
||||
return queryset.filter(
|
||||
vminterface__in=interface_ids
|
||||
)
|
||||
|
||||
def _assigned_to_interface(self, queryset, name, value):
|
||||
return queryset.exclude(interface__isnull=value)
|
||||
return queryset.exclude(assigned_object_id__isnull=value)
|
||||
|
||||
|
||||
class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
|
@ -14,7 +14,7 @@ from utilities.forms import (
|
||||
ExpandableIPAddressField, ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
|
||||
BOOLEAN_WITH_BLANK_CHOICES,
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
@ -522,10 +522,33 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
||||
#
|
||||
|
||||
class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModelForm):
|
||||
interface = forms.ModelChoiceField(
|
||||
device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
filter_for={
|
||||
'interface': 'device_id'
|
||||
}
|
||||
)
|
||||
)
|
||||
interface = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False
|
||||
)
|
||||
virtual_machine = DynamicModelChoiceField(
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
filter_for={
|
||||
'vminterface': 'virtual_machine_id'
|
||||
}
|
||||
)
|
||||
)
|
||||
vminterface = DynamicModelChoiceField(
|
||||
queryset=VMInterface.objects.all(),
|
||||
required=False,
|
||||
label='Interface'
|
||||
)
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
@ -597,8 +620,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = [
|
||||
'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'interface', 'primary_for_parent',
|
||||
'nat_site', 'nat_rack', 'nat_inside', 'tenant_group', 'tenant', 'tags',
|
||||
'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'primary_for_parent', 'nat_site', 'nat_rack',
|
||||
'nat_inside', 'tenant_group', 'tenant', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'status': StaticSelect2(),
|
||||
@ -610,7 +633,14 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
||||
# Initialize helper selectors
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial', {}).copy()
|
||||
if instance and instance.nat_inside and instance.nat_inside.device is not None:
|
||||
if instance:
|
||||
if type(instance.assigned_object) is Interface:
|
||||
initial['device'] = instance.assigned_object.device
|
||||
initial['interface'] = instance.assigned_object
|
||||
elif type(instance.assigned_object) is VMInterface:
|
||||
initial['virtual_machine'] = instance.assigned_object.virtual_machine
|
||||
initial['vminterface'] = instance.assigned_object
|
||||
if instance.nat_inside and instance.nat_inside.device is not None:
|
||||
initial['nat_site'] = instance.nat_inside.device.site
|
||||
initial['nat_rack'] = instance.nat_inside.device.rack
|
||||
initial['nat_device'] = instance.nat_inside.device
|
||||
@ -620,22 +650,9 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
||||
|
||||
self.fields['vrf'].empty_label = 'Global'
|
||||
|
||||
# Limit interface selections to those belonging to the parent device/VM
|
||||
if self.instance and self.instance.interface:
|
||||
self.fields['interface'].queryset = Interface.objects.filter(
|
||||
device=self.instance.interface.device, virtual_machine=self.instance.interface.virtual_machine
|
||||
).prefetch_related(
|
||||
'device__primary_ip4',
|
||||
'device__primary_ip6',
|
||||
'virtual_machine__primary_ip4',
|
||||
'virtual_machine__primary_ip6',
|
||||
) # We prefetch the primary address fields to ensure cache invalidation does not balk on the save()
|
||||
else:
|
||||
self.fields['interface'].choices = []
|
||||
|
||||
# Initialize primary_for_parent if IP address is already assigned
|
||||
if self.instance.pk and self.instance.interface is not None:
|
||||
parent = self.instance.interface.parent
|
||||
if self.instance.pk and self.instance.assigned_object:
|
||||
parent = self.instance.assigned_object.parent
|
||||
if (
|
||||
self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or
|
||||
self.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk
|
||||
@ -645,32 +662,39 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Cannot select both a device interface and a VM interface
|
||||
if self.cleaned_data.get('interface') and self.cleaned_data.get('vminterface'):
|
||||
raise forms.ValidationError("Cannot select both a device interface and a virtual machine interface")
|
||||
|
||||
# Primary IP assignment is only available if an interface has been assigned.
|
||||
if self.cleaned_data.get('primary_for_parent') and not self.cleaned_data.get('interface'):
|
||||
interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
|
||||
if self.cleaned_data.get('primary_for_parent') and not interface:
|
||||
self.add_error(
|
||||
'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs."
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# Set assigned object
|
||||
interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
|
||||
if interface:
|
||||
self.instance.assigned_object = interface
|
||||
|
||||
ipaddress = super().save(*args, **kwargs)
|
||||
|
||||
# Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
|
||||
if self.cleaned_data['primary_for_parent']:
|
||||
parent = self.cleaned_data['interface'].parent
|
||||
if interface and self.cleaned_data['primary_for_parent']:
|
||||
if ipaddress.address.version == 4:
|
||||
parent.primary_ip4 = ipaddress
|
||||
interface.parent.primary_ip4 = ipaddress
|
||||
else:
|
||||
parent.primary_ip6 = ipaddress
|
||||
parent.save()
|
||||
elif self.cleaned_data['interface']:
|
||||
parent = self.cleaned_data['interface'].parent
|
||||
if ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress:
|
||||
parent.primary_ip4 = None
|
||||
parent.save()
|
||||
elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress:
|
||||
parent.primary_ip6 = None
|
||||
parent.save()
|
||||
interface.primary_ip6 = ipaddress
|
||||
interface.parent.save()
|
||||
elif interface and ipaddress.address.version == 4 and interface.parent.primary_ip4 == ipaddress:
|
||||
interface.parent.primary_ip4 = None
|
||||
interface.parent.save()
|
||||
elif interface and ipaddress.address.version == 6 and interface.parent.primary_ip6 == ipaddress:
|
||||
interface.parent.primary_ip4 = None
|
||||
interface.parent.save()
|
||||
|
||||
return ipaddress
|
||||
|
||||
@ -742,7 +766,7 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
|
||||
help_text='Parent VM of assigned interface (if any)'
|
||||
)
|
||||
interface = CSVModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
queryset=Interface.objects.none(), # Can also refer to VMInterface
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned interface'
|
||||
@ -761,21 +785,17 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
if data:
|
||||
|
||||
# Limit interface queryset by assigned device or virtual machine
|
||||
# Limit interface queryset by assigned device
|
||||
if data.get('device'):
|
||||
params = {
|
||||
f"device__{self.fields['device'].to_field_name}": data.get('device')
|
||||
}
|
||||
self.fields['interface'].queryset = Interface.objects.filter(
|
||||
**{f"device__{self.fields['device'].to_field_name}": data['device']}
|
||||
)
|
||||
|
||||
# Limit interface queryset by assigned device
|
||||
elif data.get('virtual_machine'):
|
||||
params = {
|
||||
f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data.get('virtual_machine')
|
||||
}
|
||||
else:
|
||||
params = {
|
||||
'device': None,
|
||||
'virtual_machine': None,
|
||||
}
|
||||
self.fields['interface'].queryset = self.fields['interface'].queryset.filter(**params)
|
||||
self.fields['interface'].queryset = VMInterface.objects.filter(
|
||||
**{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
@ -790,6 +810,10 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# Set interface assignment
|
||||
if self.cleaned_data['interface']:
|
||||
self.instance.assigned_object = self.cleaned_data['interface']
|
||||
|
||||
ipaddress = super().save(*args, **kwargs)
|
||||
|
||||
# Set as primary for device/VM
|
||||
@ -1194,13 +1218,12 @@ class ServiceForm(BootstrapMixin, CustomFieldModelForm):
|
||||
|
||||
# Limit IP address choices to those assigned to interfaces of the parent device/VM
|
||||
if self.instance.device:
|
||||
vc_interface_ids = [i['id'] for i in self.instance.device.vc_interfaces.values('id')]
|
||||
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
|
||||
interface_id__in=vc_interface_ids
|
||||
interface__in=self.instance.device.vc_interfaces.values_list('id', flat=True)
|
||||
)
|
||||
elif self.instance.virtual_machine:
|
||||
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
|
||||
interface__virtual_machine=self.instance.virtual_machine
|
||||
vminterface__in=self.instance.virtual_machine.interfaces.values_list('id', flat=True)
|
||||
)
|
||||
else:
|
||||
self.fields['ipaddresses'].choices = []
|
||||
|
40
netbox/ipam/migrations/0037_ipaddress_assignment.py
Normal file
40
netbox/ipam/migrations/0037_ipaddress_assignment.py
Normal file
@ -0,0 +1,40 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def set_assigned_object_type(apps, schema_editor):
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
IPAddress = apps.get_model('ipam', 'IPAddress')
|
||||
|
||||
device_ct = ContentType.objects.get(app_label='dcim', model='interface').pk
|
||||
IPAddress.objects.update(assigned_object_type=device_ct)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('ipam', '0036_standardize_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='ipaddress',
|
||||
old_name='interface',
|
||||
new_name='assigned_object_id',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ipaddress',
|
||||
name='assigned_object_id',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ipaddress',
|
||||
name='assigned_object_type',
|
||||
field=models.ForeignKey(blank=True, limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'dcim'), ('model', 'interface')), models.Q(('app_label', 'virtualization'), ('model', 'vminterface')), _connector='OR')), null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=set_assigned_object_type
|
||||
),
|
||||
]
|
@ -1,10 +1,11 @@
|
||||
import netaddr
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import F, Q
|
||||
from django.db.models import F
|
||||
from django.urls import reverse
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
@ -14,7 +15,7 @@ from extras.utils import extras_features
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import serialize_object
|
||||
from virtualization.models import VirtualMachine
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .fields import IPNetworkField, IPAddressField
|
||||
@ -215,7 +216,9 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
|
||||
})
|
||||
|
||||
# Ensure that the aggregate being added is not covered by an existing aggregate
|
||||
covering_aggregates = Aggregate.objects.filter(prefix__net_contains_or_equals=str(self.prefix))
|
||||
covering_aggregates = Aggregate.objects.unrestricted().filter(
|
||||
prefix__net_contains_or_equals=str(self.prefix)
|
||||
)
|
||||
if self.pk:
|
||||
covering_aggregates = covering_aggregates.exclude(pk=self.pk)
|
||||
if covering_aggregates:
|
||||
@ -226,7 +229,7 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
|
||||
})
|
||||
|
||||
# Ensure that the aggregate being added does not cover an existing aggregate
|
||||
covered_aggregates = Aggregate.objects.filter(prefix__net_contained=str(self.prefix))
|
||||
covered_aggregates = Aggregate.objects.unrestricted().filter(prefix__net_contained=str(self.prefix))
|
||||
if self.pk:
|
||||
covered_aggregates = covered_aggregates.exclude(pk=self.pk)
|
||||
if covered_aggregates:
|
||||
@ -254,7 +257,7 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
|
||||
"""
|
||||
Determine the prefix utilization of the aggregate and return it as a percentage.
|
||||
"""
|
||||
queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix))
|
||||
queryset = Prefix.objects.unrestricted().filter(prefix__net_contained_or_equal=str(self.prefix))
|
||||
child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
|
||||
return int(float(child_prefixes.size) / self.prefix.size * 100)
|
||||
|
||||
@ -552,7 +555,10 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
|
||||
"container", calculate utilization based on child prefixes. For all others, count child IP addresses.
|
||||
"""
|
||||
if self.status == PrefixStatusChoices.STATUS_CONTAINER:
|
||||
queryset = Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf)
|
||||
queryset = Prefix.objects.unrestricted().filter(
|
||||
prefix__net_contained=str(self.prefix),
|
||||
vrf=self.vrf
|
||||
)
|
||||
child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
|
||||
return int(float(child_prefixes.size) / self.prefix.size * 100)
|
||||
else:
|
||||
@ -606,13 +612,22 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
||||
blank=True,
|
||||
help_text='The functional role of this IP'
|
||||
)
|
||||
interface = models.ForeignKey(
|
||||
to='dcim.Interface',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='ip_addresses',
|
||||
assigned_object_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
limit_choices_to=IPADDRESS_ASSIGNMENT_MODELS,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
assigned_object_id = models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
assigned_object = GenericForeignKey(
|
||||
ct_field='assigned_object_type',
|
||||
fk_field='assigned_object_id'
|
||||
)
|
||||
nat_inside = models.OneToOneField(
|
||||
to='self',
|
||||
on_delete=models.SET_NULL,
|
||||
@ -643,11 +658,11 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
||||
objects = IPAddressManager()
|
||||
|
||||
csv_headers = [
|
||||
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'is_primary',
|
||||
'address', 'vrf', 'tenant', 'status', 'role', 'assigned_object_type', 'assigned_object_id', 'is_primary',
|
||||
'dns_name', 'description',
|
||||
]
|
||||
clone_fields = [
|
||||
'vrf', 'tenant', 'status', 'role', 'description', 'interface',
|
||||
'vrf', 'tenant', 'status', 'role', 'description',
|
||||
]
|
||||
|
||||
STATUS_CLASS_MAP = {
|
||||
@ -707,32 +722,31 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
||||
)
|
||||
})
|
||||
|
||||
if self.pk:
|
||||
|
||||
# Check for primary IP assignment that doesn't match the assigned device/VM
|
||||
device = Device.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
|
||||
if self.pk and type(self.assigned_object) is Interface:
|
||||
device = Device.objects.unrestricted().filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
|
||||
if device:
|
||||
if self.interface is None:
|
||||
if self.assigned_object is None:
|
||||
raise ValidationError({
|
||||
'interface': "IP address is primary for device {} but not assigned".format(device)
|
||||
'interface': f"IP address is primary for device {device} but not assigned to an interface"
|
||||
})
|
||||
elif (device.primary_ip4 == self or device.primary_ip6 == self) and self.interface.device != device:
|
||||
elif self.assigned_object.device != device:
|
||||
raise ValidationError({
|
||||
'interface': "IP address is primary for device {} but assigned to {} ({})".format(
|
||||
device, self.interface.device, self.interface
|
||||
)
|
||||
'interface': f"IP address is primary for device {device} but assigned to "
|
||||
f"{self.assigned_object.device} ({self.assigned_object})"
|
||||
})
|
||||
vm = VirtualMachine.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
|
||||
elif self.pk and type(self.assigned_object) is VMInterface:
|
||||
vm = VirtualMachine.unrestricted().objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
|
||||
if vm:
|
||||
if self.interface is None:
|
||||
if self.assigned_object is None:
|
||||
raise ValidationError({
|
||||
'interface': "IP address is primary for virtual machine {} but not assigned".format(vm)
|
||||
'vminterface': f"IP address is primary for virtual machine {vm} but not assigned to an "
|
||||
f"interface"
|
||||
})
|
||||
elif (vm.primary_ip4 == self or vm.primary_ip6 == self) and self.interface.virtual_machine != vm:
|
||||
elif self.interface.virtual_machine != vm:
|
||||
raise ValidationError({
|
||||
'interface': "IP address is primary for virtual machine {} but assigned to {} ({})".format(
|
||||
vm, self.interface.virtual_machine, self.interface
|
||||
)
|
||||
'vminterface': f"IP address is primary for virtual machine {vm} but assigned to "
|
||||
f"{self.assigned_object.virtual_machine} ({self.assigned_object})"
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@ -743,29 +757,27 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def to_objectchange(self, action):
|
||||
# Annotate the assigned Interface (if any)
|
||||
try:
|
||||
parent_obj = self.interface
|
||||
except ObjectDoesNotExist:
|
||||
parent_obj = None
|
||||
|
||||
# Annotate the assigned object, if any
|
||||
return ObjectChange(
|
||||
changed_object=self,
|
||||
object_repr=str(self),
|
||||
action=action,
|
||||
related_object=parent_obj,
|
||||
related_object=self.assigned_object,
|
||||
object_data=serialize_object(self)
|
||||
)
|
||||
|
||||
def to_csv(self):
|
||||
|
||||
# Determine if this IP is primary for a Device
|
||||
is_primary = False
|
||||
if self.address.version == 4 and getattr(self, 'primary_ip4_for', False):
|
||||
is_primary = True
|
||||
elif self.address.version == 6 and getattr(self, 'primary_ip6_for', False):
|
||||
is_primary = True
|
||||
else:
|
||||
is_primary = False
|
||||
|
||||
obj_type = None
|
||||
if self.assigned_object_type:
|
||||
obj_type = f'{self.assigned_object_type.app_label}.{self.assigned_object_type.model}'
|
||||
|
||||
return (
|
||||
self.address,
|
||||
@ -773,9 +785,8 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
||||
self.tenant.name if self.tenant else None,
|
||||
self.get_status_display(),
|
||||
self.get_role_display(),
|
||||
self.device.identifier if self.device else None,
|
||||
self.virtual_machine.name if self.virtual_machine else None,
|
||||
self.interface.name if self.interface else None,
|
||||
obj_type,
|
||||
self.assigned_object_id,
|
||||
is_primary,
|
||||
self.dns_name,
|
||||
self.description,
|
||||
@ -796,18 +807,6 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
||||
self.address.prefixlen = value
|
||||
mask_length = property(fset=_set_mask_length)
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
if self.interface:
|
||||
return self.interface.device
|
||||
return None
|
||||
|
||||
@property
|
||||
def virtual_machine(self):
|
||||
if self.interface:
|
||||
return self.interface.virtual_machine
|
||||
return None
|
||||
|
||||
def get_status_class(self):
|
||||
return self.STATUS_CLASS_MAP.get(self.status)
|
||||
|
||||
|
@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
|
||||
|
||||
from dcim.models import Interface
|
||||
from tenancy.tables import COL_TENANT
|
||||
from utilities.tables import BaseTable, BooleanColumn, TagColumn, ToggleColumn
|
||||
from utilities.tables import BaseTable, BooleanColumn, ButtonsColumn, TagColumn, ToggleColumn
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
|
||||
RIR_UTILIZATION = """
|
||||
@ -25,15 +25,6 @@ RIR_UTILIZATION = """
|
||||
</div>
|
||||
"""
|
||||
|
||||
RIR_ACTIONS = """
|
||||
<a href="{% url 'ipam:rir_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.ipam.change_rir %}
|
||||
<a href="{% url 'ipam:rir_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% load helpers %}
|
||||
{% if record.pk %}{% utilization_graph record.get_utilization %}{% else %}—{% endif %}
|
||||
@ -47,15 +38,6 @@ ROLE_VLAN_COUNT = """
|
||||
<a href="{% url 'ipam:vlan_list' %}?role={{ record.slug }}">{{ value }}</a>
|
||||
"""
|
||||
|
||||
ROLE_ACTIONS = """
|
||||
<a href="{% url 'ipam:role_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.ipam.change_role %}
|
||||
<a href="{% url 'ipam:role_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
PREFIX_LINK = """
|
||||
{% if record.has_children %}
|
||||
<span class="text-nowrap" style="padding-left: {{ record.depth }}0px "><i class="fa fa-caret-right"></i></a>
|
||||
@ -92,14 +74,6 @@ IPADDRESS_ASSIGN_LINK = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
IPADDRESS_PARENT = """
|
||||
{% if record.interface %}
|
||||
<a href="{{ record.interface.parent.get_absolute_url }}">{{ record.interface.parent }}</a>
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
VRF_LINK = """
|
||||
{% if record.vrf %}
|
||||
<a href="{{ record.vrf.get_absolute_url }}">{{ record.vrf }}</a>
|
||||
@ -144,10 +118,7 @@ VLAN_ROLE_LINK = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
VLANGROUP_ACTIONS = """
|
||||
<a href="{% url 'ipam:vlangroup_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
VLANGROUP_ADD_VLAN = """
|
||||
{% with next_vid=record.get_next_available_vid %}
|
||||
{% if next_vid and perms.ipam.add_vlan %}
|
||||
<a href="{% url 'ipam:vlan_add' %}?site={{ record.site_id }}&group={{ record.pk }}&vid={{ next_vid }}" title="Add VLAN" class="btn btn-xs btn-success">
|
||||
@ -155,9 +126,6 @@ VLANGROUP_ACTIONS = """
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% if perms.ipam.change_vlangroup %}
|
||||
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
VLAN_MEMBER_UNTAGGED = """
|
||||
@ -168,7 +136,7 @@ VLAN_MEMBER_UNTAGGED = """
|
||||
|
||||
VLAN_MEMBER_ACTIONS = """
|
||||
{% if perms.dcim.change_interface %}
|
||||
<a href="{% if record.device %}{% url 'dcim:interface_edit' pk=record.pk %}{% else %}{% url 'virtualization:interface_edit' pk=record.pk %}{% endif %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil"></i></a>
|
||||
<a href="{% if record.device %}{% url 'dcim:interface_edit' pk=record.pk %}{% else %}{% url 'virtualization:vminterface_edit' pk=record.pk %}{% endif %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@ -222,11 +190,7 @@ class RIRTable(BaseTable):
|
||||
aggregate_count = tables.Column(
|
||||
verbose_name='Aggregates'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=RIR_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
actions = ButtonsColumn(RIR, pk_field='slug')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RIR
|
||||
@ -330,11 +294,7 @@ class RoleTable(BaseTable):
|
||||
orderable=False,
|
||||
verbose_name='VLANs'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=ROLE_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
actions = ButtonsColumn(Role, pk_field='slug')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Role
|
||||
@ -431,18 +391,14 @@ class IPAddressTable(BaseTable):
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=TENANT_LINK
|
||||
)
|
||||
parent = tables.TemplateColumn(
|
||||
template_code=IPADDRESS_PARENT,
|
||||
orderable=False
|
||||
)
|
||||
interface = tables.Column(
|
||||
orderable=False
|
||||
assigned = tables.BooleanColumn(
|
||||
accessor='assigned_object_id'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPAddress
|
||||
fields = (
|
||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'dns_name', 'description',
|
||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned', 'dns_name', 'description',
|
||||
)
|
||||
row_attrs = {
|
||||
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
|
||||
@ -465,11 +421,11 @@ class IPAddressDetailTable(IPAddressTable):
|
||||
|
||||
class Meta(IPAddressTable.Meta):
|
||||
fields = (
|
||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'parent', 'interface', 'dns_name',
|
||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'assigned', 'dns_name',
|
||||
'description', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'dns_name', 'description',
|
||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned', 'dns_name', 'description',
|
||||
)
|
||||
|
||||
|
||||
@ -481,17 +437,13 @@ class IPAddressAssignTable(BaseTable):
|
||||
status = tables.TemplateColumn(
|
||||
template_code=STATUS_LABEL
|
||||
)
|
||||
parent = tables.TemplateColumn(
|
||||
template_code=IPADDRESS_PARENT,
|
||||
orderable=False
|
||||
)
|
||||
interface = tables.Column(
|
||||
assigned_object = tables.Column(
|
||||
orderable=False
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPAddress
|
||||
fields = ('address', 'dns_name', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'description')
|
||||
fields = ('address', 'dns_name', 'vrf', 'status', 'role', 'tenant', 'assigned_object', 'description')
|
||||
orderable = False
|
||||
|
||||
|
||||
@ -532,10 +484,9 @@ class VLANGroupTable(BaseTable):
|
||||
vlan_count = tables.Column(
|
||||
verbose_name='VLANs'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=VLANGROUP_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
actions = ButtonsColumn(
|
||||
model=VLANGroup,
|
||||
prepend_template=VLANGROUP_ADD_VLAN
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
|
@ -416,7 +416,7 @@ class VLANTest(APIViewTestCases.APIViewTestCase):
|
||||
"""
|
||||
Attempt and fail to delete a VLAN with a Prefix assigned to it.
|
||||
"""
|
||||
vlan = VLAN.objects.first()
|
||||
vlan = VLAN.objects.unrestricted().first()
|
||||
Prefix.objects.create(prefix=IPNetwork('192.0.2.0/24'), vlan=vlan)
|
||||
|
||||
self.add_permissions('ipam.delete_vlan')
|
||||
|
@ -4,7 +4,7 @@ from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer,
|
||||
from ipam.choices import *
|
||||
from ipam.filters import *
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
from virtualization.models import Cluster, ClusterType, VirtualMachine
|
||||
from virtualization.models import Cluster, ClusterType, VirtualMachine, VMInterface
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
|
||||
|
||||
@ -375,6 +375,13 @@ class IPAddressTestCase(TestCase):
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
interfaces = (
|
||||
Interface(device=devices[0], name='Interface 1'),
|
||||
Interface(device=devices[1], name='Interface 2'),
|
||||
Interface(device=devices[2], name='Interface 3'),
|
||||
)
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
|
||||
clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
|
||||
cluster = Cluster.objects.create(type=clustertype, name='Cluster 1')
|
||||
|
||||
@ -385,15 +392,12 @@ class IPAddressTestCase(TestCase):
|
||||
)
|
||||
VirtualMachine.objects.bulk_create(virtual_machines)
|
||||
|
||||
interfaces = (
|
||||
Interface(device=devices[0], name='Interface 1'),
|
||||
Interface(device=devices[1], name='Interface 2'),
|
||||
Interface(device=devices[2], name='Interface 3'),
|
||||
Interface(virtual_machine=virtual_machines[0], name='Interface 1'),
|
||||
Interface(virtual_machine=virtual_machines[1], name='Interface 2'),
|
||||
Interface(virtual_machine=virtual_machines[2], name='Interface 3'),
|
||||
vminterfaces = (
|
||||
VMInterface(virtual_machine=virtual_machines[0], name='Interface 1'),
|
||||
VMInterface(virtual_machine=virtual_machines[1], name='Interface 2'),
|
||||
VMInterface(virtual_machine=virtual_machines[2], name='Interface 3'),
|
||||
)
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
VMInterface.objects.bulk_create(vminterfaces)
|
||||
|
||||
tenant_groups = (
|
||||
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
|
||||
@ -411,16 +415,16 @@ class IPAddressTestCase(TestCase):
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
ipaddresses = (
|
||||
IPAddress(address='10.0.0.1/24', tenant=None, vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
|
||||
IPAddress(address='10.0.0.2/24', tenant=tenants[0], vrf=vrfs[0], interface=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||
IPAddress(address='10.0.0.3/24', tenant=tenants[1], vrf=vrfs[1], interface=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||
IPAddress(address='10.0.0.4/24', tenant=tenants[2], vrf=vrfs[2], interface=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||
IPAddress(address='10.0.0.1/25', tenant=None, vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||
IPAddress(address='2001:db8::1/64', tenant=None, vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
|
||||
IPAddress(address='2001:db8::2/64', tenant=tenants[0], vrf=vrfs[0], interface=interfaces[3], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||
IPAddress(address='2001:db8::3/64', tenant=tenants[1], vrf=vrfs[1], interface=interfaces[4], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||
IPAddress(address='2001:db8::4/64', tenant=tenants[2], vrf=vrfs[2], interface=interfaces[5], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||
IPAddress(address='2001:db8::1/65', tenant=None, vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||
IPAddress(address='10.0.0.1/24', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
|
||||
IPAddress(address='10.0.0.2/24', tenant=tenants[0], vrf=vrfs[0], assigned_object=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||
IPAddress(address='10.0.0.3/24', tenant=tenants[1], vrf=vrfs[1], assigned_object=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||
IPAddress(address='10.0.0.4/24', tenant=tenants[2], vrf=vrfs[2], assigned_object=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||
IPAddress(address='10.0.0.1/25', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||
IPAddress(address='2001:db8::1/64', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
|
||||
IPAddress(address='2001:db8::2/64', tenant=tenants[0], vrf=vrfs[0], assigned_object=vminterfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||
IPAddress(address='2001:db8::3/64', tenant=tenants[1], vrf=vrfs[1], assigned_object=vminterfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||
IPAddress(address='2001:db8::4/64', tenant=tenants[2], vrf=vrfs[2], assigned_object=vminterfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||
IPAddress(address='2001:db8::1/65', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||
)
|
||||
IPAddress.objects.bulk_create(ipaddresses)
|
||||
|
||||
@ -487,7 +491,14 @@ class IPAddressTestCase(TestCase):
|
||||
params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'interface': ['Interface 1', 'Interface 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_vminterface(self):
|
||||
vminterfaces = VMInterface.objects.all()[:2]
|
||||
params = {'vminterface_id': [vminterfaces[0].pk, vminterfaces[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'vminterface': ['Interface 1', 'Interface 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_assigned_to_interface(self):
|
||||
params = {'assigned_to_interface': 'true'}
|
||||
|
@ -43,7 +43,7 @@ class TestPrefix(TestCase):
|
||||
Prefix(prefix=netaddr.IPNetwork('192.0.2.0/24')),
|
||||
Prefix(prefix=netaddr.IPNetwork('192.0.2.0/24')),
|
||||
))
|
||||
duplicate_prefix_pks = [p.pk for p in prefixes[0].get_duplicates()]
|
||||
duplicate_prefix_pks = [p.pk for p in prefixes[0].get_duplicates().unrestricted()]
|
||||
|
||||
self.assertSetEqual(set(duplicate_prefix_pks), {prefixes[1].pk, prefixes[2].pk})
|
||||
|
||||
@ -227,7 +227,7 @@ class TestIPAddress(TestCase):
|
||||
IPAddress(address=netaddr.IPNetwork('192.0.2.1/24')),
|
||||
IPAddress(address=netaddr.IPNetwork('192.0.2.1/24')),
|
||||
))
|
||||
duplicate_ip_pks = [p.pk for p in ips[0].get_duplicates()]
|
||||
duplicate_ip_pks = [p.pk for p in ips[0].get_duplicates().unrestricted()]
|
||||
|
||||
self.assertSetEqual(set(duplicate_ip_pks), {ips[1].pk, ips[2].pk})
|
||||
|
||||
|
@ -236,7 +236,6 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'tenant': None,
|
||||
'status': IPAddressStatusChoices.STATUS_RESERVED,
|
||||
'role': IPAddressRoleChoices.ROLE_ANYCAST,
|
||||
'interface': None,
|
||||
'nat_inside': None,
|
||||
'dns_name': 'example',
|
||||
'description': 'A new IP address',
|
||||
|
@ -24,6 +24,7 @@ urlpatterns = [
|
||||
path('rirs/import/', views.RIRBulkImportView.as_view(), name='rir_import'),
|
||||
path('rirs/delete/', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
|
||||
path('rirs/<slug:slug>/edit/', views.RIREditView.as_view(), name='rir_edit'),
|
||||
path('rirs/<slug:slug>/delete/', views.RIRDeleteView.as_view(), name='rir_delete'),
|
||||
path('vrfs/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}),
|
||||
|
||||
# Aggregates
|
||||
@ -43,6 +44,7 @@ urlpatterns = [
|
||||
path('roles/import/', views.RoleBulkImportView.as_view(), name='role_import'),
|
||||
path('roles/delete/', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
|
||||
path('roles/<slug:slug>/edit/', views.RoleEditView.as_view(), name='role_edit'),
|
||||
path('roles/<slug:slug>/delete/', views.RoleDeleteView.as_view(), name='role_delete'),
|
||||
path('roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}),
|
||||
|
||||
# Prefixes
|
||||
@ -77,6 +79,7 @@ urlpatterns = [
|
||||
path('vlan-groups/import/', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'),
|
||||
path('vlan-groups/delete/', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
|
||||
path('vlan-groups/<int:pk>/edit/', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
|
||||
path('vlan-groups/<int:pk>/delete/', views.VLANGroupDeleteView.as_view(), name='vlangroup_delete'),
|
||||
path('vlan-groups/<int:pk>/vlans/', views.VLANGroupVLANsView.as_view(), name='vlangroup_vlans'),
|
||||
path('vlan-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}),
|
||||
|
||||
|
93
netbox/ipam/utils.py
Normal file
93
netbox/ipam/utils.py
Normal file
@ -0,0 +1,93 @@
|
||||
import netaddr
|
||||
|
||||
from .constants import *
|
||||
from .models import Prefix, VLAN
|
||||
|
||||
|
||||
def add_available_prefixes(parent, prefix_list):
|
||||
"""
|
||||
Create fake Prefix objects for all unallocated space within a prefix.
|
||||
"""
|
||||
|
||||
# Find all unallocated space
|
||||
available_prefixes = netaddr.IPSet(parent) ^ netaddr.IPSet([p.prefix for p in prefix_list])
|
||||
available_prefixes = [Prefix(prefix=p) for p in available_prefixes.iter_cidrs()]
|
||||
|
||||
# Concatenate and sort complete list of children
|
||||
prefix_list = list(prefix_list) + available_prefixes
|
||||
prefix_list.sort(key=lambda p: p.prefix)
|
||||
|
||||
return prefix_list
|
||||
|
||||
|
||||
def add_available_ipaddresses(prefix, ipaddress_list, is_pool=False):
|
||||
"""
|
||||
Annotate ranges of available IP addresses within a given prefix. If is_pool is True, the first and last IP will be
|
||||
considered usable (regardless of mask length).
|
||||
"""
|
||||
|
||||
output = []
|
||||
prev_ip = None
|
||||
|
||||
# Ignore the network and broadcast addresses for non-pool IPv4 prefixes larger than /31.
|
||||
if prefix.version == 4 and prefix.prefixlen < 31 and not is_pool:
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.first + 1)
|
||||
last_ip_in_prefix = netaddr.IPAddress(prefix.last - 1)
|
||||
else:
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.first)
|
||||
last_ip_in_prefix = netaddr.IPAddress(prefix.last)
|
||||
|
||||
if not ipaddress_list:
|
||||
return [(
|
||||
int(last_ip_in_prefix - first_ip_in_prefix + 1),
|
||||
'{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
|
||||
)]
|
||||
|
||||
# Account for any available IPs before the first real IP
|
||||
if ipaddress_list[0].address.ip > first_ip_in_prefix:
|
||||
skipped_count = int(ipaddress_list[0].address.ip - first_ip_in_prefix)
|
||||
first_skipped = '{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
|
||||
output.append((skipped_count, first_skipped))
|
||||
|
||||
# Iterate through existing IPs and annotate free ranges
|
||||
for ip in ipaddress_list:
|
||||
if prev_ip:
|
||||
diff = int(ip.address.ip - prev_ip.address.ip)
|
||||
if diff > 1:
|
||||
first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
|
||||
output.append((diff - 1, first_skipped))
|
||||
output.append(ip)
|
||||
prev_ip = ip
|
||||
|
||||
# Include any remaining available IPs
|
||||
if prev_ip.address.ip < last_ip_in_prefix:
|
||||
skipped_count = int(last_ip_in_prefix - prev_ip.address.ip)
|
||||
first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
|
||||
output.append((skipped_count, first_skipped))
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def add_available_vlans(vlan_group, vlans):
|
||||
"""
|
||||
Create fake records for all gaps between used VLANs
|
||||
"""
|
||||
if not vlans:
|
||||
return [{'vid': VLAN_VID_MIN, 'available': VLAN_VID_MAX - VLAN_VID_MIN + 1}]
|
||||
|
||||
prev_vid = VLAN_VID_MAX
|
||||
new_vlans = []
|
||||
for vlan in vlans:
|
||||
if vlan.vid - prev_vid > 1:
|
||||
new_vlans.append({'vid': prev_vid + 1, 'available': vlan.vid - prev_vid - 1})
|
||||
prev_vid = vlan.vid
|
||||
|
||||
if vlans[0].vid > VLAN_VID_MIN:
|
||||
new_vlans.append({'vid': VLAN_VID_MIN, 'available': vlans[0].vid - VLAN_VID_MIN})
|
||||
if prev_vid < VLAN_VID_MAX:
|
||||
new_vlans.append({'vid': prev_vid + 1, 'available': VLAN_VID_MAX - prev_vid})
|
||||
|
||||
vlans = list(vlans) + new_vlans
|
||||
vlans.sort(key=lambda v: v.vid if type(v) == VLAN else v['vid'])
|
||||
|
||||
return vlans
|
@ -1,6 +1,6 @@
|
||||
import netaddr
|
||||
from django.conf import settings
|
||||
from django.db.models import Count, Q
|
||||
from django.db.models import Count, Prefetch
|
||||
from django.db.models.expressions import RawSQL
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django_tables2 import RequestConfig
|
||||
@ -11,100 +11,12 @@ from utilities.views import (
|
||||
BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView,
|
||||
ObjectListView,
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
from . import filters, forms, tables
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
|
||||
|
||||
def add_available_prefixes(parent, prefix_list):
|
||||
"""
|
||||
Create fake Prefix objects for all unallocated space within a prefix.
|
||||
"""
|
||||
|
||||
# Find all unallocated space
|
||||
available_prefixes = netaddr.IPSet(parent) ^ netaddr.IPSet([p.prefix for p in prefix_list])
|
||||
available_prefixes = [Prefix(prefix=p) for p in available_prefixes.iter_cidrs()]
|
||||
|
||||
# Concatenate and sort complete list of children
|
||||
prefix_list = list(prefix_list) + available_prefixes
|
||||
prefix_list.sort(key=lambda p: p.prefix)
|
||||
|
||||
return prefix_list
|
||||
|
||||
|
||||
def add_available_ipaddresses(prefix, ipaddress_list, is_pool=False):
|
||||
"""
|
||||
Annotate ranges of available IP addresses within a given prefix. If is_pool is True, the first and last IP will be
|
||||
considered usable (regardless of mask length).
|
||||
"""
|
||||
|
||||
output = []
|
||||
prev_ip = None
|
||||
|
||||
# Ignore the network and broadcast addresses for non-pool IPv4 prefixes larger than /31.
|
||||
if prefix.version == 4 and prefix.prefixlen < 31 and not is_pool:
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.first + 1)
|
||||
last_ip_in_prefix = netaddr.IPAddress(prefix.last - 1)
|
||||
else:
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.first)
|
||||
last_ip_in_prefix = netaddr.IPAddress(prefix.last)
|
||||
|
||||
if not ipaddress_list:
|
||||
return [(
|
||||
int(last_ip_in_prefix - first_ip_in_prefix + 1),
|
||||
'{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
|
||||
)]
|
||||
|
||||
# Account for any available IPs before the first real IP
|
||||
if ipaddress_list[0].address.ip > first_ip_in_prefix:
|
||||
skipped_count = int(ipaddress_list[0].address.ip - first_ip_in_prefix)
|
||||
first_skipped = '{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
|
||||
output.append((skipped_count, first_skipped))
|
||||
|
||||
# Iterate through existing IPs and annotate free ranges
|
||||
for ip in ipaddress_list:
|
||||
if prev_ip:
|
||||
diff = int(ip.address.ip - prev_ip.address.ip)
|
||||
if diff > 1:
|
||||
first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
|
||||
output.append((diff - 1, first_skipped))
|
||||
output.append(ip)
|
||||
prev_ip = ip
|
||||
|
||||
# Include any remaining available IPs
|
||||
if prev_ip.address.ip < last_ip_in_prefix:
|
||||
skipped_count = int(last_ip_in_prefix - prev_ip.address.ip)
|
||||
first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
|
||||
output.append((skipped_count, first_skipped))
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def add_available_vlans(vlan_group, vlans):
|
||||
"""
|
||||
Create fake records for all gaps between used VLANs
|
||||
"""
|
||||
if not vlans:
|
||||
return [{'vid': VLAN_VID_MIN, 'available': VLAN_VID_MAX - VLAN_VID_MIN + 1}]
|
||||
|
||||
prev_vid = VLAN_VID_MAX
|
||||
new_vlans = []
|
||||
for vlan in vlans:
|
||||
if vlan.vid - prev_vid > 1:
|
||||
new_vlans.append({'vid': prev_vid + 1, 'available': vlan.vid - prev_vid - 1})
|
||||
prev_vid = vlan.vid
|
||||
|
||||
if vlans[0].vid > VLAN_VID_MIN:
|
||||
new_vlans.append({'vid': VLAN_VID_MIN, 'available': vlans[0].vid - VLAN_VID_MIN})
|
||||
if prev_vid < VLAN_VID_MAX:
|
||||
new_vlans.append({'vid': prev_vid + 1, 'available': VLAN_VID_MAX - prev_vid})
|
||||
|
||||
vlans = list(vlans) + new_vlans
|
||||
vlans.sort(key=lambda v: v.vid if type(v) == VLAN else v['vid'])
|
||||
|
||||
return vlans
|
||||
from .utils import add_available_ipaddresses, add_available_prefixes, add_available_vlans
|
||||
|
||||
|
||||
#
|
||||
@ -136,19 +48,16 @@ class VRFEditView(ObjectEditView):
|
||||
queryset = VRF.objects.all()
|
||||
model_form = forms.VRFForm
|
||||
template_name = 'ipam/vrf_edit.html'
|
||||
default_return_url = 'ipam:vrf_list'
|
||||
|
||||
|
||||
class VRFDeleteView(ObjectDeleteView):
|
||||
queryset = VRF.objects.all()
|
||||
default_return_url = 'ipam:vrf_list'
|
||||
|
||||
|
||||
class VRFBulkImportView(BulkImportView):
|
||||
queryset = VRF.objects.all()
|
||||
model_form = forms.VRFCSVForm
|
||||
table = tables.VRFTable
|
||||
default_return_url = 'ipam:vrf_list'
|
||||
|
||||
|
||||
class VRFBulkEditView(BulkEditView):
|
||||
@ -156,14 +65,12 @@ class VRFBulkEditView(BulkEditView):
|
||||
filterset = filters.VRFFilterSet
|
||||
table = tables.VRFTable
|
||||
form = forms.VRFBulkEditForm
|
||||
default_return_url = 'ipam:vrf_list'
|
||||
|
||||
|
||||
class VRFBulkDeleteView(BulkDeleteView):
|
||||
queryset = VRF.objects.prefetch_related('tenant')
|
||||
filterset = filters.VRFFilterSet
|
||||
table = tables.VRFTable
|
||||
default_return_url = 'ipam:vrf_list'
|
||||
|
||||
|
||||
#
|
||||
@ -196,10 +103,12 @@ class RIRListView(ObjectListView):
|
||||
'deprecated': 0,
|
||||
'available': 0,
|
||||
}
|
||||
aggregate_list = Aggregate.objects.filter(prefix__family=family, rir=rir)
|
||||
aggregate_list = Aggregate.objects.restrict(request.user).filter(prefix__family=family, rir=rir)
|
||||
for aggregate in aggregate_list:
|
||||
|
||||
queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))
|
||||
queryset = Prefix.objects.restrict(request.user).filter(
|
||||
prefix__net_contained_or_equal=str(aggregate.prefix)
|
||||
)
|
||||
|
||||
# Find all consumed space for each prefix status (we ignore containers for this purpose).
|
||||
active_prefixes = netaddr.cidr_merge(
|
||||
@ -249,21 +158,22 @@ class RIRListView(ObjectListView):
|
||||
class RIREditView(ObjectEditView):
|
||||
queryset = RIR.objects.all()
|
||||
model_form = forms.RIRForm
|
||||
default_return_url = 'ipam:rir_list'
|
||||
|
||||
|
||||
class RIRDeleteView(ObjectDeleteView):
|
||||
queryset = RIR.objects.all()
|
||||
|
||||
|
||||
class RIRBulkImportView(BulkImportView):
|
||||
queryset = RIR.objects.all()
|
||||
model_form = forms.RIRCSVForm
|
||||
table = tables.RIRTable
|
||||
default_return_url = 'ipam:rir_list'
|
||||
|
||||
|
||||
class RIRBulkDeleteView(BulkDeleteView):
|
||||
queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
|
||||
filterset = filters.RIRFilterSet
|
||||
table = tables.RIRTable
|
||||
default_return_url = 'ipam:rir_list'
|
||||
|
||||
|
||||
#
|
||||
@ -345,19 +255,16 @@ class AggregateEditView(ObjectEditView):
|
||||
queryset = Aggregate.objects.all()
|
||||
model_form = forms.AggregateForm
|
||||
template_name = 'ipam/aggregate_edit.html'
|
||||
default_return_url = 'ipam:aggregate_list'
|
||||
|
||||
|
||||
class AggregateDeleteView(ObjectDeleteView):
|
||||
queryset = Aggregate.objects.all()
|
||||
default_return_url = 'ipam:aggregate_list'
|
||||
|
||||
|
||||
class AggregateBulkImportView(BulkImportView):
|
||||
queryset = Aggregate.objects.all()
|
||||
model_form = forms.AggregateCSVForm
|
||||
table = tables.AggregateTable
|
||||
default_return_url = 'ipam:aggregate_list'
|
||||
|
||||
|
||||
class AggregateBulkEditView(BulkEditView):
|
||||
@ -365,14 +272,12 @@ class AggregateBulkEditView(BulkEditView):
|
||||
filterset = filters.AggregateFilterSet
|
||||
table = tables.AggregateTable
|
||||
form = forms.AggregateBulkEditForm
|
||||
default_return_url = 'ipam:aggregate_list'
|
||||
|
||||
|
||||
class AggregateBulkDeleteView(BulkDeleteView):
|
||||
queryset = Aggregate.objects.prefetch_related('rir')
|
||||
filterset = filters.AggregateFilterSet
|
||||
table = tables.AggregateTable
|
||||
default_return_url = 'ipam:aggregate_list'
|
||||
|
||||
|
||||
#
|
||||
@ -387,20 +292,21 @@ class RoleListView(ObjectListView):
|
||||
class RoleEditView(ObjectEditView):
|
||||
queryset = Role.objects.all()
|
||||
model_form = forms.RoleForm
|
||||
default_return_url = 'ipam:role_list'
|
||||
|
||||
|
||||
class RoleDeleteView(ObjectDeleteView):
|
||||
queryset = Role.objects.all()
|
||||
|
||||
|
||||
class RoleBulkImportView(BulkImportView):
|
||||
queryset = Role.objects.all()
|
||||
model_form = forms.RoleCSVForm
|
||||
table = tables.RoleTable
|
||||
default_return_url = 'ipam:role_list'
|
||||
|
||||
|
||||
class RoleBulkDeleteView(BulkDeleteView):
|
||||
queryset = Role.objects.all()
|
||||
table = tables.RoleTable
|
||||
default_return_url = 'ipam:role_list'
|
||||
|
||||
|
||||
#
|
||||
@ -517,7 +423,7 @@ class PrefixIPAddressesView(ObjectView):
|
||||
|
||||
# Find all IPAddresses belonging to this Prefix
|
||||
ipaddresses = prefix.get_child_ips().restrict(request.user, 'view').prefetch_related(
|
||||
'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for'
|
||||
'vrf', 'primary_ip4_for', 'primary_ip6_for'
|
||||
)
|
||||
|
||||
# Add available IP addresses to the table if requested
|
||||
@ -556,20 +462,17 @@ class PrefixEditView(ObjectEditView):
|
||||
queryset = Prefix.objects.all()
|
||||
model_form = forms.PrefixForm
|
||||
template_name = 'ipam/prefix_edit.html'
|
||||
default_return_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
class PrefixDeleteView(ObjectDeleteView):
|
||||
queryset = Prefix.objects.all()
|
||||
template_name = 'ipam/prefix_delete.html'
|
||||
default_return_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
class PrefixBulkImportView(BulkImportView):
|
||||
queryset = Prefix.objects.all()
|
||||
model_form = forms.PrefixCSVForm
|
||||
table = tables.PrefixTable
|
||||
default_return_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
class PrefixBulkEditView(BulkEditView):
|
||||
@ -577,14 +480,12 @@ class PrefixBulkEditView(BulkEditView):
|
||||
filterset = filters.PrefixFilterSet
|
||||
table = tables.PrefixTable
|
||||
form = forms.PrefixBulkEditForm
|
||||
default_return_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
class PrefixBulkDeleteView(BulkDeleteView):
|
||||
queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
||||
filterset = filters.PrefixFilterSet
|
||||
table = tables.PrefixTable
|
||||
default_return_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
#
|
||||
@ -593,7 +494,7 @@ class PrefixBulkDeleteView(BulkDeleteView):
|
||||
|
||||
class IPAddressListView(ObjectListView):
|
||||
queryset = IPAddress.objects.prefetch_related(
|
||||
'vrf__tenant', 'tenant', 'nat_inside', 'interface__device', 'interface__virtual_machine'
|
||||
'vrf__tenant', 'tenant', 'nat_inside'
|
||||
)
|
||||
filterset = filters.IPAddressFilterSet
|
||||
filterset_form = forms.IPAddressFilterForm
|
||||
@ -622,7 +523,7 @@ class IPAddressView(ObjectView):
|
||||
).exclude(
|
||||
pk=ipaddress.pk
|
||||
).prefetch_related(
|
||||
'nat_inside', 'interface__device'
|
||||
'nat_inside'
|
||||
)
|
||||
# Exclude anycast IPs if this IP is anycast
|
||||
if ipaddress.role == IPAddressRoleChoices.ROLE_ANYCAST:
|
||||
@ -630,9 +531,7 @@ class IPAddressView(ObjectView):
|
||||
duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False)
|
||||
|
||||
# Related IP table
|
||||
related_ips = IPAddress.objects.restrict(request.user, 'view').prefetch_related(
|
||||
'interface__device'
|
||||
).exclude(
|
||||
related_ips = IPAddress.objects.restrict(request.user, 'view').exclude(
|
||||
address=str(ipaddress.address)
|
||||
).filter(
|
||||
vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)
|
||||
@ -657,17 +556,21 @@ class IPAddressEditView(ObjectEditView):
|
||||
queryset = IPAddress.objects.all()
|
||||
model_form = forms.IPAddressForm
|
||||
template_name = 'ipam/ipaddress_edit.html'
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||
|
||||
interface_id = request.GET.get('interface')
|
||||
if interface_id:
|
||||
if 'interface' in request.GET:
|
||||
try:
|
||||
obj.interface = Interface.objects.get(pk=interface_id)
|
||||
obj.assigned_object = Interface.objects.get(pk=request.GET['interface'])
|
||||
except (ValueError, Interface.DoesNotExist):
|
||||
pass
|
||||
|
||||
elif 'vminterface' in request.GET:
|
||||
try:
|
||||
obj.assigned_object = VMInterface.objects.get(pk=request.GET['vminterface'])
|
||||
except (ValueError, VMInterface.DoesNotExist):
|
||||
pass
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
@ -699,9 +602,7 @@ class IPAddressAssignView(ObjectView):
|
||||
|
||||
if form.is_valid():
|
||||
|
||||
addresses = self.queryset.prefetch_related(
|
||||
'vrf', 'tenant', 'interface__device', 'interface__virtual_machine'
|
||||
)
|
||||
addresses = self.queryset.prefetch_related('vrf', 'tenant')
|
||||
# Limit to 100 results
|
||||
addresses = filters.IPAddressFilterSet(request.POST, addresses).qs[:100]
|
||||
table = tables.IPAddressAssignTable(addresses)
|
||||
@ -715,37 +616,33 @@ class IPAddressAssignView(ObjectView):
|
||||
|
||||
class IPAddressDeleteView(ObjectDeleteView):
|
||||
queryset = IPAddress.objects.all()
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
class IPAddressBulkCreateView(BulkCreateView):
|
||||
queryset = IPAddress.objects.all()
|
||||
form = forms.IPAddressBulkCreateForm
|
||||
model_form = forms.IPAddressBulkAddForm
|
||||
pattern_target = 'address'
|
||||
template_name = 'ipam/ipaddress_bulk_add.html'
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
class IPAddressBulkImportView(BulkImportView):
|
||||
queryset = IPAddress.objects.all()
|
||||
model_form = forms.IPAddressCSVForm
|
||||
table = tables.IPAddressTable
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
class IPAddressBulkEditView(BulkEditView):
|
||||
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant').prefetch_related('interface__device')
|
||||
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
|
||||
filterset = filters.IPAddressFilterSet
|
||||
table = tables.IPAddressTable
|
||||
form = forms.IPAddressBulkEditForm
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
class IPAddressBulkDeleteView(BulkDeleteView):
|
||||
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant').prefetch_related('interface__device')
|
||||
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
|
||||
filterset = filters.IPAddressFilterSet
|
||||
table = tables.IPAddressTable
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
#
|
||||
@ -762,21 +659,22 @@ class VLANGroupListView(ObjectListView):
|
||||
class VLANGroupEditView(ObjectEditView):
|
||||
queryset = VLANGroup.objects.all()
|
||||
model_form = forms.VLANGroupForm
|
||||
default_return_url = 'ipam:vlangroup_list'
|
||||
|
||||
|
||||
class VLANGroupDeleteView(ObjectDeleteView):
|
||||
queryset = VLANGroup.objects.all()
|
||||
|
||||
|
||||
class VLANGroupBulkImportView(BulkImportView):
|
||||
queryset = VLANGroup.objects.all()
|
||||
model_form = forms.VLANGroupCSVForm
|
||||
table = tables.VLANGroupTable
|
||||
default_return_url = 'ipam:vlangroup_list'
|
||||
|
||||
|
||||
class VLANGroupBulkDeleteView(BulkDeleteView):
|
||||
queryset = VLANGroup.objects.prefetch_related('site').annotate(vlan_count=Count('vlans'))
|
||||
filterset = filters.VLANGroupFilterSet
|
||||
table = tables.VLANGroupTable
|
||||
default_return_url = 'ipam:vlangroup_list'
|
||||
|
||||
|
||||
class VLANGroupVLANsView(ObjectView):
|
||||
@ -785,7 +683,9 @@ class VLANGroupVLANsView(ObjectView):
|
||||
def get(self, request, pk):
|
||||
vlan_group = get_object_or_404(self.queryset, pk=pk)
|
||||
|
||||
vlans = VLAN.objects.restrict(request.user, 'view').filter(group_id=pk)
|
||||
vlans = VLAN.objects.restrict(request.user, 'view').filter(group_id=pk).prefetch_related(
|
||||
Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user))
|
||||
)
|
||||
vlans = add_available_vlans(vlan_group, vlans)
|
||||
|
||||
vlan_table = tables.VLANDetailTable(vlans)
|
||||
@ -871,19 +771,16 @@ class VLANEditView(ObjectEditView):
|
||||
queryset = VLAN.objects.all()
|
||||
model_form = forms.VLANForm
|
||||
template_name = 'ipam/vlan_edit.html'
|
||||
default_return_url = 'ipam:vlan_list'
|
||||
|
||||
|
||||
class VLANDeleteView(ObjectDeleteView):
|
||||
queryset = VLAN.objects.all()
|
||||
default_return_url = 'ipam:vlan_list'
|
||||
|
||||
|
||||
class VLANBulkImportView(BulkImportView):
|
||||
queryset = VLAN.objects.all()
|
||||
model_form = forms.VLANCSVForm
|
||||
table = tables.VLANTable
|
||||
default_return_url = 'ipam:vlan_list'
|
||||
|
||||
|
||||
class VLANBulkEditView(BulkEditView):
|
||||
@ -891,14 +788,12 @@ class VLANBulkEditView(BulkEditView):
|
||||
filterset = filters.VLANFilterSet
|
||||
table = tables.VLANTable
|
||||
form = forms.VLANBulkEditForm
|
||||
default_return_url = 'ipam:vlan_list'
|
||||
|
||||
|
||||
class VLANBulkDeleteView(BulkDeleteView):
|
||||
queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role')
|
||||
filterset = filters.VLANFilterSet
|
||||
table = tables.VLANTable
|
||||
default_return_url = 'ipam:vlan_list'
|
||||
|
||||
|
||||
#
|
||||
@ -945,7 +840,6 @@ class ServiceBulkImportView(BulkImportView):
|
||||
queryset = Service.objects.all()
|
||||
model_form = forms.ServiceCSVForm
|
||||
table = tables.ServiceTable
|
||||
default_return_url = 'ipam:service_list'
|
||||
|
||||
|
||||
class ServiceDeleteView(ObjectDeleteView):
|
||||
@ -957,11 +851,9 @@ class ServiceBulkEditView(BulkEditView):
|
||||
filterset = filters.ServiceFilterSet
|
||||
table = tables.ServiceTable
|
||||
form = forms.ServiceBulkEditForm
|
||||
default_return_url = 'ipam:service_list'
|
||||
|
||||
|
||||
class ServiceBulkDeleteView(BulkDeleteView):
|
||||
queryset = Service.objects.prefetch_related('device', 'virtual_machine')
|
||||
filterset = filters.ServiceFilterSet
|
||||
table = tables.ServiceTable
|
||||
default_return_url = 'ipam:service_list'
|
||||
|
@ -24,7 +24,7 @@ class ObjectPermissionBackend(ModelBackend):
|
||||
Return all permissions granted to the user by an ObjectPermission.
|
||||
"""
|
||||
# Retrieve all assigned ObjectPermissions
|
||||
object_permissions = ObjectPermission.objects.filter(
|
||||
object_permissions = ObjectPermission.objects.unrestricted().filter(
|
||||
Q(users=user_obj) |
|
||||
Q(groups__user=user_obj)
|
||||
).prefetch_related('object_types')
|
||||
|
@ -208,6 +208,10 @@ PLUGINS = []
|
||||
# prefer IPv4 instead.
|
||||
PREFER_IPV4 = False
|
||||
|
||||
# Rack elevation size defaults, in pixels. For best results, the ratio of width to height should be roughly 10:1.
|
||||
RACK_ELEVATION_DEFAULT_UNIT_HEIGHT = 22
|
||||
RACK_ELEVATION_DEFAULT_UNIT_WIDTH = 220
|
||||
|
||||
# Remote authentication support
|
||||
REMOTE_AUTH_ENABLED = False
|
||||
REMOTE_AUTH_BACKEND = 'netbox.authentication.RemoteUserBackend'
|
||||
|
@ -99,6 +99,8 @@ PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
|
||||
PLUGINS = getattr(configuration, 'PLUGINS', [])
|
||||
PLUGINS_CONFIG = getattr(configuration, 'PLUGINS_CONFIG', {})
|
||||
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
|
||||
RACK_ELEVATION_DEFAULT_UNIT_HEIGHT = getattr(configuration, 'RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 22)
|
||||
RACK_ELEVATION_DEFAULT_UNIT_WIDTH = getattr(configuration, 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH', 220)
|
||||
REMOTE_AUTH_AUTO_CREATE_USER = getattr(configuration, 'REMOTE_AUTH_AUTO_CREATE_USER', False)
|
||||
REMOTE_AUTH_BACKEND = getattr(configuration, 'REMOTE_AUTH_BACKEND', 'netbox.authentication.RemoteUserBackend')
|
||||
REMOTE_AUTH_DEFAULT_GROUPS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_GROUPS', [])
|
||||
|
@ -168,356 +168,6 @@ class ExternalAuthenticationTestCase(TestCase):
|
||||
self.assertTrue(new_user.has_perms(['dcim.add_site', 'dcim.change_site']))
|
||||
|
||||
|
||||
class ObjectPermissionViewTestCase(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
cls.sites = (
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
Site(name='Site 2', slug='site-2'),
|
||||
Site(name='Site 3', slug='site-3'),
|
||||
)
|
||||
Site.objects.bulk_create(cls.sites)
|
||||
|
||||
cls.prefixes = (
|
||||
Prefix(prefix=IPNetwork('10.0.0.0/24'), site=cls.sites[0]),
|
||||
Prefix(prefix=IPNetwork('10.0.1.0/24'), site=cls.sites[0]),
|
||||
Prefix(prefix=IPNetwork('10.0.2.0/24'), site=cls.sites[0]),
|
||||
Prefix(prefix=IPNetwork('10.0.3.0/24'), site=cls.sites[1]),
|
||||
Prefix(prefix=IPNetwork('10.0.4.0/24'), site=cls.sites[1]),
|
||||
Prefix(prefix=IPNetwork('10.0.5.0/24'), site=cls.sites[1]),
|
||||
Prefix(prefix=IPNetwork('10.0.6.0/24'), site=cls.sites[2]),
|
||||
Prefix(prefix=IPNetwork('10.0.7.0/24'), site=cls.sites[2]),
|
||||
Prefix(prefix=IPNetwork('10.0.8.0/24'), site=cls.sites[2]),
|
||||
)
|
||||
Prefix.objects.bulk_create(cls.prefixes)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
||||
def test_get_object(self):
|
||||
|
||||
# Attempt to retrieve object without permission
|
||||
response = self.client.get(self.prefixes[0].get_absolute_url())
|
||||
self.assertHttpStatus(response, 403)
|
||||
|
||||
# Assign object permission
|
||||
obj_perm = ObjectPermission(
|
||||
constraints={'site__name': 'Site 1'},
|
||||
actions=['view']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix))
|
||||
|
||||
# Retrieve permitted object
|
||||
response = self.client.get(self.prefixes[0].get_absolute_url())
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
# Attempt to retrieve non-permitted object
|
||||
response = self.client.get(self.prefixes[3].get_absolute_url())
|
||||
self.assertHttpStatus(response, 404)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
||||
def test_list_objects(self):
|
||||
|
||||
# Attempt to list objects without permission
|
||||
response = self.client.get(reverse('ipam:prefix_list'))
|
||||
self.assertHttpStatus(response, 403)
|
||||
|
||||
# Assign object permission
|
||||
obj_perm = ObjectPermission(
|
||||
constraints={'site__name': 'Site 1'},
|
||||
actions=['view']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix))
|
||||
|
||||
# Retrieve all objects. Only permitted objects should be returned.
|
||||
response = self.client.get(reverse('ipam:prefix_list'))
|
||||
self.assertHttpStatus(response, 200)
|
||||
self.assertIn(str(self.prefixes[0].prefix), str(response.content))
|
||||
self.assertNotIn(str(self.prefixes[3].prefix), str(response.content))
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
||||
def test_create_object(self):
|
||||
initial_count = Prefix.objects.count()
|
||||
form_data = {
|
||||
'prefix': '10.0.9.0/24',
|
||||
'site': self.sites[1].pk,
|
||||
'status': PrefixStatusChoices.STATUS_ACTIVE,
|
||||
}
|
||||
|
||||
# Attempt to create an object without permission
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_add'),
|
||||
'data': form_data,
|
||||
'follow': False, # Do not follow 302 redirects
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 403)
|
||||
self.assertEqual(initial_count, Prefix.objects.count())
|
||||
|
||||
# Assign object permission
|
||||
obj_perm = ObjectPermission(
|
||||
constraints={'site__name': 'Site 1'},
|
||||
actions=['view', 'add']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix))
|
||||
|
||||
# Attempt to create a non-permitted object
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_add'),
|
||||
'data': form_data,
|
||||
'follow': True, # Follow 302 redirects
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 200)
|
||||
self.assertEqual(Prefix.objects.count(), initial_count)
|
||||
|
||||
# Create a permitted object
|
||||
form_data['site'] = self.sites[0].pk
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_add'),
|
||||
'data': form_data,
|
||||
'follow': True, # Follow 302 redirects
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 200)
|
||||
self.assertEqual(Prefix.objects.count(), initial_count + 1)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
||||
def test_edit_object(self):
|
||||
form_data = {
|
||||
'prefix': '10.0.9.0/24',
|
||||
'site': self.sites[0].pk,
|
||||
'status': PrefixStatusChoices.STATUS_RESERVED,
|
||||
}
|
||||
|
||||
# Attempt to edit an object without permission
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_edit', kwargs={'pk': self.prefixes[0].pk}),
|
||||
'data': form_data,
|
||||
'follow': False, # Do not follow 302 redirects
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 403)
|
||||
|
||||
# Assign object permission
|
||||
obj_perm = ObjectPermission(
|
||||
constraints={'site__name': 'Site 1'},
|
||||
actions=['view', 'change']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix))
|
||||
|
||||
# Attempt to edit a non-permitted object
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_edit', kwargs={'pk': self.prefixes[3].pk}),
|
||||
'data': form_data,
|
||||
'follow': True, # Follow 302 redirects
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 404)
|
||||
|
||||
# Edit a permitted object
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_edit', kwargs={'pk': self.prefixes[0].pk}),
|
||||
'data': form_data,
|
||||
'follow': True, # Follow 302 redirects
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 200)
|
||||
prefix = Prefix.objects.get(pk=self.prefixes[0].pk)
|
||||
self.assertEqual(prefix.status, PrefixStatusChoices.STATUS_RESERVED)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
||||
def test_delete_object(self):
|
||||
form_data = {
|
||||
'confirm': True
|
||||
}
|
||||
|
||||
# Attempt to delete object without permission
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_delete', kwargs={'pk': self.prefixes[0].pk}),
|
||||
'data': form_data,
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 403)
|
||||
|
||||
# Assign object permission
|
||||
obj_perm = ObjectPermission(
|
||||
constraints={'site__name': 'Site 1'},
|
||||
actions=['view', 'delete']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix))
|
||||
|
||||
# Delete permitted object
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_delete', kwargs={'pk': self.prefixes[0].pk}),
|
||||
'data': form_data,
|
||||
'follow': True, # Follow 302 redirects
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 200)
|
||||
self.assertFalse(Prefix.objects.filter(pk=self.prefixes[0].pk).exists())
|
||||
|
||||
# Attempt to delete non-permitted object
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_delete', kwargs={'pk': self.prefixes[3].pk}),
|
||||
'data': form_data,
|
||||
'follow': True, # Follow 302 redirects
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 404)
|
||||
self.assertTrue(Prefix.objects.filter(pk=self.prefixes[3].pk).exists())
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
||||
def test_bulk_import_objects(self):
|
||||
initial_count = Prefix.objects.count()
|
||||
form_data = {
|
||||
'csv': "prefix,status,site\n"
|
||||
"10.0.9.0/24,Active,Site 1\n"
|
||||
"10.0.10.0/24,Active,Site 2\n"
|
||||
"10.0.11.0/24,Active,Site 3\n",
|
||||
}
|
||||
|
||||
# Attempt to import objects without permission
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_import'),
|
||||
'data': form_data,
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 403)
|
||||
self.assertEqual(initial_count, Prefix.objects.count())
|
||||
|
||||
# Assign object permission
|
||||
obj_perm = ObjectPermission(
|
||||
constraints={'site__name': 'Site 1'},
|
||||
actions=['add']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix))
|
||||
|
||||
# Attempt to create non-permitted objects
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_import'),
|
||||
'data': form_data,
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 200)
|
||||
self.assertEqual(Prefix.objects.count(), initial_count)
|
||||
|
||||
# Create a permitted object
|
||||
form_data = {
|
||||
'csv': "prefix,status,site\n"
|
||||
"10.0.9.0/24,Active,Site 1\n"
|
||||
"10.0.10.0/24,Active,Site 1\n"
|
||||
"10.0.11.0/24,Active,Site 1\n",
|
||||
}
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_import'),
|
||||
'data': form_data,
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 200)
|
||||
self.assertEqual(Prefix.objects.count(), initial_count + 3)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
||||
def test_bulk_edit_objects(self):
|
||||
form_data = {
|
||||
'pk': [p.pk for p in self.prefixes],
|
||||
'status': 'reserved',
|
||||
'_apply': True,
|
||||
}
|
||||
|
||||
# Attempt to edit objects without permission
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_bulk_edit'),
|
||||
'data': form_data,
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 403)
|
||||
|
||||
# Assign object permission
|
||||
obj_perm = ObjectPermission(
|
||||
constraints={'site__name': 'Site 1'},
|
||||
actions=['change']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix))
|
||||
|
||||
# Attempt to edit non-permitted objects
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_bulk_edit'),
|
||||
'data': form_data,
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 302)
|
||||
self.assertEqual(Prefix.objects.get(pk=self.prefixes[3].pk).status, 'active')
|
||||
|
||||
# Edit permitted objects
|
||||
form_data['pk'] = [p.pk for p in self.prefixes[:3]]
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_bulk_edit'),
|
||||
'data': form_data,
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 302)
|
||||
self.assertEqual(Prefix.objects.get(pk=self.prefixes[0].pk).status, 'reserved')
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
||||
def test_bulk_delete_objects(self):
|
||||
form_data = {
|
||||
'pk': [p.pk for p in self.prefixes],
|
||||
'confirm': True,
|
||||
'_confirm': True,
|
||||
}
|
||||
|
||||
# Attempt to delete objects without permission
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_bulk_delete'),
|
||||
'data': form_data,
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 403)
|
||||
|
||||
# Assign object permission
|
||||
obj_perm = ObjectPermission(
|
||||
constraints={'site__name': 'Site 1'},
|
||||
actions=['view', 'delete']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix))
|
||||
|
||||
# Attempt to delete non-permitted object
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_bulk_delete'),
|
||||
'data': form_data,
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 200)
|
||||
self.assertTrue(Prefix.objects.filter(pk=self.prefixes[3].pk).exists())
|
||||
|
||||
# Delete permitted objects
|
||||
form_data['pk'] = [p.pk for p in self.prefixes[:3]]
|
||||
request = {
|
||||
'path': reverse('ipam:prefix_bulk_delete'),
|
||||
'data': form_data,
|
||||
}
|
||||
response = self.client.post(**request)
|
||||
self.assertHttpStatus(response, 302)
|
||||
self.assertFalse(Prefix.objects.filter(pk=self.prefixes[0].pk).exists())
|
||||
|
||||
|
||||
class ObjectPermissionAPIViewTestCase(TestCase):
|
||||
client_class = APIClient
|
||||
|
||||
|
@ -183,13 +183,6 @@ nav ul.pagination {
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
/* Racks */
|
||||
div.rack_header {
|
||||
margin-left: 32px;
|
||||
text-align: center;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
/* Devices */
|
||||
table.component-list td.subtable {
|
||||
padding: 0;
|
||||
|
@ -1,17 +1,8 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from utilities.tables import BaseTable, TagColumn, ToggleColumn
|
||||
from utilities.tables import BaseTable, ButtonsColumn, TagColumn, ToggleColumn
|
||||
from .models import SecretRole, Secret
|
||||
|
||||
SECRETROLE_ACTIONS = """
|
||||
<a href="{% url 'secrets:secretrole_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.secrets.change_secretrole %}
|
||||
<a href="{% url 'secrets:secretrole_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
|
||||
#
|
||||
# Secret roles
|
||||
@ -23,11 +14,7 @@ class SecretRoleTable(BaseTable):
|
||||
secret_count = tables.Column(
|
||||
verbose_name='Secrets'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=SECRETROLE_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
actions = ButtonsColumn(SecretRole, pk_field='slug')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = SecretRole
|
||||
|
@ -13,6 +13,7 @@ urlpatterns = [
|
||||
path('secret-roles/import/', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'),
|
||||
path('secret-roles/delete/', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'),
|
||||
path('secret-roles/<slug:slug>/edit/', views.SecretRoleEditView.as_view(), name='secretrole_edit'),
|
||||
path('secret-roles/<slug:slug>/delete/', views.SecretRoleDeleteView.as_view(), name='secretrole_delete'),
|
||||
path('secret-roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}),
|
||||
|
||||
# Secrets
|
||||
|
@ -36,20 +36,21 @@ class SecretRoleListView(ObjectListView):
|
||||
class SecretRoleEditView(ObjectEditView):
|
||||
queryset = SecretRole.objects.all()
|
||||
model_form = forms.SecretRoleForm
|
||||
default_return_url = 'secrets:secretrole_list'
|
||||
|
||||
|
||||
class SecretRoleDeleteView(ObjectDeleteView):
|
||||
queryset = SecretRole.objects.all()
|
||||
|
||||
|
||||
class SecretRoleBulkImportView(BulkImportView):
|
||||
queryset = SecretRole.objects.all()
|
||||
model_form = forms.SecretRoleCSVForm
|
||||
table = tables.SecretRoleTable
|
||||
default_return_url = 'secrets:secretrole_list'
|
||||
|
||||
|
||||
class SecretRoleBulkDeleteView(BulkDeleteView):
|
||||
queryset = SecretRole.objects.annotate(secret_count=Count('secrets'))
|
||||
table = tables.SecretRoleTable
|
||||
default_return_url = 'secrets:secretrole_list'
|
||||
|
||||
|
||||
#
|
||||
@ -147,7 +148,6 @@ class SecretEditView(ObjectEditView):
|
||||
|
||||
class SecretDeleteView(ObjectDeleteView):
|
||||
queryset = Secret.objects.all()
|
||||
default_return_url = 'secrets:secret_list'
|
||||
|
||||
|
||||
class SecretBulkImportView(BulkImportView):
|
||||
@ -155,7 +155,6 @@ class SecretBulkImportView(BulkImportView):
|
||||
model_form = forms.SecretCSVForm
|
||||
table = tables.SecretTable
|
||||
template_name = 'secrets/secret_import.html'
|
||||
default_return_url = 'secrets:secret_list'
|
||||
widget_attrs = {'class': 'requires-session-key'}
|
||||
|
||||
master_key = None
|
||||
@ -203,11 +202,9 @@ class SecretBulkEditView(BulkEditView):
|
||||
filterset = filters.SecretFilterSet
|
||||
table = tables.SecretTable
|
||||
form = forms.SecretBulkEditForm
|
||||
default_return_url = 'secrets:secret_list'
|
||||
|
||||
|
||||
class SecretBulkDeleteView(BulkDeleteView):
|
||||
queryset = Secret.objects.prefetch_related('role', 'device')
|
||||
filterset = filters.SecretFilterSet
|
||||
table = tables.SecretTable
|
||||
default_return_url = 'secrets:secret_list'
|
||||
|
@ -88,6 +88,16 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% elif position_stack %}
|
||||
<div class="col-md-11 col-md-offset-1">
|
||||
<h3 class="text-warning text-center">
|
||||
{% with last_position=position_stack|last %}
|
||||
Trace completed, but there is no Front Port corresponding to
|
||||
<a href="{{ last_position.device.get_absolute_url }}">{{ last_position.device }}</a> {{ last_position }}.<br>
|
||||
Therefore no end-to-end connection can be established.
|
||||
{% endwith %}
|
||||
</h3>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-md-11 col-md-offset-1">
|
||||
<h3 class="text-success text-center">Trace completed!</h3>
|
||||
|
103
netbox/templates/dcim/consoleport.html
Normal file
103
netbox/templates/dcim/consoleport.html
Normal file
@ -0,0 +1,103 @@
|
||||
{% extends 'dcim/device_component.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Console Port</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ instance.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
<td>{{ instance.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ instance.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
|
||||
{% plugin_left_page instance %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Connection</strong>
|
||||
</div>
|
||||
{% if instance.cable %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
{% if instance.connected_endpoint %}
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.connected_endpoint.device.get_absolute_url }}">{{ instance.connected_endpoint.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<a href="{{ instance.connected_endpoint.get_absolute_url }}">{{ instance.connected_endpoint.name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ instance.connected_endpoint.get_type_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.connected_endpoint.description|placeholder }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>Cable</td>
|
||||
<td>
|
||||
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
|
||||
<a href="{% url 'dcim:consoleport_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>
|
||||
{% if instance.connection_status %}
|
||||
<span class="label label-success">{{ instance.get_connection_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="label label-info">{{ instance.get_connection_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
Not connected
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_right_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% plugin_full_width_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
103
netbox/templates/dcim/consoleserverport.html
Normal file
103
netbox/templates/dcim/consoleserverport.html
Normal file
@ -0,0 +1,103 @@
|
||||
{% extends 'dcim/device_component.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Console Server Port</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ instance.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
<td>{{ instance.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ instance.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
|
||||
{% plugin_left_page instance %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Connection</strong>
|
||||
</div>
|
||||
{% if instance.cable %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
{% if instance.connected_endpoint %}
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.connected_endpoint.device.get_absolute_url }}">{{ instance.connected_endpoint.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<a href="{{ instance.connected_endpoint.get_absolute_url }}">{{ instance.connected_endpoint.name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ instance.connected_endpoint.get_type_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.connected_endpoint.description|placeholder }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>Cable</td>
|
||||
<td>
|
||||
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
|
||||
<a href="{% url 'dcim:consoleserverport_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>
|
||||
{% if instance.connection_status %}
|
||||
<span class="label label-success">{{ instance.get_connection_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="label label-info">{{ instance.get_connection_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
Not connected
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_right_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% plugin_full_width_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -74,6 +74,9 @@
|
||||
{% if perms.dcim.add_devicebay %}
|
||||
<li><a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Device Bays</a></li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_inventoryitem %}
|
||||
<li><a href="{% url 'dcim:inventoryitem_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Inventory Items</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -326,34 +329,85 @@
|
||||
{% plugin_left_page device %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% if console_ports or power_ports %}
|
||||
{% if console_ports %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Console / Power</strong>
|
||||
<strong>Console Ports</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body component-list">
|
||||
{% for cp in console_ports %}
|
||||
{% include 'dcim/inc/consoleport.html' %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="panel-footer noprint">
|
||||
{% if console_ports and perms.dcim.change_consoleport %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:consoleport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||
</button>
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if console_ports and perms.dcim.delete_consoleport %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if console_ports and perms.dcim.add_consoleport %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if power_ports %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Power Ports</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body component-list">
|
||||
{% for pp in power_ports %}
|
||||
{% include 'dcim/inc/powerport.html' %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
|
||||
<div class="panel-footer text-right noprint">
|
||||
{% if perms.dcim.add_consoleport %}
|
||||
<a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
|
||||
</a>
|
||||
<div class="panel-footer noprint">
|
||||
{% if power_ports and perms.dcim.change_powerport %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:powerport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:powerport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||
</button>
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:powerport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_powerport %}
|
||||
{% if power_ports and perms.dcim.delete_powerport %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'dcim:powerport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if power_ports and perms.dcim.add_powerport %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'dcim:powerport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if power_ports and poweroutlets %}
|
||||
<div class="panel panel-default">
|
||||
@ -501,10 +555,8 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% if device_bays or device.device_type.is_parent_device %}
|
||||
{% if perms.dcim.delete_devicebay %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Device Bays</strong>
|
||||
@ -553,16 +605,11 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.dcim.delete_devicebay %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if interfaces %}
|
||||
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="device" value="{{ device.pk }}" />
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Interfaces</strong>
|
||||
@ -572,7 +619,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-2 pull-right noprint">
|
||||
<input class="form-control interface-filter" type="text" placeholder="Filter" title="RegEx-enabled" style="height: 23px" />
|
||||
<input class="form-control interface-filter" type="text" placeholder="Filter" title="Filter text (regular expressions supported)" style="height: 23px" />
|
||||
</div>
|
||||
</div>
|
||||
<table id="interfaces_table" class="table table-hover table-headings panel-body component-list">
|
||||
@ -626,16 +673,11 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.dcim.delete_interface %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if consoleserverports %}
|
||||
{% if perms.dcim.delete_consoleserverport %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="device" value="{{ device.pk }}" />
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Console Server Ports</strong>
|
||||
@ -687,16 +729,11 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.dcim.delete_consoleserverport %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if poweroutlets %}
|
||||
{% if perms.dcim.delete_poweroutlet %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="device" value="{{ device.pk }}" />
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Power Outlets</strong>
|
||||
@ -749,14 +786,11 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.dcim.delete_poweroutlet %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if front_ports %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="device" value="{{ device.pk }}" />
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Front Ports</strong>
|
||||
@ -815,7 +849,6 @@
|
||||
{% if rear_ports %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="device" value="{{ device.pk }}" />
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Rear Ports</strong>
|
||||
|
41
netbox/templates/dcim/device_component.html
Normal file
41
netbox/templates/dcim/device_component.html
Normal file
@ -0,0 +1,41 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load helpers %}
|
||||
{% load perms %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row noprint">
|
||||
<div class="col-md-12">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:device_list' %}">Devices</a></li>
|
||||
<li><a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a></li>
|
||||
<li><a href="{% url instance|viewname:"list" %}?device_id={{ instance.device.pk }}">{{ instance|meta:"verbose_name_plural"|bettertitle }}</a></li>
|
||||
<li>{{ instance }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right noprint">
|
||||
{% plugin_buttons instance %}
|
||||
{% if request.user|can_change:instance %}
|
||||
<a href="{% url instance|viewname:"edit" pk=instance.pk %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span> Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if request.user|can_delete:instance %}
|
||||
<a href="{% url instance|viewname:"delete" pk=instance.pk %}" class="btn btn-danger">
|
||||
<span class="fa fa-trash" aria-hidden="true"></span> Delete
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ instance.device }} / {{ instance }}{% endblock %}</h1>
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{{ instance.get_absolute_url }}">{{ instance|meta:"verbose_name"|bettertitle }}</a>
|
||||
</li>
|
||||
{% if perms.extras.view_objectchange %}
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url instance|viewname:"changelog" pk=instance.pk %}">Change Log</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endblock %}
|
@ -5,41 +5,25 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-12">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Chassis</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Model</td>
|
||||
<td>{{ device.device_type.display_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Serial Number</td>
|
||||
<td><span>{{ device.serial|placeholder }}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Asset Tag</td>
|
||||
<td><span>{{ device.asset_tag|placeholder }}</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Hardware</strong>
|
||||
<strong>Inventory Items</strong>
|
||||
</div>
|
||||
<table class="table table-hover table-condensed panel-body" id="hardware">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if perms.dcim.change_inventoryitem or perms.dcim.delete_inventoryitem %}
|
||||
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
|
||||
{% endif %}
|
||||
<th>Name</th>
|
||||
<th></th>
|
||||
<th>Manufacturer</th>
|
||||
<th>Part ID</th>
|
||||
<th>Serial Number</th>
|
||||
<th>Asset Tag</th>
|
||||
<th>Discovered</th>
|
||||
<th>Description</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@ -52,8 +36,22 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer noprint">
|
||||
{% if inventory_items and perms.dcim.change_inventoryitem %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:inventoryitem_bulk_rename' %}?return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:inventoryitem_bulk_edit' %}?device={{ device.pk }}&return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if inventory_items and perms.dcim.delete_inventoryitem %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'dcim:inventoryitem_bulk_delete' %}?return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_inventoryitem %}
|
||||
<div class="panel-footer text-right noprint">
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'dcim:inventoryitem_add' %}?device={{ device.pk }}&return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span> Add Inventory Item
|
||||
</a>
|
||||
@ -61,5 +59,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -14,12 +14,8 @@
|
||||
{% if perms.dcim.add_interface %}<li><a href="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Interfaces</a></li>{% endif %}
|
||||
{% if perms.dcim.add_rearport %}<li><a href="{% url 'dcim:device_bulk_add_rearport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Rear Ports</a></li>{% endif %}
|
||||
{% if perms.dcim.add_devicebay %}<li><a href="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Device Bays</a></li>{% endif %}
|
||||
{% if perms.dcim.add_inventoryitem %}<li><a href="{% url 'dcim:device_bulk_add_inventoryitem' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Inventory Items</a></li>{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_virtualchassis %}
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:virtualchassis_add' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-primary btn-sm">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Create Virtual Chassis
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
70
netbox/templates/dcim/devicebay.html
Normal file
70
netbox/templates/dcim/devicebay.html
Normal file
@ -0,0 +1,70 @@
|
||||
{% extends 'dcim/device_component.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Device Bay</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ instance.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
<td>{{ instance.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
|
||||
{% plugin_left_page instance %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Installed Device</strong>
|
||||
</div>
|
||||
{% if instance.installed_device %}
|
||||
{% with device=instance.installed_device %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ device.get_absolute_url }}">{{ device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device Type</td>
|
||||
<td>{{ device.device_type }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
None
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_right_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% plugin_full_width_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -131,7 +131,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Instances</td>
|
||||
<td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ devicetype.instances.count }}</a></td>
|
||||
<td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ instance_count }}</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@ -173,7 +173,7 @@
|
||||
{% if devicetype.is_parent_device or devicebay_table.rows %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' add_url='dcim:devicebaytemplate_add' edit_url=None delete_url='dcim:devicebaytemplate_bulk_delete' %}
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' add_url='dcim:devicebaytemplate_add' edit_url='dcim:devicebaytemplate_bulk_edit' delete_url='dcim:devicebaytemplate_bulk_delete' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
91
netbox/templates/dcim/frontport.html
Normal file
91
netbox/templates/dcim/frontport.html
Normal file
@ -0,0 +1,91 @@
|
||||
{% extends 'dcim/device_component.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Front Port</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ instance.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
<td>{{ instance.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ instance.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rear Port</td>
|
||||
<td>
|
||||
<a href="{{ instance.rear_port.get_absolute_url }}">{{ instance.rear_port }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rear Port Position</td>
|
||||
<td>{{ instance.rear_port_position }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
|
||||
{% plugin_left_page instance %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Connection</strong>
|
||||
</div>
|
||||
{% if instance.cable %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Cable</td>
|
||||
<td>
|
||||
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
|
||||
<a href="{% url 'dcim:frontport_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>
|
||||
{% if instance.cable.status %}
|
||||
<span class="label label-success">{{ instance.cable.get_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="label label-info">{{ instance.cable.get_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
Not connected
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_right_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% plugin_full_width_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -16,7 +16,9 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Component</td>
|
||||
<td>{{ termination }}</td>
|
||||
<td>
|
||||
<a href="{{ termination.get_absolute_url }}">{{ termination }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
{# Circuit termination #}
|
||||
|
@ -1,8 +1,16 @@
|
||||
<tr class="consoleport{% if cp.cable %} {{ cp.cable.get_status_class }}{% endif %}">
|
||||
|
||||
{# Checkbox #}
|
||||
{% if perms.dcim.change_consoleport or perms.dcim.delete_consoleport %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ cp.pk }}" />
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
{# Name #}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp }}
|
||||
<i class="fa fa-fw fa-keyboard-o"></i>
|
||||
<a href="{{ cp.get_absolute_url }}">{{ cp }}</a>
|
||||
</td>
|
||||
|
||||
{# Type #}
|
||||
|
@ -11,7 +11,8 @@
|
||||
|
||||
{# Name #}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ csp }}
|
||||
<i class="fa fa-fw fa-keyboard-o"></i>
|
||||
<a href="{{ csp.get_absolute_url }}">{{ csp }}</a>
|
||||
</td>
|
||||
|
||||
{# Type #}
|
||||
|
@ -9,7 +9,8 @@
|
||||
|
||||
{# Name #}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i> {{ devicebay.name }}
|
||||
<i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i>
|
||||
<a href="{{ devicebay.get_absolute_url }}">{{ devicebay.name }}</a>
|
||||
</td>
|
||||
|
||||
{# Status #}
|
||||
|
@ -10,7 +10,8 @@
|
||||
|
||||
{# Name #}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-square{% if not frontport.cable %}-o{% endif %}"></i> {{ frontport }}
|
||||
<i class="fa fa-fw fa-square{% if not frontport.cable %}-o{% endif %}"></i>
|
||||
<a href="{{ frontport.get_absolute_url }}">{{ frontport }}</a>
|
||||
</td>
|
||||
|
||||
{# Type #}
|
||||
|
@ -166,7 +166,7 @@
|
||||
</ul>
|
||||
</span>
|
||||
{% endif %}
|
||||
<a href="{% if iface.device_id %}{% url 'dcim:interface_edit' pk=iface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=iface.pk %}{% endif %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs" title="Edit interface">
|
||||
<a href="{% url 'dcim:interface_edit' pk=iface.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs" title="Edit interface">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
@ -176,7 +176,7 @@
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<a href="{% if iface.device_id %}{% url 'dcim:interface_delete' pk=iface.pk %}{% else %}{% url 'virtualization:interface_delete' pk=iface.pk %}{% endif %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs" title="Delete interface">
|
||||
<a href="{% url 'dcim:interface_delete' pk=iface.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs" title="Delete interface">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@ -1,11 +1,34 @@
|
||||
{% load helpers %}
|
||||
<tr>
|
||||
<td style="padding-left: {{ indent|add:5 }}px">{{ item }}</td>
|
||||
<td>{% if not item.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
|
||||
<td>{{ item.manufacturer|default:"" }}</td>
|
||||
<td>{{ item.part_id }}</td>
|
||||
<td>{{ item.serial }}</td>
|
||||
<td>{{ item.asset_tag|default:"" }}</td>
|
||||
<td>{{ item.description }}</td>
|
||||
|
||||
{# Checkbox #}
|
||||
{% if perms.dcim.change_inventoryitem or perms.dcim.delete_inventoryitem %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ item.pk }}" />
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
<td style="padding-left: {{ indent|add:5 }}px">
|
||||
<a href="{{ item.get_absolute_url }}">{{ item }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if item.manufacturer %}
|
||||
<a href="{{ item.manufacturer.get_absolute_url }}">{{ item.manufacturer }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.part_id|placeholder }}</td>
|
||||
<td>{{ item.serial|placeholder }}</td>
|
||||
<td>{{ item.asset_tag|placeholder }}</td>
|
||||
<td>
|
||||
{% if item.discovered %}
|
||||
<span class="text-success"><i class="fa fa-check"></i></span>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.description|placeholder }}</td>
|
||||
<td class="text-right noprint">
|
||||
{% if perms.dcim.change_inventoryitem %}
|
||||
<a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
|
||||
|
@ -11,7 +11,8 @@
|
||||
|
||||
{# Name #}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-bolt"></i> {{ po }}
|
||||
<i class="fa fa-fw fa-bolt"></i>
|
||||
<a href="{{ po.get_absolute_url }}">{{ po }}</a>
|
||||
</td>
|
||||
|
||||
{# Type #}
|
||||
|
@ -1,8 +1,16 @@
|
||||
<tr class="powerport{% if pp.cable %} {{ pp.cable.get_status_class }}{% endif %}">
|
||||
|
||||
{# Checkbox #}
|
||||
{% if perms.dcim.change_powerport or perms.dcim.delete_powerport %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ pp.pk }}" />
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
{# Name #}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-bolt"></i> {{ pp }}
|
||||
<i class="fa fa-fw fa-bolt"></i>
|
||||
<a href="{{ pp.get_absolute_url }}">{{ pp }}</a>
|
||||
</td>
|
||||
|
||||
{# Type #}
|
||||
|
@ -1,4 +1,6 @@
|
||||
<div style="margin-left: -30px">
|
||||
<object data="{% url 'dcim-api:rack-elevation' pk=rack.pk %}?face={{face}}&render=svg" class="rack_elevation"></object>
|
||||
</div>
|
||||
<div class="text-center text-small">
|
||||
<a href="{% url 'dcim-api:rack-elevation' pk=rack.pk %}?face={{face}}&render=svg">
|
||||
<i class="fa fa-download"></i> Save SVG
|
||||
|
@ -10,7 +10,8 @@
|
||||
|
||||
{# Name #}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-square{% if not rearport.cable %}-o{% endif %}"></i> {{ rearport }}
|
||||
<i class="fa fa-fw fa-square{% if not rearport.cable %}-o{% endif %}"></i>
|
||||
<a href="{{ rearport.get_absolute_url }}">{{ rearport }}</a>
|
||||
</td>
|
||||
|
||||
{# Type #}
|
||||
|
@ -1,44 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'dcim/device_component.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row noprint">
|
||||
<div class="col-md-12">
|
||||
<ol class="breadcrumb">
|
||||
{% if interface.device %}
|
||||
<li><a href="{% url 'dcim:device_list' %}">Devices</a></li>
|
||||
{% else %}
|
||||
<li><a href="{% url 'virtualization:virtualmachine_list' %}">Virtual Machines</a></li>
|
||||
{% endif %}
|
||||
<li><a href="{{ interface.parent.get_absolute_url }}">{{ interface.parent }}</a></li>
|
||||
<li>{{ interface }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.dcim.change_interface %}
|
||||
<a href="{% if interface.device %}{% url 'dcim:interface_edit' pk=interface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=interface.pk %}{% endif %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span> Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.delete_interface %}
|
||||
<a href="{% if interface.device %}{% url 'dcim:interface_delete' pk=interface.pk %}{% else %}{% url 'virtualization:interface_delete' pk=interface.pk %}{% endif %}" class="btn btn-danger">
|
||||
<span class="fa fa-trash" aria-hidden="true"></span> Delete
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ interface.parent }} / {{ interface.name }}{% endblock %}</h1>
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{{ interface.get_absolute_url }}">Interface</a>
|
||||
</li>
|
||||
{% if perms.extras.view_objectchange %}
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'dcim:interface_changelog' pk=interface.pk %}">Change Log</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
@ -49,27 +11,27 @@
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>{% if interface.device %}Device{% else %}Virtual Machine{% endif %}</td>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ interface.parent.get_absolute_url }}">{{ interface.parent }}</a>
|
||||
<a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ interface.name }}</td>
|
||||
<td>{{ instance.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
<td>{{ interface.label|placeholder }}</td>
|
||||
<td>{{ instance.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ interface.get_type_display }}</td>
|
||||
<td>{{ instance.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enabled</td>
|
||||
<td>
|
||||
{% if interface.enabled %}
|
||||
{% if instance.enabled %}
|
||||
<span class="text-success"><i class="fa fa-check"></i></span>
|
||||
{% else %}
|
||||
<span class="text-danger"><i class="fa fa-close"></i></span>
|
||||
@ -79,8 +41,8 @@
|
||||
<tr>
|
||||
<td>LAG</td>
|
||||
<td>
|
||||
{% if interface.lag%}
|
||||
<a href="{{ interface.lag.get_absolute_url }}">{{ interface.lag }}</a>
|
||||
{% if instance.lag%}
|
||||
<a href="{{ instance.lag.get_absolute_url }}">{{ instance.lag }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
@ -88,37 +50,38 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ interface.description|placeholder }} </td>
|
||||
<td>{{ instance.description|placeholder }} </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MTU</td>
|
||||
<td>{{ interface.mtu|placeholder }}</td>
|
||||
<td>{{ instance.mtu|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MAC Address</td>
|
||||
<td>{{ interface.mac_address|placeholder }}</span></td>
|
||||
<td><span class="text-monospace">{{ instance.mac_address|placeholder }}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>802.1Q Mode</td>
|
||||
<td>{{ interface.get_mode_display }}</td>
|
||||
<td>{{ instance.get_mode_display }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=interface.tags.all %}
|
||||
{% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
|
||||
{% plugin_left_page instance %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% if interface.is_connectable %}
|
||||
{% if instance.is_connectable %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Connection</strong>
|
||||
</div>
|
||||
{% if interface.cable %}
|
||||
{% if instance.cable %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
{% if connected_interface %}
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ connected_interface.parent.get_absolute_url }}">{{ connected_interface.device }}</a>
|
||||
<a href="{{ connected_interface.device.get_absolute_url }}">{{ connected_interface.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -186,8 +149,8 @@
|
||||
<tr>
|
||||
<td>Cable</td>
|
||||
<td>
|
||||
<a href="{{ interface.cable.get_absolute_url }}">{{ interface.cable }}</a>
|
||||
<a href="{% url 'dcim:interface_trace' pk=interface.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
|
||||
<a href="{% url 'dcim:interface_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
@ -195,10 +158,10 @@
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>
|
||||
{% if interface.connection_status %}
|
||||
<span class="label label-success">{{ interface.get_connection_status_display }}</span>
|
||||
{% if instance.connection_status %}
|
||||
<span class="label label-success">{{ instance.get_connection_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="label label-info">{{ interface.get_connection_status_display }}</span>
|
||||
<span class="label label-info">{{ instance.get_connection_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@ -210,7 +173,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if interface.is_lag %}
|
||||
{% if instance.is_lag %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>LAG Members</strong></div>
|
||||
<table class="table table-hover table-headings panel-body">
|
||||
@ -222,10 +185,10 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for member in interface.member_interfaces.all %}
|
||||
{% for member in instance.member_interfaces.all %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ member.parent.get_absolute_url }}">{{ member.parent }}</a>
|
||||
<a href="{{ member.device.get_absolute_url }}">{{ member.device }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ member.get_absolute_url }}">{{ member }}</a>
|
||||
@ -243,6 +206,7 @@
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% plugin_right_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -255,4 +219,9 @@
|
||||
{% include 'panel_table.html' with table=vlan_table heading="VLANs" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% plugin_full_width_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
73
netbox/templates/dcim/inventoryitem.html
Normal file
73
netbox/templates/dcim/inventoryitem.html
Normal file
@ -0,0 +1,73 @@
|
||||
{% extends 'dcim/device_component.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Inventory Item</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Parent Item</td>
|
||||
<td>
|
||||
{% if instance.parent %}
|
||||
<a href="{{ instance.parent.get_absolute_url }}">{{ instance.parent }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ instance.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Manufacturer</td>
|
||||
<td>
|
||||
{% if instance.manufacturer %}
|
||||
<a href="{{ instance.manufacturer.get_absolute_url }}">{{ instance.manufacturer }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Part ID</td>
|
||||
<td>{{ instance.part_id|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Serial</td>
|
||||
<td>{{ instance.serial|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Asset Tag</td>
|
||||
<td>{{ instance.asset_tag|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
|
||||
{% plugin_left_page instance %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% plugin_right_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% plugin_full_width_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
111
netbox/templates/dcim/poweroutlet.html
Normal file
111
netbox/templates/dcim/poweroutlet.html
Normal file
@ -0,0 +1,111 @@
|
||||
{% extends 'dcim/device_component.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Power Outlet</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ instance.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
<td>{{ instance.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ instance.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Power Port</td>
|
||||
<td>{{ instance.power_port }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Feed Leg</td>
|
||||
<td>{{ instance.get_feed_leg_display }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
|
||||
{% plugin_left_page instance %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Connection</strong>
|
||||
</div>
|
||||
{% if instance.cable %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
{% if instance.connected_endpoint %}
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.connected_endpoint.device.get_absolute_url }}">{{ instance.connected_endpoint.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<a href="{{ instance.connected_endpoint.get_absolute_url }}">{{ instance.connected_endpoint.name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ instance.connected_endpoint.get_type_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.connected_endpoint.description|placeholder }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>Cable</td>
|
||||
<td>
|
||||
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
|
||||
<a href="{% url 'dcim:poweroutlet_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>
|
||||
{% if instance.connection_status %}
|
||||
<span class="label label-success">{{ instance.get_connection_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="label label-info">{{ instance.get_connection_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
Not connected
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_right_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% plugin_full_width_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
111
netbox/templates/dcim/powerport.html
Normal file
111
netbox/templates/dcim/powerport.html
Normal file
@ -0,0 +1,111 @@
|
||||
{% extends 'dcim/device_component.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Power Port</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ instance.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
<td>{{ instance.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ instance.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Maximum Draw</td>
|
||||
<td>{{ instance.maximum_draw|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Allocated Draw</td>
|
||||
<td>{{ instance.allocated_draw|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
|
||||
{% plugin_left_page instance %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Connection</strong>
|
||||
</div>
|
||||
{% if instance.cable %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
{% if instance.connected_endpoint %}
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.connected_endpoint.device.get_absolute_url }}">{{ instance.connected_endpoint.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<a href="{{ instance.connected_endpoint.get_absolute_url }}">{{ instance.connected_endpoint.name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ instance.connected_endpoint.get_type_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.connected_endpoint.description|placeholder }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>Cable</td>
|
||||
<td>
|
||||
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
|
||||
<a href="{% url 'dcim:powerport_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>
|
||||
{% if instance.connection_status %}
|
||||
<span class="label label-success">{{ instance.get_connection_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="label label-info">{{ instance.get_connection_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
Not connected
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_right_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% plugin_full_width_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -318,16 +318,12 @@
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="row" style="margin-bottom: 20px">
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<div class="rack_header">
|
||||
<div class="col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<h4>Front</h4>
|
||||
</div>
|
||||
{% include 'dcim/inc/rack_elevation.html' with face='front' %}
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<div class="rack_header">
|
||||
<div class="col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<h4>Rear</h4>
|
||||
</div>
|
||||
{% include 'dcim/inc/rack_elevation.html' with face='rear' %}
|
||||
</div>
|
||||
</div>
|
||||
|
85
netbox/templates/dcim/rearport.html
Normal file
85
netbox/templates/dcim/rearport.html
Normal file
@ -0,0 +1,85 @@
|
||||
{% extends 'dcim/device_component.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Rear Port</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ instance.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
<td>{{ instance.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ instance.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Positions</td>
|
||||
<td>{{ instance.positions }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ instance.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
|
||||
{% plugin_left_page instance %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Connection</strong>
|
||||
</div>
|
||||
{% if instance.cable %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Cable</td>
|
||||
<td>
|
||||
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
|
||||
<a href="{% url 'dcim:rearport_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>
|
||||
{% if instance.cable.status %}
|
||||
<span class="label label-success">{{ instance.cable.get_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="label label-info">{{ instance.cable.get_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
Not connected
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_right_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% plugin_full_width_page instance %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -9,7 +9,9 @@
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:virtualchassis_list' %}">Virtual Chassis</a></li>
|
||||
{% if virtualchassis.master %}
|
||||
<li><a href="{% url 'dcim:virtualchassis_list' %}?site={{ virtualchassis.master.site.slug }}">{{ virtualchassis.master.site }}</a></li>
|
||||
{% endif %}
|
||||
<li>{{ virtualchassis }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
@ -63,7 +65,17 @@
|
||||
<tr>
|
||||
<td>Domain</td>
|
||||
<td>{{ virtualchassis.domain|placeholder }}</td>
|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Master</td>
|
||||
<td>
|
||||
{% if virtualchassis.master %}
|
||||
<a href="{{ virtualchassis.master.get_absolute_url }}">{{ virtualchassis.master }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=virtualchassis.tags.all url='dcim:virtualchassis_list' %}
|
||||
@ -81,7 +93,7 @@
|
||||
<th>Master</th>
|
||||
<th>Priority</th>
|
||||
</tr>
|
||||
{% for vc_member in virtualchassis.members.all %}
|
||||
{% for vc_member in members %}
|
||||
<tr{% if vc_member == device %} class="info"{% endif %}>
|
||||
<td>
|
||||
<a href="{{ vc_member.get_absolute_url }}">{{ vc_member }}</a>
|
||||
|
22
netbox/templates/dcim/virtualchassis_add.html
Normal file
22
netbox/templates/dcim/virtualchassis_add.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% extends 'utilities/obj_edit.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block form %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Virtual Chassis</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.name %}
|
||||
{% render_field form.domain %}
|
||||
{% render_field form.tags %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Member Devices</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.site %}
|
||||
{% render_field form.rack %}
|
||||
{% render_field form.members %}
|
||||
{% render_field form.initial_position %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -4,7 +4,7 @@
|
||||
<strong>Tags</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% for tag in tags.unrestricted %}
|
||||
{% for tag in tags.all %}
|
||||
{% tag tag url %}
|
||||
{% empty %}
|
||||
<span class="text-muted">No tags assigned</span>
|
||||
|
@ -144,6 +144,12 @@
|
||||
<a href="{% url 'dcim:platform_list' %}">Platforms</a>
|
||||
</li>
|
||||
<li{% if not perms.dcim.view_virtualchassis %} class="disabled"{% endif %}>
|
||||
{% if perms.dcim.add_virtualchassis %}
|
||||
<div class="buttons pull-right">
|
||||
<a href="{% url 'dcim:virtualchassis_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
|
||||
<a href="{% url 'dcim:virtualchassis_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:virtualchassis_list' %}">Virtual Chassis</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
@ -167,16 +173,6 @@
|
||||
<a href="{% url 'dcim:manufacturer_list' %}">Manufacturers</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li class="dropdown-header">Inventory</li>
|
||||
<li{% if not perms.dcim.view_inventoryitem %} class="disabled"{% endif %}>
|
||||
{% if perms.dcim.add_inventoryitem %}
|
||||
<div class="buttons pull-right">
|
||||
<a href="{% url 'dcim:inventoryitem_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:inventoryitem_list' %}">Inventory Items</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li class="dropdown-header">Connections</li>
|
||||
<li{% if not perms.dcim.view_cable %} class="disabled"{% endif %}>
|
||||
{% if perms.dcim.add_cable %}
|
||||
@ -261,6 +257,14 @@
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:devicebay_list' %}">Device Bays</a>
|
||||
</li>
|
||||
<li{% if not perms.dcim.view_inventoryitem %} class="disabled"{% endif %}>
|
||||
{% if perms.dcim.add_inventoryitem %}
|
||||
<div class="buttons pull-right">
|
||||
<a href="{% url 'dcim:inventoryitem_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:inventoryitem_list' %}">Inventory Items</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
@ -372,6 +376,14 @@
|
||||
{% endif %}
|
||||
<a href="{% url 'virtualization:virtualmachine_list' %}">Virtual Machines</a>
|
||||
</li>
|
||||
<li{% if not perms.virtualization.view_vminterface%} class="disabled"{% endif %}>
|
||||
{% if perms.virtualization.add_vminterface %}
|
||||
<div class="buttons pull-right">
|
||||
<a href="{% url 'virtualization:vminterface_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'virtualization:vminterface_list' %}">Interfaces</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li class="dropdown-header">Clusters</li>
|
||||
<li{% if not perms.virtualization.view_cluster %} class="disabled"{% endif %}>
|
||||
|
@ -19,6 +19,7 @@
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
<form method="get">
|
||||
{% for k, v_list in request.GET.lists %}
|
||||
{% if k != 'per_page' %}
|
||||
@ -33,7 +34,6 @@
|
||||
{% endfor %}
|
||||
</select> per page
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if page %}
|
||||
<div class="text-right text-muted">
|
||||
Showing {{ page.start_index }}-{{ page.end_index }} of {{ page.paginator.count }}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user