344 lines
13 KiB
Python
344 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
# ==============================================================================
|
|
# This script fixes placeholder email addresses created by the Pagure migrator.
|
|
# When users are migrated from Pagure to Forge, the migrator creates placeholder
|
|
# @fedoraproject.org email addresses (like username@fedoraproject.org) because
|
|
# it doesn't have access to users' real email addresses. This script replaces
|
|
# those placeholder addresses with the users' actual email addresses from FAS
|
|
# (Fedora Account System).
|
|
#
|
|
# The script ONLY processes users who already have @fedoraproject.org email
|
|
# addresses (the migrator placeholders). Users with real email addresses from
|
|
# other domains are left untouched - this is not a general email sync tool.
|
|
#
|
|
# The script handles three scenarios for @fedoraproject.org users:
|
|
# 1. User exists in FAS with email(s) - replaces placeholder with first FAS email
|
|
# 2. User exists in FAS but has no emails - sets to username+fasnotfound@fedoraproject.org
|
|
# 3. User not found in FAS - sets to username+fasnotfound@fedoraproject.org
|
|
#
|
|
# This ensures migrated users get proper email addresses for notifications,
|
|
# password resets, and other account-related communications.
|
|
# ==============================================================================
|
|
#
|
|
# Prerequisites and Setup:
|
|
#
|
|
# Step 1: Install Required Python Packages
|
|
#
|
|
# This script requires the following Python packages:
|
|
#
|
|
# dnf install python3-click python3-requests python3-fasjson-client
|
|
#
|
|
# Step 2: Obtain a Forge API Token
|
|
#
|
|
# You need an admin-level API token from your Forge instance:
|
|
# 1. Log into your Forge instance (staging or production)
|
|
# 2. Go to Settings -> Applications -> Generate New Token
|
|
# 3. Give it a descriptive name like "Email Sync Script"
|
|
# 4. Select the "admin" scope (required for user management)
|
|
# 5. Copy the generated token
|
|
#
|
|
# Step 3: Set up Kerberos Authentication for FAS
|
|
#
|
|
# The script uses FAS (Fedora Account System) which requires Kerberos auth:
|
|
#
|
|
# For staging:
|
|
# kinit your_username@STG.FEDORAPROJECT.ORG
|
|
#
|
|
# For production:
|
|
# kinit your_username@FEDORAPROJECT.ORG
|
|
#
|
|
# Step 4: Run the Script
|
|
#
|
|
# Basic usage examples:
|
|
#
|
|
# # Preview placeholder email fixes against staging (safe, no modifications)
|
|
# ./update-forge-fas-emails.py --dry-run --token YOUR_API_TOKEN
|
|
#
|
|
# # Actually fix placeholder emails on staging
|
|
# ./update-forge-fas-emails.py --token YOUR_API_TOKEN
|
|
#
|
|
# # Preview placeholder email fixes against production
|
|
# ./update-forge-fas-emails.py --production --dry-run --token YOUR_API_TOKEN
|
|
#
|
|
# # Actually fix placeholder emails on production (BE CAREFUL!)
|
|
# ./update-forge-fas-emails.py --production --token YOUR_API_TOKEN
|
|
#
|
|
# # Use environment variable for token (recommended for automation)
|
|
# export FORGE_TOKEN=your_api_token
|
|
# ./update-forge-fas-emails.py --production
|
|
#
|
|
# Step 5: Understanding the Output
|
|
#
|
|
# The script provides detailed output showing:
|
|
# - Which environment it's running against
|
|
# - Authentication status with FAS
|
|
# - Progress through all Forge users
|
|
# - Actions taken for each user (SKIP, NO CHANGE, UPDATED, FAILED, ERROR)
|
|
# - Summary statistics at the end
|
|
#
|
|
# Action meanings:
|
|
# - SKIP: User doesn't have @fedoraproject.org email (not a migrator placeholder)
|
|
# - NO CHANGE: User's placeholder email already matches their FAS email
|
|
# - UPDATED/WOULD UPDATE: Placeholder email was replaced with real FAS email
|
|
# - FAILED: API call to update email failed
|
|
# - ERROR: Unexpected error occurred (usually FAS connectivity issues)
|
|
#
|
|
# Safety Features:
|
|
# - Dry-run mode lets you preview changes before making them
|
|
# - Only processes @fedoraproject.org placeholder email addresses
|
|
# - Leaves users with real email addresses untouched
|
|
# - Comprehensive logging of all actions taken
|
|
# - Staging environment available for testing
|
|
#
|
|
# ==============================================================================
|
|
|
|
"""
|
|
Script to fix placeholder email addresses created by the Pagure migrator.
|
|
Replaces @fedoraproject.org placeholder emails with real FAS email addresses.
|
|
"""
|
|
|
|
import click
|
|
import requests
|
|
import sys
|
|
from fasjson_client import Client
|
|
|
|
def update_forge_email(forge_session, username, new_email, forge_url, dry_run=False):
|
|
"""Update a user's email in Forge via admin API"""
|
|
if dry_run:
|
|
return True, "Dry run - no changes made"
|
|
|
|
try:
|
|
response = forge_session.patch(
|
|
f'{forge_url}/api/v1/admin/users/{username}',
|
|
json={'active': True, 'email': new_email}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
return True, "Success"
|
|
else:
|
|
return False, f"HTTP {response.status_code}: {response.text[:50]}"
|
|
except Exception as e:
|
|
return False, str(e)[:50]
|
|
|
|
|
|
def get_config(production=False):
|
|
"""Get configuration based on environment"""
|
|
if production:
|
|
return {
|
|
'forge_url': "https://forge.fedoraproject.org",
|
|
'fasjson_url': "https://fasjson.fedoraproject.org",
|
|
'env_name': 'PRODUCTION'
|
|
}
|
|
else:
|
|
return {
|
|
'forge_url': "https://forge.stg.fedoraproject.org",
|
|
'fasjson_url': "https://fasjson.stg.fedoraproject.org",
|
|
'env_name': 'STAGING'
|
|
}
|
|
|
|
@click.command()
|
|
@click.option(
|
|
'--production',
|
|
is_flag=True,
|
|
help='Run against production environment (default: staging)'
|
|
)
|
|
@click.option(
|
|
'--dry-run',
|
|
is_flag=True,
|
|
help='Preview changes without making them'
|
|
)
|
|
@click.option(
|
|
'--token',
|
|
required=True,
|
|
help='Forge API token for authentication',
|
|
envvar='FORGE_TOKEN'
|
|
)
|
|
def main(production, dry_run, token):
|
|
"""Sync Forge user emails with FAS.
|
|
|
|
Examples:
|
|
|
|
\b
|
|
# Run against staging (default)
|
|
./update-forge-fas-emails.py --token YOUR_API_TOKEN
|
|
|
|
\b
|
|
# Run against production
|
|
./update-forge-fas-emails.py --production --token YOUR_API_TOKEN
|
|
|
|
\b
|
|
# Preview changes without making them
|
|
./update-forge-fas-emails.py --dry-run --token YOUR_API_TOKEN
|
|
|
|
\b
|
|
# Use environment variable for token
|
|
export FORGE_TOKEN=your_api_token
|
|
./update-forge-fas-emails.py --production
|
|
"""
|
|
config = get_config(production)
|
|
|
|
print("=" * 120)
|
|
print("Forge Email Sync from FAS")
|
|
print(f"Environment: {config['env_name']}")
|
|
print(f"Forge URL: {config['forge_url']}")
|
|
print(f"FAS URL: {config['fasjson_url']}")
|
|
if dry_run:
|
|
print("*** DRY RUN MODE - NO CHANGES WILL BE MADE ***")
|
|
print("=" * 120)
|
|
print()
|
|
|
|
# Setup Forge session
|
|
forge_session = requests.Session()
|
|
forge_session.headers.update({
|
|
'Authorization': f'token {token}',
|
|
'Content-Type': 'application/json'
|
|
})
|
|
|
|
# Setup FAS client
|
|
try:
|
|
fas_client = Client(config['fasjson_url'])
|
|
whoami = fas_client.whoami()
|
|
print(f"Authenticated with FAS as: {whoami.result.get('username', 'unknown')}")
|
|
except Exception as e:
|
|
print(f"Error: Cannot connect to FAS: {e}")
|
|
env_suffix = "FEDORAPROJECT.ORG" if production else "STG.FEDORAPROJECT.ORG"
|
|
print(f"Please run 'kinit your_username@{env_suffix}' first.\n")
|
|
return
|
|
|
|
# Get all Forge users
|
|
print("\nFetching users from Forge...")
|
|
forge_users = []
|
|
page = 1
|
|
limit = 50
|
|
|
|
while True:
|
|
response = forge_session.get(
|
|
f"{config['forge_url']}/api/v1/admin/users",
|
|
params={'page': page, 'limit': limit}
|
|
)
|
|
|
|
if response.status_code != 200:
|
|
print(f"Error fetching Forge users: {response.status_code}")
|
|
return
|
|
|
|
users_data = response.json()
|
|
if not users_data:
|
|
break
|
|
|
|
forge_users.extend(users_data)
|
|
page += 1
|
|
|
|
print(f"Found {len(forge_users)} total users on Forge")
|
|
print()
|
|
|
|
# Print table header
|
|
print("-" * 120)
|
|
print(f"{'Username':<25} {'Current Email':<35} {'Action':<15} {'New Email':<30}")
|
|
print("-" * 120)
|
|
|
|
# Stats counters
|
|
skipped_non_fedora = 0
|
|
skipped_already_processed = 0
|
|
skipped_error = 0
|
|
not_in_fas_updated = 0
|
|
updated_from_fas = 0
|
|
no_change_needed = 0
|
|
update_failed = 0
|
|
|
|
# Process each user
|
|
for user in forge_users:
|
|
username = user.get('login', 'N/A')
|
|
forge_email = user.get('email', 'N/A')
|
|
|
|
# Rule 1: Skip if not @fedoraproject.org email
|
|
if not forge_email.endswith('@fedoraproject.org'):
|
|
skipped_non_fedora += 1
|
|
continue
|
|
|
|
# Rule 2: Skip if already marked as fasnotfound (previously processed)
|
|
if '+fasnotfound@fedoraproject.org' in forge_email:
|
|
skipped_already_processed += 1
|
|
continue
|
|
|
|
# Try to get user from FAS
|
|
try:
|
|
user_response = fas_client.get_user(username=username)
|
|
user_data = user_response.result
|
|
|
|
emails = user_data.get('emails', [])
|
|
|
|
if emails:
|
|
# User found in FAS with email(s) - use first email
|
|
fas_email = emails[0]
|
|
|
|
if forge_email == fas_email:
|
|
# No change needed
|
|
no_change_needed += 1
|
|
print(f"{username:<25} {forge_email:<35} {'NO CHANGE':<15} (already {fas_email})")
|
|
else:
|
|
# Update to FAS email
|
|
success, msg = update_forge_email(forge_session, username, fas_email, config['forge_url'], dry_run)
|
|
if success:
|
|
updated_from_fas += 1
|
|
action = 'WOULD UPDATE' if dry_run else 'UPDATED'
|
|
print(f"{username:<25} {forge_email:<35} {action:<15} {fas_email}")
|
|
else:
|
|
update_failed += 1
|
|
print(f"{username:<25} {forge_email:<35} {'FAILED':<15} {msg}")
|
|
else:
|
|
# User found in FAS but no emails - this should never happen, log as error
|
|
skipped_error += 1
|
|
print(f"{username:<25} {forge_email:<35} {'ERROR':<15} User in FAS but has no emails")
|
|
|
|
except Exception as e:
|
|
error_str = str(e)
|
|
if 'not found' in error_str.lower() or '404' in error_str:
|
|
# User not found in FAS - set to username+fasnotfound@fedoraproject.org
|
|
new_email = f"{username}+fasnotfound@fedoraproject.org"
|
|
if forge_email == new_email:
|
|
no_change_needed += 1
|
|
print(f"{username:<25} {forge_email:<35} {'NO CHANGE':<15} (already {new_email})")
|
|
else:
|
|
success, msg = update_forge_email(forge_session, username, new_email, config['forge_url'], dry_run)
|
|
if success:
|
|
not_in_fas_updated += 1
|
|
action = 'WOULD UPDATE' if dry_run else 'UPDATED'
|
|
print(f"{username:<25} {forge_email:<35} {action:<15} {new_email} (not in FAS)")
|
|
else:
|
|
update_failed += 1
|
|
print(f"{username:<25} {forge_email:<35} {'FAILED':<15} {msg}")
|
|
else:
|
|
# Rule 2: FAS error - don't do anything
|
|
skipped_error += 1
|
|
print(f"{username:<25} {forge_email:<35} {'ERROR':<15} {str(e)[:30]}")
|
|
|
|
# Print summary
|
|
print("-" * 120)
|
|
print()
|
|
print("=" * 120)
|
|
print("SUMMARY")
|
|
print("=" * 120)
|
|
print(f"Total Forge users: {len(forge_users)}")
|
|
print()
|
|
print(f"Skipped (non-@fedoraproject.org email): {skipped_non_fedora}")
|
|
print(f"Skipped (already processed +fasnotfound): {skipped_already_processed}")
|
|
print(f"Skipped (FAS error): {skipped_error}")
|
|
print()
|
|
print(f"No change needed: {no_change_needed}")
|
|
changes_verb = "Would be made" if dry_run else "Made"
|
|
print(f"Updated from FAS: {updated_from_fas}")
|
|
print(f"Updated to +fasnotfound (not in FAS): {not_in_fas_updated}")
|
|
print(f"Update failed: {update_failed}")
|
|
print()
|
|
total_processed = len(forge_users) - skipped_non_fedora - skipped_already_processed - skipped_error
|
|
print(f"Total @fedoraproject.org users processed: {total_processed}")
|
|
print(f"Total changes {changes_verb.lower()}: {updated_from_fas + not_in_fas_updated}")
|
|
if dry_run:
|
|
print()
|
|
print("*** DRY RUN MODE - Use --production flag without --dry-run to make actual changes ***")
|
|
print("=" * 120)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|