#!/bin/sh
#
# Synology DSM - tarea programada "Script definido por el usuario".
# Pegar este contenido completo en DSM y rellenar INTRANET_INVENTARIO_TOKEN.
# Usuario recomendado: root.

INTRANET_INVENTARIO_URL="https://10.100.21.46/intranet/apps/inventario/api.php"
INTRANET_INVENTARIO_TOKEN="PEGA_AQUI_EL_TOKEN"
INTRANET_INVENTARIO_IGNORE_TLS_ERRORS="1"
INTRANET_INVENTARIO_SKIP_SOFTWARE="1"
INTRANET_INVENTARIO_TIMEOUT="60"

export INTRANET_INVENTARIO_URL
export INTRANET_INVENTARIO_TOKEN
export INTRANET_INVENTARIO_IGNORE_TLS_ERRORS
export INTRANET_INVENTARIO_SKIP_SOFTWARE
export INTRANET_INVENTARIO_TIMEOUT

if command -v python3 >/dev/null 2>&1; then
    PYTHON_BIN="$(command -v python3)"
elif [ -x /usr/local/bin/python3 ]; then
    PYTHON_BIN="/usr/local/bin/python3"
elif [ -x /bin/python3 ]; then
    PYTHON_BIN="/bin/python3"
else
    echo "python3 no esta disponible. Instala el paquete Python 3 en Synology DSM." >&2
    exit 1
fi

"$PYTHON_BIN" - <<'PY'
import json
import os
import platform
import pwd
import shutil
import socket
import ssl
import subprocess
import time
import urllib.error
import urllib.request
from pathlib import Path


def env_bool(name):
    return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "si", "on"}


ENDPOINT = os.environ.get("INTRANET_INVENTARIO_URL", "").strip()
TOKEN = os.environ.get("INTRANET_INVENTARIO_TOKEN", "").strip()
SKIP_SOFTWARE = env_bool("INTRANET_INVENTARIO_SKIP_SOFTWARE")
IGNORE_TLS = env_bool("INTRANET_INVENTARIO_IGNORE_TLS_ERRORS")
TIMEOUT = int(os.environ.get("INTRANET_INVENTARIO_TIMEOUT", "60") or "60")

if not ENDPOINT:
    raise SystemExit("Configura INTRANET_INVENTARIO_URL.")
if not TOKEN or TOKEN == "PEGA_AQUI_EL_TOKEN":
    raise SystemExit("Configura INTRANET_INVENTARIO_TOKEN.")


def read_text(path):
    try:
        return Path(path).read_text(encoding="utf-8", errors="ignore").strip()
    except OSError:
        return ""


def run(args, timeout=6):
    try:
        completed = subprocess.run(args, check=False, capture_output=True, text=True, timeout=timeout)
        if completed.returncode != 0:
            return ""
        return completed.stdout.strip()
    except (OSError, subprocess.SubprocessError):
        return ""


def bytes_to_gb(value):
    try:
        return round(float(value) / (1024 ** 3), 2)
    except (TypeError, ValueError):
        return None


def mem_gb():
    for line in read_text("/proc/meminfo").splitlines():
        if line.startswith("MemTotal:"):
            parts = line.split()
            if len(parts) >= 2 and parts[1].isdigit():
                return round(int(parts[1]) * 1024 / (1024 ** 3), 2)
    return None


def disk_payload():
    disks = []
    seen = set()
    for mount in ["/volume1", "/volume2", "/volume3", "/"]:
        if mount in seen or not Path(mount).exists():
            continue
        seen.add(mount)
        try:
            usage = shutil.disk_usage(mount)
        except OSError:
            continue
        disks.append({
            "device_id": mount,
            "volume_name": mount,
            "total_gb": bytes_to_gb(usage.total),
            "free_gb": bytes_to_gb(usage.free),
        })
    return disks or [{
        "device_id": "/",
        "volume_name": "root",
        "total_gb": None,
        "free_gb": None,
    }]


def synology_info():
    return {
        "model": read_text("/proc/sys/kernel/syno_hw_version") or read_text("/proc/sys/kernel/syno_hw_revision"),
        "serial": read_text("/proc/sys/kernel/syno_serial"),
        "version": read_text("/etc.defaults/VERSION") or read_text("/etc/VERSION"),
    }


def synology_version_label(raw):
    data = {}
    for line in raw.splitlines():
        if "=" not in line:
            continue
        key, value = line.split("=", 1)
        data[key.strip()] = value.strip().strip('"')
    product = data.get("productversion")
    build = data.get("buildnumber")
    smallfix = data.get("smallfixnumber")
    if product and build:
        suffix = f"-{smallfix}" if smallfix and smallfix != "0" else ""
        return f"DSM {product}-{build}{suffix}"
    return "Synology DSM"


def cpu_name():
    for line in read_text("/proc/cpuinfo").splitlines():
        if "model name" in line:
            return line.split(":", 1)[1].strip()
        if line.lower().startswith("hardware"):
            return line.split(":", 1)[1].strip()
    return platform.processor() or None


