Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 25 additions & 14 deletions .github/workflows/plugin-ci-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ jobs:

- name: Lint every PHP file
run: |
if find . -path ./vendor -prune -o -name '*.php' -print \
| xargs -n1 php -l 2>&1 | grep -iv 'no syntax errors detected'; then
if find . -path ./vendor -prune -o -name '*.php' -print0 \
| xargs -0 -n1 php -l 2>&1 | grep -iv 'no syntax errors detected'; then
echo "Syntax errors found at the declared PHP floor"
exit 1
fi
Expand Down Expand Up @@ -153,19 +153,18 @@ jobs:

- name: Create MySQL Config
run: |
echo -e "[client]\nuser = root\npassword = cactiroot\nhost = 127.0.0.1\n" > ~/.my.cnf
printf '[client]\nuser = root\npassword = cactiroot\nhost = 127.0.0.1\n' \
> "$RUNNER_TEMP/gpsmap-my.cnf"

- name: Initialize Cacti Database
env:
MYSQL_AUTH_USR: '--defaults-file=~/.my.cnf'
run: |
mysql $MYSQL_AUTH_USR -e 'CREATE DATABASE IF NOT EXISTS cacti;'
mysql $MYSQL_AUTH_USR -e "CREATE USER IF NOT EXISTS 'cactiuser'@'localhost' IDENTIFIED BY 'cactiuser';"
mysql $MYSQL_AUTH_USR -e "GRANT ALL PRIVILEGES ON cacti.* TO 'cactiuser'@'localhost';"
mysql $MYSQL_AUTH_USR -e "GRANT SELECT ON mysql.time_zone_name TO 'cactiuser'@'localhost';"
mysql $MYSQL_AUTH_USR -e "FLUSH PRIVILEGES;"
mysql $MYSQL_AUTH_USR cacti < ${{ github.workspace }}/cacti/cacti.sql
mysql $MYSQL_AUTH_USR -e "INSERT INTO settings (name, value) VALUES ('path_php_binary', '/usr/bin/php')" cacti
mysql --defaults-file="$RUNNER_TEMP/gpsmap-my.cnf" -e 'CREATE DATABASE IF NOT EXISTS cacti;'
mysql --defaults-file="$RUNNER_TEMP/gpsmap-my.cnf" -e "CREATE USER IF NOT EXISTS 'cactiuser'@'localhost' IDENTIFIED BY 'cactiuser';"
mysql --defaults-file="$RUNNER_TEMP/gpsmap-my.cnf" -e "GRANT ALL PRIVILEGES ON cacti.* TO 'cactiuser'@'localhost';"
mysql --defaults-file="$RUNNER_TEMP/gpsmap-my.cnf" -e "GRANT SELECT ON mysql.time_zone_name TO 'cactiuser'@'localhost';"
mysql --defaults-file="$RUNNER_TEMP/gpsmap-my.cnf" -e "FLUSH PRIVILEGES;"
mysql --defaults-file="$RUNNER_TEMP/gpsmap-my.cnf" cacti < ${{ github.workspace }}/cacti/cacti.sql
mysql --defaults-file="$RUNNER_TEMP/gpsmap-my.cnf" -e "INSERT INTO settings (name, value) VALUES ('path_php_binary', '/usr/bin/php')" cacti

- name: Install Composer Dependencies
run: |
Expand Down Expand Up @@ -212,11 +211,15 @@ jobs:
run: |
cd ${{ github.workspace }}/cacti
sudo php cli/plugin_manage.php --plugin=gpsmap --install --enable
mysql -u cactiuser -p'cactiuser' -h 127.0.0.1 cacti -sN \
-e "SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema = 'cacti' AND table_name = 'plugin_gpsmap_dns_cache';" \
| grep -qx '1'

