blob: e5055aa1ce30d8e9ef78ff352b76f82ce761ee56 [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()
Vladislav Naumov87113082017-07-24 17:36:50 +0300118 return defaults.get('nodes_uri') or \
119 os.path.join(defaults.get('inventory_base_uri'), 'nodes')
Ales Komareka4a9f572016-12-03 20:15:50 +0100120
121
122def _get_classes_dir():
123 defaults = find_and_read_configfile()
124 return os.path.join(defaults.get('inventory_base_uri'), 'classes')
Ales Komarek166cc672016-07-27 14:17:22 +0200125
126
Adam Tengler8a1cf402017-05-16 10:59:35 +0000127def _get_cluster_dir():
128 classes_dir = _get_classes_dir()
129 return os.path.join(classes_dir, 'cluster')
130
131
Adam Tengler805666d2017-05-15 16:01:13 +0000132def _get_node_meta(name, cluster="default", environment="prd", classes=None, parameters=None):
133 host_name = name.split('.')[0]
134 domain_name = '.'.join(name.split('.')[1:])
135
136 if classes == None:
137 meta_classes = []
138 else:
139 if isinstance(classes, six.string_types):
140 meta_classes = json.loads(classes)
141 else:
142 meta_classes = classes
143
144 if parameters == None:
145 meta_parameters = {}
146 else:
147 if isinstance(parameters, six.string_types):
148 meta_parameters = json.loads(parameters)
149 else:
150 # generate dict from OrderedDict
151 meta_parameters = {k: v for (k, v) in parameters.items()}
152
153 node_meta = {
154 'classes': meta_classes,
155 'parameters': {
156 '_param': meta_parameters,
157 'linux': {
158 'system': {
159 'name': host_name,
160 'domain': domain_name,
161 'cluster': cluster,
162 'environment': environment,
163 }
164 }
165 }
166 }
167
168 return node_meta
169
170
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200171def validate_yaml_syntax():
172 '''
173 Returns list of yaml files with syntax errors
174
175 CLI Examples:
176
177 .. code-block:: bash
178
179 salt '*' reclass.validate_yaml_syntax
180 '''
181 errors = _deps(ret_classes=False, ret_errors=True)
182 if errors:
183 ret = {'Errors': errors}
184 return ret
185
186
187def soft_meta_list():
188 '''
189 Returns params list
190
191 CLI Examples:
192
193 .. code-block:: bash
194
195 salt '*' reclass.soft_meta_list
196 '''
197 return _deps(ret_classes=False)
198
199
200def class_list():
201 '''
202 Returns classes list
203
204 CLI Examples:
205
206 .. code-block:: bash
207
208 salt '*' reclass.class_list
209 '''
210 return _deps(ret_classes=True)
211
212
213def soft_meta_get(name):
214 '''
215 :param name: expects the following format: apt_mk_version
216
217 Returns detail of the params
218
219 CLI Examples:
220
221 .. code-block:: bash
222
223 salt '*' reclass.soft_meta_get apt_mk_version
224 '''
225 soft_params = _deps(ret_classes=False)
226
227 if name in soft_params:
228 return {name: soft_params.get(name)}
229 else:
230 return {'Error': 'No param {0} found'.format(name)}
231
232def class_get(name):
233 '''
234 :param name: expects the following format classes.system.linux.repo
235
236 Returns detail data of the class
237 CLI Examples:
238
239 .. code-block:: bash
240
241 salt '*' reclass.class_get classes.system.linux.repo
242 '''
243 classes = _deps(ret_classes=True)
244 tmp_name = '.' + name
245 if tmp_name in classes:
246 return {name: classes.get(tmp_name)}
247 else:
248 return {'Error': 'No class {0} found'.format(name)}
249
250
251
Ales Komarek166cc672016-07-27 14:17:22 +0200252def node_create(name, path=None, cluster="default", environment="prd", classes=None, parameters=None, **kwargs):
253 '''
254 Create a reclass node
255
256 :param name: new node FQDN
257 :param path: custom path in nodes for new node
258 :param classes: classes given to the new node
259 :param parameters: parameters given to the new node
260 :param environment: node's environment
261 :param cluster: node's cluster
262
263 CLI Examples:
264
265 .. code-block:: bash
266
267 salt '*' reclass.node_create server.domain.com classes=[system.neco1, system.neco2]
268 salt '*' reclass.node_create namespace/test enabled=False
269
270 '''
271 ret = {}
272
273 node = node_get(name=name)
274
275 if node and not "Error" in node:
276 LOG.debug("node {0} exists".format(name))
277 ret[name] = node
278 return ret
279
280 host_name = name.split('.')[0]
281 domain_name = '.'.join(name.split('.')[1:])
282
Adam Tengler805666d2017-05-15 16:01:13 +0000283 node_meta = _get_node_meta(name, cluster, environment, classes, parameters)
Ales Komarek166cc672016-07-27 14:17:22 +0200284 LOG.debug(node_meta)
285
286 if path == None:
Ales Komareka4a9f572016-12-03 20:15:50 +0100287 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200288 else:
Ales Komareka4a9f572016-12-03 20:15:50 +0100289 file_path = os.path.join(_get_nodes_dir(), path, name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200290
291 with open(file_path, 'w') as node_file:
Ales Komareka961df42016-11-21 21:50:24 +0100292 node_file.write(yaml.safe_dump(node_meta, default_flow_style=False))
Ales Komarek71f94b02016-07-27 14:48:57 +0200293
Ales Komarek166cc672016-07-27 14:17:22 +0200294 return node_get(name)
295
Ales Komareka4a9f572016-12-03 20:15:50 +0100296
Ales Komarek166cc672016-07-27 14:17:22 +0200297def node_delete(name, **kwargs):
298 '''
299 Delete a reclass node
300
301 :params node: Node name
302
303 CLI Examples:
304
305 .. code-block:: bash
306
307 salt '*' reclass.node_delete demo01.domain.com
308 salt '*' reclass.node_delete name=demo01.domain.com
309 '''
310
311 node = node_get(name=name)
312
313 if 'Error' in node:
314 return {'Error': 'Unable to retreive node'}
315
316 if node[name]['path'] == '':
Ales Komareka4a9f572016-12-03 20:15:50 +0100317 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200318 else:
Ales Komareka4a9f572016-12-03 20:15:50 +0100319 file_path = os.path.join(_get_nodes_dir(), node[name]['path'], name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200320
321 os.remove(file_path)
322
323 ret = 'Node {0} deleted'.format(name)
324
325 return ret
326
327
328def node_get(name, path=None, **kwargs):
329 '''
330 Return a specific node
331
332 CLI Examples:
333
334 .. code-block:: bash
335
336 salt '*' reclass.node_get host01.domain.com
337 salt '*' reclass.node_get name=host02.domain.com
338 '''
339 ret = {}
340 nodes = node_list(**kwargs)
341
342 if not name in nodes:
343 return {'Error': 'Error in retrieving node'}
344 ret[name] = nodes[name]
345 return ret
346
347
348def node_list(**connection_args):
349 '''
350 Return a list of available nodes
351
352 CLI Example:
353
354 .. code-block:: bash
355
356 salt '*' reclass.node_list
357 '''
358 ret = {}
359
Ales Komareka4a9f572016-12-03 20:15:50 +0100360 for root, sub_folders, files in os.walk(_get_nodes_dir()):
Adam Tengler805666d2017-05-15 16:01:13 +0000361 for fl in files:
362 file_path = os.path.join(root, fl)
363 with open(file_path, 'r') as file_handle:
364 file_read = yaml.load(file_handle.read())
365 file_data = file_read or {}
366 classes = file_data.get('classes', [])
367 parameters = file_data.get('parameters', {}).get('_param', [])
368 name = fl.replace('.yml', '')
Ales Komarek166cc672016-07-27 14:17:22 +0200369 host_name = name.split('.')[0]
370 domain_name = '.'.join(name.split('.')[1:])
Ales Komareka4a9f572016-12-03 20:15:50 +0100371 path = root.replace(_get_nodes_dir()+'/', '')
Ales Komarek166cc672016-07-27 14:17:22 +0200372 ret[name] = {
Ales Komareka4a9f572016-12-03 20:15:50 +0100373 'name': host_name,
374 'domain': domain_name,
375 'cluster': 'default',
376 'environment': 'prd',
377 'path': path,
378 'classes': classes,
379 'parameters': parameters
Ales Komarek166cc672016-07-27 14:17:22 +0200380 }
381
382 return ret
383
Ales Komareka4a9f572016-12-03 20:15:50 +0100384
Adam Tengler2b362622017-06-01 14:23:45 +0000385def _is_valid_ipv4_address(address):
386 try:
387 socket.inet_pton(socket.AF_INET, address)
388 except AttributeError:
389 try:
390 socket.inet_aton(address)
391 except socket.error:
392 return False
393 return address.count('.') == 3
394 except socket.error:
395 return False
396 return True
397
398
399def _is_valid_ipv6_address(address):
400 try:
401 socket.inet_pton(socket.AF_INET6, address)
402 except socket.error:
403 return False
404 return True
405
406
Adam Tengler1f7667b2017-06-06 16:45:51 +0000407def _get_grains(*args, **kwargs):
408 res = __salt__['saltutil.cmd'](tgt='*',
409 fun='grains.item',
410 arg=args,
411 **{'timeout': 10})
412 return res or {}
413
414
415def _guess_host_from_target(network_grains, host, domain=' '):
Adam Tengler2b362622017-06-01 14:23:45 +0000416 '''
417 Guess minion ID from given host and domain arguments. Host argument can contain
418 hostname, FQDN, IPv4 or IPv6 addresses.
419 '''
Adam Tengler1f7667b2017-06-06 16:45:51 +0000420 key = None
421 value = None
422
Adam Tengler2b362622017-06-01 14:23:45 +0000423 if _is_valid_ipv4_address(host):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000424 key = 'ipv4'
425 value = host
Adam Tengler2b362622017-06-01 14:23:45 +0000426 elif _is_valid_ipv6_address(host):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000427 key = 'ipv6'
428 value = host
Adam Tengler2b362622017-06-01 14:23:45 +0000429 elif host.endswith(domain):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000430 key = 'fqdn'
431 value = host
Adam Tengler2b362622017-06-01 14:23:45 +0000432 else:
Adam Tengler1f7667b2017-06-06 16:45:51 +0000433 key = 'fqdn'
434 value = '%s.%s' % (host, domain)
Adam Tengler2b362622017-06-01 14:23:45 +0000435
Adam Tengler1f7667b2017-06-06 16:45:51 +0000436 target = None
437 if network_grains and isinstance(network_grains, dict) and key and value:
438 for minion, grains in network_grains.items():
439 if grains.get('retcode', 1) == 0 and value in grains.get('ret', {}).get(key, ''):
440 target = minion
Adam Tengler2b362622017-06-01 14:23:45 +0000441
Adam Tengler1f7667b2017-06-06 16:45:51 +0000442 return target or host
Adam Tengler2b362622017-06-01 14:23:45 +0000443
444
445def _interpolate_graph_data(graph_data, **kwargs):
446 new_nodes = []
Adam Tengler1f7667b2017-06-06 16:45:51 +0000447 network_grains = _get_grains('ipv4', 'ipv6', 'fqdn')
Adam Tengler2b362622017-06-01 14:23:45 +0000448 for node in graph_data:
Adam Tengler69c7ba92017-06-01 15:59:01 +0000449 if not node.get('relations', []):
450 node['relations'] = []
Adam Tengler2b362622017-06-01 14:23:45 +0000451 for relation in node.get('relations', []):
Adam Tengler12a310d2017-06-05 19:11:29 +0000452 if not relation.get('status', None):
453 relation['status'] = 'unknown'
Adam Tengler2b362622017-06-01 14:23:45 +0000454 if relation.get('host_from_target', None):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000455 host = _guess_host_from_target(network_grains, relation.pop('host_from_target'))
Adam Tengler2b362622017-06-01 14:23:45 +0000456 relation['host'] = host
457 if relation.get('host_external', None):
Adam Tengler69c7ba92017-06-01 15:59:01 +0000458 parsed_host_external = [urlparse(item).netloc
459 for item
460 in relation.get('host_external', '').split(' ')
461 if urlparse(item).netloc]
462 service = parsed_host_external[0] if parsed_host_external else ''
Adam Tengler2b362622017-06-01 14:23:45 +0000463 host = relation.get('service', '')
464 relation['host'] = host
Adam Tengler69c7ba92017-06-01 15:59:01 +0000465 del relation['host_external']
Adam Tengler2b362622017-06-01 14:23:45 +0000466 relation['service'] = service
Adam Tengler69c7ba92017-06-01 15:59:01 +0000467 host_list = [n.get('host', '') for n in graph_data + new_nodes]
468 service_list = [n.get('service', '') for n in graph_data + new_nodes if host in n.get('host', '')]
469 if host not in host_list or (host in host_list and service not in service_list):
Adam Tengler2b362622017-06-01 14:23:45 +0000470 new_node = {
471 'host': host,
472 'service': service,
473 'type': relation.get('type', ''),
474 'relations': []
475 }
476 new_nodes.append(new_node)
477
478 graph_data = graph_data + new_nodes
479
480 return graph_data
481
482
483def _grain_graph_data(*args, **kwargs):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000484 ret = _get_grains('salt:graph')
Adam Tengler2b362622017-06-01 14:23:45 +0000485 graph_data = []
486 for minion_ret in ret.values():
487 if minion_ret.get('retcode', 1) == 0:
488 graph_datum = minion_ret.get('ret', {}).get('salt:graph', [])
489 graph_data = graph_data + graph_datum
490
491 graph_nodes = _interpolate_graph_data(graph_data)
492 graph = {}
493
494 for node in graph_nodes:
495 if node.get('host') not in graph:
496 graph[node.get('host')] = {}
497 graph[node.pop('host')][node.pop('service')] = node
498
499 return {'graph': graph}
500
501
502def _pillar_graph_data(*args, **kwargs):
503 graph = {}
504 nodes = inventory()
505 for node, node_data in nodes.items():
506 for role in node_data.get('roles', []):
507 if node not in graph:
508 graph[node] = {}
509 graph[node][role] = {'relations': []}
510
511 return {'graph': graph}
512
513
514def graph_data(*args, **kwargs):
515 '''
516 Returns graph data for visualization app
517
518 CLI Examples:
519
520 .. code-block:: bash
521
Adam Tengler1f7667b2017-06-06 16:45:51 +0000522 salt-call reclass.graph_data
Adam Tengler2b362622017-06-01 14:23:45 +0000523
524 '''
525 pillar_data = _pillar_graph_data().get('graph')
526 grain_data = _grain_graph_data().get('graph')
527
528 for host, services in pillar_data.items():
529 for service, service_data in services.items():
530 grain_service = grain_data.get(host, {}).get(service, {})
531 service_data.update(grain_service)
532
533 graph = []
534 for host, services in pillar_data.items():
535 for service, service_data in services.items():
536 additional_data = {
537 'host': host,
Adam Tengler12a310d2017-06-05 19:11:29 +0000538 'service': service,
539 'status': 'unknown'
Adam Tengler2b362622017-06-01 14:23:45 +0000540 }
541 service_data.update(additional_data)
542 graph.append(service_data)
543
544 for host, services in grain_data.items():
545 for service, service_data in services.items():
546 additional_data = {
547 'host': host,
Adam Tengler12a310d2017-06-05 19:11:29 +0000548 'service': service,
549 'status': 'success'
Adam Tengler2b362622017-06-01 14:23:45 +0000550 }
551 service_data.update(additional_data)
552 host_list = [g.get('host', '') for g in graph]
553 service_list = [g.get('service', '') for g in graph if g.get('host') == host]
554 if host not in host_list or (host in host_list and service not in service_list):
555 graph.append(service_data)
556
557 return {'graph': graph}
558
559
Ales Komarek166cc672016-07-27 14:17:22 +0200560def node_update(name, classes=None, parameters=None, **connection_args):
561 '''
562 Update a node metadata information, classes and parameters.
563
564 CLI Examples:
565
566 .. code-block:: bash
567
568 salt '*' reclass.node_update name=nodename classes="[clas1, class2]"
569 '''
570 node = node_get(name=name)
571 if not node.has_key('Error'):
Ales Komarek71f94b02016-07-27 14:48:57 +0200572 node = node[name.split("/")[1]]
573 else:
574 return {'Error': 'Error in retrieving node'}
Ales Komareka4a9f572016-12-03 20:15:50 +0100575
576
Adam Tengler23d965f2017-05-16 19:14:51 +0000577def _get_node_classes(node_data, class_mapping_fragment):
578 classes = []
579
580 for value_tmpl_string in class_mapping_fragment.get('value_template', []):
581 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
582 rendered_value = value_tmpl.safe_substitute(node_data)
583 classes.append(rendered_value)
584
585 for value in class_mapping_fragment.get('value', []):
586 classes.append(value)
587
588 return classes
589
590
591def _get_params(node_data, class_mapping_fragment):
592 params = {}
593
594 for param_name, param in class_mapping_fragment.items():
595 value = param.get('value', None)
596 value_tmpl_string = param.get('value_template', None)
597 if value:
598 params.update({param_name: value})
599 elif value_tmpl_string:
600 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
601 rendered_value = value_tmpl.safe_substitute(node_data)
602 params.update({param_name: rendered_value})
603
604 return params
605
606
Adam Tengler4d961142017-07-27 15:35:28 +0000607def _validate_condition(node_data, expressions):
608 # allow string expression definition for single expression conditions
609 if isinstance(expressions, six.string_types):
610 expressions = [expressions]
Adam Tengler23d965f2017-05-16 19:14:51 +0000611
Adam Tengler4d961142017-07-27 15:35:28 +0000612 result = []
613 for expression_tmpl_string in expressions:
614 expression_tmpl = Template(expression_tmpl_string.replace('<<', '${').replace('>>', '}'))
615 expression = expression_tmpl.safe_substitute(node_data)
Adam Tengler23d965f2017-05-16 19:14:51 +0000616
Adam Tengler4d961142017-07-27 15:35:28 +0000617 if expression and expression == 'all':
618 result.append(True)
619 elif expression:
620 val_a = expression.split('__')[0]
621 val_b = expression.split('__')[2]
622 condition = expression.split('__')[1]
623 if condition == 'startswith':
624 result.append(val_a.startswith(val_b))
625 elif condition == 'equals':
626 result.append(val_a == val_b)
627
628 return all(result)
Adam Tengler23d965f2017-05-16 19:14:51 +0000629
630
631def node_classify(node_name, node_data={}, class_mapping={}, **kwargs):
632 '''
633 CLassify node by given class_mapping dictionary
634
635 :param node_name: node FQDN
636 :param node_data: dictionary of known informations about the node
637 :param class_mapping: dictionary of classes and parameters, with conditions
638
639 '''
640 # clean node_data
641 node_data = {k: v for (k, v) in node_data.items() if not k.startswith('__')}
642
643 classes = []
644 node_params = {}
645 cluster_params = {}
646 ret = {'node_create': '', 'cluster_param': {}}
647
648 for type_name, node_type in class_mapping.items():
649 valid = _validate_condition(node_data, node_type.get('expression', ''))
650 if valid:
651 gen_classes = _get_node_classes(node_data, node_type.get('node_class', {}))
652 classes = classes + gen_classes
653 gen_node_params = _get_params(node_data, node_type.get('node_param', {}))
654 node_params.update(gen_node_params)
655 gen_cluster_params = _get_params(node_data, node_type.get('cluster_param', {}))
656 cluster_params.update(gen_cluster_params)
657
658 if classes:
659 create_kwargs = {'name': node_name, 'path': '_generated', 'classes': classes, 'parameters': node_params}
660 ret['node_create'] = node_create(**create_kwargs)
661
662 for name, value in cluster_params.items():
663 ret['cluster_param'][name] = cluster_meta_set(name, value)
664
665 return ret
666
667
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200668def validate_node_params(node_name, **kwargs):
669 '''
670 Validates if pillar of a node is in correct state.
671 Returns error message only if error occurred.
672
673 :param node_name: target minion ID
674
675 CLI Examples:
676
677 .. code-block:: bash
678
679 salt-call reclass.validate_node_params minion_id
680
681 '''
682 defaults = find_and_read_configfile()
683 meta = ''
684 error = None
685 try:
686 pillar = ext_pillar(node_name, {}, defaults['storage_type'], defaults['inventory_base_uri'])
687 except (ReclassException, Exception) as e:
688 msg = "Validation failed in %s on %s" % (repr(e), node_name)
689 LOG.error(msg)
690 meta = {'Error': msg}
691 s = str(type(e))
692 if 'yaml.scanner.ScannerError' in s:
693 error = re.sub(r"\r?\n?$", "", repr(str(e)), 1)
694 else:
695 error = e.message
696 if 'Error' in meta:
697 ret = {node_name: error}
698 else:
699 ret = {node_name: ''}
700 return ret
701
702
703def validate_nodes_params(**connection_args):
704 '''
705 Validates if pillar all known nodes is in correct state.
706 Returns error message for every node where problem occurred.
707
708 CLI Examples:
709
710 .. code-block:: bash
711
712 salt-call reclass.validate_nodes_params
713 '''
714 ret={}
715 nodes = node_list(**connection_args)
716 for node_name, node in nodes.items():
717 ret.update(validate_node_params(node_name))
718 return ret
719
720
Adam Tengler2b362622017-06-01 14:23:45 +0000721def node_pillar(node_name, **kwargs):
722 '''
723 Returns pillar data for given minion from reclass inventory.
724
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 '''
743 Get all nodes in inventory and their associated services/roles classification.
744
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 '''
775 List all cluster level overrides
776
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
784
785 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000786 path = os.path.join(_get_cluster_dir(), cluster, file_name)
787 try:
788 with io.open(path, 'r') as file_handle:
789 meta_yaml = yaml.safe_load(file_handle.read())
790 meta = meta_yaml or {}
791 except Exception as e:
792 msg = "Unable to load cluster metadata YAML %s: %s" % (path, repr(e))
793 LOG.debug(msg)
794 meta = {'Error': msg}
795 return meta
796
797
798def cluster_meta_delete(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000799 '''
800 Delete cluster level override entry
801
802 :param name: name of the override entry (dictionary key)
803 :param file_name: name of the override file, defaults to: overrides.yml
804
805 CLI Examples:
806
807 .. code-block:: bash
808
809 salt-call reclass.cluster_meta_delete foo
810
811 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000812 ret = {}
813 path = os.path.join(_get_cluster_dir(), cluster, file_name)
814 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
815 if 'Error' not in meta:
816 metadata = meta.get('parameters', {}).get('_param', {})
817 if name not in metadata:
818 return ret
819 del metadata[name]
820 try:
821 with io.open(path, 'w') as file_handle:
822 file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
823 except Exception as e:
824 msg = "Unable to save cluster metadata YAML: %s" % repr(e)
825 LOG.error(msg)
826 return {'Error': msg}
827 ret = 'Cluster metadata entry {0} deleted'.format(name)
828 return ret
829
830
831def cluster_meta_set(name, value, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000832 '''
833 Create cluster level override entry
834
835 :param name: name of the override entry (dictionary key)
836 :param value: value of the override entry (dictionary value)
837 :param file_name: name of the override file, defaults to: overrides.yml
838
839 CLI Examples:
840
841 .. code-block:: bash
842
843 salt-call reclass.cluster_meta_set foo bar
844
845 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000846 path = os.path.join(_get_cluster_dir(), cluster, file_name)
847 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
848 if 'Error' not in meta:
849 if not meta:
850 meta = {'parameters': {'_param': {}}}
851 metadata = meta.get('parameters', {}).get('_param', {})
852 if name in metadata and metadata[name] == value:
853 return {name: 'Cluster metadata entry %s already exists and is in correct state' % name}
854 metadata.update({name: value})
855 try:
856 with io.open(path, 'w') as file_handle:
857 file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
858 except Exception as e:
859 msg = "Unable to save cluster metadata YAML %s: %s" % (path, repr(e))
860 LOG.error(msg)
861 return {'Error': msg}
862 return cluster_meta_get(name, path, **kwargs)
863 return meta
864
865
866def cluster_meta_get(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000867 '''
868 Get single cluster level override entry
869
870 :param name: name of the override entry (dictionary key)
871 :param file_name: name of the override file, defaults to: overrides.yml
872
873 CLI Examples:
874
875 .. code-block:: bash
876
877 salt-call reclass.cluster_meta_get foo
878
879 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000880 ret = {}
881 path = os.path.join(_get_cluster_dir(), cluster, file_name)
882 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
883 metadata = meta.get('parameters', {}).get('_param', {})
884 if 'Error' in meta:
885 ret['Error'] = meta['Error']
886 elif name in metadata:
887 ret[name] = metadata.get(name)
888
889 return ret
890