diff --git a/doc/ref/modules/all/index.rst b/doc/ref/modules/all/index.rst index 32cd98e35c..4ba4a07bd1 100644 --- a/doc/ref/modules/all/index.rst +++ b/doc/ref/modules/all/index.rst @@ -218,6 +218,7 @@ Full list of builtin execution modules svn swift sysbench + syslog_ng sysmod system systemd diff --git a/doc/ref/modules/all/salt.modules.syslog_ng.rst b/doc/ref/modules/all/salt.modules.syslog_ng.rst new file mode 100644 index 0000000000..574fdc1e17 --- /dev/null +++ b/doc/ref/modules/all/salt.modules.syslog_ng.rst @@ -0,0 +1,6 @@ +====================== +salt.modules.syslog_ng +====================== + +.. automodule:: salt.modules.syslog_ng + :members: \ No newline at end of file diff --git a/doc/ref/states/all/index.rst b/doc/ref/states/all/index.rst index cb57fdd5f4..41ab64ed40 100644 --- a/doc/ref/states/all/index.rst +++ b/doc/ref/states/all/index.rst @@ -123,6 +123,7 @@ Full list of builtin state modules supervisord svn sysctl + syslog_ng test timezone tomcat diff --git a/doc/ref/states/all/salt.states.syslog_ng.rst b/doc/ref/states/all/salt.states.syslog_ng.rst new file mode 100644 index 0000000000..dbeae64a8d --- /dev/null +++ b/doc/ref/states/all/salt.states.syslog_ng.rst @@ -0,0 +1,6 @@ +===================== +salt.states.syslog_ng +===================== + +.. automodule:: salt.states.syslog_ng + :members: \ No newline at end of file diff --git a/doc/topics/tutorials/syslog_ng-state-usage.rst b/doc/topics/tutorials/syslog_ng-state-usage.rst index 15c519e536..90239180a8 100644 --- a/doc/topics/tutorials/syslog_ng-state-usage.rst +++ b/doc/topics/tutorials/syslog_ng-state-usage.rst @@ -3,7 +3,10 @@ Syslog-ng usage =============== -The syslog\_ng state modul is to generate syslog-ng +Overview +-------- + +Syslog\_ng state module is for generating syslog-ng configurations. You can do the following things: - generate syslog-ng configuration from YAML, @@ -16,130 +19,199 @@ configuration, get the version and other information about syslog-ng. Configuration ------------- -The following configuration is an example, how a complete syslog-ng -state configuration looks like: +Users can create syslog-ng configuration statements with the +:py:func:`syslog_ng.config ` function. It requires +a `name` and a `config` parameter. The `name` parameter determines the name of +the generated statement and the `config` parameter holds a parsed YAML structure. + +A statement can be declared in the following forms (both are equivalent): + +.. code-block:: yaml + + source.s_localhost: + syslog_ng.config: + - config: + - tcp: + - ip: "127.0.0.1" + - port: 1233 + +.. code-block:: yaml + + s_localhost: + syslog_ng.config: + - config: + source: + - tcp: + - ip: "127.0.0.1" + - port: 1233 + +The first one is called short form, because it needs less typing. Users can use lists +and dictionaries to specify their configuration. The format is quite self describing and +there are more examples [at the end](#examples) of this document. + +Quotation +--------- +The quotation can be tricky sometimes but here are some rules to follow: + * when a string meant to be ``"string"`` in the generated configuration, it should be like + ``'"string"'`` in the YAML document + * similarly, users should write ``"'string'"`` to get ``'string'`` in the generated configuration + +Full example +------------ + +The following configuration is an example, how a complete syslog-ng configuration looks like: .. code-block:: yaml # Set the location of the configuration file - "/home/tibi/install/syslog-ng/etc/syslog-ng.conf": - syslog_ng.set_config_file + set_location: + module.run: + - name: syslog_ng.set_config_file + - m_name: "/home/tibi/install/syslog-ng/etc/syslog-ng.conf" - # The syslog-ng and syslog-ng-ctl binaries are here. You needn't use + # The syslog-ng and syslog-ng-ctl binaries are here. You needn't use # this method if these binaries can be found in a directory in your PATH. - "/home/tibi/install/syslog-ng/sbin": - syslog_ng.set_binary_path + set_bin_path: + module.run: + - name: syslog_ng.set_binary_path + - m_name: "/home/tibi/install/syslog-ng/sbin" # Writes the first lines into the config file, also erases its previous # content - "3.6": - syslog_ng.write_version + write_version: + module.run: + - name: syslog_ng.write_version + - m_name: "3.6" + + # There is a shorter form to set the above variables + set_variables: + module.run: + - name: syslog_ng.set_parameters + - version: "3.6" + - binary_path: "/home/tibi/install/syslog-ng/sbin" + - config_file: "/home/tibi/install/syslog-ng/etc/syslog-ng.conf" + # Some global options - global_options: + options.global_options: syslog_ng.config: - config: - options: - - time_reap: 30 - - mark_freq: 10 - - keep_hostname: "yes" + - time_reap: 30 + - mark_freq: 10 + - keep_hostname: "yes" - s_localhost: + source.s_localhost: syslog_ng.config: - config: - source: - - tcp: - - ip: "127.0.0.1" - - port: 1233 + - tcp: + - ip: "127.0.0.1" + - port: 1233 - d_log_server: + destination.d_log_server: syslog_ng.config: - config: - destination: - - tcp: - - "127.0.0.1" - - port: 1234 + - tcp: + - "127.0.0.1" + - port: 1234 - l_log_to_central_server: + log.l_log_to_central_server: syslog_ng.config: - config: - log: - - source: s_localhost - - destination: d_log_server + - source: s_localhost + - destination: d_log_server some_comment: - syslog_ng.write_config: + module.run: + - name: syslog_ng.write_config - config: | # Multi line # comment - auto_start_or_reload: - {% set pids = salt["ps.pgrep"]("syslog-ng") %} - {% if pids == None or pids|length == 0 %} - syslog_ng.started: - - user: tibi - {% else %} - syslog_ng.reloaded - {% endif %} + # An other mode to use comments or existing configuration snippets + config.other_comment_form: + syslog_ng.config: + - config: | + # Multi line + # comment - #auto_stop: - # syslog_ng.stopped -The ``3.6``, ``s_devlog``, ``d_log_server``, etc. are identifiers. The -second lines in each block are functions and their first parameter is -their id. The ``- config`` is the second named parameter of the -``syslog_ng.config`` function. This function can generate the syslog-ng -configuration from YAML. If the statement (source, destination, parser, + +The :py:func:`syslog_ng.reloaded ` function can generate syslog-ng configuration from YAML. If the statement (source, destination, parser, etc.) has a name, this function uses the id as the name, otherwise (log statement) it's purpose is like a mandatory comment. -You can use ``set_binary_path`` to set the directory which contains the -syslog-ng and syslog-ng-ctl binaries. If this directory is in your PATH, -you don't need to use this function. - -Under ``auto_start_or_reload`` you can see a Jinja template. If -syslog-ng isn't running it will start it, otherwise reload it. It uses -the process name ``syslog-ng`` to determine its running state. I suggest -that you use ``service`` state if it's available on your system. - After execution this example the syslog\_ng state will generate this file: .. code-block:: text - #Generated by Salt on 2014-06-19 16:53:11 - @version: 3.6 + #Generated by Salt on 2014-08-18 00:11:11 + @version: 3.6 - options { - time_reap(30); - mark_freq(10); - keep_hostname(yes); - }; + options { + time_reap( + 30 + ); + mark_freq( + 10 + ); + keep_hostname( + yes + ); + }; - source s_localhost { - tcp( - ip("127.0.0.1"), - port(1233) - ); - }; - destination d_log_server { - tcp( - "127.0.0.1", - port(1234) - ); - }; + source s_localhost { + tcp( + ip( + 127.0.0.1 + ), + port( + 1233 + ) + ); + }; - log { - source(s_localhost); - destination(d_log_server); - }; - # Multi line - # comment + destination d_log_server { + tcp( + 127.0.0.1, + port( + 1234 + ) + ); + }; + + + log { + source( + s_localhost + ); + destination( + d_log_server + ); + }; + + + # Multi line + # comment + + + # Multi line + # comment + Users can include arbitrary texts in the generated configuration with -using the ``write_config`` function. +using the ``config`` statement (see the example above). + +Syslog_ng module functions +-------------------------- + +You can use :py:func:`syslog_ng.set_binary_path ` +to set the directory which contains the +syslog-ng and syslog-ng-ctl binaries. If this directory is in your PATH, +you don't need to use this function. There is also a :py:func:`syslog_ng.set_config_file ` +function to set the location of the configuration file. Examples -------- @@ -165,7 +237,7 @@ Simple source - config: source: - file: - - file: "/var/log/apache/access.log" + - file: ''"/var/log/apache/access.log"'' - follow_freq : 1 - flags: - no-parse @@ -180,12 +252,26 @@ OR - config: source: - file: - - "/var/log/apache/access.log" + - ''"/var/log/apache/access.log"'' - follow_freq : 1 - flags: - no-parse - validate-utf8 +OR + +.. code-block:: yaml + + source.s_tail: + syslog_ng.config: + - config: + - file: + - ''"/var/log/apache/access.log"'' + - follow_freq : 1 + - flags: + - no-parse + - validate-utf8 + Complex source ~~~~~~~~~~~~~~ @@ -228,7 +314,7 @@ Filter - config: filter: - match: - - "@json:" + - ''"@json:"'' Template ~~~~~~~~ @@ -251,7 +337,7 @@ Template -config: template: - template: - - "$ISODATE $HOST $MSG\n" + - '"$ISODATE $HOST $MSG\n"' - template_escape: - "no" @@ -274,8 +360,8 @@ Rewrite - config: rewrite: - set: - - "${.json.message}" - - value : "$MESSAGE" + - '"${.json.message}"' + - value : '"$MESSAGE"' Global options ~~~~~~~~~~~~~~ @@ -353,7 +439,7 @@ Log - rewrite: r_set_message_to_MESSAGE - destination: - file: - - "/tmp/json-input.log" + - '"/tmp/json-input.log"' - template: t_gsoc2014 - flags: final - channel: @@ -366,4 +452,3 @@ Log - file: - "/tmp/all.log" - template: t_gsoc2014 - diff --git a/salt/modules/syslog_ng.py b/salt/modules/syslog_ng.py new file mode 100644 index 0000000000..4e976c6fff --- /dev/null +++ b/salt/modules/syslog_ng.py @@ -0,0 +1,1179 @@ +# -*- coding: utf-8 -*- +''' +Module for getting information about syslog-ng +=============================================== + +:maintainer: Tibor Benke +:maturity: new +:depends: cmd +:platform: all + +This module is capable of managing syslog-ng instances which were installed + via a package manager or from source. Users can use a directory as a parameter +in the case of most functions, which contains the syslog-ng and syslog-ng-ctl +binaries. + +Syslog-ng can be installed via a package manager or from source. In the +latter case, the syslog-ng and syslog-ng-ctl binaries are not available +from the PATH, so users should set location of the sbin directory with +:mod:`syslog_ng.set_binary_path `. + +Similarly, users can specify the location of the configuration file with +:mod:`syslog_ng.set_config_file `, then +the module will use it. If it is not set, syslog-ng uses the default +configuration file. + + +''' + +from __future__ import generators, with_statement + +import time +import logging +import salt +import os +import os.path +import salt.utils +from salt.exceptions import CommandExecutionError + + +__SYSLOG_NG_BINARY_PATH = None +__SYSLOG_NG_CONFIG_FILE = '/etc/syslog-ng.conf' +__SALT_GENERATED_CONFIG_HEADER = '''#Generated by Salt on {0}''' + + +class SyslogNgError(Exception): + pass + + +log = logging.getLogger(__name__) +log.setLevel(logging.DEBUG) + +_INDENT = "" +_INDENT_STEP = " " + +# These are needed during building of the configuration tree +_current_statement = None +_current_option = None +_current_parameter = None +_current_parameter_value = None + + +def _increase_indent(): + ''' + Increases the indentation level. + ''' + global _INDENT + _INDENT += _INDENT_STEP + + +def _decrease_indent(): + ''' + Decreases the indentation level. + ''' + global _INDENT + _INDENT = _INDENT[4:] + + +def _indent(value): + ''' + Returns the indented parameter. + ''' + return "{0}{1}".format(_INDENT, value) + + +def _indentln(string): + ''' + Return the indented parameter with newline. + ''' + return _indent(string + "\n") + + +class Buildable(object): + ''' + Base class of most classes, which have a build method. + + It contains a common build function. + + Does not need examples:. + ''' + + def __init__(self, iterable, join_body_on='', append_extra_newline=True): + self.iterable = iterable + self.join_body_on = join_body_on + self.append_extra_newline = append_extra_newline + + def build_header(self): + ''' + Builds the header of a syslog-ng configuration object. + ''' + return '' + + def build_tail(self): + ''' + Builds the tail of a syslog-ng configuration object. + ''' + return '' + + def build_body(self): + ''' + Builds the body of a syslog-ng configuration object. + ''' + _increase_indent() + body_array = map(lambda x: x.build(), self.iterable) + + nl = "\n" if self.append_extra_newline else '' + + if len(self.iterable) >= 1: + body = self.join_body_on.join(body_array) + nl + else: + body = '' + + _decrease_indent() + return body + + def build(self): + ''' + Builds the textual representation of the whole configuration object + with it's children. + ''' + header = self.build_header() + body = self.build_body() + tail = self.build_tail() + return header + body + tail + + +class Statement(Buildable): + ''' + It represents a syslog-ng configuration statement, e.g. source, destination, + filter. + + Does not need examples:. + ''' + + def __init__(self, type, id='', options=None, has_name=True): + super(Statement, self).__init__(options, join_body_on='', append_extra_newline=False) + self.type = type + self.id = id + self.options = options if options else [] + self.iterable = self.options + self.has_name = has_name + + def build_header(self): + if self.has_name: + return _indentln("{0} {1} {{".format(self.type, self.id)) + else: + return _indentln("{0} {{".format(self.type)) + + def build_tail(self): + return _indentln("};") + + def add_child(self, option): + self.options.append(option) + + +class NamedStatement(Statement): + ''' + It represents a configuration statement, which has a name, e.g. a source. + + Does not need examples:. + ''' + def __init__(self, type, id='', options=None): + super(NamedStatement, self).__init__(type, id, options, has_name=True) + + +class UnnamedStatement(Statement): + ''' + It represents a configuration statement, which doesn't have a name, e.g. a + log path. + + Does not need examples:. + ''' + + def __init__(self, type, options=None): + super(UnnamedStatement, self).__init__(type, id='', options=options, has_name=False) + + +class GivenStatement(Buildable): + ''' + This statement returns a string without modification. It can be used to + use existing configuration snippets. + + Does not need examples:. + ''' + + def __init__(self, value, add_newline=True): + super(GivenStatement, self).__init__(iterable=None) + self.value = value + self.add_newline = add_newline + + def build(self): + if self.add_newline: + return self.value + "\n" + else: + return self.value + + +class Option(Buildable): + ''' + A Statement class contains Option instances. + + An instance of Option can represent a file(), tcp(), udp(), etc. option. + + Does not need examples:. + ''' + + def __init__(self, type='', params=None): + super(Option, self).__init__(params, ",\n") + self.type = type + self.params = params if params else [] + self.iterable = self.params + + def build(self): + header = _indentln("{0}(".format(self.type)) + tail = _indentln(");") + body = self.build_body() + + return header + body + tail + + def add_parameter(self, param): + self.params.append(param) + + +class Parameter(Buildable): + ''' + An Option has one or more Parameter instances. + + Does not need examples:. + ''' + + def __init__(self, iterable=None, join_body_on=''): + super(Parameter, self).__init__(iterable, join_body_on) + + +class SimpleParameter(Parameter): + ''' + A Parameter is a SimpleParameter, if it's just a simple type, like a string. + + For example: + + destination d_file { + file( + "/var/log/messages" + ); + }; + + "/var/log/messages" is a SimpleParameter. + + Does not need examples:. + ''' + + def __init__(self, value=''): + super(SimpleParameter, self).__init__() + self.value = value + + def build(self): + return _indent(self.value) + + +class TypedParameter(Parameter): + ''' + A Parameter, which has a type: + + destination d_tcp { + tcp( + ip(127.0.0.1) + ); + }; + + ip(127.0.0.1) is a TypedParameter. + + Does not need examples:. + ''' + + def __init__(self, type='', values=None): + super(TypedParameter, self).__init__(values, ",\n") + self.type = type + self.values = values if values else [] + self.iterable = self.values + + def build(self): + header = _indentln("{0}(".format(self.type)) + tail = _indent(")") + body = self.build_body() + + return header + body + tail + + def add_value(self, value): + self.values.append(value) + + +class ParameterValue(Buildable): + ''' + A TypedParameter can have one or more values. + + Does not need examples:. + ''' + + def __init__(self, iterable=None, join_body_on=''): + super(ParameterValue, self).__init__(iterable, join_body_on) + + +class SimpleParameterValue(ParameterValue): + ''' + A ParameterValuem which holds a simple type, like a string or a number. + + For example in ip(127.0.0.1) 127.0.0.1 is a SimpleParameterValue. + + Does not need examples:. + ''' + + def __init__(self, value=''): + super(SimpleParameterValue, self).__init__() + self.value = value + + def build(self): + return _indent(self.value) + + +class TypedParameterValue(ParameterValue): + ''' + We have to go deeper... + + A TypedParameter can have a 'parameter', which also have a type. For example + key_file and cert_file: + + source demo_tls_source { + tcp( + ip(0.0.0.0) + port(1999) + tls( + key_file("/opt/syslog-ng/etc/syslog-ng/key.d/syslog-ng.key") + cert_file("/opt/syslog-ng/etc/syslog-ng/cert.d/syslog-ng.cert") + ) + ); + }; + + Does not need examples:. + ''' + + def __init__(self, type='', arguments=None): + super(TypedParameterValue, self).__init__(arguments, "\n") + self.type = type + self.arguments = arguments if arguments else [] + self.iterable = self.arguments + + def build(self): + header = _indentln("{0}(".format(self.type)) + tail = _indent(")") + body = self.build_body() + + return header + body + tail + + def add_argument(self, arg): + self.arguments.append(arg) + + +class Argument(object): + ''' + A TypedParameterValue has one or more Arguments. For example this can be + the value of key_file. + + Does not need examples:. + ''' + + def __init__(self, value=''): + self.value = value + + def build(self): + return _indent(self.value) + + +def _is_statement_unnamed(statement): + ''' + Returns True, if the given statement is an unnamed statement, like log or + junction. + + ''' + return statement in ('log', 'channel', 'junction', 'options') + + +def _is_simple_type(value): + ''' + Returns True, if the given parameter value is an instance of either + int, str, float or bool. + ''' + return isinstance(value, str) or isinstance(value, int) or isinstance(value, float) or isinstance(value, bool) + + +def _get_type_id_options(name, configuration): + ''' + Returns the type, id and option of a configuration object. + ''' + # it's in a form of source.name + if '.' in name: + type, sep, id = name.partition('.') + options = configuration + else: + type = configuration.keys()[0] + id = name + options = configuration[type] + + return type, id, options + + +def _expand_one_key_dictionary(d): + ''' + Returns the only one key and it's value from a dictionary. + ''' + key = d.keys()[0] + value = d[key] + return key, value + + +def _parse_typed_parameter_typed_value(values): + ''' + Creates Arguments in a TypedParametervalue. + ''' + type, value = _expand_one_key_dictionary(values) + + _current_parameter_value.type = type + if _is_simple_type(value): + a = Argument(value) + _current_parameter_value.add_argument(a) + elif isinstance(value, list): + for i in value: + a = Argument(i) + _current_parameter_value.add_argument(a) + + +def _parse_typed_parameter(param): + ''' + Parses a TypedParameter and fills it with values. + ''' + global _current_parameter_value + type, value = _expand_one_key_dictionary(param) + _current_parameter.type = type + + if _is_simple_type(value) and value != '': + _current_parameter_value = SimpleParameterValue(value) + _current_parameter.add_value(_current_parameter_value) + elif isinstance(value, list): + for i in value: + if _is_simple_type(i): + _current_parameter_value = SimpleParameterValue(i) + _current_parameter.add_value(_current_parameter_value) + elif isinstance(i, dict): + _current_parameter_value = TypedParameterValue() + _parse_typed_parameter_typed_value(i) + _current_parameter.add_value(_current_parameter_value) + + +def _create_and_add_parameters(params): + ''' + Parses the configuration and creates Parameter instances. + ''' + global _current_parameter + if _is_simple_type(params): + _current_parameter = SimpleParameter(params) + _current_option.add_parameter(_current_parameter) + else: + # must be a list + for i in params: + if _is_simple_type(i): + _current_parameter = SimpleParameter(i) + else: + _current_parameter = TypedParameter() + _parse_typed_parameter(i) + _current_option.add_parameter(_current_parameter) + + +def _create_and_add_option(option): + ''' + Parses the configuration and creates an Option instance. + ''' + global _current_option + + _current_option = Option() + type, params = _expand_one_key_dictionary(option) + _current_option.type = type + _create_and_add_parameters(params) + _current_statement.add_child(_current_option) + + +def _parse_statement(options): + ''' + Parses the configuration and creates options the statement. + ''' + for option in options: + _create_and_add_option(option) + + +def _is_reference(arg): + ''' + Return True, if arg is a reference to a previously defined statement. + ''' + return isinstance(arg, dict) and len(arg) == 1 and isinstance(arg.values()[0], str) + + +def _is_junction(arg): + ''' + Return True, if arg is a junction statement. + ''' + return isinstance(arg, dict) and len(arg) == 1 and arg.keys()[0] == 'junction' + + +def _add_reference(reference, statement): + ''' + Adds a reference to statement. + ''' + type, value = _expand_one_key_dictionary(reference) + o = Option(type) + p = SimpleParameter(value) + o.add_parameter(p) + statement.add_child(o) + + +def _is_inline_definition(arg): + ''' + Returns True, if arg is an inline definition of a statement. + ''' + return isinstance(arg, dict) and len(arg) == 1 and isinstance(arg.values()[0], list) + + +def _add_inline_definition(item, statement): + ''' + Adds an inline definition to statement. + ''' + global _current_statement + backup = _current_statement + + type, options = _expand_one_key_dictionary(item) + _current_statement = UnnamedStatement(type=type) + _parse_statement(options) + statement.add_child(_current_statement) + + _current_statement = backup + + +def _add_junction(item): + ''' + Adds a junction to the _current_statement. + ''' + type, channels = _expand_one_key_dictionary(item) + junction = UnnamedStatement(type='junction') + for ch in channels: + type, value = _expand_one_key_dictionary(ch) + channel = UnnamedStatement(type='channel') + for j in value: + if _is_reference(j): + _add_reference(j, channel) + elif _is_inline_definition(j): + _add_inline_definition(j, channel) + junction.add_child(channel) + _current_statement.add_child(junction) + + +def _parse_log_statement(options): + ''' + Parses a log path. + ''' + for i in options: + if _is_reference(i): + _add_reference(i, _current_statement) + elif _is_junction(i): + _add_junction(i) + elif _is_inline_definition(i): + _add_inline_definition(i, _current_statement) + + +def _build_config_tree(name, configuration): + ''' + Build the configuration tree. + + The root object is _current_statement. + ''' + type, id, options = _get_type_id_options(name, configuration) + global _INDENT, _current_statement + _INDENT = '' + if type == 'config': + _current_statement = GivenStatement(options) + elif type == 'log': + _current_statement = UnnamedStatement(type='log') + _parse_log_statement(options) + else: + if _is_statement_unnamed(type): + _current_statement = UnnamedStatement(type=type) + else: + _current_statement = NamedStatement(type=type, id=id) + _parse_statement(options) + + +def _render_configuration(): + ''' + Renders the configuration tree into syslog-ng's configuration syntax. + ''' + text_repr = _current_statement.build() + _INDENT = '' + return text_repr + + +def config(name, + config, + write=True): + ''' + Builds syslog-ng configuration. This function is intended to be used from + the state module, users should not use it directly! + + name : the id of the Salt document or it is the format of .id + config : the parsed YAML code + write : if True, it writes the config into the configuration file, + otherwise just returns it + + CLI Example: + + .. code-block:: bash + + salt '*' syslog_ng.config name="s_local" config="[{'tcp':[{'ip':'127.0.0.1'},{'port':1233}]}]" + + ''' + + _build_config_tree(name, config) + configs = _render_configuration() + + if __opts__.get('test', False): + comment = "State syslog_ng will write '{0}' into {1}".format(configs, __SYSLOG_NG_CONFIG_FILE) + return _format_state_result(name, result=None, comment=comment) + + succ = write + if write: + succ = _write_config(config=configs) + + return _format_state_result(name, result=succ, changes={'new': configs, 'old': ''}) + + +def set_binary_path(name): + ''' + Sets the path, where the syslog-ng binary can be found. This function is + intended to be used from states. + + If syslog-ng is installed via a package manager, users don't need to use + this function. + + CLI Example: + + .. code-block:: bash + + salt '*' syslog_ng.set_binary_path name="/usr/sbin" + + ''' + global __SYSLOG_NG_BINARY_PATH + old = __SYSLOG_NG_BINARY_PATH + __SYSLOG_NG_BINARY_PATH = name + changes = _format_changes(old, name) + return _format_state_result(name, result=True, changes=changes) + + +def set_config_file(name): + ''' + Sets the configuration's name. This function is intended to be used from states. + + CLI Example: + + .. code-block:: bash + + salt '*' syslog_ng.set_config_file name="/etc/syslog-ng" + + ''' + global __SYSLOG_NG_CONFIG_FILE + old = __SYSLOG_NG_CONFIG_FILE + __SYSLOG_NG_CONFIG_FILE = name + changes = _format_changes(old, name) + return _format_state_result(name, result=True, changes=changes) + + +def get_config_file(): + ''' + Returns the configuration directory, which contains syslog-ng.conf. + + CLI Example: + + .. code-block:: bash + + salt '*' syslog_ng.get_config_file + + ''' + return __SYSLOG_NG_CONFIG_FILE + + +def _run_command(cmd, options=()): + ''' + Runs the command cmd with options as its CLI parameters and returns the result + as a dictionary. + ''' + cmd_with_params = [cmd] + cmd_with_params.extend(options) + + cmd_to_run = " ".join(cmd_with_params) + + try: + return __salt__['cmd.run_all'](cmd_to_run) + except Exception as err: + log.error(str(err)) + raise CommandExecutionError("Unable to run command: " + str(type(err))) + + +def _determine_config_version(syslog_ng_sbin_dir): + ret = version(syslog_ng_sbin_dir) + full_version = ret['stdout'] + dot_count = 0 + for i, c in enumerate(full_version): + if c == '.': + dot_count = dot_count + 1 + if dot_count == 2: + return full_version[0:i] + # return first 3 characters + return full_version[:3] + + +def set_parameters(version=None, + binary_path=None, + config_file=None, + *args, + **kwargs): + ''' + Sets variables. + + CLI Example: + + .. code-block:: bash + + salt '*' syslog_ng.set_parameters version="3.6" + salt '*' syslog_ng.set_parameters binary_path="/home/user/install/syslog-ng/sbin" config_file="/home/user/install/syslog-ng/etc/syslog-ng.conf" + + ''' + if binary_path: + set_binary_path(binary_path) + if config_file: + set_config_file(config_file) + if version: + version = _determine_config_version(__SYSLOG_NG_BINARY_PATH) + write_version(version) + + return _format_return_data(0) + + +def _add_to_path_envvar(directory): + ''' + Adds directory to the PATH environment variable and returns the original + one. + ''' + orig_path = os.environ["PATH"] + if directory: + if not os.path.isdir(directory): + log.error("The given parameter is not a directory") + + os.environ["PATH"] = "{0}{1}{2}".format(orig_path, os.pathsep, directory) + + return orig_path + + +def _restore_path_envvar(original): + ''' + Sets the PATH environment variable to the parameter. + ''' + if original: + os.environ["PATH"] = original + + +def _run_command_in_extended_path(syslog_ng_sbin_dir, command, params): + ''' + Runs the given command in an environment, where the syslog_ng_sbin_dir is + added then removed from the PATH. + ''' + orig_path = _add_to_path_envvar(syslog_ng_sbin_dir) + + if not salt.utils.which(command): + error_message = "Unable to execute the command '{0}'. It is not in the PATH.".format(command) + log.error(error_message) + _restore_path_envvar(orig_path) + raise CommandExecutionError(error_message) + + ret = _run_command(command, options=params) + _restore_path_envvar(orig_path) + return ret + + +def _format_return_data(retcode, stdout=None, stderr=None): + ''' + Creates a dictionary from the parameters, which can be used to return data + to Salt. + ''' + ret = {"retcode": retcode} + if stdout is not None: + ret["stdout"] = stdout + if stderr is not None: + ret["stderr"] = stderr + return ret + + +def config_test(syslog_ng_sbin_dir=None, cfgfile=None): + ''' + Runs syntax check against cfgfile. If syslog_ng_sbin_dir is specified, it + is added to the PATH during the test. + + CLI Example: + + .. code-block:: bash + + salt '*' syslog_ng.config_test + salt '*' syslog_ng.config_test /home/user/install/syslog-ng/sbin + salt '*' syslog_ng.config_test /home/user/install/syslog-ng/sbin /etc/syslog-ng/syslog-ng.conf + ''' + params = ["--syntax-only", ] + if cfgfile: + params.append("--cfgfile={0}".format(cfgfile)) + + try: + ret = _run_command_in_extended_path(syslog_ng_sbin_dir, "syslog-ng", params) + except CommandExecutionError as err: + return _format_return_data(retcode=-1, stderr=str(err)) + + retcode = ret.get("retcode", -1) + stderr = ret.get("stderr", None) + stdout = ret.get("stdout", None) + return _format_return_data(retcode, stdout, stderr) + + +def version(syslog_ng_sbin_dir=None): + ''' + Returns the version of the installed syslog-ng. If syslog_ng_sbin_dir is specified, it + is added to the PATH during the execution of the command syslog-ng. + + CLI Example: + + .. code-block:: bash + + salt '*' syslog_ng.version + salt '*' syslog_ng.version /home/user/install/syslog-ng/sbin + ''' + try: + ret = _run_command_in_extended_path(syslog_ng_sbin_dir, "syslog-ng", ("-V",)) + except CommandExecutionError as err: + return _format_return_data(retcode=-1, stderr=str(err)) + + if ret["retcode"] != 0: + return _format_return_data(ret["retcode"], stderr=ret["stderr"], stdout=ret["stdout"]) + + lines = ret["stdout"].split("\n") + # The format of the first line in the output is: + # syslog-ng 3.6.0alpha0 + version_line_index = 0 + version_column_index = 1 + v = lines[version_line_index].split()[version_column_index] + return _format_return_data(0, stdout=v) + + +def modules(syslog_ng_sbin_dir=None): + ''' + Returns the available modules. If syslog_ng_sbin_dir is specified, it + is added to the PATH during the execution of the command syslog-ng. + + CLI Example: + + .. code-block:: bash + + salt '*' syslog_ng.modules + salt '*' syslog_ng.modules /home/user/install/syslog-ng/sbin + ''' + try: + ret = _run_command_in_extended_path(syslog_ng_sbin_dir, "syslog-ng", ("-V",)) + except CommandExecutionError as err: + return _format_return_data(retcode=-1, stderr=str(err)) + + if ret["retcode"] != 0: + return _format_return_data(ret["retcode"], ret.get("stdout", None), ret.get("stderr", None)) + + lines = ret["stdout"].split("\n") + for i, line in enumerate(lines): + if line.startswith("Available-Modules"): + label, installed_modules = line.split() + return _format_return_data(ret["retcode"], stdout=installed_modules) + return _format_return_data(-1, stderr="Unable to find the modules.") + + +def stats(syslog_ng_sbin_dir=None): + ''' + Returns statistics from the running syslog-ng instance. If syslog_ng_sbin_dir is specified, it + is added to the PATH during the execution of the command syslog-ng-ctl. + + CLI Example: + + .. code-block:: bash + + salt '*' syslog_ng.stats + salt '*' syslog_ng.stats /home/user/install/syslog-ng/sbin + ''' + try: + ret = _run_command_in_extended_path(syslog_ng_sbin_dir, "syslog-ng-ctl", ("stats",)) + except CommandExecutionError as err: + return _format_return_data(retcode=-1, stderr=str(err)) + + return _format_return_data(ret["retcode"], ret.get("stdout", None), ret.get("stderr", None)) + + +def _format_changes(old='', new=''): + return {'old': old, 'new': new} + + +def _format_state_result(name, result, changes=None, comment=''): + ''' + Creates the state result dictionary. + ''' + if changes is None: + changes = {'old': '', 'new': ''} + return {'name': name, 'result': result, 'changes': changes, 'comment': comment} + + +def _add_cli_param(params, key, value): + ''' + Adds key and value as a command line parameter to params. + ''' + if value is not None: + params.append('--{0}={1}'.format(key, value)) + + +def _add_boolean_cli_param(params, key, value): + ''' + Adds key as a command line parameter to params. + ''' + if value is True: + params.append('--{0}'.format(key)) + + +def stop(name=None): + ''' + Kills syslog-ng. This function is intended to be used from the state module. + + Users shouldn't use this function, if the service module is available on + their system. If :mod:`syslog_ng.set_config_file ` + is called before, this function will use the set binary path. + + CLI Example: + + .. code-block:: bash + + salt '*' syslog_ng.stop + + ''' + pids = __salt__['ps.pgrep'](pattern='syslog-ng') + + if pids is None or len(pids) == 0: + return _format_state_result(name, + result=False, + comment='Syslog-ng is not running') + + if __opts__.get('test', False): + comment = "Syslog_ng state module will kill {0} pids" + return _format_state_result(name, result=None, comment=comment) + + res = __salt__['ps.pkill']('syslog-ng') + killed_pids = res['killed'] + + if killed_pids == pids: + changes = {'old': killed_pids, 'new': []} + return _format_state_result(name, result=True, changes=changes) + else: + return _format_state_result(name, result=False) + + +def start(name=None, + user=None, + group=None, + chroot=None, + caps=None, + no_caps=False, + pidfile=None, + enable_core=False, + fd_limit=None, + verbose=False, + debug=False, + trace=False, + yydebug=False, + persist_file=None, + control=None, + worker_threads=None): + ''' + Ensures, that syslog-ng is started via the given parameters. This function + is intended to be used from the state module. + + Users shouldn't use this function, if the service module is available on + their system. If :mod:`syslog_ng.set_config_file `, + is called before, this function will use the set binary path. + + CLI Example: + + .. code-block:: bash + + salt '*' syslog_ng.start + + ''' + params = [] + _add_cli_param(params, 'user', user) + _add_cli_param(params, 'group', group) + _add_cli_param(params, 'chroot', chroot) + _add_cli_param(params, 'caps', caps) + _add_boolean_cli_param(params, 'no-capse', no_caps) + _add_cli_param(params, 'pidfile', pidfile) + _add_boolean_cli_param(params, 'enable-core', enable_core) + _add_cli_param(params, 'fd-limit', fd_limit) + _add_boolean_cli_param(params, 'verbose', verbose) + _add_boolean_cli_param(params, 'debug', debug) + _add_boolean_cli_param(params, 'trace', trace) + _add_boolean_cli_param(params, 'yydebug', yydebug) + _add_cli_param(params, 'cfgfile', __SYSLOG_NG_CONFIG_FILE) + _add_boolean_cli_param(params, 'persist-file', persist_file) + _add_cli_param(params, 'control', control) + _add_cli_param(params, 'worker-threads', worker_threads) + cli_params = ' '.join(params) + if __SYSLOG_NG_BINARY_PATH: + syslog_ng_binary = os.path.join(__SYSLOG_NG_BINARY_PATH, 'syslog-ng') + command = syslog_ng_binary + ' ' + cli_params + + if __opts__.get('test', False): + comment = "Syslog_ng state module will start {0}".format(command) + return _format_state_result(name, result=None, comment=comment) + + result = __salt__['cmd.run_all'](command) + else: + command = 'syslog-ng ' + cli_params + + if __opts__.get('test', False): + comment = "Syslog_ng state module will start {0}".format(command) + return _format_state_result(name, result=None, comment=comment) + + result = __salt__['cmd.run_all'](command) + + if result['pid'] > 0: + succ = True + else: + succ = False + + return _format_state_result( + name, result=succ, changes={'new': command, 'old': ''} + ) + + +def reload(name): + ''' + Reloads syslog-ng. This function is intended to be used from states. + + If :mod:`syslog_ng.set_config_file `, + is called before, this function will use the set binary path. + + CLI Example: + + .. code-block:: bash + + salt '*' syslog_ng.reload + + ''' + if __SYSLOG_NG_BINARY_PATH: + syslog_ng_ctl_binary = os.path.join(__SYSLOG_NG_BINARY_PATH, 'syslog-ng-ctl') + command = syslog_ng_ctl_binary + ' reload' + result = __salt__['cmd.run_all'](command) + else: + command = 'syslog-ng-ctl reload' + result = __salt__['cmd.run_all'](command) + + succ = True if result['retcode'] == 0 else False + return _format_state_result(name, result=succ, comment=result['stdout']) + + +def _format_generated_config_header(): + ''' + Formats a header, which is prepended to all appended config. + ''' + now = time.strftime('%Y-%m-%d %H:%M:%S') + return __SALT_GENERATED_CONFIG_HEADER.format(now) + + +def write_config(config, newlines=2): + ''' + Writes the given parameter config into the config file. This function is + intended to be used from states. + + If :mod:`syslog_ng.set_config_file `, + is called before, this function will use the set config file. + + CLI Example: + + .. code-block:: bash + + salt '*' syslog_ng.write_config config="# comment" + + ''' + succ = _write_config(config, newlines) + changes = _format_changes(new=config) + return _format_state_result(name='', result=succ, changes=changes) + + +def _write_config(config, newlines=2): + ''' + Writes the given parameter config into the config file. + ''' + text = config + if isinstance(config, dict) and len(config.keys()) == 1: + key = config.keys()[0] + text = config[key] + + try: + open_flags = 'a' + + with open(__SYSLOG_NG_CONFIG_FILE, open_flags) as f: + f.write(text) + + for i in range(0, newlines): + f.write(os.linesep) + + return True + except Exception as err: + log.error(str(err)) + return False + + +def write_version(name): + ''' + Removes the previous configuration file, then creates a new one and writes the name line. + This function is intended to be used from states. + + If :mod:`syslog_ng.set_config_file `, + is called before, this function will use the set config file. + + CLI Example: + + .. code-block:: bash + + salt '*' syslog_ng.write_version name="3.6" + + ''' + line = '@version: {0}'.format(name) + try: + if os.path.exists(__SYSLOG_NG_CONFIG_FILE): + log.debug( + 'Removing previous configuration file: {0}'.format(__SYSLOG_NG_CONFIG_FILE) + ) + os.remove(__SYSLOG_NG_CONFIG_FILE) + log.debug('Configuration file successfully removed') + + header = _format_generated_config_header() + _write_config(config=header, newlines=1) + _write_config(config=line, newlines=2) + + return _format_state_result(name, result=True) + except os.error as err: + log.error( + 'Failed to remove previous configuration file {0!r} because: {1}' + .format(__SYSLOG_NG_CONFIG_FILE, str(err)) + ) + return _format_state_result(name, result=False) diff --git a/salt/states/syslog_ng.py b/salt/states/syslog_ng.py new file mode 100644 index 0000000000..4def89cc96 --- /dev/null +++ b/salt/states/syslog_ng.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +''' +State module for syslog_ng +========================== + +:maintainer: Tibor Benke +:maturity: new +:depends: cmd, ps, syslog_ng +:platform: all + +Users can generate syslog-ng configuration files from YAML format or use + plain ones and reload, start, or stop their syslog-ng by using this module. + +Details +------- + +The service module is not available on all system, so this module includes +:mod:`syslog_ng.reloaded `, +:mod:`syslog_ng.stopped `, +and :mod:`syslog_ng.started ` functions. +If the service module is available on the computers, users should use that. + +Users can generate syslog-ng configuration with +:mod:`syslog_ng.config ` function. +For more information see :doc:`syslog-ng state usage `. + +Syslog-ng configuration file format +----------------------------------- + +The syntax of a configuration snippet in syslog-ng.conf: + + .. + + object_type object_id {}; + + +These constructions are also called statements. There are options inside of them: + + .. + + option(parameter1, parameter2); option2(parameter1, parameter2); + +You can find more information about syslog-ng's configuration syntax in the +Syslog-ng Admin guide: +http://www.balabit.com/sites/default/files/documents/syslog-ng-ose-3.5-guides/en/syslog-ng-ose-v3.5-guide-admin/html-single/index.html#syslog-ng.conf.5 +''' + +from __future__ import generators, print_function, with_statement +import logging + + +log = logging.getLogger(__name__) + + +def config(name, + config, + write=True): + ''' + Builds syslog-ng configuration. + + name : the id of the Salt document + config : the parsed YAML code + write : if True, it writes the config into the configuration file, + otherwise just returns it + ''' + return __salt__['syslog_ng.config'](name, config, write) + + +def stopped(name=None): + ''' + Kills syslog-ng. + ''' + return __salt__['syslog_ng.stop'](name) + + +def started(name=None, + user=None, + group=None, + chroot=None, + caps=None, + no_caps=False, + pidfile=None, + enable_core=False, + fd_limit=None, + verbose=False, + debug=False, + trace=False, + yydebug=False, + persist_file=None, + control=None, + worker_threads=None, + *args, + **kwargs): + ''' + Ensures, that syslog-ng is started via the given parameters. + + Users shouldn't use this function, if the service module is available on + their system. + ''' + return __salt__['syslog_ng.start'](name=name, + user=user, + group=group, + chroot=chroot, + caps=caps, + no_caps=no_caps, + pidfile=pidfile, + enable_core=enable_core, + fd_limit=fd_limit, + verbose=verbose, + debug=debug, + trace=trace, + yydebug=yydebug, + persist_file=persist_file, + control=control, + worker_threads=worker_threads) + + +def reloaded(name): + ''' + Reloads syslog-ng. + ''' + return __salt__['syslog_ng.reload'](name) diff --git a/tests/unit/modules/syslog_ng_test.py b/tests/unit/modules/syslog_ng_test.py new file mode 100644 index 0000000000..325afbabad --- /dev/null +++ b/tests/unit/modules/syslog_ng_test.py @@ -0,0 +1,285 @@ +# -*- coding: utf-8 -*- +''' +Test module for syslog_ng +''' + +# Import Salt Testing libs +import salt +from salttesting import skipIf, TestCase +from salttesting.helpers import ensure_in_syspath +from salttesting.mock import NO_MOCK, NO_MOCK_REASON, MagicMock, patch +from textwrap import dedent + +ensure_in_syspath('../../') + +from salt.modules import syslog_ng + +syslog_ng.__salt__ = {} +syslog_ng.__opts__ = {} + +_VERSION = "3.6.0alpha0" +_MODULES = ("syslogformat,json-plugin,basicfuncs,afstomp,afsocket,cryptofuncs," + "afmongodb,dbparser,system-source,affile,pseudofile,afamqp," + "afsocket-notls,csvparser,linux-kmsg-format,afuser,confgen,afprog") + +VERSION_OUTPUT = """syslog-ng {0} +Installer-Version: {0} +Revision: +Compile-Date: Apr 4 2014 20:26:18 +Error opening plugin module; module='afsocket-tls', error='/home/tibi/install/syslog-ng/lib/syslog-ng/libafsocket-tls.so: undefined symbol: tls_context_setup_session' +Available-Modules: {1} +Enable-Debug: on +Enable-GProf: off +Enable-Memtrace: off +Enable-IPv6: on +Enable-Spoof-Source: off +Enable-TCP-Wrapper: off +Enable-Linux-Caps: off""".format(_VERSION, _MODULES) + +STATS_OUTPUT = """SourceName;SourceId;SourceInstance;State;Type;Number +center;;received;a;processed;0 +destination;#anon-destination0;;a;processed;0 +destination;#anon-destination1;;a;processed;0 +source;s_gsoc2014;;a;processed;0 +center;;queued;a;processed;0 +global;payload_reallocs;;a;processed;0 +global;sdata_updates;;a;processed;0 +global;msg_clones;;a;processed;0""" + +_SYSLOG_NG_NOT_INSTALLED_RETURN_VALUE = { + "retcode": -1, "stderr": + "Unable to execute the command 'syslog-ng'. It is not in the PATH." +} +_SYSLOG_NG_CTL_NOT_INSTALLED_RETURN_VALUE = { + "retcode": -1, "stderr": + "Unable to execute the command 'syslog-ng-ctl'. It is not in the PATH." +} + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class SyslogNGTestCase(TestCase): + + def test_statement_without_options(self): + s = syslog_ng.Statement("source", "s_local", options=[]) + b = s.build() + self.assertEqual(dedent( + """\ + source s_local { + }; + """), b) + + def test_non_empty_statement(self): + o1 = syslog_ng.Option("file") + o2 = syslog_ng.Option("tcp") + s = syslog_ng.Statement("source", "s_local", options=[o1, o2]) + b = s.build() + self.assertEqual(dedent( + """\ + source s_local { + file( + ); + tcp( + ); + }; + """), b) + + def test_option_with_parameters(self): + o1 = syslog_ng.Option("file") + p1 = syslog_ng.SimpleParameter('"/var/log/messages"') + p2 = syslog_ng.SimpleParameter() + p3 = syslog_ng.TypedParameter() + p3.type = "tls" + p2.value = '"/var/log/syslog"' + o1.add_parameter(p1) + o1.add_parameter(p2) + o1.add_parameter(p3) + b = o1.build() + self.assertEqual(dedent( + """\ + file( + "/var/log/messages", + "/var/log/syslog", + tls( + ) + ); + """), b) + + def test_parameter_with_values(self): + p = syslog_ng.TypedParameter() + p.type = "tls" + v1 = syslog_ng.TypedParameterValue() + v1.type = 'key_file' + + v2 = syslog_ng.TypedParameterValue() + v2.type = 'cert_file' + + p.add_value(v1) + p.add_value(v2) + + b = p.build() + self.assertEqual(dedent( + """\ + tls( + key_file( + ), + cert_file( + ) + )"""), b) + + def test_value_with_arguments(self): + t = syslog_ng.TypedParameterValue() + t.type = 'key_file' + + a1 = syslog_ng.Argument('"/opt/syslog-ng/etc/syslog-ng/key.d/syslog-ng.key"') + a2 = syslog_ng.Argument('"/opt/syslog-ng/etc/syslog-ng/key.d/syslog-ng.key"') + + t.add_argument(a1) + t.add_argument(a2) + + b = t.build() + self.assertEqual(dedent( + '''\ + key_file( + "/opt/syslog-ng/etc/syslog-ng/key.d/syslog-ng.key" + "/opt/syslog-ng/etc/syslog-ng/key.d/syslog-ng.key" + )'''), b) + + def test_end_to_end_statement_generation(self): + s = syslog_ng.Statement('source', 's_tls') + + o = syslog_ng.Option('tcp') + + ip = syslog_ng.TypedParameter('ip') + ip.add_value(syslog_ng.SimpleParameterValue("'192.168.42.2'")) + o.add_parameter(ip) + + port = syslog_ng.TypedParameter('port') + port.add_value(syslog_ng.SimpleParameterValue(514)) + o.add_parameter(port) + + tls = syslog_ng.TypedParameter('tls') + key_file = syslog_ng.TypedParameterValue('key_file') + key_file.add_argument(syslog_ng.Argument('"/opt/syslog-ng/etc/syslog-ng/key.d/syslog-ng.key"')) + cert_file = syslog_ng.TypedParameterValue('cert_file') + cert_file.add_argument(syslog_ng.Argument('"/opt/syslog-ng/etc/syslog-ng/cert.d/syslog-ng.cert"')) + peer_verify = syslog_ng.TypedParameterValue('peer_verify') + peer_verify.add_argument(syslog_ng.Argument('optional-untrusted')) + tls.add_value(key_file) + tls.add_value(cert_file) + tls.add_value(peer_verify) + o.add_parameter(tls) + + s.add_child(o) + b = s.build() + self.assertEqual(dedent( + '''\ + source s_tls { + tcp( + ip( + '192.168.42.2' + ), + port( + 514 + ), + tls( + key_file( + "/opt/syslog-ng/etc/syslog-ng/key.d/syslog-ng.key" + ), + cert_file( + "/opt/syslog-ng/etc/syslog-ng/cert.d/syslog-ng.cert" + ), + peer_verify( + optional-untrusted + ) + ) + ); + }; + '''), b) + + def test_version(self): + mock_return_value = {"retcode": 0, 'stdout': VERSION_OUTPUT} + expected_output = {"retcode": 0, "stdout": "3.6.0alpha0"} + mock_args = "syslog-ng -V" + self._assert_template(mock_args, + mock_return_value, + function_to_call=syslog_ng.version, + expected_output=expected_output) + + def test_stats(self): + mock_return_value = {"retcode": 0, 'stdout': STATS_OUTPUT} + expected_output = {"retcode": 0, "stdout": STATS_OUTPUT} + mock_args = "syslog-ng-ctl stats" + self._assert_template(mock_args, + mock_return_value, + function_to_call=syslog_ng.stats, + expected_output=expected_output) + + def test_modules(self): + mock_return_value = {"retcode": 0, 'stdout': VERSION_OUTPUT} + expected_output = {"retcode": 0, "stdout": _MODULES} + mock_args = "syslog-ng -V" + self._assert_template(mock_args, + mock_return_value, + function_to_call=syslog_ng.modules, + expected_output=expected_output) + + def test_config_test_ok(self): + mock_return_value = {"retcode": 0, "stderr": "", "stdout": "Syslog-ng startup text..."} + mock_args = "syslog-ng --syntax-only" + self._assert_template(mock_args, + mock_return_value, + function_to_call=syslog_ng.config_test, + expected_output=mock_return_value) + + def test_config_test_fails(self): + mock_return_value = {"retcode": 1, 'stderr': "Syntax error...", "stdout": ""} + mock_args = "syslog-ng --syntax-only" + self._assert_template(mock_args, + mock_return_value, + function_to_call=syslog_ng.config_test, + expected_output=mock_return_value) + + def test_config_test_cfgfile(self): + cfgfile = "/path/to/syslog-ng.conf" + mock_return_value = {"retcode": 1, 'stderr': "Syntax error...", "stdout": ""} + mock_args = "syslog-ng --syntax-only --cfgfile={0}".format(cfgfile) + self._assert_template(mock_args, + mock_return_value, + function_to_call=syslog_ng.config_test, + function_args={"cfgfile": cfgfile}, + expected_output=mock_return_value) + + def _assert_template(self, + mock_funtion_args, + mock_return_value, + function_to_call, + expected_output, + function_args=None): + if function_args is None: + function_args = {} + + installed = True + if not salt.utils.which("syslog-ng"): + installed = False + if "syslog-ng-ctl" in mock_funtion_args: + expected_output = _SYSLOG_NG_CTL_NOT_INSTALLED_RETURN_VALUE + else: + expected_output = _SYSLOG_NG_NOT_INSTALLED_RETURN_VALUE + + mock_function = MagicMock(return_value=mock_return_value) + + with patch.dict(syslog_ng.__salt__, {'cmd.run_all': mock_function}): + got = function_to_call(**function_args) + self.assertEqual(expected_output, got) + + if installed: + self.assertTrue(mock_function.called) + self.assertEqual(len(mock_function.call_args), 2) + mock_param = mock_function.call_args + self.assertTrue(mock_param[0][0].endswith(mock_funtion_args)) + + +if __name__ == '__main__': + from integration import run_tests + + run_tests(SyslogNGTestCase, needs_daemon=False) diff --git a/tests/unit/states/syslog_ng_test.py b/tests/unit/states/syslog_ng_test.py new file mode 100644 index 0000000000..1b39075a2b --- /dev/null +++ b/tests/unit/states/syslog_ng_test.py @@ -0,0 +1,388 @@ +# -*- coding: utf-8 -*- +''' +Test module for syslog_ng state +''' + +import yaml +import re +import tempfile +import os + +from salttesting import skipIf, TestCase +from salttesting.helpers import ensure_in_syspath +from salttesting.mock import NO_MOCK, NO_MOCK_REASON, MagicMock, patch + +ensure_in_syspath('../../') + +from salt.states import syslog_ng +from salt.modules import syslog_ng as syslog_ng_module + +syslog_ng.__salt__ = {} +syslog_ng_module.__salt__ = {} +syslog_ng_module.__opts__ = {'test': False} + +SOURCE_1_CONFIG = { + "id": "s_tail", + "config": ( + """ + source: + - file: + - '"/var/log/apache/access.log"' + - follow_freq : 1 + - flags: + - no-parse + - validate-utf8 + """) +} + +SOURCE_1_EXPECTED = ( +""" +source s_tail { + file( + "/var/log/apache/access.log", + follow_freq(1), + flags(no-parse, validate-utf8) + ); +}; +""" +) + +SOURCE_2_CONFIG = { + "id": "s_gsoc2014", + "config": ( + """ + source: + - tcp: + - ip: '"0.0.0.0"' + - port: 1234 + - flags: no-parse + """ + ) +} + +SOURCE_2_EXPECTED = ( +""" +source s_gsoc2014 { + tcp( + ip("0.0.0.0"), + port(1234), + flags(no-parse) + ); +};""" +) + +FILTER_1_CONFIG = { + "id": "f_json", + "config": ( + """ + filter: + - match: + - '"@json:"' + """ + ) +} + +FILTER_1_EXPECTED = ( + """ + filter f_json { + match( + "@json:" + ); + }; + """ +) + +TEMPLATE_1_CONFIG = { + "id": "t_demo_filetemplate", + "config": ( + """ + template: + - template: + - '"$ISODATE $HOST $MSG\n"' + - template_escape: + - "no" + """ + ) +} + +TEMPLATE_1_EXPECTED = ( + """ + template t_demo_filetemplate { + template( + "$ISODATE $HOST $MSG " + ); + template_escape( + no + ); + }; + """ +) + +REWRITE_1_CONFIG = { + "id": "r_set_message_to_MESSAGE", + "config": ( + """ + rewrite: + - set: + - '"${.json.message}"' + - value : '"$MESSAGE"' + """ + ) +} + +REWRITE_1_EXPECTED = ( + """ + rewrite r_set_message_to_MESSAGE { + set( + "${.json.message}", + value("$MESSAGE") + ); + }; + """ +) + +LOG_1_CONFIG = { + "id": "l_gsoc2014", + "config": ( + """ + log: + - source: s_gsoc2014 + - junction: + - channel: + - filter: f_json + - parser: p_json + - rewrite: r_set_json_tag + - rewrite: r_set_message_to_MESSAGE + - destination: + - file: + - '"/tmp/json-input.log"' + - template: t_gsoc2014 + - flags: final + - channel: + - filter: f_not_json + - parser: + - syslog-parser: [] + - rewrite: r_set_syslog_tag + - flags: final + - destination: + - file: + - '"/tmp/all.log"' + - template: t_gsoc2014 + """ + ) +} + +LOG_1_EXPECTED = ( + """ + log { + source(s_gsoc2014); + junction { + channel { + filter(f_json); + parser(p_json); + rewrite(r_set_json_tag); + rewrite(r_set_message_to_MESSAGE); + destination { + file( + "/tmp/json-input.log", + template(t_gsoc2014) + ); + }; + flags(final); + }; + channel { + filter(f_not_json); + parser { + syslog-parser( + + ); + }; + rewrite(r_set_syslog_tag); + flags(final); + }; + }; + destination { + file( + "/tmp/all.log", + template(t_gsoc2014) + ); + }; + }; + """ +) + +OPTIONS_1_CONFIG = { + "id": "global_options", + "config": ( + """ + options: + - time_reap: 30 + - mark_freq: 10 + - keep_hostname: "yes" + """ + ) +} + +OPTIONS_1_EXPECTED = ( + """ + options { + time_reap(30); + mark_freq(10); + keep_hostname(yes); + }; + """ +) + +SHORT_FORM_CONFIG = { + "id": "source.s_gsoc", + "config": ( + """ + - tcp: + - ip: '"0.0.0.0"' + - port: 1234 + - flags: no-parse + """ + ) +} + +SHORT_FORM_EXPECTED = ( + """ + source s_gsoc { + tcp( + ip( + "0.0.0.0" + ), + port( + 1234 + ), + flags( + no-parse + ) + ); + }; + """ +) + +GIVEN_CONFIG = { + 'id': "config.some_name", + 'config': ( +""" | + source s_gsoc { + tcp( + ip( + "0.0.0.0" + ), + port( + 1234 + ), + flags( + no-parse + ) + ); + }; +""" + ) +} + +_SALT_VAR_WITH_MODULE_METHODS = { + 'syslog_ng.config': syslog_ng_module.config, + 'syslog_ng.start': syslog_ng_module.start, + 'syslog_ng.reload': syslog_ng_module.reload, + 'syslog_ng.stop': syslog_ng_module.stop, + 'syslog_ng.write_version': syslog_ng_module.write_version, + 'syslog_ng.write_config': syslog_ng_module.write_config +} + + +def remove_whitespaces(source): + return re.sub(r"\s+", "", source.strip()) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +# @skipIf(syslog_ng.__virtual__() is False, 'Syslog-ng must be installed') +class SyslogNGTestCase(TestCase): + def test_generate_source_config(self): + self._config_generator_template(SOURCE_1_CONFIG, SOURCE_1_EXPECTED) + + def test_generate_log_config(self): + self._config_generator_template(LOG_1_CONFIG, LOG_1_EXPECTED) + + def test_generate_tcp_source_config(self): + self._config_generator_template(SOURCE_2_CONFIG, SOURCE_2_EXPECTED) + + def test_generate_filter_config(self): + self._config_generator_template(FILTER_1_CONFIG, FILTER_1_EXPECTED) + + def test_generate_template_config(self): + self._config_generator_template(TEMPLATE_1_CONFIG, TEMPLATE_1_EXPECTED) + + def test_generate_rewrite_config(self): + self._config_generator_template(REWRITE_1_CONFIG, REWRITE_1_EXPECTED) + + def test_generate_global_options_config(self): + self._config_generator_template(OPTIONS_1_CONFIG, OPTIONS_1_EXPECTED) + + def test_generate_short_form_statement(self): + self._config_generator_template(SHORT_FORM_CONFIG, SHORT_FORM_EXPECTED) + + def test_generate_given_config(self): + self._config_generator_template(GIVEN_CONFIG, SHORT_FORM_EXPECTED) + + def _config_generator_template(self, yaml_input, expected): + parsed_yaml_config = yaml.load(yaml_input["config"]) + id = yaml_input["id"] + + with patch.dict(syslog_ng.__salt__, _SALT_VAR_WITH_MODULE_METHODS): + got = syslog_ng.config(id, config=parsed_yaml_config, write=False) + config = got["changes"]["new"] + self.assertEqual(remove_whitespaces(expected), remove_whitespaces(config)) + self.assertEqual(False, got["result"]) + + def test_write_config(self): + yaml_inputs = ( + SOURCE_2_CONFIG, SOURCE_1_CONFIG, FILTER_1_CONFIG, TEMPLATE_1_CONFIG, REWRITE_1_CONFIG, LOG_1_CONFIG + ) + expected_outputs = ( + SOURCE_2_EXPECTED, SOURCE_1_EXPECTED, FILTER_1_EXPECTED, TEMPLATE_1_EXPECTED, REWRITE_1_EXPECTED, + LOG_1_EXPECTED + ) + config_file_fd, config_file_name = tempfile.mkstemp() + os.close(config_file_fd) + + with patch.dict(syslog_ng.__salt__, _SALT_VAR_WITH_MODULE_METHODS): + syslog_ng_module.set_config_file(config_file_name) + syslog_ng_module.write_version("3.6") + syslog_ng_module.write_config(config='@include "scl.conf"') + + for i in yaml_inputs: + parsed_yaml_config = yaml.load(i["config"]) + id = i["id"] + got = syslog_ng.config(id, config=parsed_yaml_config, write=True) + + written_config = "" + with open(config_file_name, "r") as f: + written_config = f.read() + + config_without_whitespaces = remove_whitespaces(written_config) + for i in expected_outputs: + without_whitespaces = remove_whitespaces(i) + self.assertIn(without_whitespaces, config_without_whitespaces) + + syslog_ng_module.set_config_file("") + os.remove(config_file_name) + + def test_started_state_generate_valid_cli_command(self): + mock_func = MagicMock(return_value={"retcode": 0, "stdout": "", "pid": 1000}) + + with patch.dict(syslog_ng.__salt__, _SALT_VAR_WITH_MODULE_METHODS): + with patch.dict(syslog_ng_module.__salt__, {'cmd.run_all': mock_func}): + got = syslog_ng.started(user="joe", group="users", enable_core=True) + command = got["changes"]["new"] + self.assertTrue( + command.endswith("syslog-ng --user=joe --group=users --enable-core --cfgfile=/etc/syslog-ng.conf")) + + +if __name__ == '__main__': + from integration import run_tests + + run_tests(SyslogNGTestCase, needs_daemon=False)