410 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			410 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import collections
 | 
						|
import copy
 | 
						|
import json
 | 
						|
 | 
						|
import pkg_resources
 | 
						|
import toml
 | 
						|
from dcos import emitting, jsonitem, subcommand, util
 | 
						|
from dcos.errors import DCOSException
 | 
						|
 | 
						|
emitter = emitting.FlatEmitter()
 | 
						|
 | 
						|
logger = util.get_logger(__name__)
 | 
						|
 | 
						|
 | 
						|
def set_val(name, value):
 | 
						|
    """
 | 
						|
    :param name: name of paramater
 | 
						|
    :type name: str
 | 
						|
    :param value: value to set to paramater `name`
 | 
						|
    :type param: str
 | 
						|
    :returns: Toml config
 | 
						|
    :rtype: Toml
 | 
						|
    """
 | 
						|
 | 
						|
    toml_config = util.get_config(True)
 | 
						|
 | 
						|
    section, subkey = split_key(name)
 | 
						|
 | 
						|
    config_schema = get_config_schema(section)
 | 
						|
 | 
						|
    new_value = jsonitem.parse_json_value(subkey, value, config_schema)
 | 
						|
 | 
						|
    toml_config_pre = copy.deepcopy(toml_config)
 | 
						|
    if section not in toml_config_pre._dictionary:
 | 
						|
        toml_config_pre._dictionary[section] = {}
 | 
						|
 | 
						|
    value_exists = name in toml_config
 | 
						|
    old_value = toml_config.get(name)
 | 
						|
 | 
						|
    toml_config[name] = new_value
 | 
						|
 | 
						|
    check_config(toml_config_pre, toml_config)
 | 
						|
 | 
						|
    save(toml_config)
 | 
						|
 | 
						|
    if not value_exists:
 | 
						|
        emitter.publish("[{}]: set to '{}'".format(name, new_value))
 | 
						|
    elif old_value == new_value:
 | 
						|
        emitter.publish("[{}]: already set to '{}'".format(name, old_value))
 | 
						|
    else:
 | 
						|
        emitter.publish(
 | 
						|
            "[{}]: changed from '{}' to '{}'".format(
 | 
						|
                name,
 | 
						|
                old_value,
 | 
						|
                new_value))
 | 
						|
 | 
						|
    return toml_config
 | 
						|
 | 
						|
 | 
						|
def load_from_path(path, mutable=False):
 | 
						|
    """Loads a TOML file from the path
 | 
						|
 | 
						|
    :param path: Path to the TOML file
 | 
						|
    :type path: str
 | 
						|
    :param mutable: True if the returned Toml object should be mutable
 | 
						|
    :type mutable: boolean
 | 
						|
    :returns: Map for the configuration file
 | 
						|
    :rtype: Toml | MutableToml
 | 
						|
    """
 | 
						|
 | 
						|
    util.ensure_file_exists(path)
 | 
						|
    with util.open_file(path, 'r') as config_file:
 | 
						|
        try:
 | 
						|
            toml_obj = toml.loads(config_file.read())
 | 
						|
        except Exception as e:
 | 
						|
            raise DCOSException(
 | 
						|
                'Error parsing config file at [{}]: {}'.format(path, e))
 | 
						|
        return (MutableToml if mutable else Toml)(toml_obj)
 | 
						|
 | 
						|
 | 
						|
def save(toml_config):
 | 
						|
    """
 | 
						|
    :param toml_config: TOML configuration object
 | 
						|
    :type toml_config: MutableToml or Toml
 | 
						|
    """
 | 
						|
 | 
						|
    serial = toml.dumps(toml_config._dictionary)
 | 
						|
    path = util.get_config_path()
 | 
						|
    with util.open_file(path, 'w') as config_file:
 | 
						|
        config_file.write(serial)
 | 
						|
 | 
						|
 | 
						|
def _get_path(toml_config, path):
 | 
						|
    """
 | 
						|
    :param config: Dict with the configuration values
 | 
						|
    :type config: dict
 | 
						|
    :param path: Path to the value. E.g. 'path.to.value'
 | 
						|
    :type path: str
 | 
						|
    :returns: Value stored at the given path
 | 
						|
    :rtype: double, int, str, list or dict
 | 
						|
    """
 | 
						|
 | 
						|
    for section in path.split('.'):
 | 
						|
        toml_config = toml_config[section]
 | 
						|
 | 
						|
    return toml_config
 | 
						|
 | 
						|
 | 
						|
