#!/usr/bin/env python3 """ CLI tool to orchestrate multi-stage profile simulations. """ import argparse import logging import os import signal import subprocess import sys import time from pathlib import Path from types import SimpleNamespace try: import yaml except ImportError: print("PyYAML is not installed. Please install it with: pip install PyYAML", file=sys.stderr) yaml = None # Import the main functions from the tools we are wrapping from .profile_setup_tool import main_setup_profiles from .stress_policy_tool import main_stress_policy logger = logging.getLogger(__name__) # Define default policy paths relative to the project root PROJECT_ROOT = Path(__file__).resolve().parent.parent POLICY_DIR = PROJECT_ROOT / 'policies' POLICY_FILE_SETUP = str(POLICY_DIR / '6_simulation_policy.yaml') POLICY_FILE_AUTH = str(POLICY_DIR / '7_continuous_auth.yaml') POLICY_FILE_DOWNLOAD = str(POLICY_DIR / '8_continuous_download.yaml') def add_simulation_parser(subparsers): """Adds the parser for the 'simulation' command.""" parser = subparsers.add_parser( 'simulation', description="Run multi-stage profile simulations (setup, auth, download). This provides a unified interface for the simulation workflow.", formatter_class=argparse.RawTextHelpFormatter, help="Run multi-stage profile simulations." ) # Common arguments for all simulation subcommands common_parser = argparse.ArgumentParser(add_help=False) common_parser.add_argument('--env-file', default=None, help="Path to a .env file to load. Overrides setting from policy file.") common_parser.add_argument('--redis-host', default=None, help='Redis host. Overrides policy and .env file.') common_parser.add_argument('--redis-port', type=int, default=None, help='Redis port. Overrides policy and .env file.') common_parser.add_argument('--redis-password', default=None, help='Redis password. Overrides policy and .env file.') common_parser.add_argument('--env', default='sim', help="Environment name for Redis key prefix. Default: 'sim'.") common_parser.add_argument('--expire-time-shift-minutes', type=int, default=None, help="Consider URLs expiring in N minutes as expired. Overrides policy.") common_parser.add_argument('--verbose', action='store_true', help="Enable verbose logging.") sim_subparsers = parser.add_subparsers(dest='simulation_command', help='Simulation stage to run', required=True) # --- Setup --- setup_parser = sim_subparsers.add_parser('setup', help='Set up profiles for a simulation.', parents=[common_parser]) setup_parser.add_argument('--policy-file', dest='policy', default=POLICY_FILE_SETUP, help=f'Path to the setup policy YAML file. Default: {POLICY_FILE_SETUP}') setup_parser.add_argument('--preserve-profiles', action='store_true', help="Do not clean up existing profiles.") setup_parser.add_argument('--reset-global-counters', action='store_true', help="Reset global counters like 'failed_lock_attempts'.") # --- Auth --- auth_parser = sim_subparsers.add_parser('auth', help='Run the authentication (get-info) part of the simulation.', parents=[common_parser]) auth_parser.add_argument('--policy-file', dest='policy', default=POLICY_FILE_AUTH, help=f'Path to the auth simulation policy file. Default: {POLICY_FILE_AUTH}') auth_parser.add_argument('--set', action='append', default=[], help="Override a policy setting using 'key.subkey=value' format.") # --- Download --- download_parser = sim_subparsers.add_parser('download', help='Run the download part of the simulation.', parents=[common_parser]) download_parser.add_argument('--policy-file', dest='policy', default=POLICY_FILE_DOWNLOAD, help=f'Path to the download simulation policy file. Default: {POLICY_FILE_DOWNLOAD}') download_parser.add_argument('--set', action='append', default=[], help="Override a policy setting using 'key.subkey=value' format.") def main_simulation(args): """Main dispatcher for 'simulation' command.""" # --- Load policy to get simulation parameters --- policy = {} # The 'policy' attribute is guaranteed to exist by the arg parser for all subcommands if not yaml: logger.error("Cannot load policy file because PyYAML is not installed.") return 1 try: with open(args.policy, 'r') as f: # We only need the first document if it's a multi-policy file policy = yaml.safe_load(f) or {} except (IOError, yaml.YAMLError) as e: logger.error(f"Failed to load or parse policy file {args.policy}: {e}") return 1 sim_params = policy.get('simulation_parameters', {}) effective_env_file = args.env_file or sim_params.get('env_file') if args.simulation_command == 'setup': # Create an args object that main_setup_profiles expects setup_args = SimpleNamespace( policy_file=args.policy, env_file=effective_env_file, preserve_profiles=args.preserve_profiles, reset_global_counters=args.reset_global_counters, verbose=args.verbose, redis_host=args.redis_host, redis_port=args.redis_port, redis_password=args.redis_password ) return main_setup_profiles(setup_args) elif args.simulation_command == 'auth': # This command runs the stress tool in auth (fetch_only) mode. # It is expected that the policy-enforcer is run as a separate process. stress_args = SimpleNamespace( policy=args.policy, policy_name=None, list_policies=False, show_overrides=False, set=args.set, profile_prefix=None, start_from_url_index=None, auto_merge_fragments=None, remove_fragments_after_merge=None, fragments_dir=None, remote_dir=None, cleanup=None, verbose=args.verbose, dry_run=False, disable_log_writing=False, # Redis connection args env_file=effective_env_file, redis_host=args.redis_host, redis_port=args.redis_port, redis_password=args.redis_password, env=args.env, key_prefix=None, expire_time_shift_minutes=args.expire_time_shift_minutes ) logger.info("\n--- Starting Auth Simulation (stress-policy) ---") return main_stress_policy(stress_args) elif args.simulation_command == 'download': # This is simpler, just runs the stress tool in download mode. stress_args = SimpleNamespace( policy=args.policy, policy_name=None, list_policies=False, show_overrides=False, set=args.set, profile_prefix=None, start_from_url_index=None, auto_merge_fragments=None, remove_fragments_after_merge=None, fragments_dir=None, remote_dir=None, cleanup=None, verbose=args.verbose, dry_run=False, disable_log_writing=False, # Redis connection args env_file=effective_env_file, redis_host=args.redis_host, redis_port=args.redis_port, redis_password=args.redis_password, env=args.env, key_prefix=None, expire_time_shift_minutes=args.expire_time_shift_minutes ) logger.info("\n--- Starting Download Simulation (stress-policy) ---") return main_stress_policy(stress_args) return 1 # Should not be reached