saltstack/salt

View on GitHub
salt/returners/smtp_return.py

Summary

Maintainability
B
4 hrs
Test Coverage
# -*- coding: utf-8 -*-
'''
Return salt data via email

The following fields can be set in the minion conf file. Fields are optional
unless noted otherwise.

* ``from`` (required) The name/address of the email sender.
* ``to`` (required) The names/addresses of the email recipients;
    comma-delimited. For example: ``you@example.com,someoneelse@example.com``.
* ``host`` (required) The SMTP server hostname or address.
* ``port`` The SMTP server port; defaults to ``25``.
* ``username`` The username used to authenticate to the server. If specified a
    password is also required. It is recommended but not required to also use
    TLS with this option.
* ``password`` The password used to authenticate to the server.
* ``tls`` Whether to secure the connection using TLS; defaults to ``False``
* ``subject`` The email subject line.
* ``fields`` Which fields from the returned data to include in the subject line
    of the email; comma-delimited. For example: ``id,fun``. Please note, *the
    subject line is not encrypted*.
* ``gpgowner`` A user's :file:`~/.gpg` directory. This must contain a gpg
    public key matching the address the mail is sent to. If left unset, no
    encryption will be used. Requires :program:`python-gnupg` to be installed.
* ``template`` The path to a file to be used as a template for the email body.
* ``renderer`` A Salt renderer, or render-pipe, to use to render the email
    template. Default ``jinja``.

Below is an example of the above settings in a Salt Minion configuration file:

.. code-block:: yaml

    smtp.from: me@example.net
    smtp.to: you@example.com
    smtp.host: localhost
    smtp.port: 1025

Alternative configuration values can be used by prefacing the configuration.
Any values not found in the alternative configuration will be pulled from
the default location. For example:

.. code-block:: yaml

    alternative.smtp.username: saltdev
    alternative.smtp.password: saltdev
    alternative.smtp.tls: True

To use the SMTP returner, append '--return smtp' to the ``salt`` command.

.. code-block:: bash

    salt '*' test.ping --return smtp

To use the alternative configuration, append '--return_config alternative' to the ``salt`` command.

.. versionadded:: 2015.5.0

.. code-block:: bash

    salt '*' test.ping --return smtp --return_config alternative

To override individual configuration items, append --return_kwargs '{"key:": "value"}' to the
``salt`` command.

.. versionadded:: 2016.3.0

.. code-block:: bash

    salt '*' test.ping --return smtp --return_kwargs '{"to": "user@domain.com"}'

An easy way to test the SMTP returner is to use the development SMTP server
built into Python. The command below will start a single-threaded SMTP server
that prints any email it receives to the console.

.. code-block:: python

    python -m smtpd -n -c DebuggingServer localhost:1025

.. versionadded:: 2016.11.0

It is possible to send emails with selected Salt events by configuring ``event_return`` option
for Salt Master. For example:

.. code-block:: yaml

    event_return: smtp

    event_return_whitelist:
      - salt/key

    smtp.from: me@example.net
    smtp.to: you@example.com
    smtp.host: localhost
    smtp.subject: 'Salt Master {{act}}ed key from Minion ID: {{id}}'
    smtp.template: /srv/salt/templates/email.j2

Also you need to create additional file ``/srv/salt/templates/email.j2`` with email body template:

.. code-block:: yaml

    act: {{act}}
    id: {{id}}
    result: {{result}}

This configuration enables Salt Master to send an email when accepting or rejecting minions keys.
'''

# Import python libs
from __future__ import absolute_import, print_function, unicode_literals
import os
import logging
import smtplib
from email.utils import formatdate

# Import Salt libs
from salt.ext import six
import salt.utils.jid
import salt.returners
import salt.loader
from salt.template import compile_template

try:
    import gnupg
    HAS_GNUPG = True
except ImportError:
    HAS_GNUPG = False


log = logging.getLogger(__name__)

__virtualname__ = 'smtp'


def __virtual__():
    return __virtualname__


