diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 0000000..bdcafae --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,45 @@ +name: Collector packaging +on: + push: + tags: + - '*' +jobs: + release: + name: Prepare release + runs-on: ubuntu-latest + steps: + - name: Checkout collector-base + uses: actions/checkout@v4 + with: + repository: Combodo/itop-data-collector-base + ref: 1.3.0 + sparse-checkout: | + conf + core + data + toolkit + path: ${{ github.repository }} + - name: Checkout current collector + uses: actions/checkout@v4 + with: + path: ${{ github.repository }}/collectors + - name: Create package + uses: thedoctor0/zip-release@0.7.1 + with: + filename: ../${{ github.repository }}-${{ github.ref_name }}.zip + path: '*' + directory: ${{ github.repository_owner }} + exclusions: '*.git* */composer.json */exclude.txt */Jenkinsfile' + - name: Create draft release + uses: ncipollo/release-action@v1 + with: + allowUpdates: true + artifacts: ${{ github.repository }}-${{ github.ref_name }}.zip + artifactErrorsFailBuild: true + draft: true + generateReleaseNotes: true + omitNameDuringUpdate: true + omitBodyDuringUpdate: true + omitPrereleaseDuringUpdate: true + updateOnlyUnreleased: true + token: ${{ secrets.ACCESS_TOKEN }} diff --git a/json/SnmpDiscoveryCollector.json b/json/SnmpDiscoveryCollector.json new file mode 100644 index 0000000..61b5d87 --- /dev/null +++ b/json/SnmpDiscoveryCollector.json @@ -0,0 +1,283 @@ +{ + "name": "$prefix$SNMP Discovery - $uuid$", + "description": "SNMP Discovery v$version$", + "status": "$synchro_status$", + "user_id": "$synchro_user$", + "notify_contact_id": "$contact_to_notify$", + "scope_class": "NetworkDevice", + "database_table_name": "", + "scope_restriction": "", + "full_load_periodicity": "$full_load_interval$", + "reconciliation_policy": "use_attributes", + "action_on_zero": "create", + "action_on_one": "update", + "action_on_multiple": "error", + "delete_policy": "update", + "delete_policy_update": "responds_to_snmp:no", + "delete_policy_retention": "0", + "attribute_list": [ + { + "attcode": "applicationsolution_list", + "update": "0", + "reconcile": "0", + "update_policy": "master_unlocked", + "row_separator": "|", + "attribute_separator": ";", + "value_separator": ":", + "attribute_qualifier": "'", + "finalclass": "SynchroAttLinkSet" + }, + { + "attcode": "asset_number", + "update": "0", + "reconcile": "0", + "update_policy": "master_unlocked", + "finalclass": "SynchroAttribute" + }, + { + "attcode": "brand_id", + "update": "0", + "reconcile": "0", + "update_policy": "master_unlocked", + "reconciliation_attcode": "", + "finalclass": "SynchroAttExtKey" + }, + { + "attcode": "business_criticity", + "update": "0", + "reconcile": "0", + "update_policy": "master_unlocked", + "finalclass": "SynchroAttribute" + }, + { + "attcode": "clusternetwork_id", + "update": "0", + "reconcile": "0", + "update_policy": "master_unlocked", + "reconciliation_attcode": "", + "finalclass": "SynchroAttExtKey" + }, + { + "attcode": "clusternetwork_role", + "update": "0", + "reconcile": "0", + "update_policy": "master_unlocked", + "finalclass": "SynchroAttribute" + }, + { + "attcode": "connectablecis_list", + "update": "0", + "reconcile": "0", + "update_policy": "master_unlocked", + "row_separator": "|", + "attribute_separator": ";", + "value_separator": ":", + "attribute_qualifier": "'", + "finalclass": "SynchroAttLinkSet" + }, + { + "attcode": "contacts_list", + "update": "0", + "reconcile": "0", + "update_policy": "master_unlocked", + "row_separator": "|", + "attribute_separator": ";", + "value_separator": ":", + "attribute_qualifier": "'", + "finalclass": "SynchroAttLinkSet" + }, + { + "attcode": "description", + "update": "0", + "reconcile": "0", + "update_policy": "master_unlocked", + "finalclass": "SynchroAttribute" + }, + { + "attcode": "documents_list", + "update": "0", + "reconcile": "0", + "update_policy": "master_unlocked", + "row_separator": "|", + "attribute_separator": ";", + "value_separator": ":", + "attribute_qualifier": "'", + "finalclass": "SynchroAttLinkSet" + }, + { + "attcode": "end_of_warranty", + "update": "0", + "reconcile": "0", + "update_policy": "master_unlocked", + "finalclass": "SynchroAttribute" + }, + { + "attcode": "iosversion_id", + "update": "0", + "reconcile": "0", + "update_policy": "master_unlocked", + "reconciliation_attcode": "", + "finalclass": "SynchroAttExtKey" + }, + { + "attcode": "location_id", + "update": "0", + "reconcile": "0", + "update_policy": "master_unlocked", + "reconciliation_attcode": "", + "finalclass": "SynchroAttExtKey" + }, + { + "attcode": "managementip_id", + "update": "1", + "reconcile": "1", + "update_policy": "master_unlocked", + "reconciliation_attcode": "", + "finalclass": "SynchroAttExtKey" + }, + { + "attcode": "model_id", + "update": "0", + "reconcile": "0", + "update_policy": "master_unlocked", + "reconciliation_attcode": "", + "finalclass": "SynchroAttExtKey" + }, + { + "attcode": "move2production", + "update": "0", + "reconcile": "0", + "update_policy": "master_unlocked", + "finalclass": "SynchroAttribute" + }, + { + "attcode": "name", + "update": "1", + "reconcile": "0", + "update_policy": "write_if_empty", + "finalclass": "SynchroAttribute" + }, + { + "attcode": "nb_u", + "update": "0", + "reconcile": "0", + "update_policy": "master_unlocked", + "finalclass": "SynchroAttribute" + }, + { + "attcode": "networkdevice_list", + "update": "0", + "reconcile": "0", + "update_policy": "master_unlocked", + "row_separator": "|", + "attribute_separator": ";", + "value_separator": ":", + "attribute_qualifier": "'", + "finalclass": "SynchroAttLinkSet" + }, + { + "attcode": "networkdevicetype_id", + "update": "1", + "reconcile": "0", + "update_policy": "write_if_empty", + "reconciliation_attcode": "", + "finalclass": "SynchroAttExtKey" + }, + { + "attcode": "org_id", + "update": "1", + "reconcile": "1", + "update_policy": "write_if_empty", + "reconciliation_attcode": "", + "finalclass": "SynchroAttExtKey" + }, + { + "attcode": "purchase_date", + "update": "0", + "reconcile": "0", + "update_policy": "master_unlocked", + "finalclass": "SynchroAttribute" + }, + { + "attcode": "ram", + "update": "0", + "reconcile": "0", + "update_policy": "master_unlocked", + "finalclass": "SynchroAttribute" + }, + { + "attcode": "replacement_date", + "update": "0", + "reconcile": "0", + "update_policy": "master_locked", + "finalclass": "SynchroAttribute" + }, + { + "attcode": "responds_to_snmp", + "update": "1", + "reconcile": "0", + "update_policy": "master_locked", + "finalclass": "SynchroAttribute" + }, + { + "attcode": "serialnumber", + "update": "1", + "reconcile": "0", + "update_policy": "master_unlocked", + "finalclass": "SynchroAttribute" + }, + { + "attcode": "snmp_last_discovery", + "update": "1", + "reconcile": "0", + "update_policy": "master_locked", + "finalclass": "SynchroAttribute" + }, + { + "attcode": "snmp_sysContact", + "update": "1", + "reconcile": "0", + "update_policy": "master_locked", + "finalclass": "SynchroAttribute" + }, + { + "attcode": "snmp_sysDescr", + "update": "1", + "reconcile": "0", + "update_policy": "master_locked", + "finalclass": "SynchroAttribute" + }, + { + "attcode": "snmp_sysLocation", + "update": "1", + "reconcile": "0", + "update_policy": "master_locked", + "finalclass": "SynchroAttribute" + }, + { + "attcode": "snmp_sysName", + "update": "1", + "reconcile": "0", + "update_policy": "master_locked", + "finalclass": "SynchroAttribute" + }, + { + "attcode": "snmpcredentials_id", + "update": "1", + "reconcile": "0", + "update_policy": "master_unlocked", + "reconciliation_attcode": "", + "finalclass": "SynchroAttExtKey" + }, + { + "attcode": "status", + "update": "1", + "reconcile": "0", + "update_policy": "write_if_empty", + "finalclass": "SynchroAttribute" + } + ], + "user_delete_policy": "administrators", + "url_icon": "", + "url_application": "" +} diff --git a/main.php b/main.php new file mode 100644 index 0000000..255de8e --- /dev/null +++ b/main.php @@ -0,0 +1,13 @@ + + + + + 0123-4567-89AB-CDEF + + + + + + .1.3.6.1.4.1.2011.2 + + .1.3.6.1.2.1.47.1.1.1.1.11 + + getNextNonEmpty + yes + + + + /.*/ + + .1.3.6.1.2.1.2.2.1.6 + + getNextValidMAC + no + + + + + implementation + + + + $discovery_application_uuid$ + implementation + 3600 + + diff --git a/src/SnmpCredentials.class.inc.php b/src/SnmpCredentials.class.inc.php new file mode 100644 index 0000000..066d9be --- /dev/null +++ b/src/SnmpCredentials.class.inc.php @@ -0,0 +1,33 @@ +name = $aSnmpCredentials['name']; + $oSelf->community = $aSnmpCredentials['community']; + $oSelf->securityLevel = $aSnmpCredentials['security_level']; + $oSelf->securityName = $aSnmpCredentials['security_name']; + $oSelf->authenticationProtocol = is_null($aSnmpCredentials['auth_protocol']) ? strtoupper($aSnmpCredentials['auth_protocol']) : null; + $oSelf->authenticationPassphrase = $aSnmpCredentials['auth_passphrase']; + $oSelf->privacyProtocol = is_null($aSnmpCredentials['priv_protocol']) ? strtoupper($aSnmpCredentials['priv_protocol']) : null; + $oSelf->privacyPassphrase = $aSnmpCredentials['priv_passphrase']; + $oSelf->contextName = $aSnmpCredentials['context_name']; + return $oSelf; + } +} diff --git a/src/SnmpDiscoveryCollector.class.inc.php b/src/SnmpDiscoveryCollector.class.inc.php new file mode 100644 index 0000000..fb38033 --- /dev/null +++ b/src/SnmpDiscoveryCollector.class.inc.php @@ -0,0 +1,495 @@ + List of subnets with their configured parameters + */ + protected array $aSubnets = []; + /** @var array List of all the IP addresses to discover with their subnet IP */ + protected array $aIPAddresses = []; + /** @var array Cache of potential SNMP credentials */ + protected array $aSnmpCredentials = []; + /** @var int Number of IPs that didn't respond to any SNMP request */ + protected int $iFailedIPs = 0; + + /** + * @inheritDoc + * @return void + * @throws Exception + */ + public function Init(): void + { + parent::Init(); + + // Check if modules are installed + static::CheckModuleInstallation('sv-snmp-discovery', true); + + // Load SNMP discovery application settings + $this->LoadApplicationSettings(); + } + + /** + * @return bool + * @throws Exception + */ + public function Prepare(): bool + { + // Load all responding IP addresses + $this->LoadAllIPAddresses(); + + // Load extra device info + $this->LoadDevices(); + //$this->LoadAdditionalDevices(); + + return parent::Prepare(); + } + + /** + * @return array|false + * @throws Exception + */ + protected function Fetch(): array|false + { + while ($iKey = key($this->aIPAddresses)) { + next($this->aIPAddresses); + + // Discover IP addresses as network device + if ($aData = $this->DiscoverDeviceByIP($iKey)) return $aData; + else $this->iFailedIPs++; + } + + return false; + } + + /** + * @return void + * @throws Exception + */ + protected function Cleanup(): void + { + Utils::Log(LOG_NOTICE, $this->iFailedIPs . ' non responding devices.'); + parent::Cleanup(); + } + + /** + * @param array $aPlaceHolders + * @return string|false + * @throws Exception + */ + public function GetSynchroDataSourceDefinition($aPlaceHolders = []): string|false + { + $aPlaceHolders['$uuid$'] = Utils::GetConfigurationValue('discovery_application_uuid'); + + return parent::GetSynchroDataSourceDefinition($aPlaceHolders); + } + + /** + * Workaround needed until PR merged in data-collector-base + * @link https://github.com/Combodo/itop-data-collector-base/pull/37 + * @param string[] $aHeaders + * @return void + * @throws Exception + */ + protected function AddHeader($aHeaders): void + { + $this->aCSVHeaders = array(); + foreach ($aHeaders as $sHeader) { + if (($sHeader != 'primary_key') && !$this->HeaderIsAllowed($sHeader)) { + if (!$this->AttributeIsOptional($sHeader)) { + Utils::Log(LOG_WARNING, "Invalid column '$sHeader', will be ignored."); + } + } else { + $this->aCSVHeaders[] = $sHeader; + } + } + fputcsv($this->aCSVFile[$this->iFileIndex], $this->aCSVHeaders, $this->sSeparator); + } + + /** + * Workaround needed until PR merged in iTop + * @link https://github.com/Combodo/iTop/pull/541 + * @param string $sHeader + * @return bool + */ + protected function HeaderIsAllowed(string $sHeader): bool + { + if (in_array($sHeader, [ + 'snmp_sysname', + 'snmp_sysdescr', + 'snmp_syslocation', + 'snmp_syscontact', + ])) return true; + + /** + * Workaround needed until PR merged in data-collector-base + * @link https://github.com/Combodo/itop-data-collector-base/pull/37 + * @example return parent::HeaderIsAllowed($sHeader); + */ + return array_key_exists($sHeader, $this->aFields); + } + + /** + * Check if the given module is installed in iTop + * @param string $sName Name of the module to be found + * @param bool $bRequired Whether to throw exceptions when module not found + * @return bool True when the given module is installed, false otherwise + * @throws Exception When the module is required but could not be found + */ + protected static function CheckModuleInstallation(string $sName, bool $bRequired = false): bool + { + $oRestClient = new RestClient(); + try { + $aResults = $oRestClient->Get('ModuleInstallation', ['name' => $sName], 'name,version'); + if ($aResults['code'] != 0 || empty($aResults['objects'])) { + throw new Exception($aResults['message'], $aResults['code']); + } + $aObject = current($aResults['objects']); + Utils::Log(LOG_DEBUG, sprintf('iTop module %s version %s is installed.', $aObject['fields']['name'], $aObject['fields']['version'])); + } catch (Exception $e) { + $sMessage = sprintf('%s iTop module %s is considered as not installed due to: %s', $bRequired ? 'Required' : 'Optional', $sName, $e->getMessage()); + if ($bRequired) throw new Exception($sMessage, 0, $e); + else { + Utils::Log(LOG_INFO, $sMessage); + return false; + } + } + return true; + } + + /** + * Load the SNMP discovery application settings (ID and subnet to discover) + * @return void + * @throws Exception + */ + protected function LoadApplicationSettings(): void + { + $sUUID = Utils::GetConfigurationValue('discovery_application_uuid'); + $oRestClient = new RestClient(); + + try { + $aResults = $oRestClient->Get('SNMPDiscovery', ['uuid' => $sUUID], 'ipv4subnets_list,ipv6subnets_list'); + if ($aResults['code'] != 0 || empty($aResults['objects'])) { + throw new Exception($aResults['message'], $aResults['code']); + } + + $aDiscovery = current($aResults['objects']); + $this->iApplicationID = (int)$aDiscovery['key']; + + Utils::Log(LOG_INFO, sprintf('An SNMP discovery application with UUID %s has been found in iTop.', $sUUID)); + } catch (Exception $e) { + throw new Exception(sprintf('An SNMP discovery application with UUID %s could not be found: %s', $sUUID, $e->getMessage()), 0, $e); + } + + // Prepare IPv4 subnet info + foreach ($aDiscovery['fields']['ipv4subnets_list'] as $aSubnet) { + $this->LoadSubnet($aSubnet); + } + + // Prepare IPv6 subnet info + foreach ($aDiscovery['fields']['ipv6subnets_list'] as $aSubnet) { + $this->LoadSubnet($aSubnet); + } + + Utils::Log(LOG_INFO, count($this->aSubnets) . ' subnets to discover.'); + } + + /** + * Prepare the subnet parameters list by the given subnet + * @param array{ip: string, org_id: string, default_networkdevicetype_id: string, snmpcredentials_list: int[], dhcp_range_discovery_enabled: string} $aSubnet + * @return void + * @throws Exception + */ + protected function LoadSubnet(array $aSubnet): void + { + if ($aSubnet['ipdiscovery_enabled'] == 'yes') { + $this->aSubnets[$aSubnet['ip']] = [ + 'default_org_id' => (int)$aSubnet['org_id'], + 'default_networkdevicetype_id' => (int)$aSubnet['default_networkdevicetype_id'], + 'snmpcredentials_list' => array_reduce($aSubnet['snmpcredentials_list'], function ($aCredentials, $aListItem) { + $aCredentials[] = (int)$aListItem['snmpcredentials_id']; + return $aCredentials; + }, []), + 'dhcp_range_discovery_enabled' => $aSubnet['dhcp_range_discovery_enabled'], + ]; + + if (empty($aSubnet['default_networkdevicetype_id'])) { + Utils::Log(LOG_WARNING, sprintf('No default networkdevicetype_id set for subnet %s, creation of new devices might fail.', $aSubnet['ip'])); + } + } + } + + /** + * Load all IP addresses to discover from the subnet linked to the current SNMP discovery application + * @return void + * @throws Exception + */ + protected function LoadAllIPAddresses(): void + { + // Load IPv4 addresses to discover + $aIPv4Addresses = static::LoadIPAddresses('IPv4Address', sprintf(<<iApplicationID)); + + // Load IPv6 addresses to discover + $aIPv6Addresses = static::LoadIPAddresses('IPv6Address', sprintf(<<iApplicationID)); + + $this->aIPAddresses = $aIPv4Addresses + $aIPv6Addresses; + Utils::Log(LOG_INFO, count($this->aIPAddresses) . ' addresses to process.'); + } + + /** + * Load IP addresses to discover by the given class and query + * @param 'IPv4Address'|'IPv6Address' $sClass The IP class to query + * @param string $sKeySpec The OQL to select addresses to discover + * @return array + * @throws Exception + */ + protected static function LoadIPAddresses(string $sClass, string $sKeySpec): array + { + $aIPAddresses = []; + try { + $oRestClient = new RestClient(); + + $aResults = $oRestClient->Get($sClass, $sKeySpec, 'ip,subnet_ip,responds_to_ping'); + if ($aResults['code'] != 0) { + throw new Exception($aResults['message'], $aResults['code']); + } + + if (!empty($aResults['objects'])) foreach ($aResults['objects'] as $aIPAddress) { + // Skip non responding IPs + if ($aIPAddress['fields']['responds_to_ping'] != 'no') { + $aIPAddresses[(int)$aIPAddress['key']] = $aIPAddress['fields']; + } else Utils::Log(LOG_DEBUG, sprintf('Skipping non responding IP %s.', $aIPAddress['fields']['ip'])); + } + } catch (Exception $e) { + throw new Exception(sprintf('Could not load %s: %s', $sClass, $e->getMessage())); + } + + return $aIPAddresses; + } + + /** + * Load known devices' snmp credentials so the collector doesn't need to figure out again. + * @return void + * @throws Exception + */ + protected function LoadDevices(): void + { + try { + $oRestClient = new RestClient(); + + $aResults = $oRestClient->Get('NetworkDevice', sprintf('SELECT NetworkDevice WHERE snmpcredentials_id != 0 AND managementip_id IN(%s)', implode(',', array_keys($this->aIPAddresses))), 'managementip_id,snmpcredentials_id'); + if ($aResults['code'] != 0) { + throw new Exception($aResults['message'], $aResults['code']); + } + + if (!empty($aResults['objects'])) foreach ($aResults['objects'] as $aNetworkDevice) { + $this->aDeviceSnmpCredentials[(int) $aNetworkDevice['key']] = [ + 'managementip_id' => (int) $aNetworkDevice['fields']['managementip_id'], + 'snmpcredentials_id' => (int) $aNetworkDevice['fields']['snmpcredentials_id'], + ]; + } + } catch (Exception $e) { + throw new Exception(sprintf('Could not load device credentials: %s', $e->getMessage())); + } + } + + /** + * @param integer $iKey + * @return SnmpCredentials + * @throws Exception + */ + protected function LoadSnmpCredentials(int $iKey): SnmpCredentials + { + if (!isset($this->aSnmpCredentials[$iKey])) { + $oRestClient = new RestClient(); + $aResults = $oRestClient->Get('SnmpCredentials', $iKey, 'name,community,security_level,security_name,auth_protocol,auth_passphrase,priv_protocol,priv_passphrase,context_name'); + + if ($aResults['code'] != 0 || empty($aResults['objects'])) throw new Exception($aResults['message'], $aResults['code']); + + $aCredentials = current($aResults['objects']); + $this->aSnmpCredentials[$iKey] = SnmpCredentials::fromArray($aCredentials['fields']); + } + + return $this->aSnmpCredentials[$iKey]; + } + + /** + * @param int $iKey ID of the IPAddress + * @return array{ + * primary_key: string, + * org_id: int, + * name: string, + * networkdevicetype_id: int, + * managementip_id: int, + * snmpcredentials_id: int, + * status: string, + * serialnumber: ?string, + * responds_to_snmp: 'yes', + * snmp_last_discovery: string, + * snmp_sysname: string, + * snmp_sysdescr: string, + * snmp_syscontact: string, + * snmp_syslocation: string, + * }|null + * @throws Exception + */ + protected function DiscoverDeviceByIP(int $iKey): ?array + { + ['ip' => $sIP, 'subnet_ip' => $sSubnetIP] = $this->aIPAddresses[$iKey]; + $aSubnet = $this->aSubnets[$sSubnetIP]; + Utils::Log(LOG_DEBUG, sprintf('Discovering IP %s...', $sIP)); + + // Prepare known credentials + $aDeviceCredentials = []; + foreach ($this->aDeviceSnmpCredentials as $aDevice) { + if ($aDevice['managementip_id'] == $iKey) $aDeviceCredentials[] = $aDevice['snmpcredentials_id']; + } + + // Try SNMP connection with each known credential + foreach (array_unique(array_merge($aDeviceCredentials, $aSubnet['snmpcredentials_list'])) as $iCredentialsKey) { + $oCredentials = $this->LoadSnmpCredentials($iCredentialsKey); + + Utils::Log(LOG_DEBUG, sprintf('Trying credential %s...', $oCredentials->name)); + $oSNMP = static::LoadSNMPConnection($sIP, $oCredentials); + + $sysObjectID = @$oSNMP->get(/* SNMPv2-MIB::sysObjectID */ '.1.3.6.1.2.1.1.2.0'); + + if ($sysObjectID === false) Utils::Log(LOG_DEBUG, $oSNMP->getError()); + else { + Utils::Log(LOG_INFO, sprintf('IP %s responds to %s.', $sIP, $oCredentials->name)); + Utils::Log(LOG_DEBUG, 'Device sysObjectID: '. $sysObjectID); + $sPrimaryKey = $sysObjectID . ' - '; + + // Find device serial number + ['serial' => $sSerial, 'load' => $bLoadSerial] = static::FindDeviceSerial($oSNMP, $sysObjectID); + if (!is_null($sSerial)) { + Utils::Log(LOG_DEBUG, 'Device serial: ' . $sSerial); + $sPrimaryKey .= $sSerial; + } else $sPrimaryKey .= $sIP; + + // Find device type + // ToDo: mapping table on oid? + + // Load system table info + [ + '.1.3.6.1.2.1.1.1.0' => $sSysDescr, + '.1.3.6.1.2.1.1.4.0' => $sSysContact, + '.1.3.6.1.2.1.1.5.0' => $sSysName, + '.1.3.6.1.2.1.1.6.0' => $sSysLocation, + ] = @$oSNMP->get([ + /* SNMPv2-MIB::sysDescr */ '.1.3.6.1.2.1.1.1.0', + /* SNMPv2-MIB::sysContact */ '.1.3.6.1.2.1.1.4.0', + /* SNMPv2-MIB::sysName */ '.1.3.6.1.2.1.1.5.0', + /* SNMPv2-MIB::sysLocation */ '.1.3.6.1.2.1.1.6.0', + ]); + + // Return device + return [ + 'primary_key' => $sPrimaryKey, + 'org_id' => $aSubnet['default_org_id'], + 'name' => $sSysName, + 'networkdevicetype_id' => $aSubnet['default_networkdevicetype_id'], + 'managementip_id' => $iKey, + 'snmpcredentials_id' => $iCredentialsKey, + 'status' => Utils::GetConfigurationValue('default_status'), + 'serialnumber' => $bLoadSerial ? $sSerial : null, + 'responds_to_snmp' => 'yes', + 'snmp_last_discovery' => date('Y-m-d H:i:s'), + 'snmp_sysname' => $sSysName, + 'snmp_sysdescr' => trim($sSysDescr), + 'snmp_syscontact' => $sSysContact, + 'snmp_syslocation' => $sSysLocation, + ]; + } + } + + Utils::Log(LOG_INFO, sprintf('IP %s does not respond.', $sIP)); + return null; + } + + /** + * @param SNMP $oSNMP + * @param string $sSysObjectID + * @return array{serial: string, load: bool}|null + * @throws Exception + */ + protected static function FindDeviceSerial(SNMP $oSNMP, string $sSysObjectID): ?array + { + /** @var array $aSerialDetectionOptions */ + $aSerialDetectionOptions = Utils::GetConfigurationValue('serial_detection', []); + + foreach ($aSerialDetectionOptions as $aDetectionOption) { + $sSysObjectIDMatch = $aDetectionOption['system_oid_match']; + if (($sSysObjectIDMatch[0] == '/' && preg_match($sSysObjectIDMatch, $sSysObjectID)) || substr_compare($sSysObjectID, $sSysObjectIDMatch, 0, strlen($sSysObjectIDMatch)) === 0) { + Utils::Log(LOG_DEBUG, 'sysObjectID matches with ' . $sSysObjectIDMatch); + + $bFound = false; + $sSerial = null; + $bLoadSerial = filter_var($aDetectionOption['use_as_serialnumber'], FILTER_VALIDATE_BOOLEAN); + $sSerialOid = $aDetectionOption['serial_oid']; + + if ($aDetectionOption['method'] == 'get') { + $sSerial = $oSNMP->get($sSerialOid); + $bFound = ($sSerial !== false); + } else do { + $aResult = $oSNMP->getnext([$sSerialOid]); + if (is_array($aResult)) { + $sSerial = current($aResult); + $sSerialOid = key($aResult); + switch ($aDetectionOption['method']) { + case 'getNextNonEmpty': + $bFound = !empty($sSerial); + break; + case 'getNextValidMAC': + $sPhysAddress = bin2hex($sSerial); + $bFound = (strlen($sPhysAddress) == 12 && !empty(hexdec($sPhysAddress))); + if ($bFound) $sSerial = implode(':', str_split($sPhysAddress, 2)); + break; + } + } + } while (!$bFound && substr_count($sSerialOid, $aDetectionOption['serial_oid'])); + if ($bFound) { + return ['serial' => $sSerial, 'load' => $bLoadSerial]; + } + } + } + return null; + } + + /** + * @param string $sHostname + * @param SnmpCredentials $oCredentials + * @return SNMP + */ + protected static function LoadSNMPConnection(string $sHostname, SnmpCredentials $oCredentials): SNMP + { + if (!empty($oCredentials->securityLevel)) { + $oSNMP = new SNMP(SNMP::VERSION_3, $sHostname, $oCredentials->securityName); + $oSNMP->setSecurity( + $oCredentials->securityLevel, + $oCredentials->authenticationProtocol, + $oCredentials->authenticationPassphrase, + $oCredentials->privacyProtocol, + $oCredentials->privacyPassphrase, + $oCredentials->contextName + ); + } else $oSNMP = new SNMP(SNMP::VERSION_2c, $sHostname, $oCredentials->community); + + // Plain value retrieval + $oSNMP->valueretrieval = SNMP_VALUE_PLAIN; + // Numeric OID output format + $oSNMP->oid_output_format = SNMP_OID_OUTPUT_NUMERIC; + + return $oSNMP; + } +}