blob: 732e4ef99f8e11ca32a3cd6493e421bab68c96ab [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
17
Adam Tengler2b362622017-06-01 14:23:45 +000018from urlparse import urlparse
Ales Komareka4a9f572016-12-03 20:15:50 +010019from reclass import get_storage, output
Adam Tengler2b362622017-06-01 14:23:45 +000020from reclass.adapters.salt import ext_pillar
Ales Komareka4a9f572016-12-03 20:15:50 +010021from reclass.core import Core
22from reclass.config import find_and_read_configfile
Adam Tengler23d965f2017-05-16 19:14:51 +000023from string import Template
Ales Komarek166cc672016-07-27 14:17:22 +020024
Ales Komareka4a9f572016-12-03 20:15:50 +010025LOG = logging.getLogger(__name__)
Ales Komarek166cc672016-07-27 14:17:22 +020026
Ales Komareka961df42016-11-21 21:50:24 +010027
Ales Komarek166cc672016-07-27 14:17:22 +020028def __virtual__():
29 '''
30 Only load this module if reclass
31 is installed on this minion.
32 '''
33 return 'reclass'
34
35
Ales Komareka4a9f572016-12-03 20:15:50 +010036def _get_nodes_dir():
37 defaults = find_and_read_configfile()
38 return os.path.join(defaults.get('inventory_base_uri'), 'nodes')
39
40
41def _get_classes_dir():
42 defaults = find_and_read_configfile()
43 return os.path.join(defaults.get('inventory_base_uri'), 'classes')
Ales Komarek166cc672016-07-27 14:17:22 +020044
45
Adam Tengler8a1cf402017-05-16 10:59:35 +000046def _get_cluster_dir():
47 classes_dir = _get_classes_dir()
48 return os.path.join(classes_dir, 'cluster')
49
50
Adam Tengler805666d2017-05-15 16:01:13 +000051def _get_node_meta(name, cluster="default", environment="prd", classes=None, parameters=None):
52 host_name = name.split('.')[0]
53 domain_name = '.'.join(name.split('.')[1:])
54
55 if classes == None:
56 meta_classes = []
57 else:
58 if isinstance(classes, six.string_types):
59 meta_classes = json.loads(classes)
60 else:
61 meta_classes = classes
62
63 if parameters == None:
64 meta_parameters = {}
65 else:
66 if isinstance(parameters, six.string_types):
67 meta_parameters = json.loads(parameters)
68 else:
69 # generate dict from OrderedDict
70 meta_parameters = {k: v for (k, v) in parameters.items()}
71
72 node_meta = {
73 'classes': meta_classes,
74 'parameters': {
75 '_param': meta_parameters,
76 'linux': {
77 'system': {
78 'name': host_name,
79 'domain': domain_name,
80 'cluster': cluster,
81 'environment': environment,
82 }
83 }
84 }
85 }
86
87 return node_meta
88
89
Ales Komarek166cc672016-07-27 14:17:22 +020090def node_create(name, path=None, cluster="default", environment="prd", classes=None, parameters=None, **kwargs):
91 '''
92 Create a reclass node
93
94 :param name: new node FQDN
95 :param path: custom path in nodes for new node
96 :param classes: classes given to the new node
97 :param parameters: parameters given to the new node
98 :param environment: node's environment
99 :param cluster: node's cluster
100
101 CLI Examples:
102
103 .. code-block:: bash
104
105 salt '*' reclass.node_create server.domain.com classes=[system.neco1, system.neco2]
106 salt '*' reclass.node_create namespace/test enabled=False
107
108 '''
109 ret = {}
110
111 node = node_get(name=name)
112
113 if node and not "Error" in node:
114 LOG.debug("node {0} exists".format(name))
115 ret[name] = node
116 return ret
117
118 host_name = name.split('.')[0]
119 domain_name = '.'.join(name.split('.')[1:])
120
Adam Tengler805666d2017-05-15 16:01:13 +0000121 node_meta = _get_node_meta(name, cluster, environment, classes, parameters)
Ales Komarek166cc672016-07-27 14:17:22 +0200122 LOG.debug(node_meta)
123
124 if path == None:
Ales Komareka4a9f572016-12-03 20:15:50 +0100125 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200126 else:
Ales Komareka4a9f572016-12-03 20:15:50 +0100127 file_path = os.path.join(_get_nodes_dir(), path, name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200128
129 with open(file_path, 'w') as node_file:
Ales Komareka961df42016-11-21 21:50:24 +0100130 node_file.write(yaml.safe_dump(node_meta, default_flow_style=False))
Ales Komarek71f94b02016-07-27 14:48:57 +0200131
Ales Komarek166cc672016-07-27 14:17:22 +0200132 return node_get(name)
133
Ales Komareka4a9f572016-12-03 20:15:50 +0100134
Ales Komarek166cc672016-07-27 14:17:22 +0200135def node_delete(name, **kwargs):
136 '''
137 Delete a reclass node
138
139 :params node: Node name
140
141 CLI Examples:
142
143 .. code-block:: bash
144
145 salt '*' reclass.node_delete demo01.domain.com
146 salt '*' reclass.node_delete name=demo01.domain.com
147 '''
148
149 node = node_get(name=name)
150
151 if 'Error' in node:
152 return {'Error': 'Unable to retreive node'}
153
154 if node[name]['path'] == '':
Ales Komareka4a9f572016-12-03 20:15:50 +0100155 file_path = os.path.join(_get_nodes_dir(), name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200156 else:
Ales Komareka4a9f572016-12-03 20:15:50 +0100157 file_path = os.path.join(_get_nodes_dir(), node[name]['path'], name + '.yml')
Ales Komarek166cc672016-07-27 14:17:22 +0200158
159 os.remove(file_path)
160
161 ret = 'Node {0} deleted'.format(name)
162
163 return ret
164
165
166def node_get(name, path=None, **kwargs):
167 '''
168 Return a specific node
169
170 CLI Examples:
171
172 .. code-block:: bash
173
174 salt '*' reclass.node_get host01.domain.com
175 salt '*' reclass.node_get name=host02.domain.com
176 '''
177 ret = {}
178 nodes = node_list(**kwargs)
179
180 if not name in nodes:
181 return {'Error': 'Error in retrieving node'}
182 ret[name] = nodes[name]
183 return ret
184
185
186def node_list(**connection_args):
187 '''
188 Return a list of available nodes
189
190 CLI Example:
191
192 .. code-block:: bash
193
194 salt '*' reclass.node_list
195 '''
196 ret = {}
197
Ales Komareka4a9f572016-12-03 20:15:50 +0100198 for root, sub_folders, files in os.walk(_get_nodes_dir()):
Adam Tengler805666d2017-05-15 16:01:13 +0000199 for fl in files:
200 file_path = os.path.join(root, fl)
201 with open(file_path, 'r') as file_handle:
202 file_read = yaml.load(file_handle.read())
203 file_data = file_read or {}
204 classes = file_data.get('classes', [])
205 parameters = file_data.get('parameters', {}).get('_param', [])
206 name = fl.replace('.yml', '')
Ales Komarek166cc672016-07-27 14:17:22 +0200207 host_name = name.split('.')[0]
208 domain_name = '.'.join(name.split('.')[1:])
Ales Komareka4a9f572016-12-03 20:15:50 +0100209 path = root.replace(_get_nodes_dir()+'/', '')
Ales Komarek166cc672016-07-27 14:17:22 +0200210 ret[name] = {
Ales Komareka4a9f572016-12-03 20:15:50 +0100211 'name': host_name,
212 'domain': domain_name,
213 'cluster': 'default',
214 'environment': 'prd',
215 'path': path,
216 'classes': classes,
217 'parameters': parameters
Ales Komarek166cc672016-07-27 14:17:22 +0200218 }
219
220 return ret
221
Ales Komareka4a9f572016-12-03 20:15:50 +0100222
Adam Tengler2b362622017-06-01 14:23:45 +0000223def _is_valid_ipv4_address(address):
224 try:
225 socket.inet_pton(socket.AF_INET, address)
226 except AttributeError:
227 try:
228 socket.inet_aton(address)
229 except socket.error:
230 return False
231 return address.count('.') == 3
232 except socket.error:
233 return False
234 return True
235
236
237def _is_valid_ipv6_address(address):
238 try:
239 socket.inet_pton(socket.AF_INET6, address)
240 except socket.error:
241 return False
242 return True
243
244
245def _guess_host_from_target(host, domain=None):
246 '''
247 Guess minion ID from given host and domain arguments. Host argument can contain
248 hostname, FQDN, IPv4 or IPv6 addresses.
249 '''
250 if _is_valid_ipv4_address(host):
251 tgt = 'ipv4:%s' % host
252 elif _is_valid_ipv6_address(host):
253 tgt = 'ipv6:%s' % host
254 elif host.endswith(domain):
255 tgt = 'fqdn:%s' % host
256 else:
257 tgt = 'fqdn:%s.%s' % (host, domain)
258
259 res = __salt__['saltutil.cmd'](tgt=tgt,
260 expr_form='grain',
261 fun='grains.item',
262 arg=('id',))
263
264 return res.values()[0].get('ret', {}).get('id', '')
265
266
267def _interpolate_graph_data(graph_data, **kwargs):
268 new_nodes = []
269 for node in graph_data:
270 for relation in node.get('relations', []):
271 if relation.get('host_from_target', None):
272 host = _guess_host_from_target(relation.pop('host_from_target'))
273 relation['host'] = host
274 if relation.get('host_external', None):
275 parsed_host_external = urlparse(relation.pop('host_external'))
276 service = parsed_host_external.netloc
277 host = relation.get('service', '')
278 relation['host'] = host
279 relation['service'] = service
280 if host not in [n.get('host', '') for n in graph_data + new_nodes]:
281 new_node = {
282 'host': host,
283 'service': service,
284 'type': relation.get('type', ''),
285 'relations': []
286 }
287 new_nodes.append(new_node)
288
289 graph_data = graph_data + new_nodes
290
291 return graph_data
292
293
294def _grain_graph_data(*args, **kwargs):
295 ret = __salt__['saltutil.cmd'](tgt='*',
296 fun='grains.item',
297 arg=('salt:graph',))
298 graph_data = []
299 for minion_ret in ret.values():
300 if minion_ret.get('retcode', 1) == 0:
301 graph_datum = minion_ret.get('ret', {}).get('salt:graph', [])
302 graph_data = graph_data + graph_datum
303
304 graph_nodes = _interpolate_graph_data(graph_data)
305 graph = {}
306
307 for node in graph_nodes:
308 if node.get('host') not in graph:
309 graph[node.get('host')] = {}
310 graph[node.pop('host')][node.pop('service')] = node
311
312 return {'graph': graph}
313
314
315def _pillar_graph_data(*args, **kwargs):
316 graph = {}
317 nodes = inventory()
318 for node, node_data in nodes.items():
319 for role in node_data.get('roles', []):
320 if node not in graph:
321 graph[node] = {}
322 graph[node][role] = {'relations': []}
323
324 return {'graph': graph}
325
326
327def graph_data(*args, **kwargs):
328 '''
329 Returns graph data for visualization app
330
331 CLI Examples:
332
333 .. code-block:: bash
334
335 salt '*' reclass.graph_data
336
337 '''
338 pillar_data = _pillar_graph_data().get('graph')
339 grain_data = _grain_graph_data().get('graph')
340
341 for host, services in pillar_data.items():
342 for service, service_data in services.items():
343 grain_service = grain_data.get(host, {}).get(service, {})
344 service_data.update(grain_service)
345
346 graph = []
347 for host, services in pillar_data.items():
348 for service, service_data in services.items():
349 additional_data = {
350 'host': host,
351 'service': service
352 }
353 service_data.update(additional_data)
354 graph.append(service_data)
355
356 for host, services in grain_data.items():
357 for service, service_data in services.items():
358 additional_data = {
359 'host': host,
360 'service': service
361 }
362 service_data.update(additional_data)
363 host_list = [g.get('host', '') for g in graph]
364 service_list = [g.get('service', '') for g in graph if g.get('host') == host]
365 if host not in host_list or (host in host_list and service not in service_list):
366 graph.append(service_data)
367
368 return {'graph': graph}
369
370
Ales Komarek166cc672016-07-27 14:17:22 +0200371def node_update(name, classes=None, parameters=None, **connection_args):
372 '''
373 Update a node metadata information, classes and parameters.
374
375 CLI Examples:
376
377 .. code-block:: bash
378
379 salt '*' reclass.node_update name=nodename classes="[clas1, class2]"
380 '''
381 node = node_get(name=name)
382 if not node.has_key('Error'):
Ales Komarek71f94b02016-07-27 14:48:57 +0200383 node = node[name.split("/")[1]]
384 else:
385 return {'Error': 'Error in retrieving node'}
Ales Komareka4a9f572016-12-03 20:15:50 +0100386
387
Adam Tengler23d965f2017-05-16 19:14:51 +0000388def _get_node_classes(node_data, class_mapping_fragment):
389 classes = []
390
391 for value_tmpl_string in class_mapping_fragment.get('value_template', []):
392 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
393 rendered_value = value_tmpl.safe_substitute(node_data)
394 classes.append(rendered_value)
395
396 for value in class_mapping_fragment.get('value', []):
397 classes.append(value)
398
399 return classes
400
401
402def _get_params(node_data, class_mapping_fragment):
403 params = {}
404
405 for param_name, param in class_mapping_fragment.items():
406 value = param.get('value', None)
407 value_tmpl_string = param.get('value_template', None)
408 if value:
409 params.update({param_name: value})
410 elif value_tmpl_string:
411 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
412 rendered_value = value_tmpl.safe_substitute(node_data)
413 params.update({param_name: rendered_value})
414
415 return params
416
417
418def _validate_condition(node_data, expression_tmpl_string):
419 expression_tmpl = Template(expression_tmpl_string.replace('<<', '${').replace('>>', '}'))
420 expression = expression_tmpl.safe_substitute(node_data)
421
422 if expression and expression == 'all':
423 return True
424 elif expression:
425 val_a = expression.split('__')[0]
426 val_b = expression.split('__')[2]
427 condition = expression.split('__')[1]
428 if condition == 'startswith':
429 return val_a.startswith(val_b)
430 elif condition == 'equals':
431 return val_a == val_b
432
433 return False
434
435
436def node_classify(node_name, node_data={}, class_mapping={}, **kwargs):
437 '''
438 CLassify node by given class_mapping dictionary
439
440 :param node_name: node FQDN
441 :param node_data: dictionary of known informations about the node
442 :param class_mapping: dictionary of classes and parameters, with conditions
443
444 '''
445 # clean node_data
446 node_data = {k: v for (k, v) in node_data.items() if not k.startswith('__')}
447
448 classes = []
449 node_params = {}
450 cluster_params = {}
451 ret = {'node_create': '', 'cluster_param': {}}
452
453 for type_name, node_type in class_mapping.items():
454 valid = _validate_condition(node_data, node_type.get('expression', ''))
455 if valid:
456 gen_classes = _get_node_classes(node_data, node_type.get('node_class', {}))
457 classes = classes + gen_classes
458 gen_node_params = _get_params(node_data, node_type.get('node_param', {}))
459 node_params.update(gen_node_params)
460 gen_cluster_params = _get_params(node_data, node_type.get('cluster_param', {}))
461 cluster_params.update(gen_cluster_params)
462
463 if classes:
464 create_kwargs = {'name': node_name, 'path': '_generated', 'classes': classes, 'parameters': node_params}
465 ret['node_create'] = node_create(**create_kwargs)
466
467 for name, value in cluster_params.items():
468 ret['cluster_param'][name] = cluster_meta_set(name, value)
469
470 return ret
471
472
Adam Tengler2b362622017-06-01 14:23:45 +0000473def node_pillar(node_name, **kwargs):
474 '''
475 Returns pillar data for given minion from reclass inventory.
476
477 :param node_name: target minion ID
478
479 CLI Examples:
480
481 .. code-block:: bash
482
483 salt-call reclass.node_pillar minion_id
484
485 '''
486 defaults = find_and_read_configfile()
487 pillar = ext_pillar(node_name, {}, defaults['storage_type'], defaults['inventory_base_uri'])
488 output = {node_name: pillar}
489
490 return output
491
492
Ales Komareka4a9f572016-12-03 20:15:50 +0100493def inventory(**connection_args):
494 '''
495 Get all nodes in inventory and their associated services/roles classification.
496
497 CLI Examples:
498
499 .. code-block:: bash
500
501 salt '*' reclass.inventory
502 '''
503 defaults = find_and_read_configfile()
504 storage = get_storage(defaults['storage_type'], _get_nodes_dir(), _get_classes_dir())
505 reclass = Core(storage, None)
506 nodes = reclass.inventory()["nodes"]
507 output = {}
508
509 for node in nodes:
510 service_classification = []
511 role_classification = []
512 for service in nodes[node]['parameters']:
513 if service not in ['_param', 'private_keys', 'public_keys', 'known_hosts']:
514 service_classification.append(service)
515 for role in nodes[node]['parameters'][service]:
516 if role not in ['_support', '_orchestrate', 'common']:
517 role_classification.append('%s.%s' % (service, role))
518 output[node] = {
519 'roles': role_classification,
520 'services': service_classification,
521 }
522 return output
Adam Tengler8a1cf402017-05-16 10:59:35 +0000523
524
525def cluster_meta_list(file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000526 '''
527 List all cluster level overrides
528
529 :param file_name: name of the override file, defaults to: overrides.yml
530
531 CLI Examples:
532
533 .. code-block:: bash
534
535 salt-call reclass.cluster_meta_list
536
537 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000538 path = os.path.join(_get_cluster_dir(), cluster, file_name)
539 try:
540 with io.open(path, 'r') as file_handle:
541 meta_yaml = yaml.safe_load(file_handle.read())
542 meta = meta_yaml or {}
543 except Exception as e:
544 msg = "Unable to load cluster metadata YAML %s: %s" % (path, repr(e))
545 LOG.debug(msg)
546 meta = {'Error': msg}
547 return meta
548
549
550def cluster_meta_delete(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000551 '''
552 Delete cluster level override entry
553
554 :param name: name of the override entry (dictionary key)
555 :param file_name: name of the override file, defaults to: overrides.yml
556
557 CLI Examples:
558
559 .. code-block:: bash
560
561 salt-call reclass.cluster_meta_delete foo
562
563 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000564 ret = {}
565 path = os.path.join(_get_cluster_dir(), cluster, file_name)
566 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
567 if 'Error' not in meta:
568 metadata = meta.get('parameters', {}).get('_param', {})
569 if name not in metadata:
570 return ret
571 del metadata[name]
572 try:
573 with io.open(path, 'w') as file_handle:
574 file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
575 except Exception as e:
576 msg = "Unable to save cluster metadata YAML: %s" % repr(e)
577 LOG.error(msg)
578 return {'Error': msg}
579 ret = 'Cluster metadata entry {0} deleted'.format(name)
580 return ret
581
582
583def cluster_meta_set(name, value, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000584 '''
585 Create cluster level override entry
586
587 :param name: name of the override entry (dictionary key)
588 :param value: value of the override entry (dictionary value)
589 :param file_name: name of the override file, defaults to: overrides.yml
590
591 CLI Examples:
592
593 .. code-block:: bash
594
595 salt-call reclass.cluster_meta_set foo bar
596
597 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000598 path = os.path.join(_get_cluster_dir(), cluster, file_name)
599 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
600 if 'Error' not in meta:
601 if not meta:
602 meta = {'parameters': {'_param': {}}}
603 metadata = meta.get('parameters', {}).get('_param', {})
604 if name in metadata and metadata[name] == value:
605 return {name: 'Cluster metadata entry %s already exists and is in correct state' % name}
606 metadata.update({name: value})
607 try:
608 with io.open(path, 'w') as file_handle:
609 file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
610 except Exception as e:
611 msg = "Unable to save cluster metadata YAML %s: %s" % (path, repr(e))
612 LOG.error(msg)
613 return {'Error': msg}
614 return cluster_meta_get(name, path, **kwargs)
615 return meta
616
617
618def cluster_meta_get(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000619 '''
620 Get single cluster level override entry
621
622 :param name: name of the override entry (dictionary key)
623 :param file_name: name of the override file, defaults to: overrides.yml
624
625 CLI Examples:
626
627 .. code-block:: bash
628
629 salt-call reclass.cluster_meta_get foo
630
631 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000632 ret = {}
633 path = os.path.join(_get_cluster_dir(), cluster, file_name)
634 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
635 metadata = meta.get('parameters', {}).get('_param', {})
636 if 'Error' in meta:
637 ret['Error'] = meta['Error']
638 elif name in metadata:
639 ret[name] = metadata.get(name)
640
641 return ret
642