Repository: netbox-community/netbox

Issue:
Power Ports and Power Outlets created from templates are missing data
<!--
    NOTE: This form is only for reproducible bugs. If you need assistance with
    NetBox installation, or if you have a general question, DO NOT open an
    issue. Instead, post to our mailing list:

        https://groups.google.com/forum/#!forum/netbox-discuss

    Please describe the environment in which you are running NetBox. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report.
-->
### Environment
* Python version:  3.7.3 <!-- Example: 3.7.3 -->
* NetBox version:  2.6.2 <!-- Example: 2.6.2 -->

<!--
    Describe in detail the exact steps that someone else can take to reproduce
    this bug using the current stable release of NetBox (or the current beta
    release where applicable). Begin with the creation of any necessary
    database objects and call out every operation being performed explicitly.
    If reporting a bug in the REST API, be sure to reconstruct the raw HTTP
    request(s) being made: Don't rely on a wrapper like pynetbox.
-->
### Steps to Reproduce
1. Add Power Port with maximum draw and allocated draw values for device type
2. Add Power Outlet with power port and feed leg information
3. Add a new device with this device type

<!-- What did you expect to happen? -->
### Expected Behavior
Device power ports and power outlets have the values defined in corresponding templates

<!-- What happened instead? -->
### Observed Behavior
Power ports are missing the values for maximum and allocated draw. Power outlets are missing the values for power port and feed leg


Discussion:

Resolving patch:
diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py
--- a/netbox/dcim/models.py
+++ b/netbox/dcim/models.py
@@ -31,6 +31,12 @@ class ComponentTemplateModel(models.Model):
     class Meta:
         abstract = True
 
+    def instantiate(self, device):
+        """
+        Instantiate a new component on the specified Device.
+        """
+        raise NotImplementedError()
+
     def log_change(self, user, request_id, action):
         """
         Log an ObjectChange including the parent DeviceType.
@@ -1010,6 +1016,12 @@ class Meta:
     def __str__(self):
         return self.name
 
+    def instantiate(self, device):
+        return ConsolePort(
+            device=device,
+            name=self.name
+        )
+
 
 class ConsoleServerPortTemplate(ComponentTemplateModel):
     """
@@ -1033,6 +1045,12 @@ class Meta:
     def __str__(self):
         return self.name
 
+    def instantiate(self, device):
+        return ConsoleServerPort(
+            device=device,
+            name=self.name
+        )
+
 
 class PowerPortTemplate(ComponentTemplateModel):
     """
@@ -1068,6 +1086,14 @@ class Meta:
     def __str__(self):
         return self.name
 
+    def instantiate(self, device):
+        return PowerPort(
+            device=device,
+            name=self.name,
+            maximum_draw=self.maximum_draw,
+            allocated_draw=self.allocated_draw
+        )
+
 
 class PowerOutletTemplate(ComponentTemplateModel):
     """
@@ -1112,6 +1138,18 @@ def clean(self):
                 "Parent power port ({}) must belong to the same device type".format(self.power_port)
             )
 
+    def instantiate(self, device):
+        if self.power_port:
+            power_port = PowerPort.objects.get(device=device, name=self.power_port.name)
+        else:
+            power_port = None
+        return PowerOutlet(
+            device=device,
+            name=self.name,
+            power_port=power_port,
+            feed_leg=self.feed_leg
+        )
+
 
 class InterfaceTemplate(ComponentTemplateModel):
     """
@@ -1159,6 +1197,14 @@ def form_factor(self, value):
         """
         self.type = value
 
+    def instantiate(self, device):
+        return Interface(
+            device=device,
+            name=self.name,
+            type=self.type,
+            mgmt_only=self.mgmt_only
+        )
+
 
 class FrontPortTemplate(ComponentTemplateModel):
     """
@@ -1213,6 +1259,19 @@ def clean(self):
                 )
             )
 
+    def instantiate(self, device):
+        if self.rear_port:
+            rear_port = RearPort.objects.get(device=device, name=self.rear_port.name)
+        else:
+            rear_port = None
+        return FrontPort(
+            device=device,
+            name=self.name,
+            type=self.type,
+            rear_port=rear_port,
+            rear_port_position=self.rear_port_position
+        )
+
 
 class RearPortTemplate(ComponentTemplateModel):
     """
@@ -1243,6 +1302,14 @@ class Meta:
     def __str__(self):
         return self.name
 
