Repository: netbox-community/netbox
Pull request: 10236 fix device detail for power-feed
<!--
    Thank you for your interest in contributing to NetBox! Please note that
    our contribution policy requires that a feature request or bug report be
    approved and assigned prior to opening a pull request. This helps avoid
    waste time and effort on a proposed change that we might not be able to
    accept.

    IF YOUR PULL REQUEST DOES NOT REFERENCE AN ISSUE WHICH HAS BEEN ASSIGNED
    TO YOU, IT WILL BE CLOSED AUTOMATICALLY.

    Please specify your assigned issue number on the line below.
-->
### Fixes: #10236 

<!--
    Please include a summary of the proposed changes below.
-->
Fixes issue with device display when power feed is connected but no available power.

Problem statement:
TypeError thrown at some PDU devices
### NetBox version

v3.3.0

### Python version

3.9

### Steps to Reproduce

0. upgraded netbox to v3.3.0
2. click at an affected pdu (device)

### Expected Behavior

I expect that the pdu device is displayed.

### Observed Behavior

An error page with "TypeError" is thrown. Not every pdu is affected although of same device type.

If deleting the cable of an affected pdu to the power feed, the pdu device is opened correctly. If I create the cable again, the type error page is present instead of the device page.
With option "DEBUG = True" in configuration.py the type error page displays some helpful information (attached at bottom). I think there is something wrong at the power utilization graph because of type mismatch.
```
Template error:
In template /opt/netbox/netbox/templates/dcim/device.html, error at line 252
   unsupported operand type(s) for /: 'str' and 'int'
   242 :                                         {% else %}
   243 :                                             <td class="text-muted">&mdash;</td>
   244 :                                             <td class="text-muted">&mdash;</td>
   245 :                                         {% endif %}
   246 :                                     </tr>
   247 :                                     {% for leg in utilization.legs %}
   248 :                                         <tr>
   249 :                                             <td style="padding-left: 20px">Leg {{ leg.name }}</td>
   250 :                                             <td>{{ leg.outlet_count }}</td>
   251 :                                             <td>{{ leg.allocated }}</td>
   252 :                                             <td> {{ powerfeed.available_power|divide:3 }} VA</td>
   253 :                                             {% with phase_available=powerfeed.available_power|divide:3 %}
   254 :                                                 <td>{% utilization_graph leg.allocated|percentage:phase_available %}</td>
   255 :                                             {% endwith %}
   256 :                                         </tr>
   257 :                                     {% endfor %}
   258 :                                 {% endwith %}
   259 :                             {% endfor %}
   260 :                         </table>
   261 :                     </div>
   262 :                 </div>


Traceback (most recent call last):
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/views/generic/base.py", line 84, in view
    return self.dispatch(request, *args, **kwargs)
  File "/opt/netbox/netbox/utilities/views.py", line 90, in dispatch
    return super().dispatch(request, *args, **kwargs)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/views/generic/base.py", line 119, in dispatch
    return handler(request, *args, **kwargs)
  File "/opt/netbox/netbox/netbox/views/generic/object_views.py", line 67, in get
    return render(request, self.get_template_name(), {
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/shortcuts.py", line 24, in render
    content = loader.render_to_string(template_name, context, request, using=using)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/loader.py", line 62, in render_to_string
    return template.render(context, request)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/backends/django.py", line 62, in render
    return self.template.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 175, in render
    return self._render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/test/utils.py", line 111, in instrumented_test_render
    return self.nodelist.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 1000, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 1000, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 958, in render_annotated
    return self.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/loader_tags.py", line 157, in render
    return compiled_parent._render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/test/utils.py", line 111, in instrumented_test_render
    return self.nodelist.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 1000, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 1000, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 958, in render_annotated
    return self.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/loader_tags.py", line 157, in render
    return compiled_parent._render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/test/utils.py", line 111, in instrumented_test_render
    return self.nodelist.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 1000, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 1000, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 958, in render_annotated
    return self.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/loader_tags.py", line 157, in render
    return compiled_parent._render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/test/utils.py", line 111, in instrumented_test_render
    return self.nodelist.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 1000, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 1000, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 958, in render_annotated
    return self.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/loader_tags.py", line 157, in render
    return compiled_parent._render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/test/utils.py", line 111, in instrumented_test_render
    return self.nodelist.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 1000, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 1000, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 958, in render_annotated
    return self.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/loader_tags.py", line 63, in render
    result = block.nodelist.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 1000, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 1000, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 958, in render_annotated
    return self.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/loader_tags.py", line 63, in render
    result = block.nodelist.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 1000, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 1000, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 958, in render_annotated
    return self.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/loader_tags.py", line 63, in render
    result = block.nodelist.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 1000, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 1000, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 958, in render_annotated
    return self.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/defaulttags.py", line 322, in render
    return nodelist.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 1000, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 1000, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 958, in render_annotated
    return self.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/defaulttags.py", line 238, in render
    nodelist.append(node.render_annotated(context))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 958, in render_annotated
    return self.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/defaulttags.py", line 542, in render
    return self.nodelist.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 1000, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 1000, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 958, in render_annotated
    return self.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/defaulttags.py", line 238, in render
    nodelist.append(node.render_annotated(context))
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 958, in render_annotated
    return self.render(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 1059, in render
    output = self.filter_expression.resolve(context)
  File "/opt/netbox-3.3.0/venv/lib/python3.9/site-packages/django/template/base.py", line 739, in resolve
    new_obj = func(obj, *arg_vals)
  File "/opt/netbox/netbox/utilities/templatetags/helpers.py", line 131, in divide
    return round(x / y)

Exception Type: TypeError at /dcim/devices/79/
Exception Value: unsupported operand type(s) for /: 'str' and 'int'
```

