Módulo VPSManager para WHMCS con configuración externa, acceso seguro al panel y métricas Graphite para LX.
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 
 
 
 

156 Zeilen
6.6 KiB

  1. <?php
  2. namespace VPSManager;
  3. require_once __DIR__ . '/Config.php';
  4. final class Graphite
  5. {
  6. public const RANGES = ['1h' => '-1h', '6h' => '-6h', '24h' => '-24h', '7d' => '-7d', '30d' => '-30d'];
  7. private const SUFFIXES = [
  8. 'load1' => 'load.load.shortterm',
  9. 'load5' => 'load.load.midterm',
  10. 'load15' => 'load.load.longterm',
  11. 'used' => 'memory.memory-used',
  12. 'free' => 'memory.memory-free',
  13. 'rx' => 'interface-eth0.if_octets.rx',
  14. 'tx' => 'interface-eth0.if_octets.tx',
  15. ];
  16. private $baseUrl;
  17. private $transport;
  18. public function __construct(array $config, ?callable $transport = null)
  19. {
  20. $this->baseUrl = Config::url($config, 'graphite_url');
  21. $this->transport = $transport ?? [self::class, 'request'];
  22. }
  23. public static function range($value): string
  24. {
  25. return is_string($value) && isset(self::RANGES[$value]) ? $value : '24h';
  26. }
  27. public static function validUuid($uuid): bool
  28. {
  29. return is_string($uuid) && preg_match('/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i', $uuid) === 1;
  30. }
  31. // Only WHMCS-authorized module parameters may be passed here; never request UUID/type.
  32. public function getForService(array $params, $range = '24h'): array
  33. {
  34. $fields = $params['customfields'] ?? [];
  35. if (($fields['vtype'] ?? null) !== 'lx' || !self::validUuid($fields['uuid'] ?? null)) {
  36. throw new \InvalidArgumentException('Servicio no compatible con las métricas LX.');
  37. }
  38. $range = self::range($range);
  39. $targets = [];
  40. foreach (self::SUFFIXES as $key => $suffix) {
  41. $targets[$key] = 'lx.' . $fields['uuid'] . '.' . $suffix;
  42. }
  43. $query = 'format=json&from=' . rawurlencode(self::RANGES[$range]) . '&until=now&maxDataPoints=1000';
  44. foreach ($targets as $target) {
  45. $query .= '&target=' . rawurlencode($target);
  46. }
  47. $response = call_user_func($this->transport, $this->baseUrl . '/render/?' . $query);
  48. if (!is_array($response) || ($response['status'] ?? 0) !== 200 || !is_string($response['body'] ?? null)) {
  49. throw new \RuntimeException('Graphite HTTP error.');
  50. }
  51. $raw = json_decode($response['body'], true, 32);
  52. if (substr(ltrim($response['body']), 0, 1) !== '[' || json_last_error() !== JSON_ERROR_NONE || !is_array($raw) || !array_is_list($raw)) {
  53. throw new \RuntimeException('Graphite JSON error.');
  54. }
  55. $series = array_fill_keys(array_keys($targets), []);
  56. $seen = [];
  57. foreach ($raw as $row) {
  58. if (!is_array($row) || !is_string($row['target'] ?? null)) {
  59. throw new \RuntimeException('Graphite series error.');
  60. }
  61. $key = array_search($row['target'], $targets, true);
  62. if ($key === false) {
  63. continue; // Never expose unrequested targets or metadata.
  64. }
  65. if (isset($seen[$key]) || !is_array($row['datapoints'] ?? null) || count($row['datapoints']) > 10000) {
  66. throw new \RuntimeException('Graphite series error.');
  67. }
  68. $seen[$key] = true;
  69. $points = [];
  70. foreach ($row['datapoints'] as $point) {
  71. if (!is_array($point) || count($point) !== 2 || !array_is_list($point)
  72. || !is_int($point[1]) || $point[1] < 0 || $point[1] > 253402300799) {
  73. throw new \RuntimeException('Graphite datapoint error.');
  74. }
  75. $value = $point[0];
  76. if ($value !== null && ((!is_int($value) && !is_float($value)) || !is_finite((float) $value) || $value < 0)) {
  77. throw new \RuntimeException('Graphite value error.');
  78. }
  79. if (array_key_exists($point[1], $points)) {
  80. throw new \RuntimeException('Graphite duplicate timestamp.');
  81. }
  82. $points[$point[1]] = $value;
  83. }
  84. ksort($points, SORT_NUMERIC);
  85. foreach ($points as $timestamp => $value) {
  86. $series[$key][] = ['x' => $timestamp * 1000, 'y' => $value];
  87. }
  88. }
  89. // Align memory by timestamps, never by index; absent data stays null.
  90. $used = array_column($series['used'], 'y', 'x');
  91. $free = array_column($series['free'], 'y', 'x');
  92. $times = array_unique(array_merge(array_keys($used), array_keys($free)));
  93. sort($times, SORT_NUMERIC);
  94. $total = [];
  95. $percent = [];
  96. foreach ($times as $time) {
  97. $u = $used[$time] ?? null;
  98. $f = $free[$time] ?? null;
  99. $sum = $u !== null && $f !== null ? $u + $f : null;
  100. if ($sum !== null && !is_finite((float) $sum)) {
  101. throw new \RuntimeException('Graphite memory overflow.');
  102. }
  103. $total[] = ['x' => $time, 'y' => $sum];
  104. $percent[] = ['x' => $time, 'y' => $sum !== null && $sum > 0 ? ($u / $sum) * 100 : null];
  105. }
  106. return [
  107. 'range' => $range,
  108. 'load' => ['1 min' => $series['load1'], '5 min' => $series['load5'], '15 min' => $series['load15']],
  109. 'memory' => ['Usada' => $series['used'], 'Total' => $total],
  110. 'memoryPercent' => $percent,
  111. 'network' => ['RX' => $series['rx'], 'TX' => $series['tx']], // bytes/s; presentation converts once.
  112. ];
  113. }
  114. private static function request(string $url): array
  115. {
  116. if (!function_exists('curl_init')) {
  117. throw new \RuntimeException('Graphite transport unavailable.');
  118. }
  119. $ch = curl_init($url);
  120. $body = '';
  121. curl_setopt_array($ch, [
  122. CURLOPT_CONNECTTIMEOUT => 3,
  123. CURLOPT_TIMEOUT => 5,
  124. CURLOPT_FOLLOWLOCATION => false,
  125. CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
  126. CURLOPT_SSL_VERIFYPEER => true,
  127. CURLOPT_SSL_VERIFYHOST => 2,
  128. CURLOPT_HTTPHEADER => ['Accept: application/json'],
  129. CURLOPT_WRITEFUNCTION => static function ($handle, $chunk) use (&$body) {
  130. if (strlen($body) + strlen($chunk) > 2097152) {
  131. return 0;
  132. }
  133. $body .= $chunk;
  134. return strlen($chunk);
  135. },
  136. ]);
  137. try {
  138. $ok = curl_exec($ch);
  139. $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  140. if ($ok === false) {
  141. throw new \RuntimeException('Graphite transport error.');
  142. }
  143. return ['status' => $status, 'body' => $body];
  144. } finally {
  145. curl_close($ch);
  146. }
  147. }
  148. }