#! /usr/bin/python3
#-------------------------------------------------------------------------------
#
# Copyright (C) 2020-2021 NVIDIA Corporation. ALL RIGHTS RESERVED.
# Copyright 2013,2015,2016,2018, Cumulus Networks, Inc.  All rights reserved
#
#-------------------------------------------------------------------------------
#

#-------------------------------------------------------------------------------
#
# Imports
#

import syslog
import warnings
import re
import os
import sys
import time
import signal
import traceback
import argparse
import configparser
import cumulus.platforms
import cumulus.sdnotify
import shutil
import glob
import subprocess
import datetime
import json
import pickle

# Add health history constants
ReloadDir = '/cumulus/smond/'
ReloadFile = ReloadDir + 'reload'
HealthHistoryDir = '/var/log/'
HealthHistoryFile = HealthHistoryDir + 'health_history'
MAX_ROTATED_LOGS = 3  # Keep 3 rotated logs

# Add a dictionary to track component states
component_states = {}

halt = 0

def setHaltSensorReads(h):
    global halt
    halt = h

def getHaltSensorReads():
    global halt
    return halt

def log_startup_error(error_msg, exc_info=None):
    """Log startup errors to both syslog and stderr"""
    syslog.syslog(syslog.LOG_ERR, f"SMOND STARTUP ERROR: {error_msg}")
    sys.stderr.write(f"SMOND STARTUP ERROR: {error_msg}\n")
    if exc_info:
        syslog.syslog(syslog.LOG_ERR, f"Exception details: {exc_info}")
        sys.stderr.write(f"Exception details: {exc_info}\n")

def ensure_health_history_dir():
    """Ensure the health history directory exists and is writable"""
    try:
        if not os.path.exists(HealthHistoryDir):
            os.makedirs(HealthHistoryDir, mode=0o755)
        # Test if we can write to the directory
        test_file = os.path.join(HealthHistoryDir, '.test_write')
        with open(test_file, 'w') as f:
            f.write('test')
        os.remove(test_file)
    except Exception as e:
        syslog.syslog(syslog.LOG_ERR, f"Failed to ensure health history directory: {e}")
        raise

def ensure_health_history_file():
    """Ensure the health history file exists and has proper permissions"""
    try:
        if not os.path.exists(HealthHistoryFile):
            # Create the file with proper permissions
            with open(HealthHistoryFile, 'w') as f:
                f.write('')  # Create empty file
            os.chmod(HealthHistoryFile, 0o644)
            syslog.syslog(syslog.LOG_INFO, f"Created health history file: {HealthHistoryFile}")
    except Exception as e:
        syslog.syslog(syslog.LOG_ERR, f"Failed to create health history file: {e}")
        raise

def rotate_health_history():
    """Rotate health history logs when smond starts/restarts"""
    try:
        ensure_health_history_dir()
        # If health_history exists, rotate all logs
        if os.path.exists(HealthHistoryFile):
            # Remove the oldest log if it exists
            oldest_log = f"{HealthHistoryFile}.{MAX_ROTATED_LOGS}"
            if os.path.exists(oldest_log):
                os.remove(oldest_log)
            
            # Rotate all logs
            for i in range(MAX_ROTATED_LOGS - 1, 0, -1):
                current_log = f"{HealthHistoryFile}.{i}"
                next_log = f"{HealthHistoryFile}.{i + 1}"
                if os.path.exists(current_log):
                    if os.path.exists(next_log):
                        os.remove(next_log)
                    os.rename(current_log, next_log)
            
            # Rotate current log to .1
            os.rename(HealthHistoryFile, f"{HealthHistoryFile}.1")
        
        # Ensure the new health history file exists
        ensure_health_history_file()
    except Exception as e:
        syslog.syslog(syslog.LOG_ERR, f"Failed to rotate health history logs: {e}")