Hints from issue discussion:
> 0. upgraded netbox to v3.3.0
> 1. click at an affected pdu (device)

I'm afraid that's not going to be sufficient to reproduce the reported behavior. Please try to determine steps that someone else can follow to reliably reproduce the issue.

```
Exception Type: TypeError at /dcim/devices/79/
Exception Value: unsupported operand type(s) for /: 'str' and 'int'
```

This indicates that `powerfeed.available_power` is being passed as a string. I'm not sure how that could happen, as `available_power` is a required PositiveIntegerField and can never be anything other than an integer.
Ok, I suspected that. I will see what I can do. I may need some time to figure out how to reproduce this.
I'm curious what the value of that field is for the affected power feed. Can you inspect it using the shell? (Grab the numeric ID of the feed first; in the example below it's `19`.)

```
$ ./manage.py nbshell
### NetBox interactive shell (jstretch-workstation)
### Python 3.8.13 | Django 4.0.7 | NetBox 3.3.2-dev
### lsmodels() will show available models. Use help(<model>) for more info.
>>> PowerFeed.objects.get(pk=19)
<PowerFeed: P1-10A>
>>> PowerFeed.objects.get(pk=19).available_power
3520
```
I've checked the PowerFeed but it seems that it is not a string:
```
./manage.py nbshell
### NetBox interactive shell (netbox-beta)
### Python 3.9.2 | Django 4.0.7 | NetBox 3.3.0
### lsmodels() will show available models. Use help(<model>) for more info.
>>> PowerFeed.objects.get(pk=49)
<PowerFeed: Feld01 - F04>
>>> PowerFeed.objects.get(pk=49).available_power
11085
```
So I've tried something other and played around with value "allocated draw" of the pdu's power port.
If opening page "Power Ports" of an affected pdu direct via url "/dcim/devices/[dev-id]/power-ports/" it seems that the power port has no allocated draw (displayed with a line). But if I edit this power port, adding for example 5 watts as allocated draw, the affected pdu is accessible without a type error page and device page is displayed like it should (except of this fictional allocated draw value).
I checked the nbshell in both cases:
- case with non set allocated draw:
```
### NetBox interactive shell (netbox-beta)
### Python 3.9.2 | Django 4.0.7 | NetBox 3.3.0
### lsmodels() will show available models. Use help(<model>) for more info.
>>> d = Device.objects.get(pk=48)
>>> 
>>> p=d.powerports.get()
>>> p.get_power_draw()
{'allocated': 0, 'maximum': 5097, 'outlet_count': 24, 'legs': [{'name': 'A', 'allocated': 0, 'maximum': 1695, 'outlet_count': 8}, {'name': 'B', 'allocated': 0, 'maximum': 2002, 'outlet_count': 8}, {'name': 'C', 'allocated': 0, 'maximum': 1400, 'outlet_count': 8}]}
```

- case with manually set allocated draw to 120W:

