跳到主內容

OCS 類別裡刪除軟體名稱未更新

  BUBU 因客人有反應在 OCS Inventory 的軟體類別有做好分類,但某個分類裡誤填了其他軟體名稱,但刪除了去查看該分類還是有存剛剛所填的軟體名稱。以下是透過 AI 生成程式來修補此問題,原生的不知道是那段有問題造成無法更新。

執行過程


  程式是用 python 去執行,因此有需要安裝必要的套件才能正常使用

安裝相關套件

  • 安裝必要套件
apt install -y libmariadb-dev python3-venv python3-dev -y
  • 建議 python 虛擬環境
python3 -m venv ./venv
  • 進入到虛擬環境
source ./venv/bin/activate
  • 安裝 mariadb 套件
pip3 install mariadb
程式執行

  • 分成兩支程式來處理

  • 建立模組 vim ocs_module.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
OCS Inventory 軟體類別清理工具 - 輔助模組
提供用於清理 OCS Inventory 軟體類別的功能函數
"""

import mariadb
import logging
import re
import sys
from datetime import datetime

# 建立日誌記錄器
logger = logging.getLogger("OCS-Cleanup")


def connect_to_database(host, user, password, database, port=3306):
    """
    連接到 OCS Inventory 資料庫
    
    參數:
    host (str): 資料庫服務器主機名或 IP
    user (str): 資料庫使用者名稱
    password (str): 資料庫密碼
    database (str): 資料庫名稱
    port (int): 資料庫端口,默認為 3306
    
    返回:
    connection: 資料庫連接對象
    """
    try:
        print(f"嘗試連接資料庫 {host}:{port}...")
        # 嘗試建立與 MariaDB 資料庫的連接
        connection = mariadb.connect(
            host=host,
            user=user,
            password=password,
            database=database,
            port=port
        )
        print("資料庫連接成功")
        logger.info("成功連接到資料庫")
        return connection
    except mariadb.Error as err:
        # 如果連接失敗,記錄錯誤並退出程序
        error_msg = f"資料庫連接錯誤: {err}"
        print(error_msg)
        logger.error(error_msg)
        sys.exit(1)


def get_valid_software_expressions(connection, category_id):
    """
    獲取指定類別的有效軟體表達式
    
    參數:
    connection: 資料庫連接對象
    category_id (int): 軟體類別的 ID
    
    返回:
    list: 該類別的所有軟體表達式列表
    """
    cursor = connection.cursor(dictionary=True)
    try:
        # 執行 SQL 查詢,獲取指定類別的軟體表達式
        cursor.execute(
            "SELECT ID, SOFTWARE_EXP, SIGN_VERSION, VERSION, PUBLISHER FROM software_category_exp WHERE CATEGORY_ID = ?",
            (category_id,)
        )
        expressions = cursor.fetchall()
        logger.info(f"找到 {len(expressions)} 個類別 {category_id} 的軟體表達式")
        return expressions
    except mariadb.Error as err:
        error_msg = f"查詢軟體表達式錯誤: {err}"
        print(error_msg)
        logger.error(error_msg)
        return []
    finally:
        cursor.close()


def get_all_software_categories(connection):
    """
    獲取所有軟體類別清單
    
    參數:
    connection: 資料庫連接對象
    
    返回:
    list: 所有軟體類別的列表,每個類別是包含 ID 和 CATEGORY_NAME 的字典
    """
    cursor = connection.cursor(dictionary=True)
    try:
        print("正在查詢所有軟體類別...")
        # 執行 SQL 查詢,獲取所有軟體類別
        cursor.execute("SELECT ID, CATEGORY_NAME, OS FROM software_categories ORDER BY CATEGORY_NAME")
        categories = cursor.fetchall()
        print(f"找到 {len(categories)} 個軟體類別")
        logger.info(f"找到 {len(categories)} 個軟體類別")
        return categories
    except mariadb.Error as err:
        error_msg = f"查詢軟體類別錯誤: {err}"
        print(error_msg)
        logger.error(error_msg)
        return []
    finally:
        cursor.close()


def get_linked_software(connection, category_id):
    """
    獲取已連結到該類別的軟體
    
    參數:
    connection: 資料庫連接對象
    category_id (int): 軟體類別的 ID
    
    返回:
    list: 連結到該類別的所有軟體列表
    """
    cursor = connection.cursor(dictionary=True)
    try:
        # 執行 SQL 查詢,連接多個表獲取軟體資訊
        query = """
        SELECT scl.ID, sn.NAME, sp.PUBLISHER, sv.VERSION, 
               sn.ID as NAME_ID, sp.ID as PUBLISHER_ID, sv.ID as VERSION_ID
        FROM software_categories_link scl
        JOIN software_name sn ON scl.NAME_ID = sn.ID
        JOIN software_publisher sp ON scl.PUBLISHER_ID = sp.ID
        JOIN software_version sv ON scl.VERSION_ID = sv.ID
        WHERE scl.CATEGORY_ID = ?
        """
        cursor.execute(query, (category_id,))
        linked_software = cursor.fetchall()
        logger.info(f"找到 {len(linked_software)} 個連結到類別 {category_id} 的軟體")
        return linked_software
    except mariadb.Error as err:
        error_msg = f"查詢連結軟體錯誤: {err}"
        print(error_msg)
        logger.error(error_msg)
        return []
    finally:
        cursor.close()


def convert_to_regex(expression):
    """
    將 OCS 通配符轉換為正則表達式
    
    參數:
    expression (str): 含有 OCS 通配符的表達式
    
    返回:
    str: 轉換後的正則表達式
    """
    # 替換通配符: ? 替換為 . (匹配任意單個字元),* 替換為 .* (匹配任意數量的任意字元)
    regex = expression.replace("?", ".").replace("*", ".*")
    # 添加開始和結束標記,確保完全匹配
    return f"^{regex}$"


def should_keep_software(software, expressions, debug=False):
    """
    根據表達式判斷軟體是否應保留
    
    參數:
    software (dict): 軟體資訊
    expressions (list): 有效的軟體表達式列表
    debug (bool): 是否輸出詳細調試資訊
    
    返回:
    tuple: (是否保留, 匹配的表達式)
    """
    software_name = software['NAME']
    software_publisher = software['PUBLISHER']
    software_version = software['VERSION']
    
    if debug:
        logger.debug(f"檢查軟體: {software_name}")
        logger.debug(f"軟體字元表示: {repr(software_name)}")
    
    for exp in expressions:
        name_pattern = convert_to_regex(exp['SOFTWARE_EXP'])
        
        if debug:
            logger.debug(f"比對表達式: {exp['SOFTWARE_EXP']} -> 正則: {name_pattern}")
            logger.debug(f"表達式字元表示: {repr(exp['SOFTWARE_EXP'])}")
        
        if re.match(name_pattern, software_name, re.IGNORECASE):
            if debug:
                logger.debug(f"名稱匹配成功")
            
            # 如果定義了發行商條件,檢查發行商是否匹配
            if exp['PUBLISHER'] and exp['PUBLISHER'] != software_publisher:
                if debug:
                    logger.debug(f"發行商不匹配: 期望 {exp['PUBLISHER']}, 實際 {software_publisher}")
                continue
            
            # 如果定義了版本條件,檢查版本是否匹配
            if exp['VERSION']:
                if exp['SIGN_VERSION'] == '=' and exp['VERSION'] != software_version:
                    if debug:
                        logger.debug(f"版本不匹配: 期望 {exp['VERSION']}, 實際 {software_version}")
                    continue
            
            # 如果通過所有條件,表示應保留該軟體
            if debug:
                logger.debug(f"所有條件匹配,軟體將被保留")
            return True, exp['SOFTWARE_EXP']
    
    # 如果沒有匹配任何表達式,表示不應保留
    if debug:
        logger.debug(f"沒有匹配任何表達式,軟體將被刪除")
    return False, None


def delete_invalid_links(connection, category_id, link_ids_to_delete):
    """
    刪除不再有效的軟體連結
    
    參數:
    connection: 資料庫連接對象
    category_id (int): 軟體類別的 ID
    link_ids_to_delete (list): 要刪除的連結 ID 列表
    
    返回:
    int: 成功刪除的記錄數
    """
    if not link_ids_to_delete:
        logger.info("沒有需要刪除的無效連結")
        return 0
    
    cursor = connection.cursor()
    try:
        # 建立用於 IN 子句的參數占位符
        placeholders = ', '.join(['?'] * len(link_ids_to_delete))
        # 構建刪除 SQL 查詢
        delete_query = f"DELETE FROM software_categories_link WHERE ID IN ({placeholders})"
        cursor.execute(delete_query, link_ids_to_delete)
        connection.commit()
        deleted_count = cursor.rowcount
        logger.info(f"成功刪除 {deleted_count} 個無效連結")
        return deleted_count
    except mariadb.Error as err:
        error_msg = f"刪除無效連結錯誤: {err}"
        print(error_msg)
        logger.error(error_msg)
        connection.rollback()
        return 0
    finally:
        cursor.close()


def reset_software_link_categories(connection, category_id, invalid_combinations):
    """
    將不再有效的軟體組合在 software_link 表中的 CATEGORY_ID 重置為 null
    
    參數:
    connection: 資料庫連接對象
    category_id (int): 軟體類別的 ID
    invalid_combinations (list): 無效的軟體組合列表,每個組合是 (NAME_ID, PUBLISHER_ID, VERSION_ID) 的元組
    
    返回:
    int: 成功更新的記錄數
    """
    if not invalid_combinations:
        logger.info("沒有需要在 software_link 中重置的無效組合")
        return 0
    
    cursor = connection.cursor()
    try:
        update_count = 0
        # 對每個無效組合執行更新
        for combo in invalid_combinations:
            name_id, publisher_id, version_id = combo
            # 構建更新 SQL 查詢,將 CATEGORY_ID 設為 NULL
            reset_query = """
            UPDATE software_link 
            SET CATEGORY_ID = NULL 
            WHERE NAME_ID = ? AND PUBLISHER_ID = ? AND VERSION_ID = ? 
            AND CATEGORY_ID = ?
            """
            cursor.execute(reset_query, (name_id, publisher_id, version_id, category_id))
            update_count += cursor.rowcount
        
        connection.commit()
        logger.info(f"已將 {update_count} 筆 software_link 資料中的 CATEGORY_ID 重置為 null")
        return update_count
    except mariadb.Error as err:
        error_msg = f"重置 software_link 中的 CATEGORY_ID 錯誤: {err}"
        print(error_msg)
        logger.error(error_msg)
        connection.rollback()
        return 0
    finally:
        cursor.close()


def display_categories_menu(categories):
    """
    顯示軟體類別選單
    
    參數:
    categories (list): 所有軟體類別的列表
    
    返回:
    None
    """
    print("\n=== 軟體類別清單 ===")
    print("ID\t類別名稱\t\t作業系統")
    print("-" * 60)
    
    for category in categories:
        # 格式化類別資訊以對齊顯示
        category_name = category['CATEGORY_NAME']
        if len(category_name) > 15:
            category_name = category_name[:12] + "..."
        
        os_info = category['OS'] if category['OS'] else "所有系統"
        
        print(f"{category['ID']}\t{category_name.ljust(15)}\t{os_info}")
    
    print("-" * 60)


def check_match_status(connection, category_id, category_name=None):
    """
    檢查軟體表達式與實際軟體的匹配情況
    
    參數:
    connection: 資料庫連接對象
    category_id (int): 軟體類別的 ID
    category_name (str, optional): 類別名稱,如果提供則顯示
    
    返回:
    None,但會在終端顯示匹配狀態
    """
    # 獲取表達式
    expressions = get_valid_software_expressions(connection, category_id)
    if not expressions:
        print(f"類別 {category_id} 沒有定義任何軟體表達式")
        return
    
    # 獲取連結的軟體
    linked_software = get_linked_software(connection, category_id)
    if not linked_software:
        print(f"類別 {category_id} 沒有連結任何軟體")
        return
    
    category_info = f"'{category_name}' " if category_name else ""
    print(f"\n=== 匹配檢查 (類別 {category_info}ID: {category_id}) ===")
    print(f"找到 {len(expressions)} 個表達式和 {len(linked_software)} 個連結軟體")
    
    print("\n表達式清單:")
    for i, exp in enumerate(expressions, 1):
        pub_info = f", 發行商: {exp['PUBLISHER']}" if exp['PUBLISHER'] else ""
        ver_info = f", 版本: {exp['SIGN_VERSION']}{exp['VERSION']}" if exp['VERSION'] else ""
        print(f"{i}. 表達式: {exp['SOFTWARE_EXP']}{pub_info}{ver_info}")
    
    print("\n軟體匹配狀態:")
    to_be_deleted = 0
    for i, software in enumerate(linked_software, 1):
        name = software['NAME']
        publisher = software['PUBLISHER']
        version = software['VERSION']
        
        keep, matching_expression = should_keep_software(software, expressions, debug=False)
        
        status = "將被保留" if keep else "將被刪除"
        match_info = f", 匹配表達式: {matching_expression}" if keep else ""
        
        if not keep:
            to_be_deleted += 1
        
        print(f"{i}. 名稱: {name}")
        print(f"   發行商: {publisher}, 版本: {version}")
        print(f"   狀態: {status}{match_info}")
        
        # 僅當軟體將被刪除時輸出字元編碼檢查
        if not keep:
            print(f"   字元編碼檢查: {repr(name)}")
        print()
    
    print(f"總計: {len(linked_software)} 個軟體中,{to_be_deleted} 個將被刪除,{len(linked_software) - to_be_deleted} 個將被保留")


def cleanup_category(connection, category_id, dry_run=False, show_match=False):
    """
    清理指定類別中不符合表達式的軟體連結
    
    參數:
    connection: 資料庫連接對象
    category_id (int): 軟體類別的 ID
    dry_run (bool): 如果為 True,只模擬執行但不實際修改資料庫
    show_match (bool): 是否顯示匹配狀態詳情
    
    返回:
    bool: 操作是否成功
    """
    # 獲取該類別的有效軟體表達式
    expressions = get_valid_software_expressions(connection, category_id)
    if not expressions:
        logger.warning(f"類別 {category_id} 沒有定義任何軟體表達式,無法清理")
        print(f"錯誤: 類別 {category_id} 沒有定義任何軟體表達式,無法清理")
        return False
    
    # 獲取已連結到該類別的軟體
    linked_software = get_linked_software(connection, category_id)
    if not linked_software:
        logger.info(f"類別 {category_id} 沒有連結任何軟體,無需清理")
        print(f"類別 {category_id} 沒有連結任何軟體,無需清理")
        return True
    
    # 如果需要顯示匹配狀態
    if show_match:
        # 查詢獲取類別名稱
        cursor = connection.cursor(dictionary=True)
        try:
            cursor.execute("SELECT CATEGORY_NAME FROM software_categories WHERE ID = ?", (category_id,))
            result = cursor.fetchone()
            category_name = result['CATEGORY_NAME'] if result else None
        except Exception as e:
            logger.error(f"獲取類別名稱錯誤: {e}")
            print(f"獲取類別名稱錯誤: {e}")
            category_name = None
        finally:
            cursor.close()
        
        # 顯示匹配狀態
        check_match_status(connection, category_id, category_name)
    
    # 找出不應保留的軟體連結
    link_ids_to_delete = []
    invalid_combinations = []
    software_to_delete = []  # 存儲要刪除的軟體詳細資訊
    
    for software in linked_software:
        # 檢查軟體是否匹配任何表達式
        keep, _ = should_keep_software(software, expressions)
        if not keep:
            link_ids_to_delete.append(software['ID'])
            invalid_combinations.append((software['NAME_ID'], software['PUBLISHER_ID'], software['VERSION_ID']))
            # 記錄將被刪除的軟體資訊
            logger.info(f"將刪除連結: ID={software['ID']}, 名稱={software['NAME']}, "
                       f"發行商={software['PUBLISHER']}, 版本={software['VERSION']}")
            # 將要刪除的軟體資訊添加到列表中
            software_to_delete.append(software)
    
    # 如果有要刪除的項目,顯示詳細信息
    if software_to_delete:
        print("\n=== 即將刪除的軟體連結 ===")
        print(f"總共 {len(software_to_delete)} 個項目將被刪除:")
        print("-" * 80)
        print(f"{'ID':<8} {'軟體名稱':<40} {'發行商':<20} {'版本':<10}")
        print("-" * 80)
        
        for sw in software_to_delete:
            name = sw['NAME']
            if len(name) > 37:
                name = name[:34] + "..."
            
            publisher = sw['PUBLISHER']
            if len(publisher) > 17:
                publisher = publisher[:14] + "..."
            
            version = sw['VERSION']
            if len(version) > 7:
                version = version[:4] + "..."
                
            print(f"{sw['ID']:<8} {name:<40} {publisher:<20} {version:<10}")
        
        print("-" * 80)
        print(f"這些項目與類別 {category_id} 的表達式不匹配,將被解除連結。")
        
        # 再次確認是否要繼續
        if not dry_run:
            confirm = input("\n確認要刪除這些項目嗎? (y/n): ")
            if confirm.lower() != 'y':
                print("已取消刪除操作。")
                return True
    else:
        print("沒有找到需要刪除的軟體連結")
        return True
    
    if dry_run:
        logger.info(f"[模擬模式] 將刪除 {len(link_ids_to_delete)} 個無效連結")
        logger.info(f"[模擬模式] 將重置 {len(invalid_combinations)} 個軟體組合在 software_link 中的 CATEGORY_ID")
        print(f"[模擬模式] 將刪除 {len(link_ids_to_delete)} 個無效連結")
        print(f"[模擬模式] 將重置 {len(invalid_combinations)} 個軟體組合在 software_link 中的 CATEGORY_ID")
        return True
    
    # 執行刪除操作
    deleted_count = delete_invalid_links(connection, category_id, link_ids_to_delete)
    
    # 重置 software_link 表中對應的 CATEGORY_ID 為 null
    if deleted_count > 0:
        reset_count = reset_software_link_categories(connection, category_id, invalid_combinations)
        print(f"成功刪除 {deleted_count} 個無效連結")
        print(f"已將 {reset_count} 筆 software_link 資料中的 CATEGORY_ID 重置為 null")
    else:
        print("沒有找到需要刪除的無效連結")
    
    return True
  • 建立腳本 vim ocs_cleanup.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
OCS Inventory 軟體類別清理工具
用於清理 OCS Inventory 軟體類別中不符合表達式的軟體連結

執行方式:
1. 直接執行 python ocs_cleanup.py 進入互動模式
2. 使用命令行參數 python ocs_cleanup.py --category-id 1 --dry-run

作者: System Engineer
版本: 1.0
日期: 2025-04-28
"""

