Skip to content
Closed
61 changes: 61 additions & 0 deletions doc/source/operations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,64 @@ value of ``dict_reference`` will actually be a dictionary.
You should now be ready to :doc:`use reclass <usage>`!

.. include:: substs.inc

Function interpolation
----------------------

Certain functions can be used to dynamically generate values. They are specified
like this::

parameters:
key: $<function(argument1, argument2, ...)>

The following functions are supported:

print
*****

This function simply concatenates all its parameters and returns a string. For
example, take this::

test: $<print(first, second, third)>

which results in this::

test: "first second third"

Yeah, it's quite useless. Now to something a bit more useful:

aggregate
*********

This can be used to extract values from hosts that satisfy certain conditions.
The syntax looks like this::

aggregate(filter, extractor)

The ``filter`` parameter specifies the condition the host has to fulfil, and
the ``extractor`` determines what will be taken from that host. Inside the
function, ``node`` refers to all parameters of a node, and regular python
dictionary functions can be used to access it.

The return value is always a dictionary that maps the nodename to the extracted
values.

Here is an example that extracts the IP address of every host in our domain
``example.com``::

hosts: $<aggregate(node['domain'] == 'example.com', node['ip'])>

which might result in something like this::

hosts:
first.example.com: 192.168.1.1
second.example.com: 192.168.1.2

Note that parameter interpolation can be used inside functions, but one has to
pay attention to proper quoting::

hosts: $<aggregate(node['domain'] == "${network:domain}", node['ip'])>

Note that chained references are not supported: If the result of a function
contains a function itself, this function will not be interpolated, but taken
verbatim. This also prevents circular references.
11 changes: 10 additions & 1 deletion reclass/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,22 @@ def _nodeinfo_as_dict(self, nodename, entity):
return ret

def nodeinfo(self, nodename):
return self._nodeinfo_as_dict(nodename, self._nodeinfo(nodename))
return self.inventory()['nodes'][nodename]

def inventory(self):
entities = {}

# first run, reference parameters are expanded
for n in self._storage.enumerate_nodes():
entities[n] = self._nodeinfo(n)

# second run, function are executed
#all_parameters = {}
#for nodename, info in entities.items():
# all_parameters.update({nodename: info.parameters})
for nodename, node in entities.items():
node.expand_functions(inventory=entities)

nodes = {}
applications = {}
classes = {}
Expand Down
3 changes: 3 additions & 0 deletions reclass/datatypes/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ def _set_parameters(self, parameters):
'instance of type %s' % type(parameters))
self._parameters = parameters

def expand_functions(self, inventory):
self.parameters.interpolate_functions(inventory)

def merge(self, other):
self._classes.merge_unique(other._classes)
self._applications.merge_unique(other._applications)
Expand Down
50 changes: 33 additions & 17 deletions reclass/datatypes/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@

from reclass.defaults import PARAMETER_INTERPOLATION_DELIMITER
from reclass.utils.dictpath import DictPath
from reclass.utils.refvalue import RefValue
from reclass.errors import InfiniteRecursionError, UndefinedVariableError
from reclass.utils.refvalue import (ReferenceStringParameter, ReferenceParameter,
ReferenceFunction, ReferenceStringFunction)
from reclass.errors import (InfiniteRecursionError, UndefinedVariableError,
UndefinedFunctionError)

