1
0
mirror of https://github.com/craigerl/aprsd.git synced 2026-01-22 05:25:34 -05:00
aprsd/aprsd/utils/config_converter.py
Walter Boring d2cb208be8 Added new owner_callsign
the owner_callsign is a new [DEFAULT]
option that must be set.  It's the legal FCC callsign
associated with the license that owns this instances of APRSD.
that might be different than callsign, because the name of the
aprsd might be a bot for the APRS network that's useful,
such as 'REPEAT'
2026-01-20 17:27:47 -05:00

278 lines
8.5 KiB
Python

"""Utilities for converting oslo_cfg CONF objects to/from JSON."""
import json
from typing import Any, Dict
from oslo_config import cfg
def conf_to_dict(conf: cfg.CONF) -> Dict[str, Any]:
"""Convert an oslo_cfg CONF object to a flat dictionary.
Converts a CONF object with hierarchical groups into a flat dictionary
where group options are prefixed with 'group_name.option_name'.
Args:
conf: The oslo_cfg CONF object to convert
Returns:
A dictionary with configuration values, where secret options are masked
Example:
>>> from oslo_config import cfg
>>> CONF = cfg.CONF
>>> d = conf_to_dict(CONF)
>>> print(d.get('aprsd.callsign'))
'W5XYZ'
"""
entries = {}
def _sanitize(opt, value):
"""Obfuscate values of options declared secret."""
if opt.secret:
return '*' * 4
return value
# Process top-level options
for opt_name in sorted(conf._opts):
opt = conf._get_opt_info(opt_name)['opt']
value = getattr(conf, opt_name)
sanitized = _sanitize(opt, value)
entries[opt_name] = sanitized
# Process group options
for group_name in sorted(conf._groups):
group_obj = conf._get_group(group_name)
group_attr = conf.GroupAttr(conf, group_obj)
for opt_name in sorted(conf._groups[group_name]._opts):
opt = conf._get_opt_info(opt_name, group_name)['opt']
value = getattr(group_attr, opt_name)
sanitized = _sanitize(opt, value)
gname_opt_name = f'{group_name}.{opt_name}'
entries[gname_opt_name] = sanitized
return entries
def conf_to_json(conf: cfg.CONF, indent: int = 2) -> str:
"""Convert an oslo_cfg CONF object to a JSON string.
Args:
conf: The oslo_cfg CONF object to convert
indent: Number of spaces for indentation (None for compact output)
Returns:
A JSON string representation of the configuration
Example:
>>> from oslo_config import cfg
>>> CONF = cfg.CONF
>>> json_str = conf_to_json(CONF)
>>> print(json_str)
"""
config_dict = conf_to_dict(conf)
return json.dumps(config_dict, indent=indent, default=_json_serializer)
def dict_to_conf(
config_dict: Dict[str, Any],
conf: cfg.CONF = None,
mask_secrets: bool = True,
) -> cfg.CONF:
"""Convert a flat dictionary back to an oslo_cfg CONF object.
Takes a flat dictionary (with keys like 'group_name.option_name' for grouped
options) and applies those values to a CONF object. Only updates options that
exist in the CONF object.
Args:
config_dict: The configuration dictionary to convert
conf: The oslo_cfg CONF object to update (uses cfg.CONF if None)
mask_secrets: If True, skips options with masked values ('****')
Returns:
The updated CONF object
Example:
>>> from oslo_config import cfg
>>> config_dict = {'aprsd.callsign': 'W5XYZ', 'log_level': 'DEBUG'}
>>> CONF = dict_to_conf(config_dict)
>>> print(CONF.aprsd.callsign)
'W5XYZ'
Note:
- Options with secret masks ('****') are skipped to avoid overwriting
with placeholder values
- Only recognized options in the CONF schema are updated
- Invalid group/option names are silently skipped
"""
if conf is None:
conf = cfg.CONF
for key, value in config_dict.items():
# Skip masked secret values
if mask_secrets and isinstance(value, str) and value == '*' * 4:
continue
if '.' in key:
# Handle grouped options
group_name, opt_name = key.split('.', 1)
try:
# Check if group exists
if group_name in conf:
group = getattr(conf, group_name)
# Check if option exists in group
if hasattr(group, opt_name):
_set_conf_value(conf, group_name, opt_name, value)
except (KeyError, AttributeError):
# Skip unrecognized groups
continue
else:
# Handle top-level options
try:
if hasattr(conf, key):
_set_conf_value(conf, None, key, value)
except (KeyError, AttributeError):
# Skip unrecognized options
continue
return conf
def json_to_conf(
json_str: str,
conf: cfg.CONF = None,
mask_secrets: bool = True,
) -> cfg.CONF:
"""Convert a JSON string back to an oslo_cfg CONF object.
Args:
json_str: The JSON string to parse
conf: The oslo_cfg CONF object to update (uses cfg.CONF if None)
mask_secrets: If True, skips options with masked values ('****')
Returns:
The updated CONF object
Raises:
json.JSONDecodeError: If the JSON string is invalid
Example:
>>> json_str = '{"aprsd.callsign": "W5XYZ", "log_level": "DEBUG"}'
>>> CONF = json_to_conf(json_str)
"""
config_dict = json.loads(json_str)
return dict_to_conf(config_dict, conf, mask_secrets)
def _set_conf_value(
conf: cfg.CONF,
group_name: str,
opt_name: str,
value: Any,
) -> None:
"""Set a configuration value in CONF object with proper type conversion.
Args:
conf: The CONF object
group_name: The group name (None for top-level options)
opt_name: The option name
value: The value to set
Raises:
KeyError: If the option is not found
"""
# Get the option metadata
if group_name:
opt_info = conf._get_opt_info(opt_name, group_name)
else:
opt_info = conf._get_opt_info(opt_name)
opt = opt_info['opt']
# Convert value to appropriate type
converted_value = _convert_value(opt, value)
# Set the value
if group_name:
# For grouped options, we need to set via the group
group = getattr(conf, group_name)
setattr(group, opt_name, converted_value)
else:
# For top-level options
setattr(conf, opt_name, converted_value)
def _convert_value(opt, value: Any) -> Any:
"""Convert a value to the appropriate type for an option.
Handles conversion for oslo_config option types:
- StrOpt: keeps as string
- IntOpt: converts to int
- FloatOpt: converts to float
- BoolOpt: converts to bool
- ListOpt: ensures it's a list
- DictOpt: ensures it's a dict
Args:
opt: The oslo_config option object
value: The value to convert
Returns:
The converted value
"""
if value is None:
return None
# Handle string representations
if isinstance(value, str):
if isinstance(opt, cfg.IntOpt):
return int(value)
elif isinstance(opt, cfg.FloatOpt):
return float(value)
elif isinstance(opt, cfg.BoolOpt):
return value.lower() in ('true', '1', 'yes', 'on')
elif isinstance(opt, (cfg.ListOpt, cfg.MultiOpt)):
# If it's a string representation of a list, parse it
if value.startswith('[') and value.endswith(']'):
return json.loads(value)
return [value] if value else []
elif isinstance(opt, cfg.DictOpt):
# If it's a string representation of a dict, parse it
if value.startswith('{') and value.endswith('}'):
return json.loads(value)
return {}
elif isinstance(value, bool) and not isinstance(opt, cfg.BoolOpt):
# If we got a bool but it's not a BoolOpt, convert to appropriate type
if isinstance(opt, cfg.StrOpt):
return str(value)
elif isinstance(opt, cfg.IntOpt):
return int(value)
elif isinstance(value, (list, tuple)):
if isinstance(opt, (cfg.ListOpt, cfg.MultiOpt)):
return list(value)
elif isinstance(opt, cfg.StrOpt):
# Convert list to comma-separated string
return ','.join(str(v) for v in value)
elif isinstance(value, dict):
if isinstance(opt, cfg.DictOpt):
return value
elif isinstance(opt, cfg.StrOpt):
return json.dumps(value)
return value
def _json_serializer(obj: Any) -> Any:
"""Custom JSON serializer for oslo_config types.
Args:
obj: The object to serialize
Returns:
A JSON-serializable representation of the object
"""
if hasattr(obj, '__iter__') and not isinstance(obj, (str, bytes)):
return list(obj)
return str(obj)