#!/usr/bin/env php * * This file is part of a Moko Consulting project. * * SPDX-License-Identifier: GPL-3.0-or-later * * FILE INFORMATION * DEFGROUP: moko-platform.CLI * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/grafana_dashboard.php * VERSION: 09.25.00 * BRIEF: Manage Grafana dashboards via API */ declare(strict_types=1); require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; use MokoEnterprise\CliFramework; class GrafanaDashboardCli extends CliFramework { private string $grafanaUrl = ''; private string $token = ''; private string $command = ''; private string $uid = ''; private string $file = ''; private int $folderId = 0; private string $folderTitle = ''; private bool $overwrite = true; protected function configure(): void { $this->setDescription('Manage Grafana dashboards via API'); $this->addArgument('--url', 'Grafana URL (or GRAFANA_URL)', ''); $this->addArgument('--token', 'API token (or GRAFANA_TOKEN)', ''); $this->addArgument('--uid', 'Dashboard UID (delete/export)', ''); $this->addArgument('--file', 'JSON file (push/export)', ''); $this->addArgument('--folder', 'Folder name (push/list)', ''); $this->addArgument('--folder-id', 'Folder ID (push/list)', '0'); $this->addArgument('--no-overwrite', 'Fail if dashboard exists', false); $this->addArgument('--command', 'Command: push, delete, list, export', ''); } protected function run(): int { // Parse positional command from raw argv $rawArgs = $_SERVER['argv'] ?? []; foreach ($rawArgs as $arg) { if (in_array($arg, ['push', 'delete', 'list', 'export'], true)) { $this->command = $arg; break; } } if ($this->command === '' && $this->getArgument('--command') !== '') { $this->command = $this->getArgument('--command'); } $this->grafanaUrl = $this->getArgument('--url'); $this->token = $this->getArgument('--token'); $this->uid = $this->getArgument('--uid'); $this->file = $this->getArgument('--file'); $this->folderTitle = $this->getArgument('--folder'); $this->folderId = (int) $this->getArgument('--folder-id'); $this->overwrite = !$this->getArgument('--no-overwrite'); if ($this->grafanaUrl === '') { $this->grafanaUrl = getenv('GRAFANA_URL') ?: ''; } $this->grafanaUrl = rtrim($this->grafanaUrl, '/'); if ($this->token === '') { $this->token = getenv('GRAFANA_TOKEN') ?: ''; } if ($this->grafanaUrl === '' || $this->token === '') { $this->log('ERROR', '--url and --token are required (or set GRAFANA_URL / GRAFANA_TOKEN env vars).'); return 1; } return match ($this->command) { 'push' => $this->pushDashboard(), 'delete' => $this->deleteDashboard(), 'list' => $this->listDashboards(), 'export' => $this->exportDashboard(), default => $this->noCommand(), }; } private function pushDashboard(): int { if ($this->file === '') { $this->log('ERROR', '--file is required for push.'); return 1; } if (!file_exists($this->file)) { $this->log('ERROR', "File not found: {$this->file}"); return 1; } $json = file_get_contents($this->file); $dashboard = json_decode($json, true); if (!is_array($dashboard)) { $this->log('ERROR', 'Invalid JSON in dashboard file.'); return 1; } if ($this->folderTitle !== '' && $this->folderId === 0) { $this->folderId = $this->resolveFolderId($this->folderTitle); if ($this->folderId < 0) { return 1; } } $dashboard['id'] = null; $payload = json_encode([ 'dashboard' => $dashboard, 'folderId' => $this->folderId, 'overwrite' => $this->overwrite, ]); $response = $this->apiRequest('POST', '/api/dashboards/db', $payload); if ($response['code'] === 200) { $data = json_decode($response['body'], true); $uid = $data['uid'] ?? '?'; $url = $data['url'] ?? ''; $status = $data['status'] ?? 'success'; $this->log('INFO', "OK: {$status} (uid: {$uid})"); if ($url !== '') { $this->log('INFO', "URL: {$this->grafanaUrl}{$url}"); } return 0; } $this->log('ERROR', "Push failed (HTTP {$response['code']})"); $this->logApiError($response['body']); return 1; } private function deleteDashboard(): int { if ($this->uid === '') { $this->log('ERROR', '--uid is required for delete.'); return 1; } $response = $this->apiRequest('DELETE', "/api/dashboards/uid/{$this->uid}"); if ($response['code'] === 200) { $this->log('INFO', "OK: Deleted dashboard {$this->uid}"); return 0; } if ($response['code'] === 404) { $this->warning("Dashboard {$this->uid} not found."); return 0; } $this->log('ERROR', "Delete failed (HTTP {$response['code']})"); $this->logApiError($response['body']); return 1; } private function listDashboards(): int { $query = '/api/search?type=dash-db'; if ($this->folderId > 0) { $query .= "&folderIds={$this->folderId}"; } if ($this->folderTitle !== '' && $this->folderId === 0) { $fid = $this->resolveFolderId($this->folderTitle); if ($fid > 0) { $query .= "&folderIds={$fid}"; } } $response = $this->apiRequest('GET', $query); if ($response['code'] !== 200) { $this->log('ERROR', "List failed (HTTP {$response['code']})"); $this->logApiError($response['body']); return 1; } $dashboards = json_decode($response['body'], true); if (!is_array($dashboards) || count($dashboards) === 0) { $this->log('INFO', 'No dashboards found.'); return 0; } fprintf(STDERR, "%-30s | %-20s | %s\n", 'Title', 'UID', 'Folder'); fprintf(STDERR, "%s\n", str_repeat('-', 75)); foreach ($dashboards as $d) { fprintf( STDERR, "%-30s | %-20s | %s\n", substr($d['title'] ?? '', 0, 30), $d['uid'] ?? '', $d['folderTitle'] ?? 'General' ); } echo "\n"; $this->log('INFO', count($dashboards) . ' dashboard(s).'); return 0; } private function exportDashboard(): int { if ($this->uid === '') { $this->log('ERROR', '--uid is required for export.'); return 1; } $response = $this->apiRequest('GET', "/api/dashboards/uid/{$this->uid}"); if ($response['code'] !== 200) { $this->log('ERROR', "Export failed (HTTP {$response['code']})"); $this->logApiError($response['body']); return 1; } $data = json_decode($response['body'], true); $dashboard = $data['dashboard'] ?? null; if ($dashboard === null) { $this->log('ERROR', 'No dashboard data in response.'); return 1; } $output = json_encode( $dashboard, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) . "\n"; if ($this->file !== '') { file_put_contents($this->file, $output); $this->log('INFO', "Exported {$this->uid} to {$this->file}"); } else { fwrite(STDOUT, $output); } return 0; } private function resolveFolderId(string $title): int { $response = $this->apiRequest('GET', '/api/folders'); if ($response['code'] !== 200) { $this->log('ERROR', "Could not fetch folders (HTTP {$response['code']})"); return -1; } $folders = json_decode($response['body'], true); if (!is_array($folders)) { return -1; } foreach ($folders as $f) { if (strcasecmp($f['title'] ?? '', $title) === 0) { return (int) ($f['id'] ?? 0); } } $this->warning("Folder \"{$title}\" not found, using General."); return 0; } private function noCommand(): int { $this->log('ERROR', 'No command specified. Use: push, delete, list, export'); return 1; } private function apiRequest( string $method, string $endpoint, ?string $body = null ): array { $url = $this->grafanaUrl . $endpoint; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'Accept: application/json', "Authorization: Bearer {$this->token}", ]); if ($body !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, $body); } $responseBody = curl_exec($ch); $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { $error = curl_error($ch); curl_close($ch); return [ 'code' => 0, 'body' => "cURL error: {$error}", ]; } curl_close($ch); return ['code' => $httpCode, 'body' => $responseBody]; } private function logApiError(string $body): void { $data = json_decode($body, true); if (is_array($data) && isset($data['message'])) { $this->log('ERROR', " Grafana: {$data['message']}"); } } } $app = new GrafanaDashboardCli(); exit($app->execute());