blob: 3cefbad91b957b1b6c94059238aacd66adaee152 [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
554 salt '*' reclass.node_update name=nodename classes="[clas1, class2]"
555 '''
556 node = node_get(name=name)
557 if not node.has_key('Error'):
Ales Komarek71f94b02016-07-27 14:48:57 +0200558 node = node[name.split("/")[1]]
559 else:
560 return {'Error': 'Error in retrieving node'}
Ales Komareka4a9f572016-12-03 20:15:50 +0100561
562
Adam Tengler23d965f2017-05-16 19:14:51 +0000563def _get_node_classes(node_data, class_mapping_fragment):
564 classes = []
565
566 for value_tmpl_string in class_mapping_fragment.get('value_template', []):
567 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
568 rendered_value = value_tmpl.safe_substitute(node_data)
569 classes.append(rendered_value)
570
571 for value in class_mapping_fragment.get('value', []):
572 classes.append(value)
573
574 return classes
575
576
577def _get_params(node_data, class_mapping_fragment):
578 params = {}
579
580 for param_name, param in class_mapping_fragment.items():
581 value = param.get('value', None)
582 value_tmpl_string = param.get('value_template', None)
583 if value:
584 params.update({param_name: value})
585 elif value_tmpl_string:
586 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
587 rendered_value = value_tmpl.safe_substitute(node_data)
588 params.update({param_name: rendered_value})
589
590 return params
591
592
Adam Tengler4d961142017-07-27 15:35:28 +0000593def _validate_condition(node_data, expressions):
594 # allow string expression definition for single expression conditions
595 if isinstance(expressions, six.string_types):
596 expressions = [expressions]
Adam Tengler23d965f2017-05-16 19:14:51 +0000597
Adam Tengler4d961142017-07-27 15:35:28 +0000598 result = []
599 for expression_tmpl_string in expressions:
600 expression_tmpl = Template(expression_tmpl_string.replace('<<', '${').replace('>>', '}'))
601 expression = expression_tmpl.safe_substitute(node_data)
Adam Tengler23d965f2017-05-16 19:14:51 +0000602
Adam Tengler4d961142017-07-27 15:35:28 +0000603 if expression and expression == 'all':
604 result.append(True)
605 elif expression:
606 val_a = expression.split('__')[0]
607 val_b = expression.split('__')[2]
608 condition = expression.split('__')[1]
609 if condition == 'startswith':
610 result.append(val_a.startswith(val_b))
611 elif condition == 'equals':
612 result.append(val_a == val_b)
613
614 return all(result)
Adam Tengler23d965f2017-05-16 19:14:51 +0000615
616
617def node_classify(node_name, node_data={}, class_mapping={}, **kwargs):
618 '''
619 CLassify node by given class_mapping dictionary
620
621 :param node_name: node FQDN
622 :param node_data: dictionary of known informations about the node
623 :param class_mapping: dictionary of classes and parameters, with conditions
624
625 '''
626 # clean node_data
627 node_data = {k: v for (k, v) in node_data.items() if not k.startswith('__')}
628
629 classes = []
630 node_params = {}
631 cluster_params = {}
632 ret = {'node_create': '', 'cluster_param': {}}
633
634 for type_name, node_type in class_mapping.items():
635 valid = _validate_condition(node_data, node_type.get('expression', ''))
636 if valid:
637 gen_classes = _get_node_classes(node_data, node_type.get('node_class', {}))
638 classes = classes + gen_classes
639 gen_node_params = _get_params(node_data, node_type.get('node_param', {}))
640 node_params.update(gen_node_params)
641 gen_cluster_params = _get_params(node_data, node_type.get('cluster_param', {}))
642 cluster_params.update(gen_cluster_params)
643
644 if classes:
645 create_kwargs = {'name': node_name, 'path': '_generated', 'classes': classes, 'parameters': node_params}
646 ret['node_create'] = node_create(**create_kwargs)
647
648 for name, value in cluster_params.items():
649 ret['cluster_param'][name] = cluster_meta_set(name, value)
650
651 return ret
652
653
Ales Komarekb0911892017-08-02 15:47:30 +0200654def validate_yaml():
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200655 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200656 Returns list of all reclass YAML files that contain syntax
657 errors.
658
659 CLI Examples:
660
661 .. code-block:: bash
662
663 salt-call reclass.validate_yaml
664 '''
665 errors = _deps(ret_classes=False, ret_errors=True)
666 if errors:
667 ret = {'Errors': errors}
668 return ret
669
670
671def validate_pillar(node_name=None, **kwargs):
672 '''
673 Validates whether the pillar of given node is in correct state.
674 If node is not specified it validates pillars of all known nodes.
675 Returns error message for every node with currupted metadata.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200676
677 :param node_name: target minion ID
678
679 CLI Examples:
680
681 .. code-block:: bash
682
Ales Komarekb0911892017-08-02 15:47:30 +0200683 salt-call reclass.validate_pillar
684 salt-call reclass.validate_pillar minion-id
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200685 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200686 if node_name is None:
687 ret={}
688 nodes = node_list(**kwargs)
689 for node_name, node in nodes.items():
690 ret.update(validate_pillar(node_name))
691 return ret
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200692 else:
Ales Komarekb0911892017-08-02 15:47:30 +0200693 defaults = find_and_read_configfile()
694 meta = ''
695 error = None
696 try:
697 pillar = ext_pillar(node_name, {}, defaults['storage_type'], defaults['inventory_base_uri'])
698 except (ReclassException, Exception) as e:
699 msg = "Validation failed in %s on %s" % (repr(e), node_name)
700 LOG.error(msg)
701 meta = {'Error': msg}
702 s = str(type(e))
703 if 'yaml.scanner.ScannerError' in s:
704 error = re.sub(r"\r?\n?$", "", repr(str(e)), 1)
705 else:
706 error = e.message
707 if 'Error' in meta:
708 ret = {node_name: error}
709 else:
710 ret = {node_name: ''}
711 return ret
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200712
713
Adam Tengler2b362622017-06-01 14:23:45 +0000714def node_pillar(node_name, **kwargs):
715 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200716 Returns pillar metadata for given node from reclass inventory.
Adam Tengler2b362622017-06-01 14:23:45 +0000717
718 :param node_name: target minion ID
719
720 CLI Examples:
721
722 .. code-block:: bash
723
724 salt-call reclass.node_pillar minion_id
725
726 '''
727 defaults = find_and_read_configfile()
728 pillar = ext_pillar(node_name, {}, defaults['storage_type'], defaults['inventory_base_uri'])
729 output = {node_name: pillar}
730
731 return output
732
733
Ales Komareka4a9f572016-12-03 20:15:50 +0100734def inventory(**connection_args):
735 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200736 Get all nodes in inventory and their associated services/roles.
Ales Komareka4a9f572016-12-03 20:15:50 +0100737
738 CLI Examples:
739
740 .. code-block:: bash
741
742 salt '*' reclass.inventory
743 '''
744 defaults = find_and_read_configfile()
745 storage = get_storage(defaults['storage_type'], _get_nodes_dir(), _get_classes_dir())
746 reclass = Core(storage, None)
747 nodes = reclass.inventory()["nodes"]
748 output = {}
749
750 for node in nodes:
751 service_classification = []
752 role_classification = []
753 for service in nodes[node]['parameters']:
754 if service not in ['_param', 'private_keys', 'public_keys', 'known_hosts']:
755 service_classification.append(service)
756 for role in nodes[node]['parameters'][service]:
757 if role not in ['_support', '_orchestrate', 'common']:
758 role_classification.append('%s.%s' % (service, role))
759 output[node] = {
760 'roles': role_classification,
761 'services': service_classification,
762 }
763 return output
Adam Tengler8a1cf402017-05-16 10:59:35 +0000764
765
766def cluster_meta_list(file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000767 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200768 List all cluster level soft metadata overrides.
Adam Tengler2b362622017-06-01 14:23:45 +0000769
770 :param file_name: name of the override file, defaults to: overrides.yml
771
772 CLI Examples:
773
774 .. code-block:: bash
775
776 salt-call reclass.cluster_meta_list
Adam Tengler2b362622017-06-01 14:23:45 +0000777 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000778 path = os.path.join(_get_cluster_dir(), cluster, file_name)
779 try:
780 with io.open(path, 'r') as file_handle:
781 meta_yaml = yaml.safe_load(file_handle.read())
782 meta = meta_yaml or {}
783 except Exception as e:
784 msg = "Unable to load cluster metadata YAML %s: %s" % (path, repr(e))
785 LOG.debug(msg)
786 meta = {'Error': msg}
787 return meta
788
789
790def cluster_meta_delete(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000791 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200792 Delete cluster level soft metadata override entry.
Adam Tengler2b362622017-06-01 14:23:45 +0000793
794 :param name: name of the override entry (dictionary key)
795 :param file_name: name of the override file, defaults to: overrides.yml
796
797 CLI Examples:
798
799 .. code-block:: bash
800
801 salt-call reclass.cluster_meta_delete foo
Adam Tengler2b362622017-06-01 14:23:45 +0000802 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000803 ret = {}
804 path = os.path.join(_get_cluster_dir(), cluster, file_name)
805 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
806 if 'Error' not in meta:
807 metadata = meta.get('parameters', {}).get('_param', {})
808 if name not in metadata:
809 return ret
810 del metadata[name]
811 try:
812 with io.open(path, 'w') as file_handle:
813 file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
814 except Exception as e:
815 msg = "Unable to save cluster metadata YAML: %s" % repr(e)
816 LOG.error(msg)
817 return {'Error': msg}
818 ret = 'Cluster metadata entry {0} deleted'.format(name)
819 return ret
820
821
822def cluster_meta_set(name, value, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000823 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200824 Create cluster level metadata override entry.
Adam Tengler2b362622017-06-01 14:23:45 +0000825
826 :param name: name of the override entry (dictionary key)
827 :param value: value of the override entry (dictionary value)
828 :param file_name: name of the override file, defaults to: overrides.yml
829
830 CLI Examples:
831
832 .. code-block:: bash
833
834 salt-call reclass.cluster_meta_set foo bar
Adam Tengler2b362622017-06-01 14:23:45 +0000835 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000836 path = os.path.join(_get_cluster_dir(), cluster, file_name)
837 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
838 if 'Error' not in meta:
839 if not meta:
840 meta = {'parameters': {'_param': {}}}
841 metadata = meta.get('parameters', {}).get('_param', {})
842 if name in metadata and metadata[name] == value:
843 return {name: 'Cluster metadata entry %s already exists and is in correct state' % name}
844 metadata.update({name: value})
845 try:
846 with io.open(path, 'w') as file_handle:
847 file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
848 except Exception as e:
849 msg = "Unable to save cluster metadata YAML %s: %s" % (path, repr(e))
850 LOG.error(msg)
851 return {'Error': msg}
852 return cluster_meta_get(name, path, **kwargs)
853 return meta
854
855
856def cluster_meta_get(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000857 '''
858 Get single cluster level override entry
859
860 :param name: name of the override entry (dictionary key)
861 :param file_name: name of the override file, defaults to: overrides.yml
862
863 CLI Examples:
864
865 .. code-block:: bash
866
867 salt-call reclass.cluster_meta_get foo
868
869 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000870 ret = {}
871 path = os.path.join(_get_cluster_dir(), cluster, file_name)
872 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
873 metadata = meta.get('parameters', {}).get('_param', {})
874 if 'Error' in meta:
875 ret['Error'] = meta['Error']
876 elif name in metadata:
877 ret[name] = metadata.get(name)
878
879 return ret
880