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:
. - 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) 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]]