""" Schedules API blueprint. Handles endpoints for managing scheduled scans including CRUD operations and manual triggering. """ import logging from flask import Blueprint, jsonify, request, current_app from web.auth.decorators import api_auth_required from web.services.schedule_service import ScheduleService from web.services.scan_service import ScanService logger = logging.getLogger(__name__) bp = Blueprint('schedules', __name__) @bp.route('', methods=['GET']) @api_auth_required def list_schedules(): """ List all schedules with pagination and filtering. Query parameters: page: Page number (default: 1) per_page: Items per page (default: 20) enabled: Filter by enabled status (true/false) Returns: JSON response with paginated schedules list """ try: # Parse query parameters page = request.args.get('page', 1, type=int) per_page = request.args.get('per_page', 20, type=int) enabled_str = request.args.get('enabled', type=str) # Parse enabled filter enabled_filter = None if enabled_str is not None: enabled_filter = enabled_str.lower() == 'true' # Get schedules schedule_service = ScheduleService(current_app.db_session) result = schedule_service.list_schedules(page, per_page, enabled_filter) return jsonify(result), 200 except Exception as e: logger.error(f"Error listing schedules: {str(e)}", exc_info=True) return jsonify({'error': 'Internal server error'}), 500 @bp.route('/', methods=['GET']) @api_auth_required def get_schedule(schedule_id): """ Get details for a specific schedule. Args: schedule_id: Schedule ID Returns: JSON response with schedule details including execution history """ try: schedule_service = ScheduleService(current_app.db_session) schedule = schedule_service.get_schedule(schedule_id) return jsonify(schedule), 200 except ValueError as e: # Schedule not found return jsonify({'error': str(e)}), 404 except Exception as e: logger.error(f"Error getting schedule {schedule_id}: {str(e)}", exc_info=True) return jsonify({'error': 'Internal server error'}), 500 @bp.route('', methods=['POST']) @api_auth_required def create_schedule(): """ Create a new schedule. Request body: name: Schedule name (required) config_file: Path to YAML config (required) cron_expression: Cron expression (required, e.g., '0 2 * * *') enabled: Whether schedule is active (optional, default: true) Returns: JSON response with created schedule ID """ try: data = request.get_json() or {} # Validate required fields required = ['name', 'config_file', 'cron_expression'] missing = [field for field in required if field not in data] if missing: return jsonify({'error': f'Missing required fields: {", ".join(missing)}'}), 400 # Create schedule schedule_service = ScheduleService(current_app.db_session) schedule_id = schedule_service.create_schedule( name=data['name'], config_file=data['config_file'], cron_expression=data['cron_expression'], enabled=data.get('enabled', True) ) # Get the created schedule schedule = schedule_service.get_schedule(schedule_id) # Add to APScheduler if enabled if schedule['enabled'] and hasattr(current_app, 'scheduler'): try: current_app.scheduler.add_scheduled_scan( schedule_id=schedule_id, config_file=schedule['config_file'], cron_expression=schedule['cron_expression'] ) logger.info(f"Schedule {schedule_id} added to APScheduler") except Exception as e: logger.error(f"Failed to add schedule {schedule_id} to APScheduler: {str(e)}") # Continue anyway - schedule is created in DB return jsonify({ 'schedule_id': schedule_id, 'message': 'Schedule created successfully', 'schedule': schedule }), 201 except ValueError as e: # Validation error return jsonify({'error': str(e)}), 400 except Exception as e: logger.error(f"Error creating schedule: {str(e)}", exc_info=True) return jsonify({'error': 'Internal server error'}), 500 @bp.route('/', methods=['PUT']) @api_auth_required def update_schedule(schedule_id): """ Update an existing schedule. Args: schedule_id: Schedule ID to update Request body: name: Schedule name (optional) config_file: Path to YAML config (optional) cron_expression: Cron expression (optional) enabled: Whether schedule is active (optional) Returns: JSON response with updated schedule """ try: data = request.get_json() or {} if not data: return jsonify({'error': 'No update data provided'}), 400 # Update schedule schedule_service = ScheduleService(current_app.db_session) # Store old state to check if scheduler update needed old_schedule = schedule_service.get_schedule(schedule_id) # Perform update updated_schedule = schedule_service.update_schedule(schedule_id, **data) # Update in APScheduler if needed if hasattr(current_app, 'scheduler'): try: # If cron expression or config changed, or enabled status changed cron_changed = 'cron_expression' in data config_changed = 'config_file' in data enabled_changed = 'enabled' in data if enabled_changed: if updated_schedule['enabled']: # Re-add to scheduler (replaces existing) current_app.scheduler.add_scheduled_scan( schedule_id=schedule_id, config_file=updated_schedule['config_file'], cron_expression=updated_schedule['cron_expression'] ) logger.info(f"Schedule {schedule_id} enabled and added to APScheduler") else: # Remove from scheduler current_app.scheduler.remove_scheduled_scan(schedule_id) logger.info(f"Schedule {schedule_id} disabled and removed from APScheduler") elif (cron_changed or config_changed) and updated_schedule['enabled']: # Reload schedule in APScheduler current_app.scheduler.add_scheduled_scan( schedule_id=schedule_id, config_file=updated_schedule['config_file'], cron_expression=updated_schedule['cron_expression'] ) logger.info(f"Schedule {schedule_id} reloaded in APScheduler") except Exception as e: logger.error(f"Failed to update schedule {schedule_id} in APScheduler: {str(e)}") # Continue anyway - schedule is updated in DB return jsonify({ 'message': 'Schedule updated successfully', 'schedule': updated_schedule }), 200 except ValueError as e: # Schedule not found or validation error if 'not found' in str(e): return jsonify({'error': str(e)}), 404 return jsonify({'error': str(e)}), 400 except Exception as e: logger.error(f"Error updating schedule {schedule_id}: {str(e)}", exc_info=True) return jsonify({'error': 'Internal server error'}), 500 @bp.route('/', methods=['DELETE']) @api_auth_required def delete_schedule(schedule_id): """ Delete a schedule. Note: Associated scans are NOT deleted (schedule_id becomes null). Active scans will complete normally. Args: schedule_id: Schedule ID to delete Returns: JSON response with deletion status """ try: # Remove from APScheduler first if hasattr(current_app, 'scheduler'): try: current_app.scheduler.remove_scheduled_scan(schedule_id) logger.info(f"Schedule {schedule_id} removed from APScheduler") except Exception as e: logger.warning(f"Failed to remove schedule {schedule_id} from APScheduler: {str(e)}") # Continue anyway # Delete from database schedule_service = ScheduleService(current_app.db_session) schedule_service.delete_schedule(schedule_id) return jsonify({ 'message': 'Schedule deleted successfully', 'schedule_id': schedule_id }), 200 except ValueError as e: # Schedule not found return jsonify({'error': str(e)}), 404 except Exception as e: logger.error(f"Error deleting schedule {schedule_id}: {str(e)}", exc_info=True) return jsonify({'error': 'Internal server error'}), 500 @bp.route('//trigger', methods=['POST']) @api_auth_required def trigger_schedule(schedule_id): """ Manually trigger a scheduled scan. Creates a new scan with the schedule's configuration and queues it for immediate execution. Args: schedule_id: Schedule ID to trigger Returns: JSON response with triggered scan ID """ try: # Get schedule schedule_service = ScheduleService(current_app.db_session) schedule = schedule_service.get_schedule(schedule_id) # Trigger scan scan_service = ScanService(current_app.db_session) # Get scheduler if available scheduler = current_app.scheduler if hasattr(current_app, 'scheduler') else None scan_id = scan_service.trigger_scan( config_file=schedule['config_file'], triggered_by='manual', schedule_id=schedule_id, scheduler=scheduler ) logger.info(f"Manual trigger of schedule {schedule_id} created scan {scan_id}") return jsonify({ 'message': 'Scan triggered successfully', 'schedule_id': schedule_id, 'scan_id': scan_id }), 201 except ValueError as e: # Schedule not found return jsonify({'error': str(e)}), 404 except Exception as e: logger.error(f"Error triggering schedule {schedule_id}: {str(e)}", exc_info=True) return jsonify({'error': 'Internal server error'}), 500 # Health check endpoint @bp.route('/health', methods=['GET']) def health_check(): """ Health check endpoint for monitoring. Returns: JSON response with API health status """ return jsonify({ 'status': 'healthy', 'api': 'schedules', 'version': '1.0.0-phase1' })