def log_health_history(component, old_state, new_state, details=""):
    """Log state changes to health history file"""
    try:
        ensure_health_history_dir()
        ensure_health_history_file()
        
        # Skip if state hasn't changed and it's not the first time we're seeing this component
        if old_state == new_state and component in component_states:
            return
            
        timestamp = datetime.datetime.now().isoformat()
        log_msg = ""
        
        if old_state == 'OK' and new_state == 'BAD':
            # Error level log for OK->BAD transition
            log_msg = f"[{timestamp}] ERROR: {component} changed from {old_state} to {new_state}"
            if details:
                log_msg += f": {details}"
            syslog.syslog(syslog.LOG_ERR, log_msg)
        elif old_state == 'BAD' and new_state == 'OK':
            # Info level log for BAD->OK transition with "Cleared:" prefix
            log_msg = f"[{timestamp}] Cleared: {component} changed from {old_state} to {new_state}"
            if details:
                log_msg += f": {details}"
            syslog.syslog(syslog.LOG_INFO, log_msg)
        elif old_state == 'UNKNOWN' and new_state == 'OK':
            # Info level log for UNKNOWN->OK transition
            log_msg = f"[{timestamp}] INFO: {component} initialized to {new_state}"
            if details:
                log_msg += f": {details}"
            syslog.syslog(syslog.LOG_INFO, log_msg)
        elif old_state == 'UNKNOWN' and new_state == 'BAD':
            # Error level log for UNKNOWN->BAD transition
            log_msg = f"[{timestamp}] ERROR: {component} initial state is {new_state}"
            if details:
                log_msg += f": {details}"
            syslog.syslog(syslog.LOG_ERR, log_msg)
        elif component not in component_states:
            # Log initial state for any component
            if new_state == 'BAD':
                log_msg = f"[{timestamp}] ERROR: {component} initial state is {new_state}"
            else:
                log_msg = f"[{timestamp}] INFO: {component} initial state is {new_state}"
            if details:
                log_msg += f": {details}"
            syslog.syslog(syslog.LOG_ERR if new_state == 'BAD' else syslog.LOG_INFO, log_msg)
        
        if log_msg:
            with open(HealthHistoryFile, 'a') as f:
                f.write(log_msg + "\n")
                f.flush()  # Ensure the write is completed
                os.fsync(f.fileno())  # Force write to disk
        
        # Update the component state
        component_states[component] = new_state
    except Exception as e:
        syslog.syslog(syslog.LOG_ERR, f"Failed to write to health history: {e}")
        syslog.syslog(syslog.LOG_ERR, f"Error details: {str(e)}")
        syslog.syslog(syslog.LOG_ERR, f"Component: {component}, Old State: {old_state}, New State: {new_state}")

# Classes
#
class ArgParseError(RuntimeError):
    pass

class SMONDRuntimeError(RuntimeError):
    pass

#-------------------------------------------------------------------------------
#
# Functions
#

#--------------------
#
# warning formats
#

def smondwarn(message, category, filename, lineno, line=None):
    return '%s:%s : %s : %s\n' % (filename, lineno, category.__name__, message)

#--------------------
#
# check to see if an instance is already running
#
def already_running(pidfile):
    myname=os.path.basename(sys.argv[0])
    try:
        if not os.path.isfile(pidfile):
            return False
        oldpid = re.findall('\D*(\d+).*', (open(pidfile, 'r').readline()))[0]
        if not os.path.exists('/proc/%s' % oldpid):
            return False
        if myname not in open('/proc/%s/cmdline' % oldpid, 'r').readline():
            return False
        sys.stderr.write("%s already running as process %s\n" % (myname, oldpid))
        return True
    except Exception as inst:
        raise SMONDRuntimeError("unable to validate pidfile %s: %s" %
                                (pidfile, str(inst)))

#--------------------
#
# normal exit
#
def exit_normally(signum=0, frame=None):
    setHaltSensorReads(1)
    syslog.syslog(syslog.LOG_INFO, "exiting normally")
    sys.stderr.write("%s : exiting normally\n" % str(sys.argv[0]))
    exit(0)