def unset(name):
 | 
						|
    """
 | 
						|
    :param name: name of config value to unset
 | 
						|
    :type name: str
 | 
						|
    :returns: process status
 | 
						|
    :rtype: None
 | 
						|
    """
 | 
						|
 | 
						|
    toml_config = util.get_config(True)
 | 
						|
    toml_config_pre = copy.deepcopy(toml_config)
 | 
						|
    section = name.split(".", 1)[0]
 | 
						|
    if section not in toml_config_pre._dictionary:
 | 
						|
        toml_config_pre._dictionary[section] = {}
 | 
						|
    value = toml_config.pop(name, None)
 | 
						|
    if value is None:
 | 
						|
        raise DCOSException("Property {!r} doesn't exist".format(name))
 | 
						|
    elif isinstance(value, collections.Mapping):
 | 
						|
        raise DCOSException(_generate_choice_msg(name, value))
 | 
						|
    else:
 | 
						|
        emitter.publish("Removed [{}]".format(name))
 | 
						|
        save(toml_config)
 | 
						|
        return
 | 
						|
 | 
						|
 | 
						|
def _generate_choice_msg(name, value):
 | 
						|
    """
 | 
						|
    :param name: name of the property
 | 
						|
    :type name: str
 | 
						|
    :param value: dictionary for the value
 | 
						|
    :type value: dcos.config.Toml
 | 
						|
    :returns: an error message for top level properties
 | 
						|
    :rtype: str
 | 
						|
    """
 | 
						|
 | 
						|
    message = ("Property {!r} doesn't fully specify a value - "
 | 
						|
               "possible properties are:").format(name)
 | 
						|
    for key, _ in sorted(value.property_items()):
 | 
						|
        message += '\n{}.{}'.format(name, key)
 | 
						|
 | 
						|
    return message
 | 
						|
 | 
						|
 | 
						|
def _iterator(parent, dictionary):
 | 
						|
    """
 | 
						|
    :param parent: Path to the value parameter
 | 
						|
    :type parent: str
 | 
						|
    :param dictionary: Value of the key
 | 
						|
    :type dictionary: collection.Mapping
 | 
						|
    :returns: An iterator of tuples for each property and value
 | 
						|
    :rtype: iterator of (str, any) where any can be str, int, double, list
 | 
						|
    """
 | 
						|
 | 
						|
    for key, value in dictionary.items():
 | 
						|
 | 
						|
        new_key = key
 | 
						|
        if parent is not None:
 | 
						|
            new_key = "{}.{}".format(parent, key)
 | 
						|
 | 
						|
        if not isinstance(value, collections.Mapping):
 | 
						|
            yield (new_key, value)
 | 
						|
        else:
 | 
						|
            for x in _iterator(new_key, value):
 | 
						|
                yield x
 | 
						|
 | 
						|
 | 
						|
def split_key(name):
 | 
						|
    """
 | 
						|
    :param name: the full property path - e.g. marathon.url
 | 
						|
    :type name: str
 | 
						|
    :returns: the section and property name
 | 
						|
    :rtype: (str, str)
 | 
						|
    """
 | 
						|
 | 
						|
    terms = name.split('.', 1)
 | 
						|
    if len(terms) != 2:
 | 
						|
        raise DCOSException('Property name must have both a section and '
 | 
						|
                            'key: <section>.<key> - E.g. marathon.url')
 | 
						|
 | 
						|
    return (terms[0], terms[1])
 | 
						|
 | 
						|
 | 
						|
def get_config_schema(command):
 | 
						|
    """
 | 
						|
    :param command: the subcommand name
 | 
						|
    :type command: str
 | 
						|
    :returns: the subcommand's configuration schema
 | 
						|
    :rtype: dict
 | 
						|
    """
 | 
						|
 | 
						|
    # core.* config variables are special.  They're valid, but don't
 | 
						|
    # correspond to any particular subcommand, so we must handle them
 | 
						|
    # separately.
 | 
						|
    if command == "core":
 | 
						|
        return json.loads(
 | 
						|
            pkg_resources.resource_string(
 | 
						|
                'dcos',
 | 
						|
                'data/config-schema/core.json').decode('utf-8'))
 | 
						|
 | 
						|
    executable = subcommand.command_executables(command)
 | 
						|
    return subcommand.config_schema(executable, command)
 | 
						|
 | 
						|
 | 
						|
