Repository: nautobot/nautobot

Issue:
Configuration Context not applied based on nested Tenant Groups in LTM
<!--
    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): v1.6.4
* Python version: 3.11
* Database platform, version: PG15
* 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. Create nested Tenant Groups with test configuration context
2. Put an object (device) as a Tenant which is a member of a nested Tenant Group
3. Open the Config Context tab, observe the contents of Rendered Context and Source Contexts widget

<!-- What did you expect to happen? -->
### Expected Behavior
To see the configuration context applied in Rendered Context and also Source Contexts as a source for context

<!-- What happened instead? -->
### Observed Behavior
The configuration context is not applied

### Notes

- Previously a nested tenant issue (#4768) was reported as affecting Nautobot 2.x. Subsequently this was patched for 2.x and closed with #4838. However it is also the case that the 1.6 release is still affected by this bug.
- On first discussions for back-port of the fix in #4838, the issue identified and fixed was noted to be for 2.x only.
- Configuration Context is working for immediate Tenant Groups, the nested Tenant Groups are affected


Discussion:
Confirmed this is also affecting LTM

# Testing Data

![image](https://github.com/nautobot/nautobot/assets/75227981/acb1142b-d8f3-4238-9369-3b170fafd1d7)

![image](https://github.com/nautobot/nautobot/assets/75227981/6c30cf52-7690-4be0-b644-9f4d8ca21e67)

![image](https://github.com/nautobot/nautobot/assets/75227981/bca19f42-7125-47ae-b7b1-f8097b99af7d)

# Scenario 1 (Confirmed working on 2.2)

![image](https://github.com/nautobot/nautobot/assets/75227981/2caab073-b875-4e3c-916d-c2cf894ce43f)

![image](https://github.com/nautobot/nautobot/assets/75227981/3604b77b-85e6-45fd-b25d-a7cb8a0e2a54)


# Scenario 2

![image](https://github.com/nautobot/nautobot/assets/75227981/ac99cf44-0381-4443-8066-a56892b979a6)

![image](https://github.com/nautobot/nautobot/assets/75227981/56cf88c8-4de7-4e6f-b979-7a6ca224889f)



Resolving patch:
diff --git a/nautobot/extras/querysets.py b/nautobot/extras/querysets.py
--- a/nautobot/extras/querysets.py
+++ b/nautobot/extras/querysets.py
@@ -31,6 +31,10 @@ def get_for_object(self, obj):
 
         # Get the group of the assigned tenant, if any
         tenant_group = obj.tenant.group if obj.tenant else None
+        if tenant_group:
+            tenant_groups = tenant_group.get_ancestors(include_self=True)
+        else:
+            tenant_groups = []
 
         # Match against the directly assigned region as well as any parent regions.
         region = getattr(obj.site, "region", None)
@@ -56,7 +60,7 @@ def get_for_object(self, obj):
             Q(cluster_groups=cluster_group) | Q(cluster_groups=None),
             Q(clusters=cluster) | Q(clusters=None),
             Q(device_redundancy_groups=device_redundancy_group) | Q(device_redundancy_groups=None),
-            Q(tenant_groups=tenant_group) | Q(tenant_groups=None),
+            Q(tenant_groups__in=tenant_groups) | Q(tenant_groups=None),
             Q(tenants=obj.tenant) | Q(tenants=None),
             Q(tags__slug__in=obj.tags.slugs()) | Q(tags=None),
         ]
@@ -124,13 +128,12 @@ def _get_config_context_filters(self):
             Q(platforms=OuterRef("platform")) | Q(platforms=None),
             Q(cluster_groups=OuterRef("cluster__group")) | Q(cluster_groups=None),
             Q(clusters=OuterRef("cluster")) | Q(clusters=None),
-            Q(tenant_groups=OuterRef("tenant__group")) | Q(tenant_groups=None),
             Q(tenants=OuterRef("tenant")) | Q(tenants=None),
             Q(tags__pk__in=Subquery(TaggedItem.objects.filter(**tag_query_filters).values_list("tag_id", flat=True)))
             | Q(tags=None),
             is_active=True,
         )
-
+        tenant_group_field = "tenant__group"
         if self.model._meta.model_name == "device":
             base_query.add((Q(roles=OuterRef("device_role")) | Q(roles=None)), Q.AND)
             base_query.add((Q(device_types=OuterRef("device_type")) | Q(device_types=None)), Q.AND)
@@ -158,6 +161,18 @@ def _get_config_context_filters(self):
             ),
             Q.AND,
         )
+        base_query.add(
+            (
+                Q(
+                    tenant_groups__tree_id=OuterRef(f"{tenant_group_field}__tree_id"),
+                    tenant_groups__level__lte=OuterRef(f"{tenant_group_field}__level"),
+                    tenant_groups__lft__lte=OuterRef(f"{tenant_group_field}__lft"),
+                    tenant_groups__rght__gte=OuterRef(f"{tenant_group_field}__rght"),
+                )
+                | Q(tenant_groups=None)
+            ),
+            Q.AND,
+        )
 
         return base_query
 


Test patch:
diff --git a/nautobot/extras/tests/test_models.py b/nautobot/extras/tests/test_models.py
--- a/nautobot/extras/tests/test_models.py
+++ b/nautobot/extras/tests/test_models.py
@@ -127,7 +127,10 @@ def setUp(self):
         self.location = Location.objects.create(name="Location 1", location_type=location_type, site=self.site)
         self.platform = Platform.objects.create(name="Platform")
         self.tenantgroup = TenantGroup.objects.create(name="Tenant Group")
+        self.parent_tenantgroup = TenantGroup.objects.create(name="Parent Tenant Group")
+        self.child_tenantgroup = TenantGroup.objects.create(name="Child Tenant Group", parent=self.parent_tenantgroup)
         self.tenant = Tenant.objects.create(name="Tenant", group=self.tenantgroup)
+        self.child_tenant = Tenant.objects.create(name="Child Tenant", group=self.child_tenantgroup)
         self.tag, self.tag2 = Tag.objects.get_for_model(Device)[:2]
         self.dynamic_groups = DynamicGroup.objects.create(
             name="Dynamic Group",
@@ -300,6 +303,65 @@ def test_annotation_same_as_get_for_object_virtualmachine_relations(self):
         ]:
             self.assertIn(key, vm_context)
 
+    def test_annotation_same_as_get_for_object_device_relations_in_child_tenant_groups(self):
+        tenant_group_context = ConfigContext.objects.create(
+            name="parent_tenant_group", weight=100, data={"parent-group-1": 1}
+        )
+        tenant_group_context.tenant_groups.add(self.parent_tenantgroup)
+        tenant_group_context = ConfigContext.objects.create(
+            name="child_tenant_group", weight=100, data={"child-group-1": 2}
+        )
+        tenant_group_context.tenant_groups.add(self.child_tenantgroup)
+        tenant_context = ConfigContext.objects.create(name="child_tenant", weight=100, data={"child-tenant-1": 3})
+        tenant_context.tenants.add(self.child_tenant)
+        device = Device.objects.create(
+            name="Child Tenant Device",
+            site=self.site,
+            location=self.location,
+            device_role=self.devicerole,
+            device_type=self.devicetype,
+            tenant=self.child_tenant,
+        )
+        device_context = device.get_config_context()
+        for key in ["parent-group-1", "child-group-1", "child-tenant-1"]:
+            self.assertIn(key, device_context)
+
+    def test_annotation_same_as_get_for_object_virtualmachine_relations_in_child_tenant_groups(self):
+        tenant_group_context = ConfigContext.objects.create(
+            name="parent_tenant_group", weight=100, data={"parent-group-1": 1}
+        )
+        tenant_group_context.tenant_groups.add(self.parent_tenantgroup)
+        tenant_group_context = ConfigContext.objects.create(
+            name="child_tenant_group", weight=200, data={"child-group-1": 2}
+        )
+        tenant_group_context.tenant_groups.add(self.child_tenantgroup)
+        tenant_context = ConfigContext.objects.create(name="child_tenant", weight=300, data={"child-tenant-1": 3})
+        tenant_context.tenants.add(self.child_tenant)
+        vm_status = Status.objects.get_for_model(VirtualMachine).first()
+        cluster_group = ClusterGroup.objects.create(name="Cluster Group")
+        cluster_type = ClusterType.objects.create(name="Cluster Type 1")
+        cluster = Cluster.objects.create(
+            name="Cluster",
+            group=cluster_group,
+            type=cluster_type,
+            location=self.location,
+            tenant=self.child_tenant,
+        )
+        virtual_machine = VirtualMachine.objects.create(
+            name="Child Location VM",
+            cluster=cluster,
+            role=self.devicerole,
+            status=vm_status,
+            tenant=self.child_tenant,
+        )
+        vm_context = virtual_machine.get_config_context()
+        for key in [
+            "parent-group-1",
+            "child-group-1",
+            "child-tenant-1",
+        ]:
+            self.assertIn(key, vm_context)
+
     def test_multiple_tags_return_distinct_objects(self):
         """
         Tagged items use a generic relationship, which results in duplicate rows being returned when queried.