跳到主內容

GLPI Windows AD 同步腳本

  因 GLPI 在串 Windows AD 服務無法自動匯入使用者資訊,需手動處理同步,官方有提供指令方式來同步,我這邊一樣是用腳本方式來處理

執行過程


  • 參考官方提供的 CLI 指令來進行修改
    • --only-create-new:僅建立新使用者
    • --only-update-existing:僅更新已存在的使用者
# 新增加使用者
php bin/console glpi:ldap:synchronize_users --ldap-server-id=1 --only-create-new
# 更新使用者
php bin/console glpi:ldap:synchronize_users --ldap-server-id=1 --only-update-existing
  • 建立同步的腳本 vim glpi_ldap_sync.py 將下腳本貼上。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# 引入必要的模組
import subprocess  # 用於執行外部命令
import argparse    # 用於處理命令列參數
import os          # 用於檔案和目錄操作
import logging     # 用於記錄日誌
import datetime    # 用於獲取當前日期時間
import sys         # 用於系統操作如退出

# 預設設定字典,包含所有可配置的參數
DEFAULT_CONFIG = {
    "php_path": "php",                    # PHP 解釋器的路徑
    "console_path": "/var/www/html/glpi/bin/console",        # GLPI 控制台路徑
    "command": "glpi:ldap:synchronize_users",  # GLPI LDAP 同步命令
    "server_id": 1,                       # 預設 LDAP 伺服器 ID
    "exclude_file": "/tmp/exclude_users.txt",  # 預設排除使用者檔案名稱
    "log_dir": "/var/log/glpiAD/logs",                    # 預設日誌檔案目錄
    "sync_mode": "all"                    # 預設同步模式(全部同步)
}

# 同步模式字典,將模式名稱映射到命令列參數
SYNC_MODES = {
    "all": [],                       # 預設模式:全部同步
    "create": ["--only-create-new"],  # 只建立新使用者
    "update": ["--only-update-existing"]  # 只更新現有使用者
}

def read_exclude_list(file_path):
    """從文字檔讀取排除使用者清單
    
    Args:
        file_path (str): 排除使用者清單檔案路徑
        
    Returns:
        list: 排除使用者名稱列表
    """
    exclude_users = []  # 初始化空列表用於存儲排除的使用者
    
    # 檢查檔案是否存在
    if not os.path.exists(file_path):
        print(f"警告: 排除使用者檔案 '{file_path}' 不存在,將不排除任何使用者")
        return exclude_users  # 如果檔案不存在,返回空列表
    
    try:
        # 開啟文字檔並讀取內容
        with open(file_path, 'r', encoding='utf-8') as f:
            for line in f:  # 逐行讀取檔案
                # 移除前後空白字元並忽略空行和註解行
                username = line.strip()
                if username and not username.startswith('#'):
                    exclude_users.append(username)  # 將有效的使用者名稱加入列表
        
        return exclude_users  # 返回讀取到的排除使用者列表
    except Exception as e:  # 捕獲任何可能的錯誤
        print(f"讀取排除使用者檔案時發生錯誤: {str(e)}")
        return []  # 發生錯誤時返回空列表

def setup_logger(log_dir=None, log_name=None):
    """設定日誌記錄器
    
    Args:
        log_dir (str, optional): 日誌檔案目錄,如未指定則使用預設值
        log_name (str, optional): 日誌檔案名稱,如未指定則自動生成
        
    Returns:
        tuple: (logger物件, 日誌檔案完整路徑)
    """
    if log_name is None:
        # 如果未指定日誌名稱,使用當前時間創建格式化的檔案名
        timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        log_name = f"glpi_ldap_sync_{timestamp}.log"
    
    if log_dir is None:
        # 如果未指定日誌目錄,使用預設值
        log_dir = DEFAULT_CONFIG["log_dir"]
    
    # 確保日誌目錄存在,如不存在則建立
    os.makedirs(log_dir, exist_ok=True)
    log_path = os.path.join(log_dir, log_name)  # 組合完整的日誌檔案路徑
    
    # 配置日誌格式和處理器
    logging.basicConfig(
        level=logging.INFO,  # 設定日誌級別為 INFO
        format='%(asctime)s - %(levelname)s - %(message)s',  # 設定日誌格式
        handlers=[
            logging.FileHandler(log_path, encoding='utf-8'),  # 寫入到檔案
            logging.StreamHandler(sys.stdout)  # 同時輸出到標準輸出
        ]
    )
    
    return logging.getLogger(), log_path  # 返回日誌器物件和日誌檔案路徑

