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 22abstract class CHostBase extends CApiService { 23 24 public const ACCESS_RULES = [ 25 'get' => ['min_user_type' => USER_TYPE_ZABBIX_USER], 26 'create' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN], 27 'update' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN], 28 'delete' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN] 29 ]; 30 31 protected $tableName = 'hosts'; 32 protected $tableAlias = 'h'; 33 34 /** 35 * Links the templates to the given hosts. 36 * 37 * @param array $templateIds 38 * @param array $targetIds an array of host IDs to link the templates to 39 * 40 * @return array an array of added hosts_templates rows, with 'hostid' and 'templateid' set for each row 41 */ 42 protected function link(array $templateIds, array $targetIds) { 43 if (empty($templateIds)) { 44 return; 45 } 46 47 // permission check 48 $templateIds = array_unique($templateIds); 49 50 $count = API::Template()->get([ 51 'countOutput' => true, 52 'templateids' => $templateIds 53 ]); 54 55 if ($count != count($templateIds)) { 56 self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); 57 } 58 59 // check if someone passed duplicate templates in the same query 60 $templateIdDuplicates = zbx_arrayFindDuplicates($templateIds); 61 if (!zbx_empty($templateIdDuplicates)) { 62 $duplicatesFound = []; 63 foreach ($templateIdDuplicates as $value => $count) { 64 $duplicatesFound[] = _s('template ID "%1$s" is passed %2$s times', $value, $count); 65 } 66 self::exception( 67 ZBX_API_ERROR_PARAMETERS, 68 _s('Cannot pass duplicate template IDs for the linkage: %1$s.', implode(', ', $duplicatesFound)) 69 ); 70 } 71 72 // get DB templates which exists in all targets 73 $res = DBselect('SELECT * FROM hosts_templates WHERE '.dbConditionInt('hostid', $targetIds)); 74 $mas = []; 75 while ($row = DBfetch($res)) { 76 if (!isset($mas[$row['templateid']])) { 77 $mas[$row['templateid']] = []; 78 } 79 $mas[$row['templateid']][$row['hostid']] = 1; 80 } 81 $targetIdCount = count($targetIds); 82 $commonDBTemplateIds = []; 83 foreach ($mas as $templateId => $targetList) { 84 if (count($targetList) == $targetIdCount) { 85 $commonDBTemplateIds[] = $templateId; 86 } 87 } 88 89 // check if there are any template with triggers which depends on triggers in templates which will be not linked 90 $commonTemplateIds = array_unique(array_merge($commonDBTemplateIds, $templateIds)); 91 foreach ($templateIds as $templateid) { 92 $triggerids = []; 93 $dbTriggers = get_triggers_by_hostid($templateid); 94 while ($trigger = DBfetch($dbTriggers)) { 95 $triggerids[$trigger['triggerid']] = $trigger['triggerid']; 96 } 97 98 $sql = 'SELECT DISTINCT h.host'. 99 ' FROM trigger_depends td,functions f,items i,hosts h'. 100 ' WHERE ('. 101 dbConditionInt('td.triggerid_down', $triggerids). 102 ' AND f.triggerid=td.triggerid_up'. 103 ' )'. 104 ' AND i.itemid=f.itemid'. 105 ' AND h.hostid=i.hostid'. 106 ' AND '.dbConditionInt('h.hostid', $commonTemplateIds, true). 107 ' AND h.status='.HOST_STATUS_TEMPLATE; 108 if ($dbDepHost = DBfetch(DBselect($sql))) { 109 $tmpTpls = API::Template()->get([ 110 'templateids' => $templateid, 111 'output'=> API_OUTPUT_EXTEND 112 ]); 113 $tmpTpl = reset($tmpTpls); 114 115 self::exception(ZBX_API_ERROR_PARAMETERS, 116 _s('Trigger in template "%1$s" has dependency with trigger in template "%2$s".', $tmpTpl['host'], $dbDepHost['host'])); 117 } 118 } 119 120 $res = DBselect( 121 'SELECT ht.hostid,ht.templateid'. 122 ' FROM hosts_templates ht'. 123 ' WHERE '.dbConditionInt('ht.hostid', $targetIds). 124 ' AND '.dbConditionInt('ht.templateid', $templateIds) 125 ); 126 $linked = []; 127 while ($row = DBfetch($res)) { 128 $linked[$row['templateid']][$row['hostid']] = true; 129 } 130 131 // add template linkages, if problems rollback later 132 $hostsLinkageInserts = []; 133 134 foreach ($templateIds as $templateid) { 135 $linked_targets = array_key_exists($templateid, $linked) ? $linked[$templateid] : []; 136 137 foreach ($targetIds as $targetid) { 138 if (array_key_exists($targetid, $linked_targets)) { 139 continue; 140 } 141 142 $hostsLinkageInserts[] = ['hostid' => $targetid, 'templateid' => $templateid]; 143 } 144 } 145 146 if ($hostsLinkageInserts) { 147 self::checkCircularLinkage($hostsLinkageInserts); 148 self::checkDoubleLinkage($hostsLinkageInserts); 149 150 DB::insertBatch('hosts_templates', $hostsLinkageInserts); 151 } 152 153 // check if all trigger templates are linked to host. 154 // we try to find template that is not linked to hosts ($targetids) 155 // and exists trigger which reference that template and template from ($templateids) 156 $sql = 'SELECT DISTINCT h.host'. 157 ' FROM functions f,items i,triggers t,hosts h'. 158 ' WHERE f.itemid=i.itemid'. 159 ' AND f.triggerid=t.triggerid'. 160 ' AND i.hostid=h.hostid'. 161 ' AND h.status='.HOST_STATUS_TEMPLATE. 162 ' AND NOT EXISTS (SELECT 1 FROM hosts_templates ht WHERE ht.templateid=i.hostid AND '.dbConditionInt('ht.hostid', $targetIds).')'. 163 ' AND EXISTS (SELECT 1 FROM functions ff,items ii WHERE ff.itemid=ii.itemid AND ff.triggerid=t.triggerid AND '.dbConditionInt('ii.hostid', $templateIds). ')'; 164 if ($dbNotLinkedTpl = DBfetch(DBSelect($sql, 1))) { 165 self::exception(ZBX_API_ERROR_PARAMETERS, 166 _s('Trigger has items from template "%1$s" that is not linked to host.', $dbNotLinkedTpl['host']) 167 ); 168 } 169 170 return $hostsLinkageInserts; 171 } 172 173 protected function unlink($templateids, $targetids = null) { 174 $cond = ['templateid' => $templateids]; 175 if (!is_null($targetids)) { 176 $cond['hostid'] = $targetids; 177 } 178 DB::delete('hosts_templates', $cond); 179 180 if (!is_null($targetids)) { 181 $hosts = API::Host()->get([ 182 'hostids' => $targetids, 183 'output' => ['hostid', 'host'], 184 'nopermissions' => true 185 ]); 186 } 187 else{ 188 $hosts = API::Host()->get([ 189 'templateids' => $templateids, 190 'output' => ['hostid', 'host'], 191 'nopermissions' => true 192 ]); 193 } 194 195 if (!empty($hosts)) { 196 $templates = API::Template()->get([ 197 'templateids' => $templateids, 198 'output' => ['hostid', 'host'], 199 'nopermissions' => true 200 ]); 201 202 $hosts = implode(', ', zbx_objectValues($hosts, 'host')); 203 $templates = implode(', ', zbx_objectValues($templates, 'host')); 204 205 info(_s('Templates "%1$s" unlinked from hosts "%2$s".', $templates, $hosts)); 206 } 207 } 208 209 /** 210 * Searches for circular linkages for specific template. 211 * 212 * @param array $links[<templateid>][<hostid>] The list of linkages. 213 * @param string $templateid ID of the template to check circular linkages. 214 * @param array $hostids[<hostid>] 215 * 216 * @throws APIException if circular linkage is found. 217 */ 218 private static function checkTemplateCircularLinkage(array $links, $templateid, array $hostids) { 219 if (array_key_exists($templateid, $hostids)) { 220 self::exception(ZBX_API_ERROR_PARAMETERS, _('Circular template linkage is not allowed.')); 221 } 222 223 foreach ($hostids as $hostid => $foo) { 224 if (array_key_exists($hostid, $links)) { 225 self::checkTemplateCircularLinkage($links, $templateid, $links[$hostid]); 226 } 227 } 228 } 229 230 /** 231 * Searches for circular linkages. 232 * 233 * @param array $host_templates 234 * @param string $host_templates[]['templateid'] 235 * @param string $host_templates[]['hostid'] 236 */ 237 private static function checkCircularLinkage(array $host_templates) { 238 $links = []; 239 240 foreach ($host_templates as $host_template) { 241 $links[$host_template['templateid']][$host_template['hostid']] = true; 242 } 243 244 $templateids = array_keys($links); 245 $_templateids = $templateids; 246 247 do { 248 $result = DBselect( 249 'SELECT ht.templateid,ht.hostid'. 250 ' FROM hosts_templates ht'. 251 ' WHERE '.dbConditionId('ht.hostid', $_templateids) 252 ); 253 254 $_templateids = []; 255 256 while ($row = DBfetch($result)) { 257 if (!array_key_exists($row['templateid'], $links)) { 258 $_templateids[$row['templateid']] = true; 259 } 260 261 $links[$row['templateid']][$row['hostid']] = true; 262 } 263 264 $_templateids = array_keys($_templateids); 265 } 266 while ($_templateids); 267 268 foreach ($templateids as $templateid) { 269 self::checkTemplateCircularLinkage($links, $templateid, $links[$templateid]); 270 } 271 } 272 273 /** 274 * Searches for double linkages. 275 * 276 * @param array $links[<hostid>][<templateid>] The list of linked template IDs by host ID. 277 * @param string $hostid 278 * 279 * @throws APIException if double linkage is found. 280 * 281 * @return array An array of the linked templates for the selected host. 282 */ 283 private static function checkTemplateDoubleLinkage(array $links, $hostid) { 284 $templateids = $links[$hostid]; 285 286 foreach ($links[$hostid] as $templateid => $foo) { 287 if (array_key_exists($templateid, $links)) { 288 $_templateids = self::checkTemplateDoubleLinkage($links, $templateid); 289 290 if (array_intersect_key($templateids, $_templateids)) { 291 self::exception(ZBX_API_ERROR_PARAMETERS, 292 _('Template cannot be linked to another template more than once even through other templates.') 293 ); 294 } 295 296 $templateids += $_templateids; 297 } 298 } 299 300 return $templateids; 301 } 302 303 /** 304 * Searches for double linkages. 305 * 306 * @param array $host_templates 307 * @param string $host_templates[]['templateid'] 308 * @param string $host_templates[]['hostid'] 309 */ 310 private static function checkDoubleLinkage(array $host_templates) { 311 $links = []; 312 $templateids = []; 313 $hostids = []; 314 315 foreach ($host_templates as $host_template) { 316 $links[$host_template['hostid']][$host_template['templateid']] = true; 317 $templateids[$host_template['templateid']] = true; 318 $hostids[$host_template['hostid']] = true; 319 } 320 321 $_hostids = array_keys($hostids); 322 323 do { 324 $result = DBselect( 325 'SELECT ht.hostid'. 326 ' FROM hosts_templates ht'. 327 ' WHERE '.dbConditionId('ht.templateid', $_hostids) 328 ); 329 330 $_hostids = []; 331 332 while ($row = DBfetch($result)) { 333 if (!array_key_exists($row['hostid'], $hostids)) { 334 $_hostids[$row['hostid']] = true; 335 } 336 337 $hostids[$row['hostid']] = true; 338 } 339 340 $_hostids = array_keys($_hostids); 341 } 342 while ($_hostids); 343 344 $_templateids = array_keys($templateids + $hostids); 345 $templateids = []; 346 347 do { 348 $result = DBselect( 349 'SELECT ht.templateid,ht.hostid'. 350 ' FROM hosts_templates ht'. 351 ' WHERE '.dbConditionId('hostid', $_templateids) 352 ); 353 354 $_templateids = []; 355 356 while ($row = DBfetch($result)) { 357 if (!array_key_exists($row['templateid'], $templateids)) { 358 $_templateids[$row['templateid']] = true; 359 } 360 361 $templateids[$row['templateid']] = true; 362 $links[$row['hostid']][$row['templateid']] = true; 363 } 364 365 $_templateids = array_keys($_templateids); 366 } 367 while ($_templateids); 368 369 foreach ($hostids as $hostid => $foo) { 370 self::checkTemplateDoubleLinkage($links, $hostid); 371 } 372 } 373 374 /** 375 * Updates tags by deleting existing tags if they are not among the input tags, and adding missing ones. 376 * 377 * @param array $host_tags 378 * @param int $host_tags[<hostid>] 379 * @param string $host_tags[<hostid>][]['tag'] 380 * @param string $host_tags[<hostid>][]['value'] 381 */ 382 protected function updateTags(array $host_tags): void { 383 if (!$host_tags) { 384 return; 385 } 386 387 $insert = []; 388 $db_tags = DB::select('host_tag', [ 389 'output' => ['hosttagid', 'hostid', 'tag', 'value'], 390 'filter' => ['hostid' => array_keys($host_tags)], 391 'preservekeys' => true 392 ]); 393 394 $db_host_tags = []; 395 foreach ($db_tags as $db_tag) { 396 $db_host_tags[$db_tag['hostid']][] = $db_tag; 397 } 398 399 foreach ($host_tags as $hostid => $tags) { 400 foreach (zbx_toArray($tags) as $tag) { 401 if (array_key_exists($hostid, $db_host_tags)) { 402 $tag += ['value' => '']; 403 404 foreach ($db_host_tags[$hostid] as $db_tag) { 405 if ($tag['tag'] === $db_tag['tag'] && $tag['value'] === $db_tag['value']) { 406 unset($db_tags[$db_tag['hosttagid']]); 407 $tag = null; 408 break; 409 } 410 } 411 } 412 413 if ($tag !== null) { 414 $insert[] = ['hostid' => $hostid] + $tag; 415 } 416 } 417 } 418 419 if ($db_tags) { 420 DB::delete('host_tag', ['hosttagid' => array_keys($db_tags)]); 421 } 422 423 if ($insert) { 424 DB::insert('host_tag', $insert); 425 } 426 } 427 428 /** 429 * Creates user macros for hosts, templates and host prototypes. 430 * 431 * @param array $hosts 432 * @param array $hosts[]['templateid|hostid'] 433 * @param array $hosts[]['macros'] (optional) 434 */ 435 protected function createHostMacros(array $hosts): void { 436 $id_field_name = $this instanceof CTemplate ? 'templateid' : 'hostid'; 437 438 $ins_hostmacros = []; 439 440 foreach ($hosts as $host) { 441 if (array_key_exists('macros', $host)) { 442 foreach ($host['macros'] as $macro) { 443 $ins_hostmacros[] = ['hostid' => $host[$id_field_name]] + $macro; 444 } 445 } 446 } 447 448 if ($ins_hostmacros) { 449 DB::insert('hostmacro', $ins_hostmacros); 450 } 451 } 452 453 /** 454 * Adding "macros" to the each host object. 455 * 456 * @param array $db_hosts 457 * 458 * @return array 459 */ 460 protected function getHostMacros(array $db_hosts): array { 461 foreach ($db_hosts as &$db_host) { 462 $db_host['macros'] = []; 463 } 464 unset($db_host); 465 466 $options = [ 467 'output' => ['hostmacroid', 'hostid', 'macro', 'type', 'value', 'description'], 468 'filter' => ['hostid' => array_keys($db_hosts)] 469 ]; 470 $db_macros = DBselect(DB::makeSql('hostmacro', $options)); 471 472 while ($db_macro = DBfetch($db_macros)) { 473 $hostid = $db_macro['hostid']; 474 unset($db_macro['hostid']); 475 476 $db_hosts[$hostid]['macros'][$db_macro['hostmacroid']] = $db_macro; 477 } 478 479 return $db_hosts; 480 } 481 482 /** 483 * Checks user macros for host.update, template.update and hostprototype.update methods. 484 * 485 * @param array $hosts 486 * @param array $hosts[]['templateid|hostid'] 487 * @param array $hosts[]['macros'] (optional) 488 * @param array $db_hosts 489 * @param array $db_hosts[<hostid>]['macros'] 490 * 491 * @return array Array of passed hosts/templates with padded macros data, when it's necessary. 492 * 493 * @throws APIException if input of host macros data is invalid. 494 */ 495 protected function validateHostMacros(array $hosts, array $db_hosts): array { 496 $hostmacro_defaults = [ 497 'type' => DB::getDefault('hostmacro', 'type') 498 ]; 499 500 // Populating new host macro objects for correct inheritance. 501 if ($this instanceof CHostPrototype) { 502 $hostmacro_defaults['description'] = DB::getDefault('hostmacro', 'description'); 503 } 504 505 $id_field_name = $this instanceof CTemplate ? 'templateid' : 'hostid'; 506 507 foreach ($hosts as $i1 => &$host) { 508 if (!array_key_exists('macros', $host)) { 509 continue; 510 } 511 512 $db_host = $db_hosts[$host[$id_field_name]]; 513 $path = '/'.($i1 + 1).'/macros'; 514 515 $db_macros = array_column($db_host['macros'], 'hostmacroid', 'macro'); 516 $macros = []; 517 518 foreach ($host['macros'] as $i2 => &$hostmacro) { 519 if (!array_key_exists('hostmacroid', $hostmacro)) { 520 foreach (['macro', 'value'] as $field_name) { 521 if (!array_key_exists($field_name, $hostmacro)) { 522 self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', $path, 523 _s('the parameter "%1$s" is missing', $field_name) 524 )); 525 } 526 } 527 528 $hostmacro += $hostmacro_defaults; 529 } 530 else { 531 if (!array_key_exists($hostmacro['hostmacroid'], $db_host['macros'])) { 532 self::exception(ZBX_API_ERROR_PERMISSIONS, 533 _('No permissions to referred object or it does not exist!') 534 ); 535 } 536 537 $db_hostmacro = $db_host['macros'][$hostmacro['hostmacroid']]; 538 $hostmacro += array_intersect_key($db_hostmacro, array_flip(['macro', 'type'])); 539 540 if ($hostmacro['type'] != $db_hostmacro['type']) { 541 if ($db_hostmacro['type'] == ZBX_MACRO_TYPE_SECRET) { 542 $hostmacro += ['value' => '']; 543 } 544 545 if ($hostmacro['type'] == ZBX_MACRO_TYPE_VAULT) { 546 $hostmacro += ['value' => $db_hostmacro['value']]; 547 } 548 } 549 550 // Populating new host macro objects for correct inheritance. 551 if ($this instanceof CHostPrototype) { 552 $hostmacro += array_intersect_key($db_hostmacro, array_flip(['value', 'description'])); 553 } 554 555 $macros[$hostmacro['hostmacroid']] = $hostmacro['macro']; 556 } 557 558 if (array_key_exists('value', $hostmacro) && $hostmacro['type'] == ZBX_MACRO_TYPE_VAULT) { 559 if (!CApiInputValidator::validate(['type' => API_VAULT_SECRET], $hostmacro['value'], 560 $path.'/'.($i2 + 1).'/value', $error)) { 561 self::exception(ZBX_API_ERROR_PARAMETERS, $error); 562 } 563 } 564 } 565 unset($hostmacro); 566 567 // Checking for cross renaming of existing macros. 568 foreach ($macros as $hostmacroid => $macro) { 569 if (array_key_exists($macro, $db_macros) && bccomp($hostmacroid, $db_macros[$macro]) != 0 570 && array_key_exists($db_macros[$macro], $macros)) { 571 $hosts = DB::select('hosts', [ 572 'output' => ['name'], 573 'hostids' => $host[$id_field_name] 574 ]); 575 576 self::exception(ZBX_API_ERROR_PARAMETERS, 577 _s('Macro "%1$s" already exists on "%2$s".', $macro, $hosts[0]['name']) 578 ); 579 } 580 } 581 582 $api_input_rules = ['type' => API_OBJECTS, 'uniq' => [['macro']], 'fields' => [ 583 'macro' => ['type' => API_USER_MACRO] 584 ]]; 585 586 if (!CApiInputValidator::validateUniqueness($api_input_rules, $host['macros'], $path, $error)) { 587 self::exception(ZBX_API_ERROR_PARAMETERS, $error); 588 } 589 } 590 unset($host); 591 592 return $hosts; 593 } 594 595 /** 596 * Updates user macros for hosts, templates and host prototypes. 597 * 598 * @param array $hosts 599 * @param array $hosts[]['templateid|hostid'] 600 * @param array $hosts[]['macros'] (optional) 601 * @param array $db_hosts 602 * @param array $db_hosts[<hostid>]['macros'] An array of host macros indexed by hostmacroid. 603 */ 604 protected function updateHostMacros(array $hosts, array $db_hosts): void { 605 $id_field_name = $this instanceof CTemplate ? 'templateid' : 'hostid'; 606 607 $ins_hostmacros = []; 608 $upd_hostmacros = []; 609 $del_hostmacroids = []; 610 611 foreach ($hosts as $host) { 612 if (!array_key_exists('macros', $host)) { 613 continue; 614 } 615 616 $db_host = $db_hosts[$host[$id_field_name]]; 617 618 foreach ($host['macros'] as $hostmacro) { 619 if (array_key_exists('hostmacroid', $hostmacro)) { 620 $db_hostmacro = $db_host['macros'][$hostmacro['hostmacroid']]; 621 unset($db_host['macros'][$hostmacro['hostmacroid']]); 622 623 $upd_hostmacro = DB::getUpdatedValues('hostmacro', $hostmacro, $db_hostmacro); 624 625 if ($upd_hostmacro) { 626 $upd_hostmacros[] = [ 627 'values' => $upd_hostmacro, 628 'where' => ['hostmacroid' => $hostmacro['hostmacroid']] 629 ]; 630 } 631 } 632 else { 633 $ins_hostmacros[] = $hostmacro + ['hostid' => $host[$id_field_name]]; 634 } 635 } 636 637 $del_hostmacroids = array_merge($del_hostmacroids, array_keys($db_host['macros'])); 638 } 639 640 if ($del_hostmacroids) { 641 DB::delete('hostmacro', ['hostmacroid' => $del_hostmacroids]); 642 } 643 644 if ($upd_hostmacros) { 645 DB::update('hostmacro', $upd_hostmacros); 646 } 647 648 if ($ins_hostmacros) { 649 DB::insert('hostmacro', $ins_hostmacros); 650 } 651 } 652 653 /** 654 * Retrieves and adds additional requested data to the result set. 655 * 656 * @param array $options 657 * @param array $result 658 * 659 * @return array 660 */ 661 protected function addRelatedObjects(array $options, array $result) { 662 $result = parent::addRelatedObjects($options, $result); 663 664 $hostids = array_keys($result); 665 666 // adding macros 667 if ($options['selectMacros'] !== null && $options['selectMacros'] !== API_OUTPUT_COUNT) { 668 $macros = API::UserMacro()->get([ 669 'output' => $this->outputExtend($options['selectMacros'], ['hostid', 'hostmacroid']), 670 'hostids' => $hostids, 671 'preservekeys' => true, 672 'nopermissions' => true 673 ]); 674 675 $relationMap = $this->createRelationMap($macros, 'hostid', 'hostmacroid'); 676 $macros = $this->unsetExtraFields($macros, ['hostid', 'hostmacroid'], $options['selectMacros']); 677 $result = $relationMap->mapMany($result, $macros, 'macros', 678 array_key_exists('limitSelects', $options) ? $options['limitSelects'] : null 679 ); 680 } 681 682 return $result; 683 } 684} 685