Загрузка данных


#!/usr/bin/env python3
"""
Find active assets from Host-AC.xlsx that are missing from audit.xlsx.

Usage:
  python3 compare_missing_assets.py \
    "VMName Hypervisor ДОМ/Банк VMState Hostname IPAddress OS VMHost COD CreateDate VMwareToolsState CMDB_ENV CMDB_CRIT - Host-AC.xlsx" \
    "@Host Host.@UpdateTime Host.Fqdn Host.IpAddress - audit.xlsx" \
    missing_assets.xlsx
"""

from __future__ import annotations

import argparse
import ipaddress
from pathlib import Path

import pandas as pd


HOST_AC_COLUMNS = {
    "vm_name": "VMName",
    "vm_state": "VMState",
    "hostname": "Hostname",
    "ip": "IPAddress",
}

AUDIT_COLUMNS = {
    "host": "@Host",
    "fqdn": "Host.Fqdn",
    "ip": "Host.IpAddress",
}

ACTIVE_STATES = {"active", "poweredon", "powered on", "running", "включена", "включен"}


def normalize_host(value: object) -> str:
    if pd.isna(value):
        return ""
    text = str(value).strip().lower().rstrip(".")
    if not text:
        return ""
    return text.split(".", 1)[0]


def normalize_fqdn(value: object) -> str:
    if pd.isna(value):
        return ""
    return str(value).strip().lower().rstrip(".")


def normalize_ip(value: object) -> str:
    if pd.isna(value):
        return ""

    text = str(value).strip()
    if not text:
        return ""

    # Some exports contain several addresses in one cell.
    for separator in [",", ";", "\n", " "]:
        if separator in text:
            text = text.split(separator, 1)[0].strip()

    try:
        return str(ipaddress.ip_address(text))
    except ValueError:
        return text


def require_columns(df: pd.DataFrame, required: dict[str, str], file_label: str) -> None:
    missing = [col for col in required.values() if col not in df.columns]
    if missing:
        raise SystemExit(
            f"{file_label}: не найдены обязательные колонки: {', '.join(missing)}\n"
            f"Фактические колонки: {', '.join(map(str, df.columns))}"
        )


def is_active_state(value: object) -> bool:
    if pd.isna(value):
        return False
    return str(value).strip().lower() in ACTIVE_STATES


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Create an Excel file with active Host-AC assets missing from audit."
    )
    parser.add_argument("host_ac_xlsx", help="Source Excel file: Host-AC.xlsx")
    parser.add_argument("audit_xlsx", help="Audit Excel file: audit.xlsx")
    parser.add_argument(
        "output_xlsx",
        nargs="?",
        default="missing_assets.xlsx",
        help="Output Excel file. Default: missing_assets.xlsx",
    )
    parser.add_argument(
        "--all-states",
        action="store_true",