Reactor - node_register

Reactor file, reactor orchestrate file and reactor state file. Reclass module and states updated
with methods to automatically classify node, generate node metadata and cluster level metadata
override.
diff --git a/_modules/reclass.py b/_modules/reclass.py
index ae94541..a79410f 100644
--- a/_modules/reclass.py
+++ b/_modules/reclass.py
@@ -17,6 +17,7 @@
 from reclass import get_storage, output
 from reclass.core import Core
 from reclass.config import find_and_read_configfile
+from string import Template
 
 LOG = logging.getLogger(__name__)
 
@@ -233,6 +234,91 @@
         return {'Error': 'Error in retrieving node'}
 
 
+def _get_node_classes(node_data, class_mapping_fragment):
+    classes = []
+
+    for value_tmpl_string in class_mapping_fragment.get('value_template', []):
+        value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
+        rendered_value = value_tmpl.safe_substitute(node_data)
+        classes.append(rendered_value)
+
+    for value in class_mapping_fragment.get('value', []):
+        classes.append(value)
+
+    return classes
+
+
+def _get_params(node_data, class_mapping_fragment):
+    params = {}
+
+    for param_name, param in class_mapping_fragment.items():
+        value = param.get('value', None)
+        value_tmpl_string = param.get('value_template', None)
+        if value:
+            params.update({param_name: value})
+        elif value_tmpl_string:
+            value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
+            rendered_value = value_tmpl.safe_substitute(node_data)
+            params.update({param_name: rendered_value})
+
+    return params
+
+
+def _validate_condition(node_data, expression_tmpl_string):
+    expression_tmpl = Template(expression_tmpl_string.replace('<<', '${').replace('>>', '}'))
+    expression = expression_tmpl.safe_substitute(node_data)
+
+    if expression and expression == 'all':
+        return True
+    elif expression:
+        val_a = expression.split('__')[0]
+        val_b = expression.split('__')[2]
+        condition = expression.split('__')[1]
+        if condition == 'startswith':
+            return val_a.startswith(val_b)
+        elif condition == 'equals':
+            return val_a == val_b
+
+    return False
+
+
+def node_classify(node_name, node_data={}, class_mapping={}, **kwargs):
+    '''
+    CLassify node by given class_mapping dictionary
+
+    :param node_name: node FQDN
+    :param node_data: dictionary of known informations about the node
+    :param class_mapping: dictionary of classes and parameters, with conditions
+
+    '''
+    # clean node_data
+    node_data = {k: v for (k, v) in node_data.items() if not k.startswith('__')}
+
+    classes = []
+    node_params = {}
+    cluster_params = {}
+    ret = {'node_create': '', 'cluster_param': {}}
+
+    for type_name, node_type in class_mapping.items():
+        valid = _validate_condition(node_data, node_type.get('expression', ''))
+        if valid:
+            gen_classes = _get_node_classes(node_data, node_type.get('node_class', {}))
+            classes = classes + gen_classes
+            gen_node_params = _get_params(node_data, node_type.get('node_param', {}))
+            node_params.update(gen_node_params)
+            gen_cluster_params = _get_params(node_data, node_type.get('cluster_param', {}))
+            cluster_params.update(gen_cluster_params)
+
+    if classes:
+        create_kwargs = {'name': node_name, 'path': '_generated', 'classes': classes, 'parameters': node_params}
+        ret['node_create'] = node_create(**create_kwargs)
+
+    for name, value in cluster_params.items():
+        ret['cluster_param'][name] = cluster_meta_set(name, value)
+
+    return ret
+
+
 def inventory(**connection_args):
     '''
     Get all nodes in inventory and their associated services/roles classification.
diff --git a/_states/reclass.py b/_states/reclass.py
index 8f94b3e..4f9e09e 100644
--- a/_states/reclass.py
+++ b/_states/reclass.py
@@ -79,6 +79,27 @@
     return ret
 
 
+def dynamic_node_present(name, node_data={}, class_mapping={}, **kwargs):
+    '''
+    Classify node, create cluster level overrides and node metadata
+
+    :param name: node FQDN
+    :param node_data: dictionary of known informations about the node
+    :param class_mapping: dictionary of classes and parameters, with conditions
+
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': 'Node "{0}" already exists and it is in correct state'.format(name)}
+
+    classify_ret = __salt__['reclass.node_classify'](name, node_data, class_mapping, **kwargs)
+    ret['comment'] = 'Node "{0}" has been created'.format(name)
+    ret['changes']['Node'] = classify_ret
+
+    return ret
+
+
 def cluster_meta_present(name, value, file_name="overrides.yml", cluster="", **kwargs):
     '''
     Ensures that the cluster metadata entry exists
diff --git a/reclass/orchestrate/reactor/node_register.sls b/reclass/orchestrate/reactor/node_register.sls
new file mode 100644
index 0000000..ec00392
--- /dev/null
+++ b/reclass/orchestrate/reactor/node_register.sls
@@ -0,0 +1,22 @@
+{%- set node_name = salt['pillar.get']('event_originator') %}
+{%- set node_data = salt['pillar.get']('event_data') %}
+
+classify_node_{{ node_name }}:
+  salt.state:
+    - tgt: 'salt:master'
+    - tgt_type: pillar
+    - sls: reclass.reactor_sls.node_register
+    - queue: True
+    - pillar:
+        node_name: {{ node_name }}
+        node_data: {{ node_data }}
+
+regenerate_all_nodes:
+  salt.state:
+    - tgt: 'salt:master'
+    - tgt_type: pillar
+    - sls: reclass.storage.node
+    - queue: True
+    - requires:
+      - salt: classify_node_{{ node_name }}
+
diff --git a/reclass/reactor/node_register.sls b/reclass/reactor/node_register.sls
new file mode 100644
index 0000000..fbe5f46
--- /dev/null
+++ b/reclass/reactor/node_register.sls
@@ -0,0 +1,8 @@
+orchestrate_node_register:
+  runner.state.orchestrate:
+    - mods: reclass.orchestrate.reactor.node_register
+    - queue: True
+    - pillar:
+        event_originator: {{ data.id }}
+        event_data: {{ data.data }}
+
diff --git a/reclass/reactor_sls/node_register.sls b/reclass/reactor_sls/node_register.sls
new file mode 100644
index 0000000..af2031f
--- /dev/null
+++ b/reclass/reactor_sls/node_register.sls
@@ -0,0 +1,10 @@
+{%- set node_name = salt['pillar.get']('node_name') %}
+{%- set node_data = salt['pillar.get']('node_data') %}
+{%- set class_mapping = salt['pillar.get']('reclass:storage:class_mapping') %}
+
+classify_node_{{ node_name }}:
+  reclass.dynamic_node_present:
+    - name: {{ node_name }}
+    - node_data: {{ node_data }}
+    - class_mapping: {{ class_mapping }}
+