Issues (2963)

LibreNMS/Alert/Transport/Sensu.php (1 issue)

1
<?php
2
/* Copyright (C) 2020 Adam Bishop <[email protected]>
3
 * This program is free software: you can redistribute it and/or modify
4
 * it under the terms of the GNU General Public License as published by
5
 * the Free Software Foundation, either version 3 of the License, or
6
 * (at your option) any later version.
7
 *
8
 * This program is distributed in the hope that it will be useful,
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 * GNU General Public License for more details.
11
 *
12
 * You should have received a copy of the GNU General Public License
13
 * along with this program.  If not, see <https://www.gnu.org/licenses/>. */
14
15
/**
16
 * API Transport
17
 *
18
 * @author Adam Bishop <[email protected]>
19
 * @copyright 2020 Adam Bishop, LibreNMS
20
 * @license GPL
21
 */
22
23
namespace LibreNMS\Alert\Transport;
24
25
use GuzzleHttp\Client;
26
use GuzzleHttp\Exception\GuzzleException;
27
use Illuminate\Support\Facades\Log;
28
use LibreNMS\Alert\Transport;
29
use LibreNMS\Config;
30
use LibreNMS\Enum\AlertState;
31
32
class Sensu extends Transport
33
{
34
    // Sensu alert coding
35
    const OK = 0;
36
    const WARNING = 1;
37
    const CRITICAL = 2;
38
    const UNKNOWN = 3;
39
40
    private static $status = [
41
        'ok' => Sensu::OK,
42
        'warning' => Sensu::WARNING,
43
        'critical' => Sensu::CRITICAL,
44
    ];
45
46
    private static $severity = [
0 ignored issues
show
The private property $severity is not used, and could be removed.
Loading history...
47
        'recovered' => AlertState::RECOVERED,
48
        'alert' => AlertState::ACTIVE,
49
        'acknowledged' => AlertState::ACKNOWLEDGED,
50
        'worse' => AlertState::WORSE,
51
        'better' => AlertState::BETTER,
52
    ];
53
54
    private static $client = null;
55
56
    public function deliverAlert($obj, $opts)
57
    {
58
        $sensu_opts = [];
59
        $sensu_opts['url'] = $this->config['sensu-url'] ? $this->config['sensu-url'] : 'http://127.0.0.1:3031';
60
        $sensu_opts['namespace'] = $this->config['sensu-namespace'] ? $this->config['sensu-namespace'] : 'default';
61
        $sensu_opts['prefix'] = $this->config['sensu-prefix'];
62
        $sensu_opts['source-key'] = $this->config['sensu-source-key'];
63
64
        Sensu::$client = new Client();
65
66
        try {
67
            return $this->contactSensu($obj, $sensu_opts);
68
        } catch (GuzzleException $e) {
69
            return 'Sending event to Sensu failed: ' . $e->getMessage();
70
        }
71
    }
72
73
    public static function contactSensu($obj, $opts)
74
    {
75
        // The Sensu agent should be running on the poller - events can be sent directly to the backend but this has not been tested, and likely needs mTLS.
76
        // The agent API is documented at https://docs.sensu.io/sensu-go/latest/reference/agent/#create-monitoring-events-using-the-agent-api
77
        if (Sensu::$client->request('GET', $opts['url'] . '/healthz')->getStatusCode() !== 200) {
78
            return 'Sensu API is not responding';
79
        }
80
81
        if ($obj['state'] !== AlertState::RECOVERED && $obj['state'] !== AlertState::ACKNOWLEDGED && $obj['alerted'] === 0) {
82
            // If this is the first event, send a forced "ok" dated (rrd.step / 2) seconds ago to tell Sensu the last time the check was healthy
83
            $data = Sensu::generateData($obj, $opts, Sensu::OK, round(Config::get('rrd.step', 300) / 2));
84
            Log::debug('Sensu transport sent last good event to socket: ', $data);
85
86
            $result = Sensu::$client->request('POST', $opts['url'] . '/events', ['json' => $data]);
87
            if ($result->getStatusCode() !== 202) {
88
                return $result->getReasonPhrase();
89
            }
90
91
            sleep(5);
92
        }
93
94
        $data = Sensu::generateData($obj, $opts, Sensu::calculateStatus($obj['state'], $obj['severity']));
95
        Log::debug('Sensu transport sent event to socket: ', $data);
96
97
        $result = Sensu::$client->request('POST', $opts['url'] . '/events', ['json' => $data]);
98
        if ($result->getStatusCode() === 202) {
99
            return true;
100
        }
101
102
        return $result->getReasonPhrase();
103
    }
104
105
    public static function generateData($obj, $opts, $status, $offset = 0)
106
    {
107
        return [
108
            'check' => [
109
                'metadata' => [
110
                    'name' => Sensu::checkName($opts['prefix'], $obj['name']),
111
                    'namespace' => $opts['namespace'],
112
                    'annotations' => Sensu::generateAnnotations($obj),
113
                ],
114
                'command' => sprintf('LibreNMS: %s', $obj['builder']),
115
                'executed' => time() - $offset,
116
                'interval' => Config::get('rrd.step', 300),
117
                'issued' => time() - $offset,
118
                'output' => $obj['msg'],
119
                'status' => $status,
120
            ],
121
            'entity' => [
122
                'metadata' => [
123
                    'name' => Sensu::getEntityName($obj, $opts['source-key']),
124
                    'namespace' => $opts['namespace'],
125
                ],
126
                'system' => [
127
                    'hostname' => $obj['hostname'],
128
                    'os' => $obj['os'],
129
                ],
130
            ],
131
        ];
132
    }
133
134
    public static function generateAnnotations($obj)
135
    {
136
        return array_filter([
137
            'generated-by' => 'LibreNMS',
138
            'acknowledged' => $obj['state'] === AlertState::ACKNOWLEDGED ? 'true' : 'false',
139
            'contact' => $obj['sysContact'],
140
            'description' => $obj['sysDescr'],
141
            'location' => $obj['location'],
142
            'documentation' => $obj['proc'],
143
            'librenms-notes' => $obj['notes'],
144
            'librenms-device-id' => strval($obj['device_id']),
145
            'librenms-rule-id' => strval($obj['rule_id']),
146
            'librenms-status-reason' => $obj['status_reason'],
147
        ], function (string $s): bool {
148
            return (bool) strlen($s); // strlen returns 0 for null, false or '', but 1 for integer 0 - unlike empty()
149
        });
150
    }
151
152
    public static function calculateStatus($state, $severity)
153
    {
154
        // Sensu only has a single short (status) to indicate both severity and status, so we need to map LibreNMS' state and severity onto it
155
156
        if ($state === AlertState::RECOVERED) {
157
            // LibreNMS alert is resolved, send ok
158
            return Sensu::OK;
159
        }
160
161
        if (array_key_exists($severity, Sensu::$status)) {
162
            // Severity is known, map the LibreNMS severity to the Sensu status
163
            return Sensu::$status[$severity];
164
        }
165
166
        // LibreNMS severity does not map to Sensu, send unknown
167
        return Sensu::UNKNOWN;
168
    }
169
170
    public static function getEntityName($obj, $key)
171
    {
172
        if ($key === 'shortname') {
173
            return Sensu::shortenName($obj['hostname']);
174
        }
175
176
        return $obj[$key];
177
    }
178
179
    public static function shortenName($name)
180
    {
181
        // Shrink the last domain components - e.g. librenms.corp.example.net becomes librenms.cen
182
        $components = explode('.', $name);
183
        $count = count($components);
184
        $trim = min([3, $count - 1]);
185
        $result = '';
186
187
        if ($count <= 2) {  // Can't be shortened
188
            return $name;
189
        }
190
191
        for ($i = $count - 1; $i >= $count - $trim; $i--) {
192
            // Walk the array in reverse order, taking the first letter from the $trim sections
193
            $result = sprintf('%s%s', substr($components[$i], 0, 1), $result);
194
            unset($components[$i]);
195
        }
196
197
        return sprintf('%s.%s', implode('.', $components), $result);
198
    }
199
200
    public static function checkName($prefix, $name)
201
    {
202
        $check = strtolower(str_replace(' ', '-', $name));
203
204
        if ($prefix) {
205
            return sprintf('%s-%s', $prefix, $check);
206
        }
207
208
        return $check;
209
    }
210
211
    public static function configTemplate()
212
    {
213
        return [
214
            'config' => [
215
                [
216
                    'title' => 'Sensu Endpoint',
217
                    'name' => 'sensu-url',
218
                    'descr' => 'To configure the agent API, see https://docs.sensu.io/sensu-go/latest/reference/agent/#api-configuration-flags (default: "http://localhost:3031")',
219
                    'type' => 'text',
220
                ],
221
                [
222
                    'title' => 'Sensu Namespace',
223
                    'name' => 'sensu-namespace',
224
                    'descr' => 'The Sensu namespace that hosts exist in (default: "default")',
225
                    'type' => 'text',
226
                ],
227
                [
228
                    'title' => 'Check Prefix',
229
                    'name' => 'sensu-prefix',
230
                    'descr' => 'An optional string to prefix the checks with',
231
                    'type' => 'text',
232
                ],
233
                [
234
                    'title' => 'Source Key',
235
                    'name' => 'sensu-source-key',
236
                    'descr' => 'Should events be attributed to entities by hostname, sysName or shortname (default: hostname)',
237
                    'type' => 'select',
238
                    'options' => [
239
                        'hostname' => 'hostname',
240
                        'sysName' => 'sysName',
241
                        'shortname' => 'shortname',
242
                    ],
243
                    'default' => 'hostname',
244
                ],
245
            ],
246
            'validation' => [
247
                'sensu-url' => 'url',
248
                'sensu-source-key' => 'required|in:hostname,sysName,shortname',
249
            ],
250
        ];
251
    }
252
}
253