Repository: nautobot/nautobot

Issue:
WebUI InterfaceBulkCreateForm has not status field
<!--
    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.

    This form is only for reporting reproducible bugs. If you need assistance
    with Nautobot installation, or if you have a general question, please start a
    discussion instead: https://github.com/nautobot/nautobot/discussions

    Please describe the environment in which you are running Nautobot. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report, and that any plugins have been disabled.
-->
### Environment
* Nautobot version: 1.5.4
* Python version: 3.8.10
* Database platform, version: postgresql 12+214ubuntu0.1 
* Middleware(s): n/a

<!--
    Describe in detail the exact steps that someone else can take to reproduce
    this bug using the current stable release of Nautobot. 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 client
    library such as pynautobot.
-->
### Steps to Reproduce
1. WebUI - select one or more devices from /dcim/devices/
2. Add components -> Interfaces button at bottom, resulting in entry form at /dcim/devices/interfaces/add/
3. Submit completed form

<!-- What did you expect to happen? -->
### Expected Behavior
Specified interface to be created on the selected devices.

<!-- What happened instead? -->
### Observed Behavior
Error due to status field not being defined:
```
<class 'ValueError'>

'InterfaceBulkCreateForm' has no field named 'status'.

Python version: 3.8.10
Nautobot version: 1.5.4
```


Discussion:
Looks to have been an oversight in #1753. Thanks for the report!

Resolving patch:
diff --git a/nautobot/dcim/forms.py b/nautobot/dcim/forms.py
--- a/nautobot/dcim/forms.py
+++ b/nautobot/dcim/forms.py
@@ -2360,7 +2360,7 @@ class ConsolePortCreateForm(ComponentCreateForm):
     )
 
 
-class ConsolePortBulkCreateForm(form_from_model(ConsolePort, ["type"]), DeviceBulkAddComponentForm):
+class ConsolePortBulkCreateForm(form_from_model(ConsolePort, ["type", "tags"]), DeviceBulkAddComponentForm):
     field_order = ("name_pattern", "label_pattern", "type", "description", "tags")
 
 
@@ -2427,7 +2427,7 @@ class ConsoleServerPortCreateForm(ComponentCreateForm):
     )
 
 
-class ConsoleServerPortBulkCreateForm(form_from_model(ConsoleServerPort, ["type"]), DeviceBulkAddComponentForm):
+class ConsoleServerPortBulkCreateForm(form_from_model(ConsoleServerPort, ["type", "tags"]), DeviceBulkAddComponentForm):
     field_order = ("name_pattern", "label_pattern", "type", "description", "tags")
 
 
@@ -2501,7 +2501,7 @@ class PowerPortCreateForm(ComponentCreateForm):
 
 
 class PowerPortBulkCreateForm(
-    form_from_model(PowerPort, ["type", "maximum_draw", "allocated_draw"]),
+    form_from_model(PowerPort, ["type", "maximum_draw", "allocated_draw", "tags"]),
     DeviceBulkAddComponentForm,
 ):
     field_order = (
@@ -2600,7 +2600,7 @@ def __init__(self, *args, **kwargs):
         self.fields["power_port"].queryset = PowerPort.objects.filter(device=device)
 
 
-class PowerOutletBulkCreateForm(form_from_model(PowerOutlet, ["type", "feed_leg"]), DeviceBulkAddComponentForm):
+class PowerOutletBulkCreateForm(form_from_model(PowerOutlet, ["type", "feed_leg", "tags"]), DeviceBulkAddComponentForm):
     field_order = (
         "name_pattern",
         "label_pattern",
@@ -2879,17 +2879,29 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
 
 
 class InterfaceBulkCreateForm(
-    form_from_model(Interface, ["type", "enabled", "mtu", "mgmt_only"]),
+    form_from_model(Interface, ["enabled", "mtu", "mgmt_only", "mode", "tags"]),
     DeviceBulkAddComponentForm,
 ):
+    type = forms.ChoiceField(
+        choices=InterfaceTypeChoices,
+        widget=StaticSelect2(),
+    )
+    status = DynamicModelChoiceField(
+        required=True,
+        queryset=Status.objects.all(),
+        query_params={"content_types": Interface._meta.label_lower},
+    )
+
     field_order = (
         "name_pattern",
         "label_pattern",
+        "status",
         "type",
         "enabled",
         "mtu",
         "mgmt_only",
         "description",
+        "mode",
         "tags",
     )
 
@@ -3292,7 +3304,7 @@ class RearPortCreateForm(ComponentCreateForm):
     )
 
 
-class RearPortBulkCreateForm(form_from_model(RearPort, ["type", "positions"]), DeviceBulkAddComponentForm):
+class RearPortBulkCreateForm(form_from_model(RearPort, ["type", "positions", "tags"]), DeviceBulkAddComponentForm):
     field_order = (
         "name_pattern",
         "label_pattern",
@@ -3377,7 +3389,7 @@ def __init__(self, device_bay, *args, **kwargs):
         ).exclude(pk=device_bay.device.pk)
 
 
-class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):
+class DeviceBayBulkCreateForm(form_from_model(DeviceBay, ["tags"]), DeviceBulkAddComponentForm):
     field_order = ("name_pattern", "label_pattern", "description", "tags")
 
 
