blob: eace0bfcc6cfb214e1f79a941474ccf89fd0ee17 [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
Adam Tengler2b362622017-06-01 14:23:45 +000019from urlparse import urlparse
Ales Komareka4a9f572016-12-03 20:15:50 +010020from reclass import get_storage, output
Adam Tengler2b362622017-06-01 14:23:45 +000021from reclass.adapters.salt import ext_pillar
Ales Komareka4a9f572016-12-03 20:15:50 +010022from reclass.core import Core
23from reclass.config import find_and_read_configfile
Adam Tengler23d965f2017-05-16 19:14:51 +000024from string import Template
Jiri Broulik7ccb5342017-07-20 17:07:47 +020025from reclass.errors import ReclassException
26
Ales Komarek166cc672016-07-27 14:17:22 +020027
Ales Komareka4a9f572016-12-03 20:15:50 +010028LOG = logging.getLogger(__name__)
Ales Komarek166cc672016-07-27 14:17:22 +020029
Ales Komareka961df42016-11-21 21:50:24 +010030
Ales Komarek166cc672016-07-27 14:17:22 +020031def __virtual__():
32 '''
33 Only load this module if reclass
34 is installed on this minion.
35 '''
36 return 'reclass'
37
38
Jiri Broulik7ccb5342017-07-20 17:07:47 +020039def _deps(ret_classes=True, ret_errors=False):
40 '''
41 Returns classes if ret_classes=True, else returns soft_params if ret_classes=False
42 '''
43 defaults = find_and_read_configfile()
44 path = defaults.get('inventory_base_uri')
45 classes = {}
46 soft_params = {}
47 errors = []
48
49 # find classes
50 for root, dirs, files in os.walk(path):
51 if 'init.yml' in files:
52 class_file = root + '/' + 'init.yml'
53 class_name = class_file.replace(path, '')[:-9].replace('/', '.')
54 classes[class_name] = {'file': class_file}
55
56 for f in files:
57 if f.endswith('.yml') and f != 'init.yml':
58 class_file = root + '/' + f
59 class_name = class_file.replace(path, '')[:-4].replace('/', '.')
60 classes[class_name] = {'file': class_file}
61
62 # read classes
63 for class_name, params in classes.items():
64 with open(params['file'], 'r') as f:
65 # read raw data
66 raw = f.read()
67 pr = re.findall('\${_param:(.*?)}', raw)
68 if pr:
69 params['params_required'] = list(set(pr))
70
71 # load yaml
72 try:
73 data = yaml.load(raw)
74 except yaml.scanner.ScannerError as e:
75 errors.append(params['file'] + ' ' + str(e))
76 pass
77
78 if type(data) == dict:
79 if data.get('classes'):
80 params['includes'] = data.get('classes', [])
81 if data.get('parameters') and data['parameters'].get('_param'):
82 params['params_created'] = data['parameters']['_param']
83
84 if not(data.get('classes') or data.get('parameters')):
85 errors.append(params['file'] + ' ' + 'file missing classes and parameters')
86 else:
87 errors.append(params['file'] + ' ' + 'is not valid yaml')
88
89 if ret_classes:
90 return classes
91 elif ret_errors:
92 return errors
93
94 # find parameters and its usage
95 for class_name, params in classes.items():
96 for pn, pv in params.get('params_created', {}).items():
97 # create param if missing
98 if pn not in soft_params:
99 soft_params[pn] = {'created_at': {}, 'required_at': []}
100
101 # add created_at
102 if class_name not in soft_params[pn]['created_at']:
103 soft_params[pn]['created_at'][class_name] = pv
104
105 for pn in params.get('params_required', []):
106 # create param if missing
107 if pn not in soft_params:
108 soft_params[pn] = {'created_at': {}, 'required_at': []}
109
110 # add created_at
111 soft_params[pn]['required_at'].append(class_name)
112
113 return soft_params
114
115
Ales Komareka4a9f572016-12-03 20:15:50 +0100116def _get_nodes_dir():
117 defaults = find_and_read_configfile()
118 return os.path.join(defaults.get('inventory_base_uri'), 'nodes')
119
120
121def _get_classes_dir():
122 defaults = find_and_read_configfile()
123 return os.path.join(defaults.get('inventory_base_uri'), 'classes')
Ales Komarek166cc672016-07-27 14:17:22 +0200124
125
Adam Tengler8a1cf402017-05-16 10:59:35 +0000126def _get_cluster_dir():
127 classes_dir = _get_classes_dir()
128 return os.path.join(classes_dir, 'cluster')
129
130
Adam Tengler805666d2017-05-15 16:01:13 +0000131def _get_node_meta(name, cluster="default", environment="prd", classes=None, parameters=None):
132 host_name = name.split('.')[0]
133 domain_name = '.'.join(name.split('.')[1:])
134
135 if classes == None:
136 meta_classes = []
137 else:
138 if isinstance(classes, six.string_types):
139 meta_classes = json.loads(classes)
140 else:
141 meta_classes = classes
142
143 if parameters == None:
144 meta_parameters = {}
145 else:
146 if isinstance(parameters, six.string_types):
147 meta_parameters = json.loads(parameters)
148 else:
149 # generate dict from OrderedDict
150 meta_parameters = {k: v for (k, v) in parameters.items()}
151
152 node_meta = {
153 'classes': meta_classes,
154 'parameters': {
155 '_param': meta_parameters,
156 'linux': {
157 'system': {
158 'name': host_name,
159 'domain': domain_name,
160 'cluster': cluster,
161 'environment': environment,
162 }
163 }
164 }
165 }
166
167 return node_meta
168
169
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200170def validate_yaml_syntax():
171 '''
172 Returns list of yaml files with syntax errors
173
174 CLI Examples:
175
176 .. code-block:: bash
177
178 salt '*' reclass.validate_yaml_syntax
179 '''
180 errors = _deps(ret_classes=False, ret_errors=True)
181 if errors:
182 ret = {'Errors': errors}
183 return ret
184
185
186def soft_meta_list():
187 '''
188 Returns params list
189
190 CLI Examples:
191
192 .. code-block:: bash
193
194 salt '*' reclass.soft_meta_list
195 '''
196 return _deps(ret_classes=False)
197
198
199def class_list():
200 '''
201 Returns classes list
202
203 CLI Examples:
204
205 .. code-block:: bash
206
207 salt '*' reclass.class_list
208 '''
209 return _deps(ret_classes=True)
210
211
212def soft_meta_get(name):
213 '''
214 :param name: expects the following format: apt_mk_version
215
216 Returns detail of the params
217
218 CLI Examples:
219
220 .. code-block:: bash
221
222 salt '*' reclass.soft_meta_get apt_mk_version
223 '''
224 soft_params = _deps(ret_classes=False)
225
226 if name in soft_params:
227 return {name: soft_params.get(name)}
228 else:
229 return {'Error': 'No param {0} found'.format(name)}
230
231def class_get(name):
232 '''
233 :param name: expects the following format classes.system.linux.repo
234
235 Returns detail data of the class
236 CLI Examples:
237
238 .. code-block:: bash
239
240 salt '*' reclass.class_get classes.system.linux.repo
241 '''
242 classes = _deps(ret_classes=True)
243 tmp_name = '.' + name
244 if tmp_name in classes:
245 return {name: classes.get(tmp_name)}
246 else:
247 return {'Error': 'No class {0} found'.format(name)}
248
249
250
Ales Komarek166cc672016-07-27 14:17:22 +0200251def node_create(name, path=None, cluster="default", environment="prd", classes=None, parameters=None, **kwargs):
252 '''
253 Create a reclass node
254
255 :param name: new node FQDN
256 :param path: custom path in nodes for new node
257 :param classes: classes given to the new node
258 :param parameters: parameters given to the new node
259 :param environment: node's environment
260 :param cluster: node's cluster
261
262 CLI Examples:
263
264 .. code-block:: bash
265
266 salt '*' reclass.node_create server.domain.com classes=[system.neco1, system.neco2]
267 salt '*' reclass.node_create namespace/test enabled=False
268
269 '''
270 ret = {}
271
272 node = node_get(name=name)
273
274 if node and not "Error" in node:
275 LOG.debug("node {0} exists".format(name))
276 ret[name] = node
277 return ret
278
279 host_name = name.split('.')[0]
280 domain_name = '.'.join(name.split('.')[1:])
281
Adam Tengler805666d2017-05-15 16:01:13 +0000282 node_meta = _get_node_meta(name, cluster, environment, classes, parameters)
Ales Komarek166cc672016-07-27 14:17:22 +0200283 LOG.debug(node_meta)
284
285 if path == None:
Ales Komareka4a9f572016-12-03 20:15:50 +0100286 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200287 else:
Ales Komareka4a9f572016-12-03 20:15:50 +0100288 file_path = os.path.join(_get_nodes_dir(), path, name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200289
290 with open(file_path, 'w') as node_file:
Ales Komareka961df42016-11-21 21:50:24 +0100291 node_file.write(yaml.safe_dump(node_meta, default_flow_style=False))
Ales Komarek71f94b02016-07-27 14:48:57 +0200292
Ales Komarek166cc672016-07-27 14:17:22 +0200293 return node_get(name)
294
Ales Komareka4a9f572016-12-03 20:15:50 +0100295
Ales Komarek166cc672016-07-27 14:17:22 +0200296def node_delete(name, **kwargs):
297 '''
298 Delete a reclass node
299
300 :params node: Node name
301
302 CLI Examples:
303
304 .. code-block:: bash
305
306 salt '*' reclass.node_delete demo01.domain.com
307 salt '*' reclass.node_delete name=demo01.domain.com
308 '''
309
310 node = node_get(name=name)
311
312 if 'Error' in node:
313 return {'Error': 'Unable to retreive node'}
314
315 if node[name]['path'] == '':
Ales Komareka4a9f572016-12-03 20:15:50 +0100316 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200317 else:
Ales Komareka4a9f572016-12-03 20:15:50 +0100318 file_path = os.path.join(_get_nodes_dir(), node[name]['path'], name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200319
320 os.remove(file_path)
321
322 ret = 'Node {0} deleted'.format(name)
323
324 return ret
325
326
327def node_get(name, path=None, **kwargs):
328 '''
329 Return a specific node
330
331 CLI Examples:
332
333 .. code-block:: bash
334
335 salt '*' reclass.node_get host01.domain.com
336 salt '*' reclass.node_get name=host02.domain.com
337 '''
338 ret = {}
339 nodes = node_list(**kwargs)
340
341 if not name in nodes:
342 return {'Error': 'Error in retrieving node'}
343 ret[name] = nodes[name]
344 return ret
345
346
347def node_list(**connection_args):
348 '''
349 Return a list of available nodes
350
351 CLI Example:
352
353 .. code-block:: bash
354
355 salt '*' reclass.node_list
356 '''
357 ret = {}
358
Ales Komareka4a9f572016-12-03 20:15:50 +0100359 for root, sub_folders, files in os.walk(_get_nodes_dir()):
Adam Tengler805666d2017-05-15 16:01:13 +0000360 for fl in files:
361 file_path = os.path.join(root, fl)
362 with open(file_path, 'r') as file_handle:
363 file_read = yaml.load(file_handle.read())
364 file_data = file_read or {}
365 classes = file_data.get('classes', [])
366 parameters = file_data.get('parameters', {}).get('_param', [])
367 name = fl.replace('.yml', '')
Ales Komarek166cc672016-07-27 14:17:22 +0200368 host_name = name.split('.')[0]
369 domain_name = '.'.join(name.split('.')[1:])
Ales Komareka4a9f572016-12-03 20:15:50 +0100370 path = root.replace(_get_nodes_dir()+'/', '')
Ales Komarek166cc672016-07-27 14:17:22 +0200371 ret[name] = {
Ales Komareka4a9f572016-12-03 20:15:50 +0100372 'name': host_name,
373 'domain': domain_name,
374 'cluster': 'default',
375 'environment': 'prd',
376 'path': path,
377 'classes': classes,
378 'parameters': parameters
Ales Komarek166cc672016-07-27 14:17:22 +0200379 }
380
381 return ret
382
Ales Komareka4a9f572016-12-03 20:15:50 +0100383
Adam Tengler2b362622017-06-01 14:23:45 +0000384def _is_valid_ipv4_address(address):
385 try:
386 socket.inet_pton(socket.AF_INET, address)
387 except AttributeError:
388 try:
389 socket.inet_aton(address)
390 except socket.error:
391 return False
392 return address.count('.') == 3
393 except socket.error:
394 return False
395 return True
396
397
398def _is_valid_ipv6_address(address):
399 try:
400 socket.inet_pton(socket.AF_INET6, address)
401 except socket.error:
402 return False
403 return True
404
405
Adam Tengler1f7667b2017-06-06 16:45:51 +0000406def _get_grains(*args, **kwargs):
407 res = __salt__['saltutil.cmd'](tgt='*',
408 fun='grains.item',
409 arg=args,
410 **{'timeout': 10})
411 return res or {}
412
413
414def _guess_host_from_target(network_grains, host, domain=' '):
Adam Tengler2b362622017-06-01 14:23:45 +0000415 '''
416 Guess minion ID from given host and domain arguments. Host argument can contain
417 hostname, FQDN, IPv4 or IPv6 addresses.
418 '''
Adam Tengler1f7667b2017-06-06 16:45:51 +0000419 key = None
420 value = None
421
Adam Tengler2b362622017-06-01 14:23:45 +0000422 if _is_valid_ipv4_address(host):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000423 key = 'ipv4'
424 value = host
Adam Tengler2b362622017-06-01 14:23:45 +0000425 elif _is_valid_ipv6_address(host):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000426 key = 'ipv6'
427 value = host
Adam Tengler2b362622017-06-01 14:23:45 +0000428 elif host.endswith(domain):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000429 key = 'fqdn'
430 value = host
Adam Tengler2b362622017-06-01 14:23:45 +0000431 else:
Adam Tengler1f7667b2017-06-06 16:45:51 +0000432 key = 'fqdn'
433 value = '%s.%s' % (host, domain)
Adam Tengler2b362622017-06-01 14:23:45 +0000434
Adam Tengler1f7667b2017-06-06 16:45:51 +0000435 target = None
436 if network_grains and isinstance(network_grains, dict) and key and value:
437 for minion, grains in network_grains.items():
438 if grains.get('retcode', 1) == 0 and value in grains.get('ret', {}).get(key, ''):
439 target = minion
Adam Tengler2b362622017-06-01 14:23:45 +0000440
Adam Tengler1f7667b2017-06-06 16:45:51 +0000441 return target or host
Adam Tengler2b362622017-06-01 14:23:45 +0000442
443
444def _interpolate_graph_data(graph_data, **kwargs):
445 new_nodes = []
Adam Tengler1f7667b2017-06-06 16:45:51 +0000446 network_grains = _get_grains('ipv4', 'ipv6', 'fqdn')
Adam Tengler2b362622017-06-01 14:23:45 +0000447 for node in graph_data:
Adam Tengler69c7ba92017-06-01 15:59:01 +0000448 if not node.get('relations', []):
449 node['relations'] = []
Adam Tengler2b362622017-06-01 14:23:45 +0000450 for relation in node.get('relations', []):
Adam Tengler12a310d2017-06-05 19:11:29 +0000451 if not relation.get('status', None):
452 relation['status'] = 'unknown'
Adam Tengler2b362622017-06-01 14:23:45 +0000453 if relation.get('host_from_target', None):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000454 host = _guess_host_from_target(network_grains, relation.pop('host_from_target'))
Adam Tengler2b362622017-06-01 14:23:45 +0000455 relation['host'] = host
456 if relation.get('host_external', None):
Adam Tengler69c7ba92017-06-01 15:59:01 +0000457 parsed_host_external = [urlparse(item).netloc
458 for item
459 in relation.get('host_external', '').split(' ')
460 if urlparse(item).netloc]
461 service = parsed_host_external[0] if parsed_host_external else ''
Adam Tengler2b362622017-06-01 14:23:45 +0000462 host = relation.get('service', '')
463 relation['host'] = host
Adam Tengler69c7ba92017-06-01 15:59:01 +0000464 del relation['host_external']
Adam Tengler2b362622017-06-01 14:23:45 +0000465 relation['service'] = service
Adam Tengler69c7ba92017-06-01 15:59:01 +0000466 host_list = [n.get('host', '') for n in graph_data + new_nodes]
467 service_list = [n.get('service', '') for n in graph_data + new_nodes if host in n.get('host', '')]
468 if host not in host_list or (host in host_list and service not in service_list):
Adam Tengler2b362622017-06-01 14:23:45 +0000469 new_node = {
470 'host': host,
471 'service': service,
472 'type': relation.get('type', ''),
473 'relations': []
474 }
475 new_nodes.append(new_node)
476
477 graph_data = graph_data + new_nodes
478
479 return graph_data
480
481
482def _grain_graph_data(*args, **kwargs):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000483 ret = _get_grains('salt:graph')
Adam Tengler2b362622017-06-01 14:23:45 +0000484 graph_data = []
485 for minion_ret in ret.values():
486 if minion_ret.get('retcode', 1) == 0:
487 graph_datum = minion_ret.get('ret', {}).get('salt:graph', [])
488 graph_data = graph_data + graph_datum
489
490 graph_nodes = _interpolate_graph_data(graph_data)
491 graph = {}
492
493 for node in graph_nodes:
494 if node.get('host') not in graph:
495 graph[node.get('host')] = {}
496 graph[node.pop('host')][node.pop('service')] = node
497
498 return {'graph': graph}
499
500
501def _pillar_graph_data(*args, **kwargs):
502 graph = {}
503 nodes = inventory()
504 for node, node_data in nodes.items():
505 for role in node_data.get('roles', []):
506 if node not in graph:
507 graph[node] = {}
508 graph[node][role] = {'relations': []}
509
510 return {'graph': graph}
511
512
513def graph_data(*args, **kwargs):
514 '''
515 Returns graph data for visualization app
516
517 CLI Examples:
518
519 .. code-block:: bash
520
Adam Tengler1f7667b2017-06-06 16:45:51 +0000521 salt-call reclass.graph_data
Adam Tengler2b362622017-06-01 14:23:45 +0000522
523 '''
524 pillar_data = _pillar_graph_data().get('graph')
525 grain_data = _grain_graph_data().get('graph')
526
527 for host, services in pillar_data.items():
528 for service, service_data in services.items():
529 grain_service = grain_data.get(host, {}).get(service, {})
530 service_data.update(grain_service)
531
532 graph = []
533 for host, services in pillar_data.items():
534 for service, service_data in services.items():
535 additional_data = {
536 'host': host,
Adam Tengler12a310d2017-06-05 19:11:29 +0000537 'service': service,
538 'status': 'unknown'
Adam Tengler2b362622017-06-01 14:23:45 +0000539 }
540 service_data.update(additional_data)
541 graph.append(service_data)
542
543 for host, services in grain_data.items():
544 for service, service_data in services.items():
545 additional_data = {
546 'host': host,
Adam Tengler12a310d2017-06-05 19:11:29 +0000547 'service': service,
548 'status': 'success'
Adam Tengler2b362622017-06-01 14:23:45 +0000549 }
550 service_data.update(additional_data)
551 host_list = [g.get('host', '') for g in graph]
552 service_list = [g.get('service', '') for g in graph if g.get('host') == host]
553 if host not in host_list or (host in host_list and service not in service_list):
554 graph.append(service_data)
555
556 return {'graph': graph}
557
558
Ales Komarek166cc672016-07-27 14:17:22 +0200559def node_update(name, classes=None, parameters=None, **connection_args):
560 '''
561 Update a node metadata information, classes and parameters.
562
563 CLI Examples:
564
565 .. code-block:: bash
566
567 salt '*' reclass.node_update name=nodename classes="[clas1, class2]"
568 '''
569 node = node_get(name=name)
570 if not node.has_key('Error'):
Ales Komarek71f94b02016-07-27 14:48:57 +0200571 node = node[name.split("/")[1]]
572 else:
573 return {'Error': 'Error in retrieving node'}
Ales Komareka4a9f572016-12-03 20:15:50 +0100574
575
Adam Tengler23d965f2017-05-16 19:14:51 +0000576def _get_node_classes(node_data, class_mapping_fragment):
577 classes = []
578
579 for value_tmpl_string in class_mapping_fragment.get('value_template', []):
580 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
581 rendered_value = value_tmpl.safe_substitute(node_data)
582 classes.append(rendered_value)
583
584 for value in class_mapping_fragment.get('value', []):
585 classes.append(value)
586
587 return classes
588
589
590def _get_params(node_data, class_mapping_fragment):
591 params = {}
592
593 for param_name, param in class_mapping_fragment.items():
594 value = param.get('value', None)
595 value_tmpl_string = param.get('value_template', None)
596 if value:
597 params.update({param_name: value})
598 elif value_tmpl_string:
599 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
600 rendered_value = value_tmpl.safe_substitute(node_data)
601 params.update({param_name: rendered_value})
602
603 return params
604
605
606def _validate_condition(node_data, expression_tmpl_string):
607 expression_tmpl = Template(expression_tmpl_string.replace('<<', '${').replace('>>', '}'))
608 expression = expression_tmpl.safe_substitute(node_data)
609
610 if expression and expression == 'all':
611 return 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 return val_a.startswith(val_b)
618 elif condition == 'equals':
619 return val_a == val_b
620
621 return False
622
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
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200661def validate_node_params(node_name, **kwargs):
662 '''
663 Validates if pillar of a node is in correct state.
664 Returns error message only if error occurred.
665
666 :param node_name: target minion ID
667
668 CLI Examples:
669
670 .. code-block:: bash
671
672 salt-call reclass.validate_node_params minion_id
673
674 '''
675 defaults = find_and_read_configfile()
676 meta = ''
677 error = None
678 try:
679 pillar = ext_pillar(node_name, {}, defaults['storage_type'], defaults['inventory_base_uri'])
680 except (ReclassException, Exception) as e:
681 msg = "Validation failed in %s on %s" % (repr(e), node_name)
682 LOG.error(msg)
683 meta = {'Error': msg}
684 s = str(type(e))
685 if 'yaml.scanner.ScannerError' in s:
686 error = re.sub(r"\r?\n?$", "", repr(str(e)), 1)
687 else:
688 error = e.message
689 if 'Error' in meta:
690 ret = {node_name: error}
691 else:
692 ret = {node_name: ''}
693 return ret
694
695
696def validate_nodes_params(**connection_args):
697 '''
698 Validates if pillar all known nodes is in correct state.
699 Returns error message for every node where problem occurred.
700
701 CLI Examples:
702
703 .. code-block:: bash
704
705 salt-call reclass.validate_nodes_params
706 '''
707 ret={}
708 nodes = node_list(**connection_args)
709 for node_name, node in nodes.items():
710 ret.update(validate_node_params(node_name))
711 return ret
712
713
Adam Tengler2b362622017-06-01 14:23:45 +0000714def node_pillar(node_name, **kwargs):
715 '''
716 Returns pillar data for given minion from reclass inventory.
717
718 :param node_name: target minion ID
719
720 CLI Examples:
721
722 .. code-block:: bash
723
724 salt-call reclass.node_pillar minion_id
725
726 '''
727 defaults = find_and_read_configfile()
728 pillar = ext_pillar(node_name, {}, defaults['storage_type'], defaults['inventory_base_uri'])
729 output = {node_name: pillar}
730
731 return output
732
733
Ales Komareka4a9f572016-12-03 20:15:50 +0100734def inventory(**connection_args):
735 '''
736 Get all nodes in inventory and their associated services/roles classification.
737
738 CLI Examples:
739
740 .. code-block:: bash
741
742 salt '*' reclass.inventory
743 '''
744 defaults = find_and_read_configfile()
745 storage = get_storage(defaults['storage_type'], _get_nodes_dir(), _get_classes_dir())
746 reclass = Core(storage, None)
747 nodes = reclass.inventory()["nodes"]
748 output = {}
749
750 for node in nodes:
751 service_classification = []
752 role_classification = []
753 for service in nodes[node]['parameters']:
754 if service not in ['_param', 'private_keys', 'public_keys', 'known_hosts']:
755 service_classification.append(service)
756 for role in nodes[node]['parameters'][service]:
757 if role not in ['_support', '_orchestrate', 'common']:
758 role_classification.append('%s.%s' % (service, role))
759 output[node] = {
760 'roles': role_classification,
761 'services': service_classification,
762 }
763 return output
Adam Tengler8a1cf402017-05-16 10:59:35 +0000764
765
766def cluster_meta_list(file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000767 '''
768 List all cluster level overrides
769
770 :param file_name: name of the override file, defaults to: overrides.yml
771
772 CLI Examples:
773
774 .. code-block:: bash
775
776 salt-call reclass.cluster_meta_list
777
778 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000779 path = os.path.join(_get_cluster_dir(), cluster, file_name)
780 try:
781 with io.open(path, 'r') as file_handle:
782 meta_yaml = yaml.safe_load(file_handle.read())
783 meta = meta_yaml or {}
784 except Exception as e:
785 msg = "Unable to load cluster metadata YAML %s: %s" % (path, repr(e))
786 LOG.debug(msg)
787 meta = {'Error': msg}
788 return meta
789
790
791def cluster_meta_delete(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000792 '''
793 Delete cluster level override entry
794
795 :param name: name of the override entry (dictionary key)
796 :param file_name: name of the override file, defaults to: overrides.yml
797
798 CLI Examples:
799
800 .. code-block:: bash
801
802 salt-call reclass.cluster_meta_delete foo
803
804 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000805 ret = {}
806 path = os.path.join(_get_cluster_dir(), cluster, file_name)
807 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
808 if 'Error' not in meta:
809 metadata = meta.get('parameters', {}).get('_param', {})
810 if name not in metadata:
811 return ret
812 del metadata[name]
813 try:
814 with io.open(path, 'w') as file_handle:
815 file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
816 except Exception as e:
817 msg = "Unable to save cluster metadata YAML: %s" % repr(e)
818 LOG.error(msg)
819 return {'Error': msg}
820 ret = 'Cluster metadata entry {0} deleted'.format(name)
821 return ret
822
823
824def cluster_meta_set(name, value, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000825 '''
826 Create cluster level override entry
827
828 :param name: name of the override entry (dictionary key)
829 :param value: value of the override entry (dictionary value)
830 :param file_name: name of the override file, defaults to: overrides.yml
831
832 CLI Examples:
833
834 .. code-block:: bash
835
836 salt-call reclass.cluster_meta_set foo bar
837
838 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000839 path = os.path.join(_get_cluster_dir(), cluster, file_name)
840 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
841 if 'Error' not in meta:
842 if not meta:
843 meta = {'parameters': {'_param': {}}}
844 metadata = meta.get('parameters', {}).get('_param', {})
845 if name in metadata and metadata[name] == value:
846 return {name: 'Cluster metadata entry %s already exists and is in correct state' % name}
847 metadata.update({name: value})
848 try:
849 with io.open(path, 'w') as file_handle:
850 file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
851 except Exception as e:
852 msg = "Unable to save cluster metadata YAML %s: %s" % (path, repr(e))
853 LOG.error(msg)
854 return {'Error': msg}
855 return cluster_meta_get(name, path, **kwargs)
856 return meta
857
858
859def cluster_meta_get(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000860 '''
861 Get single cluster level override entry
862
863 :param name: name of the override entry (dictionary key)
864 :param file_name: name of the override file, defaults to: overrides.yml
865
866 CLI Examples:
867
868 .. code-block:: bash
869
870 salt-call reclass.cluster_meta_get foo
871
872 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000873 ret = {}
874 path = os.path.join(_get_cluster_dir(), cluster, file_name)
875 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
876 metadata = meta.get('parameters', {}).get('_param', {})
877 if 'Error' in meta:
878 ret['Error'] = meta['Error']
879 elif name in metadata:
880 ret[name] = metadata.get(name)
881
882 return ret
883