def check_config(toml_config_pre, toml_config_post):
 | 
						|
    """
 | 
						|
    :param toml_config_pre: dictionary for the value before change
 | 
						|
    :type toml_config_pre: dcos.api.config.Toml
 | 
						|
    :param toml_config_post: dictionary for the value with change
 | 
						|
    :type toml_config_post: dcos.api.config.Toml
 | 
						|
    :returns: process status
 | 
						|
    :rtype: int
 | 
						|
    """
 | 
						|
 | 
						|
    errors_pre = util.validate_json(toml_config_pre._dictionary,
 | 
						|
                                    generate_root_schema(toml_config_pre))
 | 
						|
    errors_post = util.validate_json(toml_config_post._dictionary,
 | 
						|
                                     generate_root_schema(toml_config_post))
 | 
						|
 | 
						|
    logger.info('Comparing changes in the configuration...')
 | 
						|
    logger.info('Errors before the config command: %r', errors_pre)
 | 
						|
    logger.info('Errors after the config command: %r', errors_post)
 | 
						|
 | 
						|
    if len(errors_post) != 0:
 | 
						|
        if len(errors_pre) == 0:
 | 
						|
            raise DCOSException(util.list_to_err(errors_post))
 | 
						|
 | 
						|
        def _errs(errs):
 | 
						|
            return set([e.split('\n')[0] for e in errs])
 | 
						|
 | 
						|
        diff_errors = _errs(errors_post) - _errs(errors_pre)
 | 
						|
        if len(diff_errors) != 0:
 | 
						|
            raise DCOSException(util.list_to_err(errors_post))
 | 
						|
 | 
						|
 | 
						|
def generate_choice_msg(name, value):
 | 
						|
    """
 | 
						|
    :param name: name of the property
 | 
						|
    :type name: str
 | 
						|
    :param value: dictionary for the value
 | 
						|
    :type value: dcos.config.Toml
 | 
						|
    :returns: an error message for top level properties
 | 
						|
    :rtype: str
 | 
						|
    """
 | 
						|
 | 
						|
    message = ("Property {!r} doesn't fully specify a value - "
 | 
						|
               "possible properties are:").format(name)
 | 
						|
    for key, _ in sorted(value.property_items()):
 | 
						|
        message += '\n{}.{}'.format(name, key)
 | 
						|
 | 
						|
    return message
 | 
						|
 | 
						|
 | 
						|
def generate_root_schema(toml_config):
 | 
						|
    """
 | 
						|
    :param toml_configs: dictionary of values
 | 
						|
    :type toml_config: TomlConfig
 | 
						|
    :returns: configuration_schema
 | 
						|
    :rtype: jsonschema
 | 
						|
    """
 | 
						|
 | 
						|
    root_schema = {
 | 
						|
        '$schema': 'http://json-schema.org/schema#',
 | 
						|
        'type': 'object',
 | 
						|
        'properties': {},
 | 
						|
        'additionalProperties': False,
 | 
						|
    }
 | 
						|
 | 
						|
    # Load the config schema from all the subsections into the root schema
 | 
						|
    for section in toml_config.keys():
 | 
						|
        config_schema = get_config_schema(section)
 | 
						|
        root_schema['properties'][section] = config_schema
 | 
						|
 | 
						|
    return root_schema
 | 
						|
 | 
						|
 | 
						|
