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.

557 lines
17 KiB

<?php declare(strict_types = 0);
/*
** 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.
**/
/**
* A converter to convert trigger expression syntax from 5.2 to 5.4.
*/
class C52TriggerExpressionConverter extends CConverter {
/**
* Functions which are not related to item.
*
* @var array
*/
protected $standalone_functions;
/**
* State of each host reference being present in some non-standalone function.
*
* @var array
*/
protected $hanged_refs = [];
/**
* Host for simplified functions.
*
* @var string|null
*/
protected $host;
/**
* Item for simplified functions.
*
* @var string|null
*/
protected $item;
/**
* Old trigger expression syntax parser.
*
* @var C10TriggerExpression
*/
protected $parser;
public function __construct() {
$this->parser = new C10TriggerExpression(['allow_func_only' => true]);
$this->standalone_functions = getStandaloneFunctions();
}
/**
* Converts trigger expression to new syntax.
*
* @param array $trigger_data
* @param string $trigger_data['expression']
* @param string $trigger_data['host'] (optional)
* @param string $trigger_data['item'] (optional)
*
* @return string
*/
public function convert($trigger_data) {
$this->item = (array_key_exists('item', $trigger_data) && $trigger_data['item']) ? $trigger_data['item'] : '';
$this->host = (array_key_exists('host', $trigger_data) && $this->item) ? $trigger_data['host'] : '';
if ($this->parser->parse($trigger_data['expression']) !== false) {
$functions = $this->parser->result->getTokensByType(C10TriggerExprParserResult::TOKEN_TYPE_FUNCTION_MACRO);
$this->hanged_refs = $this->checkHangedFunctionsPerHost($functions);
$extra_expressions = [];
for ($i = count($functions) - 1; $i >= 0; $i--) {
[$new_expr, $extra_expr] = $this->convertFunction($functions[$i]['data'], $this->host, $this->item);
$trigger_data['expression'] = substr_replace($trigger_data['expression'], $new_expr,
$functions[$i]['pos'], $functions[$i]['length']
);
if ($extra_expr !== null) {
$extra_expressions[] = $extra_expr;
}
}
if ($extra_expressions) {
$extra_expressions = array_keys(array_flip($extra_expressions));
$trigger_data['expression'] = '('.$trigger_data['expression'].')';
$extra_expressions = array_reverse($extra_expressions);
$trigger_data['expression'] .= ' or '.implode(' or ', $extra_expressions);
}
}
return $trigger_data['expression'];
}
/**
* Convert function to new syntax.
*
* @param array $fn Function to convert.
* @param string $host_name Host name.
* @param string $item_key Item key.
*
* @return array
*/
protected function convertFunction(array $fn, string $host_name, string $item_key): array {
if ($fn['item'] === '' && $fn['host'] === '') {
$query = sprintf('/%s/%s', $host_name, $item_key);
$has_hanged_functions = $this->hanged_refs[''];
}
else {
$query = sprintf('/%s/%s', $fn['host'], $fn['item']);
$has_hanged_functions = array_key_exists($fn['host'], $this->hanged_refs)
? $this->hanged_refs[$fn['host']]
: false;
}
$extra_expr = null;
$parameters = [
'unquotable' => array_filter($fn['functionParamsRaw']['parameters'], function ($param) {
return ($param['type'] == C10FunctionParser::PARAM_UNQUOTED && $param['raw'] === '');
}),
'indicated' => array_filter($fn['functionParamsRaw']['parameters'], function ($param) {
return ($param['type'] == C10FunctionParser::PARAM_QUOTED || $param['raw'] !== '');
})
];
switch ($fn['functionName']) {
case 'abschange':
$new_expression = sprintf('abs(change(%1$s))', $query);
break;
case 'band':
$params = self::convertParameters($fn['functionParams'], $parameters, $fn['functionName']);
$timeshift = self::paramsToString([$params[0]]);
$mask = self::paramsToString([$params[1]]);
$new_expression = sprintf('bitand(last(%1$s%2$s)%3$s)', $query, $timeshift, $mask);
break;
case 'change':
$new_expression = sprintf('change(%1$s)', $query);
break;
case 'delta':
$params = self::convertParameters($fn['functionParams'], $parameters, $fn['functionName']);
$params = self::paramsToString($params);
$new_expression = sprintf('(max(%1$s%2$s)-min(%1$s%2$s))', $query, $params);
break;
case 'diff':
$new_expression = sprintf('(last(%1$s,#1)<>last(%1$s,#2))', $query);
break;
case 'prev':
$new_expression = sprintf('last(%1$s,#2)', $query);
break;
case 'trenddelta':
$params = self::convertParameters($fn['functionParams'], $parameters, $fn['functionName']);
$params = self::paramsToString($params);
$new_expression = sprintf('(trendmax(%1$s%2$s)-trendmin(%1$s%2$s))', $query, $params);
break;
case 'iregexp':
case 'regexp':
case 'str':
$params = self::convertParameters($fn['functionParams'], $parameters, $fn['functionName']);
$params = self::paramsToString($params);
$new_expression = sprintf('find(%1$s%2$s)', $query, $params);
break;
case 'strlen':
$params = self::convertParameters($fn['functionParams'], $parameters, $fn['functionName']);
$params = self::paramsToString($params);
$new_expression = sprintf('length(last(%1$s%2$s))', $query, $params);
break;
case 'date':
case 'dayofmonth':
case 'dayofweek':
case 'time':
case 'now':
$new_expression = $fn['functionName'].'()';
if (!$has_hanged_functions) {
$extra_expr = sprintf('(last(%1$s)<>last(%1$s))', $query);
}
break;
case 'logseverity':
$new_expression = sprintf('logseverity(%1$s)', $query);
break;
default:
$new_expression = sprintf('%s(%s%s)', $fn['functionName'], $query,
self::paramsToString(self::convertParameters($fn['functionParams'], $parameters,
$fn['functionName']
))
);
break;
}
return [$new_expression, $extra_expr];
}
/**
* Convert function parameters to new syntax.
*
* @param array $parameters List of parameters according to the previous syntax.
* @param array $param_dets
* @param array $param_dets['unquotable'] List of numeric indexes for parameters that don't need to be quoted.
* @param array $param_dets['indicated'] List of numeric indexes for parameters that are especially indicated and
* must be kept.
* @param string $fn_name Function name.
*
* @return array
*/
private static function convertParameters(array $parameters, array $param_dets, string $fn_name): array {
switch ($fn_name) {
// (sec|#num,<time_shift>)
case 'delta':
case 'avg':
case 'max':
case 'min':
case 'sum':
// (sec|#num,<time_shift>,percentage)
case 'percentile':
$parameters += ['', ''];
$parameters[0] = self::convertParamSec($parameters[0]);
$parameters[1] = self::convertTimeshift($parameters[1]);
$parameters[0] = ((string) $parameters[0] === '0') ? '#1' : $parameters[0];
if ($parameters[1] !== '') {
$parameters[0] .= ':'.$parameters[1];
}
unset($parameters[1], $param_dets['unquotable'][1], $param_dets['indicated'][1]);
break;
// (sec|#num,<time_shift>,threshold,<fit>)
case 'timeleft':
$parameters += ['', '', '', ''];
$parameters[0] = self::convertParamSec($parameters[0]);
$parameters[1] = self::convertTimeshift($parameters[1]);
$parameters[0] = ((string) $parameters[0] === '0') ? '#1' : $parameters[0];
if ($parameters[1] !== '') {
$parameters[0] .= ':'.$parameters[1];
}
unset($parameters[1], $param_dets['unquotable'][1], $param_dets['indicated'][1]);
if ($parameters[3] === '') {
// Don't quote unspecified <fit>.
$param_dets['unquotable'][3] = true;
}
break;
// (<#num>,<time_shift>)
case 'strlen':
case 'last':
$parameters += ['', ''];
if (!self::isMacro($parameters[0])
&& (substr($parameters[0], 0, 1) !== '#'
|| !ctype_digit(substr($parameters[0], 1))
|| (int) substr($parameters[0], 1) === 0)) {
$parameters[0] = '';
}
$parameters[1] = self::convertTimeshift($parameters[1]);
if ($parameters[1] !== '') {
$parameters[0] = ($parameters[0] === '') ? '#1' : $parameters[0];
$parameters[0] .= ':'.$parameters[1];
}
unset($parameters[1], $param_dets['unquotable'][1], $param_dets['indicated'][1]);
break;
// (sec|#num,<time_shift>,time,<fit>,<mode>)
case 'forecast':
$parameters += ['', '', '', '', ''];
$parameters[0] = self::convertParamSec($parameters[0]);
$parameters[1] = self::convertTimeshift($parameters[1]);
$parameters[0] = ((string) $parameters[0] === '0') ? '#1' : $parameters[0];
if ($parameters[1] !== '') {
$parameters[0] .= ':'.$parameters[1];
}
unset($parameters[1], $param_dets['unquotable'][1], $param_dets['indicated'][1]);
$parameters[2] = self::convertParamSec($parameters[2]);
if ($parameters[3] === '') {
// Don't quote unspecified <fit>.
$param_dets['unquotable'][3] = true;
}
if ($parameters[4] === '') {
// Don't quote unspecified <mode>.
$param_dets['unquotable'][4] = true;
}
break;
// (<sec|#num>,mask,<time_shift>)
case 'band':
$parameters += ['', '', ''];
if (!self::isMacro($parameters[0])
&& (substr($parameters[0], 0, 1) !== '#'
|| !ctype_digit(substr($parameters[0], 1))
|| (int) substr($parameters[0], 1) === 0)) {
$parameters[0] = '';
}
$parameters[2] = self::convertTimeshift($parameters[2]);
if ($parameters[2] !== '') {
$parameters[0] = ($parameters[0] === '') ? '#1' : $parameters[0];
$parameters[0] .= ':'.$parameters[2];
}
unset($parameters[2], $param_dets['unquotable'][2], $param_dets['indicated'][2]);
break;
// (sec|#num,<pattern>,<operator>,<time_shift>)
case 'count':
$parameters += ['', '', '', ''];
$parameters[0] = self::convertParamSec($parameters[0]);
$parameters[3] = self::convertTimeshift($parameters[3]);
$parameters[0] = ((string) $parameters[0] === '0') ? '#1' : $parameters[0];
if ($parameters[3] !== '') {
$parameters[0] .= ':'.$parameters[3];
}
if ($parameters[2] === 'band') {
$parameters[2] = 'bitand';
}
elseif ($parameters[2] === '') {
// Don't quote unspecified <operator>.
$param_dets['unquotable'][2] = true;
}
$parameters[3] = $parameters[1];
unset($param_dets['unquotable'][3], $param_dets['indicated'][3], $parameters[1]);
if (array_key_exists(1, $param_dets['unquotable'])) {
$param_dets['unquotable'][3] = true;
unset($param_dets['unquotable'][1]);
}
if (array_key_exists(1, $param_dets['indicated'])) {
$param_dets['indicated'][3] = true;
unset($param_dets['indicated'][1]);
}
break;
// (sec,<mode>)
case 'nodata':
$parameters += ['', ''];
$parameters[0] = self::convertParamSec($parameters[0]);
if ($parameters[1] === '') {
// Don't quote unspecified <mode>.
$param_dets['unquotable'][1] = true;
}
break;
// (sec)
case 'fuzzytime':
$parameters += [''];
$parameters[0] = self::convertParamSec($parameters[0]);
break;
// (<pattern>,<sec|#num>)
case 'iregexp':
case 'regexp':
case 'str':
$parameters += ['', ''];
$parameters = [
self::convertParamSec($parameters[1]),
($fn_name === 'str') ? 'like' : $fn_name,
$parameters[0]
];
unset($param_dets['unquotable'][1]);
if (array_key_exists(0, $param_dets['indicated'])) {
$param_dets['indicated'][2] = true;
unset($param_dets['indicated'][0]);
}
break;
// (period,period_shift)
case 'trendavg':
case 'trendcount':
case 'trenddelta':
case 'trendmax':
case 'trendmin':
case 'trendsum':
$parameters += ['', ''];
$parameters[0] = self::convertParamPeriod($parameters[0]);
if ($parameters[1] !== '') {
$parameters[0] .= ':'.$parameters[1];
}
unset($parameters[1], $param_dets['unquotable'][1], $param_dets['indicated'][1]);
break;
case 'logeventid':
case 'logsource':
array_unshift($parameters, '');
if (array_key_exists(0, $param_dets['indicated'])) {
$param_dets['indicated'][1] = true;
unset($param_dets['indicated'][0]);
}
break;
}
// Keys in $parameters array to skip from quoting.
$param_dets['unquotable'] = array_keys($param_dets['unquotable']);
$param_dets['indicated'] = array_keys($param_dets['indicated']);
$functions_with_period_parameter = ['delta', 'avg', 'max', 'min', 'sum', 'last', 'strlen', 'percentile',
'timeleft', 'forecast', 'band', 'count', 'fuzzytime', 'nodata', 'iregexp', 'regexp', 'str', 'trendavg',
'trendcount', 'trenddelta', 'trendmax', 'trendmin', 'trendsum', 'logeventid', 'logsource'
];
if (in_array($fn_name, $functions_with_period_parameter)) {
$param_dets['unquotable'][] = 0;
}
if (in_array($fn_name, ['forecast', 'timeleft', 'percentile'])) {
// Time parameter don't need to be quoted for forecast() function.
$param_dets['unquotable'][] = 2;
}
elseif ($fn_name === 'band') {
// Mask parameter don't need to be quoted for bitand() function.
$param_dets['unquotable'][] = 1;
}
array_walk($parameters, function (&$param, $i) use ($param_dets) {
if (in_array($i, $param_dets['unquotable'])) {
return;
}
$param = CHistFunctionParser::quoteParam($param);
});
// Remove empty parameters from the end of the parameters array.
foreach (array_reverse(array_keys($parameters)) as $i) {
if (in_array($i, $param_dets['indicated'])) {
break;
}
if ($parameters[$i] !== '""' && $parameters[$i] !== '') {
break;
}
unset($parameters[$i]);
}
return array_values($parameters);
}
/**
* Convert seconds.
*
* @param string $param Parameter to convert.
*
* @return string
*/
private static function convertParamSec(string $param): string {
return (preg_match('/^(?<num>\d+)(?<suffix>['.ZBX_TIME_SUFFIXES.']{0,1})$/', $param, $m) && $m['num'] > 0)
? $m['num'].($m['suffix'] !== '' ? $m['suffix'] : 's')
: $param;
}
/**
* Convert period.
*
* @param string $param Parameter to convert.
*
* @return string
*/
private static function convertParamPeriod(string $param): string {
return (preg_match('/^(?<num>\d+)(?<suffix>[hdwMy]{0,1})$/', $param, $m) && $m['num'] > 0)
? $m['num'].($m['suffix'] !== '' ? $m['suffix'] : 's')
: $param;
}
/**
* Convert time shift.
*
* @param string $param Parameter to convert.
*
* @return string
*/
private static function convertTimeshift(string $param): string {
$param = (preg_match('/^(?<num>\d+)(?<suffix>['.ZBX_TIME_SUFFIXES.']{0,1})$/', $param, $m) && $m['num'] > 0)
? $m['num'].($m['suffix'] !== '' ? $m['suffix'] : 's')
: $param;
return ($param !== '') ? 'now-'.$param : '';
}
/**
* Concatenate parameters into comma separated string.
*
* @param array $parameters Parameter to concatenate.
*
* @return string
*/
private static function paramsToString(array $parameters): string {
$parameters = rtrim(implode(',', $parameters), ',');
return ($parameters === '') ? '' : ','.$parameters;
}
/**
* Check if each particular host reference would be linked through at least one functions according to the new
* trigger expression syntax.
*
* @param array $tokens
*
* @return array
*/
protected function checkHangedFunctionsPerHost(array $tokens): array {
$hanged_refs = [];
foreach ($tokens as $token) {
$fn = $token['data'];
if (!array_key_exists($fn['host'], $hanged_refs)) {
$hanged_refs[$fn['host']] = false;
}
if (!in_array($fn['functionName'], $this->standalone_functions)) {
$hanged_refs[$fn['host']] = true;
}
}
return $hanged_refs;
}
/**
* Check if given string is valid user or lld macro.
*
* @param string $param
*
* @return bool
*/
private static function isMacro(string $param): bool {
foreach ([new CUserMacroParser(), new CLLDMacroParser(), new CLLDMacroFunctionParser()] as $parser) {
if ($parser->parse($param) == CParser::PARSE_SUCCESS) {
return true;
}
}
return false;
}
}