blob: e9880c578e5cdc8831ad1e91b24adfb1c60d07c6 [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
Adam Tengler23d965f2017-05-16 19:14:51 +000020from string import Template
Alexandr Lovtsovd3954682019-02-22 18:53:20 +030021
22try:
23 from reclass import get_storage, output
24 from reclass.adapters.salt import ext_pillar
25 from reclass.core import Core
26 from reclass.config import find_and_read_configfile
27 from reclass.errors import ReclassException
28 HAS_RECLASS = True
29except ImportError:
30 HAS_RECLASS = False
Jiri Broulik7ccb5342017-07-20 17:07:47 +020031
Ales Komarek166cc672016-07-27 14:17:22 +020032
Ales Komareka4a9f572016-12-03 20:15:50 +010033LOG = logging.getLogger(__name__)
Ales Komarek166cc672016-07-27 14:17:22 +020034
Ales Komareka961df42016-11-21 21:50:24 +010035
Ales Komarek166cc672016-07-27 14:17:22 +020036def __virtual__():
37 '''
38 Only load this module if reclass
39 is installed on this minion.
40 '''
Alexandr Lovtsovd3954682019-02-22 18:53:20 +030041 if not HAS_RECLASS:
42 return False, "'reclass' python library is unavailable"
Ales Komarek166cc672016-07-27 14:17:22 +020043 return 'reclass'
44
45
Jiri Broulik7ccb5342017-07-20 17:07:47 +020046def _deps(ret_classes=True, ret_errors=False):
47 '''
48 Returns classes if ret_classes=True, else returns soft_params if ret_classes=False
49 '''
Vasyl Saienko6f47d532018-08-29 09:04:43 +000050 defaults = find_and_read_configfile()
Jiri Broulik7ccb5342017-07-20 17:07:47 +020051 path = defaults.get('inventory_base_uri')
52 classes = {}
53 soft_params = {}
54 errors = []
55
56 # find classes
57 for root, dirs, files in os.walk(path):
Petr Michalec46a5bad2017-09-18 20:11:43 +020058 # skip hidden files and folders in reclass dir
59 files = [f for f in files if not f[0] == '.']
60 dirs[:] = [d for d in dirs if not d[0] == '.']
61 # translate found init.yml to valid class name
Jiri Broulik7ccb5342017-07-20 17:07:47 +020062 if 'init.yml' in files:
63 class_file = root + '/' + 'init.yml'
64 class_name = class_file.replace(path, '')[:-9].replace('/', '.')
65 classes[class_name] = {'file': class_file}
66
67 for f in files:
68 if f.endswith('.yml') and f != 'init.yml':
69 class_file = root + '/' + f
70 class_name = class_file.replace(path, '')[:-4].replace('/', '.')
71 classes[class_name] = {'file': class_file}
72
73 # read classes
74 for class_name, params in classes.items():
azvyagintsevf5264d52017-12-12 11:49:42 +020075 LOG.debug("Processing:{}".format(params['file']))
Jiri Broulik7ccb5342017-07-20 17:07:47 +020076 with open(params['file'], 'r') as f:
77 # read raw data
78 raw = f.read()
79 pr = re.findall('\${_param:(.*?)}', raw)
80 if pr:
81 params['params_required'] = list(set(pr))
82
83 # load yaml
84 try:
85 data = yaml.load(raw)
86 except yaml.scanner.ScannerError as e:
87 errors.append(params['file'] + ' ' + str(e))
88 pass
89
90 if type(data) == dict:
91 if data.get('classes'):
92 params['includes'] = data.get('classes', [])
93 if data.get('parameters') and data['parameters'].get('_param'):
94 params['params_created'] = data['parameters']['_param']
95
96 if not(data.get('classes') or data.get('parameters')):
97 errors.append(params['file'] + ' ' + 'file missing classes and parameters')
98 else:
99 errors.append(params['file'] + ' ' + 'is not valid yaml')
100
101 if ret_classes:
102 return classes
103 elif ret_errors:
104 return errors
105
106 # find parameters and its usage
107 for class_name, params in classes.items():
108 for pn, pv in params.get('params_created', {}).items():
109 # create param if missing
110 if pn not in soft_params:
111 soft_params[pn] = {'created_at': {}, 'required_at': []}
112
113 # add created_at
114 if class_name not in soft_params[pn]['created_at']:
115 soft_params[pn]['created_at'][class_name] = pv
116
117 for pn in params.get('params_required', []):
118 # create param if missing
119 if pn not in soft_params:
120 soft_params[pn] = {'created_at': {}, 'required_at': []}
121
122 # add created_at
123 soft_params[pn]['required_at'].append(class_name)
124
125 return soft_params
126
127
Ales Komareka4a9f572016-12-03 20:15:50 +0100128def _get_nodes_dir():
Vasyl Saienko6f47d532018-08-29 09:04:43 +0000129 defaults = find_and_read_configfile()
130 return defaults.get('nodes_uri') or \
131 os.path.join(defaults.get('inventory_base_uri'), 'nodes')
132
Ales Komareka4a9f572016-12-03 20:15:50 +0100133
134def _get_classes_dir():
Vasyl Saienko6f47d532018-08-29 09:04:43 +0000135 defaults = find_and_read_configfile()
136 return os.path.join(defaults.get('inventory_base_uri'), 'classes')
Ales Komarek166cc672016-07-27 14:17:22 +0200137
138
Adam Tengler8a1cf402017-05-16 10:59:35 +0000139def _get_cluster_dir():
140 classes_dir = _get_classes_dir()
141 return os.path.join(classes_dir, 'cluster')
142
143
Adam Tengler805666d2017-05-15 16:01:13 +0000144def _get_node_meta(name, cluster="default", environment="prd", classes=None, parameters=None):
145 host_name = name.split('.')[0]
146 domain_name = '.'.join(name.split('.')[1:])
147
148 if classes == None:
149 meta_classes = []
150 else:
151 if isinstance(classes, six.string_types):
152 meta_classes = json.loads(classes)
153 else:
154 meta_classes = classes
155
156 if parameters == None:
157 meta_parameters = {}
158 else:
159 if isinstance(parameters, six.string_types):
160 meta_parameters = json.loads(parameters)
161 else:
162 # generate dict from OrderedDict
163 meta_parameters = {k: v for (k, v) in parameters.items()}
164
165 node_meta = {
166 'classes': meta_classes,
167 'parameters': {
168 '_param': meta_parameters,
169 'linux': {
170 'system': {
171 'name': host_name,
172 'domain': domain_name,
173 'cluster': cluster,
174 'environment': environment,
175 }
176 }
177 }
178 }
179
180 return node_meta
181
182
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200183def soft_meta_list():
184 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200185 Returns all defined soft metadata parameters.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200186
187 CLI Examples:
188
189 .. code-block:: bash
190
191 salt '*' reclass.soft_meta_list
192 '''
193 return _deps(ret_classes=False)
194
195
196def class_list():
197 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200198 Returns list of all classes defined within reclass inventory.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200199
200 CLI Examples:
201
202 .. code-block:: bash
203
204 salt '*' reclass.class_list
205 '''
206 return _deps(ret_classes=True)
207
208
209def soft_meta_get(name):
210 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200211 Returns single soft metadata parameter.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200212
Ales Komarekb0911892017-08-02 15:47:30 +0200213 :param name: expects the following format: apt_mk_version
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200214
215 CLI Examples:
216
217 .. code-block:: bash
218
Ales Komarekb0911892017-08-02 15:47:30 +0200219 salt '*' reclass.soft_meta_get openstack_version
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200220 '''
221 soft_params = _deps(ret_classes=False)
222
223 if name in soft_params:
Ales Komarekb0911892017-08-02 15:47:30 +0200224 return {name: soft_params.get(name)}
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200225 else:
Ales Komarekb0911892017-08-02 15:47:30 +0200226 return {'Error': 'No param {0} found'.format(name)}
227
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200228
229def class_get(name):
230 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200231 Returns detailes information about class file in reclass inventory.
232
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200233 :param name: expects the following format classes.system.linux.repo
234
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200235 CLI Examples:
236
237 .. code-block:: bash
238
239 salt '*' reclass.class_get classes.system.linux.repo
240 '''
241 classes = _deps(ret_classes=True)
242 tmp_name = '.' + name
243 if tmp_name in classes:
Ales Komarekb0911892017-08-02 15:47:30 +0200244 return {name: classes.get(tmp_name)}
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200245 else:
Ales Komarekb0911892017-08-02 15:47:30 +0200246 return {'Error': 'No class {0} found'.format(name)}
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200247
248
Ales Komarek166cc672016-07-27 14:17:22 +0200249def node_create(name, path=None, cluster="default", environment="prd", classes=None, parameters=None, **kwargs):
250 '''
251 Create a reclass node
252
253 :param name: new node FQDN
254 :param path: custom path in nodes for new node
255 :param classes: classes given to the new node
256 :param parameters: parameters given to the new node
257 :param environment: node's environment
258 :param cluster: node's cluster
259
260 CLI Examples:
261
262 .. code-block:: bash
263
264 salt '*' reclass.node_create server.domain.com classes=[system.neco1, system.neco2]
265 salt '*' reclass.node_create namespace/test enabled=False
Petr Michalec46a5bad2017-09-18 20:11:43 +0200266
Ales Komarek166cc672016-07-27 14:17:22 +0200267 '''
268 ret = {}
269
270 node = node_get(name=name)
271
272 if node and not "Error" in node:
273 LOG.debug("node {0} exists".format(name))
274 ret[name] = node
275 return ret
276
277 host_name = name.split('.')[0]
278 domain_name = '.'.join(name.split('.')[1:])
279
Adam Tengler805666d2017-05-15 16:01:13 +0000280 node_meta = _get_node_meta(name, cluster, environment, classes, parameters)
Ales Komarek166cc672016-07-27 14:17:22 +0200281 LOG.debug(node_meta)
282
283 if path == None:
Ales Komareka4a9f572016-12-03 20:15:50 +0100284 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200285 else:
Ales Komareka4a9f572016-12-03 20:15:50 +0100286 file_path = os.path.join(_get_nodes_dir(), path, name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200287
288 with open(file_path, 'w') as node_file:
Ales Komareka961df42016-11-21 21:50:24 +0100289 node_file.write(yaml.safe_dump(node_meta, default_flow_style=False))
Ales Komarek71f94b02016-07-27 14:48:57 +0200290
Ales Komarek166cc672016-07-27 14:17:22 +0200291 return node_get(name)
292
Ales Komareka4a9f572016-12-03 20:15:50 +0100293
Ales Komarek166cc672016-07-27 14:17:22 +0200294def node_delete(name, **kwargs):
295 '''
296 Delete a reclass node
297
298 :params node: Node name
299
300 CLI Examples:
301
302 .. code-block:: bash
303
304 salt '*' reclass.node_delete demo01.domain.com
305 salt '*' reclass.node_delete name=demo01.domain.com
306 '''
307
308 node = node_get(name=name)
309
310 if 'Error' in node:
311 return {'Error': 'Unable to retreive node'}
312
313 if node[name]['path'] == '':
Ales Komareka4a9f572016-12-03 20:15:50 +0100314 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200315 else:
Ales Komareka4a9f572016-12-03 20:15:50 +0100316 file_path = os.path.join(_get_nodes_dir(), node[name]['path'], name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200317
318 os.remove(file_path)
319
320 ret = 'Node {0} deleted'.format(name)
321
322 return ret
323
324
325def node_get(name, path=None, **kwargs):
326 '''
327 Return a specific node
328
329 CLI Examples:
330
331 .. code-block:: bash
332
333 salt '*' reclass.node_get host01.domain.com
334 salt '*' reclass.node_get name=host02.domain.com
335 '''
336 ret = {}
337 nodes = node_list(**kwargs)
338
339 if not name in nodes:
340 return {'Error': 'Error in retrieving node'}
341 ret[name] = nodes[name]
342 return ret
343
344
345def node_list(**connection_args):
346 '''
347 Return a list of available nodes
348
349 CLI Example:
350
351 .. code-block:: bash
352
353 salt '*' reclass.node_list
354 '''
355 ret = {}
356
Ales Komareka4a9f572016-12-03 20:15:50 +0100357 for root, sub_folders, files in os.walk(_get_nodes_dir()):
Petr Michalec46a5bad2017-09-18 20:11:43 +0200358 # skip hidden files and folders in reclass dir
359 files = [f for f in files if not f[0] == '.']
Petr Michalec55a43322017-09-19 17:49:56 +0200360 sub_folders[:] = [d for d in sub_folders if not d[0] == '.']
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', [])
root950f88e2019-03-05 12:15:42 +0000367 parameters = file_data.get('parameters', {}).get('_param', {})
Adam Tengler805666d2017-05-15 16:01:13 +0000368 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):
Ales Komarekb0911892017-08-02 15:47:30 +0200458 parsed_host_external = [urlparse.urlparse(item).netloc
Adam Tengler69c7ba92017-06-01 15:59:01 +0000459 for item
460 in relation.get('host_external', '').split(' ')
Ales Komarekb0911892017-08-02 15:47:30 +0200461 if urlparse.urlparse(item).netloc]
Adam Tengler69c7ba92017-06-01 15:59:01 +0000462 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
Petr Michalec46a5bad2017-09-18 20:11:43 +0200523
Adam Tengler2b362622017-06-01 14:23:45 +0000524 '''
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
car-da0da41492017-08-25 11:01:26 +0200568 salt '*' reclass.node_update name=nodename classes="[clas1, class2]" parameters="{param: value, another_param: another_value}"
Ales Komarek166cc672016-07-27 14:17:22 +0200569 '''
570 node = node_get(name=name)
car-da0da41492017-08-25 11:01:26 +0200571 if node.has_key('Error'):
Ales Komarek71f94b02016-07-27 14:48:57 +0200572 return {'Error': 'Error in retrieving node'}
azvyagintsevf5264d52017-12-12 11:49:42 +0200573
car-da0da41492017-08-25 11:01:26 +0200574 for name, values in node.items():
575 param = values.get('parameters', {})
576 path = values.get('path')
577 cluster = values.get('cluster')
578 environment = values.get('environment')
579 write_class = values.get('classes', [])
azvyagintsevf5264d52017-12-12 11:49:42 +0200580
car-da0da41492017-08-25 11:01:26 +0200581 if parameters:
582 param.update(parameters)
azvyagintsevf5264d52017-12-12 11:49:42 +0200583
car-da0da41492017-08-25 11:01:26 +0200584 if classes:
585 for classe in classes:
586 if not classe in write_class:
587 write_class.append(classe)
azvyagintsevf5264d52017-12-12 11:49:42 +0200588
car-da0da41492017-08-25 11:01:26 +0200589 node_meta = _get_node_meta(name, cluster, environment, write_class, param)
590 LOG.debug(node_meta)
azvyagintsevf5264d52017-12-12 11:49:42 +0200591
car-da0da41492017-08-25 11:01:26 +0200592 if path == None:
593 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
594 else:
595 file_path = os.path.join(_get_nodes_dir(), path, name + '.yml')
azvyagintsevf5264d52017-12-12 11:49:42 +0200596
car-da0da41492017-08-25 11:01:26 +0200597 with open(file_path, 'w') as node_file:
598 node_file.write(yaml.safe_dump(node_meta, default_flow_style=False))
azvyagintsevf5264d52017-12-12 11:49:42 +0200599
car-da0da41492017-08-25 11:01:26 +0200600 return node_get(name)
Ales Komareka4a9f572016-12-03 20:15:50 +0100601
602
Adam Tengler23d965f2017-05-16 19:14:51 +0000603def _get_node_classes(node_data, class_mapping_fragment):
604 classes = []
605
606 for value_tmpl_string in class_mapping_fragment.get('value_template', []):
607 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
608 rendered_value = value_tmpl.safe_substitute(node_data)
609 classes.append(rendered_value)
610
611 for value in class_mapping_fragment.get('value', []):
612 classes.append(value)
613
614 return classes
615
616
617def _get_params(node_data, class_mapping_fragment):
618 params = {}
619
620 for param_name, param in class_mapping_fragment.items():
621 value = param.get('value', None)
622 value_tmpl_string = param.get('value_template', None)
623 if value:
624 params.update({param_name: value})
625 elif value_tmpl_string:
626 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
627 rendered_value = value_tmpl.safe_substitute(node_data)
628 params.update({param_name: rendered_value})
629
630 return params
631
632
Adam Tengler4d961142017-07-27 15:35:28 +0000633def _validate_condition(node_data, expressions):
634 # allow string expression definition for single expression conditions
635 if isinstance(expressions, six.string_types):
636 expressions = [expressions]
Adam Tengler23d965f2017-05-16 19:14:51 +0000637
Adam Tengler4d961142017-07-27 15:35:28 +0000638 result = []
639 for expression_tmpl_string in expressions:
640 expression_tmpl = Template(expression_tmpl_string.replace('<<', '${').replace('>>', '}'))
641 expression = expression_tmpl.safe_substitute(node_data)
Adam Tengler23d965f2017-05-16 19:14:51 +0000642
Adam Tengler4d961142017-07-27 15:35:28 +0000643 if expression and expression == 'all':
644 result.append(True)
645 elif expression:
646 val_a = expression.split('__')[0]
647 val_b = expression.split('__')[2]
648 condition = expression.split('__')[1]
649 if condition == 'startswith':
650 result.append(val_a.startswith(val_b))
651 elif condition == 'equals':
652 result.append(val_a == val_b)
653
654 return all(result)
Adam Tengler23d965f2017-05-16 19:14:51 +0000655
656
657def node_classify(node_name, node_data={}, class_mapping={}, **kwargs):
658 '''
659 CLassify node by given class_mapping dictionary
660
661 :param node_name: node FQDN
662 :param node_data: dictionary of known informations about the node
663 :param class_mapping: dictionary of classes and parameters, with conditions
664
665 '''
666 # clean node_data
667 node_data = {k: v for (k, v) in node_data.items() if not k.startswith('__')}
668
669 classes = []
670 node_params = {}
671 cluster_params = {}
672 ret = {'node_create': '', 'cluster_param': {}}
673
674 for type_name, node_type in class_mapping.items():
675 valid = _validate_condition(node_data, node_type.get('expression', ''))
676 if valid:
677 gen_classes = _get_node_classes(node_data, node_type.get('node_class', {}))
678 classes = classes + gen_classes
679 gen_node_params = _get_params(node_data, node_type.get('node_param', {}))
680 node_params.update(gen_node_params)
681 gen_cluster_params = _get_params(node_data, node_type.get('cluster_param', {}))
682 cluster_params.update(gen_cluster_params)
683
684 if classes:
685 create_kwargs = {'name': node_name, 'path': '_generated', 'classes': classes, 'parameters': node_params}
686 ret['node_create'] = node_create(**create_kwargs)
687
688 for name, value in cluster_params.items():
689 ret['cluster_param'][name] = cluster_meta_set(name, value)
690
691 return ret
692
693
Ales Komarekb0911892017-08-02 15:47:30 +0200694def validate_yaml():
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200695 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200696 Returns list of all reclass YAML files that contain syntax
697 errors.
698
699 CLI Examples:
700
701 .. code-block:: bash
702
703 salt-call reclass.validate_yaml
704 '''
705 errors = _deps(ret_classes=False, ret_errors=True)
706 if errors:
707 ret = {'Errors': errors}
708 return ret
709
710
711def validate_pillar(node_name=None, **kwargs):
712 '''
713 Validates whether the pillar of given node is in correct state.
714 If node is not specified it validates pillars of all known nodes.
715 Returns error message for every node with currupted metadata.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200716
717 :param node_name: target minion ID
718
719 CLI Examples:
720
721 .. code-block:: bash
722
Ales Komarekb0911892017-08-02 15:47:30 +0200723 salt-call reclass.validate_pillar
724 salt-call reclass.validate_pillar minion-id
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200725 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200726 if node_name is None:
727 ret={}
728 nodes = node_list(**kwargs)
729 for node_name, node in nodes.items():
730 ret.update(validate_pillar(node_name))
731 return ret
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200732 else:
Vasyl Saienko6f47d532018-08-29 09:04:43 +0000733 defaults = find_and_read_configfile()
Ales Komarekb0911892017-08-02 15:47:30 +0200734 meta = ''
735 error = None
736 try:
737 pillar = ext_pillar(node_name, {}, defaults['storage_type'], defaults['inventory_base_uri'])
738 except (ReclassException, Exception) as e:
739 msg = "Validation failed in %s on %s" % (repr(e), node_name)
740 LOG.error(msg)
741 meta = {'Error': msg}
742 s = str(type(e))
743 if 'yaml.scanner.ScannerError' in s:
744 error = re.sub(r"\r?\n?$", "", repr(str(e)), 1)
745 else:
746 error = e.message
747 if 'Error' in meta:
748 ret = {node_name: error}
749 else:
750 ret = {node_name: ''}
751 return ret
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200752
753
Adam Tengler2b362622017-06-01 14:23:45 +0000754def node_pillar(node_name, **kwargs):
755 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200756 Returns pillar metadata for given node from reclass inventory.
Adam Tengler2b362622017-06-01 14:23:45 +0000757
758 :param node_name: target minion ID
759
760 CLI Examples:
761
762 .. code-block:: bash
763
764 salt-call reclass.node_pillar minion_id
765
766 '''
Vasyl Saienko6f47d532018-08-29 09:04:43 +0000767 defaults = find_and_read_configfile()
Adam Tengler2b362622017-06-01 14:23:45 +0000768 pillar = ext_pillar(node_name, {}, defaults['storage_type'], defaults['inventory_base_uri'])
769 output = {node_name: pillar}
770
771 return output
772
773
Ales Komareka4a9f572016-12-03 20:15:50 +0100774def inventory(**connection_args):
775 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200776 Get all nodes in inventory and their associated services/roles.
Ales Komareka4a9f572016-12-03 20:15:50 +0100777
778 CLI Examples:
779
780 .. code-block:: bash
781
782 salt '*' reclass.inventory
783 '''
Vasyl Saienko6f47d532018-08-29 09:04:43 +0000784 defaults = find_and_read_configfile()
Ales Komareka4a9f572016-12-03 20:15:50 +0100785 storage = get_storage(defaults['storage_type'], _get_nodes_dir(), _get_classes_dir())
786 reclass = Core(storage, None)
787 nodes = reclass.inventory()["nodes"]
788 output = {}
789
790 for node in nodes:
791 service_classification = []
792 role_classification = []
793 for service in nodes[node]['parameters']:
794 if service not in ['_param', 'private_keys', 'public_keys', 'known_hosts']:
795 service_classification.append(service)
796 for role in nodes[node]['parameters'][service]:
797 if role not in ['_support', '_orchestrate', 'common']:
798 role_classification.append('%s.%s' % (service, role))
799 output[node] = {
800 'roles': role_classification,
801 'services': service_classification,
802 }
803 return output
Adam Tengler8a1cf402017-05-16 10:59:35 +0000804
805
806def cluster_meta_list(file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000807 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200808 List all cluster level soft metadata overrides.
Adam Tengler2b362622017-06-01 14:23:45 +0000809
810 :param file_name: name of the override file, defaults to: overrides.yml
811
812 CLI Examples:
813
814 .. code-block:: bash
815
816 salt-call reclass.cluster_meta_list
Adam Tengler2b362622017-06-01 14:23:45 +0000817 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000818 path = os.path.join(_get_cluster_dir(), cluster, file_name)
819 try:
820 with io.open(path, 'r') as file_handle:
821 meta_yaml = yaml.safe_load(file_handle.read())
822 meta = meta_yaml or {}
823 except Exception as e:
824 msg = "Unable to load cluster metadata YAML %s: %s" % (path, repr(e))
825 LOG.debug(msg)
826 meta = {'Error': msg}
827 return meta
828
829
830def cluster_meta_delete(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000831 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200832 Delete cluster level soft metadata override entry.
Adam Tengler2b362622017-06-01 14:23:45 +0000833
834 :param name: name of the override entry (dictionary key)
835 :param file_name: name of the override file, defaults to: overrides.yml
836
837 CLI Examples:
838
839 .. code-block:: bash
840
841 salt-call reclass.cluster_meta_delete foo
Adam Tengler2b362622017-06-01 14:23:45 +0000842 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000843 ret = {}
844 path = os.path.join(_get_cluster_dir(), cluster, file_name)
845 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
846 if 'Error' not in meta:
847 metadata = meta.get('parameters', {}).get('_param', {})
848 if name not in metadata:
849 return ret
850 del metadata[name]
851 try:
852 with io.open(path, 'w') as file_handle:
Petr Michalec7361f432018-07-10 13:44:18 +0200853 file_handle.write(unicode(yaml.safe_dump(meta, default_flow_style=False)))
Adam Tengler8a1cf402017-05-16 10:59:35 +0000854 except Exception as e:
855 msg = "Unable to save cluster metadata YAML: %s" % repr(e)
856 LOG.error(msg)
857 return {'Error': msg}
858 ret = 'Cluster metadata entry {0} deleted'.format(name)
859 return ret
860
861
862def cluster_meta_set(name, value, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000863 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200864 Create cluster level metadata override entry.
Adam Tengler2b362622017-06-01 14:23:45 +0000865
866 :param name: name of the override entry (dictionary key)
867 :param value: value of the override entry (dictionary value)
868 :param file_name: name of the override file, defaults to: overrides.yml
869
870 CLI Examples:
871
872 .. code-block:: bash
873
874 salt-call reclass.cluster_meta_set foo bar
Adam Tengler2b362622017-06-01 14:23:45 +0000875 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000876 path = os.path.join(_get_cluster_dir(), cluster, file_name)
877 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
878 if 'Error' not in meta:
879 if not meta:
880 meta = {'parameters': {'_param': {}}}
881 metadata = meta.get('parameters', {}).get('_param', {})
882 if name in metadata and metadata[name] == value:
883 return {name: 'Cluster metadata entry %s already exists and is in correct state' % name}
884 metadata.update({name: value})
885 try:
886 with io.open(path, 'w') as file_handle:
Petr Michalec7361f432018-07-10 13:44:18 +0200887 file_handle.write(unicode(yaml.safe_dump(meta, default_flow_style=False)))
Adam Tengler8a1cf402017-05-16 10:59:35 +0000888 except Exception as e:
889 msg = "Unable to save cluster metadata YAML %s: %s" % (path, repr(e))
890 LOG.error(msg)
891 return {'Error': msg}
892 return cluster_meta_get(name, path, **kwargs)
893 return meta
894
895
896def cluster_meta_get(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000897 '''
898 Get single cluster level override entry
899
900 :param name: name of the override entry (dictionary key)
901 :param file_name: name of the override file, defaults to: overrides.yml
902
903 CLI Examples:
904
905 .. code-block:: bash
906
907 salt-call reclass.cluster_meta_get foo
908
909 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000910 ret = {}
911 path = os.path.join(_get_cluster_dir(), cluster, file_name)
912 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
913 metadata = meta.get('parameters', {}).get('_param', {})
914 if 'Error' in meta:
915 ret['Error'] = meta['Error']
916 elif name in metadata:
917 ret[name] = metadata.get(name)
918
919 return ret
920