blob: a79410fe8c36c903589e366c1dc74619bb1960f5 [file] [log] [blame]
Ales Komarek166cc672016-07-27 14:17:22 +02001# -*- coding: utf-8 -*-
2'''
Ales Komareka4a9f572016-12-03 20:15:50 +01003Module for handling reclass metadata models.
Ales Komarek166cc672016-07-27 14:17:22 +02004
5'''
6
7from __future__ import absolute_import
8
Adam Tengler8a1cf402017-05-16 10:59:35 +00009import io
10import json
Ales Komarek166cc672016-07-27 14:17:22 +020011import logging
12import os
13import sys
Ales Komareka961df42016-11-21 21:50:24 +010014import six
Ales Komarek166cc672016-07-27 14:17:22 +020015import yaml
16
Ales Komareka4a9f572016-12-03 20:15:50 +010017from reclass import get_storage, output
18from reclass.core import Core
19from reclass.config import find_and_read_configfile
Adam Tengler23d965f2017-05-16 19:14:51 +000020from string import Template
Ales Komarek166cc672016-07-27 14:17:22 +020021
Ales Komareka4a9f572016-12-03 20:15:50 +010022LOG = logging.getLogger(__name__)
Ales Komarek166cc672016-07-27 14:17:22 +020023
Ales Komareka961df42016-11-21 21:50:24 +010024
Ales Komarek166cc672016-07-27 14:17:22 +020025def __virtual__():
26 '''
27 Only load this module if reclass
28 is installed on this minion.
29 '''
30 return 'reclass'
31
32
Ales Komareka4a9f572016-12-03 20:15:50 +010033def _get_nodes_dir():
34 defaults = find_and_read_configfile()
35 return os.path.join(defaults.get('inventory_base_uri'), 'nodes')
36
37
38def _get_classes_dir():
39 defaults = find_and_read_configfile()
40 return os.path.join(defaults.get('inventory_base_uri'), 'classes')
Ales Komarek166cc672016-07-27 14:17:22 +020041
42
Adam Tengler8a1cf402017-05-16 10:59:35 +000043def _get_cluster_dir():
44 classes_dir = _get_classes_dir()
45 return os.path.join(classes_dir, 'cluster')
46
47
Adam Tengler805666d2017-05-15 16:01:13 +000048def _get_node_meta(name, cluster="default", environment="prd", classes=None, parameters=None):
49 host_name = name.split('.')[0]
50 domain_name = '.'.join(name.split('.')[1:])
51
52 if classes == None:
53 meta_classes = []
54 else:
55 if isinstance(classes, six.string_types):
56 meta_classes = json.loads(classes)
57 else:
58 meta_classes = classes
59
60 if parameters == None:
61 meta_parameters = {}
62 else:
63 if isinstance(parameters, six.string_types):
64 meta_parameters = json.loads(parameters)
65 else:
66 # generate dict from OrderedDict
67 meta_parameters = {k: v for (k, v) in parameters.items()}
68
69 node_meta = {
70 'classes': meta_classes,
71 'parameters': {
72 '_param': meta_parameters,
73 'linux': {
74 'system': {
75 'name': host_name,
76 'domain': domain_name,
77 'cluster': cluster,
78 'environment': environment,
79 }
80 }
81 }
82 }
83
84 return node_meta
85
86
Ales Komarek166cc672016-07-27 14:17:22 +020087def node_create(name, path=None, cluster="default", environment="prd", classes=None, parameters=None, **kwargs):
88 '''
89 Create a reclass node
90
91 :param name: new node FQDN
92 :param path: custom path in nodes for new node
93 :param classes: classes given to the new node
94 :param parameters: parameters given to the new node
95 :param environment: node's environment
96 :param cluster: node's cluster
97
98 CLI Examples:
99
100 .. code-block:: bash
101
102 salt '*' reclass.node_create server.domain.com classes=[system.neco1, system.neco2]
103 salt '*' reclass.node_create namespace/test enabled=False
104
105 '''
106 ret = {}
107
108 node = node_get(name=name)
109
110 if node and not "Error" in node:
111 LOG.debug("node {0} exists".format(name))
112 ret[name] = node
113 return ret
114
115 host_name = name.split('.')[0]
116 domain_name = '.'.join(name.split('.')[1:])
117
Adam Tengler805666d2017-05-15 16:01:13 +0000118 node_meta = _get_node_meta(name, cluster, environment, classes, parameters)
Ales Komarek166cc672016-07-27 14:17:22 +0200119 LOG.debug(node_meta)
120
121 if path == None:
Ales Komareka4a9f572016-12-03 20:15:50 +0100122 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200123 else:
Ales Komareka4a9f572016-12-03 20:15:50 +0100124 file_path = os.path.join(_get_nodes_dir(), path, name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200125
126 with open(file_path, 'w') as node_file:
Ales Komareka961df42016-11-21 21:50:24 +0100127 node_file.write(yaml.safe_dump(node_meta, default_flow_style=False))
Ales Komarek71f94b02016-07-27 14:48:57 +0200128
Ales Komarek166cc672016-07-27 14:17:22 +0200129 return node_get(name)
130
Ales Komareka4a9f572016-12-03 20:15:50 +0100131
Ales Komarek166cc672016-07-27 14:17:22 +0200132def node_delete(name, **kwargs):
133 '''
134 Delete a reclass node
135
136 :params node: Node name
137
138 CLI Examples:
139
140 .. code-block:: bash
141
142 salt '*' reclass.node_delete demo01.domain.com
143 salt '*' reclass.node_delete name=demo01.domain.com
144 '''
145
146 node = node_get(name=name)
147
148 if 'Error' in node:
149 return {'Error': 'Unable to retreive node'}
150
151 if node[name]['path'] == '':
Ales Komareka4a9f572016-12-03 20:15:50 +0100152 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200153 else:
Ales Komareka4a9f572016-12-03 20:15:50 +0100154 file_path = os.path.join(_get_nodes_dir(), node[name]['path'], name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200155
156 os.remove(file_path)
157
158 ret = 'Node {0} deleted'.format(name)
159
160 return ret
161
162
163def node_get(name, path=None, **kwargs):
164 '''
165 Return a specific node
166
167 CLI Examples:
168
169 .. code-block:: bash
170
171 salt '*' reclass.node_get host01.domain.com
172 salt '*' reclass.node_get name=host02.domain.com
173 '''
174 ret = {}
175 nodes = node_list(**kwargs)
176
177 if not name in nodes:
178 return {'Error': 'Error in retrieving node'}
179 ret[name] = nodes[name]
180 return ret
181
182
183def node_list(**connection_args):
184 '''
185 Return a list of available nodes
186
187 CLI Example:
188
189 .. code-block:: bash
190
191 salt '*' reclass.node_list
192 '''
193 ret = {}
194
Ales Komareka4a9f572016-12-03 20:15:50 +0100195 for root, sub_folders, files in os.walk(_get_nodes_dir()):
Adam Tengler805666d2017-05-15 16:01:13 +0000196 for fl in files:
197 file_path = os.path.join(root, fl)
198 with open(file_path, 'r') as file_handle:
199 file_read = yaml.load(file_handle.read())
200 file_data = file_read or {}
201 classes = file_data.get('classes', [])
202 parameters = file_data.get('parameters', {}).get('_param', [])
203 name = fl.replace('.yml', '')
Ales Komarek166cc672016-07-27 14:17:22 +0200204 host_name = name.split('.')[0]
205 domain_name = '.'.join(name.split('.')[1:])
Ales Komareka4a9f572016-12-03 20:15:50 +0100206 path = root.replace(_get_nodes_dir()+'/', '')
Ales Komarek166cc672016-07-27 14:17:22 +0200207 ret[name] = {
Ales Komareka4a9f572016-12-03 20:15:50 +0100208 'name': host_name,
209 'domain': domain_name,
210 'cluster': 'default',
211 'environment': 'prd',
212 'path': path,
213 'classes': classes,
214 'parameters': parameters
Ales Komarek166cc672016-07-27 14:17:22 +0200215 }
216
217 return ret
218
Ales Komareka4a9f572016-12-03 20:15:50 +0100219
Ales Komarek166cc672016-07-27 14:17:22 +0200220def node_update(name, classes=None, parameters=None, **connection_args):
221 '''
222 Update a node metadata information, classes and parameters.
223
224 CLI Examples:
225
226 .. code-block:: bash
227
228 salt '*' reclass.node_update name=nodename classes="[clas1, class2]"
229 '''
230 node = node_get(name=name)
231 if not node.has_key('Error'):
Ales Komarek71f94b02016-07-27 14:48:57 +0200232 node = node[name.split("/")[1]]
233 else:
234 return {'Error': 'Error in retrieving node'}
Ales Komareka4a9f572016-12-03 20:15:50 +0100235
236
Adam Tengler23d965f2017-05-16 19:14:51 +0000237def _get_node_classes(node_data, class_mapping_fragment):
238 classes = []
239
240 for value_tmpl_string in class_mapping_fragment.get('value_template', []):
241 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
242 rendered_value = value_tmpl.safe_substitute(node_data)
243 classes.append(rendered_value)
244
245 for value in class_mapping_fragment.get('value', []):
246 classes.append(value)
247
248 return classes
249
250
251def _get_params(node_data, class_mapping_fragment):
252 params = {}
253
254 for param_name, param in class_mapping_fragment.items():
255 value = param.get('value', None)
256 value_tmpl_string = param.get('value_template', None)
257 if value:
258 params.update({param_name: value})
259 elif value_tmpl_string:
260 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
261 rendered_value = value_tmpl.safe_substitute(node_data)
262 params.update({param_name: rendered_value})
263
264 return params
265
266
267def _validate_condition(node_data, expression_tmpl_string):
268 expression_tmpl = Template(expression_tmpl_string.replace('<<', '${').replace('>>', '}'))
269 expression = expression_tmpl.safe_substitute(node_data)
270
271 if expression and expression == 'all':
272 return True
273 elif expression:
274 val_a = expression.split('__')[0]
275 val_b = expression.split('__')[2]
276 condition = expression.split('__')[1]
277 if condition == 'startswith':
278 return val_a.startswith(val_b)
279 elif condition == 'equals':
280 return val_a == val_b
281
282 return False
283
284
285def node_classify(node_name, node_data={}, class_mapping={}, **kwargs):
286 '''
287 CLassify node by given class_mapping dictionary
288
289 :param node_name: node FQDN
290 :param node_data: dictionary of known informations about the node
291 :param class_mapping: dictionary of classes and parameters, with conditions
292
293 '''
294 # clean node_data
295 node_data = {k: v for (k, v) in node_data.items() if not k.startswith('__')}
296
297 classes = []
298 node_params = {}
299 cluster_params = {}
300 ret = {'node_create': '', 'cluster_param': {}}
301
302 for type_name, node_type in class_mapping.items():
303 valid = _validate_condition(node_data, node_type.get('expression', ''))
304 if valid:
305 gen_classes = _get_node_classes(node_data, node_type.get('node_class', {}))
306 classes = classes + gen_classes
307 gen_node_params = _get_params(node_data, node_type.get('node_param', {}))
308 node_params.update(gen_node_params)
309 gen_cluster_params = _get_params(node_data, node_type.get('cluster_param', {}))
310 cluster_params.update(gen_cluster_params)
311
312 if classes:
313 create_kwargs = {'name': node_name, 'path': '_generated', 'classes': classes, 'parameters': node_params}
314 ret['node_create'] = node_create(**create_kwargs)
315
316 for name, value in cluster_params.items():
317 ret['cluster_param'][name] = cluster_meta_set(name, value)
318
319 return ret
320
321
Ales Komareka4a9f572016-12-03 20:15:50 +0100322def inventory(**connection_args):
323 '''
324 Get all nodes in inventory and their associated services/roles classification.
325
326 CLI Examples:
327
328 .. code-block:: bash
329
330 salt '*' reclass.inventory
331 '''
332 defaults = find_and_read_configfile()
333 storage = get_storage(defaults['storage_type'], _get_nodes_dir(), _get_classes_dir())
334 reclass = Core(storage, None)
335 nodes = reclass.inventory()["nodes"]
336 output = {}
337
338 for node in nodes:
339 service_classification = []
340 role_classification = []
341 for service in nodes[node]['parameters']:
342 if service not in ['_param', 'private_keys', 'public_keys', 'known_hosts']:
343 service_classification.append(service)
344 for role in nodes[node]['parameters'][service]:
345 if role not in ['_support', '_orchestrate', 'common']:
346 role_classification.append('%s.%s' % (service, role))
347 output[node] = {
348 'roles': role_classification,
349 'services': service_classification,
350 }
351 return output
Adam Tengler8a1cf402017-05-16 10:59:35 +0000352
353
354def cluster_meta_list(file_name="overrides.yml", cluster="", **kwargs):
355 path = os.path.join(_get_cluster_dir(), cluster, file_name)
356 try:
357 with io.open(path, 'r') as file_handle:
358 meta_yaml = yaml.safe_load(file_handle.read())
359 meta = meta_yaml or {}
360 except Exception as e:
361 msg = "Unable to load cluster metadata YAML %s: %s" % (path, repr(e))
362 LOG.debug(msg)
363 meta = {'Error': msg}
364 return meta
365
366
367def cluster_meta_delete(name, file_name="overrides.yml", cluster="", **kwargs):
368 ret = {}
369 path = os.path.join(_get_cluster_dir(), cluster, file_name)
370 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
371 if 'Error' not in meta:
372 metadata = meta.get('parameters', {}).get('_param', {})
373 if name not in metadata:
374 return ret
375 del metadata[name]
376 try:
377 with io.open(path, 'w') as file_handle:
378 file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
379 except Exception as e:
380 msg = "Unable to save cluster metadata YAML: %s" % repr(e)
381 LOG.error(msg)
382 return {'Error': msg}
383 ret = 'Cluster metadata entry {0} deleted'.format(name)
384 return ret
385
386
387def cluster_meta_set(name, value, file_name="overrides.yml", cluster="", **kwargs):
388 path = os.path.join(_get_cluster_dir(), cluster, file_name)
389 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
390 if 'Error' not in meta:
391 if not meta:
392 meta = {'parameters': {'_param': {}}}
393 metadata = meta.get('parameters', {}).get('_param', {})
394 if name in metadata and metadata[name] == value:
395 return {name: 'Cluster metadata entry %s already exists and is in correct state' % name}
396 metadata.update({name: value})
397 try:
398 with io.open(path, 'w') as file_handle:
399 file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
400 except Exception as e:
401 msg = "Unable to save cluster metadata YAML %s: %s" % (path, repr(e))
402 LOG.error(msg)
403 return {'Error': msg}
404 return cluster_meta_get(name, path, **kwargs)
405 return meta
406
407
408def cluster_meta_get(name, file_name="overrides.yml", cluster="", **kwargs):
409 ret = {}
410 path = os.path.join(_get_cluster_dir(), cluster, file_name)
411 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
412 metadata = meta.get('parameters', {}).get('_param', {})
413 if 'Error' in meta:
414 ret['Error'] = meta['Error']
415 elif name in metadata:
416 ret[name] = metadata.get(name)
417
418 return ret
419