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