← Volver al blog

Automatización de inventario IT con Snipe-IT y sincronización con Active Directory

Guía técnica para implementar un sistema de gestión de inventario IT centralizado mediante Snipe-IT con sincronización automática desde Active Directory, reduciendo carga operativa y mejorando precisión de datos.

Introducción

En entornos corporativos medianos y grandes, mantener un inventario IT preciso y actualizado es una tarea crítica que consume recursos significativos si se gestiona manualmente. Snipe-IT, como solución de Asset Management de código abierto, ofrece capacidades robustas para centralizar este control. Sin embargo, su verdadero potencial se desbloquea cuando se integra con Active Directory mediante scripts de sincronización automática, eliminando redundancia y garantizando consistencia de datos.

Este artículo aborda la implementación práctica de esta integración en entornos de producción, considerando desafíos reales como sincronización bidireccional parcial, manejo de conflictos de datos y auditoría de cambios.

Arquitectura de la solución

Componentes clave

La arquitectura propuesta consta de tres capas:

Capa de datos: Active Directory como fuente de verdad para identidades de usuario y pertenencia a departamentos. Snipe-IT como repositorio centralizado de assets físicos, licencias y ubicaciones.

Capa de integración: Scripts de Python o PowerShell ejecutados en servidor dedicado, responsables de la sincronización programada y transformación de datos entre sistemas.

Capa de auditoría: Logs centralizados y webhooks que registran cambios, facilitando troubleshooting y compliance.

Flujo de sincronización

La sincronización típica funciona de manera unidireccional desde AD hacia Snipe-IT: los usuarios creados en AD se replican automáticamente, sus atributos (departamento, ubicación, teléfono) se mapean a campos en Snipe-IT, y se establecen asignaciones de equipos basadas en grupos de seguridad.

Implementación con PowerShell

Para entornos Windows dominados por Active Directory, PowerShell es la opción más práctica. A continuación, un script de sincronización funcional:

# Script: Sync-ADtoSnipeIT.ps1
# Requisitos: módulo ActiveDirectory, acceso API Snipe-IT

param(
    [string]$SnipeITUrl = "https://snipe-it.empresa.local",
    [string]$ApiToken = $env:SNIPE_API_TOKEN,
    [string]$SearchBase = "OU=Usuarios,DC=empresa,DC=local"
)

# Función para obtener usuarios de AD
function Get-ADUsersForSnipe {
    param([string]$BasePath)
    
    $users = Get-ADUser -Filter * -SearchBase $BasePath -Properties `
        Department, Location, Title, EmailAddress, Telephone | 
        Select-Object SamAccountName, Name, Department, Location, Title, EmailAddress, Telephone
    
    return $users
}

# Función para sincronizar en Snipe-IT
function Sync-UserToSnipeIT {
    param(
        [object]$ADUser,
        [string]$ApiUrl,
        [string]$Token
    )
    
    $headers = @{
        "Authorization" = "Bearer $Token"
        "Content-Type" = "application/json"
        "Accept" = "application/json"
    }
    
    $snipeUser = @{
        first_name = ($ADUser.Name -split '\s')[0]
        last_name = ($ADUser.Name -split '\s', 2)[1]
        username = $ADUser.SamAccountName
        email = $ADUser.EmailAddress
        department_id = (Get-SnipeDepartmentId -Department $ADUser.Department -ApiUrl $ApiUrl -Token $Token)
        location_id = (Get-SnipeLocationId -Location $ADUser.Location -ApiUrl $ApiUrl -Token $Token)
    }
    
    try {
        $uri = "$ApiUrl/api/v1/users"
        $existingUser = Invoke-RestMethod -Uri "$uri?search=$($ADUser.SamAccountName)" `
            -Headers $headers -Method GET
        
        if ($existingUser.total -eq 0) {
            # Crear usuario nuevo
            Invoke-RestMethod -Uri $uri -Headers $headers -Method POST `
                -Body ($snipeUser | ConvertTo-Json)
            Write-Host "Usuario creado: $($ADUser.SamAccountName)"
        } else {
            # Actualizar usuario existente
            $userId = $existingUser.rows[0].id
            Invoke-RestMethod -Uri "$uri/$userId" -Headers $headers -Method PATCH `
                -Body ($snipeUser | ConvertTo-Json)
            Write-Host "Usuario actualizado: $($ADUser.SamAccountName)"
        }
    } catch {
        Write-Error "Error sincronizando $($ADUser.SamAccountName): $_"
    }
}

# Ejecución principal
$adUsers = Get-ADUsersForSnipe -BasePath $SearchBase
$adUsers | ForEach-Object {
    Sync-UserToSnipeIT -ADUser $_ -ApiUrl $SnipeITUrl -Token $ApiToken
}

Write-Host "Sincronización completada en $(Get-Date)"

Implementación con Python

Para entornos heterogéneos o mayor flexibilidad, Python con librerías como ldap3 y requests proporciona control fino:

#!/usr/bin/env python3
# Script: sync_ad_to_snipeip.py
# pip install ldap3 requests python-dotenv

import ldap3
import requests
import json
import logging
from datetime import datetime
from dotenv import load_dotenv
import os

load_dotenv()

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

class ADSnipeSynchronizer:
    def __init__(self):
        self.ad_server = os.getenv('AD_SERVER')
        self.ad_user = os.getenv('AD_USER')
        self.ad_password = os.getenv('AD_PASSWORD')
        self.snipe_url = os.getenv('SNIPE_URL')
        self.snipe_token = os.getenv('SNIPE_TOKEN')
        self.headers = {
            'Authorization': f'Bearer {self.snipe_token}',
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        }
    
    def connect_ad(self):
        """Establece conexión a Active Directory"""
        server = ldap3.Server(self.ad_server, get_info=ldap3.ALL)
        self.conn = ldap3.Connection(
            server, 
            user=self.ad_user, 
            password=self.ad_password,
            auto_bind=True
        )
        logger.info("Conexión AD establecida")
    
    def get_ad_users(self, search_base):
        """Obtiene usuarios de AD con filtro"""
        search_filter = '(&(objectClass=user)(mail=*))'
        self.conn.search(
            search_base=search_base,
            search_filter=search_filter,
            attributes=['sAMAccountName', 'givenName', 'sn', 'mail', 
                       'department', 'physicalDeliveryOfficeName', 'telephoneNumber']
        )
        return self.conn.entries
    
    def sync_users(self, search_base):
        """Sincroniza usuarios de AD a