Merge branch 'master' into 18907_lazy_unmount_when_fails

This commit is contained in:
David Murphy 2023-12-10 19:58:35 -07:00 committed by GitHub
commit a3f5ff9a7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 476 additions and 281 deletions

View file

@ -16,7 +16,7 @@ on:
env:
COLUMNS: 190
CACHE_SEED: SEED-1 # Bump the number to invalidate all caches
CACHE_SEED: SEED-2 # Bump the number to invalidate all caches
RELENV_DATA: "${{ github.workspace }}/.relenv"
permissions:

View file

@ -22,7 +22,7 @@ on:
env:
COLUMNS: 190
CACHE_SEED: SEED-1 # Bump the number to invalidate all caches
CACHE_SEED: SEED-2 # Bump the number to invalidate all caches
RELENV_DATA: "${{ github.workspace }}/.relenv"
permissions:

View file

@ -21,7 +21,7 @@ on:
env:
COLUMNS: 190
CACHE_SEED: SEED-1 # Bump the number to invalidate all caches
CACHE_SEED: SEED-2 # Bump the number to invalidate all caches
RELENV_DATA: "${{ github.workspace }}/.relenv"
permissions:

View file

@ -12,7 +12,7 @@ on:
env:
COLUMNS: 190
CACHE_SEED: SEED-1 # Bump the number to invalidate all caches
CACHE_SEED: SEED-2 # Bump the number to invalidate all caches
RELENV_DATA: "${{ github.workspace }}/.relenv"
permissions:

View file

@ -37,7 +37,7 @@ on:
env:
COLUMNS: 190
CACHE_SEED: SEED-1 # Bump the number to invalidate all caches
CACHE_SEED: SEED-2 # Bump the number to invalidate all caches
RELENV_DATA: "${{ github.workspace }}/.relenv"
permissions:

View file

@ -34,7 +34,7 @@ on:
env:
COLUMNS: 190
CACHE_SEED: SEED-1 # Bump the number to invalidate all caches
CACHE_SEED: SEED-2 # Bump the number to invalidate all caches
RELENV_DATA: "${{ github.workspace }}/.relenv"
<%- endblock env %>

1
changelog/57204.fixed.md Normal file
View file

@ -0,0 +1 @@
Removed an unused assignment in file.patch

1
changelog/59806.fixed.md Normal file
View file

@ -0,0 +1 @@
Return error if patch file passed to state file.patch is malformed.

View file

@ -3,3 +3,4 @@ python_version: "3.10.13"
relenv_version: "0.14.2"
release-branches:
- "3006.x"
- "3007.x"

View file