def ethtool_wol(iface):
    if not shutil.which("ethtool"):
        return "Desconocido"
    output = run(["ethtool", iface])
    for line in output.splitlines():
        stripped = line.strip()
        if stripped.lower().startswith("wake-on:"):
            value = stripped.split(":", 1)[1].strip().lower()
            return "Enabled" if "g" in value else "Disabled"
    return "Desconocido"


def network_inventory():
    adapters = []
    ip_json = run(["ip", "-j", "addr", "show"])
    if ip_json:
        try:
            entries = json.loads(ip_json)
        except json.JSONDecodeError:
            entries = []
        for item in entries:
            iface = str(item.get("ifname") or "")
            if not iface or iface == "lo":
                continue
            mac = read_text(f"/sys/class/net/{iface}/address")
            if not mac or mac == "00:00:00:00:00:00":
                continue
            ipv4 = []
            ipv6 = []
            for addr in item.get("addr_info") or []:
                if addr.get("family") == "inet" and addr.get("local"):
                    ipv4.append(str(addr["local"]))
                elif addr.get("family") == "inet6" and addr.get("local"):
                    ipv6.append(str(addr["local"]))
            adapters.append({
                "name": iface,
                "interface_description": iface,
                "status": read_text(f"/sys/class/net/{iface}/operstate") or "",
                "mac_address": mac.upper(),
                "link_speed": "",
                "ipv4": ipv4,
                "ipv6": ipv6,
                "wake_on_magic_packet": ethtool_wol(iface),
            })
        return adapters

    for net_path in Path("/sys/class/net").glob("*"):
        iface = net_path.name
        if iface == "lo":
            continue
        mac = read_text(net_path / "address")
        if not mac or mac == "00:00:00:00:00:00":
            continue
        adapters.append({
            "name": iface,
            "interface_description": iface,
            "status": read_text(net_path / "operstate") or "",
            "mac_address": mac.upper(),
            "link_speed": "",
            "ipv4": [],
            "ipv6": [],
            "wake_on_magic_packet": ethtool_wol(iface),
        })
    return adapters


def installed_software():
    if SKIP_SOFTWARE:
        return []
    items = []
    if shutil.which("synopkg"):
        output = run(["synopkg", "list"], timeout=20)
        for line in output.splitlines():
            parts = line.split()
            if parts:
                items.append({
                    "name": parts[0],
                    "version": parts[1] if len(parts) > 1 else None,
                    "publisher": "Synology",
                    "install_date": None,
                    "uninstall_key": parts[0],
                })
    return items[:2500]


def current_user():
    try:
        return pwd.getpwuid(os.getuid()).pw_name
    except KeyError:
        return os.environ.get("USER") or None


syno = synology_info()
caption = synology_version_label(syno["version"])
hostname = socket.gethostname()
payload = {
    "inventory_version": "linux-1.0",
    "collected_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
    "computer": {
        "name": hostname,
        "domain": socket.getfqdn().split(".", 1)[1] if "." in socket.getfqdn() else None,
        "manufacturer": "Synology",
        "model": syno["model"] or None,
        "serial_number": syno["serial"] or None,
        "bios_version": None,
    },
    "os": {
        "caption": caption,
        "version": caption.replace("DSM ", "", 1) if caption.startswith("DSM ") else None,
        "build": platform.release(),
        "architecture": platform.machine(),
        "install_date": None,
        "last_boot_up_time": None,
    },
    "hardware": {
        "processor": cpu_name(),
        "ram_gb": mem_gb(),
        "disks": disk_payload(),
    },
    "users": {
        "current_user": current_user(),
        "last_logged_on": current_user(),
        "previous_domain_users": [],
    },
    "network": network_inventory(),
    "software": installed_software(),
}

body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
request = urllib.request.Request(
    ENDPOINT,
    data=body,
    headers={
        "Content-Type": "application/json; charset=utf-8",
        "X-Inventory-Token": TOKEN,
        "X-Inventory-Agent": "intranet-inventory-synology-task/1.0",
    },
    method="POST",
)
context = ssl._create_unverified_context() if IGNORE_TLS else None

try:
    with urllib.request.urlopen(request, timeout=TIMEOUT, context=context) as response:
        raw = response.read().decode("utf-8", errors="replace")
except urllib.error.HTTPError as exc:
    detail = exc.read().decode("utf-8", errors="replace")
    raise SystemExit(f"No se pudo enviar el inventario: HTTP {exc.code} {detail}")
except urllib.error.URLError as exc:
    raise SystemExit(f"No se pudo enviar el inventario: {exc.reason}")

try:
    parsed = json.loads(raw)
except json.JSONDecodeError:
    raise SystemExit(f"Respuesta no JSON de la intranet: {raw[:300]}")

if not parsed.get("ok"):
    raise SystemExit(f"La intranet rechazo el inventario: {parsed.get('error') or raw[:300]}")

print(f"Inventario enviado: {parsed.get('equipo', hostname)}.")
PY
