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
22/**
23 * Class containing methods for operations with services.
24 */
25class CService extends CApiService {
26
27	public const ACCESS_RULES = [
28		'get' => ['min_user_type' => USER_TYPE_ZABBIX_USER],
29		'getsla' => ['min_user_type' => USER_TYPE_ZABBIX_USER],
30		'create' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
31		'update' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
32		'delete' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
33		'adddependencies' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
34		'deletedependencies' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
35		'addtimes' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
36		'deletetimes' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN]
37	];
38
39	protected $tableName = 'services';
40	protected $tableAlias = 's';
41	protected $sortColumns = ['sortorder', 'name'];
42
43	public function __construct() {
44		parent::__construct();
45
46		$this->getOptions = array_merge($this->getOptions, [
47			'parentids' => null,
48			'childids' => null,
49			'countOutput' => false,
50			'selectParent' => null,
51			'selectDependencies' => null,
52			'selectParentDependencies' => null,
53			'selectTimes' => null,
54			'selectAlarms' => null,
55			'selectTrigger' => null,
56			'sortfield' => '',
57			'sortorder' => ''
58		]);
59	}
60
61	/**
62	 * Get services.
63	 *
64	 * Allowed options:
65	 * - parentids                      - fetch the services that are hardlinked to the given parent services;
66	 * - childids                       - fetch the services that are hardlinked to the given child services;
67	 * - countOutput                    - return the number of the results as an integer;
68	 * - selectParent                   - include the parent service in the result;
69	 * - selectDependencies             - include service child dependencies in the result;
70	 * - selectParentDependencies       - include service parent dependencies in the result;
71	 * - selectTimes                    - include service times in the result;
72	 * - selectAlarms                   - include alarms generated by the service;
73	 * - selectTrigger                  - include the linked trigger;
74	 * - sortfield                      - name of columns to sort by;
75	 * - sortorder                      - sort order.
76	 *
77	 * @param array $options
78	 *
79	 * @return array
80	 */
81	public function get(array $options) {
82		$options = zbx_array_merge($this->getOptions, $options);
83
84		// build and execute query
85		$sql = $this->createSelectQuery($this->tableName(), $options);
86		$res = DBselect($sql, $options['limit']);
87
88		// fetch results
89		$result = [];
90		while ($row = DBfetch($res)) {
91			// a count query, return a single result
92			if ($options['countOutput']) {
93				$result = $row['rowscount'];
94			}
95			// a normal select query
96			else {
97				$result[$row[$this->pk()]] = $row;
98			}
99		}
100
101		if ($options['countOutput']) {
102			return $result;
103		}
104
105		if ($result) {
106			$result = $this->addRelatedObjects($options, $result);
107			$result = $this->unsetExtraFields($result, ['triggerid'], $options['output']);
108		}
109
110		if (!$options['preservekeys']) {
111			$result = zbx_cleanHashes($result);
112		}
113
114		return $result;
115	}
116
117	/**
118	 * Validates the input parameters for the create() method.
119	 *
120	 * @throws APIException if the input is invalid
121	 *
122	 * @param array $services
123	 */
124	protected function validateCreate(array $services) {
125		foreach ($services as $service) {
126			$this->checkName($service);
127			$this->checkAlgorithm($service);
128			$this->checkShowSla($service);
129			$this->checkGoodSla($service);
130			$this->checkSortOrder($service);
131			$this->checkTriggerId($service);
132			$this->checkStatus($service);
133			$this->checkParentId($service);
134
135			$error = _s('Wrong fields for service "%1$s".', $service['name']);
136			$this->checkUnsupportedFields($this->tableName(), $service, $error, [
137				'parentid', 'dependencies', 'times'
138			]);
139		}
140
141		$this->checkTriggerPermissions($services);
142	}
143
144	/**
145	 * Creates the given services.
146	 *
147	 * @param array $services
148	 *
149	 * @return array
150	 */
151	public function create(array $services) {
152		$services = zbx_toArray($services);
153		$this->validateCreate($services);
154
155		// save the services
156		$serviceIds = DB::insert($this->tableName(), $services);
157
158		$dependencies = [];
159		$serviceTimes = [];
160		foreach ($services as $key => $service) {
161			$serviceId = $serviceIds[$key];
162
163			// save dependencies
164			if (!empty($service['dependencies'])) {
165				foreach ($service['dependencies'] as $dependency) {
166					$dependency['serviceid'] = $serviceId;
167					$dependencies[] = $dependency;
168				}
169			}
170
171			// save parent service
172			if (!empty($service['parentid'])) {
173				$dependencies[] = [
174					'serviceid' => $service['parentid'],
175					'dependsOnServiceid' => $serviceId,
176					'soft' => 0
177				];
178			}
179
180			// save service times
181			if (isset($service['times'])) {
182				foreach ($service['times'] as $serviceTime) {
183					$serviceTime['serviceid'] = $serviceId;
184					$serviceTimes[] = $serviceTime;
185				}
186			}
187		}
188
189		if ($dependencies) {
190			$this->addDependencies($dependencies);
191		}
192
193		if ($serviceTimes) {
194			$this->addTimes($serviceTimes);
195		}
196
197		updateItServices();
198
199		return ['serviceids' => $serviceIds];
200	}
201
202	/**
203	 * Validates the input parameters for the update() method.
204	 *
205	 * @throws APIException if the input is invalid
206	 *
207	 * @param array $services
208	 */
209	public function validateUpdate(array $services) {
210		foreach ($services as $service) {
211			if (empty($service['serviceid'])) {
212				self::exception(ZBX_API_ERROR_PARAMETERS, _('Invalid method parameters.'));
213			}
214		}
215
216		$this->checkServicePermissions(zbx_objectValues($services, 'serviceid'));
217
218		$services = $this->extendObjects($this->tableName(), $services, ['name']);
219		foreach ($services as $service) {
220			$this->checkName($service);
221
222			if (isset($service['algorithm'])) {
223				$this->checkAlgorithm($service);
224			}
225			if (isset($service['showsla'])) {
226				$this->checkShowSla($service);
227			}
228			if (isset($service['goodsla'])) {
229				$this->checkGoodSla($service);
230			}
231			if (isset($service['sortorder'])) {
232				$this->checkSortOrder($service);
233			}
234			if (isset($service['triggerid'])) {
235				$this->checkTriggerId($service);
236			}
237			if (isset($service['status'])) {
238				$this->checkStatus($service);
239			}
240			if (isset($service['parentid'])) {
241				$this->checkParentId($service);
242			}
243
244			$error = _s('Wrong fields for service "%1$s".', $service['name']);
245			$this->checkUnsupportedFields($this->tableName(), $service, $error, [
246				'parentid', 'dependencies', 'times'
247			]);
248		}
249
250		$this->checkTriggerPermissions($services);
251	}
252
253	/**
254	 * Updates the given services.
255	 *
256	 * @param array $services
257	 *
258	 * @return array
259	 */
260	public function update(array $services) {
261		$services = zbx_toArray($services);
262		$this->validateUpdate($services);
263
264		// save the services
265		foreach ($services as $service) {
266			DB::updateByPk($this->tableName(), $service['serviceid'], $service);
267		}
268
269		// update dependencies
270		$dependencies = [];
271		$parentDependencies = [];
272		$serviceTimes = [];
273		$deleteParentsForServiceIds = [];
274		$deleteDependenciesForServiceIds = [];
275		$deleteTimesForServiceIds = [];
276		foreach ($services as $service) {
277			if (isset($service['dependencies'])) {
278				$deleteDependenciesForServiceIds[] = $service['serviceid'];
279
280				if ($service['dependencies']) {
281					foreach ($service['dependencies'] as $dependency) {
282						$dependency['serviceid'] = $service['serviceid'];
283						$dependencies[] = $dependency;
284					}
285				}
286			}
287
288			// update parent
289			if (isset($service['parentid'])) {
290				$deleteParentsForServiceIds[] = $service['serviceid'];
291
292				if ($service['parentid']) {
293					$parentDependencies[] = [
294						'serviceid' => $service['parentid'],
295						'dependsOnServiceid' => $service['serviceid'],
296						'soft' => 0
297					];
298				}
299			}
300
301			// save service times
302			if (isset($service['times'])) {
303				$deleteTimesForServiceIds[] = $service['serviceid'];
304
305				foreach ($service['times'] as $serviceTime) {
306					$serviceTime['serviceid'] = $service['serviceid'];
307					$serviceTimes[] = $serviceTime;
308				}
309			}
310		}
311
312		// replace dependencies
313		if ($deleteParentsForServiceIds) {
314			$this->deleteParentDependencies(zbx_objectValues($services, 'serviceid'));
315		}
316		if ($deleteDependenciesForServiceIds) {
317			$this->deleteDependencies(array_unique($deleteDependenciesForServiceIds));
318		}
319		if ($parentDependencies || $dependencies) {
320			$this->addDependencies(array_merge($parentDependencies, $dependencies));
321		}
322
323		// replace service times
324		if ($deleteTimesForServiceIds) {
325			$this->deleteTimes($deleteTimesForServiceIds);
326		}
327		if ($serviceTimes) {
328			$this->addTimes($serviceTimes);
329		}
330
331		updateItServices();
332
333		return ['serviceids' => zbx_objectValues($services, 'serviceid')];
334	}
335
336	/**
337	 * Validates the input parameters for the delete() method.
338	 *
339	 * @throws APIException if the input is invalid
340	 *
341	 * @param array $serviceIds
342	 */
343	public function validateDelete($serviceIds) {
344		if (!$serviceIds) {
345			self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.'));
346		}
347
348		$this->checkServicePermissions($serviceIds);
349		$this->checkThatServicesDontHaveChildren($serviceIds);
350	}
351
352	/**
353	 * Delete services.
354	 *
355	 * @param array $serviceIds
356	 *
357	 * @return array
358	 */
359	public function delete(array $serviceIds) {
360		$this->validateDelete($serviceIds);
361
362		DB::delete($this->tableName(), ['serviceid' => $serviceIds]);
363
364		updateItServices();
365
366		return ['serviceids' => $serviceIds];
367	}
368
369	/**
370	 * Validates the input parameters for the addDependencies() method.
371	 *
372	 * @throws APIException if the input is invalid
373	 *
374	 * @param array $dependencies
375	 */
376	protected function validateAddDependencies(array $dependencies) {
377		if (!$dependencies) {
378			self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.'));
379		}
380
381		foreach ($dependencies as $dependency) {
382			if (empty($dependency['serviceid']) || empty($dependency['dependsOnServiceid'])) {
383				self::exception(ZBX_API_ERROR_PARAMETERS, _('Invalid method parameters.'));
384			}
385		}
386
387		$serviceIds = array_merge(
388			zbx_objectValues($dependencies, 'serviceid'),
389			zbx_objectValues($dependencies, 'dependsOnServiceid')
390		);
391		$serviceIds = array_unique($serviceIds);
392		$this->checkServicePermissions($serviceIds);
393
394		foreach ($dependencies as $dependency) {
395			$this->checkDependency($dependency);
396
397			$this->checkUnsupportedFields('services_links', $dependency,
398				_s('Wrong fields for dependency for service "%1$s".', $dependency['serviceid']),
399				['dependsOnServiceid', 'serviceid']
400			);
401		}
402
403		$this->checkForHardlinkedDependencies($dependencies);
404		$this->checkThatParentsDontHaveTriggers($dependencies);
405		$this->checkForCircularityInDependencies($dependencies);
406	}
407
408	/**
409	 * Add the given service dependencies.
410	 *
411	 * @param array $dependencies   an array of service dependencies, each pair in the form of
412	 *                              array('serviceid' => 1, 'dependsOnServiceid' => 2, 'soft' => 0)
413	 *
414	 * @return array
415	 */
416	public function addDependencies(array $dependencies) {
417		$dependencies = zbx_toArray($dependencies);
418		$this->validateAddDependencies($dependencies);
419
420		$data = [];
421		foreach ($dependencies as $dependency) {
422			$data[] = [
423				'serviceupid' => $dependency['serviceid'],
424				'servicedownid' => $dependency['dependsOnServiceid'],
425				'soft' => $dependency['soft']
426			];
427		}
428		DB::insert('services_links', $data);
429
430		return ['serviceids' => zbx_objectValues($dependencies, 'serviceid')];
431	}
432
433	/**
434	 * Validates the input for the deleteDependencies() method.
435	 *
436	 * @throws APIException if the given input is invalid
437	 *
438	 * @param array $serviceIds
439	 */
440	protected function validateDeleteDependencies(array $serviceIds) {
441		if (!$serviceIds) {
442			self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.'));
443		}
444
445		$this->checkServicePermissions($serviceIds);
446	}
447
448	/**
449	 * Deletes all dependencies for the given services.
450	 *
451	 * @param array $serviceIds
452	 *
453	 * @return boolean
454	 */
455	public function deleteDependencies($serviceIds) {
456		$serviceIds = zbx_toArray($serviceIds);
457		$this->validateDeleteDependencies($serviceIds);
458
459		DB::delete('services_links', [
460			'serviceupid' =>  $serviceIds
461		]);
462
463		return ['serviceids' => $serviceIds];
464	}
465
466	/**
467	 * Validates the input for the addTimes() method.
468	 *
469	 * @throws APIException if the given input is invalid
470	 *
471	 * @param array $serviceTimes
472	 */
473	public function validateAddTimes(array $serviceTimes) {
474		foreach ($serviceTimes as $serviceTime) {
475			$this->checkTime($serviceTime);
476
477			$this->checkUnsupportedFields('services_times', $serviceTime,
478				_s('Wrong fields for time for service "%1$s".', $serviceTime['serviceid'])
479			);
480		}
481
482		$this->checkServicePermissions(array_unique(zbx_objectValues($serviceTimes, 'serviceid')));
483	}
484
485	/**
486	 * Adds the given service times.
487	 *
488	 * @param array $serviceTimes an array of service times
489	 *
490	 * @return array
491	 */
492	public function addTimes(array $serviceTimes) {
493		$serviceTimes = zbx_toArray($serviceTimes);
494		$this->validateAddTimes($serviceTimes);
495
496		DB::insert('services_times', $serviceTimes);
497
498		return ['serviceids' => zbx_objectValues($serviceTimes, 'serviceid')];
499	}
500
501	/**
502	 * Validates the input for the deleteTimes() method.
503	 *
504	 * @throws APIException if the given input is invalid
505	 *
506	 * @param array $serviceIds
507	 */
508	protected function validateDeleteTimes(array $serviceIds) {
509		if (!$serviceIds) {
510			self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.'));
511		}
512
513		$this->checkServicePermissions($serviceIds);
514	}
515
516	/**
517	 * Returns availability-related information about the given services during the given time intervals.
518	 *
519	 * Available options:
520	 *  - serviceids    - a single service ID or an array of service IDs;
521	 *  - intervals     - a single time interval or an array of time intervals, each containing:
522	 *      - from          - the beginning of the interval, timestamp;
523	 *      - to            - the end of the interval, timestamp.
524	 *
525	 * Returns the following availability information for each service:
526	 *  - status            - the current status of the service;
527	 *  - problems          - an array of triggers that are currently in problem state and belong to the given service
528	 *                        or it's descendants;
529	 *  - sla               - an array of requested intervals with SLA information:
530	 *      - from              - the beginning of the interval;
531	 *      - to                - the end of the interval;
532	 *      - okTime            - the time the service was in OK state, in seconds;
533	 *      - problemTime       - the time the service was in problem state, in seconds;
534	 *      - downtimeTime      - the time the service was down, in seconds.
535	 *
536	 * If the service calculation algorithm is set to SERVICE_ALGORITHM_NONE, the method will return an empty 'problems'
537	 * array and null for all of the calculated values.
538	 *
539	 * @param array $options
540	 *
541	 * @return array    as array(serviceId2 => data1, serviceId2 => data2, ...)
542	 */
543	public function getSla(array $options) {
544		$serviceIds = (isset($options['serviceids'])) ? zbx_toArray($options['serviceids']) : null;
545		$intervals = (isset($options['intervals'])) ? zbx_toArray($options['intervals']) : [];
546
547		// fetch services
548		$services = $this->get([
549			'output' => ['serviceid', 'name', 'status', 'algorithm'],
550			'selectTimes' => API_OUTPUT_EXTEND,
551			'selectParentDependencies' => ['serviceupid'],
552			'serviceids' => $serviceIds,
553			'preservekeys' => true
554		]);
555
556		$rs = [];
557		if ($services) {
558			$usedSeviceIds = [];
559
560			$problemServiceIds = [];
561			foreach ($services as &$service) {
562				// don't calculate SLA for services with disabled status calculation
563				if ($this->isStatusEnabled($service)) {
564					$usedSeviceIds[$service['serviceid']] = $service['serviceid'];
565					$service['alarms'] = [];
566
567					if ($service['status'] > 0) {
568						$problemServiceIds[] = $service['serviceid'];
569					}
570				}
571			}
572			unset($service);
573
574			// initial data
575			foreach ($services as $service) {
576				$rs[$service['serviceid']] = [
577					'status' => ($this->isStatusEnabled($service)) ? $service['status'] : null,
578					'problems' => [],
579					'sla' => []
580				];
581			}
582
583			if ($usedSeviceIds) {
584				// add service alarms
585				if ($intervals) {
586					$intervalConditions = [];
587					foreach ($intervals as $interval) {
588						$intervalConditions[] = 'sa.clock BETWEEN '.zbx_dbstr($interval['from']).' AND '.zbx_dbstr($interval['to']);
589					}
590					$query = DBselect(
591						'SELECT *'.
592						' FROM service_alarms sa'.
593						' WHERE '.dbConditionInt('sa.serviceid', $usedSeviceIds).
594							' AND ('.implode(' OR ', $intervalConditions).')'.
595						' ORDER BY sa.servicealarmid'
596					);
597					while ($data = DBfetch($query)) {
598						$services[$data['serviceid']]['alarms'][] = $data;
599					}
600				}
601
602				// add problem triggers
603				if ($problemServiceIds) {
604					$problemTriggers = $this->fetchProblemTriggers($problemServiceIds);
605					$rs = $this->escalateProblems($services, $problemTriggers, $rs);
606				}
607
608				$slaCalculator = new CServicesSlaCalculator();
609
610				// calculate SLAs
611				foreach ($intervals as $interval) {
612					$latestValues = $this->fetchLatestValues($usedSeviceIds, $interval['from']);
613
614					foreach ($services as $service) {
615						$serviceId = $service['serviceid'];
616
617						// only calculate the sla for services which require it
618						if (isset($usedSeviceIds[$serviceId])) {
619							$latestValue = (isset($latestValues[$serviceId])) ? $latestValues[$serviceId] : 0;
620							$intervalSla = $slaCalculator->calculateSla($service['alarms'], $service['times'],
621								$interval['from'], $interval['to'], $latestValue
622							);
623						}
624						else {
625							$intervalSla = [
626								'ok' => null,
627								'okTime' => null,
628								'problemTime' => null,
629								'downtimeTime' => null
630							];
631						}
632
633						$rs[$service['serviceid']]['sla'][] = [
634							'from' => $interval['from'],
635							'to' => $interval['to'],
636							'sla' => $intervalSla['ok'],
637							'okTime' => $intervalSla['okTime'],
638							'problemTime' => $intervalSla['problemTime'],
639							'downtimeTime' => $intervalSla['downtimeTime']
640						];
641					}
642				}
643			}
644		}
645
646		return $rs;
647	}
648
649	/**
650	 * Deletes all service times for the given services.
651	 *
652	 * @param array $serviceIds
653	 *
654	 * @return boolean
655	 */
656	public function deleteTimes($serviceIds) {
657		$serviceIds = zbx_toArray($serviceIds);
658		$this->validateDeleteTimes($serviceIds);
659
660		DB::delete('services_times', [
661			'serviceid' =>  $serviceIds
662		]);
663
664		return ['serviceids' => $serviceIds];
665	}
666
667	/**
668	 * Deletes the dependencies of the parent services on the given services.
669	 *
670	 * @param $serviceIds
671	 */
672	protected function deleteParentDependencies($serviceIds) {
673		DB::delete('services_links', [
674			'servicedownid' => $serviceIds,
675			'soft' => 0
676		]);
677	}
678
679	/**
680	 * Returns an array of triggers which are in a problem state and are linked to the given services.
681	 *
682	 * @param array $serviceIds
683	 *
684	 * @return array    in the form of array(serviceId1 => array(triggerId => trigger), ...)
685	 */
686	protected function fetchProblemTriggers(array $serviceIds) {
687		$sql = 'SELECT s.serviceid,t.triggerid'.
688				' FROM services s,triggers t'.
689				' WHERE s.status>0'.
690					' AND t.triggerid=s.triggerid'.
691					' AND '.dbConditionInt('s.serviceid', $serviceIds).
692				' ORDER BY s.status DESC,t.description';
693
694		// get service reason
695		$triggers = DBfetchArray(DBSelect($sql));
696
697		$rs = [];
698		foreach ($triggers as $trigger) {
699			$serviceId = $trigger['serviceid'];
700			unset($trigger['serviceid']);
701
702			$rs[$serviceId] = [$trigger['triggerid'] => $trigger];
703		}
704
705		return $rs;
706	}
707
708	/**
709	 * Escalates the problem triggers from the child services to their parents and adds them to $slaData.
710	 * The escalation will stop if a service has status calculation disabled or is in OK state.
711	 *
712	 * @param array $services
713	 * @param array $serviceProblems    an array of service triggers defines as
714	 *                                  array(serviceId1 => array(triggerId => trigger), ...)
715	 * @param array $slaData
716	 *
717	 * @return array
718	 */
719	protected function escalateProblems(array $services, array $serviceProblems, array $slaData) {
720		$parentProblems = [];
721		foreach ($serviceProblems as $serviceId => $problemTriggers) {
722			$service = $services[$serviceId];
723
724			// add the problem trigger of the current service to the data
725			$slaData[$serviceId]['problems'] = zbx_array_merge($slaData[$serviceId]['problems'], $problemTriggers);
726
727			// add the same trigger to the parent services
728			foreach ($service['parentDependencies'] as $dependency) {
729				$parentServiceId = $dependency['serviceupid'];
730
731				if (isset($services[$parentServiceId])) {
732					$parentService = $services[$parentServiceId];
733
734					// escalate only if status calculation is enabled for the parent service and it's in problem state
735					if ($this->isStatusEnabled($parentService) && $parentService['status']) {
736						if (!isset($parentProblems[$parentServiceId])) {
737							$parentProblems[$parentServiceId] = [];
738						}
739						$parentProblems[$parentServiceId] = zbx_array_merge($parentProblems[$parentServiceId], $problemTriggers);
740					}
741				}
742			}
743		}
744
745		// propagate the problems to the parents
746		if ($parentProblems) {
747			$slaData = $this->escalateProblems($services, $parentProblems, $slaData);
748		}
749
750		return $slaData;
751	}
752
753	/**
754	 * Returns the value of the latest service alarm before the given time.
755	 *
756	 * @param array $serviceIds
757	 * @param int $beforeTime
758	 *
759	 * @return array
760	 */
761	protected function fetchLatestValues(array $serviceIds, $beforeTime) {
762		// The query will return the alarms with the latest servicealarmid for each service, before $beforeTime.
763		$query = DBSelect(
764			'SELECT sa.serviceid,sa.value'.
765			' FROM (SELECT sa2.serviceid,MAX(sa2.servicealarmid) AS servicealarmid'.
766					' FROM service_alarms sa2'.
767					' WHERE sa2.clock<'.zbx_dbstr($beforeTime).
768						' AND '.dbConditionInt('sa2.serviceid', $serviceIds).
769					' GROUP BY sa2.serviceid) ss2'.
770			' JOIN service_alarms sa ON sa.servicealarmid = ss2.servicealarmid'
771		);
772		$rs = [];
773		while ($alarm = DBfetch($query)) {
774			$rs[$alarm['serviceid']] = $alarm['value'];
775		}
776
777		return $rs;
778	}
779
780	/**
781	 * Returns an array of dependencies that are children of the given services. Performs permission checks.
782	 *
783	 * @param array $parentServiceIds
784	 * @param $output
785	 *
786	 * @return array    an array of service links sorted by "sortorder" in ascending order
787	 */
788	protected function fetchChildDependencies(array $parentServiceIds, $output) {
789		$sqlParts = API::getApiService()->createSelectQueryParts('services_links', 'sl', [
790			'output' => $output,
791			'filter' => ['serviceupid' => $parentServiceIds]
792		]);
793
794		// sort by sortorder
795		$sqlParts['from'][] = $this->tableName().' '.$this->tableAlias();
796		$sqlParts['where'][] = 'sl.servicedownid='.$this->fieldId('serviceid');
797		$sqlParts = $this->addQueryOrder($this->fieldId('sortorder'), $sqlParts);
798		$sqlParts = $this->addQueryOrder($this->fieldId('serviceid'), $sqlParts);
799
800		// add permission filter
801		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
802			$sqlParts = $this->addPermissionFilter($sqlParts);
803		}
804
805		$sql = self::createSelectQueryFromParts($sqlParts);
806
807		return DBfetchArray(DBselect($sql));
808	}
809
810	/**
811	 * Returns an array of dependencies from the parent services to the given services.
812	 * Performs permission checks.
813	 *
814	 * @param array $childServiceIds
815	 * @param $output
816	 * @param boolean $soft             if set to true, will return only soft-linked dependencies
817	 *
818	 * @return array    an array of service links sorted by "sortorder" in ascending order
819	 */
820	protected function fetchParentDependencies(array $childServiceIds, $output, $soft = null) {
821		$sqlParts = API::getApiService()->createSelectQueryParts('services_links', 'sl', [
822			'output' => $output,
823			'filter' => ['servicedownid' => $childServiceIds]
824		]);
825
826		$sqlParts['from'][] = $this->tableName().' '.$this->tableAlias();
827		$sqlParts['where'][] = 'sl.serviceupid='.$this->fieldId('serviceid');
828		if ($soft !== null) {
829			$sqlParts['where'][] = 'sl.soft='.($soft ? 1 : 0);
830		}
831		$sqlParts = $this->addQueryOrder($this->fieldId('sortorder'), $sqlParts);
832		$sqlParts = $this->addQueryOrder($this->fieldId('serviceid'), $sqlParts);
833
834		// add permission filter
835		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
836			$sqlParts = $this->addPermissionFilter($sqlParts);
837		}
838
839		$sql = self::createSelectQueryFromParts($sqlParts);
840
841		return DBfetchArray(DBselect($sql));
842	}
843
844	/**
845	 * Returns true if status calculation is enabled for the given service.
846	 *
847	 * @param array $service
848	 *
849	 * @return bool
850	 */
851	protected function isStatusEnabled(array $service) {
852		return ($service['algorithm'] != SERVICE_ALGORITHM_NONE);
853	}
854
855	/**
856	 * Validates the "name" field.
857	 *
858	 * @throws APIException if the name is missing
859	 *
860	 * @param array $service
861	 */
862	protected function checkName(array $service) {
863		if (!isset($service['name']) || zbx_empty($service['name'])) {
864			self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty name.'));
865		}
866	}
867
868	/**
869	 * Validates the "algorithm" field. Assumes the "name" field is valid.
870	 *
871	 * @throws APIException if the name is missing or invalid
872	 *
873	 * @param array $service
874	 */
875	protected function checkAlgorithm(array $service) {
876		if (!isset($service['algorithm']) || !serviceAlgorithm($service['algorithm'])) {
877			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect algorithm for service "%1$s".', $service['name']));
878		}
879	}
880
881	/**
882	 * Validates the "showsla" field. Assumes the "name" field is valid.
883	 *
884	 * @throws APIException if the name is missing or is not a boolean value
885	 *
886	 * @param array $service
887	 */
888	protected function checkShowSla(array $service) {
889		$showSlaValues = [
890			SERVICE_SHOW_SLA_OFF => true,
891			SERVICE_SHOW_SLA_ON => true
892		];
893		if (!isset($service['showsla']) || !isset($showSlaValues[$service['showsla']])) {
894			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect calculate SLA value for service "%1$s".', $service['name']));
895		}
896	}
897
898	/**
899	 * Validates the "showsla" field. Assumes the "name" field is valid.
900	 *
901	 * @throws APIException if the value is missing, or is out of bounds
902	 *
903	 * @param array $service
904	 */
905	protected function checkGoodSla(array $service) {
906		if ((!empty($service['showsla']) && empty($service['goodsla']))
907				|| (isset($service['goodsla'])
908					&& (!is_numeric($service['goodsla']) || $service['goodsla'] < 0 || $service['goodsla'] > 100))) {
909
910			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect acceptable SLA for service "%1$s".', $service['name']));
911		}
912	}
913
914	/**
915	 * Validates the "sortorder" field. Assumes the "name" field is valid.
916	 *
917	 * @throws APIException if the value is missing, or is out of bounds
918	 *
919	 * @param array $service
920	 */
921	protected function checkSortOrder(array $service) {
922		if (!isset($service['sortorder']) || !zbx_is_int($service['sortorder'])
923			|| $service['sortorder'] < 0 || $service['sortorder'] > 999) {
924
925			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect sort order for service "%1$s".', $service['name']));
926		}
927	}
928
929	/**
930	 * Validates the "triggerid" field. Assumes the "name" field is valid.
931	 *
932	 * @throws APIException if the value is incorrect
933	 *
934	 * @param array $service
935	 */
936	protected function checkTriggerId(array $service) {
937		if (!empty($service['triggerid']) && !zbx_is_int($service['triggerid'])) {
938			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect trigger ID for service "%1$s".', $service['name']));
939		}
940	}
941
942	/**
943	 * Validates the "parentid" field. Assumes the "name" field is valid.
944	 *
945	 * @throws APIException if the value is incorrect
946	 *
947	 * @param array $service
948	 */
949	protected function checkParentId(array $service) {
950		if (!empty($service['parentid']) && !zbx_is_int($service['parentid'])) {
951			if (isset($service['name'])) {
952				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect parent for service "%1$s".', $service['name']));
953			}
954			else {
955				self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect parent service.'));
956			}
957		}
958
959		if (isset($service['serviceid']) && idcmp($service['serviceid'], $service['parentid'])) {
960			self::exception(ZBX_API_ERROR_PARAMETERS, _('Service cannot be parent and child at the same time.'));
961		}
962	}
963
964	/**
965	 * Validates the "status" field. Assumes the "name" field is valid.
966	 *
967	 * @throws APIException if the value is incorrect
968	 *
969	 * @param array $service
970	 */
971	protected function checkStatus(array $service) {
972		if (!empty($service['status']) && !zbx_is_int($service['status'])) {
973			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect status for service "%1$s".', $service['name']));
974		}
975	}
976
977	/**
978	 * Checks that the user has read access to the given triggers.
979	 *
980	 * @throws APIException if the user doesn't have permission to access any of the triggers
981	 *
982	 * @param array $services
983	 */
984	protected function checkTriggerPermissions(array $services) {
985		$triggerids = [];
986		foreach ($services as $service) {
987			if (!empty($service['triggerid'])) {
988				$triggerids[$service['triggerid']] = true;
989			}
990		}
991
992		if ($triggerids) {
993			$count = API::Trigger()->get([
994				'countOutput' => true,
995				'triggerids' => array_keys($triggerids)
996			]);
997
998			if ($count != count($triggerids)) {
999				self::exception(ZBX_API_ERROR_PERMISSIONS,
1000					_('No permissions to referred object or it does not exist!')
1001				);
1002			}
1003		}
1004	}
1005
1006	/**
1007	 * Checks that all of the given services are readable.
1008	 *
1009	 * @throws APIException if at least one of the services doesn't exist
1010	 *
1011	 * @param array $serviceids
1012	 */
1013	protected function checkServicePermissions(array $serviceids) {
1014		if ($serviceids) {
1015			$serviceids = array_unique($serviceids);
1016
1017			$count = $this->get([
1018				'countOutput' => true,
1019				'serviceids' => $serviceids
1020			]);
1021
1022			if ($count != count($serviceids)) {
1023				self::exception(ZBX_API_ERROR_PERMISSIONS,
1024					_('No permissions to referred object or it does not exist!')
1025				);
1026			}
1027		}
1028	}
1029
1030	/**
1031	 * Checks that none of the given services have any children.
1032	 *
1033	 * @throws APIException if at least one of the services has a child service
1034	 *
1035	 * @param array $serviceIds
1036	 */
1037	protected function checkThatServicesDontHaveChildren(array $serviceIds) {
1038		$child = API::getApiService()->select('services_links', [
1039			'output' => ['serviceupid'],
1040			'filter' => [
1041				'serviceupid' => $serviceIds,
1042				'soft' => 0
1043			],
1044			'limit' => 1
1045		]);
1046		$child = reset($child);
1047		if ($child) {
1048			$service = API::getApiService()->select($this->tableName(), [
1049				'output' => ['name'],
1050				'serviceids' => $child['serviceupid'],
1051				'limit' => 1
1052			]);
1053			$service = reset($service);
1054			self::exception(ZBX_API_ERROR_PERMISSIONS,
1055				_s('Service "%1$s" cannot be deleted, because it is dependent on another service.', $service['name'])
1056			);
1057		}
1058	}
1059
1060	/**
1061	 * Checks that the given dependency is valid.
1062	 *
1063	 * @throws APIException if the dependency is invalid
1064	 *
1065	 * @param array $dependency
1066	 */
1067	protected function checkDependency(array $dependency) {
1068		if (idcmp($dependency['serviceid'], $dependency['dependsOnServiceid'])) {
1069			$service = API::getApiService()->select($this->tableName(), [
1070				'output' => ['name'],
1071				'serviceids' => $dependency['serviceid']
1072			]);
1073			$service = reset($service);
1074			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Service "%1$s" cannot be dependent on itself.', $service['name']));
1075		}
1076
1077		// check 'soft' field value
1078		if (!isset($dependency['soft']) || !in_array((int) $dependency['soft'], [0, 1], true)) {
1079			$service = API::getApiService()->select($this->tableName(), [
1080				'output' => ['name'],
1081				'serviceids' => $dependency['serviceid']
1082			]);
1083			$service = reset($service);
1084			self::exception(ZBX_API_ERROR_PARAMETERS,
1085				_s('Incorrect "soft" field value for dependency for service "%1$s".', $service['name'])
1086			);
1087		}
1088	}
1089
1090	/**
1091	 * Checks that that none of the given services are hard linked to a different service.
1092	 * Assumes the dependencies are valid.
1093	 *
1094	 * @throws APIException if at a least one service is hard linked to another service
1095	 *
1096	 * @param array $dependencies
1097	 */
1098	protected function checkForHardlinkedDependencies(array $dependencies) {
1099		// only check hard dependencies
1100		$hardDepServiceIds = [];
1101		foreach ($dependencies as $dependency) {
1102			if (!$dependency['soft']) {
1103				$hardDepServiceIds[] = $dependency['dependsOnServiceid'];
1104			}
1105		}
1106
1107		if ($hardDepServiceIds) {
1108			// look for at least one hardlinked service among the given
1109			$hardDepServiceIds = array_unique($hardDepServiceIds);
1110			$dep = API::getApiService()->select('services_links', [
1111				'output' => ['servicedownid'],
1112				'filter' => [
1113					'soft' => 0,
1114					'servicedownid' => $hardDepServiceIds
1115				],
1116				'limit' => 1
1117			]);
1118			if ($dep) {
1119				$dep = reset($dep);
1120				$service = API::getApiService()->select($this->tableName(), [
1121					'output' => ['name'],
1122					'serviceids' => $dep['servicedownid']
1123				]);
1124				$service = reset($service);
1125				self::exception(ZBX_API_ERROR_PARAMETERS,
1126					_s('Service "%1$s" is already hardlinked to a different service.', $service['name'])
1127				);
1128			}
1129		}
1130	}
1131
1132	/**
1133	 * Checks that none of the parent services are linked to a trigger. Assumes the dependencies are valid.
1134	 *
1135	 * @throws APIException if at least one of the parent services is linked to a trigger
1136	 *
1137	 * @param array $dependencies
1138	 */
1139	protected function checkThatParentsDontHaveTriggers(array $dependencies) {
1140		$parentServiceIds = array_unique(zbx_objectValues($dependencies, 'serviceid'));
1141		if ($parentServiceIds) {
1142			$query = DBselect(
1143				'SELECT s.triggerid,s.name'.
1144					' FROM services s '.
1145					' WHERE '.dbConditionInt('s.serviceid', $parentServiceIds).
1146					' AND s.triggerid IS NOT NULL', 1);
1147			if ($parentService = DBfetch($query)) {
1148				self::exception(ZBX_API_ERROR_PARAMETERS,
1149					_s('Service "%1$s" cannot be linked to a trigger and have children at the same time.', $parentService['name']));
1150			}
1151		}
1152	}
1153
1154	/**
1155	 * Checks that dependencies will not create cycles in service dependencies.
1156	 *
1157	 * @throws APIException if at least one cycle is possible
1158	 *
1159	 * @param array $depsToValid	dependency list to be validated
1160	 */
1161	protected function checkForCircularityInDependencies($depsToValid) {
1162		$dbDeps = API::getApiService()->select('services_links', [
1163			'output' => ['serviceupid', 'servicedownid']
1164		]);
1165
1166		// create existing dependency acyclic graph
1167		$arr = [];
1168		foreach ($dbDeps as $dbDep) {
1169			if (!isset($arr[$dbDep['serviceupid']])) {
1170				$arr[$dbDep['serviceupid']] = [];
1171			}
1172			$arr[$dbDep['serviceupid']][$dbDep['servicedownid']] = $dbDep['servicedownid'];
1173		}
1174
1175		// check for circularity and add dependencies to the graph
1176		foreach ($depsToValid as $dep) {
1177			$this->DFCircularitySearch($dep['serviceid'], $dep['dependsOnServiceid'], $arr);
1178			$arr[$dep['serviceid']][$dep['dependsOnServiceid']] = $dep['dependsOnServiceid'];
1179		}
1180
1181	}
1182
1183	/**
1184	 * Depth First Search recursive function to find circularity and rise exception.
1185	 *
1186	 * @throws APIException if cycle is possible
1187	 *
1188	 * @param int $id	dependency from id
1189	 * @param int $depId	dependency to id
1190	 * @param ref $arr	reference to graph structure. Structure is associative array with keys as "from id"
1191	 *			and values as arrays with keys and values as "to id".
1192	 */
1193	protected function dfCircularitySearch($id, $depId, &$arr) {
1194		if ($id == $depId) {
1195			// cycle found
1196			self::exception(ZBX_API_ERROR_PARAMETERS, _('Services form a circular dependency.'));
1197		}
1198		if (isset($arr[$depId])) {
1199			foreach ($arr[$depId] as $dep) {
1200				$this->DFCircularitySearch($id, $dep, $arr);
1201			}
1202		}
1203	}
1204
1205	/**
1206	 * Checks that the given service time is valid.
1207	 *
1208	 * @throws APIException if the service time is invalid
1209	 *
1210	 * @param array $serviceTime
1211	 */
1212	protected function checkTime(array $serviceTime) {
1213		if (empty($serviceTime['serviceid'])) {
1214			self::exception(ZBX_API_ERROR_PARAMETERS, _('Invalid method parameters.'));
1215		}
1216
1217		checkServiceTime($serviceTime);
1218	}
1219
1220	protected function applyQueryFilterOptions($tableName, $tableAlias, array $options, array $sqlParts) {
1221		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
1222			// if services with specific trigger IDs were requested, return only the ones accessible to the current user.
1223			if (is_array($options['filter']) && array_key_exists('triggerid', $options['filter'])) {
1224				$accessibleTriggers = API::Trigger()->get([
1225					'output' => ['triggerid'],
1226					'triggerids' => $options['filter']['triggerid']
1227				]);
1228				$options['filter']['triggerid'] = zbx_objectValues($accessibleTriggers, 'triggerid');
1229			}
1230			// otherwise return services with either no triggers, or any trigger accessible to the current user
1231			else {
1232				$sqlParts = $this->addPermissionFilter($sqlParts);
1233			}
1234		}
1235
1236		$sqlParts = parent::applyQueryFilterOptions($tableName, $tableAlias, $options, $sqlParts);
1237
1238		// parentids
1239		if ($options['parentids'] !== null) {
1240			$sqlParts['from'][] = 'services_links slp';
1241			$sqlParts['where'][] = $this->fieldId('serviceid').'=slp.servicedownid AND slp.soft=0';
1242			$sqlParts['where'][] = dbConditionInt('slp.serviceupid', (array) $options['parentids']);
1243		}
1244		// childids
1245		if ($options['childids'] !== null) {
1246			$sqlParts['from'][] = 'services_links slc';
1247			$sqlParts['where'][] = $this->fieldId('serviceid').'=slc.serviceupid AND slc.soft=0';
1248			$sqlParts['where'][] = dbConditionInt('slc.servicedownid', (array) $options['childids']);
1249		}
1250
1251		return $sqlParts;
1252	}
1253
1254	protected function addRelatedObjects(array $options, array $result) {
1255		$result = parent::addRelatedObjects($options, $result);
1256
1257		$serviceIds = array_keys($result);
1258
1259		// selectDependencies
1260		if ($options['selectDependencies'] !== null && $options['selectDependencies'] != API_OUTPUT_COUNT) {
1261			$dependencies = $this->fetchChildDependencies($serviceIds,
1262				$this->outputExtend($options['selectDependencies'], ['serviceupid', 'linkid'])
1263			);
1264			$dependencies = zbx_toHash($dependencies, 'linkid');
1265			$relationMap = $this->createRelationMap($dependencies, 'serviceupid', 'linkid');
1266
1267			$dependencies = $this->unsetExtraFields($dependencies, ['serviceupid', 'linkid'], $options['selectDependencies']);
1268			$result = $relationMap->mapMany($result, $dependencies, 'dependencies');
1269		}
1270
1271		// selectParentDependencies
1272		if ($options['selectParentDependencies'] !== null && $options['selectParentDependencies'] != API_OUTPUT_COUNT) {
1273			$dependencies = $this->fetchParentDependencies($serviceIds,
1274				$this->outputExtend($options['selectParentDependencies'], ['servicedownid', 'linkid'])
1275			);
1276			$dependencies = zbx_toHash($dependencies, 'linkid');
1277			$relationMap = $this->createRelationMap($dependencies, 'servicedownid', 'linkid');
1278
1279			$dependencies = $this->unsetExtraFields($dependencies, ['servicedownid', 'linkid'],
1280				$options['selectParentDependencies']
1281			);
1282			$result = $relationMap->mapMany($result, $dependencies, 'parentDependencies');
1283		}
1284
1285		// selectParent
1286		if ($options['selectParent'] !== null && $options['selectParent'] != API_OUTPUT_COUNT) {
1287			$dependencies = $this->fetchParentDependencies($serviceIds, ['servicedownid', 'serviceupid'], false);
1288			$relationMap = $this->createRelationMap($dependencies, 'servicedownid', 'serviceupid');
1289			$parents = $this->get([
1290				'output' => $options['selectParent'],
1291				'serviceids' => $relationMap->getRelatedIds(),
1292				'preservekeys' => true
1293			]);
1294			$result = $relationMap->mapOne($result, $parents, 'parent');
1295		}
1296
1297		// selectTimes
1298		if ($options['selectTimes'] !== null && $options['selectTimes'] != API_OUTPUT_COUNT) {
1299			$serviceTimes = API::getApiService()->select('services_times', [
1300				'output' => $this->outputExtend($options['selectTimes'], ['serviceid', 'timeid']),
1301				'filter' => ['serviceid' => $serviceIds],
1302				'preservekeys' => true
1303			]);
1304			$relationMap = $this->createRelationMap($serviceTimes, 'serviceid', 'timeid');
1305
1306			$serviceTimes = $this->unsetExtraFields($serviceTimes, ['serviceid', 'timeid'], $options['selectTimes']);
1307			$result = $relationMap->mapMany($result, $serviceTimes, 'times');
1308		}
1309
1310		// selectAlarms
1311		if ($options['selectAlarms'] !== null && $options['selectAlarms'] != API_OUTPUT_COUNT) {
1312			$serviceAlarms = API::getApiService()->select('service_alarms', [
1313				'output' => $this->outputExtend($options['selectAlarms'], ['serviceid', 'servicealarmid']),
1314				'filter' => ['serviceid' => $serviceIds],
1315				'preservekeys' => true
1316			]);
1317			$relationMap = $this->createRelationMap($serviceAlarms, 'serviceid', 'servicealarmid');
1318
1319			$serviceAlarms = $this->unsetExtraFields($serviceAlarms, ['serviceid', 'servicealarmid'],
1320				$options['selectAlarms']
1321			);
1322			$result = $relationMap->mapMany($result, $serviceAlarms, 'alarms');
1323		}
1324
1325		// selectTrigger
1326		if ($options['selectTrigger'] !== null && $options['selectTrigger'] != API_OUTPUT_COUNT) {
1327			$relationMap = $this->createRelationMap($result, 'serviceid', 'triggerid');
1328			$triggers = API::getApiService()->select('triggers', [
1329				'output' => $options['selectTrigger'],
1330				'triggerids' => $relationMap->getRelatedIds(),
1331				'preservekeys' => true
1332			]);
1333			$result = $relationMap->mapOne($result, $triggers, 'trigger');
1334		}
1335
1336		return $result;
1337	}
1338
1339	protected function applyQueryOutputOptions($tableName, $tableAlias, array $options, array $sqlParts) {
1340		$sqlParts = parent::applyQueryOutputOptions($tableName, $tableAlias, $options, $sqlParts);
1341
1342		if (!$options['countOutput']) {
1343			if ($options['selectTrigger'] !== null) {
1344				$sqlParts = $this->addQuerySelect($this->fieldId('triggerid'), $sqlParts);
1345			}
1346		}
1347
1348		return $sqlParts;
1349	}
1350
1351	/**
1352	 * Add permission filter SQL query part
1353	 *
1354	 * @param array $sqlParts
1355	 *
1356	 * @return string
1357	 */
1358	protected function addPermissionFilter($sqlParts) {
1359		$userGroups = getUserGroupsByUserId(self::$userData['userid']);
1360
1361		$sqlParts['where'][] = '(EXISTS ('.
1362									'SELECT NULL'.
1363									' FROM functions f,items i,hosts_groups hgg'.
1364									' JOIN rights r'.
1365										' ON r.id=hgg.groupid'.
1366										' AND '.dbConditionInt('r.groupid', $userGroups).
1367									' WHERE s.triggerid=f.triggerid'.
1368										' AND f.itemid=i.itemid'.
1369										' AND i.hostid=hgg.hostid'.
1370									' GROUP BY f.triggerid'.
1371									' HAVING MIN(r.permission)>'.PERM_DENY.
1372									')'.
1373								' OR s.triggerid IS NULL)';
1374
1375		return $sqlParts;
1376	}
1377}
1378