- name: Check PHP Syntax for Plugin
run: |
cd ${{ github.workspace }}/cacti/plugins/gpsmap
if find . -name '*.php' -exec php -l {} 2>&1 \; | grep -iv 'no syntax errors detected'; then
if find . -name '*.php' -print0 | xargs -0 -n1 php -l 2>&1 | grep -iv 'no syntax errors detected'; then
echo "Syntax errors found!"
exit 1
fi
Expand Down Expand Up @@ -293,12 +296,15 @@ jobs:
fi
echo "Using host_template id $TPL"
sudo php cli/add_device.php --description=mapped --ip=127.0.0.1 --template="$TPL"
sudo php cli/add_device.php --description=mapped-hostname --ip=localhost --template="$TPL"
mysql -u cactiuser -p'cactiuser' -h 127.0.0.1 cacti \
-e "UPDATE host SET latitude='51.5074', longitude='-0.1278' WHERE description='mapped';"
-e "UPDATE host SET latitude='51.5074', longitude='-0.1278'
WHERE description IN ('mapped', 'mapped-hostname');"
mysql -u cactiuser -p'cactiuser' -h 127.0.0.1 cacti \
-e "INSERT INTO gpsmap_templates (templateID, templateName, upimage, recoverimage, downimage, AP)
SELECT host_template_id, 'ci', 'Green.png', 'Orange.png', 'Red.png', 0
FROM host WHERE description='mapped' LIMIT 1;"
sudo php plugins/gpsmap/gpsmap_dns.php

- name: Run Cacti Poller
run: |
Expand All @@ -325,6 +331,11 @@ jobs:
cat all.xml
exit 1
fi
if ! grep -q 'address="localhost"' all.xml; then
echo "all.xml contains no hostname-resolved marker"
cat all.xml
exit 1
fi

- name: View Cacti Logs
if: always()
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
# review pipeline scratch
.diffcheck.php
.rv.tar
tests/_dbg.php
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Changelog

--- 2.2 ---

* issue#6, issue#104, issue#107, issue#109, issue#110: Refactor the poller for single-load dual-stack maps, asynchronous last-known-good DNS, thold-aware markers, and safe artifact pruning

--- 2.1 ---

* issue: Add gpsmap_templates table index to be unique by device template
Expand Down
2 changes: 1 addition & 1 deletion INFO
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[info]
name = gpsmap
version = 2.1
version = 2.2
longname = Maps Plugin
author = Andrew Aloia, Wixiweb, The Cacti Group
email =
Expand Down
36 changes: 25 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@ Maps tab reads a static file instead of querying the database.

## Features

* Devices plotted by latitude and longitude, coloured by Up, Recovering, Down,
Disabled or Undefined status
* IPv4 and IPv6 Devices plotted by latitude and longitude, coloured by Up,
Recovering, Down, Disabled or Undefined status. When thold is enabled, an
otherwise-up Device with an active threshold uses its Down icon

* Per Device Template marker icons for the Up, Recovering and Down states

* Coverage overlay drawing an area around Access Points, sized to reach the
furthest Device sharing the Access Point's group number, and optionally
limited to an arc

* Drill down by subnet, from the first octet through to individual Devices
* Drill down by subnet: /8, /16 and /24 for IPv4, and /16, /32 and /48 for IPv6

* KML export alongside the XML, so the same data opens in Google Earth

Expand Down Expand Up @@ -55,8 +56,8 @@ JavaScript, and there is no workaround inside Cacti.

Installing adds seven columns to Cacti's `host` table (`latitude`, `longitude`,
`GPScoverage`, `start`, `stop`, `groupnum`, `rdistance`) and creates the
`gpsmap_templates` table. Uninstalling deliberately leaves both in place so
coordinates survive an accidental removal.
`gpsmap_templates` and `plugin_gpsmap_dns_cache` tables. Uninstalling
deliberately leaves them in place so coordinates survive an accidental removal.

## Configuration

Expand Down Expand Up @@ -139,13 +140,21 @@ Understanding the order of events explains most of what can go wrong.
1. The poller runs and Cacti calls the plugin's `poller_bottom` hook.

2. The plugin selects every Device whose Device Template appears in
`gpsmap_templates`, resolves each hostname to an address, and discards any
Device without coordinates or whose name does not resolve to IPv4.
`gpsmap_templates`. Literal IPv4 and IPv6 addresses are used directly;
configured names use the last address in `plugin_gpsmap_dns_cache`.

3. It writes `plugins/gpsmap/XML/all.xml`, `all.kml` and `all-top.html`, then
repeats for each subnet prefix so the drill-down views are prebuilt.

4. Opening the Maps tab serves a page that loads Google's map library and
4. It starts a separate DNS refresh worker after publishing. A newly configured
hostname can therefore appear on the following poll, while slow DNS never
holds up the poller. A failed refresh retains the last-known address.

