blob: 3c06da174a41e6f3b8ef0f9eaa34431bfd51f7f2 [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):
Petr Michalec46a5bad2017-09-18 20:11:43 +020052 # skip hidden files and folders in reclass dir
53 files = [f for f in files if not f[0] == '.']
54 dirs[:] = [d for d in dirs if not d[0] == '.']
55 # translate found init.yml to valid class name
Jiri Broulik7ccb5342017-07-20 17:07:47 +020056 if 'init.yml' in files:
57 class_file = root + '/' + 'init.yml'
58 class_name = class_file.replace(path, '')[:-9].replace('/', '.')
59 classes[class_name] = {'file': class_file}
60
61 for f in files:
62 if f.endswith('.yml') and f != 'init.yml':
63 class_file = root + '/' + f
64 class_name = class_file.replace(path, '')[:-4].replace('/', '.')
65 classes[class_name] = {'file': class_file}
66
67 # read classes
68 for class_name, params in classes.items():
azvyagintsevf5264d52017-12-12 11:49:42 +020069 LOG.debug("Processing:{}".format(params['file']))
Jiri Broulik7ccb5342017-07-20 17:07:47 +020070 with open(params['file'], 'r') as f:
71 # read raw data
72 raw = f.read()
73 pr = re.findall('\${_param:(.*?)}', raw)
74 if pr:
75 params['params_required'] = list(set(pr))
76
77 # load yaml
78 try:
79 data = yaml.load(raw)
80 except yaml.scanner.ScannerError as e:
81 errors.append(params['file'] + ' ' + str(e))
82 pass
83
84 if type(data) == dict:
85 if data.get('classes'):
86 params['includes'] = data.get('classes', [])
87 if data.get('parameters') and data['parameters'].get('_param'):
88 params['params_created'] = data['parameters']['_param']
89
90 if not(data.get('classes') or data.get('parameters')):
91 errors.append(params['file'] + ' ' + 'file missing classes and parameters')
92 else:
93 errors.append(params['file'] + ' ' + 'is not valid yaml')
94
95 if ret_classes:
96 return classes
97 elif ret_errors:
98 return errors
99
100 # find parameters and its usage
101 for class_name, params in classes.items():
102 for pn, pv in params.get('params_created', {}).items():
103 # create param if missing
104 if pn not in soft_params:
105 soft_params[pn] = {'created_at': {}, 'required_at': []}
106
107 # add created_at
108 if class_name not in soft_params[pn]['created_at']:
109 soft_params[pn]['created_at'][class_name] = pv
110
111 for pn in params.get('params_required', []):
112 # create param if missing
113 if pn not in soft_params:
114 soft_params[pn] = {'created_at': {}, 'required_at': []}
115
116 # add created_at
117 soft_params[pn]['required_at'].append(class_name)
118
119 return soft_params
120
121
Ales Komareka4a9f572016-12-03 20:15:50 +0100122def _get_nodes_dir():
123 defaults = find_and_read_configfile()
Vladislav Naumov87113082017-07-24 17:36:50 +0300124 return defaults.get('nodes_uri') or \
125 os.path.join(defaults.get('inventory_base_uri'), 'nodes')
Ales Komareka4a9f572016-12-03 20:15:50 +0100126
127
128def _get_classes_dir():
129 defaults = find_and_read_configfile()
130 return os.path.join(defaults.get('inventory_base_uri'), 'classes')
Ales Komarek166cc672016-07-27 14:17:22 +0200131
132
Adam Tengler8a1cf402017-05-16 10:59:35 +0000133def _get_cluster_dir():
134 classes_dir = _get_classes_dir()
135 return os.path.join(classes_dir, 'cluster')
136
137
Adam Tengler805666d2017-05-15 16:01:13 +0000138def _get_node_meta(name, cluster="default", environment="prd", classes=None, parameters=None):
139 host_name = name.split('.')[0]
140 domain_name = '.'.join(name.split('.')[1:])
141
142 if classes == None:
143 meta_classes = []
144 else:
145 if isinstance(classes, six.string_types):
146 meta_classes = json.loads(classes)
147 else:
148 meta_classes = classes
149
150 if parameters == None:
151 meta_parameters = {}
152 else:
153 if isinstance(parameters, six.string_types):
154 meta_parameters = json.loads(parameters)
155 else:
156 # generate dict from OrderedDict
157 meta_parameters = {k: v for (k, v) in parameters.items()}
158
159 node_meta = {
160 'classes': meta_classes,
161 'parameters': {
162 '_param': meta_parameters,
163 'linux': {
164 'system': {
165 'name': host_name,
166 'domain': domain_name,
167 'cluster': cluster,
168 'environment': environment,
169 }
170 }
171 }
172 }
173
174 return node_meta
175
176
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200177def soft_meta_list():
178 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200179 Returns all defined soft metadata parameters.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200180
181 CLI Examples:
182
183 .. code-block:: bash
184
185 salt '*' reclass.soft_meta_list
186 '''
187 return _deps(ret_classes=False)
188
189
190def class_list():
191 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200192 Returns list of all classes defined within reclass inventory.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200193
194 CLI Examples:
195
196 .. code-block:: bash
197
198 salt '*' reclass.class_list
199 '''
200 return _deps(ret_classes=True)
201
202
203def soft_meta_get(name):
204 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200205 Returns single soft metadata parameter.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200206
Ales Komarekb0911892017-08-02 15:47:30 +0200207 :param name: expects the following format: apt_mk_version
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200208
209 CLI Examples:
210
211 .. code-block:: bash
212
Ales Komarekb0911892017-08-02 15:47:30 +0200213 salt '*' reclass.soft_meta_get openstack_version
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200214 '''
215 soft_params = _deps(ret_classes=False)
216
217 if name in soft_params:
Ales Komarekb0911892017-08-02 15:47:30 +0200218 return {name: soft_params.get(name)}
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200219 else:
Ales Komarekb0911892017-08-02 15:47:30 +0200220 return {'Error': 'No param {0} found'.format(name)}
221
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200222
223def class_get(name):
224 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200225 Returns detailes information about class file in reclass inventory.
226
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200227 :param name: expects the following format classes.system.linux.repo
228
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200229 CLI Examples:
230
231 .. code-block:: bash
232
233 salt '*' reclass.class_get classes.system.linux.repo
234 '''
235 classes = _deps(ret_classes=True)
236 tmp_name = '.' + name
237 if tmp_name in classes:
Ales Komarekb0911892017-08-02 15:47:30 +0200238 return {name: classes.get(tmp_name)}
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200239 else:
Ales Komarekb0911892017-08-02 15:47:30 +0200240 return {'Error': 'No class {0} found'.format(name)}
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200241
242
Ales Komarek166cc672016-07-27 14:17:22 +0200243def node_create(name, path=None, cluster="default", environment="prd", classes=None, parameters=None, **kwargs):
244 '''
245 Create a reclass node
246
247 :param name: new node FQDN
248 :param path: custom path in nodes for new node
249 :param classes: classes given to the new node
250 :param parameters: parameters given to the new node
251 :param environment: node's environment
252 :param cluster: node's cluster
253
254 CLI Examples:
255
256 .. code-block:: bash
257
258 salt '*' reclass.node_create server.domain.com classes=[system.neco1, system.neco2]
259 salt '*' reclass.node_create namespace/test enabled=False
Petr Michalec46a5bad2017-09-18 20:11:43 +0200260
Ales Komarek166cc672016-07-27 14:17:22 +0200261 '''
262 ret = {}
263
264 node = node_get(name=name)
265
266 if node and not "Error" in node:
267 LOG.debug("node {0} exists".format(name))
268 ret[name] = node
269 return ret
270
271 host_name = name.split('.')[0]
272 domain_name = '.'.join(name.split('.')[1:])
273
Adam Tengler805666d2017-05-15 16:01:13 +0000274 node_meta = _get_node_meta(name, cluster, environment, classes, parameters)
Ales Komarek166cc672016-07-27 14:17:22 +0200275 LOG.debug(node_meta)
276
277 if path == None:
Ales Komareka4a9f572016-12-03 20:15:50 +0100278 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200279 else:
Ales Komareka4a9f572016-12-03 20:15:50 +0100280 file_path = os.path.join(_get_nodes_dir(), path, name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200281
282 with open(file_path, 'w') as node_file:
Ales Komareka961df42016-11-21 21:50:24 +0100283 node_file.write(yaml.safe_dump(node_meta, default_flow_style=False))
Ales Komarek71f94b02016-07-27 14:48:57 +0200284
Ales Komarek166cc672016-07-27 14:17:22 +0200285 return node_get(name)
286
Ales Komareka4a9f572016-12-03 20:15:50 +0100287
Ales Komarek166cc672016-07-27 14:17:22 +0200288def node_delete(name, **kwargs):
289 '''
290 Delete a reclass node
291
292 :params node: Node name
293
294 CLI Examples:
295
296 .. code-block:: bash
297
298 salt '*' reclass.node_delete demo01.domain.com
299 salt '*' reclass.node_delete name=demo01.domain.com
300 '''
301
302 node = node_get(name=name)
303
304 if 'Error' in node:
305 return {'Error': 'Unable to retreive node'}
306
307 if node[name]['path'] == '':
Ales Komareka4a9f572016-12-03 20:15:50 +0100308 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200309 else:
Ales Komareka4a9f572016-12-03 20:15:50 +0100310 file_path = os.path.join(_get_nodes_dir(), node[name]['path'], name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200311
312 os.remove(file_path)
313
314 ret = 'Node {0} deleted'.format(name)
315
316 return ret
317
318
319def node_get(name, path=None, **kwargs):
320 '''
321 Return a specific node
322
323 CLI Examples:
324
325 .. code-block:: bash
326
327 salt '*' reclass.node_get host01.domain.com
328 salt '*' reclass.node_get name=host02.domain.com
329 '''
330 ret = {}
331 nodes = node_list(**kwargs)
332
333 if not name in nodes:
334 return {'Error': 'Error in retrieving node'}
335 ret[name] = nodes[name]
336 return ret
337
338
339def node_list(**connection_args):
340 '''
341 Return a list of available nodes
342
343 CLI Example:
344
345 .. code-block:: bash
346
347 salt '*' reclass.node_list
348 '''
349 ret = {}
350
Ales Komareka4a9f572016-12-03 20:15:50 +0100351 for root, sub_folders, files in os.walk(_get_nodes_dir()):
Petr Michalec46a5bad2017-09-18 20:11:43 +0200352 # skip hidden files and folders in reclass dir
353 files = [f for f in files if not f[0] == '.']
Petr Michalec55a43322017-09-19 17:49:56 +0200354 sub_folders[:] = [d for d in sub_folders if not d[0] == '.']
Adam Tengler805666d2017-05-15 16:01:13 +0000355 for fl in files:
356 file_path = os.path.join(root, fl)
357 with open(file_path, 'r') as file_handle:
358 file_read = yaml.load(file_handle.read())
359 file_data = file_read or {}
360 classes = file_data.get('classes', [])
361 parameters = file_data.get('parameters', {}).get('_param', [])
362 name = fl.replace('.yml', '')
Ales Komarek166cc672016-07-27 14:17:22 +0200363 host_name = name.split('.')[0]
364 domain_name = '.'.join(name.split('.')[1:])
Ales Komareka4a9f572016-12-03 20:15:50 +0100365 path = root.replace(_get_nodes_dir()+'/', '')
Ales Komarek166cc672016-07-27 14:17:22 +0200366 ret[name] = {
Ales Komareka4a9f572016-12-03 20:15:50 +0100367 'name': host_name,
368 'domain': domain_name,
369 'cluster': 'default',
370 'environment': 'prd',
371 'path': path,
372 'classes': classes,
373 'parameters': parameters
Ales Komarek166cc672016-07-27 14:17:22 +0200374 }
375
376 return ret
377
Ales Komareka4a9f572016-12-03 20:15:50 +0100378
Adam Tengler2b362622017-06-01 14:23:45 +0000379def _is_valid_ipv4_address(address):
380 try:
381 socket.inet_pton(socket.AF_INET, address)
382 except AttributeError:
383 try:
384 socket.inet_aton(address)
385 except socket.error:
386 return False
387 return address.count('.') == 3
388 except socket.error:
389 return False
390 return True
391
392
393def _is_valid_ipv6_address(address):
394 try:
395 socket.inet_pton(socket.AF_INET6, address)
396 except socket.error:
397 return False
398 return True
399
400
Adam Tengler1f7667b2017-06-06 16:45:51 +0000401def _get_grains(*args, **kwargs):
402 res = __salt__['saltutil.cmd'](tgt='*',
403 fun='grains.item',
404 arg=args,
405 **{'timeout': 10})
406 return res or {}
407
408
409def _guess_host_from_target(network_grains, host, domain=' '):
Adam Tengler2b362622017-06-01 14:23:45 +0000410 '''
411 Guess minion ID from given host and domain arguments. Host argument can contain
412 hostname, FQDN, IPv4 or IPv6 addresses.
413 '''
Adam Tengler1f7667b2017-06-06 16:45:51 +0000414 key = None
415 value = None
416
Adam Tengler2b362622017-06-01 14:23:45 +0000417 if _is_valid_ipv4_address(host):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000418 key = 'ipv4'
419 value = host
Adam Tengler2b362622017-06-01 14:23:45 +0000420 elif _is_valid_ipv6_address(host):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000421 key = 'ipv6'
422 value = host
Adam Tengler2b362622017-06-01 14:23:45 +0000423 elif host.endswith(domain):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000424 key = 'fqdn'
425 value = host
Adam Tengler2b362622017-06-01 14:23:45 +0000426 else:
Adam Tengler1f7667b2017-06-06 16:45:51 +0000427 key = 'fqdn'
428 value = '%s.%s' % (host, domain)
Adam Tengler2b362622017-06-01 14:23:45 +0000429
Adam Tengler1f7667b2017-06-06 16:45:51 +0000430 target = None
431 if network_grains and isinstance(network_grains, dict) and key and value:
432 for minion, grains in network_grains.items():
433 if grains.get('retcode', 1) == 0 and value in grains.get('ret', {}).get(key, ''):
434 target = minion
Adam Tengler2b362622017-06-01 14:23:45 +0000435
Adam Tengler1f7667b2017-06-06 16:45:51 +0000436 return target or host
Adam Tengler2b362622017-06-01 14:23:45 +0000437
438
439def _interpolate_graph_data(graph_data, **kwargs):
440 new_nodes = []
Adam Tengler1f7667b2017-06-06 16:45:51 +0000441 network_grains = _get_grains('ipv4', 'ipv6', 'fqdn')
Adam Tengler2b362622017-06-01 14:23:45 +0000442 for node in graph_data:
Adam Tengler69c7ba92017-06-01 15:59:01 +0000443 if not node.get('relations', []):
444 node['relations'] = []
Adam Tengler2b362622017-06-01 14:23:45 +0000445 for relation in node.get('relations', []):
Adam Tengler12a310d2017-06-05 19:11:29 +0000446 if not relation.get('status', None):
447 relation['status'] = 'unknown'
Adam Tengler2b362622017-06-01 14:23:45 +0000448 if relation.get('host_from_target', None):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000449 host = _guess_host_from_target(network_grains, relation.pop('host_from_target'))
Adam Tengler2b362622017-06-01 14:23:45 +0000450 relation['host'] = host
451 if relation.get('host_external', None):
Ales Komarekb0911892017-08-02 15:47:30 +0200452 parsed_host_external = [urlparse.urlparse(item).netloc
Adam Tengler69c7ba92017-06-01 15:59:01 +0000453 for item
454 in relation.get('host_external', '').split(' ')
Ales Komarekb0911892017-08-02 15:47:30 +0200455 if urlparse.urlparse(item).netloc]
Adam Tengler69c7ba92017-06-01 15:59:01 +0000456 service = parsed_host_external[0] if parsed_host_external else ''
Adam Tengler2b362622017-06-01 14:23:45 +0000457 host = relation.get('service', '')
458 relation['host'] = host
Adam Tengler69c7ba92017-06-01 15:59:01 +0000459 del relation['host_external']
Adam Tengler2b362622017-06-01 14:23:45 +0000460 relation['service'] = service
Adam Tengler69c7ba92017-06-01 15:59:01 +0000461 host_list = [n.get('host', '') for n in graph_data + new_nodes]
462 service_list = [n.get('service', '') for n in graph_data + new_nodes if host in n.get('host', '')]
463 if host not in host_list or (host in host_list and service not in service_list):
Adam Tengler2b362622017-06-01 14:23:45 +0000464 new_node = {
465 'host': host,
466 'service': service,
467 'type': relation.get('type', ''),
468 'relations': []
469 }
470 new_nodes.append(new_node)
471
472 graph_data = graph_data + new_nodes
473
474 return graph_data
475
476
477def _grain_graph_data(*args, **kwargs):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000478 ret = _get_grains('salt:graph')
Adam Tengler2b362622017-06-01 14:23:45 +0000479 graph_data = []
480 for minion_ret in ret.values():
481 if minion_ret.get('retcode', 1) == 0:
482 graph_datum = minion_ret.get('ret', {}).get('salt:graph', [])
483 graph_data = graph_data + graph_datum
484
485 graph_nodes = _interpolate_graph_data(graph_data)
486 graph = {}
487
488 for node in graph_nodes:
489 if node.get('host') not in graph:
490 graph[node.get('host')] = {}
491 graph[node.pop('host')][node.pop('service')] = node
492
493 return {'graph': graph}
494
495
496def _pillar_graph_data(*args, **kwargs):
497 graph = {}
498 nodes = inventory()
499 for node, node_data in nodes.items():
500 for role in node_data.get('roles', []):
501 if node not in graph:
502 graph[node] = {}
503 graph[node][role] = {'relations': []}
504
505 return {'graph': graph}
506
507
508def graph_data(*args, **kwargs):
509 '''
510 Returns graph data for visualization app
511
512 CLI Examples:
513
514 .. code-block:: bash
515
Adam Tengler1f7667b2017-06-06 16:45:51 +0000516 salt-call reclass.graph_data
Petr Michalec46a5bad2017-09-18 20:11:43 +0200517
Adam Tengler2b362622017-06-01 14:23:45 +0000518 '''
519 pillar_data = _pillar_graph_data().get('graph')
520 grain_data = _grain_graph_data().get('graph')
521
522 for host, services in pillar_data.items():
523 for service, service_data in services.items():
524 grain_service = grain_data.get(host, {}).get(service, {})
525 service_data.update(grain_service)
526
527 graph = []
528 for host, services in pillar_data.items():
529 for service, service_data in services.items():
530 additional_data = {
531 'host': host,
Adam Tengler12a310d2017-06-05 19:11:29 +0000532 'service': service,
533 'status': 'unknown'
Adam Tengler2b362622017-06-01 14:23:45 +0000534 }
535 service_data.update(additional_data)
536 graph.append(service_data)
537
538 for host, services in grain_data.items():
539 for service, service_data in services.items():
540 additional_data = {
541 'host': host,
Adam Tengler12a310d2017-06-05 19:11:29 +0000542 'service': service,
543 'status': 'success'
Adam Tengler2b362622017-06-01 14:23:45 +0000544 }
545 service_data.update(additional_data)
546 host_list = [g.get('host', '') for g in graph]
547 service_list = [g.get('service', '') for g in graph if g.get('host') == host]
548 if host not in host_list or (host in host_list and service not in service_list):
549 graph.append(service_data)
550
551 return {'graph': graph}
552
553
Ales Komarek166cc672016-07-27 14:17:22 +0200554def node_update(name, classes=None, parameters=None, **connection_args):
555 '''
556 Update a node metadata information, classes and parameters.
557
558 CLI Examples:
559
560 .. code-block:: bash
561
car-da0da41492017-08-25 11:01:26 +0200562 salt '*' reclass.node_update name=nodename classes="[clas1, class2]" parameters="{param: value, another_param: another_value}"
Ales Komarek166cc672016-07-27 14:17:22 +0200563 '''
564 node = node_get(name=name)
car-da0da41492017-08-25 11:01:26 +0200565 if node.has_key('Error'):
Ales Komarek71f94b02016-07-27 14:48:57 +0200566 return {'Error': 'Error in retrieving node'}
azvyagintsevf5264d52017-12-12 11:49:42 +0200567
car-da0da41492017-08-25 11:01:26 +0200568 for name, values in node.items():
569 param = values.get('parameters', {})
570 path = values.get('path')
571 cluster = values.get('cluster')
572 environment = values.get('environment')
573 write_class = values.get('classes', [])
azvyagintsevf5264d52017-12-12 11:49:42 +0200574
car-da0da41492017-08-25 11:01:26 +0200575 if parameters:
576 param.update(parameters)
azvyagintsevf5264d52017-12-12 11:49:42 +0200577
car-da0da41492017-08-25 11:01:26 +0200578 if classes:
579 for classe in classes:
580 if not classe in write_class:
581 write_class.append(classe)
azvyagintsevf5264d52017-12-12 11:49:42 +0200582
car-da0da41492017-08-25 11:01:26 +0200583 node_meta = _get_node_meta(name, cluster, environment, write_class, param)
584 LOG.debug(node_meta)
azvyagintsevf5264d52017-12-12 11:49:42 +0200585
car-da0da41492017-08-25 11:01:26 +0200586 if path == None:
587 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
588 else:
589 file_path = os.path.join(_get_nodes_dir(), path, name + '.yml')
azvyagintsevf5264d52017-12-12 11:49:42 +0200590
car-da0da41492017-08-25 11:01:26 +0200591 with open(file_path, 'w') as node_file:
592 node_file.write(yaml.safe_dump(node_meta, default_flow_style=False))
azvyagintsevf5264d52017-12-12 11:49:42 +0200593
car-da0da41492017-08-25 11:01:26 +0200594 return node_get(name)
Ales Komareka4a9f572016-12-03 20:15:50 +0100595
596
Adam Tengler23d965f2017-05-16 19:14:51 +0000597def _get_node_classes(node_data, class_mapping_fragment):
598 classes = []
599
600 for value_tmpl_string in class_mapping_fragment.get('value_template', []):
601 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
602 rendered_value = value_tmpl.safe_substitute(node_data)
603 classes.append(rendered_value)
604
605 for value in class_mapping_fragment.get('value', []):
606 classes.append(value)
607
608 return classes
609
610
611def _get_params(node_data, class_mapping_fragment):
612 params = {}
613
614 for param_name, param in class_mapping_fragment.items():
615 value = param.get('value', None)
616 value_tmpl_string = param.get('value_template', None)
617 if value:
618 params.update({param_name: value})
619 elif value_tmpl_string:
620 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
621 rendered_value = value_tmpl.safe_substitute(node_data)
622 params.update({param_name: rendered_value})
623
624 return params
625
626
Adam Tengler4d961142017-07-27 15:35:28 +0000627def _validate_condition(node_data, expressions):
628 # allow string expression definition for single expression conditions
629 if isinstance(expressions, six.string_types):
630 expressions = [expressions]
Adam Tengler23d965f2017-05-16 19:14:51 +0000631
Adam Tengler4d961142017-07-27 15:35:28 +0000632 result = []
633 for expression_tmpl_string in expressions:
634 expression_tmpl = Template(expression_tmpl_string.replace('<<', '${').replace('>>', '}'))
635 expression = expression_tmpl.safe_substitute(node_data)
Adam Tengler23d965f2017-05-16 19:14:51 +0000636
Adam Tengler4d961142017-07-27 15:35:28 +0000637 if expression and expression == 'all':
638 result.append(True)
639 elif expression:
640 val_a = expression.split('__')[0]
641 val_b = expression.split('__')[2]
642 condition = expression.split('__')[1]
643 if condition == 'startswith':
644 result.append(val_a.startswith(val_b))
645 elif condition == 'equals':
646 result.append(val_a == val_b)
647
648 return all(result)
Adam Tengler23d965f2017-05-16 19:14:51 +0000649
650
651def node_classify(node_name, node_data={}, class_mapping={}, **kwargs):
652 '''
653 CLassify node by given class_mapping dictionary
654
655 :param node_name: node FQDN
656 :param node_data: dictionary of known informations about the node
657 :param class_mapping: dictionary of classes and parameters, with conditions
658
659 '''
660 # clean node_data
661 node_data = {k: v for (k, v) in node_data.items() if not k.startswith('__')}
662
663 classes = []
664 node_params = {}
665 cluster_params = {}
666 ret = {'node_create': '', 'cluster_param': {}}
667
668 for type_name, node_type in class_mapping.items():
669 valid = _validate_condition(node_data, node_type.get('expression', ''))
670 if valid:
671 gen_classes = _get_node_classes(node_data, node_type.get('node_class', {}))
672 classes = classes + gen_classes
673 gen_node_params = _get_params(node_data, node_type.get('node_param', {}))
674 node_params.update(gen_node_params)
675 gen_cluster_params = _get_params(node_data, node_type.get('cluster_param', {}))
676 cluster_params.update(gen_cluster_params)
677
678 if classes:
679 create_kwargs = {'name': node_name, 'path': '_generated', 'classes': classes, 'parameters': node_params}
680 ret['node_create'] = node_create(**create_kwargs)
681
682 for name, value in cluster_params.items():
683 ret['cluster_param'][name] = cluster_meta_set(name, value)
684
685 return ret
686
687
Ales Komarekb0911892017-08-02 15:47:30 +0200688def validate_yaml():
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200689 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200690 Returns list of all reclass YAML files that contain syntax
691 errors.
692
693 CLI Examples:
694
695 .. code-block:: bash
696
697 salt-call reclass.validate_yaml
698 '''
699 errors = _deps(ret_classes=False, ret_errors=True)
700 if errors:
701 ret = {'Errors': errors}
702 return ret
703
704
705def validate_pillar(node_name=None, **kwargs):
706 '''
707 Validates whether the pillar of given node is in correct state.
708 If node is not specified it validates pillars of all known nodes.
709 Returns error message for every node with currupted metadata.
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200710
711 :param node_name: target minion ID
712
713 CLI Examples:
714
715 .. code-block:: bash
716
Ales Komarekb0911892017-08-02 15:47:30 +0200717 salt-call reclass.validate_pillar
718 salt-call reclass.validate_pillar minion-id
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200719 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200720 if node_name is None:
721 ret={}
722 nodes = node_list(**kwargs)
723 for node_name, node in nodes.items():
724 ret.update(validate_pillar(node_name))
725 return ret
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200726 else:
Ales Komarekb0911892017-08-02 15:47:30 +0200727 defaults = find_and_read_configfile()
728 meta = ''
729 error = None
730 try:
731 pillar = ext_pillar(node_name, {}, defaults['storage_type'], defaults['inventory_base_uri'])
732 except (ReclassException, Exception) as e:
733 msg = "Validation failed in %s on %s" % (repr(e), node_name)
734 LOG.error(msg)
735 meta = {'Error': msg}
736 s = str(type(e))
737 if 'yaml.scanner.ScannerError' in s:
738 error = re.sub(r"\r?\n?$", "", repr(str(e)), 1)
739 else:
740 error = e.message
741 if 'Error' in meta:
742 ret = {node_name: error}
743 else:
744 ret = {node_name: ''}
745 return ret
Jiri Broulik7ccb5342017-07-20 17:07:47 +0200746
747
Adam Tengler2b362622017-06-01 14:23:45 +0000748def node_pillar(node_name, **kwargs):
749 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200750 Returns pillar metadata for given node from reclass inventory.
Adam Tengler2b362622017-06-01 14:23:45 +0000751
752 :param node_name: target minion ID
753
754 CLI Examples:
755
756 .. code-block:: bash
757
758 salt-call reclass.node_pillar minion_id
759
760 '''
761 defaults = find_and_read_configfile()
762 pillar = ext_pillar(node_name, {}, defaults['storage_type'], defaults['inventory_base_uri'])
763 output = {node_name: pillar}
764
765 return output
766
767
Ales Komareka4a9f572016-12-03 20:15:50 +0100768def inventory(**connection_args):
769 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200770 Get all nodes in inventory and their associated services/roles.
Ales Komareka4a9f572016-12-03 20:15:50 +0100771
772 CLI Examples:
773
774 .. code-block:: bash
775
776 salt '*' reclass.inventory
777 '''
778 defaults = find_and_read_configfile()
779 storage = get_storage(defaults['storage_type'], _get_nodes_dir(), _get_classes_dir())
780 reclass = Core(storage, None)
781 nodes = reclass.inventory()["nodes"]
782 output = {}
783
784 for node in nodes:
785 service_classification = []
786 role_classification = []
787 for service in nodes[node]['parameters']:
788 if service not in ['_param', 'private_keys', 'public_keys', 'known_hosts']:
789 service_classification.append(service)
790 for role in nodes[node]['parameters'][service]:
791 if role not in ['_support', '_orchestrate', 'common']:
792 role_classification.append('%s.%s' % (service, role))
793 output[node] = {
794 'roles': role_classification,
795 'services': service_classification,
796 }
797 return output
Adam Tengler8a1cf402017-05-16 10:59:35 +0000798
799
800def cluster_meta_list(file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000801 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200802 List all cluster level soft metadata overrides.
Adam Tengler2b362622017-06-01 14:23:45 +0000803
804 :param file_name: name of the override file, defaults to: overrides.yml
805
806 CLI Examples:
807
808 .. code-block:: bash
809
810 salt-call reclass.cluster_meta_list
Adam Tengler2b362622017-06-01 14:23:45 +0000811 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000812 path = os.path.join(_get_cluster_dir(), cluster, file_name)
813 try:
814 with io.open(path, 'r') as file_handle:
815 meta_yaml = yaml.safe_load(file_handle.read())
816 meta = meta_yaml or {}
817 except Exception as e:
818 msg = "Unable to load cluster metadata YAML %s: %s" % (path, repr(e))
819 LOG.debug(msg)
820 meta = {'Error': msg}
821 return meta
822
823
824def cluster_meta_delete(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000825 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200826 Delete cluster level soft metadata override entry.
Adam Tengler2b362622017-06-01 14:23:45 +0000827
828 :param name: name of the override entry (dictionary key)
829 :param file_name: name of the override file, defaults to: overrides.yml
830
831 CLI Examples:
832
833 .. code-block:: bash
834
835 salt-call reclass.cluster_meta_delete foo
Adam Tengler2b362622017-06-01 14:23:45 +0000836 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000837 ret = {}
838 path = os.path.join(_get_cluster_dir(), cluster, file_name)
839 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
840 if 'Error' not in meta:
841 metadata = meta.get('parameters', {}).get('_param', {})
842 if name not in metadata:
843 return ret
844 del metadata[name]
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" % repr(e)
850 LOG.error(msg)
851 return {'Error': msg}
852 ret = 'Cluster metadata entry {0} deleted'.format(name)
853 return ret
854
855
856def cluster_meta_set(name, value, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000857 '''
Ales Komarekb0911892017-08-02 15:47:30 +0200858 Create cluster level metadata override entry.
Adam Tengler2b362622017-06-01 14:23:45 +0000859
860 :param name: name of the override entry (dictionary key)
861 :param value: value of the override entry (dictionary value)
862 :param file_name: name of the override file, defaults to: overrides.yml
863
864 CLI Examples:
865
866 .. code-block:: bash
867
868 salt-call reclass.cluster_meta_set foo bar
Adam Tengler2b362622017-06-01 14:23:45 +0000869 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000870 path = os.path.join(_get_cluster_dir(), cluster, file_name)
871 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
872 if 'Error' not in meta:
873 if not meta:
874 meta = {'parameters': {'_param': {}}}
875 metadata = meta.get('parameters', {}).get('_param', {})
876 if name in metadata and metadata[name] == value:
877 return {name: 'Cluster metadata entry %s already exists and is in correct state' % name}
878 metadata.update({name: value})
879 try:
880 with io.open(path, 'w') as file_handle:
881 file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
882 except Exception as e:
883 msg = "Unable to save cluster metadata YAML %s: %s" % (path, repr(e))
884 LOG.error(msg)
885 return {'Error': msg}
886 return cluster_meta_get(name, path, **kwargs)
887 return meta
888
889
890def cluster_meta_get(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000891 '''
892 Get single cluster level override entry
893
894 :param name: name of the override entry (dictionary key)
895 :param file_name: name of the override file, defaults to: overrides.yml
896
897 CLI Examples:
898
899 .. code-block:: bash
900
901 salt-call reclass.cluster_meta_get foo
902
903 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000904 ret = {}
905 path = os.path.join(_get_cluster_dir(), cluster, file_name)
906 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
907 metadata = meta.get('parameters', {}).get('_param', {})
908 if 'Error' in meta:
909 ret['Error'] = meta['Error']
910 elif name in metadata:
911 ret[name] = metadata.get(name)
912
913 return ret
914