blob: f32bf0ad2db7ca97128fe1f48bcdb8cff8af407e [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 '''
Petr Michalecd9a3d4b2018-08-28 10:50:53 +020044 defaults = __salt__['config.get']('reclass', None) or find_and_read_configfile()
Jiri Broulik7ccb5342017-07-20 17:07:47 +020045 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():
azvyagintsevf5264d52017-12-12 11:49:42 +020069 LOG.debug("Processing:{}".format(params['file']))
Jiri Broulik7ccb5342017-07-20 17:07:47 +020070 with open(params['file'], 'r') as f:
71 # read raw data
72 raw = f.read()
73 pr = re.findall('\${_param:(.*?)}', raw)
74 if pr:
75 params['params_required'] = list(set(pr))
76
77 # load yaml
78 try:
79 data = yaml.load(raw)
80 except yaml.scanner.ScannerError as e:
81 errors.append(params['file'] + ' ' + str(e))
82 pass
83
84 if type(data) == dict:
85 if data.get('classes'):
86 params['includes'] = data.get('classes', [])
87 if data.get('parameters') and data['parameters'].get('_param'):
88 params['params_created'] = data['parameters']['_param']
89
90 if not(data.get('classes') or data.get('parameters')):
91 errors.append(params['file'] + ' ' + 'file missing classes and parameters')
92 else:
93 errors.append(params['file'] + ' ' + 'is not valid yaml')
94
95 if ret_classes:
96 return classes
97 elif ret_errors:
98 return errors
99
100 # find parameters and its usage
101 for class_name, params in classes.items():
102 for pn, pv in params.get('params_created', {}).items():
103 # create param if missing
104 if pn not in soft_params:
105 soft_params[pn] = {'created_at': {}, 'required_at': []}
106
107 # add created_at
108 if class_name not in soft_params[pn]['created_at']:
109 soft_params[pn]['created_at'][class_name] = pv
110
111 for pn in params.get('params_required', []):
112 # create param if missing
113 if pn not in soft_params:
114 soft_params[pn] = {'created_at': {}, 'required_at': []}
115
116 # add created_at
117 soft_params[pn]['required_at'].append(class_name)
118
119 return soft_params
120
121
Ales Komareka4a9f572016-12-03 20:15:50 +0100122def _get_nodes_dir():
Petr Michalecd9a3d4b2018-08-28 10:50:53 +0200123 defaults = __salt__['config.get']('reclass', None) or find_and_read_configfile()
124 return defaults.get('nodes_uri') or os.path.join(defaults.get('inventory_base_uri'), 'nodes')
Ales Komareka4a9f572016-12-03 20:15:50 +0100125
126def _get_classes_dir():
Petr Michalecd9a3d4b2018-08-28 10:50:53 +0200127 defaults = __salt__['config.get']('reclass', None) or find_and_read_configfile()
128 return defaults.get('classes_uri') or os.path.join(defaults.get('inventory_base_uri'), 'classes')
Ales Komarek166cc672016-07-27 14:17:22 +0200129
130
Adam Tengler8a1cf402017-05-16 10:59:35 +0000131def _get_cluster_dir():
132 classes_dir = _get_classes_dir()
133 return os.path.join(classes_dir, 'cluster')
134
135
Adam Tengler805666d2017-05-15 16:01:13 +0000136def _get_node_meta(name, cluster="default", environment="prd", classes=None, parameters=None):
137 host_name = name.split('.')[0]
138 domain_name = '.'.join(name.split('.')[1:])
139
140 if classes == None:
141 meta_classes = []
142 else:
143 if isinstance(classes, six.string_types):
144 meta_classes = json.loads(classes)
145 else:
146 meta_classes = classes
147
148 if parameters == None:
149 meta_parameters = {}
150 else:
151 if isinstance(parameters, six.string_types):
152 meta_parameters = json.loads(parameters)
153 else:
154 # generate dict from OrderedDict
155 meta_parameters = {k: v for (k, v) in parameters.items()}
156
157 node_meta = {
158 'classes': meta_classes,
159 'parameters': {
160 '_param': meta_parameters,
161 'linux': {
162 'system': {
163 'name': host_name,
164 'domain': domain_name,
165 'cluster': cluster,
166 'environment': environment,
167 }
168 }
169 }
170 }
171
172 return node_meta
173
174
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200175def soft_meta_list():
176 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200177 Returns all defined soft metadata parameters.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200178
179 CLI Examples:
180
181 .. code-block:: bash
182
183 salt '*' reclass.soft_meta_list
184 '''
185 return _deps(ret_classes=False)
186
187
188def class_list():
189 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200190 Returns list of all classes defined within reclass inventory.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200191
192 CLI Examples:
193
194 .. code-block:: bash
195
196 salt '*' reclass.class_list
197 '''
198 return _deps(ret_classes=True)
199
200
201def soft_meta_get(name):
202 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200203 Returns single soft metadata parameter.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200204
Ales Komarekb0911892017-08-02 15:47:30 +0200205 :param name: expects the following format: apt_mk_version
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200206
207 CLI Examples:
208
209 .. code-block:: bash
210
Ales Komarekb0911892017-08-02 15:47:30 +0200211 salt '*' reclass.soft_meta_get openstack_version
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200212 '''
213 soft_params = _deps(ret_classes=False)
214
215 if name in soft_params:
Ales Komarekb0911892017-08-02 15:47:30 +0200216 return {name: soft_params.get(name)}
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200217 else:
Ales Komarekb0911892017-08-02 15:47:30 +0200218 return {'Error': 'No param {0} found'.format(name)}
219
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200220
221def class_get(name):
222 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200223 Returns detailes information about class file in reclass inventory.
224
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200225 :param name: expects the following format classes.system.linux.repo
226
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200227 CLI Examples:
228
229 .. code-block:: bash
230
231 salt '*' reclass.class_get classes.system.linux.repo
232 '''
233 classes = _deps(ret_classes=True)
234 tmp_name = '.' + name
235 if tmp_name in classes:
Ales Komarekb0911892017-08-02 15:47:30 +0200236 return {name: classes.get(tmp_name)}
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200237 else:
Ales Komarekb0911892017-08-02 15:47:30 +0200238 return {'Error': 'No class {0} found'.format(name)}
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200239
240
Ales Komarek166cc672016-07-27 14:17:22 +0200241def node_create(name, path=None, cluster="default", environment="prd", classes=None, parameters=None, **kwargs):
242 '''
243 Create a reclass node
244
245 :param name: new node FQDN
246 :param path: custom path in nodes for new node
247 :param classes: classes given to the new node
248 :param parameters: parameters given to the new node
249 :param environment: node's environment
250 :param cluster: node's cluster
251
252 CLI Examples:
253
254 .. code-block:: bash
255
256 salt '*' reclass.node_create server.domain.com classes=[system.neco1, system.neco2]
257 salt '*' reclass.node_create namespace/test enabled=False
Petr Michalec46a5bad2017-09-18 20:11:43 +0200258
Ales Komarek166cc672016-07-27 14:17:22 +0200259 '''
260 ret = {}
261
262 node = node_get(name=name)
263
264 if node and not "Error" in node:
265 LOG.debug("node {0} exists".format(name))
266 ret[name] = node
267 return ret
268
269 host_name = name.split('.')[0]
270 domain_name = '.'.join(name.split('.')[1:])
271
Adam Tengler805666d2017-05-15 16:01:13 +0000272 node_meta = _get_node_meta(name, cluster, environment, classes, parameters)
Ales Komarek166cc672016-07-27 14:17:22 +0200273 LOG.debug(node_meta)
274
275 if path == None:
Ales Komareka4a9f572016-12-03 20:15:50 +0100276 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200277 else:
Ales Komareka4a9f572016-12-03 20:15:50 +0100278 file_path = os.path.join(_get_nodes_dir(), path, name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200279
280 with open(file_path, 'w') as node_file:
Ales Komareka961df42016-11-21 21:50:24 +0100281 node_file.write(yaml.safe_dump(node_meta, default_flow_style=False))
Ales Komarek71f94b02016-07-27 14:48:57 +0200282
Ales Komarek166cc672016-07-27 14:17:22 +0200283 return node_get(name)
284
Ales Komareka4a9f572016-12-03 20:15:50 +0100285
Ales Komarek166cc672016-07-27 14:17:22 +0200286def node_delete(name, **kwargs):
287 '''
288 Delete a reclass node
289
290 :params node: Node name
291
292 CLI Examples:
293
294 .. code-block:: bash
295
296 salt '*' reclass.node_delete demo01.domain.com
297 salt '*' reclass.node_delete name=demo01.domain.com
298 '''
299
300 node = node_get(name=name)
301
302 if 'Error' in node:
303 return {'Error': 'Unable to retreive node'}
304
305 if node[name]['path'] == '':
Ales Komareka4a9f572016-12-03 20:15:50 +0100306 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200307 else:
Ales Komareka4a9f572016-12-03 20:15:50 +0100308 file_path = os.path.join(_get_nodes_dir(), node[name]['path'], name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200309
310 os.remove(file_path)
311
312 ret = 'Node {0} deleted'.format(name)
313
314 return ret
315
316
317def node_get(name, path=None, **kwargs):
318 '''
319 Return a specific node
320
321 CLI Examples:
322
323 .. code-block:: bash
324
325 salt '*' reclass.node_get host01.domain.com
326 salt '*' reclass.node_get name=host02.domain.com
327 '''
328 ret = {}
329 nodes = node_list(**kwargs)
330
331 if not name in nodes:
332 return {'Error': 'Error in retrieving node'}
333 ret[name] = nodes[name]
334 return ret
335
336
337def node_list(**connection_args):
338 '''
339 Return a list of available nodes
340
341 CLI Example:
342
343 .. code-block:: bash
344
345 salt '*' reclass.node_list
346 '''
347 ret = {}
348
Ales Komareka4a9f572016-12-03 20:15:50 +0100349 for root, sub_folders, files in os.walk(_get_nodes_dir()):
Petr Michalec46a5bad2017-09-18 20:11:43 +0200350 # skip hidden files and folders in reclass dir
351 files = [f for f in files if not f[0] == '.']
Petr Michalec55a43322017-09-19 17:49:56 +0200352 sub_folders[:] = [d for d in sub_folders if not d[0] == '.']
Adam Tengler805666d2017-05-15 16:01:13 +0000353 for fl in files:
354 file_path = os.path.join(root, fl)
355 with open(file_path, 'r') as file_handle:
356 file_read = yaml.load(file_handle.read())
357 file_data = file_read or {}
358 classes = file_data.get('classes', [])
359 parameters = file_data.get('parameters', {}).get('_param', [])
360 name = fl.replace('.yml', '')
Ales Komarek166cc672016-07-27 14:17:22 +0200361 host_name = name.split('.')[0]
362 domain_name = '.'.join(name.split('.')[1:])
Ales Komareka4a9f572016-12-03 20:15:50 +0100363 path = root.replace(_get_nodes_dir()+'/', '')
Ales Komarek166cc672016-07-27 14:17:22 +0200364 ret[name] = {
Ales Komareka4a9f572016-12-03 20:15:50 +0100365 'name': host_name,
366 'domain': domain_name,
367 'cluster': 'default',
368 'environment': 'prd',
369 'path': path,
370 'classes': classes,
371 'parameters': parameters
Ales Komarek166cc672016-07-27 14:17:22 +0200372 }
373
374 return ret
375
Ales Komareka4a9f572016-12-03 20:15:50 +0100376
Adam Tengler2b362622017-06-01 14:23:45 +0000377def _is_valid_ipv4_address(address):
378 try:
379 socket.inet_pton(socket.AF_INET, address)
380 except AttributeError:
381 try:
382 socket.inet_aton(address)
383 except socket.error:
384 return False
385 return address.count('.') == 3
386 except socket.error:
387 return False
388 return True
389
390
391def _is_valid_ipv6_address(address):
392 try:
393 socket.inet_pton(socket.AF_INET6, address)
394 except socket.error:
395 return False
396 return True
397
398
Adam Tengler1f7667b2017-06-06 16:45:51 +0000399def _get_grains(*args, **kwargs):
400 res = __salt__['saltutil.cmd'](tgt='*',
401 fun='grains.item',
402 arg=args,
403 **{'timeout': 10})
404 return res or {}
405
406
407def _guess_host_from_target(network_grains, host, domain=' '):
Adam Tengler2b362622017-06-01 14:23:45 +0000408 '''
409 Guess minion ID from given host and domain arguments. Host argument can contain
410 hostname, FQDN, IPv4 or IPv6 addresses.
411 '''
Adam Tengler1f7667b2017-06-06 16:45:51 +0000412 key = None
413 value = None
414
Adam Tengler2b362622017-06-01 14:23:45 +0000415 if _is_valid_ipv4_address(host):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000416 key = 'ipv4'
417 value = host
Adam Tengler2b362622017-06-01 14:23:45 +0000418 elif _is_valid_ipv6_address(host):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000419 key = 'ipv6'
420 value = host
Adam Tengler2b362622017-06-01 14:23:45 +0000421 elif host.endswith(domain):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000422 key = 'fqdn'
423 value = host
Adam Tengler2b362622017-06-01 14:23:45 +0000424 else:
Adam Tengler1f7667b2017-06-06 16:45:51 +0000425 key = 'fqdn'
426 value = '%s.%s' % (host, domain)
Adam Tengler2b362622017-06-01 14:23:45 +0000427
Adam Tengler1f7667b2017-06-06 16:45:51 +0000428 target = None
429 if network_grains and isinstance(network_grains, dict) and key and value:
430 for minion, grains in network_grains.items():
431 if grains.get('retcode', 1) == 0 and value in grains.get('ret', {}).get(key, ''):
432 target = minion
Adam Tengler2b362622017-06-01 14:23:45 +0000433
Adam Tengler1f7667b2017-06-06 16:45:51 +0000434 return target or host
Adam Tengler2b362622017-06-01 14:23:45 +0000435
436
437def _interpolate_graph_data(graph_data, **kwargs):
438 new_nodes = []
Adam Tengler1f7667b2017-06-06 16:45:51 +0000439 network_grains = _get_grains('ipv4', 'ipv6', 'fqdn')
Adam Tengler2b362622017-06-01 14:23:45 +0000440 for node in graph_data:
Adam Tengler69c7ba92017-06-01 15:59:01 +0000441 if not node.get('relations', []):
442 node['relations'] = []
Adam Tengler2b362622017-06-01 14:23:45 +0000443 for relation in node.get('relations', []):
Adam Tengler12a310d2017-06-05 19:11:29 +0000444 if not relation.get('status', None):
445 relation['status'] = 'unknown'
Adam Tengler2b362622017-06-01 14:23:45 +0000446 if relation.get('host_from_target', None):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000447 host = _guess_host_from_target(network_grains, relation.pop('host_from_target'))
Adam Tengler2b362622017-06-01 14:23:45 +0000448 relation['host'] = host
449 if relation.get('host_external', None):
Ales Komarekb0911892017-08-02 15:47:30 +0200450 parsed_host_external = [urlparse.urlparse(item).netloc
Adam Tengler69c7ba92017-06-01 15:59:01 +0000451 for item
452 in relation.get('host_external', '').split(' ')
Ales Komarekb0911892017-08-02 15:47:30 +0200453 if urlparse.urlparse(item).netloc]
Adam Tengler69c7ba92017-06-01 15:59:01 +0000454 service = parsed_host_external[0] if parsed_host_external else ''
Adam Tengler2b362622017-06-01 14:23:45 +0000455 host = relation.get('service', '')
456 relation['host'] = host
Adam Tengler69c7ba92017-06-01 15:59:01 +0000457 del relation['host_external']
Adam Tengler2b362622017-06-01 14:23:45 +0000458 relation['service'] = service
Adam Tengler69c7ba92017-06-01 15:59:01 +0000459 host_list = [n.get('host', '') for n in graph_data + new_nodes]
460 service_list = [n.get('service', '') for n in graph_data + new_nodes if host in n.get('host', '')]
461 if host not in host_list or (host in host_list and service not in service_list):
Adam Tengler2b362622017-06-01 14:23:45 +0000462 new_node = {
463 'host': host,
464 'service': service,
465 'type': relation.get('type', ''),
466 'relations': []
467 }
468 new_nodes.append(new_node)
469
470 graph_data = graph_data + new_nodes
471
472 return graph_data
473
474
475def _grain_graph_data(*args, **kwargs):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000476 ret = _get_grains('salt:graph')
Adam Tengler2b362622017-06-01 14:23:45 +0000477 graph_data = []
478 for minion_ret in ret.values():
479 if minion_ret.get('retcode', 1) == 0:
480 graph_datum = minion_ret.get('ret', {}).get('salt:graph', [])
481 graph_data = graph_data + graph_datum
482
483 graph_nodes = _interpolate_graph_data(graph_data)
484 graph = {}
485
486 for node in graph_nodes:
487 if node.get('host') not in graph:
488 graph[node.get('host')] = {}
489 graph[node.pop('host')][node.pop('service')] = node
490
491 return {'graph': graph}
492
493
494def _pillar_graph_data(*args, **kwargs):
495 graph = {}
496 nodes = inventory()
497 for node, node_data in nodes.items():
498 for role in node_data.get('roles', []):
499 if node not in graph:
500 graph[node] = {}
501 graph[node][role] = {'relations': []}
502
503 return {'graph': graph}
504
505
506def graph_data(*args, **kwargs):
507 '''
508 Returns graph data for visualization app
509
510 CLI Examples:
511
512 .. code-block:: bash
513
Adam Tengler1f7667b2017-06-06 16:45:51 +0000514 salt-call reclass.graph_data
Petr Michalec46a5bad2017-09-18 20:11:43 +0200515
Adam Tengler2b362622017-06-01 14:23:45 +0000516 '''
517 pillar_data = _pillar_graph_data().get('graph')
518 grain_data = _grain_graph_data().get('graph')
519
520 for host, services in pillar_data.items():
521 for service, service_data in services.items():
522 grain_service = grain_data.get(host, {}).get(service, {})
523 service_data.update(grain_service)
524
525 graph = []
526 for host, services in pillar_data.items():
527 for service, service_data in services.items():
528 additional_data = {
529 'host': host,
Adam Tengler12a310d2017-06-05 19:11:29 +0000530 'service': service,
531 'status': 'unknown'
Adam Tengler2b362622017-06-01 14:23:45 +0000532 }
533 service_data.update(additional_data)
534 graph.append(service_data)
535
536 for host, services in grain_data.items():
537 for service, service_data in services.items():
538 additional_data = {
539 'host': host,
Adam Tengler12a310d2017-06-05 19:11:29 +0000540 'service': service,
541 'status': 'success'
Adam Tengler2b362622017-06-01 14:23:45 +0000542 }
543 service_data.update(additional_data)
544 host_list = [g.get('host', '') for g in graph]
545 service_list = [g.get('service', '') for g in graph if g.get('host') == host]
546 if host not in host_list or (host in host_list and service not in service_list):
547 graph.append(service_data)
548
549 return {'graph': graph}
550
551
Ales Komarek166cc672016-07-27 14:17:22 +0200552def node_update(name, classes=None, parameters=None, **connection_args):
553 '''
554 Update a node metadata information, classes and parameters.
555
556 CLI Examples:
557
558 .. code-block:: bash
559
car-da0da41492017-08-25 11:01:26 +0200560 salt '*' reclass.node_update name=nodename classes="[clas1, class2]" parameters="{param: value, another_param: another_value}"
Ales Komarek166cc672016-07-27 14:17:22 +0200561 '''
562 node = node_get(name=name)
car-da0da41492017-08-25 11:01:26 +0200563 if node.has_key('Error'):
Ales Komarek71f94b02016-07-27 14:48:57 +0200564 return {'Error': 'Error in retrieving node'}
azvyagintsevf5264d52017-12-12 11:49:42 +0200565
car-da0da41492017-08-25 11:01:26 +0200566 for name, values in node.items():
567 param = values.get('parameters', {})
568 path = values.get('path')
569 cluster = values.get('cluster')
570 environment = values.get('environment')
571 write_class = values.get('classes', [])
azvyagintsevf5264d52017-12-12 11:49:42 +0200572
car-da0da41492017-08-25 11:01:26 +0200573 if parameters:
574 param.update(parameters)
azvyagintsevf5264d52017-12-12 11:49:42 +0200575
car-da0da41492017-08-25 11:01:26 +0200576 if classes:
577 for classe in classes:
578 if not classe in write_class:
579 write_class.append(classe)
azvyagintsevf5264d52017-12-12 11:49:42 +0200580
car-da0da41492017-08-25 11:01:26 +0200581 node_meta = _get_node_meta(name, cluster, environment, write_class, param)
582 LOG.debug(node_meta)
azvyagintsevf5264d52017-12-12 11:49:42 +0200583
car-da0da41492017-08-25 11:01:26 +0200584 if path == None:
585 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
586 else:
587 file_path = os.path.join(_get_nodes_dir(), path, name + '.yml')
azvyagintsevf5264d52017-12-12 11:49:42 +0200588
car-da0da41492017-08-25 11:01:26 +0200589 with open(file_path, 'w') as node_file:
590 node_file.write(yaml.safe_dump(node_meta, default_flow_style=False))
azvyagintsevf5264d52017-12-12 11:49:42 +0200591
car-da0da41492017-08-25 11:01:26 +0200592 return node_get(name)
Ales Komareka4a9f572016-12-03 20:15:50 +0100593
594
Adam Tengler23d965f2017-05-16 19:14:51 +0000595def _get_node_classes(node_data, class_mapping_fragment):
596 classes = []
597
598 for value_tmpl_string in class_mapping_fragment.get('value_template', []):
599 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
600 rendered_value = value_tmpl.safe_substitute(node_data)
601 classes.append(rendered_value)
602
603 for value in class_mapping_fragment.get('value', []):
604 classes.append(value)
605
606 return classes
607
608
609def _get_params(node_data, class_mapping_fragment):
610 params = {}
611
612 for param_name, param in class_mapping_fragment.items():
613 value = param.get('value', None)
614 value_tmpl_string = param.get('value_template', None)
615 if value:
616 params.update({param_name: value})
617 elif value_tmpl_string:
618 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
619 rendered_value = value_tmpl.safe_substitute(node_data)
620 params.update({param_name: rendered_value})
621
622 return params
623
624
Adam Tengler4d961142017-07-27 15:35:28 +0000625def _validate_condition(node_data, expressions):
626 # allow string expression definition for single expression conditions
627 if isinstance(expressions, six.string_types):
628 expressions = [expressions]
Adam Tengler23d965f2017-05-16 19:14:51 +0000629
Adam Tengler4d961142017-07-27 15:35:28 +0000630 result = []
631 for expression_tmpl_string in expressions:
632 expression_tmpl = Template(expression_tmpl_string.replace('<<', '${').replace('>>', '}'))
633 expression = expression_tmpl.safe_substitute(node_data)
Adam Tengler23d965f2017-05-16 19:14:51 +0000634
Adam Tengler4d961142017-07-27 15:35:28 +0000635 if expression and expression == 'all':
636 result.append(True)
637 elif expression:
638 val_a = expression.split('__')[0]
639 val_b = expression.split('__')[2]
640 condition = expression.split('__')[1]
641 if condition == 'startswith':
642 result.append(val_a.startswith(val_b))
643 elif condition == 'equals':
644 result.append(val_a == val_b)
645
646 return all(result)
Adam Tengler23d965f2017-05-16 19:14:51 +0000647
648
649def node_classify(node_name, node_data={}, class_mapping={}, **kwargs):
650 '''
651 CLassify node by given class_mapping dictionary
652
653 :param node_name: node FQDN
654 :param node_data: dictionary of known informations about the node
655 :param class_mapping: dictionary of classes and parameters, with conditions
656
657 '''
658 # clean node_data
659 node_data = {k: v for (k, v) in node_data.items() if not k.startswith('__')}
660
661 classes = []
662 node_params = {}
663 cluster_params = {}
664 ret = {'node_create': '', 'cluster_param': {}}
665
666 for type_name, node_type in class_mapping.items():
667 valid = _validate_condition(node_data, node_type.get('expression', ''))
668 if valid:
669 gen_classes = _get_node_classes(node_data, node_type.get('node_class', {}))
670 classes = classes + gen_classes
671 gen_node_params = _get_params(node_data, node_type.get('node_param', {}))
672 node_params.update(gen_node_params)
673 gen_cluster_params = _get_params(node_data, node_type.get('cluster_param', {}))
674 cluster_params.update(gen_cluster_params)
675
676 if classes:
677 create_kwargs = {'name': node_name, 'path': '_generated', 'classes': classes, 'parameters': node_params}
678 ret['node_create'] = node_create(**create_kwargs)
679
680 for name, value in cluster_params.items():
681 ret['cluster_param'][name] = cluster_meta_set(name, value)
682
683 return ret
684
685
Ales Komarekb0911892017-08-02 15:47:30 +0200686def validate_yaml():
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200687 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200688 Returns list of all reclass YAML files that contain syntax
689 errors.
690
691 CLI Examples:
692
693 .. code-block:: bash
694
695 salt-call reclass.validate_yaml
696 '''
697 errors = _deps(ret_classes=False, ret_errors=True)
698 if errors:
699 ret = {'Errors': errors}
700 return ret
701
702
703def validate_pillar(node_name=None, **kwargs):
704 '''
705 Validates whether the pillar of given node is in correct state.
706 If node is not specified it validates pillars of all known nodes.
707 Returns error message for every node with currupted metadata.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200708
709 :param node_name: target minion ID
710
711 CLI Examples:
712
713 .. code-block:: bash
714
Ales Komarekb0911892017-08-02 15:47:30 +0200715 salt-call reclass.validate_pillar
716 salt-call reclass.validate_pillar minion-id
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200717 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200718 if node_name is None:
719 ret={}
720 nodes = node_list(**kwargs)
721 for node_name, node in nodes.items():
722 ret.update(validate_pillar(node_name))
723 return ret
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200724 else:
Petr Michalecd9a3d4b2018-08-28 10:50:53 +0200725 defaults = __salt__['config.get']('reclass', None) or find_and_read_configfile()
Ales Komarekb0911892017-08-02 15:47:30 +0200726 meta = ''
727 error = None
728 try:
729 pillar = ext_pillar(node_name, {}, defaults['storage_type'], defaults['inventory_base_uri'])
730 except (ReclassException, Exception) as e:
731 msg = "Validation failed in %s on %s" % (repr(e), node_name)
732 LOG.error(msg)
733 meta = {'Error': msg}
734 s = str(type(e))
735 if 'yaml.scanner.ScannerError' in s:
736 error = re.sub(r"\r?\n?$", "", repr(str(e)), 1)
737 else:
738 error = e.message
739 if 'Error' in meta:
740 ret = {node_name: error}
741 else:
742 ret = {node_name: ''}
743 return ret
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200744
745
Adam Tengler2b362622017-06-01 14:23:45 +0000746def node_pillar(node_name, **kwargs):
747 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200748 Returns pillar metadata for given node from reclass inventory.
Adam Tengler2b362622017-06-01 14:23:45 +0000749
750 :param node_name: target minion ID
751
752 CLI Examples:
753
754 .. code-block:: bash
755
756 salt-call reclass.node_pillar minion_id
757
758 '''
Petr Michalecd9a3d4b2018-08-28 10:50:53 +0200759 defaults = __salt__['config.get']('reclass', None) or find_and_read_configfile()
Adam Tengler2b362622017-06-01 14:23:45 +0000760 pillar = ext_pillar(node_name, {}, defaults['storage_type'], defaults['inventory_base_uri'])
761 output = {node_name: pillar}
762
763 return output
764
765
Ales Komareka4a9f572016-12-03 20:15:50 +0100766def inventory(**connection_args):
767 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200768 Get all nodes in inventory and their associated services/roles.
Ales Komareka4a9f572016-12-03 20:15:50 +0100769
770 CLI Examples:
771
772 .. code-block:: bash
773
774 salt '*' reclass.inventory
775 '''
Petr Michalecd9a3d4b2018-08-28 10:50:53 +0200776 defaults = __salt__['config.get']('reclass', None) or find_and_read_configfile()
Ales Komareka4a9f572016-12-03 20:15:50 +0100777 storage = get_storage(defaults['storage_type'], _get_nodes_dir(), _get_classes_dir())
778 reclass = Core(storage, None)
779 nodes = reclass.inventory()["nodes"]
780 output = {}
781
782 for node in nodes:
783 service_classification = []
784 role_classification = []
785 for service in nodes[node]['parameters']:
786 if service not in ['_param', 'private_keys', 'public_keys', 'known_hosts']:
787 service_classification.append(service)
788 for role in nodes[node]['parameters'][service]:
789 if role not in ['_support', '_orchestrate', 'common']:
790 role_classification.append('%s.%s' % (service, role))
791 output[node] = {
792 'roles': role_classification,
793 'services': service_classification,
794 }
795 return output
Adam Tengler8a1cf402017-05-16 10:59:35 +0000796
797
798def cluster_meta_list(file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000799 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200800 List all cluster level soft metadata overrides.
Adam Tengler2b362622017-06-01 14:23:45 +0000801
802 :param file_name: name of the override file, defaults to: overrides.yml
803
804 CLI Examples:
805
806 .. code-block:: bash
807
808 salt-call reclass.cluster_meta_list
Adam Tengler2b362622017-06-01 14:23:45 +0000809 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000810 path = os.path.join(_get_cluster_dir(), cluster, file_name)
811 try:
812 with io.open(path, 'r') as file_handle:
813 meta_yaml = yaml.safe_load(file_handle.read())
814 meta = meta_yaml or {}
815 except Exception as e:
816 msg = "Unable to load cluster metadata YAML %s: %s" % (path, repr(e))
817 LOG.debug(msg)
818 meta = {'Error': msg}
819 return meta
820
821
822def cluster_meta_delete(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000823 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200824 Delete cluster level soft metadata override entry.
Adam Tengler2b362622017-06-01 14:23:45 +0000825
826 :param name: name of the override entry (dictionary key)
827 :param file_name: name of the override file, defaults to: overrides.yml
828
829 CLI Examples:
830
831 .. code-block:: bash
832
833 salt-call reclass.cluster_meta_delete foo
Adam Tengler2b362622017-06-01 14:23:45 +0000834 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000835 ret = {}
836 path = os.path.join(_get_cluster_dir(), cluster, file_name)
837 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
838 if 'Error' not in meta:
839 metadata = meta.get('parameters', {}).get('_param', {})
840 if name not in metadata:
841 return ret
842 del metadata[name]
843 try:
844 with io.open(path, 'w') as file_handle:
Petr Michalec7361f432018-07-10 13:44:18 +0200845 file_handle.write(unicode(yaml.safe_dump(meta, default_flow_style=False)))
Adam Tengler8a1cf402017-05-16 10:59:35 +0000846 except Exception as e:
847 msg = "Unable to save cluster metadata YAML: %s" % repr(e)
848 LOG.error(msg)
849 return {'Error': msg}
850 ret = 'Cluster metadata entry {0} deleted'.format(name)
851 return ret
852
853
854def cluster_meta_set(name, value, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000855 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200856 Create cluster level metadata override entry.
Adam Tengler2b362622017-06-01 14:23:45 +0000857
858 :param name: name of the override entry (dictionary key)
859 :param value: value of the override entry (dictionary value)
860 :param file_name: name of the override file, defaults to: overrides.yml
861
862 CLI Examples:
863
864 .. code-block:: bash
865
866 salt-call reclass.cluster_meta_set foo bar
Adam Tengler2b362622017-06-01 14:23:45 +0000867 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000868 path = os.path.join(_get_cluster_dir(), cluster, file_name)
869 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
870 if 'Error' not in meta:
871 if not meta:
872 meta = {'parameters': {'_param': {}}}
873 metadata = meta.get('parameters', {}).get('_param', {})
874 if name in metadata and metadata[name] == value:
875 return {name: 'Cluster metadata entry %s already exists and is in correct state' % name}
876 metadata.update({name: value})
877 try:
878 with io.open(path, 'w') as file_handle:
Petr Michalec7361f432018-07-10 13:44:18 +0200879 file_handle.write(unicode(yaml.safe_dump(meta, default_flow_style=False)))
Adam Tengler8a1cf402017-05-16 10:59:35 +0000880 except Exception as e:
881 msg = "Unable to save cluster metadata YAML %s: %s" % (path, repr(e))
882 LOG.error(msg)
883 return {'Error': msg}
884 return cluster_meta_get(name, path, **kwargs)
885 return meta
886
887
888def cluster_meta_get(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000889 '''
890 Get single cluster level override entry
891
892 :param name: name of the override entry (dictionary key)
893 :param file_name: name of the override file, defaults to: overrides.yml
894
895 CLI Examples:
896
897 .. code-block:: bash
898
899 salt-call reclass.cluster_meta_get foo
900
901 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000902 ret = {}
903 path = os.path.join(_get_cluster_dir(), cluster, file_name)
904 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
905 metadata = meta.get('parameters', {}).get('_param', {})
906 if 'Error' in meta:
907 ret['Error'] = meta['Error']
908 elif name in metadata:
909 ret[name] = metadata.get(name)
910
911 return ret
912