def build_ldap_filter(exclude_users):
    """建構 LDAP 過濾條件
    
    Args:
        exclude_users (list): 要排除的使用者列表
        
    Returns:
        str: LDAP 過濾條件字串
    """
    if not exclude_users:
        # 如果沒有排除的使用者,返回基本過濾條件
        return "(objectClass=user)"
    
    # 建構排除使用者的過濾條件,為每個使用者建立一個「非」條件
    exclude_filter_parts = []
    for user in exclude_users:
        exclude_filter_parts.append(f"(!(sAMAccountName={user}))")
    
    # 組合完整的 LDAP 過濾條件,使用 AND 連接所有條件
    return f"(&(objectClass=user){''.join(exclude_filter_parts)})"

def run_glpi_sync(server_id=None, exclude_users=None, exclude_file=None, log_dir=None, auto_confirm=False, sync_mode="all"):
    """執行 GLPI LDAP 同步程序
    
    Args:
        server_id (int, optional): LDAP 伺服器 ID
        exclude_users (list, optional): 從命令列指定的排除使用者列表
        exclude_file (str, optional): 包含排除使用者的檔案路徑
        log_dir (str, optional): 日誌檔案目錄
        auto_confirm (bool, optional): 是否自動確認提示
        sync_mode (str, optional): 同步模式 (all/create/update)
        
    Returns:
        tuple: (退出碼, 日誌檔案路徑)
    """
    # 使用預設值(如果未指定)
    if server_id is None:
        server_id = DEFAULT_CONFIG["server_id"]
    
    if log_dir is None:
        log_dir = DEFAULT_CONFIG["log_dir"]
    
    # 確認同步模式是否有效
    if sync_mode not in SYNC_MODES:
        print(f"警告: 無效的同步模式 '{sync_mode}',使用預設的 'all' 模式")
        sync_mode = "all"
    
    # 設定日誌
    logger, log_path = setup_logger(log_dir)
    
    logger.info("開始 GLPI LDAP 同步作業")
    logger.info(f"LDAP 伺服器 ID: {server_id}")
    logger.info(f"同步模式: {sync_mode}")
    
    # 處理排除使用者清單
    all_exclude_users = []
    
    # 如果有從參數傳入的排除使用者,加入總列表
    if exclude_users:
        all_exclude_users.extend(exclude_users)
    
    # 如果有指定排除使用者檔案,從檔案讀取並加入總列表
    if exclude_file:
        file_exclude_users = read_exclude_list(exclude_file)
        if file_exclude_users:
            logger.info(f"從檔案 '{exclude_file}' 讀取到 {len(file_exclude_users)} 個排除使用者")
            all_exclude_users.extend(file_exclude_users)
    
    # 去除重複的使用者名稱
    all_exclude_users = list(set(all_exclude_users))
    
    if all_exclude_users:
        logger.info(f"總共排除 {len(all_exclude_users)} 個使用者")
        logger.info(f"排除的使用者: {', '.join(all_exclude_users)}")
    
    # 建立 LDAP 過濾條件
    ldap_filter = build_ldap_filter(all_exclude_users)
    logger.info(f"使用的 LDAP 過濾條件: {ldap_filter}")
    
    # 建立要執行的命令列表
    command = [
        DEFAULT_CONFIG["php_path"],                   # PHP 路徑
        DEFAULT_CONFIG["console_path"],               # GLPI 控制台路徑
        DEFAULT_CONFIG["command"],                    # GLPI LDAP 同步命令
        f"--ldap-server-id={server_id}",              # LDAP 伺服器 ID 參數
        f"--ldap-filter={ldap_filter}"                # LDAP 過濾條件參數
    ]
    
    # 添加同步模式特定的參數
    command.extend(SYNC_MODES[sync_mode])
    
    logger.info(f"執行命令: {' '.join(command)}")
    
    # 執行命令
    try:
        # 如果設定自動確認,則提供「yes」作為輸入
        input_text = "yes\n" if auto_confirm else None
        # 使用 subprocess 創建新進程並執行命令
        process = subprocess.Popen(
            command,
            stdin=subprocess.PIPE,    # 重定向標準輸入
            stdout=subprocess.PIPE,   # 重定向標準輸出
            stderr=subprocess.PIPE,   # 重定向標準錯誤
            text=True                 # 以文字模式處理輸入輸出
        )
        
        # 與進程通信,提供輸入並獲取輸出
        stdout, stderr = process.communicate(input=input_text)
        
        # 記錄標準輸出
        logger.info("=== STDOUT ===")
        for line in stdout.splitlines():
            logger.info(line)
        
        # 如果有錯誤輸出,則記錄
        if stderr:
            logger.error("=== STDERR ===")
            for line in stderr.splitlines():
                logger.error(line)
        
        # 檢查執行結果
        if process.returncode == 0:
            logger.info("GLPI LDAP 同步作業完成")
        else:
            logger.error(f"GLPI LDAP 同步作業失敗,退出碼:{process.returncode}")
        
        return process.returncode, log_path  # 返回進程退出碼和日誌路徑
        
    except Exception as e:  # 捕獲任何可能的錯誤
        logger.error(f"執行過程中發生錯誤: {str(e)}")
        return 1, log_path  # 發生錯誤時返回錯誤碼和日誌路徑

