blob: f15eb44cdee46dd5485722b8f1ef85e0a49028db [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
car-da0da41492017-08-25 11:01:26 +0200561 salt '*' reclass.node_update name=nodename classes="[clas1, class2]" parameters="{param: value, another_param: another_value}"
Ales Komarek166cc672016-07-27 14:17:22 +0200562 '''
563 node = node_get(name=name)
car-da0da41492017-08-25 11:01:26 +0200564 if node.has_key('Error'):
Ales Komarek71f94b02016-07-27 14:48:57 +0200565 return {'Error': 'Error in retrieving node'}
car-da0da41492017-08-25 11:01:26 +0200566
567 for name, values in node.items():
568 param = values.get('parameters', {})
569 path = values.get('path')
570 cluster = values.get('cluster')
571 environment = values.get('environment')
572 write_class = values.get('classes', [])
573
574 if parameters:
575 param.update(parameters)
576
577 if classes:
578 for classe in classes:
579 if not classe in write_class:
580 write_class.append(classe)
581
582 node_meta = _get_node_meta(name, cluster, environment, write_class, param)
583 LOG.debug(node_meta)
584
585 if path == None:
586 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
587 else:
588 file_path = os.path.join(_get_nodes_dir(), path, name + '.yml')
589
590 with open(file_path, 'w') as node_file:
591 node_file.write(yaml.safe_dump(node_meta, default_flow_style=False))
592
593 return node_get(name)
Ales Komareka4a9f572016-12-03 20:15:50 +0100594
595
Adam Tengler23d965f2017-05-16 19:14:51 +0000596def _get_node_classes(node_data, class_mapping_fragment):
597 classes = []
598
599 for value_tmpl_string in class_mapping_fragment.get('value_template', []):
600 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
601 rendered_value = value_tmpl.safe_substitute(node_data)
602 classes.append(rendered_value)
603
604 for value in class_mapping_fragment.get('value', []):
605 classes.append(value)
606
607 return classes
608
609
610def _get_params(node_data, class_mapping_fragment):
611 params = {}
612
613 for param_name, param in class_mapping_fragment.items():
614 value = param.get('value', None)
615 value_tmpl_string = param.get('value_template', None)
616 if value:
617 params.update({param_name: value})
618 elif value_tmpl_string:
619 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
620 rendered_value = value_tmpl.safe_substitute(node_data)
621 params.update({param_name: rendered_value})
622
623 return params
624
625
Adam Tengler4d961142017-07-27 15:35:28 +0000626def _validate_condition(node_data, expressions):
627 # allow string expression definition for single expression conditions
628 if isinstance(expressions, six.string_types):
629 expressions = [expressions]
Adam Tengler23d965f2017-05-16 19:14:51 +0000630
Adam Tengler4d961142017-07-27 15:35:28 +0000631 result = []
632 for expression_tmpl_string in expressions:
633 expression_tmpl = Template(expression_tmpl_string.replace('<<', '${').replace('>>', '}'))
634 expression = expression_tmpl.safe_substitute(node_data)
Adam Tengler23d965f2017-05-16 19:14:51 +0000635
Adam Tengler4d961142017-07-27 15:35:28 +0000636 if expression and expression == 'all':
637 result.append(True)
638 elif expression:
639 val_a = expression.split('__')[0]
640 val_b = expression.split('__')[2]
641 condition = expression.split('__')[1]
642 if condition == 'startswith':
643 result.append(val_a.startswith(val_b))
644 elif condition == 'equals':
645 result.append(val_a == val_b)
646
647 return all(result)
Adam Tengler23d965f2017-05-16 19:14:51 +0000648
649
650def node_classify(node_name, node_data={}, class_mapping={}, **kwargs):
651 '''
652 CLassify node by given class_mapping dictionary
653
654 :param node_name: node FQDN
655 :param node_data: dictionary of known informations about the node
656 :param class_mapping: dictionary of classes and parameters, with conditions
657
658 '''
659 # clean node_data
660 node_data = {k: v for (k, v) in node_data.items() if not k.startswith('__')}
661
662 classes = []
663 node_params = {}
664 cluster_params = {}
665 ret = {'node_create': '', 'cluster_param': {}}
666
667 for type_name, node_type in class_mapping.items():
668 valid = _validate_condition(node_data, node_type.get('expression', ''))
669 if valid:
670 gen_classes = _get_node_classes(node_data, node_type.get('node_class', {}))
671 classes = classes + gen_classes
672 gen_node_params = _get_params(node_data, node_type.get('node_param', {}))
673 node_params.update(gen_node_params)
674 gen_cluster_params = _get_params(node_data, node_type.get('cluster_param', {}))
675 cluster_params.update(gen_cluster_params)
676
677 if classes:
678 create_kwargs = {'name': node_name, 'path': '_generated', 'classes': classes, 'parameters': node_params}
679 ret['node_create'] = node_create(**create_kwargs)
680
681 for name, value in cluster_params.items():
682 ret['cluster_param'][name] = cluster_meta_set(name, value)
683
684 return ret
685
686
Ales Komarekb0911892017-08-02 15:47:30 +0200687def validate_yaml():
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200688 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200689 Returns list of all reclass YAML files that contain syntax
690 errors.
691
692 CLI Examples:
693
694 .. code-block:: bash
695
696 salt-call reclass.validate_yaml
697 '''
698 errors = _deps(ret_classes=False, ret_errors=True)
699 if errors:
700 ret = {'Errors': errors}
701 return ret
702
703
704def validate_pillar(node_name=None, **kwargs):
705 '''
706 Validates whether the pillar of given node is in correct state.
707 If node is not specified it validates pillars of all known nodes.
708 Returns error message for every node with currupted metadata.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200709
710 :param node_name: target minion ID
711
712 CLI Examples:
713
714 .. code-block:: bash
715
Ales Komarekb0911892017-08-02 15:47:30 +0200716 salt-call reclass.validate_pillar
717 salt-call reclass.validate_pillar minion-id
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200718 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200719 if node_name is None:
720 ret={}
721 nodes = node_list(**kwargs)
722 for node_name, node in nodes.items():
723 ret.update(validate_pillar(node_name))
724 return ret
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200725 else:
Ales Komarekb0911892017-08-02 15:47:30 +0200726 defaults = find_and_read_configfile()
727 meta = ''
728 error = None
729 try:
730 pillar = ext_pillar(node_name, {}, defaults['storage_type'], defaults['inventory_base_uri'])
731 except (ReclassException, Exception) as e:
732 msg = "Validation failed in %s on %s" % (repr(e), node_name)
733 LOG.error(msg)
734 meta = {'Error': msg}
735 s = str(type(e))
736 if 'yaml.scanner.ScannerError' in s:
737 error = re.sub(r"\r?\n?$", "", repr(str(e)), 1)
738 else:
739 error = e.message
740 if 'Error' in meta:
741 ret = {node_name: error}
742 else:
743 ret = {node_name: ''}
744 return ret
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200745
746
Adam Tengler2b362622017-06-01 14:23:45 +0000747def node_pillar(node_name, **kwargs):
748 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200749 Returns pillar metadata for given node from reclass inventory.
Adam Tengler2b362622017-06-01 14:23:45 +0000750
751 :param node_name: target minion ID
752
753 CLI Examples:
754
755 .. code-block:: bash
756
757 salt-call reclass.node_pillar minion_id
758
759 '''
760 defaults = find_and_read_configfile()
761 pillar = ext_pillar(node_name, {}, defaults['storage_type'], defaults['inventory_base_uri'])
762 output = {node_name: pillar}
763
764 return output
765
766
Ales Komareka4a9f572016-12-03 20:15:50 +0100767def inventory(**connection_args):
768 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200769 Get all nodes in inventory and their associated services/roles.
Ales Komareka4a9f572016-12-03 20:15:50 +0100770
771 CLI Examples:
772
773 .. code-block:: bash
774
775 salt '*' reclass.inventory
776 '''
777 defaults = find_and_read_configfile()
778 storage = get_storage(defaults['storage_type'], _get_nodes_dir(), _get_classes_dir())
779 reclass = Core(storage, None)
780 nodes = reclass.inventory()["nodes"]
781 output = {}
782
783 for node in nodes:
784 service_classification = []
785 role_classification = []
786 for service in nodes[node]['parameters']:
787 if service not in ['_param', 'private_keys', 'public_keys', 'known_hosts']:
788 service_classification.append(service)
789 for role in nodes[node]['parameters'][service]:
790 if role not in ['_support', '_orchestrate', 'common']:
791 role_classification.append('%s.%s' % (service, role))
792 output[node] = {
793 'roles': role_classification,
794 'services': service_classification,
795 }
796 return output
Adam Tengler8a1cf402017-05-16 10:59:35 +0000797
798
799def cluster_meta_list(file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000800 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200801 List all cluster level soft metadata overrides.
Adam Tengler2b362622017-06-01 14:23:45 +0000802
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_list
Adam Tengler2b362622017-06-01 14:23:45 +0000810 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000811 path = os.path.join(_get_cluster_dir(), cluster, file_name)
812 try:
813 with io.open(path, 'r') as file_handle:
814 meta_yaml = yaml.safe_load(file_handle.read())
815 meta = meta_yaml or {}
816 except Exception as e:
817 msg = "Unable to load cluster metadata YAML %s: %s" % (path, repr(e))
818 LOG.debug(msg)
819 meta = {'Error': msg}
820 return meta
821
822
823def cluster_meta_delete(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000824 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200825 Delete cluster level soft metadata override entry.
Adam Tengler2b362622017-06-01 14:23:45 +0000826
827 :param name: name of the override entry (dictionary key)
828 :param file_name: name of the override file, defaults to: overrides.yml
829
830 CLI Examples:
831
832 .. code-block:: bash
833
834 salt-call reclass.cluster_meta_delete foo
Adam Tengler2b362622017-06-01 14:23:45 +0000835 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000836 ret = {}
837 path = os.path.join(_get_cluster_dir(), cluster, file_name)
838 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
839 if 'Error' not in meta:
840 metadata = meta.get('parameters', {}).get('_param', {})
841 if name not in metadata:
842 return ret
843 del metadata[name]
844 try:
845 with io.open(path, 'w') as file_handle:
846 file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
847 except Exception as e:
848 msg = "Unable to save cluster metadata YAML: %s" % repr(e)
849 LOG.error(msg)
850 return {'Error': msg}
851 ret = 'Cluster metadata entry {0} deleted'.format(name)
852 return ret
853
854
855def cluster_meta_set(name, value, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000856 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200857 Create cluster level metadata override entry.
Adam Tengler2b362622017-06-01 14:23:45 +0000858
859 :param name: name of the override entry (dictionary key)
860 :param value: value of the override entry (dictionary value)
861 :param file_name: name of the override file, defaults to: overrides.yml
862
863 CLI Examples:
864
865 .. code-block:: bash
866
867 salt-call reclass.cluster_meta_set foo bar
Adam Tengler2b362622017-06-01 14:23:45 +0000868 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000869 path = os.path.join(_get_cluster_dir(), cluster, file_name)
870 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
871 if 'Error' not in meta:
872 if not meta:
873 meta = {'parameters': {'_param': {}}}
874 metadata = meta.get('parameters', {}).get('_param', {})
875 if name in metadata and metadata[name] == value:
876 return {name: 'Cluster metadata entry %s already exists and is in correct state' % name}
877 metadata.update({name: value})
878 try:
879 with io.open(path, 'w') as file_handle:
880 file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
881 except Exception as e:
882 msg = "Unable to save cluster metadata YAML %s: %s" % (path, repr(e))
883 LOG.error(msg)
884 return {'Error': msg}
885 return cluster_meta_get(name, path, **kwargs)
886 return meta
887
888
889def cluster_meta_get(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000890 '''
891 Get single cluster level override entry
892
893 :param name: name of the override entry (dictionary key)
894 :param file_name: name of the override file, defaults to: overrides.yml
895
896 CLI Examples:
897
898 .. code-block:: bash
899
900 salt-call reclass.cluster_meta_get foo
901
902 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000903 ret = {}
904 path = os.path.join(_get_cluster_dir(), cluster, file_name)
905 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
906 metadata = meta.get('parameters', {}).get('_param', {})
907 if 'Error' in meta:
908 ret['Error'] = meta['Error']
909 elif name in metadata:
910 ret[name] = metadata.get(name)
911
912 return ret
913