Módulo VPSManager para WHMCS con configuración externa, acceso seguro al panel y métricas Graphite para LX.
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 
 
 

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