From cb991993406c9d213cd476c5b9f264999c18a43d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 3 Mar 2026 08:17:55 -0500 Subject: [PATCH] Initial POC for #21025 --- netbox/dcim/api/views.py | 9 +- .../0227_device_config_context_data.py | 21 + netbox/dcim/views.py | 2 +- netbox/extras/api/mixins.py | 23 - netbox/extras/graphql/mixins.py | 14 +- .../commands/rebuild_config_context.py | 40 + .../0135_config_context_triggers.py | 1112 +++++++++++++++++ netbox/extras/models/configs.py | 21 +- netbox/extras/querysets.py | 2 +- netbox/extras/tests/test_models.py | 5 +- netbox/virtualization/api/views.py | 4 +- ...0053_virtualmachine_config_context_data.py | 16 + netbox/virtualization/views.py | 2 +- 13 files changed, 1215 insertions(+), 56 deletions(-) create mode 100644 netbox/dcim/migrations/0227_device_config_context_data.py create mode 100644 netbox/extras/management/commands/rebuild_config_context.py create mode 100644 netbox/extras/migrations/0135_config_context_triggers.py create mode 100644 netbox/virtualization/migrations/0053_virtualmachine_config_context_data.py diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index a3bc7386a..71e5880b8 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -12,7 +12,7 @@ from dcim import filtersets from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH from dcim.models import * from dcim.svg import CableTraceSVG -from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin +from extras.api.mixins import RenderConfigMixin from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired from netbox.api.metadata import ContentTypeMetadata from netbox.api.pagination import StripCountAnnotationsPaginator @@ -398,12 +398,7 @@ class PlatformViewSet(MPTTLockedMixin, NetBoxModelViewSet): # Devices/modules # -class DeviceViewSet( - SequentialBulkCreatesMixin, - ConfigContextQuerySetMixin, - RenderConfigMixin, - NetBoxModelViewSet -): +class DeviceViewSet(SequentialBulkCreatesMixin, RenderConfigMixin, NetBoxModelViewSet): queryset = Device.objects.prefetch_related( 'parent_bay', # Referenced by DeviceSerializer.get_parent_device() ) diff --git a/netbox/dcim/migrations/0227_device_config_context_data.py b/netbox/dcim/migrations/0227_device_config_context_data.py new file mode 100644 index 000000000..4f95ecc37 --- /dev/null +++ b/netbox/dcim/migrations/0227_device_config_context_data.py @@ -0,0 +1,21 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0226_modulebay_rebuild_tree'), + ] + + operations = [ + migrations.AddField( + model_name='device', + name='config_context_data', + field=models.JSONField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name='module', + name='config_context_data', + field=models.JSONField(blank=True, editable=False, null=True), + ), + ] diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index df6fcd55c..34449ec33 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2683,7 +2683,7 @@ class DeviceInventoryView(DeviceComponentsView): @register_model_view(Device, 'configcontext', path='config-context') class DeviceConfigContextView(ObjectConfigContextView): - queryset = Device.objects.annotate_config_context_data() + queryset = Device.objects.all() base_template = 'dcim/device/base.html' tab = ViewTab( label=_('Config Context'), diff --git a/netbox/extras/api/mixins.py b/netbox/extras/api/mixins.py index a98218b78..4912f3c1a 100644 --- a/netbox/extras/api/mixins.py +++ b/netbox/extras/api/mixins.py @@ -10,34 +10,11 @@ from netbox.api.renderers import TextRenderer from .serializers import ConfigTemplateSerializer __all__ = ( - 'ConfigContextQuerySetMixin', 'ConfigTemplateRenderMixin', 'RenderConfigMixin', ) -class ConfigContextQuerySetMixin: - """ - Used by views that work with config context models (device and virtual machine). - Provides a get_queryset() method which deals with adding the config context - data annotation or not. - """ - def get_queryset(self): - """ - Build the proper queryset based on the request context - - If the `brief` query param equates to True or the `exclude` query param - includes `config_context` as a value, return the base queryset. - - Else, return the queryset annotated with config context data - """ - queryset = super().get_queryset() - request = self.get_serializer_context()['request'] - if self.brief or 'config_context' in request.query_params.get('exclude', []): - return queryset - return queryset.annotate_config_context_data() - - class ConfigTemplateRenderMixin: """ Provides a method to return a rendered ConfigTemplate as REST API data. diff --git a/netbox/extras/graphql/mixins.py b/netbox/extras/graphql/mixins.py index 96f9719a3..a077f4961 100644 --- a/netbox/extras/graphql/mixins.py +++ b/netbox/extras/graphql/mixins.py @@ -22,19 +22,7 @@ if TYPE_CHECKING: @strawberry.type class ConfigContextMixin: - @classmethod - def get_queryset(cls, queryset, info: Info, **kwargs): - queryset = super().get_queryset(queryset, info, **kwargs) - - # If `config_context` is requested, call annotate_config_context_data() on the queryset - selected = {f.name for f in info.selected_fields[0].selections} - if 'config_context' in selected and hasattr(queryset, 'annotate_config_context_data'): - return queryset.annotate_config_context_data() - - return queryset - - # Ensure `local_context_data` is fetched when `config_context` is requested - @strawberry_django.field(only=['local_context_data']) + @strawberry_django.field(only=['config_context_data', 'local_context_data']) def config_context(self) -> strawberry.scalars.JSON: return self.get_config_context() diff --git a/netbox/extras/management/commands/rebuild_config_context.py b/netbox/extras/management/commands/rebuild_config_context.py new file mode 100644 index 000000000..84ea12a6d --- /dev/null +++ b/netbox/extras/management/commands/rebuild_config_context.py @@ -0,0 +1,40 @@ +from django.core.management.base import BaseCommand +from django.db import connection + + +class Command(BaseCommand): + help = 'Rebuild pre-rendered config context data for all devices and/or virtual machines' + + def add_arguments(self, parser): + parser.add_argument( + '--devices-only', + action='store_true', + help='Only rebuild config context data for devices', + ) + parser.add_argument( + '--vms-only', + action='store_true', + help='Only rebuild config context data for virtual machines', + ) + + def handle(self, *args, **options): + devices_only = options['devices_only'] + vms_only = options['vms_only'] + + with connection.cursor() as cursor: + if not vms_only: + self.stdout.write('Rebuilding config context data for devices...') + cursor.execute( + 'UPDATE dcim_device SET config_context_data = compute_config_context_for_device(id)' + ) + self.stdout.write(self.style.SUCCESS(f' Updated {cursor.rowcount} devices')) + + if not devices_only: + self.stdout.write('Rebuilding config context data for virtual machines...') + cursor.execute( + 'UPDATE virtualization_virtualmachine ' + 'SET config_context_data = compute_config_context_for_vm(id)' + ) + self.stdout.write(self.style.SUCCESS(f' Updated {cursor.rowcount} virtual machines')) + + self.stdout.write(self.style.SUCCESS('Done.')) diff --git a/netbox/extras/migrations/0135_config_context_triggers.py b/netbox/extras/migrations/0135_config_context_triggers.py new file mode 100644 index 000000000..56cdf1d4f --- /dev/null +++ b/netbox/extras/migrations/0135_config_context_triggers.py @@ -0,0 +1,1112 @@ +from django.db import migrations + + +FORWARD_SQL = """ +-- ============================================================================= +-- 1. jsonb_deepmerge(jsonb, jsonb) - recursive deep merge +-- ============================================================================= +CREATE OR REPLACE FUNCTION jsonb_deepmerge(original jsonb, new_data jsonb) RETURNS jsonb AS $$ +DECLARE + result jsonb := original; + key text; + new_val jsonb; + orig_val jsonb; +BEGIN + IF original IS NULL THEN RETURN new_data; END IF; + IF new_data IS NULL THEN RETURN original; END IF; + + FOR key, new_val IN SELECT * FROM jsonb_each(new_data) + LOOP + orig_val := result -> key; + IF orig_val IS NOT NULL + AND jsonb_typeof(orig_val) = 'object' AND orig_val != '{}'::jsonb + AND jsonb_typeof(new_val) = 'object' AND new_val != '{}'::jsonb + THEN + result := jsonb_set(result, ARRAY[key], jsonb_deepmerge(orig_val, new_val)); + ELSE + result := jsonb_set(result, ARRAY[key], new_val); + END IF; + END LOOP; + RETURN result; +END; +$$ LANGUAGE plpgsql IMMUTABLE; + + +-- ============================================================================= +-- 2. compute_config_context_for_device(bigint) RETURNS jsonb +-- ============================================================================= +CREATE OR REPLACE FUNCTION compute_config_context_for_device(device_pk bigint) RETURNS jsonb AS $$ +DECLARE + -- Device attributes + _site_id bigint; + _location_id bigint; + _role_id bigint; + _platform_id bigint; + _cluster_id bigint; + _tenant_id bigint; + _device_type_id bigint; + _local_context_data jsonb; + -- Site FK attributes + _site_region_id bigint; + _site_group_id bigint; + -- Region MPTT + _region_tree_id integer; + _region_level integer; + _region_lft integer; + _region_rght integer; + -- SiteGroup MPTT + _sitegroup_tree_id integer; + _sitegroup_level integer; + _sitegroup_lft integer; + _sitegroup_rght integer; + -- Role MPTT + _role_tree_id integer; + _role_level integer; + _role_lft integer; + _role_rght integer; + -- Platform MPTT + _platform_tree_id integer; + _platform_level integer; + _platform_lft integer; + _platform_rght integer; + -- Location MPTT + _location_tree_id integer; + _location_level integer; + _location_lft integer; + _location_rght integer; + -- Cluster attributes + _cluster_type_id bigint; + _cluster_group_id bigint; + -- Tenant attributes + _tenant_group_id bigint; + -- Tag IDs + _tag_ids integer[]; + -- Loop/result + ctx record; + result jsonb := '{}'::jsonb; +BEGIN + -- Fetch device attributes + SELECT site_id, location_id, role_id, platform_id, cluster_id, tenant_id, + device_type_id, local_context_data + INTO _site_id, _location_id, _role_id, _platform_id, _cluster_id, _tenant_id, + _device_type_id, _local_context_data + FROM dcim_device WHERE id = device_pk; + + IF NOT FOUND THEN RETURN NULL; END IF; + + -- Fetch site's region and group + SELECT region_id, group_id INTO _site_region_id, _site_group_id + FROM dcim_site WHERE id = _site_id; + + -- Fetch region MPTT fields + IF _site_region_id IS NOT NULL THEN + SELECT tree_id, level, lft, rght + INTO _region_tree_id, _region_level, _region_lft, _region_rght + FROM dcim_region WHERE id = _site_region_id; + END IF; + + -- Fetch site group MPTT fields + IF _site_group_id IS NOT NULL THEN + SELECT tree_id, level, lft, rght + INTO _sitegroup_tree_id, _sitegroup_level, _sitegroup_lft, _sitegroup_rght + FROM dcim_sitegroup WHERE id = _site_group_id; + END IF; + + -- Fetch role MPTT fields + IF _role_id IS NOT NULL THEN + SELECT tree_id, level, lft, rght + INTO _role_tree_id, _role_level, _role_lft, _role_rght + FROM dcim_devicerole WHERE id = _role_id; + END IF; + + -- Fetch platform MPTT fields + IF _platform_id IS NOT NULL THEN + SELECT tree_id, level, lft, rght + INTO _platform_tree_id, _platform_level, _platform_lft, _platform_rght + FROM dcim_platform WHERE id = _platform_id; + END IF; + + -- Fetch location MPTT fields + IF _location_id IS NOT NULL THEN + SELECT tree_id, level, lft, rght + INTO _location_tree_id, _location_level, _location_lft, _location_rght + FROM dcim_location WHERE id = _location_id; + END IF; + + -- Fetch cluster type and group + IF _cluster_id IS NOT NULL THEN + SELECT type_id, group_id INTO _cluster_type_id, _cluster_group_id + FROM virtualization_cluster WHERE id = _cluster_id; + END IF; + + -- Fetch tenant group + IF _tenant_id IS NOT NULL THEN + SELECT group_id INTO _tenant_group_id + FROM tenancy_tenant WHERE id = _tenant_id; + END IF; + + -- Fetch device's tag IDs + SELECT array_agg(tag_id) INTO _tag_ids + FROM extras_taggeditem + WHERE object_id = device_pk + AND content_type_id = ( + SELECT id FROM django_content_type + WHERE app_label = 'dcim' AND model = 'device' + ); + + -- Find all matching active ConfigContexts, ordered by weight, name + FOR ctx IN + SELECT cc.data + FROM extras_configcontext cc + WHERE cc.is_active = TRUE + -- regions + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_regions WHERE configcontext_id = cc.id) + OR (_site_region_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_regions ecr + JOIN dcim_region r ON r.id = ecr.region_id + WHERE ecr.configcontext_id = cc.id + AND r.tree_id = _region_tree_id + AND r.level <= _region_level + AND r.lft <= _region_lft + AND r.rght >= _region_rght + )) + ) + -- site_groups + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_site_groups WHERE configcontext_id = cc.id) + OR (_site_group_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_site_groups ecsg + JOIN dcim_sitegroup sg ON sg.id = ecsg.sitegroup_id + WHERE ecsg.configcontext_id = cc.id + AND sg.tree_id = _sitegroup_tree_id + AND sg.level <= _sitegroup_level + AND sg.lft <= _sitegroup_lft + AND sg.rght >= _sitegroup_rght + )) + ) + -- sites + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_sites WHERE configcontext_id = cc.id) + OR EXISTS ( + SELECT 1 FROM extras_configcontext_sites + WHERE configcontext_id = cc.id AND site_id = _site_id + ) + ) + -- locations + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_locations WHERE configcontext_id = cc.id) + OR (_location_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_locations ecl + JOIN dcim_location loc ON loc.id = ecl.location_id + WHERE ecl.configcontext_id = cc.id + AND loc.tree_id = _location_tree_id + AND loc.level <= _location_level + AND loc.lft <= _location_lft + AND loc.rght >= _location_rght + )) + ) + -- device_types + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_device_types WHERE configcontext_id = cc.id) + OR EXISTS ( + SELECT 1 FROM extras_configcontext_device_types + WHERE configcontext_id = cc.id AND devicetype_id = _device_type_id + ) + ) + -- roles + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_roles WHERE configcontext_id = cc.id) + OR (_role_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_roles ecr + JOIN dcim_devicerole dr ON dr.id = ecr.devicerole_id + WHERE ecr.configcontext_id = cc.id + AND dr.tree_id = _role_tree_id + AND dr.level <= _role_level + AND dr.lft <= _role_lft + AND dr.rght >= _role_rght + )) + ) + -- platforms + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_platforms WHERE configcontext_id = cc.id) + OR (_platform_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_platforms ecp + JOIN dcim_platform p ON p.id = ecp.platform_id + WHERE ecp.configcontext_id = cc.id + AND p.tree_id = _platform_tree_id + AND p.level <= _platform_level + AND p.lft <= _platform_lft + AND p.rght >= _platform_rght + )) + ) + -- cluster_types + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_cluster_types WHERE configcontext_id = cc.id) + OR (_cluster_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_cluster_types + WHERE configcontext_id = cc.id AND clustertype_id = _cluster_type_id + )) + ) + -- cluster_groups + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_cluster_groups WHERE configcontext_id = cc.id) + OR (_cluster_id IS NOT NULL AND _cluster_group_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_cluster_groups + WHERE configcontext_id = cc.id AND clustergroup_id = _cluster_group_id + )) + ) + -- clusters + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_clusters WHERE configcontext_id = cc.id) + OR (_cluster_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_clusters + WHERE configcontext_id = cc.id AND cluster_id = _cluster_id + )) + ) + -- tenant_groups + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_tenant_groups WHERE configcontext_id = cc.id) + OR (_tenant_id IS NOT NULL AND _tenant_group_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_tenant_groups + WHERE configcontext_id = cc.id AND tenantgroup_id = _tenant_group_id + )) + ) + -- tenants + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_tenants WHERE configcontext_id = cc.id) + OR (_tenant_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_tenants + WHERE configcontext_id = cc.id AND tenant_id = _tenant_id + )) + ) + -- tags + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_tags WHERE configcontext_id = cc.id) + OR (_tag_ids IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_tags + WHERE configcontext_id = cc.id AND tag_id = ANY(_tag_ids) + )) + ) + ORDER BY cc.weight, cc.name + LOOP + result := jsonb_deepmerge(result, ctx.data); + END LOOP; + + -- Merge local_context_data last (highest priority) + IF _local_context_data IS NOT NULL AND _local_context_data != 'null'::jsonb THEN + result := jsonb_deepmerge(result, _local_context_data); + END IF; + + RETURN result; +END; +$$ LANGUAGE plpgsql STABLE; + + +-- ============================================================================= +-- 3. compute_config_context_for_vm(bigint) RETURNS jsonb +-- ============================================================================= +CREATE OR REPLACE FUNCTION compute_config_context_for_vm(vm_pk bigint) RETURNS jsonb AS $$ +DECLARE + -- VM attributes + _site_id bigint; + _role_id bigint; + _platform_id bigint; + _cluster_id bigint; + _tenant_id bigint; + _local_context_data jsonb; + -- Site FK attributes + _site_region_id bigint; + _site_group_id bigint; + -- Region MPTT + _region_tree_id integer; + _region_level integer; + _region_lft integer; + _region_rght integer; + -- SiteGroup MPTT + _sitegroup_tree_id integer; + _sitegroup_level integer; + _sitegroup_lft integer; + _sitegroup_rght integer; + -- Role MPTT + _role_tree_id integer; + _role_level integer; + _role_lft integer; + _role_rght integer; + -- Platform MPTT + _platform_tree_id integer; + _platform_level integer; + _platform_lft integer; + _platform_rght integer; + -- Cluster attributes + _cluster_type_id bigint; + _cluster_group_id bigint; + -- Tenant attributes + _tenant_group_id bigint; + -- Tag IDs + _tag_ids integer[]; + -- Loop/result + ctx record; + result jsonb := '{}'::jsonb; +BEGIN + -- Fetch VM attributes + SELECT site_id, role_id, platform_id, cluster_id, tenant_id, local_context_data + INTO _site_id, _role_id, _platform_id, _cluster_id, _tenant_id, _local_context_data + FROM virtualization_virtualmachine WHERE id = vm_pk; + + IF NOT FOUND THEN RETURN NULL; END IF; + + -- Fetch site's region and group + IF _site_id IS NOT NULL THEN + SELECT region_id, group_id INTO _site_region_id, _site_group_id + FROM dcim_site WHERE id = _site_id; + END IF; + + -- Fetch region MPTT fields + IF _site_region_id IS NOT NULL THEN + SELECT tree_id, level, lft, rght + INTO _region_tree_id, _region_level, _region_lft, _region_rght + FROM dcim_region WHERE id = _site_region_id; + END IF; + + -- Fetch site group MPTT fields + IF _site_group_id IS NOT NULL THEN + SELECT tree_id, level, lft, rght + INTO _sitegroup_tree_id, _sitegroup_level, _sitegroup_lft, _sitegroup_rght + FROM dcim_sitegroup WHERE id = _site_group_id; + END IF; + + -- Fetch role MPTT fields + IF _role_id IS NOT NULL THEN + SELECT tree_id, level, lft, rght + INTO _role_tree_id, _role_level, _role_lft, _role_rght + FROM dcim_devicerole WHERE id = _role_id; + END IF; + + -- Fetch platform MPTT fields + IF _platform_id IS NOT NULL THEN + SELECT tree_id, level, lft, rght + INTO _platform_tree_id, _platform_level, _platform_lft, _platform_rght + FROM dcim_platform WHERE id = _platform_id; + END IF; + + -- Fetch cluster type and group + IF _cluster_id IS NOT NULL THEN + SELECT type_id, group_id INTO _cluster_type_id, _cluster_group_id + FROM virtualization_cluster WHERE id = _cluster_id; + END IF; + + -- Fetch tenant group + IF _tenant_id IS NOT NULL THEN + SELECT group_id INTO _tenant_group_id + FROM tenancy_tenant WHERE id = _tenant_id; + END IF; + + -- Fetch VM's tag IDs + SELECT array_agg(tag_id) INTO _tag_ids + FROM extras_taggeditem + WHERE object_id = vm_pk + AND content_type_id = ( + SELECT id FROM django_content_type + WHERE app_label = 'virtualization' AND model = 'virtualmachine' + ); + + -- Find all matching active ConfigContexts, ordered by weight, name + FOR ctx IN + SELECT cc.data + FROM extras_configcontext cc + WHERE cc.is_active = TRUE + -- regions + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_regions WHERE configcontext_id = cc.id) + OR (_site_region_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_regions ecr + JOIN dcim_region r ON r.id = ecr.region_id + WHERE ecr.configcontext_id = cc.id + AND r.tree_id = _region_tree_id + AND r.level <= _region_level + AND r.lft <= _region_lft + AND r.rght >= _region_rght + )) + ) + -- site_groups + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_site_groups WHERE configcontext_id = cc.id) + OR (_site_group_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_site_groups ecsg + JOIN dcim_sitegroup sg ON sg.id = ecsg.sitegroup_id + WHERE ecsg.configcontext_id = cc.id + AND sg.tree_id = _sitegroup_tree_id + AND sg.level <= _sitegroup_level + AND sg.lft <= _sitegroup_lft + AND sg.rght >= _sitegroup_rght + )) + ) + -- sites + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_sites WHERE configcontext_id = cc.id) + OR (_site_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_sites + WHERE configcontext_id = cc.id AND site_id = _site_id + )) + ) + -- locations: VMs never match location-scoped contexts + AND NOT EXISTS (SELECT 1 FROM extras_configcontext_locations WHERE configcontext_id = cc.id) + -- device_types: VMs never match device-type-scoped contexts + AND NOT EXISTS (SELECT 1 FROM extras_configcontext_device_types WHERE configcontext_id = cc.id) + -- roles + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_roles WHERE configcontext_id = cc.id) + OR (_role_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_roles ecr + JOIN dcim_devicerole dr ON dr.id = ecr.devicerole_id + WHERE ecr.configcontext_id = cc.id + AND dr.tree_id = _role_tree_id + AND dr.level <= _role_level + AND dr.lft <= _role_lft + AND dr.rght >= _role_rght + )) + ) + -- platforms + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_platforms WHERE configcontext_id = cc.id) + OR (_platform_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_platforms ecp + JOIN dcim_platform p ON p.id = ecp.platform_id + WHERE ecp.configcontext_id = cc.id + AND p.tree_id = _platform_tree_id + AND p.level <= _platform_level + AND p.lft <= _platform_lft + AND p.rght >= _platform_rght + )) + ) + -- cluster_types + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_cluster_types WHERE configcontext_id = cc.id) + OR (_cluster_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_cluster_types + WHERE configcontext_id = cc.id AND clustertype_id = _cluster_type_id + )) + ) + -- cluster_groups + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_cluster_groups WHERE configcontext_id = cc.id) + OR (_cluster_id IS NOT NULL AND _cluster_group_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_cluster_groups + WHERE configcontext_id = cc.id AND clustergroup_id = _cluster_group_id + )) + ) + -- clusters + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_clusters WHERE configcontext_id = cc.id) + OR (_cluster_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_clusters + WHERE configcontext_id = cc.id AND cluster_id = _cluster_id + )) + ) + -- tenant_groups + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_tenant_groups WHERE configcontext_id = cc.id) + OR (_tenant_id IS NOT NULL AND _tenant_group_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_tenant_groups + WHERE configcontext_id = cc.id AND tenantgroup_id = _tenant_group_id + )) + ) + -- tenants + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_tenants WHERE configcontext_id = cc.id) + OR (_tenant_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_tenants + WHERE configcontext_id = cc.id AND tenant_id = _tenant_id + )) + ) + -- tags + AND ( + NOT EXISTS (SELECT 1 FROM extras_configcontext_tags WHERE configcontext_id = cc.id) + OR (_tag_ids IS NOT NULL AND EXISTS ( + SELECT 1 FROM extras_configcontext_tags + WHERE configcontext_id = cc.id AND tag_id = ANY(_tag_ids) + )) + ) + ORDER BY cc.weight, cc.name + LOOP + result := jsonb_deepmerge(result, ctx.data); + END LOOP; + + -- Merge local_context_data last (highest priority) + IF _local_context_data IS NOT NULL AND _local_context_data != 'null'::jsonb THEN + result := jsonb_deepmerge(result, _local_context_data); + END IF; + + RETURN result; +END; +$$ LANGUAGE plpgsql STABLE; + + +-- ============================================================================= +-- 4. Helper wrappers: refresh_device_config_context / refresh_vm_config_context +-- ============================================================================= +CREATE OR REPLACE FUNCTION refresh_device_config_context(device_pk bigint) RETURNS void AS $$ +BEGIN + UPDATE dcim_device + SET config_context_data = compute_config_context_for_device(device_pk) + WHERE id = device_pk; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION refresh_vm_config_context(vm_pk bigint) RETURNS void AS $$ +BEGIN + UPDATE virtualization_virtualmachine + SET config_context_data = compute_config_context_for_vm(vm_pk) + WHERE id = vm_pk; +END; +$$ LANGUAGE plpgsql; + + +-- ============================================================================= +-- 5. Bulk refresh helpers +-- ============================================================================= +CREATE OR REPLACE FUNCTION refresh_all_device_config_contexts() RETURNS void AS $$ +BEGIN + UPDATE dcim_device SET config_context_data = compute_config_context_for_device(id); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION refresh_all_vm_config_contexts() RETURNS void AS $$ +BEGIN + UPDATE virtualization_virtualmachine SET config_context_data = compute_config_context_for_vm(id); +END; +$$ LANGUAGE plpgsql; + + +-- ============================================================================= +-- CATEGORY A: Device/VM attribute change triggers +-- ============================================================================= + +-- A1: Device INSERT +CREATE OR REPLACE FUNCTION trg_device_insert_config_context() RETURNS trigger AS $$ +BEGIN + PERFORM refresh_device_config_context(NEW.id); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_device_config_context_insert + AFTER INSERT ON dcim_device + FOR EACH ROW + EXECUTE FUNCTION trg_device_insert_config_context(); + +-- A2: Device UPDATE +CREATE OR REPLACE FUNCTION trg_device_update_config_context() RETURNS trigger AS $$ +BEGIN + PERFORM refresh_device_config_context(NEW.id); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_device_config_context_update + AFTER UPDATE ON dcim_device + FOR EACH ROW + WHEN ( + OLD.site_id IS DISTINCT FROM NEW.site_id OR + OLD.location_id IS DISTINCT FROM NEW.location_id OR + OLD.role_id IS DISTINCT FROM NEW.role_id OR + OLD.platform_id IS DISTINCT FROM NEW.platform_id OR + OLD.cluster_id IS DISTINCT FROM NEW.cluster_id OR + OLD.tenant_id IS DISTINCT FROM NEW.tenant_id OR + OLD.device_type_id IS DISTINCT FROM NEW.device_type_id OR + OLD.local_context_data IS DISTINCT FROM NEW.local_context_data + ) + EXECUTE FUNCTION trg_device_update_config_context(); + +-- A3: VM INSERT +CREATE OR REPLACE FUNCTION trg_vm_insert_config_context() RETURNS trigger AS $$ +BEGIN + PERFORM refresh_vm_config_context(NEW.id); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_vm_config_context_insert + AFTER INSERT ON virtualization_virtualmachine + FOR EACH ROW + EXECUTE FUNCTION trg_vm_insert_config_context(); + +-- A4: VM UPDATE +CREATE OR REPLACE FUNCTION trg_vm_update_config_context() RETURNS trigger AS $$ +BEGIN + PERFORM refresh_vm_config_context(NEW.id); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_vm_config_context_update + AFTER UPDATE ON virtualization_virtualmachine + FOR EACH ROW + WHEN ( + OLD.site_id IS DISTINCT FROM NEW.site_id OR + OLD.role_id IS DISTINCT FROM NEW.role_id OR + OLD.platform_id IS DISTINCT FROM NEW.platform_id OR + OLD.cluster_id IS DISTINCT FROM NEW.cluster_id OR + OLD.tenant_id IS DISTINCT FROM NEW.tenant_id OR + OLD.local_context_data IS DISTINCT FROM NEW.local_context_data + ) + EXECUTE FUNCTION trg_vm_update_config_context(); + + +-- ============================================================================= +-- CATEGORY B: ConfigContext direct changes +-- ============================================================================= + +CREATE OR REPLACE FUNCTION trg_configcontext_change_refresh_all() RETURNS trigger AS $$ +BEGIN + PERFORM refresh_all_device_config_contexts(); + PERFORM refresh_all_vm_config_contexts(); + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_configcontext_insert + AFTER INSERT ON extras_configcontext + FOR EACH ROW + EXECUTE FUNCTION trg_configcontext_change_refresh_all(); + +CREATE TRIGGER trg_configcontext_delete + AFTER DELETE ON extras_configcontext + FOR EACH ROW + EXECUTE FUNCTION trg_configcontext_change_refresh_all(); + +CREATE OR REPLACE FUNCTION trg_configcontext_update_refresh_all() RETURNS trigger AS $$ +BEGIN + PERFORM refresh_all_device_config_contexts(); + PERFORM refresh_all_vm_config_contexts(); + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_configcontext_update + AFTER UPDATE ON extras_configcontext + FOR EACH ROW + WHEN ( + OLD.data IS DISTINCT FROM NEW.data OR + OLD.weight IS DISTINCT FROM NEW.weight OR + OLD.is_active IS DISTINCT FROM NEW.is_active OR + OLD.name IS DISTINCT FROM NEW.name + ) + EXECUTE FUNCTION trg_configcontext_update_refresh_all(); + + +-- ============================================================================= +-- CATEGORY C: ConfigContext M2M assignment changes +-- ============================================================================= + +CREATE OR REPLACE FUNCTION trg_configcontext_m2m_refresh_all() RETURNS trigger AS $$ +BEGIN + PERFORM refresh_all_device_config_contexts(); + PERFORM refresh_all_vm_config_contexts(); + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +-- regions +CREATE TRIGGER trg_cc_regions_insert AFTER INSERT ON extras_configcontext_regions + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); +CREATE TRIGGER trg_cc_regions_delete AFTER DELETE ON extras_configcontext_regions + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); + +-- site_groups +CREATE TRIGGER trg_cc_site_groups_insert AFTER INSERT ON extras_configcontext_site_groups + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); +CREATE TRIGGER trg_cc_site_groups_delete AFTER DELETE ON extras_configcontext_site_groups + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); + +-- sites +CREATE TRIGGER trg_cc_sites_insert AFTER INSERT ON extras_configcontext_sites + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); +CREATE TRIGGER trg_cc_sites_delete AFTER DELETE ON extras_configcontext_sites + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); + +-- locations +CREATE TRIGGER trg_cc_locations_insert AFTER INSERT ON extras_configcontext_locations + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); +CREATE TRIGGER trg_cc_locations_delete AFTER DELETE ON extras_configcontext_locations + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); + +-- device_types +CREATE TRIGGER trg_cc_device_types_insert AFTER INSERT ON extras_configcontext_device_types + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); +CREATE TRIGGER trg_cc_device_types_delete AFTER DELETE ON extras_configcontext_device_types + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); + +-- roles +CREATE TRIGGER trg_cc_roles_insert AFTER INSERT ON extras_configcontext_roles + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); +CREATE TRIGGER trg_cc_roles_delete AFTER DELETE ON extras_configcontext_roles + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); + +-- platforms +CREATE TRIGGER trg_cc_platforms_insert AFTER INSERT ON extras_configcontext_platforms + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); +CREATE TRIGGER trg_cc_platforms_delete AFTER DELETE ON extras_configcontext_platforms + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); + +-- cluster_types +CREATE TRIGGER trg_cc_cluster_types_insert AFTER INSERT ON extras_configcontext_cluster_types + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); +CREATE TRIGGER trg_cc_cluster_types_delete AFTER DELETE ON extras_configcontext_cluster_types + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); + +-- cluster_groups +CREATE TRIGGER trg_cc_cluster_groups_insert AFTER INSERT ON extras_configcontext_cluster_groups + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); +CREATE TRIGGER trg_cc_cluster_groups_delete AFTER DELETE ON extras_configcontext_cluster_groups + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); + +-- clusters +CREATE TRIGGER trg_cc_clusters_insert AFTER INSERT ON extras_configcontext_clusters + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); +CREATE TRIGGER trg_cc_clusters_delete AFTER DELETE ON extras_configcontext_clusters + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); + +-- tenant_groups +CREATE TRIGGER trg_cc_tenant_groups_insert AFTER INSERT ON extras_configcontext_tenant_groups + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); +CREATE TRIGGER trg_cc_tenant_groups_delete AFTER DELETE ON extras_configcontext_tenant_groups + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); + +-- tenants +CREATE TRIGGER trg_cc_tenants_insert AFTER INSERT ON extras_configcontext_tenants + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); +CREATE TRIGGER trg_cc_tenants_delete AFTER DELETE ON extras_configcontext_tenants + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); + +-- tags +CREATE TRIGGER trg_cc_tags_insert AFTER INSERT ON extras_configcontext_tags + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); +CREATE TRIGGER trg_cc_tags_delete AFTER DELETE ON extras_configcontext_tags + FOR EACH STATEMENT EXECUTE FUNCTION trg_configcontext_m2m_refresh_all(); + + +-- ============================================================================= +-- CATEGORY D: Hierarchical/related model changes +-- ============================================================================= + +-- D1: Site (region_id or group_id changes) +CREATE OR REPLACE FUNCTION trg_site_update_config_context() RETURNS trigger AS $$ +BEGIN + UPDATE dcim_device SET config_context_data = compute_config_context_for_device(id) + WHERE site_id = NEW.id; + UPDATE virtualization_virtualmachine SET config_context_data = compute_config_context_for_vm(id) + WHERE site_id = NEW.id; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_site_config_context_update + AFTER UPDATE ON dcim_site + FOR EACH ROW + WHEN ( + OLD.region_id IS DISTINCT FROM NEW.region_id OR + OLD.group_id IS DISTINCT FROM NEW.group_id + ) + EXECUTE FUNCTION trg_site_update_config_context(); + +-- D2: Cluster (type_id or group_id changes) +CREATE OR REPLACE FUNCTION trg_cluster_update_config_context() RETURNS trigger AS $$ +BEGIN + UPDATE dcim_device SET config_context_data = compute_config_context_for_device(id) + WHERE cluster_id = NEW.id; + UPDATE virtualization_virtualmachine SET config_context_data = compute_config_context_for_vm(id) + WHERE cluster_id = NEW.id; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_cluster_config_context_update + AFTER UPDATE ON virtualization_cluster + FOR EACH ROW + WHEN ( + OLD.type_id IS DISTINCT FROM NEW.type_id OR + OLD.group_id IS DISTINCT FROM NEW.group_id + ) + EXECUTE FUNCTION trg_cluster_update_config_context(); + +-- D3: Tenant (group_id changes) +CREATE OR REPLACE FUNCTION trg_tenant_update_config_context() RETURNS trigger AS $$ +BEGIN + UPDATE dcim_device SET config_context_data = compute_config_context_for_device(id) + WHERE tenant_id = NEW.id; + UPDATE virtualization_virtualmachine SET config_context_data = compute_config_context_for_vm(id) + WHERE tenant_id = NEW.id; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_tenant_config_context_update + AFTER UPDATE ON tenancy_tenant + FOR EACH ROW + WHEN (OLD.group_id IS DISTINCT FROM NEW.group_id) + EXECUTE FUNCTION trg_tenant_update_config_context(); + +-- D4: Region (MPTT fields change) +CREATE OR REPLACE FUNCTION trg_region_update_config_context() RETURNS trigger AS $$ +BEGIN + UPDATE dcim_device SET config_context_data = compute_config_context_for_device(dcim_device.id) + FROM dcim_site WHERE dcim_device.site_id = dcim_site.id AND dcim_site.region_id = NEW.id; + UPDATE virtualization_virtualmachine SET config_context_data = compute_config_context_for_vm(virtualization_virtualmachine.id) + FROM dcim_site WHERE virtualization_virtualmachine.site_id = dcim_site.id AND dcim_site.region_id = NEW.id; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_region_config_context_update + AFTER UPDATE ON dcim_region + FOR EACH ROW + WHEN ( + OLD.lft IS DISTINCT FROM NEW.lft OR + OLD.rght IS DISTINCT FROM NEW.rght OR + OLD.tree_id IS DISTINCT FROM NEW.tree_id OR + OLD.level IS DISTINCT FROM NEW.level OR + OLD.parent_id IS DISTINCT FROM NEW.parent_id + ) + EXECUTE FUNCTION trg_region_update_config_context(); + +-- D5: SiteGroup (MPTT fields change) +CREATE OR REPLACE FUNCTION trg_sitegroup_update_config_context() RETURNS trigger AS $$ +BEGIN + UPDATE dcim_device SET config_context_data = compute_config_context_for_device(dcim_device.id) + FROM dcim_site WHERE dcim_device.site_id = dcim_site.id AND dcim_site.group_id = NEW.id; + UPDATE virtualization_virtualmachine SET config_context_data = compute_config_context_for_vm(virtualization_virtualmachine.id) + FROM dcim_site WHERE virtualization_virtualmachine.site_id = dcim_site.id AND dcim_site.group_id = NEW.id; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_sitegroup_config_context_update + AFTER UPDATE ON dcim_sitegroup + FOR EACH ROW + WHEN ( + OLD.lft IS DISTINCT FROM NEW.lft OR + OLD.rght IS DISTINCT FROM NEW.rght OR + OLD.tree_id IS DISTINCT FROM NEW.tree_id OR + OLD.level IS DISTINCT FROM NEW.level OR + OLD.parent_id IS DISTINCT FROM NEW.parent_id + ) + EXECUTE FUNCTION trg_sitegroup_update_config_context(); + +-- D6: DeviceRole (MPTT fields change) +CREATE OR REPLACE FUNCTION trg_devicerole_update_config_context() RETURNS trigger AS $$ +BEGIN + UPDATE dcim_device SET config_context_data = compute_config_context_for_device(id) + WHERE role_id = NEW.id; + UPDATE virtualization_virtualmachine SET config_context_data = compute_config_context_for_vm(id) + WHERE role_id = NEW.id; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_devicerole_config_context_update + AFTER UPDATE ON dcim_devicerole + FOR EACH ROW + WHEN ( + OLD.lft IS DISTINCT FROM NEW.lft OR + OLD.rght IS DISTINCT FROM NEW.rght OR + OLD.tree_id IS DISTINCT FROM NEW.tree_id OR + OLD.level IS DISTINCT FROM NEW.level OR + OLD.parent_id IS DISTINCT FROM NEW.parent_id + ) + EXECUTE FUNCTION trg_devicerole_update_config_context(); + +-- D7: Platform (MPTT fields change) +CREATE OR REPLACE FUNCTION trg_platform_update_config_context() RETURNS trigger AS $$ +BEGIN + UPDATE dcim_device SET config_context_data = compute_config_context_for_device(id) + WHERE platform_id = NEW.id; + UPDATE virtualization_virtualmachine SET config_context_data = compute_config_context_for_vm(id) + WHERE platform_id = NEW.id; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_platform_config_context_update + AFTER UPDATE ON dcim_platform + FOR EACH ROW + WHEN ( + OLD.lft IS DISTINCT FROM NEW.lft OR + OLD.rght IS DISTINCT FROM NEW.rght OR + OLD.tree_id IS DISTINCT FROM NEW.tree_id OR + OLD.level IS DISTINCT FROM NEW.level OR + OLD.parent_id IS DISTINCT FROM NEW.parent_id + ) + EXECUTE FUNCTION trg_platform_update_config_context(); + +-- D8: Location (MPTT fields change) +CREATE OR REPLACE FUNCTION trg_location_update_config_context() RETURNS trigger AS $$ +BEGIN + UPDATE dcim_device SET config_context_data = compute_config_context_for_device(id) + WHERE location_id = NEW.id; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_location_config_context_update + AFTER UPDATE ON dcim_location + FOR EACH ROW + WHEN ( + OLD.lft IS DISTINCT FROM NEW.lft OR + OLD.rght IS DISTINCT FROM NEW.rght OR + OLD.tree_id IS DISTINCT FROM NEW.tree_id OR + OLD.level IS DISTINCT FROM NEW.level OR + OLD.parent_id IS DISTINCT FROM NEW.parent_id + ) + EXECUTE FUNCTION trg_location_update_config_context(); + + +-- ============================================================================= +-- CATEGORY E: Tag changes on devices/VMs +-- ============================================================================= + +CREATE OR REPLACE FUNCTION trg_taggeditem_config_context() RETURNS trigger AS $$ +DECLARE + item record; + device_ct_id integer; + vm_ct_id integer; +BEGIN + IF TG_OP = 'DELETE' THEN + item := OLD; + ELSE + item := NEW; + END IF; + + SELECT id INTO device_ct_id FROM django_content_type + WHERE app_label = 'dcim' AND model = 'device'; + + SELECT id INTO vm_ct_id FROM django_content_type + WHERE app_label = 'virtualization' AND model = 'virtualmachine'; + + IF item.content_type_id = device_ct_id THEN + PERFORM refresh_device_config_context(item.object_id); + ELSIF item.content_type_id = vm_ct_id THEN + PERFORM refresh_vm_config_context(item.object_id); + END IF; + + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_taggeditem_config_context_insert + AFTER INSERT ON extras_taggeditem + FOR EACH ROW + EXECUTE FUNCTION trg_taggeditem_config_context(); + +CREATE TRIGGER trg_taggeditem_config_context_delete + AFTER DELETE ON extras_taggeditem + FOR EACH ROW + EXECUTE FUNCTION trg_taggeditem_config_context(); +""" + + +REVERSE_SQL = """ +-- Drop all triggers +DROP TRIGGER IF EXISTS trg_device_config_context_insert ON dcim_device; +DROP TRIGGER IF EXISTS trg_device_config_context_update ON dcim_device; +DROP TRIGGER IF EXISTS trg_vm_config_context_insert ON virtualization_virtualmachine; +DROP TRIGGER IF EXISTS trg_vm_config_context_update ON virtualization_virtualmachine; +DROP TRIGGER IF EXISTS trg_configcontext_insert ON extras_configcontext; +DROP TRIGGER IF EXISTS trg_configcontext_delete ON extras_configcontext; +DROP TRIGGER IF EXISTS trg_configcontext_update ON extras_configcontext; +DROP TRIGGER IF EXISTS trg_cc_regions_insert ON extras_configcontext_regions; +DROP TRIGGER IF EXISTS trg_cc_regions_delete ON extras_configcontext_regions; +DROP TRIGGER IF EXISTS trg_cc_site_groups_insert ON extras_configcontext_site_groups; +DROP TRIGGER IF EXISTS trg_cc_site_groups_delete ON extras_configcontext_site_groups; +DROP TRIGGER IF EXISTS trg_cc_sites_insert ON extras_configcontext_sites; +DROP TRIGGER IF EXISTS trg_cc_sites_delete ON extras_configcontext_sites; +DROP TRIGGER IF EXISTS trg_cc_locations_insert ON extras_configcontext_locations; +DROP TRIGGER IF EXISTS trg_cc_locations_delete ON extras_configcontext_locations; +DROP TRIGGER IF EXISTS trg_cc_device_types_insert ON extras_configcontext_device_types; +DROP TRIGGER IF EXISTS trg_cc_device_types_delete ON extras_configcontext_device_types; +DROP TRIGGER IF EXISTS trg_cc_roles_insert ON extras_configcontext_roles; +DROP TRIGGER IF EXISTS trg_cc_roles_delete ON extras_configcontext_roles; +DROP TRIGGER IF EXISTS trg_cc_platforms_insert ON extras_configcontext_platforms; +DROP TRIGGER IF EXISTS trg_cc_platforms_delete ON extras_configcontext_platforms; +DROP TRIGGER IF EXISTS trg_cc_cluster_types_insert ON extras_configcontext_cluster_types; +DROP TRIGGER IF EXISTS trg_cc_cluster_types_delete ON extras_configcontext_cluster_types; +DROP TRIGGER IF EXISTS trg_cc_cluster_groups_insert ON extras_configcontext_cluster_groups; +DROP TRIGGER IF EXISTS trg_cc_cluster_groups_delete ON extras_configcontext_cluster_groups; +DROP TRIGGER IF EXISTS trg_cc_clusters_insert ON extras_configcontext_clusters; +DROP TRIGGER IF EXISTS trg_cc_clusters_delete ON extras_configcontext_clusters; +DROP TRIGGER IF EXISTS trg_cc_tenant_groups_insert ON extras_configcontext_tenant_groups; +DROP TRIGGER IF EXISTS trg_cc_tenant_groups_delete ON extras_configcontext_tenant_groups; +DROP TRIGGER IF EXISTS trg_cc_tenants_insert ON extras_configcontext_tenants; +DROP TRIGGER IF EXISTS trg_cc_tenants_delete ON extras_configcontext_tenants; +DROP TRIGGER IF EXISTS trg_cc_tags_insert ON extras_configcontext_tags; +DROP TRIGGER IF EXISTS trg_cc_tags_delete ON extras_configcontext_tags; +DROP TRIGGER IF EXISTS trg_site_config_context_update ON dcim_site; +DROP TRIGGER IF EXISTS trg_cluster_config_context_update ON virtualization_cluster; +DROP TRIGGER IF EXISTS trg_tenant_config_context_update ON tenancy_tenant; +DROP TRIGGER IF EXISTS trg_region_config_context_update ON dcim_region; +DROP TRIGGER IF EXISTS trg_sitegroup_config_context_update ON dcim_sitegroup; +DROP TRIGGER IF EXISTS trg_devicerole_config_context_update ON dcim_devicerole; +DROP TRIGGER IF EXISTS trg_platform_config_context_update ON dcim_platform; +DROP TRIGGER IF EXISTS trg_location_config_context_update ON dcim_location; +DROP TRIGGER IF EXISTS trg_taggeditem_config_context_insert ON extras_taggeditem; +DROP TRIGGER IF EXISTS trg_taggeditem_config_context_delete ON extras_taggeditem; + +-- Drop all trigger functions +DROP FUNCTION IF EXISTS trg_device_insert_config_context(); +DROP FUNCTION IF EXISTS trg_device_update_config_context(); +DROP FUNCTION IF EXISTS trg_vm_insert_config_context(); +DROP FUNCTION IF EXISTS trg_vm_update_config_context(); +DROP FUNCTION IF EXISTS trg_configcontext_change_refresh_all(); +DROP FUNCTION IF EXISTS trg_configcontext_update_refresh_all(); +DROP FUNCTION IF EXISTS trg_configcontext_m2m_refresh_all(); +DROP FUNCTION IF EXISTS trg_site_update_config_context(); +DROP FUNCTION IF EXISTS trg_cluster_update_config_context(); +DROP FUNCTION IF EXISTS trg_tenant_update_config_context(); +DROP FUNCTION IF EXISTS trg_region_update_config_context(); +DROP FUNCTION IF EXISTS trg_sitegroup_update_config_context(); +DROP FUNCTION IF EXISTS trg_devicerole_update_config_context(); +DROP FUNCTION IF EXISTS trg_platform_update_config_context(); +DROP FUNCTION IF EXISTS trg_location_update_config_context(); +DROP FUNCTION IF EXISTS trg_taggeditem_config_context(); + +-- Drop helper functions +DROP FUNCTION IF EXISTS refresh_all_vm_config_contexts(); +DROP FUNCTION IF EXISTS refresh_all_device_config_contexts(); +DROP FUNCTION IF EXISTS refresh_vm_config_context(bigint); +DROP FUNCTION IF EXISTS refresh_device_config_context(bigint); +DROP FUNCTION IF EXISTS compute_config_context_for_vm(bigint); +DROP FUNCTION IF EXISTS compute_config_context_for_device(bigint); +DROP FUNCTION IF EXISTS jsonb_deepmerge(jsonb, jsonb); +""" + + +POPULATE_SQL = """ +UPDATE dcim_device SET config_context_data = compute_config_context_for_device(id); +UPDATE virtualization_virtualmachine SET config_context_data = compute_config_context_for_vm(id); +""" + +CLEAR_SQL = """ +UPDATE dcim_device SET config_context_data = NULL; +UPDATE virtualization_virtualmachine SET config_context_data = NULL; +""" + + +class Migration(migrations.Migration): + dependencies = [ + ('extras', '0134_owner'), + ('dcim', '0227_device_config_context_data'), + ('virtualization', '0053_virtualmachine_config_context_data'), + ('tenancy', '0023_add_mptt_tree_indexes'), + ] + + operations = [ + migrations.RunSQL( + sql=FORWARD_SQL, + reverse_sql=REVERSE_SQL, + ), + migrations.RunSQL( + sql=POPULATE_SQL, + reverse_sql=CLEAR_SQL, + ), + ] diff --git a/netbox/extras/models/configs.py b/netbox/extras/models/configs.py index 88ec1276e..79d2bc01b 100644 --- a/netbox/extras/models/configs.py +++ b/netbox/extras/models/configs.py @@ -225,6 +225,11 @@ class ConfigContextModel(models.Model): "Local config context data takes precedence over source contexts in the final rendered config context" ) ) + config_context_data = models.JSONField( + blank=True, + null=True, + editable=False, + ) class Meta: abstract = True @@ -234,19 +239,21 @@ class ConfigContextModel(models.Model): Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs. Return the rendered configuration context for a device or VM. """ - data = {} + # Use pre-rendered cached field if available + if self.config_context_data is not None: + return self.config_context_data - if not hasattr(self, 'config_context_data'): - # The annotation is not available, so we fall back to manually querying for the config context objects - config_context_data = ConfigContext.objects.get_for_object(self, aggregate_data=True) or [] + # Fall back to annotation if queryset was annotated + data = {} + if hasattr(self, '_annotated_config_context_data'): + config_context_data = self._annotated_config_context_data or [] else: - # The attribute may exist, but the annotated value could be None if there is no config context data - config_context_data = self.config_context_data or [] + # Last resort: compute on-the-fly + config_context_data = ConfigContext.objects.get_for_object(self, aggregate_data=True) or [] for context in config_context_data: data = deepmerge(data, context) - # If the object has local config context data defined, merge it last if self.local_context_data: data = deepmerge(data, self.local_context_data) diff --git a/netbox/extras/querysets.py b/netbox/extras/querysets.py index 7602aee53..6e0f0b569 100644 --- a/netbox/extras/querysets.py +++ b/netbox/extras/querysets.py @@ -90,7 +90,7 @@ class ConfigContextModelQuerySet(RestrictedQuerySet): """ from extras.models import ConfigContext return self.annotate( - config_context_data=Subquery( + _annotated_config_context_data=Subquery( ConfigContext.objects.filter( self._get_config_context_filters() ).annotate( diff --git a/netbox/extras/tests/test_models.py b/netbox/extras/tests/test_models.py index e4cd4ff43..ead156a2c 100644 --- a/netbox/extras/tests/test_models.py +++ b/netbox/extras/tests/test_models.py @@ -206,6 +206,7 @@ class ConfigContextTest(TestCase): "b": 456, "c": 777 } + device.refresh_from_db() self.assertEqual(device.get_config_context(), expected_data) def test_name_ordering_after_weight(self): @@ -235,6 +236,7 @@ class ConfigContextTest(TestCase): "b": 456, "c": 789 } + device.refresh_from_db() self.assertEqual(device.get_config_context(), expected_data) def test_schema_validation(self): @@ -303,6 +305,7 @@ class ConfigContextTest(TestCase): ) ConfigContext.objects.bulk_create([context1, context2, context3, context4]) + device.refresh_from_db() annotated_queryset = Device.objects.filter(name=device.name).annotate_config_context_data() self.assertEqual(device.get_config_context(), annotated_queryset[0].get_config_context()) @@ -666,7 +669,7 @@ class ConfigContextTest(TestCase): self.assertFalse(queryset.query.distinct) # Check that tag subqueries DO use DISTINCT by inspecting the annotation - config_annotation = queryset.query.annotations.get('config_context_data') + config_annotation = queryset.query.annotations.get('_annotated_config_context_data') self.assertIsNotNone(config_annotation) def find_tag_subqueries(where_node): diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index 4fe0e9ca7..013515b9b 100644 --- a/netbox/virtualization/api/views.py +++ b/netbox/virtualization/api/views.py @@ -1,7 +1,7 @@ from django.db.models import Sum from rest_framework.routers import APIRootView -from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin +from extras.api.mixins import RenderConfigMixin from netbox.api.viewsets import NetBoxModelViewSet from utilities.query_functions import CollateAsChar from virtualization import filtersets @@ -48,7 +48,7 @@ class ClusterViewSet(NetBoxModelViewSet): # Virtual machines # -class VirtualMachineViewSet(ConfigContextQuerySetMixin, RenderConfigMixin, NetBoxModelViewSet): +class VirtualMachineViewSet(RenderConfigMixin, NetBoxModelViewSet): queryset = VirtualMachine.objects.all() filterset_class = filtersets.VirtualMachineFilterSet diff --git a/netbox/virtualization/migrations/0053_virtualmachine_config_context_data.py b/netbox/virtualization/migrations/0053_virtualmachine_config_context_data.py new file mode 100644 index 000000000..62fde9750 --- /dev/null +++ b/netbox/virtualization/migrations/0053_virtualmachine_config_context_data.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('virtualization', '0052_gfk_indexes'), + ] + + operations = [ + migrations.AddField( + model_name='virtualmachine', + name='config_context_data', + field=models.JSONField(blank=True, editable=False, null=True), + ), + ] diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 1fca15c5e..4cd91844f 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -487,7 +487,7 @@ class VirtualMachineVirtualDisksView(generic.ObjectChildrenView): @register_model_view(VirtualMachine, 'configcontext', path='config-context') class VirtualMachineConfigContextView(ObjectConfigContextView): - queryset = VirtualMachine.objects.annotate_config_context_data() + queryset = VirtualMachine.objects.all() base_template = 'virtualization/virtualmachine.html' tab = ViewTab( label=_('Config Context'),