5. Generated subnet files not refreshed for three poller intervals are removed.
Cleanup runs only after a successful Device query; unrelated files are never
touched.

6. Opening the Maps tab serves a page that loads Google's map library and
fetches the matching `.xml` file over HTTP.

Nothing appears until a poll cycle has completed after configuration. If
Expand All @@ -165,9 +174,14 @@ cycle has completed since both were set; and `plugins/gpsmap/XML` contains
files newer than the last poll.

**Some Devices are missing.** A Device is skipped when it has no coordinates,
when its hostname does not resolve to an IPv4 address, when its Device Template
is not in Map Templates, or when it is disabled and Display Disabled Devices is
off.
when its hostname has no cached address yet, when its Device Template is not in
Map Templates, or when it is disabled and Display Disabled Devices is off. For
a newly added hostname, allow one additional poll cycle for the asynchronous DNS
worker to populate the cache.

**An Up Device has a red marker.** When the thold plugin is enabled, gpsmap
uses the configured Down icon for an otherwise-up Device with any active,
enabled threshold. Disable or clear the threshold to restore the Up icon.

**The XML directory is empty.** The Data Collector cannot write to it. Check
ownership, permissions and SELinux context. The Cacti log records a failure
Expand Down
56 changes: 28 additions & 28 deletions gpsmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,17 @@

general_header();

//set headers to NOT cache a page
header("Cache-Control: no-cache, no-store, must-revalidate");
header("Pragma: no-cache");
header("Expires: 0");
// set headers to NOT cache a page
header('Cache-Control: no-cache, no-store, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');