class Parameters(object):
'''
Expand Down Expand Up @@ -70,37 +72,37 @@ def as_dict(self):
return self._base.copy()

def _update_scalar(self, cur, new, path):
if isinstance(cur, RefValue) and path in self._occurrences:
# If the current value already holds a RefValue, we better forget
if isinstance(cur, ReferenceStringParameter) and path in self._occurrences:
# If the current value already holds a ReferenceStringParameter, we better forget
# the occurrence, or else interpolate() will later overwrite
# unconditionally. If the new value is a RefValue, the occurrence
# unconditionally. If the new value is a ReferenceStringParameter, the occurrence
# will be added again further on
del self._occurrences[path]

if self.delimiter is None or not isinstance(new, (types.StringTypes,
RefValue)):
ReferenceStringParameter)):
# either there is no delimiter defined (and hence no references
# are being used), or the new value is not a string (and hence
# cannot be turned into a RefValue), and not a RefValue. We can
# cannot be turned into a ReferenceStringParameter), and not a ReferenceStringParameter. We can
# shortcut and just return the new scalar
return new

elif isinstance(new, RefValue):
# the new value is (already) a RefValue, so we need not touch it
elif isinstance(new, ReferenceStringParameter):
# the new value is (already) a ReferenceStringParameter, so we need not touch it
# at all
ret = new

else:
# the new value is a string, let's see if it contains references,
# by way of wrapping it in a RefValue and querying the result
ret = RefValue(new, self.delimiter)
# by way of wrapping it in a ReferenceStringParameter and querying the result
ret = ReferenceStringParameter(new, self.delimiter)
if not ret.has_references():
# do not replace with RefValue instance if there are no
# references, i.e. discard the RefValue in ret, just return
# do not replace with ReferenceStringParameter instance if there are no
# references, i.e. discard the ReferenceStringParameter in ret, just return
# the new value
return new

# So we now have a RefValue. Let's, keep a reference to the instance
# So we now have a ReferenceStringParameter. Let's, keep a reference to the instance
# we just created, in a dict indexed by the dictionary path, instead
# of just a list. The keys are required to resolve dependencies during
# interpolation
Expand Down Expand Up @@ -171,6 +173,21 @@ def merge(self, other):
def has_unresolved_refs(self):
return len(self._occurrences) > 0

def interpolate_functions(self, inventory):
self._interpolate_functions_inner(self._base, inventory)

def _interpolate_functions_inner(self, node, inventory):
for k, v in node.items():
if isinstance(v, dict):
self._interpolate_functions_inner(node[k], inventory)
elif isinstance(v, types.StringTypes):
refval = ReferenceStringFunction(v)
if refval.has_references():
try:
node[k] = refval.render(inventory)
except UndefinedFunctionError as e:
raise UndefinedFunctionError(e.var)

def interpolate(self):
while self.has_unresolved_refs():
# we could use a view here, but this is simple enough:
Expand All @@ -182,7 +199,7 @@ def interpolate(self):
def _interpolate_inner(self, path, refvalue):
self._occurrences[path] = True # mark as seen
for ref in refvalue.get_references():
path_from_ref = DictPath(self.delimiter, ref)
path_from_ref = DictPath(self.delimiter, ref.string)
try:
refvalue_inner = self._occurrences[path_from_ref]

Expand All @@ -201,14 +218,13 @@ def _interpolate_inner(self, path, refvalue):
# Therefore, if we encounter True instead of a refvalue,
# it means that we have already processed it and are now
# faced with a cyclical reference.
raise InfiniteRecursionError(path, ref)
raise InfiniteRecursionError(path, ref.string)
self._interpolate_inner(path_from_ref, refvalue_inner)

except KeyError as e:
# not actually an error, but we are done resolving all
# dependencies of the current ref, so move on
continue

try:
new = refvalue.render(self._base)
path.set_value(self._base, new)
Expand Down
2 changes: 2 additions & 0 deletions reclass/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@

PARAMETER_INTERPOLATION_SENTINELS = ('${', '}')
PARAMETER_INTERPOLATION_DELIMITER = ':'

FUNCTION_INTERPOLATION_SENTINELS = ('$<', '>')
13 changes: 12 additions & 1 deletion reclass/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
import posix, sys
import traceback

from reclass.defaults import PARAMETER_INTERPOLATION_SENTINELS
from reclass.defaults import (PARAMETER_INTERPOLATION_SENTINELS,
FUNCTION_INTERPOLATION_SENTINELS)

class ReclassException(Exception):

Expand Down Expand Up @@ -144,6 +145,16 @@ def set_context(self, context):
self._context = context


class UndefinedFunctionError(InterpolationError):
def __init__(self, var):
super(UndefinedFunctionError, self).__init__(msg=None)
self._var = var
var = property(lambda self: self._var)

def _get_message(self):
return "Unknown function in " + self._var.join(FUNCTION_INTERPOLATION_SENTINELS)


class IncompleteInterpolationError(InterpolationError):

def __init__(self, string, end_sentinel):
Expand Down
56 changes: 56 additions & 0 deletions reclass/utils/function.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#
# -*- coding: utf-8 -*-
#
# This file is part of reclass (http://github.com/madduck/reclass)
#
# Copyright © 2007–14 martin f. krafft <madduck@madduck.net>
# Released under the terms of the Artistic Licence 2.0
#

from reclass.errors import UndefinedFunctionError


def get_function(name):
if name == 'print':
return FunctionPrint()
if name == 'aggregate':
return FunctionAggregate()
else:
raise UndefinedFunctionError(name)


class Function(object):
def __init__(self):
pass

def execute(self, *args, **kwargs):
pass


class FunctionPrint(Function):
def __init__(self):
super(FunctionPrint, self).__init__()

def execute(self, inventory, *args):
return " ".join(args)


class FunctionAggregate(Function):
def __init__(self):
super(FunctionAggregate, self).__init__()

def execute(self, inventory, *args):
func_filter, func_extract = args[0:2]
result = {}
matching_hosts = {}
for hostname, hostinfo in inventory.items():
node = hostinfo.parameters.as_dict()
try:
if eval(func_filter):
matching_hosts.update({hostname: node})
for hostname, hostinfo in matching_hosts.items():
expr = func_extract.replace("node", "hostinfo")
result[hostname] = eval(expr)
except KeyError:
raise
return result
Loading