#
# Main
#
def main():
    try:
        # Initialize syslog first
        syslog.openlog("smond", syslog.LOG_PID)
        syslog.syslog(syslog.LOG_INFO, "Starting smond service")
        
        # Initialize configuration variables
        interval = 10
        crit_action_path = None
        thres_action_path = None
        interval_count = 0
        fan_dir_syslog_period = 30  # Default to 30 intervals
        
        # Load configuration
        path = '/etc/smond.conf'
        config = configparser.ConfigParser()
        if os.path.isfile(path):
            config.read(path)
            if config.has_option("DEFAULT", "interval"):
                interval = int(config.get("DEFAULT", "interval"))
                fan_dir_syslog_period = 300 / interval  # 5 minutes in intervals
            if config.has_option("DEFAULT", "crit_action_path"):
                crit_action_path = config.get("DEFAULT", "crit_action_path")
            if config.has_option("DEFAULT", "thres_action_path"):
                thres_action_path = config.get("DEFAULT", "thres_action_path")

        # Initialize health history logging
        try:
            rotate_health_history()
        except Exception as e:
            log_startup_error(f"Failed to initialize health history: {str(e)}")
            raise

        pidfile = "/var/run/smond.pid"
        if already_running(pidfile):
            sys.exit()
        else:
            open(pidfile, 'w').write(str(os.getpid()))

        # Create the smond reload file
        if not os.path.exists(ReloadDir):
            os.makedirs(ReloadDir)
        reload_file = open(ReloadFile, "w")
        reload_file.close()        

        init_ps_redundant_config()

        syslog.syslog(syslog.LOG_INFO, "Monitoring HW Sensors")
        while True:
            # Check if we need to do a reload of smond
            if os.path.exists(ReloadFile):
                os.remove(ReloadFile)            

                p = cumulus.platforms.probe()
                # Since p.sensors is created one time at init and never
                # reinstantiated, the sensors are not recreated without
                # an explicit function call
                if hasattr(p.sensors, 'reprobe'):
                    p.sensors.reprobe()
                    
                units = []

                if p.sensors is None:
                    syslog.syslog(syslog.LOG_INFO, "smond doesnt manage sensors on this platform")
                    # This dir is needs to satisfy ledmgrd and smonctl requirements.
                    status_path = '/run/cache/cumulus/unit_state'
                    if not os.path.exists(status_path):
                        os.makedirs(status_path)
                    units = None
                else:
                    for unit in p.sensors.units:
                        cp = unit
                        if os.path.exists(cp.status_path):
                            shutil.rmtree(cp.status_path, ignore_errors=True)
                        if p.cpld is not None:
                            cp.update_cpld_path(p.cpld.path)
                        if cp.attrs['type'] == 'temp':
                            cp.update_crit_action_path(crit_action_path)
                            cp.update_thres_action_path(thres_action_path)
                        units.append(cp)

                # This file is created when all sensors have been updated in
                # /run/cache/cumulus/unit_state. ledmgrd is a consumer of this
                # file.
                done_path = '/run/cache/cumulus/unit_state_init_done'
                if os.path.isfile(done_path):
                    os.remove(done_path)

            if units:
                b2f = f2b = 0
                for unit in units:
                    old_state = unit.attrs['state']
                    unit.run_state()
                    new_state = unit.attrs['state']
                    
                    if old_state != new_state or unit.attrs['name'] not in component_states:
                        details = f"Value: {unit.attrs.get('value', 'N/A')}"
                        log_health_history(unit.attrs['name'], old_state, new_state, details)
                        
                        msg = '%s' %unit.attrs['name']
                        if unit.attrs['name'] != unit.attrs['description']:
                            msg += '(%s)' %unit.attrs['description']
                        msg += ': state changed from %s to %s' \
                            %(unit.attrs['prev_state'], unit.attrs['state'])
                        syslog.syslog(syslog.LOG_WARNING, msg)
                    if unit.attrs['msg'] is not None and \
                        unit.attrs['msg'] != unit.attrs['prev_msg']:
                        syslog.syslog(syslog.LOG_WARNING, unit.attrs['msg'])

                    # System & PSU fan direction monitoring
                    if unit.attrs['type'] == 'fan':
                        dir = unit.get_fan_dir()
                        if dir == 'B2F':
                            b2f += 1
                        elif dir == 'F2B':
                            f2b += 1

                # If we have a mix of B2F and F2B fans, generate a critical level syslog message
                if b2f > 0 and f2b > 0 and interval_count % fan_dir_syslog_period == 0:
                    msg = 'Fan direction mismatch! (%d B2F fans, %d F2B fans)' % (b2f,f2b)
                    syslog.syslog(syslog.LOG_CRIT, msg)
                    
            if not os.path.isfile(done_path):
                open(done_path, 'a').close()

                # Tell systemd that we are initialized and ready
                # Done after first past, because this is what ledmgrd
                # is looking for, and the file is removed just prior to
                # the main loop being entered
                rc = cumulus.sdnotify.sd_notify(0, bytes('READY=1', 'ASCII'))
           
            # Update system status files
            update_system_status_files()
            
            interval_count += 1
            time.sleep(interval)
            if getHaltSensorReads():
                return 0 
        return 0
    except Exception as e:
        syslog.syslog(syslog.LOG_ERR, f"Error in main(): {str(e)}")
        sys.stderr.write(f"{sys.argv[0]} : ERROR : {str(e)}\n")
        exit(1)