```
### NetBox interactive shell (netbox-beta)
### Python 3.9.2 | Django 4.0.7 | NetBox 3.3.0
### lsmodels() will show available models. Use help(<model>) for more info.
>>> d = Device.objects.get(pk=48)
>>> p=d.powerports.get()
>>> p.get_power_draw()
{'allocated': 120, 'maximum': 0, 'outlet_count': 24, 'legs': []}
```
In both cases I can not recognize any string values. I hope this information is helpful somehow.
@jeremystretch - We hit this bug with three phase power feeds. It seems that this line has a bug,

https://github.com/netbox-community/netbox/blob/385a0f979ec460023d3d7f8e9003ddc373fb5ac3/netbox/templates/dcim/device.html#L232

https://github.com/netbox-community/netbox/blob/385a0f979ec460023d3d7f8e9003ddc373fb5ac3/netbox/templates/dcim/rack.html#L176

https://github.com/netbox-community/netbox/blob/385a0f979ec460023d3d7f8e9003ddc373fb5ac3/netbox/templates/dcim/powerfeed.html#L44-L53

We monkey patched this with ugly solution:

```
diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html
index 6cc8597495f16fb7be6ec0cb280655e54b756650..95b9ddf151c29356f9a61403cec92b7a3782a633 100644
--- a/netbox/templates/dcim/device.html
+++ b/netbox/templates/dcim/device.html
@@ -229,7 +229,7 @@
                                 <th>Utilization</th>
                             </tr>
                             {% for powerport in object.powerports.all %}
-                                {% with utilization=powerport.get_power_draw powerfeed=powerport.connected_endpoint %}
+                                {% with utilization=powerport.get_power_draw powerfeed=powerport.connected_endpoints.0 %}
                                     <tr>
                                         <td>{{ powerport }}</td>
                                         <td>{{ utilization.outlet_count }}</td>
diff --git a/netbox/templates/dcim/powerfeed.html b/netbox/templates/dcim/powerfeed.html
index 584454df85e9b1ad963959b77970df48e81c9774..f6f201336c4187119cf76937051936a678d1ce02 100644
--- a/netbox/templates/dcim/powerfeed.html
+++ b/netbox/templates/dcim/powerfeed.html
@@ -41,16 +41,18 @@
                     <tr>
                         <th scope="row">Connected Device</th>
                         <td>
-                            {% if object.connected_endpoint %}
-                                {{ object.connected_endpoint.device|linkify }} ({{ object.connected_endpoint }})
+                            {% with connected_endpoint=object.connected_endpoints.0 %}
+                            {% if connected_endpoint %}
+                                {{ connected_endpoint.device|linkify }} ({{ connected_endpoint }})
                             {% else %}
                                 {{ ''|placeholder }}
                             {% endif %}
+                            {% endwith %}
                         </td>
                     </tr>
                     <tr>
                         <th scope="row">Utilization (Allocated)</th>
-                        {% with utilization=object.connected_endpoint.get_power_draw %}
+                        {% with utilization=object.connected_endpoints.0.get_power_draw %}
                             {% if utilization %}
                                 <td>
                                     {{ utilization.allocated }}VA / {{ object.available_power }}VA
diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html
index 51e873ffaf70a8d56596f99cb8367644fd2018a2..87ece4169d9252535776c4b7860e0fbaa3f8dc54 100644
--- a/netbox/templates/dcim/rack.html
+++ b/netbox/templates/dcim/rack.html
@@ -173,7 +173,7 @@
                                 <td>{{ powerfeed|linkify }}</td>
                                 <td>{% badge powerfeed.get_status_display bg_color=powerfeed.get_status_color %}</td>
                                 <td>{% badge powerfeed.get_type_display bg_color=powerfeed.get_type_color %}</td>
-                                {% with power_port=powerfeed.connected_endpoint %}
+                                {% with power_port=powerfeed.connected_endpoints.0 %}
                                     {% if power_port %}
                                         <td>{% utilization_graph power_port.get_power_draw.allocated|percentage:powerfeed.available_power %}</td>
                                     {% else %}
```
@jaylik Thank's for your monkey patch. I tried your device.html template patch. Now affected PDUs displayed properly without error page (I didn't check it with all affected PDUs, but checked two of them).