import os
import sys
import logging
import argparse
from datetime import datetime

# 匯入模組檔案中的函數
try:
    from ocs_module import (
        connect_to_database, get_all_software_categories, get_valid_software_expressions,
        get_linked_software, check_match_status, cleanup_category, display_categories_menu,
        logger  # 從模組匯入日誌記錄器
    )
    print("成功匯入 OCS 模組")
except ImportError as e:
    print(f"錯誤: 無法匯入 OCS 模組,請確保 ocs_module.py 文件在同一目錄中: {e}")
    sys.exit(1)

# 程式開始執行
print("OCS 軟體類別清理工具啟動...")

# 資料庫連線設定
DB_CONFIG = {
    'host': '127.0.0.1',  # 資料庫服務器的 IP 地址
    'user': 'user',            # 資料庫使用者名稱 (請根據實際環境替換)
    'password': 'your_db_password',  # 資料庫密碼 (請根據實際環境替換)
    'database': 'ocsweb',      # 要連接的資料庫名稱
    'port': 3306               # 資料庫服務器端口
}

# 日誌設定
LOG_CONFIG = {
    'log_dir': 'logs',  # 日誌檔案存放的目錄
    'log_filename': 'ocs_cleanup_{datetime}.log',  # 日誌檔案名稱格式
    'log_level': logging.INFO  # 日誌級別
}

