mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Further work on power feed modeling
This commit is contained in:
parent
705f82e416
commit
681e20133a
@ -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
|
||||
|
@ -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'],
|
||||
|
@ -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:
|
||||
|
@ -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')},
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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">
|
||||
|
||||
</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>
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user