Merge branch 'develop-2.9' into 2006-scripts-reports-background

This commit is contained in:
Jeremy Stretch 2020-07-02 11:00:59 -04:00 committed by GitHub
commit f98fa364c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
144 changed files with 4747 additions and 3590 deletions

View File

@ -156,9 +156,13 @@ direction = ChoiceVar(choices=CHOICES)
### ObjectVar ### 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 ### FileVar
@ -222,10 +226,7 @@ class NewBranchScript(Script):
) )
switch_model = ObjectVar( switch_model = ObjectVar(
description="Access switch model", description="Access switch model",
queryset = DeviceType.objects.filter( queryset = DeviceType.objects.all()
manufacturer__name='Cisco',
model__in=['Catalyst 3560X-48T', 'Catalyst 3750X-48T']
)
) )
def run(self, data, commit): def run(self, data, commit):

View File

@ -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 ## REMOTE_AUTH_ENABLED
Default: `False` Default: `False`

View File

@ -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. 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 ## 6. Add field to forms
If the new field has static choices, add it to the `FieldChoicesViewSet` for the app.
## 7. Add field to forms
Extend any forms to include the new field as appropriate. Common forms include: 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 * **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) * **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. 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. 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. 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: 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:

View File

@ -1,11 +1,20 @@
# NetBox v2.8 # 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 ### 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 * [#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 * [#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 * [#4775](https://github.com/netbox-community/netbox/issues/4775) - Allow selecting an alternate device type when creating component templates
--- ---

View File

@ -1,4 +1,4 @@
# NetBox v2.8 # NetBox v2.9
## v2.9.0 (FUTURE) ## v2.9.0 (FUTURE)
@ -10,9 +10,15 @@ NetBox v2.9 replaces Django's built-in permissions framework with one that suppo
### Enhancements ### 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 * [#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 * [#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 * [#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 ### 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. * 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. * 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 ### 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 `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. * 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 `webhooks` Redis queue configuration (use `tasks` instead).
* Dropped backward compatibility for the `/admin/webhook-backend-status` URL (moved to `/admin/background-tasks/`). * 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.

View File

@ -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 django.shortcuts import get_object_or_404
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
@ -28,8 +28,8 @@ class ProviderViewSet(CustomFieldModelViewSet):
""" """
A convenience method for rendering graphs for a particular provider. A convenience method for rendering graphs for a particular provider.
""" """
provider = get_object_or_404(Provider, pk=pk) provider = get_object_or_404(self.queryset, pk=pk)
queryset = Graph.objects.filter(type__model='provider') queryset = Graph.objects.restrict(request.user).filter(type__model='provider')
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': provider}) serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': provider})
return Response(serializer.data) return Response(serializer.data)
@ -52,7 +52,10 @@ class CircuitTypeViewSet(ModelViewSet):
class CircuitViewSet(CustomFieldModelViewSet): class CircuitViewSet(CustomFieldModelViewSet):
queryset = Circuit.objects.prefetch_related( 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') ).prefetch_related('tags')
serializer_class = serializers.CircuitSerializer serializer_class = serializers.CircuitSerializer
filterset_class = filters.CircuitFilterSet filterset_class = filters.CircuitFilterSet

View File

@ -239,7 +239,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
return self.STATUS_CLASS_MAP.get(self.status) return self.STATUS_CLASS_MAP.get(self.status)
def _get_termination(self, side): def _get_termination(self, side):
for ct in self.terminations.all(): for ct in self.terminations.unrestricted():
if ct.term_side == side: if ct.term_side == side:
return ct return ct
return None return None

View File

@ -10,7 +10,7 @@ def update_circuit(instance, **kwargs):
""" """
When a CircuitTermination has been modified, update the last_updated time of its parent Circuit. 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() time = timezone.now()
for circuit in circuits: for circuit in circuits:
circuit.last_updated = time circuit.last_updated = time

View File

@ -2,19 +2,9 @@ import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from tenancy.tables import COL_TENANT 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 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 = """ STATUS_LABEL = """
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span> <span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
""" """
@ -53,11 +43,7 @@ class CircuitTypeTable(BaseTable):
circuit_count = tables.Column( circuit_count = tables.Column(
verbose_name='Circuits' verbose_name='Circuits'
) )
actions = tables.TemplateColumn( actions = ButtonsColumn(CircuitType, pk_field='slug')
template_code=CIRCUITTYPE_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = CircuitType model = CircuitType

View File

@ -49,7 +49,7 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
""" """
Test retrieval of Graphs assigned to Providers. 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') ct = ContentType.objects.get(app_label='circuits', model='provider')
graphs = ( graphs = (
Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=1'), Graph(type=ct, name='Graph 1', source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=1'),

View File

@ -25,6 +25,7 @@ urlpatterns = [
path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'), 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/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>/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}), path('circuit-types/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
# Circuits # Circuits

View File

@ -60,19 +60,16 @@ class ProviderEditView(ObjectEditView):
queryset = Provider.objects.all() queryset = Provider.objects.all()
model_form = forms.ProviderForm model_form = forms.ProviderForm
template_name = 'circuits/provider_edit.html' template_name = 'circuits/provider_edit.html'
default_return_url = 'circuits:provider_list'
class ProviderDeleteView(ObjectDeleteView): class ProviderDeleteView(ObjectDeleteView):
queryset = Provider.objects.all() queryset = Provider.objects.all()
default_return_url = 'circuits:provider_list'
class ProviderBulkImportView(BulkImportView): class ProviderBulkImportView(BulkImportView):
queryset = Provider.objects.all() queryset = Provider.objects.all()
model_form = forms.ProviderCSVForm model_form = forms.ProviderCSVForm
table = tables.ProviderTable table = tables.ProviderTable
default_return_url = 'circuits:provider_list'
class ProviderBulkEditView(BulkEditView): class ProviderBulkEditView(BulkEditView):
@ -80,14 +77,12 @@ class ProviderBulkEditView(BulkEditView):
filterset = filters.ProviderFilterSet filterset = filters.ProviderFilterSet
table = tables.ProviderTable table = tables.ProviderTable
form = forms.ProviderBulkEditForm form = forms.ProviderBulkEditForm
default_return_url = 'circuits:provider_list'
class ProviderBulkDeleteView(BulkDeleteView): class ProviderBulkDeleteView(BulkDeleteView):
queryset = Provider.objects.annotate(count_circuits=Count('circuits')) queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
filterset = filters.ProviderFilterSet filterset = filters.ProviderFilterSet
table = tables.ProviderTable table = tables.ProviderTable
default_return_url = 'circuits:provider_list'
# #
@ -102,20 +97,21 @@ class CircuitTypeListView(ObjectListView):
class CircuitTypeEditView(ObjectEditView): class CircuitTypeEditView(ObjectEditView):
queryset = CircuitType.objects.all() queryset = CircuitType.objects.all()
model_form = forms.CircuitTypeForm model_form = forms.CircuitTypeForm
default_return_url = 'circuits:circuittype_list'
class CircuitTypeDeleteView(ObjectDeleteView):
queryset = CircuitType.objects.all()
class CircuitTypeBulkImportView(BulkImportView): class CircuitTypeBulkImportView(BulkImportView):
queryset = CircuitType.objects.all() queryset = CircuitType.objects.all()
model_form = forms.CircuitTypeCSVForm model_form = forms.CircuitTypeCSVForm
table = tables.CircuitTypeTable table = tables.CircuitTypeTable
default_return_url = 'circuits:circuittype_list'
class CircuitTypeBulkDeleteView(BulkDeleteView): class CircuitTypeBulkDeleteView(BulkDeleteView):
queryset = CircuitType.objects.annotate(circuit_count=Count('circuits')) queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
table = tables.CircuitTypeTable table = tables.CircuitTypeTable
default_return_url = 'circuits:circuittype_list'
# #
@ -165,19 +161,16 @@ class CircuitEditView(ObjectEditView):
queryset = Circuit.objects.all() queryset = Circuit.objects.all()
model_form = forms.CircuitForm model_form = forms.CircuitForm
template_name = 'circuits/circuit_edit.html' template_name = 'circuits/circuit_edit.html'
default_return_url = 'circuits:circuit_list'
class CircuitDeleteView(ObjectDeleteView): class CircuitDeleteView(ObjectDeleteView):
queryset = Circuit.objects.all() queryset = Circuit.objects.all()
default_return_url = 'circuits:circuit_list'
class CircuitBulkImportView(BulkImportView): class CircuitBulkImportView(BulkImportView):
queryset = Circuit.objects.all() queryset = Circuit.objects.all()
model_form = forms.CircuitCSVForm model_form = forms.CircuitCSVForm
table = tables.CircuitTable table = tables.CircuitTable
default_return_url = 'circuits:circuit_list'
class CircuitBulkEditView(BulkEditView): class CircuitBulkEditView(BulkEditView):
@ -185,14 +178,12 @@ class CircuitBulkEditView(BulkEditView):
filterset = filters.CircuitFilterSet filterset = filters.CircuitFilterSet
table = tables.CircuitTable table = tables.CircuitTable
form = forms.CircuitBulkEditForm form = forms.CircuitBulkEditForm
default_return_url = 'circuits:circuit_list'
class CircuitBulkDeleteView(BulkDeleteView): class CircuitBulkDeleteView(BulkDeleteView):
queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant').prefetch_related('terminations__site') queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
filterset = filters.CircuitFilterSet filterset = filters.CircuitFilterSet
table = tables.CircuitTable table = tables.CircuitTable
default_return_url = 'circuits:circuit_list'
class CircuitSwapTerminations(ObjectEditView): class CircuitSwapTerminations(ObjectEditView):

View File

@ -332,7 +332,7 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.VirtualChassis model = models.VirtualChassis
fields = ['id', 'url', 'master', 'member_count'] fields = ['id', 'name', 'url', 'master', 'member_count']
# #

View File

@ -1,3 +1,4 @@
from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from drf_yasg.utils import swagger_serializer_method from drf_yasg.utils import swagger_serializer_method
from rest_framework import serializers from rest_framework import serializers
@ -183,10 +184,10 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
default=RackElevationDetailRenderChoices.RENDER_JSON default=RackElevationDetailRenderChoices.RENDER_JSON
) )
unit_width = serializers.IntegerField( unit_width = serializers.IntegerField(
default=RACK_ELEVATION_UNIT_WIDTH_DEFAULT default=settings.RACK_ELEVATION_DEFAULT_UNIT_WIDTH
) )
unit_height = serializers.IntegerField( unit_height = serializers.IntegerField(
default=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT default=settings.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT
) )
legend_width = serializers.IntegerField( legend_width = serializers.IntegerField(
default=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT default=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT
@ -245,7 +246,7 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
fields = ['id', 'device_type', 'name', 'label', 'type'] fields = ['id', 'device_type', 'name', 'label', 'type', 'description']
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer): class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
@ -258,7 +259,7 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = ['id', 'device_type', 'name', 'label', 'type'] fields = ['id', 'device_type', 'name', 'label', 'type', 'description']
class PowerPortTemplateSerializer(ValidatedModelSerializer): class PowerPortTemplateSerializer(ValidatedModelSerializer):
@ -271,7 +272,7 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = PowerPortTemplate 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): class PowerOutletTemplateSerializer(ValidatedModelSerializer):
@ -292,7 +293,7 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = PowerOutletTemplate 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): class InterfaceTemplateSerializer(ValidatedModelSerializer):
@ -301,7 +302,7 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
fields = ['id', 'device_type', 'name', 'label', 'type', 'mgmt_only'] fields = ['id', 'device_type', 'name', 'label', 'type', 'mgmt_only', 'description']
class RearPortTemplateSerializer(ValidatedModelSerializer): class RearPortTemplateSerializer(ValidatedModelSerializer):
@ -310,7 +311,7 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = RearPortTemplate model = RearPortTemplate
fields = ['id', 'device_type', 'name', 'type', 'positions'] fields = ['id', 'device_type', 'name', 'type', 'positions', 'description']
class FrontPortTemplateSerializer(ValidatedModelSerializer): class FrontPortTemplateSerializer(ValidatedModelSerializer):
@ -320,7 +321,7 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = FrontPortTemplate 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): class DeviceBayTemplateSerializer(ValidatedModelSerializer):
@ -328,7 +329,7 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = DeviceBayTemplate 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): class VirtualChassisSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
master = NestedDeviceSerializer() master = NestedDeviceSerializer(required=False)
member_count = serializers.IntegerField(read_only=True) member_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = VirtualChassis model = VirtualChassis
fields = ['id', 'master', 'domain', 'tags', 'member_count'] fields = ['id', 'name', 'domain', 'master', 'tags', 'member_count']
# #

View File

@ -29,6 +29,7 @@ from utilities.api import (
get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, ModelViewSet, ServiceUnavailable, get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, ModelViewSet, ServiceUnavailable,
) )
from utilities.utils import get_subquery from utilities.utils import get_subquery
from utilities.metadata import ContentTypeMetadata
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from . import serializers from . import serializers
from .exceptions import MissingFilterException 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). 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 # Initialize the path array
path = [] path = []
@ -103,8 +104,8 @@ class SiteViewSet(CustomFieldModelViewSet):
""" """
A convenience method for rendering graphs for a particular site. A convenience method for rendering graphs for a particular site.
""" """
site = get_object_or_404(Site, pk=pk) site = get_object_or_404(self.queryset, pk=pk)
queryset = Graph.objects.filter(type__model='site') queryset = Graph.objects.restrict(request.user).filter(type__model='site')
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': site}) serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': site})
return Response(serializer.data) 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 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) serializer = serializers.RackElevationDetailFilterSerializer(data=request.GET)
if not serializer.is_valid(): if not serializer.is_valid():
return Response(serializer.errors, 400) return Response(serializer.errors, 400)
@ -226,7 +227,7 @@ class ManufacturerViewSet(ModelViewSet):
# #
class DeviceTypeViewSet(CustomFieldModelViewSet): 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') device_count=Count('instances')
) )
serializer_class = serializers.DeviceTypeSerializer serializer_class = serializers.DeviceTypeSerializer
@ -347,8 +348,8 @@ class DeviceViewSet(CustomFieldModelViewSet):
""" """
A convenience method for rendering graphs for a particular Device. A convenience method for rendering graphs for a particular Device.
""" """
device = get_object_or_404(Device, pk=pk) device = get_object_or_404(self.queryset, pk=pk)
queryset = Graph.objects.filter(type__model='device') queryset = Graph.objects.restrict(request.user).filter(type__model='device')
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': device}) serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': device})
return Response(serializer.data) return Response(serializer.data)
@ -369,7 +370,7 @@ class DeviceViewSet(CustomFieldModelViewSet):
""" """
Execute a NAPALM method on a Device 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: if not device.primary_ip:
raise ServiceUnavailable("This device does not have a primary IP address configured.") raise ServiceUnavailable("This device does not have a primary IP address configured.")
if device.platform is None: if device.platform is None:
@ -496,8 +497,8 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet):
""" """
A convenience method for rendering graphs for a particular interface. A convenience method for rendering graphs for a particular interface.
""" """
interface = get_object_or_404(Interface, pk=pk) interface = get_object_or_404(self.queryset, pk=pk)
queryset = Graph.objects.filter(type__model='interface') queryset = Graph.objects.restrict(request.user).filter(type__model='interface')
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': interface}) serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': interface})
return Response(serializer.data) return Response(serializer.data)
@ -567,6 +568,7 @@ class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
# #
class CableViewSet(ModelViewSet): class CableViewSet(ModelViewSet):
metadata_class = ContentTypeMetadata
queryset = Cable.objects.prefetch_related( queryset = Cable.objects.prefetch_related(
'termination_a', 'termination_b' 'termination_a', 'termination_b'
) )
@ -655,7 +657,11 @@ class ConnectedDeviceViewSet(ViewSet):
raise MissingFilterException(detail='Request must include "peer_device" and "peer_interface" filters.') raise MissingFilterException(detail='Request must include "peer_device" and "peer_interface" filters.')
# Determine local interface from peer interface's connection # 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 local_interface = peer_interface._connected_interface
if local_interface is None: if local_interface is None:

View File

@ -260,6 +260,7 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h' TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h'
TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h' TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h'
# NEMA non-locking # NEMA non-locking
TYPE_NEMA_115P = 'nema-1-15p'
TYPE_NEMA_515P = 'nema-5-15p' TYPE_NEMA_515P = 'nema-5-15p'
TYPE_NEMA_520P = 'nema-5-20p' TYPE_NEMA_520P = 'nema-5-20p'
TYPE_NEMA_530P = 'nema-5-30p' TYPE_NEMA_530P = 'nema-5-30p'
@ -268,16 +269,27 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_NEMA_620P = 'nema-6-20p' TYPE_NEMA_620P = 'nema-6-20p'
TYPE_NEMA_630P = 'nema-6-30p' TYPE_NEMA_630P = 'nema-6-30p'
TYPE_NEMA_650P = 'nema-6-50p' 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 # NEMA locking
TYPE_NEMA_L115P = 'nema-l1-15p'
TYPE_NEMA_L515P = 'nema-l5-15p' TYPE_NEMA_L515P = 'nema-l5-15p'
TYPE_NEMA_L520P = 'nema-l5-20p' TYPE_NEMA_L520P = 'nema-l5-20p'
TYPE_NEMA_L530P = 'nema-l5-30p' 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_L620P = 'nema-l6-20p'
TYPE_NEMA_L630P = 'nema-l6-30p' TYPE_NEMA_L630P = 'nema-l6-30p'
TYPE_NEMA_L650P = 'nema-l6-50p' TYPE_NEMA_L650P = 'nema-l6-50p'
TYPE_NEMA_L1030P = 'nema-l10-30p'
TYPE_NEMA_L1420P = 'nema-l14-20p' TYPE_NEMA_L1420P = 'nema-l14-20p'
TYPE_NEMA_L1430P = 'nema-l14-30p' 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_L2120P = 'nema-l21-20p'
TYPE_NEMA_L2130P = 'nema-l21-30p' TYPE_NEMA_L2130P = 'nema-l21-30p'
# California style # California style
@ -324,6 +336,7 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_IEC_3PNE9H, '3P+N+E 9H'), (TYPE_IEC_3PNE9H, '3P+N+E 9H'),
)), )),
('NEMA (Non-locking)', ( ('NEMA (Non-locking)', (
(TYPE_NEMA_115P, 'NEMA 1-15P'),
(TYPE_NEMA_515P, 'NEMA 5-15P'), (TYPE_NEMA_515P, 'NEMA 5-15P'),
(TYPE_NEMA_520P, 'NEMA 5-20P'), (TYPE_NEMA_520P, 'NEMA 5-20P'),
(TYPE_NEMA_530P, 'NEMA 5-30P'), (TYPE_NEMA_530P, 'NEMA 5-30P'),
@ -332,17 +345,28 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_NEMA_620P, 'NEMA 6-20P'), (TYPE_NEMA_620P, 'NEMA 6-20P'),
(TYPE_NEMA_630P, 'NEMA 6-30P'), (TYPE_NEMA_630P, 'NEMA 6-30P'),
(TYPE_NEMA_650P, 'NEMA 6-50P'), (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)', ( ('NEMA (Locking)', (
(TYPE_NEMA_L115P, 'NEMA L1-15P'),
(TYPE_NEMA_L515P, 'NEMA L5-15P'), (TYPE_NEMA_L515P, 'NEMA L5-15P'),
(TYPE_NEMA_L520P, 'NEMA L5-20P'), (TYPE_NEMA_L520P, 'NEMA L5-20P'),
(TYPE_NEMA_L530P, 'NEMA L5-30P'), (TYPE_NEMA_L530P, 'NEMA L5-30P'),
(TYPE_NEMA_L550P, 'NEMA L5-50P'),
(TYPE_NEMA_L615P, 'NEMA L6-15P'), (TYPE_NEMA_L615P, 'NEMA L6-15P'),
(TYPE_NEMA_L620P, 'NEMA L6-20P'), (TYPE_NEMA_L620P, 'NEMA L6-20P'),
(TYPE_NEMA_L630P, 'NEMA L6-30P'), (TYPE_NEMA_L630P, 'NEMA L6-30P'),
(TYPE_NEMA_L650P, 'NEMA L6-50P'), (TYPE_NEMA_L650P, 'NEMA L6-50P'),
(TYPE_NEMA_L1030P, 'NEMA L10-30P'),
(TYPE_NEMA_L1420P, 'NEMA L14-20P'), (TYPE_NEMA_L1420P, 'NEMA L14-20P'),
(TYPE_NEMA_L1430P, 'NEMA L14-30P'), (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_L2120P, 'NEMA L21-20P'),
(TYPE_NEMA_L2130P, 'NEMA L21-30P'), (TYPE_NEMA_L2130P, 'NEMA L21-30P'),
)), )),
@ -397,6 +421,7 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h' TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h'
TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h' TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h'
# NEMA non-locking # NEMA non-locking
TYPE_NEMA_115R = 'nema-1-15r'
TYPE_NEMA_515R = 'nema-5-15r' TYPE_NEMA_515R = 'nema-5-15r'
TYPE_NEMA_520R = 'nema-5-20r' TYPE_NEMA_520R = 'nema-5-20r'
TYPE_NEMA_530R = 'nema-5-30r' TYPE_NEMA_530R = 'nema-5-30r'
@ -405,16 +430,27 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_NEMA_620R = 'nema-6-20r' TYPE_NEMA_620R = 'nema-6-20r'
TYPE_NEMA_630R = 'nema-6-30r' TYPE_NEMA_630R = 'nema-6-30r'
TYPE_NEMA_650R = 'nema-6-50r' 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 # NEMA locking
TYPE_NEMA_L115R = 'nema-l1-15r'
TYPE_NEMA_L515R = 'nema-l5-15r' TYPE_NEMA_L515R = 'nema-l5-15r'
TYPE_NEMA_L520R = 'nema-l5-20r' TYPE_NEMA_L520R = 'nema-l5-20r'
TYPE_NEMA_L530R = 'nema-l5-30r' 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_L620R = 'nema-l6-20r'
TYPE_NEMA_L630R = 'nema-l6-30r' TYPE_NEMA_L630R = 'nema-l6-30r'
TYPE_NEMA_L650R = 'nema-l6-50r' TYPE_NEMA_L650R = 'nema-l6-50r'
TYPE_NEMA_L1030R = 'nema-l10-30r'
TYPE_NEMA_L1420R = 'nema-l14-20r' TYPE_NEMA_L1420R = 'nema-l14-20r'
TYPE_NEMA_L1430R = 'nema-l14-30r' 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_L2120R = 'nema-l21-20r'
TYPE_NEMA_L2130R = 'nema-l21-30r' TYPE_NEMA_L2130R = 'nema-l21-30r'
# California style # California style
@ -462,6 +498,7 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_IEC_3PNE9H, '3P+N+E 9H'), (TYPE_IEC_3PNE9H, '3P+N+E 9H'),
)), )),
('NEMA (Non-locking)', ( ('NEMA (Non-locking)', (
(TYPE_NEMA_115R, 'NEMA 1-15R'),
(TYPE_NEMA_515R, 'NEMA 5-15R'), (TYPE_NEMA_515R, 'NEMA 5-15R'),
(TYPE_NEMA_520R, 'NEMA 5-20R'), (TYPE_NEMA_520R, 'NEMA 5-20R'),
(TYPE_NEMA_530R, 'NEMA 5-30R'), (TYPE_NEMA_530R, 'NEMA 5-30R'),
@ -470,17 +507,28 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_NEMA_620R, 'NEMA 6-20R'), (TYPE_NEMA_620R, 'NEMA 6-20R'),
(TYPE_NEMA_630R, 'NEMA 6-30R'), (TYPE_NEMA_630R, 'NEMA 6-30R'),
(TYPE_NEMA_650R, 'NEMA 6-50R'), (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)', ( ('NEMA (Locking)', (
(TYPE_NEMA_L115R, 'NEMA L1-15R'),
(TYPE_NEMA_L515R, 'NEMA L5-15R'), (TYPE_NEMA_L515R, 'NEMA L5-15R'),
(TYPE_NEMA_L520R, 'NEMA L5-20R'), (TYPE_NEMA_L520R, 'NEMA L5-20R'),
(TYPE_NEMA_L530R, 'NEMA L5-30R'), (TYPE_NEMA_L530R, 'NEMA L5-30R'),
(TYPE_NEMA_L550R, 'NEMA L5-50R'),
(TYPE_NEMA_L615R, 'NEMA L6-15R'), (TYPE_NEMA_L615R, 'NEMA L6-15R'),
(TYPE_NEMA_L620R, 'NEMA L6-20R'), (TYPE_NEMA_L620R, 'NEMA L6-20R'),
(TYPE_NEMA_L630R, 'NEMA L6-30R'), (TYPE_NEMA_L630R, 'NEMA L6-30R'),
(TYPE_NEMA_L650R, 'NEMA L6-50R'), (TYPE_NEMA_L650R, 'NEMA L6-50R'),
(TYPE_NEMA_L1030R, 'NEMA L10-30R'),
(TYPE_NEMA_L1420R, 'NEMA L14-20R'), (TYPE_NEMA_L1420R, 'NEMA L14-20R'),
(TYPE_NEMA_L1430R, 'NEMA L14-30R'), (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_L2120R, 'NEMA L21-20R'),
(TYPE_NEMA_L2130R, 'NEMA L21-30R'), (TYPE_NEMA_L2130R, 'NEMA L21-30R'),
)), )),