//decide what needs to be shown
// decide what needs to be shown
switch ($show) {
//selected nodes
// selected nodes
case 'setup':
$body = __('Please make sure to properly configure Maps first under Settings > Maps', 'gpsmap');

break;
default:
if (!$parameter) {
Expand All @@ -46,44 +47,44 @@
/* Reject any parameter that contains directory traversal sequences or
* characters outside the safe set. basename() alone does not strip
* embedded ../ so we validate the whole value first. */
if (!preg_match('/^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*$/', $parameter)) {
if (!preg_match('/^(?:all|[0-9]+(?:\.[0-9]+){0,2}|v6-(?:16-[0-9a-f]{4}|32-[0-9a-f]{8}|48-[0-9a-f]{12}))$/', $parameter)) {
$parameter = 'all';
}

$fileLocation = './plugins/gpsmap/XML/' . $parameter . '-top.html';

if (file_exists($fileLocation)) {
echo file_get_contents($fileLocation);
print file_get_contents($fileLocation);
}

break;
}

//---------------------------------------------------------------
// ---------------------------------------------------------------

if ($show != 'setup') { ?>
<script type='text/javascript'>
var initialLat = <?php echo is_finite((float) $initialLat) ? json_encode((float) $initialLat) : '0'; ?>;
var initialLng = <?php echo is_finite((float) $initialLong) ? json_encode((float) $initialLong) : '0'; ?>;
var initialZoom = <?php echo json_encode((int) $initialzoom); ?>;
var initialLat = <?php print is_finite((float) $initialLat) ? json_encode((float) $initialLat) : '0'; ?>;
var initialLng = <?php print is_finite((float) $initialLong) ? json_encode((float) $initialLong) : '0'; ?>;
var initialZoom = <?php print json_encode((int) $initialzoom); ?>;
</script>
<script type='text/javascript'>
gpsmap.refreshMap = <?php echo json_encode((string) $refreshMap, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.initialLat = <?php echo is_finite((float) $initialLat) ? json_encode((float) $initialLat) : '0'; ?>;
gpsmap.initialLng = <?php echo is_finite((float) $initialLong) ? json_encode((float) $initialLong) : '0'; ?>;
gpsmap.initialZoom = <?php echo json_encode((int) $initialzoom); ?>;
gpsmap.refreshMap = <?php print json_encode((string) $refreshMap, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.initialLat = <?php print is_finite((float) $initialLat) ? json_encode((float) $initialLat) : '0'; ?>;
gpsmap.initialLng = <?php print is_finite((float) $initialLong) ? json_encode((float) $initialLong) : '0'; ?>;
gpsmap.initialZoom = <?php print json_encode((int) $initialzoom); ?>;
gpsmap.initialized = false;
gpsmap.liColor = <?php echo json_encode((string) $liColor, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.liWidth = <?php echo json_encode((string) $liWidth, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.liOpa = <?php echo json_encode((string) $liOpa, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.fillColor = <?php echo json_encode((string) $fillColor, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.fillOpa = <?php echo json_encode((string) $fillOpa, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.circleQuality = <?php echo json_encode((string) $circleQuality, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.enableWeather = <?php echo json_encode((string) $enableWeather, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.coverageOverlay = <?php echo json_encode((string) $coverageMap, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.liColor = <?php print json_encode((string) $liColor, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.liWidth = <?php print json_encode((string) $liWidth, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.liOpa = <?php print json_encode((string) $liOpa, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.fillColor = <?php print json_encode((string) $fillColor, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.fillOpa = <?php print json_encode((string) $fillOpa, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.circleQuality = <?php print json_encode((string) $circleQuality, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.enableWeather = <?php print json_encode((string) $enableWeather, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.coverageOverlay = <?php print json_encode((string) $coverageMap, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.markerArray = [];
gpsmap.downloadURL = <?php echo json_encode($config['url_path'] . 'plugins/gpsmap/XML/' . $parameter . '.xml', JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.t_error = <?php echo is_finite((float) $terror) ? json_encode((float) $terror) : '0'; ?>;
gpsmap.downloadURL = <?php print json_encode($config['url_path'] . 'plugins/gpsmap/XML/' . $parameter . '.xml', JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>;
gpsmap.t_error = <?php print is_finite((float) $terror) ? json_encode((float) $terror) : '0'; ?>;

<?php include_once('plugins/gpsmap/includes/icons.php'); ?>
<?php include_once('plugins/gpsmap/includes/customicons.php'); ?>
Expand All @@ -101,7 +102,6 @@
<div id="mouse-position"></div>
<div id="map"></div>
';
echo $body;
print $body;

bottom_footer();

13 changes: 13 additions & 0 deletions gpsmap_dns.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env php
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
*/

chdir(__DIR__ . '/../../');
require_once('./include/cli_check.php');
require_once($config['base_path'] . '/plugins/gpsmap/includes/dns.php');

exit(gpsmap_dns_refresh_exit_code());
46 changes: 45 additions & 1 deletion gpsmap_security.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* includes setup.php on install, uninstall and config check, so the poller
* would not have them. */
if (!defined('GPSMAP_ICON_EXTENSIONS')) {
define('GPSMAP_ICON_EXTENSIONS', array('png', 'jpg', 'jpeg', 'gif'));
define('GPSMAP_ICON_EXTENSIONS', ['png', 'jpg', 'jpeg', 'gif']);
}

/* An icon's base name is emitted as a JavaScript assignment target
Expand All @@ -38,3 +38,47 @@ function gpsmap_normalize_icon_name($value, $icon_array, $default = 'Green.png')

return $value;
}

// ---------------------------------------------------------------
function getIcons() {
$iconArray = [];
global $config;

/* Built from base_path so poller and CLI callers resolve it too; only web
* entry points chdir() to the Cacti root. */
$dir = $config['base_path'] . '/plugins/gpsmap/images/icons';

/* Suppressed rather than warned: this runs on the Map Templates page, and a
* missing icon folder should not print a PHP warning into the form. */
$icons = @opendir($dir);

if ($icons === false) {
cacti_log('WARNING: gpsmap could not open icon directory ' . $dir, false, 'GPSMAP');

return $iconArray;
}

while (false !== ($icon = readdir($icons))) {
if (!is_file($dir . '/' . $icon)) {
continue;
}

/* Offer only names the map can actually render. icons.php emits each
* base name as a JavaScript identifier, so a file this rule rejects
* would appear in the dropdown, save cleanly, and then silently fail
* to draw. */
if (!in_array(strtolower(pathinfo($icon, PATHINFO_EXTENSION)), GPSMAP_ICON_EXTENSIONS, true)) {
continue;
}

if (gpsmap_icon_identifier($icon) === null) {
continue;
}

$iconArray[$icon] = $icon;
}

closedir($icons);

return $iconArray;
}
Loading
Loading