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())