mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-29 20:23:01 +02:00
Merge pull request #1268 from kernelkit/syslog-matching
Syslog Enhancements
This commit is contained in:
@@ -31,6 +31,8 @@ bind "set completion-ignore-case on"
|
||||
# show all completions immediately instead of ringing bell
|
||||
bind "set show-all-if-ambiguous on"
|
||||
|
||||
export LANG=C.UTF-8
|
||||
|
||||
log()
|
||||
{
|
||||
local fn="/var/log/syslog"
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
set COLORTERM=yes
|
||||
rlimit soft core infinity
|
||||
set LANG=C.UTF-8
|
||||
rlimit soft core infinity
|
||||
|
||||
@@ -3,6 +3,7 @@ alias la='ls -A'
|
||||
alias ll='ls -alF'
|
||||
alias ls='ls --color=auto'
|
||||
|
||||
export LANG=C.UTF-8
|
||||
export EDITOR=/usr/bin/edit
|
||||
export VISUAL=/usr/bin/edit
|
||||
export LESS="-P %f (press h for help or q to quit)"
|
||||
|
||||
@@ -48,6 +48,17 @@ All notable changes to the project are documented in this file.
|
||||
- Add CLI commands for managing boot partition order: `show boot-order` and
|
||||
`set boot-order` allow viewing and changing the boot order from the CLI,
|
||||
complementing the existing YANG RPC support, issue #1032
|
||||
- Extended syslog filtering capabilities, issue #1091:
|
||||
- Add support for pattern matching using POSIX extended regular expressions
|
||||
on message content (IETF `select-match` feature)
|
||||
- Add support for advanced severity comparison: exact match (`equals`) and
|
||||
exclusion (`block`/`stop`) in addition to the default equals-or-higher
|
||||
(IETF `select-adv-compare` feature)
|
||||
- Add support for hostname-based filtering, useful when acting as a log
|
||||
server to route messages from different devices to separate log files
|
||||
- Add support for property-based filtering with operators (contains, isequal,
|
||||
startswith, regex, ereregex) on message properties (msg, msgid, programname,
|
||||
hostname, source, data), with optional case-insensitive and negate modifiers
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
+108
-2
@@ -191,8 +191,114 @@ admin@example:/>
|
||||
```
|
||||
|
||||
See the above [Log to File](#log-to-file) section on how to set up
|
||||
filtering of received logs to local files. Please note, filtering based
|
||||
on property, e.g., hostname, is not supported yet.
|
||||
filtering of received logs to local files. Advanced filtering based
|
||||
on hostname and message properties is also available, see the next
|
||||
section for details.
|
||||
|
||||
## Advanced Filtering
|
||||
|
||||
The syslog subsystem supports several advanced filtering options that
|
||||
allow fine-grained control over which messages are logged. These can
|
||||
be combined with facility and severity filters to create sophisticated
|
||||
logging rules.
|
||||
|
||||
### Pattern Matching
|
||||
|
||||
Messages can be filtered using regular expressions (POSIX extended regex)
|
||||
on the message content. This is useful when you want to log only messages
|
||||
containing specific keywords or patterns:
|
||||
|
||||
```
|
||||
admin@example:/config/> edit syslog actions log-file file:errors
|
||||
admin@example:/config/syslog/…/file:errors/> set pattern-match "ERROR|CRITICAL|FATAL"
|
||||
admin@example:/config/syslog/…/file:errors/> set facility-list all severity info
|
||||
admin@example:/config/syslog/…/file:errors/> leave
|
||||
admin@example:/>
|
||||
```
|
||||
|
||||
This will log all messages containing ERROR, CRITICAL, or FATAL.
|
||||
|
||||
### Advanced Severity Comparison
|
||||
|
||||
By default, severity filtering uses "equals-or-higher" comparison,
|
||||
meaning a severity of `error` will match error, critical, alert, and
|
||||
emergency messages. You can change this behavior:
|
||||
|
||||
```
|
||||
admin@example:/config/> edit syslog actions log-file file:daemon-errors
|
||||
admin@example:/config/syslog/…/file:daemon-errors/> set facility-list daemon
|
||||
admin@example:/config/syslog/…/daemon/> set severity error
|
||||
admin@example:/config/syslog/…/daemon/> set advanced-compare compare equals
|
||||
admin@example:/config/syslog/…/daemon/> leave
|
||||
admin@example:/>
|
||||
```
|
||||
|
||||
This will log only `error` severity messages, not higher severities.
|
||||
|
||||
You can also block specific severities:
|
||||
|
||||
```
|
||||
admin@example:/config/syslog/…/daemon/> set advanced-compare action block
|
||||
```
|
||||
|
||||
This will exclude `error` messages from the log.
|
||||
|
||||
### Hostname Filtering
|
||||
|
||||
When acting as a log server, you can filter messages by hostname. This
|
||||
is useful for directing logs from different devices to separate files:
|
||||
|
||||
```
|
||||
admin@example:/config/> edit syslog actions log-file file:router1
|
||||
admin@example:/config/syslog/…/file:router1/> set hostname-filter router1
|
||||
admin@example:/config/syslog/…/file:router1/> set facility-list all severity info
|
||||
admin@example:/config/syslog/…/file:router1/> leave
|
||||
admin@example:/>
|
||||
```
|
||||
|
||||
Multiple hostnames can be added to the filter list.
|
||||
|
||||
### Property-Based Filtering
|
||||
|
||||
For more advanced filtering, you can match on specific message properties
|
||||
using various comparison operators:
|
||||
|
||||
```
|
||||
admin@example:/config/> edit syslog actions log-file file:myapp
|
||||
admin@example:/config/syslog/…/file:myapp/> edit property-filter
|
||||
admin@example:/config/syslog/…/property-filter/> set property programname
|
||||
admin@example:/config/syslog/…/property-filter/> set operator isequal
|
||||
admin@example:/config/syslog/…/property-filter/> set value myapp
|
||||
admin@example:/config/syslog/…/property-filter/> leave
|
||||
admin@example:/>
|
||||
```
|
||||
|
||||
Available properties:
|
||||
- `msg`: Message body
|
||||
- `msgid`: RFC5424 message identifier
|
||||
- `programname`: Program/tag name
|
||||
- `hostname`: Source hostname
|
||||
- `source`: Alias for hostname
|
||||
- `data`: RFC5424 structured data
|
||||
|
||||
Available operators:
|
||||
- `contains`: Substring match
|
||||
- `isequal`: Exact equality
|
||||
- `startswith`: Prefix match
|
||||
- `regex`: Basic regular expression
|
||||
- `ereregex`: Extended regular expression (POSIX ERE)
|
||||
|
||||
The comparison can be made case-insensitive:
|
||||
|
||||
```
|
||||
admin@example:/config/syslog/…/property-filter/> set case-insensitive true
|
||||
```
|
||||
|
||||
Or negated to exclude matching messages:
|
||||
|
||||
```
|
||||
admin@example:/config/syslog/…/property-filter/> set negate true
|
||||
```
|
||||
|
||||
### Facilities
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# -l :: Keep kernel looging to console
|
||||
# -H :: Log remote messages using hostname from the message
|
||||
# -l :: Keep kernel logging to console
|
||||
# -m0 :: Disable periodic syslog MARK entries
|
||||
# -n :: Disable DNS query for every request, trust hostname in message
|
||||
# -s :: Enable secure mode, don't listen to remote logs
|
||||
# -r 1M:5 :: Log rotation every 1 MiB and keep 5 rotated ones
|
||||
SYSLOGD_ARGS="-l -m0"
|
||||
SYSLOGD_ARGS="-H -l -m0"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
From 31dfe0d16461d2852f3fc56cb82aed3a23db022f Mon Sep 17 00:00:00 2001
|
||||
From: Joachim Wiberg <troglobit@gmail.com>
|
||||
Date: Thu, 25 Sep 2025 16:48:31 +0200
|
||||
Subject: [PATCH] syslogd: fix UTF-8 handling with -8 flag, for RFC5424
|
||||
Subject: [PATCH 1/4] syslogd: fix UTF-8 handling with -8 flag, for RFC5424
|
||||
compliance
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
From f9b6531430c6485af4697a5868a6f43c93d20419 Mon Sep 17 00:00:00 2001
|
||||
From: Joachim Wiberg <troglobit@gmail.com>
|
||||
Date: Fri, 26 Sep 2025 08:42:57 +0200
|
||||
Subject: [PATCH 2/4] syslogd: fix parentheses handling in RFC 3164 tag parsing
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Organization: Wires
|
||||
|
||||
Some applications use tag names like "(polkit-agent)" which sysklogd do
|
||||
not handle well. This patch aims to address this and also add a bit of
|
||||
logic to normalize such tags:
|
||||
|
||||
- Allow parentheses in tag character set for broader compatibility
|
||||
- Smart parsing: strip surrounding parentheses from complete tags like
|
||||
"(polkit-agent):" → "polkit-agent", while preserving partial parentheses
|
||||
like "app(version):" unchanged
|
||||
- Maintain RFC compliance: both RFC3164 and RFC5424 allow parentheses
|
||||
- Preserve all existing functionality and edge case handling
|
||||
|
||||
Fixes #104
|
||||
|
||||
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
||||
---
|
||||
src/syslogd.c | 35 ++++++++++++++++++++++++++++-------
|
||||
1 file changed, 28 insertions(+), 7 deletions(-)
|
||||
|
||||
diff --git a/src/syslogd.c b/src/syslogd.c
|
||||
index fa82d98..37e1920 100644
|
||||
--- a/src/syslogd.c
|
||||
+++ b/src/syslogd.c
|
||||
@@ -1309,15 +1309,36 @@ parsemsg_rfc3164_app_name_procid(char **msg, char **app_name, char **procid)
|
||||
m = *msg;
|
||||
|
||||
/* Application name. */
|
||||
- app_name_begin = m;
|
||||
- app_name_length = strspn(m,
|
||||
- "abcdefghijklmnopqrstuvwxyz"
|
||||
- "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
- "0123456789"
|
||||
- "._-/");
|
||||
+ /* Check if tag is surrounded by parentheses like (polkit-agent) */
|
||||
+ if (*m == '(') {
|
||||
+ char *closing = strchr(m + 1, ')');
|
||||
+ if (closing && (closing[1] == ':' || closing[1] == '[' || isblank(closing[1]) || closing[1] == '\0')) {
|
||||
+ /* Found complete parenthetical tag, strip parentheses */
|
||||
+ app_name_begin = m + 1;
|
||||
+ app_name_length = closing - (m + 1);
|
||||
+ m = closing + 1;
|
||||
+ } else {
|
||||
+ /* Incomplete or malformed, treat normally */
|
||||
+ app_name_begin = m;
|
||||
+ app_name_length = strspn(m,
|
||||
+ "abcdefghijklmnopqrstuvwxyz"
|
||||
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
+ "0123456789"
|
||||
+ "._-/()");
|
||||
+ m += app_name_length;
|
||||
+ }
|
||||
+ } else {
|
||||
+ app_name_begin = m;
|
||||
+ app_name_length = strspn(m,
|
||||
+ "abcdefghijklmnopqrstuvwxyz"
|
||||
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
+ "0123456789"
|
||||
+ "._-/()");
|
||||
+ m += app_name_length;
|
||||
+ }
|
||||
+
|
||||
if (app_name_length == 0)
|
||||
goto bad;
|
||||
- m += app_name_length;
|
||||
|
||||
/* Process identifier (optional). */
|
||||
if (*m == '[') {
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
From 10372296093cc70f3d21b46359a4cfbd8312c968 Mon Sep 17 00:00:00 2001
|
||||
From: Joachim Wiberg <troglobit@gmail.com>
|
||||
Date: Fri, 26 Sep 2025 10:07:45 +0200
|
||||
Subject: [PATCH 3/4] test: verify parenthetical tag parsing
|
||||
Organization: Wires
|
||||
|
||||
Regression test for issue #104.
|
||||
|
||||
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
||||
---
|
||||
test/Makefile.am | 5 +-
|
||||
test/parens.sh | 119 +++++++++++++++++++++++++++++++++++++++++++++++
|
||||
2 files changed, 122 insertions(+), 2 deletions(-)
|
||||
create mode 100755 test/parens.sh
|
||||
|
||||
diff --git a/test/Makefile.am b/test/Makefile.am
|
||||
index 7162e6c..59a38cf 100644
|
||||
--- a/test/Makefile.am
|
||||
+++ b/test/Makefile.am
|
||||
@@ -3,10 +3,10 @@ EXTRA_DIST += api.sh local.sh unicode.sh remote.sh fwd.sh mark.sh \
|
||||
memleak.sh facility.sh notify.sh rotate_all.sh secure.sh \
|
||||
logger.sh listen.sh sighup.sh tag.sh hostname.sh \
|
||||
property.sh raw.sh regression.sh multicast.sh \
|
||||
- mcast-fwd.sh mcast-iface.sh
|
||||
+ mcast-fwd.sh mcast-iface.sh parens.sh
|
||||
CLEANFILES = *~ *.trs *.log
|
||||
TEST_EXTENSIONS = .sh
|
||||
-TESTS_ENVIRONMENT= unshare -mrun
|
||||
+TESTS_ENVIRONMENT= unshare -mrun --map-auto
|
||||
|
||||
check_PROGRAMS = api
|
||||
api_SOURCES = api.c
|
||||
@@ -30,6 +30,7 @@ TESTS += rotate_all.sh
|
||||
TESTS += secure.sh
|
||||
TESTS += sighup.sh
|
||||
TESTS += tag.sh
|
||||
+TESTS += parens.sh
|
||||
TESTS += hostname.sh
|
||||
TESTS += property.sh
|
||||
TESTS += raw.sh
|
||||
diff --git a/test/parens.sh b/test/parens.sh
|
||||
new file mode 100755
|
||||
index 0000000..43b8744
|
||||
--- /dev/null
|
||||
+++ b/test/parens.sh
|
||||
@@ -0,0 +1,119 @@
|
||||
+#!/bin/sh
|
||||
+# Verify parentheses handling in log tags for RFC3164 messages.
|
||||
+# Regression test for issue #104.
|
||||
+#
|
||||
+. "${srcdir:-.}/lib.sh"
|
||||
+
|
||||
+
|
||||
+MSG1="Failed to execute /usr/bin/pkttyagent: No such file or directory"
|
||||
+MSG2="Normal application message"
|
||||
+MSG3="Version specific message"
|
||||
+MSG4="Service startup message"
|
||||
+
|
||||
+LOGDIR="$DIR/log"
|
||||
+SYSLOG="${LOGDIR}/syslog"
|
||||
+
|
||||
+setup_syslogd()
|
||||
+{
|
||||
+ mkdir -p "$LOGDIR"
|
||||
+ cat <<-EOF >"${CONF}"
|
||||
+ # Log everything for testing
|
||||
+ *.* -$SYSLOG
|
||||
+ EOF
|
||||
+ setup -m0
|
||||
+}
|
||||
+
|
||||
+
|
||||
+extract_tag()
|
||||
+{
|
||||
+ msg="$1"
|
||||
+ actual_line=$(grep "$msg" "$SYSLOG" | tail -1)
|
||||
+ if [ -n "$actual_line" ]; then
|
||||
+ echo "$actual_line" | sed -n 's/.*[0-9][0-9] [^ ]* \([^:]*\):.*/\1/p'
|
||||
+ fi
|
||||
+}
|
||||
+
|
||||
+show_result()
|
||||
+{
|
||||
+ input="$1"
|
||||
+ expected="$2"
|
||||
+ got="$3"
|
||||
+
|
||||
+ echo "Input: '$input'"
|
||||
+ echo "Expected: '$expected'"
|
||||
+ echo "Got: '$got'"
|
||||
+}
|
||||
+
|
||||
+verify()
|
||||
+{
|
||||
+ input_tag="$1"
|
||||
+ expected_tag="$2"
|
||||
+ msg="$3"
|
||||
+ expected_pattern="${4:-$expected_tag}"
|
||||
+
|
||||
+ actual_tag=$(extract_tag "$msg")
|
||||
+ if [ -n "$actual_tag" ]; then
|
||||
+ show_result "$input_tag" "$expected_tag" "$actual_tag"
|
||||
+ if grep -q "$expected_pattern.*$msg" "$SYSLOG"; then
|
||||
+ return 0
|
||||
+ else
|
||||
+ echo "Log contents:"
|
||||
+ cat "$SYSLOG"
|
||||
+ return 1
|
||||
+ fi
|
||||
+ else
|
||||
+ echo "Message not found in log"
|
||||
+ echo "Log contents:"
|
||||
+ cat "$SYSLOG"
|
||||
+ return 1
|
||||
+ fi
|
||||
+}
|
||||
+
|
||||
+test_normal_tag()
|
||||
+{
|
||||
+ logger -b -ip user.info -t "normal-app" "$MSG2"
|
||||
+ verify "normal-app" "normal-app" "$MSG2"
|
||||
+}
|
||||
+
|
||||
+test_paren_stripping()
|
||||
+{
|
||||
+ logger -b -ip user.info -t "(polkit-agent)" "$MSG1"
|
||||
+ verify "(polkit-agent)" "polkit-agent" "$MSG1"
|
||||
+}
|
||||
+
|
||||
+test_service_stripping()
|
||||
+{
|
||||
+ logger -b -ip user.info -t "(service-name)" "$MSG4"
|
||||
+ verify "(service-name)" "service-name" "$MSG4"
|
||||
+}
|
||||
+
|
||||
+test_partial_parens()
|
||||
+{
|
||||
+ logger -b -ip user.info -t "app(version)" "$MSG3"
|
||||
+ verify "app(version)" "app(version)" "$MSG3"
|
||||
+}
|
||||
+
|
||||
+test_normal_tag_with_pid()
|
||||
+{
|
||||
+ pid="$$"
|
||||
+
|
||||
+ logger -b -ip user.info -t "normal-app[${pid}]" "$MSG2"
|
||||
+ verify "normal-app[${pid}]" "normal-app[${pid}]" "$MSG2" "normal-app\[${pid}\]"
|
||||
+}
|
||||
+
|
||||
+test_paren_with_pid()
|
||||
+{
|
||||
+ pid="$$"
|
||||
+
|
||||
+ logger -b -ip user.info -t "(polkit-agent)[$pid]" "$MSG1"
|
||||
+ verify "(polkit-agent)[$pid]" "polkit-agent[$pid]" "$MSG1" "polkit-agent\[$pid\]"
|
||||
+}
|
||||
+
|
||||
+
|
||||
+run_step "Set up syslogd for parentheses testing" setup_syslogd
|
||||
+run_step "Test parenthetical tag stripping" test_paren_stripping
|
||||
+run_step "Test normal tag preservation" test_normal_tag
|
||||
+run_step "Test partial parentheses preservation" test_partial_parens
|
||||
+run_step "Test service parenthetical tag" test_service_stripping
|
||||
+run_step "Test parenthetical tag with PID" test_paren_with_pid
|
||||
+run_step "Test normal tag with PID" test_normal_tag_with_pid
|
||||
--
|
||||
2.43.0
|
||||
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
From c1c3f253a8963c6e9e41fafcf9b888f3f5a34906 Mon Sep 17 00:00:00 2001
|
||||
From: Joachim Wiberg <troglobit@gmail.com>
|
||||
Date: Mon, 3 Nov 2025 13:11:56 +0100
|
||||
Subject: [PATCH 4/4] syslogd: unescape Linux /dev/kmsg messages before
|
||||
sanitization
|
||||
Organization: Wires
|
||||
|
||||
Linux's /dev/kmsg interface escapes all non-printable characters and
|
||||
backslashes using C-style hex encoding (\xHH format). Preventing the
|
||||
recent UTF-8 sanitization improvements from working correctly, since
|
||||
UTF-8 sequences arrive as escaped text like "\xe2\x80\x94" rather than
|
||||
as actual bytes, see [1] for details.
|
||||
|
||||
This commit implements "unescaping" of the kernel's C-style format
|
||||
before applying the UTF-8-aware sanitization. Ensuring that UTF-8
|
||||
content also in kernel messages is properly preserved when using the
|
||||
-8 flag.
|
||||
|
||||
[1]: https://www.kernel.org/doc/Documentation/ABI/testing/dev-kmsg
|
||||
|
||||
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
||||
---
|
||||
src/syslogd.c | 81 ++++++++++++++++++++++++++++++++++++++++++++++++---
|
||||
1 file changed, 77 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/src/syslogd.c b/src/syslogd.c
|
||||
index 37e1920..0b6bc30 100644
|
||||
--- a/src/syslogd.c
|
||||
+++ b/src/syslogd.c
|
||||
@@ -155,6 +155,7 @@ static int KernLog = 1; /* Track kernel logs by default */
|
||||
static int KeepKernFac; /* Keep remotely logged kernel facility */
|
||||
static int KeepKernTime; /* Keep kernel timestamp, evern after initial read */
|
||||
static int KeepKernConsole; /* Keep kernel logging to console */
|
||||
+static int IsLinuxKmsg; /* Set if reading from Linux /dev/kmsg */
|
||||
|
||||
static int rotate_opt; /* Set if command line option has been given (wins) */
|
||||
static off_t RotateSz = 0; /* Max file size (bytes) before rotating, disabled by default */
|
||||
@@ -651,8 +652,10 @@ int main(int argc, char *argv[])
|
||||
_PATH_KLOG);
|
||||
else
|
||||
kern_console_off();
|
||||
- } else
|
||||
+ } else {
|
||||
+ IsLinuxKmsg = 1;
|
||||
kern_console_off();
|
||||
+ }
|
||||
}
|
||||
no_klogd:
|
||||
consfile.f_type = F_CONSOLE;
|
||||
@@ -1062,6 +1065,61 @@ utf8_valid(const unsigned char *in, size_t len)
|
||||
return 1;
|
||||
}
|
||||
|
||||
+/*
|
||||
+ * Unescapes Linux /dev/kmsg messages that use C-style hex encoding.
|
||||
+ * Converts "\xHH" sequences back to bytes and "\\" back to "\".
|
||||
+ * Returns the new length of the unescaped string.
|
||||
+ */
|
||||
+static size_t kmsg_unescape(char *msg)
|
||||
+{
|
||||
+ char *src, *dst;
|
||||
+ int hi, lo;
|
||||
+
|
||||
+ src = dst = msg;
|
||||
+ while (*src) {
|
||||
+ if (*src == '\\' && src[1]) {
|
||||
+ if (src[1] == 'x' && src[2] && src[3]) {
|
||||
+ /* Decode \xHH */
|
||||
+ hi = src[2];
|
||||
+ lo = src[3];
|
||||
+
|
||||
+ /* Convert hex digits to values */
|
||||
+ if (hi >= '0' && hi <= '9')
|
||||
+ hi = hi - '0';
|
||||
+ else if (hi >= 'a' && hi <= 'f')
|
||||
+ hi = hi - 'a' + 10;
|
||||
+ else if (hi >= 'A' && hi <= 'F')
|
||||
+ hi = hi - 'A' + 10;
|
||||
+ else
|
||||
+ goto copy_literal;
|
||||
+
|
||||
+ if (lo >= '0' && lo <= '9')
|
||||
+ lo = lo - '0';
|
||||
+ else if (lo >= 'a' && lo <= 'f')
|
||||
+ lo = lo - 'a' + 10;
|
||||
+ else if (lo >= 'A' && lo <= 'F')
|
||||
+ lo = lo - 'A' + 10;
|
||||
+ else
|
||||
+ goto copy_literal;
|
||||
+
|
||||
+ *dst++ = (char)((hi << 4) | lo);
|
||||
+ src += 4;
|
||||
+ continue;
|
||||
+ } else if (src[1] == '\\') {
|
||||
+ /* Decode \\ to \ */
|
||||
+ *dst++ = '\\';
|
||||
+ src += 2;
|
||||
+ continue;
|
||||
+ }
|
||||
+ }
|
||||
+copy_literal:
|
||||
+ *dst++ = *src++;
|
||||
+ }
|
||||
+ *dst = '\0';
|
||||
+
|
||||
+ return dst - msg;
|
||||
+}
|
||||
+
|
||||
/*
|
||||
* Removes characters from log messages that are unsafe to display.
|
||||
* Preserves valid UTF-8 sequences, including BOM, with -8 flag.
|
||||
@@ -1773,9 +1831,24 @@ void printsys(char *msg)
|
||||
parsemsg_rfc3164_app_name_procid(&p, &buffer.app_name, &buffer.proc_id);
|
||||
|
||||
q = lp;
|
||||
- while (*p != '\0' && (c = *p++) != '\n' && q < &line[MAXLINE])
|
||||
- *q++ = c;
|
||||
- *q = '\0';
|
||||
+ if (IsLinuxKmsg) {
|
||||
+ char tmp[MAXLINE + 1];
|
||||
+ char *t = tmp;
|
||||
+
|
||||
+ while (*p != '\0' && *p != '\n' && t < &tmp[MAXLINE])
|
||||
+ *t++ = *p++;
|
||||
+ *t = '\0';
|
||||
+
|
||||
+ /* Unescape \xHH sequences and \\ */
|
||||
+ kmsg_unescape(tmp);
|
||||
+
|
||||
+ /* Sanitize the unescaped message with UTF-8 support */
|
||||
+ parsemsg_remove_unsafe_characters(tmp, lp, MAXLINE);
|
||||
+ } else {
|
||||
+ while (*p != '\0' && (c = *p++) != '\n' && q < &line[MAXLINE])
|
||||
+ *q++ = c;
|
||||
+ *q = '\0';
|
||||
+ }
|
||||
|
||||
logmsg(&buffer);
|
||||
}
|
||||
--
|
||||
2.43.0
|
||||
|
||||
+119
-20
@@ -6,17 +6,19 @@
|
||||
|
||||
#include "core.h"
|
||||
|
||||
#define XPATH_BASE_ "/ietf-syslog:syslog"
|
||||
#define XPATH_FILE_ XPATH_BASE_"/actions/file/log-file"
|
||||
#define XPATH_REMOTE_ XPATH_BASE_"/actions/remote/destination"
|
||||
#define XPATH_ROTATE_ XPATH_BASE_"/infix-syslog:file-rotation"
|
||||
#define XPATH_SERVER_ XPATH_BASE_"/infix-syslog:server"
|
||||
#define XPATH_BASE_ "/ietf-syslog:syslog"
|
||||
#define XPATH_FILE_ XPATH_BASE_"/actions/file"
|
||||
#define XPATH_LOG_FILE XPATH_BASE_"/actions/file/log-file"
|
||||
#define XPATH_REMOTE_ XPATH_BASE_"/actions/remote"
|
||||
#define XPATH_REMOTE_DST XPATH_BASE_"/actions/remote/destination"
|
||||
#define XPATH_ROTATE_ XPATH_BASE_"/infix-syslog:file-rotation"
|
||||
#define XPATH_SERVER_ XPATH_BASE_"/infix-syslog:server"
|
||||
|
||||
#define SYSLOG_D_ "/etc/syslog.d"
|
||||
#define SYSLOG_FILE SYSLOG_D_"/log-file-%s.conf"
|
||||
#define SYSLOG_REMOTE SYSLOG_D_"/remote-%s.conf"
|
||||
#define SYSLOG_ROTATE SYSLOG_D_"/rotate.conf"
|
||||
#define SYSLOG_SERVER SYSLOG_D_"/server.conf"
|
||||
#define SYSLOG_D_ "/etc/syslog.d"
|
||||
#define SYSLOG_FILE SYSLOG_D_"/log-file-%s.conf"
|
||||
#define SYSLOG_REMOTE SYSLOG_D_"/remote-%s.conf"
|
||||
#define SYSLOG_ROTATE SYSLOG_D_"/rotate.conf"
|
||||
#define SYSLOG_SERVER SYSLOG_D_"/server.conf"
|
||||
|
||||
struct addr {
|
||||
char *address;
|
||||
@@ -94,22 +96,86 @@ static const char *sxlate(const char *severity)
|
||||
|
||||
static size_t selector(sr_session_ctx_t *session, struct action *act)
|
||||
{
|
||||
char xpath[strlen(act->xpath) + 32];
|
||||
char xpath[strlen(act->xpath) + 64];
|
||||
bool has_pattern = false;
|
||||
sr_val_t *list = NULL;
|
||||
size_t count = 0;
|
||||
size_t num = 0;
|
||||
char *pattern;
|
||||
int rc;
|
||||
|
||||
/* Check for hostname-filter (infix-syslog augment) */
|
||||
snprintf(xpath, sizeof(xpath), "%s/infix-syslog:hostname-filter", act->xpath);
|
||||
rc = sr_get_items(session, xpath, 0, 0, &list, &count);
|
||||
if (rc == SR_ERR_OK && count > 0) {
|
||||
fprintf(act->fp, "+");
|
||||
for (size_t i = 0; i < count; i++)
|
||||
fprintf(act->fp, "%s%s", i ? "," : "", list[i].data.string_val);
|
||||
fprintf(act->fp, "\n");
|
||||
|
||||
sr_free_values(list, count);
|
||||
list = NULL;
|
||||
count = 0;
|
||||
}
|
||||
|
||||
/* Check for property-filter (infix-syslog augment) */
|
||||
char *property = srx_get_str(session, "%s/infix-syslog:property-filter/property", act->xpath);
|
||||
if (property) {
|
||||
char *operator = srx_get_str(session, "%s/infix-syslog:property-filter/operator", act->xpath);
|
||||
char *value = srx_get_str(session, "%s/infix-syslog:property-filter/value", act->xpath);
|
||||
char *case_str = srx_get_str(session, "%s/infix-syslog:property-filter/case-insensitive", act->xpath);
|
||||
char *negate_str = srx_get_str(session, "%s/infix-syslog:property-filter/negate", act->xpath);
|
||||
|
||||
if (operator && value) {
|
||||
/* Build operator prefix: [!][icase_]operator */
|
||||
char op_prefix[32] = "";
|
||||
|
||||
/* Only apply negate if explicitly set to true */
|
||||
if (negate_str && !strcmp(negate_str, "true"))
|
||||
strlcat(op_prefix, "!", sizeof(op_prefix));
|
||||
|
||||
/* Only apply icase_ if explicitly set to true */
|
||||
if (case_str && !strcmp(case_str, "true"))
|
||||
strlcat(op_prefix, "icase_", sizeof(op_prefix));
|
||||
|
||||
strlcat(op_prefix, operator, sizeof(op_prefix));
|
||||
|
||||
/* Property-based filter: :property, [!][icase_]operator, "value" */
|
||||
fprintf(act->fp, ":%s, %s, \"%s\"\n", property, op_prefix, value);
|
||||
has_pattern = true;
|
||||
}
|
||||
|
||||
free(property);
|
||||
free(operator);
|
||||
free(value);
|
||||
free(case_str);
|
||||
free(negate_str);
|
||||
}
|
||||
|
||||
/* Check for pattern-match (select-match feature) */
|
||||
pattern = srx_get_str(session, "%s/pattern-match", act->xpath);
|
||||
if (pattern) {
|
||||
/* Property-based filter: :msg, ereregex, "pattern" */
|
||||
fprintf(act->fp, ":msg, ereregex, \"%s\"\n", pattern);
|
||||
has_pattern = true;
|
||||
free(pattern);
|
||||
}
|
||||
|
||||
snprintf(xpath, sizeof(xpath), "%s/facility-filter/facility-list", act->xpath);
|
||||
rc = sr_get_items(session, xpath, 0, 0, &list, &count);
|
||||
if (rc != SR_ERR_OK) {
|
||||
ERROR("Cannot find facility-list for syslog file:%s: %s\n", act->name, sr_strerror(rc));
|
||||
if (rc || !count) {
|
||||
if (has_pattern) {
|
||||
fprintf(act->fp, "*.*");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
sr_val_t *entry = &list[i];
|
||||
char *facility, *severity;
|
||||
char *facility, *severity, *compare, *action_str;
|
||||
const char *prefix = "";
|
||||
|
||||
facility = srx_get_str(session, "%s/facility", entry->xpath);
|
||||
if (!facility)
|
||||
@@ -120,16 +186,45 @@ static size_t selector(sr_session_ctx_t *session, struct action *act)
|
||||
continue;
|
||||
}
|
||||
|
||||
fprintf(act->fp, "%s%s.%s", i ? ";" : "", fxlate(facility), sxlate(severity));
|
||||
/* Check for advanced-compare (select-adv-compare feature) */
|
||||
compare = srx_get_str(session, "%s/advanced-compare/compare", entry->xpath);
|
||||
action_str = srx_get_str(session, "%s/advanced-compare/action", entry->xpath);
|
||||
|
||||
/*
|
||||
* Handle compare + action combinations:
|
||||
* - compare: equals -> use '=' after dot (facility.=severity)
|
||||
* - compare: equals-or-higher (default) -> no prefix
|
||||
* - action: block/stop -> use '!' after dot (facility.!severity)
|
||||
* - action: log (default) -> no prefix modification
|
||||
*
|
||||
* Note: action block/stop takes precedence over compare equals
|
||||
*/
|
||||
/* First check compare */
|
||||
if (compare && strstr(compare, "equals") && !strstr(compare, "equals-or-higher")) {
|
||||
prefix = "=";
|
||||
}
|
||||
|
||||
/* Then check action (can override compare) */
|
||||
if (action_str) {
|
||||
const char *a = action_str;
|
||||
if (strstr(a, "block") || strstr(a, "stop")) {
|
||||
prefix = "!";
|
||||
}
|
||||
free(action_str);
|
||||
}
|
||||
|
||||
fprintf(act->fp, "%s%s.%s%s", i ? ";" : "", fxlate(facility), prefix, sxlate(severity));
|
||||
num++;
|
||||
|
||||
free(facility);
|
||||
free(severity);
|
||||
free(compare);
|
||||
}
|
||||
|
||||
sr_free_values(list, count);
|
||||
|
||||
return num;
|
||||
/* Return non-zero if we have either pattern or facilities */
|
||||
return has_pattern ? (num > 0 ? num : 1) : num;
|
||||
}
|
||||
|
||||
static void action(sr_session_ctx_t *session, const char *name, const char *xpath, struct addr *addr)
|
||||
@@ -142,10 +237,12 @@ static void action(sr_session_ctx_t *session, const char *name, const char *xpat
|
||||
char *sz, *cnt, *fmt;
|
||||
char opts[80] = "\t";
|
||||
char *sep = ";";
|
||||
const char *fn;
|
||||
|
||||
act.fp = fopen(filename(name, addr ? true : false, act.path, sizeof(act.path)), "w");
|
||||
fn = filename(name, addr ? true : false, act.path, sizeof(act.path));
|
||||
act.fp = fopen(fn, "w");
|
||||
if (!act.fp) {
|
||||
ERRNO("Failed opening %s", act.path);
|
||||
ERRNO("Failed opening %s", fn);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -234,7 +331,7 @@ static int file_change(sr_session_ctx_t *session, struct lyd_node *config,struct
|
||||
LYX_LIST_FOR_EACH(files, file, "log-file") {
|
||||
struct lyd_node *node = lydx_get_child(file, "name");
|
||||
enum lydx_op op = lydx_get_op(node);
|
||||
char path[512] = XPATH_FILE_;
|
||||
char path[512] = XPATH_LOG_FILE;
|
||||
const char *name;
|
||||
|
||||
name = getnm(node, path, sizeof(path));
|
||||
@@ -263,7 +360,7 @@ static int remote_change(sr_session_ctx_t *session, struct lyd_node *config, str
|
||||
LYX_LIST_FOR_EACH(dremotes, remote, "destination") {
|
||||
struct lyd_node *node = lydx_get_child(remote, "name");
|
||||
enum lydx_op op = lydx_get_op(node);
|
||||
char path[512] = XPATH_REMOTE_;
|
||||
char path[512] = XPATH_REMOTE_DST;
|
||||
const char *name;
|
||||
|
||||
name = getnm(node, path, sizeof(path));
|
||||
@@ -365,9 +462,11 @@ done:
|
||||
|
||||
return SR_ERR_OK;
|
||||
}
|
||||
|
||||
int ietf_syslog_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd)
|
||||
{
|
||||
int rc = SR_ERR_OK;
|
||||
|
||||
if ((rc = file_change(session, config, diff, event, confd)))
|
||||
return rc;
|
||||
if ((rc = remote_change(session, config, diff, event, confd)))
|
||||
|
||||
@@ -218,6 +218,7 @@ static int change_clock(sr_session_ctx_t *session, struct lyd_node *config, stru
|
||||
|
||||
if (diff && !lydx_get_xpathf(diff, XPATH_BASE_"/clock"))
|
||||
return SR_ERR_OK;
|
||||
|
||||
switch (event) {
|
||||
case SR_EV_ENABLED: /* first time, on register. */
|
||||
case SR_EV_CHANGE: /* regular change (copy cand running) */
|
||||
|
||||
@@ -18,8 +18,8 @@ MODULES=(
|
||||
"iana-if-type@2023-01-26.yang"
|
||||
# NOTE: ietf-tls-client must be version matched with ietf-tls-server, used by netopeer2!
|
||||
"ietf-tls-client@2023-12-28.yang"
|
||||
"ietf-syslog@2024-03-21.yang -e file-action -e file-limit-size -e remote-action"
|
||||
"infix-syslog@2024-07-19.yang"
|
||||
"ietf-syslog@2024-03-21.yang -e file-action -e file-limit-size -e remote-action -e select-adv-compare -e select-match"
|
||||
"infix-syslog@2025-11-17.yang"
|
||||
"iana-hardware@2018-03-13.yang"
|
||||
"ietf-hardware@2018-03-13.yang -e hardware-state -e hardware-sensor"
|
||||
"infix-hardware@2025-10-30.yang"
|
||||
|
||||
@@ -16,6 +16,11 @@ module infix-syslog {
|
||||
contact "kernelkit@googlegroups.com";
|
||||
description "Infix augments and deviations to ietf-syslog, draft 32.";
|
||||
|
||||
revision 2025-11-17 {
|
||||
description "Add hostname-filter support.";
|
||||
reference "internal";
|
||||
}
|
||||
|
||||
revision 2024-07-19 {
|
||||
description "Initial revision, based on IETF syslog YANG draft 32.
|
||||
The following changes have been made in this model:
|
||||
@@ -164,6 +169,89 @@ module infix-syslog {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
leaf-list hostname-filter {
|
||||
type inet:domain-name;
|
||||
description "Filter messages by hostname. Messages from the listed
|
||||
hostnames will be logged to this file.";
|
||||
}
|
||||
|
||||
container property-filter {
|
||||
presence "Enable property-based filtering";
|
||||
description "Advanced property-based message filtering. Allows filtering
|
||||
on specific message fields with various comparison operators.";
|
||||
|
||||
leaf property {
|
||||
type enumeration {
|
||||
enum msg {
|
||||
description "The message body (SYSLOG-MSG field).";
|
||||
}
|
||||
enum msgid {
|
||||
description "The RFC5424 message identifier (MSGID field).";
|
||||
}
|
||||
enum programname {
|
||||
description "The originating program or tag name.";
|
||||
}
|
||||
enum hostname {
|
||||
description "The message source hostname.";
|
||||
}
|
||||
enum source {
|
||||
description "The message source (alias for hostname).";
|
||||
}
|
||||
enum data {
|
||||
description "The RFC5424 structured data (SD field).";
|
||||
}
|
||||
}
|
||||
mandatory true;
|
||||
description "The message property to match against.";
|
||||
}
|
||||
|
||||
leaf operator {
|
||||
type enumeration {
|
||||
enum contains {
|
||||
description "Substring match (case-sensitive).";
|
||||
}
|
||||
enum isequal {
|
||||
description "Exact equality match.";
|
||||
}
|
||||
enum startswith {
|
||||
description "Prefix match.";
|
||||
}
|
||||
enum regex {
|
||||
description "Basic regular expression match.";
|
||||
}
|
||||
enum ereregex {
|
||||
description "Extended regular expression match (POSIX ERE).";
|
||||
}
|
||||
}
|
||||
mandatory true;
|
||||
description "The comparison operator to use.";
|
||||
}
|
||||
|
||||
leaf value {
|
||||
type string;
|
||||
mandatory true;
|
||||
description "The value to compare against.";
|
||||
}
|
||||
|
||||
leaf case-insensitive {
|
||||
type boolean;
|
||||
description "Perform case-insensitive comparison.";
|
||||
}
|
||||
|
||||
leaf negate {
|
||||
type boolean;
|
||||
description "Negate the comparison result. When set to true, the
|
||||
filter matches messages that do NOT satisfy the
|
||||
specified condition.
|
||||
|
||||
For example:
|
||||
- operator: contains, value: 'ERROR', negate: false
|
||||
→ matches messages containing 'ERROR'
|
||||
- operator: contains, value: 'ERROR', negate: true
|
||||
→ matches messages NOT containing 'ERROR'";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
augment "/syslog:syslog/syslog:actions/syslog:remote/syslog:destination" {
|
||||
|
||||
@@ -5,9 +5,29 @@ Tests verifying IETF standard syslog configuration and operation:
|
||||
|
||||
- Basic local syslog functionality and log generation
|
||||
- Remote syslog forwarding and network logging
|
||||
- Pattern matching using POSIX regex on message content
|
||||
- Advanced comparison operators and actions (equals vs equals-or-higher)
|
||||
- Hostname-based filtering for incoming messages
|
||||
- Property-based filtering with operators and negation
|
||||
|
||||
include::basic/Readme.adoc[]
|
||||
|
||||
<<<
|
||||
|
||||
include::remote/Readme.adoc[]
|
||||
|
||||
<<<
|
||||
|
||||
include::pattern_match/Readme.adoc[]
|
||||
|
||||
<<<
|
||||
|
||||
include::advanced_compare/Readme.adoc[]
|
||||
|
||||
<<<
|
||||
|
||||
include::hostname_filter/Readme.adoc[]
|
||||
|
||||
<<<
|
||||
|
||||
include::property_filter/Readme.adoc[]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
test.adoc
|
||||
@@ -0,0 +1,25 @@
|
||||
=== Syslog Advanced Compare
|
||||
|
||||
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ietf_syslog/advanced_compare]
|
||||
|
||||
==== Description
|
||||
|
||||
Verify the select-adv-compare feature: filtering syslog messages based on
|
||||
severity with advanced comparison operators (equals vs equals-or-higher) and
|
||||
actions (log vs block/stop).
|
||||
|
||||
==== Topology
|
||||
|
||||
image::topology.svg[Syslog Advanced Compare topology, align=center, scaledwidth=75%]
|
||||
|
||||
==== Sequence
|
||||
|
||||
. Set up topology and attach to target DUT
|
||||
. Clean up old log files from previous test runs
|
||||
. Configure syslog with advanced-compare
|
||||
. Send test messages at all severity levels
|
||||
. Verify exact-errors log contains only error messages
|
||||
. Verify no-debug log blocks all messages
|
||||
. Verify baseline log contains info and higher
|
||||
|
||||
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Syslog Advanced Compare
|
||||
|
||||
Verify the select-adv-compare feature: filtering syslog messages based on
|
||||
severity with advanced comparison operators (equals vs equals-or-higher) and
|
||||
actions (log vs block/stop).
|
||||
|
||||
"""
|
||||
|
||||
import infamy
|
||||
import time
|
||||
|
||||
with infamy.Test() as test:
|
||||
with test.step("Set up topology and attach to target DUT"):
|
||||
env = infamy.Env()
|
||||
target = env.attach("target", "mgmt")
|
||||
tgtssh = env.attach("target", "mgmt", "ssh")
|
||||
|
||||
with test.step("Clean up old log files from previous test runs"):
|
||||
tgtssh.runsh("sudo rm -f /var/log/{exact-errors,no-debug,baseline}")
|
||||
|
||||
with test.step("Configure syslog with advanced-compare"):
|
||||
target.put_config_dicts({
|
||||
"ietf-syslog": {
|
||||
"syslog": {
|
||||
"actions": {
|
||||
"file": {
|
||||
"log-file": [{
|
||||
"name": "file:exact-errors",
|
||||
"facility-filter": {
|
||||
"facility-list": [{
|
||||
"facility": "daemon",
|
||||
"severity": "error",
|
||||
"advanced-compare": {
|
||||
"compare": "equals"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
"name": "file:no-debug",
|
||||
"facility-filter": {
|
||||
"facility-list": [{
|
||||
"facility": "daemon",
|
||||
"severity": "debug",
|
||||
"advanced-compare": {
|
||||
"action": "block"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
"name": "file:baseline",
|
||||
"facility-filter": {
|
||||
"facility-list": [{
|
||||
"facility": "daemon",
|
||||
"severity": "info"
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
with test.step("Send test messages at all severity levels"):
|
||||
tgtssh.runsh("logger -t advtest -p daemon.emerg 'Emergency: system is unusable'")
|
||||
tgtssh.runsh("logger -t advtest -p daemon.alert 'Alert: immediate action required'")
|
||||
tgtssh.runsh("logger -t advtest -p daemon.crit 'Critical: critical condition'")
|
||||
tgtssh.runsh("logger -t advtest -p daemon.err 'Error: error condition'")
|
||||
tgtssh.runsh("logger -t advtest -p daemon.warning 'Warning: warning condition'")
|
||||
tgtssh.runsh("logger -t advtest -p daemon.notice 'Notice: normal but significant'")
|
||||
tgtssh.runsh("logger -t advtest -p daemon.info 'Info: informational message'")
|
||||
tgtssh.runsh("logger -t advtest -p daemon.debug 'Debug: debug-level message'")
|
||||
time.sleep(1)
|
||||
|
||||
with test.step("Verify exact-errors log contains only error messages"):
|
||||
rc = tgtssh.runsh("grep -c 'advtest' /var/log/exact-errors 2>/dev/null")
|
||||
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
|
||||
if count != 1:
|
||||
test.fail(f"Expected 1 message in /var/log/exact-errors (error only), got {count}")
|
||||
|
||||
rc = tgtssh.runsh("grep -q 'Error: error condition' /var/log/exact-errors 2>/dev/null")
|
||||
if rc.returncode != 0:
|
||||
test.fail("Expected error message in /var/log/exact-errors")
|
||||
|
||||
rc = tgtssh.runsh("grep -c 'Emergency\\|Alert\\|Critical' /var/log/exact-errors 2>/dev/null")
|
||||
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
|
||||
if count != 0:
|
||||
test.fail(f"Expected 0 higher severity messages in /var/log/exact-errors, got {count}")
|
||||
|
||||
with test.step("Verify no-debug log blocks all messages"):
|
||||
rc = tgtssh.runsh("grep -c 'advtest' /var/log/no-debug 2>/dev/null")
|
||||
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
|
||||
if count != 0:
|
||||
test.fail(f"Expected 0 messages in /var/log/no-debug (all blocked), got {count}")
|
||||
|
||||
with test.step("Verify baseline log contains info and higher"):
|
||||
rc = tgtssh.runsh("grep -c 'advtest' /var/log/baseline 2>/dev/null")
|
||||
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
|
||||
if count != 7:
|
||||
test.fail(f"Expected 7 messages in /var/log/baseline (info and higher), got {count}")
|
||||
|
||||
rc = tgtssh.runsh("grep -c 'Debug: debug-level' /var/log/baseline 2>/dev/null")
|
||||
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
|
||||
if count != 0:
|
||||
test.fail(f"Expected 0 debug messages in /var/log/baseline, got {count}")
|
||||
|
||||
rc = tgtssh.runsh("grep -q 'Info: informational' /var/log/baseline 2>/dev/null")
|
||||
if rc.returncode != 0:
|
||||
test.fail("Expected info message in /var/log/baseline")
|
||||
|
||||
test.succeed()
|
||||
@@ -0,0 +1 @@
|
||||
../../../infamy/topologies/1x1.dot
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Title: 1x1 Pages: 1 -->
|
||||
<svg width="424pt" height="45pt"
|
||||
viewBox="0.00 0.00 424.03 45.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 41)">
|
||||
<title>1x1</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-41 420.03,-41 420.03,4 -4,4"/>
|
||||
<!-- host -->
|
||||
<g id="node1" class="node">
|
||||
<title>host</title>
|
||||
<polygon fill="none" stroke="black" points="0,-0.5 0,-36.5 100,-36.5 100,-0.5 0,-0.5"/>
|
||||
<text text-anchor="middle" x="25" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||
<polyline fill="none" stroke="black" points="50,-0.5 50,-36.5 "/>
|
||||
<text text-anchor="middle" x="75" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
</g>
|
||||
<!-- target -->
|
||||
<g id="node2" class="node">
|
||||
<title>target</title>
|
||||
<polygon fill="none" stroke="black" points="300.03,-0.5 300.03,-36.5 416.03,-36.5 416.03,-0.5 300.03,-0.5"/>
|
||||
<text text-anchor="middle" x="325.03" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="350.03,-0.5 350.03,-36.5 "/>
|
||||
<text text-anchor="middle" x="383.03" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
|
||||
</g>
|
||||
<!-- host--target -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt--target:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M100,-18.5C100,-18.5 300.03,-18.5 300.03,-18.5"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -4,3 +4,15 @@
|
||||
|
||||
- name: Remote syslog
|
||||
case: remote/test.py
|
||||
|
||||
- name: Syslog Pattern Matching
|
||||
case: pattern_match/test.py
|
||||
|
||||
- name: Syslog Advanced Compare
|
||||
case: advanced_compare/test.py
|
||||
|
||||
- name: Syslog Hostname Filtering
|
||||
case: hostname_filter/test.py
|
||||
|
||||
- name: Syslog Property Filtering
|
||||
case: property_filter/test.py
|
||||
|
||||
@@ -4,7 +4,10 @@ ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ietf_syslog/basic]
|
||||
|
||||
==== Description
|
||||
|
||||
Add syslog actions to log to local files, then verify new log files have been created.
|
||||
Add syslog actions matching on different facility and severity levels to
|
||||
log to local files, then verify new log files have been created. In one
|
||||
case we use an absolute `file:/path/to/bar.log` and in another a relative
|
||||
path `file:foo`.
|
||||
|
||||
==== Topology
|
||||
|
||||
@@ -13,7 +16,7 @@ image::topology.svg[Syslog Basic topology, align=center, scaledwidth=75%]
|
||||
==== Sequence
|
||||
|
||||
. Set up topology and attach to target DUT
|
||||
. Configure syslog on DUT to log to files '/log/bar.log' (absolute path) and 'foo' (non-absolute).
|
||||
. Verify log files /var/log/foo and /var/log/bar.log have been created
|
||||
. Configure syslog on DUT to log to files '/log/bar.log' and 'foo'
|
||||
. Verify log files have been created
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Syslog Basic
|
||||
"""Syslog Basic
|
||||
|
||||
Add syslog actions matching on different facility and severity levels to
|
||||
log to local files, then verify new log files have been created. In one
|
||||
case we use an absolute `file:/path/to/bar.log` and in another a relative
|
||||
path `file:foo`.
|
||||
|
||||
Add syslog actions to log to local files, then verify new log files have been created.
|
||||
"""
|
||||
|
||||
import infamy
|
||||
@@ -16,51 +19,42 @@ with infamy.Test() as test:
|
||||
factory = env.get_password("target")
|
||||
address = target.get_mgmt_ip()
|
||||
|
||||
with test.step("Configure syslog on DUT to log to files '/log/bar.log' (absolute path) and 'foo' (non-absolute)."):
|
||||
with test.step("Configure syslog on DUT to log to files '/log/bar.log' and 'foo'"):
|
||||
target.put_config_dicts({
|
||||
"ietf-syslog": {
|
||||
"syslog": {
|
||||
"actions": {
|
||||
"file": {
|
||||
"log-file": [
|
||||
{
|
||||
"log-file": [{
|
||||
"name": "file:foo",
|
||||
"facility-filter": {
|
||||
"facility-list": [
|
||||
{
|
||||
"facility": "auth",
|
||||
"severity": "info"
|
||||
},
|
||||
{
|
||||
"facility": "authpriv",
|
||||
"severity": "debug"
|
||||
}
|
||||
]
|
||||
"facility-list": [{
|
||||
"facility": "auth",
|
||||
"severity": "info"
|
||||
}, {
|
||||
"facility": "authpriv",
|
||||
"severity": "debug"
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
}, {
|
||||
"name": "file:/log/bar.log",
|
||||
"facility-filter": {
|
||||
"facility-list": [
|
||||
{
|
||||
"facility": "all",
|
||||
"severity": "critical"
|
||||
},
|
||||
{
|
||||
"facility": "mail",
|
||||
"severity": "warning"
|
||||
}
|
||||
]
|
||||
"facility-list": [{
|
||||
"facility": "all",
|
||||
"severity": "critical"
|
||||
}, {
|
||||
"facility": "mail",
|
||||
"severity": "warning"
|
||||
}]
|
||||
}
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
with test.step("Verify log files /var/log/foo and /var/log/bar.log have been created"):
|
||||
with test.step("Verify log files have been created"):
|
||||
user = tgtssh.runsh("ls /var/log/{foo,bar.log}").stdout
|
||||
if "/var/log/foo" not in user:
|
||||
test.fail()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
test.adoc
|
||||
@@ -0,0 +1,25 @@
|
||||
=== Syslog Hostname Filtering
|
||||
|
||||
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ietf_syslog/hostname_filter]
|
||||
|
||||
==== Description
|
||||
|
||||
Verify hostname-filter feature: filtering incoming syslog messages based
|
||||
on the hostname. Tests log sink scenario with multiple remote clients.
|
||||
|
||||
==== Topology
|
||||
|
||||
image::topology.svg[Syslog Hostname Filtering topology, align=center, scaledwidth=75%]
|
||||
|
||||
==== Sequence
|
||||
|
||||
. Set up topology and attach to DUTs
|
||||
. Clean up old log files on server
|
||||
. Configure server as syslog sink with hostname filtering
|
||||
. Configure client to forward logs to server
|
||||
. Send log messages with different hostnames
|
||||
. Verify router1 log contains only router1 messages
|
||||
. Verify router2 log contains only router2 messages
|
||||
. Verify all-hosts log contains all messages
|
||||
|
||||
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Syslog Hostname Filtering
|
||||
|
||||
Verify hostname-filter feature: filtering incoming syslog messages based
|
||||
on the hostname. Tests log sink scenario with multiple remote clients.
|
||||
|
||||
"""
|
||||
|
||||
import infamy
|
||||
import time
|
||||
|
||||
TEST_MESSAGES = [
|
||||
("router1", "Message from router1"),
|
||||
("router1", "Another from router1"),
|
||||
("router2", "Message from router2"),
|
||||
("other", "Message from a different host"),
|
||||
]
|
||||
|
||||
with infamy.Test() as test:
|
||||
with test.step("Set up topology and attach to DUTs"):
|
||||
env = infamy.Env()
|
||||
client = env.attach("client", "mgmt")
|
||||
server = env.attach("server", "mgmt")
|
||||
clientssh = env.attach("client", "mgmt", "ssh")
|
||||
serverssh = env.attach("server", "mgmt", "ssh")
|
||||
|
||||
with test.step("Clean up old log files on server"):
|
||||
serverssh.runsh("sudo rm -f /var/log/{router1,router2,all-hosts}")
|
||||
|
||||
with test.step("Configure server as syslog sink with hostname filtering"):
|
||||
_, server_link = env.ltop.xlate("server", "link")
|
||||
|
||||
server.put_config_dicts({
|
||||
"ietf-interfaces": {
|
||||
"interfaces": {
|
||||
"interface": [{
|
||||
"name": server_link,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [{
|
||||
"ip": "10.0.0.1",
|
||||
"prefix-length": 24,
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
"ietf-syslog": {
|
||||
"syslog": {
|
||||
"actions": {
|
||||
"file": {
|
||||
"log-file": [{
|
||||
"name": "file:router1",
|
||||
"infix-syslog:hostname-filter": ["router1"],
|
||||
"facility-filter": {
|
||||
"facility-list": [{
|
||||
"facility": "all",
|
||||
"severity": "info"
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
"name": "file:router2",
|
||||
"infix-syslog:hostname-filter": ["router2"],
|
||||
"facility-filter": {
|
||||
"facility-list": [{
|
||||
"facility": "all",
|
||||
"severity": "info"
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
"name": "file:all-hosts",
|
||||
"facility-filter": {
|
||||
"facility-list": [{
|
||||
"facility": "all",
|
||||
"severity": "info"
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
"infix-syslog:server": {
|
||||
"enabled": True,
|
||||
"listen": {
|
||||
"udp": [{
|
||||
"port": 514,
|
||||
"address": "10.0.0.1"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
with test.step("Configure client to forward logs to server"):
|
||||
_, client_link = env.ltop.xlate("client", "link")
|
||||
|
||||
client.put_config_dicts({
|
||||
"ietf-interfaces": {
|
||||
"interfaces": {
|
||||
"interface": [{
|
||||
"name": client_link,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [{
|
||||
"ip": "10.0.0.2",
|
||||
"prefix-length": 24,
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
"ietf-syslog": {
|
||||
"syslog": {
|
||||
"actions": {
|
||||
"remote": {
|
||||
"destination": [{
|
||||
"name": "server",
|
||||
"udp": {
|
||||
"address": "10.0.0.1"
|
||||
},
|
||||
"facility-filter": {
|
||||
"facility-list": [{
|
||||
"facility": "all",
|
||||
"severity": "info"
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
with test.step("Send log messages with different hostnames"):
|
||||
for hostname, message in TEST_MESSAGES:
|
||||
clientssh.runsh(f"logger -t test -p daemon.info -H {hostname} -h 10.0.0.1 '{message}'")
|
||||
time.sleep(2)
|
||||
|
||||
with test.step("Verify router1 log contains only router1 messages"):
|
||||
rc = serverssh.runsh("cat /var/log/router1 2>/dev/null")
|
||||
log_content = rc.stdout if rc.returncode == 0 else ""
|
||||
|
||||
router1_messages = [msg for host, msg in TEST_MESSAGES if host == "router1"]
|
||||
missing = [msg for msg in router1_messages if msg not in log_content]
|
||||
if missing:
|
||||
test.fail(f"Missing router1 messages in /var/log/router1: {missing}")
|
||||
|
||||
unwanted_messages = [msg for host, msg in TEST_MESSAGES if host != "router1"]
|
||||
found = [msg for msg in unwanted_messages if msg in log_content]
|
||||
if found:
|
||||
test.fail(f"Found unwanted messages in /var/log/router1: {found}")
|
||||
|
||||
with test.step("Verify router2 log contains only router2 messages"):
|
||||
rc = serverssh.runsh("cat /var/log/router2 2>/dev/null")
|
||||
log_content = rc.stdout if rc.returncode == 0 else ""
|
||||
|
||||
router2_messages = [msg for host, msg in TEST_MESSAGES if host == "router2"]
|
||||
missing = [msg for msg in router2_messages if msg not in log_content]
|
||||
if missing:
|
||||
test.fail(f"Missing router2 messages in /var/log/router2: {missing}")
|
||||
|
||||
unwanted_messages = [msg for host, msg in TEST_MESSAGES if host != "router2"]
|
||||
found = [msg for msg in unwanted_messages if msg in log_content]
|
||||
if found:
|
||||
test.fail(f"Found unwanted messages in /var/log/router2: {found}")
|
||||
|
||||
with test.step("Verify all-hosts log contains all messages"):
|
||||
rc = serverssh.runsh("cat /var/log/all-hosts 2>/dev/null")
|
||||
log_content = rc.stdout if rc.returncode == 0 else ""
|
||||
|
||||
expected_messages = [msg for _, msg in TEST_MESSAGES]
|
||||
missing = [msg for msg in expected_messages if msg not in log_content]
|
||||
|
||||
if missing:
|
||||
test.fail(f"Missing messages in /var/log/all-hosts: {missing}")
|
||||
|
||||
test.succeed()
|
||||
@@ -0,0 +1 @@
|
||||
../remote/topology.dot
|
||||
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Title: 2x2 Pages: 1 -->
|
||||
<svg width="432pt" height="305pt"
|
||||
viewBox="0.00 0.00 432.03 304.62" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 300.62)">
|
||||
<title>2x2</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-300.62 428.03,-300.62 428.03,4 -4,4"/>
|
||||
<!-- host -->
|
||||
<g id="node1" class="node">
|
||||
<title>host</title>
|
||||
<polygon fill="none" stroke="black" points="0,-125.31 0,-171.31 108,-171.31 108,-125.31 0,-125.31"/>
|
||||
<text text-anchor="middle" x="25" y="-144.61" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||
<polyline fill="none" stroke="black" points="50,-125.31 50,-171.31 "/>
|
||||
<text text-anchor="middle" x="79" y="-156.11" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt1</text>
|
||||
<polyline fill="none" stroke="black" points="50,-148.31 108,-148.31 "/>
|
||||
<text text-anchor="middle" x="79" y="-133.11" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt2</text>
|
||||
</g>
|
||||
<!-- client -->
|
||||
<g id="node2" class="node">
|
||||
<title>client</title>
|
||||
<polygon fill="none" stroke="black" points="308.03,-250.12 308.03,-296.12 424.03,-296.12 424.03,-250.12 308.03,-250.12"/>
|
||||
<text text-anchor="middle" x="333.03" y="-280.92" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="308.03,-273.12 358.03,-273.12 "/>
|
||||
<text text-anchor="middle" x="333.03" y="-257.92" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
|
||||
<polyline fill="none" stroke="black" points="358.03,-250.12 358.03,-296.12 "/>
|
||||
<text text-anchor="middle" x="391.03" y="-269.42" font-family="DejaVu Sans Mono, Book" font-size="14.00">client</text>
|
||||
</g>
|
||||
<!-- host--client -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt1--client:mgmt</title>
|
||||
<path fill="none" stroke="lightgrey" stroke-width="2" d="M108,-160.31C108,-160.31 308.03,-285.12 308.03,-285.12"/>
|
||||
</g>
|
||||
<!-- server -->
|
||||
<g id="node3" class="node">
|
||||
<title>server</title>
|
||||
<polygon fill="none" stroke="black" points="308.03,-0.5 308.03,-46.5 424.03,-46.5 424.03,-0.5 308.03,-0.5"/>
|
||||
<text text-anchor="middle" x="333.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
|
||||
<polyline fill="none" stroke="black" points="308.03,-23.5 358.03,-23.5 "/>
|
||||
<text text-anchor="middle" x="333.03" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="358.03,-0.5 358.03,-46.5 "/>
|
||||
<text text-anchor="middle" x="391.03" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">server</text>
|
||||
</g>
|
||||
<!-- host--server -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:mgmt2--server:mgmt</title>
|
||||
<path fill="none" stroke="lightgrey" stroke-width="2" d="M108,-136.31C108,-136.31 308.03,-11.5 308.03,-11.5"/>
|
||||
</g>
|
||||
<!-- client--server -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>client:link--server:link</title>
|
||||
<path fill="none" stroke="black" stroke-width="2" d="M333.03,-250.12C333.03,-250.12 333.03,-46.5 333.03,-46.5"/>
|
||||
<text text-anchor="middle" x="292.03" y="-50.3" font-family="DejaVu Serif, Book" font-size="14.00">10.0.0.1/24</text>
|
||||
<text text-anchor="middle" x="292.03" y="-238.92" font-family="DejaVu Serif, Book" font-size="14.00">10.0.0.2/24</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1 @@
|
||||
test.adoc
|
||||
@@ -0,0 +1,25 @@
|
||||
=== Syslog Pattern Matching
|
||||
|
||||
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ietf_syslog/pattern_match]
|
||||
|
||||
==== Description
|
||||
|
||||
Verify the select-match feature: filtering syslog messages based on
|
||||
pattern-match (POSIX regex) on message content. Tests both simple
|
||||
substring matching and complex regex patterns.
|
||||
|
||||
==== Topology
|
||||
|
||||
image::topology.svg[Syslog Pattern Matching topology, align=center, scaledwidth=75%]
|
||||
|
||||
==== Sequence
|
||||
|
||||
. Set up topology and attach to target DUT
|
||||
. Clean up old log files from previous test runs
|
||||
. Configure syslog with pattern-match filters
|
||||
. Send test messages with various patterns
|
||||
. Verify errors log contains ERROR and CRITICAL messages
|
||||
. Verify routers log contains matching router[0-9]+ pattern
|
||||
. Verify all-messages log contains all test messages
|
||||
|
||||
|
||||
Executable
+103
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Syslog Pattern Matching
|
||||
|
||||
Verify the select-match feature: filtering syslog messages based on
|
||||
pattern-match (POSIX regex) on message content. Tests both simple
|
||||
substring matching and complex regex patterns.
|
||||
"""
|
||||
|
||||
import infamy
|
||||
import time
|
||||
|
||||
with infamy.Test() as test:
|
||||
with test.step("Set up topology and attach to target DUT"):
|
||||
env = infamy.Env()
|
||||
target = env.attach("target", "mgmt")
|
||||
tgtssh = env.attach("target", "mgmt", "ssh")
|
||||
|
||||
with test.step("Clean up old log files from previous test runs"):
|
||||
tgtssh.runsh("sudo rm -f /var/log/{errors,routers,all-messages}")
|
||||
|
||||
with test.step("Configure syslog with pattern-match filters"):
|
||||
target.put_config_dicts({
|
||||
"ietf-syslog": {
|
||||
"syslog": {
|
||||
"actions": {
|
||||
"file": {
|
||||
"log-file": [{
|
||||
"name": "file:errors",
|
||||
"pattern-match": "ERROR|CRITICAL",
|
||||
}, {
|
||||
"name": "file:routers",
|
||||
"pattern-match": "router[0-9]+",
|
||||
"facility-filter": {
|
||||
"facility-list": [{
|
||||
"facility": "all",
|
||||
"severity": "info"
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
"name": "file:all-messages",
|
||||
"facility-filter": {
|
||||
"facility-list": [{
|
||||
"facility": "all",
|
||||
"severity": "info"
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
with test.step("Send test messages with various patterns"):
|
||||
tgtssh.runsh("logger -t test -p daemon.info 'ERROR: Connection failed on interface eth0'")
|
||||
tgtssh.runsh("logger -t test -p daemon.info 'CRITICAL: System temperature high'")
|
||||
tgtssh.runsh("logger -t test -p daemon.info 'Status update from router1: link up'")
|
||||
tgtssh.runsh("logger -t test -p daemon.info 'Status update from router42: link down'")
|
||||
tgtssh.runsh("logger -t test -p daemon.info 'INFO: Normal operation message'")
|
||||
tgtssh.runsh("logger -t test -p daemon.info 'DEBUG: Verbose logging enabled'")
|
||||
time.sleep(1)
|
||||
|
||||
with test.step("Verify errors log contains ERROR and CRITICAL messages"):
|
||||
rc = tgtssh.runsh("grep -c 'ERROR\\|CRITICAL' /var/log/errors 2>/dev/null")
|
||||
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
|
||||
if count != 2:
|
||||
test.fail(f"Expected 2 ERROR/CRITICAL messages in /var/log/errors, got {count}")
|
||||
|
||||
# Verify it does NOT contain other messages
|
||||
rc = tgtssh.runsh("grep -c 'router1\\|Normal operation\\|Verbose' /var/log/errors 2>/dev/null")
|
||||
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
|
||||
if count != 0:
|
||||
test.fail(f"Expected 0 non-error messages in /var/log/errors, got {count}")
|
||||
|
||||
with test.step("Verify routers log contains matching router[0-9]+ pattern"):
|
||||
rc = tgtssh.runsh("grep -c 'router[0-9]\\+' /var/log/routers 2>/dev/null")
|
||||
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
|
||||
if count != 2:
|
||||
test.fail(f"Expected 2 router messages in /var/log/routers, got {count}")
|
||||
|
||||
# Verify both router1 and router42 are present
|
||||
rc = tgtssh.runsh("grep -q 'router1' /var/log/routers 2>/dev/null")
|
||||
if rc.returncode != 0:
|
||||
test.fail("Expected router1 message in /var/log/routers")
|
||||
|
||||
rc = tgtssh.runsh("grep -q 'router42' /var/log/routers 2>/dev/null")
|
||||
if rc.returncode != 0:
|
||||
test.fail("Expected router42 message in /var/log/routers")
|
||||
|
||||
# Verify it does NOT contain error or normal messages
|
||||
rc = tgtssh.runsh("grep -c 'ERROR\\|CRITICAL\\|Normal operation\\|Verbose' /var/log/routers 2>/dev/null")
|
||||
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
|
||||
if count != 0:
|
||||
test.fail(f"Expected 0 non-router messages in /var/log/routers, got {count}")
|
||||
|
||||
with test.step("Verify all-messages log contains all test messages"):
|
||||
rc = tgtssh.runsh("grep -c 'test' /var/log/all-messages 2>/dev/null")
|
||||
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
|
||||
if count != 6:
|
||||
test.fail(f"Expected 6 total messages in /var/log/all-messages, got {count}")
|
||||
|
||||
test.succeed()
|
||||
@@ -0,0 +1 @@
|
||||
../../../infamy/topologies/1x1.dot
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Title: 1x1 Pages: 1 -->
|
||||
<svg width="424pt" height="45pt"
|
||||
viewBox="0.00 0.00 424.03 45.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 41)">
|
||||
<title>1x1</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-41 420.03,-41 420.03,4 -4,4"/>
|
||||
<!-- host -->
|
||||
<g id="node1" class="node">
|
||||
<title>host</title>
|
||||
<polygon fill="none" stroke="black" points="0,-0.5 0,-36.5 100,-36.5 100,-0.5 0,-0.5"/>
|
||||
<text text-anchor="middle" x="25" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||
<polyline fill="none" stroke="black" points="50,-0.5 50,-36.5 "/>
|
||||
<text text-anchor="middle" x="75" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
</g>
|
||||
<!-- target -->
|
||||
<g id="node2" class="node">
|
||||
<title>target</title>
|
||||
<polygon fill="none" stroke="black" points="300.03,-0.5 300.03,-36.5 416.03,-36.5 416.03,-0.5 300.03,-0.5"/>
|
||||
<text text-anchor="middle" x="325.03" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="350.03,-0.5 350.03,-36.5 "/>
|
||||
<text text-anchor="middle" x="383.03" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
|
||||
</g>
|
||||
<!-- host--target -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt--target:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M100,-18.5C100,-18.5 300.03,-18.5 300.03,-18.5"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1 @@
|
||||
test.adoc
|
||||
@@ -0,0 +1,25 @@
|
||||
=== Syslog Property Filtering
|
||||
|
||||
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ietf_syslog/property_filter]
|
||||
|
||||
==== Description
|
||||
|
||||
Verify property-filter feature: filtering syslog messages based on various
|
||||
message properties with different operators, case-insensitivity, and negation.
|
||||
|
||||
==== Topology
|
||||
|
||||
image::topology.svg[Syslog Property Filtering topology, align=center, scaledwidth=75%]
|
||||
|
||||
==== Sequence
|
||||
|
||||
. Set up topology and attach to target DUT
|
||||
. Clean up old log files
|
||||
. Configure syslog with property filters
|
||||
. Send test messages
|
||||
. Verify myapp log contains only myapp messages
|
||||
. Verify not-error log excludes ERROR messages
|
||||
. Verify case-test log matches case-insensitive 'warning'
|
||||
. Verify baseline log contains all messages
|
||||
|
||||
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Syslog Property Filtering
|
||||
|
||||
Verify property-filter feature: filtering syslog messages based on various
|
||||
message properties with different operators, case-insensitivity, and negation.
|
||||
|
||||
"""
|
||||
|
||||
import infamy
|
||||
import time
|
||||
|
||||
TEST_MESSAGES = [
|
||||
("myapp", "Application startup"),
|
||||
("myapp", "Processing request"),
|
||||
("otherapp", "Different program"),
|
||||
("test", "ERROR: Connection failed"),
|
||||
("test", "INFO: Normal message"),
|
||||
("test", "WARNING: Check config"),
|
||||
("test", "Warning: lowercase"),
|
||||
]
|
||||
|
||||
with infamy.Test() as test:
|
||||
with test.step("Set up topology and attach to target DUT"):
|
||||
env = infamy.Env()
|
||||
target = env.attach("target", "mgmt")
|
||||
tgtssh = env.attach("target", "mgmt", "ssh")
|
||||
|
||||
with test.step("Clean up old log files"):
|
||||
tgtssh.runsh("sudo rm -f /var/log/{myapp,not-error,case-test,baseline}")
|
||||
|
||||
with test.step("Configure syslog with property filters"):
|
||||
target.put_config_dicts({
|
||||
"ietf-syslog": {
|
||||
"syslog": {
|
||||
"actions": {
|
||||
"file": {
|
||||
"log-file": [{
|
||||
"name": "file:myapp",
|
||||
"infix-syslog:property-filter": {
|
||||
"property": "programname",
|
||||
"operator": "isequal",
|
||||
"value": "myapp"
|
||||
},
|
||||
"facility-filter": {
|
||||
"facility-list": [{
|
||||
"facility": "all",
|
||||
"severity": "info"
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
"name": "file:not-error",
|
||||
"infix-syslog:property-filter": {
|
||||
"property": "msg",
|
||||
"operator": "contains",
|
||||
"value": "ERROR",
|
||||
"negate": True
|
||||
},
|
||||
"facility-filter": {
|
||||
"facility-list": [{
|
||||
"facility": "all",
|
||||
"severity": "info"
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
"name": "file:case-test",
|
||||
"infix-syslog:property-filter": {
|
||||
"property": "msg",
|
||||
"operator": "contains",
|
||||
"value": "warning",
|
||||
"case-insensitive": True
|
||||
},
|
||||
"facility-filter": {
|
||||
"facility-list": [{
|
||||
"facility": "all",
|
||||
"severity": "info"
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
"name": "file:baseline",
|
||||
"facility-filter": {
|
||||
"facility-list": [{
|
||||
"facility": "all",
|
||||
"severity": "info"
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
with test.step("Send test messages"):
|
||||
for tag, msg in TEST_MESSAGES:
|
||||
tgtssh.runsh(f"logger -t {tag} -p daemon.info '{msg}'")
|
||||
time.sleep(1)
|
||||
|
||||
with test.step("Verify myapp log contains only myapp messages"):
|
||||
rc = tgtssh.runsh("grep -c 'myapp' /var/log/myapp 2>/dev/null")
|
||||
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
|
||||
if count != 2:
|
||||
test.fail(f"Expected 2 myapp messages in /var/log/myapp, got {count}")
|
||||
|
||||
rc = tgtssh.runsh("grep -c 'otherapp' /var/log/myapp 2>/dev/null")
|
||||
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
|
||||
if count != 0:
|
||||
test.fail(f"Expected 0 otherapp messages in /var/log/myapp, got {count}")
|
||||
|
||||
with test.step("Verify not-error log excludes ERROR messages"):
|
||||
rc = tgtssh.runsh("grep -c 'test' /var/log/not-error 2>/dev/null")
|
||||
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
|
||||
if count != 3:
|
||||
test.fail(f"Expected 3 non-ERROR messages in /var/log/not-error, got {count}")
|
||||
|
||||
rc = tgtssh.runsh("grep -c 'ERROR' /var/log/not-error 2>/dev/null")
|
||||
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
|
||||
if count != 0:
|
||||
test.fail(f"Expected 0 ERROR messages in /var/log/not-error, got {count}")
|
||||
|
||||
with test.step("Verify case-test log matches case-insensitive 'warning'"):
|
||||
rc = tgtssh.runsh("grep -c 'WARNING\\|Warning' /var/log/case-test 2>/dev/null")
|
||||
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
|
||||
if count != 2:
|
||||
test.fail(f"Expected 2 warning messages in /var/log/case-test, got {count}")
|
||||
|
||||
with test.step("Verify baseline log contains all messages"):
|
||||
for tag, msg in TEST_MESSAGES:
|
||||
rc = tgtssh.runsh(f"grep -q '{msg}' /var/log/baseline 2>/dev/null")
|
||||
if rc.returncode != 0:
|
||||
test.fail(f"Expected message '{msg}' not found in /var/log/baseline")
|
||||
|
||||
test.succeed()
|
||||
@@ -0,0 +1 @@
|
||||
../../../infamy/topologies/1x1.dot
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Title: 1x1 Pages: 1 -->
|
||||
<svg width="424pt" height="45pt"
|
||||
viewBox="0.00 0.00 424.03 45.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 41)">
|
||||
<title>1x1</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-41 420.03,-41 420.03,4 -4,4"/>
|
||||
<!-- host -->
|
||||
<g id="node1" class="node">
|
||||
<title>host</title>
|
||||
<polygon fill="none" stroke="black" points="0,-0.5 0,-36.5 100,-36.5 100,-0.5 0,-0.5"/>
|
||||
<text text-anchor="middle" x="25" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||
<polyline fill="none" stroke="black" points="50,-0.5 50,-36.5 "/>
|
||||
<text text-anchor="middle" x="75" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
</g>
|
||||
<!-- target -->
|
||||
<g id="node2" class="node">
|
||||
<title>target</title>
|
||||
<polygon fill="none" stroke="black" points="300.03,-0.5 300.03,-36.5 416.03,-36.5 416.03,-0.5 300.03,-0.5"/>
|
||||
<text text-anchor="middle" x="325.03" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="350.03,-0.5 350.03,-36.5 "/>
|
||||
<text text-anchor="middle" x="383.03" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
|
||||
</g>
|
||||
<!-- host--target -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt--target:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M100,-18.5C100,-18.5 300.03,-18.5 300.03,-18.5"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -30,15 +30,13 @@ with infamy.Test() as test:
|
||||
"name": client_link,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [
|
||||
{
|
||||
"address": [{
|
||||
"ip": "10.0.0.2",
|
||||
"prefix-length": 24,
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
}
|
||||
]}
|
||||
}]
|
||||
}
|
||||
},
|
||||
"ietf-syslog": {
|
||||
"syslog": {
|
||||
@@ -47,100 +45,81 @@ with infamy.Test() as test:
|
||||
"log-file": [{
|
||||
"name": "file:security",
|
||||
"facility-filter": {
|
||||
"facility-list": [
|
||||
{
|
||||
"facility-list": [{
|
||||
"facility": "auth",
|
||||
"severity": "all"
|
||||
},
|
||||
{
|
||||
}, {
|
||||
"facility": "audit",
|
||||
"severity": "all"
|
||||
}
|
||||
]
|
||||
}]
|
||||
},
|
||||
"infix-syslog:log-format": "rfc5424"
|
||||
}]
|
||||
},
|
||||
"remote": {
|
||||
"destination": [
|
||||
{
|
||||
"destination": [{
|
||||
"name": "server",
|
||||
"udp": {
|
||||
"address": "10.0.0.1"
|
||||
},
|
||||
"facility-filter": {
|
||||
"facility-list": [
|
||||
{
|
||||
"facility-list": [{
|
||||
"facility": "audit",
|
||||
"severity": "all"
|
||||
},
|
||||
{
|
||||
}, {
|
||||
"facility": "auth",
|
||||
"severity": "all"
|
||||
}]
|
||||
},
|
||||
"infix-syslog:log-format": "rfc5424"
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
server.put_config_dicts({
|
||||
"ietf-interfaces": {
|
||||
"interfaces": {
|
||||
"interface": [
|
||||
{
|
||||
"name": server_link,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [
|
||||
{
|
||||
"ip": "10.0.0.1",
|
||||
"prefix-length": 24,
|
||||
}
|
||||
]
|
||||
}
|
||||
"interface": [{
|
||||
"name": server_link,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [{
|
||||
"ip": "10.0.0.1",
|
||||
"prefix-length": 24,
|
||||
}]
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
},
|
||||
"ietf-syslog": {
|
||||
"syslog": {
|
||||
"actions": {
|
||||
"file": {
|
||||
"log-file": [
|
||||
{
|
||||
"name": "file:security",
|
||||
"facility-filter": {
|
||||
"facility-list": [
|
||||
{
|
||||
"facility": "auth",
|
||||
"severity": "all"
|
||||
},
|
||||
{
|
||||
"facility": "audit",
|
||||
"severity": "all"
|
||||
}
|
||||
]
|
||||
},
|
||||
"infix-syslog:log-format": "rfc5424"
|
||||
}
|
||||
]
|
||||
"log-file": [{
|
||||
"name": "file:security",
|
||||
"facility-filter": {
|
||||
"facility-list": [{
|
||||
"facility": "auth",
|
||||
"severity": "all"
|
||||
}, {
|
||||
"facility": "audit",
|
||||
"severity": "all"
|
||||
}]
|
||||
},
|
||||
"infix-syslog:log-format": "rfc5424"
|
||||
}]
|
||||
}
|
||||
},
|
||||
"infix-syslog:server": {
|
||||
"enabled": True,
|
||||
"listen": {
|
||||
"udp": [
|
||||
{
|
||||
"port": 514,
|
||||
"address": "10.0.0.1"
|
||||
}
|
||||
]
|
||||
"udp": [{
|
||||
"port": 514,
|
||||
"address": "10.0.0.1"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,7 +127,6 @@ with infamy.Test() as test:
|
||||
})
|
||||
|
||||
with test.step("Send security:notice log message from client and verify reception of client log message, incl. sorting to /log/security on server"):
|
||||
infamy.until(lambda: syslog_check(clientssh, serverssh) == True)
|
||||
|
||||
infamy.until(lambda: syslog_check(clientssh, serverssh) == True)
|
||||
|
||||
test.succeed()
|
||||
|
||||
Reference in New Issue
Block a user