cmds: Add a simple syslog daemon.
This will let us extend telemetry gathering to mobile apps. The initial
use case will be isoping tests run by the Android Fiber TV app.
Change-Id: I155fec280ebf4a544aa5cb31a36dc88ad27ff1be
diff --git a/Makefile b/Makefile
index 146023b..f42685c 100644
--- a/Makefile
+++ b/Makefile
@@ -16,11 +16,12 @@
BUILD_SIGNING?= # default off: needs libgtest
BUILD_JSONPOLL?=n
BUILD_BOUNCER?= # default off: costly
+BUILD_SYSLOG?= # default off: new
BUILD_PRESTERASTATS?=n
export BUILD_HNVRAM BUILD_SSDP BUILD_DNSSD BUILD_LOGUPLOAD \
BUILD_BLUETOOTH BUILD_WAVEGUIDE BUILD_DVBUTILS BUILD_SYSMGR \
BUILD_STATUTILS BUILD_CRYPTDEV BUILD_SIGNING BUILD_JSONPOLL \
- BUILD_PRESTERASTATS BUILD_CACHE_WARMING BUILD_BOUNCER
+ BUILD_PRESTERASTATS BUILD_CACHE_WARMING BUILD_BOUNCER BUILD_SYSLOG
# note: libgpio is not built here. It's conditionally built
# via buildroot/packages/google/google_platform/google_platform.mk
diff --git a/cmds/.gitignore b/cmds/.gitignore
index fcc39f5..f881e18 100644
--- a/cmds/.gitignore
+++ b/cmds/.gitignore
@@ -1,6 +1,7 @@
*.o
*.tmp.*
alivemonitor
+anonid
asus_hosts
balloon
bsa2bluez
@@ -43,6 +44,7 @@
statcatcher
statpitcher
stun
+syslogd
test_dir
test_file
udpburst
diff --git a/cmds/Makefile b/cmds/Makefile
index b8ff87e..848b417 100644
--- a/cmds/Makefile
+++ b/cmds/Makefile
@@ -56,7 +56,6 @@
wait-until-created \
watch-dir
-HOST_TARGETS=$(addprefix host-,$(TARGETS)) host-isoping_fuzz
LIB_TARGETS=\
stdoutline.so
HOST_TEST_TARGETS=\
@@ -94,6 +93,12 @@
TARGETS += statpitcher statcatcher
endif
+ifeq ($(BUILD_SYSLOG),y)
+TARGETS += syslogd
+endif
+
+HOST_TARGETS=$(addprefix host-,$(TARGETS)) host-isoping_fuzz
+
AS=$(CROSS_COMPILE)as
CC=$(CROSS_COMPILE)gcc
CXX=$(CROSS_COMPILE)g++
diff --git a/cmds/syslogd.py b/cmds/syslogd.py
new file mode 100755
index 0000000..97f5628
--- /dev/null
+++ b/cmds/syslogd.py
@@ -0,0 +1,86 @@
+#!/usr/bin/python
+"""syslogd: a simple syslog daemon."""
+
+import errno
+import os
+import re
+import socket
+import sys
+
+import options
+
+optspec = """
+syslogd [-f FILTERFILE] [-l listenip] [-p port]
+--
+f,filters= path to a file containing filters [/etc/syslog.conf]
+l,listenip= IP address to listen on [::]
+p,port= UDP port to listen for syslog messages on [5514]
+v,verbose increase log output
+"""
+
+
+MAX_LENGTH = 1180
+
+
+def serve(sock, filters, verbose=False):
+ """Handle a raw syslog message on the wire."""
+ while True:
+ raw_message, address = sock.recvfrom(4096)
+ client_ip = address[0]
+
+ # util-linux `logger` writes a trailing NUL;
+ # /dev/log required this long ago.
+ message = raw_message.strip('\x00\r\n')
+
+ # Require only printable 7-bit ASCII to remain (RFC3164).
+ if not all(32 <= ord(c) <= 126 for c in message):
+ print >>sys.stderr, ('[%s]: discarded message with unprintable characters'
+ % client_ip)
+ continue
+
+ if len(message) > MAX_LENGTH:
+ print >>sys.stderr, ('[%s]: discarded %dB message over max length %dB'
+ % (client_ip, len(message), MAX_LENGTH))
+ continue
+
+ for f in filters:
+ if f.search(message):
+ if verbose:
+ print >>sys.stderr, 'matched by filter: %r' % f.pattern
+
+ print '[%s]: %s' % (client_ip, message)
+ sys.stdout.flush()
+ break
+ else:
+ if verbose:
+ print >>sys.stderr, ('[%s]: discarded unrecognized message: %s'
+ % (client_ip, message))
+ else:
+ print >>sys.stderr, '[%s]: discarded unrecognized message' % client_ip
+
+
+def main(argv):
+ o = options.Options(optspec)
+ opt, unused_flags, unused_extra = o.parse(argv)
+
+ if opt.filters:
+ filter_patterns = open(opt.filters).read().splitlines()
+ filters = [re.compile(p) for p in filter_patterns]
+ print >>sys.stderr, 'using filters: %r' % filter_patterns
+
+ sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
+ sock.bind((opt.listenip, opt.port))
+
+ print >>sys.stderr, 'listening on UDP [%s]:%d' % (opt.listenip, opt.port)
+ try:
+ os.makedirs('/tmp/syslogd')
+ except OSError as e:
+ if e.errno != errno.EEXIST:
+ raise
+
+ open('/tmp/syslogd/ready', 'w')
+ serve(sock, filters, opt.verbose)
+
+
+if __name__ == '__main__':
+ main(sys.argv)
diff --git a/cmds/test-syslogd.py b/cmds/test-syslogd.py
new file mode 100755
index 0000000..7235aaf
--- /dev/null
+++ b/cmds/test-syslogd.py
@@ -0,0 +1,100 @@
+#!/usr/bin/python
+
+"""Tests for the syslogd program."""
+
+import errno
+import os
+import re
+import select
+import socket
+import subprocess
+import tempfile
+import time
+
+from wvtest.wvtest import WVFAIL
+from wvtest.wvtest import WVPASS
+from wvtest.wvtest import wvtest
+from wvtest.wvtest import wvtest_main
+
+
+def ChompLeadingIP(line):
+ _, message = re.split(r'^\[.*\]: ', line)
+ return message
+
+
+@wvtest
+def TestSyslogd():
+ """spin up and test a syslogd server."""
+ subprocess.call(['pkill', '-f', 'python syslogd.py'])
+ try:
+ os.remove('/tmp/syslogd/ready')
+ except OSError as e:
+ if e.errno != errno.ENOENT: raise
+
+ filters = tempfile.NamedTemporaryFile(bufsize=0, suffix='.conf', delete=False)
+ print >>filters, 'PASS'
+ filters.close()
+
+ out_r, out_w = os.pipe()
+ err_r, err_w = os.pipe()
+ subprocess.Popen(['python', 'syslogd.py', '-f', filters.name, '-v'],
+ stdout=out_w, stderr=err_w)
+
+ while True:
+ try:
+ if 'ready' in os.listdir('/tmp/syslogd'): break
+ time.sleep(0.1)
+ except OSError as e:
+ if e.errno != errno.ENOENT: raise
+
+ def _Read():
+ r, unused_w, unused_x = select.select([out_r, err_r], [], [], 30)
+ out = ''
+ err = ''
+
+ if out_r in r: out = os.read(out_r, 4096)
+ if err_r in r: err = os.read(err_r, 4096)
+
+ if out or err:
+ return out, err
+ else:
+ raise Exception('read timed out')
+
+ _Read() # discard syslogd startup messages
+
+ addr = ('::', 5514)
+ s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
+
+ s.sendto('a\nErROR: b\nw: c', addr)
+ out, err = _Read()
+ WVFAIL(out)
+ WVPASS(ChompLeadingIP(err).startswith('discarded'))
+
+ s.sendto('a\tb\r\nabba\tbbb\naa\t\tb\tc\n', addr)
+ out, err = _Read()
+ WVFAIL(out)
+ WVPASS(ChompLeadingIP(err).startswith('discarded'))
+
+ s.sendto(''.join(chr(i) for i in range(33)) + '\n', addr)
+ out, err = _Read()
+ WVFAIL(out)
+ WVPASS(ChompLeadingIP(err).startswith('discarded'))
+
+ s.sendto('Test PASSes', addr)
+ time.sleep(1) # make sure both streams update at once
+ out, err = _Read()
+ WVPASS(ChompLeadingIP(out).startswith('Test PASSes'))
+
+ s.sendto('TooLongToPASS' * 100, addr)
+ out, err = _Read()
+ WVFAIL(out)
+ WVPASS(ChompLeadingIP(err).startswith('discarded'))
+
+ s.sendto('NoMatchFAILS', addr)
+ out, err = _Read()
+ WVFAIL(out)
+ WVPASS(ChompLeadingIP(err).startswith('discarded'))
+
+
+if __name__ == '__main__':
+ wvtest_main()