From 562d8d4718eeac65008d511eb205f5e1a9b9dc6a Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sat, 24 Oct 2026 01:24:54 -0600 Subject: [PATCH 1/2] fix: look up A and AAAA records and accept IPv6 sender addresses Signed-off-by: Thomas Vincent --- CHANGELOG.md | 2 + README.md | 7 + includes/functions.php | 111 ++++++++++- includes/settings.php | 2 +- locales/po/cacti.pot | 2 +- tests/Unit/HostAddressResolutionTest.php | 243 +++++++++++++++++++++++ tests/Unit/HostnameLookupBudgetTest.php | 4 +- 7 files changed, 365 insertions(+), 6 deletions(-) create mode 100644 tests/Unit/HostAddressResolutionTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 269f93e0..7bad3bbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,8 @@ * security: Substitute alert command tokens in a single pass so values containing a token name are not expanded again +* bug: Check sender names with A and AAAA records, accept IPv4 and IPv6 addresses without a lookup and store IPv6 addresses in one canonical form + * issue#317: Expire partitions by their actual UTC upper boundaries instead of partition count, preserving cutoff-spanning partitions and supporting legacy TO_DAYS layouts. * issue#317: Defer automatic partition reorganization when overflow estimates exceed 100,000 rows or 128 MiB, or cannot be read; preserve the safety partition and report the reason through maintenance health. diff --git a/README.md b/README.md index 26c90d54..dc0827e2 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,13 @@ If you are upgrading to version 2.5 from an earlier version, make sure that you update this template format and restart rsyslog. You may loose some syslog data, but doing this in a timely fashion, will minimize data loss. +Senders that only have an IPv6 address are supported. With hostname resolution +enabled the plugin accepts an IPv4 or IPv6 address as it is, and stores an IPv6 +address in its compressed lower case form (for example `2001:db8::1`) so one +address is kept under one device name. Names are looked up with A and AAAA +records. The `host` columns hold 64 characters, which is enough for any IPv6 +address. + Ensure you restart rsyslog after these changes are completed. Other logging servers such as Syslog-NG are also supported with this plugin. Please see some additional documentation here: [Cacti Documentation diff --git a/includes/functions.php b/includes/functions.php index d87dd1e2..386200ed 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -6121,6 +6121,95 @@ function syslog_strip_incoming_domains($max_seq) { } } +/** + * Return the canonical text of an IPv4 or IPv6 address, or false when the + * sender name is not an address literal. + * + * IPv6 text is normalized (lower case, compressed, no brackets) so that one + * address arriving in several spellings is stored under one host name. A zone + * suffix such as %eth0 is kept as sent. + * + * @param string $name The host name as sent by the remote system + * + * @return string|false The canonical address text, or false for a name + */ +function syslog_host_address(string $name): string|false { + if (preg_match('/^\[?([^\]%]+)(%[A-Za-z0-9_.-]+)?\]?$/', trim($name), $match) !== 1 || !filter_var($match[1], FILTER_VALIDATE_IP)) { + return false; + } + + $zone = $match[2] ?? ''; + + // Only an IPv6 address can carry a zone. + if ($zone !== '' && strpos($match[1], ':') === false) { + return false; + } + + return inet_ntop(inet_pton($match[1])) . $zone; +} + +/** + * Whether a host name has an address record. Looks for A and AAAA records so a + * sender that only has an IPv6 address counts, and falls back to the system + * resolver, which also reads the hosts file. + * + * @param string $name The host name to look up + * @param callable $records Returns the A and AAAA records for a name, like dns_get_record() + * @param callable $legacy Returns the IPv4 address for a name, or the name itself, like gethostbyname() + * + * @return bool True when the name resolves + */ +function syslog_host_has_address(string $name, ?callable $records = null, ?callable $legacy = null): bool { + $records ??= 'dns_get_record'; + $legacy ??= 'gethostbyname'; + + // A failed query is an answer here, not a PHP warning for the Cacti log. + set_error_handler(static fn() => true); + + try { + $found = $records($name, DNS_A | DNS_AAAA); + } finally { + restore_error_handler(); + } + + if (is_array($found) && cacti_sizeof($found)) { + return true; + } + + return $name != $legacy($name); +} + +/** + * Point the unprocessed messages of one sender name at another name. + * + * @param string $from The name currently stored + * @param string $to The name to store + * @param int $max_seq The max_seq for syslog_incoming messages to process + * @param int $seq_start Optional first seq of a slice (0 for no bound) + * @param int $seq_end Optional last seq of a slice (0 for no bound) + * + * @return void + */ +function syslog_set_incoming_host(string $from, string $to, $max_seq, $seq_start = 0, $seq_end = 0): void { + global $syslogdb_default; + + if ($seq_start > 0 && $seq_end > 0) { + syslog_db_execute_prepared("UPDATE `$syslogdb_default`.`syslog_incoming` + SET host = ? + WHERE host = ? + AND `status` = 1 + AND `seq` BETWEEN ? AND ?", + [$to, $from, $seq_start, $seq_end]); + } else { + syslog_db_execute_prepared("UPDATE `$syslogdb_default`.`syslog_incoming` + SET host = ? + WHERE host = ? + AND `status` = 1 + AND `seq` <= ?", + [$to, $from, $max_seq]); + } +} + /** * Check if the hostname is in the cacti hosts table * Some devices only send IP addresses in syslog messages, and may not be in the DNS @@ -6318,10 +6407,11 @@ function syslog_update_reference_tables($max_seq) { * @param int $seq_end Optional last seq of a slice (0 for no bound) * @param int $budget_seconds Seconds after which DNS lookups stop for this call; * hosts not found in Cacti are then left as received + * @param callable|null $dns_records Replacement for dns_get_record(), used by tests * * @return int The number of distinct hosts resolved */ -function syslog_resolve_incoming_hosts($max_seq, $seq_start = 0, $seq_end = 0, $budget_seconds = SYSLOG_RESOLVE_BUDGET_SECONDS) { +function syslog_resolve_incoming_hosts($max_seq, $seq_start = 0, $seq_end = 0, $budget_seconds = SYSLOG_RESOLVE_BUDGET_SECONDS, ?callable $dns_records = null) { global $syslogdb_default; if (read_config_option('syslog_resolve_hostname') != 'on') { @@ -6353,6 +6443,23 @@ function syslog_resolve_incoming_hosts($max_seq, $seq_start = 0, $seq_end = 0, $ $resolved = false; $dns_asked = false; + $address = syslog_host_address($host['host']); + + if ($address !== false) { + // An address needs no lookup. A device in Cacti may still give it a + // better name, matched as sent first and then in canonical form. + if (!syslog_check_cacti_hosts($host['host'], $max_seq, $seq_start, $seq_end)) { + if ($address !== $host['host']) { + syslog_set_incoming_host($host['host'], $address, $max_seq, $seq_start, $seq_end); + } + + syslog_check_cacti_hosts($address, $max_seq, $seq_start, $seq_end); + } + + $resolved_count++; + + continue; + } // Check if hostname resolves via DNS (only if DNS is enabled). Senders // choose these names, so lookups stop once the time budget is spent. @@ -6362,7 +6469,7 @@ function syslog_resolve_incoming_hosts($max_seq, $seq_start = 0, $seq_end = 0, $ } else { $dns_asked = true; - if ($host['host'] != gethostbyname($host['host'])) { + if (syslog_host_has_address($host['host'], $dns_records)) { // DNS resolved successfully $resolved = true; } diff --git a/includes/settings.php b/includes/settings.php index 724c4119..a8ab6cad 100644 --- a/includes/settings.php +++ b/includes/settings.php @@ -129,7 +129,7 @@ function syslog_config_settings(): void { ], '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'), + 'description' => __('If this checkbox is set, all hostnames are resolved via DNS lookup first (If enabled), using A and AAAA records. IPv4 and IPv6 addresses need no lookup, and IPv6 addresses are stored in compressed lower case form. 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' => '' ], diff --git a/locales/po/cacti.pot b/locales/po/cacti.pot index 8f2bd67e..e02b8776 100644 --- a/locales/po/cacti.pot +++ b/locales/po/cacti.pot @@ -697,7 +697,7 @@ msgid "Enable Hostname Resolution" msgstr "" #: includes/settings.php -msgid "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'." +msgid "If this checkbox is set, all hostnames are resolved via DNS lookup first (If enabled), using A and AAAA records. IPv4 and IPv6 addresses need no lookup, and IPv6 addresses are stored in compressed lower case form. 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'." msgstr "" #: includes/settings.php diff --git a/tests/Unit/HostAddressResolutionTest.php b/tests/Unit/HostAddressResolutionTest.php new file mode 100644 index 00000000..c493e0bf --- /dev/null +++ b/tests/Unit/HostAddressResolutionTest.php @@ -0,0 +1,243 @@ +toBe($expected); +})->with([ + 'ipv4' => ['203.0.113.5', '203.0.113.5'], + 'ipv6 compressed' => ['2001:db8::1', '2001:db8::1'], + 'ipv6 expanded' => ['2001:0db8:0000:0000:0000:0000:0000:0001', '2001:db8::1'], + 'ipv6 mixed case' => ['2001:DB8::AbC', '2001:db8::abc'], + 'ipv6 brackets' => ['[2001:db8::0001]', '2001:db8::1'], + 'ipv6 mapped ipv4' => ['::FFFF:203.0.113.5', '::ffff:203.0.113.5'], + 'ipv6 zone' => ['FE80::1%eth0', 'fe80::1%eth0'], + 'ipv6 unspecified' => ['0:0:0:0:0:0:0:0', '::'], + 'name' => ['router1.example.net', false], + 'name with digits' => ['203.0.113.5.example.net', false], + 'out of range ipv4' => ['256.1.1.1', false], + 'bad ipv6' => ['2001:db8::g', false], + 'ipv4 with zone' => ['203.0.113.5%eth0', false], + 'empty' => ['', false], + 'several brackets' => ['[[2001:db8::1]]', false], +]); + +it('counts an A record, an AAAA record or both as resolving', function ($records, $legacy, $expected) { + $answers = static fn($name, $types) => $records; + $old = static fn($name) => $legacy ?? $name; + + expect(syslog_host_has_address('sender.example.net', $answers, $old))->toBe($expected); +})->with([ + 'A only' => [[['type' => 'A', 'ip' => '203.0.113.5']], null, true], + 'AAAA only' => [[['type' => 'AAAA', 'ipv6' => '2001:db8::1']], null, true], + 'both' => [[['type' => 'A', 'ip' => '203.0.113.5'], ['type' => 'AAAA', 'ipv6' => '2001:db8::1']], null, true], + 'none' => [[], null, false], + 'failed query' => [false, null, false], + 'hosts file entry only' => [[], '203.0.113.9', true], + 'failed query, hosts file' => [false, '203.0.113.9', true], +]); + +it('asks for both record types and keeps a failed query out of the log', function () { + $asked = []; + + $records = function ($name, $types) use (&$asked) { + $asked[] = [$name, $types]; + trigger_error('DNS Query failed', E_USER_WARNING); + + return false; + }; + + expect(syslog_host_has_address('missing.example.net', $records, static fn($name) => $name))->toBeFalse() + ->and($asked)->toBe([['missing.example.net', DNS_A | DNS_AAAA]]); +}); + +it('resolves a name from the hosts file with the system resolver', function () { + expect(syslog_host_has_address('localhost'))->toBeTrue(); +}); + +function resolve_hosts_setup(array $hosts, array $options = [], array $devices = []): stdClass { + $GLOBALS['syslogdb_default'] = 'syslogdb'; + + $state = new stdClass(); + $state->updates = []; + $state->logs = []; + + test_override('read_config_option', static fn($key) => ($options + ['syslog_resolve_hostname' => 'on', 'syslog_no_dns' => ''])[$key] ?? ''); + test_override('syslog_db_fetch_assoc_prepared', static fn() => $hosts); + test_override('db_fetch_row_prepared', static fn($sql, $params) => isset($devices[$params[0]]) ? ['description' => $devices[$params[0]]] : []); + test_override('syslog_db_execute_prepared', function ($sql, $params) use ($state) { + $state->updates[] = $params; + + return true; + }); + test_override('cacti_log', function ($message) use ($state) { + $state->logs[] = $message; + }); + + return $state; +} + +it('treats addresses as resolved without a lookup and leaves a canonical one alone', function ($name) { + $state = resolve_hosts_setup([['host' => $name]]); + + expect(syslog_resolve_incoming_hosts(100))->toBe(1) + ->and($state->updates)->toBe([]) + ->and($state->logs)->toBe([]); +})->with(['203.0.113.5', '2001:db8::1', '::1']); + +it('stores one canonical spelling for an IPv6 sender', function ($name) { + $state = resolve_hosts_setup([['host' => $name]]); + + expect(syslog_resolve_incoming_hosts(100))->toBe(1) + ->and($state->updates)->toBe([['2001:db8::1', $name, 100]]); +})->with(['2001:DB8::1', '2001:0db8:0000:0000:0000:0000:0000:0001', '[2001:db8::1]']); + +it('limits the canonical rename to a slice of messages', function () { + $state = resolve_hosts_setup([['host' => '2001:DB8::1']]); + + syslog_resolve_incoming_hosts(100, 10, 20); + + expect($state->updates)->toBe([['2001:db8::1', '2001:DB8::1', 10, 20]]); +}); + +it('names a device found in Cacti by the spelling it uses', function ($sent, $stored, $renames) { + $state = resolve_hosts_setup([['host' => $sent]], [], [$stored => 'Core Router']); + + expect(syslog_resolve_incoming_hosts(100))->toBe(1) + ->and($state->updates)->toBe($renames); +})->with([ + 'ipv4' => ['203.0.113.5', '203.0.113.5', [['Core Router', '203.0.113.5', 100]]], + 'ipv6 as sent' => ['2001:DB8::1', '2001:DB8::1', [['Core Router', '2001:DB8::1', 100]]], + 'ipv6 stored in canonical form' => ['2001:DB8::1', '2001:db8::1', [['2001:db8::1', '2001:DB8::1', 100], ['Core Router', '2001:db8::1', 100]]], +]); + +it('marks a name that does not resolve and is not a device', function () { + $state = resolve_hosts_setup([['host' => 'no-such-sender.invalid']]); + + expect(syslog_resolve_incoming_hosts(100))->toBe(0) + ->and($state->updates)->toBe([['unresolved-no-such-sender.invalid', 'no-such-sender.invalid', 100]]) + ->and($state->logs)->toHaveCount(1); +}); + +it('keeps a name that resolves', function () { + $state = resolve_hosts_setup([['host' => 'localhost']]); + + expect(syslog_resolve_incoming_hosts(100))->toBe(1) + ->and($state->updates)->toBe([]); +}); + +it('skips the lookup when DNS is turned off but still resolves addresses and devices', function () { + $state = resolve_hosts_setup( + [['host' => 'localhost'], ['host' => '2001:DB8::2'], ['host' => 'known.example.net']], + ['syslog_no_dns' => 'on'], + ['known.example.net' => 'Known Device'] + ); + + expect(syslog_resolve_incoming_hosts(100))->toBe(2) + ->and($state->updates)->toBe([ + ['unresolved-localhost', 'localhost', 100], + ['2001:db8::2', '2001:DB8::2', 100], + ['Known Device', 'known.example.net', 100], + ]); +}); + +it('does nothing when name checking is off or the host is empty', function () { + $state = resolve_hosts_setup([['host' => '']], ['syslog_resolve_hostname' => '']); + + expect(syslog_resolve_incoming_hosts(100))->toBe(0) + ->and($state->updates)->toBe([]); + + $state = resolve_hosts_setup([['host' => ''], ['other' => 'x']]); + + expect(syslog_resolve_incoming_hosts(100))->toBe(0) + ->and($state->updates)->toBe([]); +}); + +it('limits a marked name to a slice of messages', function () { + $state = resolve_hosts_setup([['host' => 'no-such-sender.invalid']]); + + syslog_resolve_incoming_hosts(100, 10, 20); + + expect($state->updates)->toBe([['unresolved-no-such-sender.invalid', 'no-such-sender.invalid', 10, 20]]); +}); + +function aaaa_only_answers(array &$asked): callable { + return function ($name, $types) use (&$asked) { + $asked[] = $name; + + return [['type' => 'AAAA', 'ipv6' => '2001:db8::53']]; + }; +} + +it('counts a name with only an AAAA record as resolved within the budget', function () { + $asked = []; + $state = resolve_hosts_setup([['host' => 'v6-one.example.net'], ['host' => 'v6-two.example.net']]); + + expect(syslog_resolve_incoming_hosts(100, 0, 0, 60, aaaa_only_answers($asked)))->toBe(2) + ->and($asked)->toBe(['v6-one.example.net', 'v6-two.example.net']) + ->and($state->updates)->toBe([]) + ->and($state->logs)->toBe([]); +}); + +it('stops looking up AAAA-only names once the budget is spent and leaves them as received', function () { + $asked = []; + $state = resolve_hosts_setup([['host' => 'v6-one.example.net'], ['host' => 'v6-two.example.net']]); + + expect(syslog_resolve_incoming_hosts(100, 0, 0, 0, aaaa_only_answers($asked)))->toBe(0) + ->and($asked)->toBe([]) + ->and($state->updates)->toBe([]) + ->and($state->logs)->toHaveCount(1) + ->and($state->logs[0])->toContain('budget'); +}); + +it('does not charge address literals to the budget', function () { + $asked = []; + $state = resolve_hosts_setup([['host' => '203.0.113.5'], ['host' => '2001:DB8::7']]); + + expect(syslog_resolve_incoming_hosts(100, 0, 0, 0, aaaa_only_answers($asked)))->toBe(2) + ->and($asked)->toBe([]) + ->and($state->updates)->toBe([['2001:db8::7', '2001:DB8::7', 100]]) + ->and($state->logs)->toBe([]); +}); + +it('resolves literals and devices after the budget is spent and reports the skipped name once', function () { + $asked = []; + $state = resolve_hosts_setup( + [['host' => 'v6-one.example.net'], ['host' => '2001:DB8::7'], ['host' => 'known.example.net'], ['host' => 'v6-two.example.net']], + [], + ['known.example.net' => 'Known Device'] + ); + + expect(syslog_resolve_incoming_hosts(100, 0, 0, 0, aaaa_only_answers($asked)))->toBe(2) + ->and($asked)->toBe([]) + ->and($state->updates)->toBe([['2001:db8::7', '2001:DB8::7', 100], ['Known Device', 'known.example.net', 100]]) + ->and($state->logs)->toHaveCount(1); +}); + +it('never asks DNS when lookups are disabled, whatever the budget', function ($budget) { + $asked = []; + $state = resolve_hosts_setup([['host' => 'v6-one.example.net']], ['syslog_no_dns' => 'on']); + + expect(syslog_resolve_incoming_hosts(100, 0, 0, $budget, aaaa_only_answers($asked)))->toBe(0) + ->and($asked)->toBe([]) + ->and($state->updates)->toBe([['unresolved-v6-one.example.net', 'v6-one.example.net', 100]]) + ->and(array_filter($state->logs, static fn($line) => str_contains($line, 'budget')))->toBe([]); +})->with([0, 60]); diff --git a/tests/Unit/HostnameLookupBudgetTest.php b/tests/Unit/HostnameLookupBudgetTest.php index 4d915872..d6a4a471 100644 --- a/tests/Unit/HostnameLookupBudgetTest.php +++ b/tests/Unit/HostnameLookupBudgetTest.php @@ -62,10 +62,10 @@ function resolve_setup(array $hosts): stdClass { it('still marks a name unresolved when the lookup was made and failed', function () { - $state = resolve_setup([['host' => '192.0.2.1']]); + $state = resolve_setup([['host' => 'no-such-sender.invalid']]); expect(syslog_resolve_incoming_hosts(100))->toBe(0) - ->and($state->updates)->toBe([['unresolved-192.0.2.1', '192.0.2.1', 100]]); + ->and($state->updates)->toBe([['unresolved-no-such-sender.invalid', 'no-such-sender.invalid', 100]]); }); From c293f6a6dc380056640f3ef4a96636d7f3afe704 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sat, 24 Oct 2026 01:25:59 -0600 Subject: [PATCH 2/2] test: populate a valid alert rule in CI and assert that it fires Signed-off-by: Thomas Vincent --- .github/workflows/plugin-ci-workflow.yml | 29 ++++++++++++++++++------ CHANGELOG.md | 2 ++ tests/bin/populate_syslog_incoming.sh | 13 ++++++++--- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index f421b52d..d2017c1d 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -313,6 +313,18 @@ jobs: cat log/cacti.log exit 1 fi + # The populated alert rule (id 1) must have been evaluated and have fired + # for the host "bob": the processor logs it and stores it in syslog_logs. + if ! grep -q "The Syslog Alert 'interface down alert' with Severity 'Warning', has been Triggered on Host 'bob'" log/cacti.log; then + echo "The populated alert rule did not fire" + cat log/cacti.log + exit 1 + fi + alerts=$(mysql --defaults-file="$HOME/.my.cnf" cacti -N -e "SELECT COUNT(*) FROM syslog_logs WHERE alert_id = 1 AND host = 'bob'") + if [ "$alerts" -lt 1 ]; then + echo "The populated alert rule left no alert log row" + exit 1 + fi # Runs the real processor over queued messages and reads the results back. # The script removes its rows and restores cacti.log to its starting size. @@ -333,14 +345,17 @@ jobs: if: always() run: | if [ -f ${{ github.workspace }}/cacti/log/cacti.log ]; then - if sudo grep -E "FATAL:|ERROR:|WARNING:" "${{ github.workspace }}/cacti/log/cacti.log"; then + status=0 + matches=$(sudo grep -E "FATAL:|ERROR:|WARNING:" "${{ github.workspace }}/cacti/log/cacti.log") || status=$? + if [ "$status" -gt 1 ]; then + echo "Unable to inspect Cacti log" + exit "$status" + fi + # The only entry allowed is the alert the populated rule is meant to raise. + unexpected=$(printf '%s\n' "$matches" | grep -v -F "The Syslog Alert 'interface down alert' with Severity 'Warning', has been Triggered on Host 'bob'" | grep -v '^$') || true + if [ -n "$unexpected" ]; then + printf '%s\n' "$unexpected" echo "Found FATAL/ERROR/WARNING entries in Cacti log" exit 1 - else - status=$? - if [ "$status" -ne 1 ]; then - echo "Unable to inspect Cacti log" - exit "$status" - fi fi fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bad3bbb..166cb735 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,8 @@ * security: Substitute alert command tokens in a single pass so values containing a token name are not expanded again +* test: Insert a valid alert rule in the CI test data and assert in the workflow that the processor evaluates and fires it + * bug: Check sender names with A and AAAA records, accept IPv4 and IPv6 addresses without a lookup and store IPv6 addresses in one canonical form * issue#317: Expire partitions by their actual UTC upper boundaries instead of partition count, preserving cutoff-spanning partitions and supporting legacy TO_DAYS layouts. diff --git a/tests/bin/populate_syslog_incoming.sh b/tests/bin/populate_syslog_incoming.sh index e51878f6..2063308b 100644 --- a/tests/bin/populate_syslog_incoming.sh +++ b/tests/bin/populate_syslog_incoming.sh @@ -1,14 +1,21 @@ #!/bin/bash # filepath: insert_syslog_test_data.sh +# A failed insert must fail the CI step instead of leaving the run without data. +set -euo pipefail + # Number of iterations ITERATIONS=100 -SQL_ALERT_RULE_INSERT="INSERT INTO syslog_alert (id,hash,name,severity,method,level,num,type,enabled,repeat_alert,open_ticket,message,body,user,date,email,notify,command,notes) - VALUES (1,'8f440030d3425e37cb66e5df54902bb0','interface down alert',1,0,1,1,'messageb','on',0,'','interface down','admin',1767376990,NULL,0,NULL,NULL);" +# Every NOT NULL column without a default must be listed (message and +# suppression_schedule), or the insert fails under strict SQL mode. Empty +# strings are used for the text columns the processor reads, because a NULL +# there raises a PHP deprecation notice in the Cacti log. +SQL_ALERT_RULE_INSERT="REPLACE INTO syslog_alert (id,hash,name,severity,method,level,num,type,enabled,repeat_alert,open_ticket,message,body,user,date,email,notify,command,recovery_command,notes,suppression_schedule) + VALUES (1,'8f440030d3425e37cb66e5df54902bb0','interface down alert',1,0,1,1,'messageb','on',0,'','interface down','','admin',1767376990,'',0,'','','','');" -SQL_REMOVAL_RULE_INSERT="INSERT INTO syslog_remove (id,hash,name,type,enabled,method,message,user,date,notes) +SQL_REMOVAL_RULE_INSERT="REPLACE INTO syslog_remove (id,hash,name,type,enabled,method,message,user,date,notes) VALUES (1,'0faf589f7b6b7da1bcec40b92340487d','exploded','messageb','on','del', 'the box exploded','admin',1767376604,NULL);"