You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

3333 lines
117 KiB

<?php
/*
** Zabbix
** Copyright (C) 2001-2023 Zabbix SIA
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
**/
/**
* Class containing common methods for operations with triggers.
*/
abstract class CTriggerGeneral extends CApiService {
protected const FLAGS = null;
/**
* @abstract
*
* @param array $options
*
* @return array
*/
abstract public function get(array $options = []);
/**
* Prepares and returns an array of child triggers, inherited from triggers $tpl_triggers on the given hosts.
*
* @param array $tpl_triggers
* @param string $tpl_triggers[<tnum>]['triggerid']
*/
private function prepareInheritedTriggers(array $tpl_triggers, array $hostids = null, array &$ins_triggers = null,
array &$upd_triggers = null, array &$db_triggers = null) {
$ins_triggers = [];
$upd_triggers = [];
$db_triggers = [];
$result = DBselect(
'SELECT DISTINCT t.triggerid,h.hostid'.
' FROM triggers t,functions f,items i,hosts h'.
' WHERE t.triggerid=f.triggerid'.
' AND f.itemid=i.itemid'.
' AND i.hostid=h.hostid'.
' AND '.dbConditionInt('t.triggerid', zbx_objectValues($tpl_triggers, 'triggerid')).
' AND '.dbConditionInt('h.status', [HOST_STATUS_TEMPLATE])
);
$tpl_hostids_by_triggerid = [];
$tpl_hostids = [];
while ($row = DBfetch($result)) {
$tpl_hostids_by_triggerid[$row['triggerid']][] = $row['hostid'];
$tpl_hostids[$row['hostid']] = true;
}
// Unset host-level triggers.
foreach ($tpl_triggers as $tnum => $tpl_trigger) {
if (!array_key_exists($tpl_trigger['triggerid'], $tpl_hostids_by_triggerid)) {
unset($tpl_triggers[$tnum]);
}
}
if (!$tpl_triggers) {
// Nothing to inherit, just exit.
return;
}
$hosts_by_tpl_hostid = self::getLinkedHosts(array_keys($tpl_hostids), $hostids);
$chd_triggers_tpl = $this->getHostTriggersByTemplateId(array_keys($tpl_hostids_by_triggerid), $hostids);
$tpl_triggers_by_description = [];
// Preparing list of missing triggers on linked hosts.
foreach ($tpl_triggers as $tpl_trigger) {
$hostids = [];
foreach ($tpl_hostids_by_triggerid[$tpl_trigger['triggerid']] as $tpl_hostid) {
if (array_key_exists($tpl_hostid, $hosts_by_tpl_hostid)) {
foreach ($hosts_by_tpl_hostid[$tpl_hostid] as $host) {
if (array_key_exists($host['hostid'], $chd_triggers_tpl)
&& array_key_exists($tpl_trigger['triggerid'], $chd_triggers_tpl[$host['hostid']])) {
continue;
}
$hostids[$host['hostid']] = true;
}
}
}
if ($hostids) {
$tpl_triggers_by_description[$tpl_trigger['description']][] = [
'triggerid' => $tpl_trigger['triggerid'],
'expression' => $tpl_trigger['expression'],
'recovery_mode' => $tpl_trigger['recovery_mode'],
'recovery_expression' => $tpl_trigger['recovery_expression'],
'hostids' => $hostids
];
}
}
$chd_triggers_all = array_replace_recursive($chd_triggers_tpl,
$this->getHostTriggersByDescription($tpl_triggers_by_description)
);
$expression_parser = new CExpressionParser([
'usermacros' => true,
'lldmacros' => $this instanceof CTriggerPrototype
]);
$recovery_expression_parser = new CExpressionParser([
'usermacros' => true,
'lldmacros' => $this instanceof CTriggerPrototype
]);
// List of triggers to check for duplicates. Grouped by description.
$descriptions = [];
$triggerids = [];
$output = ['url_name', 'url', 'status', 'priority', 'comments', 'type', 'correlation_mode', 'correlation_tag',
'manual_close', 'opdata', 'event_name'
];
if ($this instanceof CTriggerPrototype) {
$output[] = 'discover';
}
$db_tpl_triggers = DB::select('triggers', [
'output' => $output,
'triggerids' => array_keys($tpl_hostids_by_triggerid),
'preservekeys' => true
]);
foreach ($tpl_triggers as $tpl_trigger) {
$db_tpl_trigger = $db_tpl_triggers[$tpl_trigger['triggerid']];
$tpl_hostid = $tpl_hostids_by_triggerid[$tpl_trigger['triggerid']][0];
// expression: func(/template/item) => func(/host/item)
if ($expression_parser->parse($tpl_trigger['expression']) != CParser::PARSE_SUCCESS) {
self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
'expression', $expression_parser->getError()
));
}
// recovery_expression: func(/template/item) => func(/host/item)
if ($tpl_trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
if ($recovery_expression_parser->parse($tpl_trigger['recovery_expression']) != CParser::PARSE_SUCCESS) {
self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
'recovery_expression', $recovery_expression_parser->getError()
));
}
}
$new_trigger = $tpl_trigger;
$new_trigger['uuid'] = '';
unset($new_trigger['triggerid'], $new_trigger['templateid']);
if (array_key_exists($tpl_hostid, $hosts_by_tpl_hostid)) {
foreach ($hosts_by_tpl_hostid[$tpl_hostid] as $host) {
$new_trigger['expression'] = $tpl_trigger['expression'];
$hist_functions = $expression_parser->getResult()->getTokensOfTypes(
[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
);
$hist_function = end($hist_functions);
do {
$query_parameter = $hist_function['data']['parameters'][0];
$new_trigger['expression'] = substr_replace($new_trigger['expression'],
'/'.$host['host'].'/'.$query_parameter['data']['item'], $query_parameter['pos'],
$query_parameter['length']
);
}
while ($hist_function = prev($hist_functions));
if ($tpl_trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
$new_trigger['recovery_expression'] = $tpl_trigger['recovery_expression'];
$hist_functions = $recovery_expression_parser->getResult()->getTokensOfTypes(
[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
);
$hist_function = end($hist_functions);
do {
$query_parameter = $hist_function['data']['parameters'][0];
$new_trigger['recovery_expression'] = substr_replace($new_trigger['recovery_expression'],
'/'.$host['host'].'/'.$query_parameter['data']['item'], $query_parameter['pos'],
$query_parameter['length']
);
}
while ($hist_function = prev($hist_functions));
}
if (array_key_exists($host['hostid'], $chd_triggers_all)
&& array_key_exists($tpl_trigger['triggerid'], $chd_triggers_all[$host['hostid']])) {
$chd_trigger = $chd_triggers_all[$host['hostid']][$tpl_trigger['triggerid']];
$upd_triggers[] = $new_trigger + [
'triggerid' => $chd_trigger['triggerid'],
'templateid' => $tpl_trigger['triggerid']
];
$db_triggers[$chd_trigger['triggerid']] = $chd_trigger;
$triggerids[] = $chd_trigger['triggerid'];
$check_duplicates = ($chd_trigger['description'] !== $new_trigger['description']
|| $chd_trigger['expression'] !== $new_trigger['expression']
|| $chd_trigger['recovery_expression'] !== $new_trigger['recovery_expression']);
if ($check_duplicates) {
$descriptions[$new_trigger['description']][] = [
'expression' => $new_trigger['expression'],
'recovery_expression' => $new_trigger['recovery_expression'],
'host' => [
'hostid' => $host['hostid'],
'status' => $host['status']
]
];
}
}
else {
$ins_triggers[] = $new_trigger + $db_tpl_trigger + ['templateid' => $tpl_trigger['triggerid']];
}
}
}
}
if ($triggerids) {
// Add trigger tags.
$result = DBselect(
'SELECT tt.triggertagid,tt.triggerid,tt.tag,tt.value'.
' FROM trigger_tag tt'.
' WHERE '.dbConditionInt('tt.triggerid', $triggerids)
);
$trigger_tags = [];
while ($row = DBfetch($result)) {
$trigger_tags[$row['triggerid']][] = [
'triggertagid' => $row['triggertagid'],
'tag' => $row['tag'],
'value' => $row['value']
];
}
foreach ($db_triggers as &$db_trigger) {
$db_trigger['tags'] = array_key_exists($db_trigger['triggerid'], $trigger_tags)
? $trigger_tags[$db_trigger['triggerid']]
: [];
}
unset($db_trigger);
// Add discovery rule IDs.
if ($this instanceof CTriggerPrototype) {
$result = DBselect(
'SELECT id.parent_itemid,f.triggerid'.
' FROM item_discovery id,functions f'.
' WHERE '.dbConditionInt('f.triggerid', $triggerids).
' AND f.itemid=id.itemid'
);
$drule_by_triggerid = [];
while ($row = DBfetch($result)) {
$drule_by_triggerid[$row['triggerid']] = $row['parent_itemid'];
}
foreach ($db_triggers as &$db_trigger) {
$db_trigger['discoveryRule']['itemid'] = $drule_by_triggerid[$db_trigger['triggerid']];
}
unset($db_trigger);
}
}
$this->checkDuplicates($descriptions);
}
/**
* Returns list of linked hosts.
*
* Output format:
* [
* <tpl_hostid> => [
* [
* 'hostid' => <hostid>,
* 'host' => <host>
* ],
* ...
* ],
* ...
* ]
*
* @param array $tpl_hostids
* @param array $hostids The function will return a list of all linked hosts if no hostids are specified.
*
* @return array
*/
private static function getLinkedHosts(array $tpl_hostids, array $hostids = null) {
// Fetch all child hosts and templates
$sql = 'SELECT ht.hostid,ht.templateid,h.host,h.status'.
' FROM hosts_templates ht,hosts h'.
' WHERE ht.hostid=h.hostid'.
' AND '.dbConditionInt('ht.templateid', $tpl_hostids).
' AND '.dbConditionInt('h.flags', [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_CREATED]);
if ($hostids !== null) {
$sql .= ' AND '.dbConditionInt('ht.hostid', $hostids);
}
$result = DBselect($sql);
$hosts_by_tpl_hostid = [];
while ($row = DBfetch($result)) {
$hosts_by_tpl_hostid[$row['templateid']][] = [
'hostid' => $row['hostid'],
'host' => $row['host'],
'status' => $row['status']
];
}
return $hosts_by_tpl_hostid;
}
/**
* Returns list of already linked triggers.
*
* Output format:
* [
* <hostid> => [
* <tpl_triggerid> => ['triggerid' => <triggerid>],
* ...
* ],
* ...
* ]
*
* @param array $tpl_triggerids
* @param array $hostids The function will return a list of all linked triggers if no hosts are specified.
*
* @return array
*/
private function getHostTriggersByTemplateId(array $tpl_triggerids, array $hostids = null) {
$output = 't.triggerid,t.expression,t.description,t.url_name,t.url,t.status,t.priority,t.comments,t.type,'.
't.recovery_mode,t.recovery_expression,t.correlation_mode,t.correlation_tag,t.manual_close,t.opdata,'.
't.templateid,t.event_name,i.hostid';
if ($this instanceof CTriggerPrototype) {
$output .= ',t.discover';
}
// Preparing list of triggers by templateid.
$sql = 'SELECT DISTINCT '.$output.
' FROM triggers t,functions f,items i'.
' WHERE t.triggerid=f.triggerid'.
' AND f.itemid=i.itemid'.
' AND '.dbConditionInt('t.templateid', $tpl_triggerids);
if ($hostids !== null) {
$sql .= ' AND '.dbConditionInt('i.hostid', $hostids);
}
$chd_triggers = DBfetchArray(DBselect($sql));
$chd_triggers = CMacrosResolverHelper::resolveTriggerExpressions($chd_triggers,
['sources' => ['expression', 'recovery_expression']]
);
$chd_triggers_tpl = [];
foreach ($chd_triggers as $chd_trigger) {
$hostid = $chd_trigger['hostid'];
unset($chd_trigger['hostid']);
$chd_triggers_tpl[$hostid][$chd_trigger['templateid']] = $chd_trigger;
}
return $chd_triggers_tpl;
}
/**
* Returns list of not inherited triggers with same name and expression.
*
* Output format:
* [
* <hostid> => [
* <tpl_triggerid> => ['triggerid' => <triggerid>],
* ...
* ],
* ...
* ]
*
* @param array $tpl_triggers_by_description The list of hostids, grouped by trigger description and expression.
*
* @return array
*/
private function getHostTriggersByDescription(array $tpl_triggers_by_description) {
$chd_triggers_description = [];
$expression_parser = new CExpressionParser([
'usermacros' => true,
'lldmacros' => $this instanceof CTriggerPrototype
]);
$recovery_expression_parser = new CExpressionParser([
'usermacros' => true,
'lldmacros' => $this instanceof CTriggerPrototype
]);
$output = 't.triggerid,t.expression,t.description,t.url_name,t.url,t.status,t.priority,t.comments,t.type,'.
't.recovery_mode,t.recovery_expression,t.correlation_mode,t.correlation_tag,t.manual_close,t.opdata,'.
't.event_name,i.hostid,h.host';
if ($this instanceof CTriggerPrototype) {
$output .= ',t.discover';
}
foreach ($tpl_triggers_by_description as $description => $tpl_triggers) {
$hostids = [];
foreach ($tpl_triggers as $tpl_trigger) {
$hostids += $tpl_trigger['hostids'];
}
$chd_triggers = DBfetchArray(DBselect(
'SELECT DISTINCT '.$output.
' FROM triggers t,functions f,items i,hosts h'.
' WHERE t.triggerid=f.triggerid'.
' AND f.itemid=i.itemid'.
' AND i.hostid=h.hostid'.
' AND '.dbConditionString('t.description', [$description]).
' AND '.dbConditionInt('i.hostid', array_keys($hostids))
));
$chd_triggers = CMacrosResolverHelper::resolveTriggerExpressions($chd_triggers,
['sources' => ['expression', 'recovery_expression']]
);
foreach ($tpl_triggers as $tpl_trigger) {
// expression: func(/template/item) => func(/host/item)
if ($expression_parser->parse($tpl_trigger['expression']) != CParser::PARSE_SUCCESS) {
self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
'expression', $expression_parser->getError()
));
}
// recovery_expression: func(/template/item) => func(/host/item)
if ($tpl_trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
if ($recovery_expression_parser->parse($tpl_trigger['recovery_expression']) !=
CParser::PARSE_SUCCESS) {
self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
'recovery_expression', $recovery_expression_parser->getError()
));
}
}
foreach ($chd_triggers as $chd_trigger) {
if (!array_key_exists($chd_trigger['hostid'], $tpl_trigger['hostids'])) {
continue;
}
if ($chd_trigger['recovery_mode'] != $tpl_trigger['recovery_mode']) {
continue;
}
// Replace template name in /host/key reference to target host name.
$expression = $tpl_trigger['expression'];
$hist_functions = $expression_parser->getResult()->getTokensOfTypes(
[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
);
$hist_function = end($hist_functions);
do {
$query_parameter = $hist_function['data']['parameters'][0];
$expression = substr_replace($expression,
'/'.$chd_trigger['host'].'/'.$query_parameter['data']['item'], $query_parameter['pos'],
$query_parameter['length']
);
}
while ($hist_function = prev($hist_functions));
if ($chd_trigger['expression'] !== $expression) {
continue;
}
if ($tpl_trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
$recovery_expression = $tpl_trigger['recovery_expression'];
$hist_functions = $recovery_expression_parser->getResult()->getTokensOfTypes(
[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
);
$hist_function = end($hist_functions);
do {
$query_parameter = $hist_function['data']['parameters'][0];
$recovery_expression = substr_replace($recovery_expression,
'/'.$chd_trigger['host'].'/'.$query_parameter['data']['item'], $query_parameter['pos'],
$query_parameter['length']
);
}
while ($hist_function = prev($hist_functions));
if ($chd_trigger['recovery_expression'] !== $recovery_expression) {
continue;
}
}
$hostid = $chd_trigger['hostid'];
unset($chd_trigger['hostid'], $chd_trigger['host']);
$chd_triggers_description[$hostid][$tpl_trigger['triggerid']] = $chd_trigger + ['templateid' => 0];
}
}
}
return $chd_triggers_description;
}
/**
* Updates children of triggers on the given hosts and propagates the inheritance to all child hosts.
* All of the child triggers that became obsolete will be deleted if the given triggers were assigned to a different
* template or host.
*
* @param array $triggers
* @param string $triggers[]['triggerid']
* @param string $triggers[]['description']
* @param string $triggers[]['expression']
* @param int $triggers[]['recovery mode']
* @param string $triggers[]['recovery_expression']
* @param array $hostids
*/
protected function inherit(array $triggers, array $hostids = null) {
$this->prepareInheritedTriggers($triggers, $hostids, $ins_triggers, $upd_triggers, $db_triggers);
if ($ins_triggers) {
$this->createReal($ins_triggers, true);
}
if ($upd_triggers) {
$this->updateReal($upd_triggers, $db_triggers, true);
}
if ($ins_triggers || $upd_triggers) {
$this->inherit(array_merge($ins_triggers + $upd_triggers));
}
}
/**
* Populate an array by "hostid" keys.
*
* @param array $descriptions
* @param string $descriptions[<description>][]['expression']
*
* @throws APIException If host or template does not exists.
*
* @return array
*/
protected function populateHostIds($descriptions) {
$expression_parser = new CExpressionParser([
'usermacros' => true,
'lldmacros' => $this instanceof CTriggerPrototype
]);
$hosts = [];
foreach ($descriptions as $description => $triggers) {
foreach ($triggers as $index => $trigger) {
$expression_parser->parse($trigger['expression']);
$hosts[$expression_parser->getResult()->getHosts()[0]][$description][] = $index;
}
}
$db_hosts = DBselect(
'SELECT h.hostid,h.host,h.status'.
' FROM hosts h'.
' WHERE '.dbConditionString('h.host', array_keys($hosts)).
' AND '.dbConditionInt('h.status',
[HOST_STATUS_MONITORED, HOST_STATUS_NOT_MONITORED, HOST_STATUS_TEMPLATE]
)
);
while ($db_host = DBfetch($db_hosts)) {
foreach ($hosts[$db_host['host']] as $description => $indexes) {
foreach ($indexes as $index) {
$descriptions[$description][$index]['host'] = [
'hostid' => $db_host['hostid'],
'status' => $db_host['status']
];
}
}
unset($hosts[$db_host['host']]);
}
if ($hosts) {
$error_wrong_host = ($this instanceof CTrigger)
? _('Incorrect trigger expression. Host "%1$s" does not exist or you have no access to this host.')
: _('Incorrect trigger prototype expression. Host "%1$s" does not exist or you have no access to this host.');
self::exception(ZBX_API_ERROR_PARAMETERS, _params($error_wrong_host, [key($hosts)]));
}
return $descriptions;
}
/**
* @param array $triggers
* @param array $descriptions
*
* @throws APIException
*/
private static function validateUuid(array $triggers, array $descriptions): void {
foreach ($descriptions as $_triggers) {
foreach ($_triggers as $_trigger) {
$triggers[$_trigger['index']]['host_status'] = $_trigger['host']['status'];
}
}
$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_ALLOW_UNEXPECTED, 'uniq' => [['uuid']], 'fields' => [
'host_status' => ['type' => API_ANY],
'uuid' => ['type' => API_MULTIPLE, 'rules' => [
['if' => ['field' => 'host_status', 'in' => HOST_STATUS_TEMPLATE], 'type' => API_UUID],
['else' => true, 'type' => API_STRING_UTF8, 'in' => DB::getDefault('triggers', 'uuid'), 'unset' => true]
]]
]];
if (!CApiInputValidator::validate($api_input_rules, $triggers, '/', $error)) {
self::exception(ZBX_API_ERROR_PARAMETERS, $error);
}
}
/**
* Add the UUID to those of the given triggers that belong to a template and don't have the 'uuid' parameter set.
*
* @param array $triggers
* @param array $descriptions
*/
private static function addUuid(array &$triggers, array $descriptions): void {
foreach ($descriptions as $_triggers) {
foreach ($_triggers as $_trigger) {
if ($_trigger['host']['status'] == HOST_STATUS_TEMPLATE
&& !array_key_exists('uuid', $triggers[$_trigger['index']])) {
$triggers[$_trigger['index']]['uuid'] = generateUuidV4();
}
}
}
}
/**
* Verify trigger UUIDs are not repeated.
*
* @param array $triggers
* @param array|null $db_triggers
*
* @throws APIException
*/
private static function checkUuidDuplicates(array $triggers, array $db_triggers = null): void {
$trigger_indexes = [];
foreach ($triggers as $i => $trigger) {
if (!array_key_exists('uuid', $trigger) || $trigger['uuid'] === '') {
continue;
}
if ($db_triggers === null || $trigger['uuid'] !== $db_triggers[$trigger['triggerid']]['uuid']) {
$trigger_indexes[$trigger['uuid']] = $i;
}
}
if (!$trigger_indexes) {
return;
}
$duplicates = DB::select('triggers', [
'output' => ['uuid'],
'filter' => [
'flags' => static::FLAGS,
'uuid' => array_keys($trigger_indexes)
],
'limit' => 1
]);
if ($duplicates) {
switch (static::FLAGS) {
case ZBX_FLAG_DISCOVERY_NORMAL:
$error = _s('Invalid parameter "%1$s": %2$s.', '/'.($trigger_indexes[$duplicates[0]['uuid']] + 1),
_('trigger with the same UUID already exists')
);
break;
case ZBX_FLAG_DISCOVERY_PROTOTYPE:
$error = _s('Invalid parameter "%1$s": %2$s.', '/'.($trigger_indexes[$duplicates[0]['uuid']] + 1),
_('trigger prototype with the same UUID already exists')
);
break;
}
self::exception(ZBX_API_ERROR_PARAMETERS, $error);
}
}
/**
* Checks triggers for duplicates.
*
* @param array $descriptions
* @param string $descriptions[<description>][]['expression']
* @param string $descriptions[<description>][]['recovery_expression']
* @param string $descriptions[<description>][]['hostid']
*
* @throws APIException if at least one trigger exists
*/
protected function checkDuplicates(array $descriptions) {
foreach ($descriptions as $description => $triggers) {
$hostids = [];
$expressions = [];
foreach ($triggers as $trigger) {
if (array_key_exists('name_updated', $trigger) && !$trigger['name_updated']) {
continue;
}
$hostids[$trigger['host']['hostid']] = true;
$expressions[$trigger['expression']][$trigger['recovery_expression']] = $trigger['host']['hostid'];
}
if (!$hostids) {
continue;
}
$db_triggers = DBfetchArray(DBselect(
'SELECT DISTINCT t.expression,t.recovery_expression'.
' FROM triggers t,functions f,items i,hosts h'.
' WHERE t.triggerid=f.triggerid'.
' AND f.itemid=i.itemid'.
' AND i.hostid=h.hostid'.
' AND '.dbConditionString('t.description', [$description]).
' AND '.dbConditionInt('i.hostid', array_keys($hostids))
));
$db_triggers = CMacrosResolverHelper::resolveTriggerExpressions($db_triggers,
['sources' => ['expression', 'recovery_expression']]
);
foreach ($db_triggers as $db_trigger) {
$expression = $db_trigger['expression'];
$recovery_expression = $db_trigger['recovery_expression'];
if (array_key_exists($expression, $expressions)
&& array_key_exists($recovery_expression, $expressions[$expression])) {
$error_already_exists = ($this instanceof CTrigger)
? _('Trigger "%1$s" already exists on "%2$s".')
: _('Trigger prototype "%1$s" already exists on "%2$s".');
$db_hosts = DB::select('hosts', [
'output' => ['name'],
'hostids' => $expressions[$expression][$recovery_expression]
]);
self::exception(ZBX_API_ERROR_PARAMETERS,
_params($error_already_exists, [$description, $db_hosts[0]['name']])
);
}
}
}
}
protected function addRelatedObjects(array $options, array $result) {
$result = parent::addRelatedObjects($options, $result);
$triggerids = array_keys($result);
// adding groups
$this->addRelatedGroups($options, $result, 'selectGroups');
$this->addRelatedGroups($options, $result, 'selectHostGroups');
$this->addRelatedGroups($options, $result, 'selectTemplateGroups');
// adding hosts
if ($options['selectHosts'] !== null && $options['selectHosts'] != API_OUTPUT_COUNT) {
$res = DBselect(
'SELECT f.triggerid,i.hostid'.
' FROM functions f,items i'.
' WHERE '.dbConditionInt('f.triggerid', $triggerids).
' AND f.itemid=i.itemid'
);
$relationMap = new CRelationMap();
while ($relation = DBfetch($res)) {
$relationMap->addRelation($relation['triggerid'], $relation['hostid']);
}
$hosts = API::Host()->get([
'output' => $options['selectHosts'],
'hostids' => $relationMap->getRelatedIds(),
'templated_hosts' => true,
'nopermissions' => true,
'preservekeys' => true
]);
if (!is_null($options['limitSelects'])) {
order_result($hosts, 'host');
}
$result = $relationMap->mapMany($result, $hosts, 'hosts', $options['limitSelects']);
}
// adding functions
if ($options['selectFunctions'] !== null && $options['selectFunctions'] != API_OUTPUT_COUNT) {
$functions = API::getApiService()->select('functions', [
'output' => $this->outputExtend($options['selectFunctions'], ['triggerid', 'functionid']),
'filter' => ['triggerid' => $triggerids],
'preservekeys' => true
]);
// Rename column 'name' to 'function'.
$function = reset($functions);
if ($function && array_key_exists('name', $function)) {
$functions = CArrayHelper::renameObjectsKeys($functions, ['name' => 'function']);
}
$relationMap = $this->createRelationMap($functions, 'triggerid', 'functionid');
$functions = $this->unsetExtraFields($functions, ['triggerid', 'functionid'], $options['selectFunctions']);
$result = $relationMap->mapMany($result, $functions, 'functions');
}
// Adding trigger tags.
if ($options['selectTags'] !== null && $options['selectTags'] != API_OUTPUT_COUNT) {
$tags = API::getApiService()->select('trigger_tag', [
'output' => $this->outputExtend($options['selectTags'], ['triggerid']),
'filter' => ['triggerid' => $triggerids],
'preservekeys' => true
]);
$relationMap = $this->createRelationMap($tags, 'triggerid', 'triggertagid');
$tags = $this->unsetExtraFields($tags, ['triggertagid', 'triggerid'], []);
$result = $relationMap->mapMany($result, $tags, 'tags');
}
return $result;
}
/**
* Adds related host or template groups requested by "select*" options to the resulting object set.
*
* @param array $options [IN] Original input options.
* @param array $result [IN/OUT] Result output.
* @param string $option [IN] Possible values:
* - "selectGroups" (deprecated);
* - "selectHostGroups";
* - "selectTemplateGroups".
*/
private function addRelatedGroups(array $options, array &$result, string $option): void {
if ($options[$option] === null || $options[$option] === API_OUTPUT_COUNT) {
return;
}
$res = DBselect(
'SELECT f.triggerid,hg.groupid'.
' FROM functions f,items i,hosts_groups hg'.
' WHERE '.dbConditionInt('f.triggerid', array_keys($result)).
' AND f.itemid=i.itemid'.
' AND i.hostid=hg.hostid'
);
$relationMap = new CRelationMap();
while ($relation = DBfetch($res)) {
$relationMap->addRelation($relation['triggerid'], $relation['groupid']);
}
switch ($option) {
case 'selectGroups':
$output_tag = 'groups';
$entities = [API::HostGroup(), API::TemplateGroup()];
break;
case 'selectHostGroups':
$entities = [API::HostGroup()];
$output_tag = 'hostgroups';
break;
case 'selectTemplateGroups':
$entities = [API::TemplateGroup()];
$output_tag = 'templategroups';
break;
}
$groups = [];
foreach ($entities as $entity) {
$groups += $entity->get([
'output' => $options[$option],
'groupids' => $relationMap->getRelatedIds(),
'preservekeys' => true
]);
}
$result = $relationMap->mapMany($result, $groups, $output_tag);
}
/**
* Validate integrity of trigger recovery properties.
*
* @static
*
* @param array $trigger
* @param int $trigger['recovery_mode']
* @param string $trigger['recovery_expression']
*
* @throws APIException if validation failed.
*/
private static function checkTriggerRecoveryMode(array $trigger) {
if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
if ($trigger['recovery_expression'] === '') {
self::exception(ZBX_API_ERROR_PARAMETERS,
_s('Incorrect value for field "%1$s": %2$s.', 'recovery_expression', _('cannot be empty'))
);
}
}
elseif ($trigger['recovery_expression'] !== '') {
self::exception(ZBX_API_ERROR_PARAMETERS,
_s('Incorrect value for field "%1$s": %2$s.', 'recovery_expression', _('should be empty'))
);
}
}
/**
* Validate trigger correlation mode and related properties.
*
* @static
*
* @param array $trigger
* @param int $trigger['correlation_mode']
* @param string $trigger['correlation_tag']
* @param int $trigger['recovery_mode']
*
* @throws APIException if validation failed.
*/
private static function checkTriggerCorrelationMode(array $trigger) {
if ($trigger['correlation_mode'] == ZBX_TRIGGER_CORRELATION_TAG) {
if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_NONE) {
self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
'correlation_mode', _s('unexpected value "%1$s"', $trigger['correlation_mode'])
));
}
if ($trigger['correlation_tag'] === '') {
self::exception(ZBX_API_ERROR_PARAMETERS,
_s('Incorrect value for field "%1$s": %2$s.', 'correlation_tag', _('cannot be empty'))
);
}
}
elseif ($trigger['correlation_tag'] !== '') {
self::exception(ZBX_API_ERROR_PARAMETERS,
_s('Incorrect value for field "%1$s": %2$s.', 'correlation_tag', _('should be empty'))
);
}
}
/**
* Validate trigger to be created.
*
* @param array $triggers [IN/OUT]
* @param array $triggers[]['description'] [IN]
* @param string $triggers[]['expression'] [IN]
* @param string $triggers[]['opdata'] [IN]
* @param string $triggers[]['event_name'] [IN]
* @param string $triggers[]['comments'] [IN] (optional)
* @param int $triggers[]['priority'] [IN] (optional)
* @param int $triggers[]['status'] [IN] (optional)
* @param int $triggers[]['type'] [IN] (optional)
* @param string $triggers[]['url_name'] [IN] (optional)
* @param string $triggers[]['url'] [IN] (optional)
* @param int $triggers[]['recovery_mode'] [IN/OUT] (optional)
* @param string $triggers[]['recovery_expression'] [IN/OUT] (optional)
* @param int $triggers[]['correlation_mode'] [IN/OUT] (optional)
* @param string $triggers[]['correlation_tag'] [IN/OUT] (optional)
* @param int $triggers[]['manual_close'] [IN] (optional)
* @param int $triggers[]['discover'] [IN] (optional) for trigger prototypes only
* @param array $triggers[]['tags'] [IN] (optional)
* @param string $triggers[]['tags'][]['tag'] [IN]
* @param string $triggers[]['tags'][]['value'] [IN/OUT] (optional)
* @param array $triggers[]['dependencies'] [IN] (optional)
* @param string $triggers[]['dependencies'][]['triggerid'] [IN]
*
* @throws APIException if validation failed.
*/
protected function validateCreate(array &$triggers) {
$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['description', 'expression']], 'fields' => [
'uuid' => ['type' => API_ANY],
'description' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('triggers', 'description')],
'expression' => ['type' => API_TRIGGER_EXPRESSION, 'flags' => API_REQUIRED | API_NOT_EMPTY | API_ALLOW_LLD_MACRO],
'event_name' => ['type' => API_EVENT_NAME, 'length' => DB::getFieldLength('triggers', 'event_name')],
'opdata' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'opdata')],
'comments' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'comments')],
'priority' => ['type' => API_INT32, 'in' => implode(',', range(TRIGGER_SEVERITY_NOT_CLASSIFIED, TRIGGER_SEVERITY_COUNT - 1))],
'status' => ['type' => API_INT32, 'in' => implode(',', [TRIGGER_STATUS_ENABLED, TRIGGER_STATUS_DISABLED])],
'type' => ['type' => API_INT32, 'in' => implode(',', [TRIGGER_MULT_EVENT_DISABLED, TRIGGER_MULT_EVENT_ENABLED])],
'url_name' => ['type' => API_STRING_UTF8, 'flags' => API_ALLOW_USER_MACRO, 'length' => DB::getFieldLength('triggers', 'url_name')],
'url' => ['type' => API_URL, 'flags' => API_ALLOW_USER_MACRO, 'length' => DB::getFieldLength('triggers', 'url')],
'recovery_mode' => ['type' => API_INT32, 'in' => implode(',', [ZBX_RECOVERY_MODE_EXPRESSION, ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION, ZBX_RECOVERY_MODE_NONE]), 'default' => DB::getDefault('triggers', 'recovery_mode')],
'recovery_expression' => ['type' => API_TRIGGER_EXPRESSION, 'flags' => API_ALLOW_LLD_MACRO, 'default' => DB::getDefault('triggers', 'recovery_expression')],
'correlation_mode' => ['type' => API_INT32, 'in' => implode(',', [ZBX_TRIGGER_CORRELATION_NONE, ZBX_TRIGGER_CORRELATION_TAG]), 'default' => DB::getDefault('triggers', 'correlation_mode')],
'correlation_tag' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'correlation_tag'), 'default' => DB::getDefault('triggers', 'correlation_tag')],
'manual_close' => ['type' => API_INT32, 'in' => implode(',', [ZBX_TRIGGER_MANUAL_CLOSE_NOT_ALLOWED, ZBX_TRIGGER_MANUAL_CLOSE_ALLOWED])],
'tags' => ['type' => API_OBJECTS, 'uniq' => [['tag', 'value']], 'fields' => [
'tag' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('trigger_tag', 'tag')],
'value' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('trigger_tag', 'value'), 'default' => DB::getDefault('trigger_tag', 'value')]
]],
'dependencies' => ['type' => API_OBJECTS, 'uniq' => [['triggerid']], 'fields'=> [
'triggerid' => ['type' => API_ID, 'flags' => API_REQUIRED]
]]
]];
if ($this instanceof CTriggerPrototype) {
$api_input_rules['fields']['discover'] = ['type' => API_INT32, 'in' => implode(',', [TRIGGER_DISCOVER, TRIGGER_NO_DISCOVER])];
}
else {
$api_input_rules['fields']['expression']['flags'] &= ~API_ALLOW_LLD_MACRO;
$api_input_rules['fields']['recovery_expression']['flags'] &= ~API_ALLOW_LLD_MACRO;
}
if (!CApiInputValidator::validate($api_input_rules, $triggers, '/', $error)) {
self::exception(ZBX_API_ERROR_PARAMETERS, $error);
}
$descriptions = [];
foreach ($triggers as $index => $trigger) {
self::checkTriggerRecoveryMode($trigger);
self::checkTriggerCorrelationMode($trigger);
$descriptions[$trigger['description']][] = [
'index' => $index,
'expression' => $trigger['expression'],
'recovery_expression' => $trigger['recovery_expression']
];
}
$descriptions = $this->populateHostIds($descriptions);
self::validateUuid($triggers, $descriptions);
self::addUuid($triggers, $descriptions);
self::checkUuidDuplicates($triggers);
$this->checkDuplicates($descriptions);
$this->checkDependencies($triggers);
}
/**
* Validate trigger to be updated.
*
* @param array $triggers [IN/OUT]
* @param array $triggers[]['triggerid'] [IN]
* @param array $triggers[]['description'] [IN/OUT] (optional)
* @param string $triggers[]['expression'] [IN/OUT] (optional)
* @param string $triggers[]['event_name'] [IN] (optional)
* @param string $triggers[]['opdata'] [IN] (optional)
* @param string $triggers[]['comments'] [IN] (optional)
* @param int $triggers[]['priority'] [IN] (optional)
* @param int $triggers[]['status'] [IN] (optional)
* @param int $triggers[]['type'] [IN] (optional)
* @param string $triggers[]['url_name'] [IN] (optional)
* @param string $triggers[]['url'] [IN] (optional)
* @param int $triggers[]['recovery_mode'] [IN/OUT] (optional)
* @param string $triggers[]['recovery_expression'] [IN/OUT] (optional)
* @param int $triggers[]['correlation_mode'] [IN/OUT] (optional)
* @param string $triggers[]['correlation_tag'] [IN/OUT] (optional)
* @param int $triggers[]['manual_close'] [IN] (optional)
* @param int $triggers[]['discover'] [IN] (optional) for trigger prototypes only
* @param array $triggers[]['tags'] [IN] (optional)
* @param string $triggers[]['tags'][]['tag'] [IN]
* @param string $triggers[]['tags'][]['value'] [IN/OUT] (optional)
* @param array $triggers[]['dependencies'] [IN] (optional)
* @param string $triggers[]['dependencies'][]['triggerid'] [IN]
* @param array $db_triggers [OUT]
* @param array $db_triggers[<tnum>]['triggerid'] [OUT]
* @param array $db_triggers[<tnum>]['description'] [OUT]
* @param string $db_triggers[<tnum>]['expression'] [OUT]
* @param string $db_triggers[<tnum>]['event_name'] [OUT]
* @param string $db_triggers[<tnum>]['opdata'] [OUT]
* @param int $db_triggers[<tnum>]['recovery_mode'] [OUT]
* @param string $db_triggers[<tnum>]['recovery_expression'] [OUT]
* @param string $db_triggers[<tnum>]['url_name'] [OUT]
* @param string $db_triggers[<tnum>]['url'] [OUT]
* @param int $db_triggers[<tnum>]['status'] [OUT]
* @param int $db_triggers[<tnum>]['discover'] [OUT]
* @param int $db_triggers[<tnum>]['priority'] [OUT]
* @param string $db_triggers[<tnum>]['comments'] [OUT]
* @param int $db_triggers[<tnum>]['type'] [OUT]
* @param string $db_triggers[<tnum>]['templateid'] [OUT]
* @param int $db_triggers[<tnum>]['correlation_mode'] [OUT]
* @param string $db_triggers[<tnum>]['correlation_tag'] [OUT]
* @param int $db_triggers[<tnum>]['discover'] [OUT] for trigger prototypes only
*
* @throws APIException if validation failed.
*/
protected function validateUpdate(array &$triggers, array &$db_triggers = null) {
$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['description', 'expression']], 'fields' => [
'uuid' => ['type' => API_ANY],
'triggerid' => ['type' => API_ID, 'flags' => API_REQUIRED],
'description' => ['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('triggers', 'description')],
'expression' => ['type' => API_TRIGGER_EXPRESSION, 'flags' => API_NOT_EMPTY | API_ALLOW_LLD_MACRO],
'event_name' => ['type' => API_EVENT_NAME, 'length' => DB::getFieldLength('triggers', 'event_name')],
'opdata' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'opdata')],
'comments' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'comments')],
'priority' => ['type' => API_INT32, 'in' => implode(',', range(TRIGGER_SEVERITY_NOT_CLASSIFIED, TRIGGER_SEVERITY_COUNT - 1))],
'status' => ['type' => API_INT32, 'in' => implode(',', [TRIGGER_STATUS_ENABLED, TRIGGER_STATUS_DISABLED])],
'type' => ['type' => API_INT32, 'in' => implode(',', [TRIGGER_MULT_EVENT_DISABLED, TRIGGER_MULT_EVENT_ENABLED])],
'url_name' => ['type' => API_STRING_UTF8, 'flags' => API_ALLOW_USER_MACRO, 'length' => DB::getFieldLength('triggers', 'url_name')],
'url' => ['type' => API_URL, 'flags' => API_ALLOW_USER_MACRO, 'length' => DB::getFieldLength('triggers', 'url')],
'recovery_mode' => ['type' => API_INT32, 'in' => implode(',', [ZBX_RECOVERY_MODE_EXPRESSION, ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION, ZBX_RECOVERY_MODE_NONE])],
'recovery_expression' => ['type' => API_TRIGGER_EXPRESSION, 'flags' => API_ALLOW_LLD_MACRO],
'correlation_mode' => ['type' => API_INT32, 'in' => implode(',', [ZBX_TRIGGER_CORRELATION_NONE, ZBX_TRIGGER_CORRELATION_TAG])],
'correlation_tag' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'correlation_tag')],
'manual_close' => ['type' => API_INT32, 'in' => implode(',', [ZBX_TRIGGER_MANUAL_CLOSE_NOT_ALLOWED, ZBX_TRIGGER_MANUAL_CLOSE_ALLOWED])],
'tags' => ['type' => API_OBJECTS, 'uniq' => [['tag', 'value']], 'fields' => [
'tag' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('trigger_tag', 'tag')],
'value' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('trigger_tag', 'value'), 'default' => DB::getDefault('trigger_tag', 'value')]
]],
'dependencies' => ['type' => API_OBJECTS, 'uniq' => [['triggerid']], 'fields'=> [
'triggerid' => ['type' => API_ID, 'flags' => API_REQUIRED]
]]
]];
if ($this instanceof CTriggerPrototype) {
$api_input_rules['fields']['discover'] = ['type' => API_INT32, 'in' => implode(',', [TRIGGER_DISCOVER, TRIGGER_NO_DISCOVER])];
}
else {
$api_input_rules['fields']['expression']['flags'] &= ~API_ALLOW_LLD_MACRO;
$api_input_rules['fields']['recovery_expression']['flags'] &= ~API_ALLOW_LLD_MACRO;
}
if (!CApiInputValidator::validate($api_input_rules, $triggers, '/', $error)) {
self::exception(ZBX_API_ERROR_PARAMETERS, $error);
}
$options = [
'output' => ['uuid', 'triggerid', 'description', 'expression', 'url_name', 'url', 'status', 'priority',
'comments', 'type', 'templateid', 'recovery_mode', 'recovery_expression', 'correlation_mode',
'correlation_tag', 'manual_close', 'opdata', 'event_name'
],
'triggerids' => zbx_objectValues($triggers, 'triggerid'),
'editable' => true,
'preservekeys' => true
];
$class = get_class($this);
switch ($class) {
case 'CTrigger':
$error_cannot_update = _('Cannot update "%1$s" for templated trigger "%2$s".');
$options['output'][] = 'flags';
// Discovered fields, except status, cannot be updated.
$update_discovered_validator = new CUpdateDiscoveredValidator([
'allowed' => ['triggerid', 'status'],
'messageAllowedField' => _('Cannot update "%2$s" for a discovered trigger "%1$s".')
]);
break;
case 'CTriggerPrototype':
$error_cannot_update = _('Cannot update "%1$s" for templated trigger prototype "%2$s".');
$options['output'][] = 'discover';
$options['selectDiscoveryRule'] = ['itemid'];
break;
default:
self::exception(ZBX_API_ERROR_INTERNAL, _('Internal error.'));
}
$db_triggers = $this->get($options);
if (count($db_triggers) != count($triggers)) {
self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
}
$db_triggers = CMacrosResolverHelper::resolveTriggerExpressions($db_triggers, [
'sources' => ['expression', 'recovery_expression']
]);
$this->addAffectedObjects($triggers, $db_triggers);
$read_only_fields = ['uuid', 'description', 'expression', 'recovery_mode', 'recovery_expression',
'correlation_mode', 'correlation_tag', 'manual_close'
];
$descriptions = [];
foreach ($triggers as $index => &$trigger) {
$db_trigger = $db_triggers[$trigger['triggerid']];
$description = array_key_exists('description', $trigger)
? $trigger['description']
: $db_trigger['description'];
if ($class === 'CTrigger') {
$update_discovered_validator->setObjectName($description);
$this->checkPartialValidator($trigger, $update_discovered_validator, $db_trigger);
}
if ($db_trigger['templateid'] != 0) {
$this->checkNoParameters($trigger, $read_only_fields, $error_cannot_update, $description);
}
$field_names = ['description', 'expression', 'recovery_mode', 'manual_close'];
foreach ($field_names as $field_name) {
if (!array_key_exists($field_name, $trigger)) {
$trigger[$field_name] = $db_trigger[$field_name];
}
}
if (!array_key_exists('recovery_expression', $trigger)) {
$trigger['recovery_expression'] = ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION)
? $db_trigger['recovery_expression']
: '';
}
if (!array_key_exists('correlation_mode', $trigger)) {
$trigger['correlation_mode'] = ($trigger['recovery_mode'] != ZBX_RECOVERY_MODE_NONE)
? $db_trigger['correlation_mode']
: ZBX_TRIGGER_CORRELATION_NONE;
}
if (!array_key_exists('correlation_tag', $trigger)) {
$trigger['correlation_tag'] = ($trigger['correlation_mode'] == ZBX_TRIGGER_CORRELATION_TAG)
? $db_trigger['correlation_tag']
: '';
}
self::checkTriggerRecoveryMode($trigger);
self::checkTriggerCorrelationMode($trigger);
$name_updated = $trigger['expression'] !== $db_trigger['expression']
|| $trigger['recovery_expression'] !== $db_trigger['recovery_expression']
|| $trigger['description'] !== $db_trigger['description'];
if ($name_updated || array_key_exists('uuid', $trigger)) {
$descriptions[$trigger['description']][] = [
'index' => $index,
'expression' => $trigger['expression'],
'recovery_expression' => $trigger['recovery_expression'],
'name_updated' => $name_updated
];
}
}
unset($trigger);
if ($descriptions) {
$descriptions = $this->populateHostIds($descriptions);
self::validateUuid($triggers, $descriptions);
self::checkUuidDuplicates($triggers, $db_triggers);
$this->checkDuplicates($descriptions);
}
$this->checkDependencies($triggers, $db_triggers);
$this->checkDependenciesLinks($triggers, $db_triggers);
}
/**
* @param array $triggers
* @param array $db_triggers
*/
private function addAffectedObjects(array $triggers, array &$db_triggers): void {
if ($this instanceof CTrigger) {
self::addAffectedHosts($triggers, $db_triggers);
}
self::addAffectedTags($triggers, $db_triggers);
self::addAffectedDependencies($triggers, $db_triggers);
}
/**
* @param array $triggers
* @param array $db_triggers
*/
protected static function addAffectedHosts(array $triggers, array &$db_triggers): void {
$triggerids = [];
foreach ($triggers as $trigger) {
$db_trigger = $db_triggers[$trigger['triggerid']];
if ((array_key_exists('expression', $trigger) && $trigger['expression'] !== $db_trigger['expression'])
|| (array_key_exists('recovery_expression', $trigger)
&& $trigger['recovery_expression'] !== $db_trigger['recovery_expression'])) {
$triggerids[] = $trigger['triggerid'];
$db_triggers[$trigger['triggerid']]['hosts'] = [];
}
}
if (!$triggerids) {
return;
}
$result = DBselect(
'SELECT DISTINCT f.triggerid,i.hostid'.
' FROM functions f,items i'.
' WHERE f.itemid=i.itemid'.
' AND '.dbConditionId('f.triggerid', $triggerids)
);
while ($row = DBfetch($result)) {
$db_triggers[$row['triggerid']]['hosts'][$row['hostid']] = true;
}
}
/**
* @param array $triggers
* @param array $db_triggers
*/
private static function addAffectedTags(array $triggers, array &$db_triggers): void {
$triggerids = [];
foreach ($triggers as $trigger) {
if (array_key_exists('tags', $trigger)) {
$triggerids[] = $trigger['triggerid'];
$db_triggers[$trigger['triggerid']]['tags'] = [];
}
}
if (!$triggerids) {
return;
}
$options = [
'output' => ['triggertagid', 'triggerid', 'tag', 'value'],
'filter' => ['triggerid' => $triggerids]
];
$db_tags = DBselect(DB::makeSql('trigger_tag', $options));
while ($db_tag = DBfetch($db_tags)) {
$db_triggers[$db_tag['triggerid']]['tags'][$db_tag['triggertagid']] =
array_diff_key($db_tag, array_flip(['triggerid']));
}
}
/**
* @param array $triggers
* @param array $db_triggers
*/
protected static function addAffectedDependencies(array $triggers, array &$db_triggers): void {
$triggerids = [];
foreach ($triggers as $trigger) {
$db_trigger = $db_triggers[$trigger['triggerid']];
if (array_key_exists('dependencies', $trigger) || array_key_exists('hosts', $db_trigger)) {
$triggerids[] = $trigger['triggerid'];
$db_triggers[$trigger['triggerid']]['dependencies'] = [];
}
}
if (!$triggerids) {
return;
}
$options = [
'output' => ['triggerdepid', 'triggerid_down', 'triggerid_up'],
'filter' => ['triggerid_down' => $triggerids]
];
$db_deps = DBselect(DB::makeSql('trigger_depends', $options));
while ($db_dep = DBfetch($db_deps)) {
$db_triggers[$db_dep['triggerid_down']]['dependencies'][$db_dep['triggerdepid']] = [
'triggerdepid' => $db_dep['triggerdepid'],
'triggerid' => $db_dep['triggerid_up']
];
}
}
/**
* Check trigger dependencies of the given triggers.
*
* @param array $triggers
* @param array|null $db_triggers
*
* @throws APIException
*/
private function checkDependencies(array $triggers, array $db_triggers = null): void {
$edit_triggerids_up = [];
foreach ($triggers as $trigger) {
if (!array_key_exists('dependencies', $trigger)) {
continue;
}
$triggerids_up = array_column($trigger['dependencies'], 'triggerid');
if ($db_triggers === null) {
$edit_triggerids_up += array_flip($triggerids_up);
}
else {
$db_triggerids_up = array_column($db_triggers[$trigger['triggerid']]['dependencies'], 'triggerid');
$ins_triggerids_up = array_flip(array_diff($triggerids_up, $db_triggerids_up));
$del_triggerids_up = array_flip(array_diff($db_triggerids_up, $triggerids_up));
$edit_triggerids_up += $ins_triggerids_up + $del_triggerids_up;
}
}
if (!$edit_triggerids_up) {
return;
}
$count = API::Trigger()->get([
'countOutput' => true,
'triggerids' => array_keys($edit_triggerids_up)
]);
if ($this instanceof CTriggerPrototype) {
$trigger_prototypes_up = API::TriggerPrototype()->get([
'output' => [],
'triggerids' => array_keys($edit_triggerids_up),
'preservekeys' => true
]);
$count += count($trigger_prototypes_up);
}
if ($count != count($edit_triggerids_up)) {
self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
}
if ($this instanceof CTriggerPrototype) {
$ins_dependencies = [];
$triggerids = [];
foreach ($triggers as $trigger) {
if (!array_key_exists('dependencies', $triggers)) {
continue;
}
$db_triggers_up = ($db_triggers !== null)
? array_column($db_triggers[$trigger['triggerid']]['dependencies'], null, 'triggerid')
: [];
foreach ($trigger['dependencies'] as $trigger_up) {
if (!array_key_exists($trigger_up['triggerid'], $db_triggers_up)
&& array_key_exists($trigger_up['triggerid'], $trigger_prototypes_up)) {
$ins_dependencies[$trigger_up['triggerid']][$trigger['triggerid']] = true;
$triggerids[$trigger['triggerid']] = true;
}
}
}
if (!$ins_dependencies) {
return;
}
$result = DBselect(
'SELECT DISTINCT f.triggerid,id.parent_itemid'.
' FROM functions f,item_discovery id'.
' WHERE f.itemid=id.itemid'.
' AND '.dbConditionId('f.triggerid', array_keys($ins_dependencies + $triggerids))
);
$lld_rules = [];
while ($row = DBfetch($result)) {
$lld_rules[$row['triggerid']] = $row['parent_itemid'];
}
foreach ($ins_dependencies as $triggerid_up => $triggerids) {
foreach ($triggerids as $triggerid) {
if (bccomp($lld_rules[$triggerid_up], $lld_rules[$triggerid]) != 0) {
self::exception(ZBX_API_ERROR_PARAMETERS,
_('Trigger prototype "%1$s" cannot depend on the trigger prototype "%2$s", because dependencies on trigger prototypes from another LLD rule are not allowed.')
);
}
}
}
}
}
/**
* Check linkage of dependencies.
*
* @param array $triggers
* @param array|null $db_triggers
*/
protected function checkDependenciesLinks(array $triggers, array $db_triggers = null): void {
$ins_dependencies = [];
$del_dependencies = [];
foreach ($triggers as $trigger) {
if (!array_key_exists('dependencies', $trigger)) {
continue;
}
$db_triggers_up = ($db_triggers !== null)
? array_column($db_triggers[$trigger['triggerid']]['dependencies'], null, 'triggerid')
: [];
foreach ($trigger['dependencies'] as $trigger_up) {
if (array_key_exists($trigger_up['triggerid'], $db_triggers_up)) {
unset($db_triggers_up[$trigger_up['triggerid']]);
}
else {
$ins_dependencies[$trigger_up['triggerid']][$trigger['triggerid']] = true;
}
}
foreach ($db_triggers_up as $db_trigger_up) {
$del_dependencies[$db_trigger_up['triggerid']][$trigger['triggerid']] = true;
}
}
if ($ins_dependencies) {
if ($this instanceof CTriggerPrototype) {
$options = [
'output' => ['triggerid', 'flags'],
'triggerids' => array_keys($ins_dependencies)
];
$result = DBselect(DB::makeSql('triggers', $options));
$ins_dependencies_prototypes = [];
while ($row = DBfetch($result)) {
if ($row['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
/*
* Trigger cannot depend on trigger prototypes, so it's enough to check circular dependencies
* only among the trigger prototypes.
*/
$ins_dependencies_prototypes[$row['triggerid']] = $ins_dependencies[$row['triggerid']];
/*
* Since the trigger prototypes can depend only on trigger prototypes from the same LLD rule,
* for such dependencies it is not necessary to check the dependencies of host and template
* triggers. Only dependencies on triggers are required to be checked.
*/
unset($ins_dependencies[$row['triggerid']]);
}
}
}
if ($db_triggers !== null) {
if ($this instanceof CTriggerPrototype && $ins_dependencies_prototypes) {
self::checkCircularDependencies($ins_dependencies_prototypes, $del_dependencies);
}
else {
self::checkCircularDependencies($ins_dependencies, $del_dependencies);
}
}
$trigger_hosts = self::getTriggerHosts($ins_dependencies);
self::checkDependenciesOfHostTriggers($ins_dependencies, $trigger_hosts);
self::checkDependenciesOfTemplateTriggers($ins_dependencies, $trigger_hosts);
}
}
/**
* Inserts trigger or trigger prototypes records into the database.
*
* @param array $triggers [IN/OUT]
* @param array $triggers[]['triggerid'] [OUT]
* @param array $triggers[]['description'] [IN]
* @param string $triggers[]['expression'] [IN]
* @param int $triggers[]['recovery_mode'] [IN]
* @param string $triggers[]['recovery_expression'] [IN]
* @param string $triggers[]['url_name'] [IN] (optional)
* @param string $triggers[]['url'] [IN] (optional)
* @param int $triggers[]['status'] [IN] (optional)
* @param int $triggers[]['priority'] [IN] (optional)
* @param string $triggers[]['comments'] [IN] (optional)
* @param int $triggers[]['type'] [IN] (optional)
* @param string $triggers[]['templateid'] [IN] (optional)
* @param array $triggers[]['tags'] [IN] (optional)
* @param string $triggers[]['tags'][]['tag'] [IN]
* @param string $triggers[]['tags'][]['value'] [IN]
* @param int $triggers[]['correlation_mode'] [IN] (optional)
* @param string $triggers[]['correlation_tag'] [IN] (optional)
* @param bool $inherited [IN] (optional) If set to true, trigger will be created for
* non-editable host/template.
*
* @throws APIException
*/
protected function createReal(array &$triggers, $inherited = false) {
$new_triggers = $triggers;
$new_functions = [];
$triggers_functions = [];
$new_tags = [];
$this->implode_expressions($new_triggers, null, $triggers_functions, $inherited);
$triggerid = DB::reserveIds('triggers', count($new_triggers));
foreach ($new_triggers as $tnum => &$new_trigger) {
$new_trigger['triggerid'] = $triggerid;
$triggers[$tnum]['triggerid'] = $triggerid;
foreach ($triggers_functions[$tnum] as $trigger_function) {
$trigger_function['triggerid'] = $triggerid;
$new_functions[] = $trigger_function;
}
if ($this instanceof CTriggerPrototype) {
$new_trigger['flags'] = ZBX_FLAG_DISCOVERY_PROTOTYPE;
}
if (array_key_exists('tags', $new_trigger)) {
foreach ($new_trigger['tags'] as $tag) {
$tag['triggerid'] = $triggerid;
$new_tags[] = $tag;
}
}
$triggerid = bcadd($triggerid, 1, 0);
}
unset($new_trigger);
DB::insert('triggers', $new_triggers, false);
DB::insertBatch('functions', $new_functions, false);
if ($new_tags) {
DB::insert('trigger_tag', $new_tags);
}
if (!$inherited) {
$resource = ($this instanceof CTrigger) ? CAudit::RESOURCE_TRIGGER : CAudit::RESOURCE_TRIGGER_PROTOTYPE;
$this->addAuditBulk(CAudit::ACTION_ADD, $resource, $triggers);
}
}
/**
* Update trigger or trigger prototypes records in the database.
*
* @param array $triggers [IN] list of triggers to be updated
* @param array $triggers[<tnum>]['triggerid'] [IN]
* @param array $triggers[<tnum>]['description'] [IN]
* @param string $triggers[<tnum>]['expression'] [IN]
* @param int $triggers[<tnum>]['recovery_mode'] [IN]
* @param string $triggers[<tnum>]['recovery_expression'] [IN]
* @param string $triggers[<tnum>]['url_name'] [IN] (optional)
* @param string $triggers[<tnum>]['url'] [IN] (optional)
* @param int $triggers[<tnum>]['status'] [IN] (optional)
* @param int $triggers[<tnum>]['priority'] [IN] (optional)
* @param string $triggers[<tnum>]['comments'] [IN] (optional)
* @param int $triggers[<tnum>]['type'] [IN] (optional)
* @param string $triggers[<tnum>]['templateid'] [IN] (optional)
* @param array $triggers[<tnum>]['tags'] [IN]
* @param string $triggers[<tnum>]['tags'][]['tag'] [IN]
* @param string $triggers[<tnum>]['tags'][]['value'] [IN]
* @param int $triggers[<tnum>]['correlation_mode'] [IN]
* @param string $triggers[<tnum>]['correlation_tag'] [IN]
* @param array $db_triggers [IN]
* @param array $db_triggers[<tnum>]['triggerid'] [IN]
* @param array $db_triggers[<tnum>]['description'] [IN]
* @param string $db_triggers[<tnum>]['expression'] [IN]
* @param int $db_triggers[<tnum>]['recovery_mode'] [IN]
* @param string $db_triggers[<tnum>]['recovery_expression'] [IN]
* @param string $db_triggers[<tnum>]['url_name'] [IN]
* @param string $db_triggers[<tnum>]['url'] [IN]
* @param int $db_triggers[<tnum>]['status'] [IN]
* @param int $db_triggers[<tnum>]['priority'] [IN]
* @param string $db_triggers[<tnum>]['comments'] [IN]
* @param int $db_triggers[<tnum>]['type'] [IN]
* @param string $db_triggers[<tnum>]['templateid'] [IN]
* @param array $db_triggers[<tnum>]['discoveryRule'] [IN] For trigger prototypes only.
* @param string $db_triggers[<tnum>]['discoveryRule']['itemid'] [IN]
* @param array $db_triggers[<tnum>]['tags'] [IN]
* @param string $db_triggers[<tnum>]['tags'][]['tag'] [IN]
* @param string $db_triggers[<tnum>]['tags'][]['value'] [IN]
* @param int $db_triggers[<tnum>]['correlation_mode'] [IN]
* @param string $db_triggers[<tnum>]['correlation_tag'] [IN]
* @param bool $inherited [IN] (optional) If set to true, trigger will be
* created for non-editable
* host/template.
*
* @throws APIException
*/
protected function updateReal(array $triggers, array $db_triggers, $inherited = false) {
$upd_triggers = [];
$new_functions = [];
$del_functions_triggerids = [];
$triggers_functions = [];
$new_tags = [];
$del_triggertagids = [];
$save_triggers = $triggers;
$this->implode_expressions($triggers, $db_triggers, $triggers_functions, $inherited);
foreach ($triggers as $tnum => $trigger) {
$db_trigger = $db_triggers[$trigger['triggerid']];
$upd_trigger = ['values' => [], 'where' => ['triggerid' => $trigger['triggerid']]];
if (array_key_exists($tnum, $triggers_functions)) {
$del_functions_triggerids[] = $trigger['triggerid'];
foreach ($triggers_functions[$tnum] as $trigger_function) {
$trigger_function['triggerid'] = $trigger['triggerid'];
$new_functions[] = $trigger_function;
}
$upd_trigger['values']['expression'] = $trigger['expression'];
$upd_trigger['values']['recovery_expression'] = $trigger['recovery_expression'];
}
if (array_key_exists('uuid', $trigger)) {
$upd_trigger['values']['uuid'] = $trigger['uuid'];
}
if ($trigger['description'] !== $db_trigger['description']) {
$upd_trigger['values']['description'] = $trigger['description'];
}
if (array_key_exists('event_name', $trigger) && $trigger['event_name'] !== $db_trigger['event_name']) {
$upd_trigger['values']['event_name'] = $trigger['event_name'];
}
if (array_key_exists('opdata', $trigger) && $trigger['opdata'] !== $db_trigger['opdata']) {
$upd_trigger['values']['opdata'] = $trigger['opdata'];
}
if ($trigger['recovery_mode'] != $db_trigger['recovery_mode']) {
$upd_trigger['values']['recovery_mode'] = $trigger['recovery_mode'];
}
if (array_key_exists('url_name', $trigger) && $trigger['url_name'] !== $db_trigger['url_name']) {
$upd_trigger['values']['url_name'] = $trigger['url_name'];
}
if (array_key_exists('url', $trigger) && $trigger['url'] !== $db_trigger['url']) {
$upd_trigger['values']['url'] = $trigger['url'];
}
if (array_key_exists('status', $trigger) && $trigger['status'] != $db_trigger['status']) {
$upd_trigger['values']['status'] = $trigger['status'];
}
if ($this instanceof CTriggerPrototype
&& array_key_exists('discover', $trigger) && $trigger['discover'] != $db_trigger['discover']) {
$upd_trigger['values']['discover'] = $trigger['discover'];
}
if (array_key_exists('priority', $trigger) && $trigger['priority'] != $db_trigger['priority']) {
$upd_trigger['values']['priority'] = $trigger['priority'];
}
if (array_key_exists('comments', $trigger) && $trigger['comments'] !== $db_trigger['comments']) {
$upd_trigger['values']['comments'] = $trigger['comments'];
}
if (array_key_exists('type', $trigger) && $trigger['type'] != $db_trigger['type']) {
$upd_trigger['values']['type'] = $trigger['type'];
}
if (array_key_exists('templateid', $trigger) && $trigger['templateid'] != $db_trigger['templateid']) {
$upd_trigger['values']['templateid'] = $trigger['templateid'];
}
if ($trigger['correlation_mode'] != $db_trigger['correlation_mode']) {
$upd_trigger['values']['correlation_mode'] = $trigger['correlation_mode'];
}
if ($trigger['correlation_tag'] !== $db_trigger['correlation_tag']) {
$upd_trigger['values']['correlation_tag'] = $trigger['correlation_tag'];
}
if ($trigger['manual_close'] != $db_trigger['manual_close']) {
$upd_trigger['values']['manual_close'] = $trigger['manual_close'];
}
if ($upd_trigger['values']) {
$upd_triggers[] = $upd_trigger;
}
if (array_key_exists('tags', $trigger)) {
// Add new trigger tags and replace changed ones.
CArrayHelper::sort($db_trigger['tags'], ['tag', 'value']);
CArrayHelper::sort($trigger['tags'], ['tag', 'value']);
$tags_delete = $db_trigger['tags'];
$tags_add = $trigger['tags'];
foreach ($tags_delete as $dt_key => $tag_delete) {
foreach ($tags_add as $nt_key => $tag_add) {
if ($tag_delete['tag'] === $tag_add['tag'] && $tag_delete['value'] === $tag_add['value']) {
unset($tags_delete[$dt_key], $tags_add[$nt_key]);
continue 2;
}
}
}
foreach ($tags_delete as $tag_delete) {
$del_triggertagids[] = $tag_delete['triggertagid'];
}
foreach ($tags_add as $tag_add) {
$tag_add['triggerid'] = $trigger['triggerid'];
$new_tags[] = $tag_add;
}
}
}
if ($upd_triggers) {
DB::update('triggers', $upd_triggers);
}
if ($del_functions_triggerids) {
DB::delete('functions', ['triggerid' => $del_functions_triggerids]);
}
if ($new_functions) {
DB::insertBatch('functions', $new_functions, false);
}
if ($del_triggertagids) {
DB::delete('trigger_tag', ['triggertagid' => $del_triggertagids]);
}
if ($new_tags) {
DB::insert('trigger_tag', $new_tags);
}
if (!$inherited) {
$resource = ($this instanceof CTrigger) ? CAudit::RESOURCE_TRIGGER : CAudit::RESOURCE_TRIGGER_PROTOTYPE;
$this->addAuditBulk(CAudit::ACTION_UPDATE, $resource, $save_triggers, $db_triggers);
}
}
/**
* Implodes expression and recovery_expression for each trigger. Also returns array of functions and
* array of hostnames for each trigger.
*
* For example: last(/host/system.cpu.load)>10 will be translated to {12}>10 and created database representation.
*
* Note: All expressions must be already validated and exploded.
*
* @param array $triggers [IN]
* @param string $triggers[<tnum>]['description'] [IN]
* @param string $triggers[<tnum>]['expression'] [IN/OUT]
* @param int $triggers[<tnum>]['recovery_mode'] [IN]
* @param string $triggers[<tnum>]['recovery_expression'] [IN/OUT]
* @param array|null $db_triggers [IN]
* @param string $db_triggers[<triggerid>]['triggerid'] [IN]
* @param string $db_triggers[<triggerid>]['expression'] [IN]
* @param string $db_triggers[<triggerid>]['recovery_expression'] [IN]
* @param array $triggers_functions [OUT] array of the new functions which must be
* inserted into DB
* @param string $triggers_functions[<tnum>][]['functionid'] [OUT]
* @param null $triggers_functions[<tnum>][]['triggerid'] [OUT] must be initialized before insertion into DB
* @param string $triggers_functions[<tnum>][]['itemid'] [OUT]
* @param string $triggers_functions[<tnum>][]['name'] [OUT]
* @param string $triggers_functions[<tnum>][]['parameter'] [OUT]
* @param bool $inherited [IN] (optional) If set to true, triggers will be
* created for non-editable
* hosts/templates.
*
* @throws APIException if error occurred
*/
private function implode_expressions(array &$triggers, ?array $db_triggers, array &$triggers_functions, $inherited) {
$class = get_class($this);
switch ($class) {
case 'CTrigger':
$expression_parser = new CExpressionParser(['usermacros' => true]);
$error_wrong_host = _('Incorrect trigger expression. Host "%1$s" does not exist or you have no access to this host.');
$error_host_and_template = _('Incorrect trigger expression. Trigger expression elements should not belong to a template and a host simultaneously.');
break;
case 'CTriggerPrototype':
$expression_parser = new CExpressionParser(['usermacros' => true, 'lldmacros' => true]);
$error_wrong_host = _('Incorrect trigger prototype expression. Host "%1$s" does not exist or you have no access to this host.');
$error_host_and_template = _('Incorrect trigger prototype expression. Trigger prototype expression elements should not belong to a template and a host simultaneously.');
break;
default:
self::exception(ZBX_API_ERROR_INTERNAL, _('Internal error.'));
}
$hist_function_value_types = (new CHistFunctionData())->getValueTypes();
/*
* [
* <host> => [
* 'hostid' => <hostid>,
* 'host' => <host>,
* 'status' => <status>,
* 'keys' => [
* <key> => [
* 'itemid' => <itemid>,
* 'key' => <key>,
* 'value_type' => <value_type>,
* 'flags' => <flags>,
* 'lld_ruleid' => <itemid> (CTriggerProrotype only)
* ]
* ]
* ]
* ]
*/
$hosts_keys = [];
$functions_num = 0;
foreach ($triggers as $trigger) {
$expressions_changed = ($db_triggers === null
|| ($trigger['expression'] !== $db_triggers[$trigger['triggerid']]['expression']
|| $trigger['recovery_expression'] !== $db_triggers[$trigger['triggerid']]['recovery_expression']));
if (!$expressions_changed) {
continue;
}
$expression_parser->parse($trigger['expression']);
$hist_functions = $expression_parser->getResult()->getTokensOfTypes(
[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
);
if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
$expression_parser->parse($trigger['recovery_expression']);
$hist_functions = array_merge($hist_functions, $expression_parser->getResult()->getTokensOfTypes(
[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
));
}
foreach ($hist_functions as $hist_function) {
$host = $hist_function['data']['parameters'][0]['data']['host'];
$item = $hist_function['data']['parameters'][0]['data']['item'];
if (!array_key_exists($host, $hosts_keys)) {
$hosts_keys[$host] = [
'hostid' => null,
'host' => $host,
'status' => null,
'keys' => []
];
}
$hosts_keys[$host]['keys'][$item] = [
'itemid' => null,
'key' => $item,
'value_type' => null,
'flags' => null
];
}
}
if (!$hosts_keys) {
return;
}
$permission_check = $inherited
? ['nopermissions' => true]
: ['editable' => true];
$_db_hosts = API::Host()->get([
'output' => ['hostid', 'host', 'status'],
'filter' => ['host' => array_keys($hosts_keys)]
] + $permission_check);
if (count($hosts_keys) != count($_db_hosts)) {
$_db_templates = API::Template()->get([
'output' => ['templateid', 'host', 'status'],
'filter' => ['host' => array_keys($hosts_keys)]
] + $permission_check);
foreach ($_db_templates as &$_db_template) {
$_db_template['hostid'] = $_db_template['templateid'];
unset($_db_template['templateid']);
}
unset($_db_template);
$_db_hosts = array_merge($_db_hosts, $_db_templates);
}
foreach ($_db_hosts as $_db_host) {
$host_keys = &$hosts_keys[$_db_host['host']];
$host_keys['hostid'] = $_db_host['hostid'];
$host_keys['status'] = $_db_host['status'];
if ($class === 'CTriggerPrototype') {
$sql = 'SELECT i.itemid,i.key_,i.value_type,i.flags,id.parent_itemid'.
' FROM items i'.
' LEFT JOIN item_discovery id ON i.itemid=id.itemid'.
' WHERE i.hostid='.$host_keys['hostid'].
' AND '.dbConditionString('i.key_', array_keys($host_keys['keys'])).
' AND '.dbConditionInt('i.flags',
[ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_PROTOTYPE, ZBX_FLAG_DISCOVERY_CREATED]
);
}
else {
$sql = 'SELECT i.itemid,i.key_,i.value_type,i.flags'.
' FROM items i'.
' WHERE i.hostid='.$host_keys['hostid'].
' AND '.dbConditionString('i.key_', array_keys($host_keys['keys'])).
' AND '.dbConditionInt('i.flags', [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_CREATED]);
}
$_db_items = DBselect($sql);
while ($_db_item = DBfetch($_db_items)) {
$host_keys['keys'][$_db_item['key_']]['itemid'] = $_db_item['itemid'];
$host_keys['keys'][$_db_item['key_']]['value_type'] = $_db_item['value_type'];
$host_keys['keys'][$_db_item['key_']]['flags'] = $_db_item['flags'];
if ($class === 'CTriggerPrototype' && $_db_item['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
$host_keys['keys'][$_db_item['key_']]['lld_ruleid'] = $_db_item['parent_itemid'];
}
}
unset($host_keys);
}
/*
* The list of triggers with multiple templates.
*
* [
* [
* 'description' => <description>,
* 'templateids' => [<templateid>, ...]
* ],
* ...
* ]
*/
$mt_triggers = [];
if ($class === 'CTrigger') {
/*
* The list of triggers which are moved from one host or template to another.
*
* [
* <triggerid> => [
* 'description' => <description>
* ],
* ...
* ]
*/
$moved_triggers = [];
}
foreach ($triggers as $tnum => &$trigger) {
$expressions_changed = ($db_triggers === null
|| ($trigger['expression'] !== $db_triggers[$trigger['triggerid']]['expression']
|| $trigger['recovery_expression'] !== $db_triggers[$trigger['triggerid']]['recovery_expression']));
if (!$expressions_changed) {
continue;
}
$expression_parser->parse($trigger['expression']);
$hist_functions1 = $expression_parser->getResult()->getTokensOfTypes(
[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
);
$hist_functions2 = [];
if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
$expression_parser->parse($trigger['recovery_expression']);
$hist_functions2 = $expression_parser->getResult()->getTokensOfTypes(
[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
);
}
$triggers_functions[$tnum] = [];
if ($class === 'CTriggerPrototype') {
$lld_ruleids = [];
}
/*
* 0x01 - with templates
* 0x02 - with hosts
*/
$status_mask = 0x00;
// The lists of hostids and hosts which are used in the current trigger.
$hostids = [];
$hosts = [];
// Common checks.
foreach (array_merge($hist_functions1, $hist_functions2) as $hist_function) {
$host = $hist_function['data']['parameters'][0]['data']['host'];
$item = $hist_function['data']['parameters'][0]['data']['item'];
$host_keys = $hosts_keys[$host];
$key = $host_keys['keys'][$item];
if ($host_keys['hostid'] === null) {
self::exception(ZBX_API_ERROR_PARAMETERS, _params($error_wrong_host, [$host_keys['host']]));
}
if ($key['itemid'] === null) {
self::exception(ZBX_API_ERROR_PARAMETERS, _s(
'Incorrect item key "%1$s" provided for trigger expression on "%2$s".', $key['key'],
$host_keys['host']
));
}
if (!in_array($key['value_type'], $hist_function_value_types[$hist_function['data']['function']])) {
self::exception(ZBX_API_ERROR_PARAMETERS, _s(
'Incorrect item value type "%1$s" provided for trigger function "%2$s".',
itemValueTypeString($key['value_type']), $hist_function['data']['function']
));
}
if (!array_key_exists($hist_function['match'], $triggers_functions[$tnum])) {
$query_parameter = $hist_function['data']['parameters'][0];
$parameter = substr_replace($hist_function['match'], TRIGGER_QUERY_PLACEHOLDER,
$query_parameter['pos'] - $hist_function['pos'], $query_parameter['length']
);
$triggers_functions[$tnum][$hist_function['match']] = [
'functionid' => null,
'triggerid' => null,
'itemid' => $key['itemid'],
'name' => $hist_function['data']['function'],
'parameter' => substr($parameter, strlen($hist_function['data']['function']) + 1, -1)
];
$functions_num++;
}
if ($class === 'CTriggerPrototype' && $key['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
$lld_ruleids[$key['lld_ruleid']] = true;
}
$status_mask |= ($host_keys['status'] == HOST_STATUS_TEMPLATE ? 0x01 : 0x02);
$hostids[$host_keys['hostid']] = true;
$hosts[$host] = true;
}
// When both templates and hosts are referenced in expressions.
if ($status_mask == 0x03) {
self::exception(ZBX_API_ERROR_PARAMETERS, $error_host_and_template);
}
// Triggers with children cannot be moved from one template to another host or template.
if ($class === 'CTrigger' && $db_triggers !== null && $expressions_changed) {
$expression_parser->parse($db_triggers[$trigger['triggerid']]['expression']);
$old_hosts1 = $expression_parser->getResult()->getHosts();
$old_hosts2 = [];
if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
$expression_parser->parse($db_triggers[$trigger['triggerid']]['recovery_expression']);
$old_hosts2 = $expression_parser->getResult()->getHosts();
}
$is_moved = true;
foreach (array_merge($old_hosts1, $old_hosts2) as $old_host) {
if (array_key_exists($old_host, $hosts)) {
$is_moved = false;
break;
}
}
if ($is_moved) {
$moved_triggers[$trigger['triggerid']] = ['description' => $trigger['description']];
}
}
// The trigger with multiple templates.
if ($status_mask == 0x01 && count($hostids) > 1) {
$mt_triggers[] = [
'description' => $trigger['description'],
'templateids' => array_keys($hostids)
];
}
if ($class === 'CTriggerPrototype') {
$lld_ruleids = array_keys($lld_ruleids);
if (!$lld_ruleids) {
self::exception(ZBX_API_ERROR_PARAMETERS, _s(
'Trigger prototype "%1$s" must contain at least one item prototype.', $trigger['description']
));
}
elseif (count($lld_ruleids) > 1) {
self::exception(ZBX_API_ERROR_PARAMETERS, _s(
'Trigger prototype "%1$s" contains item prototypes from multiple discovery rules.',
$trigger['description']
));
}
elseif ($db_triggers !== null
&& !idcmp($lld_ruleids[0], $db_triggers[$trigger['triggerid']]['discoveryRule']['itemid'])) {
self::exception(ZBX_API_ERROR_PARAMETERS, _s('Cannot update trigger prototype "%1$s": %2$s.',
$trigger['description'], _('trigger prototype cannot be moved to another template or host')
));
}
}
}
unset($trigger);
if ($mt_triggers) {
$this->validateTriggersWithMultipleTemplates($mt_triggers);
}
if ($class === 'CTrigger' && $moved_triggers) {
$this->validateMovedTriggers($moved_triggers);
}
$functionid = DB::reserveIds('functions', $functions_num);
$expression_max_length = DB::getFieldLength('triggers', 'expression');
$recovery_expression_max_length = DB::getFieldLength('triggers', 'recovery_expression');
// Replace func(/host/item) macros with {<functionid>}.
foreach ($triggers as $tnum => &$trigger) {
$expressions_changed = ($db_triggers === null
|| ($trigger['expression'] !== $db_triggers[$trigger['triggerid']]['expression']
|| $trigger['recovery_expression'] !== $db_triggers[$trigger['triggerid']]['recovery_expression']));
if (!$expressions_changed) {
continue;
}
foreach ($triggers_functions[$tnum] as &$trigger_function) {
$trigger_function['functionid'] = $functionid;
$functionid = bcadd($functionid, 1, 0);
}
unset($trigger_function);
$expression_parser->parse($trigger['expression']);
$hist_functions = $expression_parser->getResult()->getTokensOfTypes(
[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
);
$hist_function = end($hist_functions);
do {
$trigger['expression'] = substr_replace($trigger['expression'],
'{'.$triggers_functions[$tnum][$hist_function['match']]['functionid'].'}',
$hist_function['pos'], $hist_function['length']
);
}
while ($hist_function = prev($hist_functions));
if (mb_strlen($trigger['expression']) > $expression_max_length) {
self::exception(ZBX_API_ERROR_PARAMETERS, _s(
'Invalid parameter "%1$s": %2$s.', '/'.($tnum + 1).'/expression', _('value is too long')
));
}
if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
$expression_parser->parse($trigger['recovery_expression']);
$hist_functions = $expression_parser->getResult()->getTokensOfTypes(
[CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION]
);
$hist_function = end($hist_functions);
do {
$trigger['recovery_expression'] = substr_replace($trigger['recovery_expression'],
'{'.$triggers_functions[$tnum][$hist_function['match']]['functionid'].'}',
$hist_function['pos'], $hist_function['length']
);
}
while ($hist_function = prev($hist_functions));
if (mb_strlen($trigger['recovery_expression']) > $recovery_expression_max_length) {
self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
'/'.($tnum + 1).'/recovery_expression', _('value is too long')
));
}
}
}
unset($trigger);
}
/**
* Check if all templates trigger belongs to are linked to same hosts.
*
* @param array $mt_triggers
* @param string $mt_triggers[]['description']
* @param array $mt_triggers[]['templateids']
*
* @throws APIException
*/
protected function validateTriggersWithMultipleTemplates(array $mt_triggers) {
switch (get_class($this)) {
case 'CTrigger':
$error_different_linkages = _('Trigger "%1$s" belongs to templates with different linkages.');
break;
case 'CTriggerPrototype':
$error_different_linkages = _('Trigger prototype "%1$s" belongs to templates with different linkages.');
break;
default:
self::exception(ZBX_API_ERROR_INTERNAL, _('Internal error.'));
}
$templateids = [];
foreach ($mt_triggers as $mt_trigger) {
foreach ($mt_trigger['templateids'] as $templateid) {
$templateids[$templateid] = true;
}
}
$templates = API::Template()->get([
'output' => [],
'selectHosts' => ['hostid'],
'selectTemplates' => ['templateid'],
'templateids' => array_keys($templateids),
'nopermissions' => true,
'preservekeys' => true
]);
foreach ($templates as &$template) {
$template = array_merge(
zbx_objectValues($template['hosts'], 'hostid'),
zbx_objectValues($template['templates'], 'templateid')
);
}
unset($template);
foreach ($mt_triggers as $mt_trigger) {
$compare_links = null;
foreach ($mt_trigger['templateids'] as $templateid) {
if ($compare_links === null) {
$compare_links = $templates[$templateid];
continue;
}
$linked_to = $templates[$templateid];
if (array_diff($compare_links, $linked_to) || array_diff($linked_to, $compare_links)) {
self::exception(ZBX_API_ERROR_PARAMETERS,
_params($error_different_linkages, [$mt_trigger['description']])
);
}
}
}
}
/**
* Check if moved triggers does not have children.
*
* @param array $moved_triggers
* @param string $moved_triggers[<triggerid>]['description']
*
* @throws APIException
*/
protected function validateMovedTriggers(array $moved_triggers) {
$_db_triggers = DBselect(
'SELECT t.templateid'.
' FROM triggers t'.
' WHERE '.dbConditionInt('t.templateid', array_keys($moved_triggers)),
1
);
if ($_db_trigger = DBfetch($_db_triggers)) {
self::exception(ZBX_API_ERROR_PARAMETERS, _s('Cannot update trigger "%1$s": %2$s.',
$moved_triggers[$_db_trigger['templateid']]['description'],
_('trigger with linkages cannot be moved to another template or host')
));
}
}
/**
* Adds triggers and trigger prototypes from template to hosts.
*
* @param array $data
*/
public function syncTemplates(array $data) {
$data['templateids'] = zbx_toArray($data['templateids']);
$data['hostids'] = zbx_toArray($data['hostids']);
$output = ['triggerid', 'description', 'expression', 'recovery_mode', 'recovery_expression', 'url_name', 'url',
'status', 'priority', 'comments', 'type', 'correlation_mode', 'correlation_tag', 'manual_close', 'opdata',
'event_name'
];
if ($this instanceof CTriggerPrototype) {
$output[] = 'discover';
}
$triggers = $this->get([
'output' => $output,
'selectTags' => ['tag', 'value'],
'hostids' => $data['templateids'],
'preservekeys' => true,
'nopermissions' => true
]);
$triggers = CMacrosResolverHelper::resolveTriggerExpressions($triggers,
['sources' => ['expression', 'recovery_expression']]
);
$this->inherit($triggers, $data['hostids']);
}
/**
* Check whether circular linkage occurs as a result of the given changes in trigger dependencies.
*
* @param array $ins_dependencies[<triggerid_up>][<triggerid>]
* @param array $del_dependencies[<triggerid_up>][<triggerid>]
* @param bool $inherited Whether the check gets performed during inherit.
*
* @throws APIException
*/
protected static function checkCircularDependencies(array $ins_dependencies, array $del_dependencies = [],
bool $inherited = false): void {
$links = [];
$_triggerids_down = $ins_dependencies;
do {
$options = [
'output' => ['triggerid_up', 'triggerid_down'],
'filter' => [
'triggerid_down' => array_keys($_triggerids_down)
]
];
$result = DBselect(DB::makeSql('trigger_depends', $options));
$_triggerids_down = [];
while ($row = DBfetch($result)) {
if (array_key_exists($row['triggerid_up'], $del_dependencies)
&& array_key_exists($row['triggerid_down'], $del_dependencies[$row['triggerid_up']])) {
continue;
}
if (!array_key_exists($row['triggerid_up'], $links)) {
$_triggerids_down[$row['triggerid_up']] = true;
}
$links[$row['triggerid_up']][$row['triggerid_down']] = true;
}
} while ($_triggerids_down);
foreach ($ins_dependencies as $triggerid_up => $triggerids) {
if (array_key_exists($triggerid_up, $links)) {
$links[$triggerid_up] += $triggerids;
}
else {
$links[$triggerid_up] = $triggerids;
}
}
foreach ($ins_dependencies as $triggerid_up => $triggerids) {
foreach ($triggerids as $triggerid => $foo) {
if (array_key_exists($triggerid, $links)) {
$links_path = [$triggerid => true];
if (self::circularLinkageExists($links, $triggerid_up, $links[$triggerid], $links_path)) {
$trigger_up_name = '';
$triggers = DB::select('triggers', [
'output' => ['description', 'flags'],
'triggerids' => array_keys($links_path + [$triggerid_up => true]),
'preservekeys' => true
]);
foreach ($triggers as $_triggerid => $trigger) {
$description = '"'.$trigger['description'].'"';
if (bccomp($_triggerid, $triggerid_up) == 0) {
$trigger_up_name = $description;
}
else {
$links_path[$_triggerid] = $description;
}
}
$circular_linkage = (bccomp($triggerid_up, $triggerid) == 0)
? $trigger_up_name.' -> '.$trigger_up_name
: $trigger_up_name.' -> '.implode(' -> ', $links_path).' -> '.$trigger_up_name;
if ($inherited) {
$host = DBfetch(DBselect(
'SELECT DISTINCT h.host,h.status'.
' FROM functions f,items i,hosts h'.
' WHERE f.itemid=i.itemid'.
' AND i.hostid=h.hostid'.
' AND '.dbConditionId('f.triggerid', [$triggerid_up])
));
if ($host['status'] == HOST_STATUS_TEMPLATE) {
$error = ($triggers[$triggerid]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL)
? _('Trigger "%1$s" cannot depend on the trigger "%2$s", because a circular linkage (%3$s) would occur for template "%4$s".')
: _('Trigger prototype "%1$s" cannot depend on the trigger prototype "%2$s", because a circular linkage (%3$s) would occur for template "%4$s".');
}
else {
$error = ($triggers[$triggerid]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL)
? _('Trigger "%1$s" cannot depend on the trigger "%2$s", because a circular linkage (%3$s) would occur for host "%4$s".')
: _('Trigger prototype "%1$s" cannot depend on the trigger prototype "%2$s", because a circular linkage (%3$s) would occur for host "%4$s".');
}
self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error,
$triggers[$triggerid]['description'], $triggers[$triggerid_up]['description'],
$circular_linkage, $host['host']
));
}
else {
$error = ($triggers[$triggerid]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL)
? _('Trigger "%1$s" cannot depend on the trigger "%2$s", because a circular linkage (%3$s) would occur.')
: _('Trigger prototype "%1$s" cannot depend on the trigger prototype "%2$s", because a circular linkage (%3$s) would occur.');
self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error,
$triggers[$triggerid]['description'], $triggers[$triggerid_up]['description'],
$circular_linkage
));
}
}
}
}
}
}
/**
* Recursively check whether the trigger, which a dependency is being set on, produces a circular linkage.
*
* @param array $links[<triggerid_up>][<triggerid>]
* @param string $triggerid_up
* @param array $triggerids[<triggerid>]
* @param array $links_path Circular linkage path, collected performing the check.
*
* @return bool
*/
private static function circularLinkageExists(array $links, string $triggerid_up, array $triggerids,
array &$links_path): bool {
if (array_key_exists($triggerid_up, $triggerids)) {
return true;
}
$_links_path = $links_path;
foreach ($triggerids as $triggerid => $foo) {
if (array_key_exists($triggerid, $links)) {
$links_path = $_links_path;
$triggerid_links = array_diff_key($links[$triggerid], $links_path);
if ($triggerid_links) {
$links_path[$triggerid] = true;
if (self::circularLinkageExists($links, $triggerid_up, $triggerid_links, $links_path)) {
return true;
}
}
}
}
return false;
}
/**
* Get hosts data of all triggers given in trigger dependencies array.
*
* @param array $trigger_dependencies[<triggerid_up>][<triggerid>]
*
* @return array
*/
public static function getTriggerHosts(array $trigger_dependencies): array {
$all_triggerids = $trigger_dependencies;
foreach ($trigger_dependencies as $triggerids) {
$all_triggerids += $triggerids;
}
$result = DBselect(
'SELECT DISTINCT f.triggerid,h.hostid,h.status'.
' FROM functions f,items i,hosts h'.
' WHERE f.itemid=i.itemid'.
' AND i.hostid=h.hostid'.
' AND '.dbConditionId('f.triggerid', array_keys($all_triggerids))
);
$trigger_hosts = [];
while ($row = DBfetch($result)) {
// Each trigger can have either only templateids or only hostids.
if ($row['status'] == HOST_STATUS_TEMPLATE) {
$trigger_hosts[$row['triggerid']]['templateids'][$row['hostid']] = true;
}
else {
$trigger_hosts[$row['triggerid']]['hostids'][$row['hostid']] = true;
}
}
return $trigger_hosts;
}
/**
* Check whether the trigger dependencies are correctly set for host triggers.
*
* @param array $trigger_dependencies[<triggerid_up>][<triggerid>]
* @param array $trigger_hosts
*
* @throws APIException
*/
public static function checkDependenciesOfHostTriggers(array $trigger_dependencies, array $trigger_hosts): void {
foreach ($trigger_dependencies as $triggerid_up => $triggerids) {
if (array_key_exists('templateids', $trigger_hosts[$triggerid_up])) {
foreach ($triggerids as $triggerid => $foo) {
if (array_key_exists('hostids', $trigger_hosts[$triggerid])) {
$triggers = DB::select('triggers', [
'output' => ['description', 'flags'],
'triggerids' => [$triggerid, $triggerid_up],
'preservekeys' => true
]);
$error = ($triggers[$triggerid]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL)
? _('Trigger "%1$s" cannot depend on the trigger "%2$s", because dependencies of host triggers on template triggers are not allowed.')
: _('Trigger prototype "%1$s" cannot depend on the trigger "%2$s", because dependencies of host triggers on template triggers are not allowed.');
self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $triggers[$triggerid]['description'],
$triggers[$triggerid_up]['description']
));
}
}
}
}
}
/**
* Check whether the trigger dependencies are correctly set for template triggers.
*
* @param array $trigger_dependencies[<triggerid_up>][<triggerid>]
* @param array $trigger_hosts
*
* @throws APIException
*/
public static function checkDependenciesOfTemplateTriggers(array $trigger_dependencies,
array $trigger_hosts): void {
/*
* From the given trigger dependencies we should keep only the dependencies of the template triggers. There is
* also no need to check the dependencies on triggers from the same template, because that is considered a
* valid case. Thus, among the triggers that the template triggers depends on, we should only keep triggers from
* other templates and hosts.
*/
foreach ($trigger_dependencies as $triggerid_up => $triggerids) {
$templateids_up = array_key_exists('templateids', $trigger_hosts[$triggerid_up])
? $trigger_hosts[$triggerid_up]['templateids']
: [];
foreach ($triggerids as $triggerid => $foo) {
/*
* If trigger-up and dependent trigger have at least one common template, even if they also have
* other templates, it is important to understand that at the moment of the trigger dependency
* validation all of those different templates are already linked to all child templates,
* so there is no need to check them again.
*/
if (!array_key_exists('templateids', $trigger_hosts[$triggerid])
|| array_intersect_key($templateids_up, $trigger_hosts[$triggerid]['templateids'])) {
unset($trigger_dependencies[$triggerid_up][$triggerid]);
}
}
if (!$trigger_dependencies[$triggerid_up]) {
unset($trigger_dependencies[$triggerid_up]);
}
}
if (!$trigger_dependencies) {
return;
}
self::checkTriggersUpNotFromParentTemplates($trigger_dependencies, $trigger_hosts);
self::checkTriggersUpNotFromChildTemplatesOrHosts($trigger_dependencies, $trigger_hosts);
self::checkTriggersUpTemplatesAreLinkedToChildTemplates($trigger_dependencies, $trigger_hosts);
}
/**
* Check that the triggers-up of the given trigger dependencies do not come from parent templates of dependent
* triggers.
*
* @param array $trigger_dependencies[<triggerid_up>][<triggerid>]
* @param array $trigger_hosts
*
* @throws APIException
*/
private static function checkTriggersUpNotFromParentTemplates(array $trigger_dependencies, array $trigger_hosts): void {
$templateids = [];
$template_triggers_up = [];
$dependency_templates = [];
foreach ($trigger_dependencies as $triggerid_up => $triggerids) {
if (!array_key_exists('templateids', $trigger_hosts[$triggerid_up])) {
continue;
}
$templateids_up = [];
foreach ($trigger_hosts[$triggerid_up]['templateids'] as $templateid => $foo) {
$template_triggers_up[$templateid][] = $triggerid_up;
$templateids_up[$templateid] = true;
}
foreach ($triggerids as $triggerid => $foo) {
foreach ($trigger_hosts[$triggerid]['templateids'] as $templateid => $foo) {
if (!array_key_exists($templateid, $dependency_templates)) {
$dependency_templates[$templateid] = [];
}
$dependency_templates[$templateid] += $templateids_up;
}
$templateids += $trigger_hosts[$triggerid]['templateids'];
}
}
$_templateids = $templateids;
$template_links = [];
do {
$options = [
'output' => ['hostid', 'templateid'],
'filter' => ['hostid' => array_keys($_templateids)]
];
$result = DBselect(DB::makeSql('hosts_templates', $options));
$_templateids = [];
while ($row = DBfetch($result)) {
$template_links[$row['hostid']][$row['templateid']] = true;
if (!array_key_exists($row['templateid'], $template_links)) {
$_templateids[$row['templateid']] = true;
}
}
} while ($_templateids);
if (!$template_links) {
return;
}
// Check if the trigger-up is a trigger from the parent template of the dependent trigger.
foreach ($dependency_templates as $templateid => $templateids_up) {
if (array_key_exists($templateid, $template_links)) {
foreach ($templateids_up as $templateid_up => $foo) {
if (self::checkTemplateUpExistsInTemplateLinks($template_links, $templateid, $templateid_up)) {
foreach ($template_triggers_up[$templateid_up] as $triggerid_up) {
foreach ($trigger_dependencies[$triggerid_up] as $triggerid => $foo) {
if (array_key_exists($templateid, $trigger_hosts[$triggerid]['templateids'])) {
break 2;
}
}
}
$triggers = DB::select('triggers', [
'output' => ['description', 'flags'],
'triggerids' => [$triggerid, $triggerid_up],
'preservekeys' => true
]);
$templates = DB::select('hosts', [
'output' => ['host'],
'hostids' => $templateid_up
]);
$error = ($triggers[$triggerid]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL)
? _('Trigger "%1$s" cannot depend on the trigger "%2$s" from the template "%3$s", because dependencies on triggers from the parent template are not allowed.')
: _('Trigger prototype "%1$s" cannot depend on the trigger "%2$s" from the template "%3$s", because dependencies on triggers from the parent template are not allowed.');
self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $triggers[$triggerid]['description'],
$triggers[$triggerid_up]['description'], $templates[0]['host']
));
}
}
}
}
}
/**
* Check that the triggers-up of the given trigger dependencies are not from the child templates or hosts of the
* dependent triggers.
*
* @param array $trigger_dependencies[<triggerid_up>][<triggerid>]
* @param array $trigger_hosts
*
* @throws APIException
*/
private static function checkTriggersUpNotFromChildTemplatesOrHosts(array $trigger_dependencies,
array $trigger_hosts): void {
$templateids = [];
$template_triggers_up = [];
$dependency_templates = [];
foreach ($trigger_dependencies as $triggerid_up => $triggerids) {
$templateids_up = [];
if (array_key_exists('templateids', $trigger_hosts[$triggerid_up])) {
foreach ($trigger_hosts[$triggerid_up]['templateids'] as $templateid => $foo) {
$template_triggers_up[$templateid][] = $triggerid_up;
$templateids_up[$templateid] = true;
}
}
else {
foreach ($trigger_hosts[$triggerid_up]['hostids'] as $hostid => $foo) {
$template_triggers_up[$hostid][] = $triggerid_up;
$templateids_up[$hostid] = true;
}
}
foreach ($triggerids as $triggerid => $foo) {
foreach ($trigger_hosts[$triggerid]['templateids'] as $templateid => $foo) {
if (!array_key_exists($templateid, $dependency_templates)) {
$dependency_templates[$templateid] = [];
}
$dependency_templates[$templateid] += $templateids_up;
}
$templateids += $trigger_hosts[$triggerid]['templateids'];
}
}
$template_links = [];
do {
$options = [
'output' => ['hostid', 'templateid'],
'filter' => ['templateid' => array_keys($templateids)]
];
$result = DBselect(DB::makeSql('hosts_templates', $options));
$templateids = [];
while ($row = DBfetch($result)) {
$template_links[$row['templateid']][$row['hostid']] = true;
if (!array_key_exists($row['hostid'], $template_links)) {
$templateids[$row['hostid']] = true;
}
}
} while ($templateids);
if (!$template_links) {
return;
}
// Check if each trigger-up is a trigger from the child template or host of the dependent trigger.
foreach ($dependency_templates as $templateid => $templateids_up) {
if (array_key_exists($templateid, $template_links)) {
foreach ($templateids_up as $templateid_up => $foo) {
if (self::checkTemplateUpExistsInTemplateLinks($template_links, $templateid, $templateid_up)) {
foreach ($template_triggers_up[$templateid_up] as $triggerid_up) {
foreach ($trigger_dependencies[$triggerid_up] as $triggerid => $foo) {
if (array_key_exists($templateid, $trigger_hosts[$triggerid]['templateids'])) {
break 2;
}
}
}
$triggers = DB::select('triggers', [
'output' => ['description', 'flags'],
'triggerids' => [$triggerid, $triggerid_up],
'preservekeys' => true
]);
$templates = DB::select('hosts', [
'output' => ['host', 'status'],
'hostids' => $templateid_up
]);
if ($triggers[$triggerid]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL) {
$error = ($templates[0]['status'] == HOST_STATUS_TEMPLATE)
? _('Trigger "%1$s" cannot depend on the trigger "%2$s" from the template "%3$s", because dependencies on triggers from a child template or host are not allowed.')
: _('Trigger "%1$s" cannot depend on the trigger "%2$s" from the host "%3$s", because dependencies on triggers from a child template or host are not allowed.');
}
else {
$error = ($templates[0]['status'] == HOST_STATUS_TEMPLATE)
? _('Trigger prototype "%1$s" cannot depend on the trigger "%2$s" from the template "%3$s", because dependencies on triggers from a child template or host are not allowed.')
: _('Trigger prototype "%1$s" cannot depend on the trigger "%2$s" from the host "%3$s", because dependencies on triggers from a child template or host are not allowed.');
}
self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $triggers[$triggerid]['description'],
$triggers[$triggerid_up]['description'], $templates[0]['host']
));
}
}
}
}
}
/**
* Recursively check if the given template-up exists in the chain of the given template links.
*
* @param array $template_links[<templateid>][<templateid_up>]
* @param string $templateid
* @param string $templateid_up
*
* @return bool
*/
private static function checkTemplateUpExistsInTemplateLinks(array $template_links, string $templateid,
string $templateid_up): bool {
if (array_key_exists($templateid_up, $template_links[$templateid])) {
return true;
}
foreach ($template_links[$templateid] as $_templateid => $foo) {
if (array_key_exists($_templateid, $template_links)
&& self::checkTemplateUpExistsInTemplateLinks($template_links, $_templateid, $templateid_up)) {
return true;
}
}
return false;
}
/**
* Check if the triggers-up templates of the given trigger dependencies are linked to child templates of the
* dependent triggers.
*
* @param array $trigger_dependencies[<triggerid_up>][<triggerid>]
* @param array $trigger_hosts
*
* @throws APIException
*/
private static function checkTriggersUpTemplatesAreLinkedToChildTemplates(array $trigger_dependencies,
array $trigger_hosts): void {
$templateids_up = [];
$templateids = [];
foreach ($trigger_dependencies as $triggerid_up => $triggerids) {
if (!array_key_exists('templateids', $trigger_hosts[$triggerid_up])) {
unset($trigger_dependencies[$triggerid_up]);
continue;
}
$templateids_up += $trigger_hosts[$triggerid_up]['templateids'];
foreach ($triggerids as $triggerid => $foo) {
$templateids += $trigger_hosts[$triggerid]['templateids'];
}
}
$options = [
'output' => ['templateid', 'hostid'],
'filter' => [
'templateid' => array_keys($templateids)
]
];
$result = DBselect(DB::makeSql('hosts_templates', $options));
$template_links = [];
while ($row = DBfetch($result)) {
$template_links[$row['templateid']][$row['hostid']] = [];
}
$result = DBselect(
'SELECT ht.templateid,ht.hostid,htt.templateid AS host_templateid'.
' FROM hosts_templates ht,hosts_templates htt'.
' WHERE ht.hostid=htt.hostid'.
' AND ht.templateid!=htt.templateid'.
' AND '.dbConditionId('ht.templateid', array_keys($templateids)).
' AND '.dbConditionId('htt.templateid', array_keys($templateids_up))
);
while ($row = DBfetch($result)) {
$template_links[$row['templateid']][$row['hostid']][$row['host_templateid']] = true;
}
foreach ($trigger_dependencies as $triggerid_up => $triggerids) {
/*
* If trigger belongs to more than one template, then it is not possible to link only part of them to
* another host or template. That means each template ID of that trigger would have the same hosts
* in template links. And vice versa, if at least one of the trigger templates was not found in template
* links, then the trigger is not inherited further.
*/
$templateid_up = key($trigger_hosts[$triggerid_up]['templateids']);
foreach ($triggerids as $triggerid => $foo) {
$templateid = key($trigger_hosts[$triggerid]['templateids']);
if (!array_key_exists($templateid, $template_links)) {
continue;
}
foreach ($template_links[$templateid] as $hostid => $host_templateids) {
if (!array_key_exists($templateid_up, $host_templateids)) {
$triggers = DB::select('triggers', [
'output' => ['description', 'flags'],
'triggerids' => [$triggerid, $triggerid_up],
'preservekeys' => true
]);
$hosts = DB::select('hosts', [
'output' => ['host', 'status'],
'hostids' => [$templateid_up, $hostid],
'preservekeys' => true
]);
if ($triggers[$triggerid]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL) {
$error = ($hosts[$hostid]['status'] == HOST_STATUS_TEMPLATE)
? _('Trigger "%1$s" cannot depend on the trigger "%2$s", because the template "%3$s" is not linked to the template "%4$s".')
: _('Trigger "%1$s" cannot depend on the trigger "%2$s", because the template "%3$s" is not linked to the host "%4$s".');
}
else {
$error = ($hosts[$hostid]['status'] == HOST_STATUS_TEMPLATE)
? _('Trigger prototype "%1$s" cannot depend on the trigger "%2$s", because the template "%3$s" is not linked to the template "%4$s".')
: _('Trigger prototype "%1$s" cannot depend on the trigger "%2$s", because the template "%3$s" is not linked to the host "%4$s".');
}
self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $triggers[$triggerid]['description'],
$triggers[$triggerid_up]['description'], $hosts[$templateid_up]['host'],
$hosts[$hostid]['host']
));
}
}
}
}
}
/**
* Update the trigger dependencies.
*
* @param array $triggers
* @param array|null $db_triggers
*/
public static function updateDependencies(array &$triggers, array $db_triggers = null): void {
$ins_trigger_deps = [];
$del_triggerdepids = [];
$edit_dependencies = [];
foreach ($triggers as &$trigger) {
if (!array_key_exists('dependencies', $trigger)) {
continue;
}
$db_triggers_up = ($db_triggers !== null)
? array_column($db_triggers[$trigger['triggerid']]['dependencies'], null, 'triggerid')
: [];
foreach ($trigger['dependencies'] as &$trigger_up) {
if (array_key_exists($trigger_up['triggerid'], $db_triggers_up)) {
$trigger_up['triggerdepid'] = $db_triggers_up[$trigger_up['triggerid']]['triggerdepid'];
unset($db_triggers_up[$trigger_up['triggerid']]);
}
else {
$ins_trigger_deps[] = [
'triggerid_down' => $trigger['triggerid'],
'triggerid_up' => $trigger_up['triggerid']
];
$edit_dependencies[$trigger['triggerid']][$trigger_up['triggerid']] = true;
}
}
unset($trigger_up);
foreach ($db_triggers_up as $db_trigger_up) {
$del_triggerdepids[] = $db_trigger_up['triggerdepid'];
$edit_dependencies[$trigger['triggerid']][$db_trigger_up['triggerid']] = false;
}
}
unset($trigger);
if ($del_triggerdepids) {
DB::delete('trigger_depends', ['triggerdepid' => $del_triggerdepids]);
}
if ($ins_trigger_deps) {
$triggerdepids = DB::insertBatch('trigger_depends', $ins_trigger_deps);
}
foreach ($triggers as &$trigger) {
if (!array_key_exists('dependencies', $trigger)) {
continue;
}
foreach ($trigger['dependencies'] as &$trigger_up) {
if (!array_key_exists('triggerdepid', $trigger_up)) {
$trigger_up['triggerdepid'] = array_shift($triggerdepids);
}
}
unset($trigger_up);
}
unset($trigger);
if ($edit_dependencies) {
$edit_dependencies = self::getTemplatedDependencies($edit_dependencies);
if ($edit_dependencies) {
self::inheritDependencies($edit_dependencies);
}
}
}
/**
* Filter for dependencies whose dependent triggers belong to templates that are further linked
* to some templates or hosts.
*
* @param array $edit_dependencies[<triggerid>][<triggerid_up>]
*
* @return array
*/
protected static function getTemplatedDependencies(array $edit_dependencies): array {
$triggerids = DBfetchColumn(DBselect(
'SELECT DISTINCT f.triggerid'.
' FROM functions f,items i,hosts h'.
' WHERE f.itemid=i.itemid'.
' AND i.hostid=h.hostid'.
' AND '.dbConditionId('f.triggerid', array_keys($edit_dependencies)).
' AND '.dbConditionInt('h.status', [HOST_STATUS_TEMPLATE]).
' AND EXISTS('.
'SELECT NULL'.
' FROM hosts_templates ht,hosts h2'.
' WHERE i.hostid=ht.templateid'.
' AND ht.hostid=h2.hostid'.
' AND '.dbConditionInt('h2.flags', [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_CREATED]).
')'
), 'triggerid');
return array_intersect_key($edit_dependencies, array_flip($triggerids));
}
/**
* Inherit the given trigger dependencies.
*
* @param array $edit_dependencies[<triggerid>][<triggerid_up>]
* @param array $hostids
*/
protected static function inheritDependencies(array $edit_dependencies, array $hostids = null): void {
$all_triggerids = $edit_dependencies;
$all_triggerids_up = [];
foreach ($edit_dependencies as $triggerids_up) {
$all_triggerids += $triggerids_up;
$all_triggerids_up += $triggerids_up;
}
$hostids_condition = ($hostids !== null) ? ' AND '.dbConditionId('i.hostid', $hostids) : '';
$result = DBselect(
'SELECT DISTINCT t.templateid,t.triggerid,t.flags,i.hostid'.
' FROM triggers t,functions f,items i'.
' WHERE t.triggerid=f.triggerid'.
' AND f.itemid=i.itemid'.
' AND '.dbConditionId('t.templateid', array_keys($all_triggerids)).
$hostids_condition
);
$trigger_flags = [];
$trigger_links = [];
$tpl_triggerids_up = [];
while ($row = DBfetch($result)) {
$trigger_links[$row['templateid']][$row['hostid']] = $row['triggerid'];
$trigger_flags[$row['triggerid']] = $row['flags'];
if (array_key_exists($row['templateid'], $all_triggerids_up)) {
$tpl_triggerids_up[$row['templateid']] = true;
}
}
$del_triggerdepids = [];
$_edit_dependencies = [];
if ($tpl_triggerids_up) {
if ($hostids === null) {
$result = DBselect(
'SELECT DISTINCT t.triggerid,td.triggerid_up,td.triggerdepid,i.hostid'.
' FROM triggers t,trigger_depends td,triggers tt,functions f,items i'.
' WHERE t.triggerid=td.triggerid_down'.
' AND td.triggerid_up=tt.triggerid'.
' AND tt.triggerid=f.triggerid'.
' AND f.itemid=i.itemid'.
' AND '.dbConditionId('t.templateid', array_keys($edit_dependencies)).
' AND '.dbConditionId('tt.templateid', array_keys($tpl_triggerids_up))
);
}
else {
$result = DBselect(
'SELECT DISTINCT t.triggerid,td.triggerid_up,td.triggerdepid,ii.hostid'.
' FROM triggers t,functions f,items i,trigger_depends td,triggers tt,functions ff,items ii'.
' WHERE t.triggerid=f.triggerid'.
' AND f.itemid=i.itemid'.
' AND t.triggerid=td.triggerid_down'.
' AND td.triggerid_up=tt.triggerid'.
' AND tt.triggerid=ff.triggerid'.
' AND ff.itemid=ii.itemid'.
' AND '.dbConditionId('t.templateid', array_keys($edit_dependencies)).
' AND '.dbConditionId('i.hostid', $hostids).
' AND '.dbConditionId('tt.templateid', array_keys($tpl_triggerids_up))
);
}
$tpl_child_dependencies = [];
while ($row = DBfetch($result)) {
$tpl_child_dependencies[$row['triggerid']][$row['triggerid_up']][$row['hostid']] = $row['triggerdepid'];
}
foreach ($edit_dependencies as $triggerid => $triggerids_up) {
$triggerids_up = array_intersect_key($triggerids_up, $tpl_triggerids_up);
if (!$triggerids_up) {
continue;
}
foreach ($trigger_links[$triggerid] as $hostid => $child_triggerid) {
$upd_child_triggerids_up = [];
if (array_key_exists($child_triggerid, $tpl_child_dependencies)) {
foreach ($tpl_child_dependencies[$child_triggerid] as $child_triggerid_up => $hostids_up) {
$hostid_up = key($hostids_up);
$triggerdepid = reset($hostids_up);
if (in_array($child_triggerid_up, $upd_child_triggerids_up)
|| in_array($triggerdepid, $del_triggerdepids)) {
continue;
}
foreach ($triggerids_up as $triggerid_up => $add) {
if (array_key_exists($hostid_up, $trigger_links[$triggerid_up])
&& bccomp($child_triggerid_up, $trigger_links[$triggerid_up][$hostid_up]) == 0) {
if ($add) {
$upd_child_triggerids_up[] = $child_triggerid_up;
}
else {
$del_triggerdepids[] = $hostids_up[$hostid_up];
$_edit_dependencies[$child_triggerid][$child_triggerid_up] = false;
}
}
}
}
}
foreach ($triggerids_up as $triggerid_up => $add) {
if ($add) {
$child_triggerid_up = $trigger_links[$triggerid_up][$hostid];
if (!in_array($child_triggerid_up, $upd_child_triggerids_up)) {
$_edit_dependencies[$child_triggerid][$child_triggerid_up] = true;
}
}
}
}
}
}
$host_triggerids_up = array_diff_key($all_triggerids_up, $tpl_triggerids_up);
if ($host_triggerids_up) {
if ($hostids === null) {
$result = DBselect(
'SELECT DISTINCT t.triggerid,td.triggerid_up,td.triggerdepid'.
' FROM triggers t,trigger_depends td'.
' WHERE t.triggerid=td.triggerid_down'.
' AND '.dbConditionId('t.templateid', array_keys($edit_dependencies)).
' AND '.dbConditionId('td.triggerid_up', array_keys($host_triggerids_up))
);
}
else {
$result = DBselect(
'SELECT DISTINCT t.triggerid,td.triggerid_up,td.triggerdepid'.
' FROM triggers t,functions f,items i,trigger_depends td'.
' WHERE t.triggerid=f.triggerid'.
' AND f.itemid=i.itemid'.
' AND t.triggerid=td.triggerid_down'.
' AND '.dbConditionId('t.templateid', array_keys($edit_dependencies)).
' AND '.dbConditionId('i.hostid', $hostids).
' AND '.dbConditionId('td.triggerid_up', array_keys($host_triggerids_up))
);
}
$host_child_dependencies = [];
while ($row = DBfetch($result)) {
$host_child_dependencies[$row['triggerid']][$row['triggerid_up']] = $row['triggerdepid'];
}
foreach ($edit_dependencies as $triggerid => $triggerids_up) {
$triggerids_up = array_intersect_key($triggerids_up, $host_triggerids_up);
if (!$triggerids_up) {
continue;
}
foreach ($trigger_links[$triggerid] as $hostid => $child_triggerid) {
$upd_child_triggerids_up = [];
if (array_key_exists($child_triggerid, $host_child_dependencies)) {
foreach ($host_child_dependencies[$child_triggerid] as $child_triggerid_up => $triggerdepid) {
if (array_key_exists($child_triggerid_up, $triggerids_up)) {
$add = $triggerids_up[$child_triggerid_up];
if ($add) {
$upd_child_triggerids_up[] = $child_triggerid_up;
}
else {
$del_triggerdepids[] = $triggerdepid;
$_edit_dependencies[$child_triggerid][$child_triggerid_up] = false;
}
}
}
}
foreach ($triggerids_up as $triggerid_up => $add) {
if ($add && !in_array($triggerid_up, $upd_child_triggerids_up)) {
$_edit_dependencies[$child_triggerid][$triggerid_up] = true;
}
}
}
}
}
$ins_dependencies = [];
$del_dependencies = [];
$ins_trigger_deps = [];
foreach ($_edit_dependencies as $triggerid => $triggerids_up) {
foreach ($triggerids_up as $triggerid_up => $add) {
if ($add) {
if ($trigger_flags[$triggerid] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
if (array_key_exists($triggerid_up, $trigger_flags)
&& $trigger_flags[$triggerid_up] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
$ins_dependencies[$triggerid_up][$triggerid] = true;
}
}
else {
$ins_dependencies[$triggerid_up][$triggerid] = true;
}
$ins_trigger_deps[] = [
'triggerid_down' => $triggerid,
'triggerid_up' => $triggerid_up
];
}
else {
$del_dependencies[$triggerid_up][$triggerid] = true;
}
}
}
if ($ins_dependencies) {
self::checkCircularDependencies($ins_dependencies, $del_dependencies, true);
}
if ($del_triggerdepids) {
DB::delete('trigger_depends', ['triggerdepid' => $del_triggerdepids]);
}
if ($ins_trigger_deps) {
DB::insertBatch('trigger_depends', $ins_trigger_deps);
}
if ($_edit_dependencies) {
$_edit_dependencies = self::getTemplatedDependencies($_edit_dependencies);
if ($_edit_dependencies) {
self::inheritDependencies($_edit_dependencies);
}
}
}
/**
* Inherit the trigger dependencies of the given templates to the given hosts.
*
* @param array $templateids
* @param array $hostids
*/
public static function syncTemplateDependencies(array $templateids, array $hostids): void {
$result = DBselect(
'SELECT DISTINCT f.triggerid,td.triggerid_up'.
' FROM items i,functions f,trigger_depends td'.
' WHERE i.itemid=f.itemid'.
' AND f.triggerid=td.triggerid_down'.
' AND '.dbConditionId('i.hostid', $templateids)
);
$edit_dependencies = [];
while ($row = DBfetch($result)) {
$edit_dependencies[$row['triggerid']][$row['triggerid_up']] = true;
}
if ($edit_dependencies) {
$edit_dependencies = self::getTemplatedDependencies($edit_dependencies);
if ($edit_dependencies) {
self::inheritDependencies($edit_dependencies, $hostids);
}
}
}
}