1: <?php declare(strict_types=1);
2:
3: /**
4: * +------------------------------------------------------------+
5: * | apnscp |
6: * +------------------------------------------------------------+
7: * | Copyright (c) Apis Networks |
8: * +------------------------------------------------------------+
9: * | Licensed under Artistic License 2.0 |
10: * +------------------------------------------------------------+
11: * | Author: Matt Saladna (msaladna@apisnetworks.com) |
12: * +------------------------------------------------------------+
13: */
14: class Argos_Module extends Module_Skeleton
15: {
16: const DEFAULT_BACKEND = 'default';
17:
18: protected $exportedFunctions = ['*' => PRIVILEGE_ADMIN];
19:
20: /**
21: * Get monitored service status
22: *
23: * @param null|string $service optional service status
24: * @return array
25: */
26: public function status(string $service = null): array
27: {
28: if (!IS_CLI) {
29: return $this->query('argos_status', $service);
30: }
31:
32: $ret = \Util_Process_Safe::exec(['monit', '-B', 'status', '%s'], $service);
33:
34: if (!$ret['success']) {
35: error('Failed to query status: %s', $ret['stderr']);
36: return [];
37: }
38:
39: $status = $this->filterStatus($ret['stdout']);
40:
41: return $service ? $status[$service] ?? [] : $status;
42: }
43:
44: /**
45: * Restart service
46: *
47: * @param string $service
48: * @return bool
49: */
50: public function restart(string $service): bool
51: {
52: if (!IS_CLI) {
53: return $this->query('argos_restart', $service);
54: }
55:
56: $ret = Util_Process_Safe::exec(['monit', 'restart', '%s'], $service);
57:
58: return $ret['success'] ?: error('Failed to restart %s service: %s ', $service, $ret['stderr']);
59: }
60:
61: /**
62: * Stop service monitoring
63: *
64: * @param string $service service name or "all"
65: * @return bool
66: */
67: public function unmonitor(string $service): bool
68: {
69: if (!IS_CLI) {
70: return $this->query('argos_unmonitor', $service);
71: }
72:
73: $ret = \Util_Process_Safe::exec(['monit', 'unmonitor', '%s'], $service);
74:
75: return $ret['success'] ?: error('Failed to stop %s monitoring: %s ', $service, $ret['stderr']);
76: }
77:
78: /**
79: * Resume service monitoring
80: *
81: * @param string $service service name or "all"
82: * @return bool
83: */
84: public function monitor(string $service): bool
85: {
86: if (!IS_CLI) {
87: return $this->query('argos_monitor', $service);
88: }
89:
90: $ret = \Util_Process_Safe::exec(['monit', 'monitor', '%s'], $service);
91:
92: return $ret['success'] ?: error('Failed to start %s monitoring: %s ', $service, $ret['stderr']);
93: }
94:
95: /**
96: * List monitored items
97: *
98: * @return array
99: */
100: public function list_monitored(): array
101: {
102: if (!IS_CLI) {
103: return $this->query('argos_list_monitored');
104: }
105:
106: $ret = \Util_Process::exec(['monit', 'summary', '-B']);
107: if (!$ret['success']) {
108: error('Failed to query monit');
109: return [];
110: }
111:
112: if (!preg_match_all('/^\s(?!Service Name)(\S+)/m', $ret['stdout'], $matches, PREG_SET_ORDER)) {
113: return [];
114: }
115:
116: return array_column($matches, 1);
117: }
118:
119: /**
120: * Get a count of failed processes
121: *
122: * @param bool $down headcount for downed services or up
123: * @return int
124: */
125: public function headcount(bool $down = true): int
126: {
127: if (!IS_CLI) {
128: return $this->query('argos_headcount', $down);
129: }
130:
131: $ret = \Util_Process_Safe::exec(['monit', 'report', $down ? 'down' : 'up']);
132:
133: return $ret['success'] ? (int)$ret['stdout'] : (int)error('Failed to query Monit: %s', $ret['stderr']);
134: }
135:
136: /**
137: * Validate all services are active
138: *
139: * @return array status
140: */
141: public function validate_all(): array
142: {
143: if (!IS_CLI) {
144: return $this->query('argos_validate_all');
145: }
146:
147: // 1 used in clear/failed modes
148: $ret = \Util_Process_Safe::exec(['monit', 'validate'], [0, 1]);
149:
150: if (!$ret['success']) {
151: error('Failed to validate services: %s ', $ret['stdout']);
152: return [];
153: }
154:
155: return $this->filterStatus($ret['stdout']);
156: }
157:
158: private function filterStatus(string $response): array
159: {
160: $serviceBuckets = preg_split('/^(?=Process|Filesystem|Program|System\s+)/m', $response);
161: array_shift($serviceBuckets); // uptime
162: $services = [];
163: foreach ($serviceBuckets as $bucket) {
164: if (!preg_match_all(Regex::ARGOS_SERVICE_STATUS, $bucket, $matches, PREG_SET_ORDER)) {
165: continue;
166: }
167:
168: $processName = null;
169: $fields = [
170: // in ms
171: 'timing' => null
172: ];
173:
174: foreach ($matches as $m) {
175: if ($m['proc']) {
176: $processName = $m['proc'];
177: $fields['type'] = strtolower($m['type']);
178: continue;
179: }
180:
181: $var = str_replace(' ', '_', $m['name']);
182: $value = $m['value'];
183:
184: switch ($var) {
185: case 'inodes_free':
186: $value = strtok($value, ' ');
187: case 'pid':
188: case 'parent_pid':
189: case 'uid':
190: case 'gid':
191: case 'threads':
192: case 'children':
193: case 'effective_uid':
194: case 'last_exit_value':
195: case 'inodes_total':
196: $value = (int)$value;
197: break;
198: case 'unix_socket_response_time':
199: case 'port_response_time':
200: $fields['timing'] = (float)strtok($value, ' ');
201: break;
202: // in hundredths
203: case 'cpu':
204: case 'cpu_total':
205: if (false === strpos($value, ' ')) {
206: $value = (float)$value / 100;
207: }
208: break;
209: case 'memory':
210: case 'memory_total':
211: $fields[$var . '_raw'] = (int)Formatter::changeBytes(str_replace(' ', '', substr($value, strpos($value, '[') + 1, -1)));
212: break;
213: case 'disk_write':
214: case 'disk_read':
215: $fields[$var . '_raw'] = (int)Formatter::changeBytes(str_replace(' ', '',
216: substr($value, $pos = strpos($value, '[') + 1, strrpos($value, ' ') - $pos)));
217: $fields[$var . '_bw_raw'] = (int)Formatter::changeBytes(str_replace(' ', '',
218: substr($value, 0, strpos($value, '/'))));
219: break;
220: case 'block_size':
221: $value = (int)Formatter::changeBytes($value, 'B');
222: break;
223: case 'read':
224: case 'write':
225: $fields[$var . '_bw_raw'] = (int)Formatter::changeBytes(
226: str_replace(' ', '', substr($value, 0, strpos($value, '/')))
227: );
228:
229: $fields[$var . '_iops_raw'] = (float)strtok(substr(
230: $value,
231: $pos = strpos($value, ',')+1
232: ), ' ');
233:
234: break;
235: case 'data_collected':
236: // via libmonit/src/system/Time.h, but ignores TZ. Use system default
237: $value = DateTime::createFromFormat('D, d M Y H:i:s', $value,
238: new DateTimeZone(TIMEZONE))->getTimestamp();
239: break;
240: }
241: $fields[$var] = $value;
242: $fields['failed'] = $fields['status'] !== 'OK';
243: $fields['monitored'] = 0 !== strcasecmp($fields['status'], 'not monitored');
244: }
245:
246: $services[$processName] = $fields;
247: }
248:
249: return $services;
250: }
251:
252: public function config(string $backend, ?array $newparams)
253: {
254: deprecated_func('use config_relay');
255: return $this->config_relay($backend, $newparams);
256: }
257:
258: /**
259: * Set configuration relay backend for Argos
260: *
261: * @param string $backend backend name
262: * @param array|null $newparams parameters to apply, null to delete backend
263: * @return bool|array
264: */
265: public function config_relay(string $backend, ?array $newparams)
266: {
267: if (!IS_CLI) {
268: return $this->query('argos_config_relay', $backend, $newparams);
269: }
270:
271: if (!\in_array($backend, \Opcenter\Argos\Config::get()->getBackends(), true)) {
272: return error("Unknown backend `%s'", $backend);
273: }
274:
275: // reset backend with null
276: if (null === $newparams) {
277: $provider = array_get(\Opcenter\Argos\Config::get()->backend($backend), 'backend', $backend);
278: if (!\Opcenter\Argos\Config::get()->deleteBackend($backend)) {
279: warn("Failed to delete backend `%s'", $backend);
280: }
281: if (!\Opcenter\Argos\Config::get()->createBackend($provider, $backend)) {
282: return error("Failed to create backend `%s'", $backend);
283: }
284:
285: return true;
286: }
287:
288: $cfg = \Opcenter\Argos\Config::get();
289: $backend = $cfg->backend($backend);
290: // writing backend vars
291: foreach ($newparams as $k => $v) {
292: $backend[$k] = $v;
293: }
294:
295: return true;
296: }
297:
298: /**
299: * Get Argos configuration relay
300: *
301: * @param string $backend
302: * @param null $param
303: * @return array|null
304: * @throws ReflectionException
305: */
306: public function get_config_relay(string $backend, $param = null): ?array
307: {
308: if (!IS_CLI) {
309: return $this->query('argos_get_config_relay', $backend);
310: }
311:
312: if (!\in_array($backend, \Opcenter\Argos\Config::get()->getBackends(), true)) {
313: error("Unknown backend `%s'", $backend);
314: return null;
315: }
316:
317: // reading backend vars
318: $cfg = \Opcenter\Argos\Config::get()->backend($backend)->toArray();
319:
320: return $param ? array_get($cfg, $param, null) : $cfg;
321: }
322:
323: /**
324: * Set default backend
325: *
326: * @param $backend
327: * @return bool
328: */
329: public function set_default_relay($backend)
330: {
331: if (!IS_CLI) {
332: return $this->query('argos_set_default_relay', $backend);
333: }
334: $backends = $this->get_backends();
335: foreach ((array)$backend as $b) {
336: if (!\in_array($b, $backends, true)) {
337: return error("Invalid backend `%s'", $b);
338: }
339: }
340:
341: return \Opcenter\Argos\Config::get()->setDefault($backend);
342: }
343:
344: /**
345: * Get configured Argos backends
346: *
347: * @return array|null
348: */
349: public function get_backends(): ?array
350: {
351: if (!IS_CLI) {
352: return $this->query('argos_get_backends');
353: }
354:
355: if (!($cfg = \Opcenter\Argos\Config::get())) {
356: return null;
357: }
358:
359: return $cfg->getBackends();
360: }
361:
362: /**
363: * Create a new backend
364: *
365: * @param string $name
366: * @param string $driver
367: * @return bool
368: */
369: public function create_backend(string $name, string $driver): bool
370: {
371: if (!IS_CLI) {
372: return $this->query('argos_create_backend', $name, $driver);
373: }
374: if (\in_array($name, $this->get_backends(), true)) {
375: return error("Backend `%s' already exists", $name);
376: }
377: if (!\in_array($driver, $this->get_backend_relays(), true)) {
378: return error("Invalid backend relay `%s'. Use get_backend_relays() to view all", $driver);
379: }
380:
381: $conf = \Opcenter\Argos\Config::get();
382: $conf->createBackend($driver, $name);
383: $conf->sync();
384:
385: return true;
386: }
387:
388: /**
389: * Get relays for backend
390: *
391: * @return array
392: */
393: public function get_backend_relays(): array
394: {
395: if (!IS_CLI) {
396: return $this->query('argos_get_backend_relays');
397: }
398:
399: return \Opcenter\Argos\Backend::getBackends();
400: }
401:
402: /**
403: * Test Argos configuration
404: *
405: * @param string $backend
406: * @return mixed
407: */
408: public function test(string $backend = null)
409: {
410: return $this->send('Argos test alert', $backend, '💯 test');
411: }
412:
413: /**
414: * Relay a message through Argos
415: *
416: * @param string $msg
417: * @param string $backend
418: * @param string|null $title
419: * @return mixed
420: */
421: public function send(string $msg, string $backend = null, string $title = null)
422: {
423: if (DEMO_ADMIN_LOCK && posix_getuid()) {
424: return error("Demo may not send Argos relays");
425: }
426:
427: if (!IS_CLI) {
428: return $this->query('argos_send', $msg, $backend, $title);
429: }
430:
431: if (!file_exists(\Opcenter\Argos\Config::CONFIGURATION_FILE)) {
432: return error(
433: "%s is missing - run argos.init Scope first. See Monitoring.md in docs/",
434: \Opcenter\Argos\Config::CONFIGURATION_FILE
435: );
436: }
437: if ($title) {
438: $title = '-t ' . escapeshellarg($title);
439: }
440: if ($backend) {
441: $backend = '-b ' . escapeshellarg($backend);
442: }
443:
444: return array_get(
445: \Util_Process_Safe::exec('ntfy -c %(config)s ' . $title . ' ' . $backend . ' send %(msg)s',
446: [
447: 'config' => \Opcenter\Argos\Config::CONFIGURATION_FILE,
448: 'msg' => $msg,
449: ]
450: ),
451: 'success',
452: false
453: );
454: }
455:
456: /**
457: * Argos monitor active
458: *
459: * @return bool
460: */
461: public function active(): bool
462: {
463: return (new \Opcenter\Dbus\Systemd)->read('monit', 'ActiveState', 'unit') === 'active';
464: }
465:
466: /**
467: * Reset monitoring
468: *
469: * @return bool
470: */
471: public function reset_all(): bool
472: {
473: $ret = \Util_Process_Safe::exec(['monit', 'monitor', 'all'], [0]);
474: return $ret['success'] ?: error('Failed to reset monitoring services: %s ', $ret['stdout']);
475: }
476:
477: public function _housekeeping()
478: {
479: $this->reset_all();
480: }
481:
482: }
483: