/ src / cli / delete_user.py
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()