Skip to content

Commit

Permalink
Send events to MQ when hosts are renamed.
Browse files Browse the repository at this point in the history
  - Captures the old name with pre_save.
  - Makes no assumtion on signal consistency in Django...

Fixes #476
  • Loading branch information
terjekv committed Jul 21, 2023
1 parent 3680b7d commit 954920d
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 61 deletions.
197 changes: 137 additions & 60 deletions mreg/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
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
Expand All @@ -27,8 +33,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():
Expand All @@ -40,20 +46,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()
Expand All @@ -63,39 +71,39 @@ 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.
qs = Ipaddress.objects.filter(ipaddress=instance.ipaddress)
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)
Expand All @@ -108,7 +116,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
Expand All @@ -123,8 +131,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):
Expand All @@ -147,7 +160,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)


Expand Down Expand Up @@ -175,7 +190,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
"""
Expand All @@ -199,38 +216,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)
Expand All @@ -254,52 +280,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()
Expand All @@ -308,37 +343,79 @@ 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")


@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")
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ requires = [
]

[tool.setuptools]
py-modules = ["mreg", "mregsite", "hostpolicy"]
py-modules = ["mreg", "mregsite", "hostpolicy"]

0 comments on commit 954920d

Please sign in to comment.