#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
LOI Zabbix Linux Monitoring Agent
Pure Python 3 - Zero external dependencies required (No pip, No psutil needed)
"""

import sys
import os
import re
import glob
import time
import json
import socket
import shutil
import platform
import argparse
import subprocess

CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "agent_config.json")
DEFAULT_PORT = 65000

class LinuxAgent:
    def __init__(self, server_ip=None, agent_name=None):
        self.server_ip = server_ip
        self.agent_name = agent_name
        self.port = DEFAULT_PORT

        # State for CPU calculation
        self.prev_cpu_total = 0
        self.prev_cpu_idle = 0

        # State for Network calculation
        self.prev_net_in = 0
        self.prev_net_out = 0
        self.prev_net_time = time.time()

        # State for Disk I/O + last measured agent->server latency
        self.prev_disk_ticks = None
        self.prev_disk_time = 0.0
        self.last_latency_ms = 0

        # Root login tracking (wtmp via `last`)
        self._seen_logins = set()
        self._last_login_poll = 0
        self._pending_login_fails = []

        # File-watch state (paths pushed by collector ACK)
        self.file_watch_paths = []
        self.file_snapshot = None
        self._watch_cycle = 0

        # Web visitor tracking (nginx/apache access logs, 5-minute window)
        self._weblog_offsets = {}
        self._web_ip_seen = {}

        self.ensure_config()
        self.init_baselines()

    def ensure_config(self):
        # 1. Load from file if exists
        if os.path.isfile(CONFIG_FILE):
            try:
                with open(CONFIG_FILE, "r", encoding="utf-8") as f:
                    cfg = json.load(f)
                    if not self.server_ip and "server_ip" in cfg:
                        self.server_ip = cfg["server_ip"]
                    if not self.agent_name and "agent_name" in cfg:
                        self.agent_name = cfg["agent_name"]
            except Exception as e:
                print(f"[WARN] Config load error: {e}")

        # 2. Defaults if still empty
        if not self.server_ip:
            default_ip = "loizabbix.loict.co.kr"
            if sys.stdin.isatty():
                val = input(f"1. 모니터링 서버 IP 주소 [기본값: {default_ip}]: ").strip()
                self.server_ip = val if val else default_ip
            else:
                self.server_ip = default_ip

        if not self.agent_name:
            default_name = socket.gethostname()
            if sys.stdin.isatty():
                val = input(f"2. 에이전트 이름 (호스트명) [기본값: {default_name}]: ").strip()
                self.agent_name = val if val else default_name
            else:
                self.agent_name = default_name

        # 3. Save config
        try:
            with open(CONFIG_FILE, "w", encoding="utf-8") as f:
                json.dump({"server_ip": self.server_ip, "agent_name": self.agent_name}, f, indent=2)
        except Exception as e:
            print(f"[WARN] Config save error: {e}")

    def init_baselines(self):
        # CPU
        total, idle = self.read_cpu_times()
        self.prev_cpu_total = total
        self.prev_cpu_idle = idle

        # Network
        net_in, net_out = self.read_network_bytes()
        self.prev_net_in = net_in
        self.prev_net_out = net_out
        self.prev_net_time = time.time()

        # Disk I/O baseline
        self.prev_disk_ticks = self.read_disk_io_totals()
        self.prev_disk_time = time.time()

    def read_cpu_times(self):
        try:
            with open("/proc/stat", "r") as f:
                for line in f:
                    if line.startswith("cpu "):
                        parts = [float(x) for x in line.strip().split()[1:]]
                        idle = parts[3] + (parts[4] if len(parts) > 4 else 0)
                        total = sum(parts)
                        return total, idle
        except Exception:
            pass
        return 0, 0

    def get_cpu_percent(self):
        total, idle = self.read_cpu_times()
        diff_total = total - self.prev_cpu_total
        diff_idle = idle - self.prev_cpu_idle
        self.prev_cpu_total = total
        self.prev_cpu_idle = idle

        if diff_total <= 0:
            return 0.0
        cpu = (diff_total - diff_idle) / diff_total * 100.0
        return round(max(0.0, min(100.0, cpu)), 1)

    def get_memory_info(self):
        total_kb = 0
        avail_kb = 0
        try:
            with open("/proc/meminfo", "r") as f:
                for line in f:
                    if line.startswith("MemTotal:"):
                        total_kb = int(line.split()[1])
                    elif line.startswith("MemAvailable:"):
                        avail_kb = int(line.split()[1])
            if avail_kb == 0:
                # Fallback for old kernels
                free_kb = 0
                buffers_kb = 0
                cached_kb = 0
                with open("/proc/meminfo", "r") as f:
                    for line in f:
                        if line.startswith("MemFree:"): free_kb = int(line.split()[1])
                        elif line.startswith("Buffers:"): buffers_kb = int(line.split()[1])
                        elif line.startswith("Cached:"): cached_kb = int(line.split()[1])
                avail_kb = free_kb + buffers_kb + cached_kb
        except Exception:
            pass

        total_mb = round(total_kb / 1024.0, 1)
        used_mb = round((total_kb - avail_kb) / 1024.0, 1)
        pct = round((used_mb / total_mb * 100.0), 1) if total_mb > 0 else 0.0
        return total_mb, used_mb, pct

    def get_disk_info(self):
        mounts = set()
        disks = []
        try:
            with open("/proc/mounts", "r") as f:
                for line in f:
                    parts = line.split()
                    dev = parts[0]
                    mp = parts[1]
                    fstype = parts[2]
                    # Filter physical filesystems
                    if dev.startswith("/dev/") and fstype not in ["squashfs", "iso9660"]:
                        mounts.add(mp)
        except Exception:
            mounts.add("/")

        if not mounts:
            mounts.add("/")

        for mp in sorted(mounts):
            try:
                st = os.statvfs(mp)
                total = st.f_blocks * st.f_frsize
                free = st.f_bavail * st.f_frsize
                used = total - free
                if total > 0:
                    total_gb = round(total / (1024**3), 1)
                    used_gb = round(used / (1024**3), 1)
                    free_gb = round(free / (1024**3), 1)
                    pct = round(used / total * 100.0, 1)
                    disks.append({
                        "drive": mp,
                        "total_gb": total_gb,
                        "used_gb": used_gb,
                        "free_gb": free_gb,
                        "percent": pct
                    })
            except Exception:
                pass
        return disks

    def read_network_bytes(self):
        total_in = 0
        total_out = 0
        try:
            with open("/proc/net/dev", "r") as f:
                lines = f.readlines()[2:]
                for line in lines:
                    parts = line.replace(":", " ").split()
                    if len(parts) >= 10:
                        iface = parts[0]
                        if iface != "lo" and not iface.startswith("docker") and not iface.startswith("veth"):
                            rx = int(parts[1])
                            tx = int(parts[9])
                            total_in += rx
                            total_out += tx
        except Exception:
            pass
        return total_in, total_out

    def get_network_rates(self):
        cur_in, cur_out = self.read_network_bytes()
        now = time.time()
        elapsed = now - self.prev_net_time
        if elapsed < 0.5:
            elapsed = 1.0

        in_bps = int(max(0, (cur_in - self.prev_net_in) / elapsed))
        out_bps = int(max(0, (cur_out - self.prev_net_out) / elapsed))

        self.prev_net_in = cur_in
        self.prev_net_out = cur_out
        self.prev_net_time = now
        return in_bps, out_bps

    def get_local_ip(self):
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect((self.server_ip, self.port))
            ip = s.getsockname()[0]
            s.close()
            return ip
        except Exception:
            return "127.0.0.1"

    def get_machine_id(self):
        for path in ("/etc/machine-id", "/var/lib/dbus/machine-id"):
            try:
                with open(path, "r") as f:
                    mid = f.read().strip().split()[0]
                    if mid:
                        return mid
            except Exception:
                pass
        return ""

    def get_os_info(self):
        try:
            if os.path.isfile("/etc/os-release"):
                with open("/etc/os-release") as f:
                    for line in f:
                        if line.startswith("PRETTY_NAME="):
                            return line.split("=")[1].strip().strip('"')
            return f"Linux {platform.release()}"
        except Exception:
            return "Linux"

    def get_antivirus_info(self):
        """Detect common Linux AV products. State: ON(active) / OFF / 설치됨(unchecked)."""
        candidates = [
            ("ClamAV", ["clamscan", "clamdscan", "freshclam"],
             ["/etc/clamav", "/etc/clamav/clamd.conf"],
             ["clamav-daemon", "clamd", "clamav-freshclam"]),
            ("Sophos", ["savscan", "savdstatus"],
             ["/opt/sophos-av", "/opt/sophos-av/bin/savdstatus"],
             ["sav-protect", "sophos-av"]),
            ("ESET", ["esets_scan"],
             ["/opt/eset/esets/sbin/esets_daemon"],
             ["esets_daemon", "esets"]),
            ("Kaspersky", ["kesl-control"],
             ["/opt/kaspersky/kesl/bin/kesl-control"],
             ["kesl-supervisor", "kesl"]),
            ("Comodo", ["cmdscan"],
             ["/opt/COMODO/ccav"],
             ["cmdagent"]),
        ]
        products = []
        error = None
        try:
            for name, binaries, paths, services in candidates:
                found = any(shutil.which(b) for b in binaries) or \
                        any(os.path.exists(p) for p in paths)
                if not found:
                    continue
                state = "설치됨"
                active = self._any_service_active(services)
                if active is True:
                    state = "ON"
                elif active is False:
                    state = "OFF"
                products.append({"name": name, "state": state})
        except Exception as e:
            error = str(e)
        result = {"installed": len(products) > 0, "products": products}
        if error:
            result["error"] = error
        return result

    def _any_service_active(self, services):
        """True/False via systemctl/pidof, None when it cannot be determined."""
        for svc in services:
            try:
                r = subprocess.run(["systemctl", "is-active", svc],
                                   capture_output=True, text=True, timeout=5)
                if r.stdout.strip() == "active":
                    return True
            except Exception:
                pass
            try:
                r = subprocess.run(["pidof", svc],
                                   capture_output=True, text=True, timeout=5)
                if r.returncode == 0 and r.stdout.strip():
                    return True
            except Exception:
                pass
        # systemctl itself missing -> cannot determine; otherwise assume stopped
        if shutil.which("systemctl") is None and shutil.which("pidof") is None:
            return None
        return False

    def read_disk_io_totals(self):
        """Sum read/write ticks + completed I/Os over whole disks (/sys/block)."""
        r_ms = 0
        w_ms = 0
        ios = 0
        try:
            devs = os.listdir("/sys/block")
        except Exception:
            return r_ms, w_ms, ios
        for dev in devs:
            if dev.startswith(("loop", "ram", "fd", "sr", "dm-")):
                continue
            try:
                with open("/sys/block/" + dev + "/stat") as f:
                    fields = f.read().split()
                # 0:reads_completed 3:ms_reading 4:writes_completed 7:ms_writing
                r_ms += int(fields[3])
                w_ms += int(fields[7])
                ios += int(fields[0]) + int(fields[4])
            except Exception:
                pass
        return r_ms, w_ms, ios

    def get_disk_io_stats(self):
        cur = self.read_disk_io_totals()
        now = time.time()
        if self.prev_disk_ticks is None:
            self.prev_disk_ticks = cur
            self.prev_disk_time = now
            return 0.0, 0.0
        elapsed_ms = max(1.0, (now - self.prev_disk_time) * 1000.0)
        dr = max(0, cur[0] - self.prev_disk_ticks[0])
        dw = max(0, cur[1] - self.prev_disk_ticks[1])
        d_ios = max(0, cur[2] - self.prev_disk_ticks[2])
        util = round(min(100.0, (dr + dw) / elapsed_ms * 100.0), 1)
        await_ms = round((dr + dw) / d_ios, 1) if d_ios > 0 else 0.0
        self.prev_disk_ticks = cur
        self.prev_disk_time = now
        return util, await_ms

    _MONTHS = {"Jan": "01", "Feb": "02", "Mar": "03", "Apr": "04",
               "May": "05", "Jun": "06", "Jul": "07", "Aug": "08",
               "Sep": "09", "Oct": "10", "Nov": "11", "Dec": "12"}

    def _fmt_last_time(self, s):
        try:
            p = s.split()  # [Mon, Sep, 01, 10:00:00, 2025]
            return "%s-%s-%s %s" % (p[4], self._MONTHS[p[1]], p[2].zfill(2), p[3])
        except Exception:
            return s

    def get_admin_logins(self):
        """Root access: direct logins (wtmp) + su/sudo escalation (auth log)."""
        logins = []
        try:
            if time.time() - self._last_login_poll < 30:
                return logins, []
            self._last_login_poll = time.time()
            all_logins = self._all_last_logins()
            root_direct = [e for e in all_logins if e["user"] == "root"]
            admin_logins = root_direct + self._root_auth_logins()
            return admin_logins[:20], all_logins[:50]
        except Exception:
            pass
        return [], []

    def _root_last_logins(self):
        return [e for e in self._all_last_logins() if e["user"] == "root"][:20]

    def _all_last_logins(self):
        """Recent interactive logons for ALL users (wtmp). Pseudo-users skipped."""
        logins = []
        try:
            r = subprocess.run(["last", "-F", "-w"],
                               capture_output=True, text=True, timeout=10)
            if r.returncode != 0:
                return logins

            now = time.time()
            for line in r.stdout.splitlines():
                parts = line.split()
                if len(parts) < 7:
                    continue
                username = parts[0]
                if username in ("reboot", "shutdown", "wtmp", ""):
                    continue
                # user pts/0 203.0.113.5 Mon Sep 01 10:00:00 2025 ...
                # user tty1 Mon Sep 01 10:00:00 2025 ... / user :0 ... (no host)
                if parts[1].startswith("tty") or parts[1].startswith(":"):
                    if len(parts) < 7:
                        continue
                    tty, host, tstart = parts[1], "-", 2
                else:
                    if len(parts) < 8:
                        continue
                    tty, host, tstart = parts[1], parts[2], 3
                login_raw = " ".join(parts[tstart:tstart + 5])
                if len(login_raw.split()) < 5:
                    continue
                key = (username, tty, host, login_raw)
                if key in self._seen_logins:
                    continue
                self._seen_logins.add(key)
                login_time = self._fmt_last_time(login_raw)
                try:
                    age = now - time.mktime(time.strptime(login_time, "%Y-%m-%d %H:%M:%S"))
                except Exception:
                    age = 0
                if age > 3600:
                    continue  # old history: remember, don't report
                logins.append({"user": username, "logon_type": tty,
                               "ip": host, "time": login_time})
                if len(logins) >= 50:
                    break
            while len(self._seen_logins) > 500:
                self._seen_logins.pop()
        except Exception:
            pass
        return logins

    def _fmt_syslog_time(self, line):
        # "Sep 19 10:00:01 host ..." (no year) -> "YYYY-MM-DD HH:MM:SS"
        try:
            p = line.split()
            year = time.strftime("%Y")
            timestr = "%s-%s-%s %s" % (year, self._MONTHS[p[0]], p[1].zfill(2), p[2])
            if time.mktime(time.strptime(timestr, "%Y-%m-%d %H:%M:%S")) > time.time() + 86400:
                timestr = "%d-%s-%s %s" % (int(year) - 1, self._MONTHS[p[0]], p[1].zfill(2), p[2])
            return timestr
        except Exception:
            return time.strftime("%Y-%m-%d %H:%M:%S")

    def _parse_auth_line(self, line):
        # su to root: "su[123]: pam_unix(su:session): session opened for user root by admin(uid=1000)"
        m = re.search(r"pam_unix\(su:session\): session opened for user root by (\S+)", line)
        if m:
            by = m.group(1).split("(")[0]
            return {"user": "root", "logon_type": "su(" + by + ")",
                    "ip": "-", "time": self._fmt_syslog_time(line)}
        # sudo to root shell: "sudo: admin : TTY=pts/0 ; ... COMMAND=/bin/bash"
        m = re.search(r"sudo:\s+(\S+)\s*:.*COMMAND=(/bin/(?:ba)?sh|/usr/bin/su|/bin/su)\b", line)
        if m:
            return {"user": "root", "logon_type": "sudo(" + m.group(1) + ")",
                    "ip": "-", "time": self._fmt_syslog_time(line)}
        return None

    def _root_auth_logins(self):
        """su/sudo escalation from auth log. Offset-tracked: first sight starts at EOF."""
        logins = []
        if not hasattr(self, "_auth_offsets"):
            self._auth_offsets = {}
        for path in ("/var/log/auth.log", "/var/log/secure"):
            if not os.path.isfile(path):
                continue
            try:
                st = os.stat(path)
                key = (st.st_dev, st.st_ino)
                prev = self._auth_offsets.get(path)
                if prev is None or prev.get("key") != key:
                    # First sight or rotated: start at end to avoid history flood.
                    self._auth_offsets[path] = {"key": key, "pos": st.st_size}
                    continue
                pos = prev.get("pos", 0)
                if st.st_size < pos:
                    pos = 0
                if st.st_size == pos:
                    continue
                with open(path, "r", errors="ignore") as f:
                    f.seek(pos)
                    lines = f.readlines()
                    pos = f.tell()
                self._auth_offsets[path] = {"key": key, "pos": pos}
                fails = {}
                for line in lines[-500:]:
                    entry = self._parse_auth_line(line)
                    if entry:
                        logins.append(entry)
                    if len(logins) >= 20:
                        break
                    # SSH brute force: "Failed password for root/admin from 1.2.3.4"
                    m = re.search(r"Failed \S+ for (invalid user )?(\S+) from (\S+)", line)
                    if m:
                        user = m.group(2)
                        ip = m.group(3)
                        k = (user, ip)
                        fails[k] = fails.get(k, 0) + 1
                for (user, ip), cnt in fails.items():
                    if cnt >= 5 and len(self._pending_login_fails) < 20:
                        self._pending_login_fails.append(
                            {"user": user, "ip": ip, "count": cnt, "source": "SSH"})
            except Exception:
                pass
        return logins

    def check_file_changes(self):
        """Poll watched folders every 6th cycle (~30s). First run only seeds baseline."""
        changes = []
        try:
            self._watch_cycle += 1
            if self._watch_cycle % 6 != 0 and self.file_snapshot is not None:
                return changes
            snap = {}
            for root_path in self.file_watch_paths:
                if not root_path or not os.path.isdir(root_path):
                    continue
                for dirpath, dirnames, filenames in os.walk(root_path, followlinks=False):
                    for fn in filenames:
                        fp = os.path.join(dirpath, fn)
                        try:
                            st = os.stat(fp)
                            snap[fp] = (st.st_mtime, st.st_size)
                        except Exception:
                            pass
                        if len(snap) >= 20000:
                            break
                    if len(snap) >= 20000:
                        break
            if self.file_snapshot is None:
                self.file_snapshot = snap
                return changes
            now_str = time.strftime("%Y-%m-%d %H:%M:%S")
            for fp, meta in snap.items():
                if fp not in self.file_snapshot:
                    changes.append({"path": fp, "change": "생성", "time": now_str})
                elif meta != self.file_snapshot[fp]:
                    changes.append({"path": fp, "change": "수정", "time": now_str})
                if len(changes) >= 50:
                    break
            for fp in self.file_snapshot:
                if fp not in snap:
                    changes.append({"path": fp, "change": "삭제", "time": now_str})
                if len(changes) >= 50:
                    break
            self.file_snapshot = snap
        except Exception:
            pass
        return changes[:50]

    WEB_LOG_GLOBS = ("/var/log/nginx/*access*.log",
                       "/var/log/apache2/*access*.log",
                       "/var/log/httpd/*access*log*")

    def get_web_visitors(self):
        """Unique visitor IPs in the trailing 5 minutes (access logs)."""
        hits = 0
        try:
            cutoff = time.time() - 300
            for pattern in self.WEB_LOG_GLOBS:
                for path in glob.glob(pattern):
                    hits += self._scan_web_log(path, cutoff)
            for ip in [k for k, v in self._web_ip_seen.items() if v < cutoff]:
                del self._web_ip_seen[ip]
            return len(self._web_ip_seen), hits
        except Exception:
            return 0, 0

    def _scan_web_log(self, path, cutoff):
        hits = 0
        try:
            st = os.stat(path)
            key = (st.st_dev, st.st_ino)
            prev = self._weblog_offsets.get(path)
            if prev is None or prev.get("key") != key:
                # First sight or rotated: start at end (ramps up within minutes).
                self._weblog_offsets[path] = {"key": key, "pos": st.st_size}
                return 0
            pos = prev.get("pos", 0)
            if st.st_size < pos:
                pos = 0
            if st.st_size == pos:
                return 0
            with open(path, "r", errors="ignore") as f:
                f.seek(pos)
                lines = f.readlines()
                self._weblog_offsets[path] = {"key": key, "pos": f.tell()}
            # 203.0.113.5 - - [19/Sep/2026:16:30:00 +0900] "GET / ..." (local tz assumed)
            for line in lines[-5000:]:
                p1 = line.find(" ")
                p2 = line.find("[")
                p3 = line.find("]")
                if p1 < 0 or p2 < 0 or p3 < 0:
                    continue
                ip = line[:p1]
                try:
                    ts = time.mktime(time.strptime(line[p2 + 1:p3].rsplit(" ", 1)[0], "%d/%b/%Y:%H:%M:%S"))
                except Exception:
                    continue
                if ts < cutoff:
                    continue
                hits += 1
                if ip != "-":
                    self._web_ip_seen[ip] = ts
        except Exception:
            pass
        return hits

    def collect_payload(self):
        cpu_pct = self.get_cpu_percent()
        total_mb, used_mb, mem_pct = self.get_memory_info()
        disks = self.get_disk_info()
        in_bps, out_bps = self.get_network_rates()
        disk_io_pct, disk_await_ms = self.get_disk_io_stats()
        admin_logins, user_logins = self.get_admin_logins()
        file_changes = self.check_file_changes()
        web_unique, web_hits = self.get_web_visitors()
        login_fails = list(self._pending_login_fails)
        self._pending_login_fails = []

        payload = {
            "host_name": self.agent_name,
            "ip": self.get_local_ip(),
            "machine_id": self.get_machine_id(),
            "os": self.get_os_info(),
            "cpu_percent": cpu_pct,
            "cpu_cores": os.cpu_count() or 0,
            "antivirus": self.get_antivirus_info(),
            "disk_io_percent": disk_io_pct,
            "disk_await_ms": disk_await_ms,
            "admin_logins": admin_logins,
            "user_logins": user_logins,
            "login_failures": login_fails,
            "file_changes": file_changes,
            "web_visitors": {
                "unique_ips": web_unique,
                "hits": web_hits,
                "connections": 0
            },
            "memory_percent": mem_pct,
            "memory_used_mb": used_mb,
            "memory_total_mb": total_mb,
            "disks": disks,
            "network": {
                "in_bps": in_bps,
                "out_bps": out_bps,
                "latency_ms": self.last_latency_ms
            }
        }
        return payload

    def send_metrics(self, payload):
        data = json.dumps(payload) + "\n"
        start = time.time()
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.settimeout(5.0)
            s.connect((self.server_ip, self.port))
            self.last_latency_ms = int((time.time() - start) * 1000)
            s.sendall(data.encode("utf-8"))
            response = s.recv(1024).decode("utf-8").strip()
            try:
                ack = json.loads(response)
                if isinstance(ack, dict) and "watch_paths" in ack:
                    self.file_watch_paths = [p for p in ack["watch_paths"] if p]
            except Exception:
                pass
            return response

    def run(self):
        print("=" * 54)
        print("  LOI Zabbix Linux Monitoring Agent")
        print("=" * 54)
        print(f"[INFO] Agent Name   : {self.agent_name}")
        print(f"[INFO] Monitoring IP: {self.server_ip}")
        print(f"[INFO] Target Port  : TCP {self.port}")
        print("[INFO] Starting metric collection every 5 seconds... (Press Ctrl+C to stop)")

        while True:
            try:
                time.sleep(5)
                payload = self.collect_payload()
                ack = self.send_metrics(payload)
                now_str = time.strftime("%H:%M:%S")
                cpu = payload['cpu_percent']
                mem = payload['memory_percent']
                in_kb = payload['network']['in_bps'] / 1024.0
                out_kb = payload['network']['out_bps'] / 1024.0
                print(f"[{now_str}] Sent: CPU {cpu}%, RAM {mem}%, NetIn {in_kb:.1f}KB/s, NetOut {out_kb:.1f}KB/s | Server: {ack}")
            except KeyboardInterrupt:
                print("\n[INFO] Stopping Linux Agent...")
                break
            except Exception as e:
                now_str = time.strftime("%H:%M:%S")
                print(f"[{now_str}] [ERROR] TCP Send Failed: {e}")

def main():
    parser = argparse.ArgumentParser(description="LOI Zabbix Linux Monitoring Agent")
    parser.add_argument("-server", "-s", dest="server", help="Monitoring Server IP Address")
    parser.add_argument("-name", "-n", dest="name", help="Agent Host Name")
    args = parser.parse_args()

    agent = LinuxAgent(server_ip=args.server, agent_name=args.name)
    agent.run()

if __name__ == "__main__":
    main()
