mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
Merge branch 'master' into 18907_lazy_unmount_when_fails
This commit is contained in:
commit
a3f5ff9a7b
32 changed files with 476 additions and 281 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -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:
|
||||
|
|
2
.github/workflows/nightly.yml
vendored
2
.github/workflows/nightly.yml
vendored
|
@ -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:
|
||||
|
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -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:
|
||||
|
|
2
.github/workflows/scheduled.yml
vendored
2
.github/workflows/scheduled.yml
vendored
|
@ -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:
|
||||
|
|
2
.github/workflows/staging.yml
vendored
2
.github/workflows/staging.yml
vendored
|
@ -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:
|
||||
|
|
2
.github/workflows/templates/layout.yml.jinja
vendored
2
.github/workflows/templates/layout.yml.jinja
vendored
|
@ -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
1
changelog/57204.fixed.md
Normal file
|
@ -0,0 +1 @@
|
|||
Removed an unused assignment in file.patch
|
1
changelog/59806.fixed.md
Normal file
1
changelog/59806.fixed.md
Normal file
|
@ -0,0 +1 @@
|
|||
Return error if patch file passed to state file.patch is malformed.
|
|
@ -3,3 +3,4 @@ python_version: "3.10.13"
|
|||
relenv_version: "0.14.2"
|
||||
release-branches:
|
||||
- "3006.x"
|
||||
- "3007.x"
|
||||
|
|
1
doc/_themes/saltstack/layout.html
vendored
1
doc/_themes/saltstack/layout.html
vendored
|
@ -21,6 +21,7 @@
|
|||
|
||||
{# Remove old version of jQuery #}
|
||||
{% set js_blacklist = [
|
||||
'_static/documentation_options.js',
|
||||
'_static/jquery.js',
|
||||
] %}
|
||||
|
||||
|
|
|
@ -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`__
|
||||
|
||||
|
|
|
@ -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
|
||||
---------------------
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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-----'}}
|
||||
|
|
0
tests/pytests/integration/cluster/__init__.py
Normal file
0
tests/pytests/integration/cluster/__init__.py
Normal file
150
tests/pytests/integration/cluster/conftest.py
Normal file
150
tests/pytests/integration/cluster/conftest.py
Normal 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
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
74
tests/pytests/scenarios/cluster/test_cluster.py
Normal file
74
tests/pytests/scenarios/cluster/test_cluster.py
Normal 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()
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue