From 9471f33f2604e9db7b7930514d1e8e1108093910 Mon Sep 17 00:00:00 2001 From: Terje Kvernes Date: Sun, 28 May 2023 17:02:36 +0200 Subject: [PATCH] Send events to MQ when hosts are renamed. - Captures the old name with pre_save. - Makes no assumtion on signal consistency in Django... Fixes #476 --- mreg/signals.py | 222 ++++++++++++++++++++++++++++++++++-------------- pyproject.toml | 17 +++- 2 files changed, 174 insertions(+), 65 deletions(-) diff --git a/mreg/signals.py b/mreg/signals.py index f9e120ba..8814749a 100644 --- a/mreg/signals.py +++ b/mreg/signals.py @@ -6,17 +6,40 @@ from django.contrib.auth.models import Group from django.core.exceptions import ValidationError from django.db import transaction -from django.db.models.signals import m2m_changed, post_delete, post_save, pre_delete, pre_save +from django.db.models.signals import ( + m2m_changed, + post_delete, + post_save, + pre_delete, + pre_save, +) from django.dispatch import receiver from django_auth_ldap.backend import populate_user from rest_framework.exceptions import PermissionDenied -from .models import (Cname, ForwardZoneMember, Hinfo, History, Host, HostGroup, - Ipaddress, Loc, Mx, NameServer, Naptr, - NetGroupRegexPermission, Network, PtrOverride, - ForwardZone, ReverseZone, Srv, Sshfp, Txt) +from .models import ( + Cname, + ForwardZoneMember, + Hinfo, + History, + Host, + HostGroup, + Ipaddress, + Loc, + Mx, + NameServer, + Naptr, + NetGroupRegexPermission, + Network, + PtrOverride, + ForwardZone, + ReverseZone, + Srv, + Sshfp, + Txt, +) from .mqsender import MQSender @@ -25,8 +48,8 @@ def populate_user_from_ldap(sender, signal, user=None, ldap_user=None, **kwargs) """Find all groups from ldap with attr LDAP_GROUP_ATTR and matching the regular expression LDAP_GROUP_RE. Will wipe previous group memberships before adding new.""" - LDAP_GROUP_ATTR = getattr(settings, 'LDAP_GROUP_ATTR', None) - LDAP_GROUP_RE = getattr(settings, 'LDAP_GROUP_RE', None) + LDAP_GROUP_ATTR = getattr(settings, "LDAP_GROUP_ATTR", None) + LDAP_GROUP_RE = getattr(settings, "LDAP_GROUP_RE", None) if LDAP_GROUP_ATTR is None or LDAP_GROUP_RE is None: return with transaction.atomic(): @@ -38,20 +61,22 @@ def populate_user_from_ldap(sender, signal, user=None, ldap_user=None, **kwargs) for group_str in ldap_groups: res = group_re.match(group_str) if res: - group_name = res.group('group_name') + group_name = res.group("group_name") group, created = Group.objects.get_or_create(name=group_name) user.groups.add(group) def _signal_history(resource, name, action, model, model_id, data): - user = 'system-signals' - history = History(user=user, - resource=resource, - name=name, - model_id=model_id, - model=model, - action=action, - data=data) + user = "system-signals" + history = History( + user=user, + resource=resource, + name=name, + model_id=model_id, + model=model, + action=action, + data=data, + ) try: history.full_clean() @@ -61,13 +86,14 @@ def _signal_history(resource, name, action, model, model_id, data): def _signal_host_history(host, action, model, data): - _signal_history('host', host.name, action, model, host.id, data) + _signal_history("host", host.name, action, model, host.id, data) # Update PtrOverride whenever a Ipaddress is created or changed @receiver(pre_save, sender=Ipaddress) -def updated_ipaddress_fix_ptroverride(sender, instance, raw, using, update_fields, **kwargs): - +def updated_ipaddress_fix_ptroverride( + sender, instance, raw, using, update_fields, **kwargs +): def _create_ptr_if_ipaddress_in_use(): # Can only add a PtrOverride if count == 1, otherwise we can not guess which # one should get it. @@ -75,25 +101,24 @@ def _create_ptr_if_ipaddress_in_use(): if qs and qs.count() == 1: host = qs.first().host if not PtrOverride.objects.filter(ipaddress=instance.ipaddress).exists(): - data = {'ipaddress': instance.ipaddress} + data = {"ipaddress": instance.ipaddress} PtrOverride.objects.create(host=host, **data) - _signal_host_history(host, 'create', 'PtrOverride', data) + _signal_host_history(host, "create", "PtrOverride", data) if instance.id: oldinstance = Ipaddress.objects.get(id=instance.id) if oldinstance.ipaddress != instance.ipaddress: - data = {'ipaddress': oldinstance.ipaddress} + data = {"ipaddress": oldinstance.ipaddress} qs = PtrOverride.objects.filter(host=instance.host, **data) if qs.exists(): qs.delete() - _signal_host_history(instance.host, 'destroy', 'PtrOverride', data) + _signal_host_history(instance.host, "destroy", "PtrOverride", data) _create_ptr_if_ipaddress_in_use() else: _create_ptr_if_ipaddress_in_use() def _common_update_zone(signal, sender, instance): - @functools.lru_cache() def _get_zone_for_ip(ip): return ReverseZone.get_zone_by_ip(ip) @@ -106,7 +131,7 @@ def _get_zone_for_ip(ip): oldzone = sender.objects.get(id=instance.id).zone zones.add(oldzone) - if hasattr(instance, 'host'): + if hasattr(instance, "host"): zones.add(instance.host.zone) if signal == "pre_save" and instance.host.id: oldzone = Host.objects.get(id=instance.host.id).zone @@ -121,8 +146,13 @@ def _get_zone_for_ip(ip): if signal == "pre_save" and sender == Host and instance.id: oldname = Host.objects.get(id=instance.id).name if instance.name != oldname: - for model in (Cname, Srv,): - for i in model.objects.filter(host=instance).exclude(zone=instance.zone): + for model in ( + Cname, + Srv, + ): + for i in model.objects.filter(host=instance).exclude( + zone=instance.zone + ): zones.add(i.zone) for model in (Ipaddress, PtrOverride): for i in model.objects.filter(host=instance): @@ -145,7 +175,9 @@ def _get_zone_for_ip(ip): @receiver(pre_save, sender=Srv) @receiver(pre_save, sender=Sshfp) @receiver(pre_save, sender=Txt) -def updated_objects_update_zone_serial(sender, instance, raw, using, update_fields, **kwargs): +def updated_objects_update_zone_serial( + sender, instance, raw, using, update_fields, **kwargs +): _common_update_zone("pre_save", sender, instance) @@ -173,7 +205,9 @@ def _host_update_m2m_relations(instance): @receiver(pre_save, sender=Host) -def host_update_m2m_relations_on_rename(sender, instance, raw, using, update_fields, **kwargs): +def host_update_m2m_relations_on_rename( + sender, instance, raw, using, update_fields, **kwargs +): """ Update hostgroup and hostpolicy on host rename """ @@ -197,38 +231,47 @@ def host_update_m2m_relations_on_delete(sender, instance, using, **kwargs): @receiver(m2m_changed, sender=HostGroup.hosts.through) @receiver(m2m_changed, sender=HostGroup.parent.through) -def hostgroup_update_updated_at_on_changes(sender, instance, action, model, - reverse, pk_set, **kwargs): +def hostgroup_update_updated_at_on_changes( + sender, instance, action, model, reverse, pk_set, **kwargs +): """ Update the hostgroups updated_at field whenever its hosts or parent m2m relations have successfully been altered. """ - if action in ('post_add', 'post_remove', 'post_clear',): + if action in ( + "post_add", + "post_remove", + "post_clear", + ): instance.save() @receiver(m2m_changed, sender=HostGroup.parent.through) -def prevent_hostgroup_parent_recursion(sender, instance, action, model, - reverse, pk_set, **kwargs): +def prevent_hostgroup_parent_recursion( + sender, instance, action, model, reverse, pk_set, **kwargs +): """ pk_set contains the group(s) being added to a group instance is the group getting new group members This prevents groups from being able to become their own parent """ - if action != 'pre_add': + if action != "pre_add": return if instance.id in pk_set: - raise PermissionDenied(detail='A group can not be its own child') + raise PermissionDenied(detail="A group can not be its own child") for parent in instance.parent.all(): if parent.id in pk_set: - raise PermissionDenied(detail='Recursive memberships are not allowed.' - ' This group is a member of %s' % parent.name) + raise PermissionDenied( + detail="Recursive memberships are not allowed." + " This group is a member of %s" % parent.name + ) elif parent.parent.exists(): - prevent_hostgroup_parent_recursion(sender, parent, action, model, - reverse, pk_set, **kwargs) + prevent_hostgroup_parent_recursion( + sender, parent, action, model, reverse, pk_set, **kwargs + ) @receiver(pre_delete, sender=Ipaddress) @@ -252,52 +295,61 @@ def prevent_nameserver_deletion(sender, instance, using, **kwargs): return zones = list() - for i in ('forwardzone', 'reversezone', 'forwardzonedelegation', - 'reversezonedelegation'): + for i in ( + "forwardzone", + "reversezone", + "forwardzonedelegation", + "reversezonedelegation", + ): qs = getattr(nameserver, f"{i}_set") if qs.exists(): - zones.append([i, list(qs.values_list('name', flat=True))]) + zones.append([i, list(qs.values_list("name", flat=True))]) if zones: if sender == Ipaddress: - raise PermissionDenied(f'IP {instance.ipaddress} is the only IP for host {name} ' - f'that is used as nameserver in {zones}') - raise PermissionDenied(detail=f'Host {name} is a nameserver in {zones} and cannot ' - 'be deleted until it is removed from them.') + raise PermissionDenied( + f"IP {instance.ipaddress} is the only IP for host {name} " + f"that is used as nameserver in {zones}" + ) + raise PermissionDenied( + detail=f"Host {name} is a nameserver in {zones} and cannot " + "be deleted until it is removed from them." + ) @receiver(post_delete, sender=Network) def cleanup_network_permissions(sender, instance, **kwargs): """Remove any permissions equal to or smaller than the newly deleted - Network's network range.""" + Network's network range.""" NetGroupRegexPermission.objects.filter( - range__net_contained_or_equal=instance.network).delete() + range__net_contained_or_equal=instance.network + ).delete() @receiver(post_save, sender=Host) def add_auto_txt_records_on_new_host(sender, instance, created, **kwargs): """Create TXT record(s) for a host if the host's zone defines - records in settings.TXT_AUTO_RECORDS.""" + records in settings.TXT_AUTO_RECORDS.""" if created: - autozones = getattr(settings, 'TXT_AUTO_RECORDS', None) + autozones = getattr(settings, "TXT_AUTO_RECORDS", None) if autozones is None: return if instance.zone is None: return for data in autozones.get(instance.zone.name, []): Txt.objects.create(host=instance, txt=data) - _signal_host_history(instance, 'create', 'Txt', {'txt': data}) + _signal_host_history(instance, "create", "Txt", {"txt": data}) @receiver(post_save, sender=ForwardZone) def update_hosts_when_zone_is_added(sender, instance, created, **kwargs): """When a zone is created, any existing hosts that would be in that zone - must be updated.""" + must be updated.""" if created: zonename = "." + instance.name for h in Host.objects.filter(name__endswith=zonename): # The filter will also match hosts in sub-zones, so we must check for that. - if "." in h.name[0:-len(zonename)]: + if "." in h.name[0 : -len(zonename)]: continue h.zone = instance h.save() @@ -306,9 +358,9 @@ def update_hosts_when_zone_is_added(sender, instance, created, **kwargs): @receiver(post_delete, sender=Ipaddress) def send_event_ip_removed_from_host(sender, instance, **kwargs): obj = { - 'host': instance.host.name, - 'ipaddress': instance.ipaddress, - 'action': 'remove_ip_from_host', + "host": instance.host.name, + "ipaddress": instance.ipaddress, + "action": "remove_ip_from_host", } MQSender().send_event(obj, "host.ipaddress") @@ -316,27 +368,69 @@ def send_event_ip_removed_from_host(sender, instance, **kwargs): @receiver(post_save, sender=Ipaddress) def send_event_ip_added_to_host(sender, instance, created, **kwargs): obj = { - 'host': instance.host.name, - 'ipaddress': instance.ipaddress, - 'action': 'add_ip_to_host', + "host": instance.host.name, + "ipaddress": instance.ipaddress, + "action": "add_ip_to_host", } MQSender().send_event(obj, "host.ipaddress") +# In case of host rename, we need to know the old name, so we +# capture it here and store it in the instance. Note that we first +# try to get the object in its original state from the database, +# and if it does not exist there, we set the old name to None as +# we are being called as part of an object creation. +@receiver(pre_save, sender=Host) +def capture_old_name(sender, instance, **kwargs): + try: + obj = sender.objects.get(pk=instance.pk) + instance._old_name = obj.name + except sender.DoesNotExist: + instance._old_name = None + + +# Process host events, and send them to the message queue. +# If the hostname itself is changed, send a host_updated +# event with both the old and new hostname. +# The old hostname is captured in the pre_save signal above. +# Also note that _old_name is not a field in the model, so it +# will not be saved to the database. @receiver(post_save, sender=Host) def send_event_host_created(sender, instance, created, **kwargs): if created: obj = { - 'host': instance.name, - 'action': 'host_created', + "host": instance.name, + "action": "host_created", } MQSender().send_event(obj, "host") + else: + # There are situations in Django where singals do not + # complete correctly, or pre_save isn't triggered at + # all. In these cases, _old_name will not be set, but + # the created boolean sent to post_save will still be + # false. + # To handle such eventualities, we don't simply assume + # that old_name is set during updates. The best we can + # do in these cases is to send a host_updated event. + old_name = getattr(instance, "_old_name", None) + if old_name is not None and old_name != instance.name: + obj = { + "old_host": old_name, + "new_host": instance.name, + "action": "host_updated", + } + else: + obj = { + "host": instance.name, + "action": "host_updated", + } + MQSender().send_event(obj, "host") @receiver(post_delete, sender=Host) def send_event_host_removed(sender, instance, **kwargs): obj = { - 'host': instance.name, - 'action': 'host_removed', + "host": instance.name, + "action": "host_removed", } MQSender().send_event(obj, "host") diff --git a/pyproject.toml b/pyproject.toml index 226cc84f..b090b1a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,4 +6,19 @@ exclude = [ "mreg/migrations/", "hostpolicy/migrations/", ".tox", -] \ No newline at end of file +] + +[project] +name = "mreg" +version = "0.0.1" + +[build-system] +build-backend = "setuptools.build_meta" +requires = [ + "setuptools >= 46.1.0", + "wheel", + "toml" +] + +[tool.setuptools] +py-modules = ["mreg", "mregsite", "hostpolicy"] \ No newline at end of file