def check_thermal_alarms():
    """
    Checks physical ports for module temperature alarms/warnings.
    To reduce CPU impact, scan a small batch of physical ports per invocation
    and rotate across calls. Returns 'BAD' if any temperature alarm/warning
    is found in the current batch; otherwise, returns 'OK'.

    Behavior details:
    - Deduplicate to physical ports (swpX) from /sys/module/sx_netdev/swp*
    - Persist the scan index in /run so subsequent calls continue where the
      previous left off
    - Use 'ethtool -m <port>' and look for temperature alarm/warning flags
    """
    IDX_FILE = "/run/smond_thermal_idx"
    BATCH_SIZE = 5

    # Discover all interface directories (may include sub-ports)
    iface_dirs = glob.glob("/sys/module/sx_netdev/swp*")
    if not iface_dirs:
        return 'OK'

    # Derive a stable, unique list of physical ports (strip sub-port suffix)
    physical_ports = set()
    for iface_dir in iface_dirs:
        iface = os.path.basename(iface_dir)
        physical_port = re.sub(r'(swp\d+)s\d+.*', r'\1', iface)
        physical_ports.add(physical_port)
    port_list = sorted(physical_ports)
    if not port_list:
        return 'OK'

    # Load last index (persisted across runs) and compute this batch
    idx = 0
    try:
        if os.path.isfile(IDX_FILE):
            with open(IDX_FILE, 'r') as f:
                raw = f.read().strip()
                if raw:
                    idx = int(raw)
    except Exception:
        idx = 0
    if idx < 0:
        idx = 0
    idx %= len(port_list)
    end = min(idx + BATCH_SIZE, len(port_list))
    batch = port_list[idx:end]

    # Pre-compile pattern for temperature alarms/warnings
    pattern = re.compile(r"Module temperature (high|low) (alarm|warning)\s*:\s*On", re.IGNORECASE)

    for port in batch:
        # Optional optimization: skip if no module present for the base port dir
        port_dir = None
        for iface_dir in iface_dirs:
            if os.path.basename(iface_dir) == port:
                port_dir = iface_dir
                break
        # Prefer breakout subport (s0) for ethtool if present (e.g., swp1s0)
        target_port = port
        breakout_dir = None
        breakout_name = f"{port}s0"
        for iface_dir in iface_dirs:
            if os.path.basename(iface_dir) == breakout_name:
                breakout_dir = iface_dir
                break
        if breakout_dir:
            target_port = breakout_name

        if port_dir:
            present_path = os.path.join(port_dir, "module", "present")
            if os.path.isfile(present_path):
                try:
                    with open(present_path, "r") as f:
                        present = f.read().strip()
                    if int(present) != 1:
                        # Module not present; skip ethtool
                        continue
                except Exception:
                    # If present cannot be read, fall through and try ethtool
                    pass

        # Query module diagnostics and parse alarm flags
        try:
            result = subprocess.run(
                ["ethtool", "-m", target_port],
                stdout=subprocess.PIPE,
                stderr=subprocess.DEVNULL,
                text=True,
                check=True
            )
            output = result.stdout
            # Throttle immediately after ethtool to avoid bursty calls
            try:
                time.sleep(0.1)
            except Exception:
                pass
        except Exception:
            # Ignore ports where ethtool fails and continue
            continue

        if pattern.search(output):
            return 'BAD'

    # Advance and persist next starting index (fixed progression; wrap to 0 after last batch)
    try:
        if idx + BATCH_SIZE >= len(port_list):
            next_idx = 0
        else:
            next_idx = idx + BATCH_SIZE
        with open(IDX_FILE, 'w') as f:
            f.write(str(next_idx))
    except Exception:
        pass

    return 'OK'