+    def instantiate(self, device):
+        return RearPort(
+            device=device,
+            name=self.name,
+            type=self.type,
+            positions=self.positions
+        )
+
 
 class DeviceBayTemplate(ComponentTemplateModel):
     """
@@ -1266,6 +1333,12 @@ class Meta:
     def __str__(self):
         return self.name
 
+    def instantiate(self, device):
+        return DeviceBay(
+            device=device,
+            name=self.name
+        )
+
 
 #
 # Devices
@@ -1640,45 +1713,28 @@ def save(self, *args, **kwargs):
         # If this is a new Device, instantiate all of the related components per the DeviceType definition
         if is_new:
             ConsolePort.objects.bulk_create(
-                [ConsolePort(device=self, name=template.name) for template in
-                 self.device_type.consoleport_templates.all()]
+                [x.instantiate(self) for x in self.device_type.consoleport_templates.all()]
             )
             ConsoleServerPort.objects.bulk_create(
-                [ConsoleServerPort(device=self, name=template.name) for template in
-                 self.device_type.consoleserverport_templates.all()]
+                [x.instantiate(self) for x in self.device_type.consoleserverport_templates.all()]
             )
             PowerPort.objects.bulk_create(
-                [PowerPort(device=self, name=template.name) for template in
-                 self.device_type.powerport_templates.all()]
+                [x.instantiate(self) for x in self.device_type.powerport_templates.all()]
             )
             PowerOutlet.objects.bulk_create(
-                [PowerOutlet(device=self, name=template.name) for template in
-                 self.device_type.poweroutlet_templates.all()]
+                [x.instantiate(self) for x in self.device_type.poweroutlet_templates.all()]
             )
             Interface.objects.bulk_create(
-                [Interface(device=self, name=template.name, type=template.type,
-                           mgmt_only=template.mgmt_only) for template in self.device_type.interface_templates.all()]
+                [x.instantiate(self) for x in self.device_type.interface_templates.all()]
+            )
+            RearPort.objects.bulk_create(
+                [x.instantiate(self) for x in self.device_type.rearport_templates.all()]
+            )
+            FrontPort.objects.bulk_create(
+                [x.instantiate(self) for x in self.device_type.frontport_templates.all()]
             )
-            RearPort.objects.bulk_create([
-                RearPort(
-                    device=self,
-                    name=template.name,
-                    type=template.type,
-                    positions=template.positions
-                ) for template in self.device_type.rearport_templates.all()
-            ])
-            FrontPort.objects.bulk_create([
-                FrontPort(
-                    device=self,
-                    name=template.name,
-                    type=template.type,
-                    rear_port=RearPort.objects.get(device=self, name=template.rear_port.name),
-                    rear_port_position=template.rear_port_position,
-                ) for template in self.device_type.frontport_templates.all()
-            ])
             DeviceBay.objects.bulk_create(
-                [DeviceBay(device=self, name=template.name) for template in
-                 self.device_type.device_bay_templates.all()]
+                [x.instantiate(self) for x in self.device_type.device_bay_templates.all()]
             )
 
         # Update Site and Rack assignment for any child Devices


Test patch:
diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py
--- a/netbox/dcim/tests/test_models.py
+++ b/netbox/dcim/tests/test_models.py
@@ -1,6 +1,5 @@
 from django.test import TestCase
 
-from dcim.constants import *
 from dcim.models import *
 
 
@@ -152,6 +151,137 @@ def test_mount_zero_ru(self):
         self.assertTrue(pdu)
 
 
+class DeviceTestCase(TestCase):
+
+    def setUp(self):
+
+        self.site = Site.objects.create(name='Test Site 1', slug='test-site-1')
+        manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
+        self.device_type = DeviceType.objects.create(
+            manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
+        )
+        self.device_role = DeviceRole.objects.create(
+            name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
+        )
+
+        # Create DeviceType components
+        ConsolePortTemplate(
+            device_type=self.device_type,
+            name='Console Port 1'
+        ).save()
+
+        ConsoleServerPortTemplate(
+            device_type=self.device_type,
+            name='Console Server Port 1'
+        ).save()
+
+        ppt = PowerPortTemplate(
+            device_type=self.device_type,
+            name='Power Port 1',
+            maximum_draw=1000,
+            allocated_draw=500
+        )
+        ppt.save()
+
+        PowerOutletTemplate(
+            device_type=self.device_type,
+            name='Power Outlet 1',
+            power_port=ppt,
+            feed_leg=POWERFEED_LEG_A
+        ).save()
+
+        InterfaceTemplate(
+            device_type=self.device_type,
+            name='Interface 1',
+            type=IFACE_TYPE_1GE_FIXED,
+            mgmt_only=True
+        ).save()
+
+        rpt = RearPortTemplate(
+            device_type=self.device_type,
+            name='Rear Port 1',
+            type=PORT_TYPE_8P8C,
+            positions=8
+        )
+        rpt.save()
+
+        FrontPortTemplate(
+            device_type=self.device_type,
+            name='Front Port 1',
+            type=PORT_TYPE_8P8C,
+            rear_port=rpt,
+            rear_port_position=2
+        ).save()
+
+        DeviceBayTemplate(
+            device_type=self.device_type,
+            name='Device Bay 1'
+        ).save()
+
+    def test_device_creation(self):
+        """
+        Ensure that all Device components are copied automatically from the DeviceType.
+        """
+        d = Device(
+            site=self.site,
+            device_type=self.device_type,
+            device_role=self.device_role,
+            name='Test Device 1'
+        )
+        d.save()
+
+        ConsolePort.objects.get(
+            device=d,
+            name='Console Port 1'
+        )
+
+        ConsoleServerPort.objects.get(
+            device=d,
+            name='Console Server Port 1'
+        )
+
+        pp = PowerPort.objects.get(
+            device=d,
+            name='Power Port 1',
+            maximum_draw=1000,
+            allocated_draw=500
+        )
+
+        PowerOutlet.objects.get(
+            device=d,
+            name='Power Outlet 1',
+            power_port=pp,
+            feed_leg=POWERFEED_LEG_A
+        )
+
+        Interface.objects.get(
+            device=d,
+            name='Interface 1',
+            type=IFACE_TYPE_1GE_FIXED,
+            mgmt_only=True
+        )
+
+        rp = RearPort.objects.get(
+            device=d,
+            name='Rear Port 1',
+            type=PORT_TYPE_8P8C,
+            positions=8
+        )
+
+        FrontPort.objects.get(
+            device=d,
+            name='Front Port 1',
+            type=PORT_TYPE_8P8C,
+            rear_port=rp,
+            rear_port_position=2
+        )
+
+        DeviceBay.objects.get(
+            device=d,
+            name='Device Bay 1'
+        )
+
+
 class CableTestCase(TestCase):
 
     def setUp(self):