blob: a79410fe8c36c903589e366c1dc74619bb1960f5 [file] [log] [blame]
# -*- coding: utf-8 -*-
'''
Module for handling reclass metadata models.
'''
from __future__ import absolute_import
import io
import json
import logging
import os
import sys
import six
import yaml
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__)
def __virtual__():
'''
Only load this module if reclass
is installed on this minion.
'''
return 'reclass'
def _get_nodes_dir():
defaults = find_and_read_configfile()
return os.path.join(defaults.get('inventory_base_uri'), 'nodes')
def _get_classes_dir():
defaults = find_and_read_configfile()
return os.path.join(defaults.get('inventory_base_uri'), 'classes')
def _get_cluster_dir():
classes_dir = _get_classes_dir()
return os.path.join(classes_dir, 'cluster')
def _get_node_meta(name, cluster="default", environment="prd", classes=None, parameters=None):
host_name = name.split('.')[0]
domain_name = '.'.join(name.split('.')[1:])
if classes == None:
meta_classes = []
else:
if isinstance(classes, six.string_types):
meta_classes = json.loads(classes)
else:
meta_classes = classes
if parameters == None:
meta_parameters = {}
else:
if isinstance(parameters, six.string_types):
meta_parameters = json.loads(parameters)
else:
# generate dict from OrderedDict
meta_parameters = {k: v for (k, v) in parameters.items()}
node_meta = {
'classes': meta_classes,
'parameters': {
'_param': meta_parameters,
'linux': {
'system': {
'name': host_name,
'domain': domain_name,
'cluster': cluster,
'environment': environment,
}
}
}
}
return node_meta
def node_create(name, path=None, cluster="default", environment="prd", classes=None, parameters=None, **kwargs):
'''
Create a reclass node
:param name: new node FQDN
:param path: custom path in nodes for new node
:param classes: classes given to the new node
:param parameters: parameters given to the new node
:param environment: node's environment
:param cluster: node's cluster
CLI Examples:
.. code-block:: bash
salt '*' reclass.node_create server.domain.com classes=[system.neco1, system.neco2]
salt '*' reclass.node_create namespace/test enabled=False
'''
ret = {}
node = node_get(name=name)
if node and not "Error" in node:
LOG.debug("node {0} exists".format(name))
ret[name] = node
return ret
host_name = name.split('.')[0]
domain_name = '.'.join(name.split('.')[1:])
node_meta = _get_node_meta(name, cluster, environment, classes, parameters)
LOG.debug(node_meta)
if path == None:
file_path = os.path.join(_get_nodes_dir(), name + '.yml')
else:
file_path = os.path.join(_get_nodes_dir(), path, name + '.yml')
with open(file_path, 'w') as node_file:
node_file.write(yaml.safe_dump(node_meta, default_flow_style=False))
return node_get(name)
def node_delete(name, **kwargs):
'''
Delete a reclass node
:params node: Node name
CLI Examples:
.. code-block:: bash
salt '*' reclass.node_delete demo01.domain.com
salt '*' reclass.node_delete name=demo01.domain.com
'''
node = node_get(name=name)
if 'Error' in node:
return {'Error': 'Unable to retreive node'}
if node[name]['path'] == '':
file_path = os.path.join(_get_nodes_dir(), name + '.yml')
else:
file_path = os.path.join(_get_nodes_dir(), node[name]['path'], name + '.yml')
os.remove(file_path)
ret = 'Node {0} deleted'.format(name)
return ret
def node_get(name, path=None, **kwargs):
'''
Return a specific node
CLI Examples:
.. code-block:: bash
salt '*' reclass.node_get host01.domain.com
salt '*' reclass.node_get name=host02.domain.com
'''
ret = {}
nodes = node_list(**kwargs)
if not name in nodes:
return {'Error': 'Error in retrieving node'}
ret[name] = nodes[name]
return ret
def node_list(**connection_args):
'''
Return a list of available nodes
CLI Example:
.. code-block:: bash
salt '*' reclass.node_list
'''
ret = {}
for root, sub_folders, files in os.walk(_get_nodes_dir()):
for fl in files:
file_path = os.path.join(root, fl)
with open(file_path, 'r') as file_handle:
file_read = yaml.load(file_handle.read())
file_data = file_read or {}
classes = file_data.get('classes', [])
parameters = file_data.get('parameters', {}).get('_param', [])
name = fl.replace('.yml', '')
host_name = name.split('.')[0]
domain_name = '.'.join(name.split('.')[1:])
path = root.replace(_get_nodes_dir()+'/', '')
ret[name] = {
'name': host_name,
'domain': domain_name,
'cluster': 'default',
'environment': 'prd',
'path': path,
'classes': classes,
'parameters': parameters
}
return ret
def node_update(name, classes=None, parameters=None, **connection_args):
'''
Update a node metadata information, classes and parameters.
CLI Examples:
.. code-block:: bash
salt '*' reclass.node_update name=nodename classes="[clas1, class2]"
'''
node = node_get(name=name)
if not node.has_key('Error'):
node = node[name.split("/")[1]]
else:
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.
CLI Examples:
.. code-block:: bash
salt '*' reclass.inventory
'''
defaults = find_and_read_configfile()
storage = get_storage(defaults['storage_type'], _get_nodes_dir(), _get_classes_dir())
reclass = Core(storage, None)
nodes = reclass.inventory()["nodes"]
output = {}
for node in nodes:
service_classification = []
role_classification = []
for service in nodes[node]['parameters']:
if service not in ['_param', 'private_keys', 'public_keys', 'known_hosts']:
service_classification.append(service)
for role in nodes[node]['parameters'][service]:
if role not in ['_support', '_orchestrate', 'common']:
role_classification.append('%s.%s' % (service, role))
output[node] = {
'roles': role_classification,
'services': service_classification,
}
return output
def cluster_meta_list(file_name="overrides.yml", cluster="", **kwargs):
path = os.path.join(_get_cluster_dir(), cluster, file_name)
try:
with io.open(path, 'r') as file_handle:
meta_yaml = yaml.safe_load(file_handle.read())
meta = meta_yaml or {}
except Exception as e:
msg = "Unable to load cluster metadata YAML %s: %s" % (path, repr(e))
LOG.debug(msg)
meta = {'Error': msg}
return meta
def cluster_meta_delete(name, file_name="overrides.yml", cluster="", **kwargs):
ret = {}
path = os.path.join(_get_cluster_dir(), cluster, file_name)
meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
if 'Error' not in meta:
metadata = meta.get('parameters', {}).get('_param', {})
if name not in metadata:
return ret
del metadata[name]
try:
with io.open(path, 'w') as file_handle:
file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
except Exception as e:
msg = "Unable to save cluster metadata YAML: %s" % repr(e)
LOG.error(msg)
return {'Error': msg}
ret = 'Cluster metadata entry {0} deleted'.format(name)
return ret
def cluster_meta_set(name, value, file_name="overrides.yml", cluster="", **kwargs):
path = os.path.join(_get_cluster_dir(), cluster, file_name)
meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
if 'Error' not in meta:
if not meta:
meta = {'parameters': {'_param': {}}}
metadata = meta.get('parameters', {}).get('_param', {})
if name in metadata and metadata[name] == value:
return {name: 'Cluster metadata entry %s already exists and is in correct state' % name}
metadata.update({name: value})
try:
with io.open(path, 'w') as file_handle:
file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
except Exception as e:
msg = "Unable to save cluster metadata YAML %s: %s" % (path, repr(e))
LOG.error(msg)
return {'Error': msg}
return cluster_meta_get(name, path, **kwargs)
return meta
def cluster_meta_get(name, file_name="overrides.yml", cluster="", **kwargs):
ret = {}
path = os.path.join(_get_cluster_dir(), cluster, file_name)
meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
metadata = meta.get('parameters', {}).get('_param', {})
if 'Error' in meta:
ret['Error'] = meta['Error']
elif name in metadata:
ret[name] = metadata.get(name)
return ret