def main():
    """主函數,處理命令列參數並執行同步"""
    # 建立命令列參數解析器
    parser = argparse.ArgumentParser(description='GLPI LDAP 使用者同步工具')
    # 添加 LDAP 伺服器 ID 參數
    parser.add_argument('--server-id', type=int, default=DEFAULT_CONFIG["server_id"],
                        help=f'LDAP 伺服器 ID (預設值: {DEFAULT_CONFIG["server_id"]})')
    # 添加排除使用者列表參數
    parser.add_argument('--exclude', type=str, nargs='+',
                        help='要排除的使用者列表 (使用空格分隔多個使用者)')
    # 添加排除使用者檔案參數
    parser.add_argument('--exclude-file', type=str, default=DEFAULT_CONFIG["exclude_file"],
                        help=f'包含排除使用者的文字檔 (每行一個使用者名稱,預設: {DEFAULT_CONFIG["exclude_file"]})')
    # 添加日誌目錄參數
    parser.add_argument('--log-dir', type=str, default=DEFAULT_CONFIG["log_dir"],
                        help=f'日誌檔案儲存目錄 (預設: {DEFAULT_CONFIG["log_dir"]})')
    # 添加自動確認參數
    parser.add_argument('--auto-confirm', action='store_true',
                        help='自動確認所有提示 (預設: False)')
    # 添加同步模式參數
    parser.add_argument('--mode', type=str, choices=list(SYNC_MODES.keys()), default=DEFAULT_CONFIG["sync_mode"],
                        help='同步模式: all (全部同步), create (只建立新使用者), update (只更新現有使用者) (預設: all)')
    
    # 解析命令列參數
    args = parser.parse_args()
    
    # 執行 GLPI LDAP 同步
    return_code, log_path = run_glpi_sync(
        server_id=args.server_id,           # 傳入 LDAP 伺服器 ID
        exclude_users=args.exclude,         # 傳入排除使用者列表
        exclude_file=args.exclude_file,     # 傳入排除使用者檔案
        log_dir=args.log_dir,               # 傳入日誌目錄
        auto_confirm=args.auto_confirm,     # 傳入是否自動確認
        sync_mode=args.mode                 # 傳入同步模式
    )
    
    # 顯示完成訊息
    print(f"同步作業已完成,詳細日誌已儲存至: {log_path}")
    # 使用返回的退出碼退出程式
    sys.exit(return_code)

# 程式入口點,只有當此檔案被直接執行時才會執行以下代碼
if __name__ == "__main__":
    main()  # 呼叫主函數
  • 使用方式
    • 基本使用 (全部同步):
    python3 glpi_ldap_sync.py
    
    • 只建立新使用者:
    python3 glpi_ldap_sync.py --mode create
    
    • 只更新現有使用者:
    python3 glpi_ldap_sync.py --mode update
    
    • 組合使用:
    # 只更新現有使用者,並排除特定使用者,自動確認
    python3 glpi_ldap_sync.py --mode update --exclude baduser1 baduser2 --auto-confirm
    
    # 只建立新使用者,使用自訂排除檔案
    python3 glpi_ldap_sync.py --mode create --exclude-file custom_exclude.txt --log-dir /var/log/glpi
    

補充說明


備註





參考相關網頁