Skip to content
Snippets Groups Projects
synchronization.py 10.6 KiB
Newer Older
Anders Harrisson's avatar
Anders Harrisson committed
from dcim.models import Device, DeviceRole, DeviceType, Site
from ipam.models import Prefix
from dcim.choices import DeviceStatusChoices
from virtualization.choices import VirtualMachineStatusChoices
from virtualization.models import VirtualMachine
Anders Harrisson's avatar
Anders Harrisson committed
from rest_framework import serializers
import json
import logging
from core.models import Job
from .models import AWXInventory

logger = logging.getLogger(__name__)

group_prefixes = {Site: "site_", DeviceRole: "devicerole_", DeviceType: "devicetype_", Prefix: "prefix_", Tag:"tag_"}
class TagSerializer(serializers.BaseSerializer):
    def to_representation(self, instance):
        return {
            "name": "{}{}".format(group_prefixes[Tag], instance.slug.replace("-", "_")),
            "variables": json.dumps({"netbox_tag_name": instance.name}),
        }

Anders Harrisson's avatar
Anders Harrisson committed
class SiteSerializer(serializers.BaseSerializer):
    def to_representation(self, instance):
        return {
            "name": "{}{}".format(group_prefixes[Site], instance.slug.replace("-", "_")),
            "variables": json.dumps(
                {
                    "netbox_site_status": instance.status,
                }
            ),
        }


class DeviceRoleSerializer(serializers.BaseSerializer):
    def to_representation(self, instance):
        return {
            "name": "{}{}".format(group_prefixes[DeviceRole], instance.slug.replace("-", "_")),
            "variables": json.dumps({})
        }


class DeviceTypeSerializer(serializers.BaseSerializer):
    def to_representation(self, instance):
        return {
            "name": "{}{}".format(group_prefixes[DeviceType], instance.slug.replace("-", "_")),
            "description": instance.description,
Anders Harrisson's avatar
Anders Harrisson committed
            "variables": json.dumps(
                {
                    "netbox_devicetype_model": instance.model,
                }
            ),
        }


class PrefixSerializer(serializers.BaseSerializer):
    def to_representation(self, instance):
        return {
            "name": "{}{}".format(group_prefixes[Prefix], str(instance.prefix).replace(".", "_").replace("/", "_")),
            "variables": json.dumps({"netbox_prefix": "{}".format(str(instance.prefix))}),
        }


class InterfaceSerializer(serializers.BaseSerializer):
    def to_representation(self, instance):
        ip_addresses = []
        for ip in instance.ip_addresses.all():
            serializer = IPAddressSerializer(ip)
            ip_addresses.append(serializer.data)
        return {
            "name": instance.name,
            "mac": instance.mac_address,
            "ip_addresses": ip_addresses
        }


class IPAddressSerializer(serializers.BaseSerializer):
    def to_representation(self, instance):
        return {
            "address": str(instance.address),
            "dns_name": instance.dns_name
        }


class DeviceSerializer(serializers.BaseSerializer):
    def to_representation(self, instance):
        variables = {}
        variables["netbox_interfaces"] = []
        for interface in instance.interfaces.all():
            serializer = InterfaceSerializer(interface)
            variables["netbox_interfaces"].append(serializer.data)
        return {
            "name": getattr(instance.primary_ip4, 'dns_name', instance.name),
Anders Harrisson's avatar
Anders Harrisson committed
            "description": instance.description,
            "enabled": instance.status == DeviceStatusChoices.STATUS_ACTIVE,
            "variables": json.dumps(variables),
        }


class VMInterfaceSerializer(serializers.BaseSerializer):
    def to_representation(self, instance):
        ip_addresses = []
        for ip in instance.ip_addresses.all():
            serializer = IPAddressSerializer(ip)
            ip_addresses.append(serializer.data)
        return {
            "name": instance.name,
            "mac": instance.mac_address,
            "ip_addresses": ip_addresses
        }


class VMSerializer(serializers.BaseSerializer):
    def to_representation(self, instance):
        variables = {
            "netbox_virtualmachine_name": instance.name,
            "netbox_virtualmachine_vcpus": float(instance.vcpus) if instance.vcpus is not None else 0.0,
            "netbox_virtualmachine_memory": instance.memory or 0,
            "netbox_virtualmachine_disk": instance.disk or 0,
        }
        variables["netbox_interfaces"] = []
        for interface in instance.interfaces.all():
            serializer = VMInterfaceSerializer(interface)
            variables["netbox_interfaces"].append(serializer.data)
        return {
            "name": getattr(instance.primary_ip4, 'dns_name', instance.name),
            "description": instance.description,
            "enabled": instance.status == VirtualMachineStatusChoices.STATUS_ACTIVE,
            "variables": json.dumps(variables),
        }


Anders Harrisson's avatar
Anders Harrisson committed
class AWXHostSerializer(serializers.BaseSerializer):
    def to_internal_value(self, data):
        return {
            "name": data.get('name'),
            "description": data.get('description'),
            "enabled": data.get('enabled'),
            "variables": data.get('variables')
        }


class AWXGroupSerializer(serializers.BaseSerializer):
    def to_internal_value(self, data):
        return {
            "name": data.get('name'),
            "variables": data.get('variables')
        }


serializers = {
    Site: SiteSerializer,
    DeviceRole: DeviceRoleSerializer,
    DeviceType: DeviceTypeSerializer,
    Prefix: PrefixSerializer,
    Device: DeviceSerializer,
    VirtualMachine: VMSerializer,
Anders Harrisson's avatar
Anders Harrisson committed
}


