Repository: nautobot/nautobot

Issue:
Can't use LAG as Parent to Virtual Interface
<!--
    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 (Docker tag too if applicable): 1.6.0
* Python version: 3.9
* Database platform, version: Postgres 14
* Middleware(s):

<!--
    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. Create LAG interface ae0
2. Create Virtual Interface ae0.0
3. While creating Virtual interface, try to select LAG Interface as Parent

<!-- What did you expect to happen? -->
### Expected Behavior
I was hoping that the LAG interface would be in the list of valid parents, but that does not seem to be the case.

<!-- What happened instead? -->
### Observed Behavior
 It appears that only physical interfaces can be selected as parents. Is this expected? Or am I modeling the the subinterface of a LAG incorrectly? In this case, the sub interface is an IP interface not a switched interface. So using the Bridge model doesn't seem appropriate. 


Discussion:
Looks like the same issue as netbox-community/netbox#6106, which should be a very simple fix a la https://github.com/netbox-community/netbox/commit/81193eb55054763ede647b0eb27cf61632b5ba3d. 
This would work. I guess this will also fix the case of `lo.0` and `lo` where both of these are virtual. I was going to work around this by making `lo`'s interface type to `other`. 

Resolving patch:
diff --git a/nautobot/dcim/forms.py b/nautobot/dcim/forms.py
--- a/nautobot/dcim/forms.py
+++ b/nautobot/dcim/forms.py
@@ -2139,9 +2139,6 @@ class InterfaceForm(InterfaceCommonForm, NautobotModelForm):
         queryset=Interface.objects.all(),
         required=False,
         label="Parent interface",
-        query_params={
-            "kind": "physical",
-        },
         help_text="Assigned parent interface",
     )
     bridge = DynamicModelChoiceField(
diff --git a/nautobot/dcim/models/device_components.py b/nautobot/dcim/models/device_components.py
--- a/nautobot/dcim/models/device_components.py
+++ b/nautobot/dcim/models/device_components.py
@@ -604,18 +604,13 @@ def clean(self):
             # An interface cannot be its own parent
             if self.parent_interface_id == self.pk:
                 raise ValidationError({"parent_interface": "An interface cannot be its own parent."})
+
             # A physical interface cannot have a parent interface
             if hasattr(self, "type") and self.type != InterfaceTypeChoices.TYPE_VIRTUAL:
                 raise ValidationError(
                     {"parent_interface": "Only virtual interfaces may be assigned to a parent interface."}
                 )
 
-            # A virtual interface cannot be a parent interface
-            if getattr(self.parent_interface, "type", None) == InterfaceTypeChoices.TYPE_VIRTUAL:
-                raise ValidationError(
-                    {"parent_interface": "Virtual interfaces may not be parents of other interfaces."}
-                )
-
             # An interface's parent must belong to the same device or virtual chassis
             if self.parent_interface.device != self.device:
                 if getattr(self.device, "virtual_chassis", None) is None:


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
@@ -1805,6 +1805,7 @@ def setUpTestData(cls):
                 device=device, name="BRIDGE", status=status_active, type=InterfaceTypeChoices.TYPE_BRIDGE
             ),
         )
+        cls.lag_interface = interfaces[3]
         # Required by ViewTestCases.DeviceComponentViewTestCase.test_bulk_rename
         cls.selected_objects = interfaces
         cls.selected_objects_parent_name = device.name
@@ -1897,6 +1898,25 @@ def setUpTestData(cls):
             f"1000base-t,Interface 6,{device.name},{device.location.name},{statuses[1].name}",
         )
 
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
+    def test_create_virtual_interface_with_parent_lag(self):
+        """https://github.com/nautobot/nautobot/issues/4436."""
+        self.add_permissions("dcim.add_interface")
+        form_data = self.form_data.copy()
+        del form_data["name"]
+        form_data["name_pattern"] = "LAG.0"
+        form_data["type"] = InterfaceTypeChoices.TYPE_VIRTUAL
+        form_data["parent_interface"] = self.lag_interface
+        del form_data["lag"]
+        request = {
+            "path": self._get_url("add"),
+            "data": post_data(form_data),
+        }
+        self.assertHttpStatus(self.client.post(**request), 302)
+        instance = self._get_queryset().order_by("last_updated").last()
+        self.assertEqual(instance.type, InterfaceTypeChoices.TYPE_VIRTUAL)
+        self.assertEqual(instance.parent_interface, self.lag_interface)
+
 
 class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = FrontPort