mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-08-17 20:35:40 +02:00
580 lines
26 KiB
Python
580 lines
26 KiB
Python
# This file was contributed to the Volatility Framework Version 3.
|
|
# Copyright (C) 2018 Volatility Foundation.
|
|
#
|
|
# THE LICENSED WORK IS PROVIDED UNDER THE TERMS OF THE Volatility Contributors
|
|
# Public License V1.0("LICENSE") AS FIRST COMPLETED BY: Volatility Foundation,
|
|
# Inc. ANY USE, PUBLIC DISPLAY, PUBLIC PERFORMANCE, REPRODUCTION OR DISTRIBUTION
|
|
# OF, OR PREPARATION OF SUBSEQUENT WORKS, DERIVATIVE WORKS OR DERIVED WORKS BASED
|
|
# ON, THE LICENSED WORK CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS LICENSE AND ITS
|
|
# TERMS, WHETHER OR NOT SUCH RECIPIENT READS THE TERMS OF THE LICENSE. "LICENSED
|
|
# WORK,” “RECIPIENT" AND “DISTRIBUTOR" ARE DEFINED IN THE LICENSE. A COPY OF THE
|
|
# LICENSE IS LOCATED IN THE TEXT FILE ENTITLED "LICENSE.txt" ACCOMPANYING THE
|
|
# CONTENTS OF THIS FILE. IF A COPY OF THE LICENSE DOES NOT ACCOMPANY THIS FILE, A
|
|
# COPY OF THE LICENSE MAY ALSO BE OBTAINED AT THE FOLLOWING WEB SITE:
|
|
# https://www.volatilityfoundation.org/license/vcpl_v1.0
|
|
#
|
|
# Software distributed under the License is distributed on an "AS IS" basis,
|
|
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
|
|
# specific language governing rights and limitations under the License.
|
|
#
|
|
"""The configuration module contains classes and functions for interacting with the
|
|
configuration and requirement trees.
|
|
|
|
Volatility plugins can specify a list of requirements (which may have subrequirements, thus forming a requirement tree).
|
|
These requirement trees can contain values, which are contained in a complementary configuration tree. These two trees
|
|
act as a protocol between the plugins and users. The plugins provide requirements that must be fulfilled, and the users
|
|
provide configurations values that fulfill those requirements. Where the user does not provide sufficient configuration
|
|
values, automagic modules may extend the configuration tree themselves.
|
|
"""
|
|
|
|
import collections.abc
|
|
import copy
|
|
import json
|
|
import logging
|
|
import random
|
|
import string
|
|
import sys
|
|
from abc import ABCMeta, abstractmethod
|
|
from typing import Any, ClassVar, Dict, Generator, List, Optional, Type, Union
|
|
|
|
from volatility.framework import constants, interfaces
|
|
from volatility.framework.interfaces.context import ContextInterface
|
|
|
|
CONFIG_SEPARATOR = "."
|
|
"""Use to specify the separator between configuration hierarchies"""
|
|
|
|
vollog = logging.getLogger(__name__)
|
|
|
|
BasicTypes = (int, bool, bytes, str)
|
|
SimpleTypes = Union[int, bool, bytes, str]
|
|
ConfigSimpleType = Union[SimpleTypes, List[SimpleTypes]]
|
|
|
|
|
|
def path_join(*args) -> str:
|
|
"""Joins configuration paths together"""
|
|
# If a path element (particularly the first) is empty, then remove it from the list
|
|
args = tuple([arg for arg in args if arg])
|
|
return CONFIG_SEPARATOR.join(args)
|
|
|
|
|
|
def parent_path(value: str) -> str:
|
|
"""Returns the parent configuration path from a configuration path"""
|
|
return CONFIG_SEPARATOR.join(value.split(CONFIG_SEPARATOR)[:-1])
|
|
|
|
|
|
def path_depth(path: str, depth: int = 1) -> str:
|
|
"""Returns the `path` up to a certain depth
|
|
|
|
Note that `depth` can be negative (such as `-x`) and will return all elements except for the last `x` components
|
|
"""
|
|
return path_join(path.split(CONFIG_SEPARATOR)[:depth])
|
|
|
|
|
|
class HierarchicalDict(collections.abc.Mapping):
|
|
"""The core of configuration data, it is a mapping class that stores keys within itself, and also stores lower
|
|
hierarchies.
|
|
|
|
"""
|
|
|
|
def __init__(self, initial_dict: Dict = None, separator: str = CONFIG_SEPARATOR) -> None:
|
|
if not (isinstance(separator, str) and len(separator) == 1):
|
|
raise TypeError("Separator must be a one character string: {}".format(separator))
|
|
self._separator = separator
|
|
self._data = {} # type: Dict[str, ConfigSimpleType]
|
|
self._subdict = {} # type: Dict[str, 'HierarchicalDict']
|
|
if isinstance(initial_dict, str):
|
|
initial_dict = json.loads(initial_dict)
|
|
if isinstance(initial_dict, dict):
|
|
for k, v in initial_dict.items():
|
|
self[k] = v
|
|
elif initial_dict is not None:
|
|
raise TypeError(
|
|
"Initial_dict must be a dictionary or JSON string containing a dictionary: {}".format(initial_dict))
|
|
|
|
@property
|
|
def separator(self) -> str:
|
|
"""Specifies the hierarchy separator in use in this HierarchyDict"""
|
|
return self._separator
|
|
|
|
@property
|
|
def data(self) -> Dict:
|
|
"""Returns just the data-containing mappings on this level of the Hierarchy"""
|
|
return self._data.copy()
|
|
|
|
def _key_head(self, key: str) -> str:
|
|
"""Returns the first division of a key based on the dict separator,
|
|
or the full key if the separator is not present
|
|
"""
|
|
if self.separator in key:
|
|
return key[:key.index(self.separator)]
|
|
else:
|
|
return key
|
|
|
|
def _key_tail(self, key: str) -> str:
|
|
"""Returns all but the first division of a key based on the dict separator,
|
|
or None if the separator is not in the key
|
|
"""
|
|
if self.separator in key:
|
|
return key[key.index(self.separator) + 1:]
|
|
return ''
|
|
|
|
def __iter__(self):
|
|
"""Returns an iterator object that supports the iterator protocol"""
|
|
return self.generator()
|
|
|
|
def generator(self) -> Generator[str, None, None]:
|
|
"""A generator for the data in this level and lower levels of this mapping"""
|
|
for key in self._data:
|
|
yield key
|
|
for subdict_key in self._subdict:
|
|
for key in self._subdict[subdict_key]:
|
|
yield subdict_key + self.separator + key
|
|
|
|
def __getitem__(self, key):
|
|
"""Gets an item, traversing down the trees to get to the final value"""
|
|
try:
|
|
if self.separator in key:
|
|
subdict = self._subdict[self._key_head(key)]
|
|
return subdict[self._key_tail(key)]
|
|
else:
|
|
return self._data[key]
|
|
except KeyError:
|
|
raise KeyError(key)
|
|
|
|
def __setitem__(self, key: str, value: Any) -> None:
|
|
"""Sets an item or creates a subdict and sets the item within that"""
|
|
self._setitem(key, value)
|
|
|
|
def _setitem(self, key: str, value: Any, is_data: bool = True) -> None:
|
|
"""Set an item or appends a whole subtree at a key location"""
|
|
if self.separator in key:
|
|
subdict = self._subdict.get(self._key_head(key), HierarchicalDict(separator = self.separator))
|
|
subdict._setitem(self._key_tail(key), value, is_data)
|
|
self._subdict[self._key_head(key)] = subdict
|
|
else:
|
|
if is_data:
|
|
self._data[key] = self._sanitize_value(value)
|
|
else:
|
|
if not isinstance(value, HierarchicalDict):
|
|
raise TypeError(
|
|
"HierarchicalDicts can only store HierarchicalDicts within their structure: {}".format(
|
|
type(value)))
|
|
self._subdict[key] = value
|
|
|
|
def _sanitize_value(self, value: Any) -> ConfigSimpleType:
|
|
"""Method to ensure all values are standard values and not volatility objects containing contexts"""
|
|
if isinstance(value, bool):
|
|
return bool(value)
|
|
elif isinstance(value, int):
|
|
return int(value)
|
|
elif isinstance(value, str):
|
|
return str(value)
|
|
elif isinstance(value, bytes):
|
|
return bytes(value)
|
|
elif isinstance(value, list):
|
|
new_list = []
|
|
for element in value:
|
|
element_value = self._sanitize_value(element)
|
|
if isinstance(element_value, list):
|
|
raise TypeError("Configuration list types cannot contain list types")
|
|
new_list.append(element_value)
|
|
return new_list
|
|
else:
|
|
raise TypeError("Invalid type stored in configuration")
|
|
|
|
def __delitem__(self, key: str) -> None:
|
|
"""Deletes an item from the hierarchical dict"""
|
|
try:
|
|
if self.separator in key:
|
|
subdict = self._subdict[self._key_head(key)]
|
|
del subdict[self._key_tail(key)]
|
|
else:
|
|
del self._data[self._key_head(key)]
|
|
except KeyError:
|
|
raise KeyError(key)
|
|
|
|
def __contains__(self, key: Any) -> bool:
|
|
"""Determines whether the key is present in the hierarchy"""
|
|
if self.separator in key:
|
|
try:
|
|
subdict = self._subdict[self._key_head(key)]
|
|
return self._key_tail(key) in subdict
|
|
except KeyError:
|
|
return False
|
|
else:
|
|
return key in self._data
|
|
|
|
def __len__(self) -> int:
|
|
"""Returns the length of all items"""
|
|
return len(self._data) + sum([len(subdict) for subdict in self._subdict])
|
|
|
|
def branch(self, key: str) -> 'HierarchicalDict':
|
|
"""Returns the HierarchicalDict housed under the key
|
|
|
|
This differs from the data property, in that it is directed by the `key`, and all layers under that key are
|
|
returned, not just those in that level.
|
|
|
|
Higher layers are not prefixed with the location of earlier layers, so branching a hierarchy containing `a.b.c.d`
|
|
on `a.b` would return a hierarchy containing `c.d`, not `a.b.c.d`.
|
|
|
|
@param key: The location within the hierarchy to return higher layers.
|
|
"""
|
|
try:
|
|
if self.separator in key:
|
|
return self._subdict[self._key_head(key)].branch(self._key_tail(key))
|
|
else:
|
|
return self._subdict[key]
|
|
except KeyError:
|
|
self._setitem(key = key, value = HierarchicalDict(separator = self.separator), is_data = False)
|
|
return HierarchicalDict()
|
|
|
|
def splice(self, key: str, value: 'HierarchicalDict') -> None:
|
|
"""Splices an existing HierarchicalDictionary under a specific key
|
|
|
|
This can be thought of as an inverse of :func:`branch`, although `branch` does not remove the requested
|
|
hierarchy, it simply returns it.
|
|
"""
|
|
if not isinstance(key, str) or not isinstance(value, HierarchicalDict):
|
|
raise TypeError("Splice requires a string key and HierarchicalDict value")
|
|
self._setitem(key, value, False)
|
|
|
|
def merge(self, key: str, value: 'HierarchicalDict', overwrite: bool = False):
|
|
"""Acts similarly to splice, but maintains previous values
|
|
|
|
If overwrite is true, then entries in the new value are used over those that exist within key already
|
|
|
|
@param key: The location within the hierarchy at which to merge the `value`
|
|
@type key: str
|
|
@param value: HierarchicalDict to be merged under the key node
|
|
@type value: HierarchicalDict
|
|
"""
|
|
if not isinstance(key, str) or not isinstance(value, HierarchicalDict):
|
|
raise TypeError("Splice requires a string key and HierarchicalDict value")
|
|
for item in dict(value):
|
|
if self.get(key + self._separator + item, None):
|
|
if overwrite:
|
|
self[key + self._separator + item] = value[item]
|
|
else:
|
|
self[key + self._separator + item] = value[item]
|
|
|
|
def clone(self) -> 'HierarchicalDict':
|
|
"""Duplicates the configuration, allowing changes without affecting the original"""
|
|
return copy.deepcopy(self)
|
|
|
|
def __str__(self) -> str:
|
|
"""Turns the Hierarchical dict into a string representation"""
|
|
return json.dumps(dict([(key, self[key]) for key in sorted(self.generator())]), indent = 2)
|
|
|
|
|
|
class RequirementInterface(metaclass = ABCMeta):
|
|
"""Class that defines a requirement
|
|
|
|
A requirement is a means for plugins and other framework components to request specific configuration data.
|
|
Requirements can either be simple types (such as
|
|
:class:`~volatility.framework.configuration.requirements.SimpleTypeRequirement`,
|
|
:class:`~volatility.framework.configuration.requirements.IntRequirement`,
|
|
:class:`~volatility.framework.configuration.requirements.BytesRequirement` and
|
|
:class:`~volatility.framework.configuration.requirements.StringRequirement`) or complex types (such
|
|
as :class:`TranslationLayerRequirement`, :class:`SymbolTableRequirement` and :class:`ClassRequirement`
|
|
"""
|
|
|
|
def __init__(self,
|
|
name: str,
|
|
description: str = None,
|
|
default: Optional[ConfigSimpleType] = None,
|
|
optional: bool = False) -> None:
|
|
super().__init__()
|
|
if CONFIG_SEPARATOR in name:
|
|
raise ValueError("Name cannot contain the config-hierarchy divider ({})".format(CONFIG_SEPARATOR))
|
|
self._name = name
|
|
self._description = description or ""
|
|
self._default = default
|
|
self._optional = optional
|
|
self._requirements = {} # type: Dict[str, RequirementInterface]
|
|
|
|
def __repr__(self) -> str:
|
|
return "<" + self.__class__.__name__ + ": " + self.name + ">"
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
"""The name of the Requirement. Names cannot contain "." since this is used within the configuration hierarchy."""
|
|
return self._name
|
|
|
|
@property
|
|
def description(self) -> str:
|
|
"""A short description of what the Requirement is designed to affect or achieve."""
|
|
return self._description
|
|
|
|
@property
|
|
def default(self) -> Optional[ConfigSimpleType]:
|
|
"""Returns the default value if one is set"""
|
|
return self._default
|
|
|
|
@property
|
|
def optional(self) -> bool:
|
|
"""Whether the Requirement is optional or not"""
|
|
return self._optional
|
|
|
|
@optional.setter
|
|
def optional(self, value) -> None:
|
|
"""Sets the optional value for a requirement"""
|
|
self._optional = bool(value)
|
|
|
|
def config_value(self, context: ContextInterface, config_path: str,
|
|
default: ConfigSimpleType = None) -> ConfigSimpleType:
|
|
"""Returns the value for this Requirement from its config path"""
|
|
return context.config.get(config_path, default)
|
|
|
|
# Child operations
|
|
@property
|
|
def requirements(self) -> Dict[str, 'RequirementInterface']:
|
|
"""Returns a dictionary of all the child requirements, indexed by name"""
|
|
return self._requirements.copy()
|
|
|
|
def add_requirement(self, requirement: 'RequirementInterface') -> None:
|
|
"""Adds a child to the list of requirements"""
|
|
self._requirements[requirement.name] = requirement
|
|
|
|
def remove_requirement(self, requirement: 'RequirementInterface') -> None:
|
|
"""Removes a child from the list of requirements"""
|
|
del self._requirements[requirement.name]
|
|
|
|
def unsatisfied_children(self, context: ContextInterface, config_path: str) -> Dict[str, 'RequirementInterface']:
|
|
"""Method that will validate all child requirements"""
|
|
result = {}
|
|
for requirement in self.requirements.values():
|
|
if not requirement.optional:
|
|
subresult = requirement.unsatisfied(context, path_join(config_path, self._name))
|
|
result.update(subresult)
|
|
return result
|
|
|
|
# Validation routines
|
|
@abstractmethod
|
|
def unsatisfied(self, context: ContextInterface, config_path: str) -> Dict[str, 'RequirementInterface']:
|
|
"""Method to validate the value stored at config_path for the configuration object against a context
|
|
|
|
Returns a list containing its own name (or multiple unsatisfied requirement names) when invalid
|
|
"""
|
|
|
|
|
|
class SimpleTypeRequirement(RequirementInterface):
|
|
"""Class to represent a single simple type (such as a boolean, a string, an integer or a series of bytes)"""
|
|
instance_type = bool # type: ClassVar[Type]
|
|
|
|
def add_requirement(self, requirement: RequirementInterface):
|
|
"""Always raises a TypeError as instance requirements cannot have children"""
|
|
raise TypeError("Instance Requirements cannot have subrequirements")
|
|
|
|
def remove_requirement(self, requirement: RequirementInterface):
|
|
"""Always raises a TypeError as instance requirements cannot have children"""
|
|
raise TypeError("Instance Requirements cannot have subrequirements")
|
|
|
|
def unsatisfied(self, context: ContextInterface, config_path: str) -> Dict[str, RequirementInterface]:
|
|
"""Validates the instance requirement based upon its `instance_type`."""
|
|
config_path = path_join(config_path, self.name)
|
|
|
|
value = self.config_value(context, config_path, None)
|
|
if not isinstance(value, self.instance_type):
|
|
vollog.log(
|
|
constants.LOGLEVEL_V,
|
|
"TypeError - {} requirements only accept {} type: {}".format(self.name, self.instance_type.__name__,
|
|
value))
|
|
return {config_path: self}
|
|
return {}
|
|
|
|
|
|
class ClassRequirement(RequirementInterface):
|
|
"""Requires a specific class. This is used as means to serialize specific classes for :class:`TranslationLayerRequirement`
|
|
and :class:`SymbolTableRequirement` classes."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self._cls = None
|
|
|
|
@property
|
|
def cls(self) -> Type:
|
|
return self._cls
|
|
|
|
def unsatisfied(self, context: ContextInterface, config_path: str) -> Dict[str, RequirementInterface]:
|
|
"""Checks to see if a class can be recovered"""
|
|
config_path = path_join(config_path, self.name)
|
|
|
|
value = self.config_value(context, config_path, None)
|
|
self._cls = None
|
|
if value is not None and isinstance(value, str):
|
|
if "." in value:
|
|
# TODO: consider importing the prefix
|
|
module = sys.modules.get(value[:value.rindex(".")], None)
|
|
class_name = value[value.rindex(".") + 1:]
|
|
if hasattr(module, class_name):
|
|
self._cls = getattr(module, class_name)
|
|
else:
|
|
if value in globals():
|
|
self._cls = globals()[value]
|
|
if self._cls is None:
|
|
return {config_path: self}
|
|
return {}
|
|
|
|
|
|
class ConstructableRequirementInterface(RequirementInterface):
|
|
"""Defines a Requirement that can be constructed based on their own requirements.
|
|
|
|
This effectively offers a means for serializing specific python types, to be reconstructed based on simple
|
|
configuration data. Each constructable records a `class` requirement, which indicates the object that will be
|
|
constructed. That class may have its own requirements (which is why validation of a ConstructableRequirement
|
|
must happen after the class configuration value has been provided). These values are then provided to the object's
|
|
constructor by name as arguments (as well as the standard `context` and `config_path` arguments.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.add_requirement(ClassRequirement("class", "Class of the constructable requirement"))
|
|
self._current_class_requirements = set()
|
|
|
|
@abstractmethod
|
|
def construct(self, context: ContextInterface, config_path: str) -> None:
|
|
"""Method for constructing within the context any required elements from subrequirements"""
|
|
|
|
def _validate_class(self, context: ContextInterface, config_path: str) -> None:
|
|
"""Method to check if the class Requirement is valid and if so populate the other requirements
|
|
(but no need to validate, since we're invalid already)
|
|
"""
|
|
class_req = self.requirements['class']
|
|
subreq_config_path = path_join(config_path, self.name)
|
|
if not class_req.unsatisfied(context, subreq_config_path) and isinstance(class_req, ClassRequirement):
|
|
# We have a class, and since it's validated we can construct our requirements from it
|
|
if issubclass(class_req.cls, ConfigurableInterface):
|
|
# In case the class has changed, clear out the old requirements
|
|
for old_req in self._current_class_requirements.copy():
|
|
del self._requirements[old_req]
|
|
self._current_class_requirements.remove(old_req)
|
|
# And add the new ones
|
|
for requirement in class_req.cls.get_requirements():
|
|
self._current_class_requirements.add(requirement.name)
|
|
self.add_requirement(requirement)
|
|
|
|
def _construct_class(self, context: ContextInterface, config_path: str,
|
|
requirement_dict: Dict[str, object] = None) -> Optional['interfaces.objects.ObjectInterface']:
|
|
"""Constructs the class, handing args and the subrequirements as parameters to __init__"""
|
|
if self.requirements["class"].unsatisfied(context, config_path):
|
|
return None
|
|
|
|
if not isinstance(self.requirements["class"], ClassRequirement):
|
|
return None
|
|
cls = self.requirements["class"].cls
|
|
|
|
# These classes all have a name property
|
|
# We could subclass this out as a NameableInterface, but it seems a little excessive
|
|
# FIXME: We can't test this, because importing the other interfaces causes all kinds of import loops
|
|
# if not issubclass(cls, [interfaces.layers.TranslationLayerInterface,
|
|
# interfaces.symbols.SymbolTableInterface]):
|
|
# return None
|
|
|
|
if requirement_dict is None:
|
|
requirement_dict = {}
|
|
|
|
# Fulfillment must happen, exceptions happening here mean the requirements aren't correct
|
|
# and these need to be raised and fixed, rather than caught and ignored
|
|
obj = cls(**requirement_dict)
|
|
context.config[config_path] = obj.name
|
|
return obj
|
|
|
|
|
|
class ConfigurableRequirementInterface(RequirementInterface):
|
|
"""Simple Abstract class to provide build_required_config"""
|
|
|
|
def build_configuration(self, context: ContextInterface, config_path: str, value: Any) -> HierarchicalDict:
|
|
"""Proxies to a ConfigurableInterface if necessary"""
|
|
|
|
|
|
class ConfigurableInterface(metaclass = ABCMeta):
|
|
"""Class to allow objects to have requirements and read configuration data from the context config tree"""
|
|
|
|
def __init__(self, context: ContextInterface, config_path: str) -> None:
|
|
"""Basic initializer that allows configurables to access their own config settings"""
|
|
super().__init__()
|
|
self._context = context
|
|
self._config_path = config_path
|
|
self._config_cache = None # type: Optional[HierarchicalDict]
|
|
|
|
@property
|
|
def context(self) -> ContextInterface:
|
|
return self._context
|
|
|
|
@property
|
|
def config_path(self) -> str:
|
|
return self._config_path
|
|
|
|
@config_path.setter
|
|
def config_path(self, value: str) -> None:
|
|
self._config_path = value
|
|
self._config_cache = None
|
|
|
|
@property
|
|
def config(self) -> HierarchicalDict:
|
|
"""The Hierarchical configuration Dictionary for this Configurable object"""
|
|
if self._config_cache is None:
|
|
self._config_cache = self._context.config.branch(self._config_path)
|
|
return self._config_cache
|
|
|
|
def build_configuration(self) -> HierarchicalDict:
|
|
"""Constructs a HierarchicalDictionary of all the options required to build this component in the current context.
|
|
|
|
Ensures that if the class has been created, it can be recreated using the configuration built
|
|
Inheriting classes must override this to ensure any dependent classes update their configurations too
|
|
"""
|
|
result = HierarchicalDict()
|
|
for req in self.get_requirements():
|
|
value = self.config.get(req.name, None)
|
|
# Do not include the name of constructed classes
|
|
if value is not None and not isinstance(req, ConstructableRequirementInterface):
|
|
result[req.name] = value
|
|
if isinstance(req, ConfigurableRequirementInterface):
|
|
if value is not None:
|
|
result.splice(req.name, req.build_configuration(self.context, self.config_path, value))
|
|
return result
|
|
|
|
@classmethod
|
|
def get_requirements(cls) -> List[RequirementInterface]:
|
|
"""Returns a list of RequirementInterface objects required by this object"""
|
|
return []
|
|
|
|
@classmethod
|
|
def unsatisfied(cls, context: ContextInterface, config_path: str) -> Dict[str, RequirementInterface]:
|
|
"""Returns a list of the names of all unsatisfied requirements
|
|
|
|
Since a satisfied set of requirements will return [], it can be used in tests as follows:
|
|
|
|
.. code-block:: python
|
|
|
|
unmet = configurable.unsatisfied(context, config_path)
|
|
if unmet:
|
|
raise RuntimeError("Unsatisfied requirements: {}".format(unmet)
|
|
"""
|
|
result = {}
|
|
for requirement in cls.get_requirements():
|
|
if not requirement.optional:
|
|
subresult = requirement.unsatisfied(context, config_path)
|
|
result.update(subresult)
|
|
return result
|
|
|
|
def make_subconfig(self, *args, **kwargs) -> str:
|
|
"""Constructs a new subconfig, containing each element from kwargs and returns the full config_path to it"""
|
|
if args:
|
|
vollog.debug("Non-keyword arguments to make_subconfig are ignored - this is a bug in the calling code")
|
|
|
|
random_config_dict = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits)
|
|
for _ in range(8))
|
|
new_config_path = path_join(self.config_path, random_config_dict)
|
|
# TODO: Check that the new_config_path is empty, although it's not critical if it's not since the values are merged in
|
|
|
|
# This should check that each k corresponds to a requirement and each v is of the appropriate type
|
|
# This would require knowledge of the new configurable itself to verify, and they should do validation in the
|
|
# constructor anyway, however, to prevent bad types getting into the config tree we just verify that v is a simple type
|
|
for k, v in kwargs.items():
|
|
if not isinstance(v, (int, str, bool, float, bytes)):
|
|
raise TypeError("Config values passed to make_subconfig can only be simple types")
|
|
self.context.config[path_join(new_config_path, k)] = v
|
|
|
|
return new_config_path
|