Skip to content

Commit

Permalink
feat(macdefaults): Add basic support for array-add and dict-add
Browse files Browse the repository at this point in the history
  • Loading branch information
cdalvaro committed May 8, 2024
1 parent c98af08 commit 5ac919d
Show file tree
Hide file tree
Showing 4 changed files with 437 additions and 108 deletions.
89 changes: 39 additions & 50 deletions salt/modules/macdefaults.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""
Set defaults on macOS.
Set defaults settings on macOS.
This module uses defaults cli under the hood to read and write defaults on macOS.
So the module is limited to the capabilities of the defaults command.
Thus, the module is limited to the capabilities of the defaults command.
Read macOS defaults help page for more information on the defaults command.
Read macOS defaults help page for more information on defaults command.
"""

Expand Down Expand Up @@ -33,40 +33,40 @@ def __virtual__():

def write(domain, key, value, vtype=None, user=None, type=None):
"""
Write a default to the system.
Write a default to the system
Limitations:
- There is no multi-level support for arrays and dictionaries.
- Internal values types for arrays and dictionaries cannot be specified.
- There is no multi-level support for arrays and dictionaries
- Internal values types for arrays and dictionaries cannot be specified
CLI Example:
.. code-block:: bash
salt '*' macdefaults.write com.apple.CrashReporter DialogType Server
salt '*' macdefaults.write NSGlobalDomain ApplePersistence True type=bool
salt '*' macdefaults.write NSGlobalDomain ApplePersistence True vtype=bool
domain
The name of the domain to write to.
The name of the domain to write to
key
The key of the given domain to write to.
The key of the given domain to write to
value
The value to write to the given key.
The value to write to the given key
vtype
The type of value to be written, valid types are string, data, int[eger],
float, bool[ean], date, array, array-add, dict, dict-add
type
Deprecated! Use vtype instead.
type collides with Python's built-in type() function.
This parameter will be removed in 3009.
Deprecated! Use vtype instead
type collides with Python's built-in type() function
This parameter will be removed in 3009
user
The user to write the defaults to.
The user to write the defaults to
"""
if type is not None:
Expand Down Expand Up @@ -95,7 +95,7 @@ def write(domain, key, value, vtype=None, user=None, type=None):
elif not isinstance(value, list):
raise ValueError("Value must be a list, dict, int, float, bool, or string")

# Quote values that are not integers or floats
# Quote values that are neither integers nor floats
value = map(lambda v: str(v) if isinstance(v, (int, float)) else f'"{v}"', value)

cmd = f'write "{domain}" "{key}" -{vtype} {" ".join(value)}'
Expand Down Expand Up @@ -169,25 +169,25 @@ def delete(domain, key, user=None):

def read_type(domain, key, user=None):
"""
Read the type of the given type.
If the given key is not found, then return None.
Read a default type from the system
If the key is not found, None is returned.
CLI Example:
.. code-block:: bash
salt '*' macdefaults.read-type com.apple.CrashReporter DialogType
salt '*' macdefaults.read_type com.apple.CrashReporter DialogType
salt '*' macdefaults.read_type NSGlobalDomain ApplePersistence
domain
The name of the domain to read from.
The name of the domain to read from
key
The key of the given domain to read the type of.
The key of the given domain to read the type of
user
The user to read the defaults as.
The user to read the defaults as
"""
cmd = f'read-type "{domain}" "{key}"'
Expand All @@ -203,23 +203,13 @@ def read_type(domain, key, user=None):

def _default_to_python(value, vtype=None):
"""
Cast a value returned by defaults in vytpe to Python type.
CLI Example:
.. code-block:: bash
salt '*' macdefaults.cast_value_to_type "1" int
salt '*' macdefaults.cast_value_to_type "1.0" float
salt '*' macdefaults.cast_value_to_type "TRUE" bool
Cast the value returned by the defaults command in vytpe to Python type
value
The value to cast.
The value to cast
vtype
The type to cast the value to.
The type to cast the value to
"""
if vtype in ["integer", "int"]:
Expand All @@ -238,10 +228,10 @@ def _default_to_python(value, vtype=None):
def _parse_defaults_array(value):
"""
Parse an array from a string returned by `defaults read`
and returns the array content as a list.
and returns the array content as a list
value
A multiline string with the array content, including the surrounding parenthesis.
A multiline string with the array content, including the surrounding parenthesis
"""
lines = value.splitlines()
Expand All @@ -268,12 +258,11 @@ def _parse_defaults_array(value):
def _parse_defaults_dict(value):
"""
Parse a dictionary from a string returned by `defaults read`
and returns the dictionary content as a Python dictionary
value (str):
A multiline string with the dictionary content, including the surrounding curly braces.
A multiline string with the dictionary content, including the surrounding curly braces
Returns:
dict: The dictionary content as a Python dictionary.
"""
lines = value.splitlines()
if not re.match(r"\s*\{", lines[0]) or not re.match(r"\s*\}", lines[-1]):
Expand All @@ -294,10 +283,10 @@ def _parse_defaults_dict(value):

def _convert_to_number_if_possible(value):
"""
Convert a string to a number if possible.
Convert a string to a number if possible
value
The string to convert.
The string to convert
"""
try:
Expand All @@ -311,15 +300,15 @@ def _convert_to_number_if_possible(value):

def _convert_to_defaults_boolean(value):
"""
Convert a boolean to a string that can be used with the defaults command.
Convert a boolean to a string that can be used with the defaults command
value
The boolean value to convert.
The boolean value to convert
"""
if value is True:
if value in (True, 1):
return "TRUE"
if value is False:
if value in (False, 0):
return "FALSE"

BOOLEAN_ALLOWED_VALUES = ["TRUE", "YES", "FALSE", "NO"]
Expand All @@ -333,14 +322,14 @@ def _convert_to_defaults_boolean(value):

def _run_defaults_cmd(action, runas=None):
"""
Run a 'defaults' command.
Run the 'defaults' command with the given action
action
The action to perform with all of its parameters.
The action to perform with all of its parameters
Example: 'write com.apple.CrashReporter DialogType "Server"'
runas
The user to run the command as.
The user to run the command as
"""
ret = __salt__["cmd.run_all"](f"defaults {action}", runas=runas)
Expand All @@ -354,10 +343,10 @@ def _run_defaults_cmd(action, runas=None):

def _remove_timestamp(text):
"""
Remove the timestamp from the output of the defaults command.
Remove the timestamp from the output of the defaults command if found
text
The text to remove the timestamp from.
The text to remove the timestamp from
"""
pattern = r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d{3})?\s+defaults\[\d+\:\d+\]"
Expand Down
76 changes: 59 additions & 17 deletions salt/states/macdefaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,8 @@
"""

