mirror of
https://github.com/saltstack/salt.git
synced 2025-04-16 09:40:20 +00:00
Add defaults
SSH wrapper module
This is a 1:1 copy of the execution module, incl. tests...
This commit is contained in:
parent
4915107fc0
commit
47a609fab0
3 changed files with 456 additions and 0 deletions
1
changelog/51605.fixed.md
Normal file
1
changelog/51605.fixed.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fixed defaults.merge is not available when using salt-ssh
|
240
salt/client/ssh/wrapper/defaults.py
Normal file
240
salt/client/ssh/wrapper/defaults.py
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
"""
|
||||||
|
SSH wrapper module to work with salt formula defaults files
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import salt.fileclient
|
||||||
|
import salt.utils.data
|
||||||
|
import salt.utils.dictupdate as dictupdate
|
||||||
|
import salt.utils.files
|
||||||
|
import salt.utils.json
|
||||||
|
import salt.utils.url
|
||||||
|
import salt.utils.yaml
|
||||||
|
|
||||||
|
__virtualname__ = "defaults"
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _mk_client():
|
||||||
|
"""
|
||||||
|
Create a file client and add it to the context
|
||||||
|
"""
|
||||||
|
return salt.fileclient.get_file_client(__opts__)
|
||||||
|
|
||||||
|
|
||||||
|
def _load(formula):
|
||||||
|
"""
|
||||||
|
Generates a list of salt://<formula>/defaults.(json|yaml) files
|
||||||
|
and fetches them from the Salt master.
|
||||||
|
|
||||||
|
Returns first defaults file as python dict.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Compute possibilities
|
||||||
|
paths = []
|
||||||
|
for ext in ("yaml", "json"):
|
||||||
|
source_url = salt.utils.url.create(formula + "/defaults." + ext)
|
||||||
|
paths.append(source_url)
|
||||||
|
# Fetch files from master
|
||||||
|
with _mk_client() as client:
|
||||||
|
defaults_files = client.cache_files(paths)
|
||||||
|
|
||||||
|
for file_ in defaults_files:
|
||||||
|
if not file_:
|
||||||
|
# Skip empty string returned by cp.fileclient.cache_files.
|
||||||
|
continue
|
||||||
|
|
||||||
|
suffix = file_.rsplit(".", 1)[-1]
|
||||||
|
if suffix == "yaml":
|
||||||
|
loader = salt.utils.yaml.safe_load
|
||||||
|
elif suffix == "json":
|
||||||
|
loader = salt.utils.json.load
|
||||||
|
else:
|
||||||
|
log.debug("Failed to determine loader for %r", file_)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if os.path.exists(file_):
|
||||||
|
log.debug("Reading defaults from %r", file_)
|
||||||
|
with salt.utils.files.fopen(file_) as fhr:
|
||||||
|
defaults = loader(fhr)
|
||||||
|
log.debug("Read defaults %r", defaults)
|
||||||
|
|
||||||
|
return defaults or {}
|
||||||
|
|
||||||
|
|
||||||
|
def get(key, default=""):
|
||||||
|
"""
|
||||||
|
defaults.get is used much like pillar.get except that it will read
|
||||||
|
a default value for a pillar from defaults.json or defaults.yaml
|
||||||
|
files that are stored in the root of a salt formula.
|
||||||
|
|
||||||
|
CLI Example:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
salt '*' defaults.get core:users:root
|
||||||
|
|
||||||
|
The defaults is computed from pillar key. The first entry is considered as
|
||||||
|
the formula namespace.
|
||||||
|
|
||||||
|
For example, querying ``core:users:root`` will try to load
|
||||||
|
``salt://core/defaults.yaml`` and ``salt://core/defaults.json``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Determine formula namespace from query
|
||||||
|
if ":" in key:
|
||||||
|
namespace, key = key.split(":", 1)
|
||||||
|
else:
|
||||||
|
namespace, key = key, None
|
||||||
|
|
||||||
|
# Fetch and load defaults formula files from states.
|
||||||
|
defaults = _load(namespace)
|
||||||
|
|
||||||
|
# Fetch value
|
||||||
|
if key:
|
||||||
|
return salt.utils.data.traverse_dict_and_list(defaults, key, default)
|
||||||
|
else:
|
||||||
|
return defaults
|
||||||
|
|
||||||
|
|
||||||
|
def merge(dest, src, merge_lists=False, in_place=True, convert_none=True):
|
||||||
|
"""
|
||||||
|
defaults.merge
|
||||||
|
Allows deep merging of dicts in formulas.
|
||||||
|
|
||||||
|
merge_lists : False
|
||||||
|
If True, it will also merge lists instead of replace their items.
|
||||||
|
|
||||||
|
in_place : True
|
||||||
|
If True, it will merge into dest dict,
|
||||||
|
if not it will make a new copy from that dict and return it.
|
||||||
|
|
||||||
|
convert_none : True
|
||||||
|
If True, it will convert src and dest to empty dicts if they are None.
|
||||||
|
If True and dest is None but in_place is True, raises TypeError.
|
||||||
|
If False it will make a new copy from that dict and return it.
|
||||||
|
|
||||||
|
.. versionadded:: 3005
|
||||||
|
|
||||||
|
CLI Example:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
salt '*' defaults.merge '{a: b}' '{d: e}'
|
||||||
|
|
||||||
|
It is more typical to use this in a templating language in formulas,
|
||||||
|
instead of directly on the command-line.
|
||||||
|
"""
|
||||||
|
# Force empty dicts if applicable (useful for cleaner templating)
|
||||||
|
src = {} if (src is None and convert_none) else src
|
||||||
|
if dest is None and convert_none:
|
||||||
|
if in_place:
|
||||||
|
raise TypeError("Can't perform in-place merge into NoneType")
|
||||||
|
else:
|
||||||
|
dest = {}
|
||||||
|
|
||||||
|
if in_place:
|
||||||
|
merged = dest
|
||||||
|
else:
|
||||||
|
merged = copy.deepcopy(dest)
|
||||||
|
return dictupdate.update(merged, src, merge_lists=merge_lists)
|
||||||
|
|
||||||
|
|
||||||
|
def deepcopy(source):
|
||||||
|
"""
|
||||||
|
defaults.deepcopy
|
||||||
|
Allows deep copy of objects in formulas.
|
||||||
|
|
||||||
|
By default, Python does not copy objects,
|
||||||
|
it creates bindings between a target and an object.
|
||||||
|
|
||||||
|
It is more typical to use this in a templating language in formulas,
|
||||||
|
instead of directly on the command-line.
|
||||||
|
"""
|
||||||
|
return copy.deepcopy(source)
|
||||||
|
|
||||||
|
|
||||||
|
def update(dest, defaults, merge_lists=True, in_place=True, convert_none=True):
|
||||||
|
"""
|
||||||
|
defaults.update
|
||||||
|
Allows setting defaults for group of data set e.g. group for nodes.
|
||||||
|
|
||||||
|
This function is a combination of defaults.merge
|
||||||
|
and defaults.deepcopy to avoid redundant in jinja.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
group01:
|
||||||
|
defaults:
|
||||||
|
enabled: True
|
||||||
|
extra:
|
||||||
|
- test
|
||||||
|
- stage
|
||||||
|
nodes:
|
||||||
|
host01:
|
||||||
|
index: foo
|
||||||
|
upstream: bar
|
||||||
|
host02:
|
||||||
|
index: foo2
|
||||||
|
upstream: bar2
|
||||||
|
|
||||||
|
.. code-block:: jinja
|
||||||
|
|
||||||
|
{% do salt['defaults.update'](group01.nodes, group01.defaults) %}
|
||||||
|
|
||||||
|
Each node will look like the following:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
host01:
|
||||||
|
enabled: True
|
||||||
|
index: foo
|
||||||
|
upstream: bar
|
||||||
|
extra:
|
||||||
|
- test
|
||||||
|
- stage
|
||||||
|
|
||||||
|
merge_lists : True
|
||||||
|
If True, it will also merge lists instead of replace their items.
|
||||||
|
|
||||||
|
in_place : True
|
||||||
|
If True, it will merge into dest dict.
|
||||||
|
if not it will make a new copy from that dict and return it.
|
||||||
|
|
||||||
|
convert_none : True
|
||||||
|
If True, it will convert src and dest to empty dicts if they are None.
|
||||||
|
If True and dest is None but in_place is True, raises TypeError.
|
||||||
|
If False it will make a new copy from that dict and return it.
|
||||||
|
|
||||||
|
.. versionadded:: 3005
|
||||||
|
|
||||||
|
It is more typical to use this in a templating language in formulas,
|
||||||
|
instead of directly on the command-line.
|
||||||
|
"""
|
||||||
|
# Force empty dicts if applicable here
|
||||||
|
if in_place:
|
||||||
|
if dest is None:
|
||||||
|
raise TypeError("Can't perform in-place update into NoneType")
|
||||||
|
else:
|
||||||
|
nodes = dest
|
||||||
|
else:
|
||||||
|
dest = {} if (dest is None and convert_none) else dest
|
||||||
|
nodes = deepcopy(dest)
|
||||||
|
|
||||||
|
defaults = {} if (defaults is None and convert_none) else defaults
|
||||||
|
|
||||||
|
for node_name, node_vars in nodes.items():
|
||||||
|
defaults_vars = deepcopy(defaults)
|
||||||
|
node_vars = merge(
|
||||||
|
defaults_vars, node_vars, merge_lists=merge_lists, convert_none=convert_none
|
||||||
|
)
|
||||||
|
nodes[node_name] = node_vars
|
||||||
|
|
||||||
|
return nodes
|
215
tests/pytests/unit/client/ssh/wrapper/test_defaults.py
Normal file
215
tests/pytests/unit/client/ssh/wrapper/test_defaults.py
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
"""
|
||||||
|
Test cases for salt.client.ssh.wrapper.defaults
|
||||||
|
|
||||||
|
This has been copied 1:1 from tests.pytests.unit.modules.test_defaults
|
||||||
|
"""
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import salt.client.ssh.wrapper.defaults as defaults
|
||||||
|
from tests.support.mock import MagicMock, patch
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def configure_loader_modules():
|
||||||
|
return {defaults: {}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_mock():
|
||||||
|
"""
|
||||||
|
Test if it execute a defaults client run and return a dict
|
||||||
|
"""
|
||||||
|
with patch.object(inspect, "stack", MagicMock(return_value=[])), patch(
|
||||||
|
"salt.client.ssh.wrapper.defaults.get",
|
||||||
|
MagicMock(return_value={"users": {"root": [0]}}),
|
||||||
|
):
|
||||||
|
assert defaults.get("core:users:root") == {"users": {"root": [0]}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_with_list_merging():
|
||||||
|
"""
|
||||||
|
Test deep merging of dicts with merge_lists enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
src_dict = {
|
||||||
|
"string_key": "string_val_src",
|
||||||
|
"list_key": ["list_val_src"],
|
||||||
|
"dict_key": {"dict_key_src": "dict_val_src"},
|
||||||
|
}
|
||||||
|
|
||||||
|
dest_dict = {
|
||||||
|
"string_key": "string_val_dest",
|
||||||
|
"list_key": ["list_val_dest"],
|
||||||
|
"dict_key": {"dict_key_dest": "dict_val_dest"},
|
||||||
|
}
|
||||||
|
|
||||||
|
merged_dict = {
|
||||||
|
"string_key": "string_val_src",
|
||||||
|
"list_key": ["list_val_dest", "list_val_src"],
|
||||||
|
"dict_key": {
|
||||||
|
"dict_key_dest": "dict_val_dest",
|
||||||
|
"dict_key_src": "dict_val_src",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults.merge(dest_dict, src_dict, merge_lists=True)
|
||||||
|
assert dest_dict == merged_dict
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_without_list_merging():
|
||||||
|
"""
|
||||||
|
Test deep merging of dicts with merge_lists disabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
src = {
|
||||||
|
"string_key": "string_val_src",
|
||||||
|
"list_key": ["list_val_src"],
|
||||||
|
"dict_key": {"dict_key_src": "dict_val_src"},
|
||||||
|
}
|
||||||
|
|
||||||
|
dest = {
|
||||||
|
"string_key": "string_val_dest",
|
||||||
|
"list_key": ["list_val_dest"],
|
||||||
|
"dict_key": {"dict_key_dest": "dict_val_dest"},
|
||||||
|
}
|
||||||
|
|
||||||
|
merged = {
|
||||||
|
"string_key": "string_val_src",
|
||||||
|
"list_key": ["list_val_src"],
|
||||||
|
"dict_key": {
|
||||||
|
"dict_key_dest": "dict_val_dest",
|
||||||
|
"dict_key_src": "dict_val_src",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults.merge(dest, src, merge_lists=False)
|
||||||
|
assert dest == merged
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_not_in_place():
|
||||||
|
"""
|
||||||
|
Test deep merging of dicts not in place.
|
||||||
|
"""
|
||||||
|
|
||||||
|
src = {"nested_dict": {"A": "A"}}
|
||||||
|
|
||||||
|
dest = {"nested_dict": {"B": "B"}}
|
||||||
|
|
||||||
|
dest_orig = {"nested_dict": {"B": "B"}}
|
||||||
|
|
||||||
|
merged = {"nested_dict": {"A": "A", "B": "B"}}
|
||||||
|
|
||||||
|
final = defaults.merge(dest, src, in_place=False)
|
||||||
|
assert dest == dest_orig
|
||||||
|
assert final == merged
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_src_is_none():
|
||||||
|
"""
|
||||||
|
Test deep merging of dicts not in place.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dest = {"nested_dict": {"B": "B"}}
|
||||||
|
|
||||||
|
dest_orig = {"nested_dict": {"B": "B"}}
|
||||||
|
|
||||||
|
final = defaults.merge(dest, None, in_place=False)
|
||||||
|
assert dest == dest_orig
|
||||||
|
assert final == dest_orig
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_dest_is_none():
|
||||||
|
"""
|
||||||
|
Test deep merging of dicts not in place.
|
||||||
|
"""
|
||||||
|
|
||||||
|
src = {"nested_dict": {"B": "B"}}
|
||||||
|
|
||||||
|
src_orig = {"nested_dict": {"B": "B"}}
|
||||||
|
|
||||||
|
final = defaults.merge(None, src, in_place=False)
|
||||||
|
assert src == src_orig
|
||||||
|
assert final == src_orig
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_in_place_dest_is_none():
|
||||||
|
"""
|
||||||
|
Test deep merging of dicts not in place.
|
||||||
|
"""
|
||||||
|
|
||||||
|
src = {"nested_dict": {"B": "B"}}
|
||||||
|
|
||||||
|
pytest.raises(TypeError, defaults.merge, None, src)
|
||||||
|
|
||||||
|
|
||||||
|
def test_deepcopy():
|
||||||
|
"""
|
||||||
|
Test a deep copy of object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
src = {"A": "A", "B": "B"}
|
||||||
|
|
||||||
|
dist = defaults.deepcopy(src)
|
||||||
|
dist.update({"C": "C"})
|
||||||
|
|
||||||
|
result = {"A": "A", "B": "B", "C": "C"}
|
||||||
|
|
||||||
|
assert src != dist
|
||||||
|
assert dist == result
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_in_place():
|
||||||
|
"""
|
||||||
|
Test update with defaults values in place.
|
||||||
|
"""
|
||||||
|
|
||||||
|
group01 = {
|
||||||
|
"defaults": {"enabled": True, "extra": ["test", "stage"]},
|
||||||
|
"nodes": {"host01": {"index": "foo", "upstream": "bar"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
host01 = {
|
||||||
|
"enabled": True,
|
||||||
|
"index": "foo",
|
||||||
|
"upstream": "bar",
|
||||||
|
"extra": ["test", "stage"],
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults.update(group01["nodes"], group01["defaults"])
|
||||||
|
assert group01["nodes"]["host01"] == host01
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_with_defaults_none():
|
||||||
|
group01 = {
|
||||||
|
"defaults": {"enabled": True, "extra": ["test", "stage"]},
|
||||||
|
"nodes": {"host01": {"index": "foo", "upstream": "bar"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
host01 = {
|
||||||
|
"index": "foo",
|
||||||
|
"upstream": "bar",
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults.update(group01["nodes"], None)
|
||||||
|
assert group01["nodes"]["host01"] == host01
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_with_dest_none():
|
||||||
|
group01 = {
|
||||||
|
"defaults": {"enabled": True, "extra": ["test", "stage"]},
|
||||||
|
"nodes": {"host01": {"index": "foo", "upstream": "bar"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = defaults.update(None, group01["defaults"], in_place=False)
|
||||||
|
assert ret == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_in_place_with_dest_none():
|
||||||
|
group01 = {
|
||||||
|
"defaults": {"enabled": True, "extra": ["test", "stage"]},
|
||||||
|
"nodes": {"host01": {"index": "foo", "upstream": "bar"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
pytest.raises(TypeError, defaults.update, None, group01["defaults"])
|
Loading…
Add table
Reference in a new issue