diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index fd9396bf..c044f907 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -17,7 +17,7 @@ When generating code for this repository: ### Key Dependencies - Cacti core framework (`api_plugin_*`, `db_*`, `read_config_option()`, `get_filter_request_var()`, etc.). -- This plugin's own `syslog_db_*` wrapper layer (`database.php`) instead of core `db_*` for syslog tables. +- This plugin's own `syslog_db_*` wrapper layer (`includes/database.php`) instead of core `db_*` for syslog tables. - Optional: `gettext` for internationalization. ## Project Context @@ -29,9 +29,10 @@ This is the **Syslog Plugin** for Cacti, a PHP-based network monitoring and grap ## Project Structure ``` plugin_syslog/ -├── setup.php # Install/uninstall/upgrade hooks, schema, realms, menu entries -├── database.php # syslog_db_* wrapper layer (dual-database support) -├── functions.php # Core logic: search DSL, CSV export, partitions, alerts +├── setup.php # Compatibility facade, lifecycle and upgrade orchestration +├── includes/ # Owned setup modules: schema, processing, settings, UI hooks +├── includes/database.php # syslog_db_* wrapper layer (dual-database support) +├── includes/functions.php # Core logic: search DSL, CSV export, partitions, alerts ├── syslog.php # Main UI entry point (System Logs / Alert Logs tabs) ├── syslog_alerts.php # Alert rule administration ├── syslog_removal.php # Removal rule administration @@ -49,8 +50,9 @@ plugin_syslog/ ## Architecture & Data Flow - **Dual Database Support:** The plugin can store data in the main Cacti database OR a dedicated syslog database. - - **Critical:** ALWAYS use the `syslog_db_*` wrapper functions (defined in `database.php`) for all database operations. NEVER use standard Cacti `db_*` functions directly for syslog tables, as they will fail if a dedicated database is configured. -- **Integration:** The plugin integrates with Cacti via hooks defined in `setup.php`. + - **Critical:** ALWAYS use the `syslog_db_*` wrapper functions (defined in `includes/database.php`) for all database operations. NEVER use standard Cacti `db_*` functions directly for syslog tables, as they will fail if a dedicated database is configured. +- **Integration:** `setup.php` is the compatibility bootstrap; Cacti hooks are + registered there but point at the module that owns each callback. - **Poller Integration:** Background processes (`syslog_process.php`, `syslog_removal.php`) are triggered by Cacti's poller or run independently. - **Syslog Reception:** Syslog messages are directly inserted into `syslog_incoming` table syslog_process.php then processes them. @@ -63,8 +65,9 @@ plugin_syslog/ - **Schema:** Tables are defined/updated in `setup.php` (`syslog_setup_table_new`). ### Cacti Integration Patterns -- **Hooks:** Register hooks in `plugin_syslog_install()` in `setup.php`. - - Example: `api_plugin_register_hook('syslog', 'top_header_tabs', 'syslog_show_tab', 'setup.php');` +- **Hooks:** Register hooks in `plugin_syslog_install()` in `setup.php`, but + point each registration to the owning module (for example, + `includes/navigation.php` for `syslog_show_tab`). - **Permissions:** Register realms in `setup.php`. - Example: `api_plugin_register_realm('syslog', 'syslog.php', 'Syslog User', 1);` - **UI:** Follow Cacti's UI patterns (top tabs, breadcrumbs, filter bars). @@ -172,6 +175,26 @@ integer/numeric context (e.g. arithmetic, strict `===` comparisons). - **Type Safety:** Add type hints to function arguments and return types where possible, ensuring backward compatibility with supported PHP versions. - **Cleanup:** Remove unused variables and commented-out code blocks found in the modified sections. +## `setup.php` Module Boundaries + +- Keep `setup.php` as the compatibility facade: lifecycle entry points, + `plugin_syslog_version()`, `syslog_check_upgrade()` and its realm/permission + orchestration, `syslog_connect()`, and config-file discovery only. +- Load modules lazily with `require_once` at the lifecycle or callback boundary + that needs them. Do not eagerly load every `includes/*.php` module from + `setup.php`; Cacti hook registration already identifies and loads the owning + callback module. +- Put schema creation and migrations in `includes/schema.php`; retain + `includes/database.php` as the `syslog_db_*` wrapper layer. +- Put poller and replication callbacks in `includes/processing.php`; settings + hooks in `includes/settings.php`; tabs/navigation/graph buttons in + `includes/navigation.php`; install adviser UI in `includes/installer.php`; + and utilities callbacks in `includes/utilities.php`. +- Preserve public function names, signatures, SQL, and include order during a + move. Never combine a function move with a hook-path change or logic change. +- Source-inspection tests must use the central function-owner map in + `tests/bootstrap-unit.php`; update that map in the same commit as a move. + ## DBA & Query Optimization - **Query Analysis:** Always review SQL queries for performance. Suggest indexes if filtering by non-indexed columns. - **Prepared Statements:** Prefer `syslog_db_execute_prepared` over string concatenation for security and performance. @@ -231,9 +254,9 @@ Document all changes in `CHANGELOG.md` under `--- develop ---`, prefixed by type ## Key Files - `setup.php`: Plugin installation, hook registration, and schema updates. -- `database.php`: Database abstraction layer wrappers (`syslog_db_*`). +- `includes/database.php`: Database abstraction layer wrappers (`syslog_db_*`). - `config.php.dist`: Template for database configuration. -- `functions.php`: Core logic and utility functions. +- `includes/functions.php`: Core logic and utility functions. - `syslog.php`: Main UI entry point. ## CI & Dependency Baselines diff --git a/CHANGELOG.md b/CHANGELOG.md index df657411..e889fd7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ --- develop --- +* feature: Add Remote Data Collector processing, rules-sync, and record-storage options to the setup advisor; reject Aria selections whenever any collector option is enabled +* change: Use TEXT for message columns previously limited to VARCHAR(2048), including existing installations * ci: Drop Ruby from the CodeQL language matrix; the plugin ships no Ruby source, so the Ruby database build failed with "no source code seen" * dev: Move the shared function and database libraries to includes/ and point the entry-point includes, coverage source, and tests at the new path * feature: Restyle the System Logs and Alert Logs severity legends as rounded, evenly spaced, solid-colour chips that line up with the Thold status legends, with theme-appropriate backgrounds loaded from a per-theme css/.css file (falling back to a theme-neutral css/legend.css) diff --git a/README.md b/README.md index 73f874d6..265e7f3f 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,8 @@ tables, but the plugin does not migrate the table automatically. Only the InnoDB storage engine and, on MariaDB, the Aria storage engine are supported. Other engines, including MyISAM, are no longer offered and fall -back to InnoDB when legacy settings request them. +back to InnoDB when legacy settings request them. Enabling any Remote Data +Collector option requires InnoDB; the setup advisor selects it automatically. ## Important Version 4.0 Release Notes diff --git a/docs/setup-modules.md b/docs/setup-modules.md new file mode 100644 index 00000000..a8b9ea71 --- /dev/null +++ b/docs/setup-modules.md @@ -0,0 +1,56 @@ +# `setup.php` module ownership + +`setup.php` is the Syslog plugin's compatibility entry point. Page and worker +entry points may continue to include it directly; lifecycle code loads only +the module it needs, while Cacti loads hook callback modules by their registered +path. + +This is a relocation refactor. A module move must not rename a function, +change SQL, or change hook ownership in the same commit. + +## Stable facade + +`setup.php` owns the plugin lifecycle and upgrade orchestration: + +- `plugin_syslog_install()` and `plugin_syslog_uninstall()` +- `plugin_syslog_check_config()` and `plugin_syslog_upgrade()` +- `plugin_syslog_version()` and `syslog_check_upgrade()` +- `syslog_connect()` as the compatibility facade for direct `setup.php` + consumers +- `syslog_determine_config()` and `syslog_config_safe()` +- realm/permission upgrade helpers called by `syslog_check_upgrade()` + +## Module map + +| Module | Owns | +| --- | --- | +| `includes/schema.php` | Schema creation, migration helpers, capacity checks, and table setup | +| `includes/processing.php` | Poller and replication callbacks | +| `includes/settings.php` | Settings definitions, permission configuration, and Settings-page assets | +| `includes/navigation.php` | Tabs, navigation text, refresh handling, and graph buttons | +| `includes/installer.php` | Install/uninstall advisor and confirmation rendering | +| `includes/utilities.php` | Utilities menu/action callbacks and the purge dialog | + +`includes/database.php` remains the dual-database `syslog_db_*` wrapper layer; +it is not replaced by `includes/schema.php`. + +## Hook ownership + +After extraction, registrations must reference the owning module: + +| Hook callback group | Owning file | +| --- | --- | +| `syslog_config_settings`, `syslog_settings_bottom`, `syslog_config_arrays`, `syslog_config_insert` | `includes/settings.php` | +| `syslog_show_tab`, `syslog_draw_navigation_text`, `syslog_graph_buttons` | `includes/navigation.php` | +| `syslog_poller_bottom`, `syslog_replicate_out` | `includes/processing.php` | +| `syslog_utilities_list`, `syslog_utilities_action` | `includes/utilities.php` | + +Each hook-path change follows the move in a separate commit. The installer +entry points remain registered through `setup.php`. + +## Test rule + +Source-inspection tests must obtain a function's source through +`plugin_test_read_function_source()` instead of hard-coding `setup.php`. +Whenever ownership changes, update that central map and the test in the same +commit. diff --git a/includes/functions.php b/includes/functions.php index 0cb4d129..f5eda384 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -1189,6 +1189,58 @@ function syslog_validate_storage_engine($engine) { return 'InnoDB'; } +/** + * Whether any Remote Data Collector option requires InnoDB storage. + * + * @param array $options The install options, when supplied. + * + * @return bool + */ +function syslog_remote_collector_requires_innodb(array $options = []): bool { + foreach (['syslog_remote_enabled', 'syslog_remote_sync_rules', 'syslog_remote_store_records'] as $option) { + $value = array_key_exists($option, $options) ? $options[$option] : read_config_option($option); + + if ($value === 'on') { + return true; + } + } + + return false; +} + +/** + * Resolve the install storage engine, enforcing InnoDB for collector options. + * + * @param mixed $engine The requested storage engine name. + * @param array $options The install options, when supplied. + * + * @return string + */ +function syslog_install_storage_engine($engine, array $options = []): string { + $engine = syslog_validate_storage_engine($engine); + + if (syslog_remote_collector_requires_innodb($options) && $engine !== 'InnoDB') { + cacti_log('SYSLOG WARNING: Remote Data Collector options require InnoDB storage. Falling back to InnoDB', false, 'SYSLOG'); + + return 'InnoDB'; + } + + return $engine; +} + +/** + * Whether the requested storage engine is compatible with install options. + * + * @param mixed $engine The requested storage engine name. + * @param array $options The install options. + * + * @return bool + */ +function syslog_install_storage_engine_is_compatible($engine, array $options = []): bool { + return !syslog_remote_collector_requires_innodb($options) + || syslog_validate_storage_engine($engine) === 'InnoDB'; +} + /** * syslog_notice_traditional_tables - Raise the deprecation notice for * traditional (non-partitioned) Syslog tables. @@ -2704,24 +2756,6 @@ function syslog_partition_check($table, $time = null) { } } -/** - * Report whether a request variable differs from its remembered session value. - * - * @param string $request The request variable name to check. - * @param string $session The session variable name to compare against. - * - * @return int|null 1 when the value changed, or null when unchanged or unset. - */ -function syslog_check_changed(string $request, string $session): ?int { - if ((isset_request_var($request)) && (isset($_SESSION[$session]))) { - if (get_request_var($request) != $_SESSION[$session]) { - return 1; - } - } - - return null; -} - /** * syslog_get_removal_rule_fields - Trusted field map used to compile * structured filter rules for removal processing and editor validation. diff --git a/includes/installer.php b/includes/installer.php new file mode 100644 index 00000000..b510dc05 --- /dev/null +++ b/includes/installer.php @@ -0,0 +1,318 @@ + $submitted_options The rejected install options to redisplay. + * + * @return void + */ +function syslog_install_advisor(int $syslog_exists, array $submitted_options = []): void { + global $config, $syslog_retentions; + + top_header(); + + syslog_config_arrays(); + + $fields_syslog_update = [ + 'upgrade_type' => [ + 'method' => 'drop_array', + 'friendly_name' => __('What upgrade/install type do you wish to use', 'syslog'), + 'description' => __('When you have very large tables, performing a Truncate will be much quicker. If you are concerned about archive data, you can choose either Inline, which will freeze your browser for the period of this upgrade, or background, which will create a background process to bring your old syslog data from a backup table to the new syslog format. Again this process can take several hours.', 'syslog'), + 'value' => 'truncate', + 'array' => [ + 'truncate' => __('Truncate Syslog Table', 'syslog'), + 'inline' => __('Inline Upgrade', 'syslog'), + 'background' => __('Background Upgrade', 'syslog') + ] + ], + 'engine' => [ + 'method' => 'drop_array', + 'friendly_name' => __('Database Storage Engine', 'syslog'), + 'description' => __('The storage engine for the analytical tables. Only InnoDB and, on MariaDB, Aria storage engines are supported. Any Remote Data Collector option requires InnoDB; incompatible combinations cannot be submitted.', 'syslog'), + 'value' => isset($submitted_options['engine']) ? $submitted_options['engine'] : 'innodb', + 'array' => [ + 'innodb' => __('InnoDB Storage', 'syslog'), + 'aria' => __('Aria Storage', 'syslog') + ] + ], + 'syslog_remote_header' => [ + 'method' => 'spacer', + 'friendly_name' => __('Remote Message Processing', 'syslog') + ], + 'syslog_remote_enabled' => [ + 'method' => 'checkbox', + 'friendly_name' => __('Enable Remote Data Collector Message Processing', 'syslog'), + 'description' => __('If your Remote Data Collectors have their own Syslog databases and process their messages independently, check this checkbox. By checking this Checkbox, your Remote Data Collectors will need to maintain their own \'config_local.php\' file in order to inform Syslog to use an independent database for message display and processing. Please use the template file \'config_local.php.dist\' for this purpose. WARNING: Syslog tables will be automatically created as soon as this option is enabled.', 'syslog'), + 'value' => $submitted_options ? (isset($submitted_options['syslog_remote_enabled']) ? 'on' : '') : read_config_option('syslog_remote_enabled') + ], + 'syslog_remote_sync_rules' => [ + 'method' => 'checkbox', + 'friendly_name' => __('Remote Data Collector Rules Sync', 'syslog'), + 'description' => __('If your Remote Data Collectors have their own Syslog databases and process thrie messages independently, check this checkbox if you wish the Main Cacti databases Alerts, Removal and Report rules to be sent to the Remote Cacti System.', 'syslog'), + 'value' => $submitted_options ? (isset($submitted_options['syslog_remote_sync_rules']) ? 'on' : '') : read_config_option('syslog_remote_sync_rules') + ], + 'syslog_remote_store_records' => [ + 'method' => 'checkbox', + 'friendly_name' => __('Store Records on Remote Collector', 'syslog'), + 'description' => __('Keep processed messages in the remote collector syslog table after they have been delivered to the Main Collector. When disabled, records are retained locally only while the Main Collector is unavailable and are removed after confirmed recovery delivery.', 'syslog'), + 'value' => $submitted_options ? (isset($submitted_options['syslog_remote_store_records']) ? 'on' : '') : read_config_option('syslog_remote_store_records') + ], + 'db_type' => [ + 'method' => 'hidden', + 'value' => 'part' + ], + 'days' => [ + 'method' => 'drop_array', + 'friendly_name' => __('Retention Policy', 'syslog'), + 'description' => __('Choose how many days of Syslog values you wish to maintain in the database.', 'syslog'), + 'value' => '30', + 'array' => $syslog_retentions + ], + 'mode' => [ + 'method' => 'hidden', + 'value' => 'install' + ], + 'install' => [ + 'method' => 'hidden', + 'value' => 'true' + ], + 'id' => [ + 'method' => 'hidden', + 'value' => 'syslog' + ] + ]; + + $fields_syslog_update['dayparts'] = [ + 'method' => 'drop_array', + 'friendly_name' => __('Partitions per Day', 'syslog'), + 'description' => __('Select the number of partitions per day that you wish to create.', 'syslog'), + 'value' => '1', + 'array' => [ + '1' => __('%d Per Day', 1, 'syslog'), + '2' => __('%d Per Day', 2, 'syslog'), + '4' => __('%d Per Day', 4, 'syslog'), + '6' => __('%d Per Day', 6, 'syslog'), + '12' => __('%d Per Day', 12, 'syslog') + ] + ]; + + if ($syslog_exists) { + $type = __('Upgrade', 'syslog'); + } else { + $type = __('Install', 'syslog'); + } + + syslog_connect(); + $database = syslog_db_fetch_row('SHOW GLOBAL VARIABLES LIKE "version"'); + + // remove Aria as a storage engine if this is mysql + if (!is_array($database) || !isset($database['Value']) || stripos($database['Value'], 'mariadb') === false) { + unset($fields_syslog_update['engine']['array']['aria']); + } elseif (!isset($submitted_options['engine'])) { + $fields_syslog_update['engine']['value'] = 'aria'; + } + + if (!$submitted_options && syslog_remote_collector_requires_innodb()) { + $fields_syslog_update['engine']['value'] = 'innodb'; + } + + print "'; + } else { + unset($fields_syslog_update['upgrade_type']); + print '

' . __('You have several options to choose from when installing Syslog. The first is the Database Architecture. Partitioned tables are the only supported architecture as they prevent the size of the tables from becoming excessive thus slowing queries. Traditional non-partitioned tables are deprecated and are no longer offered for new installs.', 'syslog') . '

' . __('You can also set the database storage engine for the analytical tables syslog and syslog_remove. Only the InnoDB storage engine and, on MariaDB, the Aria storage engine are supported. If using MariaDB, we recommend the Aria Storage Engine when no Remote Data Collector options are enabled; collector options require InnoDB.', 'syslog') . '

'; + print '

' . __('Aria storage is unavailable while any Remote Data Collector option is enabled. InnoDB has been selected.', 'syslog') . '

'; + print '

' . __('You can also select the retention duration. Please keep in mind that if you have several hosts logging to syslog, this table can become quite large.', 'syslog') . '

'; + } + html_end_box(); + + print ""; + + html_start_box(__('Syslog %s Settings', $type, 'syslog'), '100%', '', '3', 'center', ''); + + draw_edit_form([ + 'config' => ['no_form_tag' => true], + 'fields' => inject_form_variables($fields_syslog_update, [])] + ); + + ?> + +
"; + html_start_box(__('Syslog %s Advisor', $type, 'syslog'), '100%', '', '3', 'center', ''); + print '
'; + + if ($syslog_exists) { + print "

" . __('WARNING: Syslog Upgrade is Time Consuming!!!', 'syslog') . '

'; + print '

' . __('The upgrade of the \'main\' syslog table can be a very time consuming process. As such, it is recommended that you either reduce the size of your syslog table prior to upgrading, or choose the background option

If you choose the background option, your legacy syslog table will be renamed, and a new syslog table will be created. Then, an upgrade process will be launched in the background. Again, this background process can quite a bit of time to complete. However, your data will be preserved

Regardless of your choice, all existing removal and alert rules will be maintained during the upgrade process.

Press \'Upgrade\' to proceed with the upgrade, or \'Cancel\' to return to the Plugins menu.', 'syslog') . '

'; + + bottom_footer(); + exit; +} + +/** + * Display the uninstall advisor form. + * + * @return void + */ +function syslog_uninstall_advisor(): void { + global $config, $syslogdb_default; + + syslog_connect(); + + $syslog_exists = sizeof(syslog_db_fetch_row("SHOW TABLES FROM `$syslogdb_default` LIKE 'syslog'")); + + top_header(); + + $fields_syslog_update = [ + 'uninstall_method' => [ + 'method' => 'drop_array', + 'friendly_name' => __('What uninstall method do you want to use?', 'syslog'), + 'description' => __('When uninstalling syslog, you can remove everything, or only components, just in case you plan on re-installing in the future.', 'syslog'), + 'value' => 'all', + 'array' => ['all' => __('Remove Everything (Logs, Tables, Settings)', 'syslog'), 'syslog' => __('Syslog Data Only', 'syslog')], + ], + 'mode' => [ + 'method' => 'hidden', + 'value' => 'uninstall' + ], + 'uninstall' => [ + 'method' => 'hidden', + 'value' => 'true' + ], + 'id' => [ + 'method' => 'hidden', + 'value' => 'syslog' + ] + ]; + + form_start('plugins.php'); + + print "
"; + + html_start_box(__('Syslog Uninstall Preferences', 'syslog'), '100%', '', '3', 'center', ''); + + draw_edit_form([ + 'config' => [], + 'fields' => inject_form_variables($fields_syslog_update, [])] + ); + + html_end_box(); + + syslog_confirm_button('uninstall', 'plugins.php', $syslog_exists); + + print '
'; + + bottom_footer(); + exit; +} + +/** + * Display the confirm and cancel buttons for the install/uninstall forms. + * + * @param string $action The action being confirmed, either 'install' or 'uninstall'. + * @param string $cancel_url The URL to load when the action is cancelled. + * @param int $syslog_exists Whether the Syslog table already exists. + * + * @return void + */ +function syslog_confirm_button(string $action, string $cancel_url, int $syslog_exists): void { + if ($action == 'install') { + if ($syslog_exists) { + $value = __('Upgrade', 'syslog'); + } else { + $value = __('Install', 'syslog'); + } + } else { + $value = __('Uninstall', 'syslog'); + } + + ?> + + + + +
+ ' type='button' value=''> + + +
+ + ' . __('Syslog', 'syslog') . ''; + } else { + print '' . __('Syslog', 'syslog') . ''; + } + } +} + + +/** + * Add the Syslog pages to the Cacti navigation structure. + * + * @param array> $nav The navigation array provided by Cacti. + * + * @return array> The navigation array with the Syslog pages added. + */ +function syslog_draw_navigation_text($nav) { + global $config; + + $nav['syslog.php:'] = ['title' => __('Syslog', 'syslog'), 'mapping' => '', 'url' => $config['url_path'] . 'plugins/syslog/syslog.php', 'level' => '1']; + $nav['syslog_removal.php:'] = ['title' => __('Syslog Removals', 'syslog'), 'mapping' => 'index.php:', 'url' => $config['url_path'] . 'plugins/syslog/syslog_removal.php', 'level' => '1']; + $nav['syslog_removal.php:edit'] = ['title' => __('(Edit)', 'syslog'), 'mapping' => 'index.php:,syslog_removal.php:', 'url' => 'syslog_removal.php', 'level' => '2']; + $nav['syslog_removal.php:newedit'] = ['title' => __('(Edit)', 'syslog'), 'mapping' => 'index.php:,syslog_removal.php:', 'url' => 'syslog_removal.php', 'level' => '2']; + $nav['syslog_removal.php:actions'] = ['title' => __('(Actions)', 'syslog'), 'mapping' => 'index.php:,syslog_removal.php:', 'url' => 'syslog_removal.php', 'level' => '2']; + + $nav['syslog_alerts.php:'] = ['title' => __('Syslog Alerts', 'syslog'), 'mapping' => 'index.php:', 'url' => $config['url_path'] . 'plugins/syslog/syslog_alerts.php', 'level' => '1']; + $nav['syslog_alerts.php:edit'] = ['title' => __('(Edit)', 'syslog'), 'mapping' => 'index.php:,syslog_alerts.php:', 'url' => 'syslog_alerts.php', 'level' => '2']; + $nav['syslog_alerts.php:newedit'] = ['title' => __('(Edit)', 'syslog'), 'mapping' => 'index.php:,syslog_alerts.php:', 'url' => 'syslog_alerts.php', 'level' => '2']; + $nav['syslog_alerts.php:actions'] = ['title' => __('(Actions)', 'syslog'), 'mapping' => 'index.php:,syslog_alerts.php:', 'url' => 'syslog_alerts.php', 'level' => '2']; + + $nav['syslog_reports.php:'] = ['title' => __('Syslog Reports', 'syslog'), 'mapping' => 'index.php:', 'url' => $config['url_path'] . 'plugins/syslog/syslog_reports.php', 'level' => '1']; + $nav['syslog_reports.php:edit'] = ['title' => __('(Edit)', 'syslog'), 'mapping' => 'index.php:,syslog_reports.php:', 'url' => 'syslog_reports.php', 'level' => '2']; + $nav['syslog_reports.php:actions'] = ['title' => __('(Actions)', 'syslog'), 'mapping' => 'index.php:,syslog_reports.php:', 'url' => 'syslog_reports.php', 'level' => '2']; + $nav['syslog_saved_searches.php:'] = ['title' => __('Saved Search Templates', 'syslog'), 'mapping' => 'index.php:', 'url' => $config['url_path'] . 'plugins/syslog/syslog_saved_searches.php', 'level' => '1']; + $nav['syslog_dashboards.php:'] = ['title' => __('Dashboards', 'syslog'), 'mapping' => 'index.php:', 'url' => $config['url_path'] . 'plugins/syslog/syslog_dashboards.php', 'level' => '1']; + $nav['syslog.php:actions'] = ['title' => __('Syslog', 'syslog'), 'mapping' => '', 'url' => $config['url_path'] . 'plugins/syslog/syslog.php', 'level' => '1']; + + return $nav; +} + + +/** + * Display a link to the Syslog page filtered for the graphed device. + * + * @param array> $graph_elements The graph elements provided by Cacti. + * + * @return void + */ +function syslog_graph_buttons($graph_elements = []): void { + global $config, $timespan, $graph_timeshifts; + + if (!syslog_config_safe()) { + return; + } + + syslog_connect(); + + if (get_nfilter_request_var('action') == 'view') { + return; + } + + if (get_current_page() == 'graph_view.php') { + if (isset_request_var('graph_end') && strlen(get_filter_request_var('graph_end'))) { + $date1 = date('Y-m-d H:i:s', get_filter_request_var('graph_start')); + $date2 = date('Y-m-d H:i:s', get_filter_request_var('graph_end')); + } else { + $date1 = $timespan['current_value_date1']; + $date2 = $timespan['current_value_date2']; + } + } else { + return; + } + + if (isset($graph_elements[1]['local_graph_id'])) { + $host_id = db_fetch_cell_prepared('SELECT host_id + FROM graph_local + WHERE id = ?', + [$graph_elements[1]['local_graph_id']]); + + $sql_where = ''; + $sql_params = []; + + if (!empty($host_id)) { + $host = db_fetch_row_prepared('SELECT id, description, hostname + FROM host WHERE id = ?', + [$host_id]); + + if (is_array($host) && cacti_sizeof($host)) { + if (!is_ipaddress($host['description'])) { + $parts = explode('.', $host['description']); + + $sql_where = 'WHERE host LIKE ? OR host = ?'; + + $sql_params[] = $parts[0] . '.%'; + $sql_params[] = $host['description']; + } else { + $sql_where = 'WHERE host = ?'; + + $sql_params[] = $host['description']; + } + + if (!is_ipaddress($host['hostname'])) { + $parts = explode('.', $host['hostname']); + + $sql_where .= ($sql_where != '' ? ' OR ' : 'WHERE ') . 'host LIKE ? OR host = ?'; + + $sql_params[] = $parts[0] . '.%'; + $sql_params[] = $host['hostname']; + } else { + $sql_where .= ($sql_where != '' ? ' OR ' : 'WHERE ') . 'host = ?'; + + $sql_params[] = $host['hostname']; + } + + if ($sql_where != '') { + $host_id = syslog_db_fetch_cell_prepared('SELECT host_id FROM syslog_hosts ' . $sql_where, $sql_params); + + if ($host_id) { + $url = $config['url_path'] . 'plugins/syslog/syslog.php?tab=syslog&reset=1&host=' . $host_id . '&date1=' . $date1 . '&date2=' . $date2; + + print "
"; + } + } + } + } + } +} diff --git a/includes/processing.php b/includes/processing.php new file mode 100644 index 00000000..d645c3d9 --- /dev/null +++ b/includes/processing.php @@ -0,0 +1,151 @@ + $data The replication data provided by Cacti, + * containing the remote poller id, the remote + * connection id and the replication class. + * + * @return array The replication data, unmodified. + */ +function syslog_replicate_out($data) { + syslog_connect(); + + if (read_config_option('syslog_remote_enabled') == 'on' && read_config_option('syslog_remote_sync_rules') == 'on') { + $remote_poller_id = $data['remote_poller_id']; + $rcnn_id = $data['rcnn_id']; + $class = $data['class']; + + cacti_log('INFO: Replicating for the Syslog Plugin', false, 'REPLICATE'); + + if ($class == 'all') { + $tdata = syslog_db_fetch_assoc('SELECT * FROM syslog_alert'); + replicate_out_table($rcnn_id, $tdata, 'syslog_alert', $remote_poller_id); + $tdata = syslog_db_fetch_assoc('SELECT * FROM syslog_device_rule'); + replicate_out_table($rcnn_id, $tdata, 'syslog_device_rule', $remote_poller_id); + $tdata = syslog_db_fetch_assoc('SELECT * FROM syslog_remove'); + replicate_out_table($rcnn_id, $tdata, 'syslog_remove', $remote_poller_id); + $tdata = syslog_db_fetch_assoc('SELECT * FROM syslog_reports'); + replicate_out_table($rcnn_id, $tdata, 'syslog_reports', $remote_poller_id); + } + } + + return $data; +} + +/** + * Pull the Syslog rules tables from the main Cacti database. + * + * @return void + */ +function syslog_replicate_in(): void { + syslog_connect(); + + if (read_config_option('syslog_remote_enabled') == 'on' && read_config_option('syslog_remote_sync_rules') == 'on') { + $data = db_fetch_assoc('SELECT * FROM syslog_alert'); + syslog_replace_data('syslog_alert', $data); + + $data = db_fetch_assoc('SELECT * FROM syslog_device_rule'); + syslog_replace_data('syslog_device_rule', $data); + + $data = db_fetch_assoc('SELECT * FROM syslog_remove'); + syslog_replace_data('syslog_remove', $data); + + $data = db_fetch_assoc('SELECT * FROM syslog_reports'); + syslog_replace_data('syslog_reports', $data); + } +} + +/** + * Poller bottom hook, launches the Syslog message processing process. + * + * @return void + */ +function syslog_poller_bottom(): void { + global $config; + + if (syslog_config_safe()) { + syslog_connect(); + syslog_status_set('last_polling_time', time()); + + $command_string = (string) read_config_option('path_php_binary'); + $extra_args = ' -q ' . $config['base_path'] . '/plugins/syslog/syslog_process.php'; + exec_background($command_string, $extra_args); + } else { + cacti_log('WARNING: You have installed the Syslog plugin, but you have not properly set a config.php or config_local.php', false, 'POLLER'); + } +} + +/** + * Replace the data in the local table with the replicated data. + * + * @param string $table The name of the table to replace. + * @param array> $data The replicated rows, passed by reference. + * + * @return void + */ +function syslog_replace_data($table, &$data) { + if (cacti_sizeof($data)) { + $sqlData = []; + $sqlQuery = []; + $columns = array_keys($data[0]); + $create_sql = ''; + + $create = db_fetch_row('SHOW CREATE TABLE ' . $table); + + if (is_array($create)) { + if (isset($create["CREATE TABLE `$table`"])) { + $create_sql = $create["CREATE TABLE `$table`"]; + } elseif (isset($create['Create Table'])) { + $create_sql = $create['Create Table']; + } + } + + if (!syslog_db_table_exists($table)) { + if ($create_sql == '') { + cacti_log('WARNING: Unable to derive CREATE TABLE SQL for `' . $table . '` during Syslog replication.', false, 'REPLICATE'); + + return; + } + + syslog_db_execute($create_sql); + syslog_db_execute("TRUNCATE TABLE $table"); + } + + // Make the prefix + $sql_prefix = "INSERT INTO $table (`" . implode('`,`', $columns) . '`) VALUES '; + + // Make the suffix + $sql_suffix = ' ON DUPLICATE KEY UPDATE '; + + foreach ($columns as $c) { + $sql_suffix .= " $c = VALUES($c),"; + } + $sql_suffix = trim($sql_suffix, ','); + + // Construct the prepared statement + foreach ($data as $row) { + $sqlQuery[] = '(' . trim(str_repeat('?, ', cacti_sizeof($columns)), ', ') . ')'; + + foreach ($row as $col) { + $sqlData[] = $col; + } + } + + $sql = implode(', ', $sqlQuery); + + syslog_db_execute_prepared($sql_prefix . $sql . $sql_suffix, $sqlData); + } +} diff --git a/includes/schema.php b/includes/schema.php new file mode 100644 index 00000000..9ab9c562 --- /dev/null +++ b/includes/schema.php @@ -0,0 +1,702 @@ + 'TEXT NOT NULL', + 'syslog_removed' => 'TEXT NOT NULL', + 'syslog_incoming' => 'TEXT NOT NULL', + 'syslog_replication_output' => 'TEXT NOT NULL', + 'syslog_reports' => 'TEXT DEFAULT NULL', + ]; + + foreach ($tables as $table => $definition) { + if (!syslog_db_table_exists($table, false)) { + continue; + } + + $type = syslog_db_fetch_cell_prepared( + 'SELECT COLUMN_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = \'message\'', + [$syslogdb_default, $table], + '', + false + ); + + if (strtolower((string) $type) !== 'text') { + syslog_db_execute("ALTER TABLE `$syslogdb_default`.`$table` MODIFY COLUMN `message` $definition"); + } + } +} + +/** + * Add an immutable remote-event identity to locally retained history. + * + * A remote collector uses these nullable columns only while the Main + * Collector is unavailable and "Store records on remote collector" is off. + * They let recovery remove exactly the temporary local copies after central + * receipt, without risking removal of otherwise identical log messages. + * + * @return void + */ +function syslog_ensure_replication_history_columns(): void { + global $syslogdb_default; + + if (!syslog_db_table_exists('syslog', false) + || syslog_db_column_exists('syslog', 'replication_source_event_id', false)) { + return; + } + + syslog_db_add_column('syslog', [ + 'name' => 'replication_source_poller_id', + 'type' => 'int(10) unsigned', + 'NULL' => true, + 'after' => 'seq' + ]); + syslog_db_add_column('syslog', [ + 'name' => 'replication_source_event_id', + 'type' => 'bigint unsigned', + 'NULL' => true, + 'after' => 'replication_source_poller_id' + ]); + syslog_db_execute("ALTER TABLE `$syslogdb_default`.`syslog` + ADD KEY `replication_source` (`replication_source_poller_id`, `replication_source_event_id`)"); +} + +/** Ensure remote replication transactions use transactional tables. */ +function syslog_ensure_replication_storage_engine(): void { + global $config, $syslogdb_default; + + if (!isset($config['poller_id']) || (int) $config['poller_id'] <= 1 || !syslog_remote_collector_requires_innodb()) { + return; + } + + foreach (['syslog_incoming', 'syslog', 'syslog_removed'] as $table) { + $engine = syslog_db_fetch_cell_prepared( + 'SELECT ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?', + [$syslogdb_default, $table], + '', + false + ); + + if (is_string($engine) && strcasecmp($engine, 'InnoDB') !== 0) { + syslog_db_execute("ALTER TABLE `$syslogdb_default`.`$table` ENGINE=InnoDB"); + } + } +} + +/** Create the device-wide alert handling rules table on all Syslog collectors. */ +function syslog_create_device_rule_table(): void { + global $syslogdb_default; + + syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_device_rule` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `host` varchar(64) NOT NULL, + `enabled` char(2) NOT NULL DEFAULT 'on', + `mute_mode` varchar(16) NOT NULL DEFAULT 'none', + `mute_until` int(10) unsigned NOT NULL DEFAULT '0', + `pass_through_priority` int(10) NOT NULL DEFAULT '-1', + `allow_maintenance` char(2) NOT NULL DEFAULT '', + `notes` varchar(255) NOT NULL DEFAULT '', + `user` varchar(32) NOT NULL DEFAULT '', + `date` int(10) unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `host` (`host`), + KEY `enabled` (`enabled`)) + ENGINE=InnoDB + ROW_FORMAT=Dynamic"); +} + +/** Repair share-grant tables on installs whose plugin version is already current. */ +function syslog_ensure_share_tables(): void { + global $syslogdb_default; + + if (!syslog_db_table_exists('syslog_dashboards_perm', false)) { + syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_dashboards_perm` ( + `dashboard_id` int(10) NOT NULL default '0', + `type` varchar(5) NOT NULL default '', + `item_id` int(10) NOT NULL default '0', + PRIMARY KEY (`dashboard_id`, `type`, `item_id`)) + ENGINE=InnoDB + ROW_FORMAT=Dynamic"); + } + + if (!syslog_db_table_exists('syslog_saved_searches_perm', false)) { + syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_saved_searches_perm` ( + `search_id` int(10) NOT NULL default '0', + `type` varchar(5) NOT NULL default '', + `item_id` int(10) NOT NULL default '0', + PRIMARY KEY (`search_id`, `type`, `item_id`)) + ENGINE=InnoDB + ROW_FORMAT=Dynamic"); + } +} + +/** + * Create the durable, Syslog-owned remote collector replication outbox. + * + * source_poller_id/source_event_id is the immutable distributed identity. + * The portable payload uses incoming-table values rather than local + * host/program surrogate IDs. Delivery is intentionally deferred. + * + * @return void + */ +function syslog_create_replication_output_table(): void { + global $syslogdb_default; + + syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_replication_output` ( + `source_poller_id` int(10) unsigned NOT NULL COMMENT 'Originating Cacti data collector ID', + `source_event_id` bigint unsigned NOT NULL COMMENT 'Immutable local syslog_incoming sequence', + `facility_id` int(10) unsigned default NULL COMMENT 'Portable syslog facility value', + `priority_id` int(10) unsigned default NULL COMMENT 'Portable syslog priority value', + `program` varchar(40) default NULL COMMENT 'Source program text', + `logtime` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT 'Original event timestamp', + `host` varchar(64) default NULL COMMENT 'Source host text', + `message` TEXT NOT NULL COMMENT 'Source message text', + `disposition` varchar(16) NOT NULL COMMENT 'Local archival target: syslog or syslog_removed', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Outbox creation time', + `acknowledged_at` timestamp NULL DEFAULT NULL COMMENT 'Reserved for future destination acknowledgement', + `attempts` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'Reserved for future delivery retries', + PRIMARY KEY (`source_poller_id`, `source_event_id`), + KEY `backlog` (`acknowledged_at`, `created_at`, `source_poller_id`, `source_event_id`)) + ENGINE=InnoDB + ROW_FORMAT=Dynamic"); +} + +/** + * Create the Main Collector receipt boundary for remote Syslog delivery. + * + * The partitioned history tables deliberately retain their existing keys: + * MySQL requires every unique key on a partitioned table to include the + * partitioning column. A small unpartitioned receipt table therefore owns + * distributed idempotency and is committed with the archival insert. + * + * @return void + */ +function syslog_create_replication_receipts_table(): void { + global $config, $syslogdb_default; + + if (isset($config['poller_id']) && (int) $config['poller_id'] > 1) { + return; + } + + syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_replication_receipts` ( + `source_poller_id` int(10) unsigned NOT NULL, + `source_event_id` bigint unsigned NOT NULL, + `disposition` varchar(16) NOT NULL, + `accepted_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`source_poller_id`, `source_event_id`), + KEY `accepted_at` (`accepted_at`)) + ENGINE=InnoDB + ROW_FORMAT=Dynamic"); +} + +/** Create main-collector telemetry for the latest batch from each remote poller. */ +function syslog_create_replication_collectors_table(): void { + global $config, $syslogdb_default; + + if (isset($config['poller_id']) && (int) $config['poller_id'] > 1) { + return; + } + + syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_replication_collectors` ( + `source_poller_id` int(10) unsigned NOT NULL, + `last_batch_id` char(32) NOT NULL, + `last_batch_count` int(10) unsigned NOT NULL DEFAULT '0', + `last_received` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`source_poller_id`), + KEY `last_received` (`last_received`)) + ENGINE=InnoDB + ROW_FORMAT=Dynamic"); +} + +/** Create the plugin-owned, expiring local recovery-worker lease table. */ +function syslog_create_replication_recovery_table(): void { + global $syslogdb_default; + + syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_replication_recovery` ( + `name` varchar(32) NOT NULL, + `owner_token` char(32) NOT NULL, + `acquired_at` int(10) unsigned NOT NULL, + `heartbeat_at` int(10) unsigned NOT NULL, + `pid` int(10) unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`name`), + KEY `heartbeat_at` (`heartbeat_at`)) + ENGINE=InnoDB + ROW_FORMAT=Dynamic"); +} + +/** + * Create the partitioned Syslog table, including past and future partitions. + * + * @param string $engine The storage engine to create the table with. + * @param float|int|string $days The number of days of retention to provision. + * @param int|string|false $ahead_days The number of days of partitions to create + * ahead of today, or false when unconfigured. + * + * @return void + */ +function syslog_create_partitioned_syslog_table($engine = 'InnoDB', $days = 30, $ahead_days = 3) { + global $config, $syslogdb_default, $syslog_levels; + + syslog_connect(); + + $engine = syslog_validate_storage_engine($engine); + + // Guard the partition loop against non-numeric or extreme values: a + // crafted 'days' option would otherwise loop indefinitely or emit a + // very large DDL statement. + $days = (int) $days; + + if ($days < 0 || $days > 365) { + $days = 30; + } + + if (!is_numeric($ahead_days) || (int) $ahead_days < 1 || (int) $ahead_days > 7) { + $ahead_days = 3; + } + + $ahead_days = (int) $ahead_days; + + if (stripos($engine, 'aria') !== false) { + $row_format = 'ROW_FORMAT=Page'; + } else { + $row_format = 'ROW_FORMAT=Dynamic'; + } + + $sql = "CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog` ( + facility_id int(10) unsigned default NULL, + priority_id int(10) unsigned default NULL, + program_id int(10) unsigned default NULL, + host_id int(10) unsigned default NULL, + logtime timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + message TEXT NOT NULL, + seq bigint unsigned NOT NULL auto_increment, + replication_source_poller_id int(10) unsigned default NULL, + replication_source_event_id bigint unsigned default NULL, + PRIMARY KEY(seq, logtime), + INDEX `seq` (`seq`), + INDEX `replication_source` (`replication_source_poller_id`, `replication_source_event_id`), + INDEX logtime (logtime), + INDEX program_id (program_id), + INDEX host_id (host_id), + INDEX priority_id (priority_id), + INDEX facility_id (facility_id)) + ENGINE=$engine + $row_format + PARTITION BY RANGE (UNIX_TIMESTAMP(logtime))\n"; + + $now = time(); + + $parts = ''; + + /* + * Partition boundaries are integer epochs computed in PHP and injected + * as numeric literals. This keeps both MySQL and PHP session time zones + * out of the equation: the boundary is always the next UTC midnight + * after the labeled day. + * + * $days counts today as one of the retained days, so the historical + * loop bound is $days - 1 (today plus $days - 1 prior days = $days + * total). This must stay in lockstep with the $days + $ahead_days + * "keep_partitions" math in syslog_partition_remove(), or newly + * installed tables over-provision by one partition and the very next + * retention prune immediately deletes the oldest one. + * + * $days = 0 is a valid "Indefinite" retention setting (never pruned by + * syslog_partition_remove(), which only prunes when $days > 0), but + * $days - 1 would then be -1 and skip creating today's concrete + * partition entirely. Clamp the starting bound to 0 so today is always + * created regardless of retention. + */ + for ($i = max($days - 1, 0); $i >= (0 - $ahead_days); $i--) { + $day_epoch = $now - ($i * 86400); + $boundary_epoch = (intdiv($day_epoch, 86400) + 1) * 86400; + $format = gmdate('Ymd', $day_epoch); + + $parts .= ($parts !== '' ? ",\n" : '(') . ' PARTITION d' . $format . ' VALUES LESS THAN (' . $boundary_epoch . ')'; + } + + $parts .= ",\nPARTITION dMaxValue VALUES LESS THAN MAXVALUE);"; + + syslog_db_execute($sql . $parts); +} + +/** + * Create the Syslog database tables, dropping existing ones on truncate. + * + * @param array $options The install options, either from the + * request, saved settings, or the Syslog + * config file. + * + * @return void + */ +function syslog_setup_table_new(array $options): void { + global $config, $settings, $syslogdb_default, $syslog_levels; + + syslog_connect(); + + $tables = []; + + $syslog_levels = [ + 0 => 'emerg', + 1 => 'crit', + 2 => 'alert', + 3 => 'err', + 4 => 'warn', + 5 => 'notice', + 6 => 'info', + 7 => 'debug', + 8 => 'other' + ]; + + // Set default if they are not set. + if (!cacti_sizeof($options)) { + $options['upgrade_type'] = read_config_option('syslog_install_upgrade_type'); + $options['engine'] = read_config_option('syslog_install_engine'); + $options['db_type'] = read_config_option('syslog_install_db_type'); + $options['days'] = read_config_option('syslog_install_days'); + + if (empty($options['upgrade_type'])) { + $options['upgrade_type'] = 'upgrade'; + } + + if (empty($options['engine'])) { + $options['engine'] = 'InnoDB'; + } + + if (empty($options['db_type'])) { + $options['db_type'] = 'part'; + } + + if (empty($options['days'])) { + $options['days'] = 30; + } + } + + // 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 + // options were supplied (request, saved settings, or legacy config.php). + if (!isset($options['db_type']) || $options['db_type'] != 'part') { + if (isset($options['db_type']) && $options['db_type'] == 'trad') { + cacti_log("SYSLOG WARNING: The 'trad' database architecture setting is deprecated. Partitioned tables are the only supported architecture; creating partitioned tables", false, 'SYSLOG'); + } + + $options['db_type'] = 'part'; + } + + // validate some simple information + $truncate = isset($options['upgrade_type']) && $options['upgrade_type'] == 'truncate' ? true : false; + $engine = syslog_install_storage_engine(isset($options['engine']) ? $options['engine'] : 'InnoDB', $options); + $syslogexists = sizeof(syslog_db_fetch_row("SHOW TABLES FROM `$syslogdb_default` LIKE 'syslog'")); + + // Aria is only supported on MariaDB. If a legacy setting requests Aria + // on MySQL, fall back to InnoDB so table creation cannot fail. + if (stripos($engine, 'aria') !== false) { + $database = syslog_db_fetch_row('SHOW GLOBAL VARIABLES LIKE "version"'); + $version = is_array($database) && isset($database['Value']) ? (string) $database['Value'] : ''; + + if (stripos($version, 'mariadb') === false) { + cacti_log('SYSLOG WARNING: Aria storage engine requested on a non-MariaDB database. Falling back to InnoDB', false, 'SYSLOG'); + + $engine = 'InnoDB'; + } + } + + // Partition retention days are required for partitioned table creation + // regardless of how the options were supplied. '0' is a valid value + // (Indefinite retention) and must not be coerced. + if (!isset($options['days']) || !is_numeric($options['days']) || (int) $options['days'] < 0) { + $options['days'] = 30; + } + + // set table construction settings for the remote pollers + set_config_option('syslog_install_upgrade_type', empty($options['upgrade_type']) ? '' : $options['upgrade_type'], true); + set_config_option('syslog_install_engine', $engine, true); + set_config_option('syslog_install_db_type', empty($options['db_type']) ? '' : $options['db_type'], true); + set_config_option('syslog_install_days', empty($options['days']) ? '' : $options['days'], true); + + if ($truncate) { + syslog_db_execute("DROP TABLE IF EXISTS `$syslogdb_default`.`syslog`"); + } + + // The syslog table is created partitioned; the helper also selects the + // matching ROW_FORMAT for the chosen engine. + syslog_create_partitioned_syslog_table($engine, $options['days'], read_config_option('syslog_partition_ahead_days')); + + if ($truncate) { + syslog_db_execute("DROP TABLE IF EXISTS `$syslogdb_default`.`syslog_alert`"); + } + + syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_alert` ( + `id` int(10) NOT NULL auto_increment, + `hash` varchar(32) NOT NULL default '', + `name` varchar(255) NOT NULL default '', + `severity` int(10) UNSIGNED NOT NULL default '0', + `method` int(10) unsigned NOT NULL default '0', + `level` int(10) unsigned NOT NULL default '0', + `num` int(10) unsigned NOT NULL default '1', + `type` varchar(16) NOT NULL default '', + `enabled` CHAR(2) default 'on', + `repeat_alert` int(10) unsigned NOT NULL default '0', + `cooldown_minutes` int(10) NOT NULL default '-1', + `deduplication_minutes` int(10) NOT NULL default '-1', + `suppression_schedule` text NOT NULL, + `maintenance_mode` varchar(16) NOT NULL default 'inherit', + `maintenance_days` varchar(32) NOT NULL default '1,2,3,4,5', + `maintenance_start` char(5) NOT NULL default '00:00', + `maintenance_end` char(5) NOT NULL default '00:00', + `maintenance_datetime_start` varchar(16) NOT NULL default '', + `maintenance_datetime_end` varchar(16) NOT NULL default '', + `open_ticket` CHAR(2) default '', + `message` TEXT NOT NULL, + `body` VARCHAR(8192) NOT NULL default '', + `user` varchar(32) NOT NULL default '', + `date` int(16) NOT NULL default '0', + `email` varchar(255) default NULL, + `notify` int(10) unsigned NOT NULL default '0', + `command` varchar(255) default NULL, + `notes` varchar(255) default NULL, + PRIMARY KEY (id)) + ENGINE=InnoDB"); + + if ($truncate) { + syslog_db_execute("DROP TABLE IF EXISTS `$syslogdb_default`.`syslog_incoming`"); + syslog_db_execute("DROP TABLE IF EXISTS `$syslogdb_default`.`syslog_replication_output`"); + } + + syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_incoming` ( + facility_id int(10) unsigned default NULL, + priority_id int(10) unsigned default NULL, + program varchar(40) default NULL, + logtime TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00', + host varchar(64) default NULL, + message TEXT NOT NULL, + seq bigint unsigned NOT NULL auto_increment, + `status` tinyint(4) NOT NULL default '0', + PRIMARY KEY (seq), + INDEX program (program), + INDEX `status` (`status`)) + ENGINE=InnoDB + ROW_FORMAT=Dynamic"); + +syslog_create_replication_output_table(); +syslog_create_replication_receipts_table(); +syslog_create_replication_collectors_table(); +syslog_create_replication_recovery_table(); + + syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_alert_suppression` ( + `alert_id` int(10) unsigned NOT NULL, + `scope_key` varchar(255) NOT NULL, + `dedup_hash` char(40) NOT NULL, + `last_sent` int(10) unsigned NOT NULL default '0', + PRIMARY KEY (`alert_id`, `scope_key`, `dedup_hash`), + INDEX `last_sent` (`last_sent`)) + ENGINE=InnoDB"); + + if ($truncate) { + syslog_db_execute("DROP TABLE IF EXISTS `$syslogdb_default`.`syslog_remove`"); + } + + if (stripos($engine, 'aria') !== false) { + $row_format = 'ROW_FORMAT=Page'; + } else { + $row_format = 'ROW_FORMAT=Dynamic'; + } + + syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_remove` ( + id int(10) NOT NULL auto_increment, + `hash` varchar(32) NOT NULL default '', + name varchar(255) NOT NULL default '', + `type` varchar(16) NOT NULL default '', + enabled CHAR(2) DEFAULT 'on', + method CHAR(5) DEFAULT 'del', + message TEXT NOT NULL, + `user` varchar(32) NOT NULL default '', + `date` int(16) NOT NULL default '0', + notes varchar(255) default NULL, + PRIMARY KEY (id)) + ENGINE=$engine + $row_format"); + + $present = syslog_db_fetch_row("SHOW TABLES FROM `$syslogdb_default` LIKE 'syslog_reports'"); + + if (cacti_sizeof($present)) { + $newreport = syslog_db_column_exists('syslog_reports', 'body'); + } else { + $newreport = true; + } + + if ($truncate || !$newreport) { + syslog_db_execute("DROP TABLE IF EXISTS `$syslogdb_default`.`syslog_reports`"); + } + + syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_reports` ( + id int(10) NOT NULL auto_increment, + `hash` varchar(32) NOT NULL default '', + name varchar(255) NOT NULL default '', + `type` varchar(16) NOT NULL default '', + enabled CHAR(2) DEFAULT 'on', + timespan int(16) NOT NULL default '0', + timepart char(5) NOT NULL default '00:00', + lastsent int(16) NOT NULL default '0', + body varchar(8192) NOT NULL default '0', + message TEXT default NULL, + `user` varchar(32) NOT NULL default '', + `date` int(16) NOT NULL default '0', + email varchar(255) default NULL, + notify int(10) unsigned NOT NULL default '0', + notes varchar(255) default NULL, + PRIMARY KEY (id)) + ENGINE=InnoDB + ROW_FORMAT=Dynamic"); + + syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_saved_searches` ( + `id` int(10) NOT NULL auto_increment, + `name` varchar(128) NOT NULL default '', + `search` text NOT NULL, + `removal` int(10) NOT NULL default '1', + `grouping` int(10) NOT NULL default '0', + `user` varchar(32) NOT NULL default '', + `is_global` char(2) NOT NULL default '', + `date` int(16) NOT NULL default '0', + PRIMARY KEY (`id`), + KEY owner (`user`)) + ENGINE=InnoDB + ROW_FORMAT=Dynamic"); + + syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_status` ( + `name` varchar(64) NOT NULL default '', + `value` text NOT NULL, + `updated` int(16) NOT NULL default '0', + PRIMARY KEY (`name`)) + ENGINE=InnoDB + ROW_FORMAT=Dynamic"); + + if ($truncate) { + syslog_db_execute("DROP TABLE IF EXISTS `$syslogdb_default`.`syslog_hosts`"); + } + + syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_programs` ( + `program_id` int(10) unsigned NOT NULL auto_increment, + `program` VARCHAR(40) NOT NULL, + `last_updated` TIMESTAMP NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + PRIMARY KEY (`program`), + INDEX host_id (`program_id`), + INDEX last_updated (`last_updated`)) + ENGINE=InnoDB + ROW_FORMAT=Dynamic + COMMENT='Contains all programs currently in the syslog table'"); + + syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_hosts` ( + `host_id` int(10) unsigned NOT NULL auto_increment, + `host` VARCHAR(64) NOT NULL, + `last_updated` TIMESTAMP NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + PRIMARY KEY (`host`), + INDEX host_id (`host_id`), + INDEX last_updated (`last_updated`)) + ENGINE=InnoDB + ROW_FORMAT=Dynamic + COMMENT='Contains all hosts currently in the syslog table'"); + + 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, + `facility` varchar(10) NOT NULL, + `last_updated` TIMESTAMP NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + PRIMARY KEY (`facility_id`), + INDEX last_updated (`last_updated`)) + ENGINE=InnoDB + ROW_FORMAT=Dynamic"); + + syslog_db_execute("INSERT 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`"); + + syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_priorities` ( + `priority_id` int(10) unsigned NOT NULL, + `priority` varchar(10) NOT NULL, + `last_updated` TIMESTAMP NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + PRIMARY KEY (`priority_id`), + INDEX last_updated (`last_updated`)) + ENGINE=InnoDB + ROW_FORMAT=Dynamic"); + + syslog_db_execute("INSERT 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` ( + `host_id` int(10) unsigned NOT NULL, + `facility_id` int(10) unsigned NOT NULL, + `last_updated` TIMESTAMP NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + PRIMARY KEY (`host_id`,`facility_id`)) + ENGINE=InnoDB + ROW_FORMAT=Dynamic"); + + if ($truncate) { + syslog_db_execute("DROP TABLE IF EXISTS `$syslogdb_default`.`syslog_removed`"); + } + + syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_removed` LIKE `$syslogdb_default`.`syslog`"); + + syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_logs` ( + alert_id int(10) unsigned not null default '0', + logseq bigint unsigned NOT NULL, + logtime TIMESTAMP NOT NULL default '0000-00-00 00:00:00', + logmsg varchar(1024) default NULL, + host varchar(64) default NULL, + facility_id int(10) unsigned default NULL, + priority_id int(10) unsigned default NULL, + program_id int(10) unsigned default NULL, + count integer unsigned NOT NULL default '0', + html blob default NULL, + seq bigint unsigned NOT NULL auto_increment, + PRIMARY KEY (seq), + INDEX `logseq` (`logseq`), + INDEX `program_id` (`program_id`), + INDEX `alert_id` (`alert_id`), + INDEX `host` (`host`), + INDEX `logtime` (`logtime`), + INDEX `priority_id` (`priority_id`), + INDEX `facility_id` (`facility_id`)) + ENGINE=InnoDB + ROW_FORMAT=Dynamic"); + + syslog_ensure_share_tables(); + + if (!isset($settings['syslog'])) { + syslog_config_settings(); + } + + foreach ($settings['syslog'] as $name => $values) { + if (isset($values['default'])) { + set_config_option($name, $values['default']); + } + } + + // ensure that the setting if set previously to truncate is switched to upgrade + set_config_option('syslog_install_upgrade_type', 'upgrade'); +} diff --git a/includes/settings.php b/includes/settings.php new file mode 100644 index 00000000..56ae969a --- /dev/null +++ b/includes/settings.php @@ -0,0 +1,598 @@ + __('Monday', 'syslog'), '2' => __('Tuesday', 'syslog'), '3' => __('Wednesday', 'syslog'), + '4' => __('Thursday', 'syslog'), '5' => __('Friday', 'syslog'), '6' => __('Saturday', 'syslog'), '7' => __('Sunday', 'syslog'), + '1,2,3,4,5' => __('Monday through Friday', 'syslog'), '6,7' => __('Saturday and Sunday', 'syslog'), + '1,2,3,4,5,6,7' => __('Every day', 'syslog') + ]; + + return $include_disabled ? ['0' => __('Disabled', 'syslog')] + $options : $options; +} + +function syslog_config_settings(): void { + global $config, $tabs, $formats, $settings, $syslog_retentions, $syslog_alert_retentions, $syslog_refresh; + + include_once($config['base_path'] . '/lib/reports.php'); + + if (get_nfilter_request_var('tab') == 'syslog') { + $formats = reports_get_format_files(); + } elseif (empty($formats)) { + $formats = []; + } + + $tabs['syslog'] = __('Syslog', 'syslog'); + + $temp = [ + 'syslog_header' => [ + 'friendly_name' => __('General Settings', 'syslog'), + 'method' => 'spacer', + ], + 'syslog_enabled' => [ + 'friendly_name' => __('Syslog Enabled', 'syslog'), + 'description' => __('If this checkbox is set, records will be transferred from the Syslog Incoming table to the main syslog table and Alerts and Reports will be enabled. Please keep in mind that if the system is disabled log entries will still accumulate into the Syslog Incoming table as this is defined by the rsyslog or syslog-ng process.', 'syslog'), + 'method' => 'checkbox', + 'default' => 'on' + ], + 'syslog_domains' => [ + 'friendly_name' => __('Strip Domains', 'syslog'), + 'description' => __('A comma delimited list of domains that you wish to remove from the syslog hostname, Examples would be \'mydomain.com, otherdomain.com\'', 'syslog'), + 'method' => 'textbox', + 'default' => '', + 'size' => 80, + 'max_length' => 255, + ], + 'syslog_refresh' => [ + 'friendly_name' => __('Refresh Interval', 'syslog'), + 'description' => __('This is the time in seconds before the page refreshes.', 'syslog'), + 'method' => 'drop_array', + 'default' => '300', + 'array' => $syslog_refresh + ], + 'syslog_maxrecords' => [ + 'friendly_name' => __('Max Report Records', 'syslog'), + 'description' => __('For Threshold based Alerts, what is the maximum number that you wish to show in the report. This is used to limit the size of the html log and Email.', 'syslog'), + 'method' => 'drop_array', + 'default' => '100', + 'array' => [ + 20 => __('%d Records', 20, 'syslog'), + 40 => __('%d Records', 40, 'syslog'), + 60 => __('%d Records', 60, 'syslog'), + 100 => __('%d Records', 100, 'syslog'), + 200 => __('%d Records', 200, 'syslog'), + 400 => __('%d Records', 400, 'syslog') + ] + ], + 'syslog_max_workers' => [ + 'friendly_name' => __('Maximum Syslog Processing Processes', 'syslog'), + 'description' => __('When set above 1, incoming message processing is spread across parallel worker processes. Each worker handles a disjoint range of incoming messages, speeding up hostname resolution and message transfer under heavy loads. A setting of 1 processes all messages in a single process. Worker processes are not supported on Windows or when the PHP pcntl and POSIX extensions are unavailable, in which case processing falls back to a single process.', 'syslog'), + 'method' => 'drop_array', + 'default' => '1', + 'array' => [ + 1 => __('%d Worker Process', 1, 'syslog'), + 2 => __('%d Worker Processes', 2, 'syslog'), + 4 => __('%d Worker Processes', 4, 'syslog'), + 6 => __('%d Worker Processes', 6, 'syslog'), + 8 => __('%d Worker Processes', 8, 'syslog'), + 16 => __('%d Worker Processes', 16, 'syslog') + ] + ], + 'syslog_ticket_command' => [ + 'friendly_name' => __('Command for Opening Tickets', 'syslog'), + 'description' => __('This command will be executed for opening Help Desk Tickets. The command will be required to parse multiple input parameters as follows: --alert-name, --severity, --hostlist, --message. The hostlist will be a comma delimited list of hosts impacted by the alert.', 'syslog'), + 'method' => 'textbox', + 'max_length' => 255, + 'size' => 80 + ], + 'syslog_html_header' => [ + 'friendly_name' => __('Host Discovery Options', 'syslog'), + 'method' => 'spacer', + 'collapsible' => 'true' + ], + 'syslog_resolve_hostname' => [ + 'friendly_name' => __('Enable Hostname Resolution', 'syslog'), + 'description' => __('If this checkbox is set, all hostnames are resolved via DNS lookup first (If enabled). If the DNS lookup fails, the system will attempt to resolve the hostname against the Cacti host table and replace it with the Cacti host description. If both DNS and Cacti lookups fail, records are assigned a prefix \'unresolved-Original_hostname\'.', 'syslog'), + 'method' => 'checkbox', + 'default' => '' + ], + 'syslog_no_dns' => [ + 'friendly_name' => __('Skip DNS Resolution for incoming hosts', 'syslog'), + 'description' => __('If this checkbox is set, the system will not attempt to resolve hosts via DNS lookups. This is useful for environments where DNS resolution is not possible or not desired.', 'syslog'), + 'method' => 'checkbox', + 'default' => '' + ], + 'syslog_html_notification_header' => [ + 'friendly_name' => __('HTML Notification Settings', 'syslog'), + 'method' => 'spacer', + 'collapsible' => 'true' + ], + 'syslog_html' => [ + 'friendly_name' => __('Enable HTML Based Email', 'syslog'), + 'description' => __('If this checkbox is set, all Emails will be sent in HTML format. Otherwise, Emails will be sent in plain text.', 'syslog'), + 'method' => 'checkbox', + 'default' => 'on' + ], + 'syslog_format_file' => [ + 'friendly_name' => __('Format File to Use', 'syslog'), + 'method' => 'drop_array', + 'default' => 'default.format', + 'description' => __('Choose the custom html wrapper and CSS file to use. This file contains both html and CSS to wrap around your report. If it contains more than simply CSS, you need to place a special tag inside of the file. This format tag will be replaced by the report content. These files are located in the \'formats\' directory.', 'syslog'), + 'array' => $formats + ], + 'syslog_retention_header' => [ + 'friendly_name' => __('Data Retention Settings', 'syslog'), + 'method' => 'spacer', + 'collapsible' => 'true' + ], + 'syslog_retention' => [ + 'friendly_name' => __('Syslog Retention', 'syslog'), + 'description' => __('This is the number of days to keep events.', 'syslog'), + 'method' => 'drop_array', + 'default' => '30', + 'array' => $syslog_retentions + ], + 'syslog_partition_ahead_days' => [ + 'friendly_name' => __('Partition Pre-create Window', 'syslog'), + 'description' => __('This is the number of future daily partitions to maintain for partitioned Syslog tables.', 'syslog'), + 'method' => 'drop_array', + 'default' => '3', + 'array' => [ + '1' => __('%d Day', 1, 'syslog'), + '2' => __('%d Days', 2, 'syslog'), + '3' => __('%d Days', 3, 'syslog'), + '4' => __('%d Days', 4, 'syslog'), + '5' => __('%d Days', 5, 'syslog'), + '6' => __('%d Days', 6, 'syslog'), + '7' => __('%d Days', 7, 'syslog') + ] + ], + 'syslog_partition_recover_limit' => [ + 'friendly_name' => __('Partition Recovery Limit', 'syslog'), + 'description' => __('When partitions are missing (for example after maintenance downtime), at most this many missing partitions per table are created on each poller run. The remaining gap heals over subsequent runs; retention pruning stays deferred until the future partition horizon is fully restored. Applies to partitioned Syslog tables only.', 'syslog'), + 'method' => 'drop_array', + 'default' => '3', + 'array' => [ + '1' => __('%d Partition per run', 1, 'syslog'), + '2' => __('%d Partitions per run', 2, 'syslog'), + '3' => __('%d Partitions per run', 3, 'syslog'), + '5' => __('%d Partitions per run', 5, 'syslog'), + '10' => __('%d Partitions per run', 10, 'syslog'), + '31' => __('%d Partitions per run', 31, 'syslog') + ] + ], + 'syslog_stale_threshold' => [ + 'friendly_name' => __('Collector Staleness Threshold', 'syslog'), + 'description' => __('The Syslog Status page shows a warning when no new message has arrived within this many seconds. Set to 0 to use the default of 300 seconds.', 'syslog'), + 'method' => 'drop_array', + 'default' => '300', + 'array' => [ + '0' => __('Default (300 seconds)', 'syslog'), + '120' => __('%d seconds', 120, 'syslog'), + '300' => __('%d seconds', 300, 'syslog'), + '600' => __('%d seconds', 600, 'syslog'), + '1800' => __('%d seconds', 1800, 'syslog'), + '3600' => __('%d seconds', 3600, 'syslog') + ] + ], + 'syslog_backlog_threshold' => [ + 'friendly_name' => __('Collector Backlog Threshold', 'syslog'), + 'description' => __('The Syslog Status page shows a warning when more than this many messages sit unprocessed in the Syslog Incoming table. Set to 0 to use the default of 10000 messages.', 'syslog'), + 'method' => 'drop_array', + 'default' => '10000', + 'array' => [ + '0' => __('Default (10000 messages)', 'syslog'), + '1000' => __('%d Messages', 1000, 'syslog'), + '5000' => __('%d Messages', 5000, 'syslog'), + '10000' => __('%d Messages', 10000, 'syslog'), + '50000' => __('%d Messages', 50000, 'syslog') + ] + ], + 'syslog_alert_retention' => [ + 'friendly_name' => __('Syslog Alert Retention', 'syslog'), + 'description' => __('This is the number of days to keep alert logs.', 'syslog'), + 'method' => 'drop_array', + 'default' => '30', + 'array' => $syslog_alert_retentions + ], + 'syslog_alert_suppression_header' => [ + 'friendly_name' => __('Alert Suppression and Maintenance', 'syslog'), + 'method' => 'spacer', + 'collapsible' => 'true' + ], + 'syslog_alert_maintenance_days' => [ + 'friendly_name' => __('Maintenance Window Days', 'syslog'), + 'description' => __('Days when all alert notifications are muted during the configured maintenance timeframe.', 'syslog'), + 'method' => 'drop_array', + 'array' => syslog_alert_maintenance_day_options(true), + 'default' => '0' + ], + 'syslog_alert_maintenance_start' => [ + 'friendly_name' => __('Maintenance Window Starts', 'syslog'), + 'description' => __('Local start time for the global maintenance window.', 'syslog'), + 'method' => 'drop_array', + 'array' => syslog_alert_maintenance_time_options(), + 'default' => '00:00' + ], + 'syslog_alert_maintenance_end' => [ + 'friendly_name' => __('Maintenance Window Ends', 'syslog'), + 'description' => __('Local end time for the global maintenance window. An earlier end time continues into the next day.', 'syslog'), + 'method' => 'drop_array', + 'array' => syslog_alert_maintenance_time_options(), + 'default' => '00:00' + ], + 'syslog_alert_maintenance_datetime_start' => [ + 'friendly_name' => __('One-time Maintenance Starts', 'syslog'), + 'description' => __('Optional local date and time to start a one-time global maintenance window.', 'syslog'), + 'method' => 'textbox', 'size' => '18', 'max_length' => '16', 'default' => '' + ], + 'syslog_alert_maintenance_datetime_end' => [ + 'friendly_name' => __('One-time Maintenance Ends', 'syslog'), + 'description' => __('Optional local date and time to end a one-time global maintenance window. Format: YYYY-MM-DD HH:MM.', 'syslog'), + 'method' => 'textbox', 'size' => '18', 'max_length' => '16', 'default' => '' + ], + 'syslog_alert_cooldown_minutes' => [ + 'friendly_name' => __('Default Alert Cooldown', 'syslog'), + 'description' => __('Suppress any repeat notification for the same rule and reporting scope for this many minutes. Per-rule values override this setting. Set to 0 to disable.', 'syslog'), + 'method' => 'textbox', + 'size' => '6', + 'max_length' => '6', + 'default' => '0' + ], + 'syslog_alert_deduplication_minutes' => [ + 'friendly_name' => __('Default Duplicate Suppression', 'syslog'), + 'description' => __('Suppress a notification with the same matched message set for this many minutes. Per-rule values override this setting. Set to 0 to disable.', 'syslog'), + 'method' => 'textbox', + 'size' => '6', + 'max_length' => '6', + 'default' => '0' + ], + 'syslog_remote_header' => [ + 'friendly_name' => __('Remote Message Processing', 'syslog'), + 'method' => 'spacer', + ], + 'syslog_remote_enabled' => [ + 'friendly_name' => __('Enable Remote Data Collector Message Processing', 'syslog'), + 'description' => __('If your Remote Data Collectors have their own Syslog databases and process their messages independently, check this checkbox. By checking this Checkbox, your Remote Data Collectors will need to maintain their own \'config_local.php\' file in order to inform Syslog to use an independent database for message display and processing. Please use the template file \'config_local.php.dist\' for this purpose. WARNING: Syslog tables will be automatically created as soon as this option is enabled.', 'syslog'), + 'method' => 'checkbox', + 'default' => '' + ], + 'syslog_remote_sync_rules' => [ + 'friendly_name' => __('Remote Data Collector Rules Sync', 'syslog'), + 'description' => __('If your Remote Data Collectors have their own Syslog databases and process thrie messages independently, check this checkbox if you wish the Main Cacti databases Alerts, Removal and Report rules to be sent to the Remote Cacti System.', 'syslog'), + 'method' => 'checkbox', + 'default' => '' + ], + 'syslog_remote_store_records' => [ + 'friendly_name' => __('Store Records on Remote Collector', 'syslog'), + 'description' => __('Keep processed messages in the remote collector syslog table after they have been delivered to the Main Collector. When disabled, records are retained locally only while the Main Collector is unavailable and are removed after confirmed recovery delivery.', 'syslog'), + 'method' => 'checkbox', + 'default' => '' + ], + 'syslog_replication_recovery_header' => [ + 'friendly_name' => __('Remote Syslog Recovery', 'syslog'), + 'method' => 'spacer', + ], + 'syslog_replication_recovery_records_per_run' => [ + 'friendly_name' => __('Maximum Recovery Records Per Execution', 'syslog'), + 'description' => __('Maximum number of retained remote Syslog events sent to the Main Collector by one recovery worker execution. Each central database transaction remains capped at 100 records. Increase gradually while observing Main Collector load and lock waits.', 'syslog'), + 'method' => 'drop_array', + 'default' => '2000', + 'array' => [ + '500' => __('%d Records', 500, 'syslog'), + '1000' => __('%d Records', 1000, 'syslog'), + '2000' => __('%d Records', 2000, 'syslog'), + '3000' => __('%d Records', 3000, 'syslog'), + '5000' => __('%d Records', 5000, 'syslog'), + '10000' => __('%d Records', 10000, 'syslog') + ] + ], + 'syslog_replication_recovery_batch_delay_ms' => [ + 'friendly_name' => __('Recovery Pause Between Batches', 'syslog'), + 'description' => __('Pause between each 100-record Main Collector delivery transaction. A longer pause lowers Main Collector pressure; a shorter pause speeds backlog convergence.', 'syslog'), + 'method' => 'drop_array', + 'default' => '100', + 'array' => [ + '0' => __('No pause', 'syslog'), + '25' => __('%d milliseconds', 25, 'syslog'), + '50' => __('%d milliseconds', 50, 'syslog'), + '100' => __('%d milliseconds', 100, 'syslog'), + '250' => __('%d milliseconds', 250, 'syslog'), + '500' => __('%d milliseconds', 500, 'syslog'), + '1000' => __('%d second', 1, 'syslog') + ] + ], + ]; + + if (isset($settings['syslog'])) { + $settings['syslog'] = array_merge($settings['syslog'], $temp); + } else { + $settings['syslog'] = $temp; + } +} + +/** Refresh permission labels, filenames and grouping from current realm IDs. */ +function syslog_refresh_permission_roles(): void { + global $user_auth_realms, $user_auth_realm_filenames, $user_auth_roles; + + $realms = db_fetch_assoc_prepared('SELECT id, file, display FROM plugin_realms WHERE plugin = ? ORDER BY id', ['syslog']); + if (!is_array($realms)) { + return; + } + $ids = []; + foreach ($realms as $realm) { + $id = (int) $realm['id'] + 100; + $ids[] = $id; + $user_auth_realms[$id] = $realm['display']; + foreach (explode(',', $realm['file']) as $file) { + $user_auth_realm_filenames[$file] = $id; + unset($_SESSION['sess_auth_names'][$file]); + } + } + $user_auth_roles[__('Syslog', 'syslog')] = array_values(array_unique($ids)); + + $rule_administrator = $user_auth_realm_filenames['syslog_rule_administrator.php'] ?? 0; + if ($rule_administrator && is_realm_allowed($rule_administrator)) { + foreach (['syslog_alerts.php', 'syslog_removal.php', 'syslog_reports.php'] as $file) { + $user_auth_realm_filenames[$file] = $rule_administrator; + } + } + + $administrator = $user_auth_realm_filenames['syslog_administrator.php'] ?? 0; + if ($administrator && is_realm_allowed($administrator)) { + foreach ([ + 'syslog.php', + 'syslog_alerts.php', + 'syslog_removal.php', + 'syslog_reports.php', + 'syslog_rule_administrator.php', + 'syslog_saved_searches.php', + 'syslog_saved_searches_share.php', + 'syslog_dashboards.php', + 'syslog_dashboards_share.php' + ] as $file) { + $user_auth_realm_filenames[$file] = $administrator; + } + } +} + +/** + * Setup the Syslog specific global arrays used throughout the plugin. + * + * @return void + */ +function syslog_config_arrays(): void { + global $syslog_actions, $config, $menu, $message_types, $severities, $messages; + global $syslog_levels, $syslog_facilities, $syslog_freqs, $syslog_times, $syslog_refresh; + global $syslog_retentions, $syslog_alert_retentions, $menu_glyphs; + + $syslog_actions = [ + 1 => __('Delete', 'syslog'), + 2 => __('Disable', 'syslog'), + 3 => __('Enable', 'syslog'), + 4 => __('Export', 'syslog') + ]; + + $syslog_levels = [ + 0 => 'emerg', + 1 => 'crit', + 2 => 'alert', + 3 => 'err', + 4 => 'warn', + 5 => 'notice', + 6 => 'info', + 7 => 'debug', + 8 => 'other' + ]; + + $syslog_facilities = [ + 0 => 'kernel', + 1 => 'user', + 2 => 'mail', + 3 => 'daemon', + 4 => 'auth', + 5 => 'syslog', + 6 => 'lpr', + 7 => 'news', + 8 => 'uucp', + 9 => 'cron', + 10 => 'authpriv', + 11 => 'ftp', + 12 => 'ntp', + 13 => 'log audit', + 14 => 'log alert', + 15 => 'cron', + 16 => 'local0', + 17 => 'local1', + 18 => 'local2', + 19 => 'local3', + 20 => 'local4', + 21 => 'local5', + 22 => 'local6', + 23 => 'local7' + ]; + + $syslog_retentions = [ + '0' => __('Indefinite', 'syslog'), + '1' => __('%d Day', 1, 'syslog'), + '2' => __('%d Days', 2, 'syslog'), + '3' => __('%d Days', 3, 'syslog'), + '4' => __('%d Days', 4, 'syslog'), + '5' => __('%d Days', 5, 'syslog'), + '6' => __('%d Days', 6, 'syslog'), + '7' => __('%d Week', 1, 'syslog'), + '14' => __('%d Weeks', 2, 'syslog'), + '30' => __('%d Month', 1, 'syslog'), + '60' => __('%d Months', 2, 'syslog'), + '90' => __('%d Months', 3, 'syslog'), + '120' => __('%d Months', 4, 'syslog'), + '160' => __('%d Months', 5, 'syslog'), + '183' => __('%d Months', 6, 'syslog'), + '365' => __('%d Year', 1, 'syslog') + ]; + + $syslog_alert_retentions = [ + '0' => __('Indefinite', 'syslog'), + '1' => __('%d Day', 1, 'syslog'), + '2' => __('%d Days', 2, 'syslog'), + '3' => __('%d Days', 3, 'syslog'), + '4' => __('%d Days', 4, 'syslog'), + '5' => __('%d Days', 5, 'syslog'), + '6' => __('%d Days', 6, 'syslog'), + '7' => __('%d Week', 1, 'syslog'), + '14' => __('%d Weeks', 2, 'syslog'), + '30' => __('%d Month', 1, 'syslog'), + '60' => __('%d Months', 2, 'syslog'), + '90' => __('%d Months', 3, 'syslog'), + '120' => __('%d Months', 4, 'syslog'), + '160' => __('%d Months', 5, 'syslog'), + '183' => __('%d Months', 6, 'syslog'), + '365' => __('%d Year', 1, 'syslog') + ]; + + $syslog_refresh = [ + 9999999 => __('Never', 'syslog'), + '60' => __('%d Minute', 1, 'syslog'), + '120' => __('%d Minutes', 2, 'syslog'), + '300' => __('%d Minutes', 5, 'syslog'), + '600' => __('%d Minutes', 10, 'syslog') + ]; + + $severities = [ + '0' => __('Notice', 'syslog'), + '1' => __('Warning', 'syslog'), + '2' => __('Critical', 'syslog') + ]; + + $message_types = [ + 'messageb' => __('Begins with', 'syslog'), + 'messagec' => __('Contains', 'syslog'), + 'messagee' => __('Ends with', 'syslog'), + 'host' => __('Hostname is', 'syslog'), + 'program' => __('Program is', 'syslog'), + 'facility' => __('Facility is', 'syslog'), + 'filter' => __('Filter Builder', 'syslog'), + 'sql' => __('SQL Expression', 'syslog') + ]; + + $syslog_freqs = [ + '86400' => __('Daily', 'syslog'), + '604800' => __('Weekly', 'syslog') + ]; + + for ($i = 0; $i <= 86400; $i += 1800) { + $minute = $i % 3600; + + if ($minute > 0) { + $minute = '30'; + } else { + $minute = '00'; + } + + if ($i > 0) { + $hour = strrev(substr(strrev('00' . intval($i / 3600)),0,2)); + } else { + $hour = '00'; + } + + $syslog_times[$i] = $hour . ':' . $minute; + } + + if (syslog_config_safe()) { + $menu2 = []; + + foreach ($menu as $temp => $temp2) { + $menu2[$temp] = $temp2; + + if ($temp == __('Import/Export')) { + $menu2[__('Syslog Settings', 'syslog')]['plugins/syslog/syslog_alerts.php'] = __('Alert Rules', 'syslog'); + $menu2[__('Syslog Settings', 'syslog')]['plugins/syslog/syslog_device_rules.php'] = __('Device Alert Rules', 'syslog'); + $menu2[__('Syslog Settings', 'syslog')]['plugins/syslog/syslog_removal.php'] = __('Removal Rules', 'syslog'); + $menu2[__('Syslog Settings', 'syslog')]['plugins/syslog/syslog_reports.php'] = __('Report Rules', 'syslog'); + $menu2[__('Syslog Settings', 'syslog')]['plugins/syslog/syslog_saved_searches.php'] = __('Saved Search Templates', 'syslog'); + $menu2[__('Syslog Settings', 'syslog')]['plugins/syslog/syslog_dashboards.php'] = __('Dashboards', 'syslog'); + } + } + $menu = $menu2; + + $menu_glyphs[__('Syslog Settings', 'syslog')] = 'fa fa-life-ring'; + } + + syslog_refresh_permission_roles(); + + if (isset($_SESSION['syslog_info']) && $_SESSION['syslog_info'] != '') { + $messages['syslog_info'] = ['message' => $_SESSION['syslog_info'], 'type' => 'info']; + } + + if (isset($_SESSION['syslog_error']) && $_SESSION['syslog_error'] != '') { + $messages['syslog_error'] = ['message' => $_SESSION['syslog_error'], 'type' => 'error']; + } +} + +/** Attach Cacti's date-time picker after the Syslog Settings form is drawn. */ +function syslog_settings_bottom(): void { + if (get_nfilter_request_var('tab') !== 'syslog') { + return; + } + ?> + + + + + + '> + + + + + + + + 'replication_source_poller_id', - 'type' => 'int(10) unsigned', - 'NULL' => true, - 'after' => 'seq' - ]); - syslog_db_add_column('syslog', [ - 'name' => 'replication_source_event_id', - 'type' => 'bigint unsigned', - 'NULL' => true, - 'after' => 'replication_source_poller_id' - ]); - syslog_db_execute("ALTER TABLE `$syslogdb_default`.`syslog` - ADD KEY `replication_source` (`replication_source_poller_id`, `replication_source_event_id`)"); -} - -/** Create the device-wide alert handling rules table on all Syslog collectors. */ -function syslog_create_device_rule_table(): void { - global $syslogdb_default; - - syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_device_rule` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `host` varchar(64) NOT NULL, - `enabled` char(2) NOT NULL DEFAULT 'on', - `mute_mode` varchar(16) NOT NULL DEFAULT 'none', - `mute_until` int(10) unsigned NOT NULL DEFAULT '0', - `pass_through_priority` int(10) NOT NULL DEFAULT '-1', - `allow_maintenance` char(2) NOT NULL DEFAULT '', - `notes` varchar(255) NOT NULL DEFAULT '', - `user` varchar(32) NOT NULL DEFAULT '', - `date` int(10) unsigned NOT NULL DEFAULT '0', - PRIMARY KEY (`id`), - UNIQUE KEY `host` (`host`), - KEY `enabled` (`enabled`)) - ENGINE=InnoDB - ROW_FORMAT=Dynamic"); -} - -/** - * Create the durable, Syslog-owned remote collector replication outbox. - * - * source_poller_id/source_event_id is the immutable distributed identity. - * The portable payload uses incoming-table values rather than local - * host/program surrogate IDs. Delivery is intentionally deferred. - * - * @return void - */ -function syslog_create_replication_output_table(): void { - global $syslogdb_default; - - syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_replication_output` ( - `source_poller_id` int(10) unsigned NOT NULL COMMENT 'Originating Cacti data collector ID', - `source_event_id` bigint unsigned NOT NULL COMMENT 'Immutable local syslog_incoming sequence', - `facility_id` int(10) unsigned default NULL COMMENT 'Portable syslog facility value', - `priority_id` int(10) unsigned default NULL COMMENT 'Portable syslog priority value', - `program` varchar(40) default NULL COMMENT 'Source program text', - `logtime` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT 'Original event timestamp', - `host` varchar(64) default NULL COMMENT 'Source host text', - `message` varchar(2048) NOT NULL DEFAULT '' COMMENT 'Source message text', - `disposition` varchar(16) NOT NULL COMMENT 'Local archival target: syslog or syslog_removed', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Outbox creation time', - `acknowledged_at` timestamp NULL DEFAULT NULL COMMENT 'Reserved for future destination acknowledgement', - `attempts` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'Reserved for future delivery retries', - PRIMARY KEY (`source_poller_id`, `source_event_id`), - KEY `backlog` (`acknowledged_at`, `created_at`, `source_poller_id`, `source_event_id`)) - ENGINE=InnoDB - ROW_FORMAT=Dynamic"); -} - -/** - * Create the Main Collector receipt boundary for remote Syslog delivery. - * - * The partitioned history tables deliberately retain their existing keys: - * MySQL requires every unique key on a partitioned table to include the - * partitioning column. A small unpartitioned receipt table therefore owns - * distributed idempotency and is committed with the archival insert. - * - * @return void - */ -function syslog_create_replication_receipts_table(): void { - global $config, $syslogdb_default; - - if (isset($config['poller_id']) && (int) $config['poller_id'] > 1) { - return; - } - - syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_replication_receipts` ( - `source_poller_id` int(10) unsigned NOT NULL, - `source_event_id` bigint unsigned NOT NULL, - `disposition` varchar(16) NOT NULL, - `accepted_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`source_poller_id`, `source_event_id`), - KEY `accepted_at` (`accepted_at`)) - ENGINE=InnoDB - ROW_FORMAT=Dynamic"); -} - -/** Create main-collector telemetry for the latest batch from each remote poller. */ -function syslog_create_replication_collectors_table(): void { - global $config, $syslogdb_default; - - if (isset($config['poller_id']) && (int) $config['poller_id'] > 1) { - return; - } - - syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_replication_collectors` ( - `source_poller_id` int(10) unsigned NOT NULL, - `last_batch_id` char(32) NOT NULL, - `last_batch_count` int(10) unsigned NOT NULL DEFAULT '0', - `last_received` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`source_poller_id`), - KEY `last_received` (`last_received`)) - ENGINE=InnoDB - ROW_FORMAT=Dynamic"); -} - -/** Create the plugin-owned, expiring local recovery-worker lease table. */ -function syslog_create_replication_recovery_table(): void { - global $syslogdb_default; - - syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_replication_recovery` ( - `name` varchar(32) NOT NULL, - `owner_token` char(32) NOT NULL, - `acquired_at` int(10) unsigned NOT NULL, - `heartbeat_at` int(10) unsigned NOT NULL, - `pid` int(10) unsigned NOT NULL DEFAULT '0', - PRIMARY KEY (`name`), - KEY `heartbeat_at` (`heartbeat_at`)) - ENGINE=InnoDB - ROW_FORMAT=Dynamic"); -} - -/** - * Create the partitioned Syslog table, including past and future partitions. - * - * @param string $engine The storage engine to create the table with. - * @param float|int|string $days The number of days of retention to provision. - * @param int|string|false $ahead_days The number of days of partitions to create - * ahead of today, or false when unconfigured. - * - * @return void - */ -function syslog_create_partitioned_syslog_table($engine = 'InnoDB', $days = 30, $ahead_days = 3) { - global $config, $syslogdb_default, $syslog_levels; - - syslog_connect(); - - $engine = syslog_validate_storage_engine($engine); - - // Guard the partition loop against non-numeric or extreme values: a - // crafted 'days' option would otherwise loop indefinitely or emit a - // very large DDL statement. - $days = (int) $days; - - if ($days < 0 || $days > 365) { - $days = 30; - } - - if (!is_numeric($ahead_days) || (int) $ahead_days < 1 || (int) $ahead_days > 7) { - $ahead_days = 3; - } - - $ahead_days = (int) $ahead_days; - - if (stripos($engine, 'aria') !== false) { - $row_format = 'ROW_FORMAT=Page'; - } else { - $row_format = 'ROW_FORMAT=Dynamic'; - } - - $sql = "CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog` ( - facility_id int(10) unsigned default NULL, - priority_id int(10) unsigned default NULL, - program_id int(10) unsigned default NULL, - host_id int(10) unsigned default NULL, - logtime timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', - message varchar(2048) NOT NULL default '', - seq bigint unsigned NOT NULL auto_increment, - replication_source_poller_id int(10) unsigned default NULL, - replication_source_event_id bigint unsigned default NULL, - PRIMARY KEY(seq, logtime), - INDEX `seq` (`seq`), - INDEX `replication_source` (`replication_source_poller_id`, `replication_source_event_id`), - INDEX logtime (logtime), - INDEX program_id (program_id), - INDEX host_id (host_id), - INDEX priority_id (priority_id), - INDEX facility_id (facility_id)) - ENGINE=$engine - $row_format - PARTITION BY RANGE (UNIX_TIMESTAMP(logtime))\n"; - - $now = time(); - - $parts = ''; - - /* - * Partition boundaries are integer epochs computed in PHP and injected - * as numeric literals. This keeps both MySQL and PHP session time zones - * out of the equation: the boundary is always the next UTC midnight - * after the labeled day. - * - * $days counts today as one of the retained days, so the historical - * loop bound is $days - 1 (today plus $days - 1 prior days = $days - * total). This must stay in lockstep with the $days + $ahead_days - * "keep_partitions" math in syslog_partition_remove(), or newly - * installed tables over-provision by one partition and the very next - * retention prune immediately deletes the oldest one. - * - * $days = 0 is a valid "Indefinite" retention setting (never pruned by - * syslog_partition_remove(), which only prunes when $days > 0), but - * $days - 1 would then be -1 and skip creating today's concrete - * partition entirely. Clamp the starting bound to 0 so today is always - * created regardless of retention. - */ - for ($i = max($days - 1, 0); $i >= (0 - $ahead_days); $i--) { - $day_epoch = $now - ($i * 86400); - $boundary_epoch = (intdiv($day_epoch, 86400) + 1) * 86400; - $format = gmdate('Ymd', $day_epoch); - - $parts .= ($parts !== '' ? ",\n" : '(') . ' PARTITION d' . $format . ' VALUES LESS THAN (' . $boundary_epoch . ')'; - } - - $parts .= ",\nPARTITION dMaxValue VALUES LESS THAN MAXVALUE);"; - - syslog_db_execute($sql . $parts); -} - -/** - * Create the Syslog database tables, dropping existing ones on truncate. - * - * @param array $options The install options, either from the - * request, saved settings, or the Syslog - * config file. - * - * @return void - */ -function syslog_setup_table_new(array $options): void { - global $config, $settings, $syslogdb_default, $syslog_levels; - - syslog_connect(); - - $tables = []; - - $syslog_levels = [ - 0 => 'emerg', - 1 => 'crit', - 2 => 'alert', - 3 => 'err', - 4 => 'warn', - 5 => 'notice', - 6 => 'info', - 7 => 'debug', - 8 => 'other' - ]; - - // Set default if they are not set. - if (!cacti_sizeof($options)) { - $options['upgrade_type'] = read_config_option('syslog_install_upgrade_type'); - $options['engine'] = read_config_option('syslog_install_engine'); - $options['db_type'] = read_config_option('syslog_install_db_type'); - $options['days'] = read_config_option('syslog_install_days'); - - if (empty($options['upgrade_type'])) { - $options['upgrade_type'] = 'upgrade'; - } - - if (empty($options['engine'])) { - $options['engine'] = 'InnoDB'; - } - - if (empty($options['db_type'])) { - $options['db_type'] = 'part'; - } - - if (empty($options['days'])) { - $options['days'] = 30; - } - } - - // 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 - // options were supplied (request, saved settings, or legacy config.php). - if (!isset($options['db_type']) || $options['db_type'] != 'part') { - if (isset($options['db_type']) && $options['db_type'] == 'trad') { - cacti_log("SYSLOG WARNING: The 'trad' database architecture setting is deprecated. Partitioned tables are the only supported architecture; creating partitioned tables", false, 'SYSLOG'); - } - - $options['db_type'] = 'part'; - } - - // validate some simple information - $truncate = isset($options['upgrade_type']) && $options['upgrade_type'] == 'truncate' ? true : false; - $engine = syslog_validate_storage_engine(isset($options['engine']) ? $options['engine'] : 'InnoDB'); - $syslogexists = sizeof(syslog_db_fetch_row("SHOW TABLES FROM `$syslogdb_default` LIKE 'syslog'")); - - // Aria is only supported on MariaDB. If a legacy setting requests Aria - // on MySQL, fall back to InnoDB so table creation cannot fail. - if (stripos($engine, 'aria') !== false) { - $database = syslog_db_fetch_row('SHOW GLOBAL VARIABLES LIKE "version"'); - $version = is_array($database) && isset($database['Value']) ? (string) $database['Value'] : ''; - - if (stripos($version, 'mariadb') === false) { - cacti_log('SYSLOG WARNING: Aria storage engine requested on a non-MariaDB database. Falling back to InnoDB', false, 'SYSLOG'); - - $engine = 'InnoDB'; - } - } - - // Partition retention days are required for partitioned table creation - // regardless of how the options were supplied. '0' is a valid value - // (Indefinite retention) and must not be coerced. - if (!isset($options['days']) || !is_numeric($options['days']) || (int) $options['days'] < 0) { - $options['days'] = 30; - } - - // set table construction settings for the remote pollers - set_config_option('syslog_install_upgrade_type', empty($options['upgrade_type']) ? '' : $options['upgrade_type'], true); - set_config_option('syslog_install_engine', empty($options['engine']) ? '' : $options['engine'], true); - set_config_option('syslog_install_db_type', empty($options['db_type']) ? '' : $options['db_type'], true); - set_config_option('syslog_install_days', empty($options['days']) ? '' : $options['days'], true); - - if ($truncate) { - syslog_db_execute("DROP TABLE IF EXISTS `$syslogdb_default`.`syslog`"); - } - - // The syslog table is created partitioned; the helper also selects the - // matching ROW_FORMAT for the chosen engine. - syslog_create_partitioned_syslog_table($engine, $options['days'], read_config_option('syslog_partition_ahead_days')); - - if ($truncate) { - syslog_db_execute("DROP TABLE IF EXISTS `$syslogdb_default`.`syslog_alert`"); - } - - syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_alert` ( - `id` int(10) NOT NULL auto_increment, - `hash` varchar(32) NOT NULL default '', - `name` varchar(255) NOT NULL default '', - `severity` int(10) UNSIGNED NOT NULL default '0', - `method` int(10) unsigned NOT NULL default '0', - `level` int(10) unsigned NOT NULL default '0', - `num` int(10) unsigned NOT NULL default '1', - `type` varchar(16) NOT NULL default '', - `enabled` CHAR(2) default 'on', - `repeat_alert` int(10) unsigned NOT NULL default '0', - `cooldown_minutes` int(10) NOT NULL default '-1', - `deduplication_minutes` int(10) NOT NULL default '-1', - `suppression_schedule` text NOT NULL, - `maintenance_mode` varchar(16) NOT NULL default 'inherit', - `maintenance_days` varchar(32) NOT NULL default '1,2,3,4,5', - `maintenance_start` char(5) NOT NULL default '00:00', - `maintenance_end` char(5) NOT NULL default '00:00', - `maintenance_datetime_start` varchar(16) NOT NULL default '', - `maintenance_datetime_end` varchar(16) NOT NULL default '', - `open_ticket` CHAR(2) default '', - `message` TEXT NOT NULL, - `body` VARCHAR(8192) NOT NULL default '', - `user` varchar(32) NOT NULL default '', - `date` int(16) NOT NULL default '0', - `email` varchar(255) default NULL, - `notify` int(10) unsigned NOT NULL default '0', - `command` varchar(255) default NULL, - `notes` varchar(255) default NULL, - PRIMARY KEY (id)) - ENGINE=InnoDB"); - - if ($truncate) { - syslog_db_execute("DROP TABLE IF EXISTS `$syslogdb_default`.`syslog_incoming`"); - } - - syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_incoming` ( - facility_id int(10) unsigned default NULL, - priority_id int(10) unsigned default NULL, - program varchar(40) default NULL, - logtime TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00', - host varchar(64) default NULL, - message varchar(2048) NOT NULL default '', - seq bigint unsigned NOT NULL auto_increment, - `status` tinyint(4) NOT NULL default '0', - PRIMARY KEY (seq), - INDEX program (program), - INDEX `status` (`status`)) - ENGINE=InnoDB - ROW_FORMAT=Dynamic"); - -syslog_create_replication_output_table(); -syslog_create_replication_receipts_table(); -syslog_create_replication_collectors_table(); -syslog_create_replication_recovery_table(); - - syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_alert_suppression` ( - `alert_id` int(10) unsigned NOT NULL, - `scope_key` varchar(255) NOT NULL, - `dedup_hash` char(40) NOT NULL, - `last_sent` int(10) unsigned NOT NULL default '0', - PRIMARY KEY (`alert_id`, `scope_key`, `dedup_hash`), - INDEX `last_sent` (`last_sent`)) - ENGINE=InnoDB"); - - if ($truncate) { - syslog_db_execute("DROP TABLE IF EXISTS `$syslogdb_default`.`syslog_remove`"); - } - - if (stripos($engine, 'aria') !== false) { - $row_format = 'ROW_FORMAT=Page'; - } else { - $row_format = 'ROW_FORMAT=Dynamic'; - } - - syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_remove` ( - id int(10) NOT NULL auto_increment, - `hash` varchar(32) NOT NULL default '', - name varchar(255) NOT NULL default '', - `type` varchar(16) NOT NULL default '', - enabled CHAR(2) DEFAULT 'on', - method CHAR(5) DEFAULT 'del', - message TEXT NOT NULL, - `user` varchar(32) NOT NULL default '', - `date` int(16) NOT NULL default '0', - notes varchar(255) default NULL, - PRIMARY KEY (id)) - ENGINE=$engine - $row_format"); - - $present = syslog_db_fetch_row("SHOW TABLES FROM `$syslogdb_default` LIKE 'syslog_reports'"); - - if (cacti_sizeof($present)) { - $newreport = syslog_db_column_exists('syslog_reports', 'body'); - } else { - $newreport = true; - } - - if ($truncate || !$newreport) { - syslog_db_execute("DROP TABLE IF EXISTS `$syslogdb_default`.`syslog_reports`"); - } - - syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_reports` ( - id int(10) NOT NULL auto_increment, - `hash` varchar(32) NOT NULL default '', - name varchar(255) NOT NULL default '', - `type` varchar(16) NOT NULL default '', - enabled CHAR(2) DEFAULT 'on', - timespan int(16) NOT NULL default '0', - timepart char(5) NOT NULL default '00:00', - lastsent int(16) NOT NULL default '0', - body varchar(8192) NOT NULL default '0', - message varchar(2048) default NULL, - `user` varchar(32) NOT NULL default '', - `date` int(16) NOT NULL default '0', - email varchar(255) default NULL, - notify int(10) unsigned NOT NULL default '0', - notes varchar(255) default NULL, - PRIMARY KEY (id)) - ENGINE=InnoDB - ROW_FORMAT=Dynamic"); - - syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_saved_searches` ( - `id` int(10) NOT NULL auto_increment, - `name` varchar(128) NOT NULL default '', - `search` text NOT NULL, - `removal` int(10) NOT NULL default '1', - `grouping` int(10) NOT NULL default '0', - `user` varchar(32) NOT NULL default '', - `is_global` char(2) NOT NULL default '', - `date` int(16) NOT NULL default '0', - PRIMARY KEY (`id`), - KEY owner (`user`)) - ENGINE=InnoDB - ROW_FORMAT=Dynamic"); - - syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_status` ( - `name` varchar(64) NOT NULL default '', - `value` text NOT NULL, - `updated` int(16) NOT NULL default '0', - PRIMARY KEY (`name`)) - ENGINE=InnoDB - ROW_FORMAT=Dynamic"); - - if ($truncate) { - syslog_db_execute("DROP TABLE IF EXISTS `$syslogdb_default`.`syslog_hosts`"); - } - - syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_programs` ( - `program_id` int(10) unsigned NOT NULL auto_increment, - `program` VARCHAR(40) NOT NULL, - `last_updated` TIMESTAMP NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, - PRIMARY KEY (`program`), - INDEX host_id (`program_id`), - INDEX last_updated (`last_updated`)) - ENGINE=InnoDB - ROW_FORMAT=Dynamic - COMMENT='Contains all programs currently in the syslog table'"); - - syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_hosts` ( - `host_id` int(10) unsigned NOT NULL auto_increment, - `host` VARCHAR(64) NOT NULL, - `last_updated` TIMESTAMP NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, - PRIMARY KEY (`host`), - INDEX host_id (`host_id`), - INDEX last_updated (`last_updated`)) - ENGINE=InnoDB - ROW_FORMAT=Dynamic - COMMENT='Contains all hosts currently in the syslog table'"); - - 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, - `facility` varchar(10) NOT NULL, - `last_updated` TIMESTAMP NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, - PRIMARY KEY (`facility_id`), - INDEX last_updated (`last_updated`)) - ENGINE=InnoDB - ROW_FORMAT=Dynamic"); - - syslog_db_execute("INSERT 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`"); - - syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_priorities` ( - `priority_id` int(10) unsigned NOT NULL, - `priority` varchar(10) NOT NULL, - `last_updated` TIMESTAMP NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, - PRIMARY KEY (`priority_id`), - INDEX last_updated (`last_updated`)) - ENGINE=InnoDB - ROW_FORMAT=Dynamic"); - - syslog_db_execute("INSERT 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` ( - `host_id` int(10) unsigned NOT NULL, - `facility_id` int(10) unsigned NOT NULL, - `last_updated` TIMESTAMP NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, - PRIMARY KEY (`host_id`,`facility_id`)) - ENGINE=InnoDB - ROW_FORMAT=Dynamic"); - - if ($truncate) { - syslog_db_execute("DROP TABLE IF EXISTS `$syslogdb_default`.`syslog_removed`"); - } - - syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_removed` LIKE `$syslogdb_default`.`syslog`"); - - syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_logs` ( - alert_id int(10) unsigned not null default '0', - logseq bigint unsigned NOT NULL, - logtime TIMESTAMP NOT NULL default '0000-00-00 00:00:00', - logmsg varchar(1024) default NULL, - host varchar(64) default NULL, - facility_id int(10) unsigned default NULL, - priority_id int(10) unsigned default NULL, - program_id int(10) unsigned default NULL, - count integer unsigned NOT NULL default '0', - html blob default NULL, - seq bigint unsigned NOT NULL auto_increment, - PRIMARY KEY (seq), - INDEX `logseq` (`logseq`), - INDEX `program_id` (`program_id`), - INDEX `alert_id` (`alert_id`), - INDEX `host` (`host`), - INDEX `logtime` (`logtime`), - INDEX `priority_id` (`priority_id`), - INDEX `facility_id` (`facility_id`)) - ENGINE=InnoDB - ROW_FORMAT=Dynamic"); - - if (!isset($settings['syslog'])) { - syslog_config_settings(); - } - - foreach ($settings['syslog'] as $name => $values) { - if (isset($values['default'])) { - set_config_option($name, $values['default']); - } - } - - // ensure that the setting if set previously to truncate is switched to upgrade - set_config_option('syslog_install_upgrade_type', 'upgrade'); -} /** - * Replicate the Syslog rules tables to a remote data collector. - * - * @param array $data The replication data provided by Cacti, - * containing the remote poller id, the remote - * connection id and the replication class. + * Return the version information for the Syslog plugin. * - * @return array The replication data, unmodified. + * @return array The 'info' section of the plugin INFO file, or an + * empty array when the file cannot be parsed. */ -function syslog_replicate_out($data) { - syslog_connect(); +function plugin_syslog_version(): array { + $info = parse_ini_file(__DIR__ . '/INFO', true); - if (read_config_option('syslog_remote_enabled') == 'on' && read_config_option('syslog_remote_sync_rules') == 'on') { - $remote_poller_id = $data['remote_poller_id']; - $rcnn_id = $data['rcnn_id']; - $class = $data['class']; - - cacti_log('INFO: Replicating for the Syslog Plugin', false, 'REPLICATE'); - - if ($class == 'all') { - $tdata = syslog_db_fetch_assoc('SELECT * FROM syslog_alert'); - replicate_out_table($rcnn_id, $tdata, 'syslog_alert', $remote_poller_id); - $tdata = syslog_db_fetch_assoc('SELECT * FROM syslog_device_rule'); - replicate_out_table($rcnn_id, $tdata, 'syslog_device_rule', $remote_poller_id); - $tdata = syslog_db_fetch_assoc('SELECT * FROM syslog_remove'); - replicate_out_table($rcnn_id, $tdata, 'syslog_remove', $remote_poller_id); - $tdata = syslog_db_fetch_assoc('SELECT * FROM syslog_reports'); - replicate_out_table($rcnn_id, $tdata, 'syslog_reports', $remote_poller_id); - } + if (is_array($info) && isset($info['info']) && is_array($info['info'])) { + return $info['info']; } - return $data; -} - -/** - * Pull the Syslog rules tables from the main Cacti database. - * - * @return void - */ -function syslog_replicate_in(): void { - syslog_connect(); - - if (read_config_option('syslog_remote_enabled') == 'on' && read_config_option('syslog_remote_sync_rules') == 'on') { - $data = db_fetch_assoc('SELECT * FROM syslog_alert'); - syslog_replace_data('syslog_alert', $data); - - $data = db_fetch_assoc('SELECT * FROM syslog_remove'); - syslog_replace_data('syslog_remove', $data); - - $data = db_fetch_assoc('SELECT * FROM syslog_reports'); - syslog_replace_data('syslog_reports', $data); - } + return []; } /** - * Replace the data in the local table with the replicated data. - * - * @param string $table The name of the table to replace. - * @param array> $data The replicated rows, passed by reference. + * Check that the Syslog plugin dependencies are met. * - * @return void - */ -function syslog_replace_data($table, &$data) { - if (cacti_sizeof($data)) { - $sqlData = []; - $sqlQuery = []; - $columns = array_keys($data[0]); - $create_sql = ''; - - $create = db_fetch_row('SHOW CREATE TABLE ' . $table); - - if (is_array($create)) { - if (isset($create["CREATE TABLE `$table`"])) { - $create_sql = $create["CREATE TABLE `$table`"]; - } elseif (isset($create['Create Table'])) { - $create_sql = $create['Create Table']; - } - } - - if (!syslog_db_table_exists($table)) { - if ($create_sql == '') { - cacti_log('WARNING: Unable to derive CREATE TABLE SQL for `' . $table . '` during Syslog replication.', false, 'REPLICATE'); - - return; - } - - syslog_db_execute($create_sql); - syslog_db_execute("TRUNCATE TABLE $table"); - } - - // Make the prefix - $sql_prefix = "INSERT INTO $table (`" . implode('`,`', $columns) . '`) VALUES '; - - // Make the suffix - $sql_suffix = ' ON DUPLICATE KEY UPDATE '; - - foreach ($columns as $c) { - $sql_suffix .= " $c = VALUES($c),"; - } - $sql_suffix = trim($sql_suffix, ','); - - // Construct the prepared statement - foreach ($data as $row) { - $sqlQuery[] = '(' . trim(str_repeat('?, ', cacti_sizeof($columns)), ', ') . ')'; - - foreach ($row as $col) { - $sqlData[] = $col; - } - } - - $sql = implode(', ', $sqlQuery); - - syslog_db_execute_prepared($sql_prefix . $sql . $sql_suffix, $sqlData); - } -} - -/** - * Return the version information for the Syslog plugin. - * - * @return array The 'info' section of the plugin INFO file, or an - * empty array when the file cannot be parsed. - */ -function plugin_syslog_version(): array { - global $config; - $info = parse_ini_file($config['base_path'] . '/plugins/syslog/INFO', true); - - if (is_array($info) && isset($info['info']) && is_array($info['info'])) { - return $info['info']; - } - - return []; -} - -/** - * Check that the Syslog plugin dependencies are met. - * - * @return bool Always true, the Syslog plugin has no dependencies. + * @return bool Always true, the Syslog plugin has no dependencies. */ function syslog_check_dependencies(): bool { return true; } - -/** - * Poller bottom hook, launches the Syslog message processing process. - * - * @return void - */ -function syslog_poller_bottom(): void { - global $config; - - if (syslog_config_safe()) { - include_once(__DIR__ . '/includes/functions.php'); - include_once(__DIR__ . '/includes/database.php'); - - syslog_connect(); - syslog_status_set('last_polling_time', time()); - - $command_string = (string) read_config_option('path_php_binary'); - $extra_args = ' -q ' . $config['base_path'] . '/plugins/syslog/syslog_process.php'; - exec_background($command_string, $extra_args); - } else { - cacti_log('WARNING: You have installed the Syslog plugin, but you have not properly set a config.php or config_local.php', false, 'POLLER'); - } -} - -/** - * Display the install/upgrade advisor form. - * - * @param int $syslog_exists Whether the Syslog table already exists. - * - * @return void - */ -function syslog_install_advisor(int $syslog_exists): void { - global $config, $syslog_retentions; - - top_header(); - - syslog_config_arrays(); - - $fields_syslog_update = [ - 'upgrade_type' => [ - 'method' => 'drop_array', - 'friendly_name' => __('What upgrade/install type do you wish to use', 'syslog'), - 'description' => __('When you have very large tables, performing a Truncate will be much quicker. If you are concerned about archive data, you can choose either Inline, which will freeze your browser for the period of this upgrade, or background, which will create a background process to bring your old syslog data from a backup table to the new syslog format. Again this process can take several hours.', 'syslog'), - 'value' => 'truncate', - 'array' => [ - 'truncate' => __('Truncate Syslog Table', 'syslog'), - 'inline' => __('Inline Upgrade', 'syslog'), - 'background' => __('Background Upgrade', 'syslog') - ] - ], - 'engine' => [ - 'method' => 'drop_array', - 'friendly_name' => __('Database Storage Engine', 'syslog'), - 'description' => __('The storage engine for the analytical tables. Only InnoDB and, on MariaDB, Aria storage engines are supported.', 'syslog'), - 'value' => 'innodb', - 'array' => [ - 'innodb' => __('InnoDB Storage', 'syslog'), - 'aria' => __('Aria Storage', 'syslog') - ] - ], - 'db_type' => [ - 'method' => 'hidden', - 'value' => 'part' - ], - 'days' => [ - 'method' => 'drop_array', - 'friendly_name' => __('Retention Policy', 'syslog'), - 'description' => __('Choose how many days of Syslog values you wish to maintain in the database.', 'syslog'), - 'value' => '30', - 'array' => $syslog_retentions - ], - 'mode' => [ - 'method' => 'hidden', - 'value' => 'install' - ], - 'install' => [ - 'method' => 'hidden', - 'value' => 'true' - ], - 'id' => [ - 'method' => 'hidden', - 'value' => 'syslog' - ] - ]; - - $fields_syslog_update['dayparts'] = [ - 'method' => 'drop_array', - 'friendly_name' => __('Partitions per Day', 'syslog'), - 'description' => __('Select the number of partitions per day that you wish to create.', 'syslog'), - 'value' => '1', - 'array' => [ - '1' => __('%d Per Day', 1, 'syslog'), - '2' => __('%d Per Day', 2, 'syslog'), - '4' => __('%d Per Day', 4, 'syslog'), - '6' => __('%d Per Day', 6, 'syslog'), - '12' => __('%d Per Day', 12, 'syslog') - ] - ]; - - if ($syslog_exists) { - $type = __('Upgrade', 'syslog'); - } else { - $type = __('Install', 'syslog'); - } - - syslog_connect(); - $database = syslog_db_fetch_row('SHOW GLOBAL VARIABLES LIKE "version"'); - - // remove Aria as a storage engine if this is mysql - if (stripos($database['Value'], 'mariadb') === false) { - unset($fields_syslog_update['engine']['array']['aria']); - } else { - $fields_syslog_update['engine']['value'] = 'aria'; - } - - print "'; - } else { - unset($fields_syslog_update['upgrade_type']); - print '

' . __('You have several options to choose from when installing Syslog. The first is the Database Architecture. Partitioned tables are the only supported architecture as they prevent the size of the tables from becoming excessive thus slowing queries. Traditional non-partitioned tables are deprecated and are no longer offered for new installs.', 'syslog') . '

' . __('You can also set the database storage engine for the analytical tables syslog and syslog_remove. Only the InnoDB storage engine and, on MariaDB, the Aria storage engine are supported. If using MariaDB, we recommend the Aria Storage Engine as it is designed for analytical queries.', 'syslog') . '

' . __('You can also select the retention duration. Please keep in mind that if you have several hosts logging to syslog, this table can become quite large.', 'syslog') . '

'; - } - html_end_box(); - - print ""; - - html_start_box(__('Syslog %s Settings', $type, 'syslog'), '100%', '', '3', 'center', ''); - - draw_edit_form([ - 'config' => [], - 'fields' => inject_form_variables($fields_syslog_update, [])] - ); - - html_end_box(); - syslog_confirm_button('install', 'plugins.php', $syslog_exists); - print '
"; - html_start_box(__('Syslog %s Advisor', $type, 'syslog'), '100%', '', '3', 'center', ''); - print '
'; - - if ($syslog_exists) { - print "

" . __('WARNING: Syslog Upgrade is Time Consuming!!!', 'syslog') . '

'; - print '

' . __('The upgrade of the \'main\' syslog table can be a very time consuming process. As such, it is recommended that you either reduce the size of your syslog table prior to upgrading, or choose the background option

If you choose the background option, your legacy syslog table will be renamed, and a new syslog table will be created. Then, an upgrade process will be launched in the background. Again, this background process can quite a bit of time to complete. However, your data will be preserved

Regardless of your choice, all existing removal and alert rules will be maintained during the upgrade process.

Press \'Upgrade\' to proceed with the upgrade, or \'Cancel\' to return to the Plugins menu.', 'syslog') . '