# 設定日誌系統
try:
    # 確保日誌目錄存在
    os.makedirs(LOG_CONFIG['log_dir'], exist_ok=True)
    print(f"日誌目錄已確認: {LOG_CONFIG['log_dir']}")
    
    # 生成日誌檔案名稱
    log_filename = LOG_CONFIG['log_filename'].format(datetime=datetime.now().strftime('%Y%m%d_%H%M%S'))
    log_filepath = os.path.join(LOG_CONFIG['log_dir'], log_filename)
    
    # 配置日誌處理器
    file_handler = logging.FileHandler(log_filepath)
    console_handler = logging.StreamHandler(sys.stdout)
    
    # 設定日誌格式
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    file_handler.setFormatter(formatter)
    console_handler.setFormatter(formatter)
    
    # 添加處理器到日誌記錄器
    logger.setLevel(LOG_CONFIG['log_level'])
    logger.addHandler(file_handler)
    logger.addHandler(console_handler)
    
    logger.info("日誌系統已初始化")
    print(f"日誌將被寫入: {log_filepath}")

except Exception as e:
    print(f"日誌系統初始化失敗: {e}")
    print("將繼續執行但可能無法記錄日誌")


def interactive_mode(connection):
    """
    互動模式,讓使用者通過選單選擇操作和類別
    
    參數:
    connection: 資料庫連接對象
    
    返回:
    None
    """
    try:
        print("正在啟動互動模式...")
        while True:
            # 獲取所有軟體類別
            categories = get_all_software_categories(connection)
            
            if not categories:
                print("未找到任何軟體類別,請先在 OCS Inventory 中建立軟體類別。")
                logger.warning("未找到任何軟體類別")
                break
            
            # 顯示主選單
            print("\n=== OCS 軟體類別清理工具 ===")
            print("1. 顯示所有軟體類別")
            print("2. 檢查類別匹配狀態")
            print("3. 清理類別")
            print("4. 模擬清理(不實際修改資料庫)")
            print("q. 退出程式")
            
            choice = input("\n請選擇操作 (1-4 或 q): ")
            
            if choice.lower() == 'q':
                print("程式已退出。")
                break
            
            elif choice == '1':
                # 顯示所有類別
                display_categories_menu(categories)
            
            elif choice in ['2', '3', '4']:
                # 顯示類別選單供選擇
                display_categories_menu(categories)
                
                # 提示用戶選擇類別
                category_input = input("\n請輸入要操作的軟體類別ID (輸入 b 返回主選單): ")
                
                if category_input.lower() == 'b':
                    continue
                
                try:
                    category_id = int(category_input)
                    # 檢查選擇的類別是否存在
                    category_exists = False
                    category_name = ""
                    
                    for cat in categories:
                        if cat['ID'] == category_id:
                            category_exists = True
                            category_name = cat['CATEGORY_NAME']
                            break
                    
                    if not category_exists:
                        print(f"錯誤: ID 為 {category_id} 的類別不存在,請重新選擇。")
                        continue
                    
                    if choice == '2':
                        # 檢查匹配狀態
                        check_match_status(connection, category_id, category_name)
                    
                    elif choice == '3' or choice == '4':
                        dry_run = (choice == '4')  # 如果選擇 4,則為模擬模式
                        
                        if dry_run:
                            print(f"\n=== 模擬清理類別 '{category_name}' (ID: {category_id}) ===")
                        else:
                            print(f"\n=== 清理類別 '{category_name}' (ID: {category_id}) ===")
                        
                        # 先顯示匹配狀態
                        check_match_status(connection, category_id, category_name)
                        
                        # 確認是否要清理該類別
                        confirm = input(f"\n您確定要{'模擬' if dry_run else ''}清理類別 '{category_name}' (ID: {category_id}) 嗎? (y/n): ")
                        
                        if confirm.lower() != 'y':
                            print("已取消清理操作。")
                            continue
                        
                        # 執行清理操作
                        logger.info(f"開始{'模擬' if dry_run else ''}清理類別 '{category_name}' (ID: {category_id})")
                        print(f"\n開始{'模擬' if dry_run else ''}清理類別 '{category_name}'...")
                        
                        success = cleanup_category(connection, category_id, dry_run, False)
                        
                        if success:
                            logger.info(f"類別 '{category_name}' (ID: {category_id}) {'模擬' if dry_run else ''}清理完成")
                            print(f"類別 '{category_name}' {'模擬' if dry_run else ''}清理完成,日誌已儲存到: {log_filepath}")
                        else:
                            logger.error(f"類別 '{category_name}' (ID: {category_id}) 清理失敗")
                            print(f"類別 '{category_name}' 清理失敗,請查看日誌: {log_filepath}")
                
                except ValueError:
                    print("錯誤: 類別ID必須是整數。")
            
            else:
                print("無效的選擇,請重新輸入。")
    
    except KeyboardInterrupt:
        print("\n程式已被使用者中斷。")
    except Exception as e:
        logger.error(f"互動模式發生錯誤: {e}")
        print(f"發生錯誤: {e}")
        print(f"詳細錯誤已記錄到日誌檔案: {log_filepath}")