def check_cpu_logs():
    """
    Searches /var/log/syslog for CPU-related critical messages.
    Returns 'BAD' if any critical CPU or load average message is found in the last 5 minutes.
    """
    five_minutes_ago = time.time() - 5 * 60
    try:
        with open("/var/log/syslog", "r") as f:
            for line in f:
                # Check for both CPU usage and load average critical messages
                if "Critically high CPU use:" in line or "Critically high load average:" in line:
                    try:
                        # Extract timestamp from syslog line
                        timestamp_str = line.split()[0]  # Get the timestamp part
                        # Convert to datetime object
                        log_time = datetime.datetime.strptime(timestamp_str, "%Y-%m-%dT%H:%M:%S.%f%z")
                        # Convert to seconds since epoch
                        log_time_seconds = log_time.timestamp()
                        
                        if log_time_seconds >= five_minutes_ago:
                            # If we find a "no longer critically high" message, check if it's recent
                            if "no longer critically high" in line:
                                continue
                            return 'BAD'
                    except Exception as e:
                        syslog.syslog(syslog.LOG_ERR, f"Error parsing syslog timestamp: {str(e)}")
                        continue
    except Exception as e:
        syslog.syslog(syslog.LOG_ERR, f"Error reading syslog: {str(e)}")
        pass
    return 'OK'

def get_services_status():
    """
    Checks the active status of a set of services and returns a list of dictionaries.
    """
    services = ["switchd", "frr", "nvued", "lldpd", "cumulus-platform",
        "hw-management-sync", "hw-management-tc", "hw-management", "ledmgrd",
        "mft", "rasdaemon", "rsyslog", "smond", "sx_sdk", "update-ports"]
    status_list = []
    for service in services:
        try:
            proc = subprocess.run(
                ["systemctl", "is-active", service],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
                check=False
            )
            status = proc.stdout.strip()
        except Exception:
            status = "unknown"
        status_list.append({"service": service, "status": status})
    return status_list

def get_memory_status():
    """
    Determines memory utilization and returns a dictionary including the usage percentage and status.
    If usage (as a percent) is above the threshold of 90, the status is "BAD".
    Otherwise, the status is "OK".
    """
    THRESHOLD = 90
    used_mem_percent = None
    try:
        import psutil
        used_mem_percent = psutil.virtual_memory().percent
    except ImportError:
        try:
            proc = subprocess.run(["free"], stdout=subprocess.PIPE, text=True, check=True)
            for line in proc.stdout.splitlines():
                if line.startswith("Mem:"):
                    parts = line.split()
                    if len(parts) >= 3:
                        total = float(parts[1])
                        used = float(parts[2])
                        used_mem_percent = round((used / total) * 100)
                    break
        except Exception:
            pass
    if used_mem_percent is None:
        return {"memory_usage_percent": None, "status": "unknown"}
    elif used_mem_percent > THRESHOLD:
        return {"memory_usage_percent": used_mem_percent, "status": "BAD"}
    else:
        return {"memory_usage_percent": used_mem_percent, "status": "OK"}

def check_disk_status():
    """
    Checks root ('/') and /var filesystem usage and returns a dict for each mount point.
    Uses a 95% threshold for 'Not OK'.
    """
    mount_points = {'/': 'diskCheck', '/var': 'var-log'}
    result = {}
    for mount, name in mount_points.items():
        try:
            st = os.statvfs(mount)
            total = st.f_blocks * st.f_frsize
            free = st.f_bfree * st.f_frsize
            used_percent = 100 - (free / total * 100)
            if used_percent > 95:
                result[name] = {"type": "Software", "state": "Not OK", "used_percent": used_percent}
                result["process"] = {"type": "Software", "state": "Not OK"}
            else:
                result[name] = {"type": "Software", "state": "OK", "used_percent": used_percent}
        except Exception as e:
            result[name] = {"type": "Software", "state": "unknown", "used_percent": None}
    if result:
        # For compatibility with old code, keep 'disk' as alias for '/'
        if 'diskCheck' in result:
            result['disk'] = result['diskCheck']
    return result

