#!/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: 01.00.00 * BRIEF: Manage Grafana dashboards via API */ declare(strict_types=1); final class GrafanaDashboard { 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; public function run(): int { $this->parseArgs(); if ($this->grafanaUrl === '') { $this->grafanaUrl = getenv('GRAFANA_URL') ?: ''; } 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).' ); $this->printUsage(); 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("OK: {$status} (uid: {$uid})"); if ($url !== '') { $this->log("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("OK: Deleted dashboard {$this->uid}"); return 0; } if ($response['code'] === 404) { $this->log( "WARN: 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('No dashboards found.'); return 0; } $this->log(sprintf( '%-30s | %-20s | %s', 'Title', 'UID', 'Folder' )); $this->log(str_repeat('-', 75)); foreach ($dashboards as $d) { $this->log(sprintf( '%-30s | %-20s | %s', substr($d['title'] ?? '', 0, 30), $d['uid'] ?? '', $d['folderTitle'] ?? 'General' )); } $this->log(''); $this->log(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( "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->log( "WARN: Folder \"{$title}\" not found, " . "using General." ); return 0; } private function noCommand(): int { $this->log('ERROR: No command specified.'); $this->printUsage(); return 1; } private function parseArgs(): void { $args = $_SERVER['argv'] ?? []; $count = count($args); for ($i = 1; $i < $count; $i++) { switch ($args[$i]) { case 'push': case 'delete': case 'list': case 'export': $this->command = $args[$i]; break; case '--url': $this->grafanaUrl = rtrim( $args[++$i] ?? '', '/' ); break; case '--token': $this->token = $args[++$i] ?? ''; break; case '--uid': $this->uid = $args[++$i] ?? ''; break; case '--file': $this->file = $args[++$i] ?? ''; break; case '--folder-id': $this->folderId = (int) ( $args[++$i] ?? 0 ); break; case '--folder': $this->folderTitle = $args[++$i] ?? ''; break; case '--no-overwrite': $this->overwrite = false; break; case '--help': case '-h': $this->printUsage(); exit(0); default: $this->log( "WARNING: Unknown arg: {$args[$i]}" ); break; } } } private function printUsage(): void { $u = 'Usage: grafana_dashboard.php ' . '--url --token [options]'; $this->log($u); $this->log(''); $this->log('Commands:'); $this->log(' push Create/update dashboard from JSON'); $this->log(' delete Delete a dashboard by UID'); $this->log(' list List dashboards (optionally by folder)'); $this->log(' export Export dashboard JSON by UID'); $this->log(''); $this->log('Options:'); $this->log(' --url Grafana URL (or GRAFANA_URL)'); $this->log(' --token API token (or GRAFANA_TOKEN)'); $this->log(' --uid Dashboard UID (delete/export)'); $this->log(' --file JSON file (push/export)'); $this->log(' --folder Folder name (push/list)'); $this->log(' --folder-id Folder ID (push/list)'); $this->log(' --no-overwrite Fail if dashboard exists'); $this->log(' --help, -h Show this help'); } 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(" Grafana: {$data['message']}"); } } private function log(string $message): void { fwrite(STDERR, $message . PHP_EOL); } } $app = new GrafanaDashboard(); exit($app->run());