def main():
    """
    主函數,處理命令行參數並執行清理操作
    """
    # 建立命令行參數解析器
    parser = argparse.ArgumentParser(description='OCS Inventory 軟體類別清理工具')
    # 添加各種命令行參數
    parser.add_argument('--host', default=DB_CONFIG['host'], help=f'資料庫主機位址 (預設: {DB_CONFIG["host"]})')
    parser.add_argument('--user', default=DB_CONFIG['user'], help=f'資料庫使用者名稱 (預設: {DB_CONFIG["user"]})')
    parser.add_argument('--password', default=DB_CONFIG['password'], help='資料庫密碼 (預設: [已配置])')
    parser.add_argument('--database', default=DB_CONFIG['database'], help=f'資料庫名稱 (預設: {DB_CONFIG["database"]})')
    parser.add_argument('--port', type=int, default=DB_CONFIG['port'], help=f'資料庫連接埠 (預設: {DB_CONFIG["port"]})')
    parser.add_argument('--category-id', type=int, help='要清理的軟體類別 ID')
    parser.add_argument('--dry-run', action='store_true', help='模擬執行但不實際修改資料庫')
    parser.add_argument('--check-only', action='store_true', help='只檢查匹配狀態,不執行清理')
    parser.add_argument('--log-dir', default=LOG_CONFIG['log_dir'], help=f'日誌檔案目錄 (預設: {LOG_CONFIG["log_dir"]})')
    parser.add_argument('--debug', action='store_true', help='啟用調試模式,顯示更詳細的日誌')
    
    args = parser.parse_args()  # 解析命令行參數
    
    # 如果啟用調試模式,設置日誌級別為 DEBUG
    if args.debug:
        logger.setLevel(logging.DEBUG)
        logger.debug("調試模式已啟用")
    
    # 連接資料庫
    connection = connect_to_database(
        args.host, args.user, args.password, args.database, args.port
    )
    
    try:
        if args.category_id:
            # 如果指定了類別 ID
            if args.check_only:
                # 如果只檢查匹配狀態
                check_match_status(connection, args.category_id)
            else:
                # 執行清理操作
                if args.dry_run:
                    logger.info("=== 模擬模式 ===")  # 如果是模擬模式,記錄提示
                    print("=== 模擬模式 ===")
                
                # 先顯示匹配狀態
                check_match_status(connection, args.category_id)
                
                # 不需要在這裡詢問是否繼續,因為在cleanup_category函式中已加入了詳細確認步驟
                success = cleanup_category(connection, args.category_id, args.dry_run, False)  # 執行清理
                
                if success:
                    # 如果清理成功,記錄成功信息
                    logger.info(f"類別 {args.category_id} 清理" + ("模擬 " if args.dry_run else "") + "完成")
                else:
                    # 如果清理失敗,記錄失敗信息
                    logger.error(f"類別 {args.category_id} 清理失敗")
        else:
            # 如果沒有指定類別 ID,進入互動模式
            interactive_mode(connection)
    finally:
        connection.close()  # 確保在退出前關閉資料庫連接


