summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cloudinit/config/cc_syslog.py183
-rw-r--r--doc/examples/cloud-config-syslog.txt30
-rw-r--r--tests/unittests/test_handler/test_handler_syslog.py32
3 files changed, 245 insertions, 0 deletions
diff --git a/cloudinit/config/cc_syslog.py b/cloudinit/config/cc_syslog.py
new file mode 100644
index 00000000..21a8e8a9
--- /dev/null
+++ b/cloudinit/config/cc_syslog.py
@@ -0,0 +1,183 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2015 Canonical Ltd.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from cloudinit import log as logging
+from cloudinit import util
+from cloudinit.settings import PER_INSTANCE
+
+import re
+
+LOG = logging.getLogger(__name__)
+
+frequency = PER_INSTANCE
+
+BUILTIN_CFG = {
+ 'remotes_file': '/etc/rsyslog.d/20-cloudinit-remotes.conf',
+ 'remotes': {},
+ 'service_name': 'rsyslog',
+}
+
+COMMENT_RE = re.compile(r'[ ]*[#]+[ ]*')
+HOST_PORT_RE = re.compile(
+ r'^(?P<proto>[@]{0,2})'
+ '(([[](?P<bracket_addr>[^\]]*)[\]])|(?P<addr>[^:]*))'
+ '([:](?P<port>[0-9]+))?$')
+
+
+def parse_remotes_line(line, name=None):
+ try:
+ data, comment = COMMENT_RE.split(line)
+ comment = comment.strip()
+ except ValueError:
+ data, comment = (line, None)
+
+ toks = data.strip().split()
+ match = None
+ if len(toks) == 1:
+ host_port = data
+ elif len(toks) == 2:
+ match, host_port = toks
+ else:
+ raise ValueError("line had multiple spaces: %s" % data)
+
+ toks = HOST_PORT_RE.match(host_port)
+
+ if not toks:
+ raise ValueError("Invalid host specification '%s'" % host_port)
+
+ proto = toks.group('proto')
+ addr = toks.group('addr') or toks.group('bracket_addr')
+ port = toks.group('port')
+ print("host_port: %s" % addr)
+ print("port: %s" % port)
+
+ if addr.startswith("[") and not addr.endswith("]"):
+ raise ValueError("host spec had invalid brackets: %s" % addr)
+
+ if comment and not name:
+ name = comment
+
+ t = SyslogRemotesLine(name=name, match=match, proto=proto,
+ addr=addr, port=port)
+ t.validate()
+ return t
+
+
+class SyslogRemotesLine(object):
+ def __init__(self, name=None, match=None, proto=None, addr=None,
+ port=None):
+ if not match:
+ match = "*.*"
+ self.name = name
+ self.match = match
+ if proto == "@":
+ proto = "udp"
+ elif proto == "@@":
+ proto = "tcp"
+ self.proto = proto
+
+ self.addr = addr
+ if port:
+ self.port = int(port)
+ else:
+ self.port = None
+
+ def validate(self):
+ if self.port:
+ try:
+ int(self.port)
+ except ValueError:
+ raise ValueError("port '%s' is not an integer" % self.port)
+
+ if not self.addr:
+ raise ValueError("address is required")
+
+ def __repr__(self):
+ return "[name=%s match=%s proto=%s address=%s port=%s]" % (
+ self.name, self.match, self.proto, self.addr, self.port
+ )
+
+ def __str__(self):
+ buf = self.match + " "
+ if self.proto == "udp":
+ buf += " @"
+ elif self.proto == "tcp":
+ buf += " @@"
+
+ if ":" in self.addr:
+ buf += "[" + self.addr + "]"
+ else:
+ buf += self.addr
+
+ if self.port:
+ buf += ":%s" % self.port
+
+ if self.name:
+ buf += " # %s" % self.name
+ return buf
+
+
+def remotes_to_rsyslog_cfg(remotes, header=None):
+ if not remotes:
+ return None
+ lines = []
+ if header is not None:
+ lines.append(header)
+ for name, line in remotes.items():
+ try:
+ lines.append(parse_remotes_line(line, name=name))
+ except ValueError as e:
+ LOG.warn("failed loading remote %s: %s [%s]", name, line, e)
+ return '\n'.join(str(lines)) + '\n'
+
+
+def reload_syslog(systemd, service='rsyslog'):
+ if systemd:
+ cmd = ['systemctl', 'reload-or-try-restart', service]
+ else:
+ cmd = ['service', service, 'reload']
+ try:
+ util.subp(cmd, capture=True)
+ except util.ProcessExecutionError as e:
+ LOG.warn("Failed to reload syslog using '%s': %s", ' '.join(cmd), e)
+
+
+def handle(name, cfg, cloud, log, args):
+ cfgin = cfg.get('syslog')
+ if not cfgin:
+ cfgin = {}
+ mycfg = util.mergemanydict([cfgin, BUILTIN_CFG])
+
+ remotes_file = mycfg.get('remotes_file')
+ if util.is_false(remotes_file):
+ LOG.debug("syslog/remotes_file empty, doing nothing")
+ return
+
+ remotes = mycfg.get('remotes_dict', {})
+ if remotes and not isinstance(remotes, dict):
+ LOG.warn("syslog/remotes: content is not a dictionary")
+ return
+
+ config_data = remotes_to_rsyslog_cfg(
+ remotes, header="#cloud-init syslog module")
+
+ util.write_file(remotes_file, config_data)
+
+ reload_syslog(
+ systemd=cloud.distro.uses_systemd(),
+ service=mycfg.get('service_name'))
diff --git a/doc/examples/cloud-config-syslog.txt b/doc/examples/cloud-config-syslog.txt
new file mode 100644
index 00000000..9ec5e120
--- /dev/null
+++ b/doc/examples/cloud-config-syslog.txt
@@ -0,0 +1,30 @@
+## syslog module allows you to configure the systems syslog.
+## configuration of syslog is under the top level cloud-config
+## entry 'syslog'.
+##
+## "remotes"
+## remotes is a dictionary. items are of 'name: remote_info'
+## name is simply a name (example 'maas'). It has no importance other than
+## for cloud-init merging configs
+##
+## remote_info is of the format
+## * optional filter for log messages
+## default if not present: *.*
+## * optional leading '@' or '@@' (indicates udp or tcp).
+## default if not present (udp): @
+## This is rsyslog format for that. if not present, is '@' which is udp
+## * ipv4 or ipv6 or hostname
+## ipv6 addresses must be encoded in [::1] format. example: @[fd00::1]:514
+## * optional port
+## port defaults to 514
+##
+## Example:
+#cloud-config
+syslog:
+ remotes:
+ # udp to host 'maas.mydomain' port 514
+ maashost: maas.mydomain
+ # udp to ipv4 host on port 514
+ maas: "@[10.5.1.56]:514"
+ # tcp to host ipv6 host on port 555
+ maasipv6: "*.* @@[FE80::0202:B3FF:FE1E:8329]:555"
diff --git a/tests/unittests/test_handler/test_handler_syslog.py b/tests/unittests/test_handler/test_handler_syslog.py
new file mode 100644
index 00000000..bbfd521e
--- /dev/null
+++ b/tests/unittests/test_handler/test_handler_syslog.py
@@ -0,0 +1,32 @@
+from cloudinit.config.cc_syslog import (
+ parse_remotes_line, SyslogRemotesLine, remotes_to_rsyslog_cfg)
+from cloudinit import util
+from .. import helpers as t_help
+
+
+class TestParseRemotesLine(t_help.TestCase):
+ def test_valid_port(self):
+ r = parse_remotes_line("foo:9")
+ self.assertEqual(9, r.port)
+
+ def test_invalid_port(self):
+ with self.assertRaises(ValueError):
+ parse_remotes_line("*.* foo:abc")
+
+ def test_valid_ipv6(self):
+ r = parse_remotes_line("*.* [::1]")
+ self.assertEqual("*.* [::1]", str(r))
+
+ def test_valid_ipv6_with_port(self):
+ r = parse_remotes_line("*.* [::1]:100")
+ self.assertEqual(r.port, 100)
+ self.assertEqual(r.addr, "::1")
+ self.assertEqual("*.* [::1]:100", str(r))
+
+ def test_invalid_multiple_colon(self):
+ with self.assertRaises(ValueError):
+ parse_remotes_line("*.* ::1:100")
+
+ def test_name_in_string(self):
+ r = parse_remotes_line("syslog.host", name="foobar")
+ self.assertEqual("*.* syslog.host # foobar", str(r))