'; - - bottom_footer(); - exit; -} - -/** - * Display the uninstall advisor form. - * - * @return void - */ -function syslog_uninstall_advisor(): void { - global $config, $syslogdb_default; - - syslog_connect(); - - $syslog_exists = sizeof(syslog_db_fetch_row("SHOW TABLES FROM `$syslogdb_default` LIKE 'syslog'")); - - top_header(); - - $fields_syslog_update = [ - 'uninstall_method' => [ - 'method' => 'drop_array', - 'friendly_name' => __('What uninstall method do you want to use?', 'syslog'), - 'description' => __('When uninstalling syslog, you can remove everything, or only components, just in case you plan on re-installing in the future.', 'syslog'), - 'value' => 'all', - 'array' => ['all' => __('Remove Everything (Logs, Tables, Settings)', 'syslog'), 'syslog' => __('Syslog Data Only', 'syslog')], - ], - 'mode' => [ - 'method' => 'hidden', - 'value' => 'uninstall' - ], - 'uninstall' => [ - 'method' => 'hidden', - 'value' => 'true' - ], - 'id' => [ - 'method' => 'hidden', - 'value' => 'syslog' - ] - ]; - - form_start('plugins.php'); - - print "
"; - - html_start_box(__('Syslog Uninstall Preferences', 'syslog'), '100%', '', '3', 'center', ''); - - draw_edit_form([ - 'config' => [], - 'fields' => inject_form_variables($fields_syslog_update, [])] - ); - - html_end_box(); - - syslog_confirm_button('uninstall', 'plugins.php', $syslog_exists); - - print '
'; - - bottom_footer(); - exit; -} - -/** - * Display the confirm and cancel buttons for the install/uninstall forms. - * - * @param string $action The action being confirmed, either 'install' or 'uninstall'. - * @param string $cancel_url The URL to load when the action is cancelled. - * @param int $syslog_exists Whether the Syslog table already exists. - * - * @return void - */ -function syslog_confirm_button(string $action, string $cancel_url, int $syslog_exists): void { - if ($action == 'install') { - if ($syslog_exists) { - $value = __('Upgrade', 'syslog'); - } else { - $value = __('Install', 'syslog'); - } - } else { - $value = __('Uninstall', 'syslog'); - } - - ?> - - - - -
- ' type='button' value=''> - - -
- - __('Monday', 'syslog'), '2' => __('Tuesday', 'syslog'), '3' => __('Wednesday', 'syslog'), - '4' => __('Thursday', 'syslog'), '5' => __('Friday', 'syslog'), '6' => __('Saturday', 'syslog'), '7' => __('Sunday', 'syslog'), - '1,2,3,4,5' => __('Monday through Friday', 'syslog'), '6,7' => __('Saturday and Sunday', 'syslog'), - '1,2,3,4,5,6,7' => __('Every day', 'syslog') - ]; - - return $include_disabled ? ['0' => __('Disabled', 'syslog')] + $options : $options; -} - -function syslog_config_settings(): void { - global $config, $tabs, $formats, $settings, $syslog_retentions, $syslog_alert_retentions, $syslog_refresh; - - include_once($config['base_path'] . '/lib/reports.php'); - - if (get_nfilter_request_var('tab') == 'syslog') { - $formats = reports_get_format_files(); - } elseif (empty($formats)) { - $formats = []; - } - - $tabs['syslog'] = __('Syslog', 'syslog'); - - $temp = [ - 'syslog_header' => [ - 'friendly_name' => __('General Settings', 'syslog'), - 'method' => 'spacer', - ], - 'syslog_enabled' => [ - 'friendly_name' => __('Syslog Enabled', 'syslog'), - 'description' => __('If this checkbox is set, records will be transferred from the Syslog Incoming table to the main syslog table and Alerts and Reports will be enabled. Please keep in mind that if the system is disabled log entries will still accumulate into the Syslog Incoming table as this is defined by the rsyslog or syslog-ng process.', 'syslog'), - 'method' => 'checkbox', - 'default' => 'on' - ], - 'syslog_domains' => [ - 'friendly_name' => __('Strip Domains', 'syslog'), - 'description' => __('A comma delimited list of domains that you wish to remove from the syslog hostname, Examples would be \'mydomain.com, otherdomain.com\'', 'syslog'), - 'method' => 'textbox', - 'default' => '', - 'size' => 80, - 'max_length' => 255, - ], - 'syslog_refresh' => [ - 'friendly_name' => __('Refresh Interval', 'syslog'), - 'description' => __('This is the time in seconds before the page refreshes.', 'syslog'), - 'method' => 'drop_array', - 'default' => '300', - 'array' => $syslog_refresh - ], - 'syslog_maxrecords' => [ - 'friendly_name' => __('Max Report Records', 'syslog'), - 'description' => __('For Threshold based Alerts, what is the maximum number that you wish to show in the report. This is used to limit the size of the html log and Email.', 'syslog'), - 'method' => 'drop_array', - 'default' => '100', - 'array' => [ - 20 => __('%d Records', 20, 'syslog'), - 40 => __('%d Records', 40, 'syslog'), - 60 => __('%d Records', 60, 'syslog'), - 100 => __('%d Records', 100, 'syslog'), - 200 => __('%d Records', 200, 'syslog'), - 400 => __('%d Records', 400, 'syslog') - ] - ], - 'syslog_max_workers' => [ - 'friendly_name' => __('Maximum Syslog Processing Processes', 'syslog'), - 'description' => __('When set above 1, incoming message processing is spread across parallel worker processes. Each worker handles a disjoint range of incoming messages, speeding up hostname resolution and message transfer under heavy loads. A setting of 1 processes all messages in a single process. Worker processes are not supported on Windows or when the PHP pcntl and POSIX extensions are unavailable, in which case processing falls back to a single process.', 'syslog'), - 'method' => 'drop_array', - 'default' => '1', - 'array' => [ - 1 => __('%d Worker Process', 1, 'syslog'), - 2 => __('%d Worker Processes', 2, 'syslog'), - 4 => __('%d Worker Processes', 4, 'syslog'), - 6 => __('%d Worker Processes', 6, 'syslog'), - 8 => __('%d Worker Processes', 8, 'syslog'), - 16 => __('%d Worker Processes', 16, 'syslog') - ] - ], - 'syslog_ticket_command' => [ - 'friendly_name' => __('Command for Opening Tickets', 'syslog'), - 'description' => __('This command will be executed for opening Help Desk Tickets. The command will be required to parse multiple input parameters as follows: --alert-name, --severity, --hostlist, --message. The hostlist will be a comma delimited list of hosts impacted by the alert.', 'syslog'), - 'method' => 'textbox', - 'max_length' => 255, - 'size' => 80 - ], - 'syslog_html_header' => [ - 'friendly_name' => __('Host Discovery Options', 'syslog'), - 'method' => 'spacer', - 'collapsible' => 'true' - ], - 'syslog_resolve_hostname' => [ - 'friendly_name' => __('Enable Hostname Resolution', 'syslog'), - 'description' => __('If this checkbox is set, all hostnames are resolved via DNS lookup first (If enabled). If the DNS lookup fails, the system will attempt to resolve the hostname against the Cacti host table and replace it with the Cacti host description. If both DNS and Cacti lookups fail, records are assigned a prefix \'unresolved-Original_hostname\'.', 'syslog'), - 'method' => 'checkbox', - 'default' => '' - ], - 'syslog_no_dns' => [ - 'friendly_name' => __('Skip DNS Resolution for incoming hosts', 'syslog'), - 'description' => __('If this checkbox is set, the system will not attempt to resolve hosts via DNS lookups. This is useful for environments where DNS resolution is not possible or not desired.', 'syslog'), - 'method' => 'checkbox', - 'default' => '' - ], - 'syslog_html_notification_header' => [ - 'friendly_name' => __('HTML Notification Settings', 'syslog'), - 'method' => 'spacer', - 'collapsible' => 'true' - ], - 'syslog_html' => [ - 'friendly_name' => __('Enable HTML Based Email', 'syslog'), - 'description' => __('If this checkbox is set, all Emails will be sent in HTML format. Otherwise, Emails will be sent in plain text.', 'syslog'), - 'method' => 'checkbox', - 'default' => 'on' - ], - 'syslog_format_file' => [ - 'friendly_name' => __('Format File to Use', 'syslog'), - 'method' => 'drop_array', - 'default' => 'default.format', - 'description' => __('Choose the custom html wrapper and CSS file to use. This file contains both html and CSS to wrap around your report. If it contains more than simply CSS, you need to place a special tag inside of the file. This format tag will be replaced by the report content. These files are located in the \'formats\' directory.', 'syslog'), - 'array' => $formats - ], - 'syslog_retention_header' => [ - 'friendly_name' => __('Data Retention Settings', 'syslog'), - 'method' => 'spacer', - 'collapsible' => 'true' - ], - 'syslog_retention' => [ - 'friendly_name' => __('Syslog Retention', 'syslog'), - 'description' => __('This is the number of days to keep events.', 'syslog'), - 'method' => 'drop_array', - 'default' => '30', - 'array' => $syslog_retentions - ], - 'syslog_partition_ahead_days' => [ - 'friendly_name' => __('Partition Pre-create Window', 'syslog'), - 'description' => __('This is the number of future daily partitions to maintain for partitioned Syslog tables.', 'syslog'), - 'method' => 'drop_array', - 'default' => '3', - 'array' => [ - '1' => __('%d Day', 1, 'syslog'), - '2' => __('%d Days', 2, 'syslog'), - '3' => __('%d Days', 3, 'syslog'), - '4' => __('%d Days', 4, 'syslog'), - '5' => __('%d Days', 5, 'syslog'), - '6' => __('%d Days', 6, 'syslog'), - '7' => __('%d Days', 7, 'syslog') - ] - ], - 'syslog_partition_recover_limit' => [ - 'friendly_name' => __('Partition Recovery Limit', 'syslog'), - 'description' => __('When partitions are missing (for example after maintenance downtime), at most this many missing partitions per table are created on each poller run. The remaining gap heals over subsequent runs; retention pruning stays deferred until the future partition horizon is fully restored. Applies to partitioned Syslog tables only.', 'syslog'), - 'method' => 'drop_array', - 'default' => '3', - 'array' => [ - '1' => __('%d Partition per run', 1, 'syslog'), - '2' => __('%d Partitions per run', 2, 'syslog'), - '3' => __('%d Partitions per run', 3, 'syslog'), - '5' => __('%d Partitions per run', 5, 'syslog'), - '10' => __('%d Partitions per run', 10, 'syslog'), - '31' => __('%d Partitions per run', 31, 'syslog') - ] - ], - 'syslog_stale_threshold' => [ - 'friendly_name' => __('Collector Staleness Threshold', 'syslog'), - 'description' => __('The Syslog Status page shows a warning when no new message has arrived within this many seconds. Set to 0 to use the default of 300 seconds.', 'syslog'), - 'method' => 'drop_array', - 'default' => '300', - 'array' => [ - '0' => __('Default (300 seconds)', 'syslog'), - '120' => __('%d seconds', 120, 'syslog'), - '300' => __('%d seconds', 300, 'syslog'), - '600' => __('%d seconds', 600, 'syslog'), - '1800' => __('%d seconds', 1800, 'syslog'), - '3600' => __('%d seconds', 3600, 'syslog') - ] - ], - 'syslog_backlog_threshold' => [ - 'friendly_name' => __('Collector Backlog Threshold', 'syslog'), - 'description' => __('The Syslog Status page shows a warning when more than this many messages sit unprocessed in the Syslog Incoming table. Set to 0 to use the default of 10000 messages.', 'syslog'), - 'method' => 'drop_array', - 'default' => '10000', - 'array' => [ - '0' => __('Default (10000 messages)', 'syslog'), - '1000' => __('%d Messages', 1000, 'syslog'), - '5000' => __('%d Messages', 5000, 'syslog'), - '10000' => __('%d Messages', 10000, 'syslog'), - '50000' => __('%d Messages', 50000, 'syslog') - ] - ], - 'syslog_alert_retention' => [ - 'friendly_name' => __('Syslog Alert Retention', 'syslog'), - 'description' => __('This is the number of days to keep alert logs.', 'syslog'), - 'method' => 'drop_array', - 'default' => '30', - 'array' => $syslog_alert_retentions - ], - 'syslog_alert_suppression_header' => [ - 'friendly_name' => __('Alert Suppression and Maintenance', 'syslog'), - 'method' => 'spacer', - 'collapsible' => 'true' - ], - 'syslog_alert_maintenance_days' => [ - 'friendly_name' => __('Maintenance Window Days', 'syslog'), - 'description' => __('Days when all alert notifications are muted during the configured maintenance timeframe.', 'syslog'), - 'method' => 'drop_array', - 'array' => syslog_alert_maintenance_day_options(true), - 'default' => '0' - ], - 'syslog_alert_maintenance_start' => [ - 'friendly_name' => __('Maintenance Window Starts', 'syslog'), - 'description' => __('Local start time for the global maintenance window.', 'syslog'), - 'method' => 'drop_array', - 'array' => syslog_alert_maintenance_time_options(), - 'default' => '00:00' - ], - 'syslog_alert_maintenance_end' => [ - 'friendly_name' => __('Maintenance Window Ends', 'syslog'), - 'description' => __('Local end time for the global maintenance window. An earlier end time continues into the next day.', 'syslog'), - 'method' => 'drop_array', - 'array' => syslog_alert_maintenance_time_options(), - 'default' => '00:00' - ], - 'syslog_alert_maintenance_datetime_start' => [ - 'friendly_name' => __('One-time Maintenance Starts', 'syslog'), - 'description' => __('Optional local date and time to start a one-time global maintenance window.', 'syslog'), - 'method' => 'textbox', 'size' => '18', 'max_length' => '16', 'default' => '' - ], - 'syslog_alert_maintenance_datetime_end' => [ - 'friendly_name' => __('One-time Maintenance Ends', 'syslog'), - 'description' => __('Optional local date and time to end a one-time global maintenance window. Format: YYYY-MM-DD HH:MM.', 'syslog'), - 'method' => 'textbox', 'size' => '18', 'max_length' => '16', 'default' => '' - ], - 'syslog_alert_cooldown_minutes' => [ - 'friendly_name' => __('Default Alert Cooldown', 'syslog'), - 'description' => __('Suppress any repeat notification for the same rule and reporting scope for this many minutes. Per-rule values override this setting. Set to 0 to disable.', 'syslog'), - 'method' => 'textbox', - 'size' => '6', - 'max_length' => '6', - 'default' => '0' - ], - 'syslog_alert_deduplication_minutes' => [ - 'friendly_name' => __('Default Duplicate Suppression', 'syslog'), - 'description' => __('Suppress a notification with the same matched message set for this many minutes. Per-rule values override this setting. Set to 0 to disable.', 'syslog'), - 'method' => 'textbox', - 'size' => '6', - 'max_length' => '6', - 'default' => '0' - ], - 'syslog_remote_header' => [ - 'friendly_name' => __('Remote Message Processing', 'syslog'), - 'method' => 'spacer', - ], - 'syslog_remote_enabled' => [ - 'friendly_name' => __('Enable Remote Data Collector Message Processing', 'syslog'), - 'description' => __('If your Remote Data Collectors have their own Syslog databases and process their messages independently, check this checkbox. By checking this Checkbox, your Remote Data Collectors will need to maintain their own \'config_local.php\' file in order to inform Syslog to use an independent database for message display and processing. Please use the template file \'config_local.php.dist\' for this purpose. WARNING: Syslog tables will be automatically created as soon as this option is enabled.', 'syslog'), - 'method' => 'checkbox', - 'default' => '' - ], - 'syslog_remote_sync_rules' => [ - 'friendly_name' => __('Remote Data Collector Rules Sync', 'syslog'), - 'description' => __('If your Remote Data Collectors have their own Syslog databases and process thrie messages independently, check this checkbox if you wish the Main Cacti databases Alerts, Removal and Report rules to be sent to the Remote Cacti System.', 'syslog'), - 'method' => 'checkbox', - 'default' => '' - ], - 'syslog_remote_store_records' => [ - 'friendly_name' => __('Store Records on Remote Collector', 'syslog'), - 'description' => __('Keep processed messages in the remote collector syslog table after they have been delivered to the Main Collector. When disabled, records are retained locally only while the Main Collector is unavailable and are removed after confirmed recovery delivery.', 'syslog'), - 'method' => 'checkbox', - 'default' => '' - ], - 'syslog_replication_recovery_header' => [ - 'friendly_name' => __('Remote Syslog Recovery', 'syslog'), - 'method' => 'spacer', - ], - 'syslog_replication_recovery_records_per_run' => [ - 'friendly_name' => __('Maximum Recovery Records Per Execution', 'syslog'), - 'description' => __('Maximum number of retained remote Syslog events sent to the Main Collector by one recovery worker execution. Each central database transaction remains capped at 100 records. Increase gradually while observing Main Collector load and lock waits.', 'syslog'), - 'method' => 'drop_array', - 'default' => '2000', - 'array' => [ - '500' => __('%d Records', 500, 'syslog'), - '1000' => __('%d Records', 1000, 'syslog'), - '2000' => __('%d Records', 2000, 'syslog'), - '3000' => __('%d Records', 3000, 'syslog'), - '5000' => __('%d Records', 5000, 'syslog'), - '10000' => __('%d Records', 10000, 'syslog') - ] - ], - 'syslog_replication_recovery_batch_delay_ms' => [ - 'friendly_name' => __('Recovery Pause Between Batches', 'syslog'), - 'description' => __('Pause between each 100-record Main Collector delivery transaction. A longer pause lowers Main Collector pressure; a shorter pause speeds backlog convergence.', 'syslog'), - 'method' => 'drop_array', - 'default' => '100', - 'array' => [ - '0' => __('No pause', 'syslog'), - '25' => __('%d milliseconds', 25, 'syslog'), - '50' => __('%d milliseconds', 50, 'syslog'), - '100' => __('%d milliseconds', 100, 'syslog'), - '250' => __('%d milliseconds', 250, 'syslog'), - '500' => __('%d milliseconds', 500, 'syslog'), - '1000' => __('%d second', 1, 'syslog') - ] - ], - ]; - - if (isset($settings['syslog'])) { - $settings['syslog'] = array_merge($settings['syslog'], $temp); - } else { - $settings['syslog'] = $temp; - } -} - -/** Attach Cacti's date-time picker after the Syslog Settings form is drawn. */ -function syslog_settings_bottom(): void { - if (get_nfilter_request_var('tab') !== 'syslog') { - return; - } - ?> - - ' . __('Syslog', 'syslog') . ''; - } else { - print '' . __('Syslog', 'syslog') . ''; - } - } -} - -/** - * Determine which Syslog config file is present, and if it is for a remote database. - * - * @return void - */ -function syslog_determine_config(): void { - global $config; - - // Setup the syslog database settings path - if (!defined('SYSLOG_CONFIG')) { - if (file_exists(__DIR__ . '/config_local.php')) { - define('SYSLOG_CONFIG', __DIR__ . '/config_local.php'); - $config['syslog_remote_db'] = true; - } elseif (file_exists(__DIR__ . '/config.php')) { - define('SYSLOG_CONFIG', __DIR__ . '/config.php'); - $config['syslog_remote_db'] = false; - } - } -} - -/** - * Check that either Syslog config file exists and is readable. - * - * @return bool True when a Syslog config file is available, false otherwise. - */ -function syslog_config_safe(): bool { - global $config; - - $files = [ - __DIR__ . '/config_local.php', - __DIR__ . '/config.php' - ]; - - foreach ($files as $file) { - if (file_exists($file) && is_readable($file)) { - return true; - } - } - - return false; -} - -/** - * Setup the Syslog specific global arrays used throughout the plugin. - * - * @return void - */ -function syslog_config_arrays(): void { - global $syslog_actions, $config, $menu, $message_types, $severities, $messages; - global $syslog_levels, $syslog_facilities, $syslog_freqs, $syslog_times, $syslog_refresh; - global $syslog_retentions, $syslog_alert_retentions, $menu_glyphs; - - $syslog_actions = [ - 1 => __('Delete', 'syslog'), - 2 => __('Disable', 'syslog'), - 3 => __('Enable', 'syslog'), - 4 => __('Export', 'syslog') - ]; - - $syslog_levels = [ - 0 => 'emerg', - 1 => 'crit', - 2 => 'alert', - 3 => 'err', - 4 => 'warn', - 5 => 'notice', - 6 => 'info', - 7 => 'debug', - 8 => 'other' - ]; - - $syslog_facilities = [ - 0 => 'kernel', - 1 => 'user', - 2 => 'mail', - 3 => 'daemon', - 4 => 'auth', - 5 => 'syslog', - 6 => 'lpr', - 7 => 'news', - 8 => 'uucp', - 9 => 'cron', - 10 => 'authpriv', - 11 => 'ftp', - 12 => 'ntp', - 13 => 'log audit', - 14 => 'log alert', - 15 => 'cron', - 16 => 'local0', - 17 => 'local1', - 18 => 'local2', - 19 => 'local3', - 20 => 'local4', - 21 => 'local5', - 22 => 'local6', - 23 => 'local7' - ]; - - $syslog_retentions = [ - '0' => __('Indefinite', 'syslog'), - '1' => __('%d Day', 1, 'syslog'), - '2' => __('%d Days', 2, 'syslog'), - '3' => __('%d Days', 3, 'syslog'), - '4' => __('%d Days', 4, 'syslog'), - '5' => __('%d Days', 5, 'syslog'), - '6' => __('%d Days', 6, 'syslog'), - '7' => __('%d Week', 1, 'syslog'), - '14' => __('%d Weeks', 2, 'syslog'), - '30' => __('%d Month', 1, 'syslog'), - '60' => __('%d Months', 2, 'syslog'), - '90' => __('%d Months', 3, 'syslog'), - '120' => __('%d Months', 4, 'syslog'), - '160' => __('%d Months', 5, 'syslog'), - '183' => __('%d Months', 6, 'syslog'), - '365' => __('%d Year', 1, 'syslog') - ]; - - $syslog_alert_retentions = [ - '0' => __('Indefinite', 'syslog'), - '1' => __('%d Day', 1, 'syslog'), - '2' => __('%d Days', 2, 'syslog'), - '3' => __('%d Days', 3, 'syslog'), - '4' => __('%d Days', 4, 'syslog'), - '5' => __('%d Days', 5, 'syslog'), - '6' => __('%d Days', 6, 'syslog'), - '7' => __('%d Week', 1, 'syslog'), - '14' => __('%d Weeks', 2, 'syslog'), - '30' => __('%d Month', 1, 'syslog'), - '60' => __('%d Months', 2, 'syslog'), - '90' => __('%d Months', 3, 'syslog'), - '120' => __('%d Months', 4, 'syslog'), - '160' => __('%d Months', 5, 'syslog'), - '183' => __('%d Months', 6, 'syslog'), - '365' => __('%d Year', 1, 'syslog') - ]; - - $syslog_refresh = [ - 9999999 => __('Never', 'syslog'), - '60' => __('%d Minute', 1, 'syslog'), - '120' => __('%d Minutes', 2, 'syslog'), - '300' => __('%d Minutes', 5, 'syslog'), - '600' => __('%d Minutes', 10, 'syslog') - ]; - - $severities = [ - '0' => __('Notice', 'syslog'), - '1' => __('Warning', 'syslog'), - '2' => __('Critical', 'syslog') - ]; - - $message_types = [ - 'messageb' => __('Begins with', 'syslog'), - 'messagec' => __('Contains', 'syslog'), - 'messagee' => __('Ends with', 'syslog'), - 'host' => __('Hostname is', 'syslog'), - 'program' => __('Program is', 'syslog'), - 'facility' => __('Facility is', 'syslog'), - 'filter' => __('Filter Builder', 'syslog'), - 'sql' => __('SQL Expression', 'syslog') - ]; - - $syslog_freqs = [ - '86400' => __('Daily', 'syslog'), - '604800' => __('Weekly', 'syslog') - ]; - - for ($i = 0; $i <= 86400; $i += 1800) { - $minute = $i % 3600; - - if ($minute > 0) { - $minute = '30'; - } else { - $minute = '00'; - } - - if ($i > 0) { - $hour = strrev(substr(strrev('00' . intval($i / 3600)),0,2)); - } else { - $hour = '00'; - } - - $syslog_times[$i] = $hour . ':' . $minute; - } - - if (syslog_config_safe()) { - $menu2 = []; - - foreach ($menu as $temp => $temp2) { - $menu2[$temp] = $temp2; - - if ($temp == __('Import/Export')) { - $menu2[__('Syslog Settings', 'syslog')]['plugins/syslog/syslog_alerts.php'] = __('Alert Rules', 'syslog'); - $menu2[__('Syslog Settings', 'syslog')]['plugins/syslog/syslog_device_rules.php'] = __('Device Alert Rules', 'syslog'); - $menu2[__('Syslog Settings', 'syslog')]['plugins/syslog/syslog_removal.php'] = __('Removal Rules', 'syslog'); - $menu2[__('Syslog Settings', 'syslog')]['plugins/syslog/syslog_reports.php'] = __('Report Rules', 'syslog'); - $menu2[__('Syslog Settings', 'syslog')]['plugins/syslog/syslog_saved_searches.php'] = __('Saved Search Templates', 'syslog'); - $menu2[__('Syslog Settings', 'syslog')]['plugins/syslog/syslog_dashboards.php'] = __('Dashboards', 'syslog'); - } - } - $menu = $menu2; - - $menu_glyphs[__('Syslog Settings', 'syslog')] = 'fa fa-life-ring'; - } - - syslog_refresh_permission_roles(); - - if (isset($_SESSION['syslog_info']) && $_SESSION['syslog_info'] != '') { - $messages['syslog_info'] = ['message' => $_SESSION['syslog_info'], 'type' => 'info']; - } - - if (isset($_SESSION['syslog_error']) && $_SESSION['syslog_error'] != '') { - $messages['syslog_error'] = ['message' => $_SESSION['syslog_error'], 'type' => 'error']; - } -} - -/** - * Add the Syslog pages to the Cacti navigation structure. - * - * @param array> $nav The navigation array provided by Cacti. - * - * @return array> The navigation array with the Syslog pages added. - */ -function syslog_draw_navigation_text($nav) { - global $config; - - $nav['syslog.php:'] = ['title' => __('Syslog', 'syslog'), 'mapping' => '', 'url' => $config['url_path'] . 'plugins/syslog/syslog.php', 'level' => '1']; - $nav['syslog_removal.php:'] = ['title' => __('Syslog Removals', 'syslog'), 'mapping' => 'index.php:', 'url' => $config['url_path'] . 'plugins/syslog/syslog_removal.php', 'level' => '1']; - $nav['syslog_removal.php:edit'] = ['title' => __('(Edit)', 'syslog'), 'mapping' => 'index.php:,syslog_removal.php:', 'url' => 'syslog_removal.php', 'level' => '2']; - $nav['syslog_removal.php:newedit'] = ['title' => __('(Edit)', 'syslog'), 'mapping' => 'index.php:,syslog_removal.php:', 'url' => 'syslog_removal.php', 'level' => '2']; - $nav['syslog_removal.php:actions'] = ['title' => __('(Actions)', 'syslog'), 'mapping' => 'index.php:,syslog_removal.php:', 'url' => 'syslog_removal.php', 'level' => '2']; - - $nav['syslog_alerts.php:'] = ['title' => __('Syslog Alerts', 'syslog'), 'mapping' => 'index.php:', 'url' => $config['url_path'] . 'plugins/syslog/syslog_alerts.php', 'level' => '1']; - $nav['syslog_alerts.php:edit'] = ['title' => __('(Edit)', 'syslog'), 'mapping' => 'index.php:,syslog_alerts.php:', 'url' => 'syslog_alerts.php', 'level' => '2']; - $nav['syslog_alerts.php:newedit'] = ['title' => __('(Edit)', 'syslog'), 'mapping' => 'index.php:,syslog_alerts.php:', 'url' => 'syslog_alerts.php', 'level' => '2']; - $nav['syslog_alerts.php:actions'] = ['title' => __('(Actions)', 'syslog'), 'mapping' => 'index.php:,syslog_alerts.php:', 'url' => 'syslog_alerts.php', 'level' => '2']; - - $nav['syslog_reports.php:'] = ['title' => __('Syslog Reports', 'syslog'), 'mapping' => 'index.php:', 'url' => $config['url_path'] . 'plugins/syslog/syslog_reports.php', 'level' => '1']; - $nav['syslog_reports.php:edit'] = ['title' => __('(Edit)', 'syslog'), 'mapping' => 'index.php:,syslog_reports.php:', 'url' => 'syslog_reports.php', 'level' => '2']; - $nav['syslog_reports.php:actions'] = ['title' => __('(Actions)', 'syslog'), 'mapping' => 'index.php:,syslog_reports.php:', 'url' => 'syslog_reports.php', 'level' => '2']; - $nav['syslog_saved_searches.php:'] = ['title' => __('Saved Search Templates', 'syslog'), 'mapping' => 'index.php:', 'url' => $config['url_path'] . 'plugins/syslog/syslog_saved_searches.php', 'level' => '1']; - $nav['syslog_dashboards.php:'] = ['title' => __('Dashboards', 'syslog'), 'mapping' => 'index.php:', 'url' => $config['url_path'] . 'plugins/syslog/syslog_dashboards.php', 'level' => '1']; - $nav['syslog.php:actions'] = ['title' => __('Syslog', 'syslog'), 'mapping' => '', 'url' => $config['url_path'] . 'plugins/syslog/syslog.php', 'level' => '1']; - - return $nav; -} - -/** - * Upgrade the Syslog database when the plugin config is loaded. - * - * @return void - */ -function syslog_config_insert(): void { - if (!syslog_config_safe()) { - return; - } - - syslog_connect(); - - syslog_check_upgrade(); -} - -/** - * Display a link to the Syslog page filtered for the graphed device. - * - * @param array> $graph_elements The graph elements provided by Cacti. - * - * @return void - */ -function syslog_graph_buttons($graph_elements = []): void { - global $config, $timespan, $graph_timeshifts; - - if (!syslog_config_safe()) { - return; - } - - syslog_connect(); - - if (get_nfilter_request_var('action') == 'view') { - return; - } - - if (get_current_page() == 'graph_view.php') { - if (isset_request_var('graph_end') && strlen(get_filter_request_var('graph_end'))) { - $date1 = date('Y-m-d H:i:s', get_filter_request_var('graph_start')); - $date2 = date('Y-m-d H:i:s', get_filter_request_var('graph_end')); - } else { - $date1 = $timespan['current_value_date1']; - $date2 = $timespan['current_value_date2']; - } - } else { - return; - } - - if (isset($graph_elements[1]['local_graph_id'])) { - $host_id = db_fetch_cell_prepared('SELECT host_id - FROM graph_local - WHERE id = ?', - [$graph_elements[1]['local_graph_id']]); - - $sql_where = ''; - $sql_params = []; - - if (!empty($host_id)) { - $host = db_fetch_row_prepared('SELECT id, description, hostname - FROM host WHERE id = ?', - [$host_id]); - - if (is_array($host) && cacti_sizeof($host)) { - if (!is_ipaddress($host['description'])) { - $parts = explode('.', $host['description']); - - $sql_where = 'WHERE host LIKE ? OR host = ?'; - - $sql_params[] = $parts[0] . '.%'; - $sql_params[] = $host['description']; - } else { - $sql_where = 'WHERE host = ?'; - - $sql_params[] = $host['description']; - } - - if (!is_ipaddress($host['hostname'])) { - $parts = explode('.', $host['hostname']); - - $sql_where .= ($sql_where != '' ? ' OR ' : 'WHERE ') . 'host LIKE ? OR host = ?'; - - $sql_params[] = $parts[0] . '.%'; - $sql_params[] = $host['hostname']; - } else { - $sql_where .= ($sql_where != '' ? ' OR ' : 'WHERE ') . 'host = ?'; - - $sql_params[] = $host['hostname']; - } - - if ($sql_where != '') { - $host_id = syslog_db_fetch_cell_prepared('SELECT host_id FROM syslog_hosts ' . $sql_where, $sql_params); - - if ($host_id) { - $url = $config['url_path'] . 'plugins/syslog/syslog.php?tab=syslog&reset=1&host=' . $host_id . '&date1=' . $date1 . '&date2=' . $date2; - - print "
"; - } - } - } - } - } -} - -/** - * Handle the Syslog utilities actions. - * - * @param string $action The action requested from the utilities page. - * - * @return string|void The action, unmodified when it is not a Syslog action, or - * nothing when the Syslog config file is not available. - */ -function syslog_utilities_action($action) { - global $config, $refresh; - - if (!syslog_config_safe()) { - return; - } - - if ($action === 'purge_syslog_hosts') { - if ($_SERVER['REQUEST_METHOD'] !== 'POST') { - cacti_log('WARNING: syslog purge blocked -- non-POST request', false, 'SYSLOG'); - raise_message('syslog_method_error', __('Invalid request. Please try again.', 'syslog'), MESSAGE_LEVEL_ERROR); - header('Location: utilities.php?header=false'); - exit; - } - - // csrf_check($fatal) returns bool; $fatal=false tells the helper not to - // die/exit on failure so we can log and redirect with a user-visible - // message ourselves. - if (!function_exists('csrf_check')) { - cacti_log('WARNING: syslog purge blocked -- CSRF validation unavailable', false, 'SYSLOG'); - raise_message('syslog_csrf_unavailable', __('Invalid request. Please try again.', 'syslog'), MESSAGE_LEVEL_ERROR); - header('Location: utilities.php?header=false'); - exit; - } - - if (!csrf_check(false)) { - cacti_log('WARNING: syslog purge blocked -- CSRF token validation failed', false, 'SYSLOG'); - raise_message('syslog_csrf_error', __('Invalid request. Please try again.', 'syslog'), MESSAGE_LEVEL_ERROR); - header('Location: utilities.php?header=false'); - exit; - } - - $records = 0; - - syslog_db_execute('DELETE FROM syslog_hosts - WHERE host_id NOT IN ( - SELECT DISTINCT host_id - FROM syslog - UNION - SELECT DISTINCT host_id - FROM syslog_removed - )'); - $records += syslog_db_affected_rows(); - - syslog_db_execute('DELETE FROM syslog_host_facilities - WHERE host_id NOT IN ( - SELECT DISTINCT host_id - FROM syslog - UNION - SELECT DISTINCT host_id - FROM syslog_removed - )'); - $records += syslog_db_affected_rows(); - - raise_message('syslog_info', __('There were %s Device records removed from the Syslog database', $records, 'syslog'), MESSAGE_LEVEL_INFO); - - header('Location: utilities.php?header=false'); - exit; - } - - return $action; -} - -/** - * Add the Syslog purge utility to the utilities page. - * - * @return void - */ -function syslog_utilities_list(): void { - global $config; - - if (!syslog_config_safe()) { - return; - } - - html_header([__('Syslog Utilities', 'syslog')], 2); ?> - - - - '> - - - - - - - - $status The status values. - * - * @return string The formatted runtime statistics. - */ -function syslog_status_format_runtime_stats(array $status): string { - if ($status['polling_runtime_last'] === '' || !is_numeric($status['polling_runtime_last'])) { - return __('Never', 'syslog'); - } - - return sprintf( - '%s (%s / %s / %s %s)', - syslog_status_format_seconds($status['polling_runtime_last']), - number_format((float) $status['polling_runtime_min'], 3), - number_format((float) $status['polling_runtime_avg'], 3), - number_format((float) $status['polling_runtime_max'], 3), - __('min/avg/max', 'syslog') - ); -} - /** * Format a counter for the status tab, or '0' when it is unset. * @@ -568,17 +544,6 @@ function syslog_status_format_age(int $seconds): string { return implode(' ', array_slice($parts, 0, 2)); } -/** - * Format an age value for the collector health display. - * - * @param int $seconds The age in seconds. - * - * @return string The formatted age. - */ -function syslog_status_format_age_value(int $seconds): string { - return syslog_status_format_age($seconds); -} - /** * Read live storage metrics only when rendering the status tab. * @@ -2089,33 +2054,6 @@ function syslog_filter(string $sql_where, string $tab): void { $sandbox . '/setup.php', $root . '/includes/functions.php' => $sandbox . '/includes/functions.php', + $root . '/includes/schema.php' => $sandbox . '/includes/schema.php', + $root . '/includes/processing.php' => $sandbox . '/includes/processing.php', + $root . '/includes/settings.php' => $sandbox . '/includes/settings.php', + $root . '/includes/navigation.php' => $sandbox . '/includes/navigation.php', + $root . '/includes/installer.php' => $sandbox . '/includes/installer.php', + $root . '/includes/utilities.php' => $sandbox . '/includes/utilities.php', ]; foreach ($staged as $src => $dst) { @@ -122,7 +130,7 @@ function csrf_check($fatal = true) { } require_once __DIR__ . '/includes/functions.php'; - require_once __DIR__ . '/setup.php'; + require_once __DIR__ . '/includes/utilities.php'; switch ($scenario) { case 'render': diff --git a/tests/Security/IncludePathNormalizationTest.php b/tests/Security/IncludePathNormalizationTest.php index c621fa25..fa0492dd 100644 --- a/tests/Security/IncludePathNormalizationTest.php +++ b/tests/Security/IncludePathNormalizationTest.php @@ -8,11 +8,8 @@ */ /* - * Regression coverage for the include-path hardening: every plugin - * entrypoint must include functions.php/database.php via an __DIR__-based - * path (or the chdir('../../') CWD-relative style paired with the chdir - * call), never a bare CWD-relative include that silently depends on the - * working directory the script happened to be launched from. + * Entrypoints must load setup.php through a CWD-independent path. + * syslog_connect() loads functions.php and database.php from setup.php's directory. */ it('includes plugin sources via a CWD-independent path from every entrypoint', function () { @@ -23,6 +20,7 @@ 'syslog_alerts.php', 'syslog_removal.php', 'syslog_batch_transfer.php', + 'syslog_device_rules.php', 'syslog_reports.php', 'syslog_process.php', ]; @@ -39,11 +37,6 @@ } } - // setup.php is not part of the standard per-entrypoint include chain; it is - // pulled in on demand via $config['base_path'] where a runtime setup step is - // actually needed, so only functions.php and database.php are required here. - $required_includes = ['includes/functions.php', 'includes/database.php']; - foreach ($entrypoints as $file) { $path = $root . '/' . $file; @@ -57,22 +50,8 @@ throw new RuntimeException("Failed to read $file"); } - foreach ($required_includes as $inc) { - // Entrypoints use one of two equivalent styles: __DIR__-based (no - // chdir) or chdir('../../') plus a CWD-relative plugin path. Either - // resolves to the same file; only require that one of them is used. - $dir_pattern = '/include_once\s*\(\s*__DIR__\s*\.\s*[\'"]\/\s*' . preg_quote($inc, '/') . '[\'"]\s*\)/'; - $cwd_pattern = '/include_once\s*\(\s*[\'"]\.\/plugins\/syslog\/' . preg_quote($inc, '/') . '[\'"]\s*\)/'; - - if (!preg_match($dir_pattern, $content) && !preg_match($cwd_pattern, $content)) { - throw new RuntimeException("$file must include $inc via __DIR__ or the chdir('../../') CWD-relative style"); - } - - // The CWD-relative style is only safe when paired with a chdir to - // the Cacti base path; without it the include is CWD-dependent. - if (preg_match($cwd_pattern, $content) && !preg_match('/chdir\s*\(\s*[\'"]\.\.\/\.\.\/[\'"]\s*\)/', $content)) { - throw new RuntimeException("$file uses CWD-relative include for $inc without chdir('../../')"); - } + if (!str_contains($content, "include_once(__DIR__ . '/setup.php');")) { + throw new RuntimeException("$file must load setup.php through __DIR__"); } } @@ -83,15 +62,17 @@ } $expected_functions = [ - 'plugin_syslog_install', - 'plugin_syslog_check_config', - 'syslog_connect', - 'syslog_determine_config', + 'plugin_syslog_install' => 'setup.php', + 'plugin_syslog_check_config' => 'setup.php', + 'syslog_connect' => 'setup.php', + 'syslog_determine_config' => 'setup.php', ]; - foreach ($expected_functions as $func) { - if (!preg_match('/function\s+' . preg_quote($func, '/') . '\s*\(/', $setup)) { - throw new RuntimeException("setup.php missing expected function: $func"); + foreach ($expected_functions as $func => $owner) { + $source = plugin_test_read_source($owner); + + if (!preg_match('/function\s+' . preg_quote($func, '/') . '\s*\(/', $source)) { + throw new RuntimeException("$owner missing expected function: $func"); } } @@ -110,12 +91,12 @@ throw new RuntimeException('functions.php missing syslog_apply_selected_items_action'); } - if (!preg_match('/include_once\s*\(\s*__DIR__\s*\.\s*[\'"]\/includes\/functions\.php[\'"]\s*\)/', $setup)) { - throw new RuntimeException('setup.php must use __DIR__ for the includes/functions.php include'); + if (!preg_match('/require_once\s*\(\s*__DIR__\s*\.\s*[\'"]\/includes\/functions\.php[\'"]\s*\)/', $setup)) { + throw new RuntimeException('setup.php must use __DIR__ for the includes/functions.php require'); } - if (!preg_match('/include_once\s*\(\s*__DIR__\s*\.\s*[\'"]\/includes\/database\.php[\'"]\s*\)/', $setup)) { - throw new RuntimeException('setup.php must use __DIR__ for the includes/database.php include'); + if (!preg_match('/require_once\s*\(\s*__DIR__\s*\.\s*[\'"]\/includes\/database\.php[\'"]\s*\)/', $setup)) { + throw new RuntimeException('setup.php must use __DIR__ for the includes/database.php require'); } expect(true)->toBeTrue(); diff --git a/tests/Security/ItemShareVisibilityTest.php b/tests/Security/ItemShareVisibilityTest.php index c2faaaee..747bc0e7 100644 --- a/tests/Security/ItemShareVisibilityTest.php +++ b/tests/Security/ItemShareVisibilityTest.php @@ -14,6 +14,24 @@ * must stay closed for anything that was never granted. */ +it('repairs missing share tables even when no version upgrade is pending', function () { + syslog_load_plugin_source('includes/schema.php'); + $GLOBALS['syslogdb_default'] = 'cacti'; + $created = []; + + test_override('syslog_db_table_exists', fn ($table) => $table === 'syslog_dashboards_perm'); + test_override('syslog_db_execute', function ($sql) use (&$created) { $created[] = $sql; return true; }); + + syslog_ensure_share_tables(); + + expect($created)->toHaveCount(1) + ->and($created[0])->toContain('CREATE TABLE IF NOT EXISTS `cacti`.`syslog_saved_searches_perm`'); + + $setup = plugin_test_read_source('setup.php'); + expect(strpos($setup, 'syslog_ensure_share_tables();')) + ->toBeLessThan(strpos($setup, "api_plugin_upgrade_register('syslog')")); +}); + it('resolves shared ids from user and group grants', function () { syslog_load_plugin_source('includes/functions.php'); diff --git a/tests/Security/MariadbDetectionStrictTest.php b/tests/Security/MariadbDetectionStrictTest.php index f6dfa730..903dbbe6 100644 --- a/tests/Security/MariadbDetectionStrictTest.php +++ b/tests/Security/MariadbDetectionStrictTest.php @@ -14,16 +14,16 @@ */ it('uses a strict comparison for MariaDB detection', function () { - $setup = plugin_test_read_source('setup.php'); + $schema = plugin_test_read_source('includes/schema.php'); - $legacyPattern = '/stripos\s*\(\s*\$database\s*\[\s*[\'"]{1}Value[\'"]{1}\s*\]\s*,\s*[\'"]{1}mariadb[\'"]{1}\s*\)\s*==\s*false/'; - $fixedPattern = '/stripos\s*\(\s*\$database\s*\[\s*[\'"]{1}Value[\'"]{1}\s*\]\s*,\s*[\'"]{1}mariadb[\'"]{1}\s*\)\s*===\s*false/'; + $legacyPattern = '/stripos\s*\(\s*\$version\s*,\s*[\'"]{1}mariadb[\'"]{1}\s*\)\s*==\s*false/'; + $fixedPattern = '/stripos\s*\(\s*\$version\s*,\s*[\'"]{1}mariadb[\'"]{1}\s*\)\s*===\s*false/'; - if (preg_match($legacyPattern, $setup)) { + if (preg_match($legacyPattern, $schema)) { throw new RuntimeException('Legacy loose MariaDB stripos comparison is still present.'); } - if (!preg_match($fixedPattern, $setup)) { + if (!preg_match($fixedPattern, $schema)) { throw new RuntimeException('Strict MariaDB stripos comparison is missing.'); } diff --git a/tests/Security/RemoteCollectorInstallOptionsTest.php b/tests/Security/RemoteCollectorInstallOptionsTest.php new file mode 100644 index 00000000..64d772e6 --- /dev/null +++ b/tests/Security/RemoteCollectorInstallOptionsTest.php @@ -0,0 +1,103 @@ + 'on']))->toBeTrue(); + expect(syslog_install_storage_engine_is_compatible('Aria', [$option => 'on']))->toBeFalse(); + expect(syslog_install_storage_engine('Aria', [$option => 'on']))->toBe('InnoDB'); + } + + expect(syslog_remote_collector_requires_innodb([ + 'syslog_remote_enabled' => '', + 'syslog_remote_sync_rules' => '', + 'syslog_remote_store_records' => '' + ]))->toBeFalse(); + + $settings['syslog_remote_enabled'] = 'on'; + expect(syslog_install_storage_engine_is_compatible('Aria'))->toBeFalse(); + expect(syslog_install_storage_engine_is_compatible('InnoDB'))->toBeTrue(); + expect(syslog_install_storage_engine_is_compatible('Aria', ['syslog_remote_enabled' => '']))->toBeTrue(); + expect(syslog_install_storage_engine('Aria'))->toBe('InnoDB'); + expect(syslog_install_storage_engine('Aria', ['syslog_remote_enabled' => '']))->toBe('Aria'); +}); + +it('normalizes transactional remote tables to InnoDB before replication runs', function () { + $GLOBALS['config'] = ['poller_id' => 2]; + $GLOBALS['syslogdb_default'] = 'remote_syslog'; + $calls = []; + + test_override('read_config_option', fn ($name) => $name === 'syslog_remote_enabled' ? 'on' : ''); + test_override('syslog_db_fetch_cell_prepared', fn ($sql, $params) => $params[1] === 'syslog' ? 'Aria' : 'InnoDB'); + test_override('syslog_db_execute', function ($sql) use (&$calls) { + $calls[] = $sql; + + return true; + }); + syslog_load_plugin_source('includes/schema.php'); + + syslog_ensure_replication_storage_engine(); + + expect($calls)->toHaveCount(1) + ->and($calls[0])->toContain('ALTER TABLE `remote_syslog`.`syslog` ENGINE=InnoDB'); +}); + +it('includes the Remote Data Collector options in the install advisor and saves them', function () { + $installer = plugin_test_read_source('includes/installer.php'); + $setup = plugin_test_read_function_source('syslog_setup_table_new'); + $update = plugin_test_read_source('setup.php'); + + foreach (['syslog_remote_enabled', 'syslog_remote_sync_rules', 'syslog_remote_store_records'] as $option) { + expect(strpos($installer, "'" . $option . "' => ["))->not->toBeFalse(); + expect(strpos($update, "set_config_option(\$option"))->not->toBeFalse(); + } + + expect(strpos($setup, 'syslog_install_storage_engine('))->not->toBeFalse(); + $compatibility_check = strpos($update, 'if (!syslog_install_storage_engine_is_compatible('); + $settings_save = strpos($update, 'set_config_option($option', $compatibility_check); + $table_setup = strpos($update, 'syslog_setup_table_new($options)', $compatibility_check); + + expect($compatibility_check)->not->toBeFalse(); + expect($settings_save)->toBeGreaterThan($compatibility_check); + expect($table_setup)->toBeGreaterThan($compatibility_check); + expect(strpos($installer, "'config' => ['no_form_tag' => true]"))->not->toBeFalse(); + expect(strpos($installer, "option[value=\"aria\"]').detach()"))->not->toBeFalse(); + expect(strpos($installer, "engine.val('innodb')"))->not->toBeFalse(); + expect(strpos($installer, "event.stopImmediatePropagation()"))->not->toBeFalse(); + expect(strpos($installer, "color:#000;font-weight:bold;"))->not->toBeFalse(); + expect(strpos($installer, 'syslog_engine_compatibility_notice'))->not->toBeFalse(); + + $notice = strpos($installer, 'syslog_engine_compatibility_notice'); + $settings_box = strpos($installer, "html_start_box(__('Syslog %s Settings'"); + expect($notice)->toBeLessThan($settings_box); + expect($setup)->toContain('syslog_replication_output`'); + expect($setup)->toContain('syslog_replication_recovery`'); +}); diff --git a/tests/Security/ReplicationCreateSqlTest.php b/tests/Security/ReplicationCreateSqlTest.php index 683eafe1..c7699e64 100644 --- a/tests/Security/ReplicationCreateSqlTest.php +++ b/tests/Security/ReplicationCreateSqlTest.php @@ -43,7 +43,7 @@ $GLOBALS['issue258_logs'][] = $message; }); - syslog_load_plugin_source('setup.php'); + syslog_load_plugin_source('includes/processing.php'); $data = [ ['id' => 1, 'hash' => 'abc123', 'name' => 'sample'], diff --git a/tests/Security/TraditionalTableDeprecationTest.php b/tests/Security/TraditionalTableDeprecationTest.php index 4739ee20..bbf9a92e 100644 --- a/tests/Security/TraditionalTableDeprecationTest.php +++ b/tests/Security/TraditionalTableDeprecationTest.php @@ -53,10 +53,10 @@ }); it('always creates partitioned tables regardless of legacy options', function () { - $setup = plugin_test_read_source('setup.php'); + $setup = plugin_test_read_function_source('syslog_setup_table_new'); if (!preg_match('/function\s+syslog_setup_table_new\s*\(.*?\n\}/s', $setup, $m)) { - throw new RuntimeException('Could not extract syslog_setup_table_new from setup.php'); + throw new RuntimeException('Could not extract syslog_setup_table_new from its declared source'); } $function = $m[0]; @@ -70,16 +70,16 @@ expect(strpos($function, 'syslog_create_partitioned_syslog_table(')) ->not->toBeFalse('Partitioned table creation is missing from syslog_setup_table_new'); - // The engine must flow through the validation helper. - expect(strpos($function, 'syslog_validate_storage_engine(')) - ->not->toBeFalse('Storage engine validation is missing from syslog_setup_table_new'); + // The engine must flow through the install policy helper. + expect(strpos($function, 'syslog_install_storage_engine(')) + ->not->toBeFalse('Storage engine policy is missing from syslog_setup_table_new'); }); it('coerces a legacy trad db_type setting to partitioned', function () { - $setup = plugin_test_read_source('setup.php'); + $setup = plugin_test_read_function_source('syslog_setup_table_new'); if (!preg_match('/function\s+syslog_setup_table_new\s*\(.*?\n\}/s', $setup, $m)) { - throw new RuntimeException('Could not extract syslog_setup_table_new from setup.php'); + throw new RuntimeException('Could not extract syslog_setup_table_new from its declared source'); } $function = $m[0]; @@ -250,4 +250,4 @@ expect(syslog_notice_traditional_tables(true))->toBeFalse('A missing table must not raise the notice'); expect($calls['logs'])->toHaveCount(0, 'No log line is expected when the table is missing'); expect($calls['messages'])->toHaveCount(0, 'No UI message is expected when the table is missing'); -}); \ No newline at end of file +}); diff --git a/tests/Unit/CollectorHealthTest.php b/tests/Unit/CollectorHealthTest.php index 344f3b0c..05f0b830 100644 --- a/tests/Unit/CollectorHealthTest.php +++ b/tests/Unit/CollectorHealthTest.php @@ -48,7 +48,7 @@ function collector_health_load_functions(): void { return; } - foreach (['syslog_status_collector_health', 'syslog_status_collector_unavailable', 'syslog_status_format_age', 'syslog_status_format_age_value', 'syslog_status_format_count'] as $name) { + foreach (['syslog_status_collector_health', 'syslog_status_collector_unavailable', 'syslog_status_format_age', 'syslog_status_format_count'] as $name) { $code = collector_health_extract_function($name); if (!function_exists($name)) { @@ -267,4 +267,4 @@ function collector_health_capture_db(array &$calls, array $overrides = []): void expect(syslog_status_format_age(90))->toContain('minute'); expect(syslog_status_format_age(5400))->toContain('hour'); expect(syslog_status_format_age(90000))->toContain('day'); -}); \ No newline at end of file +}); diff --git a/tests/Unit/DeviceRulePolicyTest.php b/tests/Unit/DeviceRulePolicyTest.php index 8987e3eb..6da9c88d 100644 --- a/tests/Unit/DeviceRulePolicyTest.php +++ b/tests/Unit/DeviceRulePolicyTest.php @@ -43,9 +43,14 @@ }); it('creates and replicates device rules for remote collectors', function () { - $setup = plugin_test_read_source('setup.php'); + $schema = plugin_test_read_source('includes/schema.php'); + $processing = plugin_test_read_source('includes/processing.php'); + $setup = plugin_test_read_source('setup.php'); - expect($setup)->toContain('CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_device_rule`') + expect($schema)->toContain('CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_device_rule`'); + expect($setup)->toContain('syslog_create_device_rule_table();'); + expect($processing) ->toContain("replicate_out_table(\$rcnn_id, \$tdata, 'syslog_device_rule', \$remote_poller_id)") - ->toContain('syslog_create_device_rule_table();'); + ->toContain("db_fetch_assoc('SELECT * FROM syslog_device_rule')") + ->toContain("syslog_replace_data('syslog_device_rule', \$data)"); }); diff --git a/tests/Unit/MessageColumnTextTest.php b/tests/Unit/MessageColumnTextTest.php new file mode 100644 index 00000000..2b298240 --- /dev/null +++ b/tests/Unit/MessageColumnTextTest.php @@ -0,0 +1,23 @@ +not->toMatch('/message\s+varchar\(2048\)/i') + ->not->toMatch('/TEXT\s+NOT\s+NULL\s+DEFAULT\s+[\'\"]{1}[\'\"]{1}/i') + ->toContain("'syslog_incoming' => 'TEXT NOT NULL'") + ->toContain("'syslog_reports' => 'TEXT DEFAULT NULL'") + ->toContain("MODIFY COLUMN `message` \$definition") + ->toContain('message TEXT NOT NULL') + ->toContain('message TEXT default NULL'); + + expect($setup)->toContain('syslog_ensure_message_text();'); +}); diff --git a/tests/Unit/PartitionPrecreateBoundaryTest.php b/tests/Unit/PartitionPrecreateBoundaryTest.php index 05944126..cbb17226 100644 --- a/tests/Unit/PartitionPrecreateBoundaryTest.php +++ b/tests/Unit/PartitionPrecreateBoundaryTest.php @@ -22,10 +22,10 @@ function partition_precreate_extract_function() { syslog_load_plugin_source('includes/functions.php'); - $setup = plugin_test_read_source('setup.php'); + $setup = plugin_test_read_function_source('syslog_create_partitioned_syslog_table'); if (!preg_match('/function\s+syslog_create_partitioned_syslog_table\s*\(.*?\n\}/s', $setup, $m)) { - throw new RuntimeException('Could not extract syslog_create_partitioned_syslog_table from setup.php'); + throw new RuntimeException('Could not extract syslog_create_partitioned_syslog_table from its declared source'); } $source = $m[0]; diff --git a/tests/Unit/ReplicationDeliveryTest.php b/tests/Unit/ReplicationDeliveryTest.php index 4d687996..42599680 100644 --- a/tests/Unit/ReplicationDeliveryTest.php +++ b/tests/Unit/ReplicationDeliveryTest.php @@ -77,31 +77,22 @@ }); it('removes only temporary remote syslog copies after confirmed central delivery', function () { - unset($GLOBALS['syslog_replication_connection_failed']); - $GLOBALS['config'] = ['poller_id' => 2, 'connection' => 'online']; - $GLOBALS['database_default'] = 'main_syslog'; - $GLOBALS['remote_db_cnn_id'] = new stdClass(); $GLOBALS['syslog_cnn'] = new stdClass(); $GLOBALS['syslogdb_default'] = 'remote_syslog'; $calls = []; - test_override('read_config_option', fn ($name) => $name === 'syslog_remote_enabled' ? 'on' : ''); - test_override('syslog_db_fetch_assoc', fn () => [[ - 'source_poller_id' => 2, 'source_event_id' => 100, 'facility_id' => 16, - 'priority_id' => 6, 'program' => 'app', 'logtime_epoch' => 1758801600, - 'host' => 'remote-host', 'message' => 'hello', 'disposition' => 'syslog', - ]]); - test_override('db_execute', function ($sql) use (&$calls) { $calls[] = $sql; return true; }); - test_override('db_execute_prepared', function ($sql) use (&$calls) { $calls[] = $sql; return true; }); + test_override('read_config_option', fn () => ''); test_override('syslog_db_execute_prepared', function ($sql) use (&$calls) { $calls[] = $sql; return true; }); - test_override('db_affected_rows', fn () => 1); syslog_load_plugin_source('includes/functions.php'); - expect(syslog_replication_deliver_online())->toBe(1) + expect(syslog_replication_cleanup_local_history([[ + 'source_poller_id' => 2, 'source_event_id' => 100, 'facility_id' => 16, + 'priority_id' => 6, 'program' => 'app', 'logtime_epoch' => 1758801600, + 'host' => 'remote-host', 'message' => 'hello', 'disposition' => 'syslog', + ]]))->toBeTrue() ->and(implode("\n", $calls))->toContain('DELETE FROM `remote_syslog`.`syslog` WHERE') - ->toContain('replication_source_event_id') - ->toContain('DELETE FROM `remote_syslog`.`syslog_replication_output`'); + ->toContain('replication_source_event_id'); }); it('keeps remote records locally only while Main Collector delivery is unavailable by default', function () { @@ -230,7 +221,7 @@ it('uses an atomic, token-scoped expiring recovery lease', function () { syslog_load_plugin_source('includes/functions.php'); - $source = file_get_contents(__DIR__ . '/../../includes/functions.php'); + $source = plugin_test_read_source('includes/functions.php'); expect($source)->toContain('INSERT INTO `$syslogdb_default`.`syslog_replication_recovery`') ->toContain('ON DUPLICATE KEY UPDATE') @@ -239,7 +230,7 @@ }); it('keeps recovery bounded and delegates batch delivery to the Phase 2 primitive', function () { syslog_load_plugin_source('includes/functions.php'); - $source = file_get_contents(__DIR__ . '/../../includes/functions.php'); + $source = plugin_test_read_source('includes/functions.php'); expect($source)->toContain('syslog_replication_recovery_records_per_run()') ->toContain('syslog_replication_recovery_batch_delay_us()') diff --git a/tests/Unit/SetupModuleBoundaryTest.php b/tests/Unit/SetupModuleBoundaryTest.php new file mode 100644 index 00000000..250f2d9b --- /dev/null +++ b/tests/Unit/SetupModuleBoundaryTest.php @@ -0,0 +1,100 @@ +toEqualCanonicalizing($allowed); +}); + +it('keeps config-array dependencies in the directly loaded settings module', function () { + $settings = plugin_test_read_source('includes/settings.php'); + + expect($settings)->toContain('function syslog_refresh_permission_roles(): void'); +}); + +it('boots the compatibility facade before Cacti can invoke registered callbacks', function () { + $setup = plugin_test_read_source('setup.php'); + + preg_match_all("/api_plugin_register_hook\\([^\\n]*['\"]includes\\/([^'\"]+\\.php)['\"]/", $setup, $matches); + + foreach (array_unique($matches[1]) as $module) { + $source = plugin_test_read_source('includes/' . $module); + $bootstrap = strpos($source, "require_once(dirname(__DIR__) . '/setup.php');"); + $callback = strpos($source, 'function '); + + expect($bootstrap)->not->toBeFalse() + ->toBeLessThan($callback); + } +}); + +it('loads extracted modules only at the lifecycle boundary that needs them', function () { + $setup = plugin_test_read_source('setup.php'); + + expect($setup)->toContain("require_once(__DIR__ . '/includes/schema.php');") + ->toContain("require_once(__DIR__ . '/includes/settings.php');") + ->toContain("require_once(__DIR__ . '/includes/installer.php');") + ->not->toContain("require_once(__DIR__ . '/includes/processing.php');") + ->not->toContain("require_once(__DIR__ . '/includes/navigation.php');") + ->not->toContain("require_once(__DIR__ . '/includes/utilities.php');"); +}); + +it('boots CLI entrypoints through the setup compatibility facade', function () { + foreach (['syslog_counter.php', 'syslog_recovery.php'] as $entrypoint) { + $source = plugin_test_read_source($entrypoint); + + expect($source)->toContain("include_once(__DIR__ . '/setup.php');") + ->not->toContain("include_once(__DIR__ . '/functions.php');") + ->not->toContain("include_once(__DIR__ . '/database.php');"); + } +}); + +it('registers extracted callbacks against their owning module', function () { + $setup = plugin_test_read_source('setup.php'); + + $ownership = [ + 'syslog_config_arrays' => 'settings.php', + 'syslog_config_settings' => 'settings.php', + 'syslog_settings_bottom' => 'settings.php', + 'syslog_config_insert' => 'settings.php', + 'syslog_show_tab' => 'navigation.php', + 'syslog_draw_navigation_text' => 'navigation.php', + 'syslog_graph_buttons' => 'navigation.php', + 'syslog_poller_bottom' => 'processing.php', + 'syslog_replicate_out' => 'processing.php', + 'syslog_utilities_list' => 'utilities.php', + 'syslog_utilities_action' => 'utilities.php', + ]; + + foreach ($ownership as $callback => $module) { + expect($setup)->toMatch('/api_plugin_register_hook\([^\n]*[\'\"]' . preg_quote($callback, '/') . '[\'\"][^\n]*[\'\"]includes\/' . preg_quote($module, '/') . '[\'\"]/'); + } +}); diff --git a/tests/Unit/SetupSmallHelpersTest.php b/tests/Unit/SetupSmallHelpersTest.php index 61ebc6f9..5b4dff0d 100644 --- a/tests/Unit/SetupSmallHelpersTest.php +++ b/tests/Unit/SetupSmallHelpersTest.php @@ -8,7 +8,7 @@ /* * Unit coverage for a handful of small, pure setup.php functions that had * no coverage at all: plugin_syslog_version(), syslog_check_dependencies(), - * syslog_top_graph_refresh(), and syslog_draw_navigation_text(). + * and syslog_draw_navigation_text(). * * These are deliberately narrow: this plugin's setup.php also contains * much larger functions (plugin_syslog_uninstall(), syslog_connect(), @@ -21,6 +21,7 @@ beforeAll(function () { require_once __DIR__ . '/../../setup.php'; + require_once __DIR__ . '/../../includes/navigation.php'; }); beforeEach(function () { @@ -53,11 +54,6 @@ expect(syslog_check_dependencies())->toBeTrue(); }); -it('passes the refresh value through unchanged', function () { - expect(syslog_top_graph_refresh(30))->toBe(30); - expect(syslog_top_graph_refresh(0))->toBe(0); -}); - it('adds the syslog breadcrumb entries without disturbing existing ones', function () { $nav = syslog_draw_navigation_text(['other.php:' => ['title' => 'Other']]); diff --git a/tests/Unit/SyslogSettingsWorkersTest.php b/tests/Unit/SyslogSettingsWorkersTest.php index 8ac27c43..d95bc559 100644 --- a/tests/Unit/SyslogSettingsWorkersTest.php +++ b/tests/Unit/SyslogSettingsWorkersTest.php @@ -29,7 +29,7 @@ function syslog_worker_settings_extract_array(string $file, string $key): string it('defines the syslog_max_workers setting with single process default', function () { $root = dirname(__DIR__, 2); - $element = syslog_worker_settings_extract_array($root . '/setup.php', 'syslog_max_workers'); + $element = syslog_worker_settings_extract_array($root . '/includes/settings.php', 'syslog_max_workers'); expect(str_contains($element, "'method' => 'drop_array'"))->toBeTrue('Rendered as a dropdown'); expect(str_contains($element, "'default' => '1'"))->toBeTrue('Defaults to a single worker process'); @@ -38,7 +38,7 @@ function syslog_worker_settings_extract_array(string $file, string $key): string it('offers the documented worker choices', function () { $root = dirname(__DIR__, 2); - $element = syslog_worker_settings_extract_array($root . '/setup.php', 'syslog_max_workers'); + $element = syslog_worker_settings_extract_array($root . '/includes/settings.php', 'syslog_max_workers'); foreach ([1, 2, 4, 6, 8, 16] as $choice) { expect(preg_match('/\t\t\t\t' . $choice . '\s+=> __\(\'%d Worker Process/', $element))->toBe(1, "Choice $choice present"); @@ -49,9 +49,9 @@ function syslog_worker_settings_extract_array(string $file, string $key): string it('documents the parallel behavior in the description', function () { $root = dirname(__DIR__, 2); - $element = syslog_worker_settings_extract_array($root . '/setup.php', 'syslog_max_workers'); + $element = syslog_worker_settings_extract_array($root . '/includes/settings.php', 'syslog_max_workers'); expect(str_contains($element, 'parallel worker processes'))->toBeTrue(); expect(str_contains($element, 'disjoint range'))->toBeTrue(); expect(str_contains($element, 'Windows'))->toBeTrue(); -}); \ No newline at end of file +}); diff --git a/tests/bin/patch-coverage.php b/tests/bin/patch-coverage.php index c3e09ee0..bd2376a3 100644 --- a/tests/bin/patch-coverage.php +++ b/tests/bin/patch-coverage.php @@ -173,8 +173,18 @@ function changed_lines($base_ref) { 'syslog_dashboards.php', 'syslog_device_rules.php', 'syslog_batch_transfer.php', + 'syslog_counter.php', 'syslog_process.php', 'syslog_recovery.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. + 'includes/installer.php', + 'includes/navigation.php', + 'includes/processing.php', + 'includes/schema.php', + 'includes/settings.php', + 'includes/utilities.php', // setup.php is unit-loadable (SetupSmallHelpersTest exercises its pure // helpers), but the only lines this change touches are the include_once // chain inside syslog_connect()/syslog_poller_bottom(), which run solely in a diff --git a/tests/bootstrap-unit.php b/tests/bootstrap-unit.php index 7dd6f7d1..48e5a81b 100644 --- a/tests/bootstrap-unit.php +++ b/tests/bootstrap-unit.php @@ -646,6 +646,28 @@ function plugin_test_read_source($relative_file) { } } +if (!function_exists('plugin_test_read_function_source')) { + /** + * Read the declared source file for a public Syslog function. + * + * @param string $function Function name. + * + * @return string + */ + function plugin_test_read_function_source($function) { + $owners = [ + 'syslog_create_partitioned_syslog_table' => 'includes/schema.php', + 'syslog_setup_table_new' => 'includes/schema.php', + ]; + + if (!isset($owners[$function])) { + throw new RuntimeException("No source owner registered for function: {$function}"); + } + + return plugin_test_read_source($owners[$function]); + } +} + /** * Load a plugin source file at global scope. * diff --git a/tests/regression/panel_resize_test.php b/tests/regression/panel_resize_test.php index 6aa71910..cad29d2f 100644 --- a/tests/regression/panel_resize_test.php +++ b/tests/regression/panel_resize_test.php @@ -61,7 +61,6 @@ function check($actual, $expected, $label) { 'function syslog_dashboard_removals()', 'function syslog_dashboard_top_n_cap()', 'function syslog_dashboard_width_cap()', - 'function syslog_dashboard_height_min()', 'function syslog_dashboard_height_cap()', 'function syslog_dashboard_panel_settings(', 'function syslog_dashboard_username(', @@ -131,4 +130,4 @@ function syslog_dashboard_share() { check($last['params'][10], 1, 'width preserved'); check($last['params'][11], 0, 'height preserved'); -echo "panel_resize_test passed\n"; \ No newline at end of file +echo "panel_resize_test passed\n";