3241 |
rexy |
1 |
<?php
|
|
|
2 |
|
|
|
3 |
namespace mbolli\nfsen_ng\datasources;
|
|
|
4 |
|
|
|
5 |
use JetBrains\PhpStorm\ExpectedValues;
|
|
|
6 |
use mbolli\nfsen_ng\common\Config;
|
|
|
7 |
use mbolli\nfsen_ng\common\Debug;
|
|
|
8 |
|
|
|
9 |
class Rrd implements Datasource {
|
|
|
10 |
private readonly Debug $d;
|
|
|
11 |
private array $fields = [
|
|
|
12 |
'flows',
|
|
|
13 |
'flows_tcp',
|
|
|
14 |
'flows_udp',
|
|
|
15 |
'flows_icmp',
|
|
|
16 |
'flows_other',
|
|
|
17 |
'packets',
|
|
|
18 |
'packets_tcp',
|
|
|
19 |
'packets_udp',
|
|
|
20 |
'packets_icmp',
|
|
|
21 |
'packets_other',
|
|
|
22 |
'bytes',
|
|
|
23 |
'bytes_tcp',
|
|
|
24 |
'bytes_udp',
|
|
|
25 |
'bytes_icmp',
|
|
|
26 |
'bytes_other',
|
|
|
27 |
];
|
|
|
28 |
|
|
|
29 |
private array $layout = [
|
|
|
30 |
'0.5:1:' . ((60 / (1 * 5)) * 24 * 45), // 45 days of 5 min samples
|
|
|
31 |
'0.5:6:' . ((60 / (6 * 5)) * 24 * 90), // 90 days of 30 min samples
|
|
|
32 |
'0.5:24:' . ((60 / (24 * 5)) * 24 * 360), // 360 days of 2 hour samples
|
|
|
33 |
'0.5:288:1080', // 1080 days of daily samples
|
|
|
34 |
// = 3 years of data
|
|
|
35 |
];
|
|
|
36 |
|
|
|
37 |
public function __construct() {
|
|
|
38 |
$this->d = Debug::getInstance();
|
|
|
39 |
|
|
|
40 |
if (!\function_exists('rrd_version')) {
|
|
|
41 |
throw new \Exception('Please install the PECL rrd library.');
|
|
|
42 |
}
|
|
|
43 |
}
|
|
|
44 |
|
|
|
45 |
/**
|
|
|
46 |
* Gets the timestamps of the first and last entry of this specific source.
|
|
|
47 |
*/
|
|
|
48 |
public function date_boundaries(string $source): array {
|
|
|
49 |
$rrdFile = $this->get_data_path($source);
|
|
|
50 |
|
|
|
51 |
return [rrd_first($rrdFile), rrd_last($rrdFile)];
|
|
|
52 |
}
|
|
|
53 |
|
|
|
54 |
/**
|
|
|
55 |
* Gets the timestamp of the last update of this specific source.
|
|
|
56 |
*
|
|
|
57 |
* @return int timestamp or false
|
|
|
58 |
*/
|
|
|
59 |
public function last_update(string $source = '', int $port = 0): int {
|
|
|
60 |
$rrdFile = $this->get_data_path($source, $port);
|
|
|
61 |
$last_update = rrd_last($rrdFile);
|
|
|
62 |
|
|
|
63 |
// $this->d->log('Last update of ' . $rrdFile . ': ' . date('d.m.Y H:i', $last_update), LOG_DEBUG);
|
|
|
64 |
return (int) $last_update;
|
|
|
65 |
}
|
|
|
66 |
|
|
|
67 |
/**
|
|
|
68 |
* Create a new RRD file for a source.
|
|
|
69 |
*
|
|
|
70 |
* @param string $source e.g. gateway or server_xyz
|
|
|
71 |
* @param bool $reset overwrites existing RRD file if true
|
|
|
72 |
*/
|
|
|
73 |
public function create(string $source, int $port = 0, bool $reset = false): bool {
|
|
|
74 |
$rrdFile = $this->get_data_path($source, $port);
|
|
|
75 |
|
|
|
76 |
// check if folder exists
|
|
|
77 |
if (!file_exists(\dirname($rrdFile))) {
|
|
|
78 |
mkdir(\dirname($rrdFile), 0o755, true);
|
|
|
79 |
}
|
|
|
80 |
|
|
|
81 |
// check if folder has correct access rights
|
|
|
82 |
if (!is_writable(\dirname($rrdFile))) {
|
|
|
83 |
$this->d->log('Error creating ' . $rrdFile . ': Not writable', \LOG_CRIT);
|
|
|
84 |
|
|
|
85 |
return false;
|
|
|
86 |
}
|
|
|
87 |
// check if file already exists
|
|
|
88 |
if (file_exists($rrdFile)) {
|
|
|
89 |
if ($reset === true) {
|
|
|
90 |
unlink($rrdFile);
|
|
|
91 |
} else {
|
|
|
92 |
$this->d->log('Error creating ' . $rrdFile . ': File already exists', \LOG_ERR);
|
|
|
93 |
|
|
|
94 |
return false;
|
|
|
95 |
}
|
|
|
96 |
}
|
|
|
97 |
|
|
|
98 |
$start = strtotime('3 years ago');
|
|
|
99 |
$starttime = (int) $start - ($start % 300);
|
|
|
100 |
|
|
|
101 |
$creator = new \RRDCreator($rrdFile, (string) $starttime, 60 * 5);
|
|
|
102 |
foreach ($this->fields as $field) {
|
|
|
103 |
$creator->addDataSource($field . ':ABSOLUTE:600:U:U');
|
|
|
104 |
}
|
|
|
105 |
foreach ($this->layout as $rra) {
|
|
|
106 |
$creator->addArchive('AVERAGE:' . $rra);
|
|
|
107 |
$creator->addArchive('MAX:' . $rra);
|
|
|
108 |
}
|
|
|
109 |
|
|
|
110 |
$saved = $creator->save();
|
|
|
111 |
if ($saved === false) {
|
|
|
112 |
$this->d->log('Error saving RRD data structure to ' . $rrdFile, \LOG_ERR);
|
|
|
113 |
}
|
|
|
114 |
|
|
|
115 |
return $saved;
|
|
|
116 |
}
|
|
|
117 |
|
|
|
118 |
/**
|
|
|
119 |
* Write to an RRD file with supplied data.
|
|
|
120 |
*
|
|
|
121 |
* @throws \Exception
|
|
|
122 |
*/
|
|
|
123 |
public function write(array $data): bool {
|
|
|
124 |
$rrdFile = $this->get_data_path($data['source'], $data['port']);
|
|
|
125 |
if (!file_exists($rrdFile)) {
|
|
|
126 |
$this->create($data['source'], $data['port'], false);
|
|
|
127 |
}
|
|
|
128 |
|
|
|
129 |
$nearest = (int) $data['date_timestamp'] - ($data['date_timestamp'] % 300);
|
|
|
130 |
$this->d->log('Writing to file ' . $rrdFile, \LOG_DEBUG);
|
|
|
131 |
|
|
|
132 |
// write data
|
|
|
133 |
$updater = new \RRDUpdater($rrdFile);
|
|
|
134 |
|
|
|
135 |
return $updater->update($data['fields'], (string) $nearest);
|
|
|
136 |
}
|
|
|
137 |
|
|
|
138 |
/**
|
|
|
139 |
* @param string $type flows/packets/traffic
|
|
|
140 |
* @param string $display protocols/sources/ports
|
|
|
141 |
*/
|
|
|
142 |
public function get_graph_data(
|
|
|
143 |
int $start,
|
|
|
144 |
int $end,
|
|
|
145 |
array $sources,
|
|
|
146 |
array $protocols,
|
|
|
147 |
array $ports,
|
|
|
148 |
#[ExpectedValues(['flows', 'packets', 'bytes', 'bits'])]
|
|
|
149 |
string $type = 'flows',
|
|
|
150 |
#[ExpectedValues(['protocols', 'sources', 'ports'])]
|
|
|
151 |
string $display = 'sources',
|
|
|
152 |
): array|string {
|
|
|
153 |
$options = [
|
|
|
154 |
'--start',
|
|
|
155 |
$start - ($start % 300),
|
|
|
156 |
'--end',
|
|
|
157 |
$end - ($end % 300),
|
|
|
158 |
'--maxrows',
|
|
|
159 |
300,
|
|
|
160 |
// number of values. works like the width value (in pixels) in rrd_graph
|
|
|
161 |
// '--step', 1200, // by default, rrdtool tries to get data for each row. if you want rrdtool to get data at a one-hour resolution, set step to 3600.
|
|
|
162 |
'--json',
|
|
|
163 |
];
|
|
|
164 |
|
|
|
165 |
$useBits = false;
|
|
|
166 |
if ($type === 'bits') {
|
|
|
167 |
$type = 'bytes';
|
|
|
168 |
$useBits = true;
|
|
|
169 |
}
|
|
|
170 |
|
|
|
171 |
if (empty($protocols)) {
|
|
|
172 |
$protocols = ['tcp', 'udp', 'icmp', 'other'];
|
|
|
173 |
}
|
|
|
174 |
if (empty($sources)) {
|
|
|
175 |
$sources = Config::$cfg['general']['sources'];
|
|
|
176 |
}
|
|
|
177 |
if (empty($ports)) {
|
|
|
178 |
$ports = Config::$cfg['general']['ports'];
|
|
|
179 |
}
|
|
|
180 |
|
|
|
181 |
switch ($display) {
|
|
|
182 |
case 'protocols':
|
|
|
183 |
foreach ($protocols as $protocol) {
|
|
|
184 |
$rrdFile = $this->get_data_path($sources[0]);
|
|
|
185 |
$proto = ($protocol === 'any') ? '' : '_' . $protocol;
|
|
|
186 |
$legend = array_filter([$protocol, $type, $sources[0]]);
|
|
|
187 |
$options[] = 'DEF:data' . $sources[0] . $protocol . '=' . $rrdFile . ':' . $type . $proto . ':AVERAGE';
|
|
|
188 |
$options[] = 'XPORT:data' . $sources[0] . $protocol . ':' . implode('_', $legend);
|
|
|
189 |
}
|
|
|
190 |
break;
|
|
|
191 |
case 'sources':
|
|
|
192 |
foreach ($sources as $source) {
|
|
|
193 |
$rrdFile = $this->get_data_path($source);
|
|
|
194 |
$proto = ($protocols[0] === 'any') ? '' : '_' . $protocols[0];
|
|
|
195 |
$legend = array_filter([$source, $type, $protocols[0]]);
|
|
|
196 |
$options[] = 'DEF:data' . $source . '=' . $rrdFile . ':' . $type . $proto . ':AVERAGE';
|
|
|
197 |
$options[] = 'XPORT:data' . $source . ':' . implode('_', $legend);
|
|
|
198 |
}
|
|
|
199 |
break;
|
|
|
200 |
case 'ports':
|
|
|
201 |
foreach ($ports as $port) {
|
|
|
202 |
$source = ($sources[0] === 'any') ? '' : $sources[0];
|
|
|
203 |
$proto = ($protocols[0] === 'any') ? '' : '_' . $protocols[0];
|
|
|
204 |
$legend = array_filter([$port, $type, $source, $protocols[0]]);
|
|
|
205 |
$rrdFile = $this->get_data_path($source, $port);
|
|
|
206 |
$options[] = 'DEF:data' . $source . $port . '=' . $rrdFile . ':' . $type . $proto . ':AVERAGE';
|
|
|
207 |
$options[] = 'XPORT:data' . $source . $port . ':' . implode('_', $legend);
|
|
|
208 |
}
|
|
|
209 |
}
|
|
|
210 |
|
|
|
211 |
ob_start();
|
|
|
212 |
$data = rrd_xport($options);
|
|
|
213 |
$error = ob_get_clean(); // rrd_xport weirdly prints stuff on error
|
|
|
214 |
|
|
|
215 |
if (!\is_array($data)) {
|
|
|
216 |
return $error . '. ' . rrd_error();
|
|
|
217 |
}
|
|
|
218 |
|
|
|
219 |
// remove invalid numbers and create processable array
|
|
|
220 |
$output = [
|
|
|
221 |
'data' => [],
|
|
|
222 |
'start' => $data['start'],
|
|
|
223 |
'end' => $data['end'],
|
|
|
224 |
'step' => $data['step'],
|
|
|
225 |
'legend' => [],
|
|
|
226 |
];
|
|
|
227 |
foreach ($data['data'] as $source) {
|
|
|
228 |
$output['legend'][] = $source['legend'];
|
|
|
229 |
foreach ($source['data'] as $date => $measure) {
|
|
|
230 |
// ignore non-valid measures
|
|
|
231 |
if (is_nan($measure)) {
|
|
|
232 |
$measure = null;
|
|
|
233 |
}
|
|
|
234 |
|
|
|
235 |
if ($type === 'bytes' && $useBits) {
|
|
|
236 |
$measure *= 8;
|
|
|
237 |
}
|
|
|
238 |
|
|
|
239 |
// add measure to output array
|
|
|
240 |
if (\array_key_exists($date, $output['data'])) {
|
|
|
241 |
$output['data'][$date][] = $measure;
|
|
|
242 |
} else {
|
|
|
243 |
$output['data'][$date] = [$measure];
|
|
|
244 |
}
|
|
|
245 |
}
|
|
|
246 |
}
|
|
|
247 |
|
|
|
248 |
return $output;
|
|
|
249 |
}
|
|
|
250 |
|
|
|
251 |
/**
|
|
|
252 |
* Creates a new database for every source/port combination.
|
|
|
253 |
*/
|
|
|
254 |
public function reset(array $sources): bool {
|
|
|
255 |
$return = false;
|
|
|
256 |
if (empty($sources)) {
|
|
|
257 |
$sources = Config::$cfg['general']['sources'];
|
|
|
258 |
}
|
|
|
259 |
$ports = Config::$cfg['general']['ports'];
|
|
|
260 |
$ports[] = 0;
|
|
|
261 |
foreach ($ports as $port) {
|
|
|
262 |
if ($port !== 0) {
|
|
|
263 |
$return = $this->create('', $port, true);
|
|
|
264 |
}
|
|
|
265 |
if ($return === false) {
|
|
|
266 |
return false;
|
|
|
267 |
}
|
|
|
268 |
|
|
|
269 |
foreach ($sources as $source) {
|
|
|
270 |
$return = $this->create($source, $port, true);
|
|
|
271 |
if ($return === false) {
|
|
|
272 |
return false;
|
|
|
273 |
}
|
|
|
274 |
}
|
|
|
275 |
}
|
|
|
276 |
|
|
|
277 |
return true;
|
|
|
278 |
}
|
|
|
279 |
|
|
|
280 |
/**
|
|
|
281 |
* Concatenates the path to the source's rrd file.
|
|
|
282 |
*/
|
|
|
283 |
public function get_data_path(string $source = '', int $port = 0): string {
|
|
|
284 |
if ((int) $port === 0) {
|
|
|
285 |
$port = '';
|
|
|
286 |
} else {
|
|
|
287 |
$port = (empty($source)) ? $port : '_' . $port;
|
|
|
288 |
}
|
|
|
289 |
$path = Config::$path . \DIRECTORY_SEPARATOR . 'datasources' . \DIRECTORY_SEPARATOR . 'data' . \DIRECTORY_SEPARATOR . $source . $port . '.rrd';
|
|
|
290 |
|
|
|
291 |
if (!file_exists($path)) {
|
|
|
292 |
$this->d->log('Was not able to find ' . $path, \LOG_INFO);
|
|
|
293 |
}
|
|
|
294 |
|
|
|
295 |
return $path;
|
|
|
296 |
}
|
|
|
297 |
}
|