delete_user.py
1 #!/usr/bin/env python3 2 """CLI tool for deleting Ag3ntum users. 3 4 This removes the user from the Ag3ntum database and cleans up their 5 user directory. The Linux user account is NOT deleted to avoid 6 affecting host users (especially in direct UID mapping mode). 7 """ 8 import argparse 9 import asyncio 10 import logging 11 import sys 12 from pathlib import Path 13 14 # Add project root to path 15 sys.path.insert(0, str(Path(__file__).parent.parent.parent)) 16 17 from src.db.database import AsyncSessionLocal, init_db, engine 18 from src.db import models # noqa: F401 - Import models to register with Base.metadata 19 from src.services.user_service import user_service 20 21 logging.basicConfig(level=logging.INFO) 22 logger = logging.getLogger(__name__) 23 24 25 async def delete_user( 26 username: str, 27 force: bool = False, 28 ) -> None: 29 """Delete a user from Ag3ntum (database + user directory only).""" 30 try: 31 # Ensure database tables exist 32 await init_db() 33 34 async with AsyncSessionLocal() as db: 35 # First check if user exists 36 from sqlalchemy import select 37 from src.db.models import User 38 39 result = await db.execute( 40 select(User).where(User.username == username) 41 ) 42 user = result.scalar_one_or_none() 43 44 if not user: 45 logger.error(f"User '{username}' not found") 46 raise ValueError(f"User '{username}' not found") 47 48 if not force: 49 logger.info(f"User to delete:") 50 logger.info(f" ID: {user.id}") 51 logger.info(f" Username: {user.username}") 52 logger.info(f" Email: {user.email}") 53 logger.info(f" Linux UID: {user.linux_uid}") 54 logger.info("") 55 logger.info("Use --force to confirm deletion") 56 return 57 58 # Always keep Linux user to avoid affecting host users 59 deleted = await user_service.delete_user( 60 db=db, 61 username=username, 62 delete_linux_user=False, 63 ) 64 65 if deleted: 66 logger.info(f"User '{username}' deleted successfully") 67 logger.info(" (Linux user account preserved)") 68 else: 69 logger.error(f"Failed to delete user '{username}'") 70 raise ValueError(f"Failed to delete user '{username}'") 71 72 except ValueError: 73 raise 74 finally: 75 # Always clean up database connections and wait for background tasks 76 await engine.dispose() 77 78 # Give aiosqlite threads time to clean up 79 await asyncio.sleep(0.1) 80 81 82 def main(): 83 parser = argparse.ArgumentParser( 84 description="Delete Ag3ntum user (removes from database, preserves Linux user)" 85 ) 86 parser.add_argument("--username", required=True, help="Username to delete") 87 parser.add_argument( 88 "--force", 89 action="store_true", 90 help="Confirm deletion (required to actually delete)", 91 ) 92 93 args = parser.parse_args() 94 95 # Use asyncio.run with explicit cleanup 96 try: 97 asyncio.run( 98 delete_user( 99 username=args.username, 100 force=args.force, 101 ) 102 ) 103 # Force clean exit for CLI tool (aiosqlite may have lingering threads) 104 sys.exit(0) 105 except ValueError: 106 # Errors already logged 107 sys.exit(1) 108 except KeyboardInterrupt: 109 logger.info("Operation cancelled by user") 110 sys.exit(130) 111 112 113 if __name__ == "__main__": 114 main()