blob: 23b1e9983d85f6acbbba73832e287068a26728f5 [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'):
Ales Komarek71f94b02016-07-27 14:48:57 +0200558 return {'Error': 'Error in retrieving node'}
car-da0da41492017-08-25 11:01:26 +0200559
560 for name, values in node.items():
561 param = values.get('parameters', {})
562 path = values.get('path')
563 cluster = values.get('cluster')
564 environment = values.get('environment')
565 write_class = values.get('classes', [])
566
567 if parameters:
568 param.update(parameters)
569
570 if classes:
571 for classe in classes:
572 if not classe in write_class:
573 write_class.append(classe)
574
575 node_meta = _get_node_meta(name, cluster, environment, write_class, param)
576 LOG.debug(node_meta)
577
578 if path == None:
579 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
580 else:
581 file_path = os.path.join(_get_nodes_dir(), path, name + '.yml')
582
583 with open(file_path, 'w') as node_file:
584 node_file.write(yaml.safe_dump(node_meta, default_flow_style=False))
585
586 return node_get(name)
Ales Komareka4a9f572016-12-03 20:15:50 +0100587
588
Adam Tengler23d965f2017-05-16 19:14:51 +0000589def _get_node_classes(node_data, class_mapping_fragment):
590 classes = []
591
592 for value_tmpl_string in class_mapping_fragment.get('value_template', []):
593 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
594 rendered_value = value_tmpl.safe_substitute(node_data)
595 classes.append(rendered_value)
596
597 for value in class_mapping_fragment.get('value', []):
598 classes.append(value)
599
600 return classes
601
602
603def _get_params(node_data, class_mapping_fragment):
604 params = {}
605
606 for param_name, param in class_mapping_fragment.items():
607 value = param.get('value', None)
608 value_tmpl_string = param.get('value_template', None)
609 if value:
610 params.update({param_name: value})
611 elif value_tmpl_string:
612 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
613 rendered_value = value_tmpl.safe_substitute(node_data)
614 params.update({param_name: rendered_value})
615
616 return params
617
618
Adam Tengler4d961142017-07-27 15:35:28 +0000619def _validate_condition(node_data, expressions):
620 # allow string expression definition for single expression conditions
621 if isinstance(expressions, six.string_types):
622 expressions = [expressions]
Adam Tengler23d965f2017-05-16 19:14:51 +0000623
Adam Tengler4d961142017-07-27 15:35:28 +0000624 result = []
625 for expression_tmpl_string in expressions:
626 expression_tmpl = Template(expression_tmpl_string.replace('<<', '${').replace('>>', '}'))
627 expression = expression_tmpl.safe_substitute(node_data)
Adam Tengler23d965f2017-05-16 19:14:51 +0000628
Adam Tengler4d961142017-07-27 15:35:28 +0000629 if expression and expression == 'all':
630 result.append(True)
631 elif expression:
632 val_a = expression.split('__')[0]
633 val_b = expression.split('__')[2]
634 condition = expression.split('__')[1]
635 if condition == 'startswith':
636 result.append(val_a.startswith(val_b))
637 elif condition == 'equals':
638 result.append(val_a == val_b)
639
640 return all(result)
Adam Tengler23d965f2017-05-16 19:14:51 +0000641
642
643def node_classify(node_name, node_data={}, class_mapping={}, **kwargs):
644 '''
645 CLassify node by given class_mapping dictionary
646
647 :param node_name: node FQDN
648 :param node_data: dictionary of known informations about the node
649 :param class_mapping: dictionary of classes and parameters, with conditions
650
651 '''
652 # clean node_data
653 node_data = {k: v for (k, v) in node_data.items() if not k.startswith('__')}
654
655 classes = []
656 node_params = {}
657 cluster_params = {}
658 ret = {'node_create': '', 'cluster_param': {}}
659
660 for type_name, node_type in class_mapping.items():
661 valid = _validate_condition(node_data, node_type.get('expression', ''))
662 if valid:
663 gen_classes = _get_node_classes(node_data, node_type.get('node_class', {}))
664 classes = classes + gen_classes
665 gen_node_params = _get_params(node_data, node_type.get('node_param', {}))
666 node_params.update(gen_node_params)
667 gen_cluster_params = _get_params(node_data, node_type.get('cluster_param', {}))
668 cluster_params.update(gen_cluster_params)
669
670 if classes:
671 create_kwargs = {'name': node_name, 'path': '_generated', 'classes': classes, 'parameters': node_params}
672 ret['node_create'] = node_create(**create_kwargs)
673
674 for name, value in cluster_params.items():
675 ret['cluster_param'][name] = cluster_meta_set(name, value)
676
677 return ret
678
679
Ales Komarekb0911892017-08-02 15:47:30 +0200680def validate_yaml():
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200681 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200682 Returns list of all reclass YAML files that contain syntax
683 errors.
684
685 CLI Examples:
686
687 .. code-block:: bash
688
689 salt-call reclass.validate_yaml
690 '''
691 errors = _deps(ret_classes=False, ret_errors=True)
692 if errors:
693 ret = {'Errors': errors}
694 return ret
695
696
697def validate_pillar(node_name=None, **kwargs):
698 '''
699 Validates whether the pillar of given node is in correct state.
700 If node is not specified it validates pillars of all known nodes.
701 Returns error message for every node with currupted metadata.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200702
703 :param node_name: target minion ID
704
705 CLI Examples:
706
707 .. code-block:: bash
708
Ales Komarekb0911892017-08-02 15:47:30 +0200709 salt-call reclass.validate_pillar
710 salt-call reclass.validate_pillar minion-id
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200711 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200712 if node_name is None:
713 ret={}
714 nodes = node_list(**kwargs)
715 for node_name, node in nodes.items():
716 ret.update(validate_pillar(node_name))
717 return ret
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200718 else:
Ales Komarekb0911892017-08-02 15:47:30 +0200719 defaults = find_and_read_configfile()
720 meta = ''
721 error = None
722 try:
723 pillar = ext_pillar(node_name, {}, defaults['storage_type'], defaults['inventory_base_uri'])
724 except (ReclassException, Exception) as e:
725 msg = "Validation failed in %s on %s" % (repr(e), node_name)
726 LOG.error(msg)
727 meta = {'Error': msg}
728 s = str(type(e))
729 if 'yaml.scanner.ScannerError' in s:
730 error = re.sub(r"\r?\n?$", "", repr(str(e)), 1)
731 else:
732 error = e.message
733 if 'Error' in meta:
734 ret = {node_name: error}
735 else:
736 ret = {node_name: ''}
737 return ret
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200738
739
Adam Tengler2b362622017-06-01 14:23:45 +0000740def node_pillar(node_name, **kwargs):
741 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200742 Returns pillar metadata for given node from reclass inventory.
Adam Tengler2b362622017-06-01 14:23:45 +0000743
744 :param node_name: target minion ID
745
746 CLI Examples:
747
748 .. code-block:: bash
749
750 salt-call reclass.node_pillar minion_id
751
752 '''
753 defaults = find_and_read_configfile()
754 pillar = ext_pillar(node_name, {}, defaults['storage_type'], defaults['inventory_base_uri'])
755 output = {node_name: pillar}
756
757 return output
758
759
Ales Komareka4a9f572016-12-03 20:15:50 +0100760def inventory(**connection_args):
761 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200762 Get all nodes in inventory and their associated services/roles.
Ales Komareka4a9f572016-12-03 20:15:50 +0100763
764 CLI Examples:
765
766 .. code-block:: bash
767
768 salt '*' reclass.inventory
769 '''
770 defaults = find_and_read_configfile()
771 storage = get_storage(defaults['storage_type'], _get_nodes_dir(), _get_classes_dir())
772 reclass = Core(storage, None)
773 nodes = reclass.inventory()["nodes"]
774 output = {}
775
776 for node in nodes:
777 service_classification = []
778 role_classification = []
779 for service in nodes[node]['parameters']:
780 if service not in ['_param', 'private_keys', 'public_keys', 'known_hosts']:
781 service_classification.append(service)
782 for role in nodes[node]['parameters'][service]:
783 if role not in ['_support', '_orchestrate', 'common']:
784 role_classification.append('%s.%s' % (service, role))
785 output[node] = {
786 'roles': role_classification,
787 'services': service_classification,
788 }
789 return output
Adam Tengler8a1cf402017-05-16 10:59:35 +0000790
791
792def cluster_meta_list(file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000793 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200794 List all cluster level soft metadata overrides.
Adam Tengler2b362622017-06-01 14:23:45 +0000795
796 :param file_name: name of the override file, defaults to: overrides.yml
797
798 CLI Examples:
799
800 .. code-block:: bash
801
802 salt-call reclass.cluster_meta_list
Adam Tengler2b362622017-06-01 14:23:45 +0000803 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000804 path = os.path.join(_get_cluster_dir(), cluster, file_name)
805 try:
806 with io.open(path, 'r') as file_handle:
807 meta_yaml = yaml.safe_load(file_handle.read())
808 meta = meta_yaml or {}
809 except Exception as e:
810 msg = "Unable to load cluster metadata YAML %s: %s" % (path, repr(e))
811 LOG.debug(msg)
812 meta = {'Error': msg}
813 return meta
814
815
816def cluster_meta_delete(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000817 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200818 Delete cluster level soft metadata override entry.
Adam Tengler2b362622017-06-01 14:23:45 +0000819
820 :param name: name of the override entry (dictionary key)
821 :param file_name: name of the override file, defaults to: overrides.yml
822
823 CLI Examples:
824
825 .. code-block:: bash
826
827 salt-call reclass.cluster_meta_delete foo
Adam Tengler2b362622017-06-01 14:23:45 +0000828 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000829 ret = {}
830 path = os.path.join(_get_cluster_dir(), cluster, file_name)
831 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
832 if 'Error' not in meta:
833 metadata = meta.get('parameters', {}).get('_param', {})
834 if name not in metadata:
835 return ret
836 del metadata[name]
837 try:
838 with io.open(path, 'w') as file_handle:
839 file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
840 except Exception as e:
841 msg = "Unable to save cluster metadata YAML: %s" % repr(e)
842 LOG.error(msg)
843 return {'Error': msg}
844 ret = 'Cluster metadata entry {0} deleted'.format(name)
845 return ret
846
847
848def cluster_meta_set(name, value, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000849 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200850 Create cluster level metadata override entry.
Adam Tengler2b362622017-06-01 14:23:45 +0000851
852 :param name: name of the override entry (dictionary key)
853 :param value: value of the override entry (dictionary value)
854 :param file_name: name of the override file, defaults to: overrides.yml
855
856 CLI Examples:
857
858 .. code-block:: bash
859
860 salt-call reclass.cluster_meta_set foo bar
Adam Tengler2b362622017-06-01 14:23:45 +0000861 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000862 path = os.path.join(_get_cluster_dir(), cluster, file_name)
863 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
864 if 'Error' not in meta:
865 if not meta:
866 meta = {'parameters': {'_param': {}}}
867 metadata = meta.get('parameters', {}).get('_param', {})
868 if name in metadata and metadata[name] == value:
869 return {name: 'Cluster metadata entry %s already exists and is in correct state' % name}
870 metadata.update({name: value})
871 try:
872 with io.open(path, 'w') as file_handle:
873 file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
874 except Exception as e:
875 msg = "Unable to save cluster metadata YAML %s: %s" % (path, repr(e))
876 LOG.error(msg)
877 return {'Error': msg}
878 return cluster_meta_get(name, path, **kwargs)
879 return meta
880
881
882def cluster_meta_get(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000883 '''
884 Get single cluster level override entry
885
886 :param name: name of the override entry (dictionary key)
887 :param file_name: name of the override file, defaults to: overrides.yml
888
889 CLI Examples:
890
891 .. code-block:: bash
892
893 salt-call reclass.cluster_meta_get foo
894
895 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000896 ret = {}
897 path = os.path.join(_get_cluster_dir(), cluster, file_name)
898 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
899 metadata = meta.get('parameters', {}).get('_param', {})
900 if 'Error' in meta:
901 ret['Error'] = meta['Error']
902 elif name in metadata:
903 ret[name] = metadata.get(name)
904
905 return ret
906