diff --git a/backend/app/services/discovery/config_parser.py b/backend/app/services/discovery/config_parser.py index 59fc399..b3afaa6 100644 --- a/backend/app/services/discovery/config_parser.py +++ b/backend/app/services/discovery/config_parser.py @@ -3,6 +3,7 @@ CAPS INI config parsing utilities. All functions are pure (no network I/O, no DB). """ +import configparser as _cp import logging import re from configparser import RawConfigParser @@ -36,7 +37,12 @@ def parse_config( all_rack_hosts = [rack_host] parser = RawConfigParser(strict=False) - parser.read_string(config_text) + try: + parser.read_string(config_text) + except _cp.Error as exc: + # Malformed lines in the config — parser is already partially populated + # with all sections up to the error. Log and continue with what we have. + logger.warning("[discovery] Config parse warning for %s: %s", rack_host, exc) discovered: list[DiscoveredDevice] = [] seen_ips: set[str] = set() diff --git a/backend/app/services/discovery/orchestrator.py b/backend/app/services/discovery/orchestrator.py index 1bcfbd8..1232139 100644 --- a/backend/app/services/discovery/orchestrator.py +++ b/backend/app/services/discovery/orchestrator.py @@ -123,63 +123,73 @@ async def discover_via_ssh( neighbor_candidates: set[str] = set() discovered_device_ips: set[str] = set() + all_rack_ips = [h for _, h, _, _ in rack_hosts] + for rack_idx, (external_id, rack_host, rack_name, _rack_type_db) in enumerate(rack_hosts): await _emit("disc_rack_start", { "rack_name": rack_name, "rack_host": rack_host, "index": rack_idx, "total": len(rack_hosts), }) - # Allow per-device config_path override stored in connection_params. - effective_config_path = await _resolve_rack_config_path( - db, obj, rack_host, config_path - ) + _rack_found: int = 0 + _rack_ssh_ok: bool = False + _flush_error: Exception | None = None - rack_ssh_ok, config_text = await _ssh_fetch_rack_config( - rack_host, ssh_port, ssh_options, effective_config_path, rack_name, result, _emit - ) + try: + # Allow per-device config_path override stored in connection_params. + effective_config_path = await _resolve_rack_config_path( + db, obj, rack_host, config_path + ) - await _upsert_rack_device( - db, obj, external_id, rack_host, rack_name, - rack_id_mapping, rack_ssh_ok, now, result - ) + rack_ssh_ok, config_text = await _ssh_fetch_rack_config( + rack_host, ssh_port, ssh_options, effective_config_path, rack_name, result, _emit + ) + _rack_ssh_ok = rack_ssh_ok + + await _upsert_rack_device( + db, obj, external_id, rack_host, rack_name, + rack_id_mapping, rack_ssh_ok, now, result + ) + + if rack_ssh_ok: + neigh_ips = await get_ip_neighbors_multi(rack_host, ssh_port, ssh_options) + neighbor_candidates.update(neigh_ips) + discovered_device_ips.add(rack_host) + + await _emit("disc_action", {"message": f"Парсинг конфигурации {rack_name}..."}) + devices_found = parse_config(config_text, rack_host, all_rack_ips) + logger.info("[discovery] Found %d devices from plugins in %s", len(devices_found), rack_host) + _rack_found = len(devices_found) + + for found in devices_found: + await _upsert_plugin_device( + db, obj, found, external_id, rack_id_mapping, result + ) + discovered_device_ips.add(found.ip) - if not rack_ssh_ok: try: await db.flush() except Exception as exc: - raise RuntimeError(f"DB flush failed for offline rack {rack_host}: {exc}") from exc - await _emit("disc_rack_done", { - "rack_name": rack_name, "rack_host": rack_host, "found": 0, "ssh_ok": False - }) - continue + # Session is now invalid after a failed flush — capture the error so we can + # still emit disc_rack_done before propagating and aborting discovery. + _flush_error = RuntimeError(f"DB flush failed after rack {rack_host}: {exc}") from exc - neigh_ips = await get_ip_neighbors_multi(rack_host, ssh_port, ssh_options) - neighbor_candidates.update(neigh_ips) - discovered_device_ips.add(rack_host) - - all_rack_ips = [h for _, h, _, _ in rack_hosts] - await _emit("disc_action", {"message": f"Парсинг конфигурации {rack_name}..."}) - devices_found = parse_config(config_text, rack_host, all_rack_ips) - logger.info("[discovery] Found %d devices from plugins in %s", len(devices_found), rack_host) - - for found in devices_found: - await _upsert_plugin_device( - db, obj, found, external_id, rack_id_mapping, result - ) - discovered_device_ips.add(found.ip) - - try: - await db.flush() except Exception as exc: - # Session is now invalid — re-raise so the outer handler emits disc_done with error. - # Continuing would cause every subsequent DB call to fail silently, producing - # an incomplete discovery with no clear termination signal to the frontend. - raise RuntimeError(f"DB flush failed after rack {rack_host}: {exc}") from exc + # Unexpected per-rack error (e.g. SSH timeout, config parse failure not caught + # at a lower level). Record it and fall through to emit disc_rack_done. + result.errors.append(f"Rack {rack_name} ({rack_host}): {exc}") + logger.error("[discovery] Rack %s (%s) error: %s", rack_host, rack_name, exc, exc_info=True) + + # disc_rack_done is always emitted — frontend must never be left with a spinner + # for a rack that already received disc_rack_start. await _emit("disc_rack_done", { "rack_name": rack_name, "rack_host": rack_host, - "found": len(devices_found), "ssh_ok": True, + "found": _rack_found, "ssh_ok": _rack_ssh_ok, }) + if _flush_error: + raise _flush_error + # ── Phase 2.5: Slave rack discovery via ARP ─────────────────────────────── all_rack_ips_set = {h for _, h, _, _ in rack_hosts} infra_ips = {ip for ip in [obj.server_ip, db_host] if ip}