From 6970029c661ab858a55dd467e5c593694ab39512 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Fri, 24 Jul 2015 16:58:57 -0400 Subject: commit initial re-work/re-implementation of syslog config --- cloudinit/config/cc_syslog.py | 183 +++++++++++++++++++++ doc/examples/cloud-config-syslog.txt | 30 ++++ .../unittests/test_handler/test_handler_syslog.py | 32 ++++ 3 files changed, 245 insertions(+) create mode 100644 cloudinit/config/cc_syslog.py create mode 100644 doc/examples/cloud-config-syslog.txt create mode 100644 tests/unittests/test_handler/test_handler_syslog.py 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 +# +# 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 . + +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[@]{0,2})' + '(([[](?P[^\]]*)[\]])|(?P[^:]*))' + '([:](?P[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)) -- cgit v1.2.3 From 247f2cecb72a852a42f147645e80b538eee05f93 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Mon, 27 Jul 2015 14:20:29 -0400 Subject: update existing rsyslog module with better code and doc --- cloudinit/config/cc_rsyslog.py | 190 +++++++++++++++++++++++++++++++++-------- 1 file changed, 153 insertions(+), 37 deletions(-) diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py index 57486edc..7d5657bc 100644 --- a/cloudinit/config/cc_rsyslog.py +++ b/cloudinit/config/cc_rsyslog.py @@ -17,37 +17,129 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +""" +rsyslog module allows configuration of syslog logging via rsyslog +Configuration is done under the cloud-config top level 'rsyslog'. + +Under 'rsyslog' you can define: + - configs: [default=[]] + this is a list. entries in it are a string or a dictionary. + each entry has 2 parts: + * content + * filename + if the entry is a string, then it is assigned to 'content'. + for each entry, content is written to the provided filename. + if filename is not provided, its default is read from 'config_filename' + + Content here can be any valid rsyslog configuration. No format + specific format is enforced. + + For simply logging to an existing remote syslog server, via udp: + configs: ["*.* @192.168.1.1"] + + - config_filename: [default=20-cloud-config.conf] + this is the file name to use if none is provided in a config entry. + - config_dir: [default=/etc/rsyslog.d] + this directory is used for filenames that are not absolute paths. + - service_reload_command: [default="auto"] + this command is executed if files have been written and thus the syslog + daemon needs to be told. + +Note, since cloud-init 0.5 a legacy version of rsyslog config has been +present and is still supported. See below for the mappings between old +value and new value: + old value -> new value + 'rsyslog' -> rsyslog/configs + 'rsyslog_filename' -> rsyslog/config_filename + 'rsyslog_dir' -> rsyslog/config_dir + +the legacy config does not support 'service_reload_command'. + +Example config: + #cloud-config + rsyslog: + configs: + - "*.* @@192.158.1.1" + - content: "*.* @@192.0.2.1:10514" + - filename: 01-examplecom.conf + - content: | + *.* @@syslogd.example.com + config_dir: config_dir + config_filename: config_filename + service_reload_command: [your, syslog, restart, command] + +Example Legacy config: + #cloud-config + rsyslog: + - "*.* @@192.158.1.1" + rsyslog_dir: /etc/rsyslog-config.d/ + rsyslog_filename: 99-local.conf +""" import os +import six +from cloudinit import log as logging from cloudinit import util DEF_FILENAME = "20-cloud-config.conf" DEF_DIR = "/etc/rsyslog.d" +DEF_RELOAD = "auto" +KEYNAME_CONFIGS = 'configs' +KEYNAME_FILENAME = 'config_filename' +KEYNAME_DIR = 'config_dir' +KEYNAME_RELOAD = 'service_reload_command' +KEYNAME_LEGACY_FILENAME = 'rsyslog_filename' +KEYNAME_LEGACY_DIR = 'rsyslog_dir' -def handle(name, cfg, cloud, log, _args): - # rsyslog: - # - "*.* @@192.158.1.1" - # - content: "*.* @@192.0.2.1:10514" - # - filename: 01-examplecom.conf - # content: | - # *.* @@syslogd.example.com +LOG = logging.getLogger(__name__) - # process 'rsyslog' - if 'rsyslog' not in cfg: - log.debug(("Skipping module named %s," - " no 'rsyslog' key in configuration"), name) - return - def_dir = cfg.get('rsyslog_dir', DEF_DIR) - def_fname = cfg.get('rsyslog_filename', DEF_FILENAME) +def reload_syslog(command=DEF_RELOAD, systemd=False): + service = 'rsyslog' + if command == DEF_RELOAD: + if systemd: + cmd = ['systemctl', 'reload-or-try-restart', service] + else: + cmd = ['service', service, 'reload'] + else: + cmd = command + util.subp(cmd, capture=True) + + +def load_config(cfg): + # return an updated config with entries of the correct type + # support converting the old top level format into new format + mycfg = cfg.get('rsyslog', {}) + + if isinstance(mycfg, list): + mycfg[KEYNAME_CONFIGS] = mycfg + if KEYNAME_LEGACY_FILENAME in cfg: + mycfg[KEYNAME_FILENAME] = cfg[KEYNAME_LEGACY_FILENAME] + if KEYNAME_LEGACY_DIR in cfg: + mycfg[KEYNAME_DIR] = cfg[KEYNAME_DIR] + + fillup = ( + (KEYNAME_DIR, DEF_DIR, six.text_type), + (KEYNAME_FILENAME, DEF_FILENAME, six.text_type) + (KEYNAME_RELOAD, DEF_RELOAD, (six.text_type, list))) + for key, default, vtypes in fillup: + if key not in mycfg or not isinstance(mycfg[key], vtypes): + mycfg[key] = default + + return mycfg + + +def apply_rsyslog_changes(configs, def_fname, cfg_dir): + # apply the changes in 'configs' to the paths in def_fname and cfg_dir + # return a list of the files changed files = [] - for i, ent in enumerate(cfg['rsyslog']): + for cur_pos, ent in enumerate(configs): if isinstance(ent, dict): if "content" not in ent: - log.warn("No 'content' entry in config entry %s", i + 1) + LOG.warn("No 'content' entry in config entry %s", cur_pos + 1) continue content = ent['content'] filename = ent.get("filename", def_fname) @@ -57,15 +149,14 @@ def handle(name, cfg, cloud, log, _args): filename = filename.strip() if not filename: - log.warn("Entry %s has an empty filename", i + 1) + LOG.warn("Entry %s has an empty filename", cur_pos + 1) continue if not filename.startswith("/"): - filename = os.path.join(def_dir, filename) + filename = os.path.join(cfg_dir, filename) # Truncate filename first time you see it - omode = "ab" - if filename not in files: + omode = "ab" if filename not in files: omode = "wb" files.append(filename) @@ -73,24 +164,49 @@ def handle(name, cfg, cloud, log, _args): contents = "%s\n" % (content) util.write_file(filename, contents, omode=omode) except Exception: - util.logexc(log, "Failed to write to %s", filename) + util.logexc(LOG, "Failed to write to %s", filename) + + return files + + +def handle(name, cfg, cloud, log, _args): + # rsyslog: + # configs: + # - "*.* @@192.158.1.1" + # - content: "*.* @@192.0.2.1:10514" + # - filename: 01-examplecom.conf + # content: | + # *.* @@syslogd.example.com + # config_dir: DEF_DIR + # config_filename: DEF_FILENAME + # service_reload: "auto" + + if 'rsyslog' not in cfg: + log.debug(("Skipping module named %s," + " no 'rsyslog' key in configuration"), name) + return + + mycfg = load_config(cfg) + if not mycfg['configs']: + log.debug("Empty config rsyslog['configs'], nothing to do") + return + + changes = apply_rsyslog_changes( + configs=mycfg[KEYNAME_CONFIGS], + def_fname=mycfg[KEYNAME_FILENAME], + cfg_dir=mycfg[KEYNAME_DIR]) + + if not changes: + log.debug("restart of syslog not necessary, no changes made") + return - # Attempt to restart syslogd - restarted = False try: - # If this config module is running at cloud-init time - # (before rsyslog is running) we don't actually have to - # restart syslog. - # - # Upstart actually does what we want here, in that it doesn't - # start a service that wasn't running already on 'restart' - # it will also return failure on the attempt, so 'restarted' - # won't get set. - log.debug("Restarting rsyslog") - util.subp(['service', 'rsyslog', 'restart']) - restarted = True - except Exception: - util.logexc(log, "Failed restarting rsyslog") + restarted = reload_syslog( + service=mycfg[KEYNAME_RELOAD], + systemd=cloud.distro.uses_systemd()), + except util.ProcessExecutionError as e: + restarted = False + log.warn("Failed to reload syslog", e) if restarted: # This only needs to run if we *actually* restarted @@ -98,4 +214,4 @@ def handle(name, cfg, cloud, log, _args): cloud.cycle_logging() # This should now use rsyslog if # the logging was setup to use it... - log.debug("%s configured %s files", name, files) + log.debug("%s configured %s files", name, changes) -- cgit v1.2.3 From 22cb92421234c31b783ed9df01439c734535ba01 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Mon, 27 Jul 2015 16:49:30 -0400 Subject: add rsyslog tests reasonable test of reworked rsyslog module --- cloudinit/config/cc_rsyslog.py | 27 ++--- .../unittests/test_handler/test_handler_rsyslog.py | 113 +++++++++++++++++++++ 2 files changed, 122 insertions(+), 18 deletions(-) create mode 100644 tests/unittests/test_handler/test_handler_rsyslog.py diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py index 7d5657bc..a07200c3 100644 --- a/cloudinit/config/cc_rsyslog.py +++ b/cloudinit/config/cc_rsyslog.py @@ -113,17 +113,18 @@ def load_config(cfg): # support converting the old top level format into new format mycfg = cfg.get('rsyslog', {}) - if isinstance(mycfg, list): - mycfg[KEYNAME_CONFIGS] = mycfg + if isinstance(cfg.get('rsyslog'), list): + mycfg = {KEYNAME_CONFIGS: cfg.get('rsyslog')} if KEYNAME_LEGACY_FILENAME in cfg: mycfg[KEYNAME_FILENAME] = cfg[KEYNAME_LEGACY_FILENAME] if KEYNAME_LEGACY_DIR in cfg: - mycfg[KEYNAME_DIR] = cfg[KEYNAME_DIR] + mycfg[KEYNAME_DIR] = cfg[KEYNAME_LEGACY_DIR] fillup = ( - (KEYNAME_DIR, DEF_DIR, six.text_type), - (KEYNAME_FILENAME, DEF_FILENAME, six.text_type) - (KEYNAME_RELOAD, DEF_RELOAD, (six.text_type, list))) + (KEYNAME_CONFIGS, [], list), + (KEYNAME_DIR, DEF_DIR, six.string_types), + (KEYNAME_FILENAME, DEF_FILENAME, six.string_types), + (KEYNAME_RELOAD, DEF_RELOAD, six.string_types + (list,))) for key, default, vtypes in fillup: if key not in mycfg or not isinstance(mycfg[key], vtypes): @@ -156,7 +157,8 @@ def apply_rsyslog_changes(configs, def_fname, cfg_dir): filename = os.path.join(cfg_dir, filename) # Truncate filename first time you see it - omode = "ab" if filename not in files: + omode = "ab" + if filename not in files: omode = "wb" files.append(filename) @@ -170,17 +172,6 @@ def apply_rsyslog_changes(configs, def_fname, cfg_dir): def handle(name, cfg, cloud, log, _args): - # rsyslog: - # configs: - # - "*.* @@192.158.1.1" - # - content: "*.* @@192.0.2.1:10514" - # - filename: 01-examplecom.conf - # content: | - # *.* @@syslogd.example.com - # config_dir: DEF_DIR - # config_filename: DEF_FILENAME - # service_reload: "auto" - if 'rsyslog' not in cfg: log.debug(("Skipping module named %s," " no 'rsyslog' key in configuration"), name) diff --git a/tests/unittests/test_handler/test_handler_rsyslog.py b/tests/unittests/test_handler/test_handler_rsyslog.py new file mode 100644 index 00000000..3501ff95 --- /dev/null +++ b/tests/unittests/test_handler/test_handler_rsyslog.py @@ -0,0 +1,113 @@ +import os +import shutil +import tempfile + +from cloudinit.config.cc_rsyslog import ( + load_config, DEF_FILENAME, DEF_DIR, DEF_RELOAD, apply_rsyslog_changes) +from cloudinit import util + +from .. import helpers as t_help + + +class TestLoadConfig(t_help.TestCase): + def setUp(self): + super(TestLoadConfig, self).setUp() + self.basecfg = { + 'config_filename': DEF_FILENAME, + 'config_dir': DEF_DIR, + 'service_reload_command': DEF_RELOAD, + 'configs': [], + } + + def test_legacy_full(self): + found = load_config({ + 'rsyslog': ['*.* @192.168.1.1'], + 'rsyslog_dir': "mydir", + 'rsyslog_filename': "myfilename"}) + expected = { + 'configs': ['*.* @192.168.1.1'], + 'config_dir': "mydir", + 'config_filename': 'myfilename', + 'service_reload_command': 'auto'} + self.assertEqual(found, expected) + + def test_legacy_defaults(self): + found = load_config({ + 'rsyslog': ['*.* @192.168.1.1']}) + self.basecfg.update({ + 'configs': ['*.* @192.168.1.1']}) + self.assertEqual(found, self.basecfg) + + def test_new_defaults(self): + self.assertEqual(load_config({}), self.basecfg) + + def test_new_configs(self): + cfgs = ['*.* myhost', '*.* my2host'] + self.basecfg.update({'configs': cfgs}) + self.assertEqual( + load_config({'rsyslog': {'configs': cfgs}}), + self.basecfg) + + +class TestApplyChanges(t_help.TestCase): + def setUp(self): + self.tmp = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.tmp) + + def test_simple(self): + cfgline = "*.* foohost" + changed = apply_rsyslog_changes( + configs=[cfgline], def_fname="foo.cfg", cfg_dir=self.tmp) + + fname = os.path.join(self.tmp, "foo.cfg") + self.assertEqual([fname], changed) + self.assertEqual( + util.load_file(fname), cfgline + "\n") + + def test_multiple_files(self): + configs = [ + '*.* foohost', + {'content': 'abc', 'filename': 'my.cfg'}, + {'content': 'filefoo-content', + 'filename': os.path.join(self.tmp, 'mydir/mycfg')}, + ] + + changed = apply_rsyslog_changes( + configs=configs, def_fname="default.cfg", cfg_dir=self.tmp) + + expected = [ + (os.path.join(self.tmp, "default.cfg"), + "*.* foohost\n"), + (os.path.join(self.tmp, "my.cfg"), "abc\n"), + (os.path.join(self.tmp, "mydir/mycfg"), "filefoo-content\n"), + ] + self.assertEqual([f[0] for f in expected], changed) + actual = [] + for fname, _content in expected: + util.load_file(fname) + actual.append((fname, util.load_file(fname),)) + self.assertEqual(expected, actual) + + def test_repeat_def(self): + configs = ['*.* foohost', "*.warn otherhost"] + + changed = apply_rsyslog_changes( + configs=configs, def_fname="default.cfg", cfg_dir=self.tmp) + + fname = os.path.join(self.tmp, "default.cfg") + self.assertEqual([fname], changed) + + expected_content = '\n'.join([c for c in configs]) + '\n' + found_content = util.load_file(fname) + self.assertEqual(expected_content, found_content) + + def test_multiline_content(self): + configs = ['line1', 'line2\nline3\n'] + + changed = apply_rsyslog_changes( + configs=configs, def_fname="default.cfg", cfg_dir=self.tmp) + + fname = os.path.join(self.tmp, "default.cfg") + expected_content = '\n'.join([c for c in configs]) + '\n' + found_content = util.load_file(fname) + self.assertEqual(expected_content, found_content) -- cgit v1.2.3 From 39f25660714e5640be3dce576a6cfdee9a1672c8 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Mon, 27 Jul 2015 20:53:44 -0400 Subject: use 'restart' rather than 'reload' on non-systemd systems Testing on trusty shows that: service rsyslog reload does produce a message like: rsyslogd was HUPed but does not result in new config being in honored. Using restart does, and with upstart that should be fine (as upstart will start only if previously running). --- cloudinit/config/cc_rsyslog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py index a07200c3..82e382e8 100644 --- a/cloudinit/config/cc_rsyslog.py +++ b/cloudinit/config/cc_rsyslog.py @@ -102,7 +102,7 @@ def reload_syslog(command=DEF_RELOAD, systemd=False): if systemd: cmd = ['systemctl', 'reload-or-try-restart', service] else: - cmd = ['service', service, 'reload'] + cmd = ['service', service, 'restart'] else: cmd = command util.subp(cmd, capture=True) -- cgit v1.2.3 From cae9122f88f8369454b03b97a5386d3135941fd9 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Mon, 27 Jul 2015 21:02:51 -0400 Subject: fix kwarg --- cloudinit/config/cc_rsyslog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py index 82e382e8..9599e925 100644 --- a/cloudinit/config/cc_rsyslog.py +++ b/cloudinit/config/cc_rsyslog.py @@ -193,7 +193,7 @@ def handle(name, cfg, cloud, log, _args): try: restarted = reload_syslog( - service=mycfg[KEYNAME_RELOAD], + command=mycfg[KEYNAME_RELOAD], systemd=cloud.distro.uses_systemd()), except util.ProcessExecutionError as e: restarted = False -- cgit v1.2.3 From 6dd505fd02e0933d8770c8932a927940f6a0e025 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 28 Jul 2015 09:27:26 -0400 Subject: add support for 'remotes' --- cloudinit/config/cc_rsyslog.py | 156 ++++++++++++++++++++- cloudinit/config/cc_syslog.py | 2 +- doc/examples/cloud-config-rsyslog.txt | 37 +++++ doc/examples/cloud-config-syslog.txt | 30 ---- .../unittests/test_handler/test_handler_rsyslog.py | 38 ++++- 5 files changed, 227 insertions(+), 36 deletions(-) create mode 100644 doc/examples/cloud-config-rsyslog.txt delete mode 100644 doc/examples/cloud-config-syslog.txt diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py index 9599e925..8c02e826 100644 --- a/cloudinit/config/cc_rsyslog.py +++ b/cloudinit/config/cc_rsyslog.py @@ -37,10 +37,33 @@ Under 'rsyslog' you can define: For simply logging to an existing remote syslog server, via udp: configs: ["*.* @192.168.1.1"] + - remotes: [default={}] + This is a dictionary of name / value pairs. + In comparison to 'config's, it is more focused in that it only supports + remote syslog configuration. It is not rsyslog specific, and could + convert to other syslog implementations. + + Each entry in remotes is a 'name' and a 'value'. + * name: an string identifying the entry. good practice would indicate + using a consistent and identifiable string for the producer. + For example, the MAAS service could use 'maas' as the key. + * value consists of the following parts: + * optional filter for log messages + default if not present: *.* + * optional leading '@' or '@@' (indicates udp or tcp respectively). + default if not present (udp): @ + This is rsyslog format for that. if not present, is '@'. + * ipv4 or ipv6 or hostname + ipv6 addresses must be in [::1] format. (@[fd00::1]:514) + * optional port + port defaults to 514 + - config_filename: [default=20-cloud-config.conf] this is the file name to use if none is provided in a config entry. + - config_dir: [default=/etc/rsyslog.d] this directory is used for filenames that are not absolute paths. + - service_reload_command: [default="auto"] this command is executed if files have been written and thus the syslog daemon needs to be told. @@ -61,9 +84,12 @@ Example config: configs: - "*.* @@192.158.1.1" - content: "*.* @@192.0.2.1:10514" - - filename: 01-examplecom.conf + filename: 01-example.conf - content: | *.* @@syslogd.example.com + remotes: + maas: "192.168.1.1" + juju: "10.0.4.1" config_dir: config_dir config_filename: config_filename service_reload_command: [your, syslog, restart, command] @@ -77,6 +103,7 @@ Example Legacy config: """ import os +import re import six from cloudinit import log as logging @@ -85,6 +112,7 @@ from cloudinit import util DEF_FILENAME = "20-cloud-config.conf" DEF_DIR = "/etc/rsyslog.d" DEF_RELOAD = "auto" +DEF_REMOTES = {} KEYNAME_CONFIGS = 'configs' KEYNAME_FILENAME = 'config_filename' @@ -92,9 +120,15 @@ KEYNAME_DIR = 'config_dir' KEYNAME_RELOAD = 'service_reload_command' KEYNAME_LEGACY_FILENAME = 'rsyslog_filename' KEYNAME_LEGACY_DIR = 'rsyslog_dir' +KEYNAME_REMOTES = 'remotes' LOG = logging.getLogger(__name__) +COMMENT_RE = re.compile(r'[ ]*[#]+[ ]*') +HOST_PORT_RE = re.compile( + r'^(?P[@]{0,2})' + '(([[](?P[^\]]*)[\]])|(?P[^:]*))' + '([:](?P[0-9]+))?$') def reload_syslog(command=DEF_RELOAD, systemd=False): service = 'rsyslog' @@ -124,7 +158,8 @@ def load_config(cfg): (KEYNAME_CONFIGS, [], list), (KEYNAME_DIR, DEF_DIR, six.string_types), (KEYNAME_FILENAME, DEF_FILENAME, six.string_types), - (KEYNAME_RELOAD, DEF_RELOAD, six.string_types + (list,))) + (KEYNAME_RELOAD, DEF_RELOAD, six.string_types + (list,)), + (KEYNAME_REMOTES, DEF_REMOTES, dict)) for key, default, vtypes in fillup: if key not in mycfg or not isinstance(mycfg[key], vtypes): @@ -171,6 +206,113 @@ def apply_rsyslog_changes(configs, def_fname, cfg_dir): return files +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') + + 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, footer=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) + if footer is not None: + lines.append(footer) + return '\n'.join(str(lines)) + '\n' + + def handle(name, cfg, cloud, log, _args): if 'rsyslog' not in cfg: log.debug(("Skipping module named %s," @@ -178,6 +320,16 @@ def handle(name, cfg, cloud, log, _args): return mycfg = load_config(cfg) + configs = mycfg[KEYNAME_CONFIGS] + + if mycfg[KEYNAME_REMOTES]: + configs.append( + remotes_to_rsyslog_cfg( + mycfg[KEYNAME_REMOTES], + header="# begin remotes", + footer="# end remotes", + )) + if not mycfg['configs']: log.debug("Empty config rsyslog['configs'], nothing to do") return diff --git a/cloudinit/config/cc_syslog.py b/cloudinit/config/cc_syslog.py index 21a8e8a9..27793f8b 100644 --- a/cloudinit/config/cc_syslog.py +++ b/cloudinit/config/cc_syslog.py @@ -168,7 +168,7 @@ def handle(name, cfg, cloud, log, args): LOG.debug("syslog/remotes_file empty, doing nothing") return - remotes = mycfg.get('remotes_dict', {}) + remotes = mycfg.get('remotes', {}) if remotes and not isinstance(remotes, dict): LOG.warn("syslog/remotes: content is not a dictionary") return diff --git a/doc/examples/cloud-config-rsyslog.txt b/doc/examples/cloud-config-rsyslog.txt new file mode 100644 index 00000000..ff60e3a8 --- /dev/null +++ b/doc/examples/cloud-config-rsyslog.txt @@ -0,0 +1,37 @@ +## the rsyslog module allows you to configure the systems syslog. +## configuration of syslog is under the top level cloud-config +## entry 'rsyslog'. +## +## Example: +#cloud-config +rsyslog: + 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" + configs: + - "*.* @@192.158.1.1" + - content: "*.* @@192.0.2.1:10514" + filename: 01-example.conf + - content: | + *.* @@syslogd.example.com + config_dir: /etc/rsyslog.d + config_filename: 20-cloud-config.conf + service_reload_command: [your, syslog, reload, command] + +## Additionally the following legacy format is supported +## it is converted into the format above before use. +## rsyslog_filename -> rsyslog/config_filename +## rsyslog_dir -> rsyslog/config_dir +## rsyslog -> rsyslog/configs +# rsyslog: +# - "*.* @@192.158.1.1" +# - content: "*.* @@192.0.2.1:10514" +# filename: 01-example.conf +# - content: | +# *.* @@syslogd.example.com +# rsyslog_filename: 20-cloud-config.conf +# rsyslog_dir: /etc/rsyslog.d diff --git a/doc/examples/cloud-config-syslog.txt b/doc/examples/cloud-config-syslog.txt deleted file mode 100644 index 9ec5e120..00000000 --- a/doc/examples/cloud-config-syslog.txt +++ /dev/null @@ -1,30 +0,0 @@ -## 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_rsyslog.py b/tests/unittests/test_handler/test_handler_rsyslog.py index 3501ff95..0bace685 100644 --- a/tests/unittests/test_handler/test_handler_rsyslog.py +++ b/tests/unittests/test_handler/test_handler_rsyslog.py @@ -3,7 +3,8 @@ import shutil import tempfile from cloudinit.config.cc_rsyslog import ( - load_config, DEF_FILENAME, DEF_DIR, DEF_RELOAD, apply_rsyslog_changes) + apply_rsyslog_changes, DEF_DIR, DEF_FILENAME, DEF_RELOAD, load_config, + parse_remotes_line) from cloudinit import util from .. import helpers as t_help @@ -17,6 +18,7 @@ class TestLoadConfig(t_help.TestCase): 'config_dir': DEF_DIR, 'service_reload_command': DEF_RELOAD, 'configs': [], + 'remotes': {}, } def test_legacy_full(self): @@ -24,12 +26,14 @@ class TestLoadConfig(t_help.TestCase): 'rsyslog': ['*.* @192.168.1.1'], 'rsyslog_dir': "mydir", 'rsyslog_filename': "myfilename"}) - expected = { + self.basecfg.update({ 'configs': ['*.* @192.168.1.1'], 'config_dir': "mydir", 'config_filename': 'myfilename', 'service_reload_command': 'auto'} - self.assertEqual(found, expected) + ) + + self.assertEqual(found, self.basecfg) def test_legacy_defaults(self): found = load_config({ @@ -111,3 +115,31 @@ class TestApplyChanges(t_help.TestCase): expected_content = '\n'.join([c for c in configs]) + '\n' found_content = util.load_file(fname) self.assertEqual(expected_content, found_content) + + +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)) -- cgit v1.2.3 From f61a62434b36ab873b2b82a5ba69eda826755bfc Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 28 Jul 2015 10:12:02 -0400 Subject: fix bug in remotes_to_rsyslog_cfg, add test --- cloudinit/config/cc_rsyslog.py | 4 +-- .../unittests/test_handler/test_handler_rsyslog.py | 32 ++++++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py index 8c02e826..915ab420 100644 --- a/cloudinit/config/cc_rsyslog.py +++ b/cloudinit/config/cc_rsyslog.py @@ -305,12 +305,12 @@ def remotes_to_rsyslog_cfg(remotes, header=None, footer=None): lines.append(header) for name, line in remotes.items(): try: - lines.append(parse_remotes_line(line, name=name)) + lines.append(str(parse_remotes_line(line, name=name))) except ValueError as e: LOG.warn("failed loading remote %s: %s [%s]", name, line, e) if footer is not None: lines.append(footer) - return '\n'.join(str(lines)) + '\n' + return '\n'.join(lines) + "\n" def handle(name, cfg, cloud, log, _args): diff --git a/tests/unittests/test_handler/test_handler_rsyslog.py b/tests/unittests/test_handler/test_handler_rsyslog.py index 0bace685..292559c5 100644 --- a/tests/unittests/test_handler/test_handler_rsyslog.py +++ b/tests/unittests/test_handler/test_handler_rsyslog.py @@ -4,7 +4,7 @@ import tempfile from cloudinit.config.cc_rsyslog import ( apply_rsyslog_changes, DEF_DIR, DEF_FILENAME, DEF_RELOAD, load_config, - parse_remotes_line) + parse_remotes_line, remotes_to_rsyslog_cfg) from cloudinit import util from .. import helpers as t_help @@ -80,10 +80,10 @@ class TestApplyChanges(t_help.TestCase): configs=configs, def_fname="default.cfg", cfg_dir=self.tmp) expected = [ - (os.path.join(self.tmp, "default.cfg"), - "*.* foohost\n"), - (os.path.join(self.tmp, "my.cfg"), "abc\n"), - (os.path.join(self.tmp, "mydir/mycfg"), "filefoo-content\n"), + (os.path.join(self.tmp, "default.cfg"), + "*.* foohost\n"), + (os.path.join(self.tmp, "my.cfg"), "abc\n"), + (os.path.join(self.tmp, "mydir/mycfg"), "filefoo-content\n"), ] self.assertEqual([f[0] for f in expected], changed) actual = [] @@ -108,7 +108,7 @@ class TestApplyChanges(t_help.TestCase): def test_multiline_content(self): configs = ['line1', 'line2\nline3\n'] - changed = apply_rsyslog_changes( + apply_rsyslog_changes( configs=configs, def_fname="default.cfg", cfg_dir=self.tmp) fname = os.path.join(self.tmp, "default.cfg") @@ -143,3 +143,23 @@ class TestParseRemotesLine(t_help.TestCase): def test_name_in_string(self): r = parse_remotes_line("syslog.host", name="foobar") self.assertEqual("*.* syslog.host # foobar", str(r)) + + +class TestRemotesToSyslog(t_help.TestCase): + def test_simple(self): + # str rendered line must appear in remotes_to_ryslog_cfg return + mycfg = "*.* myhost" + myline = str(parse_remotes_line(mycfg, name="myname")) + r = remotes_to_rsyslog_cfg({'myname': mycfg}) + lines = r.splitlines() + self.assertEqual(1, len(lines)) + self.assertTrue(myline in r.splitlines()) + + def test_header_footer(self): + header = "#foo head" + footer = "#foo foot" + r = remotes_to_rsyslog_cfg( + {'myname': "*.* myhost"}, header=header, footer=footer) + lines = r.splitlines() + self.assertTrue(header, lines[0]) + self.assertTrue(footer, lines[-1]) -- cgit v1.2.3 From c6e7fb1752a93ed534080adf0588e4c7cdd99071 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 28 Jul 2015 10:34:31 -0400 Subject: add trailing newline only if necessary --- cloudinit/config/cc_rsyslog.py | 9 +++++---- tests/unittests/test_handler/test_handler_rsyslog.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py index 915ab420..2bb00728 100644 --- a/cloudinit/config/cc_rsyslog.py +++ b/cloudinit/config/cc_rsyslog.py @@ -188,8 +188,7 @@ def apply_rsyslog_changes(configs, def_fname, cfg_dir): LOG.warn("Entry %s has an empty filename", cur_pos + 1) continue - if not filename.startswith("/"): - filename = os.path.join(cfg_dir, filename) + filename = os.path.join(cfg_dir, filename) # Truncate filename first time you see it omode = "ab" @@ -198,8 +197,10 @@ def apply_rsyslog_changes(configs, def_fname, cfg_dir): files.append(filename) try: - contents = "%s\n" % (content) - util.write_file(filename, contents, omode=omode) + endl = "" + if not content.endswith("\n"): + endl = "\n" + util.write_file(filename, content + endl, omode=omode) except Exception: util.logexc(LOG, "Failed to write to %s", filename) diff --git a/tests/unittests/test_handler/test_handler_rsyslog.py b/tests/unittests/test_handler/test_handler_rsyslog.py index 292559c5..e7666615 100644 --- a/tests/unittests/test_handler/test_handler_rsyslog.py +++ b/tests/unittests/test_handler/test_handler_rsyslog.py @@ -112,7 +112,7 @@ class TestApplyChanges(t_help.TestCase): configs=configs, def_fname="default.cfg", cfg_dir=self.tmp) fname = os.path.join(self.tmp, "default.cfg") - expected_content = '\n'.join([c for c in configs]) + '\n' + expected_content = '\n'.join([c for c in configs]) found_content = util.load_file(fname) self.assertEqual(expected_content, found_content) -- cgit v1.2.3 From 0a581732a40ff814b1fc0dace9f519b7a5c779e6 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 28 Jul 2015 10:48:32 -0400 Subject: must declare proto of '@' --- cloudinit/config/cc_rsyslog.py | 6 ++++-- tests/unittests/test_handler/test_handler_rsyslog.py | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py index 2bb00728..5ecf1629 100644 --- a/cloudinit/config/cc_rsyslog.py +++ b/cloudinit/config/cc_rsyslog.py @@ -251,6 +251,8 @@ class SyslogRemotesLine(object): match = "*.*" self.name = name self.match = match + if not proto: + proto = "udp" if proto == "@": proto = "udp" elif proto == "@@": @@ -281,9 +283,9 @@ class SyslogRemotesLine(object): def __str__(self): buf = self.match + " " if self.proto == "udp": - buf += " @" + buf += "@" elif self.proto == "tcp": - buf += " @@" + buf += "@@" if ":" in self.addr: buf += "[" + self.addr + "]" diff --git a/tests/unittests/test_handler/test_handler_rsyslog.py b/tests/unittests/test_handler/test_handler_rsyslog.py index e7666615..7bfa65a9 100644 --- a/tests/unittests/test_handler/test_handler_rsyslog.py +++ b/tests/unittests/test_handler/test_handler_rsyslog.py @@ -128,13 +128,13 @@ class TestParseRemotesLine(t_help.TestCase): def test_valid_ipv6(self): r = parse_remotes_line("*.* [::1]") - self.assertEqual("*.* [::1]", str(r)) + 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)) + self.assertEqual("*.* @[::1]:100", str(r)) def test_invalid_multiple_colon(self): with self.assertRaises(ValueError): @@ -142,7 +142,7 @@ class TestParseRemotesLine(t_help.TestCase): def test_name_in_string(self): r = parse_remotes_line("syslog.host", name="foobar") - self.assertEqual("*.* syslog.host # foobar", str(r)) + self.assertEqual("*.* @syslog.host # foobar", str(r)) class TestRemotesToSyslog(t_help.TestCase): -- cgit v1.2.3 From 8c6a61bb87119affc047af3ec94c1eaae324f4f6 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 28 Jul 2015 10:48:49 -0400 Subject: doc: mention how to run syslog server --- doc/examples/cloud-config-rsyslog.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/examples/cloud-config-rsyslog.txt b/doc/examples/cloud-config-rsyslog.txt index ff60e3a8..d54960e8 100644 --- a/doc/examples/cloud-config-rsyslog.txt +++ b/doc/examples/cloud-config-rsyslog.txt @@ -35,3 +35,12 @@ rsyslog: # *.* @@syslogd.example.com # rsyslog_filename: 20-cloud-config.conf # rsyslog_dir: /etc/rsyslog.d + +## to configure rsyslog to accept remote logging on Ubuntu +## write the following into /etc/rsyslog.d/20-remote-udp.conf +## $ModLoad imudp +## $UDPServerRun 514 +## $template LogRemote,"/var/log/maas/rsyslog/%HOSTNAME%/messages" +## :fromhost-ip, !isequal, "127.0.0.1" ?LogRemote +## then: +## sudo service rsyslog restart -- cgit v1.2.3 From d5f93dbd908c349548554cb69ca3afd05077cf57 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 28 Jul 2015 10:49:48 -0400 Subject: remove 'syslog' module (its been moved to rsyslog) --- cloudinit/config/cc_syslog.py | 183 --------------------- .../unittests/test_handler/test_handler_syslog.py | 32 ---- 2 files changed, 215 deletions(-) delete mode 100644 cloudinit/config/cc_syslog.py delete mode 100644 tests/unittests/test_handler/test_handler_syslog.py diff --git a/cloudinit/config/cc_syslog.py b/cloudinit/config/cc_syslog.py deleted file mode 100644 index 27793f8b..00000000 --- a/cloudinit/config/cc_syslog.py +++ /dev/null @@ -1,183 +0,0 @@ -# vi: ts=4 expandtab -# -# Copyright (C) 2015 Canonical Ltd. -# -# Author: Scott Moser -# -# 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 . - -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[@]{0,2})' - '(([[](?P[^\]]*)[\]])|(?P[^:]*))' - '([:](?P[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', {}) - 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/tests/unittests/test_handler/test_handler_syslog.py b/tests/unittests/test_handler/test_handler_syslog.py deleted file mode 100644 index bbfd521e..00000000 --- a/tests/unittests/test_handler/test_handler_syslog.py +++ /dev/null @@ -1,32 +0,0 @@ -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)) -- cgit v1.2.3