@@ -3505,7 +3517,7 @@ class Meta:
 
 
 class InventoryItemBulkCreateForm(
-    form_from_model(InventoryItem, ["manufacturer", "part_id", "serial", "asset_tag", "discovered"]),
+    form_from_model(InventoryItem, ["manufacturer", "part_id", "serial", "asset_tag", "discovered", "tags"]),
     DeviceBulkAddComponentForm,
 ):
     field_order = (
diff --git a/nautobot/virtualization/forms.py b/nautobot/virtualization/forms.py
--- a/nautobot/virtualization/forms.py
+++ b/nautobot/virtualization/forms.py
@@ -762,11 +762,17 @@ class Meta:
 
 
 class VMInterfaceBulkCreateForm(
-    form_from_model(VMInterface, ["enabled", "mtu", "description", "mode"]),
+    form_from_model(VMInterface, ["enabled", "mtu", "description", "mode", "tags"]),
     VirtualMachineBulkAddComponentForm,
 ):
+    status = DynamicModelChoiceField(
+        queryset=Status.objects.all(),
+        query_params={"content_types": VMInterface._meta.label_lower},
+    )
+
     field_order = (
         "name_pattern",
+        "status",
         "enabled",
         "mtu",
         "description",


Test patch:
diff --git a/nautobot/dcim/tests/test_views.py b/nautobot/dcim/tests/test_views.py
--- a/nautobot/dcim/tests/test_views.py
+++ b/nautobot/dcim/tests/test_views.py
@@ -1,4 +1,5 @@
 from decimal import Decimal
+import unittest
 
 import pytz
 import yaml
@@ -1850,6 +1851,15 @@ def setUpTestData(cls):
             "tags": [t.pk for t in Tag.objects.get_for_model(PowerOutlet)],
         }
 
+        cls.bulk_add_data = {
+            "device": device.pk,
+            "name_pattern": "Power Outlet [4-6]",
+            "type": PowerOutletTypeChoices.TYPE_IEC_C13,
+            "feed_leg": PowerOutletFeedLegChoices.FEED_LEG_B,
+            "description": "A power outlet",
+            "tags": [t.pk for t in Tag.objects.get_for_model(PowerOutlet)],
+        }
+
         cls.bulk_edit_data = {
             "type": PowerOutletTypeChoices.TYPE_IEC_C15,
             "power_port": powerports[1].pk,
@@ -1910,6 +1920,7 @@ def setUpTestData(cls):
         cls.bulk_create_data = {
             "device": device.pk,
             "name_pattern": "Interface [4-6]",
+            "label_pattern": "Interface Number [4-6]",
             "type": InterfaceTypeChoices.TYPE_1GE_GBIC,
             "enabled": False,
             "bridge": interfaces[4].pk,
@@ -1917,7 +1928,7 @@ def setUpTestData(cls):
             "mac_address": EUI("01:02:03:04:05:06"),
             "mtu": 2000,
             "mgmt_only": True,
-            "description": "A front port",
+            "description": "An Interface",
             "mode": InterfaceModeChoices.MODE_TAGGED,
             "untagged_vlan": vlans[0].pk,
             "tagged_vlans": [v.pk for v in vlans[1:4]],
@@ -1925,6 +1936,20 @@ def setUpTestData(cls):
             "status": status_active.pk,
         }
 
+        cls.bulk_add_data = {
+            "device": device.pk,
+            "name_pattern": "Interface [4-6]",
+            "label_pattern": "Interface Number [4-6]",
+            "status": status_active.pk,
+            "type": InterfaceTypeChoices.TYPE_1GE_GBIC,
+            "enabled": True,
+            "mtu": 1500,
+            "mgmt_only": False,
+            "description": "An Interface",
+            "mode": InterfaceModeChoices.MODE_TAGGED,
+            "tags": [],
+        }
+
         cls.bulk_edit_data = {
             "type": InterfaceTypeChoices.TYPE_1GE_FIXED,
             "enabled": True,
@@ -1998,6 +2023,10 @@ def setUpTestData(cls):
             "Device 1,Front Port 6,8p8c,Rear Port 6,1",
         )
 
+    @unittest.skip("No DeviceBulkAddFrontPortView exists at present")
+    def test_bulk_add_component(self):
+        pass
+
 
 class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = RearPort
diff --git a/nautobot/utilities/testing/views.py b/nautobot/utilities/testing/views.py
--- a/nautobot/utilities/testing/views.py
+++ b/nautobot/utilities/testing/views.py
@@ -1328,3 +1328,53 @@ class DeviceComponentViewTestCase(
         """
 
         maxDiff = None
+        bulk_add_data = None
+        """Used for bulk-add (distinct from bulk-create) view testing; self.bulk_create_data will be used if unset."""
+
+        @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
+        def test_bulk_add_component(self):
+            """Test bulk-adding this component to devices/virtual-machines."""
+            obj_perm = ObjectPermission(name="Test permission", actions=["add"])
+            obj_perm.save()
+            obj_perm.users.add(self.user)
+            obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
+
+            initial_count = self._get_queryset().count()
+
+            data = (self.bulk_add_data or self.bulk_create_data).copy()
+
+            # Load the device-bulk-add or virtualmachine-bulk-add form
+            if "device" in data:
+                url = reverse(f"dcim:device_bulk_add_{self.model._meta.model_name}")
+                request = {
+                    "path": url,
+                    "data": post_data({"pk": data["device"]}),
+                }
+            else:
+                url = reverse(f"virtualization:virtualmachine_bulk_add_{self.model._meta.model_name}")
+                request = {
+                    "path": url,
+                    "data": post_data({"pk": data["virtual_machine"]}),
+                }
+            self.assertHttpStatus(self.client.post(**request), 200)
+
+            # Post to the device-bulk-add or virtualmachine-bulk-add form to create records
+            if "device" in data:
+                data["pk"] = data.pop("device")
+            else:
+                data["pk"] = data.pop("virtual_machine")
+            data["_create"] = ""
+            request["data"] = post_data(data)
+            self.assertHttpStatus(self.client.post(**request), 302)
+
+            updated_count = self._get_queryset().count()
+            self.assertEqual(updated_count, initial_count + self.bulk_create_count)
+
+            matching_count = 0
+            for instance in self._get_queryset().all():
+                try:
+                    self.assertInstanceEqual(instance, (self.bulk_add_data or self.bulk_create_data))
+                    matching_count += 1
+                except AssertionError:
+                    pass
+            self.assertEqual(matching_count, self.bulk_create_count)
diff --git a/nautobot/virtualization/tests/test_views.py b/nautobot/virtualization/tests/test_views.py
--- a/nautobot/virtualization/tests/test_views.py
+++ b/nautobot/virtualization/tests/test_views.py
@@ -340,6 +340,18 @@ def setUpTestData(cls):
             "tags": [t.pk for t in Tag.objects.get_for_model(VMInterface)],
         }
 
+        cls.bulk_add_data = {
+            "virtual_machine": virtualmachines[1].pk,
+            "name_pattern": "Interface [4-6]",
+            "enabled": True,
+            "status": status_active.pk,
+            "mtu": 1500,
+            "description": "New Description",
+            "mode": InterfaceModeChoices.MODE_TAGGED,
+            "custom_field_1": "Custom field data",
+            "tags": [],
+        }
+
         cls.csv_data = (
             "virtual_machine,name,status",
             "Virtual Machine 2,Interface 4,active",