diff --git a/config.php.default b/config.php.default index f6b031cd7..2da0514e0 100755 --- a/config.php.default +++ b/config.php.default @@ -49,3 +49,6 @@ $config['poller-wrapper']['alerter'] = FALSE; # Uncomment to submit callback stats via proxy #$config['callback_proxy'] = "hostname:port"; + +# Set default port association mode for new devices (default: ifIndex) +#$config['default_port_association_mode'] = 'ifIndex'; diff --git a/includes/common.php b/includes/common.php index d32f836ab..0cbb6b007 100644 --- a/includes/common.php +++ b/includes/common.php @@ -114,7 +114,7 @@ function delete_port($int_id) { dbDelete('links', "`remote_port_id` = ?", array($int_id)); dbDelete('bill_ports', "`port_id` = ?", array($int_id)); - unlink(trim($config['rrd_dir'])."/".trim($interface['hostname'])."/port-".$interface['ifIndex'].".rrd"); + unlink(get_port_rrdfile_path ($interface['hostname'], $interface['port_id'])); } function sgn($int) { @@ -143,6 +143,15 @@ function get_sensor_rrd($device, $sensor) { return($rrd_file); } +function get_port_rrdfile_path ($hostname, $port_id, $suffix = '') { + global $config; + + if (! empty ($suffix)) + $suffix = '-' . $suffix; + + return trim ($config['rrd_dir']) . '/' . safename ($hostname) . '/' . 'port-id' . safename($port_id) . safename ($suffix) . '.rrd'; +} + function get_port_by_index_cache($device_id, $ifIndex) { global $port_index_cache; @@ -1093,3 +1102,155 @@ function ip_to_sysname($device,$ip) { } return $ip; }//end ip_to_sysname + +/** + * Return valid port association modes + * @param bool $no_cache No-Cache flag (optional, default false) + * @return array + */ +function get_port_assoc_modes ($no_cache = false) { + global $config; + + if ($config['memcached']['enable'] && $no_cache === false) { + $assoc_modes = $config['memcached']['resource']->get (hash ('sha512', "port_assoc_modes")); + if (! empty ($assoc_modes)) + return $assoc_modes; + } + + $assoc_modes = Null; + foreach (dbFetchRows ("SELECT `name` FROM `port_association_mode` ORDER BY pom_id") as $row) + $assoc_modes[] = $row['name']; + + if ($config['memcached']['enable'] && $no_cache === false) + $config['memcached']['resource']->set (hash ('sha512', "port_assoc_modes"), $assoc_modes, $config['memcached']['ttl']); + + return $assoc_modes; +} + +/** + * Validate port_association_mode + * @param string $port_assoc_mode + * @return bool + */ +function is_valid_port_assoc_mode ($port_assoc_mode) { + return in_array ($port_assoc_mode, get_port_assoc_modes ()); +} + +/** + * Get DB id of given port association mode name + * @param string $port_assoc_mode + * @param bool $no_cache No-Cache flag (optional, default false) + */ +function get_port_assoc_mode_id ($port_assoc_mode, $no_cache = false) { + global $config; + + if ($config['memcached']['enable'] && $no_cache === false) { + $id = $config['memcached']['resource']->get (hash ('sha512', "port_assoc_mode_id|$port_assoc_mode")); + if (! empty ($id)) + return $id; + } + + $id = Null; + $row = dbFetchRow ("SELECT `pom_id` FROM `port_association_mode` WHERE name = ?", array ($port_assoc_mode)); + if ($row) { + $id = $row['pom_id']; + if ($config['memcached']['enable'] && $no_cache === false) + $config['memcached']['resource']->set (hash ('sha512', "port_assoc_mode_id|$port_assoc_mode"), $id, $config['memcached']['ttl']); + } + + return $id; +} + +/** + * Get name of given port association_mode ID + * @param int $port_assoc_mode_id Port association mode ID + * @param bool $no_cache No-Cache flag (optional, default false) + * @return bool + */ +function get_port_assoc_mode_name ($port_assoc_mode_id, $no_cache = false) { + global $config; + + if ($config['memcached']['enable'] && $no_cache === false) { + $name = $config['memcached']['resource']->get (hash ('sha512', "port_assoc_mode_name|$port_assoc_mode_id")); + if (! empty ($name)) + return $name; + } + + $name = Null; + $row = dbFetchRow ("SELECT `name` FROM `port_association_mode` WHERE pom_id = ?", array ($port_assoc_mode_id)); + if ($row) { + $name = $row['name']; + if ($config['memcached']['enable'] && $no_cache === false) + $config['memcached']['resource']->set (hash ('sha512', "port_assoc_mode_name|$port_assoc_mode_id"), $name, $config['memcached']['ttl']); + } + + return $name; +} + +/** + * Query all ports of the given device (by ID) and build port array and + * port association maps for ifIndex, ifName, ifDescr. Query port stats + * if told to do so, too. + * @param int $device_id ID of device to query ports for + * @param bool $with_statistics Query port statistics, too. (optional, default false) + * @return array + */ +function get_ports_mapped ($device_id, $with_statistics = false) { + $ports = []; + $maps = [ + 'ifIndex' => [], + 'ifName' => [], + 'ifDescr' => [], + ]; + + /* Query all information available for ports for this device ... */ + $query = 'SELECT * FROM `ports` WHERE `device_id` = ? ORDER BY port_id'; + if ($with_statistics) { + /* ... including any related ports_statistics if requested */ + $query = 'SELECT *, `ports_statistics`.`port_id` AS `ports_statistics_port_id`, `ports`.`port_id` AS `port_id` FROM `ports` LEFT OUTER JOIN `ports_statistics` ON `ports`.`port_id` = `ports_statistics`.`port_id` WHERE `ports`.`device_id` = ? ORDER BY ports.port_id'; + } + + // Query known ports in order of discovery to make sure the latest + // discoverd/polled port is in the mapping tables. + foreach (dbFetchRows ($query, array ($device_id)) as $port) { + // Store port information by ports port_id from DB + $ports[$port['port_id']] = $port; + + // Build maps from ifIndex, ifName, ifDescr to port_id + $maps['ifIndex'][$port['ifIndex']] = $port['port_id']; + $maps['ifName'][$port['ifName']] = $port['port_id']; + $maps['ifDescr'][$port['ifDescr']] = $port['port_id']; + } + + return [ + 'ports' => $ports, + 'maps' => $maps, + ]; +} + +/** + * Calculate port_id of given port using given devices port information and port association mode + * @param array $ports_mapped Port information of device queried by get_ports_mapped() + * @param array $port Port information as fetched from DB + * @param string $port_association_mode Port association mode to use for mapping + * @return int port_id (or Null) + */ +function get_port_id ($ports_mapped, $port, $port_association_mode) { + // Get port_id according to port_association_mode used for this device + $port_id = Null; + + /* + * Information an all ports is available through $ports_mapped['ports'] + * This might come in handy sometime in the future to add you nifty new + * port mapping schema: + * + * $ports = $ports_mapped['ports']; + */ + $maps = $ports_mapped['maps']; + + if (in_array ($port_association_mode, array ('ifIndex', 'ifName', 'ifDescr'))) { + $port_id = $maps[$port_association_mode][$port[$port_association_mode]]; + } + + return $port_id; +} diff --git a/includes/defaults.inc.php b/includes/defaults.inc.php index 328ae2ce0..d11245ae4 100644 --- a/includes/defaults.inc.php +++ b/includes/defaults.inc.php @@ -853,3 +853,6 @@ $config['notifications']['local'] = 'misc/notifications.rs // Update channel (Can be 'master' or 'release') $config['update_channel'] = 'master'; + +// Default port association mode +$config['default_port_association_mode'] = 'ifIndex'; diff --git a/includes/discovery/ports.inc.php b/includes/discovery/ports.inc.php index 32067fa05..c682fa6e5 100644 --- a/includes/discovery/ports.inc.php +++ b/includes/discovery/ports.inc.php @@ -9,27 +9,61 @@ $port_stats = snmpwalk_cache_oid($device, 'ifType', $port_stats, 'IF-MIB'); // End Building SNMP Cache Array d_echo($port_stats); -// Build array of ports in the database -// FIXME -- this stuff is a little messy, looping the array to make an array just seems wrong. :> -// -- i can make it a function, so that you don't know what it's doing. -// -- $ports_db = adamasMagicFunction($ports_db); ? -foreach (dbFetchRows('SELECT * FROM `ports` WHERE `device_id` = ?', array($device['device_id'])) as $port) { - $ports_db[$port['ifIndex']] = $port; - $ports_db_l[$port['ifIndex']] = $port['port_id']; + +// By default libreNMS uses the ifIndex to associate ports on devices with ports discoverd/polled +// before and stored in the database. On Linux boxes this is a problem as ifIndexes may be +// unstable between reboots or (re)configuration of tunnel interfaces (think: GRE/OpenVPN/Tinc/...) +// The port association configuration allows to choose between association via ifIndex, ifName, +// or maybe other means in the future. The default port association mode still is ifIndex for +// compatibility reasons. +$port_association_mode = $config['default_port_association_mode']; +if ($device['port_association_mode']) + $port_association_mode = get_port_assoc_mode_name ($device['port_association_mode']); + +// Build array of ports in the database and an ifIndex/ifName -> port_id map +$ports_mapped = get_ports_mapped ($device['id']); +$ports_db = $ports_mapped['ports']; + +// +// Rename any old RRD files still named after the previous ifIndex based naming schema. +foreach ($ports_mapped['maps']['ifIndex'] as $ifIndex => $port_id) { + foreach (array ('', 'adsl', 'dot3') as $suffix) { + $suffix_tmp = ''; + if ($suffix) + $suffix_tmp = "-$suffix"; + + $old_rrd_path = trim ($config['rrd_dir']) . '/' . $device['hostname'] . "/port-$ifIndex$suffix_tmp.rrd"; + $new_rrd_path = get_port_rrdfile_path ($device['hostname'], $port_id, $suffix); + + if (is_file ($old_rrd_path)) { + rename ($old_rrd_path, $new_rrd_path); + } + } } + // New interface detection foreach ($port_stats as $ifIndex => $port) { - // Check the port against our filters. + // Store ifIndex in port entry and prefetch ifName as we'll need it multiple times + $port['ifIndex'] = $ifIndex; + $ifName = $port['ifName']; + + // Get port_id according to port_association_mode used for this device + $port_id = get_port_id ($ports_mapped, $port, $port_association_mode); + if (is_port_valid($port, $device)) { - if (!is_array($ports_db[$ifIndex])) { - $port_id = dbInsert(array('device_id' => $device['device_id'], 'ifIndex' => $ifIndex), 'ports'); - $ports_db[$ifIndex] = dbFetchRow('SELECT * FROM `ports` WHERE `device_id` = ? AND `ifIndex` = ?', array($device['device_id'], $ifIndex)); - echo 'Adding: '.$port['ifName'].'('.$ifIndex.')('.$ports_db[$port['ifIndex']]['port_id'].')'; + + // Port newly discovered? + if (! is_array($ports_db[$port_id])) { + $port_id = dbInsert(array('device_id' => $device['device_id'], 'ifIndex' => $ifIndex, 'ifName' => $ifName), 'ports'); + $ports[$port_id] = dbFetchRow('SELECT * FROM `ports` WHERE `device_id` = ? AND `port_id` = ?', array($device['device_id'], $port_id)); + echo 'Adding: '.$ifName.'('.$ifIndex.')('.$port_id.')'; } - else if ($ports_db[$ifIndex]['deleted'] == '1') { - dbUpdate(array('deleted' => '0'), 'ports', '`port_id` = ?', array($ports_db[$ifIndex]['port_id'])); - $ports_db[$ifIndex]['deleted'] = '0'; + + // Port re-discovered after previous deletion? + else if ($ports_db[$port_id]['deleted'] == '1') { + dbUpdate(array('deleted' => '0'), 'ports', '`port_id` = ?', array($ports_db[$port_id])); + $ports_db[$port_id]['deleted'] = '0'; echo 'U'; } else { @@ -39,11 +73,13 @@ foreach ($port_stats as $ifIndex => $port) { // We've seen it. Remove it from the cache. unset($ports_l[$ifIndex]); } + + // Port vanished (mark as deleted) else { - if (is_array($ports_db[$port['ifIndex']])) { - if ($ports_db[$port['ifIndex']]['deleted'] != '1') { - dbUpdate(array('deleted' => '1'), 'ports', '`port_id` = ?', array($ports_db[$ifIndex]['port_id'])); - $ports_db[$ifIndex]['deleted'] = '1'; + if (is_array($ports_db[$port_id])) { + if ($ports_db[$port_id]['deleted'] != '1') { + dbUpdate(array('deleted' => '1'), 'ports', '`port_id` = ?', array($ports_db[$port_id])); + $ports_db[$port_id]['deleted'] = '1'; echo '-'; } } @@ -68,4 +104,3 @@ echo "\n"; // Clear Variables Here unset($port_stats); unset($ports_db); -unset($ports_db_db); diff --git a/includes/polling/port-adsl.inc.php b/includes/polling/port-adsl.inc.php index 883cfcced..e3567e26b 100644 --- a/includes/polling/port-adsl.inc.php +++ b/includes/polling/port-adsl.inc.php @@ -39,11 +39,11 @@ // adslAturPerfESs.1 = 0 seconds // adslAturPerfValidIntervals.1 = 0 // adslAturPerfInvalidIntervals.1 = 0 -if (isset($port_stats[$port['ifIndex']]['adslLineCoding'])) { +if (isset($port_stats[$port_id]['adslLineCoding'])) { // Check to make sure Port data is cached. - $this_port = &$port_stats[$port['ifIndex']]; + $this_port = &$port_stats[$port_id]; - $rrdfile = $config['rrd_dir'].'/'.$device['hostname'].'/'.safename('port-'.$port['ifIndex'].'-adsl.rrd'); + $rrdfile = get_port_rrdfile_path ($device['hostname'], $port_id, 'adsl'); $rrd_create = ' --step 300'; $rrd_create .= ' DS:AtucCurrSnrMgn:GAUGE:600:0:635'; @@ -130,8 +130,8 @@ if (isset($port_stats[$port['ifIndex']]['adslLineCoding'])) { $this_port[$oid] = ($this_port[$oid] / 10); } - if (dbFetchCell('SELECT COUNT(*) FROM `ports_adsl` WHERE `port_id` = ?', array($port['port_id'])) == '0') { - dbInsert(array('port_id' => $port['port_id']), 'ports_adsl'); + if (dbFetchCell('SELECT COUNT(*) FROM `ports_adsl` WHERE `port_id` = ?', array($port_id)) == '0') { + dbInsert(array('port_id' => $port_id), 'ports_adsl'); } $port['adsl_update'] = array('port_adsl_updated' => array('NOW()')); @@ -141,7 +141,7 @@ if (isset($port_stats[$port['ifIndex']]['adslLineCoding'])) { $port['adsl_update'][$oid] = $data; } - dbUpdate($port['adsl_update'], 'ports_adsl', '`port_id` = ?', array($port['port_id'])); + dbUpdate($port['adsl_update'], 'ports_adsl', '`port_id` = ?', array($port_id)); if ($this_port['adslAtucCurrSnrMgn'] > '1280') { $this_port['adslAtucCurrSnrMgn'] = 'U'; diff --git a/includes/polling/port-etherlike.inc.php b/includes/polling/port-etherlike.inc.php index 0576d4b19..bf947b92d 100644 --- a/includes/polling/port-etherlike.inc.php +++ b/includes/polling/port-etherlike.inc.php @@ -1,13 +1,14 @@ -// -- i can make it a function, so that you don't know what it's doing. -// -- $ports = adamasMagicFunction($ports_db); ? -// select * doesn't do what we want if multiple tables have the same column name -- last one wins :/ -$ports_db = dbFetchRows('SELECT *, `ports_statistics`.`port_id` AS `ports_statistics_port_id`, `ports`.`port_id` AS `port_id` FROM `ports` LEFT OUTER JOIN `ports_statistics` ON `ports`.`port_id` = `ports_statistics`.`port_id` WHERE `ports`.`device_id` = ?', array($device['device_id'])); +// By default libreNMS uses the ifIndex to associate ports on devices with ports discoverd/polled +// before and stored in the database. On Linux boxes this is a problem as ifIndexes may be +// unstable between reboots or (re)configuration of tunnel interfaces (think: GRE/OpenVPN/Tinc/...) +// The port association configuration allows to choose between association via ifIndex, ifName, +// or maybe other means in the future. The default port association mode still is ifIndex for +// compatibility reasons. +$port_association_mode = $config['default_port_association_mode']; +if ($device['port_association_mode']) + $port_association_mode = get_port_assoc_mode_name ($device['port_association_mode']); -foreach ($ports_db as $port) { - $ports[$port['ifIndex']] = $port; +// Query known ports and mapping table in order of discovery to make sure +// the latest discoverd/polled port is in the mapping tables. +$ports_mapped = get_ports_mapped ($device['device_id'], true); +$ports = $ports_mapped['ports']; + +// +// Rename any old RRD files still named after the previous ifIndex based naming schema. +foreach ($ports_mapped['maps']['ifIndex'] as $ifIndex => $port_id) { + foreach (array ('', 'adsl', 'dot3') as $suffix) { + $suffix_tmp = ''; + if ($suffix) + $suffix_tmp = "-$suffix"; + + $old_rrd_path = trim ($config['rrd_dir']) . '/' . $device['hostname'] . "/port-$ifIndex$suffix_tmp.rrd"; + $new_rrd_path = get_port_rrdfile_path ($device['hostname'], $port_id, $suffix); + + if (is_file ($old_rrd_path)) { + rename ($old_rrd_path, $new_rrd_path); + } + } } + // New interface detection foreach ($port_stats as $ifIndex => $port) { + // Store ifIndex in port entry and prefetch ifName as we'll need it multiple times + $port['ifIndex'] = $ifIndex; + $ifName = $port['ifName']; + + // Get port_id according to port_association_mode used for this device + $port_id = get_port_id ($ports_mapped, $port, $port_association_mode); + if (is_port_valid($port, $device)) { echo 'valid'; - if (!is_array($ports[$port['ifIndex']])) { - $port_id = dbInsert(array('device_id' => $device['device_id'], 'ifIndex' => $ifIndex), 'ports'); + + // Port newly discovered? + if (! $ports[$port_id]) { + $port_id = dbInsert(array('device_id' => $device['device_id'], 'ifIndex' => $ifIndex, 'ifName' => $ifName), 'ports'); dbInsert(array('port_id' => $port_id), 'ports_statistics'); - $ports[$port['ifIndex']] = dbFetchRow('SELECT * FROM `ports` WHERE `port_id` = ?', array($port_id)); - echo 'Adding: '.$port['ifName'].'('.$ifIndex.')('.$ports[$port['ifIndex']]['port_id'].')'; + $ports[$port_id] = dbFetchRow('SELECT * FROM `ports` WHERE `port_id` = ?', array($port_id)); + echo 'Adding: '.$ifName.'('.$ifIndex.')('.$port_id.')'; // print_r($ports); } - else if ($ports[$ifIndex]['deleted'] == '1') { - dbUpdate(array('deleted' => '0'), 'ports', '`port_id` = ?', array($ports[$ifIndex]['port_id'])); - $ports[$ifIndex]['deleted'] = '0'; - } - if ($ports[$ifIndex]['ports_statistics_port_id'] === null) { - // in case the port was created before we created the table - dbInsert(array('port_id' => $ports[$ifIndex]['port_id']), 'ports_statistics'); - } - } - else { - if ($ports[$port['ifIndex']]['deleted'] != '1') { - dbUpdate(array('deleted' => '1'), 'ports', '`port_id` = ?', array($ports[$ifIndex]['port_id'])); - $ports[$ifIndex]['deleted'] = '1'; - } - } -} -// End New interface detection + // Port re-discovered after previous deletion? + else if ($ports[$port_id]['deleted'] == 1) { + dbUpdate(array('deleted' => '0'), 'ports', '`port_id` = ?', array($port_id)); + $ports[$port_id]['deleted'] = '0'; + } + if ($ports[$port_id]['ports_statistics_port_id'] === null) { + // in case the port was created before we created the table + dbInsert(array('port_id' => $port_id), 'ports_statistics'); + } + + // Assure stable mapping + $port_stats[$ifIndex]['port_id'] = $port_id; + $ports[$port_id]['ifIndex'] = $ifIndex; + } + + // Port vanished (mark as deleted) + else { + if ($ports[$port_id]['deleted'] != '1') { + dbUpdate(array('deleted' => '1'), 'ports', '`port_id` = ?', array($port_id)); + $ports[$port_id]['deleted'] = '1'; + } + } +} // End new interface detection + + echo "\n"; // Loop ports in the DB and update where necessary foreach ($ports as $port) { - echo 'Port '.$port['ifDescr'].'('.$port['ifIndex'].') '; + $port_id = $port['port_id']; + + echo 'Port ' . $port['ifName'] . ': ' . $port['ifDescr'] . '(' . $port['ifIndex'] . ') '; if ($port_stats[$port['ifIndex']] && $port['disabled'] != '1') { // Check to make sure Port data is cached. $this_port = &$port_stats[$port['ifIndex']]; @@ -477,7 +518,7 @@ foreach ($ports as $port) { } // Update RRDs - $rrdfile = $host_rrd.'/port-'.safename($port['ifIndex'].'.rrd'); + $rrdfile = get_port_rrdfile_path ($device['hostname'], $port_id); if (!is_file($rrdfile)) { rrdtool_create( $rrdfile, @@ -576,9 +617,9 @@ foreach ($ports as $port) { // Update Database if (count($port['update'])) { - $updated = dbUpdate($port['update'], 'ports', '`port_id` = ?', array($port['port_id'])); + $updated = dbUpdate($port['update'], 'ports', '`port_id` = ?', array($port_id)); // do we want to do something else with this? - $updated += dbUpdate($port['update_extended'], 'ports_statistics', '`port_id` = ?', array($port['port_id'])); + $updated += dbUpdate($port['update_extended'], 'ports_statistics', '`port_id` = ?', array($port_id)); d_echo("$updated updated"); } @@ -588,7 +629,7 @@ foreach ($ports as $port) { echo 'Port Deleted'; // Port missing from SNMP cache. if ($port['deleted'] != '1') { - dbUpdate(array('deleted' => '1'), 'ports', '`device_id` = ? AND `ifIndex` = ?', array($device['device_id'], $port['ifIndex'])); + dbUpdate(array('deleted' => '1'), 'ports', '`device_id` = ? AND `port_id` = ?', array($device['device_id'], $port_id)); } } else { @@ -599,7 +640,7 @@ foreach ($ports as $port) { // Clear Per-Port Variables Here unset($this_port); -}//end foreach +} //end port update // Clear Variables Here unset($port_stats); diff --git a/scripts/tune_port.php b/scripts/tune_port.php index 2798469cc..14b62abe6 100755 --- a/scripts/tune_port.php +++ b/scripts/tune_port.php @@ -24,8 +24,7 @@ foreach (dbFetchRows("SELECT `device_id`,`hostname` FROM `devices` WHERE `hostna echo "Found hostname " . $device['hostname'].".......\n"; foreach (dbFetchRows("SELECT `ifIndex`,`ifName`,`ifSpeed` FROM `ports` WHERE `ifName` LIKE ? AND `device_id` = ?", array('%'.$ports.'%',$device['device_id'])) as $port) { echo "Tuning port " . $port['ifName'].".......\n"; - $host_rrd = $config['rrd_dir'].'/'.$device['hostname']; - $rrdfile = $host_rrd.'/port-'.safename($port['ifIndex'].'.rrd'); + $rrdfile = get_port_rrdfile_path ($device['hostname'], $port['port_id']); rrdtool_tune('port',$rrdfile,$port['ifSpeed']); } } diff --git a/sql-schema/096.sql b/sql-schema/096.sql new file mode 100644 index 000000000..58ae65201 --- /dev/null +++ b/sql-schema/096.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS `port_association_mode` (pom_id int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, name varchar(12) NOT NULL); +INSERT INTO port_association_mode (pom_id, name) values (1, 'ifIndex'); +INSERT INTO port_association_mode (name) values ('ifName'); +INSERT INTO port_association_mode (name) values ('ifDescr'); +ALTER TABLE devices ADD port_association_mode int(11) NOT NULL DEFAULT 1;