import logging
import re

import salt.utils.platform

log = logging.getLogger(__name__)

__virtualname__ = "macdefaults"


Expand Down Expand Up @@ -43,14 +38,13 @@ def write(name, domain, value, vtype="string", user=None):
user
The user to write the defaults to
"""
ret = {"name": name, "result": True, "comment": "", "changes": {}}

current_value = __salt__["macdefaults.read"](domain, name, user)
value = _cast_value(value, vtype)

if _compare_values(value, current_value, strict=re.match(r"-add$", vtype) is None):
if _compare_values(value, current_value, vtype):
ret["comment"] += f"{domain} {name} is already set to {value}"
else:
out = __salt__["macdefaults.write"](domain, name, value, vtype, user)
Expand All @@ -76,7 +70,6 @@ def absent(name, domain, user=None):
user
The user to write the defaults to
"""
ret = {"name": name, "result": True, "comment": "", "changes": {}}

Expand All @@ -90,34 +83,83 @@ def absent(name, domain, user=None):
return ret


def _compare_values(new, current, strict=True):
def _compare_values(new, current, vtype):
"""
Compare two values
Compare two values based on their type
new
The new value to compare
current
The current value to compare
strict
If True, the values must be exactly the same, if False, the new value
must be in the current value
vtype
The type of default value to be compared
"""
if strict:
return new == current
return new in current
if vtype == "array-add":
return _is_subarray(new, current)
if vtype == "dict-add":
return all([key in current and new[key] == current[key] for key in new.keys()])

return new == current


def _is_subarray(new, current):
"""
Check if new is a subarray of current array.
This method does not check only whether all elements in new array
are present in current array, but also whether the elements are in
the same order.
new
The new array to compare
current
The current array to compare
"""
current_len = len(current)
new_len = len(new)

if new_len == 0:
return True
if new_len > current_len:
return False

for i in range(current_len - new_len + 1):
# Check if the new array is found at this position
if current[i : i + new_len] == new:
return True

return False


def _cast_value(value, vtype):
"""
Cast the given macOS default value to Python type
value
The value to cast from macOS default
vtype
The type to cast the value from
"""

def safe_cast(val, to_type, default=None):
"""
Auxiliary function to safely cast a value to a given type
"""
try:
return to_type(val)
except ValueError:
return default

if vtype in ("bool", "boolean"):
if value not in [True, "TRUE", "YES", False, "FALSE", "NO"]:
if value not in [True, 1, "TRUE", "YES", False, 0, "FALSE", "NO"]:
raise ValueError(f"Invalid value for boolean: {value}")
return value in [True, "TRUE", "YES"]

Expand Down

0 comments on commit 5ac919d

Please sign in to comment.