def check_asic_status():
    """
    Checks for Mellanox ASIC presence and ASIC thermal reset condition.
    Returns a dict as described in platform_asic_status_get.
    """
    try:
        command = 'if lspci | grep -qi "Mell"; then echo "OK"; else echo "Not OK"; fi'
        result = subprocess.run(command, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        output = result.stdout.decode().strip()
        reset_thermal_asic_path = "/var/run/hw-management/system/reset_asic_thermal"
        # Check if system was rebooted due to ASIC thermal shutdown
        if os.path.exists(reset_thermal_asic_path):
            try:
                with open(reset_thermal_asic_path, 'r') as f:
                    reset_value = f.read().strip()
                if reset_value == '1':
                    output = "Not OK[(Thermal Reset)]"
            except Exception:
                pass
        return {"ASIC": {"type": "ASIC", "state": output}}
    except Exception as e:
        syslog.syslog(syslog.LOG_ERR, f"Could not get asic info: {str(e)}")
        return {"ASIC": {"type": "ASIC", "state": "unknown"}}

def update_system_status_files():
    """
    Creates state files for system health indicators in the same format
    as sensor units for ledmgrd to consume
    """
    status_path = '/run/cache/cumulus/unit_state'
    # Check services status first to determine overall service state
    services = get_services_status()
    services_state = 'OK'
    failed_services = []
    for service in services:
        if service['status'] != 'active':
            services_state = 'BAD'
            failed_services.append(service['service'])
    # Define the system status components and their mapping functions
    status_components = {
        'ThermalAlarm': lambda: check_thermal_alarms(),
        'CpuStatus': lambda: check_cpu_logs(),
        'MemoryStatus': lambda: get_memory_status()['status'],
        'ServicesStatus': lambda: services_state,
    }
    # Disk status (may return multiple keys)
    disk_status = check_disk_status()
    # ASIC status (returns a dict)
    asic_status = check_asic_status()
    # Add disk and asic keys to status_components
    for k, v in disk_status.items():
        status_components[k] = lambda v=v: v['state']
    for k, v in asic_status.items():
        status_components[k] = lambda v=v: v['state']
    # Create state files for each component
    for component, get_state in status_components.items():
        component_dir = os.path.join(status_path, component)
        if not os.path.exists(component_dir):
            os.makedirs(component_dir)
        # Write state file
        state_file = os.path.join(component_dir, 'state')
        try:
            # Read old state if it exists
            try:
                with open(state_file, 'r') as f:
                    old_state = f.read().strip()
            except:
                old_state = 'UNKNOWN'
            # Get new state
            new_state = get_state()
            # Write new state
            with open(state_file, 'w') as f:
                f.write(f"{new_state}\n")
            # Log state change to health history
            details = ""
            if component == 'MemoryStatus':
                mem_status = get_memory_status()
                details = f"Memory usage: {mem_status['memory_usage_percent']}%"
            elif component == 'ServicesStatus' and failed_services:
                details = f"Failed services: {', '.join(failed_services)}"
            elif component in disk_status:
                disk = disk_status[component]
                if disk['used_percent'] is not None:
                    details = f"Disk usage: {disk['used_percent']:.1f}%"
            elif component in asic_status:
                asic = asic_status[component]
                details = f"ASIC state: {asic['state']}"
            log_health_history(component, old_state, new_state, details)
        except Exception as e:
            syslog.syslog(syslog.LOG_ERR, f"Failed to write state file for {component}: {e}")
        # Write dump file with additional information
        dump_file = os.path.join(component_dir, 'dump')
        try:
            attrs = {
                'name': component,
                'description': component,
                'type': 'system',
                'state': new_state,
                'prev_state': old_state
            }
            if component == 'ServicesStatus':
                attrs['services'] = services
                if new_state == 'BAD':
                    attrs['msg'] = f"Services not active: {', '.join(failed_services)}"
                else:
                    attrs['msg'] = "All services are active"
            elif component == 'MemoryStatus':
                mem_status = get_memory_status()
                attrs['msg'] = f"Memory usage: {mem_status['memory_usage_percent']}%"
            elif component in disk_status:
                disk = disk_status[component]
                if disk['used_percent'] is not None:
                    attrs['msg'] = f"Disk usage: {disk['used_percent']:.1f}%"
            elif component in asic_status:
                asic = asic_status[component]
                attrs['msg'] = f"ASIC state: {asic['state']}"
            with open(dump_file, 'wb') as f:
                pickle.dump(attrs, f)
        except Exception as e:
            syslog.syslog(syslog.LOG_ERR, f"Failed to write dump file for {component}: {e}")


"""
    Checks for ps_redundant.conf file and initializes it based on VPD data.
    If ps_redundant.conf doesn't exist:
        - Checks VPD data file for MAX_PSU and MIN_PSU
        - If both values exist, writes PS_R_GRID_REDUNDANCY to config
        - If values don't exist, writes PS_R_REDUNDANCY_NOT_SUPPORTED to config
"""

def init_ps_redundant_config():
        PS_REDUNDANT_CONFIG_FILE = "/etc/cumulus/ps_redundant.conf"
        VPD_DATA_FILE = "/var/run/hw-management/eeprom/vpd_data"
        # PS redundancy config values:
        # 0 = No redundancy(System must have MIN_PSU number of psus)
        # 1 = PS-Redundant(System must have MIN_PSU + 1 number of psus)
        # 2 = Grid redundancy(System must have MIN_PSU * 2 num of psus)
        # 5 = Redundancy configuration not supported

        PS_R_GRID_REDUNDANCY = 2
        PS_R_REDUNDANCY_NOT_SUPPORTED = 5
        CONFIG_FILE_PS_R_POLICY = "psu_redundancy_policy"

        # Check if ps_redundant.conf exists
        if os.path.exists(PS_REDUNDANT_CONFIG_FILE):
            return

        # Check VPD data file for MAX_PSU and MIN_PSU
        has_min_psu = 0
        has_max_psu = 0

        if os.path.exists(VPD_DATA_FILE):
            try:
                with open(VPD_DATA_FILE, 'r') as f:
                    for line in f:
                        line = line.strip()
                        if line.startswith('MIN_PSU'):
                            has_min_psu = 1
                        elif line.startswith('MAX_PSU'):
                            has_max_psu = 1
                        # If we found both, we can break early
                        if has_min_psu == 1 and has_max_psu == 1:
                            break
            except (FileNotFoundError, PermissionError) as e:
                syslog.syslog(syslog.LOG_WARNING, "Error reading VPD data file: %s", str(e))
                has_min_psu = 0
                has_max_psu = 0
        # Write appropriate value to ps_redundant.conf
        try:
            with open(PS_REDUNDANT_CONFIG_FILE, 'w') as ps_r_config_file:
                if has_min_psu == 1 and has_max_psu == 1:
                    syslog.syslog(syslog.LOG_WARNING, "PS-R supported,Writing PS_R_GRID_REDUNDANCY to config")
                    ps_r_config_file.write(f"{CONFIG_FILE_PS_R_POLICY} = {PS_R_GRID_REDUNDANCY}")
                else:
                    syslog.syslog(syslog.LOG_WARNING, "PS-R Not supported, Writing PS_R_REDUNDANCY_NOT_SUPPORTED to config")
                    ps_r_config_file.write(f"{CONFIG_FILE_PS_R_POLICY} = {PS_R_REDUNDANCY_NOT_SUPPORTED}")
        except (PermissionError, IOError) as e:
            syslog.syslog(syslog.LOG_WARNING, "Error writing to ps_redundant.conf: %s", str(e))


#--------------------
#
# execution check
#
if __name__ == "__main__":
    try:
        signal.signal(signal.SIGTERM, exit_normally)
        # Cause all warnings to always be triggered.
        warnings.simplefilter("always")
        warnings.formatwarning = smondwarn
        syslog.openlog(": %s : " % sys.argv[0])
        syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_INFO))
        exit(main())
    except SMONDRuntimeError as errstr:
        syslog.syslog(syslog.LOG_ERR, "ERROR : %s" % str(errstr))
        sys.stderr.write("%s : ERROR : %s\n" % (sys.argv[0], str(errstr)))
        exit(1)
    except KeyboardInterrupt:
        exit_normally()
    except Exception:
        (exc_type, exc_value, exc_traceback) = sys.exc_info()
        err = ''.join(traceback.format_exception(exc_type, exc_value, exc_traceback))
        log = 'Unhandled Exception : %s' % err
        syslog.syslog(syslog.LOG_ERR, log)
        sys.stderr.write(log)