What I also tried without patching something: If I connect the power port of an PDU with a three-phase power feed, problem appears. Now if edit the power feed and switch to a single-phase type, the PDU is displayed without error page. As @jaylik already said, it seems that this problem appears at three-phase power feeds. What I can say, we mainly use three-phase feeds but I don't have this bug with all of our three-phase feeded PDUs.
Another try without patching something: if connecting the power port of affected PDU with two power feeds (possible since v3.3.0 because of the new cable model) the PDU is displayed properly. Both feeds are three-phase types.
Because this bug comes with upgrade to v3.3.0 I think it has something to do with the new cable model.
> What I can say, we mainly use three-phase feeds but I don't have this bug with all of our three-phase feeded PDUs.

I missed the fact, that our non-affected PDUs have a manually set allocated draw. If deleting the allocated draw of PDU power port, the exception page is present.

@baschdello do you have a repro scenario for this?  I tried using three-phase power feed and wasn't able to get it to repro.  A step-by-step repro would be greatly appreciated.
I will try this on monday with a fresh installation of v3.3.0 and give feedback.
I've reproduced this issue with a fresh installation of v3.3.0. Here are steps to reproduce:
1. create site "dc1"
2. create power-panel "usv-a"
3. create rack "rack1" and assign it to site "dc1"
4. create power-feed "feed1" with following details:
    Power Panel "usv-a"
    Rack "rack1"
    Phase "three-phase"
5. create manufacturer "manufacturer1"
6. create device-type "pdu" with manufacturer "manufacturer1"
7. add power-port "Power" to device-type "pdu" with plug type "3P+N+E 6H"
8. add power-outlets (one for each leg) "out1", "out2" and "out 3" with options:
    type "C13"
    power port "Power"
    feed leg "out1" => leg A, "out2" => leg B, "out 3" => leg C
9. create devicerole "pdu"
10. at our "rack1" create a pdu as non-racked device with the devicerole "pdu" and choose device-type "pdu"
11. connect pdu power-port to power-feed and choose:
    Power Panel "USV-A"
    Power Feed "feed1"
12. open the created device "pdu" and you will see the TypeError-page.

Best regards
baschdello

Commit message: 10236 fix device detail for power-feed

Patch to review:
diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html
index b0cd76de485..a1e87c2a891 100644
--- a/netbox/templates/dcim/device.html
+++ b/netbox/templates/dcim/device.html
@@ -247,10 +247,15 @@ <h5 class="card-header">
                                             <td style="padding-left: 20px">Leg {{ leg.name }}</td>
                                             <td>{{ leg.outlet_count }}</td>
                                             <td>{{ leg.allocated }}</td>
-                                            <td>{{ powerfeed.available_power|divide:3 }}VA</td>
-                                            {% with phase_available=powerfeed.available_power|divide:3 %}
-                                                <td>{% utilization_graph leg.allocated|percentage:phase_available %}</td>
-                                            {% endwith %}
+                                            {% if powerfeed.available_power %}
+                                                <td>{{ powerfeed.available_power|divide:3 }}VA</td>
+                                                {% with phase_available=powerfeed.available_power|divide:3 %}
+                                                    <td>{% utilization_graph leg.allocated|percentage:phase_available %}</td>
+                                                {% endwith %}
+                                            {% else %}
+                                                <td class="text-muted">&mdash;</td>
+                                                <td class="text-muted">&mdash;</td>
+                                            {% endif %}
                                         </tr>
                                     {% endfor %}
                                 {% endwith %}


Code review comments:
diff_hunk: @@ -247,10 +247,15 @@ <h5 class="card-header">
                                             <td style="padding-left: 20px">Leg {{ leg.name }}</td>
                                             <td>{{ leg.outlet_count }}</td>
                                             <td>{{ leg.allocated }}</td>
-                                            <td>{{ powerfeed.available_power|divide:3 }}VA</td>
-                                            {% with phase_available=powerfeed.available_power|divide:3 %}
-                                                <td>{% utilization_graph leg.allocated|percentage:phase_available %}</td>
-                                            {% endwith %}
+                                            {% if powerfeed.available_power %}
+                                                <td>{{ powerfeed.available_power|divide:3 }}VA</td>
+                                                {% with phase_available=powerfeed.available_power|divide:3 %}
path: netbox/templates/dcim/device.html
text: @user1:
We can avoid doing the division twice:
```suggestion
                                                {% with phase_available=powerfeed.available_power|divide:3 %}
                                                    <td>{{ phase_available }}VA</td>
```