def _get_options(ret=None):
    '''
    Get the SMTP options from salt.
    '''
    attrs = {'from': 'from',
             'to': 'to',
             'host': 'host',
             'port': 'port',
             'username': 'username',
             'password': 'password',
             'subject': 'subject',
             'gpgowner': 'gpgowner',
             'fields': 'fields',
             'tls': 'tls',
             'renderer': 'renderer',
             'template': 'template'}

    _options = salt.returners.get_returner_options(__virtualname__,
                                                   ret,
                                                   attrs,
                                                   __salt__=__salt__,
                                                   __opts__=__opts__)
    return _options


def returner(ret):
    '''
    Send an email with the data
    '''

    _options = _get_options(ret)
    from_addr = _options.get('from')
    to_addrs = _options.get('to').split(',')
    host = _options.get('host')
    port = _options.get('port')
    user = _options.get('username')
    passwd = _options.get('password')
    subject = _options.get('subject') or 'Email from Salt'
    gpgowner = _options.get('gpgowner')
    fields = _options.get('fields').split(',') if 'fields' in _options else []
    smtp_tls = _options.get('tls')

    renderer = _options.get('renderer') or 'jinja'
    rend = salt.loader.render(__opts__, {})
    blacklist = __opts__.get('renderer_blacklist')
    whitelist = __opts__.get('renderer_whitelist')

    if not port:
        port = 25
    log.debug('SMTP port has been set to %s', port)

    for field in fields:
        if field in ret:
            subject += ' {0}'.format(ret[field])
    subject = compile_template(':string:',
                               rend,
                               renderer,
                               blacklist,
                               whitelist,
                               input_data=subject,
                               **ret)
    if isinstance(subject, six.moves.StringIO):
        subject = subject.read()
    log.debug("smtp_return: Subject is '%s'", subject)

    template = _options.get('template')
    if template:
        content = compile_template(template, rend, renderer, blacklist, whitelist, **ret)
    else:
        template = ('id: {{id}}\r\n'
                    'function: {{fun}}\r\n'
                    'function args: {{fun_args}}\r\n'
                    'jid: {{jid}}\r\n'
                    'return: {{return}}\r\n')
        content = compile_template(':string:',
                                   rend,
                                   renderer,
                                   blacklist,
                                   whitelist,
                                   input_data=template,
                                   **ret)

    if gpgowner:
        if HAS_GNUPG:
            gpg = gnupg.GPG(gnupghome=os.path.expanduser('~{0}/.gnupg'.format(gpgowner)),
                            options=['--trust-model always'])
            encrypted_data = gpg.encrypt(content, to_addrs)
            if encrypted_data.ok:
                log.debug('smtp_return: Encryption successful')
                content = six.text_type(encrypted_data)
            else:
                log.error('smtp_return: Encryption failed, only an error message will be sent')
                content = 'Encryption failed, the return data was not sent.\r\n\r\n{0}\r\n{1}'.format(
                    encrypted_data.status, encrypted_data.stderr)
        else:
            log.error("gnupg python module is required in order to user gpgowner in smtp returner ; ignoring gpgowner configuration for now")
    if isinstance(content, six.moves.StringIO):
        content = content.read()

    message = ('From: {0}\r\n'
               'To: {1}\r\n'
               'Date: {2}\r\n'
               'Subject: {3}\r\n'
               '\r\n'
               '{4}').format(from_addr,
                             ', '.join(to_addrs),
                             formatdate(localtime=True),
                             subject,
                             content)

    log.debug('smtp_return: Connecting to the server...')
    server = smtplib.SMTP(host, int(port))
    if smtp_tls is True:
        server.starttls()
        log.debug('smtp_return: TLS enabled')
    if user and passwd:
        server.login(user, passwd)
        log.debug('smtp_return: Authenticated')
    # enable logging SMTP session after the login credentials were passed
    server.set_debuglevel(1)
    server.sendmail(from_addr, to_addrs, message)
    log.debug('smtp_return: Message sent.')
    server.quit()


def prep_jid(nocache=False, passed_jid=None):  # pylint: disable=unused-argument
    '''
    Do any work necessary to prepare a JID, including sending a custom id
    '''
    return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__)


def event_return(events):
    '''
    Return event data via SMTP
    '''

    for event in events:
        ret = event.get('data', False)

        if ret:
            returner(ret)