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 host prototypes. 24 */ 25class CHostPrototype extends CHostBase { 26 27 protected $sortColumns = ['hostid', 'host', 'name', 'status', 'discover']; 28 29 /** 30 * Get host prototypes. 31 * 32 * @param array $options 33 * @param bool $options['selectMacros'] Array of macros fields to be selected or string "extend". 34 * @param string|array $options['selectInterfaces'] Return an "interfaces" property with host interfaces. 35 * 36 * @return array 37 */ 38 public function get(array $options) { 39 $hosts_fields = array_keys($this->getTableSchema('hosts')['fields']); 40 $output_fields = ['hostid', 'host', 'name', 'status', 'templateid', 'inventory_mode', 'discover', 41 'custom_interfaces', 'uuid' 42 ]; 43 $link_fields = ['group_prototypeid', 'groupid', 'hostid', 'templateid']; 44 $group_fields = ['group_prototypeid', 'name', 'hostid', 'templateid']; 45 $discovery_fields = array_keys($this->getTableSchema('items')['fields']); 46 $hostmacro_fields = array_keys($this->getTableSchema('hostmacro')['fields']); 47 $interface_fields = ['type', 'useip', 'ip', 'dns', 'port', 'main', 'details']; 48 49 $api_input_rules = ['type' => API_OBJECT, 'fields' => [ 50 // filter 51 'hostids' => ['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null], 52 'discoveryids' => ['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null], 53 'filter' => ['type' => API_OBJECT, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => [ 54 'hostid' => ['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE], 55 'host' => ['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE], 56 'name' => ['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE], 57 'status' => ['type' => API_INTS32, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'in' => implode(',', [HOST_STATUS_MONITORED, HOST_STATUS_NOT_MONITORED])], 58 'templateid' => ['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE], 59 'inventory_mode' => ['type' => API_INTS32, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'in' => implode(',', [HOST_INVENTORY_DISABLED, HOST_INVENTORY_MANUAL, HOST_INVENTORY_AUTOMATIC])] 60 ]], 61 'search' => ['type' => API_OBJECT, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => [ 62 'host' => ['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE], 63 'name' => ['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE] 64 ]], 65 'searchByAny' => ['type' => API_BOOLEAN, 'default' => false], 66 'startSearch' => ['type' => API_FLAG, 'default' => false], 67 'excludeSearch' => ['type' => API_FLAG, 'default' => false], 68 'searchWildcardsEnabled' => ['type' => API_BOOLEAN, 'default' => false], 69 // output 70 'output' => ['type' => API_OUTPUT, 'in' => 'inventory_mode,'.implode(',', $output_fields), 'default' => $output_fields], 71 'countOutput' => ['type' => API_FLAG, 'default' => false], 72 'groupCount' => ['type' => API_FLAG, 'default' => false], 73 'selectGroupLinks' => ['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $link_fields), 'default' => null], 74 'selectGroupPrototypes' => ['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $group_fields), 'default' => null], 75 'selectDiscoveryRule' => ['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $discovery_fields), 'default' => null], 76 'selectParentHost' => ['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $hosts_fields), 'default' => null], 77 'selectInterfaces' => ['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $interface_fields), 'default' => null], 78 'selectTemplates' => ['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL | API_ALLOW_COUNT, 'in' => implode(',', $hosts_fields), 'default' => null], 79 'selectMacros' => ['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $hostmacro_fields), 'default' => null], 80 'selectTags' => ['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', ['tag', 'value']), 'default' => null], 81 // sort and limit 82 'sortfield' => ['type' => API_STRINGS_UTF8, 'flags' => API_NORMALIZE, 'in' => implode(',', $this->sortColumns), 'uniq' => true, 'default' => []], 83 'sortorder' => ['type' => API_SORTORDER, 'default' => []], 84 'limit' => ['type' => API_INT32, 'flags' => API_ALLOW_NULL, 'in' => '1:'.ZBX_MAX_INT32, 'default' => null], 85 // flags 86 'inherited' => ['type' => API_BOOLEAN, 'flags' => API_ALLOW_NULL, 'default' => null], 87 'editable' => ['type' => API_BOOLEAN, 'default' => false], 88 'preservekeys' => ['type' => API_BOOLEAN, 'default' => false], 89 'nopermissions' => ['type' => API_BOOLEAN, 'default' => false] // TODO: This property and frontend usage SHOULD BE removed. 90 ]]; 91 if (!CApiInputValidator::validate($api_input_rules, $options, '/', $error)) { 92 self::exception(ZBX_API_ERROR_PARAMETERS, $error); 93 } 94 95 $options['filter']['flags'] = ZBX_FLAG_DISCOVERY_PROTOTYPE; 96 97 if ($options['output'] === API_OUTPUT_EXTEND) { 98 $options['output'] = $output_fields; 99 } 100 101 // build and execute query 102 $sql = $this->createSelectQuery($this->tableName(), $options); 103 $res = DBselect($sql, $options['limit']); 104 105 // fetch results 106 $result = []; 107 while ($row = DBfetch($res)) { 108 // a count query, return a single result 109 if ($options['countOutput']) { 110 if ($options['groupCount']) { 111 $result[] = $row; 112 } 113 else { 114 $result = $row['rowscount']; 115 } 116 } 117 // a normal select query 118 else { 119 $result[$row[$this->pk()]] = $row; 120 } 121 } 122 123 if ($options['countOutput']) { 124 return $result; 125 } 126 127 if ($result) { 128 $result = $this->addRelatedObjects($options, $result); 129 $result = $this->unsetExtraFields($result, ['triggerid'], $options['output']); 130 } 131 132 if (!$options['preservekeys']) { 133 $result = zbx_cleanHashes($result); 134 } 135 136 return $result; 137 } 138 139 protected function applyQueryOutputOptions($tableName, $tableAlias, array $options, array $sqlParts) { 140 $sqlParts = parent::applyQueryOutputOptions($tableName, $tableAlias, $options, $sqlParts); 141 142 if (!$options['countOutput'] && $this->outputIsRequested('inventory_mode', $options['output'])) { 143 $sqlParts['select']['inventory_mode'] = 144 dbConditionCoalesce('hinv.inventory_mode', HOST_INVENTORY_DISABLED, 'inventory_mode'); 145 } 146 147 if ((!$options['countOutput'] && $this->outputIsRequested('inventory_mode', $options['output'])) 148 || ($options['filter'] && array_key_exists('inventory_mode', $options['filter']))) { 149 $sqlParts['left_join'][] = ['alias' => 'hinv', 'table' => 'host_inventory', 'using' => 'hostid']; 150 $sqlParts['left_table'] = ['alias' => $this->tableAlias, 'table' => $this->tableName]; 151 } 152 153 return $sqlParts; 154 } 155 156 157 /** 158 * Check for duplicated names. 159 * 160 * @param string $field_name 161 * @param array $names_by_ruleid 162 * 163 * @throws APIException if host prototype with same name already exists. 164 */ 165 private function checkDuplicates($field_name, array $names_by_ruleid) { 166 $sql_where = []; 167 foreach ($names_by_ruleid as $ruleid => $names) { 168 $sql_where[] = '(i.itemid='.$ruleid.' AND '.dbConditionString('h.'.$field_name, $names).')'; 169 } 170 171 $db_host_prototypes = DBfetchArray(DBselect( 172 'SELECT i.name AS rule,h.'.$field_name. 173 ' FROM items i,host_discovery hd,hosts h'. 174 ' WHERE i.itemid=hd.parent_itemid'. 175 ' AND hd.hostid=h.hostid'. 176 ' AND ('.implode(' OR ', $sql_where).')', 177 1 178 )); 179 180 if ($db_host_prototypes) { 181 $error = ($field_name === 'host') 182 ? _('Host prototype with host name "%1$s" already exists in discovery rule "%2$s".') 183 : _('Host prototype with visible name "%1$s" already exists in discovery rule "%2$s".'); 184 185 self::exception(ZBX_API_ERROR_PARAMETERS, 186 sprintf($error, $db_host_prototypes[0][$field_name], $db_host_prototypes[0]['rule']) 187 ); 188 } 189 } 190 191 /** 192 * Validates the input parameters for the create() method. 193 * 194 * @param array $host_prototypes 195 * 196 * @throws APIException if the input is invalid. 197 */ 198 protected function validateCreate(array &$host_prototypes) { 199 $api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['uuid'], ['ruleid', 'host'], ['ruleid', 'name']], 'fields' => [ 200 'uuid' => ['type' => API_UUID], 201 'ruleid' => ['type' => API_ID, 'flags' => API_REQUIRED], 202 'host' => ['type' => API_H_NAME, 'flags' => API_REQUIRED | API_REQUIRED_LLD_MACRO, 'length' => DB::getFieldLength('hosts', 'host')], 203 'name' => ['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('hosts', 'name'), 'default_source' => 'host'], 204 'custom_interfaces' => ['type' => API_INT32, 'in' => implode(',', [HOST_PROT_INTERFACES_INHERIT, HOST_PROT_INTERFACES_CUSTOM]), 'default' => HOST_PROT_INTERFACES_INHERIT], 205 'status' => ['type' => API_INT32, 'in' => implode(',', [HOST_STATUS_MONITORED, HOST_STATUS_NOT_MONITORED])], 206 'discover' => ['type' => API_INT32, 'in' => implode(',', [HOST_DISCOVER, HOST_NO_DISCOVER])], 207 'interfaces' => ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'fields' => [ 208 'type' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [INTERFACE_TYPE_AGENT, INTERFACE_TYPE_SNMP, INTERFACE_TYPE_IPMI, INTERFACE_TYPE_JMX])], 209 'useip' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [INTERFACE_USE_DNS, INTERFACE_USE_IP])], 210 'ip' => ['type' => API_IP, 'flags' => API_ALLOW_USER_MACRO | API_ALLOW_LLD_MACRO | API_ALLOW_MACRO, 'length' => DB::getFieldLength('interface', 'ip')], 211 'dns' => ['type' => API_DNS, 'flags' => API_ALLOW_USER_MACRO | API_ALLOW_LLD_MACRO | API_ALLOW_MACRO, 'length' => DB::getFieldLength('interface', 'dns')], 212 'port' => ['type' => API_PORT, 'flags' => API_REQUIRED | API_NOT_EMPTY | API_ALLOW_USER_MACRO | API_ALLOW_LLD_MACRO, 'length' => DB::getFieldLength('interface', 'port')], 213 'main' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [INTERFACE_SECONDARY, INTERFACE_PRIMARY])], 214 'details' => ['type' => API_MULTIPLE, 'rules' => [ 215 ['if' => ['field' => 'type', 'in' => (string) INTERFACE_TYPE_SNMP], 'type' => API_OBJECT, 'fields' => [ 216 'version' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [SNMP_V1, SNMP_V2C, SNMP_V3])], 217 'bulk' => ['type' => API_INT32, 'in' => implode(',', [SNMP_BULK_DISABLED, SNMP_BULK_ENABLED])], 218 'community' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('interface_snmp', 'community')], 219 'securityname' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('interface_snmp', 'securityname')], 220 'securitylevel' => ['type' => API_INT32, 'in' => implode(',', [ITEM_SNMPV3_SECURITYLEVEL_NOAUTHNOPRIV, ITEM_SNMPV3_SECURITYLEVEL_AUTHNOPRIV, ITEM_SNMPV3_SECURITYLEVEL_AUTHPRIV])], 221 'authpassphrase' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('interface_snmp', 'authpassphrase')], 222 'privpassphrase' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('interface_snmp', 'privpassphrase')], 223 'authprotocol' => ['type' => API_INT32, 'in' => implode(',', array_keys(getSnmpV3AuthProtocols()))], 224 'privprotocol' => ['type' => API_INT32, 'in' => implode(',', array_keys(getSnmpV3PrivProtocols()))], 225 'contextname' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('interface_snmp', 'contextname')] 226 ]], 227 ['if' => ['field' => 'type', 'in' => implode(',', [INTERFACE_TYPE_AGENT, INTERFACE_TYPE_IPMI, INTERFACE_TYPE_JMX])], 'type' => API_OBJECT, 'fields' => []] 228 ]] 229 ]], 230 'groupLinks' => ['type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'uniq' => [['groupid']], 'fields' => [ 231 'groupid' => ['type' => API_ID, 'flags' => API_REQUIRED] 232 ]], 233 'groupPrototypes' => ['type' => API_OBJECTS, 'uniq' => [['name']], 'fields' => [ 234 'name' => ['type' => API_HG_NAME, 'flags' => API_REQUIRED | API_REQUIRED_LLD_MACRO, 'length' => DB::getFieldLength('hstgrp', 'name')] 235 ]], 236 'templates' => ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['templateid']], 'fields' => [ 237 'templateid' => ['type' => API_ID, 'flags' => API_REQUIRED] 238 ]], 239 'tags' => ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['tag', 'value']], 'fields' => [ 240 'tag' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('host_tag', 'tag')], 241 'value' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('host_tag', 'value'), 'default' => DB::getDefault('host_tag', 'value')] 242 ]], 243 'macros' => ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['macro']], 'fields' => [ 244 'macro' => ['type' => API_USER_MACRO, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('hostmacro', 'macro')], 245 'type' => ['type' => API_INT32, 'in' => implode(',', [ZBX_MACRO_TYPE_TEXT, ZBX_MACRO_TYPE_SECRET, ZBX_MACRO_TYPE_VAULT]), 'default' => ZBX_MACRO_TYPE_TEXT], 246 'value' => ['type' => API_MULTIPLE, 'flags' => API_REQUIRED, 'rules' => [ 247 ['if' => ['field' => 'type', 'in' => implode(',', [ZBX_MACRO_TYPE_TEXT, ZBX_MACRO_TYPE_SECRET])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('hostmacro', 'value')], 248 ['if' => ['field' => 'type', 'in' => implode(',', [ZBX_MACRO_TYPE_VAULT])], 'type' => API_VAULT_SECRET, 'length' => DB::getFieldLength('hostmacro', 'value')] 249 ]], 250 'description' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('hostmacro', 'description')] 251 ]], 252 'inventory_mode' => ['type' => API_INT32, 'in' => implode(',', [HOST_INVENTORY_DISABLED, HOST_INVENTORY_MANUAL, HOST_INVENTORY_AUTOMATIC])] 253 ]]; 254 255 if (!CApiInputValidator::validate($api_input_rules, $host_prototypes, '/', $error)) { 256 self::exception(ZBX_API_ERROR_PARAMETERS, $error); 257 } 258 259 $hosts_by_ruleid = []; 260 $names_by_ruleid = []; 261 $groupids = []; 262 263 foreach ($host_prototypes as $host_prototype) { 264 // Collect host group ID links for latter validation. 265 foreach ($host_prototype['groupLinks'] as $group_prototype) { 266 $groupids[$group_prototype['groupid']] = true; 267 } 268 269 $hosts_by_ruleid[$host_prototype['ruleid']][] = $host_prototype['host']; 270 $names_by_ruleid[$host_prototype['ruleid']][] = $host_prototype['name']; 271 } 272 273 $ruleids = array_unique(zbx_objectValues($host_prototypes, 'ruleid')); 274 $groupids = array_keys($groupids); 275 276 $this->checkDiscoveryRulePermissions($ruleids); 277 $this->checkHostGroupsPermissions($groupids); 278 279 // Check if the host is discovered. 280 $db_discovered_hosts = DBfetchArray(DBselect( 281 'SELECT h.host'. 282 ' FROM items i,hosts h'. 283 ' WHERE i.hostid=h.hostid'. 284 ' AND '.dbConditionInt('i.itemid', $ruleids). 285 ' AND h.flags='.ZBX_FLAG_DISCOVERY_CREATED, 286 1 287 )); 288 289 if ($db_discovered_hosts) { 290 self::exception(ZBX_API_ERROR_PARAMETERS, 291 _s('Cannot create a host prototype on a discovered host "%1$s".', $db_discovered_hosts[0]['host']) 292 ); 293 } 294 295 $this->validateInterfaces($host_prototypes); 296 297 $this->checkAndAddUuid($host_prototypes); 298 $this->checkDuplicates('host', $hosts_by_ruleid); 299 $this->checkDuplicates('name', $names_by_ruleid); 300 } 301 302 /** 303 * Check that only host prototypes on templates have UUID. Add UUID to all host prototypes on templates, 304 * if it doesn't exist. 305 * 306 * @param array $host_prototypes_to_create 307 * 308 * @throws APIException 309 */ 310 protected function checkAndAddUuid(array &$host_prototypes_to_create): void { 311 $discovery_ruleids = array_flip(array_column($host_prototypes_to_create, 'ruleid')); 312 313 $db_templated_rules = DBfetchArrayAssoc(DBselect( 314 'SELECT i.itemid, h.status'. 315 ' FROM items i, hosts h'. 316 ' WHERE '.dbConditionInt('i.itemid', array_keys($discovery_ruleids)). 317 ' AND i.hostid=h.hostid'. 318 ' AND h.status = ' . HOST_STATUS_TEMPLATE 319 ), 'itemid'); 320 321 foreach ($host_prototypes_to_create as $index => &$host_prototype) { 322 if (!array_key_exists($host_prototype['ruleid'], $db_templated_rules) 323 && array_key_exists('uuid', $host_prototype)) { 324 self::exception(ZBX_API_ERROR_PARAMETERS, 325 _s('Invalid parameter "%1$s": %2$s.', '/' . ($index + 1), _s('unexpected parameter "%1$s"', 'uuid')) 326 ); 327 } 328 329 if (array_key_exists($host_prototype['ruleid'], $db_templated_rules) 330 && !array_key_exists('uuid', $host_prototype)) { 331 $host_prototype['uuid'] = generateUuidV4(); 332 } 333 } 334 unset($host_prototype); 335 336 $db_uuid = DB::select('hosts', [ 337 'output' => ['uuid'], 338 'filter' => ['uuid' => array_column($host_prototypes_to_create, 'uuid')], 339 'limit' => 1 340 ]); 341 342 if ($db_uuid) { 343 self::exception(ZBX_API_ERROR_PARAMETERS, 344 _s('Entry with UUID "%1$s" already exists.', $db_uuid[0]['uuid']) 345 ); 346 } 347 } 348 349 /** 350 * Creates the given host prototypes. 351 * 352 * @param array $host_prototypes 353 * 354 * @return array 355 */ 356 public function create(array $host_prototypes) { 357 // 'templateid' validation happens during linkage. 358 $this->validateCreate($host_prototypes); 359 360 // Merge groups into group prototypes. 361 foreach ($host_prototypes as &$host_prototype) { 362 $host_prototype['groupPrototypes'] = array_merge( 363 array_key_exists('groupPrototypes', $host_prototype) ? $host_prototype['groupPrototypes'] : [], 364 $host_prototype['groupLinks'] 365 ); 366 unset($host_prototype['groupLinks']); 367 } 368 unset($host_prototype); 369 370 $this->createReal($host_prototypes); 371 $this->inherit($host_prototypes); 372 373 $this->addAuditBulk(AUDIT_ACTION_ADD, AUDIT_RESOURCE_HOST_PROTOTYPE, $host_prototypes); 374 375 return ['hostids' => zbx_objectValues($host_prototypes, 'hostid')]; 376 } 377 378 /** 379 * Creates the host prototypes and inherits them to linked hosts and templates. 380 * 381 * @param array $hostPrototypes 382 */ 383 protected function createReal(array &$hostPrototypes) { 384 foreach ($hostPrototypes as &$hostPrototype) { 385 $hostPrototype['flags'] = ZBX_FLAG_DISCOVERY_PROTOTYPE; 386 } 387 unset($hostPrototype); 388 389 // save the host prototypes 390 $hostPrototypeIds = DB::insert($this->tableName(), $hostPrototypes); 391 392 $groupPrototypes = []; 393 $hostPrototypeDiscoveryRules = []; 394 $hostPrototypeInventory = []; 395 $hosts_tags = []; 396 foreach ($hostPrototypes as $key => &$hostPrototype) { 397 $hostPrototype['hostid'] = $hostPrototypeIds[$key]; 398 399 // save group prototypes 400 foreach ($hostPrototype['groupPrototypes'] as $groupPrototype) { 401 $groupPrototype['hostid'] = $hostPrototype['hostid']; 402 $groupPrototypes[] = $groupPrototype; 403 } 404 405 // discovery rules 406 $hostPrototypeDiscoveryRules[] = [ 407 'hostid' => $hostPrototype['hostid'], 408 'parent_itemid' => $hostPrototype['ruleid'] 409 ]; 410 411 // inventory 412 if (array_key_exists('inventory_mode', $hostPrototype) 413 && $hostPrototype['inventory_mode'] != HOST_INVENTORY_DISABLED) { 414 $hostPrototypeInventory[] = [ 415 'hostid' => $hostPrototype['hostid'], 416 'inventory_mode' => $hostPrototype['inventory_mode'] 417 ]; 418 } 419 420 // tags 421 if (array_key_exists('tags', $hostPrototype)) { 422 foreach (zbx_toArray($hostPrototype['tags']) as $tag) { 423 $hosts_tags[] = ['hostid' => $hostPrototype['hostid']] + $tag; 424 } 425 } 426 } 427 unset($hostPrototype); 428 429 // save group prototypes 430 $groupPrototypes = DB::save('group_prototype', $groupPrototypes); 431 $i = 0; 432 foreach ($hostPrototypes as &$hostPrototype) { 433 foreach ($hostPrototype['groupPrototypes'] as &$groupPrototype) { 434 $groupPrototype['group_prototypeid'] = $groupPrototypes[$i]['group_prototypeid']; 435 $i++; 436 } 437 unset($groupPrototype); 438 } 439 unset($hostPrototype); 440 441 // link host prototypes to discovery rules 442 DB::insert('host_discovery', $hostPrototypeDiscoveryRules, false); 443 444 // save inventory 445 DB::insertBatch('host_inventory', $hostPrototypeInventory, false); 446 447 // save tags 448 if ($hosts_tags) { 449 DB::insert('host_tag', $hosts_tags); 450 } 451 452 // link templates 453 foreach ($hostPrototypes as $hostPrototype) { 454 if (isset($hostPrototype['templates']) && $hostPrototype['templates']) { 455 $this->link(zbx_objectValues($hostPrototype['templates'], 'templateid'), [$hostPrototype['hostid']]); 456 } 457 } 458 459 $this->createInterfaces($hostPrototypes); 460 $this->createHostMacros($hostPrototypes); 461 } 462 463 /** 464 * Validates the input parameters for the update() method. 465 * 466 * @param array $host_prototypes 467 * @param array $db_host_prototypes 468 * 469 * @throws APIException if the input is invalid. 470 */ 471 protected function validateUpdate(array &$host_prototypes, array &$db_host_prototypes = null) { 472 $api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['hostid']], 'fields' => [ 473 'hostid' => ['type' => API_ID, 'flags' => API_REQUIRED], 474 'host' => ['type' => API_H_NAME, 'flags' => API_REQUIRED_LLD_MACRO, 'length' => DB::getFieldLength('hosts', 'host')], 475 'name' => ['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('hosts', 'name')], 476 'custom_interfaces' => ['type' => API_INT32, 'in' => implode(',', [HOST_PROT_INTERFACES_INHERIT, HOST_PROT_INTERFACES_CUSTOM])], 477 'status' => ['type' => API_INT32, 'in' => implode(',', [HOST_STATUS_MONITORED, HOST_STATUS_NOT_MONITORED])], 478 'discover' => ['type' => API_INT32, 'in' => implode(',', [ZBX_PROTOTYPE_DISCOVER, ZBX_PROTOTYPE_NO_DISCOVER])], 479 'interfaces' => ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'fields' => [ 480 'type' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [INTERFACE_TYPE_AGENT, INTERFACE_TYPE_SNMP, INTERFACE_TYPE_IPMI, INTERFACE_TYPE_JMX])], 481 'useip' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [INTERFACE_USE_DNS, INTERFACE_USE_IP])], 482 'ip' => ['type' => API_IP, 'flags' => API_ALLOW_USER_MACRO | API_ALLOW_LLD_MACRO | API_ALLOW_MACRO, 'length' => DB::getFieldLength('interface', 'ip')], 483 'dns' => ['type' => API_DNS, 'flags' => API_ALLOW_USER_MACRO | API_ALLOW_LLD_MACRO | API_ALLOW_MACRO, 'length' => DB::getFieldLength('interface', 'dns')], 484 'port' => ['type' => API_PORT, 'flags' => API_REQUIRED | API_NOT_EMPTY | API_ALLOW_USER_MACRO | API_ALLOW_LLD_MACRO, 'length' => DB::getFieldLength('interface', 'port')], 485 'main' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [INTERFACE_SECONDARY, INTERFACE_PRIMARY])], 486 'details' => ['type' => API_MULTIPLE, 'rules' => [ 487 ['if' => ['field' => 'type', 'in' => (string) INTERFACE_TYPE_SNMP], 'type' => API_OBJECT, 'fields' => [ 488 'version' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [SNMP_V1, SNMP_V2C, SNMP_V3])], 489 'bulk' => ['type' => API_INT32, 'in' => implode(',', [SNMP_BULK_DISABLED, SNMP_BULK_ENABLED])], 490 'community' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('interface_snmp', 'community')], 491 'securityname' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('interface_snmp', 'securityname')], 492 'securitylevel' => ['type' => API_INT32, 'in' => implode(',', [ITEM_SNMPV3_SECURITYLEVEL_NOAUTHNOPRIV, ITEM_SNMPV3_SECURITYLEVEL_AUTHNOPRIV, ITEM_SNMPV3_SECURITYLEVEL_AUTHPRIV])], 493 'authpassphrase' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('interface_snmp', 'authpassphrase')], 494 'privpassphrase' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('interface_snmp', 'privpassphrase')], 495 'authprotocol' => ['type' => API_INT32, 'in' => implode(',', array_keys(getSnmpV3AuthProtocols()))], 496 'privprotocol' => ['type' => API_INT32, 'in' => implode(',', array_keys(getSnmpV3PrivProtocols()))], 497 'contextname' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('interface_snmp', 'contextname')] 498 ]], 499 ['if' => ['field' => 'type', 'in' => implode(',', [INTERFACE_TYPE_AGENT, INTERFACE_TYPE_IPMI, INTERFACE_TYPE_JMX])], 'type' => API_OBJECT, 'fields' => []] 500 ]] 501 ]], 502 'groupLinks' => ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY, 'uniq' => [['group_prototypeid'], ['groupid']], 'fields' => [ 503 'group_prototypeid' => ['type' => API_ID], 504 'groupid' => ['type' => API_ID] 505 ]], 506 'groupPrototypes' => ['type' => API_OBJECTS, 'uniq' => [['group_prototypeid'], ['name']], 'fields' => [ 507 'group_prototypeid' => ['type' => API_ID], 508 'name' => ['type' => API_HG_NAME, 'flags' => API_REQUIRED_LLD_MACRO, 'length' => DB::getFieldLength('hstgrp', 'name')] 509 ]], 510 'templates' => ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['templateid']], 'fields' => [ 511 'templateid' => ['type' => API_ID, 'flags' => API_REQUIRED] 512 ]], 513 'tags' => ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['tag', 'value']], 'fields' => [ 514 'tag' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('host_tag', 'tag')], 515 'value' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('host_tag', 'value'), 'default' => DB::getDefault('host_tag', 'value')] 516 ]], 517 'macros' => ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['hostmacroid']], 'fields' => [ 518 'hostmacroid' => ['type' => API_ID], 519 'macro' => ['type' => API_USER_MACRO, 'length' => DB::getFieldLength('hostmacro', 'macro')], 520 'type' => ['type' => API_INT32, 'in' => implode(',', [ZBX_MACRO_TYPE_TEXT, ZBX_MACRO_TYPE_SECRET, ZBX_MACRO_TYPE_VAULT])], 521 'value' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('hostmacro', 'value')], 522 'description' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('hostmacro', 'description')] 523 ]], 524 'inventory_mode' => ['type' => API_INT32, 'in' => implode(',', [HOST_INVENTORY_DISABLED, HOST_INVENTORY_MANUAL, HOST_INVENTORY_AUTOMATIC])] 525 ]]; 526 527 if (!CApiInputValidator::validate($api_input_rules, $host_prototypes, '/', $error)) { 528 self::exception(ZBX_API_ERROR_PARAMETERS, $error); 529 } 530 531 $db_host_prototypes = $this->get([ 532 'output' => ['hostid', 'host', 'name', 'custom_interfaces', 'status'], 533 'selectDiscoveryRule' => ['itemid'], 534 'selectGroupLinks' => ['group_prototypeid', 'groupid'], 535 'selectGroupPrototypes' => ['group_prototypeid', 'name'], 536 'selectInterfaces' => ['type', 'useip', 'ip', 'dns', 'port', 'main', 'details'], 537 'hostids' => array_column($host_prototypes, 'hostid'), 538 'editable' => true, 539 'preservekeys' => true 540 ]); 541 542 if (count($host_prototypes) != count($db_host_prototypes)) { 543 self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); 544 } 545 546 if (array_column($host_prototypes, 'macros')) { 547 $db_host_prototypes = $this->getHostMacros($db_host_prototypes); 548 $host_prototypes = $this->validateHostMacros($host_prototypes, $db_host_prototypes); 549 } 550 551 $hosts_by_ruleid = []; 552 $names_by_ruleid = []; 553 554 foreach ($host_prototypes as &$host_prototype) { 555 $db_host_prototype = $db_host_prototypes[$host_prototype['hostid']]; 556 $host_prototype['ruleid'] = $db_host_prototype['discoveryRule']['itemid']; 557 558 if (array_key_exists('host', $host_prototype) && $host_prototype['host'] !== $db_host_prototype['host']) { 559 $hosts_by_ruleid[$host_prototype['ruleid']][] = $host_prototype['host']; 560 } 561 562 if (array_key_exists('name', $host_prototype) && $host_prototype['name'] !== $db_host_prototype['name']) { 563 $names_by_ruleid[$host_prototype['ruleid']][] = $host_prototype['name']; 564 } 565 } 566 unset($host_prototype); 567 568 $api_input_rules = ['type' => API_OBJECTS, 'uniq' => [['ruleid', 'host'], ['ruleid', 'name']], 'fields' => [ 569 'ruleid' => ['type' => API_ID], 570 'host' => ['type' => API_H_NAME], 571 'name' => ['type' => API_STRING_UTF8] 572 ]]; 573 574 if (!CApiInputValidator::validateUniqueness($api_input_rules, $host_prototypes, '/', $error)) { 575 self::exception(ZBX_API_ERROR_PARAMETERS, $error); 576 } 577 578 $groupids = []; 579 $db_groupids = []; 580 581 foreach ($host_prototypes as $host_prototype) { 582 $db_host_prototype = $db_host_prototypes[$host_prototype['hostid']]; 583 584 foreach ($db_host_prototype['groupLinks'] as $db_group_link) { 585 $db_groupids[$db_group_link['groupid']] = true; 586 } 587 588 $db_group_links = zbx_toHash($db_host_prototype['groupLinks'], 'group_prototypeid'); 589 $db_group_prototypes = zbx_toHash($db_host_prototype['groupPrototypes'], 'group_prototypeid'); 590 591 // Validate 'group_prototypeid' in 'groupLinks' property. 592 if (array_key_exists('groupLinks', $host_prototype)) { 593 foreach ($host_prototype['groupLinks'] as $group_link) { 594 if (!$group_link) { 595 self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.')); 596 } 597 598 // Don't allow invalid 'group_prototypeid' parameters which do not belong to this 'hostid'. 599 if (array_key_exists('group_prototypeid', $group_link) 600 && !array_key_exists($group_link['group_prototypeid'], $db_group_links)) { 601 self::exception(ZBX_API_ERROR_PERMISSIONS, 602 _('No permissions to referred object or it does not exist!') 603 ); 604 } 605 606 if (array_key_exists('groupid', $group_link)) { 607 $groupids[$group_link['groupid']] = true; 608 } 609 } 610 } 611 612 // Validate 'group_prototypeid' in 'groupPrototypes' property. 613 if (array_key_exists('groupPrototypes', $host_prototype)) { 614 foreach ($host_prototype['groupPrototypes'] as $group_prototype) { 615 if (!$group_prototype) { 616 self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.')); 617 } 618 619 // Don't allow invalid 'group_prototypeid' parameters which do not belong to this 'hostid'. 620 if (array_key_exists('group_prototypeid', $group_prototype) 621 && !array_key_exists($group_prototype['group_prototypeid'], $db_group_prototypes)) { 622 self::exception(ZBX_API_ERROR_PERMISSIONS, 623 _('No permissions to referred object or it does not exist!') 624 ); 625 } 626 } 627 } 628 } 629 630 // Collect only new given groupids for validation. 631 $groupids = array_diff_key($groupids, $db_groupids); 632 633 if ($groupids) { 634 $this->checkHostGroupsPermissions(array_keys($groupids)); 635 } 636 637 $host_prototypes = $this->extendObjectsByKey($host_prototypes, $db_host_prototypes, 'hostid', 638 ['host', 'name', 'custom_interfaces', 'groupLinks', 'groupPrototypes'] 639 ); 640 641 $this->validateInterfaces($host_prototypes); 642 643 if ($hosts_by_ruleid) { 644 $this->checkDuplicates('host', $hosts_by_ruleid); 645 } 646 if ($names_by_ruleid) { 647 $this->checkDuplicates('name', $names_by_ruleid); 648 } 649 } 650 651 /** 652 * Updates the given host prototypes. 653 * 654 * @param array $host_prototypes 655 * 656 * @return array 657 */ 658 public function update(array $host_prototypes) { 659 $this->validateUpdate($host_prototypes, $db_host_prototypes); 660 661 // merge group links into group prototypes 662 foreach ($host_prototypes as &$host_prototype) { 663 $host_prototype['groupPrototypes'] = 664 array_merge($host_prototype['groupPrototypes'], $host_prototype['groupLinks']); 665 unset($host_prototype['groupLinks']); 666 } 667 unset($host_prototype); 668 669 $this->updateHostMacros($host_prototypes, $db_host_prototypes); 670 671 $host_prototypes = $this->updateReal($host_prototypes); 672 $this->updateInterfaces($host_prototypes); 673 $this->inherit($host_prototypes); 674 675 foreach ($db_host_prototypes as &$db_host_prototype) { 676 unset($db_host_prototype['discoveryRule'], $db_host_prototype['groupLinks'], 677 $db_host_prototype['groupPrototypes'] 678 ); 679 } 680 unset($db_host_prototype); 681 682 $this->addAuditBulk(AUDIT_ACTION_UPDATE, AUDIT_RESOURCE_HOST_PROTOTYPE, $host_prototypes, $db_host_prototypes); 683 684 return ['hostids' => zbx_objectValues($host_prototypes, 'hostid')]; 685 } 686 687 /** 688 * Updates the host prototypes and propagates the changes to linked hosts and templates. 689 * 690 * @param array $host_prototypes 691 * 692 * @return array 693 */ 694 protected function updateReal(array $host_prototypes) { 695 // save the host prototypes 696 foreach ($host_prototypes as $host_prototype) { 697 DB::updateByPk($this->tableName(), $host_prototype['hostid'], $host_prototype); 698 } 699 700 $ex_host_prototypes = $this->get([ 701 'output' => ['hostid', 'inventory_mode'], 702 'selectGroupLinks' => API_OUTPUT_EXTEND, 703 'selectGroupPrototypes' => API_OUTPUT_EXTEND, 704 'selectTemplates' => ['templateid'], 705 'hostids' => zbx_objectValues($host_prototypes, 'hostid'), 706 'preservekeys' => true 707 ]); 708 709 // update related objects 710 $inventory_create = []; 711 $inventory_deleteids = []; 712 foreach ($host_prototypes as $key => $host_prototype) { 713 $ex_host_prototype = $ex_host_prototypes[$host_prototype['hostid']]; 714 715 // group prototypes 716 if (isset($host_prototype['groupPrototypes'])) { 717 foreach ($host_prototype['groupPrototypes'] as &$group_prototype) { 718 $group_prototype['hostid'] = $host_prototype['hostid']; 719 } 720 unset($group_prototype); 721 722 // save group prototypes 723 $ex_group_prototypes = zbx_toHash( 724 array_merge($ex_host_prototype['groupLinks'], $ex_host_prototype['groupPrototypes']), 725 'group_prototypeid' 726 ); 727 $modified_group_prototypes = []; 728 foreach ($host_prototype['groupPrototypes'] as $group_prototype) { 729 if (isset($group_prototype['group_prototypeid'])) { 730 unset($ex_group_prototypes[$group_prototype['group_prototypeid']]); 731 } 732 733 $modified_group_prototypes[] = $group_prototype; 734 } 735 if ($ex_group_prototypes) { 736 $this->deleteGroupPrototypes(array_keys($ex_group_prototypes)); 737 } 738 $host_prototypes[$key]['groupPrototypes'] = DB::save('group_prototype', $modified_group_prototypes); 739 } 740 741 // templates 742 if (isset($host_prototype['templates'])) { 743 $existing_templateids = zbx_objectValues($ex_host_prototype['templates'], 'templateid'); 744 $new_templateids = zbx_objectValues($host_prototype['templates'], 'templateid'); 745 $this->unlink(array_diff($existing_templateids, $new_templateids), [$host_prototype['hostid']]); 746 $this->link(array_diff($new_templateids, $existing_templateids), [$host_prototype['hostid']]); 747 } 748 749 // inventory 750 if (array_key_exists('inventory_mode', $host_prototype)) { 751 if ($host_prototype['inventory_mode'] == HOST_INVENTORY_DISABLED) { 752 $inventory_deleteids[] = $host_prototype['hostid']; 753 } 754 else { 755 $inventory = ['inventory_mode' => $host_prototype['inventory_mode']]; 756 757 if ($ex_host_prototype['inventory_mode'] != HOST_INVENTORY_DISABLED) { 758 if ($host_prototype['inventory_mode'] != $ex_host_prototype['inventory_mode']) { 759 DB::update('host_inventory', [ 760 'values' => $inventory, 761 'where' => ['hostid' => $host_prototype['hostid']] 762 ]); 763 } 764 } 765 else { 766 $inventory_create[] = $inventory + ['hostid' => $host_prototype['hostid']]; 767 } 768 } 769 } 770 } 771 772 // save inventory 773 DB::insertBatch('host_inventory', $inventory_create, false); 774 DB::delete('host_inventory', ['hostid' => $inventory_deleteids]); 775 776 $this->updateTags(array_column($host_prototypes, 'tags', 'hostid')); 777 778 return $host_prototypes; 779 } 780 781 /** 782 * Updates the children of the host prototypes on the given hosts and propagates the inheritance to the child hosts. 783 * 784 * @param array $hostPrototypes array of host prototypes to inherit 785 * @param array $hostids array of hosts to inherit to; if set to null, the children will be updated on all 786 * child hosts 787 * 788 * @return bool 789 */ 790 protected function inherit(array $hostPrototypes, array $hostids = null) { 791 if (empty($hostPrototypes)) { 792 return true; 793 } 794 795 // prepare the child host prototypes 796 $newHostPrototypes = $this->prepareInheritedObjects($hostPrototypes, $hostids); 797 if (!$newHostPrototypes) { 798 return true; 799 } 800 801 $insertHostPrototypes = []; 802 $updateHostPrototypes = []; 803 foreach ($newHostPrototypes as $newHostPrototype) { 804 if (isset($newHostPrototype['hostid'])) { 805 $updateHostPrototypes[] = $newHostPrototype; 806 } 807 else { 808 $insertHostPrototypes[] = $newHostPrototype; 809 } 810 } 811 812 // save the new host prototypes 813 if ($insertHostPrototypes) { 814 $this->createReal($insertHostPrototypes); 815 } 816 817 if ($updateHostPrototypes) { 818 $updateHostPrototypes = $this->updateReal($updateHostPrototypes); 819 $this->updateInterfaces($updateHostPrototypes); 820 $macros = array_column($updateHostPrototypes, 'macros', 'hostid'); 821 822 if ($macros) { 823 $this->updateMacros($macros); 824 } 825 } 826 827 $host_prototypes = array_merge($updateHostPrototypes, $insertHostPrototypes); 828 829 if ($host_prototypes) { 830 $sql = 'SELECT hd.hostid'. 831 ' FROM host_discovery hd,items i,hosts h'. 832 ' WHERE hd.parent_itemid=i.itemid'. 833 ' AND i.hostid=h.hostid'. 834 ' AND h.status='.HOST_STATUS_TEMPLATE. 835 ' AND '.dbConditionInt('hd.hostid', zbx_objectValues($host_prototypes, 'hostid')); 836 $valid_prototypes = DBfetchArrayAssoc(DBselect($sql), 'hostid'); 837 838 foreach ($host_prototypes as $key => $host_prototype) { 839 if (!array_key_exists($host_prototype['hostid'], $valid_prototypes)) { 840 unset($host_prototypes[$key]); 841 } 842 } 843 } 844 845 // propagate the inheritance to the children 846 return $this->inherit($host_prototypes); 847 } 848 849 850 /** 851 * Prepares and returns an array of child host prototypes, inherited from host prototypes $host_prototypes 852 * on the given hosts. 853 * 854 * Each host prototype must have the "ruleid" parameter set. 855 * 856 * @param array $host_prototypes 857 * @param array $hostIds 858 * 859 * @return array an array of unsaved child host prototypes 860 */ 861 protected function prepareInheritedObjects(array $host_prototypes, array $hostIds = null) { 862 // Fetch the related discovery rules with their hosts. 863 $discoveryRules = API::DiscoveryRule()->get([ 864 'output' => ['itemid', 'hostid'], 865 'selectHosts' => ['hostid'], 866 'itemids' => array_column($host_prototypes, 'ruleid'), 867 'templated' => true, 868 'nopermissions' => true, 869 'preservekeys' => true 870 ]); 871 872 // Remove host prototypes which don't belong to templates, so they cannot be inherited. 873 $host_prototypes = array_filter($host_prototypes, function ($host_prototype) use ($discoveryRules) { 874 return array_key_exists($host_prototype['ruleid'], $discoveryRules); 875 }); 876 877 // Fetch all child hosts to inherit to. Do not inherit host prototypes on discovered hosts. 878 $chdHosts = API::Host()->get([ 879 'output' => ['hostid', 'host', 'status'], 880 'selectParentTemplates' => ['templateid'], 881 'templateids' => zbx_objectValues($discoveryRules, 'hostid'), 882 'hostids' => $hostIds, 883 'nopermissions' => true, 884 'templated_hosts' => true, 885 'filter' => ['flags' => ZBX_FLAG_DISCOVERY_NORMAL] 886 ]); 887 if (empty($chdHosts)) { 888 return []; 889 } 890 891 // Fetch the child discovery rules. 892 $childDiscoveryRules = API::DiscoveryRule()->get([ 893 'output' => ['itemid', 'templateid', 'hostid'], 894 'filter' => [ 895 'templateid' => array_keys($discoveryRules) 896 ], 897 'nopermissions' => true, 898 'preservekeys' => true 899 ]); 900 901 /* 902 * Fetch child host prototypes and group them by discovery rule. "selectInterfaces" is not required, because 903 * all child are rewritten when updating parents. 904 */ 905 $childHostPrototypes = API::HostPrototype()->get([ 906 'output' => ['hostid', 'host', 'templateid'], 907 'selectGroupLinks' => API_OUTPUT_EXTEND, 908 'selectGroupPrototypes' => API_OUTPUT_EXTEND, 909 'selectDiscoveryRule' => ['itemid'], 910 'discoveryids' => zbx_objectValues($childDiscoveryRules, 'itemid'), 911 'nopermissions' => true 912 ]); 913 foreach ($childDiscoveryRules as &$childDiscoveryRule) { 914 $childDiscoveryRule['hostPrototypes'] = []; 915 } 916 unset($childDiscoveryRule); 917 foreach ($childHostPrototypes as $childHostPrototype) { 918 $discoveryRuleId = $childHostPrototype['discoveryRule']['itemid']; 919 unset($childHostPrototype['discoveryRule']); 920 921 $childDiscoveryRules[$discoveryRuleId]['hostPrototypes'][] = $childHostPrototype; 922 } 923 924 // match each discovery that the parent host prototypes belong to to the child discovery rule for each host 925 $discoveryRuleChildren = []; 926 foreach ($childDiscoveryRules as $childRule) { 927 $discoveryRuleChildren[$childRule['templateid']][$childRule['hostid']] = $childRule['itemid']; 928 } 929 930 $newHostPrototypes = []; 931 foreach ($chdHosts as $host) { 932 $hostId = $host['hostid']; 933 934 // skip items not from parent templates of current host 935 $templateIds = zbx_toHash($host['parentTemplates'], 'templateid'); 936 $parentHostPrototypes = []; 937 foreach ($host_prototypes as $inum => $parentHostPrototype) { 938 $parentTemplateId = $discoveryRules[$parentHostPrototype['ruleid']]['hostid']; 939 940 if (isset($templateIds[$parentTemplateId])) { 941 $parentHostPrototypes[$inum] = $parentHostPrototype; 942 } 943 } 944 945 foreach ($parentHostPrototypes as $parentHostPrototype) { 946 $childDiscoveryRuleId = $discoveryRuleChildren[$parentHostPrototype['ruleid']][$hostId]; 947 $exHostPrototype = null; 948 949 // check if the child discovery rule already has host prototypes 950 $exHostPrototypes = $childDiscoveryRules[$childDiscoveryRuleId]['hostPrototypes']; 951 if ($exHostPrototypes) { 952 $exHostPrototypesHosts = zbx_toHash($exHostPrototypes, 'host'); 953 $exHostPrototypesTemplateIds = zbx_toHash($exHostPrototypes, 'templateid'); 954 955 // look for an already created inherited host prototype 956 // if one exists - update it 957 if (isset($exHostPrototypesTemplateIds[$parentHostPrototype['hostid']])) { 958 $exHostPrototype = $exHostPrototypesTemplateIds[$parentHostPrototype['hostid']]; 959 960 // check if there's a host prototype on the target host with the same host name but from a different template 961 // or no template 962 if (isset($exHostPrototypesHosts[$parentHostPrototype['host']]) 963 && !idcmp($exHostPrototypesHosts[$parentHostPrototype['host']]['templateid'], $parentHostPrototype['hostid'])) { 964 965 $discoveryRule = DBfetch(DBselect('SELECT i.name FROM items i WHERE i.itemid='.zbx_dbstr($exHostPrototype['discoveryRule']['itemid']))); 966 self::exception(ZBX_API_ERROR_PARAMETERS, _s('Host prototype "%1$s" already exists on "%2$s".', $parentHostPrototype['host'], $discoveryRule['name'])); 967 } 968 } 969 970 // look for a host prototype with the same host name 971 // if one exists - convert it to an inherited host prototype 972 if (isset($exHostPrototypesHosts[$parentHostPrototype['host']])) { 973 $exHostPrototype = $exHostPrototypesHosts[$parentHostPrototype['host']]; 974 975 // check that this host prototype is not inherited from a different template 976 if ($exHostPrototype['templateid'] > 0 && !idcmp($exHostPrototype['templateid'], $parentHostPrototype['hostid'])) { 977 $discoveryRule = DBfetch(DBselect('SELECT i.name FROM items i WHERE i.itemid='.zbx_dbstr($exHostPrototype['discoveryRule']['itemid']))); 978 self::exception(ZBX_API_ERROR_PARAMETERS, _s('Host prototype "%1$s" already exists on "%2$s", inherited from another template.', $parentHostPrototype['host'], $discoveryRule['name'])); 979 } 980 } 981 } 982 983 // copy host prototype 984 $newHostPrototype = $parentHostPrototype; 985 $newHostPrototype['uuid'] = ''; 986 $newHostPrototype['ruleid'] = $discoveryRuleChildren[$parentHostPrototype['ruleid']][$hostId]; 987 $newHostPrototype['templateid'] = $parentHostPrototype['hostid']; 988 989 if (array_key_exists('macros', $newHostPrototype)) { 990 foreach ($newHostPrototype['macros'] as &$hostmacro) { 991 unset($hostmacro['hostmacroid']); 992 } 993 unset($hostmacro); 994 } 995 996 // update an existing inherited host prototype 997 if ($exHostPrototype) { 998 // look for existing group prototypes to update 999 $exGroupPrototypesByTemplateId = zbx_toHash($exHostPrototype['groupPrototypes'], 'templateid'); 1000 $exGroupPrototypesByName = zbx_toHash($exHostPrototype['groupPrototypes'], 'name'); 1001 $exGroupPrototypesByGroupId = zbx_toHash($exHostPrototype['groupLinks'], 'groupid'); 1002 1003 // look for a group prototype that can be updated 1004 foreach ($newHostPrototype['groupPrototypes'] as &$groupPrototype) { 1005 // updated an inherited item prototype by templateid 1006 if (isset($exGroupPrototypesByTemplateId[$groupPrototype['group_prototypeid']])) { 1007 $groupPrototype['group_prototypeid'] = $exGroupPrototypesByTemplateId[$groupPrototype['group_prototypeid']]['group_prototypeid']; 1008 } 1009 // updated an inherited item prototype by name 1010 elseif (isset($groupPrototype['name']) && !zbx_empty($groupPrototype['name']) 1011 && isset($exGroupPrototypesByName[$groupPrototype['name']])) { 1012 1013 $groupPrototype['templateid'] = $groupPrototype['group_prototypeid']; 1014 $groupPrototype['group_prototypeid'] = $exGroupPrototypesByName[$groupPrototype['name']]['group_prototypeid']; 1015 } 1016 // updated an inherited item prototype by group ID 1017 elseif (isset($groupPrototype['groupid']) && $groupPrototype['groupid'] 1018 && isset($exGroupPrototypesByGroupId[$groupPrototype['groupid']])) { 1019 1020 $groupPrototype['templateid'] = $groupPrototype['group_prototypeid']; 1021 $groupPrototype['group_prototypeid'] = $exGroupPrototypesByGroupId[$groupPrototype['groupid']]['group_prototypeid']; 1022 } 1023 // create a new child group prototype 1024 else { 1025 $groupPrototype['templateid'] = $groupPrototype['group_prototypeid']; 1026 unset($groupPrototype['group_prototypeid']); 1027 } 1028 1029 unset($groupPrototype['hostid']); 1030 } 1031 unset($groupPrototype); 1032 1033 $newHostPrototype['hostid'] = $exHostPrototype['hostid']; 1034 } 1035 // create a new inherited host prototype 1036 else { 1037 foreach ($newHostPrototype['groupPrototypes'] as &$groupPrototype) { 1038 $groupPrototype['templateid'] = $groupPrototype['group_prototypeid']; 1039 unset($groupPrototype['group_prototypeid'], $groupPrototype['hostid']); 1040 } 1041 unset($groupPrototype); 1042 1043 unset($newHostPrototype['hostid']); 1044 } 1045 $newHostPrototypes[] = $newHostPrototype; 1046 } 1047 } 1048 1049 return $newHostPrototypes; 1050 } 1051 1052 /** 1053 * Inherits all host prototypes from the templates given in "templateids" to hosts or templates given in "hostids". 1054 * 1055 * @param array $data 1056 * 1057 * @return bool 1058 */ 1059 public function syncTemplates(array $data) { 1060 $data['templateids'] = zbx_toArray($data['templateids']); 1061 $data['hostids'] = zbx_toArray($data['hostids']); 1062 1063 $discoveryRules = API::DiscoveryRule()->get([ 1064 'output' => ['itemid'], 1065 'hostids' => $data['templateids'] 1066 ]); 1067 $hostPrototypes = $this->get([ 1068 'discoveryids' => zbx_objectValues($discoveryRules, 'itemid'), 1069 'preservekeys' => true, 1070 'output' => API_OUTPUT_EXTEND, 1071 'selectGroupLinks' => API_OUTPUT_EXTEND, 1072 'selectGroupPrototypes' => API_OUTPUT_EXTEND, 1073 'selectTags' => ['tag', 'value'], 1074 'selectTemplates' => ['templateid'], 1075 'selectDiscoveryRule' => ['itemid'], 1076 'selectInterfaces' => ['main', 'type', 'useip', 'ip', 'dns', 'port', 'details'] 1077 ]); 1078 1079 $hostPrototypes = $this->getHostMacros($hostPrototypes); 1080 1081 foreach ($hostPrototypes as &$hostPrototype) { 1082 // merge group links into group prototypes 1083 foreach ($hostPrototype['groupLinks'] as $group) { 1084 $hostPrototype['groupPrototypes'][] = $group; 1085 } 1086 unset($hostPrototype['groupLinks']); 1087 1088 // the ID of the discovery rule must be passed in the "ruleid" parameter 1089 $hostPrototype['ruleid'] = $hostPrototype['discoveryRule']['itemid']; 1090 unset($hostPrototype['discoveryRule']); 1091 } 1092 unset($hostPrototype); 1093 1094 $this->inherit($hostPrototypes, $data['hostids']); 1095 1096 return true; 1097 } 1098 1099 /** 1100 * Validates the input parameters for the delete() method. 1101 * 1102 * @throws APIException if the input is invalid 1103 * 1104 * @param array $hostPrototypeIds 1105 * @param bool $nopermissions 1106 */ 1107 protected function validateDelete($hostPrototypeIds, $nopermissions) { 1108 if (!$hostPrototypeIds) { 1109 self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.')); 1110 } 1111 1112 if (!$nopermissions) { 1113 $this->checkHostPrototypePermissions($hostPrototypeIds); 1114 $this->checkNotInherited($hostPrototypeIds); 1115 } 1116 } 1117 1118 /** 1119 * Delete host prototypes. 1120 * 1121 * @param array $hostPrototypeIds 1122 * @param bool $nopermissions if set to true, permission and template checks will be skipped 1123 * 1124 * @return array 1125 */ 1126 public function delete(array $hostPrototypeIds, $nopermissions = false) { 1127 $this->validateDelete($hostPrototypeIds, $nopermissions); 1128 1129 // include child IDs 1130 $parentHostPrototypeIds = $hostPrototypeIds; 1131 $childHostPrototypeIds = []; 1132 do { 1133 $query = DBselect('SELECT h.hostid FROM hosts h WHERE '.dbConditionInt('h.templateid', $parentHostPrototypeIds)); 1134 $parentHostPrototypeIds = []; 1135 while ($hostPrototype = DBfetch($query)) { 1136 $parentHostPrototypeIds[] = $hostPrototype['hostid']; 1137 $childHostPrototypeIds[] = $hostPrototype['hostid']; 1138 } 1139 } while (!empty($parentHostPrototypeIds)); 1140 1141 $hostPrototypeIds = array_merge($hostPrototypeIds, $childHostPrototypeIds); 1142 1143 // Lock host prototypes before delete to prevent server from adding new LLD hosts. 1144 DBselect( 1145 'SELECT NULL'. 1146 ' FROM hosts h'. 1147 ' WHERE '.dbConditionInt('h.hostid', $hostPrototypeIds). 1148 ' FOR UPDATE' 1149 ); 1150 1151 $deleteHostPrototypes = $this->get([ 1152 'hostids' => $hostPrototypeIds, 1153 'output' => ['host'], 1154 'selectGroupPrototypes' => ['group_prototypeid'], 1155 'selectParentHost' => ['host'], 1156 'nopermissions' => true 1157 ]); 1158 1159 // delete discovered hosts 1160 $discoveredHosts = DBfetchArray(DBselect( 1161 'SELECT hostid FROM host_discovery WHERE '.dbConditionInt('parent_hostid', $hostPrototypeIds) 1162 )); 1163 if ($discoveredHosts) { 1164 API::Host()->delete(zbx_objectValues($discoveredHosts, 'hostid'), true); 1165 } 1166 1167 // delete group prototypes and discovered groups 1168 $groupPrototypeIds = []; 1169 foreach ($deleteHostPrototypes as $groupPrototype) { 1170 foreach ($groupPrototype['groupPrototypes'] as $groupPrototype) { 1171 $groupPrototypeIds[] = $groupPrototype['group_prototypeid']; 1172 } 1173 } 1174 $this->deleteGroupPrototypes($groupPrototypeIds); 1175 1176 // delete host prototypes 1177 DB::delete($this->tableName(), ['hostid' => $hostPrototypeIds]); 1178 1179 // TODO: REMOVE info 1180 foreach ($deleteHostPrototypes as $hostProtototype) { 1181 info(_s('Deleted: Host prototype "%1$s" on "%2$s".', $hostProtototype['host'], $hostProtototype['parentHost']['host'])); 1182 } 1183 1184 return ['hostids' => $hostPrototypeIds]; 1185 } 1186 1187 protected function link(array $templateids, array $targetids) { 1188 $this->checkHostPrototypePermissions($targetids); 1189 1190 $links = parent::link($templateids, $targetids); 1191 1192 foreach ($targetids as $targetid) { 1193 $linked_templates = API::Template()->get([ 1194 'output' => [], 1195 'hostids' => [$targetid], 1196 'nopermissions' => true 1197 ]); 1198 1199 $result = DBselect( 1200 'SELECT i.key_,count(*)'. 1201 ' FROM items i'. 1202 ' WHERE '.dbConditionInt('i.hostid', array_merge($templateids, array_keys($linked_templates))). 1203 ' GROUP BY i.key_'. 1204 ' HAVING count(*)>1', 1205 1 1206 ); 1207 if ($row = DBfetch($result)) { 1208 $target_templates = API::HostPrototype()->get([ 1209 'output' => ['name'], 1210 'hostids' => [$targetid], 1211 'nopermissions' => true 1212 ]); 1213 1214 self::exception(ZBX_API_ERROR_PARAMETERS, 1215 _s('Item "%1$s" already exists on "%2$s", inherited from another template.', $row['key_'], 1216 $target_templates[0]['name'] 1217 ) 1218 ); 1219 } 1220 } 1221 1222 return $links; 1223 } 1224 1225 /** 1226 * Checks if the current user has access to the given LLD rules. 1227 * 1228 * @throws APIException if the user doesn't have write permissions for the given LLD rules 1229 * 1230 * @param array $ruleids 1231 */ 1232 protected function checkDiscoveryRulePermissions(array $ruleids) { 1233 $count = API::DiscoveryRule()->get([ 1234 'countOutput' => true, 1235 'itemids' => $ruleids, 1236 'editable' => true 1237 ]); 1238 1239 if ($count != count($ruleids)) { 1240 self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); 1241 } 1242 } 1243 1244 /** 1245 * Checks if the current user has access to the given host groups. 1246 * 1247 * @throws APIException if the user doesn't have write permissions for the given host groups 1248 * 1249 * @param array $groupids 1250 */ 1251 protected function checkHostGroupsPermissions(array $groupids) { 1252 $db_groups = API::HostGroup()->get([ 1253 'output' => ['name', 'flags'], 1254 'groupids' => $groupids, 1255 'editable' => true, 1256 'preservekeys' => true 1257 ]); 1258 1259 foreach ($groupids as $groupid) { 1260 if (!array_key_exists($groupid, $db_groups)) { 1261 self::exception(ZBX_API_ERROR_PERMISSIONS, 1262 _('No permissions to referred object or it does not exist!') 1263 ); 1264 } 1265 1266 $db_group = $db_groups[$groupid]; 1267 1268 // Check if group prototypes use discovered host groups. 1269 if ($db_group['flags'] == ZBX_FLAG_DISCOVERY_CREATED) { 1270 self::exception(ZBX_API_ERROR_PARAMETERS, 1271 _s('Group prototype cannot be based on a discovered host group "%1$s".', $db_group['name']) 1272 ); 1273 } 1274 } 1275 } 1276 1277 /** 1278 * Checks if the current user has access to the given host prototypes. 1279 * 1280 * @throws APIException if the user doesn't have write permissions for the host prototypes. 1281 * 1282 * @param array $hostPrototypeIds 1283 */ 1284 protected function checkHostPrototypePermissions(array $hostPrototypeIds) { 1285 if ($hostPrototypeIds) { 1286 $hostPrototypeIds = array_unique($hostPrototypeIds); 1287 1288 $count = $this->get([ 1289 'countOutput' => true, 1290 'hostids' => $hostPrototypeIds, 1291 'editable' => true 1292 ]); 1293 1294 if ($count != count($hostPrototypeIds)) { 1295 self::exception(ZBX_API_ERROR_PERMISSIONS, 1296 _('No permissions to referred object or it does not exist!') 1297 ); 1298 } 1299 } 1300 } 1301 1302 /** 1303 * Checks if the given host prototypes are not inherited from a template. 1304 * 1305 * @throws APIException if at least one host prototype is inherited 1306 * 1307 * @param array $hostPrototypeIds 1308 */ 1309 protected function checkNotInherited(array $hostPrototypeIds) { 1310 $query = DBSelect('SELECT hostid FROM hosts h WHERE h.templateid>0 AND '.dbConditionInt('h.hostid', $hostPrototypeIds), 1); 1311 1312 if ($hostPrototype = DBfetch($query)) { 1313 self::exception(ZBX_API_ERROR_PERMISSIONS, _('Cannot delete templated host prototype.')); 1314 } 1315 } 1316 1317 protected function applyQueryFilterOptions($tableName, $tableAlias, array $options, array $sqlParts) { 1318 $sqlParts = parent::applyQueryFilterOptions($tableName, $tableAlias, $options, $sqlParts); 1319 1320 // do not return host prototypes from discovered hosts 1321 $sqlParts['from'][] = 'host_discovery hd'; 1322 $sqlParts['from'][] = 'items i'; 1323 $sqlParts['from'][] = 'hosts ph'; 1324 $sqlParts['where'][] = $this->fieldId('hostid').'=hd.hostid'; 1325 $sqlParts['where'][] = 'hd.parent_itemid=i.itemid'; 1326 $sqlParts['where'][] = 'i.hostid=ph.hostid'; 1327 $sqlParts['where'][] = 'ph.flags='.ZBX_FLAG_DISCOVERY_NORMAL; 1328 1329 if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN && !$options['nopermissions']) { 1330 $permission = $options['editable'] ? PERM_READ_WRITE : PERM_READ; 1331 1332 $sqlParts['where'][] = 'EXISTS ('. 1333 'SELECT NULL'. 1334 ' FROM '. 1335 'host_discovery hd,items i,hosts_groups hgg'. 1336 ' JOIN rights r'. 1337 ' ON r.id=hgg.groupid'. 1338 ' AND '.dbConditionInt('r.groupid', getUserGroupsByUserId(self::$userData['userid'])). 1339 ' WHERE h.hostid=hd.hostid'. 1340 ' AND hd.parent_itemid=i.itemid'. 1341 ' AND i.hostid=hgg.hostid'. 1342 ' GROUP BY hgg.hostid'. 1343 ' HAVING MIN(r.permission)>'.PERM_DENY. 1344 ' AND MAX(r.permission)>='.zbx_dbstr($permission). 1345 ')'; 1346 } 1347 1348 // discoveryids 1349 if ($options['discoveryids'] !== null) { 1350 $sqlParts['where'][] = dbConditionInt('hd.parent_itemid', (array) $options['discoveryids']); 1351 1352 if ($options['groupCount']) { 1353 $sqlParts['group']['hd'] = 'hd.parent_itemid'; 1354 } 1355 } 1356 1357 // inherited 1358 if ($options['inherited'] !== null) { 1359 $sqlParts['where'][] = ($options['inherited']) ? 'h.templateid IS NOT NULL' : 'h.templateid IS NULL'; 1360 } 1361 1362 if ($options['filter'] && array_key_exists('inventory_mode', $options['filter'])) { 1363 if ($options['filter']['inventory_mode'] !== null) { 1364 $inventory_mode_query = (array) $options['filter']['inventory_mode']; 1365 1366 $inventory_mode_where = []; 1367 $null_position = array_search(HOST_INVENTORY_DISABLED, $inventory_mode_query); 1368 1369 if ($null_position !== false) { 1370 unset($inventory_mode_query[$null_position]); 1371 $inventory_mode_where[] = 'hinv.inventory_mode IS NULL'; 1372 } 1373 1374 if ($null_position === false || $inventory_mode_query) { 1375 $inventory_mode_where[] = dbConditionInt('hinv.inventory_mode', $inventory_mode_query); 1376 } 1377 1378 $sqlParts['where'][] = (count($inventory_mode_where) > 1) 1379 ? '('.implode(' OR ', $inventory_mode_where).')' 1380 : $inventory_mode_where[0]; 1381 } 1382 } 1383 1384 return $sqlParts; 1385 } 1386 1387 /** 1388 * Retrieves and adds additional requested data to the result set. 1389 * 1390 * @param array $options 1391 * @param array $result 1392 * 1393 * @return array 1394 */ 1395 protected function addRelatedObjects(array $options, array $result) { 1396 $result = parent::addRelatedObjects($options, $result); 1397 1398 $hostPrototypeIds = array_keys($result); 1399 1400 // adding discovery rule 1401 if ($options['selectDiscoveryRule'] !== null && $options['selectDiscoveryRule'] != API_OUTPUT_COUNT) { 1402 $relationMap = $this->createRelationMap($result, 'hostid', 'parent_itemid', 'host_discovery'); 1403 $discoveryRules = API::DiscoveryRule()->get([ 1404 'output' => $options['selectDiscoveryRule'], 1405 'itemids' => $relationMap->getRelatedIds(), 1406 'nopermissions' => true, 1407 'preservekeys' => true 1408 ]); 1409 $result = $relationMap->mapOne($result, $discoveryRules, 'discoveryRule'); 1410 } 1411 1412 // adding group links 1413 if ($options['selectGroupLinks'] !== null && $options['selectGroupLinks'] != API_OUTPUT_COUNT) { 1414 $groupPrototypes = DBFetchArray(DBselect( 1415 'SELECT hg.group_prototypeid,hg.hostid'. 1416 ' FROM group_prototype hg'. 1417 ' WHERE '.dbConditionInt('hg.hostid', $hostPrototypeIds). 1418 ' AND hg.groupid IS NOT NULL' 1419 )); 1420 $relationMap = $this->createRelationMap($groupPrototypes, 'hostid', 'group_prototypeid'); 1421 $groupPrototypes = API::getApiService()->select('group_prototype', [ 1422 'output' => $options['selectGroupLinks'], 1423 'group_prototypeids' => $relationMap->getRelatedIds(), 1424 'preservekeys' => true 1425 ]); 1426 foreach ($groupPrototypes as &$groupPrototype) { 1427 unset($groupPrototype['name']); 1428 } 1429 unset($groupPrototype); 1430 $result = $relationMap->mapMany($result, $groupPrototypes, 'groupLinks'); 1431 } 1432 1433 // adding group prototypes 1434 if ($options['selectGroupPrototypes'] !== null && $options['selectGroupPrototypes'] != API_OUTPUT_COUNT) { 1435 $groupPrototypes = DBFetchArray(DBselect( 1436 'SELECT hg.group_prototypeid,hg.hostid'. 1437 ' FROM group_prototype hg'. 1438 ' WHERE '.dbConditionInt('hg.hostid', $hostPrototypeIds). 1439 ' AND hg.groupid IS NULL' 1440 )); 1441 $relationMap = $this->createRelationMap($groupPrototypes, 'hostid', 'group_prototypeid'); 1442 $groupPrototypes = API::getApiService()->select('group_prototype', [ 1443 'output' => $options['selectGroupPrototypes'], 1444 'group_prototypeids' => $relationMap->getRelatedIds(), 1445 'preservekeys' => true 1446 ]); 1447 foreach ($groupPrototypes as &$groupPrototype) { 1448 unset($groupPrototype['groupid']); 1449 } 1450 unset($groupPrototype); 1451 $result = $relationMap->mapMany($result, $groupPrototypes, 'groupPrototypes'); 1452 } 1453 1454 // adding host 1455 if ($options['selectParentHost'] !== null && $options['selectParentHost'] != API_OUTPUT_COUNT) { 1456 $hosts = []; 1457 $relationMap = new CRelationMap(); 1458 $dbRules = DBselect( 1459 'SELECT hd.hostid,i.hostid AS parent_hostid'. 1460 ' FROM host_discovery hd,items i'. 1461 ' WHERE '.dbConditionInt('hd.hostid', $hostPrototypeIds). 1462 ' AND hd.parent_itemid=i.itemid' 1463 ); 1464 while ($relation = DBfetch($dbRules)) { 1465 $relationMap->addRelation($relation['hostid'], $relation['parent_hostid']); 1466 } 1467 1468 $related_ids = $relationMap->getRelatedIds(); 1469 1470 if ($related_ids) { 1471 $hosts = API::Host()->get([ 1472 'output' => $options['selectParentHost'], 1473 'hostids' => $related_ids, 1474 'templated_hosts' => true, 1475 'nopermissions' => true, 1476 'preservekeys' => true 1477 ]); 1478 } 1479 1480 $result = $relationMap->mapOne($result, $hosts, 'parentHost'); 1481 } 1482 1483 // adding templates 1484 if ($options['selectTemplates'] !== null) { 1485 if ($options['selectTemplates'] != API_OUTPUT_COUNT) { 1486 $templates = []; 1487 $relationMap = $this->createRelationMap($result, 'hostid', 'templateid', 'hosts_templates'); 1488 $related_ids = $relationMap->getRelatedIds(); 1489 1490 if ($related_ids) { 1491 $templates = API::Template()->get([ 1492 'output' => $options['selectTemplates'], 1493 'templateids' => $related_ids, 1494 'preservekeys' => true 1495 ]); 1496 } 1497 1498 $result = $relationMap->mapMany($result, $templates, 'templates'); 1499 } 1500 else { 1501 $templates = API::Template()->get([ 1502 'hostids' => $hostPrototypeIds, 1503 'countOutput' => true, 1504 'groupCount' => true 1505 ]); 1506 $templates = zbx_toHash($templates, 'hostid'); 1507 foreach ($result as $hostid => $host) { 1508 $result[$hostid]['templates'] = array_key_exists($hostid, $templates) 1509 ? $templates[$hostid]['rowscount'] 1510 : '0'; 1511 } 1512 } 1513 } 1514 1515 // adding tags 1516 if ($options['selectTags'] !== null && $options['selectTags'] !== API_OUTPUT_COUNT) { 1517 $tags = API::getApiService()->select('host_tag', [ 1518 'output' => $this->outputExtend($options['selectTags'], ['hostid', 'hosttagid']), 1519 'filter' => ['hostid' => $hostPrototypeIds], 1520 'preservekeys' => true 1521 ]); 1522 1523 $relation_map = $this->createRelationMap($tags, 'hostid', 'hosttagid'); 1524 $tags = $this->unsetExtraFields($tags, ['hostid', 'hosttagid'], []); 1525 $result = $relation_map->mapMany($result, $tags, 'tags'); 1526 } 1527 1528 if ($options['selectInterfaces'] !== null && $options['selectInterfaces'] != API_OUTPUT_COUNT) { 1529 $interfaces = API::HostInterface()->get([ 1530 'output' => $this->outputExtend($options['selectInterfaces'], ['hostid', 'interfaceid']), 1531 'hostids' => $hostPrototypeIds, 1532 'sortfield' => 'interfaceid', 1533 'nopermissions' => true, 1534 'preservekeys' => true 1535 ]); 1536 1537 foreach (array_keys($result) as $hostid) { 1538 $result[$hostid]['interfaces'] = []; 1539 } 1540 1541 foreach ($interfaces as $interface) { 1542 $hostid = $interface['hostid']; 1543 unset($interface['hostid'], $interface['interfaceid']); 1544 $result[$hostid]['interfaces'][] = $interface; 1545 } 1546 } 1547 1548 return $result; 1549 } 1550 1551 /** 1552 * Deletes the given group prototype and all discovered groups. 1553 * Deletes also group prototype children. 1554 * 1555 * @param array $groupPrototypeIds 1556 */ 1557 protected function deleteGroupPrototypes(array $groupPrototypeIds) { 1558 // Lock group prototypes before delete to prevent server from adding new LLD elements. 1559 DBselect( 1560 'SELECT NULL'. 1561 ' FROM group_prototype gp'. 1562 ' WHERE '.dbConditionInt('gp.group_prototypeid', $groupPrototypeIds). 1563 ' FOR UPDATE' 1564 ); 1565 1566 // delete child group prototypes 1567 $groupPrototypeChildren = DBfetchArray(DBselect( 1568 'SELECT gp.group_prototypeid FROM group_prototype gp WHERE '.dbConditionInt('templateid', $groupPrototypeIds) 1569 )); 1570 if ($groupPrototypeChildren) { 1571 $this->deleteGroupPrototypes(zbx_objectValues($groupPrototypeChildren, 'group_prototypeid')); 1572 } 1573 1574 // delete discovered groups 1575 $hostGroups = DBfetchArray(DBselect( 1576 'SELECT groupid FROM group_discovery WHERE '.dbConditionInt('parent_group_prototypeid', $groupPrototypeIds) 1577 )); 1578 if ($hostGroups) { 1579 API::HostGroup()->delete(zbx_objectValues($hostGroups, 'groupid'), true); 1580 } 1581 1582 // delete group prototypes 1583 DB::delete('group_prototype', ['group_prototypeid' => $groupPrototypeIds]); 1584 } 1585 1586 /** 1587 * Update host prototype macros, key is host prototype id and value is array of arrays with macro objects. 1588 * 1589 * @param array $update_macros Array with macros objects. 1590 */ 1591 protected function updateMacros(array $update_macros): void { 1592 $ins_hostmacros = []; 1593 $upd_hostmacros = []; 1594 1595 $db_hostmacros = DB::select('hostmacro', [ 1596 'output' => ['hostid', 'macro', 'type', 'value', 'description'], 1597 'filter' => ['hostid' => array_keys($update_macros)], 1598 'preservekeys' => true 1599 ]); 1600 $host_macros = array_fill_keys(array_keys($update_macros), []); 1601 1602 foreach ($db_hostmacros as $hostmacroid => $db_hostmacro) { 1603 $host_macros[$db_hostmacro['hostid']][$db_hostmacro['macro']] = $hostmacroid; 1604 } 1605 1606 foreach ($update_macros as $hostid => $hostmacros) { 1607 foreach ($hostmacros as $hostmacro) { 1608 if (array_key_exists($hostmacro['macro'], $host_macros[$hostid])) { 1609 $hostmacroid = $host_macros[$hostid][$hostmacro['macro']]; 1610 1611 $upd_hostmacro = DB::getUpdatedValues('hostmacro', $hostmacro, $db_hostmacro); 1612 1613 if ($upd_hostmacro) { 1614 $upd_hostmacros[] = [ 1615 'values' => $upd_hostmacro, 1616 'where' => ['hostmacroid' => $hostmacroid] 1617 ]; 1618 } 1619 1620 unset($db_hostmacros[$hostmacroid], $host_macros[$hostid][$hostmacro['macro']]); 1621 } 1622 else { 1623 $ins_hostmacros[] = ['hostid' => $hostid] + $hostmacro; 1624 } 1625 } 1626 } 1627 1628 if ($db_hostmacros) { 1629 DB::delete('hostmacro', ['hostmacroid' => array_keys($db_hostmacros)]); 1630 } 1631 1632 if ($upd_hostmacros) { 1633 DB::update('hostmacro', $upd_hostmacros); 1634 } 1635 1636 if ($ins_hostmacros) { 1637 DB::insert('hostmacro', $ins_hostmacros); 1638 } 1639 } 1640 1641 /** 1642 * Validate host prototype interfaces on create and update. 1643 * 1644 * @param array $host_prototypes Array of host prototype data. 1645 * @param array $host_prototype[]['interfaces'] Host prototype interfaces. 1646 * @param int $host_prototype[]['custom_interfaces'] Use custom or inherited interfaces. 1647 * 1648 * @throws APIException if the interfaces input is invalid. 1649 */ 1650 private function validateInterfaces(array $host_prototypes): void { 1651 foreach ($host_prototypes as $hp_idx => $host_prototype) { 1652 if ($host_prototype['custom_interfaces'] == HOST_PROT_INTERFACES_CUSTOM) { 1653 if (array_key_exists('interfaces', $host_prototype) && $host_prototype['interfaces']) { 1654 foreach ($host_prototype['interfaces'] as $if_idx => $interface) { 1655 $path = '/'.($hp_idx + 1).'/interfaces/'.($if_idx + 1); 1656 1657 if ($interface['useip'] == INTERFACE_USE_DNS) { 1658 if (!array_key_exists('dns', $interface)) { 1659 self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', $path, 1660 _s('the parameter "%1$s" is missing', 'dns') 1661 )); 1662 } 1663 elseif ($interface['dns'] === '') { 1664 self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', 1665 $path.'/dns', _('cannot be empty') 1666 )); 1667 } 1668 } 1669 else { 1670 if (!array_key_exists('ip', $interface)) { 1671 self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', $path, 1672 _s('the parameter "%1$s" is missing', 'ip') 1673 )); 1674 } 1675 elseif ($interface['ip'] === '') { 1676 self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', 1677 $path.'/ip', _('cannot be empty') 1678 )); 1679 } 1680 } 1681 1682 if ($interface['type'] == INTERFACE_TYPE_SNMP) { 1683 if (array_key_exists('details', $interface)) { 1684 if ($interface['details']['version'] == SNMP_V1 1685 || $interface['details']['version'] == SNMP_V2C) { 1686 if (!array_key_exists('community', $interface['details'])) { 1687 self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', 1688 $path.'/details', _s('the parameter "%1$s" is missing', 'community') 1689 )); 1690 } 1691 elseif ($interface['details']['community'] === '') { 1692 self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', 1693 $path.'/details/community', _('cannot be empty') 1694 )); 1695 } 1696 } 1697 } 1698 else { 1699 self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', $path, 1700 _s('the parameter "%1$s" is missing', 'details') 1701 )); 1702 } 1703 } 1704 elseif (array_key_exists('details', $interface) && $interface['details']) { 1705 self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', $path, 1706 _s('unexpected parameter "%1$s"', 'details') 1707 )); 1708 } 1709 } 1710 1711 $this->checkMainInterfaces($host_prototype, $host_prototype['interfaces']); 1712 } 1713 } 1714 elseif (array_key_exists('interfaces', $host_prototype) && $host_prototype['interfaces']) { 1715 self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', 1716 '/'.($hp_idx + 1).'/interfaces', _('should be empty') 1717 )); 1718 } 1719 } 1720 } 1721 1722 /** 1723 * Check if main interfaces are correctly set for every interface type. Each host must either have only one main 1724 * interface for each interface type, or have no interface of that type at all. 1725 * 1726 * @param array $host_prototype Host prototype object. 1727 * @param string $host_prototype['name'] Host prototype name. 1728 * @param array $interfaces All single host prototype interfaces including existing ones in DB. 1729 * @param int $interfaces[]['type'] Interface type. 1730 * @param int $interfaces[]['main'] If interface type is main. 1731 * 1732 * @throws APIException if two main or no main interfaces are given. 1733 */ 1734 private function checkMainInterfaces(array $host_prototype, array $interfaces): void { 1735 $interface_types = []; 1736 1737 foreach ($interfaces as $interface) { 1738 if (!array_key_exists($interface['type'], $interface_types)) { 1739 $interface_types[$interface['type']] = ['main' => 0, 'all' => 0]; 1740 } 1741 1742 if ($interface['main'] == INTERFACE_PRIMARY) { 1743 $interface_types[$interface['type']]['main']++; 1744 } 1745 else { 1746 $interface_types[$interface['type']]['all']++; 1747 } 1748 } 1749 1750 foreach ($interface_types as $type => $counters) { 1751 if ($counters['all'] && !$counters['main']) { 1752 self::exception(ZBX_API_ERROR_PARAMETERS, _s('No default interface for "%1$s" type on "%2$s".', 1753 hostInterfaceTypeNumToName($type), $host_prototype['name'] 1754 )); 1755 } 1756 1757 if ($counters['main'] > 1) { 1758 self::exception(ZBX_API_ERROR_PARAMETERS, 1759 _('Host prototype cannot have more than one default interface of the same type.') 1760 ); 1761 } 1762 } 1763 } 1764 1765 /** 1766 * Create host prototype interfaces. 1767 * 1768 * @param array $host_prototypes Array of host prototypes. 1769 * @param array $host_prototypes[]['hostid'] Host prototype ID. 1770 * @param array $host_prototypes[]['interfaces'] Host prototype interfaces data. 1771 */ 1772 private function createInterfaces(array $host_prototypes): void { 1773 $interfaces = []; 1774 foreach ($host_prototypes as $host_prototype) { 1775 if (array_key_exists('interfaces', $host_prototype)) { 1776 foreach ($host_prototype['interfaces'] as $interface) { 1777 $interface['hostid'] = $host_prototype['hostid']; 1778 $interfaces[] = $interface; 1779 } 1780 } 1781 } 1782 1783 if ($interfaces) { 1784 $this->createInterfacesReal($interfaces); 1785 } 1786 } 1787 1788 /** 1789 * Update host prototype interfaces. 1790 * 1791 * @param array $host_prototypes Array of host prototypes. 1792 * @param array $host_prototypes[]['hostid'] Host prototype ID. 1793 * @param array $host_prototypes[]['interfaces'] Host prototype interfaces data. 1794 * @param array $db_host_prototypes Array of host prototypes from DB. 1795 * @param array $db_host_prototypes[]['interfaces'] Host prototype interfaces data from DB. 1796 */ 1797 private function updateInterfaces(array $host_prototypes): void { 1798 // We need to get interfaces with their interfaceid's. 1799 $interfaces = API::HostInterface()->get([ 1800 'output' => ['hostid', 'interfaceid', 'type', 'useip', 'ip', 'dns', 'port', 'main', 'details'], 1801 'hostids' => array_column($host_prototypes, 'hostid'), 1802 'nopermissions' => true 1803 ]); 1804 1805 $db_host_prototype_interfaces = []; 1806 foreach($interfaces as $interface) { 1807 $db_host_prototype_interfaces[$interface['hostid']][] = $interface; 1808 } 1809 1810 $interfaces_to_create = []; 1811 $interfaces_to_update = []; 1812 $interfaceids_to_delete = []; 1813 1814 foreach ($host_prototypes as $host_prototype) { 1815 $db_interfaces = array_key_exists($host_prototype['hostid'], $db_host_prototype_interfaces) 1816 ? $db_host_prototype_interfaces[$host_prototype['hostid']] 1817 : []; 1818 1819 if ($host_prototype['custom_interfaces'] == HOST_PROT_INTERFACES_CUSTOM) { 1820 if (array_key_exists('interfaces', $host_prototype)) { 1821 if ($host_prototype['interfaces']) { 1822 CArrayHelper::sort($host_prototype['interfaces'], ['type', 'ip', 'dns']); 1823 CArrayHelper::sort($db_interfaces, ['type', 'ip', 'dns']); 1824 $host_prototype['interfaces'] = array_values($host_prototype['interfaces']); 1825 $db_interfaces = array_values($db_interfaces); 1826 1827 foreach ($host_prototype['interfaces'] as $index => $interface) { 1828 if (array_key_exists($index, $db_interfaces)) { 1829 if (!$this->compareInterface($interface, $db_interfaces[$index])) { 1830 $interface['interfaceid'] = $db_interfaces[$index]['interfaceid']; 1831 $interface['hostid'] = $host_prototype['hostid']; 1832 $interfaces_to_update[] = $interface; 1833 } 1834 1835 unset($db_interfaces[$index]); 1836 } 1837 else { 1838 // All remaining interfaces should be created. 1839 $interface['hostid'] = $host_prototype['hostid']; 1840 $interfaces_to_create[] = $interface; 1841 } 1842 } 1843 } 1844 } 1845 else { 1846 // Interfaces have not changed and should not be deleted; 1847 $db_interfaces = []; 1848 } 1849 } 1850 1851 $interfaceids_to_delete += array_flip(array_column($db_interfaces, 'interfaceid')); 1852 } 1853 1854 if ($interfaceids_to_delete) { 1855 $interfaceids_to_delete = array_flip($interfaceids_to_delete); 1856 DB::delete('interface_snmp', ['interfaceid' => $interfaceids_to_delete]); 1857 DB::delete('interface', ['interfaceid' => $interfaceids_to_delete]); 1858 } 1859 1860 if ($interfaces_to_update) { 1861 $this->updateInterfacesReal($interfaces_to_update); 1862 } 1863 1864 if ($interfaces_to_create) { 1865 $this->createInterfacesReal($interfaces_to_create); 1866 } 1867 } 1868 1869 /** 1870 * Compare two interface. Retun true if they are same, return false otherwise. 1871 * 1872 * @param array $host_interface 1873 * @param array $db_interface 1874 * 1875 * @return boolean 1876 */ 1877 private function compareInterface(array $host_interface, array $db_interface): bool { 1878 $interface_fields = ['type', 'ip', 'dns', 'useip', 'port', 'main']; 1879 $snmp_fields = ['version', 'community', 'bulk', 'securityname', 'securitylevel', 'authpassphrase', 1880 'privpassphrase', 'authprotocol', 'privprotocol', 'contextname' 1881 ]; 1882 1883 foreach ($interface_fields as $field) { 1884 if (array_key_exists($field, $db_interface) 1885 && (!array_key_exists($field, $host_interface) 1886 || $host_interface[$field] != $db_interface[$field])) { 1887 return false; 1888 } 1889 } 1890 1891 if ($db_interface['type'] == INTERFACE_TYPE_SNMP) { 1892 foreach ($snmp_fields as $field) { 1893 if (array_key_exists($field, $db_interface['details']) 1894 && (!array_key_exists($field, $host_interface['details']) 1895 || $host_interface['details'][$field] != $db_interface['details'][$field])) { 1896 return false; 1897 } 1898 } 1899 } 1900 1901 return true; 1902 } 1903 1904 /** 1905 * Insert host prototype interfaces into DB. 1906 */ 1907 private function createInterfacesReal(array $interfaces): void { 1908 $interfaceids = DB::insert('interface', $interfaces); 1909 1910 $snmp_interfaces = []; 1911 foreach ($interfaceids as $key => $id) { 1912 if ($interfaces[$key]['type'] == INTERFACE_TYPE_SNMP) { 1913 $snmp_interfaces[] = ['interfaceid' => $id] + $interfaces[$key]['details']; 1914 } 1915 } 1916 1917 $this->createSnmpInterfaceDetails($snmp_interfaces); 1918 } 1919 1920 /** 1921 * Update host prototype interfaces in DB. 1922 */ 1923 private function updateInterfacesReal(array $interfaces): void { 1924 DB::delete('interface_snmp', ['interfaceid' => array_column($interfaces, 'interfaceid')]); 1925 1926 $data = []; 1927 $snmp_interfaces = []; 1928 1929 foreach ($interfaces as $interface) { 1930 if ($interface['type'] == INTERFACE_TYPE_SNMP) { 1931 $snmp_interfaces[] = ['interfaceid' => $interface['interfaceid']] + $interface['details']; 1932 } 1933 1934 unset($interface['details']); 1935 1936 $data[] = [ 1937 'values' => $interface, 1938 'where' => ['interfaceid' => $interface['interfaceid']] 1939 ]; 1940 } 1941 1942 DB::update('interface', $data); 1943 $this->createSnmpInterfaceDetails($snmp_interfaces); 1944 } 1945 1946 /** 1947 * Create host prototype SNMP interface details. 1948 * 1949 * @param array $snmp_interfaces Array of host prototype interface details. 1950 * @param int $snmp_interfaces[]['interfaceid'] Interface id. 1951 */ 1952 private function createSnmpInterfaceDetails(array $snmp_interfaces): void { 1953 if ($snmp_interfaces) { 1954 $snmp_interfaces = $this->sanitizeSnmpFields($snmp_interfaces); 1955 DB::insert('interface_snmp', $snmp_interfaces, false); 1956 } 1957 } 1958 1959 /** 1960 * Sanitize SNMP fields by version. 1961 * 1962 * @param array $interfaces 1963 * 1964 * @return array 1965 */ 1966 private function sanitizeSnmpFields(array $interfaces): array { 1967 $default_fields = [ 1968 'community' => '', 1969 'securityname' => '', 1970 'securitylevel' => DB::getDefault('interface_snmp', 'securitylevel'), 1971 'authpassphrase' => '', 1972 'privpassphrase' => '', 1973 'authprotocol' => DB::getDefault('interface_snmp', 'authprotocol'), 1974 'privprotocol' => DB::getDefault('interface_snmp', 'privprotocol'), 1975 'contextname' => '' 1976 ]; 1977 1978 foreach ($interfaces as &$interface) { 1979 if ($interface['version'] == SNMP_V1 || $interface['version'] == SNMP_V2C) { 1980 unset($interface['securityname'], $interface['securitylevel'], $interface['authpassphrase'], 1981 $interface['privpassphrase'], $interface['authprotocol'], $interface['privprotocol'], 1982 $interface['contextname'] 1983 ); 1984 } 1985 else { 1986 unset($interface['community']); 1987 } 1988 1989 $interface = $interface + $default_fields; 1990 } 1991 1992 return $interfaces; 1993 } 1994} 1995