Further work on power feed modeling

This commit is contained in:
Jeremy Stretch 2019-03-21 17:47:43 -04:00
parent 705f82e416
commit 681e20133a
10 changed files with 282 additions and 39 deletions

View File

@ -496,7 +496,7 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
queryset = PowerPort.objects.select_related(
'device', 'connected_endpoint__device'
).filter(
connected_endpoint__isnull=False
_connected_poweroutlet__isnull=False
)
serializer_class = serializers.PowerPortSerializer
filterset_class = filters.PowerConnectionFilter

View File

@ -420,7 +420,7 @@ CABLE_TERMINATION_TYPE_CHOICES = {
COMPATIBLE_TERMINATION_TYPES = {
'consoleport': ['consoleserverport', 'frontport', 'rearport'],
'consoleserverport': ['consoleport', 'frontport', 'rearport'],
'powerport': ['poweroutlet'],
'powerport': ['poweroutlet', 'powerfeed'],
'poweroutlet': ['powerport'],
'interface': ['interface', 'circuittermination', 'frontport', 'rearport'],
'frontport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],

View File

@ -10,6 +10,7 @@ from mptt.forms import TreeNodeChoiceField
from taggit.forms import TagField
from timezone_field import TimeZoneFormField
from circuits.models import Circuit, CircuitTermination, Provider
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from ipam.models import IPAddress, VLAN, VLANGroup
from tenancy.forms import TenancyForm
@ -2521,7 +2522,7 @@ class RearPortBulkDisconnectForm(ConfirmationForm):
# Cables
#
class CableCreateForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
class ConnectCableToDeviceForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
termination_b_site = forms.ModelChoiceField(
queryset=Site.objects.all(),
label='Site',
@ -2602,6 +2603,104 @@ class CableCreateForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
)
class ConnectCableToCircuitForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
termination_b_provider = forms.ModelChoiceField(
queryset=Provider.objects.all(),
label='Provider',
widget=APISelect(
api_url='/api/circuits/providers/',
filter_for={
'termination_b_circuit': 'provider_id',
}
)
)
termination_b_circuit = ChainedModelChoiceField(
queryset=Circuit.objects.all(),
chains=(
('provider', 'termination_b_provider'),
),
label='Circuit',
widget=APISelect(
api_url='/api/circuits/circuits/',
display_field='cid',
filter_for={
'termination_b_id': 'circuit_id',
}
)
)
termination_b_id = forms.IntegerField(
label='Termination',
widget=APISelect(
api_url='/api/circuits/circuit-terminations/',
disabled_indicator='cable'
)
)
class Meta:
model = Cable
fields = [
'termination_b_provider', 'termination_b_circuit', 'termination_b_id', 'type', 'status', 'label', 'color',
'length', 'length_unit',
]
class ConnectCableToPowerFeedForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
termination_b_site = forms.ModelChoiceField(
queryset=Site.objects.all(),
label='Site',
widget=APISelect(
api_url='/api/dcim/sites/',
display_field='cid',
filter_for={
'termination_b_rackgroup': 'site_id',
'termination_b_powerpanel': 'site_id',
}
)
)
termination_b_rackgroup = ChainedModelChoiceField(
queryset=RackGroup.objects.all(),
label='Rack Group',
chains=(
('site', 'termination_b_site'),
),
required=False,
widget=APISelect(
api_url='/api/dcim/rack-groups/',
display_field='cid',
filter_for={
'termination_b_powerpanel': 'rackgroup_id',
}
)
)
termination_b_powerpanel = ChainedModelChoiceField(
queryset=PowerPanel.objects.all(),
chains=(
('site', 'termination_b_site'),
('rack_group', 'termination_b_rackgroup'),
),
label='Power Panel',
widget=APISelect(
api_url='/api/dcim/power-panels/',
filter_for={
'termination_b_powerfeed': 'powerpanel_id',
}
)
)
termination_b_id = forms.IntegerField(
label='Power Feed',
widget=APISelect(
api_url='/api/dcim/power-feeds/',
)
)
class Meta:
model = Cable
fields = [
'termination_b_rackgroup', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'label',
'color', 'length', 'length_unit',
]
class CableForm(BootstrapMixin, forms.ModelForm):
class Meta:

View File

@ -1,4 +1,4 @@
# Generated by Django 2.1.7 on 2019-03-12 14:08
# Generated by Django 2.1.7 on 2019-03-21 20:59
import django.core.validators
from django.db import migrations, models
@ -21,14 +21,15 @@ class Migration(migrations.Migration):
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('name', models.CharField(max_length=50)),
('type', models.PositiveSmallIntegerField(default=1)),
('status', models.PositiveSmallIntegerField(default=1)),
('type', models.PositiveSmallIntegerField(default=1)),
('supply', models.PositiveSmallIntegerField(default=1)),
('phase', models.PositiveSmallIntegerField(default=1)),
('voltage', models.PositiveSmallIntegerField(default=120, validators=[django.core.validators.MinValueValidator(1)])),
('amperage', models.PositiveSmallIntegerField(default=20, validators=[django.core.validators.MinValueValidator(1)])),
('phase', models.PositiveSmallIntegerField(default=1)),
('max_utilization', models.PositiveSmallIntegerField(default=80, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])),
('comments', models.TextField(blank=True)),
('cable', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Cable')),
],
options={
'ordering': ['power_panel', 'name'],
@ -63,6 +64,26 @@ class Migration(migrations.Migration):
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AddField(
model_name='powerfeed',
name='connected_endpoint',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.PowerPort'),
),
migrations.AddField(
model_name='powerfeed',
name='connection_status',
field=models.NullBooleanField(),
),
migrations.RenameField(
model_name='powerport',
old_name='connected_endpoint',
new_name='_connected_poweroutlet',
),
migrations.AddField(
model_name='powerport',
name='_connected_powerfeed',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.PowerFeed'),
),
migrations.AlterUniqueTogether(
name='powerpanel',
unique_together={('site', 'name')},

View File

@ -1828,13 +1828,20 @@ class PowerPort(CableTermination, ComponentModel):
name = models.CharField(
max_length=50
)
connected_endpoint = models.OneToOneField(
_connected_poweroutlet = models.OneToOneField(
to='dcim.PowerOutlet',
on_delete=models.SET_NULL,
related_name='connected_endpoint',
blank=True,
null=True
)
_connected_powerfeed = models.OneToOneField(
to='dcim.PowerFeed',
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True
)
connection_status = models.NullBooleanField(
choices=CONNECTION_STATUS_CHOICES,
blank=True
@ -1862,6 +1869,28 @@ class PowerPort(CableTermination, ComponentModel):
self.description,
)
@property
def connected_endpoint(self):
if self._connected_poweroutlet:
return self._connected_poweroutlet
return self._connected_powerfeed
@connected_endpoint.setter
def connected_endpoint(self, value):
if value is None:
self._connected_poweroutlet = None
self._connected_powerfeed = None
elif isinstance(value, PowerOutlet):
self._connected_poweroutlet = value
self._connected_powerfeed = None
elif isinstance(value, PowerFeed):
self._connected_poweroutlet = None
self._connected_powerfeed = value
else:
raise ValueError(
"Connected endpoint must be a PowerOutlet or PowerFeed, not {}.".format(type(value))
)
#
# Power outlets
@ -2646,6 +2675,14 @@ class Cable(ChangeLoggedModel):
def get_status_class(self):
return 'success' if self.status else 'info'
def get_compatible_types(self):
"""
Return all termination types compatible with termination A.
"""
if self.termination_a is None:
return
return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name]
def get_path_endpoints(self):
"""
Traverse both ends of a cable path and return its connected endpoints. Note that one or both endpoints may be
@ -2712,7 +2749,7 @@ class PowerPanel(ChangeLoggedModel):
)
class PowerFeed(ChangeLoggedModel, CustomFieldModel):
class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
"""
An electrical circuit delivered from a PowerPanel.
"""
@ -2727,6 +2764,17 @@ class PowerFeed(ChangeLoggedModel, CustomFieldModel):
blank=True,
null=True
)
connected_endpoint = models.OneToOneField(
to='dcim.PowerPort',
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True
)
connection_status = models.NullBooleanField(
choices=CONNECTION_STATUS_CHOICES,
blank=True
)
name = models.CharField(
max_length=50
)

View File

@ -3,6 +3,7 @@ import re
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.core.paginator import EmptyPage, PageNotAnInteger
from django.db import transaction
from django.db.models import Count, F
@ -10,10 +11,11 @@ from django.forms import modelformset_factory
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.html import escape
from django.utils.http import is_safe_url
from django.utils.safestring import mark_safe
from django.views.generic import View
from circuits.models import Circuit
from circuits.models import Circuit, CircuitTermination
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from extras.views import ObjectConfigContextView
from ipam.models import Prefix, VLAN
@ -913,7 +915,7 @@ class DeviceView(View):
consoleserverports = device.consoleserverports.select_related('connected_endpoint__device', 'cable')
# Power ports
power_ports = device.powerports.select_related('connected_endpoint__device', 'cable')
power_ports = device.powerports.select_related('_connected_poweroutlet__device', 'cable')
# Power outlets
poweroutlets = device.poweroutlets.select_related('connected_endpoint__device', 'cable')
@ -1673,20 +1675,76 @@ class CableTraceView(View):
})
class CableCreateView(PermissionRequiredMixin, ObjectEditView):
class CableCreateView(PermissionRequiredMixin, GetReturnURLMixin, View):
permission_required = 'dcim.add_cable'
model = Cable
model_form = forms.CableCreateForm
template_name = 'dcim/cable_connect.html'
def alter_obj(self, obj, request, url_args, url_kwargs):
def _get_form_class(self):
if self.termination_b_type == 'circuit':
return forms.ConnectCableToCircuitForm
if self.termination_b_type == 'powerfeed':
return forms.ConnectCableToPowerFeedForm
return forms.ConnectCableToDeviceForm
def dispatch(self, request, *args, **kwargs):
# Retrieve endpoint A based on the given type and PK
termination_a_type = url_kwargs.get('termination_a_type')
termination_a_id = url_kwargs.get('termination_a_id')
obj.termination_a = termination_a_type.objects.get(pk=termination_a_id)
termination_a_type = kwargs.get('termination_a_type')
termination_a_id = kwargs.get('termination_a_id')
self.obj = Cable(
termination_a=termination_a_type.objects.get(pk=termination_a_id)
)
return obj
self.termination_b_type = request.GET.get('type')
if self.termination_b_type == 'circuit':
self.obj.termination_b_type = ContentType.objects.get_for_model(CircuitTermination)
elif self.termination_b_type == 'powerfeed':
self.obj.termination_b_type = ContentType.objects.get_for_model(PowerFeed)
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
# Parse initial data manually to avoid setting field values as lists
initial_data = {k: request.GET[k] for k in request.GET}
form = self._get_form_class()(instance=self.obj, initial=initial_data)
return render(request, self.template_name, {
'obj': self.obj,
'obj_type': Cable._meta.verbose_name,
'form': form,
'return_url': self.get_return_url(request, self.obj),
})
def post(self, request, *args, **kwargs):
form = self._get_form_class()(request.POST, request.FILES, instance=self.obj)
if form.is_valid():
obj = form.save()
msg = 'Created cable <a href="{}">{}</a>'.format(
obj.get_absolute_url(),
escape(obj)
)
messages.success(request, mark_safe(msg))
if '_addanother' in request.POST:
return redirect(request.get_full_path())
return_url = form.cleaned_data.get('return_url')
if return_url is not None and is_safe_url(url=return_url, allowed_hosts=request.get_host()):
return redirect(return_url)
else:
return redirect(self.get_return_url(request, obj))
return render(request, self.template_name, {
'obj': self.obj,
'obj_type': Cable._meta.verbose_name,
'form': form,
'return_url': self.get_return_url(request, self.obj),
})
class CableEditView(PermissionRequiredMixin, ObjectEditView):
@ -1763,11 +1821,11 @@ class ConsoleConnectionsListView(ObjectListView):
class PowerConnectionsListView(ObjectListView):
queryset = PowerPort.objects.select_related(
'device', 'connected_endpoint__device'
'device', '_connected_poweroutlet__device'
).filter(
connected_endpoint__isnull=False
_connected_poweroutlet__isnull=False
).order_by(
'cable', 'connected_endpoint__device__name', 'connected_endpoint__name'
'cable', '_connected_poweroutlet__device__name', '_connected_poweroutlet__name'
)
filter = filters.PowerConnectionFilter
filter_form = forms.PowerConnectionFilterForm

View File

@ -541,7 +541,7 @@ class TopologyMap(models.Model):
from dcim.models import PowerPort
# Add all power connections to the graph
for pp in PowerPort.objects.filter(device__in=devices, connected_endpoint__device__in=devices):
for pp in PowerPort.objects.filter(device__in=devices, _connected_poweroutlet__device__in=devices):
style = 'solid' if pp.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
self.graph.edge(pp.connected_endpoint.device.name, pp.device.name, style=style)

View File

@ -166,7 +166,7 @@ class HomeView(View):
connected_endpoint__isnull=False
)
connected_powerports = PowerPort.objects.filter(
connected_endpoint__isnull=False
_connected_poweroutlet__isnull=False
)
connected_interfaces = Interface.objects.filter(
_connected_interface__isnull=False,

View File

@ -101,21 +101,34 @@
<strong>B Side</strong>
</div>
<div class="panel-body">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
<li role="presentation"><a href="#select" aria-controls="home" role="tab" data-toggle="tab">Select</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="search">
&nbsp;
</div>
<div class="tab-pane" id="select">
{% render_field form.termination_b_site %}
{% render_field form.termination_b_rack %}
</div>
</div>
{% render_field form.termination_b_device %}
{% render_field form.termination_b_type %}
{# TODO: Clean this up #}
{% if 'termination_b_site' in form.fields %}
{% render_field form.termination_b_site %}
{% endif %}
{% if 'termination_b_rackgroup' in form.fields %}
{% render_field form.termination_b_rackgroup %}
{% endif %}
{% if 'termination_b_rack' in form.fields %}
{% render_field form.termination_b_rack %}
{% endif %}
{% if 'termination_b_device' in form.fields %}
{% render_field form.termination_b_device %}
{% endif %}
{% if 'termination_b_type' in form.fields %}
{% render_field form.termination_b_type %}
{% endif %}
{% if 'termination_b_provider' in form.fields %}
{% render_field form.termination_b_provider %}
{% endif %}
{% if 'termination_b_circuit' in form.fields %}
{% render_field form.termination_b_circuit %}
{% endif %}
{% if 'termination_b_powerpanel' in form.fields %}
{% render_field form.termination_b_powerpanel %}
{% endif %}
{% if 'termination_b_powerfeed' in form.fields %}
{% render_field form.termination_b_powerfeed %}
{% endif %}
{% render_field form.termination_b_id %}
</div>
</div>

View File

@ -20,13 +20,17 @@
</td>
{# Connection #}
{% if pp.connected_endpoint %}
{% if pp.connected_endpoint.device %}
<td>
<a href="{% url 'dcim:device' pk=pp.connected_endpoint.device.pk %}">{{ pp.connected_endpoint.device }}</a>
</td>
<td>
{{ pp.connected_endpoint }}
</td>
{% elif pp.connected_endpoint %}
<td colspan="2">
<a href="{{ pp.connected_endpoint.get_absolute_url }}">{{ pp.connected_endpoint }}</a>
</td>
{% else %}
<td colspan="2">
<span class="text-muted">Not connected</span>