HEX
Server: Apache/2.4.63 (Unix)
System: Linux Synopilou92 4.4.302+ #72806 SMP Mon Jul 21 23:16:00 CST 2025 x86_64
User: pilou92 (1026)
PHP: 8.0.30
Disabled: NONE
Upload Files
File: //volume1/@appstore/SynologyApplicationService/tools/generate_domain_migration_mapping.py
#!/usr/bin/env python3

import argparse
import csv
import json
import logging
import sys
import subprocess
import os

LOG_FILE_PATH = "/var/log/sas_domain_migrate.log"

def setup_logger():
    """Setup logger with both console and file output"""
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.INFO)

    # Avoid duplicate handlers
    if logger.handlers:
        return logger

    # Console handler
    console_handler = logging.StreamHandler(sys.stdout)
    console_handler.setLevel(logging.INFO)

    # File handler
    file_handler = logging.FileHandler(LOG_FILE_PATH)
    file_handler.setLevel(logging.DEBUG)

    # Formatter
    formatter_file = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
    formatter_console = logging.Formatter("%(levelname)s - %(message)s")

    console_handler.setFormatter(formatter_console)
    file_handler.setFormatter(formatter_file)

    logger.addHandler(console_handler)
    logger.addHandler(file_handler)

    return logger

def preaction(args):
    """Generate pre-migration domain user mapping file"""
    logger.info("Generating pre-migration domain user mapping file...")
    logger.info(f"- mapping file: {args.output}")

    try:
        with open(args.output, 'w', newline='') as csvfile:
            ad_users = list_domain_users()
            logger.info(f"Found {len(ad_users)} domain users before migration.")
            writer = csv.writer(csvfile)
            writer.writerow(['match_key', 'old_username', 'old_uid'])
            for user in ad_users:
                match_key = user['name'].split('\\')[-1]
                writer.writerow([match_key, user['name'], user['uid']])

        logger.info(f"Pre-migration user mapping file written to '{args.output}'")
    except Exception as e:
        logger.error(f"Error writing to file '{args.output}': {e}")
        raise

def postaction(args):
    """Generate pre-migration domain user mapping file"""
    logger.info("Generating the final user mapping file for domain migration...")
    logger.info(f"- old domain temporary mapping file: {args.file}")
    logger.info(f"- mapping file: {args.output}")

    user_mapping = {}
    try:
        with open(args.file, 'r', newline='') as csvfile:
            reader = csv.reader(csvfile, delimiter=',')
            next(reader) # Skip header row

            for row in reader:
                if len(row) < 3:
                    logger.warning(f"Skipping invalid row in old domain file: {row}")
                    continue
                match_key = row[0].strip()
                old_username = row[1].strip()
                old_uid = row[2].strip()
                user_mapping[match_key] = [old_username, old_uid]
    except Exception as e:
        logger.error(f"Error reading from file '{args.file}': {e}")
        raise

    ad_users = list_domain_users()
    if not ad_users:
        logger.info("No domain users found after migration.")
        return

    logger.info(f"Found {len(ad_users)} domain users after migration.")
    new_ad_user_set = set()
    for user in ad_users:
        match_key = user['name'].split('\\')[-1]
        new_ad_user_set.add(match_key)
        mapping = user_mapping.get(match_key)
        if not mapping:
            continue
        mapping.extend([user['name'], user['uid']])

    log_difference(set(user_mapping.keys()), new_ad_user_set)

    try:
        with open(args.output, 'w', newline='') as csvfile:
            writer = csv.writer(csvfile)
            writer.writerow(['old_username', 'old_uid', 'new_username', 'new_uid'])
            for mapping in user_mapping.values():
                if len(mapping) < 4:
                    logger.warning(f"Incomplete mapping for user '{mapping[0]}', skipping.")
                    continue
                writer.writerow(mapping)
        logger.info(f"Final user mapping file written to '{args.output}'")
    except Exception as e:
        logger.error(f"Error writing to file '{args.output}': {e}")
        raise

def list_domain_users():
    cmd = [
        'synowebapi',
        '--exec',
        'api=SYNO.Core.User',
        'method=list',
        'version=1',
        'action=enum',
        'type=domain',
        'additional=["uid"]',
    ]

    try:
        api_result = subprocess.run(cmd, capture_output=True, text=True, check=True)
        resp = json.loads(api_result.stdout)
        if not resp['success'] or 'data' not in resp or 'users' not in resp['data']:
            logger.error(f"API returned failure: {api_result.stdout}")
            return []

        return resp['data']['users']
    except subprocess.CalledProcessError as e:
        logger.error(f"Error executing command: {e.stderr}")
        return []

def log_difference(old_keys: set, new_keys: set):
    only_in_old = old_keys - new_keys
    only_in_new = new_keys - old_keys

    if only_in_old:
        logger.debug(f"Users only in old domain: {only_in_old}")
    if only_in_new:
        logger.debug(f"Users only in new domain: {only_in_new}")

def is_root():
    return os.geteuid() == 0

def usage():
    """Print usage information"""
    print('Domain Migration Mapping Generator')
    print('This script generates user mapping files for domain migration in two phases:')
    print('')
    print('STEP 1 - Before domain migration:')
    print('  After confirming account system synchronization, run:')
    print(f'    {sys.argv[0]} pre [--output <old_domain_csv>]')
    print('  This captures the current domain user information.')
    print('')
    print('STEP 2 - After domain migration:')
    print('  After domain conversion and confirming synchronization, run:')
    print(f'    {sys.argv[0]} post [--file <old_domain_csv>] [--output <mapping_csv>]')
    print('  This generates the final mapping file by comparing old and new domain users.')
    print('')
    print('Arguments:')
    print('  pre:')
    print('    --output, -o    Output CSV file path (default: /tmp/old_domain.csv)')
    print('')
    print('  post:')
    print('    --file, -f      Old domain CSV file path (default: /tmp/old_domain.csv)')
    print('    --output, -o    Final mapping output file path (default: final_domain_mapping.csv)')

logger = setup_logger()

def main():
    if not is_root():
        print('This script must be run as root.')
        return 1

    if len(sys.argv) < 2:
        usage()
        return 1

    command = sys.argv[1]

    if command == 'pre':
        parser = argparse.ArgumentParser(description="Generate mapping for old domain users")
        parser.add_argument('--output', '-o', required=False, help='Output CSV file path, default: /tmp/old_domain.csv', default='/tmp/old_domain.csv')

        args = parser.parse_args(sys.argv[2:])
        try:
            preaction(args)
        except Exception as e:
            logger.error(f"Pre-migration action failed: {e}")
            return 1

    elif command == 'post':
        parser = argparse.ArgumentParser(description='Generate mapping for new domain users')
        parser.add_argument('--file', '-f', required=False, help='Old domain CSV file path, default: /tmp/old_domain.csv', default='/tmp/old_domain.csv')
        parser.add_argument('--output', '-o', required=False, help='Final mapping output file path, default: final_domain_mapping.csv', default='final_domain_mapping.csv')

        args = parser.parse_args(sys.argv[2:])
        try:
            postaction(args)
        except Exception as e:
            logger.error(f"Post-migration action failed: {e}")
            return 1

    else:
        print(f"Error: unknown command '{command}'")
        usage()
        return 1

    return 0

if __name__ == "__main__":
    sys.exit(main())