blob: 96094f7b10390d524299cf117ba8ff8641d22979 [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):
52 if 'init.yml' in files:
53 class_file = root + '/' + 'init.yml'
54 class_name = class_file.replace(path, '')[:-9].replace('/', '.')
55 classes[class_name] = {'file': class_file}
56
57 for f in files:
58 if f.endswith('.yml') and f != 'init.yml':
59 class_file = root + '/' + f
60 class_name = class_file.replace(path, '')[:-4].replace('/', '.')
61 classes[class_name] = {'file': class_file}
62
63 # read classes
64 for class_name, params in classes.items():
65 with open(params['file'], 'r') as f:
66 # read raw data
67 raw = f.read()
68 pr = re.findall('\${_param:(.*?)}', raw)
69 if pr:
70 params['params_required'] = list(set(pr))
71
72 # load yaml
73 try:
74 data = yaml.load(raw)
75 except yaml.scanner.ScannerError as e:
76 errors.append(params['file'] + ' ' + str(e))
77 pass
78
79 if type(data) == dict:
80 if data.get('classes'):
81 params['includes'] = data.get('classes', [])
82 if data.get('parameters') and data['parameters'].get('_param'):
83 params['params_created'] = data['parameters']['_param']
84
85 if not(data.get('classes') or data.get('parameters')):
86 errors.append(params['file'] + ' ' + 'file missing classes and parameters')
87 else:
88 errors.append(params['file'] + ' ' + 'is not valid yaml')
89
90 if ret_classes:
91 return classes
92 elif ret_errors:
93 return errors
94
95 # find parameters and its usage
96 for class_name, params in classes.items():
97 for pn, pv in params.get('params_created', {}).items():
98 # create param if missing
99 if pn not in soft_params:
100 soft_params[pn] = {'created_at': {}, 'required_at': []}
101
102 # add created_at
103 if class_name not in soft_params[pn]['created_at']:
104 soft_params[pn]['created_at'][class_name] = pv
105
106 for pn in params.get('params_required', []):
107 # create param if missing
108 if pn not in soft_params:
109 soft_params[pn] = {'created_at': {}, 'required_at': []}
110
111 # add created_at
112 soft_params[pn]['required_at'].append(class_name)
113
114 return soft_params
115
116
Ales Komareka4a9f572016-12-03 20:15:50 +0100117def _get_nodes_dir():
118 defaults = find_and_read_configfile()
Vladislav Naumov87113082017-07-24 17:36:50 +0300119 return defaults.get('nodes_uri') or \
120 os.path.join(defaults.get('inventory_base_uri'), 'nodes')
Ales Komareka4a9f572016-12-03 20:15:50 +0100121
122
123def _get_classes_dir():
124 defaults = find_and_read_configfile()
125 return os.path.join(defaults.get('inventory_base_uri'), 'classes')
Ales Komarek166cc672016-07-27 14:17:22 +0200126
127
Adam Tengler8a1cf402017-05-16 10:59:35 +0000128def _get_cluster_dir():
129 classes_dir = _get_classes_dir()
130 return os.path.join(classes_dir, 'cluster')
131
132
Adam Tengler805666d2017-05-15 16:01:13 +0000133def _get_node_meta(name, cluster="default", environment="prd", classes=None, parameters=None):
134 host_name = name.split('.')[0]
135 domain_name = '.'.join(name.split('.')[1:])
136
137 if classes == None:
138 meta_classes = []
139 else:
140 if isinstance(classes, six.string_types):
141 meta_classes = json.loads(classes)
142 else:
143 meta_classes = classes
144
145 if parameters == None:
146 meta_parameters = {}
147 else:
148 if isinstance(parameters, six.string_types):
149 meta_parameters = json.loads(parameters)
150 else:
151 # generate dict from OrderedDict
152 meta_parameters = {k: v for (k, v) in parameters.items()}
153
154 node_meta = {
155 'classes': meta_classes,
156 'parameters': {
157 '_param': meta_parameters,
158 'linux': {
159 'system': {
160 'name': host_name,
161 'domain': domain_name,
162 'cluster': cluster,
163 'environment': environment,
164 }
165 }
166 }
167 }
168
169 return node_meta
170
171
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200172def soft_meta_list():
173 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200174 Returns all defined soft metadata parameters.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200175
176 CLI Examples:
177
178 .. code-block:: bash
179
180 salt '*' reclass.soft_meta_list
181 '''
182 return _deps(ret_classes=False)
183
184
185def class_list():
186 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200187 Returns list of all classes defined within reclass inventory.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200188
189 CLI Examples:
190
191 .. code-block:: bash
192
193 salt '*' reclass.class_list
194 '''
195 return _deps(ret_classes=True)
196
197
198def soft_meta_get(name):
199 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200200 Returns single soft metadata parameter.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200201
Ales Komarekb0911892017-08-02 15:47:30 +0200202 :param name: expects the following format: apt_mk_version
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200203
204 CLI Examples:
205
206 .. code-block:: bash
207
Ales Komarekb0911892017-08-02 15:47:30 +0200208 salt '*' reclass.soft_meta_get openstack_version
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200209 '''
210 soft_params = _deps(ret_classes=False)
211
212 if name in soft_params:
Ales Komarekb0911892017-08-02 15:47:30 +0200213 return {name: soft_params.get(name)}
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200214 else:
Ales Komarekb0911892017-08-02 15:47:30 +0200215 return {'Error': 'No param {0} found'.format(name)}
216
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200217
218def class_get(name):
219 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200220 Returns detailes information about class file in reclass inventory.
221
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200222 :param name: expects the following format classes.system.linux.repo
223
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200224 CLI Examples:
225
226 .. code-block:: bash
227
228 salt '*' reclass.class_get classes.system.linux.repo
229 '''
230 classes = _deps(ret_classes=True)
231 tmp_name = '.' + name
232 if tmp_name in classes:
Ales Komarekb0911892017-08-02 15:47:30 +0200233 return {name: classes.get(tmp_name)}
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200234 else:
Ales Komarekb0911892017-08-02 15:47:30 +0200235 return {'Error': 'No class {0} found'.format(name)}
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200236
237
Ales Komarek166cc672016-07-27 14:17:22 +0200238def node_create(name, path=None, cluster="default", environment="prd", classes=None, parameters=None, **kwargs):
239 '''
240 Create a reclass node
241
242 :param name: new node FQDN
243 :param path: custom path in nodes for new node
244 :param classes: classes given to the new node
245 :param parameters: parameters given to the new node
246 :param environment: node's environment
247 :param cluster: node's cluster
248
249 CLI Examples:
250
251 .. code-block:: bash
252
253 salt '*' reclass.node_create server.domain.com classes=[system.neco1, system.neco2]
254 salt '*' reclass.node_create namespace/test enabled=False
255
256 '''
257 ret = {}
258
259 node = node_get(name=name)
260
261 if node and not "Error" in node:
262 LOG.debug("node {0} exists".format(name))
263 ret[name] = node
264 return ret
265
266 host_name = name.split('.')[0]
267 domain_name = '.'.join(name.split('.')[1:])
268
Adam Tengler805666d2017-05-15 16:01:13 +0000269 node_meta = _get_node_meta(name, cluster, environment, classes, parameters)
Ales Komarek166cc672016-07-27 14:17:22 +0200270 LOG.debug(node_meta)
271
272 if path == None:
Ales Komareka4a9f572016-12-03 20:15:50 +0100273 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200274 else:
Ales Komareka4a9f572016-12-03 20:15:50 +0100275 file_path = os.path.join(_get_nodes_dir(), path, name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200276
277 with open(file_path, 'w') as node_file:
Ales Komareka961df42016-11-21 21:50:24 +0100278 node_file.write(yaml.safe_dump(node_meta, default_flow_style=False))
Ales Komarek71f94b02016-07-27 14:48:57 +0200279
Ales Komarek166cc672016-07-27 14:17:22 +0200280 return node_get(name)
281
Ales Komareka4a9f572016-12-03 20:15:50 +0100282
Ales Komarek166cc672016-07-27 14:17:22 +0200283def node_delete(name, **kwargs):
284 '''
285 Delete a reclass node
286
287 :params node: Node name
288
289 CLI Examples:
290
291 .. code-block:: bash
292
293 salt '*' reclass.node_delete demo01.domain.com
294 salt '*' reclass.node_delete name=demo01.domain.com
295 '''
296
297 node = node_get(name=name)
298
299 if 'Error' in node:
300 return {'Error': 'Unable to retreive node'}
301
302 if node[name]['path'] == '':
Ales Komareka4a9f572016-12-03 20:15:50 +0100303 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200304 else:
Ales Komareka4a9f572016-12-03 20:15:50 +0100305 file_path = os.path.join(_get_nodes_dir(), node[name]['path'], name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200306
307 os.remove(file_path)
308
309 ret = 'Node {0} deleted'.format(name)
310
311 return ret
312
313
314def node_get(name, path=None, **kwargs):
315 '''
316 Return a specific node
317
318 CLI Examples:
319
320 .. code-block:: bash
321
322 salt '*' reclass.node_get host01.domain.com
323 salt '*' reclass.node_get name=host02.domain.com
324 '''
325 ret = {}
326 nodes = node_list(**kwargs)
327
328 if not name in nodes:
329 return {'Error': 'Error in retrieving node'}
330 ret[name] = nodes[name]
331 return ret
332
333
334def node_list(**connection_args):
335 '''
336 Return a list of available nodes
337
338 CLI Example:
339
340 .. code-block:: bash
341
342 salt '*' reclass.node_list
343 '''
344 ret = {}
345
Ales Komareka4a9f572016-12-03 20:15:50 +0100346 for root, sub_folders, files in os.walk(_get_nodes_dir()):
Adam Tengler805666d2017-05-15 16:01:13 +0000347 for fl in files:
348 file_path = os.path.join(root, fl)
349 with open(file_path, 'r') as file_handle:
350 file_read = yaml.load(file_handle.read())
351 file_data = file_read or {}
352 classes = file_data.get('classes', [])
353 parameters = file_data.get('parameters', {}).get('_param', [])
354 name = fl.replace('.yml', '')
Ales Komarek166cc672016-07-27 14:17:22 +0200355 host_name = name.split('.')[0]
356 domain_name = '.'.join(name.split('.')[1:])
Ales Komareka4a9f572016-12-03 20:15:50 +0100357 path = root.replace(_get_nodes_dir()+'/', '')
Ales Komarek166cc672016-07-27 14:17:22 +0200358 ret[name] = {
Ales Komareka4a9f572016-12-03 20:15:50 +0100359 'name': host_name,
360 'domain': domain_name,
361 'cluster': 'default',
362 'environment': 'prd',
363 'path': path,
364 'classes': classes,
365 'parameters': parameters
Ales Komarek166cc672016-07-27 14:17:22 +0200366 }
367
368 return ret
369
Ales Komareka4a9f572016-12-03 20:15:50 +0100370
Adam Tengler2b362622017-06-01 14:23:45 +0000371def _is_valid_ipv4_address(address):
372 try:
373 socket.inet_pton(socket.AF_INET, address)
374 except AttributeError:
375 try:
376 socket.inet_aton(address)
377 except socket.error:
378 return False
379 return address.count('.') == 3
380 except socket.error:
381 return False
382 return True
383
384
385def _is_valid_ipv6_address(address):
386 try:
387 socket.inet_pton(socket.AF_INET6, address)
388 except socket.error:
389 return False
390 return True
391
392
Adam Tengler1f7667b2017-06-06 16:45:51 +0000393def _get_grains(*args, **kwargs):
394 res = __salt__['saltutil.cmd'](tgt='*',
395 fun='grains.item',
396 arg=args,
397 **{'timeout': 10})
398 return res or {}
399
400
401def _guess_host_from_target(network_grains, host, domain=' '):
Adam Tengler2b362622017-06-01 14:23:45 +0000402 '''
403 Guess minion ID from given host and domain arguments. Host argument can contain
404 hostname, FQDN, IPv4 or IPv6 addresses.
405 '''
Adam Tengler1f7667b2017-06-06 16:45:51 +0000406 key = None
407 value = None
408
Adam Tengler2b362622017-06-01 14:23:45 +0000409 if _is_valid_ipv4_address(host):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000410 key = 'ipv4'
411 value = host
Adam Tengler2b362622017-06-01 14:23:45 +0000412 elif _is_valid_ipv6_address(host):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000413 key = 'ipv6'
414 value = host
Adam Tengler2b362622017-06-01 14:23:45 +0000415 elif host.endswith(domain):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000416 key = 'fqdn'
417 value = host
Adam Tengler2b362622017-06-01 14:23:45 +0000418 else:
Adam Tengler1f7667b2017-06-06 16:45:51 +0000419 key = 'fqdn'
420 value = '%s.%s' % (host, domain)
Adam Tengler2b362622017-06-01 14:23:45 +0000421
Adam Tengler1f7667b2017-06-06 16:45:51 +0000422 target = None
423 if network_grains and isinstance(network_grains, dict) and key and value:
424 for minion, grains in network_grains.items():
425 if grains.get('retcode', 1) == 0 and value in grains.get('ret', {}).get(key, ''):
426 target = minion
Adam Tengler2b362622017-06-01 14:23:45 +0000427
Adam Tengler1f7667b2017-06-06 16:45:51 +0000428 return target or host
Adam Tengler2b362622017-06-01 14:23:45 +0000429
430
431def _interpolate_graph_data(graph_data, **kwargs):
432 new_nodes = []
Adam Tengler1f7667b2017-06-06 16:45:51 +0000433 network_grains = _get_grains('ipv4', 'ipv6', 'fqdn')
Adam Tengler2b362622017-06-01 14:23:45 +0000434 for node in graph_data:
Adam Tengler69c7ba92017-06-01 15:59:01 +0000435 if not node.get('relations', []):
436 node['relations'] = []
Adam Tengler2b362622017-06-01 14:23:45 +0000437 for relation in node.get('relations', []):
Adam Tengler12a310d2017-06-05 19:11:29 +0000438 if not relation.get('status', None):
439 relation['status'] = 'unknown'
Adam Tengler2b362622017-06-01 14:23:45 +0000440 if relation.get('host_from_target', None):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000441 host = _guess_host_from_target(network_grains, relation.pop('host_from_target'))
Adam Tengler2b362622017-06-01 14:23:45 +0000442 relation['host'] = host
443 if relation.get('host_external', None):
Ales Komarekb0911892017-08-02 15:47:30 +0200444 parsed_host_external = [urlparse.urlparse(item).netloc
Adam Tengler69c7ba92017-06-01 15:59:01 +0000445 for item
446 in relation.get('host_external', '').split(' ')
Ales Komarekb0911892017-08-02 15:47:30 +0200447 if urlparse.urlparse(item).netloc]
Adam Tengler69c7ba92017-06-01 15:59:01 +0000448 service = parsed_host_external[0] if parsed_host_external else ''
Adam Tengler2b362622017-06-01 14:23:45 +0000449 host = relation.get('service', '')
450 relation['host'] = host
Adam Tengler69c7ba92017-06-01 15:59:01 +0000451 del relation['host_external']
Adam Tengler2b362622017-06-01 14:23:45 +0000452 relation['service'] = service
Adam Tengler69c7ba92017-06-01 15:59:01 +0000453 host_list = [n.get('host', '') for n in graph_data + new_nodes]
454 service_list = [n.get('service', '') for n in graph_data + new_nodes if host in n.get('host', '')]
455 if host not in host_list or (host in host_list and service not in service_list):
Adam Tengler2b362622017-06-01 14:23:45 +0000456 new_node = {
457 'host': host,
458 'service': service,
459 'type': relation.get('type', ''),
460 'relations': []
461 }
462 new_nodes.append(new_node)
463
464 graph_data = graph_data + new_nodes
465
466 return graph_data
467
468
469def _grain_graph_data(*args, **kwargs):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000470 ret = _get_grains('salt:graph')
Adam Tengler2b362622017-06-01 14:23:45 +0000471 graph_data = []
472 for minion_ret in ret.values():
473 if minion_ret.get('retcode', 1) == 0:
474 graph_datum = minion_ret.get('ret', {}).get('salt:graph', [])
475 graph_data = graph_data + graph_datum
476
477 graph_nodes = _interpolate_graph_data(graph_data)
478 graph = {}
479
480 for node in graph_nodes:
481 if node.get('host') not in graph:
482 graph[node.get('host')] = {}
483 graph[node.pop('host')][node.pop('service')] = node
484
485 return {'graph': graph}
486
487
488def _pillar_graph_data(*args, **kwargs):
489 graph = {}
490 nodes = inventory()
491 for node, node_data in nodes.items():
492 for role in node_data.get('roles', []):
493 if node not in graph:
494 graph[node] = {}
495 graph[node][role] = {'relations': []}
496
497 return {'graph': graph}
498
499
500def graph_data(*args, **kwargs):
501 '''
502 Returns graph data for visualization app
503
504 CLI Examples:
505
506 .. code-block:: bash
507
Adam Tengler1f7667b2017-06-06 16:45:51 +0000508 salt-call reclass.graph_data
Adam Tengler2b362622017-06-01 14:23:45 +0000509
510 '''
511 pillar_data = _pillar_graph_data().get('graph')
512 grain_data = _grain_graph_data().get('graph')
513
514 for host, services in pillar_data.items():
515 for service, service_data in services.items():
516 grain_service = grain_data.get(host, {}).get(service, {})
517 service_data.update(grain_service)
518
519 graph = []
520 for host, services in pillar_data.items():
521 for service, service_data in services.items():
522 additional_data = {
523 'host': host,
Adam Tengler12a310d2017-06-05 19:11:29 +0000524 'service': service,
525 'status': 'unknown'
Adam Tengler2b362622017-06-01 14:23:45 +0000526 }
527 service_data.update(additional_data)
528 graph.append(service_data)
529
530 for host, services in grain_data.items():
531 for service, service_data in services.items():
532 additional_data = {
533 'host': host,
Adam Tengler12a310d2017-06-05 19:11:29 +0000534 'service': service,
535 'status': 'success'
Adam Tengler2b362622017-06-01 14:23:45 +0000536 }
537 service_data.update(additional_data)
538 host_list = [g.get('host', '') for g in graph]
539 service_list = [g.get('service', '') for g in graph if g.get('host') == host]
540 if host not in host_list or (host in host_list and service not in service_list):
541 graph.append(service_data)
542
543 return {'graph': graph}
544
545
Ales Komarek166cc672016-07-27 14:17:22 +0200546def node_update(name, classes=None, parameters=None, **connection_args):
547 '''
548 Update a node metadata information, classes and parameters.
549
550 CLI Examples:
551
552 .. code-block:: bash
553
car-da0da41492017-08-25 11:01:26 +0200554 salt '*' reclass.node_update name=nodename classes="[clas1, class2]" parameters="{param: value, another_param: another_value}"
Ales Komarek166cc672016-07-27 14:17:22 +0200555 '''
556 node = node_get(name=name)
car-da0da41492017-08-25 11:01:26 +0200557 if node.has_key('Error'):
558 LOG.debug("Error in retrieving node {0}".format(name))
Ales Komarek71f94b02016-07-27 14:48:57 +0200559 return {'Error': 'Error in retrieving node'}
car-da0da41492017-08-25 11:01:26 +0200560
561 for name, values in node.items():
562 param = values.get('parameters', {})
563 path = values.get('path')
564 cluster = values.get('cluster')
565 environment = values.get('environment')
566 write_class = values.get('classes', [])
567
568 if parameters:
569 param.update(parameters)
570
571 if classes:
572 for classe in classes:
573 if not classe in write_class:
574 write_class.append(classe)
575
576 node_meta = _get_node_meta(name, cluster, environment, write_class, param)
577 LOG.debug(node_meta)
578
579 if path == None:
580 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
581 else:
582 file_path = os.path.join(_get_nodes_dir(), path, name + '.yml')
583
584 with open(file_path, 'w') as node_file:
585 node_file.write(yaml.safe_dump(node_meta, default_flow_style=False))
586
587 return node_get(name)
Ales Komareka4a9f572016-12-03 20:15:50 +0100588
589
Adam Tengler23d965f2017-05-16 19:14:51 +0000590def _get_node_classes(node_data, class_mapping_fragment):
591 classes = []
592
593 for value_tmpl_string in class_mapping_fragment.get('value_template', []):
594 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
595 rendered_value = value_tmpl.safe_substitute(node_data)
596 classes.append(rendered_value)
597
598 for value in class_mapping_fragment.get('value', []):
599 classes.append(value)
600
601 return classes
602
603
604def _get_params(node_data, class_mapping_fragment):
605 params = {}
606
607 for param_name, param in class_mapping_fragment.items():
608 value = param.get('value', None)
609 value_tmpl_string = param.get('value_template', None)
610 if value:
611 params.update({param_name: value})
612 elif value_tmpl_string:
613 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
614 rendered_value = value_tmpl.safe_substitute(node_data)
615 params.update({param_name: rendered_value})
616
617 return params
618
619
Adam Tengler4d961142017-07-27 15:35:28 +0000620def _validate_condition(node_data, expressions):
621 # allow string expression definition for single expression conditions
622 if isinstance(expressions, six.string_types):
623 expressions = [expressions]
Adam Tengler23d965f2017-05-16 19:14:51 +0000624
Adam Tengler4d961142017-07-27 15:35:28 +0000625 result = []
626 for expression_tmpl_string in expressions:
627 expression_tmpl = Template(expression_tmpl_string.replace('<<', '${').replace('>>', '}'))
628 expression = expression_tmpl.safe_substitute(node_data)
Adam Tengler23d965f2017-05-16 19:14:51 +0000629
Adam Tengler4d961142017-07-27 15:35:28 +0000630 if expression and expression == 'all':
631 result.append(True)
632 elif expression:
633 val_a = expression.split('__')[0]
634 val_b = expression.split('__')[2]
635 condition = expression.split('__')[1]
636 if condition == 'startswith':
637 result.append(val_a.startswith(val_b))
638 elif condition == 'equals':
639 result.append(val_a == val_b)
640
641 return all(result)
Adam Tengler23d965f2017-05-16 19:14:51 +0000642
643
644def node_classify(node_name, node_data={}, class_mapping={}, **kwargs):
645 '''
646 CLassify node by given class_mapping dictionary
647
648 :param node_name: node FQDN
649 :param node_data: dictionary of known informations about the node
650 :param class_mapping: dictionary of classes and parameters, with conditions
651
652 '''
653 # clean node_data
654 node_data = {k: v for (k, v) in node_data.items() if not k.startswith('__')}
655
656 classes = []
657 node_params = {}
658 cluster_params = {}
659 ret = {'node_create': '', 'cluster_param': {}}
660
661 for type_name, node_type in class_mapping.items():
662 valid = _validate_condition(node_data, node_type.get('expression', ''))
663 if valid:
664 gen_classes = _get_node_classes(node_data, node_type.get('node_class', {}))
665 classes = classes + gen_classes
666 gen_node_params = _get_params(node_data, node_type.get('node_param', {}))
667 node_params.update(gen_node_params)
668 gen_cluster_params = _get_params(node_data, node_type.get('cluster_param', {}))
669 cluster_params.update(gen_cluster_params)
670
671 if classes:
672 create_kwargs = {'name': node_name, 'path': '_generated', 'classes': classes, 'parameters': node_params}
673 ret['node_create'] = node_create(**create_kwargs)
674
675 for name, value in cluster_params.items():
676 ret['cluster_param'][name] = cluster_meta_set(name, value)
677
678 return ret
679
680
Ales Komarekb0911892017-08-02 15:47:30 +0200681def validate_yaml():
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200682 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200683 Returns list of all reclass YAML files that contain syntax
684 errors.
685
686 CLI Examples:
687
688 .. code-block:: bash
689
690 salt-call reclass.validate_yaml
691 '''
692 errors = _deps(ret_classes=False, ret_errors=True)
693 if errors:
694 ret = {'Errors': errors}
695 return ret
696
697
698def validate_pillar(node_name=None, **kwargs):
699 '''
700 Validates whether the pillar of given node is in correct state.
701 If node is not specified it validates pillars of all known nodes.
702 Returns error message for every node with currupted metadata.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200703
704 :param node_name: target minion ID
705
706 CLI Examples:
707
708 .. code-block:: bash
709
Ales Komarekb0911892017-08-02 15:47:30 +0200710 salt-call reclass.validate_pillar
711 salt-call reclass.validate_pillar minion-id
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200712 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200713 if node_name is None:
714 ret={}
715 nodes = node_list(**kwargs)
716 for node_name, node in nodes.items():
717 ret.update(validate_pillar(node_name))
718 return ret
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200719 else:
Ales Komarekb0911892017-08-02 15:47:30 +0200720 defaults = find_and_read_configfile()
721 meta = ''
722 error = None
723 try:
724 pillar = ext_pillar(node_name, {}, defaults['storage_type'], defaults['inventory_base_uri'])
725 except (ReclassException, Exception) as e:
726 msg = "Validation failed in %s on %s" % (repr(e), node_name)
727 LOG.error(msg)
728 meta = {'Error': msg}
729 s = str(type(e))
730 if 'yaml.scanner.ScannerError' in s:
731 error = re.sub(r"\r?\n?$", "", repr(str(e)), 1)
732 else:
733 error = e.message
734 if 'Error' in meta:
735 ret = {node_name: error}
736 else:
737 ret = {node_name: ''}
738 return ret
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200739
740
Adam Tengler2b362622017-06-01 14:23:45 +0000741def node_pillar(node_name, **kwargs):
742 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200743 Returns pillar metadata for given node from reclass inventory.
Adam Tengler2b362622017-06-01 14:23:45 +0000744
745 :param node_name: target minion ID
746
747 CLI Examples:
748
749 .. code-block:: bash
750
751 salt-call reclass.node_pillar minion_id
752
753 '''
754 defaults = find_and_read_configfile()
755 pillar = ext_pillar(node_name, {}, defaults['storage_type'], defaults['inventory_base_uri'])
756 output = {node_name: pillar}
757
758 return output
759
760
Ales Komareka4a9f572016-12-03 20:15:50 +0100761def inventory(**connection_args):
762 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200763 Get all nodes in inventory and their associated services/roles.
Ales Komareka4a9f572016-12-03 20:15:50 +0100764
765 CLI Examples:
766
767 .. code-block:: bash
768
769 salt '*' reclass.inventory
770 '''
771 defaults = find_and_read_configfile()
772 storage = get_storage(defaults['storage_type'], _get_nodes_dir(), _get_classes_dir())
773 reclass = Core(storage, None)
774 nodes = reclass.inventory()["nodes"]
775 output = {}
776
777 for node in nodes:
778 service_classification = []
779 role_classification = []
780 for service in nodes[node]['parameters']:
781 if service not in ['_param', 'private_keys', 'public_keys', 'known_hosts']:
782 service_classification.append(service)
783 for role in nodes[node]['parameters'][service]:
784 if role not in ['_support', '_orchestrate', 'common']:
785 role_classification.append('%s.%s' % (service, role))
786 output[node] = {
787 'roles': role_classification,
788 'services': service_classification,
789 }
790 return output
Adam Tengler8a1cf402017-05-16 10:59:35 +0000791
792
793def cluster_meta_list(file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000794 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200795 List all cluster level soft metadata overrides.
Adam Tengler2b362622017-06-01 14:23:45 +0000796
797 :param file_name: name of the override file, defaults to: overrides.yml
798
799 CLI Examples:
800
801 .. code-block:: bash
802
803 salt-call reclass.cluster_meta_list
Adam Tengler2b362622017-06-01 14:23:45 +0000804 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000805 path = os.path.join(_get_cluster_dir(), cluster, file_name)
806 try:
807 with io.open(path, 'r') as file_handle:
808 meta_yaml = yaml.safe_load(file_handle.read())
809 meta = meta_yaml or {}
810 except Exception as e:
811 msg = "Unable to load cluster metadata YAML %s: %s" % (path, repr(e))
812 LOG.debug(msg)
813 meta = {'Error': msg}
814 return meta
815
816
817def cluster_meta_delete(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000818 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200819 Delete cluster level soft metadata override entry.
Adam Tengler2b362622017-06-01 14:23:45 +0000820
821 :param name: name of the override entry (dictionary key)
822 :param file_name: name of the override file, defaults to: overrides.yml
823
824 CLI Examples:
825
826 .. code-block:: bash
827
828 salt-call reclass.cluster_meta_delete foo
Adam Tengler2b362622017-06-01 14:23:45 +0000829 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000830 ret = {}
831 path = os.path.join(_get_cluster_dir(), cluster, file_name)
832 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
833 if 'Error' not in meta:
834 metadata = meta.get('parameters', {}).get('_param', {})
835 if name not in metadata:
836 return ret
837 del metadata[name]
838 try:
839 with io.open(path, 'w') as file_handle:
840 file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
841 except Exception as e:
842 msg = "Unable to save cluster metadata YAML: %s" % repr(e)
843 LOG.error(msg)
844 return {'Error': msg}
845 ret = 'Cluster metadata entry {0} deleted'.format(name)
846 return ret
847
848
849def cluster_meta_set(name, value, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000850 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200851 Create cluster level metadata override entry.
Adam Tengler2b362622017-06-01 14:23:45 +0000852
853 :param name: name of the override entry (dictionary key)
854 :param value: value of the override entry (dictionary value)
855 :param file_name: name of the override file, defaults to: overrides.yml
856
857 CLI Examples:
858
859 .. code-block:: bash
860
861 salt-call reclass.cluster_meta_set foo bar
Adam Tengler2b362622017-06-01 14:23:45 +0000862 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000863 path = os.path.join(_get_cluster_dir(), cluster, file_name)
864 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
865 if 'Error' not in meta:
866 if not meta:
867 meta = {'parameters': {'_param': {}}}
868 metadata = meta.get('parameters', {}).get('_param', {})
869 if name in metadata and metadata[name] == value:
870 return {name: 'Cluster metadata entry %s already exists and is in correct state' % name}
871 metadata.update({name: value})
872 try:
873 with io.open(path, 'w') as file_handle:
874 file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
875 except Exception as e:
876 msg = "Unable to save cluster metadata YAML %s: %s" % (path, repr(e))
877 LOG.error(msg)
878 return {'Error': msg}
879 return cluster_meta_get(name, path, **kwargs)
880 return meta
881
882
883def cluster_meta_get(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000884 '''
885 Get single cluster level override entry
886
887 :param name: name of the override entry (dictionary key)
888 :param file_name: name of the override file, defaults to: overrides.yml
889
890 CLI Examples:
891
892 .. code-block:: bash
893
894 salt-call reclass.cluster_meta_get foo
895
896 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000897 ret = {}
898 path = os.path.join(_get_cluster_dir(), cluster, file_name)
899 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
900 metadata = meta.get('parameters', {}).get('_param', {})
901 if 'Error' in meta:
902 ret['Error'] = meta['Error']
903 elif name in metadata:
904 ret[name] = metadata.get(name)
905
906 return ret
907