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.
1017 lines
28 KiB
1017 lines
28 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.
|
||
|
**/
|
||
|
|
||
|
require_once 'vendor/autoload.php';
|
||
|
|
||
|
require_once dirname(__FILE__).'/CAPITest.php';
|
||
|
require_once dirname(__FILE__).'/CZabbixClient.php';
|
||
|
require_once dirname(__FILE__).'/helpers/CLogHelper.php';
|
||
|
|
||
|
/**
|
||
|
* Base class for integration tests.
|
||
|
*/
|
||
|
class CIntegrationTest extends CAPITest {
|
||
|
|
||
|
// Default iteration count for wait operations.
|
||
|
const WAIT_ITERATIONS = 60;
|
||
|
|
||
|
// Default delays (in seconds):
|
||
|
const WAIT_ITERATION_DELAY = 1; // Wait iteration delay.
|
||
|
const WAIT_ITERATION_DELAY_FOR_SHUTDOWN = 3; // Shutdown may legitimately take a lot of time
|
||
|
const CACHE_RELOAD_DELAY = 5; // Configuration cache reload delay.
|
||
|
const HOUSEKEEPER_EXEC_DELAY = 5; // Housekeeper execution delay.
|
||
|
const DATA_PROCESSING_DELAY = 5; // Data processing delay.
|
||
|
|
||
|
// Zabbix component constants.
|
||
|
const COMPONENT_SERVER = 'server';
|
||
|
const COMPONENT_SERVER_HANODE1 = 'server_ha1';
|
||
|
const COMPONENT_PROXY = 'proxy';
|
||
|
const COMPONENT_AGENT = 'agentd';
|
||
|
const COMPONENT_AGENT2 = 'agent2';
|
||
|
|
||
|
// Zabbix component port constants.
|
||
|
const AGENT_PORT_SUFFIX = '50';
|
||
|
const SERVER_PORT_SUFFIX = '51';
|
||
|
const PROXY_PORT_SUFFIX = '52';
|
||
|
const SERVER_HANODE1_PORT_SUFFIX = '61';
|
||
|
const AGENT2_PORT_SUFFIX = '53';
|
||
|
|
||
|
/**
|
||
|
* Components required by test suite.
|
||
|
*
|
||
|
* @var array
|
||
|
*/
|
||
|
private static $suite_components = [];
|
||
|
|
||
|
/**
|
||
|
* Hosts to be enabled for test suite.
|
||
|
*
|
||
|
* @var array
|
||
|
*/
|
||
|
private static $suite_hosts = [];
|
||
|
|
||
|
/**
|
||
|
* Configuration provider for test suite.
|
||
|
*
|
||
|
* @var array
|
||
|
*/
|
||
|
private static $suite_configuration = [];
|
||
|
|
||
|
/**
|
||
|
* Components required by test case.
|
||
|
*
|
||
|
* @var array
|
||
|
*/
|
||
|
private $case_components = [];
|
||
|
|
||
|
/**
|
||
|
* Hosts to be enabled for test case.
|
||
|
*
|
||
|
* @var array
|
||
|
*/
|
||
|
private $case_hosts = [];
|
||
|
|
||
|
/**
|
||
|
* Configuration provider for test case.
|
||
|
*
|
||
|
* @var array
|
||
|
*/
|
||
|
protected static $case_configuration = [];
|
||
|
|
||
|
/**
|
||
|
* Process annotations defined on suite / case level.
|
||
|
*
|
||
|
* @param string $type annotation type ('class' or 'method')
|
||
|
*
|
||
|
* @throws Exception on invalid configuration provider
|
||
|
*/
|
||
|
protected function processAnnotations($type) {
|
||
|
$annotations = $this->getAnnotationsByType($this->annotations, $type);
|
||
|
$result = [
|
||
|
'components' => [],
|
||
|
'hosts' => [],
|
||
|
'configuration' => []
|
||
|
];
|
||
|
|
||
|
// Get required components.
|
||
|
foreach ($this->getAnnotationTokensByName($annotations, 'required-components') as $component) {
|
||
|
if ($component === 'agent') {
|
||
|
$component = self::COMPONENT_AGENT;
|
||
|
}
|
||
|
|
||
|
self::validateComponent($component);
|
||
|
$result['components'][$component] = true;
|
||
|
}
|
||
|
|
||
|
$result['components'] = array_keys($result['components']);
|
||
|
|
||
|
// Get hosts to enable.
|
||
|
foreach ($this->getAnnotationTokensByName($annotations, 'hosts') as $host) {
|
||
|
$result['hosts'][$host] = true;
|
||
|
}
|
||
|
|
||
|
$result['hosts'] = array_keys($result['hosts']);
|
||
|
|
||
|
// Get configuration from configuration data provider.
|
||
|
foreach ($this->getAnnotationTokensByName($annotations, 'configurationDataProvider') as $provider) {
|
||
|
if (!method_exists($this, $provider) || !is_array($config = call_user_func([$this, $provider]))) {
|
||
|
throw new Exception('Configuration data provider "'.$provider.'" is not valid.');
|
||
|
}
|
||
|
|
||
|
$result['configuration'] = array_merge($result['configuration'], $config);
|
||
|
}
|
||
|
|
||
|
return $result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set status for hosts.
|
||
|
*
|
||
|
* @param array $hosts array of hostids or host names
|
||
|
* @param integer $status status to be set
|
||
|
*/
|
||
|
protected static function setHostStatus($hosts, $status) {
|
||
|
if (is_scalar($hosts)) {
|
||
|
$hosts = [$hosts];
|
||
|
}
|
||
|
|
||
|
if ($hosts && is_array($hosts)) {
|
||
|
$filters = [];
|
||
|
$criteria = [];
|
||
|
|
||
|
foreach ($hosts as $host) {
|
||
|
$filters[(is_numeric($host) ? 'hostid' : 'host')][] = zbx_dbstr($host);
|
||
|
}
|
||
|
|
||
|
foreach ($filters as $key => $values) {
|
||
|
$criteria[] = $key.' in ('.implode(',', $values).')';
|
||
|
}
|
||
|
|
||
|
DBexecute('UPDATE hosts SET status='.zbx_dbstr($status).' WHERE '.implode(' OR ', $criteria));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @inheritdoc
|
||
|
*/
|
||
|
protected function onBeforeTestSuite() {
|
||
|
parent::onBeforeTestSuite();
|
||
|
|
||
|
$result = $this->processAnnotations('class');
|
||
|
self::$suite_components = $result['components'];
|
||
|
self::$suite_hosts = $result['hosts'];
|
||
|
self::$suite_configuration = self::getDefaultComponentConfiguration();
|
||
|
|
||
|
foreach (self::getComponents() as $component) {
|
||
|
if (!array_key_exists($component, $result['configuration'])) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
self::$suite_configuration[$component]
|
||
|
= array_merge(self::$suite_configuration[$component], $result['configuration'][$component]);
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
if ($this->prepareData() === false) {
|
||
|
throw new Exception('Failed to prepare data for test suite.');
|
||
|
}
|
||
|
} catch (Exception $exception) {
|
||
|
self::markTestSuiteSkipped();
|
||
|
throw $exception;
|
||
|
}
|
||
|
|
||
|
self::setHostStatus(self::$suite_hosts, HOST_STATUS_MONITORED);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Prepare data for test suite.
|
||
|
*/
|
||
|
public function prepareData() {
|
||
|
// Code is not missing here.
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Callback executed before every test case.
|
||
|
*
|
||
|
* @before
|
||
|
*/
|
||
|
public function onBeforeTestCase() {
|
||
|
parent::onBeforeTestCase();
|
||
|
|
||
|
$result = $this->processAnnotations('method');
|
||
|
$this->case_components = array_diff($result['components'], self::$suite_components);
|
||
|
$this->case_hosts = array_diff($result['hosts'], self::$suite_hosts);
|
||
|
self::$case_configuration = self::$suite_configuration;
|
||
|
|
||
|
foreach (self::getComponents() as $component) {
|
||
|
if (!array_key_exists($component, $result['configuration'])) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
self::$case_configuration[$component] = array_merge(self::$case_configuration[$component],
|
||
|
$result['configuration'][$component]
|
||
|
);
|
||
|
}
|
||
|
|
||
|
self::setHostStatus($this->case_hosts, HOST_STATUS_MONITORED);
|
||
|
|
||
|
foreach ($this->case_components as $component) {
|
||
|
if (in_array($component, self::$suite_components)) {
|
||
|
throw new Exception('Component "'.$component.'" already started on suite level.');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$components = array_merge(self::$suite_components, $this->case_components);
|
||
|
|
||
|
foreach ($components as $component) {
|
||
|
self::prepareComponentConfiguration($component, self::$case_configuration);
|
||
|
self::startComponent($component);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Callback executed after every test case.
|
||
|
*
|
||
|
* @after
|
||
|
*/
|
||
|
public function onAfterTestCase() {
|
||
|
$components = array_merge(self::$suite_components, $this->case_components);
|
||
|
|
||
|
foreach ($components as $component) {
|
||
|
self::stopComponent($component);
|
||
|
}
|
||
|
|
||
|
self::setHostStatus($this->case_hosts, HOST_STATUS_NOT_MONITORED);
|
||
|
|
||
|
parent::onAfterTestCase();
|
||
|
|
||
|
self::$case_configuration = [];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Callback executed after every test suite.
|
||
|
*
|
||
|
* @afterClass
|
||
|
*/
|
||
|
public static function onAfterTestSuite() {
|
||
|
foreach (self::$suite_components as $component) {
|
||
|
self::stopComponent($component);
|
||
|
}
|
||
|
|
||
|
if (self::$suite_hosts) {
|
||
|
global $DB;
|
||
|
DBconnect($error);
|
||
|
self::setHostStatus(self::$suite_hosts, HOST_STATUS_NOT_MONITORED);
|
||
|
DBclose();
|
||
|
}
|
||
|
|
||
|
parent::onAfterTestSuite();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get list of possible component names.
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
private static function getComponents() {
|
||
|
return [
|
||
|
self::COMPONENT_SERVER, self::COMPONENT_PROXY, self::COMPONENT_AGENT, self::COMPONENT_AGENT2,
|
||
|
self::COMPONENT_SERVER_HANODE1
|
||
|
];
|
||
|
}
|
||
|
/**
|
||
|
* Validate component name.
|
||
|
*
|
||
|
* @param string $component component name to be validated.
|
||
|
*
|
||
|
* @throws Exception on invalid component name
|
||
|
*/
|
||
|
private static function validateComponent($component) {
|
||
|
if (!in_array($component, self::getComponents())) {
|
||
|
throw new Exception('Unknown component name "'.$component.'".');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Wait for component to start.
|
||
|
*
|
||
|
* @param string $component component name
|
||
|
* @param string $waitLogLineOverride already log line to use to consider component as running
|
||
|
* @param bool $skip_pid skip PID check
|
||
|
*
|
||
|
* @throws Exception on failed wait operation
|
||
|
*/
|
||
|
protected static function waitForStartup($component, $waitLogLineOverride = '', $skip_pid = false) {
|
||
|
self::validateComponent($component);
|
||
|
|
||
|
for ($r = 0; $r < self::WAIT_ITERATIONS; $r++) {
|
||
|
$pid = @file_get_contents(self::getPidPath($component));
|
||
|
if ($skip_pid == true || ($pid && is_numeric($pid) && posix_kill($pid, 0))) {
|
||
|
switch ($component) {
|
||
|
case self::COMPONENT_SERVER_HANODE1:
|
||
|
self::waitForLogLineToBePresent($component, 'HA manager started', false, 5, 1);
|
||
|
break;
|
||
|
case self::COMPONENT_SERVER:
|
||
|
case self::COMPONENT_PROXY:
|
||
|
$line = empty($waitLogLineOverride) ? 'started [trapper #1]' : $waitLogLineOverride;
|
||
|
self::waitForLogLineToBePresent($component, $line, false, 10, 1);
|
||
|
break;
|
||
|
case self::COMPONENT_AGENT:
|
||
|
self::waitForLogLineToBePresent($component, 'started [listener #1]', false, 5, 1);
|
||
|
break;
|
||
|
|
||
|
case self::COMPONENT_AGENT2:
|
||
|
self::waitForLogLineToBePresent($component, 'Zabbix Agent2 hostname', false, 5, 1);
|
||
|
break;
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
sleep(self::WAIT_ITERATION_DELAY);
|
||
|
}
|
||
|
|
||
|
var_dump(file_get_contents(self::getLogPath(self::COMPONENT_SERVER)));
|
||
|
|
||
|
throw new Exception('Failed to wait for component "'.$component.'" to start.');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Checks absence of pid file after kill.
|
||
|
*
|
||
|
* @param string $component component name
|
||
|
*
|
||
|
*/
|
||
|
private static function checkPidKilled($component) {
|
||
|
for ($r = 0; $r < self::WAIT_ITERATIONS; $r++) {
|
||
|
if (!file_exists(self::getPidPath($component))) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
sleep(self::WAIT_ITERATION_DELAY_FOR_SHUTDOWN);
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Wait for component to stop.
|
||
|
*
|
||
|
* @param string $component
|
||
|
* @param array $child_pids
|
||
|
*
|
||
|
* @throws Exception on failed wait operation
|
||
|
*/
|
||
|
protected static function waitForShutdown($component, array $child_pids) {
|
||
|
if (!self::checkPidKilled($component)) {
|
||
|
throw new Exception('Failed to wait for component "'.$component.'" to stop.');
|
||
|
}
|
||
|
|
||
|
$failed_pids = [];
|
||
|
|
||
|
foreach ($child_pids as $child_pid) {
|
||
|
if (ctype_digit($child_pid) && posix_kill($child_pid, 0)) {
|
||
|
posix_kill($child_pid, SIGKILL);
|
||
|
$failed_pids[] = $child_pid;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!$failed_pids) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$log = CLogHelper::readLog(self::getLogPath($component), false);
|
||
|
|
||
|
throw new Exception('Multiple child processes for component "'.$component.'" did not stop gracefully:'."\n".
|
||
|
implode(', ', $failed_pids)."\n".
|
||
|
'Log file contents: '."\n".$log."\n");
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Execute command and the execution result.
|
||
|
*
|
||
|
* @param string $command command to be executed
|
||
|
* @param array $params parameters to be passed
|
||
|
* @param bool $background run command in background
|
||
|
*
|
||
|
* @return string
|
||
|
*
|
||
|
* @throws Exception on execution error
|
||
|
*/
|
||
|
protected static function executeCommand(string $command, array $params, bool $background = false) {
|
||
|
if ($params) {
|
||
|
foreach ($params as &$param) {
|
||
|
$param = escapeshellarg($param);
|
||
|
}
|
||
|
unset($param);
|
||
|
|
||
|
$params = ' '.implode(' ', $params);
|
||
|
}
|
||
|
else {
|
||
|
$params = '';
|
||
|
}
|
||
|
|
||
|
$command .= $params.($background ? ' > /dev/null 2>&1 &' : ' 2>&1');
|
||
|
|
||
|
exec($command, $output, $return);
|
||
|
|
||
|
if ($return !== 0) {
|
||
|
$output = $output ? "\n".'Output:'."\n".implode("\n", $output) : '';
|
||
|
throw new Exception('Failed to execute command "'.$command.'".'.$output);
|
||
|
}
|
||
|
|
||
|
return $output;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get default configuration of components.
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
protected static function getDefaultComponentConfiguration() {
|
||
|
global $DB;
|
||
|
|
||
|
$db = [
|
||
|
'DBName' => $DB['DATABASE'],
|
||
|
'DBUser' => $DB['USER'],
|
||
|
'DBPassword' => $DB['PASSWORD']
|
||
|
];
|
||
|
|
||
|
if ($DB['SERVER'] !== 'localhost' && $DB['SERVER'] !== '127.0.0.1') {
|
||
|
$db['DBHost'] = $DB['SERVER'];
|
||
|
}
|
||
|
|
||
|
if ($DB['PORT'] != 0) {
|
||
|
$db['DBPort'] = $DB['PORT'];
|
||
|
}
|
||
|
|
||
|
if ($DB['SCHEMA']) {
|
||
|
$db['DBSchema'] = $DB['SCHEMA'];
|
||
|
}
|
||
|
|
||
|
$configuration = [
|
||
|
self::COMPONENT_SERVER => array_merge($db, [
|
||
|
'LogFile' => PHPUNIT_COMPONENT_DIR.'zabbix_server.log',
|
||
|
'PidFile' => PHPUNIT_COMPONENT_DIR.'zabbix_server.pid',
|
||
|
'SocketDir' => PHPUNIT_COMPONENT_DIR,
|
||
|
'ListenPort' => PHPUNIT_PORT_PREFIX.self::SERVER_PORT_SUFFIX
|
||
|
]),
|
||
|
self::COMPONENT_SERVER_HANODE1 => array_merge($db, [
|
||
|
'LogFile' => PHPUNIT_COMPONENT_DIR.'zabbix_server_ha1.log',
|
||
|
'PidFile' => PHPUNIT_COMPONENT_DIR.'zabbix_server_ha1.pid',
|
||
|
'SocketDir' => PHPUNIT_COMPONENT_DIR.'ha1/',
|
||
|
'ListenPort' => PHPUNIT_PORT_PREFIX.self::SERVER_HANODE1_PORT_SUFFIX
|
||
|
]),
|
||
|
self::COMPONENT_PROXY => array_merge($db, [
|
||
|
'LogFile' => PHPUNIT_COMPONENT_DIR.'zabbix_proxy.log',
|
||
|
'PidFile' => PHPUNIT_COMPONENT_DIR.'zabbix_proxy.pid',
|
||
|
'SocketDir' => PHPUNIT_COMPONENT_DIR,
|
||
|
'ListenPort' => PHPUNIT_PORT_PREFIX.self::PROXY_PORT_SUFFIX
|
||
|
]),
|
||
|
self::COMPONENT_AGENT => [
|
||
|
'LogFile' => PHPUNIT_COMPONENT_DIR.'zabbix_agent.log',
|
||
|
'PidFile' => PHPUNIT_COMPONENT_DIR.'zabbix_agent.pid',
|
||
|
'ListenPort' => PHPUNIT_PORT_PREFIX.self::AGENT_PORT_SUFFIX
|
||
|
],
|
||
|
self::COMPONENT_AGENT2 => [
|
||
|
'LogFile' => PHPUNIT_COMPONENT_DIR.'zabbix_agent2.log',
|
||
|
'PidFile' => PHPUNIT_COMPONENT_DIR.'zabbix_agent2.pid',
|
||
|
'ControlSocket' => PHPUNIT_COMPONENT_DIR.'zabbix_agent2.sock',
|
||
|
'ListenPort' => PHPUNIT_PORT_PREFIX.self::AGENT2_PORT_SUFFIX
|
||
|
]
|
||
|
];
|
||
|
|
||
|
$configuration[self::COMPONENT_PROXY]['DBName'] .= '_proxy';
|
||
|
|
||
|
return $configuration;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create configuration file for component.
|
||
|
*
|
||
|
* @param string $component component name
|
||
|
* @param array $values configuration array
|
||
|
*
|
||
|
* @throws Exception on failed configuration file write
|
||
|
*/
|
||
|
protected static function prepareComponentConfiguration($component, $values) {
|
||
|
self::validateComponent($component);
|
||
|
|
||
|
if ($component == self::COMPONENT_SERVER_HANODE1) {
|
||
|
$path = PHPUNIT_CONFIG_SOURCE_DIR.'zabbix_'.self::COMPONENT_SERVER.'.conf';
|
||
|
}
|
||
|
else {
|
||
|
$path = PHPUNIT_CONFIG_SOURCE_DIR.'zabbix_'.$component.'.conf';
|
||
|
}
|
||
|
|
||
|
if (!file_exists($path) || ($config = @file_get_contents($path)) === false) {
|
||
|
throw new Exception('There is no configuration file for component "'.$component.'": '.$path.'.');
|
||
|
}
|
||
|
|
||
|
if (array_key_exists($component, $values) && $values[$component] && is_array($values[$component])) {
|
||
|
foreach ($values[$component] as $key => $value) {
|
||
|
$config = preg_replace('/^(\s*'.$key.'\s*=.*)$/m', '#\1', $config);
|
||
|
foreach ((array) $value as $val) {
|
||
|
$config .= "\n".$key.'='.$val;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (file_put_contents(PHPUNIT_CONFIG_DIR.'zabbix_'.$component.'.conf', $config) === false) {
|
||
|
throw new Exception('Failed to create configuration file for component "'.$component.'": '.
|
||
|
PHPUNIT_CONFIG_DIR.'zabbix_'.$component.'.conf.'
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Start component.
|
||
|
*
|
||
|
* @param string $component component name
|
||
|
* @param string $waitLogLineOverride already log line to use to consider component as running
|
||
|
* @param bool $skip_pid skip PID check
|
||
|
*
|
||
|
* @throws Exception on missing configuration or failed start
|
||
|
*/
|
||
|
protected function startComponent($component, $waitLogLineOverride = '', $skip_pid = false) {
|
||
|
self::validateComponent($component);
|
||
|
|
||
|
$config = PHPUNIT_CONFIG_DIR.'zabbix_'.$component.'.conf';
|
||
|
if (!file_exists($config)) {
|
||
|
throw new Exception('There is no configuration file for component "'.$component.'".');
|
||
|
}
|
||
|
|
||
|
self::clearLog($component);
|
||
|
|
||
|
$background = ($component === self::COMPONENT_AGENT2);
|
||
|
|
||
|
$bin_path = $component === self::COMPONENT_SERVER_HANODE1
|
||
|
? PHPUNIT_BINARY_DIR.'zabbix_'.self::COMPONENT_SERVER
|
||
|
: PHPUNIT_BINARY_DIR.'zabbix_'.$component;
|
||
|
|
||
|
self::executeCommand($bin_path, ['-c', $config], $background);
|
||
|
self::waitForStartup($component, $waitLogLineOverride, $skip_pid );
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Stop component.
|
||
|
*
|
||
|
* @param string $component component name
|
||
|
*
|
||
|
* @throws Exception on missing configuration or failed stop
|
||
|
*/
|
||
|
protected static function stopComponent($component) {
|
||
|
self::validateComponent($component);
|
||
|
|
||
|
$child_pids = [];
|
||
|
$pid = @file_get_contents(self::getPidPath($component));
|
||
|
|
||
|
if ($pid !== false && is_numeric($pid)) {
|
||
|
$output = shell_exec('pgrep -P '.$pid);
|
||
|
if ($output !== false && $output !== null) {
|
||
|
$child_pids = explode("\n", $output);
|
||
|
}
|
||
|
|
||
|
posix_kill($pid, SIGTERM);
|
||
|
}
|
||
|
self::waitForShutdown($component, $child_pids);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Stop component by using SIGKILL signal.
|
||
|
*
|
||
|
* @param string $component component name
|
||
|
*
|
||
|
* @throws Exception on missing configuration or failed stop
|
||
|
*/
|
||
|
protected static function killComponent($component) {
|
||
|
self::validateComponent($component);
|
||
|
|
||
|
$child_pids = [];
|
||
|
$pid_path = self::getPidPath($component);
|
||
|
$pid = @file_get_contents($pid_path);
|
||
|
|
||
|
if ($pid !== false && is_numeric($pid)) {
|
||
|
$output = shell_exec('pgrep -P '.$pid);
|
||
|
if ($output !== false && $output !== null) {
|
||
|
$child_pids = explode("\n", $output);
|
||
|
foreach ($child_pids as $child_pid) {
|
||
|
if (ctype_digit($child_pid) && posix_kill($child_pid, 0)) {
|
||
|
posix_kill($child_pid, SIGKILL);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
posix_kill($pid, SIGKILL);
|
||
|
}
|
||
|
|
||
|
unlink($pid_path);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get client for component.
|
||
|
*
|
||
|
* @param string $component component name
|
||
|
*
|
||
|
* @throws Exception on invalid component type
|
||
|
*/
|
||
|
protected function getClient($component) {
|
||
|
self::validateComponent($component);
|
||
|
|
||
|
if ($component === self::COMPONENT_AGENT || $component === self::COMPONENT_AGENT2) {
|
||
|
throw new Exception('There is no client available for Zabbix Agent.');
|
||
|
}
|
||
|
|
||
|
return new CZabbixClient('localhost', self::getConfigurationValue($component, 'ListenPort', 10051), 3, 3,
|
||
|
ZBX_SOCKET_BYTES_LIMIT
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get name of active component used in test.
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
protected function getActiveComponent() {
|
||
|
$components = [];
|
||
|
foreach (array_merge(self::$suite_components, $this->case_components) as $component) {
|
||
|
if ($component !== self::COMPONENT_AGENT && $component !== self::COMPONENT_AGENT2) {
|
||
|
$components[] = $component;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (count($components) === 1) {
|
||
|
return $components[0];
|
||
|
}
|
||
|
else {
|
||
|
return self::COMPONENT_SERVER;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send value for items to server.
|
||
|
*
|
||
|
* @param string $type data type
|
||
|
* @param array $values item values
|
||
|
* @param string $component component name or null for active component
|
||
|
*
|
||
|
* @return array processing result
|
||
|
*/
|
||
|
protected function sendDataValues($type, $values, $component = null) {
|
||
|
if ($component === null) {
|
||
|
$component = $this->getActiveComponent();
|
||
|
}
|
||
|
|
||
|
$client = $this->getClient($component);
|
||
|
$result = $client->sendDataValues($type, $values);
|
||
|
|
||
|
// Check that data was sent successfully.
|
||
|
$this->assertTrue(($result !== false),
|
||
|
sprintf('Component "%s" failed to receive data: %s', $component, $client->getError())
|
||
|
);
|
||
|
|
||
|
// Check that discovery data was sent.
|
||
|
$this->assertTrue(array_key_exists('processed', $result), 'Result doesn\'t contain "processed" count.');
|
||
|
$this->assertEquals(count($values), $result['processed'],
|
||
|
'Processed value count doesn\'t match sent value count.'
|
||
|
);
|
||
|
|
||
|
sleep(self::DATA_PROCESSING_DELAY);
|
||
|
|
||
|
return $result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send single item value.
|
||
|
*
|
||
|
* @param string $type data type
|
||
|
* @param string $host host name
|
||
|
* @param string $key item key
|
||
|
* @param mixed $value item value
|
||
|
* @param string $component component name or null for active component
|
||
|
*
|
||
|
* @return array processing result
|
||
|
*/
|
||
|
protected function sendDataValue($type, $host, $key, $value, $component = null) {
|
||
|
if (!is_scalar($value)) {
|
||
|
$value = json_encode($value);
|
||
|
}
|
||
|
|
||
|
$data = [
|
||
|
'host' => $host,
|
||
|
'key' => $key,
|
||
|
'value' => $value
|
||
|
];
|
||
|
|
||
|
return $this->sendDataValues($type, [$data], $component);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send values to trapper items.
|
||
|
*
|
||
|
* @param array $values item values
|
||
|
* @param string $component component name or null for active component
|
||
|
*
|
||
|
* @return array processing result
|
||
|
*/
|
||
|
protected function sendSenderValues($values, $component = null) {
|
||
|
return $this->sendDataValues('sender', $values, $component);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send single value for trapper item.
|
||
|
*
|
||
|
* @param string $host host name
|
||
|
* @param string $key item key
|
||
|
* @param mixed $value item value
|
||
|
* @param string $component component name or null for active component
|
||
|
*
|
||
|
* @return array processing result
|
||
|
*/
|
||
|
protected function sendSenderValue($host, $key, $value, $component = null) {
|
||
|
return $this->sendDataValue('sender', $host, $key, $value, $component);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send values to active agent items.
|
||
|
*
|
||
|
* @param array $values item values
|
||
|
* @param string $component component name or null for active component
|
||
|
*
|
||
|
* @return array processing result
|
||
|
*/
|
||
|
protected function sendAgentValues($values, $component = null) {
|
||
|
return $this->sendDataValues('agent', $values, $component);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send single value for active agent item.
|
||
|
*
|
||
|
* @param string $host host name
|
||
|
* @param string $key item key
|
||
|
* @param mixed $value item value
|
||
|
* @param string $component component name or null for active component
|
||
|
*
|
||
|
* @return array processing result
|
||
|
*/
|
||
|
protected function sendAgentValue($host, $key, $value, $component = null) {
|
||
|
return $this->sendDataValue('agent', $host, $key, $value, $component);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get list of active checks for host.
|
||
|
*
|
||
|
* @param string $host host name
|
||
|
* @param string $component component name or null for active component
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
protected function getActiveAgentChecks($host, $component = null) {
|
||
|
if ($component === null) {
|
||
|
$component = $this->getActiveComponent();
|
||
|
}
|
||
|
|
||
|
$client = $this->getClient($component);
|
||
|
$checks = $client->getActiveChecks($host);
|
||
|
|
||
|
if (!is_array($checks)) {
|
||
|
$this->fail('Cannot retrieve active checks for host "'.$host.'": '.$client->getError().'.');
|
||
|
}
|
||
|
|
||
|
return $checks;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Reload configuration cache.
|
||
|
*
|
||
|
* @param string $component component name or null for active component
|
||
|
*/
|
||
|
protected function reloadConfigurationCache($component = null) {
|
||
|
if ($component === null) {
|
||
|
$component = $this->getActiveComponent();
|
||
|
}
|
||
|
|
||
|
self::executeCommand(PHPUNIT_BINARY_DIR.'zabbix_'.$component, ['--runtime-control', 'config_cache_reload']);
|
||
|
|
||
|
sleep(self::CACHE_RELOAD_DELAY);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string $component component name or null for active component
|
||
|
*/
|
||
|
protected function executeHousekeeper($component = null) {
|
||
|
if ($component === null) {
|
||
|
$component = $this->getActiveComponent();
|
||
|
}
|
||
|
|
||
|
self::executeCommand(PHPUNIT_BINARY_DIR.'zabbix_'.$component, ['--runtime-control', 'housekeeper_execute']);
|
||
|
|
||
|
sleep(self::HOUSEKEEPER_EXEC_DELAY);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Request data from API until data is present (@see call).
|
||
|
*
|
||
|
* @param string $method API method to be called
|
||
|
* @param mixed $params API call params
|
||
|
* @param integer $iterations iteration count
|
||
|
* @param integer $delay iteration delay
|
||
|
* @param callable $callback Callback function to test if API response is valid.
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
public function callUntilDataIsPresent($method, $params, $iterations = null, $delay = null, $callback = null) {
|
||
|
if ($iterations === null) {
|
||
|
$iterations = self::WAIT_ITERATIONS;
|
||
|
}
|
||
|
|
||
|
if ($delay === null) {
|
||
|
$delay = self::WAIT_ITERATION_DELAY;
|
||
|
}
|
||
|
|
||
|
$exception = null;
|
||
|
for ($i = 0; $i < $iterations; $i++) {
|
||
|
try {
|
||
|
$response = $this->call($method, $params);
|
||
|
|
||
|
if (is_array($response['result']) && count($response['result']) > 0
|
||
|
&& ($callback === null || call_user_func($callback, $response))) {
|
||
|
return $response;
|
||
|
}
|
||
|
} catch (Exception $e) {
|
||
|
$exception = $e;
|
||
|
}
|
||
|
|
||
|
sleep($delay);
|
||
|
}
|
||
|
|
||
|
if ($exception !== null) {
|
||
|
throw $exception;
|
||
|
}
|
||
|
|
||
|
$this->fail('Data requested from '.$method.' API is not present within specified interval. Params used:'.
|
||
|
"\n".json_encode($params)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get path of the log file for component.
|
||
|
*
|
||
|
* @param string $component name of the component
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
protected static function getLogPath($component) {
|
||
|
self::validateComponent($component);
|
||
|
|
||
|
return self::getConfigurationValue($component, 'LogFile', '/tmp/zabbix_'.$component.'.log');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get path of the pid file for component.
|
||
|
*
|
||
|
* @param string $component name of the component
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
protected static function getPidPath($component) {
|
||
|
self::validateComponent($component);
|
||
|
|
||
|
return self::getConfigurationValue($component, 'PidFile', '/tmp/zabbix_'.$component.'.pid');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get current configuration value.
|
||
|
*
|
||
|
* @param string $component name of the component
|
||
|
* @param string $key name of the configuration parameter
|
||
|
* @param mixed $default default value
|
||
|
*
|
||
|
* @return mixed
|
||
|
*/
|
||
|
protected static function getConfigurationValue($component, $key, $default = null) {
|
||
|
$configuration = (self::$case_configuration) ? self::$case_configuration : self::$suite_configuration;
|
||
|
|
||
|
if (array_key_exists($component, $configuration) && array_key_exists($key, $configuration[$component])) {
|
||
|
return $configuration[$component][$key];
|
||
|
}
|
||
|
|
||
|
return $default;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Clear contents of log.
|
||
|
*
|
||
|
* @param string $component name of the component
|
||
|
*/
|
||
|
protected static function clearLog($component) {
|
||
|
CLogHelper::clearLog(self::getLogPath($component));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check if line is present.
|
||
|
*
|
||
|
* @param string $component name of the component
|
||
|
* @param string|array $lines line(s) to look for
|
||
|
* @param boolean $incremental flag to be used to enable incremental read
|
||
|
* @param boolean $match_regex flag to be used to match line by regex
|
||
|
*
|
||
|
* @return boolean
|
||
|
*/
|
||
|
protected static function isLogLinePresent($component, $lines, $incremental = true, $match_regex = false) {
|
||
|
return CLogHelper::isLogLinePresent(self::getLogPath($component), $lines, $incremental, $match_regex);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Wait until line is present in log.
|
||
|
*
|
||
|
* @param string $component name of the component
|
||
|
* @param string|array $lines line(s) to look for
|
||
|
* @param boolean $incremental flag to be used to enable incremental read
|
||
|
* @param integer $iterations iteration count
|
||
|
* @param integer $delay iteration delay
|
||
|
* @param boolean $match_regex flag to be used to match line by regex
|
||
|
*
|
||
|
* @throws Exception on failed wait operation
|
||
|
*/
|
||
|
protected static function waitForLogLineToBePresent($component, $lines, $incremental = true, $iterations = null, $delay = null, $match_regex = false) {
|
||
|
if ($iterations === null) {
|
||
|
$iterations = self::WAIT_ITERATIONS;
|
||
|
}
|
||
|
|
||
|
if ($delay === null) {
|
||
|
$delay = self::WAIT_ITERATION_DELAY;
|
||
|
}
|
||
|
|
||
|
for ($r = 0; $r < $iterations; $r++) {
|
||
|
if (self::isLogLinePresent($component, $lines, $incremental, $match_regex)) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
sleep($delay);
|
||
|
}
|
||
|
|
||
|
if (is_array($lines)) {
|
||
|
$quoted = [];
|
||
|
foreach ($lines as $line) {
|
||
|
$quoted[] = '"'.$line.'"';
|
||
|
}
|
||
|
|
||
|
$description = 'any of the lines ['.implode(', ', $quoted).']';
|
||
|
}
|
||
|
else {
|
||
|
$description = 'line "'.$lines.'"';
|
||
|
}
|
||
|
|
||
|
$c = CLogHelper::readLog(self::getLogPath($component), false);
|
||
|
|
||
|
if (file_exists(self::getLogPath(self::COMPONENT_AGENT))) {
|
||
|
$c2 = @CLogHelper::readLog(self::getLogPath(self::COMPONENT_AGENT), false);
|
||
|
}
|
||
|
else {
|
||
|
$c2 = '';
|
||
|
}
|
||
|
|
||
|
throw new Exception('Failed to wait for '.$description.' to be present in '.$component .
|
||
|
'log file path:'.self::getLogPath($component).' and server log file contents: ' .
|
||
|
$c . "\n and agent log file contents: " . $c2);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check if line is present.
|
||
|
*
|
||
|
* @param string $component name of the component
|
||
|
* @param string|array $cmd command
|
||
|
*
|
||
|
* @throws Exception on execution error
|
||
|
*/
|
||
|
protected function executeRuntimeControlCommand($component, $cmd) {
|
||
|
if (!is_array($cmd)) {
|
||
|
$cmd = [$cmd];
|
||
|
}
|
||
|
|
||
|
$params = ['-c', PHPUNIT_CONFIG_DIR.'zabbix_'.$component.'.conf', '--runtime-control'];
|
||
|
$args = array_merge($params, $cmd);
|
||
|
|
||
|
self::executeCommand(PHPUNIT_BINARY_DIR.'zabbix_'.$component, $args, '> /dev/null 2>&1');
|
||
|
}
|
||
|
}
|