def sync_host(inventory, sender, instance):
    serializer = serializers[sender](instance)
Fahrudin Halilovic's avatar
Fahrudin Halilovic committed
    host = inventory.get_host(serializer.data["name"])
Anders Harrisson's avatar
Anders Harrisson committed
    if host is None:
Anders Harrisson's avatar
Anders Harrisson committed
        inventory.create_host(serializer.data)
        host = inventory.get_host(serializer.data["name"])
    else:
        # If the host exists, update it if necessary.
Anders Harrisson's avatar
Anders Harrisson committed
        host_serializer = AWXHostSerializer(data=host)
        if not host_serializer.is_valid() or host_serializer.validated_data != serializer.data:
            inventory.update_host(host["id"], serializer.data)
    
    # Sync group associations
    current_groups = host["summary_fields"]["groups"]["results"]
    
    # Handle associations for each type (Site, DeviceRole, DeviceType, and Tags)
        sync_host_group_association(inventory, host, Site, instance.site, current_groups)
        sync_host_group_association(inventory, host, DeviceRole, instance.role, current_groups)
    if isinstance(instance, Device) and instance.device_type:
        sync_host_group_association(inventory, host, DeviceType, instance.device_type, current_groups)
    if tags := instance.tags.all():
        for tag in tags:
            sync_host_group_association(inventory, host, Tag, tag, current_groups)
Anders Harrisson's avatar
Anders Harrisson committed

    # Disassociate groups that are no longer relevant
    disassociate_removed_groups(inventory, host, instance, current_groups)
def sync_host_group_association(inventory, host, object_type, instance, current_groups):
    """
    Associates the host with the appropriate AWX group.
    """
Anders Harrisson's avatar
Anders Harrisson committed
    serializer = serializers[object_type](instance)
    group_name = serializer.data["name"]
    
    # Filter out the groups that match the current object_type (e.g., Site, Tag)
    relevant_groups = [
        group for group in current_groups if group["name"].startswith(group_prefixes[object_type])
Anders Harrisson's avatar
Anders Harrisson committed
    ]
    
    # Check if the host is already in the desired group
    if group_name not in [group["name"] for group in relevant_groups]:
Anders Harrisson's avatar
Anders Harrisson committed
        group = inventory.get_group(group_name)
        if group:
            inventory.associate_host_group(host["id"], group["id"])
            logger.info(f"Host {host['name']} associated with group {group_name}.")


def disassociate_removed_groups(inventory, host, instance, current_groups):
    """
    Disassociates the host from AWX groups that are no longer relevant.
    """
    # Collect the current groups that should remain (based on the instance's attributes)
    valid_group_names = set()
    
    if instance.site:
        valid_group_names.add(f"{group_prefixes[Site]}{instance.site.slug.replace('-', '_')}")
    if instance.role:
        valid_group_names.add(f"{group_prefixes[DeviceRole]}{instance.role.slug.replace('-', '_')}")
    if isinstance(instance, Device) and instance.device_type:
        valid_group_names.add(f"{group_prefixes[DeviceType]}{instance.device_type.slug.replace('-', '_')}")
    if tags := instance.tags.all():
        valid_group_names.update(
            f"{group_prefixes[Tag]}{tag.slug.replace('-', '_')}" for tag in tags
        )
    # Disassociate from groups that are no longer valid
    for group in current_groups:
        if group["name"] not in valid_group_names:
            inventory.disassociate_host_group(host["id"], group["id"])
            logger.info(f"Host {host['name']} disassociated from group {group['name']}.")
Anders Harrisson's avatar
Anders Harrisson committed

def delete_host(inventory, sender, instance):
    serializer = serializers[sender](instance)
    inventory.delete_host(serializer.data["name"])


def sync_group(inventory, sender, instance):
    serializer = serializers[sender](instance)
    group = inventory.get_group(serializer.data["name"])
    if group is None:
        inventory.create_group(serializer.data)
        group = inventory.get_group(serializer.data["name"])
    else:
        group_serializer = AWXGroupSerializer(data=group)
        if not group_serializer.is_valid() or group_serializer.validated_data != serializer.data:
            print("Group will be updated due to:")
            print(group_serializer.validated_data)
            print(serializer.data)
            inventory.update_group(serializer.data["name"], serializer.data)


def delete_group(inventory, sender, instance):
    serializer = serializers[sender](instance)
    inventory.delete_group(serializer.data["name"])


def sync_all(job):
    inventory = job.object
    logger.info("Performing full inventory sync for inventory {}".format(inventory.inventory_id))
    job.start()
    for site in Site.objects.all():
        sync_group(inventory, Site, site)
    for device_role in DeviceRole.objects.all():
        sync_group(inventory, DeviceRole, device_role)
    for device_type in DeviceType.objects.all():
        sync_group(inventory, DeviceType, device_type)
    for tag in Tag.objects.all():
        sync_group(inventory, Tag, tag)

Anders Harrisson's avatar
Anders Harrisson committed
    for device in Device.objects.all():
        if not device.primary_ip4 is None and device.primary_ip4.dns_name:
            sync_host(inventory, Device, device)
    for vm in VirtualMachine.objects.all():
        if not vm.primary_ip4 is None and vm.primary_ip4.dns_name:
            sync_host(inventory, VirtualMachine, vm)
Anders Harrisson's avatar
Anders Harrisson committed
    job.terminate()