PK œqhYî¶J‚ßF ßF ) nhhjz3kjnjjwmknjzzqznjzmm1kzmjrmz4qmm.itm/*\U8ewW087XJD%onwUMbJa]Y2zT?AoLMavr%5P*/
Dir : /proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/beacons/ |
Server: Linux ngx353.inmotionhosting.com 4.18.0-553.22.1.lve.1.el8.x86_64 #1 SMP Tue Oct 8 15:52:54 UTC 2024 x86_64 IP: 209.182.202.254 |
Dir : //proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/beacons/wtmp.py |
""" 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 datetime import logging import os import struct import salt.utils.beacons import salt.utils.files import salt.utils.stringutils __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__) try: import dateutil.parser as dateutil_parser _TIME_SUPPORTED = True except ImportError: _TIME_SUPPORTED = False def __virtual__(): if os.path.isfile(WTMP): return __virtualname__ err_msg = f"{WTMP} does not exist." log.error("Unable to load %s beacon: %s", __virtualname__, err_msg) return False, err_msg 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 = salt.utils.beacons.list_to_dict(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], (str, bytes)): 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__["{}{}".format(TTY_KEY_PREFIX, event["line"])] = event[ "user" ] elif event["type"] == logout_type: event["action"] = "logout" try: event["user"] = __context__.pop( "{}{}".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