restructure of dirs, huge docs update
This commit is contained in:
331
app/web/api/schedules.py
Normal file
331
app/web/api/schedules.py
Normal file
@@ -0,0 +1,331 @@
|
||||
"""
|
||||
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('/<int:schedule_id>', 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('/<int:schedule_id>', 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('/<int:schedule_id>', 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('/<int:schedule_id>/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'
|
||||
})
|
||||
Reference in New Issue
Block a user