Subversion Repositories ALCASAR

Rev

Details | Last modification | View Log

Rev Author Line No. Line
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
}