<?php /* ** Copyright (C) 2001-2025 Zabbix SIA ** ** This program is free software: you can redistribute it and/or modify it under the terms of ** the GNU Affero General Public License as published by the Free Software Foundation, version 3. ** ** 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 Affero General Public License for more details. ** ** You should have received a copy of the GNU Affero General Public License along with this program. ** If not, see <https://www.gnu.org/licenses/>. **/ require_once dirname(__FILE__).'/../include/CIntegrationTest.php'; /** * Test suite for discovery rules * * @backup hosts * * @onAfter deleteData */ class testDiscoveryRules extends CIntegrationTest { const DRULE_NAME = 'Test discovery rule'; const DRULE_NAME_ERR = 'Test discovery rule with error'; const DISCOVERY_ACTION_NAME = 'Test discovery action'; const DISCOVERY_ACTION_NAME_ERR = 'Test discovery action with error'; const PROXY_NAME = 'Test proxy'; const SLEEP_TIME = 1; const MAX_ATTEMPTS_DISCOVERY = 60; /* For tests with real SNMP agent */ const SNMPAGENT_VALID_OID = 'iso.3.6.1.2.1.1.1.0'; const SNMPAGENT_INVALID_OID = 'invalid.OID'; const SNMPAGENT_EXPECTED_INVALID_OID_ERR_MSG = "'SNMPv2c agent' checks failed: " . '"snmp_parse_oid(): cannot parse OID "' . self::SNMPAGENT_INVALID_OID . '": Generic error (Sub-id not found: (top) -> invalid)"'; /* For tests with simulated SNMP agent */ const SNMPSIM_HOST_IP = '127.0.10.3'; const SNMPSIM_HOST_PORT = '1024'; const SNMPSIM_DRULE_IP_RANGE = '127.0.10.3'; const SNMPSIM_VALID_OID = 'iso.3.6.1.2.1.1.1.0'; const SNMPSIM_DRULE_CONTEXT_NAME = 'host/test/test'; const SNMPSIM_USERNAME = 'zabbix'; const SNMPSIM_DRULE_SECURITY_LEVEL = ITEM_SNMPV3_SECURITYLEVEL_AUTHPRIV; const SNMPSIM_AUTH_PROTOCOL = 'MD5'; const SNMPSIM_DRULE_AUTH_PROTOCOL = ITEM_SNMPV3_AUTHPROTOCOL_MD5; const SNMPSIM_AUTH_KEY = 'zabbixAuth'; const SNMPSIM_PRIV_PROTOCOL = 'DES'; const SNMPSIM_DRULE_PRIVACY_PROTOCOL = ITEM_SNMPV3_PRIVPROTOCOL_DES; const SNMPSIM_PRIV_KEY = 'zabbixPriv'; const SNMPSIM_PROCESS_USER = 'nobody'; const SNMPSIM_PROCESS_GROUP = 'nogroup'; const SNMPSIM_DATA_DIR_REL_PATH = 'data/snmpsim'; private static $discoveryActionId; private static $discoveredHostId; /* IDs for cleanup in the end of the test */ private static $drules = array(); private static $discoveryActions = array(); private static $proxies = array(); private static function snmpsimStart(): void { $datadir = realpath(dirname(__FILE__)) . '/' . self::SNMPSIM_DATA_DIR_REL_PATH; $cmd = 'snmpsimd'; $cmd .= ' --v3-user=' . self::SNMPSIM_USERNAME; $cmd .= ' --v3-auth-key=' . self::SNMPSIM_AUTH_KEY; $cmd .= ' --v3-priv-key=' . self::SNMPSIM_PRIV_KEY; $cmd .= ' --v3-auth-proto=' . self::SNMPSIM_AUTH_PROTOCOL; $cmd .= ' --v3-priv-proto=' . self::SNMPSIM_PRIV_PROTOCOL; $cmd .= ' --process-user=' . self::SNMPSIM_PROCESS_USER; $cmd .= ' --process-group=' . self::SNMPSIM_PROCESS_GROUP; $cmd .= ' --agent-udpv4-endpoint=' . self::SNMPSIM_DRULE_IP_RANGE . ':' . self::SNMPSIM_HOST_PORT; $cmd .= ' --data-dir=' . $datadir; $cmd .= ' > /dev/null 2>&1 &'; shell_exec($cmd); } private static function snmpsimStop(): void { shell_exec('pkill snmpsimd > /dev/null 2>&1 &'); } private function waitForDiscoveryWithTags($expectedTags, $notExpectedTags = []): string { for ($i = 0; $i < self::MAX_ATTEMPTS_DISCOVERY; $i++) { try { $response = $this->call('host.get', [ 'selectTags' => ['tag', 'value'] ]); $this->assertArrayHasKey('result', $response, 'Failed to discover host before timeout'); $this->assertCount(1, $response['result'], 'Failed to discover host before timeout'); $this->assertArrayHasKey('tags', $response['result'][0], 'Failed to discover host before timeout'); $discoveredHost = $response['result'][0]; $this->assertArrayHasKey('hostid', $discoveredHost, 'Failed to get host ID of the discovered host'); $tags = $discoveredHost['tags']; $this->assertCount(count($expectedTags), $tags, 'Unexpected tags count was detected'); foreach($expectedTags as $expectedTag) { $this->assertContains($expectedTag, $tags, 'Expected tag was not found after discovery'); } foreach($notExpectedTags as $notExpectedTag) { $this->assertNotContains($notExpectedTag, $tags, 'Unexpected tag was found after discovery'); } return $discoveredHost['hostid']; } catch (Exception $e) { if ($i == self::MAX_ATTEMPTS_DISCOVERY - 1) throw $e; else sleep(self::SLEEP_TIME); } } } private function waitForDiscovery($expected_hostname): string { for ($i = 0; $i < self::MAX_ATTEMPTS_DISCOVERY; $i++) { try { $response = $this->call('host.get', [ ]); $this->assertArrayHasKey('result', $response, 'Failed to discover host before timeout'); $this->assertCount(1, $response['result'], 'Failed to discover host before timeout'); $discoveredHost = $response['result'][0]; $this->assertArrayHasKey('hostid', $discoveredHost, 'Failed to get host ID of the discovered host'); $this->assertEquals($expected_hostname, $discoveredHost['host']); break; } catch (Exception $e) { if ($i == self::MAX_ATTEMPTS_DISCOVERY - 1) throw $e; else sleep(self::SLEEP_TIME); } } return $discoveredHost['hostid']; } private function waitForDiscoveryErr($errStr): void { for ($i = 0; $i < self::MAX_ATTEMPTS_DISCOVERY; $i++) { try { $response = $this->call('drule.get', [ 'filter' => [ 'name' => self::DRULE_NAME_ERR ] ]); $this->assertArrayHasKey('result', $response, 'Failed to get discovery rule error'); $this->assertCount(1, $response['result'], 'Failed to get discovery rule error'); $drule = $response['result'][0]; $this->assertArrayHasKey('name', $drule, 'Failed to get discovery rule error'); $this->assertEquals($errStr, $drule['error'], 'Failed to get discovery rule error'); break; } catch (Exception $e) { if ($i == self::MAX_ATTEMPTS_DISCOVERY - 1) throw $e; else sleep(self::SLEEP_TIME); } } } private static function deleteAllActions(): void { if (count(self::$discoveryActions) > 0) { CDataHelper::call('action.delete', self::$discoveryActions); self::$discoveryActions = array(); } } private static function deleteAllDrules(): void { if (count(self::$drules) > 0) { CDataHelper::call('drule.delete', self::$drules); self::$drules = array(); } } private function createDruleSnmpv2($name, $iprange, $oid, $proxyId): string { $drule = [ 'name' => $name, 'delay' => '1s', 'iprange' => $iprange, 'concurrency_max' => ZBX_DISCOVERY_CHECKS_UNLIMITED, 'dchecks' => [ [ 'type' => SVC_SNMPv2c, 'key_' => $oid, 'ports' => '161', 'snmp_community' => 'public', 'uniq' => 0 ] ] ]; if (!is_null($proxyId)) { $drule['proxyid'] = $proxyId; } $response = $this->call('drule.create', $drule); $this->assertArrayHasKey('result', $response, 'Failed to create a discovery rule'); $this->assertArrayHasKey('druleids', $response['result'], 'Failed to create a discovery rule'); $this->assertCount(1, $response['result'], 'Failed to create a discovery rule'); array_push(self::$drules, $response['result']['druleids'][0]); return $response['result']['druleids'][0]; } private function createDruleSnmpv3($name, $proxyId): string { $drule = [ 'iprange' => self::SNMPSIM_DRULE_IP_RANGE, 'name' => $name, 'delay' => '1s', 'status' => 0, /* enabled */ 'concurrency_max' => ZBX_DISCOVERY_CHECKS_UNLIMITED, 'dchecks' => [ [ 'type' => SVC_SNMPv3, 'key_' => self::SNMPSIM_VALID_OID, 'ports' => self::SNMPSIM_HOST_PORT, 'snmpv3_authpassphrase' => self::SNMPSIM_AUTH_KEY, 'snmpv3_authprotocol' => self::SNMPSIM_DRULE_AUTH_PROTOCOL, 'snmpv3_contextname' => self::SNMPSIM_DRULE_CONTEXT_NAME, 'snmpv3_privpassphrase' => self::SNMPSIM_PRIV_KEY, 'snmpv3_privprotocol' => self::SNMPSIM_DRULE_PRIVACY_PROTOCOL, 'snmpv3_securitylevel' => self::SNMPSIM_DRULE_SECURITY_LEVEL, 'snmpv3_securityname' => self::SNMPSIM_USERNAME, 'uniq' => 0, 'host_source' => 2, /* IP */ 'name_source' => 2 /* IP */ ] ] ]; if (!is_null($proxyId)) { $drule['proxyid'] = $proxyId; } $response = $this->call('drule.create', $drule); $this->assertArrayHasKey('result', $response, 'Failed to create a discovery rule'); $this->assertArrayHasKey('druleids', $response['result'], 'Failed to create a discovery rule'); $this->assertCount(1, $response['result'], 'Failed to create a discovery rule'); array_push(self::$drules, $response['result']['druleids'][0]); return $response['result']['druleids'][0]; } private function createActionHostAdd($druleId, $actionName): string { $response = $this->call('action.create', [ 'name' => $actionName, 'eventsource' => EVENT_SOURCE_DISCOVERY, 'status' => ACTION_STATUS_ENABLED, 'filter' => [ 'conditions' => [ [ 'conditiontype' => ZBX_CONDITION_TYPE_DRULE, 'operator' => CONDITION_OPERATOR_EQUAL, 'value' => $druleId ], [ 'conditiontype' => ZBX_CONDITION_TYPE_DSTATUS, 'operator' => CONDITION_OPERATOR_EQUAL, 'value' => DOBJECT_STATUS_UP ] ], 'evaltype' => CONDITION_EVAL_TYPE_AND_OR ], 'operations' => [ [ 'operationtype' => OPERATION_TYPE_HOST_ADD ] ] ]); $this->assertArrayHasKey('result', $response, 'Failed to create a discovery action "' . $actionName . '"'); $this->assertArrayHasKey('actionids', $response['result'], 'Failed to create a discovery action "' . $actionName . '"'); $this->assertCount(1, $response['result']['actionids'], 'Failed to create a discovery action "' . $actionName . '"'); array_push(self::$discoveryActions, $response['result']['actionids'][0]); return $response['result']['actionids'][0]; } private function createProxy(): void { $response = $this->call('proxy.create', [ 'name' => self::PROXY_NAME, 'operating_mode' => PROXY_OPERATING_MODE_PASSIVE, 'hosts' => [], 'address' => '127.0.0.1', 'port' => PHPUNIT_PORT_PREFIX.self::PROXY_PORT_SUFFIX ]); $this->assertArrayHasKey('result', $response, 'Failed to create proxy'); $this->assertArrayHasKey('proxyids', $response['result'], 'Failed to create proxy'); $this->assertCount(1, $response['result']['proxyids'], 'Failed to create proxy'); array_push(self::$proxies, $response['result']['proxyids'][0]); } private static function deleteProxy(): void { if (count(self::$proxies) > 0) { CDataHelper::call('proxy.delete', self::$proxies); self::$proxies = array(); } } private function deleteAllHosts(): void { $response = $this->call('host.get', []); $hostids = array(); foreach ($response['result'] as $host) { $hostids[] = $host['hostid']; } $this->call('host.delete', $hostids); $response = $this->call('host.get', []); $this->assertArrayHasKey('result', $response, 'Failed to clear existing hosts'); $this->assertCount(0, $response['result'], 'Failed to clear existing hosts'); } /** * Configuration provider for proxy in database mode * * @return array */ public function proxyDBModeconfigurationProvider(): array { return [ self::COMPONENT_PROXY => [ 'ProxyMode' => PROXY_OPERATING_MODE_PASSIVE, 'Hostname' => self::PROXY_NAME, 'ListenPort' => PHPUNIT_PORT_PREFIX.self::PROXY_PORT_SUFFIX, 'ProxyBufferMode' => 'disk', 'ProxyMemoryBufferSize' => 0 ] ]; } /** * Configuration provider for proxy in memory mode * * @return array */ public function proxyMemoryModeconfigurationProvider(): array { return [ self::COMPONENT_PROXY => [ 'ProxyMode' => PROXY_OPERATING_MODE_PASSIVE, 'Hostname' => self::PROXY_NAME, 'ListenPort' => PHPUNIT_PORT_PREFIX.self::PROXY_PORT_SUFFIX, 'ProxyBufferMode' => 'memory', 'ProxyMemoryBufferSize' => '128K' ] ]; } /** * Configuration provider for proxy in hybrid mode * * @return array */ public function proxyHybridModeconfigurationProvider(): array { return [ self::COMPONENT_PROXY => [ 'ProxyMode' => PROXY_OPERATING_MODE_PASSIVE, 'Hostname' => self::PROXY_NAME, 'ListenPort' => PHPUNIT_PORT_PREFIX.self::PROXY_PORT_SUFFIX, 'ProxyBufferMode' => 'hybrid', 'ProxyMemoryBufferSize' => '128K' ] ]; } /** * @inheritdoc */ public function prepareData(): void { $this->deleteAllHosts(); self::snmpsimStart(); } /** * @required-components server */ public function testDiscoveryRules_opAddHostTags(): void { $response = $this->call('drule.create', [ 'name' => self::DRULE_NAME, 'delay' => '1s', 'iprange' => '127.0.0.1', 'dchecks' => [ [ 'type' => SVC_HTTP, 'ports' => '80' ] ] ]); $this->assertArrayHasKey('result', $response, 'Failed to create a discovery rule'); $this->assertArrayHasKey('druleids', $response['result'], 'Failed to create a discovery rule'); $this->assertCount(1, $response['result'], 'Failed to create a discovery rule'); array_push(self::$drules, $response['result']['druleids'][0]); $druleId = $response['result']['druleids'][0]; $response = $this->call('action.create', [ 'name' => self::DISCOVERY_ACTION_NAME, 'eventsource' => EVENT_SOURCE_DISCOVERY, 'status' => ACTION_STATUS_ENABLED, 'filter' => [ 'conditions' => [ [ 'conditiontype' => ZBX_CONDITION_TYPE_DRULE, 'operator' => CONDITION_OPERATOR_EQUAL, 'value' => $druleId ], [ 'conditiontype' => ZBX_CONDITION_TYPE_DSTATUS, 'operator' => CONDITION_OPERATOR_EQUAL, 'value' => DOBJECT_STATUS_UP ] ], 'evaltype' => CONDITION_EVAL_TYPE_AND_OR ], 'operations' => [ /* OPERATION_TYPE_HOST_ADD is intentionally missing. It is expected to be run by */ /* Zabbix server, because OPERATION_TYPE_HOST_TAGS_ADD is present. */ [ 'operationtype' => OPERATION_TYPE_HOST_TAGS_ADD, 'optag' => [ [ 'tag' => 'add_tag1', 'value' => 'add_value1' ], [ 'tag' => 'add_tag2', 'value' => 'add_value2' ] ] ] ] ]); $this->assertArrayHasKey('result', $response, 'Failed to create a discovery action'); $this->assertArrayHasKey('actionids', $response['result'], 'Failed to create a discovery action'); $this->assertCount(1, $response['result']['actionids'], 'Failed to create a discovery action'); self::$discoveryActionId = $response['result']['actionids'][0]; array_push(self::$discoveryActions, $response['result']['actionids'][0]); self::$discoveredHostId = $this->waitForDiscoveryWithTags([ ['tag' => 'add_tag1', 'value' => 'add_value1'], ['tag' => 'add_tag2', 'value' => 'add_value2'] ]); } /** * @depends testDiscoveryRules_opAddHostTags * @required-components server */ public function testDiscoveryRules_opDelHostTags(): void { /* Replace tags at the discovered host */ $response = $this->call('host.update', [ 'hostid' => self::$discoveredHostId, 'tags' => [ [ 'tag' => 'del_tag3', 'value' => 'del_value3' ], [ 'tag' => 'del_tag4', 'value' => 'del_value4' ] ] ]); $this->assertArrayHasKey('result', $response); $this->assertCount(1, $response['result']); $response = $this->call('action.update', [ 'actionid' => self::$discoveryActionId, 'operations' => [ [ 'operationtype' => OPERATION_TYPE_HOST_TAGS_ADD, 'optag' => [ [ 'tag' => 'add_tag1', 'value' => 'add_value1' ], [ 'tag' => 'add_tag2', 'value' => 'add_value2' ] ] ], [ 'operationtype' => OPERATION_TYPE_HOST_TAGS_REMOVE, 'optag' => [ [ 'tag' => 'del_tag3', 'value' => 'del_value3' ], [ 'tag' => 'del_tag4', 'value' => 'del_value4' ] ] ] ] ]); $this->waitForDiscoveryWithTags([ ['tag' => 'add_tag1', 'value' => 'add_value1'], ['tag' => 'add_tag2', 'value' => 'add_value2'] ], [ ['tag' => 'del_tag3', 'value' => 'del_value3'], ['tag' => 'del_tag4', 'value' => 'del_value4'] ]); } /** * @depends testDiscoveryRules_opDelHostTags * @required-components server */ public function testDiscoveryRules_snmpErrorViaServer(): void { $this->stopComponent(self::COMPONENT_SERVER); self::deleteAllActions(); self::deleteAllDrules(); $this->deleteAllHosts(); $druleId = $this->createDruleSnmpv3(self::DRULE_NAME, NULL); $this->createActionHostAdd($druleId, self::DISCOVERY_ACTION_NAME); $druleWithErrId = $this->createDruleSnmpv2(self::DRULE_NAME_ERR, '127.0.0.1', self::SNMPAGENT_INVALID_OID, NULL); $this->createActionHostAdd($druleWithErrId, self::DISCOVERY_ACTION_NAME_ERR); $this->startComponent(self::COMPONENT_SERVER); $this->waitForDiscoveryErr(self::SNMPAGENT_EXPECTED_INVALID_OID_ERR_MSG); $this->waitForDiscovery(self::SNMPSIM_HOST_IP); } private function proxyTest(): void { $this->stopComponent(self::COMPONENT_SERVER); $this->stopComponent(self::COMPONENT_PROXY); self::deleteAllActions(); self::deleteAllDrules(); $this->deleteAllHosts(); $this->deleteProxy(); $proxyId = $this->createProxy(); $druleId = $this->createDruleSnmpv3(self::DRULE_NAME, $proxyId); $this->createActionHostAdd($druleId, self::DISCOVERY_ACTION_NAME); $druleWithErrId = $this->createDruleSnmpv2(self::DRULE_NAME_ERR, '127.0.0.1', self::SNMPAGENT_INVALID_OID, $proxyId); $this->createActionHostAdd($druleWithErrId, self::DISCOVERY_ACTION_NAME_ERR); $this->startComponent(self::COMPONENT_PROXY); $this->startComponent(self::COMPONENT_SERVER); $this->waitForDiscoveryErr(self::SNMPAGENT_EXPECTED_INVALID_OID_ERR_MSG); $this->waitForDiscovery(self::SNMPSIM_HOST_IP); } /** * @depends testDiscoveryRules_snmpErrorViaServer * @required-components server,proxy * @configurationDataProvider proxyDBModeconfigurationProvider */ public function testDiscoveryRules_snmpErrorViaProxyDBMode(): void { $this->proxyTest(); } /** * @depends testDiscoveryRules_snmpErrorViaProxyDBMode * @required-components server,proxy * @configurationDataProvider proxyMemoryModeconfigurationProvider */ public function testDiscoveryRules_snmpErrorViaProxyMemoryMode(): void { $this->proxyTest(); } /** * @depends testDiscoveryRules_snmpErrorViaProxyMemoryMode * @required-components server,proxy * @configurationDataProvider proxyHybridModeconfigurationProvider */ public function testDiscoveryRules_snmpErrorViaProxyHybridMode(): void { $this->proxyTest(); } /** * Delete data objects created for this test suite */ public static function deleteData(): void { self::snmpsimStop(); self::deleteAllActions(); self::deleteAllDrules(); self::deleteProxy(); } }