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