Files
tinker_tickets/create_ticket_api.php
T

250 lines
7.6 KiB
PHP
Raw Normal View History

2024-11-30 19:48:01 -05:00
<?php
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set('display_errors', 0);
2024-11-30 19:48:01 -05:00
// Load environment variables with error check
$envFile = __DIR__ . '/.env';
if (!file_exists($envFile)) {
echo json_encode([
'success' => false,
'error' => 'Configuration file not found'
]);
exit;
}
$envVars = parse_ini_file($envFile, false, INI_SCANNER_TYPED);
2024-11-30 19:48:01 -05:00
if (!$envVars) {
echo json_encode([
'success' => false,
'error' => 'Invalid configuration file'
]);
exit;
}
// Strip quotes from values if present (parse_ini_file may include them)
foreach ($envVars as $key => $value) {
if (is_string($value)) {
if ((substr($value, 0, 1) === '"' && substr($value, -1) === '"') ||
(substr($value, 0, 1) === "'" && substr($value, -1) === "'")) {
$envVars[$key] = substr($value, 1, -1);
}
}
}
2024-11-30 19:48:01 -05:00
// Database connection with detailed error handling
$conn = new mysqli(
$envVars['DB_HOST'],
$envVars['DB_USER'],
$envVars['DB_PASS'],
$envVars['DB_NAME']
2024-11-30 19:48:01 -05:00
);
if ($conn->connect_error) {
echo json_encode([
'success' => false,
'error' => 'Database connection failed: ' . $conn->connect_error
]);
exit;
}
// Load application config so UrlHelper can resolve APP_DOMAIN
require_once __DIR__ . '/config/config.php';
2024-11-30 19:48:01 -05:00
2026-01-01 15:40:32 -05:00
// Authenticate via API key
require_once __DIR__ . '/middleware/ApiKeyAuth.php';
require_once __DIR__ . '/models/AuditLogModel.php';
require_once __DIR__ . '/helpers/UrlHelper.php';
2026-01-01 15:40:32 -05:00
$apiKeyAuth = new ApiKeyAuth($conn);
try {
$systemUser = $apiKeyAuth->authenticate();
} catch (Exception $e) {
// Authentication failed - ApiKeyAuth already sent the response
exit;
}
$userId = $systemUser['user_id'];
// Create tickets table with hash column if not exists
$createTableSQL = "CREATE TABLE IF NOT EXISTS tickets (
id INT AUTO_INCREMENT PRIMARY KEY,
ticket_id VARCHAR(9) NOT NULL,
title VARCHAR(255) NOT NULL,
hash VARCHAR(64) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_hash (hash)
)";
$conn->query($createTableSQL);
2025-02-27 22:12:28 -05:00
// Parse input regardless of content-type header
$rawInput = file_get_contents('php://input');
$data = json_decode($rawInput, true);
// Validate required fields before any processing
if (!is_array($data) || empty($data['title'])) {
// Try URL-encoded fallback
if (empty($data['title'])) {
parse_str($rawInput, $urlData);
if (!empty($urlData['title'])) {
$data = $urlData;
}
}
if (!is_array($data) || empty($data['title'])) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'title is required']);
exit;
}
}
// Generate hash from stable components
function generateTicketHash($data) {
// Extract device name if present (matches /dev/sdX, /dev/nvmeXnY patterns)
preg_match('/\/dev\/(sd[a-z]|nvme\d+n\d+)/', $data['title'], $deviceMatches);
$isDriveTicket = !empty($deviceMatches);
// Extract hostname from title [hostname][tags]...
preg_match('/\[([\w\d-]+)\]/', $data['title'], $hostMatches);
$hostname = $hostMatches[1] ?? '';
// Detect issue category (not specific attribute values)
$issueCategory = '';
$isClusterWide = false; // Flag for cluster-wide issues (exclude hostname from hash)
if (stripos($data['title'], 'SMART issues') !== false) {
$issueCategory = 'smart';
} elseif (stripos($data['title'], 'LXC') !== false || stripos($data['title'], 'storage usage') !== false) {
$issueCategory = 'storage';
} elseif (stripos($data['title'], 'memory') !== false) {
$issueCategory = 'memory';
} elseif (stripos($data['title'], 'cpu') !== false) {
$issueCategory = 'cpu';
} elseif (stripos($data['title'], 'network') !== false) {
$issueCategory = 'network';
} elseif (stripos($data['title'], 'Ceph') !== false || stripos($data['title'], '[ceph]') !== false) {
$issueCategory = 'ceph';
// Ceph cluster-wide issues should deduplicate across all nodes
// Check if this is a cluster-wide issue (not node-specific like OSD down on this node)
if (stripos($data['title'], '[cluster-wide]') !== false ||
stripos($data['title'], 'HEALTH_ERR') !== false ||
stripos($data['title'], 'HEALTH_WARN') !== false ||
stripos($data['title'], 'cluster usage') !== false) {
$isClusterWide = true;
}
}
2025-05-15 08:33:13 -04:00
// Build stable components with only static data
$stableComponents = [
'issue_category' => $issueCategory, // Generic category, not specific errors
2025-05-15 08:33:13 -04:00
'environment_tags' => array_filter(
explode('][', $data['title']),
fn($tag) => in_array($tag, ['production', 'development', 'staging', 'single-node', 'cluster-wide'])
2025-05-15 08:33:13 -04:00
)
];
// Only include hostname for non-cluster-wide issues
// This allows cluster-wide issues to deduplicate across all nodes
if (!$isClusterWide) {
$stableComponents['hostname'] = $hostname;
}
// Only include device info for drive-specific tickets
if ($isDriveTicket) {
$stableComponents['device'] = $deviceMatches[0];
}
// Sort arrays for consistent hashing
2025-05-15 08:33:13 -04:00
sort($stableComponents['environment_tags']);
return hash('sha256', json_encode($stableComponents, JSON_UNESCAPED_SLASHES));
}
// Check for duplicate tickets
$ticketHash = generateTicketHash($data);
$checkDuplicateSQL = "SELECT ticket_id FROM tickets WHERE hash = ? AND created_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)";
$checkStmt = $conn->prepare($checkDuplicateSQL);
$checkStmt->bind_param("s", $ticketHash);
$checkStmt->execute();
$result = $checkStmt->get_result();
if ($result->num_rows > 0) {
$existingTicket = $result->fetch_assoc();
echo json_encode([
'success' => false,
'error' => 'Duplicate ticket',
'existing_ticket_id' => $existingTicket['ticket_id']
]);
exit;
}
// Force JSON content type for all incoming requests
header('Content-Type: application/json');
2024-11-30 19:48:01 -05:00
// Generate ticket ID (9-digit format with leading zeros)
$ticket_id = sprintf('%09d', mt_rand(1, 999999999));
2026-01-01 15:40:32 -05:00
// Prepare insert query with created_by field
$sql = "INSERT INTO tickets (ticket_id, title, description, status, priority, category, type, hash, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
2024-11-30 19:48:01 -05:00
$stmt = $conn->prepare($sql);
// First, store all values in variables
$title = $data['title'];
$description = $data['description'];
$status = $data['status'] ?? 'Open';
$priority = $data['priority'] ?? '4';
$category = $data['category'] ?? 'General';
$type = $data['type'] ?? 'Issue';
// Then use the variables in bind_param
$stmt->bind_param(
2026-01-01 15:40:32 -05:00
"ssssssssi",
2024-11-30 19:48:01 -05:00
$ticket_id,
$title,
$description,
$status,
$priority,
$category,
$type,
2026-01-01 15:40:32 -05:00
$ticketHash,
$userId
2024-11-30 19:48:01 -05:00
);
if ($stmt->execute()) {
2026-01-01 15:40:32 -05:00
// Log ticket creation to audit log
$auditLog = new AuditLogModel($conn);
$auditLog->logTicketCreate($userId, $ticket_id, [
'title' => $title,
'priority' => $priority,
'category' => $category,
'type' => $type
]);
2024-11-30 19:48:01 -05:00
echo json_encode([
'success' => true,
'ticket_id' => $ticket_id,
'message' => 'Ticket created successfully'
]);
} else {
echo json_encode([
'success' => false,
'error' => $conn->error
]);
}
$stmt->close();
$conn->close();
// Matrix webhook notification
require_once __DIR__ . '/helpers/NotificationHelper.php';
NotificationHelper::sendTicketNotification($ticket_id, [
'title' => $title,
'priority' => $priority,
'category' => $category,
'type' => $type,
'status' => $status,
], 'automated');