feat(macdefaults): Allow selecting key separator

This commit is contained in:
Carlos Álvaro 2024-06-27 21:03:00 +02:00 committed by Daniel Wozniak
parent 8742299e98
commit 5124936f9e
4 changed files with 229 additions and 71 deletions

View file

@ -42,6 +42,7 @@ def write(
value,
vtype=None,
user=None,
key_separator=None,
dict_merge=False,
array_add=False,
type=None,
@ -55,10 +56,12 @@ def write(
salt '*' macdefaults.write com.apple.Finder DownloadsFolderListViewSettingsVersion 1
salt '*' macdefaults.write com.apple.Finder ComputerViewSettings.CustomViewStyle "icnv"
salt '*' macdefaults.write com.apple.Finder ComputerViewSettings.CustomViewStyle "icnv" key_separator='.'
salt '*' macdefaults.write com.apple.Dock lastShowIndicatorTime 737720347.089987 vtype=date
salt '*' macdefaults.write NSGlobalDomain com.apple.sound.beep.sound "/System/Library/Sounds/Blow.aiff"
domain
The name of the domain to write to
@ -66,6 +69,10 @@ def write(
The key of the given domain to write to.
It can be a nested key/index separated by dots.
key_separator
The separator to use when splitting the key into a list of keys.
If None, the key will not be split (Default).
value
The value to write to the given key.
Dates should be in the format 'YYYY-MM-DDTHH:MM:SSZ'
@ -122,7 +129,7 @@ def write(
)
plist = _load_plist(domain, user=user) or {}
keys = key.split(".")
keys = [key] if key_separator is None else key.split(key_separator)
last_key = keys[-1]
# Traverse the plist
@ -161,7 +168,7 @@ def write(
return _save_plist(domain, plist, user=user)
def read(domain, key, user=None):
def read(domain, key, user=None, key_separator=None):
"""
Read a default from the system
@ -169,10 +176,12 @@ def read(domain, key, user=None):
.. code-block:: bash
salt '*' macdefaults.read com.apple.Dock persistent-apps.1.title-data.file-label
salt '*' macdefaults.read NSGlobalDomain ApplePersistence
salt '*' macdefaults.read NSGlobalDomain key.with.dots-subKey key_separator="-"
salt '*' macdefaults.read com.apple.Dock persistent-apps.1.title-data.file-label key_separator='.'
domain
The name of the domain to read from
@ -180,6 +189,10 @@ def read(domain, key, user=None):
The key of the given domain to read from.
It can be a nested key/index separated by dots.
key_separator
The separator to use when splitting the key into a list of keys.
If None, the key will not be split (Default).
user
The user to read the defaults from
@ -191,10 +204,12 @@ def read(domain, key, user=None):
if plist is None:
return None
return _traverse_keys(plist, key.split("."))
keys = [key] if key_separator is None else key.split(key_separator)
return _traverse_keys(plist, keys)
def delete(domain, key, user=None):
def delete(domain, key, user=None, key_separator=None):
"""
Delete a default from the system
@ -206,6 +221,8 @@ def delete(domain, key, user=None):
salt '*' macdefaults.delete NSGlobalDomain ApplePersistence
salt '*' macdefaults.delete NSGlobalDomain key.with.dots key_separator='.''
domain
The name of the domain to delete from
@ -213,6 +230,10 @@ def delete(domain, key, user=None):
The key of the given domain to delete.
It can be a nested key separated by dots.
key_separator
The separator to use when splitting the key into a list of keys.
If None, the key will not be split (Default).
user
The user to delete the defaults with
@ -221,7 +242,7 @@ def delete(domain, key, user=None):
if plist is None:
return None
keys = key.split(".")
keys = [key] if key_separator is None else key.split(key_separator)
# Traverse the plist til the penultimate key.
# Last key must be handled separately since we
@ -390,9 +411,11 @@ def _save_plist(domain, plist, user=None):
Returns:
A dictionary with the defaults command result
"""
with tempfile.NamedTemporaryFile() as tmpfile:
plistlib.dump(plist, tmpfile)
with tempfile.TemporaryFile(suffix=".plist") as tmpfile:
contents = plistlib.dumps(plist)
tmpfile.write(contents)
tmpfile.flush()
tmpfile.seek(0)
cmd = f'import "{domain}" "{tmpfile.name}"'
return _run_defaults_cmd(cmd, runas=user)

View file

@ -18,7 +18,7 @@ def __virtual__():
return (False, "Only supported on macOS")
def write(name, domain, value, vtype=None, user=None):
def write(name, domain, value, vtype=None, name_separator=None, user=None):
"""
Write a default to the system
@ -26,6 +26,10 @@ def write(name, domain, value, vtype=None, user=None):
The key of the given domain to write to.
It can be a nested key/index separated by dots.
name_separator
The separator to use when splitting the name into a list of keys.
If None, the name will not be split (Default).
domain
The name of the domain to write to
@ -42,7 +46,9 @@ def write(name, domain, value, vtype=None, user=None):
"""
ret = {"name": name, "result": True, "comment": "", "changes": {}}
current_value = __salt__["macdefaults.read"](domain, name, user)
current_value = __salt__["macdefaults.read"](
domain, name, user, key_separator=name_separator
)
if vtype is not None:
try:
@ -57,7 +63,9 @@ def write(name, domain, value, vtype=None, user=None):
return ret
try:
out = __salt__["macdefaults.write"](domain, name, value, vtype, user)
out = __salt__["macdefaults.write"](
domain, name, value, vtype, user, key_separator=name_separator
)
if out["retcode"] != 0:
ret["result"] = False
ret["comment"] = f"Failed to write default. {out['stderr']}"
@ -71,7 +79,7 @@ def write(name, domain, value, vtype=None, user=None):
return ret
def absent(name, domain, user=None):
def absent(name, domain, user=None, name_separator=None):
"""
Make sure the defaults value is absent
@ -79,6 +87,10 @@ def absent(name, domain, user=None):
The key of the given domain to remove.
It can be a nested key/index separated by dots.
name_separator
The separator to use when splitting the name into a list of keys.
If None, the name will not be split (Default).
domain
The name of the domain to remove from
@ -88,7 +100,9 @@ def absent(name, domain, user=None):
"""
ret = {"name": name, "result": True, "comment": "", "changes": {}}
out = __salt__["macdefaults.delete"](domain, name, user)
out = __salt__["macdefaults.delete"](
domain, name, user, key_separator=name_separator
)
if out is None or out["retcode"] != 0:
ret["comment"] += f"{domain} {name} is already absent"

View file

@ -165,30 +165,39 @@ def test_save_plist():
tempfile_mock = MagicMock()
tempfile_mock.name = "/tmp/tmpfile"
tempfile_mock.write = MagicMock(
side_effect=lambda contents: calls.append("tempfile.write")
)
tempfile_mock.flush = MagicMock(side_effect=lambda: calls.append("tempfile.flush"))
tempfile_mock.seek = MagicMock(
side_effect=lambda x: calls.append(f"tempfile.seek({x})")
)
named_temporary_file_mock = MagicMock()
named_temporary_file_mock.return_value.__enter__.return_value = tempfile_mock
plist_mock = MagicMock(
side_effect=lambda _dict, _file: calls.append("plistlib.dump")
)
def side_effect_plistlib_dumps(plist):
calls.append("plistlib.dumps")
return str(plist).encode()
plist_mock = MagicMock(side_effect=side_effect_plistlib_dumps)
with patch(
"salt.modules.macdefaults._run_defaults_cmd", run_defaults_cmd_mock
), patch("tempfile.NamedTemporaryFile", named_temporary_file_mock), patch(
"plistlib.dump", plist_mock
), patch("tempfile.TemporaryFile", named_temporary_file_mock), patch(
"plistlib.dumps", plist_mock
):
result = macdefaults._save_plist("com.googlecode.iterm2", new_plist)
tempfile_mock.flush.assert_called()
plist_mock.assert_called_once_with(new_plist, tempfile_mock)
plist_mock.assert_called_once_with(new_plist)
run_defaults_cmd_mock.assert_called_once_with(
'import "com.googlecode.iterm2" "/tmp/tmpfile"', runas=None
)
assert result == expected_result
assert calls == [
"plistlib.dump",
"plistlib.dumps",
"tempfile.write",
"tempfile.flush",
"tempfile.seek(0)",
"macdefaults._run_defaults_cmd",
]
@ -703,7 +712,9 @@ def test_read_default_dictionary_nested_key(PLIST_OUTPUT):
mock = MagicMock(side_effect=custom_run_defaults_cmd)
with patch("salt.modules.macdefaults._run_defaults_cmd", mock):
result = macdefaults.read(
"com.googlecode.iterm2", "PointerActions.Button,1,1,,.Action"
"com.googlecode.iterm2",
"PointerActions.Button,1,1,,.Action",
key_separator=".",
)
mock.assert_called_once_with('export "com.googlecode.iterm2" -', runas=None)
assert result == "kContextMenuPointerAction"
@ -723,6 +734,7 @@ def test_read_default_dictionary_nested_key_with_array_indexes(PLIST_OUTPUT):
result = macdefaults.read(
"com.googlecode.iterm2",
"NSSplitView Subview Frames NSColorPanelSplitView.0",
key_separator=".",
)
mock.assert_called_once_with('export "com.googlecode.iterm2" -', runas=None)
assert result == "0.000000, 0.000000, 224.000000, 222.000000, NO, NO"
@ -731,6 +743,7 @@ def test_read_default_dictionary_nested_key_with_array_indexes(PLIST_OUTPUT):
result = macdefaults.read(
"com.googlecode.iterm2",
"NSSplitView Subview Frames NSColorPanelSplitView.1",
key_separator=".",
)
assert result == "0.000000, 223.000000, 224.000000, 48.000000, NO, NO"
@ -738,6 +751,7 @@ def test_read_default_dictionary_nested_key_with_array_indexes(PLIST_OUTPUT):
result = macdefaults.read(
"com.googlecode.iterm2",
"NSSplitView Subview Frames NSColorPanelSplitView.2",
key_separator=".",
)
assert result is None
@ -794,20 +808,37 @@ def test_delete_default_with_user():
"""
Test delete a default setting as a specific user
"""
load_plist_mock = MagicMock(return_value={"Crash": "bar"})
export_plist_mock = MagicMock(return_value={"retcode": 0})
original_plist = {
"Crash": {
"foo": "bar",
"baz": 0,
},
"Crash.baz": 0,
}
updated_plist = {
"Crash": {
"foo": "bar",
"baz": 0,
},
}
result = {"retcode": 0, "stdout": "Removed key", "stderr": ""}
load_plist_mock = MagicMock(return_value=original_plist)
export_plist_mock = MagicMock(return_value=result)
with patch("salt.modules.macdefaults._load_plist", load_plist_mock), patch(
"salt.modules.macdefaults._save_plist", export_plist_mock
):
macdefaults.delete("com.apple.CrashReporter", "Crash", user="frank")
macdefaults.delete("com.apple.CrashReporter", "Crash.baz", user="frank")
load_plist_mock.assert_called_once_with(
"com.apple.CrashReporter",
user="frank",
)
export_plist_mock.assert_called_once_with(
"com.apple.CrashReporter",
{},
updated_plist,
user="frank",
)
@ -837,7 +868,11 @@ def test_delete_default_with_nested_key():
with patch("salt.modules.macdefaults._load_plist", load_plist_mock), patch(
"salt.modules.macdefaults._save_plist", export_plist_mock
):
assert result == macdefaults.delete("com.apple.CrashReporter", "Crash.baz")
assert result == macdefaults.delete(
"com.apple.CrashReporter",
"Crash.baz",
key_separator=".",
)
load_plist_mock.assert_called_once_with(
"com.apple.CrashReporter",
user=None,
@ -884,7 +919,9 @@ def test_delete_default_dictionary_nested_key_with_array_indexes():
"salt.modules.macdefaults._save_plist", export_plist_mock
):
assert result == macdefaults.delete(
"com.apple.CrashReporter", "Crash.baz.1.internalKey1"
"com.apple.CrashReporter",
"Crash.baz.1.internalKey1",
key_separator=".",
)
load_plist_mock.assert_called_once_with(
"com.apple.CrashReporter",
@ -931,7 +968,11 @@ def test_delete_default_dictionary_nested_key_with_array_index_as_last_key():
with patch("salt.modules.macdefaults._load_plist", load_plist_mock), patch(
"salt.modules.macdefaults._save_plist", export_plist_mock
):
assert result == macdefaults.delete("com.apple.CrashReporter", "Crash.baz.1")
assert result == macdefaults.delete(
"com.apple.CrashReporter",
"Crash.baz.1",
key_separator=".",
)
load_plist_mock.assert_called_once_with(
"com.apple.CrashReporter",
user=None,

View file

@ -34,9 +34,16 @@ def test_write_default():
{"macdefaults.read": read_mock, "macdefaults.write": write_mock},
):
out = macdefaults.write("DialogType", "com.apple.CrashReporter", "Server")
read_mock.assert_called_once_with("com.apple.CrashReporter", "DialogType", None)
read_mock.assert_called_once_with(
"com.apple.CrashReporter", "DialogType", None, key_separator=None
)
write_mock.assert_called_once_with(
"com.apple.CrashReporter", "DialogType", "Server", None, None
"com.apple.CrashReporter",
"DialogType",
"Server",
None,
None,
key_separator=None,
)
assert out == expected
@ -45,10 +52,12 @@ def test_write_default_already_set():
"""
Test writing a default setting that is already set
"""
key = "DialogType.Value"
expected = {
"changes": {},
"comment": "com.apple.CrashReporter DialogType is already set to Server",
"name": "DialogType",
"comment": f"com.apple.CrashReporter {key} is already set to Server",
"name": key,
"result": True,
}
@ -58,8 +67,12 @@ def test_write_default_already_set():
macdefaults.__salt__,
{"macdefaults.read": read_mock, "macdefaults.write": write_mock},
):
out = macdefaults.write("DialogType", "com.apple.CrashReporter", "Server")
read_mock.assert_called_once_with("com.apple.CrashReporter", "DialogType", None)
out = macdefaults.write(
key, "com.apple.CrashReporter", "Server", name_separator="."
)
read_mock.assert_called_once_with(
"com.apple.CrashReporter", key, None, key_separator="."
)
assert not write_mock.called
assert out == expected
@ -82,9 +95,11 @@ def test_write_default_boolean():
{"macdefaults.read": read_mock, "macdefaults.write": write_mock},
):
out = macdefaults.write("Key", "com.apple.something", "YES", vtype="boolean")
read_mock.assert_called_once_with("com.apple.something", "Key", None)
read_mock.assert_called_once_with(
"com.apple.something", "Key", None, key_separator=None
)
write_mock.assert_called_once_with(
"com.apple.something", "Key", True, "boolean", None
"com.apple.something", "Key", True, "boolean", None, key_separator=None
)
assert out == expected
@ -107,7 +122,9 @@ def test_write_default_boolean_already_set():
{"macdefaults.read": read_mock, "macdefaults.write": write_mock},
):
out = macdefaults.write("Key", "com.apple.something", "YES", vtype="boolean")
read_mock.assert_called_once_with("com.apple.something", "Key", None)
read_mock.assert_called_once_with(
"com.apple.something", "Key", None, key_separator=None
)
assert not write_mock.called
assert out == expected
@ -130,9 +147,11 @@ def test_write_default_integer():
{"macdefaults.read": read_mock, "macdefaults.write": write_mock},
):
out = macdefaults.write("Key", "com.apple.something", 1337)
read_mock.assert_called_once_with("com.apple.something", "Key", None)
read_mock.assert_called_once_with(
"com.apple.something", "Key", None, key_separator=None
)
write_mock.assert_called_once_with(
"com.apple.something", "Key", 1337, None, None
"com.apple.something", "Key", 1337, None, None, key_separator=None
)
assert out == expected
@ -141,10 +160,12 @@ def test_write_default_integer_already_set():
"""
Test writing a default setting with an integer that is already set
"""
key = "Key.subKey"
expected = {
"changes": {},
"comment": "com.apple.something Key is already set to 1337",
"name": "Key",
"comment": f"com.apple.something {key} is already set to 1337",
"name": key,
"result": True,
}
@ -154,8 +175,16 @@ def test_write_default_integer_already_set():
macdefaults.__salt__,
{"macdefaults.read": read_mock, "macdefaults.write": write_mock},
):
out = macdefaults.write("Key", "com.apple.something", 1337, vtype="integer")
read_mock.assert_called_once_with("com.apple.something", "Key", None)
out = macdefaults.write(
key,
"com.apple.something",
1337,
vtype="integer",
name_separator=".",
)
read_mock.assert_called_once_with(
"com.apple.something", key, None, key_separator="."
)
assert not write_mock.called
assert out == expected
@ -178,9 +207,11 @@ def test_write_default_float():
{"macdefaults.read": read_mock, "macdefaults.write": write_mock},
):
out = macdefaults.write("Key", "com.apple.something", "0.8650", vtype="float")
read_mock.assert_called_once_with("com.apple.something", "Key", None)
read_mock.assert_called_once_with(
"com.apple.something", "Key", None, key_separator=None
)
write_mock.assert_called_once_with(
"com.apple.something", "Key", 0.865, "float", None
"com.apple.something", "Key", 0.865, "float", None, key_separator=None
)
assert out == expected
@ -203,7 +234,9 @@ def test_write_default_float_already_set():
{"macdefaults.read": read_mock, "macdefaults.write": write_mock},
):
out = macdefaults.write("Key", "com.apple.something", 0.86500)
read_mock.assert_called_once_with("com.apple.something", "Key", None)
read_mock.assert_called_once_with(
"com.apple.something", "Key", None, key_separator=None
)
assert not write_mock.called
assert out == expected
@ -212,11 +245,13 @@ def test_write_default_array():
"""
Test writing a default setting with an array
"""
key = "Key.subKey"
value = ["a", 1, 0.5, True]
expected = {
"changes": {"written": f"com.apple.something Key is set to {value}"},
"changes": {"written": f"com.apple.something {key} is set to {value}"},
"comment": "",
"name": "Key",
"name": key,
"result": True,
}
@ -226,10 +261,18 @@ def test_write_default_array():
macdefaults.__salt__,
{"macdefaults.read": read_mock, "macdefaults.write": write_mock},
):
out = macdefaults.write("Key", "com.apple.something", value, vtype="array")
read_mock.assert_called_once_with("com.apple.something", "Key", None)
out = macdefaults.write(
key,
"com.apple.something",
value,
vtype="array",
name_separator=".",
)
read_mock.assert_called_once_with(
"com.apple.something", key, None, key_separator="."
)
write_mock.assert_called_once_with(
"com.apple.something", "Key", value, "array", None
"com.apple.something", key, value, "array", None, key_separator="."
)
assert out == expected
@ -253,7 +296,9 @@ def test_write_default_array_already_set():
{"macdefaults.read": read_mock, "macdefaults.write": write_mock},
):
out = macdefaults.write("Key", "com.apple.something", value)
read_mock.assert_called_once_with("com.apple.something", "Key", None)
read_mock.assert_called_once_with(
"com.apple.something", "Key", None, key_separator=None
)
assert not write_mock.called
assert out == expected
@ -280,9 +325,16 @@ def test_write_default_array_add():
out = macdefaults.write(
"Key", "com.apple.something", write_value, vtype="array-add"
)
read_mock.assert_called_once_with("com.apple.something", "Key", None)
read_mock.assert_called_once_with(
"com.apple.something", "Key", None, key_separator=None
)
write_mock.assert_called_once_with(
"com.apple.something", "Key", write_value, "array-add", None
"com.apple.something",
"Key",
write_value,
"array-add",
None,
key_separator=None,
)
assert out == expected
@ -310,9 +362,16 @@ def test_write_default_array_add_already_set_distinct_order():
out = macdefaults.write(
"Key", "com.apple.something", write_value, vtype="array-add"
)
read_mock.assert_called_once_with("com.apple.something", "Key", None)
read_mock.assert_called_once_with(
"com.apple.something", "Key", None, key_separator=None
)
write_mock.assert_called_once_with(
"com.apple.something", "Key", write_value, "array-add", None
"com.apple.something",
"Key",
write_value,
"array-add",
None,
key_separator=None,
)
assert out == expected
@ -340,7 +399,9 @@ def test_write_default_array_add_already_set_same_order():
out = macdefaults.write(
"Key", "com.apple.something", write_value, vtype="array-add"
)
read_mock.assert_called_once_with("com.apple.something", "Key", None)
read_mock.assert_called_once_with(
"com.apple.something", "Key", None, key_separator=None
)
assert not write_mock.called
assert out == expected
@ -364,9 +425,11 @@ def test_write_default_dict():
{"macdefaults.read": read_mock, "macdefaults.write": write_mock},
):
out = macdefaults.write("Key", "com.apple.something", value, vtype="dict")
read_mock.assert_called_once_with("com.apple.something", "Key", None)
read_mock.assert_called_once_with(
"com.apple.something", "Key", None, key_separator=None
)
write_mock.assert_called_once_with(
"com.apple.something", "Key", value, "dict", None
"com.apple.something", "Key", value, "dict", None, key_separator=None
)
assert out == expected
@ -390,7 +453,9 @@ def test_write_default_dict_already_set():
{"macdefaults.read": read_mock, "macdefaults.write": write_mock},
):
out = macdefaults.write("Key", "com.apple.something", value, vtype="dict")
read_mock.assert_called_once_with("com.apple.something", "Key", None)
read_mock.assert_called_once_with(
"com.apple.something", "Key", None, key_separator=None
)
assert not write_mock.called
assert out == expected
@ -417,9 +482,16 @@ def test_write_default_dict_add():
out = macdefaults.write(
"Key", "com.apple.something", write_value, vtype="dict-add"
)
read_mock.assert_called_once_with("com.apple.something", "Key", None)
read_mock.assert_called_once_with(
"com.apple.something", "Key", None, key_separator=None
)
write_mock.assert_called_once_with(
"com.apple.something", "Key", write_value, "dict-add", None
"com.apple.something",
"Key",
write_value,
"dict-add",
None,
key_separator=None,
)
assert out == expected
@ -446,7 +518,9 @@ def test_write_default_dict_add_already_set():
out = macdefaults.write(
"Key", "com.apple.something", write_value, vtype="dict-add"
)
read_mock.assert_called_once_with("com.apple.something", "Key", None)
read_mock.assert_called_once_with(
"com.apple.something", "Key", None, key_separator=None
)
assert not write_mock.called
assert out == expected
@ -465,7 +539,9 @@ def test_absent_default_already():
mock = MagicMock(return_value=None)
with patch.dict(macdefaults.__salt__, {"macdefaults.delete": mock}):
out = macdefaults.absent("Key", "com.apple.something")
mock.assert_called_once_with("com.apple.something", "Key", None)
mock.assert_called_once_with(
"com.apple.something", "Key", None, key_separator=None
)
assert out == expected
@ -473,15 +549,19 @@ def test_absent_default_deleting_existing():
"""
Test removing an existing default value
"""
key = "Key.subKey"
expected = {
"changes": {"absent": "com.apple.something Key is now absent"},
"changes": {"absent": f"com.apple.something {key} is now absent"},
"comment": "",
"name": "Key",
"name": key,
"result": True,
}
mock = MagicMock(return_value={"retcode": 0})
with patch.dict(macdefaults.__salt__, {"macdefaults.delete": mock}):
out = macdefaults.absent("Key", "com.apple.something")
mock.assert_called_once_with("com.apple.something", "Key", None)
out = macdefaults.absent(key, "com.apple.something", name_separator=".")
mock.assert_called_once_with(
"com.apple.something", key, None, key_separator="."
)
assert out == expected