@ -21,6 +21,7 @@
{# Remove old version of jQuery #}
{% set js_blacklist = [
'_static/documentation_options.js',
'_static/jquery.js',
] %}

View file

@ -289,7 +289,7 @@ Set up an initial profile at ``/etc/salt/cloud.profiles``:
image: ami-a73264ce
size: m1.xlarge
ssh_username: ec2-user
script: /etc/salt/cloud.deploy.d/user_data.sh
script: /etc/salt/cloud.deploy.d/my_bootstrap.sh
network_interfaces:
- DeviceIndex: 0
PrivateIpAddresses:
@ -758,7 +758,7 @@ them have never been used, much less tested, by the Salt Stack team.
* `FreeBSD`__
.. __: http://www.daemonology.net/freebsd-on-ec2/
.. __: https://aws.amazon.com/marketplace/search/results?filters=vendor_id&vendor_id=92bb514d-02bc-49fd-9727-c474863f63da
* `Fedora`__

View file

@ -55,9 +55,11 @@ prepended by underscore, such as:
Modules must be synced before they can be used. This can happen a few ways,
discussed below.
.. note::
Using saltenvs besides ``base`` may not work in all contexts.
Sync Via States
~~~~~~~~~~~~~~~
@ -67,7 +69,7 @@ dynamic modules when states are run. To disable this behavior set
:conf_minion:`autoload_dynamic_modules` to ``False`` in the minion config.
When dynamic modules are autoloaded via states, only the modules defined in the
same saltenvs as the states currently being run.
same saltenv as the states currently being run are synced.
Sync Via the saltutil Module
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -78,6 +80,8 @@ or specific dynamic modules. The ``saltutil.sync_*``
:py:mod:`runner functions <salt.runners.saltutil>` can be used to sync modules
to minions and the master, respectively.
If saltenv environments are used (through the :ref:`top file <states-top>`, the :conf_minion:`environment` option of the minion configuration file, or as an argument on the command line) modules will be synced from the applied environments.
The extmods Directory
---------------------

View file

@ -1,5 +1,6 @@
.. _tutorial-gitfs:
==================================
Git Fileserver Backend Walkthrough
==================================
@ -276,10 +277,12 @@ And each repository contains some files:
edit/vim.sls
edit/vimrc
nginx/init.sls
shell/init.sls
second.git:
edit/dev_vimrc
haproxy/init.sls
shell.sls
third:
haproxy/haproxy.conf
@ -296,6 +299,12 @@ is executed. For example:
* A request for the file :strong:`salt://haproxy/haproxy.conf` will be served from the
:strong:`file:///root/third` repo.
Also a requested state file overrules a directory with an `init.sls`-file.
For example:
* A request for :strong:`state.apply shell` will be served from the
:strong:`https://github.com/example/second.git` git repo.
.. note::
This example is purposefully contrived to illustrate the behavior of the

View file

@ -1045,9 +1045,7 @@ class MasterPubServerChannel:
"""
try:
tag, data = salt.utils.event.SaltEvent.unpack(payload)
log.error("recieved event from peer %s %r", tag, data)
if tag.startswith("cluster/peer"):
log.error("Got peer join %r", data)
peer = data["peer_id"]
aes = data["peers"][self.opts["id"]]["aes"]
sig = data["peers"][self.opts["id"]]["sig"]

View file

@ -985,7 +985,7 @@ def destroy_dns_records(fqdn):
records = response["domain_records"]
if records:
record_ids = [r["id"] for r in records if r["name"].decode() == hostname]
record_ids = [r["id"] for r in records if r["name"] == hostname]
log.debug("deleting DNS record IDs: %s", record_ids)
for id_ in record_ids:
try:

View file

@ -16,6 +16,7 @@ import pathlib
import random
import stat
import sys
import tempfile
import time
import traceback
import uuid
@ -1620,19 +1621,22 @@ class Crypticle:
return b64key.replace("\n", "")
@classmethod
def read_or_generate_key(cls, path, key_size=192, remove=False):
if remove:
os.remove(path)
def write_key(cls, path, key_size=192):
directory = pathlib.Path(path).parent
with salt.utils.files.set_umask(0o177):
try:
with salt.utils.files.fopen(path, "r") as fp:
return fp.read()
except FileNotFoundError:
pass
key = cls.generate_key_string(key_size)
with salt.utils.files.fopen(path, "w") as fp:
fp.write(key)
return key
fd, tmp = tempfile.mkstemp(dir=directory, prefix="aes")
os.close(fd)
with salt.utils.files.fopen(tmp, "w") as fp:
fp.write(cls.generate_key_string(key_size))
os.rename(tmp, path)
@classmethod
def read_key(cls, path):
try:
with salt.utils.files.fopen(path, "r") as fp:
return fp.read()
except FileNotFoundError:
pass
@classmethod
def extract_keys(cls, key_string, key_size):

View file

@ -6,7 +6,6 @@ import asyncio
import collections
import copy
import ctypes
import functools
import logging
import multiprocessing
import os
@ -142,7 +141,6 @@ class SMaster:
def rotate_secrets(
cls, opts=None, event=None, use_lock=True, owner=False, publisher=None
):
log.info("Rotating master AES key")
if opts is None:
opts = {}
@ -173,6 +171,41 @@ class SMaster:
log.debug("Pinging all connected minions due to key rotation")
salt.utils.master.ping_all_connected_minions(opts)
@classmethod
def rotate_cluster_secret(
cls, opts=None, event=None, use_lock=True, owner=False, publisher=None
):
log.debug("Rotating cluster AES key")
if opts is None:
opts = {}
if use_lock:
with cls.secrets["cluster_aes"]["secret"].get_lock():
cls.secrets["cluster_aes"][
"secret"
].value = salt.utils.stringutils.to_bytes(
cls.secrets["cluster_aes"]["reload"](remove=owner)
)
else:
cls.secrets["cluster_aes"][
"secret"
].value = salt.utils.stringutils.to_bytes(
cls.secrets["cluster_aes"]["reload"](remove=owner)
)
if event:
event.fire_event(
{f"rotate_cluster_aes_key": True}, tag="rotate_cluster_aes_key"
)
if publisher:
publisher.send_aes_key_event()
if opts.get("ping_on_rotate"):
# Ping all minions to get them to pick up the new key
log.debug("Pinging all connected minions due to key rotation")
salt.utils.master.ping_all_connected_minions(opts)
class Maintenance(salt.utils.process.SignalHandlingProcess):
"""
@ -358,7 +391,7 @@ class Maintenance(salt.utils.process.SignalHandlingProcess):
if to_rotate:
if self.opts.get("cluster_id", None):
SMaster.rotate_secrets(
SMaster.rotate_cluster_secret(
self.opts, self.event, owner=True, publisher=self.ipc_publisher
)
else:
@ -714,6 +747,20 @@ class Master(SMaster):
log.critical("Master failed pre flight checks, exiting\n")
sys.exit(salt.defaults.exitcodes.EX_GENERIC)
def read_or_generate_key(self, remove=False, fs_wait=0.1):
"""
Used to manage a cluster aes session key file.
"""
path = os.path.join(self.opts["cluster_pki_dir"], ".aes")
if remove:
os.remove(path)
key = salt.crypt.Crypticle.read_key(path)
if key:
return key
salt.crypt.Crypticle.write_key(path)
time.sleep(fs_wait)
return salt.crypt.Crypticle.read_key(path)
def start(self):
"""
Turn on the master server components
@ -731,22 +778,18 @@ class Master(SMaster):
# signal handlers
with salt.utils.process.default_signals(signal.SIGINT, signal.SIGTERM):
if self.opts["cluster_id"]:
keypath = os.path.join(self.opts["cluster_pki_dir"], ".aes")
cluster_keygen = functools.partial(
salt.crypt.Crypticle.read_or_generate_key,
keypath,
)
# Setup the secrets here because the PubServerChannel may need
# them as well.
SMaster.secrets["cluster_aes"] = {
"secret": multiprocessing.Array(
ctypes.c_char, salt.utils.stringutils.to_bytes(cluster_keygen())
ctypes.c_char,
salt.utils.stringutils.to_bytes(self.read_or_generate_key()),
),
"serial": multiprocessing.Value(
ctypes.c_longlong,
lock=False, # We'll use the lock from 'secret'
),
"reload": cluster_keygen,
"reload": self.read_or_generate_key,
}
SMaster.secrets["aes"] = {
@ -779,7 +822,7 @@ class Master(SMaster):
ipc_publisher.pre_fork(self.process_manager)
self.process_manager.add_process(
EventMonitor,
args=[self.opts],
args=[self.opts, ipc_publisher],
name="EventMonitor",
)
@ -908,19 +951,19 @@ class EventMonitor(salt.utils.process.SignalHandlingProcess):
- Handle key rotate events.
"""
def __init__(self, opts, channels=None, name="EventMonitor"):
def __init__(self, opts, ipc_publisher, channels=None, name="EventMonitor"):
super().__init__(name=name)
self.opts = opts
if channels is None:
channels = []
self.channels = channels
self.ipc_publisher = ipc_publisher
async def handle_event(self, package):
"""
Event handler for publish forwarder
"""
tag, data = salt.utils.event.SaltEvent.unpack(package)
log.debug("Event monitor got event %s %r", tag, data)
if tag.startswith("salt/job") and tag.endswith("/publish"):
peer_id = data.pop("__peer_id", None)
if peer_id:
@ -937,9 +980,15 @@ class EventMonitor(salt.utils.process.SignalHandlingProcess):
for chan in self.channels:
tasks.append(asyncio.create_task(chan.publish(data)))
await asyncio.gather(*tasks)
elif tag == "rotate_aes_key":
log.debug("Event monitor recieved rotate aes key event, rotating key.")
SMaster.rotate_secrets(self.opts, owner=False)
elif tag == "rotate_cluster_aes_key":
peer_id = data.pop("__peer_id", None)
if peer_id:
log.debug("Rotating AES session key")
SMaster.rotate_cluster_secret(
self.opts, owner=False, publisher=self.ipc_publisher
)
else:
log.trace("Ignore tag %s", tag)
def run(self):
io_loop = tornado.ioloop.IOLoop()

View file

@ -1420,6 +1420,7 @@ def umount(name, device=None, user=None, util="mount", lazy=False):
cmd = f"{cmd} '{name}'"
else:
cmd = f"{cmd}'{device}'"
out = __salt__["cmd.run_all"](cmd, runas=user, python_shell=False)
if out["retcode"]:
return out["stderr"]
@ -1923,7 +1924,7 @@ def set_filesystems(
except OSError:
raise CommandExecutionError(f"File not writable {config}")
except Exception as exc:
raise CommandExecutionError("set_filesystems error exception {exc}")
raise CommandExecutionError(f"set_filesystems error exception {exc}")
return ret
@ -1972,7 +1973,7 @@ def rm_filesystems(name, device, config="/etc/filesystems"):
except OSError as exc:
raise CommandExecutionError(f"Couldn't write to {config}: {exc}")
except Exception as exc:
raise CommandExecutionError("rm_filesystems error exception {exc}")
raise CommandExecutionError(f"rm_filesystems error exception {exc}")
return modified

View file

@ -3,13 +3,13 @@ Support for reboot, shutdown, etc on POSIX-like systems.
.. note::
If you have configured a wrapper such as ``molly-guard`` to
intercept *interactive* shutdown commands, be aware that calling
``system.halt``, ``system.poweroff``, ``system.reboot``, and
``system.shutdown`` with ``salt-call`` will hang indefinitely
while the wrapper script waits for user input. Calling them with
``salt`` will work as expected.
If a wrapper such as ``molly-guard`` to intercept *interactive* shutdown
commands is configured, calling :mod:`system.halt <salt.modules.system.halt>`,
:mod:`system.poweroff <salt.modules.system.poweroff>`,
:mod:`system.reboot <salt.modules.system.reboot>`, and
:mod:`system.shutdown <salt.modules.system.shutdown>` with ``salt-call`` will
hang indefinitely while the wrapper script waits for user input. Calling them
with ``salt`` will work as expected.
"""
import os.path
@ -28,7 +28,7 @@ __virtualname__ = "system"
def __virtual__():
"""
Only supported on POSIX-like systems
Windows, Solaris, and Mac have their own modules
Windows, Solaris, and OS X have their own modules
"""
if salt.utils.platform.is_windows():
return (False, "This module is not available on Windows")
@ -137,7 +137,7 @@ def _date_bin_set_datetime(new_date):
"""
set the system date/time using the date command
Note using a strictly posix-compliant date binary we can only set the date
Note using a strictly POSIX-compliant date binary we can only set the date
up to the minute.
"""
cmd = ["date"]
@ -176,7 +176,7 @@ def _date_bin_set_datetime(new_date):
def has_settable_hwclock():
"""
Returns True if the system has a hardware clock capable of being
Returns ``True`` if the system has a hardware clock capable of being
set from software.
CLI Example:
@ -211,13 +211,13 @@ def _swclock_to_hwclock():
def _try_parse_datetime(time_str, fmts):
"""
Attempts to parse the input time_str as a date.
Attempts to parse the input ``time_str`` as a date.
:param str time_str: A string representing the time
:param list fmts: A list of date format strings.
:return: Returns a datetime object if parsed properly. Otherwise None
:rtype datetime:
:return: Returns a datetime object if parsed properly. Otherwise ``None``
:rtype: datetime
"""
result = None
for fmt in fmts:
@ -231,9 +231,9 @@ def _try_parse_datetime(time_str, fmts):
def _offset_to_min(utc_offset):
"""
Helper function that converts the utc offset string into number of minutes
offset. Input is in form "[+-]?HHMM". Example valid inputs are "+0500"
"-0300" and "0800". These would return -300, 180, 480 respectively.
Helper function that converts the UTC offset string into number of minutes
offset. Input is in form ``[+-]?HHMM``. Example valid inputs are ``+0500``
``-0300`` and ``0800``. These would return ``-300``, ``180``, ``480`` respectively.
"""
match = re.match(r"^([+-])?(\d\d)(\d\d)$", utc_offset)
if not match:
@ -250,7 +250,7 @@ def _get_offset_time(utc_offset):
"""
Will return the current time adjusted using the input timezone offset.
:rtype datetime:
:rtype: datetime
"""
if utc_offset is not None:
minutes = _offset_to_min(utc_offset)
@ -266,12 +266,12 @@ def get_system_time(utc_offset=None):
"""
Get the system time.
:param str utc_offset: The utc offset in 4 digit (+0600) format with an
optional sign (+/-). Will default to None which will use the local
timezone. To set the time based off of UTC use "'+0000'". Note: if
:param str utc_offset: The UTC offset in 4 digit (e.g. ``+0600``) format with an
optional sign (``+``/``-``). Will default to ``None`` which will use the local
timezone. To set the time based off of UTC use ``+0000``. Note: If
being passed through the command line will need to be quoted twice to
allow negative offsets.
:return: Returns the system time in HH:MM:SS AM/PM format.
allow negative offsets (e.g. ``"'+0000'"``).
:return: Returns the system time in ``HH:MM:SS AM/PM`` format.
:rtype: str
CLI Example:
@ -290,22 +290,23 @@ def set_system_time(newtime, utc_offset=None):
:param str newtime:
The time to set. Can be any of the following formats.
- HH:MM:SS AM/PM
- HH:MM AM/PM
- HH:MM:SS (24 hour)
- HH:MM (24 hour)
Note that the salt command line parser parses the date/time
before we obtain the argument (preventing us from doing utc)
- ``HH:MM:SS AM/PM``
- ``HH:MM AM/PM``
- ``HH:MM:SS`` (24 hour)
- ``HH:MM`` (24 hour)
Note that the Salt command line parser parses the date/time
before we obtain the argument (preventing us from doing UTC)
Therefore the argument must be passed in as a string.
Meaning you may have to quote the text twice from the command line.
Meaning the text might have to be quoted twice on the command line.
:param str utc_offset: The utc offset in 4 digit (+0600) format with an
optional sign (+/-). Will default to None which will use the local
timezone. To set the time based off of UTC use "'+0000'". Note: if
:param str utc_offset: The UTC offset in 4 digit (``+0600``) format with an
optional sign (``+``/``-``). Will default to ``None`` which will use the local
timezone. To set the time based off of UTC use ``+0000``. Note: If
being passed through the command line will need to be quoted twice to
allow negative offsets.
:return: Returns True if successful. Otherwise False.
allow negative offsets (e.g. ``"'+0000'"``)
:return: Returns ``True`` if successful. Otherwise ``False``.
:rtype: bool
CLI Example:
@ -331,12 +332,12 @@ def get_system_date_time(utc_offset=None):
"""
Get the system date/time.
:param str utc_offset: The utc offset in 4 digit (+0600) format with an
optional sign (+/-). Will default to None which will use the local
timezone. To set the time based off of UTC use "'+0000'". Note: if
:param str utc_offset: The UTC offset in 4 digit (``+0600``) format with an
optional sign (``+``/``-``). Will default to ``None`` which will use the local
timezone. To set the time based off of UTC use ``+0000``. Note: If
being passed through the command line will need to be quoted twice to
allow negative offsets.
:return: Returns the system time in YYYY-MM-DD hh:mm:ss format.
allow negative offsets (e.g. ``"'+0000'"``).
:return: Returns the system time in ``YYYY-MM-DD hh:mm:ss`` format.
:rtype: str
CLI Example:
@ -361,25 +362,26 @@ def set_system_date_time(
"""
Set the system date and time. Each argument is an element of the date, but
not required. If an element is not passed, the current system value for
that element will be used. For example, if you don't pass the year, the
current system year will be used. (Used by set_system_date and
set_system_time)
that element will be used. For example, if the year is not passed, the
current system year will be used. (Used by
:mod:`system.set_system_date <salt.modules.system.set_system_date>` and
:mod:`system.set_system_time <salt.modules.system.set_system_time>`)
Updates hardware clock, if present, in addition to software
(kernel) clock.
:param int years: Years digit, ie: 2015
:param int months: Months digit: 1 - 12
:param int days: Days digit: 1 - 31
:param int hours: Hours digit: 0 - 23
:param int minutes: Minutes digit: 0 - 59
:param int seconds: Seconds digit: 0 - 59
:param str utc_offset: The utc offset in 4 digit (+0600) format with an
optional sign (+/-). Will default to None which will use the local
timezone. To set the time based off of UTC use "'+0000'". Note: if
:param int years: Years digit, e.g.: ``2015``
:param int months: Months digit: ``1``-``12``
:param int days: Days digit: ``1``-``31``
:param int hours: Hours digit: ``0``-``23``
:param int minutes: Minutes digit: ``0``-``59``
:param int seconds: Seconds digit: ``0``-``59``
:param str utc_offset: The UTC offset in 4 digit (``+0600``) format with an
optional sign (``+``/``-``). Will default to ``None`` which will use the local
timezone. To set the time based off of UTC use ``+0000``. Note: If
being passed through the command line will need to be quoted twice to
allow negative offsets.
:return: True if successful. Otherwise False.
allow negative offsets (e.g. ``"'+0000'"``).
:return: ``True`` if successful. Otherwise ``False``.
:rtype: bool
CLI Example:
@ -427,11 +429,11 @@ def get_system_date(utc_offset=None):
"""
Get the system date
:param str utc_offset: The utc offset in 4 digit (+0600) format with an
optional sign (+/-). Will default to None which will use the local
timezone. To set the time based off of UTC use "'+0000'". Note: if
:param str utc_offset: The UTC offset in 4 digit (``+0600``) format with an
optional sign (``+``/``-``). Will default to ``None`` which will use the local
timezone. To set the time based off of UTC use ``+0000``. Note: If
being passed through the command line will need to be quoted twice to
allow negative offsets.
allow negative offsets (e.g. ``"'+0000'"``).
:return: Returns the system date.
:rtype: str
@ -447,17 +449,17 @@ def get_system_date(utc_offset=None):
def set_system_date(newdate, utc_offset=None):
"""
Set the system date. Use <mm-dd-yy> format for the date.
Set the system date. Use ``<mm-dd-yy>`` format for the date.
:param str newdate:
The date to set. Can be any of the following formats:
- YYYY-MM-DD
- MM-DD-YYYY
- MM-DD-YY
- MM/DD/YYYY
- MM/DD/YY
- YYYY/MM/DD
- ``YYYY-MM-DD``
- ``MM-DD-YYYY``
- ``MM-DD-YY``
- ``MM/DD/YYYY``
- ``MM/DD/YY``
- ``YYYY/MM/DD``
CLI Example:
@ -505,7 +507,7 @@ class _FixedOffset(tzinfo):
def _strip_quotes(str_q):
"""
Helper function to strip off the ' or " off of a string
Helper function to strip off the ``'`` or ``"`` off of a string
"""
if str_q[0] == str_q[-1] and str_q.startswith(("'", '"')):
return str_q[1:-1]
@ -514,11 +516,12 @@ def _strip_quotes(str_q):
def get_computer_desc():
"""
Get PRETTY_HOSTNAME value stored in /etc/machine-info
Get ``PRETTY_HOSTNAME`` value stored in ``/etc/machine-info``
If this file doesn't exist or the variable doesn't exist
return False.
return ``False``.
:return: Value of PRETTY_HOSTNAME if this does not exist False.
:return: Value of ``PRETTY_HOSTNAME`` in ``/etc/machine-info``.
If file/variable does not exist ``False``.
:rtype: str
CLI Example:
@ -560,12 +563,12 @@ def get_computer_desc():
def set_computer_desc(desc):
"""
Set PRETTY_HOSTNAME value stored in /etc/machine-info
Set ``PRETTY_HOSTNAME`` value stored in ``/etc/machine-info``
This will create the file if it does not exist. If
it is unable to create or modify this file returns False.
it is unable to create or modify this file, ``False`` is returned.
:param str desc: The computer description
:return: False on failure. True if successful.
:return: ``False`` on failure. ``True`` if successful.
CLI Example:
@ -651,6 +654,10 @@ NILRT_REBOOT_WITNESS_PATH = "/var/volatile/tmp/salt/reboot_witnessed"
@depends("_is_nilrt_family")
def set_reboot_required_witnessed():
"""
.. note::
This only applies to Minions running on NI Linux RT
This function is used to remember that an event indicating that a reboot is
required was witnessed. This function writes to a temporary filesystem so
the event gets cleared upon reboot.
@ -658,6 +665,8 @@ def set_reboot_required_witnessed():
Returns:
bool: ``True`` if successful, otherwise ``False``
CLI Example:
.. code-block:: bash
salt '*' system.set_reboot_required_witnessed
@ -681,6 +690,10 @@ def set_reboot_required_witnessed():
@depends("_is_nilrt_family")
def get_reboot_required_witnessed():
"""
.. note::
This only applies to Minions running on NI Linux RT
Determine if at any time during the current boot session the salt minion
witnessed an event indicating that a reboot is required.

View file

@ -7165,8 +7165,6 @@ def patch(
)
return ret
options = sanitized_options
try:
source_match = __salt__["file.source_list"](source, source_hash, __env__)[0]
except CommandExecutionError as exc:
@ -7265,6 +7263,10 @@ def patch(
pre_check = _patch(patch_file, patch_opts)
if pre_check["retcode"] != 0:
if not os.path.exists(patch_rejects) or os.path.getsize(patch_rejects) == 0:
ret["comment"] = pre_check["stderr"]
ret["result"] = False
return ret
# Try to reverse-apply hunks from rejects file using a dry-run.
# If this returns a retcode of 0, we know that the patch was
# already applied. Rejects are written from the base of the

View file

@ -14,6 +14,17 @@ Ensure a Linux ACL is present
- acl_name: damian
- perms: rwx
Ensure a Linux ACL is present as a default for all new objects
.. code-block:: yaml
root:
acl.present:
- name: /root
- acl_type: "default:user"
- acl_name: damian
- perms: rwx
Ensure a Linux ACL does not exist
.. code-block:: yaml

View file

@ -51,7 +51,12 @@ def present(
Default tablespace for the database
encoding
The character encoding scheme to be used in this database
The character encoding scheme to be used in this database. The encoding
has to be defined in the following format (without hyphen).
.. code-block:: yaml
- encoding: UTF8
lc_collate
The LC_COLLATE setting to be used in this database
@ -89,7 +94,7 @@ def present(
"name": name,
"changes": {},
"result": True,
"comment": "Database {} is already present".format(name),
"comment": f"Database {name} is already present",
}
db_args = {
@ -138,11 +143,11 @@ def present(
if __opts__["test"]:
ret["result"] = None
if name not in dbs:
ret["comment"] = "Database {} is set to be created".format(name)
ret["comment"] = f"Database {name} is set to be created"
else:
ret[
"comment"
] = "Database {} exists, but parameters need to be changed".format(name)
] = f"Database {name} exists, but parameters need to be changed"
return ret
if name not in dbs and __salt__["postgres.db_create"](
name,
@ -152,20 +157,20 @@ def present(
lc_ctype=lc_ctype,
owner=owner,
template=template,
**db_args
**db_args,
):
ret["comment"] = "The database {} has been created".format(name)
ret["comment"] = f"The database {name} has been created"
ret["changes"][name] = "Present"
elif name in dbs and __salt__["postgres.db_alter"](
name, tablespace=tablespace, owner=owner, owner_recurse=owner_recurse, **db_args
):
ret["comment"] = "Parameters for database {} have been changed".format(name)
ret["comment"] = f"Parameters for database {name} have been changed"
ret["changes"][name] = "Parameters changed"
elif name in dbs:
ret["comment"] = "Failed to change parameters for database {}".format(name)
ret["comment"] = f"Failed to change parameters for database {name}"
ret["result"] = False
else:
ret["comment"] = "Failed to create database {}".format(name)
ret["comment"] = f"Failed to create database {name}"
ret["result"] = False
return ret
@ -217,13 +222,13 @@ def absent(
if __salt__["postgres.db_exists"](name, **db_args):
if __opts__["test"]:
ret["result"] = None
ret["comment"] = "Database {} is set to be removed".format(name)
ret["comment"] = f"Database {name} is set to be removed"
return ret
if __salt__["postgres.db_remove"](name, **db_args):
ret["comment"] = "Database {} has been removed".format(name)
ret["comment"] = f"Database {name} has been removed"
ret["changes"][name] = "Absent"
return ret
# fallback
ret["comment"] = "Database {} is not present, so it cannot be removed".format(name)
ret["comment"] = f"Database {name} is not present, so it cannot be removed"
return ret

View file

@ -271,7 +271,7 @@ def key_str(match):
.. code-block:: python
>>> wheel.cmd('key.key_str', ['minion1'])
>>> wheel.cmd('key.print', ['minion1'])
{'minions': {'minion1': '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0B
...
TWugEQpPt\niQIDAQAB\n-----END PUBLIC KEY-----'}}

View file

@ -0,0 +1,150 @@
import logging
import subprocess
import pytest
import salt.utils.platform
log = logging.getLogger(__name__)
@pytest.fixture
def cluster_shared_path(tmp_path):
path = tmp_path / "cluster"
path.mkdir()
return path
@pytest.fixture
def cluster_pki_path(cluster_shared_path):
path = cluster_shared_path / "pki"
path.mkdir()
(path / "peers").mkdir()
return path
@pytest.fixture
def cluster_cache_path(cluster_shared_path):
path = cluster_shared_path / "cache"
path.mkdir()
return path
@pytest.fixture
def cluster_master_1(request, salt_factories, cluster_pki_path, cluster_cache_path):
config_defaults = {
"open_mode": True,
"transport": request.config.getoption("--transport"),
}
config_overrides = {
"interface": "127.0.0.1",
"cluster_id": "master_cluster",
"cluster_peers": [
"127.0.0.2",
"127.0.0.3",
],
"cluster_pki_dir": str(cluster_pki_path),
"cache_dir": str(cluster_cache_path),
}
factory = salt_factories.salt_master_daemon(
"127.0.0.1",
defaults=config_defaults,
overrides=config_overrides,
extra_cli_arguments_after_first_start_failure=["--log-level=info"],
)
with factory.started(start_timeout=120):
yield factory
@pytest.fixture
def cluster_master_2(salt_factories, cluster_master_1):
if salt.utils.platform.is_darwin() or salt.utils.platform.is_freebsd():
subprocess.check_output(["ifconfig", "lo0", "alias", "127.0.0.2", "up"])
config_defaults = {
"open_mode": True,
"transport": cluster_master_1.config["transport"],
}
config_overrides = {
"interface": "127.0.0.2",
"cluster_id": "master_cluster",
"cluster_peers": [
"127.0.0.1",
"127.0.0.3",
],
"cluster_pki_dir": cluster_master_1.config["cluster_pki_dir"],
"cache_dir": cluster_master_1.config["cache_dir"],
}
# Use the same ports for both masters, they are binding to different interfaces
for key in (
"ret_port",
"publish_port",
):
config_overrides[key] = cluster_master_1.config[key]
factory = salt_factories.salt_master_daemon(
"127.0.0.2",
defaults=config_defaults,
overrides=config_overrides,
extra_cli_arguments_after_first_start_failure=["--log-level=info"],
)
with factory.started(start_timeout=120):
yield factory
@pytest.fixture
def cluster_master_3(salt_factories, cluster_master_1):
if salt.utils.platform.is_darwin() or salt.utils.platform.is_freebsd():
subprocess.check_output(["ifconfig", "lo0", "alias", "127.0.0.3", "up"])
config_defaults = {
"open_mode": True,
"transport": cluster_master_1.config["transport"],
}
config_overrides = {
"interface": "127.0.0.3",
"cluster_id": "master_cluster",
"cluster_peers": [
"127.0.0.1",
"127.0.0.2",
],
"cluster_pki_dir": cluster_master_1.config["cluster_pki_dir"],
"cache_dir": cluster_master_1.config["cache_dir"],
}
# Use the same ports for both masters, they are binding to different interfaces
for key in (
"ret_port",
"publish_port",
):
config_overrides[key] = cluster_master_1.config[key]
factory = salt_factories.salt_master_daemon(
"127.0.0.3",
defaults=config_defaults,
overrides=config_overrides,
extra_cli_arguments_after_first_start_failure=["--log-level=info"],
)
with factory.started(start_timeout=120):
yield factory
@pytest.fixture
def cluster_minion_1(cluster_master_1):
config_defaults = {
"transport": cluster_master_1.config["transport"],
}
port = cluster_master_1.config["ret_port"]
addr = cluster_master_1.config["interface"]
config_overrides = {
"master": f"{addr}:{port}",
"test.foo": "baz",
}
factory = cluster_master_1.salt_minion_daemon(
"cluster-minion-1",
defaults=config_defaults,
overrides=config_overrides,
extra_cli_arguments_after_first_start_failure=["--log-level=info"],
)
with factory.started(start_timeout=120):
yield factory

View file

@ -929,6 +929,7 @@ def test_patch_directory_template(
- source: {all_patch_template}
- template: "jinja"
- context: {context}
- strip: 1
""".format(
base_dir=tmp_path, all_patch_template=all_patch_template, context=context
)
@ -945,7 +946,7 @@ def test_patch_directory_template(
# Check to make sure the patch was applied okay
state_run = next(iter(ret.data.values()))
assert state_run["result"] is True
assert state_run["comment"] == "Patch was already applied"
assert state_run["comment"] == "Patch successfully applied"
# Re-run the state, should succeed and there should be a message about
# a partially-applied hunk.

View file

@ -1,150 +1,17 @@
import logging
import subprocess
import pytest
# pylint: disable=unused-import
from tests.pytests.integration.cluster.conftest import (
cluster_cache_path,
cluster_master_1,
cluster_master_2,
cluster_master_3,
cluster_minion_1,
cluster_pki_path,
cluster_shared_path,
)
# pylint: enable=unused-import
import salt.utils.platform
log = logging.getLogger(__name__)
@pytest.fixture
def cluster_shared_path(tmp_path):
path = tmp_path / "cluster"
path.mkdir()
return path
@pytest.fixture
def cluster_pki_path(cluster_shared_path):
path = cluster_shared_path / "pki"
path.mkdir()
(path / "peers").mkdir()
return path
@pytest.fixture
def cluster_cache_path(cluster_shared_path):
path = cluster_shared_path / "cache"
path.mkdir()
return path
@pytest.fixture
def cluster_master_1(request, salt_factories, cluster_pki_path, cluster_cache_path):
config_defaults = {
"open_mode": True,
"transport": request.config.getoption("--transport"),
}
config_overrides = {
"interface": "127.0.0.1",
"cluster_id": "master_cluster",
"cluster_peers": [
"127.0.0.2",
"127.0.0.3",
],
"cluster_pki_dir": str(cluster_pki_path),
"cache_dir": str(cluster_cache_path),
}
factory = salt_factories.salt_master_daemon(
"127.0.0.1",
defaults=config_defaults,
overrides=config_overrides,
extra_cli_arguments_after_first_start_failure=["--log-level=info"],
)
with factory.started(start_timeout=120):
yield factory
@pytest.fixture
def cluster_master_2(salt_factories, cluster_master_1):
if salt.utils.platform.is_darwin() or salt.utils.platform.is_freebsd():
subprocess.check_output(["ifconfig", "lo0", "alias", "127.0.0.2", "up"])
config_defaults = {
"open_mode": True,
"transport": cluster_master_1.config["transport"],
}
config_overrides = {
"interface": "127.0.0.2",
"cluster_id": "master_cluster",
"cluster_peers": [
"127.0.0.1",
"127.0.0.3",
],
"cluster_pki_dir": cluster_master_1.config["cluster_pki_dir"],
"cache_dir": cluster_master_1.config["cache_dir"],
}
# Use the same ports for both masters, they are binding to different interfaces
for key in (
"ret_port",
"publish_port",
):
config_overrides[key] = cluster_master_1.config[key]
factory = salt_factories.salt_master_daemon(
"127.0.0.2",
defaults=config_defaults,
overrides=config_overrides,
extra_cli_arguments_after_first_start_failure=["--log-level=info"],
)
with factory.started(start_timeout=120):
yield factory
@pytest.fixture
def cluster_master_3(salt_factories, cluster_master_1):
if salt.utils.platform.is_darwin() or salt.utils.platform.is_freebsd():
subprocess.check_output(["ifconfig", "lo0", "alias", "127.0.0.3", "up"])
config_defaults = {
"open_mode": True,
"transport": cluster_master_1.config["transport"],
}
config_overrides = {
"interface": "127.0.0.3",
"cluster_id": "master_cluster",
"cluster_peers": [
"127.0.0.1",
"127.0.0.2",
],
"cluster_pki_dir": cluster_master_1.config["cluster_pki_dir"],
"cache_dir": cluster_master_1.config["cache_dir"],
}
# Use the same ports for both masters, they are binding to different interfaces
for key in (
"ret_port",
"publish_port",
):
config_overrides[key] = cluster_master_1.config[key]
factory = salt_factories.salt_master_daemon(
"127.0.0.3",
defaults=config_defaults,
overrides=config_overrides,
extra_cli_arguments_after_first_start_failure=["--log-level=info"],
)
with factory.started(start_timeout=120):
yield factory
@pytest.fixture
def cluster_minion_1(cluster_master_1):
config_defaults = {
"transport": cluster_master_1.config["transport"],
}
port = cluster_master_1.config["ret_port"]
addr = cluster_master_1.config["interface"]
config_overrides = {
"master": f"{addr}:{port}",
"test.foo": "baz",
}
factory = cluster_master_1.salt_minion_daemon(
"cluster-minion-1",
defaults=config_defaults,
overrides=config_overrides,
extra_cli_arguments_after_first_start_failure=["--log-level=info"],
)
with factory.started(start_timeout=120):
yield factory

View file

@ -0,0 +1,74 @@
import os
import pathlib
import time
import salt.crypt
def test_cluster_key_rotation(
cluster_master_1,
cluster_master_2,
cluster_master_3,
cluster_minion_1,
cluster_cache_path,
):
cli = cluster_master_2.salt_cli(timeout=120)
ret = cli.run("test.ping", minion_tgt="cluster-minion-1")
assert ret.data is True
# Validate the aes session key for all masters match
keys = set()
for master in (
cluster_master_1,
cluster_master_2,
cluster_master_3,
):
config = cluster_minion_1.config.copy()
config[
"master_uri"
] = f"tcp://{master.config['interface']}:{master.config['ret_port']}"
auth = salt.crypt.SAuth(config)
auth.authenticate()
assert "aes" in auth._creds
keys.add(auth._creds["aes"])
assert len(keys) == 1
orig_aes = keys.pop()
# Create a drop file and wait for the master to do a key rotation.
dfpath = pathlib.Path(cluster_master_1.config["cachedir"]) / ".dfn"
assert not dfpath.exists()
salt.crypt.dropfile(
cluster_master_1.config["cachedir"],
user=os.getlogin(),
master_id=cluster_master_1.config["id"],
)
assert dfpath.exists()
timeout = 2 * cluster_master_1.config["loop_interval"]
start = time.monotonic()
while True:
if not dfpath.exists():
break
if time.monotonic() - start > timeout:
assert False, f"Drop file never removed {dfpath}"
keys = set()
# Validate the aes session key for all masters match
for master in (
cluster_master_1,
cluster_master_2,
cluster_master_3,
):
config = cluster_minion_1.config.copy()
config[
"master_uri"
] = f"tcp://{master.config['interface']}:{master.config['ret_port']}"
auth = salt.crypt.SAuth(config)
auth.authenticate()
assert "aes" in auth._creds
keys.add(auth._creds["aes"])
assert len(keys) == 1
# Validate the aes session key actually changed
assert orig_aes != keys.pop()

View file

@ -177,7 +177,10 @@ def test_refresh_matchers():
assert ret is False
@pytest.mark.skip_on_windows
def test_refresh_modules_async_false():
# XXX: This test adds coverage but what is it really testing? Seems we'd be
# better off with at least a functional test here.
kwargs = {"async": False}
ret = saltutil.refresh_modules(**kwargs)
assert ret is False

View file

@ -284,12 +284,12 @@ def test_verify_signature_bad_sig(tmp_path):
def test_read_or_generate_key_string(tmp_path):
keyfile = tmp_path / ".aes"
assert not keyfile.exists()
first_key = salt.crypt.Crypticle.read_or_generate_key(keyfile)
assert keyfile.exists()
second_key = salt.crypt.Crypticle.read_or_generate_key(keyfile)
assert first_key == second_key
third_key = salt.crypt.Crypticle.read_or_generate_key(keyfile, remove=True)
assert second_key != third_key
first_key = salt.crypt.Crypticle.read_key(keyfile)
assert first_key is None
assert not keyfile.exists()
salt.crypt.Crypticle.write_key(keyfile)
second_key = salt.crypt.Crypticle.read_key(keyfile)
assert second_key is not None
def test_dropfile_contents(tmp_path, master_opts):

View file

@ -990,7 +990,7 @@ def test_key_rotate_no_master_match(maintenance):
def test_key_dfn_wait(cluster_maintenance):
now = time.monotonic()
key = pathlib.Path(cluster_maintenance.opts["cluster_pki_dir"]) / ".aes"
salt.crypt.Crypticle.read_or_generate_key(str(key))
salt.crypt.Crypticle.write_key(str(key))
rotate_time = time.monotonic() - (cluster_maintenance.opts["publish_session"] + 1)
os.utime(str(key), (rotate_time, rotate_time))