1<?php
2/*
3** Zabbix
4** Copyright (C) 2001-2021 Zabbix SIA
5**
6** This program is free software; you can redistribute it and/or modify
7** it under the terms of the GNU General Public License as published by
8** the Free Software Foundation; either version 2 of the License, or
9** (at your option) any later version.
10**
11** This program is distributed in the hope that it will be useful,
12** but WITHOUT ANY WARRANTY; without even the implied warranty of
13** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14** GNU General Public License for more details.
15**
16** You should have received a copy of the GNU General Public License
17** along with this program; if not, write to the Free Software
18** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19**/
20
21
22function serviceAlgorithm($algorithm = null) {
23	$algorithms = [
24		SERVICE_ALGORITHM_MAX => _('Problem, if at least one child has a problem'),
25		SERVICE_ALGORITHM_MIN => _('Problem, if all children have problems'),
26		SERVICE_ALGORITHM_NONE => _('Do not calculate')
27	];
28
29	if ($algorithm === null) {
30		return $algorithms;
31	}
32	elseif (isset($algorithms[$algorithm])) {
33		return $algorithms[$algorithm];
34	}
35	else {
36		return false;
37	}
38}
39
40function get_service_children($serviceid, $soft = 0) {
41	$children = [];
42
43	$result = DBselect(
44		'SELECT sl.servicedownid'.
45		' FROM services_links sl'.
46		' WHERE sl.serviceupid='.zbx_dbstr($serviceid).
47			($soft ? '' : ' AND sl.soft=0')
48	);
49	while ($row = DBfetch($result)) {
50		$children[] = $row['servicedownid'];
51		$children = array_merge($children, get_service_children($row['servicedownid']));
52	}
53	return $children;
54}
55
56/**
57 * Creates nodes that can be used to display the service configuration tree using the CTree class.
58 *
59 * @see CTree
60 *
61 * @param array $services
62 * @param array $parentService
63 * @param array $service
64 * @param array $dependency
65 * @param array $tree
66 */
67function createServiceConfigurationTree(array $services, &$tree, array $parentService = [], array $service = [], array $dependency = []) {
68	if (!$service) {
69		$serviceNode = [
70			'id' => 0,
71			'parentid' => 0,
72			'caption' => _('root'),
73			'trigger' => [],
74			'action' => new CHorList([
75				(new CLink(_('Add child'), 'services.php?form=1&parentname='._('root')))
76					->addClass(ZBX_STYLE_LINK_ACTION)
77			]),
78			'algorithm' => SPACE,
79			'description' => SPACE
80		];
81
82		$service = $serviceNode;
83		$service['serviceid'] = 0;
84		$service['dependencies'] = [];
85		$service['trigger'] = [];
86
87		// add all top level services as children of "root"
88		foreach ($services as $topService) {
89			if (!$topService['parent']) {
90				$service['dependencies'][] = [
91					'servicedownid' => $topService['serviceid'],
92					'soft' => 0,
93					'linkid' => 0
94				];
95			}
96		}
97
98		$tree = [$serviceNode];
99	}
100	else {
101		// service is deletable only if it has no hard dependency
102		$deletable = true;
103		foreach ($service['dependencies'] as $dep) {
104			if ($dep['soft'] == 0) {
105				$deletable = false;
106				break;
107			}
108		}
109
110		$serviceNode = [
111			'id' => $service['serviceid'],
112			'caption' => new CLink($service['name'], 'services.php?form=1&serviceid='.$service['serviceid']),
113			'action' => new CHorList([
114				(new CLink(_('Add child'),
115					'services.php?form=1&parentid='.$service['serviceid'].'&parentname='.urlencode($service['name'])
116				))->addClass(ZBX_STYLE_LINK_ACTION),
117				$deletable
118					? (new CLink(_('Delete'), 'services.php?delete=1&serviceid='.$service['serviceid']))
119						->addClass(ZBX_STYLE_LINK_ACTION)
120						->addConfirmation(_s('Delete service "%1$s"?', $service['name']))
121						->addSID()
122					: null
123			]),
124			'description' => $service['trigger'] ? $service['trigger']['description'] : '',
125			'parentid' => $parentService ? $parentService['serviceid'] : 0,
126			'algorithm' => serviceAlgorithm($service['algorithm'])
127		];
128	}
129
130	if (!$dependency || !$dependency['soft']) {
131		$tree[$serviceNode['id']] = $serviceNode;
132
133		foreach ($service['dependencies'] as $dependency) {
134			$childService = $services[$dependency['servicedownid']];
135			createServiceConfigurationTree($services, $tree, $service, $childService, $dependency);
136		}
137	}
138	else {
139		$serviceNode['caption'] = (new CSpan($serviceNode['caption']))->addClass('service-caption-soft');
140
141		$tree[$serviceNode['id'].'.'.$dependency['linkid']] = $serviceNode;
142	}
143}
144
145/**
146 * Creates nodes that can be used to display the SLA report tree using the CTree class.
147 *
148 * @see CTree
149 *
150 * @param array $services       an array of services to display in the tree
151 * @param array $slaData        sla report data, see CService::getSla()
152 * @param $period
153 * @param array $parentService
154 * @param array $service
155 * @param array $dependency
156 * @param array $tree
157 */
158function createServiceMonitoringTree(array $services, array $slaData, $period, &$tree, array $parentService = [], array $service = [], array $dependency = []) {
159	// if no parent service is given, start from the root
160	if (!$service) {
161		$serviceNode = [
162			'id' => 0,
163			'caption' => _('root'),
164			'reason' => '',
165			'sla' => '',
166			'sla2' => '',
167			'sla3' => '',
168			'parentid' => 0,
169			'status' => ''
170		];
171
172		$service = $serviceNode;
173		$service['serviceid'] = 0;
174		$service['dependencies'] = [];
175		$service['trigger'] = [];
176
177		// add all top level services as children of "root"
178		foreach ($services as $topService) {
179			if (!$topService['parent']) {
180				$service['dependencies'][] = [
181					'servicedownid' => $topService['serviceid'],
182					'soft' => 0,
183					'linkid' => 0
184				];
185			}
186		}
187
188		$tree = [$serviceNode];
189	}
190	// create a not from the given service
191	else {
192		$serviceSla = $slaData[$service['serviceid']];
193		$slaValues = reset($serviceSla['sla']);
194
195		// caption
196		// remember the selected time period when following the bar link
197		$periods = [
198			'today' => 'daily',
199			'week' => 'weekly',
200			'month' => 'monthly',
201			'year' => 'yearly',
202			24 => 'daily',
203			24 * 7 => 'weekly',
204			24 * 30 => 'monthly',
205			24 * DAY_IN_YEAR => 'yearly'
206		];
207
208		$caption = new CLink($service['name'],
209			'zabbix.php?action=report.services'.'&serviceid='.$service['serviceid'].'&period='.$periods[$period]
210		);
211
212		$trigger = $service['trigger'];
213		if ($trigger) {
214			$caption = [
215				$caption,
216				' - ',
217				new CLink($trigger['description'],
218					(new CUrl('zabbix.php'))
219						->setArgument('action', 'problem.view')
220						->setArgument('filter_triggerids[]', $trigger['triggerid'])
221						->setArgument('filter_set', '1')
222				)
223			];
224		}
225
226		// reason
227		$reason = [];
228		foreach ($serviceSla['problems'] as $problemTrigger) {
229			if ($reason) {
230				$reason[] = ', ';
231			}
232			$reason[] = new CLink($problemTrigger['description'],
233				(new CUrl('zabbix.php'))
234					->setArgument('action', 'problem.view')
235					->setArgument('filter_triggerids[]', $problemTrigger['triggerid'])
236					->setArgument('filter_set', '1')
237			);
238		}
239
240		// sla
241		$sla = '';
242		$sla2 = '';
243		$sla3 = '';
244		if ($service['showsla'] && $slaValues['sla'] !== null) {
245			$sla_good = $slaValues['sla'];
246			$sla_bad = 100 - $slaValues['sla'];
247
248			$width = 160;
249			$width_red = $width * min($sla_bad, 20) / 20;
250			$width_green = $width - $width_red;
251
252			$sla = (new CDiv(
253				new CLink([
254					(new CSpan([new CSpan('80%'), new CSpan('100%')]))->addClass(ZBX_STYLE_PROGRESS_BAR_LABEL),
255					$width_green > 0
256						? (new CSpan('&nbsp;'))
257							->addClass(ZBX_STYLE_PROGRESS_BAR_BG)
258							->addClass(ZBX_STYLE_GREEN_BG)
259							->setAttribute('style', 'width: '.$width_green.'px;')
260						: null,
261					$width_red > 0
262						? (new CSpan('&nbsp;'))
263							->addClass(ZBX_STYLE_PROGRESS_BAR_BG)
264							->addClass(ZBX_STYLE_RED_BG)
265							->setAttribute('style', 'width: '.$width_red.'px;')
266						: null
267				], 'srv_status.php?serviceid='.$service['serviceid'].'&showgraph=1'.url_param('path'))
268			))
269				->addClass(ZBX_STYLE_PROGRESS_BAR_CONTAINER)
270				->setTitle(_s('Only the last 20%% of the indicator is displayed.'));
271
272			$sla2 = (new CSpan(sprintf('%.4f', $sla_bad)))
273				->addClass($service['goodsla'] > $sla_good ? ZBX_STYLE_RED : ZBX_STYLE_GREEN);
274
275			$sla3 = [
276				(new CSpan(sprintf('%.4f', $sla_good)))
277					->addClass($service['goodsla'] > $sla_good ? ZBX_STYLE_RED : ZBX_STYLE_GREEN),
278				' / ',
279				sprintf('%.4f', $service['goodsla'])
280			];
281		}
282
283		$serviceNode = [
284			'id' => $service['serviceid'],
285			'caption' => $caption,
286			'reason' => $reason,
287			'sla' => $sla,
288			'sla2' => $sla2,
289			'sla3' => $sla3,
290			'parentid' => ($parentService) ? $parentService['serviceid'] : 0,
291			'status' => ($serviceSla['status'] !== null) ? $serviceSla['status'] : ''
292		];
293	}
294
295	// hard dependencies and dependencies for the "root" node
296	if (!$dependency || $dependency['soft'] == 0) {
297		$tree[$serviceNode['id']] = $serviceNode;
298
299		foreach ($service['dependencies'] as $dependency) {
300			$childService = $services[$dependency['servicedownid']];
301			createServiceMonitoringTree($services, $slaData, $period, $tree, $service, $childService, $dependency);
302		}
303	}
304	// soft dependencies
305	else {
306		$serviceNode['caption'] = (new CSpan($serviceNode['caption']))->addClass('service-caption-soft');
307
308		$tree[$serviceNode['id'].'.'.$dependency['linkid']] = $serviceNode;
309	}
310}
311
312/**
313 * Calculates the current service status based on it's child services.
314 *
315 * The new statuses are written to the $services array in the "newStatus" property.
316 *
317 * @param string $rootServiceId     id of the service to start calculation from
318 * @param array $servicesLinks      array with service IDs as keys and arrays of child service IDs as values
319 * @param array $services           array of services with IDs as keys
320 * @param array $triggers           array of triggers with trigger IDs as keys
321 */
322function calculateItServiceStatus($rootServiceId, array $servicesLinks, array &$services, array $triggers) {
323	$service = &$services[$rootServiceId];
324
325	// don't calculate a thread if it is already calculated
326	// it can be with soft links
327	if (isset($service['newStatus'])) {
328		return;
329	}
330
331	$newStatus = SERVICE_STATUS_OK;
332
333	// leaf service with a trigger
334	if ($service['triggerid'] != 0) {
335		if ($service['algorithm'] != SERVICE_ALGORITHM_NONE) {
336			$trigger = $triggers[$service['triggerid']];
337			$newStatus = calculateItServiceStatusByTrigger($trigger['status'], $trigger['value'], $trigger['priority']);
338		}
339	}
340	elseif (isset($servicesLinks[$rootServiceId])) {
341		// calculate status depending on children status
342		$statuses = [];
343
344		foreach ($servicesLinks[$rootServiceId] as $rootServiceId) {
345			calculateItServiceStatus($rootServiceId, $servicesLinks, $services, $triggers);
346			$statuses[] = $services[$rootServiceId]['newStatus'];
347		}
348
349		if ($statuses && $service['algorithm'] != SERVICE_ALGORITHM_NONE) {
350			$maxSeverity = max($statuses);
351
352			// always return the maximum status of child services
353			if ($service['algorithm'] == SERVICE_ALGORITHM_MAX && $maxSeverity != SERVICE_STATUS_OK) {
354				$newStatus = $maxSeverity;
355			}
356			elseif (min($statuses) != SERVICE_STATUS_OK) {
357				$newStatus = $maxSeverity;
358			}
359		}
360	}
361
362	$service['newStatus'] = $newStatus;
363}
364
365/**
366 * Checks the status of the trigger and returns the corresponding service status.
367 *
368 * @param int $triggerStatus
369 * @param int $triggerValue
370 * @param int $triggerPriority
371 *
372 * @return int
373 */
374function calculateItServiceStatusByTrigger($triggerStatus, $triggerValue, $triggerPriority) {
375	if ($triggerStatus == TRIGGER_STATUS_DISABLED || $triggerValue == TRIGGER_VALUE_FALSE) {
376		return SERVICE_STATUS_OK;
377	}
378
379	return $triggerPriority;
380}
381
382/**
383 * Updates the status of all services
384 */
385function updateItServices() {
386	$servicesLinks = [];
387	$services = [];
388	$rootServiceIds = [];
389	$triggers = [];
390
391	// auxiliary arrays
392	$triggerIds = [];
393	$servicesLinksDown = [];
394
395	$result = DBselect('SELECT sl.serviceupid,sl.servicedownid FROM services_links sl');
396
397	while ($row = DBfetch($result)) {
398		$servicesLinks[$row['serviceupid']][] = $row['servicedownid'];
399		$servicesLinksDown[$row['servicedownid']] = true;
400	}
401
402	$result = DBselect('SELECT s.serviceid,s.algorithm,s.triggerid,s.status FROM services s ORDER BY s.serviceid');
403
404	while ($row = DBfetch($result)) {
405		$services[$row['serviceid']] = [
406			'serviceid' => $row['serviceid'],
407			'algorithm' => $row['algorithm'],
408			'triggerid' => $row['triggerid'],
409			'status' => $row['status']
410		];
411
412		if (!isset($servicesLinksDown[$row['serviceid']])) {
413			$rootServiceIds[] = $row['serviceid'];
414		}
415
416		if ($row['triggerid'] != 0) {
417			$triggerIds[$row['triggerid']] = true;
418		}
419	}
420
421	if ($triggerIds) {
422		$result = DBselect(
423			'SELECT t.triggerid,t.priority,t.status,t.value'.
424			' FROM triggers t'.
425			' WHERE '.dbConditionInt('t.triggerid', array_keys($triggerIds))
426		);
427
428		while ($row = DBfetch($result)) {
429			$triggers[$row['triggerid']] = [
430				'priority' => $row['priority'],
431				'status' => $row['status'],
432				'value' => $row['value']
433			];
434		}
435	}
436
437	// clearing auxiliary variables
438	unset($triggerIds, $servicesLinksDown);
439
440	// calculating data
441	foreach ($rootServiceIds as $rootServiceId) {
442		calculateItServiceStatus($rootServiceId, $servicesLinks, $services, $triggers);
443	}
444
445	// updating changed data
446	$updates = [];
447	$inserts = [];
448	$clock = time();
449
450	foreach ($services as $service) {
451		if ($service['newStatus'] != $service['status']) {
452			$updates[] = [
453				'values' => ['status' => $service['newStatus']],
454				'where' =>  ['serviceid' => $service['serviceid']]
455			];
456			$inserts[] = [
457				'serviceid' => $service['serviceid'],
458				'clock' => $clock,
459				'value' => $service['newStatus']
460			];
461		}
462	}
463
464	if ($updates) {
465		DB::update('services', $updates);
466		DB::insert('service_alarms', $inserts);
467	}
468}
469
470/**
471 * Validate the new service time. Validation is implemented as a separate function to be available directly from the
472 * frontend.
473 *
474 * @throws APIException if the given service time is invalid
475 *
476 * @param array $serviceTime
477 */
478function checkServiceTime(array $serviceTime) {
479	// type validation
480	$serviceTypes = [
481		SERVICE_TIME_TYPE_DOWNTIME,
482		SERVICE_TIME_TYPE_ONETIME_DOWNTIME,
483		SERVICE_TIME_TYPE_UPTIME
484	];
485	if (!isset($serviceTime['type']) || !in_array($serviceTime['type'], $serviceTypes)) {
486		throw new APIException(ZBX_API_ERROR_PARAMETERS, _('Incorrect service time type.'));
487	}
488
489	// one-time downtime validation
490	if ($serviceTime['type'] == SERVICE_TIME_TYPE_ONETIME_DOWNTIME) {
491		if (!isset($serviceTime['ts_from']) || !validateUnixTime($serviceTime['ts_from'])) {
492			throw new APIException(ZBX_API_ERROR_PARAMETERS, _('Incorrect service start time.'));
493		}
494		if (!isset($serviceTime['ts_to']) || !validateUnixTime($serviceTime['ts_to'])) {
495			throw new APIException(ZBX_API_ERROR_PARAMETERS, _('Incorrect service end time.'));
496		}
497	}
498	// recurring downtime validation
499	else {
500		if (!isset($serviceTime['ts_from']) || !zbx_is_int($serviceTime['ts_from']) || $serviceTime['ts_from'] < 0 || $serviceTime['ts_from'] > SEC_PER_WEEK) {
501			throw new APIException(ZBX_API_ERROR_PARAMETERS, _('Incorrect service start time.'));
502		}
503		if (!isset($serviceTime['ts_to']) || !zbx_is_int($serviceTime['ts_to']) || $serviceTime['ts_to'] < 0 || $serviceTime['ts_to'] > SEC_PER_WEEK) {
504			throw new APIException(ZBX_API_ERROR_PARAMETERS, _('Incorrect service end time.'));
505		}
506	}
507
508	if ($serviceTime['ts_from'] >= $serviceTime['ts_to']) {
509		throw new APIException(ZBX_API_ERROR_PARAMETERS, _('Service start time must be less than end time.'));
510	}
511}
512
513/**
514 * Method to sort list of Services by 'sortorder' field and then by 'name' field if more entries has same 'sortorder'
515 * value. Separate method is needed because entries make multilevel hierarchy and branches also must be sorted according
516 * fields 'sortorder' and 'name'.
517 *
518 * @param array $services
519 *
520 * @return void
521 */
522function sortServices(array &$services) {
523	$sort_options = [
524		['field' => 'sortorder', 'order' => ZBX_SORT_UP],
525		['field' => 'name', 'order' => ZBX_SORT_UP]
526	];
527
528	// Sort first level entries.
529	CArrayHelper::sort($services, $sort_options);
530
531	// Sort dependencies.
532	foreach ($services as &$service) {
533		if ($service['dependencies']) {
534			foreach ($service['dependencies'] as &$dependent_item) {
535				$dependent_item['name'] = $services[$dependent_item['serviceid']]['name'];
536				$dependent_item['sortorder'] = $services[$dependent_item['serviceid']]['sortorder'];
537			}
538			unset($dependent_item);
539
540			CArrayHelper::sort($service['dependencies'], $sort_options);
541		}
542	}
543	unset($service);
544}
545