if __name__ == "__main__":
    # 如果直接執行此程式
    try:
        print("程式入口點啟動...")
        if len(sys.argv) == 1:
            # 無命令行參數,使用預設設定並啟動互動模式
            try:
                print("以預設設定啟動互動模式...")
                # 使用預設設定連接資料庫
                connection = connect_to_database(
                    DB_CONFIG['host'], 
                    DB_CONFIG['user'], 
                    DB_CONFIG['password'], 
                    DB_CONFIG['database'], 
                    DB_CONFIG['port']
                )
                
                # 進入互動模式
                interactive_mode(connection)
                
                # 確保關閉連接
                try:
                    connection.close()
                    print("資料庫連接已關閉")
                except:
                    pass
                    
            except Exception as e:
                # 捕獲並記錄其他任何異常
                error_msg = f"執行時發生錯誤: {e}"
                print(error_msg)
                try:
                    logger.error(error_msg)
                    print(f"詳細錯誤已記錄到日誌檔案: {log_filepath}")
                except:
                    print("無法記錄錯誤到日誌檔案")
        else:
            # 如果有提供命令行參數,使用命令行參數執行主函數
            print("以命令行參數啟動...")
            main()
    except Exception as e:
        print(f"主程式發生未捕獲的異常: {e}")
        print("請檢查您的環境設定並重試")

補充說明


備註





參考相關網頁