175 lines
5.8 KiB
Python
175 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Monitor dostępności hostów przez HTTP lub ICMP."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import concurrent.futures
|
|
import platform
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Iterable
|
|
from urllib.error import HTTPError, URLError
|
|
from urllib.parse import urlparse
|
|
from urllib.request import Request, urlopen
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CheckResult:
|
|
target: str
|
|
method: str
|
|
available: bool
|
|
detail: str
|
|
elapsed_ms: float
|
|
|
|
|
|
def normalize_url(target: str) -> str:
|
|
if "://" not in target:
|
|
return f"http://{target}"
|
|
return target
|
|
|
|
|
|
def host_from_target(target: str) -> str:
|
|
parsed = urlparse(target if "://" in target else f"//{target}")
|
|
return parsed.hostname or target.split("/")[0]
|
|
|
|
|
|
def check_http(target: str, timeout: float) -> CheckResult:
|
|
started = time.perf_counter()
|
|
url = normalize_url(target)
|
|
request = Request(url, headers={"User-Agent": "host-checker/1.0"})
|
|
|
|
try:
|
|
with urlopen(request, timeout=timeout) as response:
|
|
status = response.status
|
|
available = status < 400
|
|
detail = f"HTTP {status}"
|
|
except HTTPError as error:
|
|
# Odpowiedź HTTP, także 4xx/5xx, oznacza że host jest osiągalny.
|
|
available = True
|
|
detail = f"HTTP {error.code}"
|
|
except (URLError, TimeoutError, OSError) as error:
|
|
available = False
|
|
reason = getattr(error, "reason", error)
|
|
detail = str(reason)
|
|
|
|
return CheckResult(target, "http", available, detail, elapsed_ms(started))
|
|
|
|
|
|
def check_icmp(target: str, timeout: float) -> CheckResult:
|
|
started = time.perf_counter()
|
|
host = host_from_target(target)
|
|
system = platform.system().lower()
|
|
if system == "windows":
|
|
command = ["ping", "-n", "1", "-w", str(max(1, int(timeout * 1000))), host]
|
|
else:
|
|
command = ["ping", "-c", "1", "-W", str(max(1, int(timeout))), host]
|
|
|
|
try:
|
|
completed = subprocess.run(
|
|
command,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
timeout=timeout + 1,
|
|
check=False,
|
|
)
|
|
available = completed.returncode == 0
|
|
detail = "odpowiedź ICMP" if available else "brak odpowiedzi"
|
|
except FileNotFoundError:
|
|
available = False
|
|
detail = "nie znaleziono programu ping"
|
|
except subprocess.TimeoutExpired:
|
|
available = False
|
|
detail = "przekroczono timeout"
|
|
except OSError as error:
|
|
available = False
|
|
detail = str(error)
|
|
|
|
return CheckResult(target, "icmp", available, detail, elapsed_ms(started))
|
|
|
|
|
|
def elapsed_ms(started: float) -> float:
|
|
return (time.perf_counter() - started) * 1000
|
|
|
|
|
|
def check(target: str, method: str, timeout: float) -> CheckResult:
|
|
if method == "http":
|
|
return check_http(target, timeout)
|
|
return check_icmp(target, timeout)
|
|
|
|
|
|
def load_targets(cli_targets: list[str], filename: str | None) -> list[str]:
|
|
targets = list(cli_targets)
|
|
if filename:
|
|
for line in Path(filename).read_text(encoding="utf-8").splitlines():
|
|
line = line.strip()
|
|
if line and not line.startswith("#"):
|
|
targets.append(line)
|
|
# Zachowaj kolejność i usuń powtórzenia.
|
|
return list(dict.fromkeys(targets))
|
|
|
|
|
|
def print_results(results: Iterable[CheckResult]) -> None:
|
|
timestamp = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S%z")
|
|
for result in results:
|
|
state = "OK " if result.available else "FAIL"
|
|
print(
|
|
f"{timestamp} | {state} | {result.method.upper():5} | "
|
|
f"{result.target} | {result.detail} | {result.elapsed_ms:.0f} ms",
|
|
flush=True,
|
|
)
|
|
|
|
|
|
def run(args: argparse.Namespace, targets: list[str]) -> int:
|
|
while True:
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=args.workers) as pool:
|
|
futures = [pool.submit(check, target, args.method, args.timeout) for target in targets]
|
|
results = [future.result() for future in futures]
|
|
print_results(results)
|
|
|
|
if args.once:
|
|
return 0 if all(result.available for result in results) else 1
|
|
try:
|
|
time.sleep(args.interval)
|
|
except KeyboardInterrupt:
|
|
print("\nZatrzymano.")
|
|
return 0
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(description="Sprawdzanie dostępności hostów przez HTTP lub ICMP.")
|
|
parser.add_argument("targets", nargs="*", help="Hosty, adresy IP lub URL-e")
|
|
parser.add_argument("-f", "--file", help="Plik z hostami: jeden wpis w linii")
|
|
parser.add_argument("-m", "--method", choices=("http", "icmp"), default="http", help="Metoda testu (domyślnie: http)")
|
|
parser.add_argument("-t", "--timeout", type=float, default=3.0, help="Timeout w sekundach (domyślnie: 3)")
|
|
parser.add_argument("-i", "--interval", type=float, default=10.0, help="Odstęp między testami (domyślnie: 10 s)")
|
|
parser.add_argument("-w", "--workers", type=int, default=10, help="Liczba równoległych testów (domyślnie: 10)")
|
|
parser.add_argument("--once", action="store_true", help="Wykonaj tylko jeden test i zakończ")
|
|
return parser
|
|
|
|
|
|
def main() -> int:
|
|
parser = build_parser()
|
|
args = parser.parse_args()
|
|
if args.timeout <= 0 or args.interval < 0 or args.workers < 1:
|
|
parser.error("timeout musi być > 0, interval >= 0, a workers >= 1")
|
|
try:
|
|
targets = load_targets(args.targets, args.file)
|
|
except OSError as error:
|
|
parser.error(f"nie można odczytać pliku: {error}")
|
|
if not targets:
|
|
parser.error("podaj co najmniej jeden host albo użyj --file")
|
|
try:
|
|
return run(args, targets)
|
|
except KeyboardInterrupt:
|
|
print("\nZatrzymano.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|