class Toml(collections.Mapping):
 | 
						|
    """Class for getting value from TOML.
 | 
						|
 | 
						|
    :param dictionary: configuration dictionary
 | 
						|
    :type dictionary: dict
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, dictionary):
 | 
						|
        self._dictionary = dictionary
 | 
						|
 | 
						|
    def __getitem__(self, path):
 | 
						|
        """
 | 
						|
        :param path: Path to the value. E.g. 'path.to.value'
 | 
						|
        :type path: str
 | 
						|
        :returns: Value stored at the given path
 | 
						|
        :rtype: double, int, str, list or dict
 | 
						|
        """
 | 
						|
 | 
						|
        toml_config = _get_path(self._dictionary, path)
 | 
						|
        if isinstance(toml_config, collections.Mapping):
 | 
						|
            return Toml(toml_config)
 | 
						|
        else:
 | 
						|
            return toml_config
 | 
						|
 | 
						|
    def __iter__(self):
 | 
						|
        """
 | 
						|
        :returns: Dictionary iterator
 | 
						|
        :rtype: iterator
 | 
						|
        """
 | 
						|
 | 
						|
        return iter(self._dictionary)
 | 
						|
 | 
						|
    def property_items(self):
 | 
						|
        """Iterator for full-path keys and values
 | 
						|
 | 
						|
        :returns: Iterator for pull-path keys and values
 | 
						|
        :rtype: iterator of tuples
 | 
						|
        """
 | 
						|
 | 
						|
        return _iterator(None, self._dictionary)
 | 
						|
 | 
						|
    def __len__(self):
 | 
						|
        """
 | 
						|
        :returns: The length of the dictionary
 | 
						|
        :rtype: int
 | 
						|
        """
 | 
						|
 | 
						|
        return len(self._dictionary)
 | 
						|
 | 
						|
 | 
						|
class MutableToml(collections.MutableMapping):
 | 
						|
    """Class for managing CLI configuration through TOML.
 | 
						|
 | 
						|
    :param dictionary: configuration dictionary
 | 
						|
    :type dictionary: dict
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, dictionary):
 | 
						|
        self._dictionary = dictionary
 | 
						|
 | 
						|
    def __getitem__(self, path):
 | 
						|
        """
 | 
						|
        :param path: Path to the value. E.g. 'path.to.value'
 | 
						|
        :type path: str
 | 
						|
        :returns: Value stored at the given path
 | 
						|
        :rtype: double, int, str, list or dict
 | 
						|
        """
 | 
						|
 | 
						|
        toml_config = _get_path(self._dictionary, path)
 | 
						|
        if isinstance(toml_config, collections.MutableMapping):
 | 
						|
            return MutableToml(toml_config)
 | 
						|
        else:
 | 
						|
            return toml_config
 | 
						|
 | 
						|
    def __iter__(self):
 | 
						|
        """
 | 
						|
        :returns: Dictionary iterator
 | 
						|
        :rtype: iterator
 | 
						|
        """
 | 
						|
 | 
						|
        return iter(self._dictionary)
 | 
						|
 | 
						|
    def property_items(self):
 | 
						|
        """Iterator for full-path keys and values
 | 
						|
 | 
						|
        :returns: Iterator for pull-path keys and values
 | 
						|
        :rtype: iterator of tuples
 | 
						|
        """
 | 
						|
 | 
						|
        return _iterator(None, self._dictionary)
 | 
						|
 | 
						|
    def __len__(self):
 | 
						|
        """
 | 
						|
        :returns: The length of the dictionary
 | 
						|
        :rtype: int
 | 
						|
        """
 | 
						|
 | 
						|
        return len(self._dictionary)
 | 
						|
 | 
						|
    def __setitem__(self, path, value):
 | 
						|
        """
 | 
						|
        :param path: Path to set
 | 
						|
        :type path: str
 | 
						|
        :param value: Value to store
 | 
						|
        :type value: double, int, str, list or dict
 | 
						|
        """
 | 
						|
 | 
						|
        toml_config = self._dictionary
 | 
						|
 | 
						|
        sections = path.split('.')
 | 
						|
        for section in sections[:-1]:
 | 
						|
            toml_config = toml_config.setdefault(section, {})
 | 
						|
 | 
						|
        toml_config[sections[-1]] = value
 | 
						|
 | 
						|
    def __delitem__(self, path):
 | 
						|
        """
 | 
						|
        :param path: Path to delete
 | 
						|
        :type path: str
 | 
						|
        """
 | 
						|
        toml_config = self._dictionary
 | 
						|
 | 
						|
        sections = path.split('.')
 | 
						|
        for section in sections[:-1]:
 | 
						|
            toml_config = toml_config[section]
 | 
						|
 | 
						|
        del toml_config[sections[-1]]
 |