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.

659 lines
20 KiB

1 year ago
<?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.
**/
/**
* A class for interacting with the Zabbix server.
*
* Class CZabbixServer
*/
class CZabbixServer {
/**
* Return item queue overview.
*/
const QUEUE_OVERVIEW = 'overview';
/**
* Return item queue overview by proxy.
*/
const QUEUE_OVERVIEW_BY_PROXY = 'overview by proxy';
/**
* Return a detailed item queue.
*/
const QUEUE_DETAILS = 'details';
/**
* Response value if the request has been executed successfully.
*/
const RESPONSE_SUCCESS = 'success';
/**
* Response value if an error occurred.
*/
const RESPONSE_FAILED = 'failed';
/**
* Auxiliary constants for request() method.
*/
const ZBX_TCP_EXPECT_HEADER = 1;
const ZBX_TCP_EXPECT_DATA = 2;
/**
* Max number of bytes to read from the response for each iteration.
*/
const READ_BYTES_LIMIT = 8192;
/**
* Zabbix server host name.
*
* @var string|null
*/
protected $host;
/**
* Zabbix server port number.
*
* @var int|null
*/
protected $port;
/**
* Request connect timeout.
*
* @var int
*/
protected $connect_timeout;
/**
* Request timeout.
*
* @var int
*/
protected $timeout;
/**
* Maximum response size. If the size of the response exceeds this value, an error will be triggered.
*
* @var int
*/
protected $totalBytesLimit;
/**
* Zabbix server socket resource.
*
* @var resource
*/
protected $socket;
/**
* Error message.
*
* @var string
*/
protected $error;
/**
* Total result count (if any).
*
* @var int
*/
protected $total;
/**
* @var array $debug Section 'debug' data from server response.
*/
protected $debug = [];
/**
* Class constructor.
*
* @param string|null $host
* @param int|null $port
* @param int $connect_timeout
* @param int $timeout
* @param int $totalBytesLimit
*/
public function __construct($host, $port, $connect_timeout, $timeout, $totalBytesLimit) {
$this->host = $host;
$this->port = $port;
$this->connect_timeout = $connect_timeout;
$this->timeout = $timeout;
$this->totalBytesLimit = $totalBytesLimit;
}
/**
* Executes a script on the given host or event and returns the result.
*
* @param string $scriptid
* @param string $sid
* @param null|string $hostid
* @param null|string $eventid
*
* @return bool|array
*/
public function executeScript(string $scriptid, string $sid, ?string $hostid = null, ?string $eventid = null) {
$params = [
'request' => 'command',
'scriptid' => $scriptid,
'sid' => $sid,
'clientip' => CWebUser::getIp()
];
if ($hostid !== null) {
$params['hostid'] = $hostid;
}
if ($eventid !== null) {
$params['eventid'] = $eventid;
}
return $this->request($params);
}
/**
* @param array $data
* string $data['itemid'] (optional) Item ID.
* string $data['host'] (optional) Technical name of the host.
* string $data['key'] (optional) Item key.
* string $data['value'] Item value.
* string $data['clock'] (optional) Time when the value was received.
* string $data['ns'] (optional) Nanoseconds when the value was received.
* @param string $sid User session ID or user API token.
*
* @return array|bool
*/
public function pushHistory(array $data, string $sid) {
return $this->request([
'request' => 'history.push',
'data' => $data,
'sid' => $sid,
'clientip' => CWebUser::getIp()
]);
}
/**
* Request server to test item preprocessing steps.
*
* @param array $data Array of preprocessing steps test.
* @param string $data['value'] Value to use for preprocessing step testing.
* @param int $data['value_type'] Item value type.
* @param array $data['history'] Previous value object.
* @param string $data['history']['value'] Previous value.
* @param string $data['history']['timestamp'] Previous value time.
* @param array $data['steps'] Preprocessing step object.
* @param int $data['steps'][]['type'] Type of preprocessing step.
* @param string $data['steps'][]['params'] Parameters of preprocessing step.
* @param int $data['steps'][]['error_handler'] Error handler selected as "custom on fail".
* @param string $data['steps'][]['error_handler_params'] Parameters configured for selected error handler.
* @param string $sid User session ID.
*
* @return array
*/
public function testPreprocessingSteps(array $data, $sid) {
return $this->request([
'request' => 'preprocessing.test',
'data' => $data,
'sid' => $sid
]);
}
/**
* Request server to test item.
*
* @param array $data Array of item properties to test.
* @param string $sid User session ID.
*
* @return array
*/
public function testItem(array $data, $sid) {
/*
* Timeout for 'item.test' request is increased because since message can be forwarded from server to proxy and
* later to agent, it might take more time due network latency.
*/
$this->timeout = 60;
return $this->request([
'request' => 'item.test',
'data' => $data,
'sid' => $sid
]);
}
/**
* Retrieve item queue information.
*
* Possible $type values:
* - self::QUEUE_OVERVIEW
* - self::QUEUE_OVERVIEW_BY_PROXY
* - self::QUEUE_DETAILS
*
* @param string $type
* @param string $sid user session ID
* @param int $limit item count for details type
*
* @return bool|array
*/
public function getQueue($type, $sid, $limit = 0) {
$request = [
'request' => 'queue.get',
'sid' => $sid,
'type' => $type
];
if ($type == self::QUEUE_DETAILS) {
$request['limit'] = $limit;
}
return $this->request($request);
}
/**
* Request server to test media type.
*
* @param array $data Array of media type test data to send.
* @param string $data['mediatypeid'] Media type ID.
* @param string $data['sendto'] Message destination.
* @param string $data['subject'] Message subject.
* @param string $data['message'] Message body.
* @param string $data['params'] Custom parameters for media type webhook.
* @param string $sid User session ID.
*
* @return bool|array
*/
public function testMediaType(array $data, $sid) {
return $this->request([
'request' => 'alert.send',
'sid' => $sid,
'data' => $data
]);
}
/**
* Request server to test report.
*
* @param array $data Array of report test data to send.
* @param string $data['name'] Report name (used to make attachment file name).
* @param string $data['dashboardid'] Dashboard ID.
* @param string $data['userid'] User ID used to access the dashboard.
* @param string $data['period'] Report period. Possible values:
* 0 - ZBX_REPORT_PERIOD_DAY;
* 1 - ZBX_REPORT_PERIOD_WEEK;
* 2 - ZBX_REPORT_PERIOD_MONTH;
* 3 - ZBX_REPORT_PERIOD_YEAR.
* @param string $data['now'] Report generation time (seconds since Epoch).
* @param array $data['params'] Report parameters.
* @param string $data['params']['subject'] Report message subject.
* @param string $data['params']['body'] Report message text.
* @param string $sid User session ID.
*
* @return bool|array
*/
public function testReport(array $data, string $sid) {
return $this->request([
'request' => 'report.test',
'sid' => $sid,
'data' => $data
]);
}
/**
* Retrieve System information.
*
* @param $sid
*
* @return bool|array
*/
public function getStatus($sid) {
$response = $this->request([
'request' => 'status.get',
'type' => 'full',
'sid' => $sid
]);
if ($response === false) {
return false;
}
$api_input_rules = ['type' => API_OBJECT, 'fields' => [
'template stats' => ['type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'fields' => [
'count' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => '0:'.ZBX_MAX_INT32]
]],
'host stats' => ['type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'fields' => [
'attributes' => ['type' => API_OBJECT, 'flags' => API_REQUIRED, 'fields' => [
'proxyid' => ['type' => API_ID, 'flags' => API_REQUIRED],
'status' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [HOST_STATUS_MONITORED, HOST_STATUS_NOT_MONITORED])]
]],
'count' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => '0:'.ZBX_MAX_INT32]
]],
'item stats' => ['type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'fields' => [
'attributes' => ['type' => API_OBJECT, 'flags' => API_REQUIRED, 'fields' => [
'proxyid' => ['type' => API_ID, 'flags' => API_REQUIRED],
'status' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [ITEM_STATUS_ACTIVE, ITEM_STATUS_DISABLED])],
'state' => ['type' => API_INT32, 'in' => implode(',', [ITEM_STATE_NORMAL, ITEM_STATE_NOTSUPPORTED])]
]],
'count' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => '0:'.ZBX_MAX_INT32]
]],
'trigger stats' => ['type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'fields' => [
'attributes' => ['type' => API_OBJECT, 'flags' => API_REQUIRED, 'fields' => [
'status' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [TRIGGER_STATUS_ENABLED, TRIGGER_STATUS_DISABLED])],
'value' => ['type' => API_INT32, 'in' => implode(',', [TRIGGER_VALUE_FALSE, TRIGGER_VALUE_TRUE])]
]],
'count' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => '0:'.ZBX_MAX_INT32]
]],
'user stats' => ['type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'fields' => [
'attributes' => ['type' => API_OBJECT, 'flags' => API_REQUIRED, 'fields' => [
'status' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [ZBX_SESSION_ACTIVE, ZBX_SESSION_PASSIVE])]
]],
'count' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => '0:'.ZBX_MAX_INT32]
]],
// only for super-admins 'required performance' is available
'required performance' => ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY, 'fields' => [
'attributes' => ['type' => API_OBJECT, 'flags' => API_REQUIRED, 'fields' => [
'proxyid' => ['type' => API_ID, 'flags' => API_REQUIRED]
]],
'count' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED] // API_FLOAT 0-n
]],
'server stats' => ['type' => API_OBJECT, 'flags' => API_REQUIRED, 'fields' => [
'version' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED]
]]
]];
if (!CApiInputValidator::validate($api_input_rules, $response, '/', $this->error)) {
return false;
}
return $response;
}
/**
* Returns true if the Zabbix server is running and false otherwise.
*
* @param $sid
*
* @return bool
*/
public function isRunning($sid) {
$active_node = API::getApiService('hanode')->get([
'output' => ['address', 'port', 'lastaccess'],
'filter' => ['status' => ZBX_NODE_STATUS_ACTIVE],
'sortfield' => 'lastaccess',
'sortorder' => 'DESC',
'limit' => 1
], false);
if ($active_node && $active_node[0]['address'] === $this->host && $active_node[0]['port'] == $this->port) {
if ((time() - $active_node[0]['lastaccess']) <
timeUnitToSeconds(CSettingsHelper::getGlobal(CSettingsHelper::HA_FAILOVER_DELAY))) {
return true;
}
}
$response = $this->request([
'request' => 'status.get',
'type' => 'ping',
'sid' => $sid
]);
if ($response === false) {
return false;
}
$api_input_rules = ['type' => API_OBJECT, 'fields' => []];
return CApiInputValidator::validate($api_input_rules, $response, '/', $this->error);
}
/**
* Evaluate trigger expressions.
*
* @param array $data
* @param string $sid
*
* @return bool|array
*/
public function expressionsEvaluate(array $data, string $sid) {
$response = $this->request([
'request' => 'expressions.evaluate',
'sid' => $sid,
'data' => $data
]);
if ($response === false) {
return false;
}
$api_input_rules = ['type' => API_OBJECTS, 'fields' => [
'expression' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED],
'value' => ['type' => API_INT32, 'in' => '0,1'],
'error' => ['type' => API_STRING_UTF8]
]];
if (!CApiInputValidator::validate($api_input_rules, $response, '/', $this->error)) {
return false;
}
return $response;
}
/**
* Returns the error message.
*
* @return string
*/
public function getError() {
return $this->error;
}
/**
* Returns the total result count.
*
* @return int|null
*/
public function getTotalCount() {
return $this->total;
}
/**
* Returns debug section from server response.
*
* @return array
*/
public function getDebug() {
return $this->debug;
}
/**
* Executes a given JSON request and returns the result. Returns false if an error has occurred.
*
* @param array $params
*
* @return mixed the output of the script if it has been executed successfully or false otherwise
*/
protected function request(array $params) {
// Reset object state.
$this->error = null;
$this->total = null;
$this->debug = [];
// Connect to the server.
if (!$this->connect()) {
return false;
}
// Set timeout.
stream_set_timeout($this->socket, $this->timeout);
// Send the command.
$json = json_encode($params);
if (fwrite($this->socket, ZBX_TCP_HEADER.pack('V', strlen($json))."\x00\x00\x00\x00".$json) === false) {
$this->error = _s('Cannot send command, check connection with Zabbix server "%1$s".', $this->host);
return false;
}
$expect = self::ZBX_TCP_EXPECT_HEADER;
$response = '';
$response_len = 0;
$expected_len = null;
$now = time();
while (true) {
if ((time() - $now) >= $this->timeout) {
$this->error = _s(
'Connection timeout of %1$s seconds exceeded when connecting to Zabbix server "%2$s".',
$this->timeout, $this->host
);
return false;
}
if (!feof($this->socket) && ($buffer = fread($this->socket, self::READ_BYTES_LIMIT)) !== false) {
$response_len += strlen($buffer);
$response .= $buffer;
if ($expect == self::ZBX_TCP_EXPECT_HEADER) {
if (strncmp($response, ZBX_TCP_HEADER, min($response_len, ZBX_TCP_HEADER_LEN)) != 0) {
$this->error = _s('Incorrect response received from Zabbix server "%1$s".', $this->host);
return false;
}
if ($response_len < ZBX_TCP_HEADER_LEN) {
continue;
}
$expect = self::ZBX_TCP_EXPECT_DATA;
}
if ($response_len < ZBX_TCP_HEADER_LEN + ZBX_TCP_DATALEN_LEN) {
continue;
}
if ($expected_len === null) {
$expected_len = unpack('Vlen', substr($response, ZBX_TCP_HEADER_LEN, 4))['len'];
$expected_len += ZBX_TCP_HEADER_LEN + ZBX_TCP_DATALEN_LEN;
if ($this->totalBytesLimit != 0 && $expected_len >= $this->totalBytesLimit) {
$this->error = _s(
'Size of the response received from Zabbix server "%1$s" exceeds the allowed size of %2$s bytes. This value can be increased in the ZBX_SOCKET_BYTES_LIMIT constant in include/defines.inc.php.',
$this->host, $this->totalBytesLimit
);
return false;
}
}
if ($response_len >= $expected_len) {
break;
}
}
else {
$this->error =
_s('Cannot read the response, check connection with the Zabbix server "%1$s".', $this->host);
return false;
}
}
fclose($this->socket);
if ($expected_len > $response_len || $response_len > $expected_len) {
$this->error = _s('Incorrect response received from Zabbix server "%1$s".', $this->host);
return false;
}
$response = json_decode(substr($response, ZBX_TCP_HEADER_LEN + ZBX_TCP_DATALEN_LEN), true);
if (!$response || !$this->normalizeResponse($response)) {
$this->error = _s('Incorrect response received from Zabbix server "%1$s".', $this->host);
return false;
}
if (array_key_exists('debug', $response)) {
$this->debug = $response['debug'];
}
// Request executed successfully.
if ($response['response'] == self::RESPONSE_SUCCESS) {
// saves total count
$this->total = array_key_exists('total', $response) ? $response['total'] : null;
return array_key_exists('data', $response) ? $response['data'] : true;
}
// An error on the server side occurred.
$this->error = rtrim($response['info']);
return false;
}
/**
* Opens a socket to the Zabbix server. Returns the socket resource if the connection has been established or
* false otherwise.
*
* @return bool|resource
*/
protected function connect() {
if (!$this->socket) {
if ($this->host === null || $this->port === null) {
$this->error = _('Connection to Zabbix server failed. Incorrect configuration.');
return false;
}
if (!$socket = @fsockopen($this->host, $this->port, $errorCode, $errorMsg, $this->connect_timeout)) {
$host_port = $this->host.':'.$this->port;
switch ($errorMsg) {
case 'Connection refused':
$dErrorMsg = _s("Connection to Zabbix server \"%1\$s\" refused. Possible reasons:\n1. Incorrect \"NodeAddress\" or \"ListenPort\" in the \"zabbix_server.conf\" or server IP/DNS override in the \"zabbix.conf.php\";\n2. Security environment (for example, SELinux) is blocking the connection;\n3. Zabbix server daemon not running;\n4. Firewall is blocking TCP connection.\n", $host_port);
break;
case 'No route to host':
$dErrorMsg = _s("Zabbix server \"%1\$s\" cannot be reached. Possible reasons:\n1. Incorrect \"NodeAddress\" or \"ListenPort\" in the \"zabbix_server.conf\" or server IP/DNS override in the \"zabbix.conf.php\";\n2. Incorrect network configuration.\n", $host_port);
break;
case 'Connection timed out':
$dErrorMsg = _s("Connection to Zabbix server \"%1\$s\" timed out. Possible reasons:\n1. Incorrect \"NodeAddress\" or \"ListenPort\" in the \"zabbix_server.conf\" or server IP/DNS override in the \"zabbix.conf.php\";\n2. Firewall is blocking TCP connection.\n", $host_port);
break;
default:
$dErrorMsg = _s("Connection to Zabbix server \"%1\$s\" failed. Possible reasons:\n1. Incorrect \"NodeAddress\" or \"ListenPort\" in the \"zabbix_server.conf\" or server IP/DNS override in the \"zabbix.conf.php\";\n2. Incorrect DNS server configuration.\n", $host_port);
}
$this->error = rtrim($dErrorMsg.$errorMsg);
}
$this->socket = $socket;
}
return $this->socket;
}
/**
* Returns true if the response received from the Zabbix server is valid.
*
* @param array $response
*
* @return bool
*/
protected function normalizeResponse(array &$response) {
return (array_key_exists('response', $response) && ($response['response'] == self::RESPONSE_SUCCESS
|| $response['response'] == self::RESPONSE_FAILED && array_key_exists('info', $response))
);
}
}