From 128fc9896c7098825c843d82a24101276c4f33ff Mon Sep 17 00:00:00 2001 From: Sean Mancini Date: Fri, 9 Oct 2026 23:23:53 -0400 Subject: [PATCH 1/6] The Syslog refactor moved syslog_replicate_in() out of setup.php into includes/processing.php. But syslog_process.php still loaded only setup.php before calling that function on remote pollers. Because the refactor loads modules only when needed, the function was no longer defined, causing a fatal error. Cacti then disabled the plugin. --- syslog_process.php | 1 + tests/Unit/SetupModuleBoundaryTest.php | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/syslog_process.php b/syslog_process.php index 333be3f2..f718014e 100644 --- a/syslog_process.php +++ b/syslog_process.php @@ -214,6 +214,7 @@ } // replicate in syslog tables sync is enabled + require_once(__DIR__ . '/includes/processing.php'); syslog_replicate_in(); } diff --git a/tests/Unit/SetupModuleBoundaryTest.php b/tests/Unit/SetupModuleBoundaryTest.php index 250f2d9b..8bf3fac9 100644 --- a/tests/Unit/SetupModuleBoundaryTest.php +++ b/tests/Unit/SetupModuleBoundaryTest.php @@ -77,6 +77,14 @@ } }); +it('loads the processing module before remote CLI replication', function () { + $source = plugin_test_read_source('syslog_process.php'); + $include = strpos($source, "require_once(__DIR__ . '/includes/processing.php');"); + $call = strpos($source, 'syslog_replicate_in();'); + + expect($include)->not->toBeFalse()->toBeLessThan($call); +}); + it('registers extracted callbacks against their owning module', function () { $setup = plugin_test_read_source('setup.php'); From 4b1bf459248dc7b90509500bab37d6614d08d652 Mon Sep 17 00:00:00 2001 From: Sean Mancini Date: Sat, 10 Oct 2026 07:53:51 -0400 Subject: [PATCH 2/6] Add a cli script to allow an admin to repair the syslog schema or upgrade via cli --- cli/syslog_schema.php | 102 +++++++++++++++++ includes/schema.php | 21 +++- setup.php | 16 ++- tests/regression/cli_schema_upgrade_test.php | 114 +++++++++++++++++++ 4 files changed, 241 insertions(+), 12 deletions(-) create mode 100644 cli/syslog_schema.php create mode 100644 tests/regression/cli_schema_upgrade_test.php diff --git a/cli/syslog_schema.php b/cli/syslog_schema.php new file mode 100644 index 00000000..5cda4dae --- /dev/null +++ b/cli/syslog_schema.php @@ -0,0 +1,102 @@ + 'upgrade', + 'engine' => read_config_option('syslog_install_engine') ?: 'InnoDB', + 'db_type' => 'part', + 'days' => read_config_option('syslog_install_days') +], true); +syslog_check_upgrade(true); + +$tables = [ + 'syslog', 'syslog_alert', 'syslog_alert_suppression', 'syslog_incoming', + 'syslog_remove', 'syslog_reports', 'syslog_saved_searches', 'syslog_status', + 'syslog_programs', 'syslog_hosts', 'syslog_facilities', 'syslog_priorities', + 'syslog_host_facilities', 'syslog_removed', 'syslog_logs', + 'syslog_dashboards', 'syslog_dashboard_panels', 'syslog_dashboards_perm', + 'syslog_saved_searches_perm', 'syslog_device_rule', 'syslog_replication_output', + 'syslog_replication_recovery' +]; +if ((int) $config['poller_id'] <= 1) { + $tables[] = 'syslog_replication_receipts'; + $tables[] = 'syslog_replication_collectors'; +} + +$missing = []; +foreach ($tables as $table) { + if (!syslog_db_table_exists($table, false)) { + $missing[] = $table; + } +} +if (!empty($database_last_error)) { + fwrite(STDERR, "ERROR: Schema upgrade encountered a database error; check the Cacti log.\n"); + exit(1); +} +if ($missing) { + fwrite(STDERR, 'ERROR: Tables still missing: ' . implode(', ', $missing) . "\n"); + exit(1); +} + +print "Syslog schema upgrade completed; all required tables exist.\n"; diff --git a/includes/schema.php b/includes/schema.php index 9ab9c562..870e2945 100644 --- a/includes/schema.php +++ b/includes/schema.php @@ -345,10 +345,11 @@ function syslog_create_partitioned_syslog_table($engine = 'InnoDB', $days = 30, * @param array $options The install options, either from the * request, saved settings, or the Syslog * config file. + * @param bool $repair Preserve existing lookup tables and configured defaults. * * @return void */ -function syslog_setup_table_new(array $options): void { +function syslog_setup_table_new(array $options, bool $repair = false): void { global $config, $settings, $syslogdb_default, $syslog_levels; syslog_connect(); @@ -391,6 +392,10 @@ function syslog_setup_table_new(array $options): void { } } + if ($repair) { + $options['upgrade_type'] = 'upgrade'; + } + // Partitioned tables are the only supported architecture. Traditional // (non-partitioned) tables are deprecated, so the 'trad' architecture // silently upgrades to partitioned table creation no matter how the @@ -617,7 +622,9 @@ function syslog_setup_table_new(array $options): void { ROW_FORMAT=Dynamic COMMENT='Contains all hosts currently in the syslog table'"); - syslog_db_execute("DROP TABLE IF EXISTS `$syslogdb_default`.`syslog_facilities`"); + if (!$repair) { + syslog_db_execute("DROP TABLE IF EXISTS `$syslogdb_default`.`syslog_facilities`"); + } syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_facilities` ( `facility_id` int(10) unsigned NOT NULL, @@ -628,13 +635,15 @@ function syslog_setup_table_new(array $options): void { ENGINE=InnoDB ROW_FORMAT=Dynamic"); - syslog_db_execute("INSERT INTO `$syslogdb_default`.`syslog_facilities` (facility_id, facility) VALUES + syslog_db_execute("INSERT IGNORE INTO `$syslogdb_default`.`syslog_facilities` (facility_id, facility) VALUES (0,'kern'), (1,'user'), (2,'mail'), (3,'daemon'), (4,'auth'), (5,'syslog'), (6,'lpd'), (7,'news'), (8,'uucp'), (9,'crond'), (10,'authpriv'), (11,'ftpd'), (12,'ntpd'), (13,'logaudit'), (14,'logalert'), (15,'crond'), (16,'local0'), (17,'local1'), (18,'local2'), (19,'local3'), (20,'local4'), (21,'local5'), (22,'local6'), (23,'local7')"); - syslog_db_execute("DROP TABLE IF EXISTS `$syslogdb_default`.`syslog_priorities`"); + if (!$repair) { + syslog_db_execute("DROP TABLE IF EXISTS `$syslogdb_default`.`syslog_priorities`"); + } syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_priorities` ( `priority_id` int(10) unsigned NOT NULL, @@ -645,7 +654,7 @@ function syslog_setup_table_new(array $options): void { ENGINE=InnoDB ROW_FORMAT=Dynamic"); - syslog_db_execute("INSERT INTO `$syslogdb_default`.`syslog_priorities` (priority_id, priority) VALUES + syslog_db_execute("INSERT IGNORE INTO `$syslogdb_default`.`syslog_priorities` (priority_id, priority) VALUES (0,'emerg'), (1,'alert'), (2,'crit'), (3,'err'), (4,'warning'), (5,'notice'), (6,'info'), (7,'debug'), (8,'other')"); syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_host_facilities` ( @@ -691,7 +700,7 @@ function syslog_setup_table_new(array $options): void { syslog_config_settings(); } - foreach ($settings['syslog'] as $name => $values) { + foreach ($repair ? [] : $settings['syslog'] as $name => $values) { if (isset($values['default'])) { set_config_option($name, $values['default']); } diff --git a/setup.php b/setup.php index 262ed712..103258ea 100644 --- a/setup.php +++ b/setup.php @@ -290,9 +290,11 @@ function syslog_config_safe(): bool { /** * Connect to the Syslog database, either the local Cacti database or a remote one. * + * @param bool $repair Skip automatic install so the CLI can repair non-destructively. + * * @return bool True when a Syslog database connection is available, false otherwise. */ -function syslog_connect(): bool { +function syslog_connect(bool $repair = false): bool { global $config, $syslog_cnn, $syslogdb_default, $local_db_cnn_id, $remote_db_cnn_id, $syslog_incoming_config; syslog_determine_config(); @@ -373,7 +375,7 @@ function syslog_connect(): bool { } } - if ($connected && !syslog_db_table_exists('syslog') && api_plugin_is_enabled('syslog')) { + if (!$repair && $connected && !syslog_db_table_exists('syslog') && api_plugin_is_enabled('syslog')) { cacti_log('Setting Up Database Tables Since they do not exist', false, 'SYSLOG'); if (!isset($syslog_install_options)) { @@ -698,9 +700,11 @@ function syslog_upgrade_create_permission_realms(): bool { /** * Upgrade the Syslog database schema for legacy installs. * + * @param bool $force Run migrations even when the version is current, for CLI repair. + * * @return void */ -function syslog_check_upgrade(): void { +function syslog_check_upgrade(bool $force = false): void { global $config, $syslogdb_default, $syslog_levels, $syslog_upgrade; require_once(__DIR__ . '/includes/schema.php'); @@ -724,7 +728,7 @@ function syslog_check_upgrade(): void { // Let's only run this check if we are on a page that actually needs the data $files = ['plugins.php', 'syslog.php', 'syslog_removal.php', 'syslog_alerts.php', 'syslog_device_rules.php', 'syslog_reports.php', 'syslog_saved_searches.php', 'syslog_dashboards.php']; - if (substr($_SERVER['SCRIPT_FILENAME'], -18) != 'syslog_process.php' && !in_array(get_current_page(), $files, true)) { + if (!$force && substr($_SERVER['SCRIPT_FILENAME'], -18) != 'syslog_process.php' && !in_array(get_current_page(), $files, true)) { return; } @@ -734,7 +738,7 @@ function syslog_check_upgrade(): void { ini_set('max_execution_time', 0); if (function_exists('api_plugin_upgrade_register')) { - if (!api_plugin_upgrade_register('syslog')) { + if (!api_plugin_upgrade_register('syslog') && !$force) { // This table was introduced after the original remote schema. Ensure // an already-current remote collector can repair the omission without // requiring a plugin version change. @@ -749,7 +753,7 @@ function syslog_check_upgrade(): void { $current = $version['version']; $old = db_fetch_cell("SELECT version FROM plugin_config WHERE directory='syslog'"); - if ($current != $old) { + if ($current != $old || $force) { api_plugin_register_hook('syslog', 'replicate_out', 'syslog_replicate_out', 'includes/processing.php', 1); db_execute_prepared('UPDATE plugin_config SET diff --git a/tests/regression/cli_schema_upgrade_test.php b/tests/regression/cli_schema_upgrade_test.php new file mode 100644 index 00000000..1383e12a --- /dev/null +++ b/tests/regression/cli_schema_upgrade_test.php @@ -0,0 +1,114 @@ + ['syslog_rows' => ['default' => 750]]]; +$syslogdb_default = 'syslog'; +function syslog_connect() { return true; } +function cacti_sizeof($value) { return count($value); } +function read_config_option($name) { return ''; } +function set_config_option($name, $value, $force = false) { + global $options_saved; + $options_saved[$name] = $value; +} +function syslog_install_storage_engine($engine, $options) { return $engine; } +function syslog_db_fetch_row($sql) { return []; } +function syslog_db_execute($sql) { + global $queries; + $queries[] = $sql; + return true; +} +function syslog_create_partitioned_syslog_table($engine, $days, $ahead) { + check($days === 0, 'Repair must preserve indefinite retention'); +} +function syslog_create_replication_output_table() {} +function syslog_create_replication_receipts_table() {} +function syslog_create_replication_collectors_table() {} +function syslog_create_replication_recovery_table() {} +function syslog_ensure_share_tables() {} + +for ($run = 0; $run < 2; $run++) { + syslog_setup_table_new(['upgrade_type' => 'truncate', 'engine' => 'InnoDB', 'days' => 0], true); +} +foreach ($queries as $sql) { + check(!preg_match('/\b(DROP|TRUNCATE|DELETE)\b/i', $sql), 'Repair issued destructive SQL'); +} +check(!isset($options_saved['syslog_rows']), 'Repair reset an existing preference'); +check(substr_count(implode("\n", $queries), 'INSERT IGNORE INTO') === 4, 'Lookup seeds must tolerate repeat repairs'); + +// Run the actual CLI in an isolated Cacti layout with bootstrap/schema fixtures. +$fixture = sys_get_temp_dir() . '/syslog-cli-' . bin2hex(random_bytes(6)); +mkdir($fixture . '/include', 0700, true); +mkdir($fixture . '/plugins/syslog/cli', 0700, true); +mkdir($fixture . '/plugins/syslog/includes', 0700, true); +copy($root . '/cli/upgrade_database.php', $fixture . '/plugins/syslog/cli/upgrade_database.php'); +file_put_contents($fixture . '/include/cli_check.php', ' getenv("SYSLOG_TEST_REMOTE") ? 2 : 1];'); +file_put_contents($fixture . '/plugins/syslog/setup.php', <<<'STUB' + ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes); + $output = stream_get_contents($pipes[1]) . stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + return [proc_close($process), $output]; +} +try { + check(run_cli($fixture)[0] === 0, 'CLI repair failed'); + check(run_cli($fixture, ['--upgrade'])[0] === 0, 'CLI upgrade failed'); + check(run_cli($fixture, ['--repair', '--upgrade'])[0] === 0, 'Combined switches failed'); + check(run_cli($fixture, [])[0] === 1, 'CLI ran without an explicit action'); + check(run_cli($fixture, ['--help'])[0] === 0, 'CLI help failed'); + check(run_cli($fixture, ['--truncate'])[0] === 1, 'CLI accepted destructive option'); + putenv('SYSLOG_TEST_MISSING=syslog_dashboards'); + [$status, $output] = run_cli($fixture); + check($status === 1 && strpos($output, 'syslog_dashboards') !== false, 'CLI did not report missing table'); + putenv('SYSLOG_TEST_MISSING'); + putenv('SYSLOG_TEST_SQL_ERROR=1'); + check(run_cli($fixture)[0] === 1, 'CLI did not report migration SQL error'); + putenv('SYSLOG_TEST_SQL_ERROR'); + putenv('SYSLOG_TEST_REMOTE=1'); + check(run_cli($fixture)[0] === 0, 'Remote CLI required main-only tables'); +} finally { + putenv('SYSLOG_TEST_REMOTE'); + putenv('SYSLOG_TEST_SQL_ERROR'); + putenv('SYSLOG_TEST_MISSING'); + $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($fixture, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST); + foreach ($files as $file) { + $file->isDir() ? rmdir($file->getPathname()) : unlink($file->getPathname()); + } + rmdir($fixture); +} +print "CLI schema upgrade regression checks passed.\n"; From a2f8cb3006c92a8745e3f156152f5be915120ef3 Mon Sep 17 00:00:00 2001 From: Sean Mancini Date: Sat, 10 Oct 2026 07:57:51 -0400 Subject: [PATCH 3/6] fix import path --- cli/syslog_schema.php | 4 ++-- tests/regression/cli_schema_upgrade_test.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/syslog_schema.php b/cli/syslog_schema.php index 5cda4dae..45533bbf 100644 --- a/cli/syslog_schema.php +++ b/cli/syslog_schema.php @@ -23,7 +23,7 @@ */ // Plugin CLI scripts are one directory deeper than plugin entry points. -include(__DIR__ . '/../../include/cli_check.php'); +include(__DIR__ . '/../../../include/cli_check.php'); require_once(dirname(__DIR__) . '/setup.php'); require_once(dirname(__DIR__) . '/includes/schema.php'); require_once(dirname(__DIR__) . '/includes/settings.php'); @@ -31,7 +31,7 @@ $action_requested = false; foreach (array_slice($_SERVER['argv'], 1) as $argument) { if (in_array($argument, ['--help', '-h', '-H'], true)) { - print "Usage: php upgrade_database.php --repair|--upgrade\n --repair Repair missing tables and rerun schema migrations.\n --upgrade Upgrade the schema, including missing-table repair.\nBoth switches preserve existing data and preferences.\n"; + print "Usage: php syslog_schema.php --repair|--upgrade\n --repair Repair missing tables and rerun schema migrations.\n --upgrade Upgrade the schema, including missing-table repair.\nBoth switches preserve existing data and preferences.\n"; exit(0); } diff --git a/tests/regression/cli_schema_upgrade_test.php b/tests/regression/cli_schema_upgrade_test.php index 1383e12a..1a72f434 100644 --- a/tests/regression/cli_schema_upgrade_test.php +++ b/tests/regression/cli_schema_upgrade_test.php @@ -51,7 +51,7 @@ function syslog_ensure_share_tables() {} mkdir($fixture . '/include', 0700, true); mkdir($fixture . '/plugins/syslog/cli', 0700, true); mkdir($fixture . '/plugins/syslog/includes', 0700, true); -copy($root . '/cli/upgrade_database.php', $fixture . '/plugins/syslog/cli/upgrade_database.php'); +copy($root . '/cli/syslog_schema.php', $fixture . '/plugins/syslog/cli/syslog_schema.php'); file_put_contents($fixture . '/include/cli_check.php', ' getenv("SYSLOG_TEST_REMOTE") ? 2 : 1];'); file_put_contents($fixture . '/plugins/syslog/setup.php', <<<'STUB' ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes); $output = stream_get_contents($pipes[1]) . stream_get_contents($pipes[2]); fclose($pipes[1]); From 7ca4366aad7b414f040c1e26d299cdf1fe68b779 Mon Sep 17 00:00:00 2001 From: Sean Mancini Date: Sat, 10 Oct 2026 08:01:54 -0400 Subject: [PATCH 4/6] allow for auditing the schema --- cli/syslog_schema.php | 46 ++++++++++++++------ includes/settings.php | 4 ++ tests/regression/cli_schema_upgrade_test.php | 23 ++++++++++ 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/cli/syslog_schema.php b/cli/syslog_schema.php index 45533bbf..1b680c73 100644 --- a/cli/syslog_schema.php +++ b/cli/syslog_schema.php @@ -22,6 +22,9 @@ +-------------------------------------------------------------------------+ */ +// Suppress automatic Syslog migrations during Cacti bootstrap. +define('SYSLOG_SCHEMA_CLI', true); + // Plugin CLI scripts are one directory deeper than plugin entry points. include(__DIR__ . '/../../../include/cli_check.php'); require_once(dirname(__DIR__) . '/setup.php'); @@ -29,12 +32,18 @@ require_once(dirname(__DIR__) . '/includes/settings.php'); $action_requested = false; +$audit = false; foreach (array_slice($_SERVER['argv'], 1) as $argument) { if (in_array($argument, ['--help', '-h', '-H'], true)) { - print "Usage: php syslog_schema.php --repair|--upgrade\n --repair Repair missing tables and rerun schema migrations.\n --upgrade Upgrade the schema, including missing-table repair.\nBoth switches preserve existing data and preferences.\n"; + print "Usage: php syslog_schema.php --audit|--repair|--upgrade\n --audit Report missing tables without changing the schema.\n --repair Repair missing tables and rerun schema migrations.\n --upgrade Upgrade the schema, including missing-table repair.\nRepair and upgrade preserve existing data and preferences.\n"; exit(0); } + if ($argument === '--audit') { + $audit = true; + continue; + } + if (in_array($argument, ['--repair', '--upgrade'], true)) { $action_requested = true; continue; @@ -44,8 +53,13 @@ exit(1); } -if (!$action_requested) { - fwrite(STDERR, "ERROR: Specify --repair or --upgrade. Use --help for usage.\n"); +if ($audit && $action_requested) { + fwrite(STDERR, "ERROR: --audit cannot be combined with --repair or --upgrade.\n"); + exit(1); +} + +if (!$action_requested && !$audit) { + fwrite(STDERR, "ERROR: Specify --audit, --repair or --upgrade. Use --help for usage.\n"); exit(1); } @@ -61,14 +75,16 @@ exit(1); } -print "Upgrading Syslog schema and repairing missing tables...\n"; -syslog_setup_table_new([ - 'upgrade_type' => 'upgrade', - 'engine' => read_config_option('syslog_install_engine') ?: 'InnoDB', - 'db_type' => 'part', - 'days' => read_config_option('syslog_install_days') -], true); -syslog_check_upgrade(true); +if (!$audit) { + print "Upgrading Syslog schema and repairing missing tables...\n"; + syslog_setup_table_new([ + 'upgrade_type' => 'upgrade', + 'engine' => read_config_option('syslog_install_engine') ?: 'InnoDB', + 'db_type' => 'part', + 'days' => read_config_option('syslog_install_days') + ], true); + syslog_check_upgrade(true); +} $tables = [ 'syslog', 'syslog_alert', 'syslog_alert_suppression', 'syslog_incoming', @@ -91,12 +107,14 @@ } } if (!empty($database_last_error)) { - fwrite(STDERR, "ERROR: Schema upgrade encountered a database error; check the Cacti log.\n"); + fwrite(STDERR, "ERROR: Schema check encountered a database error; check the Cacti log.\n"); exit(1); } if ($missing) { - fwrite(STDERR, 'ERROR: Tables still missing: ' . implode(', ', $missing) . "\n"); + fwrite(STDERR, 'ERROR: Missing tables: ' . implode(', ', $missing) . "\n"); exit(1); } -print "Syslog schema upgrade completed; all required tables exist.\n"; +print $audit + ? "Syslog schema audit passed; all required tables exist.\n" + : "Syslog schema upgrade completed; all required tables exist.\n"; diff --git a/includes/settings.php b/includes/settings.php index 56ae969a..cf70ccad 100644 --- a/includes/settings.php +++ b/includes/settings.php @@ -585,6 +585,10 @@ function syslog_settings_bottom(): void { * @return void */ function syslog_config_insert(): void { + if (defined('SYSLOG_SCHEMA_CLI') && SYSLOG_SCHEMA_CLI) { + return; + } + if (!syslog_config_safe()) { return; } diff --git a/tests/regression/cli_schema_upgrade_test.php b/tests/regression/cli_schema_upgrade_test.php index 1a72f434..57a8fe73 100644 --- a/tests/regression/cli_schema_upgrade_test.php +++ b/tests/regression/cli_schema_upgrade_test.php @@ -46,6 +46,13 @@ function syslog_ensure_share_tables() {} check(!isset($options_saved['syslog_rows']), 'Repair reset an existing preference'); check(substr_count(implode("\n", $queries), 'INSERT IGNORE INTO') === 4, 'Lookup seeds must tolerate repeat repairs'); +// The real configuration hook must not migrate during schema CLI bootstrap. +define('SYSLOG_SCHEMA_CLI', true); +function syslog_config_safe() { throw new RuntimeException('CLI bootstrap ran the migration hook'); } +preg_match('/function syslog_config_insert\(\): void \{.*?^\}/ms', file_get_contents($root . '/includes/settings.php'), $hook); +eval($hook[0]); +syslog_config_insert(); + // Run the actual CLI in an isolated Cacti layout with bootstrap/schema fixtures. $fixture = sys_get_temp_dir() . '/syslog-cli-' . bin2hex(random_bytes(6)); mkdir($fixture . '/include', 0700, true); @@ -59,11 +66,13 @@ function syslog_determine_config() { define('SYSLOG_CONFIG', __FILE__); } function syslog_connect($repair) { if (!$repair) { throw new Exception('Unsafe connect'); } return true; } function read_config_option($name) { return $name === 'syslog_install_days' ? '0' : ''; } function syslog_setup_table_new($options, $repair) { + if (getenv('SYSLOG_TEST_AUDIT')) { throw new Exception('Audit attempted table repair'); } if (!$repair || $options['upgrade_type'] !== 'upgrade' || $options['days'] !== '0') { throw new Exception('Unsafe repair options'); } } function syslog_check_upgrade($force) { + if (getenv('SYSLOG_TEST_AUDIT')) { throw new Exception('Audit attempted migrations'); } if (!$force) { throw new Exception('Migrations were not forced'); } if (getenv('SYSLOG_TEST_SQL_ERROR')) { $GLOBALS['database_last_error'] = 'fixture error'; } } @@ -86,6 +95,19 @@ function run_cli($fixture, $arguments = ['--repair']) { return [proc_close($process), $output]; } try { + putenv('SYSLOG_TEST_AUDIT=1'); + [$status, $output] = run_cli($fixture, ['--audit']); + check($status === 0 && strpos($output, 'audit passed') !== false, 'Read-only audit failed'); + putenv('SYSLOG_TEST_MISSING=syslog_dashboards'); + [$status, $output] = run_cli($fixture, ['--audit']); + check($status === 1 && strpos($output, 'syslog_dashboards') !== false, 'Audit did not report missing table'); + putenv('SYSLOG_TEST_MISSING'); + check(run_cli($fixture, ['--audit', '--repair'])[0] === 1, 'Audit accepted repair'); + check(run_cli($fixture, ['--audit', '--upgrade'])[0] === 1, 'Audit accepted upgrade'); + putenv('SYSLOG_TEST_REMOTE=1'); + check(run_cli($fixture, ['--audit'])[0] === 0, 'Remote audit required main-only tables'); + putenv('SYSLOG_TEST_REMOTE'); + putenv('SYSLOG_TEST_AUDIT'); check(run_cli($fixture)[0] === 0, 'CLI repair failed'); check(run_cli($fixture, ['--upgrade'])[0] === 0, 'CLI upgrade failed'); check(run_cli($fixture, ['--repair', '--upgrade'])[0] === 0, 'Combined switches failed'); @@ -102,6 +124,7 @@ function run_cli($fixture, $arguments = ['--repair']) { putenv('SYSLOG_TEST_REMOTE=1'); check(run_cli($fixture)[0] === 0, 'Remote CLI required main-only tables'); } finally { + putenv('SYSLOG_TEST_AUDIT'); putenv('SYSLOG_TEST_REMOTE'); putenv('SYSLOG_TEST_SQL_ERROR'); putenv('SYSLOG_TEST_MISSING'); From 7a051375d291c41e7f69042227cf5661eb42c1ec Mon Sep 17 00:00:00 2001 From: Sean Mancini Date: Sat, 10 Oct 2026 08:09:54 -0400 Subject: [PATCH 5/6] Update styling --- .github/copilot-instructions.md | 3 +- cli/syslog_schema.php | 163 ++++++++++++------- tests/regression/cli_schema_upgrade_test.php | 6 +- 3 files changed, 115 insertions(+), 57 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c044f907..92790e09 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -62,7 +62,8 @@ plugin_syslog/ - **Read:** `syslog_db_fetch_assoc($sql)`, `syslog_db_fetch_cell($sql)` - **Write:** `syslog_db_execute($sql)`, `syslog_db_execute_prepared($sql, $params)` - **Connection:** Managed via `$syslog_cnn` global. -- **Schema:** Tables are defined/updated in `setup.php` (`syslog_setup_table_new`). +- **Schema:** Table creation is defined in `includes/schema.php` (`syslog_setup_table_new`); upgrade orchestration is in `setup.php`. +- **New Tables:** Whenever adding a Syslog table, you MUST update `cli/syslog_schema.php` in the same change. Include the table in its required-table list with the correct main/remote collector scope so `--audit` detects its absence, and ensure `--repair` and `--upgrade` can create it. Update the CLI schema regression checks accordingly. ### Cacti Integration Patterns - **Hooks:** Register hooks in `plugin_syslog_install()` in `setup.php`, but diff --git a/cli/syslog_schema.php b/cli/syslog_schema.php index 1b680c73..7cc3de45 100644 --- a/cli/syslog_schema.php +++ b/cli/syslog_schema.php @@ -25,58 +25,78 @@ // Suppress automatic Syslog migrations during Cacti bootstrap. define('SYSLOG_SCHEMA_CLI', true); -// Plugin CLI scripts are one directory deeper than plugin entry points. include(__DIR__ . '/../../../include/cli_check.php'); require_once(dirname(__DIR__) . '/setup.php'); require_once(dirname(__DIR__) . '/includes/schema.php'); require_once(dirname(__DIR__) . '/includes/settings.php'); -$action_requested = false; -$audit = false; -foreach (array_slice($_SERVER['argv'], 1) as $argument) { - if (in_array($argument, ['--help', '-h', '-H'], true)) { - print "Usage: php syslog_schema.php --audit|--repair|--upgrade\n --audit Report missing tables without changing the schema.\n --repair Repair missing tables and rerun schema migrations.\n --upgrade Upgrade the schema, including missing-table repair.\nRepair and upgrade preserve existing data and preferences.\n"; - exit(0); - } - - if ($argument === '--audit') { - $audit = true; - continue; - } +/* process calling arguments */ +$parms = $_SERVER['argv']; +array_shift($parms); - if (in_array($argument, ['--repair', '--upgrade'], true)) { - $action_requested = true; - continue; - } +$audit = false; +$repair = false; +$upgrade = false; - fwrite(STDERR, "ERROR: Unknown argument: $argument\n"); +if (!cacti_sizeof($parms)) { + fwrite(STDERR, 'ERROR: Specify --audit, --repair or --upgrade. Use --help for usage.' . PHP_EOL); exit(1); } -if ($audit && $action_requested) { - fwrite(STDERR, "ERROR: --audit cannot be combined with --repair or --upgrade.\n"); - exit(1); +foreach ($parms as $arg) { + switch ($arg) { + case '--audit': + $audit = true; + break; + case '--repair': + $repair = true; + break; + case '--upgrade': + $upgrade = true; + break; + case '--help': + case '-H': + case '-h': + syslog_schema_display_help(); + exit(0); + default: + fwrite(STDERR, "ERROR: Unknown argument: $arg" . PHP_EOL); + exit(1); + } } -if (!$action_requested && !$audit) { - fwrite(STDERR, "ERROR: Specify --audit, --repair or --upgrade. Use --help for usage.\n"); +if ($audit && ($repair || $upgrade)) { + fwrite(STDERR, 'ERROR: --audit cannot be combined with --repair or --upgrade.' . PHP_EOL); exit(1); } syslog_determine_config(); if (!defined('SYSLOG_CONFIG')) { - fwrite(STDERR, "ERROR: Syslog database configuration is missing.\n"); + fwrite(STDERR, 'ERROR: Syslog database configuration is missing.' . PHP_EOL); exit(1); } $database_last_error = ''; if (!syslog_connect(true)) { - fwrite(STDERR, "ERROR: Unable to connect to the Syslog database.\n"); + fwrite(STDERR, 'ERROR: Unable to connect to the Syslog database.' . PHP_EOL); exit(1); } -if (!$audit) { - print "Upgrading Syslog schema and repairing missing tables...\n"; +if ($repair || $upgrade) { + $exit_code = syslog_schema_repair_database() ? 0 : 1; +} else { + $exit_code = syslog_schema_report_audit_results() ? 0 : 1; +} + +exit($exit_code); + +/** + * Repair missing tables and rerun the existing Syslog schema migrations. + * + * @return bool Whether the migrations and final table audit succeeded. + */ +function syslog_schema_repair_database(): bool { + print 'Upgrading Syslog schema and repairing missing tables...' . PHP_EOL; syslog_setup_table_new([ 'upgrade_type' => 'upgrade', 'engine' => read_config_option('syslog_install_engine') ?: 'InnoDB', @@ -84,37 +104,70 @@ 'days' => read_config_option('syslog_install_days') ], true); syslog_check_upgrade(true); -} -$tables = [ - 'syslog', 'syslog_alert', 'syslog_alert_suppression', 'syslog_incoming', - 'syslog_remove', 'syslog_reports', 'syslog_saved_searches', 'syslog_status', - 'syslog_programs', 'syslog_hosts', 'syslog_facilities', 'syslog_priorities', - 'syslog_host_facilities', 'syslog_removed', 'syslog_logs', - 'syslog_dashboards', 'syslog_dashboard_panels', 'syslog_dashboards_perm', - 'syslog_saved_searches_perm', 'syslog_device_rule', 'syslog_replication_output', - 'syslog_replication_recovery' -]; -if ((int) $config['poller_id'] <= 1) { - $tables[] = 'syslog_replication_receipts'; - $tables[] = 'syslog_replication_collectors'; + $success = syslog_schema_report_audit_results(false); + if ($success) { + print 'Syslog schema upgrade completed; all required tables exist.' . PHP_EOL; + } + + return $success; } -$missing = []; -foreach ($tables as $table) { - if (!syslog_db_table_exists($table, false)) { - $missing[] = $table; +/** + * Report missing Syslog tables for the configured Data Collector without repairing them. + * + * @param bool $audit Print the audit success message for read-only runs. + * + * @return bool Whether all required tables exist and the audit queries succeeded. + */ +function syslog_schema_report_audit_results(bool $audit = true): bool { + global $config, $database_last_error; + + $tables = [ + 'syslog', 'syslog_alert', 'syslog_alert_suppression', 'syslog_incoming', + 'syslog_remove', 'syslog_reports', 'syslog_saved_searches', 'syslog_status', + 'syslog_programs', 'syslog_hosts', 'syslog_facilities', 'syslog_priorities', + 'syslog_host_facilities', 'syslog_removed', 'syslog_logs', + 'syslog_dashboards', 'syslog_dashboard_panels', 'syslog_dashboards_perm', + 'syslog_saved_searches_perm', 'syslog_device_rule', 'syslog_replication_output', + 'syslog_replication_recovery' + ]; + if ((int) $config['poller_id'] <= 1) { + $tables[] = 'syslog_replication_receipts'; + $tables[] = 'syslog_replication_collectors'; } -} -if (!empty($database_last_error)) { - fwrite(STDERR, "ERROR: Schema check encountered a database error; check the Cacti log.\n"); - exit(1); -} -if ($missing) { - fwrite(STDERR, 'ERROR: Missing tables: ' . implode(', ', $missing) . "\n"); - exit(1); + + $missing = []; + foreach ($tables as $table) { + if (!syslog_db_table_exists($table, false)) { + $missing[] = $table; + } + } + + if (!empty($database_last_error)) { + fwrite(STDERR, 'ERROR: Schema check encountered a database error; check the Cacti log.' . PHP_EOL); + return false; + } + if ($missing) { + fwrite(STDERR, 'ERROR: Missing tables: ' . implode(', ', $missing) . PHP_EOL); + return false; + } + + if ($audit) { + print 'Syslog schema audit passed; all required tables exist.' . PHP_EOL; + } + return true; } -print $audit - ? "Syslog schema audit passed; all required tables exist.\n" - : "Syslog schema upgrade completed; all required tables exist.\n"; +/** + * Display the Syslog schema utility usage and options. + * + * @return void + */ +function syslog_schema_display_help(): void { + print 'Usage: php syslog_schema.php --audit|--repair|--upgrade' . PHP_EOL; + print ' --audit Report missing tables without changing the schema.' . PHP_EOL; + print ' --repair Repair missing tables and rerun schema migrations.' . PHP_EOL; + print ' --upgrade Upgrade the schema, including missing-table repair.' . PHP_EOL; + print 'Repair and upgrade preserve existing data and preferences.' . PHP_EOL; +} diff --git a/tests/regression/cli_schema_upgrade_test.php b/tests/regression/cli_schema_upgrade_test.php index 57a8fe73..9a526914 100644 --- a/tests/regression/cli_schema_upgrade_test.php +++ b/tests/regression/cli_schema_upgrade_test.php @@ -62,6 +62,7 @@ function syslog_config_safe() { throw new RuntimeException('CLI bootstrap ran th file_put_contents($fixture . '/include/cli_check.php', ' getenv("SYSLOG_TEST_REMOTE") ? 2 : 1];'); file_put_contents($fixture . '/plugins/syslog/setup.php', <<<'STUB' Date: Sat, 10 Oct 2026 08:19:37 -0400 Subject: [PATCH 6/6] ci: validate schema CLI separately from Clover coverage --- .github/workflows/plugin-ci-workflow.yml | 4 ++++ tests/bin/patch-coverage.php | 3 +++ 2 files changed, 7 insertions(+) diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index 04a32f9c..965f9e53 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -206,6 +206,10 @@ jobs: include/vendor/bin/pest --configuration=plugins/syslog/phpunit.xml \ --coverage-clover=plugins/syslog/coverage/clover.xml + - name: Run Syslog schema CLI regression checks + run: php tests/regression/cli_schema_upgrade_test.php + working-directory: ${{ github.workspace }}/cacti/plugins/syslog + # Whole-file coverage is meaningless here: most of the plugin only runs # inside a live Cacti. What is enforceable is that a change covers the # lines it adds. diff --git a/tests/bin/patch-coverage.php b/tests/bin/patch-coverage.php index bd2376a3..d3f80b62 100644 --- a/tests/bin/patch-coverage.php +++ b/tests/bin/patch-coverage.php @@ -176,6 +176,9 @@ function changed_lines($base_ref) { 'syslog_counter.php', 'syslog_process.php', 'syslog_recovery.php', + // CLI bootstrap and top-level execution require Cacti; behavior is checked + // by tests/regression/cli_schema_upgrade_test.php in an isolated CLI fixture. + 'cli/syslog_schema.php', // Setup modules are loaded by Cacti hook paths rather than directly by the // isolated Pest bootstrap. Their behavior is covered by targeted tests, but // they are intentionally absent from Clover's source list.