blob: c1a118cc277755655fae596bfdf2996e3921d1d2 [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
Adam Tengler2b362622017-06-01 14:23:45 +000013import socket
Ales Komarek166cc672016-07-27 14:17:22 +020014import sys
Ales Komareka961df42016-11-21 21:50:24 +010015import six
Ales Komarek166cc672016-07-27 14:17:22 +020016import yaml
Jiri Broulik7ccb5342017-07-20 17:07:47 +020017import re
Ales Komarek166cc672016-07-27 14:17:22 +020018
Ales Komarekb0911892017-08-02 15:47:30 +020019import urlparse
20
Ales Komareka4a9f572016-12-03 20:15:50 +010021from reclass import get_storage, output
Adam Tengler2b362622017-06-01 14:23:45 +000022from reclass.adapters.salt import ext_pillar
Ales Komareka4a9f572016-12-03 20:15:50 +010023from reclass.core import Core
24from reclass.config import find_and_read_configfile
Adam Tengler23d965f2017-05-16 19:14:51 +000025from string import Template
Jiri Broulik7ccb5342017-07-20 17:07:47 +020026from reclass.errors import ReclassException
27
Ales Komarek166cc672016-07-27 14:17:22 +020028
Ales Komareka4a9f572016-12-03 20:15:50 +010029LOG = logging.getLogger(__name__)
Ales Komarek166cc672016-07-27 14:17:22 +020030
Ales Komareka961df42016-11-21 21:50:24 +010031
Ales Komarek166cc672016-07-27 14:17:22 +020032def __virtual__():
33 '''
34 Only load this module if reclass
35 is installed on this minion.
36 '''
37 return 'reclass'
38
39
Jiri Broulik7ccb5342017-07-20 17:07:47 +020040def _deps(ret_classes=True, ret_errors=False):
41 '''
42 Returns classes if ret_classes=True, else returns soft_params if ret_classes=False
43 '''
44 defaults = find_and_read_configfile()
45 path = defaults.get('inventory_base_uri')
46 classes = {}
47 soft_params = {}
48 errors = []
49
50 # find classes
51 for root, dirs, files in os.walk(path):
Petr Michalec46a5bad2017-09-18 20:11:43 +020052 # skip hidden files and folders in reclass dir
53 files = [f for f in files if not f[0] == '.']
54 dirs[:] = [d for d in dirs if not d[0] == '.']
55 # translate found init.yml to valid class name
Jiri Broulik7ccb5342017-07-20 17:07:47 +020056 if 'init.yml' in files:
57 class_file = root + '/' + 'init.yml'
58 class_name = class_file.replace(path, '')[:-9].replace('/', '.')
59 classes[class_name] = {'file': class_file}
60
61 for f in files:
62 if f.endswith('.yml') and f != 'init.yml':
63 class_file = root + '/' + f
64 class_name = class_file.replace(path, '')[:-4].replace('/', '.')
65 classes[class_name] = {'file': class_file}
66
67 # read classes
68 for class_name, params in classes.items():
69 with open(params['file'], 'r') as f:
70 # read raw data
71 raw = f.read()
72 pr = re.findall('\${_param:(.*?)}', raw)
73 if pr:
74 params['params_required'] = list(set(pr))
75
76 # load yaml
77 try:
78 data = yaml.load(raw)
79 except yaml.scanner.ScannerError as e:
80 errors.append(params['file'] + ' ' + str(e))
81 pass
82
83 if type(data) == dict:
84 if data.get('classes'):
85 params['includes'] = data.get('classes', [])
86 if data.get('parameters') and data['parameters'].get('_param'):
87 params['params_created'] = data['parameters']['_param']
88
89 if not(data.get('classes') or data.get('parameters')):
90 errors.append(params['file'] + ' ' + 'file missing classes and parameters')
91 else:
92 errors.append(params['file'] + ' ' + 'is not valid yaml')
93
94 if ret_classes:
95 return classes
96 elif ret_errors:
97 return errors
98
99 # find parameters and its usage
100 for class_name, params in classes.items():
101 for pn, pv in params.get('params_created', {}).items():
102 # create param if missing
103 if pn not in soft_params:
104 soft_params[pn] = {'created_at': {}, 'required_at': []}
105
106 # add created_at
107 if class_name not in soft_params[pn]['created_at']:
108 soft_params[pn]['created_at'][class_name] = pv
109
110 for pn in params.get('params_required', []):
111 # create param if missing
112 if pn not in soft_params:
113 soft_params[pn] = {'created_at': {}, 'required_at': []}
114
115 # add created_at
116 soft_params[pn]['required_at'].append(class_name)
117
118 return soft_params
119
120
Ales Komareka4a9f572016-12-03 20:15:50 +0100121def _get_nodes_dir():
122 defaults = find_and_read_configfile()
Vladislav Naumov87113082017-07-24 17:36:50 +0300123 return defaults.get('nodes_uri') or \
124 os.path.join(defaults.get('inventory_base_uri'), 'nodes')
Ales Komareka4a9f572016-12-03 20:15:50 +0100125
126
127def _get_classes_dir():
128 defaults = find_and_read_configfile()
129 return os.path.join(defaults.get('inventory_base_uri'), 'classes')
Ales Komarek166cc672016-07-27 14:17:22 +0200130
131
Adam Tengler8a1cf402017-05-16 10:59:35 +0000132def _get_cluster_dir():
133 classes_dir = _get_classes_dir()
134 return os.path.join(classes_dir, 'cluster')
135
136
Adam Tengler805666d2017-05-15 16:01:13 +0000137def _get_node_meta(name, cluster="default", environment="prd", classes=None, parameters=None):
138 host_name = name.split('.')[0]
139 domain_name = '.'.join(name.split('.')[1:])
140
141 if classes == None:
142 meta_classes = []
143 else:
144 if isinstance(classes, six.string_types):
145 meta_classes = json.loads(classes)
146 else:
147 meta_classes = classes
148
149 if parameters == None:
150 meta_parameters = {}
151 else:
152 if isinstance(parameters, six.string_types):
153 meta_parameters = json.loads(parameters)
154 else:
155 # generate dict from OrderedDict
156 meta_parameters = {k: v for (k, v) in parameters.items()}
157
158 node_meta = {
159 'classes': meta_classes,
160 'parameters': {
161 '_param': meta_parameters,
162 'linux': {
163 'system': {
164 'name': host_name,
165 'domain': domain_name,
166 'cluster': cluster,
167 'environment': environment,
168 }
169 }
170 }
171 }
172
173 return node_meta
174
175
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200176def soft_meta_list():
177 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200178 Returns all defined soft metadata parameters.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200179
180 CLI Examples:
181
182 .. code-block:: bash
183
184 salt '*' reclass.soft_meta_list
185 '''
186 return _deps(ret_classes=False)
187
188
189def class_list():
190 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200191 Returns list of all classes defined within reclass inventory.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200192
193 CLI Examples:
194
195 .. code-block:: bash
196
197 salt '*' reclass.class_list
198 '''
199 return _deps(ret_classes=True)
200
201
202def soft_meta_get(name):
203 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200204 Returns single soft metadata parameter.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200205
Ales Komarekb0911892017-08-02 15:47:30 +0200206 :param name: expects the following format: apt_mk_version
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200207
208 CLI Examples:
209
210 .. code-block:: bash
211
Ales Komarekb0911892017-08-02 15:47:30 +0200212 salt '*' reclass.soft_meta_get openstack_version
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200213 '''
214 soft_params = _deps(ret_classes=False)
215
216 if name in soft_params:
Ales Komarekb0911892017-08-02 15:47:30 +0200217 return {name: soft_params.get(name)}
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200218 else:
Ales Komarekb0911892017-08-02 15:47:30 +0200219 return {'Error': 'No param {0} found'.format(name)}
220
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200221
222def class_get(name):
223 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200224 Returns detailes information about class file in reclass inventory.
225
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200226 :param name: expects the following format classes.system.linux.repo
227
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200228 CLI Examples:
229
230 .. code-block:: bash
231
232 salt '*' reclass.class_get classes.system.linux.repo
233 '''
234 classes = _deps(ret_classes=True)
235 tmp_name = '.' + name
236 if tmp_name in classes:
Ales Komarekb0911892017-08-02 15:47:30 +0200237 return {name: classes.get(tmp_name)}
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200238 else:
Ales Komarekb0911892017-08-02 15:47:30 +0200239 return {'Error': 'No class {0} found'.format(name)}
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200240
241
Ales Komarek166cc672016-07-27 14:17:22 +0200242def node_create(name, path=None, cluster="default", environment="prd", classes=None, parameters=None, **kwargs):
243 '''
244 Create a reclass node
245
246 :param name: new node FQDN
247 :param path: custom path in nodes for new node
248 :param classes: classes given to the new node
249 :param parameters: parameters given to the new node
250 :param environment: node's environment
251 :param cluster: node's cluster
252
253 CLI Examples:
254
255 .. code-block:: bash
256
257 salt '*' reclass.node_create server.domain.com classes=[system.neco1, system.neco2]
258 salt '*' reclass.node_create namespace/test enabled=False
Petr Michalec46a5bad2017-09-18 20:11:43 +0200259
Ales Komarek166cc672016-07-27 14:17:22 +0200260 '''
261 ret = {}
262
263 node = node_get(name=name)
264
265 if node and not "Error" in node:
266 LOG.debug("node {0} exists".format(name))
267 ret[name] = node
268 return ret
269
270 host_name = name.split('.')[0]
271 domain_name = '.'.join(name.split('.')[1:])
272
Adam Tengler805666d2017-05-15 16:01:13 +0000273 node_meta = _get_node_meta(name, cluster, environment, classes, parameters)
Ales Komarek166cc672016-07-27 14:17:22 +0200274 LOG.debug(node_meta)
275
276 if path == None:
Ales Komareka4a9f572016-12-03 20:15:50 +0100277 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200278 else:
Ales Komareka4a9f572016-12-03 20:15:50 +0100279 file_path = os.path.join(_get_nodes_dir(), path, name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200280
281 with open(file_path, 'w') as node_file:
Ales Komareka961df42016-11-21 21:50:24 +0100282 node_file.write(yaml.safe_dump(node_meta, default_flow_style=False))
Ales Komarek71f94b02016-07-27 14:48:57 +0200283
Ales Komarek166cc672016-07-27 14:17:22 +0200284 return node_get(name)
285
Ales Komareka4a9f572016-12-03 20:15:50 +0100286
Ales Komarek166cc672016-07-27 14:17:22 +0200287def node_delete(name, **kwargs):
288 '''
289 Delete a reclass node
290
291 :params node: Node name
292
293 CLI Examples:
294
295 .. code-block:: bash
296
297 salt '*' reclass.node_delete demo01.domain.com
298 salt '*' reclass.node_delete name=demo01.domain.com
299 '''
300
301 node = node_get(name=name)
302
303 if 'Error' in node:
304 return {'Error': 'Unable to retreive node'}
305
306 if node[name]['path'] == '':
Ales Komareka4a9f572016-12-03 20:15:50 +0100307 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200308 else:
Ales Komareka4a9f572016-12-03 20:15:50 +0100309 file_path = os.path.join(_get_nodes_dir(), node[name]['path'], name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200310
311 os.remove(file_path)
312
313 ret = 'Node {0} deleted'.format(name)
314
315 return ret
316
317
318def node_get(name, path=None, **kwargs):
319 '''
320 Return a specific node
321
322 CLI Examples:
323
324 .. code-block:: bash
325
326 salt '*' reclass.node_get host01.domain.com
327 salt '*' reclass.node_get name=host02.domain.com
328 '''
329 ret = {}
330 nodes = node_list(**kwargs)
331
332 if not name in nodes:
333 return {'Error': 'Error in retrieving node'}
334 ret[name] = nodes[name]
335 return ret
336
337
338def node_list(**connection_args):
339 '''
340 Return a list of available nodes
341
342 CLI Example:
343
344 .. code-block:: bash
345
346 salt '*' reclass.node_list
347 '''
348 ret = {}
349
Ales Komareka4a9f572016-12-03 20:15:50 +0100350 for root, sub_folders, files in os.walk(_get_nodes_dir()):
Petr Michalec46a5bad2017-09-18 20:11:43 +0200351 # skip hidden files and folders in reclass dir
352 files = [f for f in files if not f[0] == '.']
Petr Michalec55a43322017-09-19 17:49:56 +0200353 sub_folders[:] = [d for d in sub_folders if not d[0] == '.']
Adam Tengler805666d2017-05-15 16:01:13 +0000354 for fl in files:
355 file_path = os.path.join(root, fl)
356 with open(file_path, 'r') as file_handle:
357 file_read = yaml.load(file_handle.read())
358 file_data = file_read or {}
359 classes = file_data.get('classes', [])
360 parameters = file_data.get('parameters', {}).get('_param', [])
361 name = fl.replace('.yml', '')
Ales Komarek166cc672016-07-27 14:17:22 +0200362 host_name = name.split('.')[0]
363 domain_name = '.'.join(name.split('.')[1:])
Ales Komareka4a9f572016-12-03 20:15:50 +0100364 path = root.replace(_get_nodes_dir()+'/', '')
Ales Komarek166cc672016-07-27 14:17:22 +0200365 ret[name] = {
Ales Komareka4a9f572016-12-03 20:15:50 +0100366 'name': host_name,
367 'domain': domain_name,
368 'cluster': 'default',
369 'environment': 'prd',
370 'path': path,
371 'classes': classes,
372 'parameters': parameters
Ales Komarek166cc672016-07-27 14:17:22 +0200373 }
374
375 return ret
376
Ales Komareka4a9f572016-12-03 20:15:50 +0100377
Adam Tengler2b362622017-06-01 14:23:45 +0000378def _is_valid_ipv4_address(address):
379 try:
380 socket.inet_pton(socket.AF_INET, address)
381 except AttributeError:
382 try:
383 socket.inet_aton(address)
384 except socket.error:
385 return False
386 return address.count('.') == 3
387 except socket.error:
388 return False
389 return True
390
391
392def _is_valid_ipv6_address(address):
393 try:
394 socket.inet_pton(socket.AF_INET6, address)
395 except socket.error:
396 return False
397 return True
398
399
Adam Tengler1f7667b2017-06-06 16:45:51 +0000400def _get_grains(*args, **kwargs):
401 res = __salt__['saltutil.cmd'](tgt='*',
402 fun='grains.item',
403 arg=args,
404 **{'timeout': 10})
405 return res or {}
406
407
408def _guess_host_from_target(network_grains, host, domain=' '):
Adam Tengler2b362622017-06-01 14:23:45 +0000409 '''
410 Guess minion ID from given host and domain arguments. Host argument can contain
411 hostname, FQDN, IPv4 or IPv6 addresses.
412 '''
Adam Tengler1f7667b2017-06-06 16:45:51 +0000413 key = None
414 value = None
415
Adam Tengler2b362622017-06-01 14:23:45 +0000416 if _is_valid_ipv4_address(host):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000417 key = 'ipv4'
418 value = host
Adam Tengler2b362622017-06-01 14:23:45 +0000419 elif _is_valid_ipv6_address(host):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000420 key = 'ipv6'
421 value = host
Adam Tengler2b362622017-06-01 14:23:45 +0000422 elif host.endswith(domain):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000423 key = 'fqdn'
424 value = host
Adam Tengler2b362622017-06-01 14:23:45 +0000425 else:
Adam Tengler1f7667b2017-06-06 16:45:51 +0000426 key = 'fqdn'
427 value = '%s.%s' % (host, domain)
Adam Tengler2b362622017-06-01 14:23:45 +0000428
Adam Tengler1f7667b2017-06-06 16:45:51 +0000429 target = None
430 if network_grains and isinstance(network_grains, dict) and key and value:
431 for minion, grains in network_grains.items():
432 if grains.get('retcode', 1) == 0 and value in grains.get('ret', {}).get(key, ''):
433 target = minion
Adam Tengler2b362622017-06-01 14:23:45 +0000434
Adam Tengler1f7667b2017-06-06 16:45:51 +0000435 return target or host
Adam Tengler2b362622017-06-01 14:23:45 +0000436
437
438def _interpolate_graph_data(graph_data, **kwargs):
439 new_nodes = []
Adam Tengler1f7667b2017-06-06 16:45:51 +0000440 network_grains = _get_grains('ipv4', 'ipv6', 'fqdn')
Adam Tengler2b362622017-06-01 14:23:45 +0000441 for node in graph_data:
Adam Tengler69c7ba92017-06-01 15:59:01 +0000442 if not node.get('relations', []):
443 node['relations'] = []
Adam Tengler2b362622017-06-01 14:23:45 +0000444 for relation in node.get('relations', []):
Adam Tengler12a310d2017-06-05 19:11:29 +0000445 if not relation.get('status', None):
446 relation['status'] = 'unknown'
Adam Tengler2b362622017-06-01 14:23:45 +0000447 if relation.get('host_from_target', None):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000448 host = _guess_host_from_target(network_grains, relation.pop('host_from_target'))
Adam Tengler2b362622017-06-01 14:23:45 +0000449 relation['host'] = host
450 if relation.get('host_external', None):
Ales Komarekb0911892017-08-02 15:47:30 +0200451 parsed_host_external = [urlparse.urlparse(item).netloc
Adam Tengler69c7ba92017-06-01 15:59:01 +0000452 for item
453 in relation.get('host_external', '').split(' ')
Ales Komarekb0911892017-08-02 15:47:30 +0200454 if urlparse.urlparse(item).netloc]
Adam Tengler69c7ba92017-06-01 15:59:01 +0000455 service = parsed_host_external[0] if parsed_host_external else ''
Adam Tengler2b362622017-06-01 14:23:45 +0000456 host = relation.get('service', '')
457 relation['host'] = host
Adam Tengler69c7ba92017-06-01 15:59:01 +0000458 del relation['host_external']
Adam Tengler2b362622017-06-01 14:23:45 +0000459 relation['service'] = service
Adam Tengler69c7ba92017-06-01 15:59:01 +0000460 host_list = [n.get('host', '') for n in graph_data + new_nodes]
461 service_list = [n.get('service', '') for n in graph_data + new_nodes if host in n.get('host', '')]
462 if host not in host_list or (host in host_list and service not in service_list):
Adam Tengler2b362622017-06-01 14:23:45 +0000463 new_node = {
464 'host': host,
465 'service': service,
466 'type': relation.get('type', ''),
467 'relations': []
468 }
469 new_nodes.append(new_node)
470
471 graph_data = graph_data + new_nodes
472
473 return graph_data
474
475
476def _grain_graph_data(*args, **kwargs):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000477 ret = _get_grains('salt:graph')
Adam Tengler2b362622017-06-01 14:23:45 +0000478 graph_data = []
479 for minion_ret in ret.values():
480 if minion_ret.get('retcode', 1) == 0:
481 graph_datum = minion_ret.get('ret', {}).get('salt:graph', [])
482 graph_data = graph_data + graph_datum
483
484 graph_nodes = _interpolate_graph_data(graph_data)
485 graph = {}
486
487 for node in graph_nodes:
488 if node.get('host') not in graph:
489 graph[node.get('host')] = {}
490 graph[node.pop('host')][node.pop('service')] = node
491
492 return {'graph': graph}
493
494
495def _pillar_graph_data(*args, **kwargs):
496 graph = {}
497 nodes = inventory()
498 for node, node_data in nodes.items():
499 for role in node_data.get('roles', []):
500 if node not in graph:
501 graph[node] = {}
502 graph[node][role] = {'relations': []}
503
504 return {'graph': graph}
505
506
507def graph_data(*args, **kwargs):
508 '''
509 Returns graph data for visualization app
510
511 CLI Examples:
512
513 .. code-block:: bash
514
Adam Tengler1f7667b2017-06-06 16:45:51 +0000515 salt-call reclass.graph_data
Petr Michalec46a5bad2017-09-18 20:11:43 +0200516
Adam Tengler2b362622017-06-01 14:23:45 +0000517 '''
518 pillar_data = _pillar_graph_data().get('graph')
519 grain_data = _grain_graph_data().get('graph')
520
521 for host, services in pillar_data.items():
522 for service, service_data in services.items():
523 grain_service = grain_data.get(host, {}).get(service, {})
524 service_data.update(grain_service)
525
526 graph = []
527 for host, services in pillar_data.items():
528 for service, service_data in services.items():
529 additional_data = {
530 'host': host,
Adam Tengler12a310d2017-06-05 19:11:29 +0000531 'service': service,
532 'status': 'unknown'
Adam Tengler2b362622017-06-01 14:23:45 +0000533 }
534 service_data.update(additional_data)
535 graph.append(service_data)
536
537 for host, services in grain_data.items():
538 for service, service_data in services.items():
539 additional_data = {
540 'host': host,
Adam Tengler12a310d2017-06-05 19:11:29 +0000541 'service': service,
542 'status': 'success'
Adam Tengler2b362622017-06-01 14:23:45 +0000543 }
544 service_data.update(additional_data)
545 host_list = [g.get('host', '') for g in graph]
546 service_list = [g.get('service', '') for g in graph if g.get('host') == host]
547 if host not in host_list or (host in host_list and service not in service_list):
548 graph.append(service_data)
549
550 return {'graph': graph}
551
552
Ales Komarek166cc672016-07-27 14:17:22 +0200553def node_update(name, classes=None, parameters=None, **connection_args):
554 '''
555 Update a node metadata information, classes and parameters.
556
557 CLI Examples:
558
559 .. code-block:: bash
560
561 salt '*' reclass.node_update name=nodename classes="[clas1, class2]"
562 '''
563 node = node_get(name=name)
564 if not node.has_key('Error'):
Ales Komarek71f94b02016-07-27 14:48:57 +0200565 node = node[name.split("/")[1]]
566 else:
567 return {'Error': 'Error in retrieving node'}
Ales Komareka4a9f572016-12-03 20:15:50 +0100568
569
Adam Tengler23d965f2017-05-16 19:14:51 +0000570def _get_node_classes(node_data, class_mapping_fragment):
571 classes = []
572
573 for value_tmpl_string in class_mapping_fragment.get('value_template', []):
574 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
575 rendered_value = value_tmpl.safe_substitute(node_data)
576 classes.append(rendered_value)
577
578 for value in class_mapping_fragment.get('value', []):
579 classes.append(value)
580
581 return classes
582
583
584def _get_params(node_data, class_mapping_fragment):
585 params = {}
586
587 for param_name, param in class_mapping_fragment.items():
588 value = param.get('value', None)
589 value_tmpl_string = param.get('value_template', None)
590 if value:
591 params.update({param_name: value})
592 elif value_tmpl_string:
593 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
594 rendered_value = value_tmpl.safe_substitute(node_data)
595 params.update({param_name: rendered_value})
596
597 return params
598
599
Adam Tengler4d961142017-07-27 15:35:28 +0000600def _validate_condition(node_data, expressions):
601 # allow string expression definition for single expression conditions
602 if isinstance(expressions, six.string_types):
603 expressions = [expressions]
Adam Tengler23d965f2017-05-16 19:14:51 +0000604
Adam Tengler4d961142017-07-27 15:35:28 +0000605 result = []
606 for expression_tmpl_string in expressions:
607 expression_tmpl = Template(expression_tmpl_string.replace('<<', '${').replace('>>', '}'))
608 expression = expression_tmpl.safe_substitute(node_data)
Adam Tengler23d965f2017-05-16 19:14:51 +0000609
Adam Tengler4d961142017-07-27 15:35:28 +0000610 if expression and expression == 'all':
611 result.append(True)
612 elif expression:
613 val_a = expression.split('__')[0]
614 val_b = expression.split('__')[2]
615 condition = expression.split('__')[1]
616 if condition == 'startswith':
617 result.append(val_a.startswith(val_b))
618 elif condition == 'equals':
619 result.append(val_a == val_b)
620
621 return all(result)
Adam Tengler23d965f2017-05-16 19:14:51 +0000622
623
624def node_classify(node_name, node_data={}, class_mapping={}, **kwargs):
625 '''
626 CLassify node by given class_mapping dictionary
627
628 :param node_name: node FQDN
629 :param node_data: dictionary of known informations about the node
630 :param class_mapping: dictionary of classes and parameters, with conditions
631
632 '''
633 # clean node_data
634 node_data = {k: v for (k, v) in node_data.items() if not k.startswith('__')}
635
636 classes = []
637 node_params = {}
638 cluster_params = {}
639 ret = {'node_create': '', 'cluster_param': {}}
640
641 for type_name, node_type in class_mapping.items():
642 valid = _validate_condition(node_data, node_type.get('expression', ''))
643 if valid:
644 gen_classes = _get_node_classes(node_data, node_type.get('node_class', {}))
645 classes = classes + gen_classes
646 gen_node_params = _get_params(node_data, node_type.get('node_param', {}))
647 node_params.update(gen_node_params)
648 gen_cluster_params = _get_params(node_data, node_type.get('cluster_param', {}))
649 cluster_params.update(gen_cluster_params)
650
651 if classes:
652 create_kwargs = {'name': node_name, 'path': '_generated', 'classes': classes, 'parameters': node_params}
653 ret['node_create'] = node_create(**create_kwargs)
654
655 for name, value in cluster_params.items():
656 ret['cluster_param'][name] = cluster_meta_set(name, value)
657
658 return ret
659
660
Ales Komarekb0911892017-08-02 15:47:30 +0200661def validate_yaml():
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200662 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200663 Returns list of all reclass YAML files that contain syntax
664 errors.
665
666 CLI Examples:
667
668 .. code-block:: bash
669
670 salt-call reclass.validate_yaml
671 '''
672 errors = _deps(ret_classes=False, ret_errors=True)
673 if errors:
674 ret = {'Errors': errors}
675 return ret
676
677
678def validate_pillar(node_name=None, **kwargs):
679 '''
680 Validates whether the pillar of given node is in correct state.
681 If node is not specified it validates pillars of all known nodes.
682 Returns error message for every node with currupted metadata.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200683
684 :param node_name: target minion ID
685
686 CLI Examples:
687
688 .. code-block:: bash
689
Ales Komarekb0911892017-08-02 15:47:30 +0200690 salt-call reclass.validate_pillar
691 salt-call reclass.validate_pillar minion-id
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200692 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200693 if node_name is None:
694 ret={}
695 nodes = node_list(**kwargs)
696 for node_name, node in nodes.items():
697 ret.update(validate_pillar(node_name))
698 return ret
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200699 else:
Ales Komarekb0911892017-08-02 15:47:30 +0200700 defaults = find_and_read_configfile()
701 meta = ''
702 error = None
703 try:
704 pillar = ext_pillar(node_name, {}, defaults['storage_type'], defaults['inventory_base_uri'])
705 except (ReclassException, Exception) as e:
706 msg = "Validation failed in %s on %s" % (repr(e), node_name)
707 LOG.error(msg)
708 meta = {'Error': msg}
709 s = str(type(e))
710 if 'yaml.scanner.ScannerError' in s:
711 error = re.sub(r"\r?\n?$", "", repr(str(e)), 1)
712 else:
713 error = e.message
714 if 'Error' in meta:
715 ret = {node_name: error}
716 else:
717 ret = {node_name: ''}
718 return ret
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200719
720
Adam Tengler2b362622017-06-01 14:23:45 +0000721def node_pillar(node_name, **kwargs):
722 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200723 Returns pillar metadata for given node from reclass inventory.
Adam Tengler2b362622017-06-01 14:23:45 +0000724
725 :param node_name: target minion ID
726
727 CLI Examples:
728
729 .. code-block:: bash
730
731 salt-call reclass.node_pillar minion_id
732
733 '''
734 defaults = find_and_read_configfile()
735 pillar = ext_pillar(node_name, {}, defaults['storage_type'], defaults['inventory_base_uri'])
736 output = {node_name: pillar}
737
738 return output
739
740
Ales Komareka4a9f572016-12-03 20:15:50 +0100741def inventory(**connection_args):
742 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200743 Get all nodes in inventory and their associated services/roles.
Ales Komareka4a9f572016-12-03 20:15:50 +0100744
745 CLI Examples:
746
747 .. code-block:: bash
748
749 salt '*' reclass.inventory
750 '''
751 defaults = find_and_read_configfile()
752 storage = get_storage(defaults['storage_type'], _get_nodes_dir(), _get_classes_dir())
753 reclass = Core(storage, None)
754 nodes = reclass.inventory()["nodes"]
755 output = {}
756
757 for node in nodes:
758 service_classification = []
759 role_classification = []
760 for service in nodes[node]['parameters']:
761 if service not in ['_param', 'private_keys', 'public_keys', 'known_hosts']:
762 service_classification.append(service)
763 for role in nodes[node]['parameters'][service]:
764 if role not in ['_support', '_orchestrate', 'common']:
765 role_classification.append('%s.%s' % (service, role))
766 output[node] = {
767 'roles': role_classification,
768 'services': service_classification,
769 }
770 return output
Adam Tengler8a1cf402017-05-16 10:59:35 +0000771
772
773def cluster_meta_list(file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000774 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200775 List all cluster level soft metadata overrides.
Adam Tengler2b362622017-06-01 14:23:45 +0000776
777 :param file_name: name of the override file, defaults to: overrides.yml
778
779 CLI Examples:
780
781 .. code-block:: bash
782
783 salt-call reclass.cluster_meta_list
Adam Tengler2b362622017-06-01 14:23:45 +0000784 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000785 path = os.path.join(_get_cluster_dir(), cluster, file_name)
786 try:
787 with io.open(path, 'r') as file_handle:
788 meta_yaml = yaml.safe_load(file_handle.read())
789 meta = meta_yaml or {}
790 except Exception as e:
791 msg = "Unable to load cluster metadata YAML %s: %s" % (path, repr(e))
792 LOG.debug(msg)
793 meta = {'Error': msg}
794 return meta
795
796
797def cluster_meta_delete(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000798 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200799 Delete cluster level soft metadata override entry.
Adam Tengler2b362622017-06-01 14:23:45 +0000800
801 :param name: name of the override entry (dictionary key)
802 :param file_name: name of the override file, defaults to: overrides.yml
803
804 CLI Examples:
805
806 .. code-block:: bash
807
808 salt-call reclass.cluster_meta_delete foo
Adam Tengler2b362622017-06-01 14:23:45 +0000809 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000810 ret = {}
811 path = os.path.join(_get_cluster_dir(), cluster, file_name)
812 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
813 if 'Error' not in meta:
814 metadata = meta.get('parameters', {}).get('_param', {})
815 if name not in metadata:
816 return ret
817 del metadata[name]
818 try:
819 with io.open(path, 'w') as file_handle:
820 file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
821 except Exception as e:
822 msg = "Unable to save cluster metadata YAML: %s" % repr(e)
823 LOG.error(msg)
824 return {'Error': msg}
825 ret = 'Cluster metadata entry {0} deleted'.format(name)
826 return ret
827
828
829def cluster_meta_set(name, value, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000830 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200831 Create cluster level metadata override entry.
Adam Tengler2b362622017-06-01 14:23:45 +0000832
833 :param name: name of the override entry (dictionary key)
834 :param value: value of the override entry (dictionary value)
835 :param file_name: name of the override file, defaults to: overrides.yml
836
837 CLI Examples:
838
839 .. code-block:: bash
840
841 salt-call reclass.cluster_meta_set foo bar
Adam Tengler2b362622017-06-01 14:23:45 +0000842 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000843 path = os.path.join(_get_cluster_dir(), cluster, file_name)
844 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
845 if 'Error' not in meta:
846 if not meta:
847 meta = {'parameters': {'_param': {}}}
848 metadata = meta.get('parameters', {}).get('_param', {})
849 if name in metadata and metadata[name] == value:
850 return {name: 'Cluster metadata entry %s already exists and is in correct state' % name}
851 metadata.update({name: value})
852 try:
853 with io.open(path, 'w') as file_handle:
854 file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
855 except Exception as e:
856 msg = "Unable to save cluster metadata YAML %s: %s" % (path, repr(e))
857 LOG.error(msg)
858 return {'Error': msg}
859 return cluster_meta_get(name, path, **kwargs)
860 return meta
861
862
863def cluster_meta_get(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000864 '''
865 Get single cluster level override entry
866
867 :param name: name of the override entry (dictionary key)
868 :param file_name: name of the override file, defaults to: overrides.yml
869
870 CLI Examples:
871
872 .. code-block:: bash
873
874 salt-call reclass.cluster_meta_get foo
875
876 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000877 ret = {}
878 path = os.path.join(_get_cluster_dir(), cluster, file_name)
879 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
880 metadata = meta.get('parameters', {}).get('_param', {})
881 if 'Error' in meta:
882 ret['Error'] = meta['Error']
883 elif name in metadata:
884 ret[name] = metadata.get(name)
885
886 return ret
887