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