View File

@ -11,8 +11,6 @@ RACK_U_HEIGHT_DEFAULT = 42
RACK_ELEVATION_BORDER_WIDTH = 2 RACK_ELEVATION_BORDER_WIDTH = 2
RACK_ELEVATION_LEGEND_WIDTH_DEFAULT = 30 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

View File

@ -79,42 +79,42 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='consoleport', model_name='consoleport',
name='_name', 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( migrations.AddField(
model_name='consoleserverport', model_name='consoleserverport',
name='_name', 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( migrations.AddField(
model_name='devicebay', model_name='devicebay',
name='_name', 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( migrations.AddField(
model_name='frontport', model_name='frontport',
name='_name', 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( migrations.AddField(
model_name='inventoryitem', model_name='inventoryitem',
name='_name', 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( migrations.AddField(
model_name='poweroutlet', model_name='poweroutlet',
name='_name', 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( migrations.AddField(
model_name='powerport', model_name='powerport',
name='_name', 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( migrations.AddField(
model_name='rearport', model_name='rearport',
name='_name', 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( migrations.RunPython(
code=naturalize_consoleports, code=naturalize_consoleports,

View File

@ -75,37 +75,37 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='consoleporttemplate', model_name='consoleporttemplate',
name='_name', 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( migrations.AddField(
model_name='consoleserverporttemplate', model_name='consoleserverporttemplate',
name='_name', 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( migrations.AddField(
model_name='devicebaytemplate', model_name='devicebaytemplate',
name='_name', 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( migrations.AddField(
model_name='frontporttemplate', model_name='frontporttemplate',
name='_name', 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( migrations.AddField(
model_name='poweroutlettemplate', model_name='poweroutlettemplate',
name='_name', 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( migrations.AddField(
model_name='powerporttemplate', model_name='powerporttemplate',
name='_name', 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( migrations.AddField(
model_name='rearporttemplate', model_name='rearporttemplate',
name='_name', 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( migrations.RunPython(
code=naturalize_consoleporttemplates, code=naturalize_consoleporttemplates,

View File

@ -43,17 +43,17 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='device', model_name='device',
name='_name', 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( migrations.AddField(
model_name='rack', model_name='rack',
name='_name', 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( migrations.AddField(
model_name='site', model_name='site',
name='_name', 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( migrations.RunPython(
code=naturalize_sites, code=naturalize_sites,

View File

@ -35,12 +35,12 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='interface', model_name='interface',
name='_name', 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( migrations.AddField(
model_name='interfacetemplate', model_name='interfacetemplate',
name='_name', 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( migrations.RunPython(
code=naturalize_interfacetemplates, code=naturalize_interfacetemplates,

View 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',
),
]

View 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),
),
]

View File

@ -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),
),
]

View File

@ -35,11 +35,12 @@ from .device_component_templates import (
PowerOutletTemplate, PowerPortTemplate, RearPortTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
) )
from .device_components import ( from .device_components import (
CableTermination, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, PowerOutlet, BaseInterface, CableTermination, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem,
PowerPort, RearPort, PowerOutlet, PowerPort, RearPort,
) )
__all__ = ( __all__ = (
'BaseInterface',
'Cable', 'Cable',
'CableTermination', 'CableTermination',
'ConsolePort', 'ConsolePort',
@ -579,7 +580,11 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
if self.pk: if self.pk:
# Validate that Rack is tall enough to house the installed Devices # 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: if top_device:
min_height = top_device.position + top_device.device_type.u_height - 1 min_height = top_device.position + top_device.device_type.u_height - 1
if self.u_height < min_height: if self.u_height < min_height:
@ -600,13 +605,13 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
# Record the original site assignment for this rack. # Record the original site assignment for this rack.
_site_id = None _site_id = None
if self.pk: 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) super().save(*args, **kwargs)
# Update racked devices if the assigned Site has been changed. # Update racked devices if the assigned Site has been changed.
if _site_id is not None and self.site_id != _site_id: 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: for device in devices:
device.site = self.site device.site = self.site
device.save() device.save()
@ -668,7 +673,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
# Add devices to rack units list # Add devices to rack units list
if self.pk: if self.pk:
queryset = Device.objects.prefetch_related( queryset = Device.objects.unrestricted().prefetch_related(
'device_type', 'device_type',
'device_type__manufacturer', 'device_type__manufacturer',
'device_role' 'device_role'
@ -744,8 +749,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
def get_elevation_svg( def get_elevation_svg(
self, self,
face=DeviceFaceChoices.FACE_FRONT, face=DeviceFaceChoices.FACE_FRONT,
unit_width=RACK_ELEVATION_UNIT_WIDTH_DEFAULT, unit_width=settings.RACK_ELEVATION_DEFAULT_UNIT_WIDTH,
unit_height=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT, unit_height=settings.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT,
legend_width=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT, legend_width=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT,
include_images=True, include_images=True,
base_url=None 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 # 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. # 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: 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 face_required = None if self.is_full_depth else d.face
u_available = d.rack.get_available_units( u_available = d.rack.get_available_units(
u_height=self.u_height, 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. # 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: 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: if racked_instance_count:
url = f"{reverse('dcim:device_list')}?manufactuer_id={self.manufacturer_id}&device_type_id={self.pk}" url = f"{reverse('dcim:device_list')}?manufactuer_id={self.manufacturer_id}&device_type_id={self.pk}"
raise ValidationError({ 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 # 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. # of the uniqueness constraint without manual intervention.
if self.name and self.tenant is None: 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({ raise ValidationError({
'name': 'A device with this name already exists.' 'name': 'A device with this name already exists.'
}) })
@ -1571,9 +1583,9 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
raise ValidationError({ raise ValidationError({
'primary_ip4': f"{self.primary_ip4} is not an IPv4 address." '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 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 pass
else: else:
raise ValidationError({ raise ValidationError({
@ -1584,9 +1596,9 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
raise ValidationError({ raise ValidationError({
'primary_ip6': f"{self.primary_ip6} is not an IPv6 address." '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 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 pass
else: else:
raise ValidationError({ 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 this is a new Device, instantiate all of the related components per the DeviceType definition
if is_new: if is_new:
ConsolePort.objects.bulk_create( 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( 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( 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( 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( 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( 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( 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( 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 # 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: for device in devices:
device.site = self.site device.site = self.site
device.rack = self.rack 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 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): def get_status_class(self):
return self.STATUS_CLASS_MAP.get(self.status) return self.STATUS_CLASS_MAP.get(self.status)
@ -1756,7 +1768,12 @@ class VirtualChassis(ChangeLoggedModel):
master = models.OneToOneField( master = models.OneToOneField(
to='Device', to='Device',
on_delete=models.PROTECT, 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( domain = models.CharField(
max_length=30, max_length=30,
@ -1766,14 +1783,14 @@ class VirtualChassis(ChangeLoggedModel):
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
csv_headers = ['master', 'domain'] csv_headers = ['name', 'domain', 'master']
class Meta: class Meta:
ordering = ['master'] ordering = ['name']
verbose_name_plural = 'virtual chassis' verbose_name_plural = 'virtual chassis'
def __str__(self): def __str__(self):
return str(self.master) if hasattr(self, 'master') else 'New Virtual Chassis' return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:virtualchassis', kwargs={'pk': self.pk}) 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 # Verify that the selected master device has been assigned to this VirtualChassis. (Skip when creating a new
# VirtualChassis.) # 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({ 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): def delete(self, *args, **kwargs):
# Check for LAG interfaces split across member chassis # Check for LAG interfaces split across member chassis
interfaces = Interface.objects.filter( interfaces = Interface.objects.unrestricted().filter(
device__in=self.members.all(), device__in=self.members.all(),
lag__isnull=False lag__isnull=False
).exclude( ).exclude(
@ -1798,8 +1815,7 @@ class VirtualChassis(ChangeLoggedModel):
) )
if interfaces: if interfaces:
raise ProtectedError( raise ProtectedError(
"Unable to delete virtual chassis {}. There are member interfaces which form a cross-chassis " f"Unable to delete virtual chassis {self}. There are member interfaces which form a cross-chassis LAG",
"LAG".format(self),
interfaces interfaces
) )
@ -1807,8 +1823,9 @@ class VirtualChassis(ChangeLoggedModel):
def to_csv(self): def to_csv(self):
return ( return (
self.master, self.name,
self.domain, self.domain,
self.master.name if self.master else None,
) )
@ -2158,12 +2175,13 @@ class Cable(ChangeLoggedModel):
return reverse('dcim:cable', args=[self.pk]) return reverse('dcim:cable', args=[self.pk])
def clean(self): def clean(self):
from circuits.models import CircuitTermination
# Validate that termination A exists # Validate that termination A exists
if not hasattr(self, 'termination_a_type'): if not hasattr(self, 'termination_a_type'):
raise ValidationError('Termination A type has not been specified') raise ValidationError('Termination A type has not been specified')
try: 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: except ObjectDoesNotExist:
raise ValidationError({ raise ValidationError({
'termination_a': 'Invalid ID for type {}'.format(self.termination_a_type) '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'): if not hasattr(self, 'termination_b_type'):
raise ValidationError('Termination B type has not been specified') raise ValidationError('Termination B type has not been specified')
try: 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: except ObjectDoesNotExist:
raise ValidationError({ raise ValidationError({
'termination_b': 'Invalid ID for type {}'.format(self.termination_b_type) '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}" 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 [ for term_a, term_b in [
(self.termination_a, self.termination_b), (self.termination_a, self.termination_b),
(self.termination_b, self.termination_a) (self.termination_b, self.termination_a)
]: ]:
if isinstance(term_a, RearPort) and term_a.positions > 1: 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( 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( 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." f"Both terminations must have the same number of positions."
) )

View File

@ -27,6 +27,11 @@ __all__ = (
class ComponentTemplateModel(models.Model): class ComponentTemplateModel(models.Model):
description = models.CharField(
max_length=200,
blank=True
)
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
class Meta: class Meta:

View File

@ -19,7 +19,6 @@ from utilities.ordering import naturalize_interface
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.query_functions import CollateAsChar from utilities.query_functions import CollateAsChar
from utilities.utils import serialize_object from utilities.utils import serialize_object
from virtualization.choices import VMInterfaceTypeChoices
__all__ = ( __all__ = (
@ -53,18 +52,12 @@ class ComponentModel(models.Model):
return self.name return self.name
def to_objectchange(self, action): def to_objectchange(self, action):
# Annotate the parent Device/VM # Annotate the parent Device
try:
parent = getattr(self, 'device', None) or getattr(self, 'virtual_machine', None)
except ObjectDoesNotExist:
# The parent device/VM has already been deleted
parent = None
return ObjectChange( return ObjectChange(
changed_object=self, changed_object=self,
object_repr=str(self), object_repr=str(self),
action=action, action=action,
related_object=parent, related_object=self.device,
object_data=serialize_object(self) object_data=serialize_object(self)
) )
@ -94,16 +87,16 @@ class CableTermination(models.Model):
object_id_field='termination_b_id' object_id_field='termination_b_id'
) )
is_path_endpoint = True
class Meta: class Meta:
abstract = True abstract = True
def trace(self): def trace(self):
""" """
Return two items: the traceable portion of a cable path, and the termination points where it splits (if any). Return three items: the traceable portion of a cable path, the termination points where it splits (if any), and
This occurs when the trace is initiated from a midpoint along a path which traverses a RearPort. In cases where the remaining positions on the position stack (if any). Splits occur when the trace is initiated from a midpoint
the originating endpoint is unknown, it is not possible to know which corresponding FrontPort to follow. 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 The path is a list representing a complete cable path, with each individual segment represented as a
three-tuple: three-tuple:
@ -123,26 +116,35 @@ class CableTermination(models.Model):
# Map a front port to its corresponding rear port # Map a front port to its corresponding rear port
if isinstance(termination, FrontPort): 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 # 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) 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 return peer_port
# Map a rear port/position to its corresponding front port # Map a rear port/position to its corresponding front port
elif isinstance(termination, RearPort): elif isinstance(termination, RearPort):
if termination.positions > 1:
# Can't map to a FrontPort without a position if there are multiple options
if not position_stack:
raise CableTraceSplit(termination)
# Can't map to a FrontPort without a position if there are multiple options front_port = position_stack.pop()
if termination.positions > 1 and not position_stack: position = front_port.rear_port_position
raise CableTraceSplit(termination)
# We can assume position 1 if the RearPort has only one position # Validate the position
position = position_stack.pop() if position_stack else 1 if position not in range(1, termination.positions + 1):
raise Exception("Invalid position for {} ({} positions): {})".format(
# Validate the position termination, termination.positions, position
if position not in range(1, termination.positions + 1): ))
raise Exception("Invalid position for {} ({} positions): {})".format( else:
termination, termination.positions, position # Don't use the stack for RearPorts with a single position. The only possible position is 1.
)) position = 1
try: try:
peer_port = FrontPort.objects.get( peer_port = FrontPort.objects.get(
@ -173,12 +175,12 @@ class CableTermination(models.Model):
if not endpoint.cable: if not endpoint.cable:
path.append((endpoint, None, None)) path.append((endpoint, None, None))
logger.debug("No cable connected") logger.debug("No cable connected")
return path, None return path, None, position_stack
# Check for loops # Check for loops
if endpoint.cable in [segment[1] for segment in path]: if endpoint.cable in [segment[1] for segment in path]:
logger.debug("Loop detected!") logger.debug("Loop detected!")
return path, None return path, None, position_stack
# Record the current segment in the path # Record the current segment in the path
far_end = endpoint.get_cable_peer() far_end = endpoint.get_cable_peer()
@ -191,10 +193,10 @@ class CableTermination(models.Model):
try: try:
endpoint = get_peer_port(far_end) endpoint = get_peer_port(far_end)
except CableTraceSplit as e: except CableTraceSplit as e:
return path, e.termination.frontports.all() return path, e.termination.frontports.all(), position_stack
if endpoint is None: if endpoint is None:
return path, None return path, None, position_stack
def get_cable_peer(self): def get_cable_peer(self):
if self.cable is None: if self.cable is None:
@ -211,7 +213,7 @@ class CableTermination(models.Model):
endpoints = [] endpoints = []
# Get the far end of the last path segment # 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] endpoint = path[-1][2]
if split_ends is not None: if split_ends is not None:
for termination in split_ends: for termination in split_ends:
@ -275,7 +277,7 @@ class ConsolePort(CableTermination, ComponentModel):
unique_together = ('device', 'name') unique_together = ('device', 'name')
def get_absolute_url(self): def get_absolute_url(self):
return self.device.get_absolute_url() return reverse('dcim:consoleport', kwargs={'pk': self.pk})
def to_csv(self): def to_csv(self):
return ( return (
@ -332,7 +334,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
unique_together = ('device', 'name') unique_together = ('device', 'name')
def get_absolute_url(self): def get_absolute_url(self):
return self.device.get_absolute_url() return reverse('dcim:consoleserverport', kwargs={'pk': self.pk})
def to_csv(self): def to_csv(self):
return ( return (
@ -415,7 +417,7 @@ class PowerPort(CableTermination, ComponentModel):
unique_together = ('device', 'name') unique_together = ('device', 'name')
def get_absolute_url(self): def get_absolute_url(self):
return self.device.get_absolute_url() return reverse('dcim:powerport', kwargs={'pk': self.pk})
def to_csv(self): def to_csv(self):
return ( return (
@ -567,7 +569,7 @@ class PowerOutlet(CableTermination, ComponentModel):
unique_together = ('device', 'name') unique_together = ('device', 'name')
def get_absolute_url(self): def get_absolute_url(self):
return self.device.get_absolute_url() return reverse('dcim:poweroutlet', kwargs={'pk': self.pk})
def to_csv(self): def to_csv(self):
return ( return (
@ -592,26 +594,7 @@ class PowerOutlet(CableTermination, ComponentModel):
# Interfaces # Interfaces
# #
@extras_features('graphs', 'export_templates', 'webhooks') class BaseInterface(models.Model):
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
)
name = models.CharField( name = models.CharField(
max_length=64 max_length=64
) )
@ -621,6 +604,42 @@ class Interface(CableTermination, ComponentModel):
max_length=100, max_length=100,
blank=True 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( label = models.CharField(
max_length=64, max_length=64,
blank=True, blank=True,
@ -656,30 +675,11 @@ class Interface(CableTermination, ComponentModel):
max_length=50, max_length=50,
choices=InterfaceTypeChoices 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( mgmt_only = models.BooleanField(
default=False, default=False,
verbose_name='OOB Management', verbose_name='OOB Management',
help_text='This interface is used only for out-of-band 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( untagged_vlan = models.ForeignKey(
to='ipam.VLAN', to='ipam.VLAN',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
@ -694,15 +694,19 @@ class Interface(CableTermination, ComponentModel):
blank=True, blank=True,
verbose_name='Tagged VLANs' 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) tags = TaggableManager(through=TaggedItem)
csv_headers = [ csv_headers = [
'device', 'virtual_machine', 'name', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'device', 'name', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'description', 'mode',
'description', 'mode',
] ]
class Meta: class Meta:
# TODO: ordering and unique_together should include virtual_machine
ordering = ('device', CollateAsChar('_name')) ordering = ('device', CollateAsChar('_name'))
unique_together = ('device', 'name') unique_together = ('device', 'name')
@ -712,7 +716,6 @@ class Interface(CableTermination, ComponentModel):
def to_csv(self): def to_csv(self):
return ( return (
self.device.identifier if self.device else None, self.device.identifier if self.device else None,
self.virtual_machine.name if self.virtual_machine else None,
self.name, self.name,
self.lag.name if self.lag else None, self.lag.name if self.lag else None,
self.get_type_display(), self.get_type_display(),
@ -726,18 +729,6 @@ class Interface(CableTermination, ComponentModel):
def clean(self): 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 # Virtual interfaces cannot be connected
if self.type in NONCONNECTABLE_IFACE_TYPES and ( if self.type in NONCONNECTABLE_IFACE_TYPES and (
self.cable or getattr(self, 'circuit_termination', False) 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]: if self.untagged_vlan and self.untagged_vlan.site not in [self.parent.site, None]:
raise ValidationError({ raise ValidationError({
'untagged_vlan': "The untagged VLAN ({}) must belong to the same site as the interface's parent " '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): def save(self, *args, **kwargs):
@ -788,21 +779,6 @@ class Interface(CableTermination, ComponentModel):
return super().save(*args, **kwargs) 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 @property
def connected_endpoint(self): def connected_endpoint(self):
""" """
@ -841,7 +817,7 @@ class Interface(CableTermination, ComponentModel):
@property @property
def parent(self): def parent(self):
return self.device or self.virtual_machine return self.device
@property @property
def is_connectable(self): def is_connectable(self):
@ -902,7 +878,6 @@ class FrontPort(CableTermination, ComponentModel):
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description'] csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
is_path_endpoint = False
class Meta: class Meta:
ordering = ('device', '_name') ordering = ('device', '_name')
@ -914,6 +889,9 @@ class FrontPort(CableTermination, ComponentModel):
def __str__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self):
return reverse('dcim:frontport', kwargs={'pk': self.pk})
def to_csv(self): def to_csv(self):
return ( return (
self.device.identifier, self.device.identifier,
@ -970,7 +948,6 @@ class RearPort(CableTermination, ComponentModel):
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = ['device', 'name', 'type', 'positions', 'description'] csv_headers = ['device', 'name', 'type', 'positions', 'description']
is_path_endpoint = False
class Meta: class Meta:
ordering = ('device', '_name') ordering = ('device', '_name')
@ -979,6 +956,9 @@ class RearPort(CableTermination, ComponentModel):
def __str__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self):
return reverse('dcim:rearport', kwargs={'pk': self.pk})
def to_csv(self): def to_csv(self):
return ( return (
self.device.identifier, self.device.identifier,
@ -1038,7 +1018,7 @@ class DeviceBay(ComponentModel):
return '{} - {}'.format(self.device.name, self.name) return '{} - {}'.format(self.device.name, self.name)
def get_absolute_url(self): def get_absolute_url(self):
return self.device.get_absolute_url() return reverse('dcim:devicebay', kwargs={'pk': self.pk})
def to_csv(self): def to_csv(self):
return ( return (
@ -1147,7 +1127,7 @@ class InventoryItem(ComponentModel):
return self.name return self.name
def get_absolute_url(self): 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): def to_csv(self):
return ( return (

View File

@ -4,20 +4,19 @@ from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from .choices import CableStatusChoices from .choices import CableStatusChoices
from .models import Cable, Device, VirtualChassis from .models import Cable, CableTermination, Device, FrontPort, RearPort, VirtualChassis
@receiver(post_save, sender=VirtualChassis) @receiver(post_save, sender=VirtualChassis)
def assign_virtualchassis_master(instance, created, **kwargs): 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: if created and instance.master:
devices = Device.objects.filter(pk=instance.master.pk) master = Device.objects.get(pk=instance.master.pk)
for device in devices: master.virtual_chassis = instance
device.virtual_chassis = instance master.vc_position = 1
device.vc_position = None master.save()
device.save()
@receiver(pre_delete, sender=VirtualChassis) @receiver(pre_delete, sender=VirtualChassis)
@ -52,7 +51,7 @@ def update_connected_endpoints(instance, **kwargs):
# Update any endpoints for this Cable. # Update any endpoints for this Cable.
endpoints = instance.termination_a.get_path_endpoints() + instance.termination_b.get_path_endpoints() endpoints = instance.termination_a.get_path_endpoints() + instance.termination_b.get_path_endpoints()
for endpoint in endpoints: for endpoint in endpoints:
path, split_ends = endpoint.trace() path, split_ends, position_stack = endpoint.trace()
# Determine overall path status (connected or planned) # Determine overall path status (connected or planned)
path_status = True path_status = True
for segment in path: for segment in path:
@ -61,9 +60,11 @@ def update_connected_endpoints(instance, **kwargs):
break break
endpoint_a = path[0][0] 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)) logger.debug("Updating path endpoints: {} <---> {}".format(endpoint_a, endpoint_b))
endpoint_a.connected_endpoint = endpoint_b endpoint_a.connected_endpoint = endpoint_b
endpoint_a.connection_status = path_status endpoint_a.connection_status = path_status

View File

@ -2,7 +2,9 @@ import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from tenancy.tables import COL_TENANT 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 ( from .models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
@ -40,69 +42,16 @@ DEVICE_LINK = """
</a> </a>
""" """
REGION_ACTIONS = """ RACKGROUP_ELEVATIONS = """
<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>
<a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&group_id={{ record.pk }}" class="btn btn-xs btn-primary" title="View 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> <i class="fa fa-eye"></i>
</a> </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 = """ RACK_DEVICE_COUNT = """
<a href="{% url 'dcim:device_list' %}?rack_id={{ record.pk }}">{{ value }}</a> <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 = """ DEVICEROLE_DEVICE_COUNT = """
<a href="{% url 'dcim:device_list' %}?role={{ record.slug }}">{{ value }}</a> <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> <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 = """ STATUS_LABEL = """
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span> <span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
""" """
@ -198,11 +138,7 @@ class RegionTable(BaseTable):
site_count = tables.Column( site_count = tables.Column(
verbose_name='Sites' verbose_name='Sites'
) )
actions = tables.TemplateColumn( actions = ButtonsColumn(Region)
template_code=REGION_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Region model = Region
@ -260,10 +196,9 @@ class RackGroupTable(BaseTable):
rack_count = tables.Column( rack_count = tables.Column(
verbose_name='Racks' verbose_name='Racks'
) )
actions = tables.TemplateColumn( actions = ButtonsColumn(
template_code=RACKGROUP_ACTIONS, model=RackGroup,
attrs={'td': {'class': 'text-right noprint'}}, prepend_template=RACKGROUP_ELEVATIONS
verbose_name=''
) )
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@ -280,11 +215,7 @@ class RackRoleTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
rack_count = tables.Column(verbose_name='Racks') rack_count = tables.Column(verbose_name='Racks')
color = tables.TemplateColumn(COLOR_LABEL) color = tables.TemplateColumn(COLOR_LABEL)
actions = tables.TemplateColumn( actions = ButtonsColumn(RackRole)
template_code=RACKROLE_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RackRole model = RackRole
@ -386,11 +317,7 @@ class RackReservationTable(BaseTable):
tags = TagColumn( tags = TagColumn(
url_name='dcim:rackreservation_list' url_name='dcim:rackreservation_list'
) )
actions = tables.TemplateColumn( actions = ButtonsColumn(RackReservation)
template_code=RACKRESERVATION_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RackReservation model = RackReservation
@ -420,11 +347,7 @@ class ManufacturerTable(BaseTable):
verbose_name='Platforms' verbose_name='Platforms'
) )
slug = tables.Column() slug = tables.Column()
actions = tables.TemplateColumn( actions = ButtonsColumn(Manufacturer, pk_field='slug')
template_code=MANUFACTURER_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Manufacturer model = Manufacturer
@ -486,22 +409,10 @@ class ConsolePortTemplateTable(ComponentTemplateTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ConsolePortTemplate model = ConsolePortTemplate
fields = ('pk', 'name', 'label', 'type', 'actions') fields = ('pk', 'name', 'label', 'type', 'description', 'actions')
empty_text = "None" 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): class ConsoleServerPortTemplateTable(ComponentTemplateTable):
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=get_component_template_actions('consoleserverporttemplate'), template_code=get_component_template_actions('consoleserverporttemplate'),
@ -511,22 +422,10 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = ('pk', 'name', 'label', 'type', 'actions') fields = ('pk', 'name', 'label', 'type', 'description', 'actions')
empty_text = "None" 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): class PowerPortTemplateTable(ComponentTemplateTable):
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=get_component_template_actions('powerporttemplate'), template_code=get_component_template_actions('powerporttemplate'),
@ -536,22 +435,10 @@ class PowerPortTemplateTable(ComponentTemplateTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = PowerPortTemplate 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" 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): class PowerOutletTemplateTable(ComponentTemplateTable):
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=get_component_template_actions('poweroutlettemplate'), template_code=get_component_template_actions('poweroutlettemplate'),
@ -561,22 +448,10 @@ class PowerOutletTemplateTable(ComponentTemplateTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = PowerOutletTemplate 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" 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): class InterfaceTemplateTable(ComponentTemplateTable):
mgmt_only = BooleanColumn( mgmt_only = BooleanColumn(
verbose_name='Management Only' verbose_name='Management Only'
@ -589,30 +464,10 @@ class InterfaceTemplateTable(ComponentTemplateTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = InterfaceTemplate model = InterfaceTemplate
fields = ('pk', 'name', 'label', 'mgmt_only', 'type', 'actions') fields = ('pk', 'name', 'label', 'mgmt_only', 'type', 'description', 'actions')
empty_text = "None" 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): class FrontPortTemplateTable(ComponentTemplateTable):
rear_port_position = tables.Column( rear_port_position = tables.Column(
verbose_name='Position' verbose_name='Position'
@ -625,22 +480,10 @@ class FrontPortTemplateTable(ComponentTemplateTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = FrontPortTemplate 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" 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): class RearPortTemplateTable(ComponentTemplateTable):
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=get_component_template_actions('rearporttemplate'), template_code=get_component_template_actions('rearporttemplate'),
@ -650,22 +493,10 @@ class RearPortTemplateTable(ComponentTemplateTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RearPortTemplate model = RearPortTemplate
fields = ('pk', 'name', 'label', 'type', 'positions', 'actions') fields = ('pk', 'name', 'label', 'type', 'positions', 'description', 'actions')
empty_text = "None" 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): class DeviceBayTemplateTable(ComponentTemplateTable):
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=get_component_template_actions('devicebaytemplate'), template_code=get_component_template_actions('devicebaytemplate'),
@ -675,7 +506,7 @@ class DeviceBayTemplateTable(ComponentTemplateTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = DeviceBayTemplate model = DeviceBayTemplate
fields = ('pk', 'name', 'label', 'actions') fields = ('pk', 'name', 'label', 'description', 'actions')
empty_text = "None" empty_text = "None"
@ -701,11 +532,8 @@ class DeviceRoleTable(BaseTable):
template_code=COLOR_LABEL, template_code=COLOR_LABEL,
verbose_name='Label' verbose_name='Label'
) )
actions = tables.TemplateColumn( vm_role = BooleanColumn()
template_code=DEVICEROLE_ACTIONS, actions = ButtonsColumn(DeviceRole, pk_field='slug')
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = DeviceRole model = DeviceRole
@ -731,11 +559,7 @@ class PlatformTable(BaseTable):
orderable=False, orderable=False,
verbose_name='VMs' verbose_name='VMs'
) )
actions = tables.TemplateColumn( actions = ButtonsColumn(Platform, pk_field='slug')
template_code=PLATFORM_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Platform model = Platform
@ -861,153 +685,108 @@ class DeviceImportTable(BaseTable):
# Device components # Device components
# #
class DeviceComponentDetailTable(BaseTable): class DeviceComponentTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(order_by=('_name',)) device = tables.Column(
cable = tables.LinkColumn() linkify=True
)
name = tables.Column(
linkify=True,
order_by=('_name',)
)
cable = tables.Column(
linkify=True
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
order_by = ('device', 'name') order_by = ('device', 'name')
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable')
sequence = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable')
class ConsolePortTable(BaseTable): class ConsolePortTable(DeviceComponentTable):
name = tables.Column(order_by=('_name',))
class Meta(BaseTable.Meta): class Meta(DeviceComponentTable.Meta):
model = ConsolePort 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): class ConsoleServerPortTable(DeviceComponentTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, ConsolePortTable.Meta): class Meta(DeviceComponentTable.Meta):
pass
class ConsoleServerPortTable(BaseTable):
name = tables.Column(order_by=('_name',))
class Meta(BaseTable.Meta):
model = ConsoleServerPort 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): class PowerPortTable(DeviceComponentTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, ConsoleServerPortTable.Meta): class Meta(DeviceComponentTable.Meta):
pass
class PowerPortTable(BaseTable):
name = tables.Column(order_by=('_name',))
class Meta(BaseTable.Meta):
model = PowerPort 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): class PowerOutletTable(DeviceComponentTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, PowerPortTable.Meta): class Meta(DeviceComponentTable.Meta):
pass
class PowerOutletTable(BaseTable):
name = tables.Column(order_by=('_name',))
class Meta(BaseTable.Meta):
model = PowerOutlet 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): class InterfaceTable(DeviceComponentTable):
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()
enabled = BooleanColumn() enabled = BooleanColumn()
class Meta(InterfaceTable.Meta): class Meta(DeviceComponentTable.Meta):
order_by = ('parent', 'name') model = Interface
fields = ('pk', 'parent', 'name', 'label', 'enabled', 'type', 'description', 'cable') fields = (
sequence = ('pk', 'parent', 'name', 'label', 'enabled', 'type', 'description', 'cable') 'pk', 'device', 'name', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'description', 'cable',
)
default_columns = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description')
class FrontPortTable(BaseTable): class FrontPortTable(DeviceComponentTable):
name = tables.Column(order_by=('_name',)) rear_port_position = tables.Column(
verbose_name='Position'
)
class Meta(BaseTable.Meta): class Meta(DeviceComponentTable.Meta):
model = FrontPort model = FrontPort
fields = ('name', 'label', 'type', 'rear_port', 'rear_port_position', 'description') fields = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable')
empty_text = "None" default_columns = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description')
class FrontPortDetailTable(DeviceComponentDetailTable): class RearPortTable(DeviceComponentTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, FrontPortTable.Meta): class Meta(DeviceComponentTable.Meta):
pass
class RearPortTable(BaseTable):
name = tables.Column(order_by=('_name',))
class Meta(BaseTable.Meta):
model = RearPort model = RearPort
fields = ('name', 'label', 'type', 'positions', 'description') fields = ('pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable')
empty_text = "None" default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
class RearPortDetailTable(DeviceComponentDetailTable): class DeviceBayTable(DeviceComponentTable):
device = tables.LinkColumn() installed_device = tables.Column(
linkify=True
)
class Meta(DeviceComponentDetailTable.Meta, RearPortTable.Meta): class Meta(DeviceComponentTable.Meta):
pass
class DeviceBayTable(BaseTable):
name = tables.Column(order_by=('_name',))
class Meta(BaseTable.Meta):
model = DeviceBay 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') fields = ('pk', 'device', 'name', 'label', 'installed_device', 'description')
sequence = ('pk', 'device', 'name', 'label', 'installed_device', 'description') default_columns = ('pk', 'device', 'name', 'label', 'installed_device', 'description')
exclude = ('cable',)
class DeviceBayImportTable(BaseTable): class InventoryItemTable(DeviceComponentTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') manufacturer = tables.Column(
installed_device = tables.LinkColumn('dcim:device', args=[Accessor('installed_device.pk')], verbose_name='Installed Device') linkify=True
)
discovered = BooleanColumn()
class Meta(BaseTable.Meta): class Meta(DeviceComponentTable.Meta):
model = DeviceBay model = InventoryItem
fields = ('device', 'name', 'installed_device', 'description') fields = (
empty_text = False '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 # Virtual chassis
# #
@ -1182,7 +938,9 @@ class InventoryItemTable(BaseTable):
class VirtualChassisTable(BaseTable): class VirtualChassisTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column( name = tables.Column(
accessor=Accessor('master__name'), linkify=True
)
master = tables.Column(
linkify=True linkify=True
) )
member_count = tables.Column( member_count = tables.Column(
@ -1194,8 +952,8 @@ class VirtualChassisTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VirtualChassis model = VirtualChassis
fields = ('pk', 'name', 'domain', 'member_count', 'tags') fields = ('pk', 'name', 'domain', 'master', 'member_count', 'tags')
default_columns = ('pk', 'name', 'domain', 'member_count') default_columns = ('pk', 'name', 'domain', 'master', 'member_count')
# #

View File

@ -28,6 +28,43 @@ class AppTest(APITestCase):
self.assertEqual(response.status_code, 200) 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): class RegionTest(APIViewTestCases.APIViewTestCase):
model = Region model = Region
brief_fields = ['id', 'name', 'site_count', 'slug', 'url'] brief_fields = ['id', 'name', 'site_count', 'slug', 'url']
@ -107,7 +144,7 @@ class SiteTest(APIViewTestCases.APIViewTestCase):
Graph.objects.bulk_create(graphs) Graph.objects.bulk_create(graphs)
self.add_permissions('dcim.view_site') 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) response = self.client.get(url, **self.header)
self.assertEqual(len(response.data), 3) self.assertEqual(len(response.data), 3)
@ -246,7 +283,7 @@ class RackTest(APIViewTestCases.APIViewTestCase):
""" """
GET a single rack elevation. GET a single rack elevation.
""" """
rack = Rack.objects.first() rack = Rack.objects.unrestricted().first()
self.add_permissions('dcim.view_rack') self.add_permissions('dcim.view_rack')
url = reverse('dcim-api:rack-elevation', kwargs={'pk': rack.pk}) 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. GET a single rack elevation in SVG format.
""" """
rack = Rack.objects.first() rack = Rack.objects.unrestricted().first()
self.add_permissions('dcim.view_rack') self.add_permissions('dcim.view_rack')
url = '{}?render=svg'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': rack.pk})) url = '{}?render=svg'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': rack.pk}))
@ -281,9 +318,7 @@ class RackReservationTest(APIViewTestCases.APIViewTestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
user = User.objects.create(username='user1', is_active=True) user = User.objects.create(username='user1', is_active=True)
site = Site.objects.create(name='Test Site 1', slug='test-site-1') site = Site.objects.create(name='Test Site 1', slug='test-site-1')
cls.racks = ( cls.racks = (
@ -878,7 +913,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
Graph.objects.bulk_create(graphs) Graph.objects.bulk_create(graphs)
self.add_permissions('dcim.view_device') 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) response = self.client.get(url, **self.header)
self.assertEqual(len(response.data), 3) 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. Check that creating a device with a duplicate name within a site fails.
""" """
device = Device.objects.first() device = Device.objects.unrestricted().first()
data = { data = {
'device_type': device.device_type.pk, 'device_type': device.device_type.pk,
'device_role': device.device_role.pk, 'device_role': device.device_role.pk,
@ -923,9 +958,10 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
class ConsolePortTest(APIViewTestCases.APIViewTestCase): class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
model = ConsolePort model = ConsolePort
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url'] brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url']
peer_termination_type = ConsoleServerPort
@classmethod @classmethod
def setUpTestData(cls): 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') class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
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):
model = ConsoleServerPort model = ConsoleServerPort
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url'] brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url']
peer_termination_type = ConsolePort
@classmethod @classmethod
def setUpTestData(cls): 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') class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
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):
model = PowerPort model = PowerPort
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url'] brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url']
peer_termination_type = PowerOutlet
@classmethod @classmethod
def setUpTestData(cls): 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') class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
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):
model = PowerOutlet model = PowerOutlet
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url'] brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url']
peer_termination_type = PowerPort
@classmethod @classmethod
def setUpTestData(cls): 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') class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
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):
model = Interface model = Interface
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url'] brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url']
peer_termination_type = Interface
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -1245,45 +1169,17 @@ class InterfaceTest(APIViewTestCases.APIViewTestCase):
Graph.objects.bulk_create(graphs) Graph.objects.bulk_create(graphs)
self.add_permissions('dcim.view_interface') 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) response = self.client.get(url, **self.header)
self.assertEqual(len(response.data), 3) self.assertEqual(len(response.data), 3)
self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?interface=Interface 1&foo=1') 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') class FrontPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
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):
model = FrontPort model = FrontPort
brief_fields = ['cable', 'device', 'id', 'name', 'url'] brief_fields = ['cable', 'device', 'id', 'name', 'url']
peer_termination_type = Interface
@classmethod @classmethod
def setUpTestData(cls): 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') class RearPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
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):
model = RearPort model = RearPort
brief_fields = ['cable', 'device', 'id', 'name', 'url'] brief_fields = ['cable', 'device', 'id', 'name', 'url']
peer_termination_type = Interface
@classmethod @classmethod
def setUpTestData(cls): 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): class DeviceBayTest(APIViewTestCases.APIViewTestCase):
model = DeviceBay model = DeviceBay
@ -1640,11 +1479,11 @@ class ConnectionTest(APITestCase):
response = self.client.post(url, data, format='json', **self.header) response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED) 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']) cable = Cable.objects.unrestricted().get(pk=response.data['id'])
consoleport1 = ConsolePort.objects.get(pk=consoleport1.pk) consoleport1 = ConsolePort.objects.unrestricted().get(pk=consoleport1.pk)
consoleserverport1 = ConsoleServerPort.objects.get(pk=consoleserverport1.pk) consoleserverport1 = ConsoleServerPort.objects.unrestricted().get(pk=consoleserverport1.pk)
self.assertEqual(cable.termination_a, consoleport1) self.assertEqual(cable.termination_a, consoleport1)
self.assertEqual(cable.termination_b, consoleserverport1) self.assertEqual(cable.termination_b, consoleserverport1)
@ -1705,12 +1544,12 @@ class ConnectionTest(APITestCase):
response = self.client.post(url, data, format='json', **self.header) response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED) 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_a.cable, cable)
self.assertEqual(cable.termination_b.cable, cable) self.assertEqual(cable.termination_b.cable, cable)
consoleport1 = ConsolePort.objects.get(pk=consoleport1.pk) consoleport1 = ConsolePort.objects.unrestricted().get(pk=consoleport1.pk)
consoleserverport1 = ConsoleServerPort.objects.get(pk=consoleserverport1.pk) consoleserverport1 = ConsoleServerPort.objects.unrestricted().get(pk=consoleserverport1.pk)
self.assertEqual(consoleport1.connected_endpoint, consoleserverport1) self.assertEqual(consoleport1.connected_endpoint, consoleserverport1)
self.assertEqual(consoleserverport1.connected_endpoint, consoleport1) self.assertEqual(consoleserverport1.connected_endpoint, consoleport1)
@ -1735,11 +1574,11 @@ class ConnectionTest(APITestCase):
response = self.client.post(url, data, format='json', **self.header) response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED) 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']) cable = Cable.objects.unrestricted().get(pk=response.data['id'])
powerport1 = PowerPort.objects.get(pk=powerport1.pk) powerport1 = PowerPort.objects.unrestricted().get(pk=powerport1.pk)
poweroutlet1 = PowerOutlet.objects.get(pk=poweroutlet1.pk) poweroutlet1 = PowerOutlet.objects.unrestricted().get(pk=poweroutlet1.pk)
self.assertEqual(cable.termination_a, powerport1) self.assertEqual(cable.termination_a, powerport1)
self.assertEqual(cable.termination_b, poweroutlet1) self.assertEqual(cable.termination_b, poweroutlet1)
@ -1771,11 +1610,11 @@ class ConnectionTest(APITestCase):
response = self.client.post(url, data, format='json', **self.header) response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED) 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']) cable = Cable.objects.unrestricted().get(pk=response.data['id'])
interface1 = Interface.objects.get(pk=interface1.pk) interface1 = Interface.objects.unrestricted().get(pk=interface1.pk)
interface2 = Interface.objects.get(pk=interface2.pk) interface2 = Interface.objects.unrestricted().get(pk=interface2.pk)
self.assertEqual(cable.termination_a, interface1) self.assertEqual(cable.termination_a, interface1)
self.assertEqual(cable.termination_b, interface2) self.assertEqual(cable.termination_b, interface2)
@ -1836,12 +1675,12 @@ class ConnectionTest(APITestCase):
response = self.client.post(url, data, format='json', **self.header) response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED) 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_a.cable, cable)
self.assertEqual(cable.termination_b.cable, cable) self.assertEqual(cable.termination_b.cable, cable)
interface1 = Interface.objects.get(pk=interface1.pk) interface1 = Interface.objects.unrestricted().get(pk=interface1.pk)
interface2 = Interface.objects.get(pk=interface2.pk) interface2 = Interface.objects.unrestricted().get(pk=interface2.pk)
self.assertEqual(interface1.connected_endpoint, interface2) self.assertEqual(interface1.connected_endpoint, interface2)
self.assertEqual(interface2.connected_endpoint, interface1) self.assertEqual(interface2.connected_endpoint, interface1)
@ -1875,11 +1714,11 @@ class ConnectionTest(APITestCase):
response = self.client.post(url, data, format='json', **self.header) response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED) 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']) cable = Cable.objects.unrestricted().get(pk=response.data['id'])
interface1 = Interface.objects.get(pk=interface1.pk) interface1 = Interface.objects.unrestricted().get(pk=interface1.pk)
circuittermination1 = CircuitTermination.objects.get(pk=circuittermination1.pk) circuittermination1 = CircuitTermination.objects.unrestricted().get(pk=circuittermination1.pk)
self.assertEqual(cable.termination_a, interface1) self.assertEqual(cable.termination_a, interface1)
self.assertEqual(cable.termination_b, circuittermination1) self.assertEqual(cable.termination_b, circuittermination1)
@ -1949,12 +1788,12 @@ class ConnectionTest(APITestCase):
response = self.client.post(url, data, format='json', **self.header) response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED) 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_a.cable, cable)
self.assertEqual(cable.termination_b.cable, cable) self.assertEqual(cable.termination_b.cable, cable)
interface1 = Interface.objects.get(pk=interface1.pk) interface1 = Interface.objects.unrestricted().get(pk=interface1.pk)
circuittermination1 = CircuitTermination.objects.get(pk=circuittermination1.pk) circuittermination1 = CircuitTermination.objects.unrestricted().get(pk=circuittermination1.pk)
self.assertEqual(interface1.connected_endpoint, circuittermination1) self.assertEqual(interface1.connected_endpoint, circuittermination1)
self.assertEqual(circuittermination1.connected_endpoint, interface1) self.assertEqual(circuittermination1.connected_endpoint, interface1)
@ -2003,7 +1842,7 @@ class ConnectedDeviceTest(APITestCase):
class VirtualChassisTest(APIViewTestCases.APIViewTestCase): class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
model = VirtualChassis model = VirtualChassis
brief_fields = ['id', 'master', 'member_count', 'url'] brief_fields = ['id', 'master', 'member_count', 'name', 'url']
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -2040,34 +1879,35 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
# Create three VirtualChassis with three members each # Create three VirtualChassis with three members each
virtual_chassis = ( virtual_chassis = (
VirtualChassis(master=devices[0], domain='domain-1'), VirtualChassis(name='Virtual Chassis 1', master=devices[0], domain='domain-1'),
VirtualChassis(master=devices[3], domain='domain-2'), VirtualChassis(name='Virtual Chassis 2', master=devices[3], domain='domain-2'),
VirtualChassis(master=devices[6], domain='domain-3'), VirtualChassis(name='Virtual Chassis 3', master=devices[6], domain='domain-3'),
) )
VirtualChassis.objects.bulk_create(virtual_chassis) VirtualChassis.objects.bulk_create(virtual_chassis)
Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis[0], vc_position=2) Device.objects.unrestricted().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.unrestricted().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.unrestricted().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.unrestricted().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.unrestricted().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[8].pk).update(virtual_chassis=virtual_chassis[2], vc_position=3)
cls.update_data = { cls.update_data = {
'master': devices[1].pk, 'name': 'Virtual Chassis X',
'domain': 'domain-x', 'domain': 'domain-x',
'master': devices[1].pk,
} }
cls.create_data = [ cls.create_data = [
{ {
'master': devices[9].pk, 'name': 'Virtual Chassis 4',
'domain': 'domain-4', 'domain': 'domain-4',
}, },
{ {
'master': devices[10].pk, 'name': 'Virtual Chassis 5',
'domain': 'domain-5', 'domain': 'domain-5',
}, },
{ {
'master': devices[11].pk, 'name': 'Virtual Chassis 6',
'domain': 'domain-6', 'domain': 'domain-6',
}, },
] ]

View File

@ -1254,8 +1254,8 @@ class DeviceTestCase(TestCase):
# Assign primary IPs for filtering # Assign primary IPs for filtering
ipaddresses = ( ipaddresses = (
IPAddress(address='192.0.2.1/24', interface=interfaces[0]), IPAddress(address='192.0.2.1/24', assigned_object=interfaces[0]),
IPAddress(address='192.0.2.2/24', interface=interfaces[1]), IPAddress(address='192.0.2.2/24', assigned_object=interfaces[1]),
) )
IPAddress.objects.bulk_create(ipaddresses) IPAddress.objects.bulk_create(ipaddresses)
Device.objects.filter(pk=devices[0].pk).update(primary_ip4=ipaddresses[0]) Device.objects.filter(pk=devices[0].pk).update(primary_ip4=ipaddresses[0])

View File

@ -363,6 +363,7 @@ class CableTestCase(TestCase):
) )
self.interface1 = Interface.objects.create(device=self.device1, name='eth0') self.interface1 = Interface.objects.create(device=self.device1, name='eth0')
self.interface2 = Interface.objects.create(device=self.device2, 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 = Cable(termination_a=self.interface1, termination_b=self.interface2)
self.cable.save() self.cable.save()
@ -370,10 +371,27 @@ class CableTestCase(TestCase):
self.patch_pannel = Device.objects.create( self.patch_pannel = Device.objects.create(
device_type=devicetype, device_role=devicerole, name='TestPatchPannel', site=site 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.rear_port1 = RearPort.objects.create(device=self.patch_pannel, name='RP1', type='8p8c')
self.front_port = FrontPort.objects.create( self.front_port1 = FrontPort.objects.create(
device=self.patch_pannel, name='F1', type=1000, rear_port=self.rear_port 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): def test_cable_creation(self):
""" """
@ -405,7 +423,7 @@ class CableTestCase(TestCase):
cable = Cable.objects.filter(pk=self.cable.pk).first() cable = Cable.objects.filter(pk=self.cable.pk).first()
self.assertIsNone(cable) 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 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 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): with self.assertRaises(ValidationError):
cable.clean() cable.clean()
@ -439,7 +457,94 @@ class CableTestCase(TestCase):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
cable.clean() 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 A cable cannot terminate to a virtual interface
""" """
@ -448,7 +553,7 @@ class CableTestCase(TestCase):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
cable.clean() 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 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 2', site=site),
Device(device_type=devicetype, device_role=devicerole, name='Panel 3', 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 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) 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) rearport = RearPort.objects.create(device=patch_panel, name='Rear Port 1', positions=4, type=PortTypeChoices.TYPE_8P8C)
FrontPort.objects.bulk_create(( FrontPort.objects.bulk_create((
FrontPort(device=patch_panel, name='Front Port 1', rear_port=rearport, rear_port_position=1, type=PortTypeChoices.TYPE_8P8C), 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), 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): def test_direct_connection(self):
""" """
Test a direct connection between two interfaces. 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_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1') termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
) )
cable.full_clean()
cable.save() cable.save()
# Retrieve endpoints # Retrieve endpoints
@ -551,22 +666,25 @@ class CablePathTestCase(TestCase):
def test_connection_via_single_rear_port(self): 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 1 2
[Device 1] ----- [Panel 1] ----- [Device 2] [Device 1] ----- [Panel 5] ----- [Device 2]
Iface1 FP1 RP1 Iface1 Iface1 FP1 RP1 Iface1
""" """
# Create cables # Create cables (FP first, RP second)
cable1 = Cable( cable1 = Cable(
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), 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() cable1.save()
cable2 = Cable( cable2 = 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'),
termination_a=Interface.objects.get(device__name='Device 2', name='Interface 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() cable2.save()
# Retrieve endpoints # Retrieve endpoints
@ -592,6 +710,97 @@ class CablePathTestCase(TestCase):
self.assertIsNone(endpoint_a.connection_status) self.assertIsNone(endpoint_a.connection_status)
self.assertIsNone(endpoint_b.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): def test_connections_via_patch(self):
""" """
Test two connections via patched rear ports: 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_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 1', name='Front Port 1')
) )
cable1.full_clean()
cable1.save() cable1.save()
cable2 = Cable( cable2 = Cable(
termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1'), 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') termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1')
) )
cable2.full_clean()
cable2.save() cable2.save()
cable3 = Cable( cable3 = Cable(
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'), 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') termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
) )
cable3.full_clean()
cable3.save() cable3.save()
cable4 = Cable( cable4 = Cable(
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'), 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') termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2')
) )
cable4.full_clean()
cable4.save() cable4.save()
cable5 = Cable( cable5 = Cable(
termination_a=Interface.objects.get(device__name='Device 4', name='Interface 1'), 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') termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2')
) )
cable5.full_clean()
cable5.save() cable5.save()
# Retrieve endpoints # Retrieve endpoints
@ -693,43 +907,51 @@ class CablePathTestCase(TestCase):
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), 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 1', name='Front Port 1')
) )
cable1.full_clean()
cable1.save() cable1.save()
cable2 = Cable( cable2 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1'), 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') termination_b=FrontPort.objects.get(device__name='Panel 3', name='Front Port 1')
) )
cable2.full_clean()
cable2.save() cable2.save()
cable3 = Cable( cable3 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 1'), 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') termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
) )
cable3.full_clean()
cable3.save() cable3.save()
cable4 = Cable( cable4 = Cable(
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'), 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') termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
) )
cable4.full_clean()
cable4.save() cable4.save()
cable5 = Cable( cable5 = Cable(
termination_a=RearPort.objects.get(device__name='Panel 3', name='Rear Port 1'), 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') termination_b=RearPort.objects.get(device__name='Panel 4', name='Rear Port 1')
) )
cable5.full_clean()
cable5.save() cable5.save()
cable6 = Cable( cable6 = Cable(
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'), 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') termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2')
) )
cable6.full_clean()
cable6.save() cable6.save()
cable7 = Cable( cable7 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2'), 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') termination_b=FrontPort.objects.get(device__name='Panel 3', name='Front Port 2')
) )
cable7.full_clean()
cable7.save() cable7.save()
cable8 = Cable( cable8 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 2'), 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') termination_b=Interface.objects.get(device__name='Device 4', name='Interface 1')
) )
cable8.full_clean()
cable8.save() cable8.save()
# Retrieve endpoints # Retrieve endpoints
@ -789,38 +1011,45 @@ class CablePathTestCase(TestCase):
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), 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 1', name='Front Port 1')
) )
cable1.full_clean()
cable1.save() cable1.save()
cable2 = Cable( cable2 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 1'), 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') termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
) )
cable2.full_clean()
cable2.save() cable2.save()
cable3 = Cable( cable3 = Cable(
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'), 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') termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1')
) )
cable3.full_clean()
cable3.save() cable3.save()
cable4 = Cable( cable4 = Cable(
termination_a=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1'), 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') termination_b=RearPort.objects.get(device__name='Panel 3', name='Rear Port 1')
) )
cable4.full_clean()
cable4.save() cable4.save()
cable5 = Cable( cable5 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 3', name='Front Port 1'), 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') termination_b=RearPort.objects.get(device__name='Panel 4', name='Rear Port 1')
) )
cable5.full_clean()
cable5.save() cable5.save()
cable6 = Cable( cable6 = Cable(
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'), 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') termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2')
) )
cable6.full_clean()
cable6.save() cable6.save()
cable7 = Cable( cable7 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 2'), 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') termination_b=Interface.objects.get(device__name='Device 4', name='Interface 1')
) )
cable7.full_clean()
cable7.save() cable7.save()
# Retrieve endpoints # Retrieve endpoints
@ -870,11 +1099,13 @@ class CablePathTestCase(TestCase):
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=CircuitTermination.objects.get(term_side='A') termination_b=CircuitTermination.objects.get(term_side='A')
) )
cable1.full_clean()
cable1.save() cable1.save()
cable2 = Cable( cable2 = Cable(
termination_a=CircuitTermination.objects.get(term_side='Z'), termination_a=CircuitTermination.objects.get(term_side='Z'),
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1') termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
) )
cable2.full_clean()
cable2.save() cable2.save()
# Retrieve endpoints # Retrieve endpoints
@ -903,30 +1134,34 @@ class CablePathTestCase(TestCase):
def test_connection_via_patched_circuit(self): def test_connection_via_patched_circuit(self):
""" """
1 2 3 4 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 Iface1 FP1 RP1 A Z RP1 FP1 Iface1
""" """
# Create cables # Create cables
cable1 = Cable( cable1 = Cable(
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'), 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() cable1.save()
cable2 = Cable( 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') termination_b=CircuitTermination.objects.get(term_side='A')
) )
cable2.full_clean()
cable2.save() cable2.save()
cable3 = Cable( cable3 = Cable(
termination_a=CircuitTermination.objects.get(term_side='Z'), 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() cable3.save()
cable4 = Cable( 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') termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
) )
cable4.full_clean()
cable4.save() cable4.save()
# Retrieve endpoints # Retrieve endpoints

View File

@ -813,14 +813,7 @@ class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase
} }
# TODO: Change base class to DeviceComponentTemplateViewTestCase class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
# Blocked by absence of bulk edit view for DeviceBays
class DeviceBayTemplateTestCase(
ViewTestCases.EditObjectViewTestCase,
ViewTestCases.DeleteObjectViewTestCase,
ViewTestCases.BulkCreateObjectsViewTestCase,
ViewTestCases.BulkDeleteObjectsViewTestCase
):
model = DeviceBayTemplate model = DeviceBayTemplate
@classmethod @classmethod
@ -848,6 +841,10 @@ class DeviceBayTemplateTestCase(
'name_pattern': 'Device Bay Template [4-6]', 'name_pattern': 'Device Bay Template [4-6]',
} }
cls.bulk_edit_data = {
'description': 'Foo bar',
}
class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase): class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = DeviceRole model = DeviceRole
@ -1194,10 +1191,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
) )
class InterfaceTestCase( class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
ViewTestCases.GetObjectViewTestCase,
ViewTestCases.DeviceComponentViewTestCase,
):
model = Interface model = Interface
@classmethod @classmethod
@ -1563,16 +1557,7 @@ class CableTestCase(
} }
# TODO: Change base class to PrimaryObjectViewTestCase class VirtualChassisTestCase(ViewTestCases.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
):
model = VirtualChassis model = VirtualChassis
@classmethod @classmethod
@ -1587,7 +1572,6 @@ class VirtualChassisTestCase(
name='Device Role', slug='device-role-1' name='Device Role', slug='device-role-1'
) )
# Create 9 member Devices
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 1', site=site),
Device(device_type=device_type, device_role=device_role, name='Device 2', 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 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 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 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) Device.objects.bulk_create(devices)
# Create three VirtualChassis with two members each # Create three VirtualChassis with three members each
vc1 = VirtualChassis.objects.create(master=devices[0], domain='domain-1') 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[1].pk).update(virtual_chassis=vc1, vc_position=2)
Device.objects.filter(pk=devices[2].pk).update(virtual_chassis=vc1, vc_position=3) 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[4].pk).update(virtual_chassis=vc2, vc_position=2)
Device.objects.filter(pk=devices[5].pk).update(virtual_chassis=vc2, vc_position=3) 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[7].pk).update(virtual_chassis=vc3, vc_position=2)
Device.objects.filter(pk=devices[8].pk).update(virtual_chassis=vc3, vc_position=3) Device.objects.filter(pk=devices[8].pk).update(virtual_chassis=vc3, vc_position=3)
cls.form_data = { cls.form_data = {
'master': devices[1].pk, 'name': 'VC4',
'domain': 'domain-x', 'domain': 'domain-4',
# Management form data for VC members # Management form data for VC members
'form-TOTAL_FORMS': 0, 'form-TOTAL_FORMS': 0,
'form-INITIAL_FORMS': 3, 'form-INITIAL_FORMS': 3,
@ -1622,6 +1612,13 @@ class VirtualChassisTestCase(
'form-MAX_NUM_FORMS': 1000, '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 = { cls.bulk_edit_data = {
'domain': 'domain-x', 'domain': 'domain-x',
} }

View File

@ -4,9 +4,9 @@ from extras.views import ObjectChangeLogView, ImageAttachmentEditView
from ipam.views import ServiceEditView from ipam.views import ServiceEditView
from . import views from . import views
from .models import ( from .models import (
Cable, ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, FrontPort, Interface, Manufacturer, Platform, Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, FrontPort, Interface,
PowerFeed, PowerPanel, PowerPort, PowerOutlet, Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site, InventoryItem, Manufacturer, Platform, PowerFeed, PowerPanel, PowerPort, PowerOutlet, Rack, RackGroup,
VirtualChassis, RackReservation, RackRole, RearPort, Region, Site, VirtualChassis,
) )
app_name = 'dcim' app_name = 'dcim'
@ -18,6 +18,7 @@ urlpatterns = [
path('regions/import/', views.RegionBulkImportView.as_view(), name='region_import'), path('regions/import/', views.RegionBulkImportView.as_view(), name='region_import'),
path('regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'), 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>/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}), path('regions/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}),
# Sites # Sites
@ -38,6 +39,7 @@ urlpatterns = [
path('rack-groups/import/', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'), 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/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>/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}), path('rack-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}),
# Rack roles # Rack roles
@ -46,6 +48,7 @@ urlpatterns = [
path('rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'), 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/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>/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}), path('rack-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}),
# Rack reservations # Rack reservations
@ -78,6 +81,7 @@ urlpatterns = [
path('manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'), path('manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
path('manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'), 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>/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}), path('manufacturers/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}),
# Device types # Device types
@ -142,7 +146,7 @@ urlpatterns = [
# Device bay templates # Device bay templates
path('device-bay-templates/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicebaytemplate_add'), 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/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>/edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'),
path('device-bay-templates/<int:pk>/delete/', views.DeviceBayTemplateDeleteView.as_view(), name='devicebaytemplate_delete'), 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/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
path('device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'), 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>/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}), path('device-roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
# Platforms # Platforms
@ -161,6 +166,7 @@ urlpatterns = [
path('platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'), path('platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'),
path('platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'), 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>/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}), path('platforms/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}),
# Devices # Devices
@ -187,12 +193,15 @@ urlpatterns = [
path('console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'), 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/import/', views.ConsolePortBulkImportView.as_view(), name='consoleport_import'),
path('console-ports/edit/', views.ConsolePortBulkEditView.as_view(), name='consoleport_bulk_edit'), 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/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>/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>/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: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'), path('devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
# Console server ports # 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/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/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/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>/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>/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: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'), path('devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
# Power ports # Power ports
@ -214,12 +225,15 @@ urlpatterns = [
path('power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'), 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/import/', views.PowerPortBulkImportView.as_view(), name='powerport_import'),
path('power-ports/edit/', views.PowerPortBulkEditView.as_view(), name='powerport_bulk_edit'), 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/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>/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>/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: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'), path('devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
# Power outlets # Power outlets
@ -230,10 +244,12 @@ urlpatterns = [
path('power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'), 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/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
path('power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'), 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>/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>/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: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'), path('devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
# Interfaces # Interfaces
@ -244,12 +260,12 @@ urlpatterns = [
path('interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'), path('interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
path('interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'), path('interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
path('interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), 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>/', views.InterfaceView.as_view(), name='interface'),
path('interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'), 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>/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>/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: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'), path('devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
# Front ports # Front ports
@ -260,10 +276,12 @@ urlpatterns = [
path('front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'), 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/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'),
path('front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'), 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>/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>/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: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'), # path('devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
# Rear ports # Rear ports
@ -274,10 +292,12 @@ urlpatterns = [
path('rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'), 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/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'),
path('rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'), 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>/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>/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: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'), path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
# Device bays # Device bays
@ -287,8 +307,10 @@ urlpatterns = [
path('device-bays/edit/', views.DeviceBayBulkEditView.as_view(), name='devicebay_bulk_edit'), 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/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
path('device-bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'), 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>/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>/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>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
path('device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'), 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'), 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/add/', views.InventoryItemCreateView.as_view(), name='inventoryitem_add'),
path('inventory-items/import/', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'), path('inventory-items/import/', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'),
path('inventory-items/edit/', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'), 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/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>/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>/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 # Cables
path('cables/', views.CableListView.as_view(), name='cable_list'), path('cables/', views.CableListView.as_view(), name='cable_list'),
@ -321,6 +346,7 @@ urlpatterns = [
# Virtual chassis # Virtual chassis
path('virtual-chassis/', views.VirtualChassisListView.as_view(), name='virtualchassis_list'), path('virtual-chassis/', views.VirtualChassisListView.as_view(), name='virtualchassis_list'),
path('virtual-chassis/add/', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'), 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/edit/', views.VirtualChassisBulkEditView.as_view(), name='virtualchassis_bulk_edit'),
path('virtual-chassis/delete/', views.VirtualChassisBulkDeleteView.as_view(), name='virtualchassis_bulk_delete'), path('virtual-chassis/delete/', views.VirtualChassisBulkDeleteView.as_view(), name='virtualchassis_bulk_delete'),
path('virtual-chassis/<int:pk>/', views.VirtualChassisView.as_view(), name='virtualchassis'), path('virtual-chassis/<int:pk>/', views.VirtualChassisView.as_view(), name='virtualchassis'),

File diff suppressed because it is too large Load Diff

View File

@ -110,7 +110,7 @@ class ExportTemplateViewSet(ModelViewSet):
# #
class TagViewSet(ModelViewSet): class TagViewSet(ModelViewSet):
queryset = Tag.restricted.annotate( queryset = Tag.objects.annotate(
tagged_items=Count('extras_taggeditem_items', distinct=True) tagged_items=Count('extras_taggeditem_items', distinct=True)
) )
serializer_class = serializers.TagSerializer serializer_class = serializers.TagSerializer

View File

@ -6,6 +6,7 @@ from django import get_version
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users', 'virtualization'] APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users', 'virtualization']
@ -52,6 +53,7 @@ class Command(BaseCommand):
pass pass
# Additional objects to include # Additional objects to include
namespace['ContentType'] = ContentType
namespace['User'] = User namespace['User'] = User
# Load convenience commands # Load convenience commands

View File

@ -540,7 +540,7 @@ class ConfigContextModel(models.Model):
# Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs # Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
data = OrderedDict() 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) data = deepmerge(data, context.data)
# If the object has local config context data defined, merge it last # If the object has local config context data defined, merge it last

View File

@ -22,14 +22,10 @@ class Tag(TagBase, ChangeLoggedModel):
blank=True, blank=True,
) )
objects = models.Manager() objects = RestrictedQuerySet.as_manager()
restricted = RestrictedQuerySet.as_manager()
csv_headers = ['name', 'slug', 'color', 'description'] csv_headers = ['name', 'slug', 'color', 'description']
def get_absolute_url(self):
return reverse('extras:tag', args=[self.slug])
def slugify(self, tag, i=None): def slugify(self, tag, i=None):
# Allow Unicode in Tag slugs (avoids empty slugs for Tags with all-Unicode names) # Allow Unicode in Tag slugs (avoids empty slugs for Tags with all-Unicode names)
slug = slugify(tag, allow_unicode=True) slug = slugify(tag, allow_unicode=True)

View File

@ -173,7 +173,7 @@ class ChoiceVar(ScriptVariable):
class ObjectVar(ScriptVariable): class ObjectVar(ScriptVariable):
""" """
NetBox object representation. The provided QuerySet will determine the choices available. A single object within NetBox.
""" """
form_field = DynamicModelChoiceField form_field = DynamicModelChoiceField

View File

@ -1,21 +1,9 @@
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor 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 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 = """ TAGGED_ITEM = """
{% if value.get_absolute_url %} {% if value.get_absolute_url %}
<a href="{{ value.get_absolute_url }}">{{ value }}</a> <a href="{{ value.get_absolute_url }}">{{ value }}</a>
@ -64,16 +52,8 @@ OBJECTCHANGE_REQUEST_ID = """
class TagTable(BaseTable): class TagTable(BaseTable):
pk = ToggleColumn() 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() color = ColorColumn()
actions = ButtonsColumn(Tag, pk_field='slug')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Tag model = Tag

View File

@ -10,7 +10,7 @@ from extras.models import ConfigContext, ObjectChange, Tag
from utilities.testing import ViewTestCases, TestCase from utilities.testing import ViewTestCases, TestCase
class TagTestCase(ViewTestCases.PrimaryObjectViewTestCase): class TagTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = Tag model = Tag
@classmethod @classmethod

View File

@ -13,7 +13,6 @@ urlpatterns = [
path('tags/import/', views.TagBulkImportView.as_view(), name='tag_import'), path('tags/import/', views.TagBulkImportView.as_view(), name='tag_import'),
path('tags/edit/', views.TagBulkEditView.as_view(), name='tag_bulk_edit'), path('tags/edit/', views.TagBulkEditView.as_view(), name='tag_bulk_edit'),
path('tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'), 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>/edit/', views.TagEditView.as_view(), name='tag_edit'),
path('tags/<str:slug>/delete/', views.TagDeleteView.as_view(), name='tag_delete'), 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}), path('tags/<str:slug>/changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}),

View File

@ -4,13 +4,15 @@ from django import template
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.contenttypes.models import ContentType 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.http import Http404, HttpResponseForbidden
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.views.generic import View from django.views.generic import View
from django_tables2 import RequestConfig 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.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator from utilities.paginator import EnhancedPaginator
from utilities.utils import copy_safe_request, shallow_compare_dict 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, BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
ContentTypePermissionRequiredMixin, ContentTypePermissionRequiredMixin,
) )
from virtualization.models import Cluster, ClusterGroup
from . import filters, forms, tables from . import filters, forms, tables
from .choices import JobResultStatusChoices from .choices import JobResultStatusChoices
from .models import ConfigContext, ImageAttachment, ObjectChange, Report, JobResult, Script, Tag, TaggedItem 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): class TagListView(ObjectListView):
queryset = Tag.restricted.annotate( queryset = Tag.objects.annotate(
items=Count('extras_taggeditem_items', distinct=True) items=Count('extras_taggeditem_items', distinct=True)
).order_by( ).order_by(
'name' 'name'
@ -40,71 +43,39 @@ class TagListView(ObjectListView):
table = tables.TagTable 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): class TagEditView(ObjectEditView):
queryset = Tag.restricted.all() queryset = Tag.objects.all()
model_form = forms.TagForm model_form = forms.TagForm
default_return_url = 'extras:tag_list'
template_name = 'extras/tag_edit.html' template_name = 'extras/tag_edit.html'
class TagDeleteView(ObjectDeleteView): class TagDeleteView(ObjectDeleteView):
queryset = Tag.restricted.all() queryset = Tag.objects.all()
default_return_url = 'extras:tag_list'
class TagBulkImportView(BulkImportView): class TagBulkImportView(BulkImportView):
queryset = Tag.restricted.all() queryset = Tag.objects.all()
model_form = forms.TagCSVForm model_form = forms.TagCSVForm
table = tables.TagTable table = tables.TagTable
default_return_url = 'extras:tag_list'
class TagBulkEditView(BulkEditView): class TagBulkEditView(BulkEditView):
queryset = Tag.restricted.annotate( queryset = Tag.objects.annotate(
items=Count('extras_taggeditem_items', distinct=True) items=Count('extras_taggeditem_items', distinct=True)
).order_by( ).order_by(
'name' 'name'
) )
table = tables.TagTable table = tables.TagTable
form = forms.TagBulkEditForm form = forms.TagBulkEditForm
default_return_url = 'extras:tag_list'
class TagBulkDeleteView(BulkDeleteView): class TagBulkDeleteView(BulkDeleteView):
queryset = Tag.restricted.annotate( queryset = Tag.objects.annotate(
items=Count('extras_taggeditem_items') items=Count('extras_taggeditem_items')
).order_by( ).order_by(
'name' 'name'
) )
table = tables.TagTable table = tables.TagTable
default_return_url = 'extras:tag_list'
# #
@ -123,6 +94,18 @@ class ConfigContextView(ObjectView):
queryset = ConfigContext.objects.all() queryset = ConfigContext.objects.all()
def get(self, request, pk): 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) configcontext = get_object_or_404(self.queryset, pk=pk)
# Determine user's preferred output format # Determine user's preferred output format
@ -144,7 +127,6 @@ class ConfigContextView(ObjectView):
class ConfigContextEditView(ObjectEditView): class ConfigContextEditView(ObjectEditView):
queryset = ConfigContext.objects.all() queryset = ConfigContext.objects.all()
model_form = forms.ConfigContextForm model_form = forms.ConfigContextForm
default_return_url = 'extras:configcontext_list'
template_name = 'extras/configcontext_edit.html' template_name = 'extras/configcontext_edit.html'
@ -153,18 +135,15 @@ class ConfigContextBulkEditView(BulkEditView):
filterset = filters.ConfigContextFilterSet filterset = filters.ConfigContextFilterSet
table = tables.ConfigContextTable table = tables.ConfigContextTable
form = forms.ConfigContextBulkEditForm form = forms.ConfigContextBulkEditForm
default_return_url = 'extras:configcontext_list'
class ConfigContextDeleteView(ObjectDeleteView): class ConfigContextDeleteView(ObjectDeleteView):
queryset = ConfigContext.objects.all() queryset = ConfigContext.objects.all()
default_return_url = 'extras:configcontext_list'
class ConfigContextBulkDeleteView(BulkDeleteView): class ConfigContextBulkDeleteView(BulkDeleteView):
queryset = ConfigContext.objects.all() queryset = ConfigContext.objects.all()
table = tables.ConfigContextTable table = tables.ConfigContextTable
default_return_url = 'extras:configcontext_list'
class ObjectConfigContextView(ObjectView): class ObjectConfigContextView(ObjectView):
@ -264,9 +243,11 @@ class ObjectChangeLogView(View):
def get(self, request, model, **kwargs): def get(self, request, model, **kwargs):
# Get object my model and kwargs (e.g. slug='foo') # Handle QuerySet restriction of parent object if needed
queryset = model.objects.restrict(request.user, 'view') if hasattr(model.objects, 'restrict'):
obj = get_object_or_404(queryset, **kwargs) 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) # Gather all changes for this object (and its related objects)
content_type = ContentType.objects.get_for_model(model) content_type = ContentType.objects.get_for_model(model)
@ -299,6 +280,7 @@ class ObjectChangeLogView(View):
return render(request, 'extras/object_changelog.html', { return render(request, 'extras/object_changelog.html', {
object_var: obj, object_var: obj,
'instance': obj, # We'll eventually standardize on 'instance` for the object variable name
'table': objectchanges_table, 'table': objectchanges_table,
'base_template': base_template, 'base_template': base_template,
'active_tab': 'changelog', 'active_tab': 'changelog',

View File

@ -1,5 +1,7 @@
from collections import OrderedDict 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 import serializers
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from rest_framework.validators import UniqueTogetherValidator from rest_framework.validators import UniqueTogetherValidator
@ -9,10 +11,12 @@ from dcim.models import Interface
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from extras.api.serializers import TaggedObjectSerializer from extras.api.serializers import TaggedObjectSerializer
from ipam.choices import * 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 ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from tenancy.api.nested_serializers import NestedTenantSerializer from tenancy.api.nested_serializers import NestedTenantSerializer
from utilities.api import ( 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 virtualization.api.nested_serializers import NestedVirtualMachineSerializer
from .nested_serializers import * from .nested_serializers import *
@ -228,18 +232,31 @@ class IPAddressSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceField(choices=IPAddressStatusChoices, required=False) status = ChoiceField(choices=IPAddressStatusChoices, required=False)
role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, 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_inside = NestedIPAddressSerializer(required=False, allow_null=True)
nat_outside = NestedIPAddressSerializer(read_only=True) nat_outside = NestedIPAddressSerializer(read_only=True)
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = [ fields = [
'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'nat_inside', 'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'assigned_object_type', 'assigned_object_id',
'nat_outside', 'dns_name', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'assigned_object', 'nat_inside', 'nat_outside', 'dns_name', 'description', 'tags', 'custom_fields',
'created', 'last_updated',
] ]
read_only_fields = ['family'] 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): class AvailableIPSerializer(serializers.Serializer):
""" """

View File

@ -1,5 +1,5 @@
from django.conf import settings 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.shortcuts import get_object_or_404
from django_pglocks import advisory_lock from django_pglocks import advisory_lock
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
@ -233,8 +233,7 @@ class PrefixViewSet(CustomFieldModelViewSet):
class IPAddressViewSet(CustomFieldModelViewSet): class IPAddressViewSet(CustomFieldModelViewSet):
queryset = IPAddress.objects.prefetch_related( queryset = IPAddress.objects.prefetch_related(
'vrf__tenant', 'tenant', 'nat_inside', 'interface__device__device_type', 'interface__virtual_machine', 'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags',
'nat_outside', 'tags',
) )
serializer_class = serializers.IPAddressSerializer serializer_class = serializers.IPAddressSerializer
filterset_class = filters.IPAddressFilterSet filterset_class = filters.IPAddressFilterSet
@ -271,6 +270,9 @@ class VLANViewSet(CustomFieldModelViewSet):
# #
class ServiceViewSet(ModelViewSet): 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 serializer_class = serializers.ServiceSerializer
filterset_class = filters.ServiceFilterSet filterset_class = filters.ServiceFilterSet

View File

@ -1,3 +1,5 @@
from django.db.models import Q
from .choices import IPAddressRoleChoices from .choices import IPAddressRoleChoices
# BGP ASN bounds # BGP ASN bounds
@ -29,6 +31,11 @@ PREFIX_LENGTH_MAX = 127 # IPv6
# IPAddresses # 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_MIN = 1
IPADDRESS_MASK_LENGTH_MAX = 128 # IPv6 IPADDRESS_MASK_LENGTH_MAX = 128 # IPv6

View File

@ -11,7 +11,7 @@ from utilities.filters import (
BaseFilterSet, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, TagFilter, BaseFilterSet, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, TagFilter,
TreeNodeMultipleChoiceFilter, TreeNodeMultipleChoiceFilter,
) )
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine, VMInterface
from .choices import * from .choices import *
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
@ -309,27 +309,38 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet,
field_name='pk', field_name='pk',
label='Device (ID)', label='Device (ID)',
) )
virtual_machine_id = django_filters.ModelMultipleChoiceFilter( virtual_machine = MultiValueCharFilter(
field_name='interface__virtual_machine', method='filter_virtual_machine',
queryset=VirtualMachine.objects.unrestricted(), field_name='name',
label='Virtual machine (ID)',
)
virtual_machine = django_filters.ModelMultipleChoiceFilter(
field_name='interface__virtual_machine__name',
queryset=VirtualMachine.objects.unrestricted(),
to_field_name='name',
label='Virtual machine (name)', label='Virtual machine (name)',
) )
virtual_machine_id = MultiValueNumberFilter(
method='filter_virtual_machine',
field_name='pk',
label='Virtual machine (ID)',
)
interface = django_filters.ModelMultipleChoiceFilter( interface = django_filters.ModelMultipleChoiceFilter(
field_name='interface__name', field_name='interface__name',
queryset=Interface.objects.unrestricted(), queryset=Interface.objects.unrestricted(),
to_field_name='name', to_field_name='name',
label='Interface (ID)', label='Interface (name)',
) )
interface_id = django_filters.ModelMultipleChoiceFilter( interface_id = django_filters.ModelMultipleChoiceFilter(
field_name='interface',
queryset=Interface.objects.unrestricted(), queryset=Interface.objects.unrestricted(),
label='Interface (ID)', 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( assigned_to_interface = django_filters.BooleanFilter(
method='_assigned_to_interface', method='_assigned_to_interface',
label='Is assigned to an interface', label='Is assigned to an interface',
@ -379,17 +390,29 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet,
return queryset.filter(address__net_mask_length=value) return queryset.filter(address__net_mask_length=value)
def filter_device(self, queryset, name, value): def filter_device(self, queryset, name, value):
try: devices = Device.objects.filter(**{'{}__in'.format(name): value})
devices = Device.objects.prefetch_related('device_type').filter(**{'{}__in'.format(name): value}) if not devices.exists():
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:
return queryset.none() 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): 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): class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):

View File

@ -14,7 +14,7 @@ from utilities.forms import (
ExpandableIPAddressField, ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, ExpandableIPAddressField, ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
BOOLEAN_WITH_BLANK_CHOICES, BOOLEAN_WITH_BLANK_CHOICES,
) )
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine, VMInterface
from .choices import * from .choices import *
from .constants import * from .constants import *
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF 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): 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(), queryset=Interface.objects.all(),
required=False 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( vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
required=False, required=False,
@ -597,8 +620,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = [ fields = [
'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'interface', 'primary_for_parent', 'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'primary_for_parent', 'nat_site', 'nat_rack',
'nat_site', 'nat_rack', 'nat_inside', 'tenant_group', 'tenant', 'tags', 'nat_inside', 'tenant_group', 'tenant', 'tags',
] ]
widgets = { widgets = {
'status': StaticSelect2(), 'status': StaticSelect2(),
@ -610,32 +633,26 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
# Initialize helper selectors # Initialize helper selectors
instance = kwargs.get('instance') instance = kwargs.get('instance')
initial = kwargs.get('initial', {}).copy() initial = kwargs.get('initial', {}).copy()
if instance and instance.nat_inside and instance.nat_inside.device is not None: if instance:
initial['nat_site'] = instance.nat_inside.device.site if type(instance.assigned_object) is Interface:
initial['nat_rack'] = instance.nat_inside.device.rack initial['device'] = instance.assigned_object.device
initial['nat_device'] = instance.nat_inside.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
kwargs['initial'] = initial kwargs['initial'] = initial
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['vrf'].empty_label = 'Global' 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 # Initialize primary_for_parent if IP address is already assigned
if self.instance.pk and self.instance.interface is not None: if self.instance.pk and self.instance.assigned_object:
parent = self.instance.interface.parent parent = self.instance.assigned_object.parent
if ( if (
self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or 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 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): def clean(self):
super().clean() 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. # 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( self.add_error(
'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs." 'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs."
) )
def save(self, *args, **kwargs): 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) ipaddress = super().save(*args, **kwargs)
# Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine. # Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
if self.cleaned_data['primary_for_parent']: if interface and self.cleaned_data['primary_for_parent']:
parent = self.cleaned_data['interface'].parent
if ipaddress.address.version == 4: if ipaddress.address.version == 4:
parent.primary_ip4 = ipaddress interface.parent.primary_ip4 = ipaddress
else: else:
parent.primary_ip6 = ipaddress interface.primary_ip6 = ipaddress
parent.save() interface.parent.save()
elif self.cleaned_data['interface']: elif interface and ipaddress.address.version == 4 and interface.parent.primary_ip4 == ipaddress:
parent = self.cleaned_data['interface'].parent interface.parent.primary_ip4 = None
if ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress: interface.parent.save()
parent.primary_ip4 = None elif interface and ipaddress.address.version == 6 and interface.parent.primary_ip6 == ipaddress:
parent.save() interface.parent.primary_ip4 = None
elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress: interface.parent.save()
parent.primary_ip6 = None
parent.save()
return ipaddress return ipaddress
@ -742,7 +766,7 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
help_text='Parent VM of assigned interface (if any)' help_text='Parent VM of assigned interface (if any)'
) )
interface = CSVModelChoiceField( interface = CSVModelChoiceField(
queryset=Interface.objects.all(), queryset=Interface.objects.none(), # Can also refer to VMInterface
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text='Assigned interface' help_text='Assigned interface'
@ -761,21 +785,17 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
if data: if data:
# Limit interface queryset by assigned device or virtual machine # Limit interface queryset by assigned device
if data.get('device'): if data.get('device'):
params = { self.fields['interface'].queryset = Interface.objects.filter(
f"device__{self.fields['device'].to_field_name}": data.get('device') **{f"device__{self.fields['device'].to_field_name}": data['device']}
} )
# Limit interface queryset by assigned device
elif data.get('virtual_machine'): elif data.get('virtual_machine'):
params = { self.fields['interface'].queryset = VMInterface.objects.filter(
f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data.get('virtual_machine') **{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
} )
else:
params = {
'device': None,
'virtual_machine': None,
}
self.fields['interface'].queryset = self.fields['interface'].queryset.filter(**params)
def clean(self): def clean(self):
super().clean() super().clean()
@ -790,6 +810,10 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
def save(self, *args, **kwargs): 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) ipaddress = super().save(*args, **kwargs)
# Set as primary for device/VM # 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 # Limit IP address choices to those assigned to interfaces of the parent device/VM
if self.instance.device: 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( 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: elif self.instance.virtual_machine:
self.fields['ipaddresses'].queryset = IPAddress.objects.filter( 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: else:
self.fields['ipaddresses'].choices = [] self.fields['ipaddresses'].choices = []

View 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
),
]

View File

@ -1,10 +1,11 @@
import netaddr import netaddr
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.core.exceptions import ValidationError, ObjectDoesNotExist from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import F, Q from django.db.models import F
from django.urls import reverse from django.urls import reverse
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
@ -14,7 +15,7 @@ from extras.utils import extras_features
from utilities.models import ChangeLoggedModel from utilities.models import ChangeLoggedModel
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.utils import serialize_object from utilities.utils import serialize_object
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine, VMInterface
from .choices import * from .choices import *
from .constants import * from .constants import *
from .fields import IPNetworkField, IPAddressField 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 # 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: if self.pk:
covering_aggregates = covering_aggregates.exclude(pk=self.pk) covering_aggregates = covering_aggregates.exclude(pk=self.pk)
if covering_aggregates: if covering_aggregates:
@ -226,7 +229,7 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
}) })
# Ensure that the aggregate being added does not cover an existing aggregate # 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: if self.pk:
covered_aggregates = covered_aggregates.exclude(pk=self.pk) covered_aggregates = covered_aggregates.exclude(pk=self.pk)
if covered_aggregates: if covered_aggregates:
@ -254,7 +257,7 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
""" """
Determine the prefix utilization of the aggregate and return it as a percentage. 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]) child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
return int(float(child_prefixes.size) / self.prefix.size * 100) 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. "container", calculate utilization based on child prefixes. For all others, count child IP addresses.
""" """
if self.status == PrefixStatusChoices.STATUS_CONTAINER: 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]) child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
return int(float(child_prefixes.size) / self.prefix.size * 100) return int(float(child_prefixes.size) / self.prefix.size * 100)
else: else:
@ -606,13 +612,22 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
blank=True, blank=True,
help_text='The functional role of this IP' help_text='The functional role of this IP'
) )
interface = models.ForeignKey( assigned_object_type = models.ForeignKey(
to='dcim.Interface', to=ContentType,
on_delete=models.CASCADE, limit_choices_to=IPADDRESS_ASSIGNMENT_MODELS,
related_name='ip_addresses', on_delete=models.PROTECT,
related_name='+',
blank=True, blank=True,
null=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( nat_inside = models.OneToOneField(
to='self', to='self',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
@ -643,11 +658,11 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
objects = IPAddressManager() objects = IPAddressManager()
csv_headers = [ 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', 'dns_name', 'description',
] ]
clone_fields = [ clone_fields = [
'vrf', 'tenant', 'status', 'role', 'description', 'interface', 'vrf', 'tenant', 'status', 'role', 'description',
] ]
STATUS_CLASS_MAP = { 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
if self.pk and type(self.assigned_object) is Interface:
# Check for primary IP assignment that doesn't match the assigned device/VM device = Device.objects.unrestricted().filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
device = Device.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
if device: if device:
if self.interface is None: if self.assigned_object is None:
raise ValidationError({ 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({ raise ValidationError({
'interface': "IP address is primary for device {} but assigned to {} ({})".format( 'interface': f"IP address is primary for device {device} but assigned to "
device, self.interface.device, self.interface 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 vm:
if self.interface is None: if self.assigned_object is None:
raise ValidationError({ 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({ raise ValidationError({
'interface': "IP address is primary for virtual machine {} but assigned to {} ({})".format( 'vminterface': f"IP address is primary for virtual machine {vm} but assigned to "
vm, self.interface.virtual_machine, self.interface f"{self.assigned_object.virtual_machine} ({self.assigned_object})"
)
}) })
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -743,29 +757,27 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
super().save(*args, **kwargs) super().save(*args, **kwargs)
def to_objectchange(self, action): def to_objectchange(self, action):
# Annotate the assigned Interface (if any) # Annotate the assigned object, if any
try:
parent_obj = self.interface
except ObjectDoesNotExist:
parent_obj = None
return ObjectChange( return ObjectChange(
changed_object=self, changed_object=self,
object_repr=str(self), object_repr=str(self),
action=action, action=action,
related_object=parent_obj, related_object=self.assigned_object,
object_data=serialize_object(self) object_data=serialize_object(self)
) )
def to_csv(self): def to_csv(self):
# Determine if this IP is primary for a Device # Determine if this IP is primary for a Device
is_primary = False
if self.address.version == 4 and getattr(self, 'primary_ip4_for', False): if self.address.version == 4 and getattr(self, 'primary_ip4_for', False):
is_primary = True is_primary = True
elif self.address.version == 6 and getattr(self, 'primary_ip6_for', False): elif self.address.version == 6 and getattr(self, 'primary_ip6_for', False):
is_primary = True 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 ( return (
self.address, self.address,
@ -773,9 +785,8 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
self.tenant.name if self.tenant else None, self.tenant.name if self.tenant else None,
self.get_status_display(), self.get_status_display(),
self.get_role_display(), self.get_role_display(),
self.device.identifier if self.device else None, obj_type,
self.virtual_machine.name if self.virtual_machine else None, self.assigned_object_id,
self.interface.name if self.interface else None,
is_primary, is_primary,
self.dns_name, self.dns_name,
self.description, self.description,
@ -796,18 +807,6 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
self.address.prefixlen = value self.address.prefixlen = value
mask_length = property(fset=_set_mask_length) 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): def get_status_class(self):
return self.STATUS_CLASS_MAP.get(self.status) return self.STATUS_CLASS_MAP.get(self.status)

View File

@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
from dcim.models import Interface from dcim.models import Interface
from tenancy.tables import COL_TENANT 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 from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
RIR_UTILIZATION = """ RIR_UTILIZATION = """
@ -25,15 +25,6 @@ RIR_UTILIZATION = """
</div> </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 = """ UTILIZATION_GRAPH = """
{% load helpers %} {% load helpers %}
{% if record.pk %}{% utilization_graph record.get_utilization %}{% else %}&mdash;{% endif %} {% if record.pk %}{% utilization_graph record.get_utilization %}{% else %}&mdash;{% endif %}
@ -47,15 +38,6 @@ ROLE_VLAN_COUNT = """
<a href="{% url 'ipam:vlan_list' %}?role={{ record.slug }}">{{ value }}</a> <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 = """ PREFIX_LINK = """
{% if record.has_children %} {% if record.has_children %}
<span class="text-nowrap" style="padding-left: {{ record.depth }}0px "><i class="fa fa-caret-right"></i></a> <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 %} {% endif %}
""" """
IPADDRESS_PARENT = """
{% if record.interface %}
<a href="{{ record.interface.parent.get_absolute_url }}">{{ record.interface.parent }}</a>
{% else %}
&mdash;
{% endif %}
"""
VRF_LINK = """ VRF_LINK = """
{% if record.vrf %} {% if record.vrf %}
<a href="{{ record.vrf.get_absolute_url }}">{{ record.vrf }}</a> <a href="{{ record.vrf.get_absolute_url }}">{{ record.vrf }}</a>
@ -144,10 +118,7 @@ VLAN_ROLE_LINK = """
{% endif %} {% endif %}
""" """
VLANGROUP_ACTIONS = """ VLANGROUP_ADD_VLAN = """
<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>
{% with next_vid=record.get_next_available_vid %} {% with next_vid=record.get_next_available_vid %}
{% if next_vid and perms.ipam.add_vlan %} {% 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"> <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> </a>
{% endif %} {% endif %}
{% endwith %} {% 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 = """ VLAN_MEMBER_UNTAGGED = """
@ -168,7 +136,7 @@ VLAN_MEMBER_UNTAGGED = """
VLAN_MEMBER_ACTIONS = """ VLAN_MEMBER_ACTIONS = """
{% if perms.dcim.change_interface %} {% 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 %} {% endif %}
""" """
@ -222,11 +190,7 @@ class RIRTable(BaseTable):
aggregate_count = tables.Column( aggregate_count = tables.Column(
verbose_name='Aggregates' verbose_name='Aggregates'
) )
actions = tables.TemplateColumn( actions = ButtonsColumn(RIR, pk_field='slug')
template_code=RIR_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RIR model = RIR
@ -330,11 +294,7 @@ class RoleTable(BaseTable):
orderable=False, orderable=False,
verbose_name='VLANs' verbose_name='VLANs'
) )
actions = tables.TemplateColumn( actions = ButtonsColumn(Role, pk_field='slug')
template_code=ROLE_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Role model = Role
@ -431,18 +391,14 @@ class IPAddressTable(BaseTable):
tenant = tables.TemplateColumn( tenant = tables.TemplateColumn(
template_code=TENANT_LINK template_code=TENANT_LINK
) )
parent = tables.TemplateColumn( assigned = tables.BooleanColumn(
template_code=IPADDRESS_PARENT, accessor='assigned_object_id'
orderable=False
)
interface = tables.Column(
orderable=False
) )
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = IPAddress model = IPAddress
fields = ( fields = (
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'dns_name', 'description', 'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned', 'dns_name', 'description',
) )
row_attrs = { row_attrs = {
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '', 'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
@ -465,11 +421,11 @@ class IPAddressDetailTable(IPAddressTable):
class Meta(IPAddressTable.Meta): class Meta(IPAddressTable.Meta):
fields = ( 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', 'description', 'tags',
) )
default_columns = ( 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( status = tables.TemplateColumn(
template_code=STATUS_LABEL template_code=STATUS_LABEL
) )
parent = tables.TemplateColumn( assigned_object = tables.Column(
template_code=IPADDRESS_PARENT,
orderable=False
)
interface = tables.Column(
orderable=False orderable=False
) )
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = IPAddress 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 orderable = False
@ -532,10 +484,9 @@ class VLANGroupTable(BaseTable):
vlan_count = tables.Column( vlan_count = tables.Column(
verbose_name='VLANs' verbose_name='VLANs'
) )
actions = tables.TemplateColumn( actions = ButtonsColumn(
template_code=VLANGROUP_ACTIONS, model=VLANGroup,
attrs={'td': {'class': 'text-right noprint'}}, prepend_template=VLANGROUP_ADD_VLAN
verbose_name=''
) )
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):

View File

@ -416,7 +416,7 @@ class VLANTest(APIViewTestCases.APIViewTestCase):
""" """
Attempt and fail to delete a VLAN with a Prefix assigned to it. 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) Prefix.objects.create(prefix=IPNetwork('192.0.2.0/24'), vlan=vlan)
self.add_permissions('ipam.delete_vlan') self.add_permissions('ipam.delete_vlan')

View File

@ -4,7 +4,7 @@ from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer,
from ipam.choices import * from ipam.choices import *
from ipam.filters import * from ipam.filters import *
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF 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 from tenancy.models import Tenant, TenantGroup
@ -375,6 +375,13 @@ class IPAddressTestCase(TestCase):
) )
Device.objects.bulk_create(devices) 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') clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
cluster = Cluster.objects.create(type=clustertype, name='Cluster 1') cluster = Cluster.objects.create(type=clustertype, name='Cluster 1')
@ -385,15 +392,12 @@ class IPAddressTestCase(TestCase):
) )
VirtualMachine.objects.bulk_create(virtual_machines) VirtualMachine.objects.bulk_create(virtual_machines)
interfaces = ( vminterfaces = (
Interface(device=devices[0], name='Interface 1'), VMInterface(virtual_machine=virtual_machines[0], name='Interface 1'),
Interface(device=devices[1], name='Interface 2'), VMInterface(virtual_machine=virtual_machines[1], name='Interface 2'),
Interface(device=devices[2], name='Interface 3'), VMInterface(virtual_machine=virtual_machines[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'),
) )
Interface.objects.bulk_create(interfaces) VMInterface.objects.bulk_create(vminterfaces)
tenant_groups = ( tenant_groups = (
TenantGroup(name='Tenant group 1', slug='tenant-group-1'), TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
@ -411,16 +415,16 @@ class IPAddressTestCase(TestCase):
Tenant.objects.bulk_create(tenants) Tenant.objects.bulk_create(tenants)
ipaddresses = ( 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.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], interface=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'), 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], interface=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'), 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], interface=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'), 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, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE), 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, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'), 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], interface=interfaces[3], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'), 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], interface=interfaces[4], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'), 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], interface=interfaces[5], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'), 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, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE), IPAddress(address='2001:db8::1/65', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
) )
IPAddress.objects.bulk_create(ipaddresses) IPAddress.objects.bulk_create(ipaddresses)
@ -487,7 +491,14 @@ class IPAddressTestCase(TestCase):
params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]} params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'interface': ['Interface 1', 'Interface 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): def test_assigned_to_interface(self):
params = {'assigned_to_interface': 'true'} params = {'assigned_to_interface': 'true'}

View File

@ -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')),
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}) 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')),
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}) self.assertSetEqual(set(duplicate_ip_pks), {ips[1].pk, ips[2].pk})

View File

@ -236,7 +236,6 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'tenant': None, 'tenant': None,
'status': IPAddressStatusChoices.STATUS_RESERVED, 'status': IPAddressStatusChoices.STATUS_RESERVED,
'role': IPAddressRoleChoices.ROLE_ANYCAST, 'role': IPAddressRoleChoices.ROLE_ANYCAST,
'interface': None,
'nat_inside': None, 'nat_inside': None,
'dns_name': 'example', 'dns_name': 'example',
'description': 'A new IP address', 'description': 'A new IP address',

View File

@ -24,6 +24,7 @@ urlpatterns = [
path('rirs/import/', views.RIRBulkImportView.as_view(), name='rir_import'), path('rirs/import/', views.RIRBulkImportView.as_view(), name='rir_import'),
path('rirs/delete/', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'), 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>/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}), path('vrfs/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}),
# Aggregates # Aggregates
@ -43,6 +44,7 @@ urlpatterns = [
path('roles/import/', views.RoleBulkImportView.as_view(), name='role_import'), path('roles/import/', views.RoleBulkImportView.as_view(), name='role_import'),
path('roles/delete/', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'), 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>/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}), path('roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}),
# Prefixes # Prefixes
@ -77,6 +79,7 @@ urlpatterns = [
path('vlan-groups/import/', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'), 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/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>/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>/vlans/', views.VLANGroupVLANsView.as_view(), name='vlangroup_vlans'),
path('vlan-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}), path('vlan-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}),

93
netbox/ipam/utils.py Normal file
View 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

View File

@ -1,6 +1,6 @@
import netaddr import netaddr
from django.conf import settings 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.db.models.expressions import RawSQL
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django_tables2 import RequestConfig from django_tables2 import RequestConfig
@ -11,100 +11,12 @@ from utilities.views import (
BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView,
ObjectListView, ObjectListView,
) )
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine, VMInterface
from . import filters, forms, tables from . import filters, forms, tables
from .choices import * from .choices import *
from .constants import * from .constants import *
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from .utils import add_available_ipaddresses, add_available_prefixes, add_available_vlans
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
# #
@ -136,19 +48,16 @@ class VRFEditView(ObjectEditView):
queryset = VRF.objects.all() queryset = VRF.objects.all()
model_form = forms.VRFForm model_form = forms.VRFForm
template_name = 'ipam/vrf_edit.html' template_name = 'ipam/vrf_edit.html'
default_return_url = 'ipam:vrf_list'
class VRFDeleteView(ObjectDeleteView): class VRFDeleteView(ObjectDeleteView):
queryset = VRF.objects.all() queryset = VRF.objects.all()
default_return_url = 'ipam:vrf_list'
class VRFBulkImportView(BulkImportView): class VRFBulkImportView(BulkImportView):
queryset = VRF.objects.all() queryset = VRF.objects.all()
model_form = forms.VRFCSVForm model_form = forms.VRFCSVForm
table = tables.VRFTable table = tables.VRFTable
default_return_url = 'ipam:vrf_list'
class VRFBulkEditView(BulkEditView): class VRFBulkEditView(BulkEditView):
@ -156,14 +65,12 @@ class VRFBulkEditView(BulkEditView):
filterset = filters.VRFFilterSet filterset = filters.VRFFilterSet
table = tables.VRFTable table = tables.VRFTable
form = forms.VRFBulkEditForm form = forms.VRFBulkEditForm
default_return_url = 'ipam:vrf_list'
class VRFBulkDeleteView(BulkDeleteView): class VRFBulkDeleteView(BulkDeleteView):
queryset = VRF.objects.prefetch_related('tenant') queryset = VRF.objects.prefetch_related('tenant')
filterset = filters.VRFFilterSet filterset = filters.VRFFilterSet
table = tables.VRFTable table = tables.VRFTable
default_return_url = 'ipam:vrf_list'
# #
@ -196,10 +103,12 @@ class RIRListView(ObjectListView):
'deprecated': 0, 'deprecated': 0,
'available': 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: 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). # Find all consumed space for each prefix status (we ignore containers for this purpose).
active_prefixes = netaddr.cidr_merge( active_prefixes = netaddr.cidr_merge(
@ -249,21 +158,22 @@ class RIRListView(ObjectListView):
class RIREditView(ObjectEditView): class RIREditView(ObjectEditView):
queryset = RIR.objects.all() queryset = RIR.objects.all()
model_form = forms.RIRForm model_form = forms.RIRForm
default_return_url = 'ipam:rir_list'
class RIRDeleteView(ObjectDeleteView):
queryset = RIR.objects.all()
class RIRBulkImportView(BulkImportView): class RIRBulkImportView(BulkImportView):
queryset = RIR.objects.all() queryset = RIR.objects.all()
model_form = forms.RIRCSVForm model_form = forms.RIRCSVForm
table = tables.RIRTable table = tables.RIRTable
default_return_url = 'ipam:rir_list'
class RIRBulkDeleteView(BulkDeleteView): class RIRBulkDeleteView(BulkDeleteView):
queryset = RIR.objects.annotate(aggregate_count=Count('aggregates')) queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
filterset = filters.RIRFilterSet filterset = filters.RIRFilterSet
table = tables.RIRTable table = tables.RIRTable
default_return_url = 'ipam:rir_list'
# #
@ -345,19 +255,16 @@ class AggregateEditView(ObjectEditView):
queryset = Aggregate.objects.all() queryset = Aggregate.objects.all()
model_form = forms.AggregateForm model_form = forms.AggregateForm
template_name = 'ipam/aggregate_edit.html' template_name = 'ipam/aggregate_edit.html'
default_return_url = 'ipam:aggregate_list'
class AggregateDeleteView(ObjectDeleteView): class AggregateDeleteView(ObjectDeleteView):
queryset = Aggregate.objects.all() queryset = Aggregate.objects.all()
default_return_url = 'ipam:aggregate_list'
class AggregateBulkImportView(BulkImportView): class AggregateBulkImportView(BulkImportView):
queryset = Aggregate.objects.all() queryset = Aggregate.objects.all()
model_form = forms.AggregateCSVForm model_form = forms.AggregateCSVForm
table = tables.AggregateTable table = tables.AggregateTable
default_return_url = 'ipam:aggregate_list'
class AggregateBulkEditView(BulkEditView): class AggregateBulkEditView(BulkEditView):
@ -365,14 +272,12 @@ class AggregateBulkEditView(BulkEditView):
filterset = filters.AggregateFilterSet filterset = filters.AggregateFilterSet
table = tables.AggregateTable table = tables.AggregateTable
form = forms.AggregateBulkEditForm form = forms.AggregateBulkEditForm
default_return_url = 'ipam:aggregate_list'
class AggregateBulkDeleteView(BulkDeleteView): class AggregateBulkDeleteView(BulkDeleteView):
queryset = Aggregate.objects.prefetch_related('rir') queryset = Aggregate.objects.prefetch_related('rir')
filterset = filters.AggregateFilterSet filterset = filters.AggregateFilterSet
table = tables.AggregateTable table = tables.AggregateTable
default_return_url = 'ipam:aggregate_list'
# #
@ -387,20 +292,21 @@ class RoleListView(ObjectListView):
class RoleEditView(ObjectEditView): class RoleEditView(ObjectEditView):
queryset = Role.objects.all() queryset = Role.objects.all()
model_form = forms.RoleForm model_form = forms.RoleForm
default_return_url = 'ipam:role_list'
class RoleDeleteView(ObjectDeleteView):
queryset = Role.objects.all()
class RoleBulkImportView(BulkImportView): class RoleBulkImportView(BulkImportView):
queryset = Role.objects.all() queryset = Role.objects.all()
model_form = forms.RoleCSVForm model_form = forms.RoleCSVForm
table = tables.RoleTable table = tables.RoleTable
default_return_url = 'ipam:role_list'
class RoleBulkDeleteView(BulkDeleteView): class RoleBulkDeleteView(BulkDeleteView):
queryset = Role.objects.all() queryset = Role.objects.all()
table = tables.RoleTable table = tables.RoleTable
default_return_url = 'ipam:role_list'
# #
@ -517,7 +423,7 @@ class PrefixIPAddressesView(ObjectView):
# Find all IPAddresses belonging to this Prefix # Find all IPAddresses belonging to this Prefix
ipaddresses = prefix.get_child_ips().restrict(request.user, 'view').prefetch_related( 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 # Add available IP addresses to the table if requested
@ -556,20 +462,17 @@ class PrefixEditView(ObjectEditView):
queryset = Prefix.objects.all() queryset = Prefix.objects.all()
model_form = forms.PrefixForm model_form = forms.PrefixForm
template_name = 'ipam/prefix_edit.html' template_name = 'ipam/prefix_edit.html'
default_return_url = 'ipam:prefix_list'
class PrefixDeleteView(ObjectDeleteView): class PrefixDeleteView(ObjectDeleteView):
queryset = Prefix.objects.all() queryset = Prefix.objects.all()
template_name = 'ipam/prefix_delete.html' template_name = 'ipam/prefix_delete.html'
default_return_url = 'ipam:prefix_list'
class PrefixBulkImportView(BulkImportView): class PrefixBulkImportView(BulkImportView):
queryset = Prefix.objects.all() queryset = Prefix.objects.all()
model_form = forms.PrefixCSVForm model_form = forms.PrefixCSVForm
table = tables.PrefixTable table = tables.PrefixTable
default_return_url = 'ipam:prefix_list'
class PrefixBulkEditView(BulkEditView): class PrefixBulkEditView(BulkEditView):
@ -577,14 +480,12 @@ class PrefixBulkEditView(BulkEditView):
filterset = filters.PrefixFilterSet filterset = filters.PrefixFilterSet
table = tables.PrefixTable table = tables.PrefixTable
form = forms.PrefixBulkEditForm form = forms.PrefixBulkEditForm
default_return_url = 'ipam:prefix_list'
class PrefixBulkDeleteView(BulkDeleteView): class PrefixBulkDeleteView(BulkDeleteView):
queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role') queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
filterset = filters.PrefixFilterSet filterset = filters.PrefixFilterSet
table = tables.PrefixTable table = tables.PrefixTable
default_return_url = 'ipam:prefix_list'
# #
@ -593,7 +494,7 @@ class PrefixBulkDeleteView(BulkDeleteView):
class IPAddressListView(ObjectListView): class IPAddressListView(ObjectListView):
queryset = IPAddress.objects.prefetch_related( queryset = IPAddress.objects.prefetch_related(
'vrf__tenant', 'tenant', 'nat_inside', 'interface__device', 'interface__virtual_machine' 'vrf__tenant', 'tenant', 'nat_inside'
) )
filterset = filters.IPAddressFilterSet filterset = filters.IPAddressFilterSet
filterset_form = forms.IPAddressFilterForm filterset_form = forms.IPAddressFilterForm
@ -622,7 +523,7 @@ class IPAddressView(ObjectView):
).exclude( ).exclude(
pk=ipaddress.pk pk=ipaddress.pk
).prefetch_related( ).prefetch_related(
'nat_inside', 'interface__device' 'nat_inside'
) )
# Exclude anycast IPs if this IP is anycast # Exclude anycast IPs if this IP is anycast
if ipaddress.role == IPAddressRoleChoices.ROLE_ANYCAST: if ipaddress.role == IPAddressRoleChoices.ROLE_ANYCAST:
@ -630,9 +531,7 @@ class IPAddressView(ObjectView):
duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False) duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False)
# Related IP table # Related IP table
related_ips = IPAddress.objects.restrict(request.user, 'view').prefetch_related( related_ips = IPAddress.objects.restrict(request.user, 'view').exclude(
'interface__device'
).exclude(
address=str(ipaddress.address) address=str(ipaddress.address)
).filter( ).filter(
vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address) vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)
@ -657,17 +556,21 @@ class IPAddressEditView(ObjectEditView):
queryset = IPAddress.objects.all() queryset = IPAddress.objects.all()
model_form = forms.IPAddressForm model_form = forms.IPAddressForm
template_name = 'ipam/ipaddress_edit.html' template_name = 'ipam/ipaddress_edit.html'
default_return_url = 'ipam:ipaddress_list'
def alter_obj(self, obj, request, url_args, url_kwargs): def alter_obj(self, obj, request, url_args, url_kwargs):
interface_id = request.GET.get('interface') if 'interface' in request.GET:
if interface_id:
try: try:
obj.interface = Interface.objects.get(pk=interface_id) obj.assigned_object = Interface.objects.get(pk=request.GET['interface'])
except (ValueError, Interface.DoesNotExist): except (ValueError, Interface.DoesNotExist):
pass pass
elif 'vminterface' in request.GET:
try:
obj.assigned_object = VMInterface.objects.get(pk=request.GET['vminterface'])
except (ValueError, VMInterface.DoesNotExist):
pass
return obj return obj
@ -699,9 +602,7 @@ class IPAddressAssignView(ObjectView):
if form.is_valid(): if form.is_valid():
addresses = self.queryset.prefetch_related( addresses = self.queryset.prefetch_related('vrf', 'tenant')
'vrf', 'tenant', 'interface__device', 'interface__virtual_machine'
)
# Limit to 100 results # Limit to 100 results
addresses = filters.IPAddressFilterSet(request.POST, addresses).qs[:100] addresses = filters.IPAddressFilterSet(request.POST, addresses).qs[:100]
table = tables.IPAddressAssignTable(addresses) table = tables.IPAddressAssignTable(addresses)
@ -715,37 +616,33 @@ class IPAddressAssignView(ObjectView):
class IPAddressDeleteView(ObjectDeleteView): class IPAddressDeleteView(ObjectDeleteView):
queryset = IPAddress.objects.all() queryset = IPAddress.objects.all()
default_return_url = 'ipam:ipaddress_list'
class IPAddressBulkCreateView(BulkCreateView): class IPAddressBulkCreateView(BulkCreateView):
queryset = IPAddress.objects.all()
form = forms.IPAddressBulkCreateForm form = forms.IPAddressBulkCreateForm
model_form = forms.IPAddressBulkAddForm model_form = forms.IPAddressBulkAddForm
pattern_target = 'address' pattern_target = 'address'
template_name = 'ipam/ipaddress_bulk_add.html' template_name = 'ipam/ipaddress_bulk_add.html'
default_return_url = 'ipam:ipaddress_list'
class IPAddressBulkImportView(BulkImportView): class IPAddressBulkImportView(BulkImportView):
queryset = IPAddress.objects.all() queryset = IPAddress.objects.all()
model_form = forms.IPAddressCSVForm model_form = forms.IPAddressCSVForm
table = tables.IPAddressTable table = tables.IPAddressTable
default_return_url = 'ipam:ipaddress_list'
class IPAddressBulkEditView(BulkEditView): 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 filterset = filters.IPAddressFilterSet
table = tables.IPAddressTable table = tables.IPAddressTable
form = forms.IPAddressBulkEditForm form = forms.IPAddressBulkEditForm
default_return_url = 'ipam:ipaddress_list'
class IPAddressBulkDeleteView(BulkDeleteView): 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 filterset = filters.IPAddressFilterSet
table = tables.IPAddressTable table = tables.IPAddressTable
default_return_url = 'ipam:ipaddress_list'
# #
@ -762,21 +659,22 @@ class VLANGroupListView(ObjectListView):
class VLANGroupEditView(ObjectEditView): class VLANGroupEditView(ObjectEditView):
queryset = VLANGroup.objects.all() queryset = VLANGroup.objects.all()
model_form = forms.VLANGroupForm model_form = forms.VLANGroupForm
default_return_url = 'ipam:vlangroup_list'
class VLANGroupDeleteView(ObjectDeleteView):
queryset = VLANGroup.objects.all()
class VLANGroupBulkImportView(BulkImportView): class VLANGroupBulkImportView(BulkImportView):
queryset = VLANGroup.objects.all() queryset = VLANGroup.objects.all()
model_form = forms.VLANGroupCSVForm model_form = forms.VLANGroupCSVForm
table = tables.VLANGroupTable table = tables.VLANGroupTable
default_return_url = 'ipam:vlangroup_list'
class VLANGroupBulkDeleteView(BulkDeleteView): class VLANGroupBulkDeleteView(BulkDeleteView):
queryset = VLANGroup.objects.prefetch_related('site').annotate(vlan_count=Count('vlans')) queryset = VLANGroup.objects.prefetch_related('site').annotate(vlan_count=Count('vlans'))
filterset = filters.VLANGroupFilterSet filterset = filters.VLANGroupFilterSet
table = tables.VLANGroupTable table = tables.VLANGroupTable
default_return_url = 'ipam:vlangroup_list'
class VLANGroupVLANsView(ObjectView): class VLANGroupVLANsView(ObjectView):
@ -785,7 +683,9 @@ class VLANGroupVLANsView(ObjectView):
def get(self, request, pk): def get(self, request, pk):
vlan_group = get_object_or_404(self.queryset, pk=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) vlans = add_available_vlans(vlan_group, vlans)
vlan_table = tables.VLANDetailTable(vlans) vlan_table = tables.VLANDetailTable(vlans)
@ -871,19 +771,16 @@ class VLANEditView(ObjectEditView):
queryset = VLAN.objects.all() queryset = VLAN.objects.all()
model_form = forms.VLANForm model_form = forms.VLANForm
template_name = 'ipam/vlan_edit.html' template_name = 'ipam/vlan_edit.html'
default_return_url = 'ipam:vlan_list'
class VLANDeleteView(ObjectDeleteView): class VLANDeleteView(ObjectDeleteView):
queryset = VLAN.objects.all() queryset = VLAN.objects.all()
default_return_url = 'ipam:vlan_list'
class VLANBulkImportView(BulkImportView): class VLANBulkImportView(BulkImportView):
queryset = VLAN.objects.all() queryset = VLAN.objects.all()
model_form = forms.VLANCSVForm model_form = forms.VLANCSVForm
table = tables.VLANTable table = tables.VLANTable
default_return_url = 'ipam:vlan_list'
class VLANBulkEditView(BulkEditView): class VLANBulkEditView(BulkEditView):
@ -891,14 +788,12 @@ class VLANBulkEditView(BulkEditView):
filterset = filters.VLANFilterSet filterset = filters.VLANFilterSet
table = tables.VLANTable table = tables.VLANTable
form = forms.VLANBulkEditForm form = forms.VLANBulkEditForm
default_return_url = 'ipam:vlan_list'
class VLANBulkDeleteView(BulkDeleteView): class VLANBulkDeleteView(BulkDeleteView):
queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role') queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role')
filterset = filters.VLANFilterSet filterset = filters.VLANFilterSet
table = tables.VLANTable table = tables.VLANTable
default_return_url = 'ipam:vlan_list'
# #
@ -945,7 +840,6 @@ class ServiceBulkImportView(BulkImportView):
queryset = Service.objects.all() queryset = Service.objects.all()
model_form = forms.ServiceCSVForm model_form = forms.ServiceCSVForm
table = tables.ServiceTable table = tables.ServiceTable
default_return_url = 'ipam:service_list'
class ServiceDeleteView(ObjectDeleteView): class ServiceDeleteView(ObjectDeleteView):
@ -957,11 +851,9 @@ class ServiceBulkEditView(BulkEditView):
filterset = filters.ServiceFilterSet filterset = filters.ServiceFilterSet
table = tables.ServiceTable table = tables.ServiceTable
form = forms.ServiceBulkEditForm form = forms.ServiceBulkEditForm
default_return_url = 'ipam:service_list'
class ServiceBulkDeleteView(BulkDeleteView): class ServiceBulkDeleteView(BulkDeleteView):
queryset = Service.objects.prefetch_related('device', 'virtual_machine') queryset = Service.objects.prefetch_related('device', 'virtual_machine')
filterset = filters.ServiceFilterSet filterset = filters.ServiceFilterSet
table = tables.ServiceTable table = tables.ServiceTable
default_return_url = 'ipam:service_list'

View File

@ -24,7 +24,7 @@ class ObjectPermissionBackend(ModelBackend):
Return all permissions granted to the user by an ObjectPermission. Return all permissions granted to the user by an ObjectPermission.
""" """
# Retrieve all assigned ObjectPermissions # Retrieve all assigned ObjectPermissions
object_permissions = ObjectPermission.objects.filter( object_permissions = ObjectPermission.objects.unrestricted().filter(
Q(users=user_obj) | Q(users=user_obj) |
Q(groups__user=user_obj) Q(groups__user=user_obj)
).prefetch_related('object_types') ).prefetch_related('object_types')

View File

@ -208,6 +208,10 @@ PLUGINS = []
# prefer IPv4 instead. # prefer IPv4 instead.
PREFER_IPV4 = False 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 authentication support
REMOTE_AUTH_ENABLED = False REMOTE_AUTH_ENABLED = False
REMOTE_AUTH_BACKEND = 'netbox.authentication.RemoteUserBackend' REMOTE_AUTH_BACKEND = 'netbox.authentication.RemoteUserBackend'

View File

@ -99,6 +99,8 @@ PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
PLUGINS = getattr(configuration, 'PLUGINS', []) PLUGINS = getattr(configuration, 'PLUGINS', [])
PLUGINS_CONFIG = getattr(configuration, 'PLUGINS_CONFIG', {}) PLUGINS_CONFIG = getattr(configuration, 'PLUGINS_CONFIG', {})
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False) 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_AUTO_CREATE_USER = getattr(configuration, 'REMOTE_AUTH_AUTO_CREATE_USER', False)
REMOTE_AUTH_BACKEND = getattr(configuration, 'REMOTE_AUTH_BACKEND', 'netbox.authentication.RemoteUserBackend') REMOTE_AUTH_BACKEND = getattr(configuration, 'REMOTE_AUTH_BACKEND', 'netbox.authentication.RemoteUserBackend')
REMOTE_AUTH_DEFAULT_GROUPS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_GROUPS', []) REMOTE_AUTH_DEFAULT_GROUPS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_GROUPS', [])

View File

@ -168,356 +168,6 @@ class ExternalAuthenticationTestCase(TestCase):
self.assertTrue(new_user.has_perms(['dcim.add_site', 'dcim.change_site'])) 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): class ObjectPermissionAPIViewTestCase(TestCase):
client_class = APIClient client_class = APIClient

View File

@ -183,13 +183,6 @@ nav ul.pagination {
margin-bottom: 8px !important; margin-bottom: 8px !important;
} }
/* Racks */
div.rack_header {
margin-left: 32px;
text-align: center;
width: 220px;
}
/* Devices */ /* Devices */
table.component-list td.subtable { table.component-list td.subtable {
padding: 0; padding: 0;

View File

@ -1,17 +1,8 @@
import django_tables2 as tables 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 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 # Secret roles
@ -23,11 +14,7 @@ class SecretRoleTable(BaseTable):
secret_count = tables.Column( secret_count = tables.Column(
verbose_name='Secrets' verbose_name='Secrets'
) )
actions = tables.TemplateColumn( actions = ButtonsColumn(SecretRole, pk_field='slug')
template_code=SECRETROLE_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = SecretRole model = SecretRole

View File

@ -13,6 +13,7 @@ urlpatterns = [
path('secret-roles/import/', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'), 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/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>/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}), path('secret-roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}),
# Secrets # Secrets

View File

@ -36,20 +36,21 @@ class SecretRoleListView(ObjectListView):
class SecretRoleEditView(ObjectEditView): class SecretRoleEditView(ObjectEditView):
queryset = SecretRole.objects.all() queryset = SecretRole.objects.all()
model_form = forms.SecretRoleForm model_form = forms.SecretRoleForm
default_return_url = 'secrets:secretrole_list'
class SecretRoleDeleteView(ObjectDeleteView):
queryset = SecretRole.objects.all()
class SecretRoleBulkImportView(BulkImportView): class SecretRoleBulkImportView(BulkImportView):
queryset = SecretRole.objects.all() queryset = SecretRole.objects.all()
model_form = forms.SecretRoleCSVForm model_form = forms.SecretRoleCSVForm
table = tables.SecretRoleTable table = tables.SecretRoleTable
default_return_url = 'secrets:secretrole_list'
class SecretRoleBulkDeleteView(BulkDeleteView): class SecretRoleBulkDeleteView(BulkDeleteView):
queryset = SecretRole.objects.annotate(secret_count=Count('secrets')) queryset = SecretRole.objects.annotate(secret_count=Count('secrets'))
table = tables.SecretRoleTable table = tables.SecretRoleTable
default_return_url = 'secrets:secretrole_list'
# #
@ -147,7 +148,6 @@ class SecretEditView(ObjectEditView):
class SecretDeleteView(ObjectDeleteView): class SecretDeleteView(ObjectDeleteView):
queryset = Secret.objects.all() queryset = Secret.objects.all()
default_return_url = 'secrets:secret_list'
class SecretBulkImportView(BulkImportView): class SecretBulkImportView(BulkImportView):
@ -155,7 +155,6 @@ class SecretBulkImportView(BulkImportView):
model_form = forms.SecretCSVForm model_form = forms.SecretCSVForm
table = tables.SecretTable table = tables.SecretTable
template_name = 'secrets/secret_import.html' template_name = 'secrets/secret_import.html'
default_return_url = 'secrets:secret_list'
widget_attrs = {'class': 'requires-session-key'} widget_attrs = {'class': 'requires-session-key'}
master_key = None master_key = None
@ -203,11 +202,9 @@ class SecretBulkEditView(BulkEditView):
filterset = filters.SecretFilterSet filterset = filters.SecretFilterSet
table = tables.SecretTable table = tables.SecretTable
form = forms.SecretBulkEditForm form = forms.SecretBulkEditForm
default_return_url = 'secrets:secret_list'
class SecretBulkDeleteView(BulkDeleteView): class SecretBulkDeleteView(BulkDeleteView):
queryset = Secret.objects.prefetch_related('role', 'device') queryset = Secret.objects.prefetch_related('role', 'device')
filterset = filters.SecretFilterSet filterset = filters.SecretFilterSet
table = tables.SecretTable table = tables.SecretTable
default_return_url = 'secrets:secret_list'

View File

@ -88,6 +88,16 @@
</table> </table>
</div> </div>
</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 %} {% else %}
<div class="col-md-11 col-md-offset-1"> <div class="col-md-11 col-md-offset-1">
<h3 class="text-success text-center">Trace completed!</h3> <h3 class="text-success text-center">Trace completed!</h3>

View 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 %}

View 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 %}

View File

@ -74,6 +74,9 @@
{% if perms.dcim.add_devicebay %} {% 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> <li><a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Device Bays</a></li>
{% endif %} {% 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> </ul>
</div> </div>
{% endif %} {% endif %}
@ -326,34 +329,85 @@
{% plugin_left_page device %} {% plugin_left_page device %}
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
{% if console_ports or power_ports %} {% if console_ports %}
<div class="panel panel-default"> <form method="post">
<div class="panel-heading"> {% csrf_token %}
<strong>Console / Power</strong> <div class="panel panel-default">
</div> <div class="panel-heading">
<table class="table table-hover panel-body component-list"> <strong>Console Ports</strong>
{% for cp in console_ports %} </div>
{% include 'dcim/inc/consoleport.html' %} <table class="table table-hover panel-body component-list">
{% endfor %} {% for cp in console_ports %}
{% for pp in power_ports %} {% include 'dcim/inc/consoleport.html' %}
{% include 'dcim/inc/powerport.html' %} {% endfor %}
{% endfor %} </table>
</table> <div class="panel-footer noprint">
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %} {% if console_ports and perms.dcim.change_consoleport %}
<div class="panel-footer text-right noprint"> <button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
{% if perms.dcim.add_consoleport %} <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
<a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary"> </button>
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port <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">
</a> <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 %} {% endif %}
{% if perms.dcim.add_powerport %} {% if console_ports and perms.dcim.delete_consoleport %}
<a href="{% url 'dcim:powerport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary"> <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-plus" aria-hidden="true"></span> Add power port <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
</a> </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 %} {% endif %}
</div> </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>
<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 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>
</div>
{% endif %}
</div>
</div>
</form>
{% endif %} {% endif %}
{% if power_ports and poweroutlets %} {% if power_ports and poweroutlets %}
<div class="panel panel-default"> <div class="panel panel-default">
@ -501,262 +555,242 @@
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
{% if device_bays or device.device_type.is_parent_device %} {% if device_bays or device.device_type.is_parent_device %}
{% if perms.dcim.delete_devicebay %} <form method="post">
<form method="post">
{% csrf_token %} {% csrf_token %}
{% endif %} <div class="panel panel-default">
<div class="panel panel-default"> <div class="panel-heading">
<div class="panel-heading"> <strong>Device Bays</strong>
<strong>Device Bays</strong> </div>
</div> <table class="table table-hover table-headings panel-body component-list">
<table class="table table-hover table-headings panel-body component-list"> <thead>
<thead>
<tr>
{% if perms.dcim.change_devicebay or perms.dcim.delete_devicebay %}
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
{% endif %}
<th>Name</th>
<th>Status</th>
<th>Description</th>
<th colspan="2">Installed Device</th>
<th></th>
</tr>
</thead>
<tbody>
{% for devicebay in device_bays %}
{% include 'dcim/inc/devicebay.html' %}
{% empty %}
<tr> <tr>
<td colspan="5" class="text-center text-muted">&mdash; No device bays defined &mdash;</td> {% if perms.dcim.change_devicebay or perms.dcim.delete_devicebay %}
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
{% endif %}
<th>Name</th>
<th>Status</th>
<th>Description</th>
<th colspan="2">Installed Device</th>
<th></th>
</tr> </tr>
{% endfor %} </thead>
</tbody> <tbody>
</table> {% for devicebay in device_bays %}
<div class="panel-footer noprint"> {% include 'dcim/inc/devicebay.html' %}
{% if device_bays and perms.dcim.change_devicebay %} {% empty %}
<button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <tr>
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename <td colspan="5" class="text-center text-muted">&mdash; No device bays defined &mdash;</td>
</button> </tr>
{% endif %} {% endfor %}
{% if device_bays and perms.dcim.delete_devicebay %} </tbody>
<button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> </table>
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected <div class="panel-footer noprint">
</button> {% if device_bays and perms.dcim.change_devicebay %}
{% endif %} <button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
{% if perms.dcim.add_devicebay %} <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
<div class="pull-right"> </button>
<a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs"> {% endif %}
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays {% if device_bays and perms.dcim.delete_devicebay %}
</a> <button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
</div> <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
<div class="clearfix"></div> </button>
{% endif %} {% endif %}
</div> {% if perms.dcim.add_devicebay %}
</div> <div class="pull-right">
{% if perms.dcim.delete_devicebay %} <a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
</form> <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
{% endif %} </a>
</div>
<div class="clearfix"></div>
{% endif %}
</div>
</div>
</form>
{% endif %} {% endif %}
{% if interfaces %} {% if interfaces %}
{% if perms.dcim.change_interface or perms.dcim.delete_interface %} <form method="post">
<form method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="device" value="{{ device.pk }}" /> <div class="panel panel-default">
{% endif %} <div class="panel-heading">
<div class="panel panel-default"> <strong>Interfaces</strong>
<div class="panel-heading"> <div class="pull-right noprint">
<strong>Interfaces</strong> <button class="btn btn-default btn-xs toggle-ips" selected="selected">
<div class="pull-right noprint"> <span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
<button class="btn btn-default btn-xs toggle-ips" selected="selected"> </button>
<span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
</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" />
</div>
</div>
<table id="interfaces_table" class="table table-hover table-headings panel-body component-list">
<thead>
<tr>
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
{% endif %}
<th>Name</th>
<th>LAG</th>
<th>Description</th>
<th>MTU</th>
<th>Mode</th>
<th>Cable</th>
<th colspan="2">Connection</th>
<th></th>
</tr>
</thead>
<tbody>
{% for iface in interfaces %}
{% include 'dcim/inc/interface.html' %}
{% endfor %}
</tbody>
</table>
<div class="panel-footer noprint">
{% if interfaces and perms.dcim.change_interface %}
<button type="submit" name="_rename" formaction="{% url 'dcim:interface_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:interface_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>
{% endif %}
{% if interfaces and perms.dcim.change_interface %}
<button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_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 interfaces and perms.dcim.delete_interface %}
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_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 perms.dcim.add_interface %}
<div class="pull-right">
<a href="{% url 'dcim:interface_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
</a>
</div> </div>
<div class="clearfix"></div> <div class="col-md-2 pull-right noprint">
{% endif %} <input class="form-control interface-filter" type="text" placeholder="Filter" title="Filter text (regular expressions supported)" style="height: 23px" />
</div> </div>
</div> </div>
{% if perms.dcim.delete_interface %} <table id="interfaces_table" class="table table-hover table-headings panel-body component-list">
</form> <thead>
{% endif %} <tr>
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
{% endif %}
<th>Name</th>
<th>LAG</th>
<th>Description</th>
<th>MTU</th>
<th>Mode</th>
<th>Cable</th>
<th colspan="2">Connection</th>
<th></th>
</tr>
</thead>
<tbody>
{% for iface in interfaces %}
{% include 'dcim/inc/interface.html' %}
{% endfor %}
</tbody>
</table>
<div class="panel-footer noprint">
{% if interfaces and perms.dcim.change_interface %}
<button type="submit" name="_rename" formaction="{% url 'dcim:interface_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:interface_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>
{% endif %}
{% if interfaces and perms.dcim.change_interface %}
<button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_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 interfaces and perms.dcim.delete_interface %}
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_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 perms.dcim.add_interface %}
<div class="pull-right">
<a href="{% url 'dcim:interface_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
</a>
</div>
<div class="clearfix"></div>
{% endif %}
</div>
</div>
</form>
{% endif %} {% endif %}
{% if consoleserverports %} {% if consoleserverports %}
{% if perms.dcim.delete_consoleserverport %} <form method="post">
<form method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="device" value="{{ device.pk }}" /> <div class="panel panel-default">
{% endif %} <div class="panel-heading">
<div class="panel panel-default"> <strong>Console Server Ports</strong>
<div class="panel-heading"> </div>
<strong>Console Server Ports</strong> <table class="table table-hover table-headings panel-body component-list">
<thead>
<tr>
{% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
{% endif %}
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Cable</th>
<th colspan="2">Connection</th>
<th></th>
</tr>
</thead>
<tbody>
{% for csp in consoleserverports %}
{% include 'dcim/inc/consoleserverport.html' %}
{% endfor %}
</tbody>
</table>
<div class="panel-footer noprint">
{% if consoleserverports and perms.dcim.change_consoleport %}
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_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:consoleserverport_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:consoleserverport_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 consoleserverports and perms.dcim.delete_consoleserverport %}
<button type="submit" formaction="{% url 'dcim:consoleserverport_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 perms.dcim.add_consoleserverport %}
<div class="pull-right">
<a href="{% url 'dcim:consoleserverport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
</a>
</div>
<div class="clearfix"></div>
{% endif %}
</div>
</div> </div>
<table class="table table-hover table-headings panel-body component-list"> </form>
<thead>
<tr>
{% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
{% endif %}
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Cable</th>
<th colspan="2">Connection</th>
<th></th>
</tr>
</thead>
<tbody>
{% for csp in consoleserverports %}
{% include 'dcim/inc/consoleserverport.html' %}
{% endfor %}
</tbody>
</table>
<div class="panel-footer noprint">
{% if consoleserverports and perms.dcim.change_consoleport %}
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_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:consoleserverport_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:consoleserverport_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 consoleserverports and perms.dcim.delete_consoleserverport %}
<button type="submit" formaction="{% url 'dcim:consoleserverport_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 perms.dcim.add_consoleserverport %}
<div class="pull-right">
<a href="{% url 'dcim:consoleserverport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
</a>
</div>
<div class="clearfix"></div>
{% endif %}
</div>
</div>
{% if perms.dcim.delete_consoleserverport %}
</form>
{% endif %}
{% endif %} {% endif %}
{% if poweroutlets %} {% if poweroutlets %}
{% if perms.dcim.delete_poweroutlet %} <form method="post">
<form method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="device" value="{{ device.pk }}" /> <div class="panel panel-default">
{% endif %} <div class="panel-heading">
<div class="panel panel-default"> <strong>Power Outlets</strong>
<div class="panel-heading"> </div>
<strong>Power Outlets</strong> <table class="table table-hover table-headings panel-body component-list">
<thead>
<tr>
{% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
{% endif %}
<th>Name</th>
<th>Type</th>
<th>Input/Leg</th>
<th>Description</th>
<th>Cable</th>
<th colspan="3">Connection</th>
<th></th>
</tr>
</thead>
<tbody>
{% for po in poweroutlets %}
{% include 'dcim/inc/poweroutlet.html' %}
{% endfor %}
</tbody>
</table>
<div class="panel-footer noprint">
{% if poweroutlets and perms.dcim.change_powerport %}
<button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_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:poweroutlet_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:poweroutlet_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 poweroutlets and perms.dcim.delete_poweroutlet %}
<button type="submit" formaction="{% url 'dcim:poweroutlet_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 perms.dcim.add_poweroutlet %}
<div class="pull-right">
<a href="{% url 'dcim:poweroutlet_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
</a>
</div>
<div class="clearfix"></div>
{% endif %}
</div>
</div> </div>
<table class="table table-hover table-headings panel-body component-list"> </form>
<thead>
<tr>
{% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
{% endif %}
<th>Name</th>
<th>Type</th>
<th>Input/Leg</th>
<th>Description</th>
<th>Cable</th>
<th colspan="3">Connection</th>
<th></th>
</tr>
</thead>
<tbody>
{% for po in poweroutlets %}
{% include 'dcim/inc/poweroutlet.html' %}
{% endfor %}
</tbody>
</table>
<div class="panel-footer noprint">
{% if poweroutlets and perms.dcim.change_powerport %}
<button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_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:poweroutlet_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:poweroutlet_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 poweroutlets and perms.dcim.delete_poweroutlet %}
<button type="submit" formaction="{% url 'dcim:poweroutlet_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 perms.dcim.add_poweroutlet %}
<div class="pull-right">
<a href="{% url 'dcim:poweroutlet_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
</a>
</div>
<div class="clearfix"></div>
{% endif %}
</div>
</div>
{% if perms.dcim.delete_poweroutlet %}
</form>
{% endif %}
{% endif %} {% endif %}
{% if front_ports %} {% if front_ports %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="device" value="{{ device.pk }}" />
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Front Ports</strong> <strong>Front Ports</strong>
@ -815,7 +849,6 @@
{% if rear_ports %} {% if rear_ports %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="device" value="{{ device.pk }}" />
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Rear Ports</strong> <strong>Rear Ports</strong>

View 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 %}

View File

@ -5,61 +5,61 @@
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-12">
<div class="panel panel-default"> <form method="post">
<div class="panel-heading"> {% csrf_token %}
<strong>Chassis</strong> <div class="panel panel-default">
</div> <div class="panel-heading">
<table class="table table-hover panel-body attr-table"> <strong>Inventory Items</strong>
<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>
</div>
<table class="table table-hover table-condensed panel-body" id="hardware">
<thead>
<tr>
<th>Name</th>
<th></th>
<th>Manufacturer</th>
<th>Part ID</th>
<th>Serial Number</th>
<th>Asset Tag</th>
<th>Description</th>
<th></th>
</tr>
</thead>
<tbody>
{% for item in inventory_items %}
{% with template_name='dcim/inc/inventoryitem.html' indent=0 %}
{% include template_name %}
{% endwith %}
{% endfor %}
</tbody>
</table>
{% if perms.dcim.add_inventoryitem %}
<div class="panel-footer text-right noprint">
<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>
</div> </div>
{% endif %} <table class="table table-hover table-condensed panel-body" id="hardware">
</div> <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>Manufacturer</th>
<th>Part ID</th>
<th>Serial Number</th>
<th>Asset Tag</th>
<th>Discovered</th>
<th>Description</th>
<th></th>
</tr>
</thead>
<tbody>
{% for item in inventory_items %}
{% with template_name='dcim/inc/inventoryitem.html' indent=0 %}
{% include template_name %}
{% endwith %}
{% 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="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>
</div>
{% endif %}
</div>
</div>
</form>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -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_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_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_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> </ul>
</div> </div>
{% endif %} {% 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 %} {% endblock %}

View 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 %}

View File

@ -131,7 +131,7 @@
</tr> </tr>
<tr> <tr>
<td>Instances</td> <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> </tr>
</table> </table>
</div> </div>
@ -173,7 +173,7 @@
{% if devicetype.is_parent_device or devicebay_table.rows %} {% if devicetype.is_parent_device or devicebay_table.rows %}
<div class="row"> <div class="row">
<div class="col-md-12"> <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>
</div> </div>
{% endif %} {% endif %}

View 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 %}

View File

@ -16,7 +16,9 @@
</tr> </tr>
<tr> <tr>
<td>Component</td> <td>Component</td>
<td>{{ termination }}</td> <td>
<a href="{{ termination.get_absolute_url }}">{{ termination }}</a>
</td>
</tr> </tr>
{% else %} {% else %}
{# Circuit termination #} {# Circuit termination #}

View File

@ -1,8 +1,16 @@
<tr class="consoleport{% if cp.cable %} {{ cp.cable.get_status_class }}{% endif %}"> <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 #} {# Name #}
<td> <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> </td>
{# Type #} {# Type #}

View File

@ -11,7 +11,8 @@
{# Name #} {# Name #}
<td> <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> </td>
{# Type #} {# Type #}

View File

@ -9,7 +9,8 @@
{# Name #} {# Name #}
<td> <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> </td>
{# Status #} {# Status #}

View File

@ -10,7 +10,8 @@
{# Name #} {# Name #}
<td> <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> </td>
{# Type #} {# Type #}

View File

@ -166,7 +166,7 @@
</ul> </ul>
</span> </span>
{% endif %} {% 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> <i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}
@ -176,7 +176,7 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button> </button>
{% else %} {% 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> <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}

View File

@ -1,11 +1,34 @@
{% load helpers %}
<tr> <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> {# Checkbox #}
<td>{{ item.manufacturer|default:"" }}</td> {% if perms.dcim.change_inventoryitem or perms.dcim.delete_inventoryitem %}
<td>{{ item.part_id }}</td> <td class="pk">
<td>{{ item.serial }}</td> <input name="pk" type="checkbox" value="{{ item.pk }}" />
<td>{{ item.asset_tag|default:"" }}</td> </td>
<td>{{ item.description }}</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">&mdash;</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">&mdash;</span>
{% endif %}
</td>
<td>{{ item.description|placeholder }}</td>
<td class="text-right noprint"> <td class="text-right noprint">
{% if perms.dcim.change_inventoryitem %} {% 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> <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>

View File

@ -11,7 +11,8 @@
{# Name #} {# Name #}
<td> <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> </td>
{# Type #} {# Type #}

View File

@ -1,8 +1,16 @@
<tr class="powerport{% if pp.cable %} {{ pp.cable.get_status_class }}{% endif %}"> <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 #} {# Name #}
<td> <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> </td>
{# Type #} {# Type #}

View File

@ -1,4 +1,6 @@
<object data="{% url 'dcim-api:rack-elevation' pk=rack.pk %}?face={{face}}&render=svg" class="rack_elevation"></object> <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"> <div class="text-center text-small">
<a href="{% url 'dcim-api:rack-elevation' pk=rack.pk %}?face={{face}}&render=svg"> <a href="{% url 'dcim-api:rack-elevation' pk=rack.pk %}?face={{face}}&render=svg">
<i class="fa fa-download"></i> Save SVG <i class="fa fa-download"></i> Save SVG

View File

@ -10,7 +10,8 @@
{# Name #} {# Name #}
<td> <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> </td>
{# Type #} {# Type #}

View File

@ -1,258 +1,227 @@
{% extends 'base.html' %} {% extends 'dcim/device_component.html' %}
{% load helpers %} {% load helpers %}
{% load plugins %}
{% 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 %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Interface</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>{% if interface.device %}Device{% else %}Virtual Machine{% endif %}</td>
<td>
<a href="{{ interface.parent.get_absolute_url }}">{{ interface.parent }}</a>
</td>
</tr>
<tr>
<td>Name</td>
<td>{{ interface.name }}</td>
</tr>
<tr>
<td>Label</td>
<td>{{ interface.label|placeholder }}</td>
</tr>
<tr>
<td>Type</td>
<td>{{ interface.get_type_display }}</td>
</tr>
<tr>
<td>Enabled</td>
<td>
{% if interface.enabled %}
<span class="text-success"><i class="fa fa-check"></i></span>
{% else %}
<span class="text-danger"><i class="fa fa-close"></i></span>
{% endif %}
</td>
</tr>
<tr>
<td>LAG</td>
<td>
{% if interface.lag%}
<a href="{{ interface.lag.get_absolute_url }}">{{ interface.lag }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Description</td>
<td>{{ interface.description|placeholder }} </td>
</tr>
<tr>
<td>MTU</td>
<td>{{ interface.mtu|placeholder }}</td>
</tr>
<tr>
<td>MAC Address</td>
<td>{{ interface.mac_address|placeholder }}</span></td>
</tr>
<tr>
<td>802.1Q Mode</td>
<td>{{ interface.get_mode_display }}</td>
</tr>
</table>
</div>
{% include 'extras/inc/tags_panel.html' with tags=interface.tags.all %}
</div>
<div class="col-md-6">
{% if interface.is_connectable %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Connection</strong> <strong>Interface</strong>
</div> </div>
{% if interface.cable %} <table class="table table-hover panel-body attr-table">
<table class="table table-hover panel-body attr-table"> <tr>
{% if connected_interface %} <td>Device</td>
<tr> <td>
<td>Device</td> <a href="{{ instance.device.get_absolute_url }}">{{ instance.device }}</a>
<td> </td>
<a href="{{ connected_interface.parent.get_absolute_url }}">{{ connected_interface.device }}</a> </tr>
</td> <tr>
</tr> <td>Name</td>
<tr> <td>{{ instance.name }}</td>
<td>Name</td> </tr>
<td> <tr>
<a href="{{ connected_interface.get_absolute_url }}">{{ connected_interface.name }}</a> <td>Label</td>
</td> <td>{{ instance.label|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<td>Type</td> <td>Type</td>
<td>{{ connected_interface.get_type_display }}</td> <td>{{ instance.get_type_display }}</td>
</tr> </tr>
<tr> <tr>
<td>Enabled</td> <td>Enabled</td>
<td> <td>
{% if connected_interface.enabled %} {% if instance.enabled %}
<span class="text-success"><i class="fa fa-check"></i></span> <span class="text-success"><i class="fa fa-check"></i></span>
{% else %} {% else %}
<span class="text-danger"><i class="fa fa-close"></i></span> <span class="text-danger"><i class="fa fa-close"></i></span>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<tr> <tr>
<td>LAG</td> <td>LAG</td>
<td> <td>
{% if connected_interface.lag%} {% if instance.lag%}
<a href="{{ connected_interface.lag.get_absolute_url }}">{{ connected_interface.lag }}</a> <a href="{{ instance.lag.get_absolute_url }}">{{ instance.lag }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Description</td> <td>Description</td>
<td>{{ connected_interface.description|placeholder }}</td> <td>{{ instance.description|placeholder }} </td>
</tr> </tr>
<tr> <tr>
<td>MTU</td> <td>MTU</td>
<td>{{ connected_interface.mtu|placeholder }}</td> <td>{{ instance.mtu|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<td>MAC Address</td> <td>MAC Address</td>
<td>{{ connected_interface.mac_address|placeholder }}</td> <td><span class="text-monospace">{{ instance.mac_address|placeholder }}</span></td>
</tr> </tr>
<tr> <tr>
<td>802.1Q Mode</td> <td>802.1Q Mode</td>
<td>{{ connected_interface.get_mode_display }}</td> <td>{{ instance.get_mode_display }}</td>
</tr> </tr>
{% elif connected_circuittermination %}
{% with ct=connected_circuittermination %}
<tr>
<td>Provider</td>
<td><a href="{{ ct.circuit.provider.get_absolute_url }}">{{ ct.circuit.provider }}</a></td>
</tr>
<tr>
<td>Circuit</td>
<td><a href="{{ ct.circuit.get_absolute_url }}">{{ ct.circuit }}</a></td>
</tr>
<tr>
<td>Side</td>
<td>{{ ct.term_side }}</td>
</tr>
{% endwith %}
{% endif %}
<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">
<i class="fa fa-share-alt" aria-hidden="true"></i>
</a>
</td>
</tr>
<tr>
<td>Connection Status</td>
<td>
{% if interface.connection_status %}
<span class="label label-success">{{ interface.get_connection_status_display }}</span>
{% else %}
<span class="label label-info">{{ interface.get_connection_status_display }}</span>
{% endif %}
</td>
</tr>
</table>
{% else %}
<div class="panel-body text-muted">
Not connected
</div>
{% endif %}
</div>
{% endif %}
{% if interface.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">
<thead>
<tr>
<th>Parent</th>
<th>Interface</th>
<th>Type</th>
</tr>
</thead>
<tbody>
{% for member in interface.member_interfaces.all %}
<tr>
<td>
<a href="{{ member.parent.get_absolute_url }}">{{ member.parent }}</a>
</td>
<td>
<a href="{{ member.get_absolute_url }}">{{ member }}</a>
</td>
<td>
{{ member.get_type_display }}
</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-muted">No member interfaces</td>
</tr>
{% endfor %}
</tbody>
</table> </table>
</div> </div>
{% endif %} {% include 'extras/inc/tags_panel.html' with tags=instance.tags.all %}
{% plugin_left_page instance %}
</div>
<div class="col-md-6">
{% if instance.is_connectable %}
<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 connected_interface %}
<tr>
<td>Device</td>
<td>
<a href="{{ connected_interface.device.get_absolute_url }}">{{ connected_interface.device }}</a>
</td>
</tr>
<tr>
<td>Name</td>
<td>
<a href="{{ connected_interface.get_absolute_url }}">{{ connected_interface.name }}</a>
</td>
</tr>
<tr>
<td>Type</td>
<td>{{ connected_interface.get_type_display }}</td>
</tr>
<tr>
<td>Enabled</td>
<td>
{% if connected_interface.enabled %}
<span class="text-success"><i class="fa fa-check"></i></span>
{% else %}
<span class="text-danger"><i class="fa fa-close"></i></span>
{% endif %}
</td>
</tr>
<tr>
<td>LAG</td>
<td>
{% if connected_interface.lag%}
<a href="{{ connected_interface.lag.get_absolute_url }}">{{ connected_interface.lag }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Description</td>
<td>{{ connected_interface.description|placeholder }}</td>
</tr>
<tr>
<td>MTU</td>
<td>{{ connected_interface.mtu|placeholder }}</td>
</tr>
<tr>
<td>MAC Address</td>
<td>{{ connected_interface.mac_address|placeholder }}</td>
</tr>
<tr>
<td>802.1Q Mode</td>
<td>{{ connected_interface.get_mode_display }}</td>
</tr>
{% elif connected_circuittermination %}
{% with ct=connected_circuittermination %}
<tr>
<td>Provider</td>
<td><a href="{{ ct.circuit.provider.get_absolute_url }}">{{ ct.circuit.provider }}</a></td>
</tr>
<tr>
<td>Circuit</td>
<td><a href="{{ ct.circuit.get_absolute_url }}">{{ ct.circuit }}</a></td>
</tr>
<tr>
<td>Side</td>
<td>{{ ct.term_side }}</td>
</tr>
{% endwith %}
{% endif %}
<tr>
<td>Cable</td>
<td>
<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>
</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>
{% endif %}
{% 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">
<thead>
<tr>
<th>Parent</th>
<th>Interface</th>
<th>Type</th>
</tr>
</thead>
<tbody>
{% for member in instance.member_interfaces.all %}
<tr>
<td>
<a href="{{ member.device.get_absolute_url }}">{{ member.device }}</a>
</td>
<td>
<a href="{{ member.get_absolute_url }}">{{ member }}</a>
</td>
<td>
{{ member.get_type_display }}
</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-muted">No member interfaces</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% plugin_right_page instance %}
</div>
</div> </div>
</div> <div class="row">
<div class="row"> <div class="col-md-12">
<div class="col-md-12"> {% include 'panel_table.html' with table=ipaddress_table heading="IP Addresses" %}
{% include 'panel_table.html' with table=ipaddress_table heading="IP Addresses" %} </div>
</div> </div>
</div> <div class="row">
<div class="row"> <div class="col-md-12">
<div class="col-md-12"> {% include 'panel_table.html' with table=vlan_table heading="VLANs" %}
{% 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> </div>
</div>
{% endblock %} {% endblock %}

View 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">&mdash;</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">&mdash;</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 %}

View 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 %}

View 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 %}

View File

@ -318,16 +318,12 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="row" style="margin-bottom: 20px"> <div class="row" style="margin-bottom: 20px">
<div class="col-md-6 col-sm-6 col-xs-12"> <div class="col-md-6 col-sm-6 col-xs-12 text-center">
<div class="rack_header"> <h4>Front</h4>
<h4>Front</h4>
</div>
{% include 'dcim/inc/rack_elevation.html' with face='front' %} {% include 'dcim/inc/rack_elevation.html' with face='front' %}
</div> </div>
<div class="col-md-6 col-sm-6 col-xs-12"> <div class="col-md-6 col-sm-6 col-xs-12 text-center">
<div class="rack_header"> <h4>Rear</h4>
<h4>Rear</h4>
</div>
{% include 'dcim/inc/rack_elevation.html' with face='rear' %} {% include 'dcim/inc/rack_elevation.html' with face='rear' %}
</div> </div>
</div> </div>

View 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 %}

View File

@ -9,7 +9,9 @@
<div class="col-sm-8 col-md-9"> <div class="col-sm-8 col-md-9">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'dcim:virtualchassis_list' %}">Virtual Chassis</a></li> <li><a href="{% url 'dcim:virtualchassis_list' %}">Virtual Chassis</a></li>
<li><a href="{% url 'dcim:virtualchassis_list' %}?site={{ virtualchassis.master.site.slug }}">{{ virtualchassis.master.site }}</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> <li>{{ virtualchassis }}</li>
</ol> </ol>
</div> </div>
@ -63,7 +65,17 @@
<tr> <tr>
<td>Domain</td> <td>Domain</td>
<td>{{ virtualchassis.domain|placeholder }}</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">&mdash;</span>
{% endif %}
</td>
</tr>
</table> </table>
</div> </div>
{% include 'extras/inc/tags_panel.html' with tags=virtualchassis.tags.all url='dcim:virtualchassis_list' %} {% include 'extras/inc/tags_panel.html' with tags=virtualchassis.tags.all url='dcim:virtualchassis_list' %}
@ -81,7 +93,7 @@
<th>Master</th> <th>Master</th>
<th>Priority</th> <th>Priority</th>
</tr> </tr>
{% for vc_member in virtualchassis.members.all %} {% for vc_member in members %}
<tr{% if vc_member == device %} class="info"{% endif %}> <tr{% if vc_member == device %} class="info"{% endif %}>
<td> <td>
<a href="{{ vc_member.get_absolute_url }}">{{ vc_member }}</a> <a href="{{ vc_member.get_absolute_url }}">{{ vc_member }}</a>

View 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 %}

View File

@ -4,7 +4,7 @@
<strong>Tags</strong> <strong>Tags</strong>
</div> </div>
<div class="panel-body"> <div class="panel-body">
{% for tag in tags.unrestricted %} {% for tag in tags.all %}
{% tag tag url %} {% tag tag url %}
{% empty %} {% empty %}
<span class="text-muted">No tags assigned</span> <span class="text-muted">No tags assigned</span>

View File

@ -144,6 +144,12 @@
<a href="{% url 'dcim:platform_list' %}">Platforms</a> <a href="{% url 'dcim:platform_list' %}">Platforms</a>
</li> </li>
<li{% if not perms.dcim.view_virtualchassis %} class="disabled"{% endif %}> <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> <a href="{% url 'dcim:virtualchassis_list' %}">Virtual Chassis</a>
</li> </li>
<li class="divider"></li> <li class="divider"></li>
@ -167,16 +173,6 @@
<a href="{% url 'dcim:manufacturer_list' %}">Manufacturers</a> <a href="{% url 'dcim:manufacturer_list' %}">Manufacturers</a>
</li> </li>
<li class="divider"></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 class="dropdown-header">Connections</li>
<li{% if not perms.dcim.view_cable %} class="disabled"{% endif %}> <li{% if not perms.dcim.view_cable %} class="disabled"{% endif %}>
{% if perms.dcim.add_cable %} {% if perms.dcim.add_cable %}
@ -261,6 +257,14 @@
{% endif %} {% endif %}
<a href="{% url 'dcim:devicebay_list' %}">Device Bays</a> <a href="{% url 'dcim:devicebay_list' %}">Device Bays</a>
</li> </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> </ul>
</li> </li>
<li class="dropdown"> <li class="dropdown">
@ -372,6 +376,14 @@
{% endif %} {% endif %}
<a href="{% url 'virtualization:virtualmachine_list' %}">Virtual Machines</a> <a href="{% url 'virtualization:virtualmachine_list' %}">Virtual Machines</a>
</li> </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="divider"></li>
<li class="dropdown-header">Clusters</li> <li class="dropdown-header">Clusters</li>
<li{% if not perms.virtualization.view_cluster %} class="disabled"{% endif %}> <li{% if not perms.virtualization.view_cluster %} class="disabled"{% endif %}>

View File

@ -19,21 +19,21 @@
{% endif %} {% endif %}
</ul> </ul>
</nav> </nav>
<form method="get">
{% for k, v_list in request.GET.lists %}
{% if k != 'per_page' %}
{% for v in v_list %}
<input type="hidden" name="{{ k }}" value="{{ v }}" />
{% endfor %}
{% endif %}
{% endfor %}
<select name="per_page" id="per_page">
{% for n in settings.PER_PAGE_DEFAULTS %}
<option value="{{ n }}"{% if page.paginator.per_page == n %} selected="selected"{% endif %}>{{ n }}</option>
{% endfor %}
</select> per page
</form>
{% endif %} {% endif %}
<form method="get">
{% for k, v_list in request.GET.lists %}
{% if k != 'per_page' %}
{% for v in v_list %}
<input type="hidden" name="{{ k }}" value="{{ v }}" />
{% endfor %}
{% endif %}
{% endfor %}
<select name="per_page" id="per_page">
{% for n in settings.PER_PAGE_DEFAULTS %}
<option value="{{ n }}"{% if page.paginator.per_page == n %} selected="selected"{% endif %}>{{ n }}</option>
{% endfor %}
</select> per page
</form>
{% if page %} {% if page %}
<div class="text-right text-muted"> <div class="text-right text-muted">
Showing {{ page.start_index }}-{{ page.end_index }} of {{ page.paginator.count }} 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