diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index f9452cebf..402fc5e13 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -215,10 +215,16 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer): class PowerOutletTemplateSerializer(ValidatedModelSerializer): device_type = NestedDeviceTypeSerializer() + power_port = PowerPortTemplateSerializer() + feed_leg = ChoiceField( + choices=POWERFEED_LEG_CHOICES, + required=False, + allow_null=True + ) class Meta: model = PowerOutletTemplate - fields = ['id', 'device_type', 'name'] + fields = ['id', 'device_type', 'name', 'power_port', 'feed_leg'] class InterfaceTemplateSerializer(ValidatedModelSerializer): @@ -372,14 +378,24 @@ class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer): class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer): device = NestedDeviceSerializer() - cable = NestedCableSerializer(read_only=True) - tags = TagListSerializerField(required=False) + power_port = NestedPowerPortSerializer() + feed_leg = ChoiceField( + choices=POWERFEED_LEG_CHOICES, + required=False, + allow_null=True + ) + cable = NestedCableSerializer( + read_only=True + ) + tags = TagListSerializerField( + required=False + ) class Meta: model = PowerOutlet fields = [ - 'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', - 'cable', 'tags', + 'id', 'device', 'name', 'power_port', 'feed_leg', 'description', 'connected_endpoint_type', + 'connected_endpoint', 'connection_status', 'cable', 'tags', ] diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index d4909b8e4..037a5a765 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -44,6 +44,8 @@ class DCIMFieldChoicesViewSet(FieldChoicesViewSet): (FrontPortTemplate, ['type']), (Interface, ['form_factor', 'mode']), (InterfaceTemplate, ['form_factor']), + (PowerOutlet, ['feed_leg']), + (PowerOutletTemplate, ['feed_leg']), (PowerPort, ['connection_status']), (Rack, ['outer_unit', 'status', 'type', 'width']), (RearPort, ['type']), diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index e3f4f190b..6c135254c 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -475,3 +475,11 @@ POWERFEED_STATUS_CHOICES = ( (POWERFEED_STATUS_PLANNED, 'Planned'), (POWERFEED_STATUS_FAILED, 'Failed'), ) +POWERFEED_LEG_A = 1 +POWERFEED_LEG_B = 2 +POWERFEED_LEG_C = 3 +POWERFEED_LEG_CHOICES = ( + (POWERFEED_LEG_A, 'A'), + (POWERFEED_LEG_B, 'B'), + (POWERFEED_LEG_C, 'C'), +) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index cb97f0e84..f8aa7708d 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -977,16 +977,29 @@ class PowerPortTemplateCreateForm(ComponentForm): class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm): + power_port = forms.ModelChoiceField( + queryset=PowerPortTemplate.objects.all(), + required=False + ) class Meta: model = PowerOutletTemplate fields = [ - 'device_type', 'name', + 'device_type', 'name', 'power_port', 'feed_leg', ] widgets = { 'device_type': forms.HiddenInput(), } + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + + # Limit power_port choices to current DeviceType + self.fields['power_port'].queryset = PowerPortTemplate.objects.filter( + device_type=self.parent + ) + class PowerOutletTemplateCreateForm(ComponentForm): name_pattern = ExpandableNameField( @@ -1972,6 +1985,10 @@ class PowerPortCreateForm(ComponentForm): # class PowerOutletForm(BootstrapMixin, forms.ModelForm): + power_port = forms.ModelChoiceField( + queryset=PowerPort.objects.all(), + required=False + ) tags = TagField( required=False ) @@ -1979,12 +1996,20 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm): class Meta: model = PowerOutlet fields = [ - 'device', 'name', 'description', 'tags', + 'device', 'name', 'power_port', 'feed_leg', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit power_port choices to the local device + self.fields['power_port'].queryset = PowerPort.objects.filter( + device=self.instance.device + ) + class PowerOutletCreateForm(ComponentForm): name_pattern = ExpandableNameField( @@ -2004,6 +2029,10 @@ class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): queryset=PowerOutlet.objects.all(), widget=forms.MultipleHiddenInput() ) + feed_leg = forms.ChoiceField( + choices=POWERFEED_LEG_CHOICES, + required=False, + ) description = forms.CharField( max_length=100, required=False diff --git a/netbox/dcim/migrations/0072_powerfeeds.py b/netbox/dcim/migrations/0072_powerfeeds.py index d8dcbf252..4eb43c20b 100644 --- a/netbox/dcim/migrations/0072_powerfeeds.py +++ b/netbox/dcim/migrations/0072_powerfeeds.py @@ -1,5 +1,3 @@ -# Generated by Django 2.1.7 on 2019-03-21 20:59 - import django.core.validators from django.db import migrations, models import django.db.models.deletion @@ -112,4 +110,24 @@ class Migration(migrations.Migration): name='powerfeed', unique_together={('power_panel', 'name')}, ), + migrations.AddField( + model_name='poweroutlet', + name='feed_leg', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='poweroutlet', + name='power_port', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='poweroutlets', to='dcim.PowerPort'), + ), + migrations.AddField( + model_name='poweroutlettemplate', + name='feed_leg', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='poweroutlettemplate', + name='power_port', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='poweroutlet_templates', to='dcim.PowerPortTemplate'), + ), ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 9e2be6a43..fcf3252cf 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1088,6 +1088,19 @@ class PowerOutletTemplate(ComponentTemplateModel): name = models.CharField( max_length=50 ) + power_port = models.ForeignKey( + to='dcim.PowerPortTemplate', + on_delete=models.SET_NULL, + blank=True, + null=True, + related_name='poweroutlet_templates' + ) + feed_leg = models.PositiveSmallIntegerField( + choices=POWERFEED_LEG_CHOICES, + blank=True, + null=True, + help_text="Phase (for three-phase feeds)" + ) objects = DeviceComponentManager() @@ -1098,6 +1111,14 @@ class PowerOutletTemplate(ComponentTemplateModel): def __str__(self): return self.name + def clean(self): + + # Validate power port assignment + if self.power_port and self.power_port.device_type != self.device_type: + raise ValidationError( + "Parent power port ({}) must belong to the same device type".format(self.power_port) + ) + class InterfaceTemplate(ComponentTemplateModel): """ @@ -1934,6 +1955,19 @@ class PowerOutlet(CableTermination, ComponentModel): name = models.CharField( max_length=50 ) + power_port = models.ForeignKey( + to='dcim.PowerPort', + on_delete=models.SET_NULL, + blank=True, + null=True, + related_name='poweroutlets' + ) + feed_leg = models.PositiveSmallIntegerField( + choices=POWERFEED_LEG_CHOICES, + blank=True, + null=True, + help_text="Phase (for three-phase feeds)" + ) connection_status = models.NullBooleanField( choices=CONNECTION_STATUS_CHOICES, blank=True @@ -1942,7 +1976,7 @@ class PowerOutlet(CableTermination, ComponentModel): objects = DeviceComponentManager() tags = TaggableManager(through=TaggedItem) - csv_headers = ['device', 'name', 'description'] + csv_headers = ['device', 'name', 'power_port', 'feed_leg', 'description'] class Meta: unique_together = ['device', 'name'] @@ -1957,9 +1991,19 @@ class PowerOutlet(CableTermination, ComponentModel): return ( self.device.identifier, self.name, + self.power_port.name if self.power_port else None, + self.get_feed_leg_display(), self.description, ) + def clean(self): + + # Validate power port assignment + if self.power_port and self.power_port.device != self.device: + raise ValidationError( + "Parent power port ({}) must belong to the same device".format(self.power_port) + ) + # # Interfaces diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 5e01ef413..34811c4c0 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -627,6 +627,7 @@