mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
382 lines
10 KiB
Python
382 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Beacon to fire events at login of users as registered in the wtmp file
|
|
|
|
.. versionadded:: 2015.5.0
|
|
|
|
|
|
Example Configuration
|
|
=====================
|
|
|
|
.. code-block:: yaml
|
|
|
|
# Fire events on all logins
|
|
beacons:
|
|
wtmp: []
|
|
|
|
# Matching on user name, using a default time range
|
|
beacons:
|
|
wtmp:
|
|
- users:
|
|
gareth:
|
|
- defaults:
|
|
time_range:
|
|
start: '8am'
|
|
end: '4pm'
|
|
|
|
# Matching on user name, overriding the default time range
|
|
beacons:
|
|
wtmp:
|
|
- users:
|
|
gareth:
|
|
time_range:
|
|
start: '7am'
|
|
end: '3pm'
|
|
- defaults:
|
|
time_range:
|
|
start: '8am'
|
|
end: '4pm'
|
|
|
|
# Matching on group name, overriding the default time range
|
|
beacons:
|
|
wtmp:
|
|
- groups:
|
|
users:
|
|
time_range:
|
|
start: '7am'
|
|
end: '3pm'
|
|
- defaults:
|
|
time_range:
|
|
start: '8am'
|
|
end: '4pm'
|
|
|
|
|
|
How to Tell What An Event Means
|
|
===============================
|
|
|
|
In the events that this beacon fires, a type of ``7`` denotes a login, while a
|
|
type of ``8`` denotes a logout. These values correspond to the ``ut_type``
|
|
value from a wtmp/utmp event (see the ``wtmp`` manpage for more information).
|
|
In the extremely unlikely case that your platform uses different values, they
|
|
can be overridden using a ``ut_type`` key in the beacon configuration:
|
|
|
|
.. code-block:: yaml
|
|
|
|
beacons:
|
|
wtmp:
|
|
- ut_type:
|
|
login: 9
|
|
logout: 10
|
|
|
|
This beacon's events include an ``action`` key which will be either ``login``
|
|
or ``logout`` depending on the event type.
|
|
|
|
.. versionchanged:: 2019.2.0
|
|
``action`` key added to beacon event, and ``ut_type`` config parameter
|
|
added.
|
|
|
|
|
|
Use Case: Posting Login/Logout Events to Slack
|
|
==============================================
|
|
|
|
This can be done using the following reactor SLS:
|
|
|
|
.. code-block:: jinja
|
|
|
|
report-wtmp:
|
|
runner.salt.cmd:
|
|
- args:
|
|
- fun: slack.post_message
|
|
- channel: mychannel # Slack channel
|
|
- from_name: someuser # Slack user
|
|
- message: "{{ data.get('action', 'Unknown event') | capitalize }} from `{{ data.get('user', '') or 'unknown user' }}` on `{{ data['id'] }}`"
|
|
|
|
Match the event like so in the master config file:
|
|
|
|
.. code-block:: yaml
|
|
|
|
reactor:
|
|
|
|
- 'salt/beacon/*/wtmp/':
|
|
- salt://reactor/wtmp.sls
|
|
|
|
.. note::
|
|
This approach uses the :py:mod:`slack execution module
|
|
<salt.modules.slack_notify>` directly on the master, and therefore requires
|
|
that the master has a slack API key in its configuration:
|
|
|
|
.. code-block:: yaml
|
|
|
|
slack:
|
|
api_key: xoxb-XXXXXXXXXXXX-XXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXX
|
|
|
|
See the :py:mod:`slack execution module <salt.modules.slack_notify>`
|
|
documentation for more information. While you can use an individual user's
|
|
API key to post to Slack, a bot user is likely better suited for this. The
|
|
:py:mod:`slack engine <salt.engines.slack>` documentation has information
|
|
on how to set up a bot user.
|
|
"""
|
|
|
|
# Import Python libs
|
|
from __future__ import absolute_import, unicode_literals
|
|
|
|
import datetime
|
|
import logging
|
|
import os
|
|
import struct
|
|
|
|
import salt.utils.files
|
|
|
|
# Import salt libs
|
|
import salt.utils.stringutils
|
|
|
|
# Import 3rd-party libs
|
|
from salt.ext import six
|
|
|
|
# pylint: disable=import-error
|
|
from salt.ext.six.moves import map
|
|
|
|
# pylint: enable=import-error
|
|
|
|
__virtualname__ = "wtmp"
|
|
WTMP = "/var/log/wtmp"
|
|
FMT = b"hi32s4s32s256shhiii4i20x"
|
|
FIELDS = [
|
|
"type",
|
|
"PID",
|
|
"line",
|
|
"inittab",
|
|
"user",
|
|
"hostname",
|
|
"exit_status",
|
|
"session",
|
|
"time",
|
|
"addr",
|
|
]
|
|
SIZE = struct.calcsize(FMT)
|
|
LOC_KEY = "wtmp.loc"
|
|
TTY_KEY_PREFIX = "wtmp.tty."
|
|
LOGIN_TYPE = 7
|
|
LOGOUT_TYPE = 8
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
# pylint: disable=import-error
|
|
try:
|
|
import dateutil.parser as dateutil_parser
|
|
|
|
_TIME_SUPPORTED = True
|
|
except ImportError:
|
|
_TIME_SUPPORTED = False
|
|
|
|
|
|
def __virtual__():
|
|
if os.path.isfile(WTMP):
|
|
return __virtualname__
|
|
return False
|
|
|
|
|
|
def _validate_time_range(trange, status, msg):
|
|
"""
|
|
Check time range
|
|
"""
|
|
# If trange is empty, just return the current status & msg
|
|
if not trange:
|
|
return status, msg
|
|
|
|
if not isinstance(trange, dict):
|
|
status = False
|
|
msg = "The time_range parameter for " "wtmp beacon must " "be a dictionary."
|
|
|
|
if not all(k in trange for k in ("start", "end")):
|
|
status = False
|
|
msg = (
|
|
"The time_range parameter for "
|
|
"wtmp beacon must contain "
|
|
"start & end options."
|
|
)
|
|
|
|
return status, msg
|
|
|
|
|
|
def _gather_group_members(group, groups, users):
|
|
"""
|
|
Gather group members
|
|
"""
|
|
_group = __salt__["group.info"](group)
|
|
|
|
if not _group:
|
|
log.warning("Group %s does not exist, ignoring.", group)
|
|
return
|
|
|
|
for member in _group["members"]:
|
|
if member not in users:
|
|
users[member] = groups[group]
|
|
|
|
|
|
def _check_time_range(time_range, now):
|
|
"""
|
|
Check time range
|
|
"""
|
|
if _TIME_SUPPORTED:
|
|
_start = dateutil_parser.parse(time_range["start"])
|
|
_end = dateutil_parser.parse(time_range["end"])
|
|
|
|
return bool(_start <= now <= _end)
|
|
else:
|
|
log.error("Dateutil is required.")
|
|
return False
|
|
|
|
|
|
def _get_loc():
|
|
"""
|
|
return the active file location
|
|
"""
|
|
if LOC_KEY in __context__:
|
|
return __context__[LOC_KEY]
|
|
|
|
|
|
def validate(config):
|
|
"""
|
|
Validate the beacon configuration
|
|
"""
|
|
vstatus = True
|
|
vmsg = "Valid beacon configuration"
|
|
|
|
# Configuration for wtmp beacon should be a list of dicts
|
|
if not isinstance(config, list):
|
|
vstatus = False
|
|
vmsg = "Configuration for wtmp beacon must be a list."
|
|
else:
|
|
_config = {}
|
|
list(map(_config.update, config))
|
|
|
|
if "users" in _config:
|
|
if not isinstance(_config["users"], dict):
|
|
vstatus = False
|
|
vmsg = "User configuration for wtmp beacon must " "be a dictionary."
|
|
else:
|
|
for user in _config["users"]:
|
|
_time_range = _config["users"][user].get("time_range", {})
|
|
vstatus, vmsg = _validate_time_range(_time_range, vstatus, vmsg)
|
|
|
|
if not vstatus:
|
|
return vstatus, vmsg
|
|
|
|
if "groups" in _config:
|
|
if not isinstance(_config["groups"], dict):
|
|
vstatus = False
|
|
vmsg = "Group configuration for wtmp beacon must " "be a dictionary."
|
|
else:
|
|
for group in _config["groups"]:
|
|
_time_range = _config["groups"][group].get("time_range", {})
|
|
vstatus, vmsg = _validate_time_range(_time_range, vstatus, vmsg)
|
|
if not vstatus:
|
|
return vstatus, vmsg
|
|
|
|
if "defaults" in _config:
|
|
if not isinstance(_config["defaults"], dict):
|
|
vstatus = False
|
|
vmsg = "Defaults configuration for wtmp beacon must " "be a dictionary."
|
|
else:
|
|
_time_range = _config["defaults"].get("time_range", {})
|
|
vstatus, vmsg = _validate_time_range(_time_range, vstatus, vmsg)
|
|
if not vstatus:
|
|
return vstatus, vmsg
|
|
|
|
return vstatus, vmsg
|
|
|
|
|
|
def beacon(config):
|
|
"""
|
|
Read the last wtmp file and return information on the logins
|
|
"""
|
|
ret = []
|
|
|
|
users = {}
|
|
groups = {}
|
|
defaults = None
|
|
|
|
login_type = LOGIN_TYPE
|
|
logout_type = LOGOUT_TYPE
|
|
|
|
for config_item in config:
|
|
if "users" in config_item:
|
|
users = config_item["users"]
|
|
|
|
if "groups" in config_item:
|
|
groups = config_item["groups"]
|
|
|
|
if "defaults" in config_item:
|
|
defaults = config_item["defaults"]
|
|
|
|
if config_item == "ut_type":
|
|
try:
|
|
login_type = config_item["ut_type"]["login"]
|
|
except KeyError:
|
|
pass
|
|
try:
|
|
logout_type = config_item["ut_type"]["logout"]
|
|
except KeyError:
|
|
pass
|
|
|
|
with salt.utils.files.fopen(WTMP, "rb") as fp_:
|
|
loc = __context__.get(LOC_KEY, 0)
|
|
if loc == 0:
|
|
fp_.seek(0, 2)
|
|
__context__[LOC_KEY] = fp_.tell()
|
|
return ret
|
|
else:
|
|
fp_.seek(loc)
|
|
while True:
|
|
now = datetime.datetime.now()
|
|
raw = fp_.read(SIZE)
|
|
if len(raw) != SIZE:
|
|
return ret
|
|
__context__[LOC_KEY] = fp_.tell()
|
|
pack = struct.unpack(FMT, raw)
|
|
event = {}
|
|
for ind, field in enumerate(FIELDS):
|
|
event[field] = pack[ind]
|
|
if isinstance(event[field], six.string_types):
|
|
if isinstance(event[field], bytes):
|
|
event[field] = salt.utils.stringutils.to_unicode(event[field])
|
|
event[field] = event[field].strip("\x00")
|
|
|
|
if event["type"] == login_type:
|
|
event["action"] = "login"
|
|
# Store the tty to identify the logout event
|
|
__context__["{0}{1}".format(TTY_KEY_PREFIX, event["line"])] = event[
|
|
"user"
|
|
]
|
|
elif event["type"] == logout_type:
|
|
event["action"] = "logout"
|
|
try:
|
|
event["user"] = __context__.pop(
|
|
"{0}{1}".format(TTY_KEY_PREFIX, event["line"])
|
|
)
|
|
except KeyError:
|
|
pass
|
|
|
|
for group in groups:
|
|
_gather_group_members(group, groups, users)
|
|
|
|
if users:
|
|
if event["user"] in users:
|
|
_user = users[event["user"]]
|
|
if isinstance(_user, dict) and "time_range" in _user:
|
|
if _check_time_range(_user["time_range"], now):
|
|
ret.append(event)
|
|
else:
|
|
if defaults and "time_range" in defaults:
|
|
if _check_time_range(defaults["time_range"], now):
|
|
ret.append(event)
|
|
else:
|
|
ret.append(event)
|
|
else:
|
|
if defaults and "time_range" in defaults:
|
|
if _check_time_range(defaults["time_range"], now):
|
|
ret.append(event)
|
|
else:
|
|
ret.append(event)
|
|
return ret
|