blob: e53aecb45f84c363788e98bc969dc8d703e6e004 [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
Adam Tengler1f7667b2017-06-06 16:45:51 +0000245def _get_grains(*args, **kwargs):
246 res = __salt__['saltutil.cmd'](tgt='*',
247 fun='grains.item',
248 arg=args,
249 **{'timeout': 10})
250 return res or {}
251
252
253def _guess_host_from_target(network_grains, host, domain=' '):
Adam Tengler2b362622017-06-01 14:23:45 +0000254 '''
255 Guess minion ID from given host and domain arguments. Host argument can contain
256 hostname, FQDN, IPv4 or IPv6 addresses.
257 '''
Adam Tengler1f7667b2017-06-06 16:45:51 +0000258 key = None
259 value = None
260
Adam Tengler2b362622017-06-01 14:23:45 +0000261 if _is_valid_ipv4_address(host):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000262 key = 'ipv4'
263 value = host
Adam Tengler2b362622017-06-01 14:23:45 +0000264 elif _is_valid_ipv6_address(host):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000265 key = 'ipv6'
266 value = host
Adam Tengler2b362622017-06-01 14:23:45 +0000267 elif host.endswith(domain):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000268 key = 'fqdn'
269 value = host
Adam Tengler2b362622017-06-01 14:23:45 +0000270 else:
Adam Tengler1f7667b2017-06-06 16:45:51 +0000271 key = 'fqdn'
272 value = '%s.%s' % (host, domain)
Adam Tengler2b362622017-06-01 14:23:45 +0000273
Adam Tengler1f7667b2017-06-06 16:45:51 +0000274 target = None
275 if network_grains and isinstance(network_grains, dict) and key and value:
276 for minion, grains in network_grains.items():
277 if grains.get('retcode', 1) == 0 and value in grains.get('ret', {}).get(key, ''):
278 target = minion
Adam Tengler2b362622017-06-01 14:23:45 +0000279
Adam Tengler1f7667b2017-06-06 16:45:51 +0000280 return target or host
Adam Tengler2b362622017-06-01 14:23:45 +0000281
282
283def _interpolate_graph_data(graph_data, **kwargs):
284 new_nodes = []
Adam Tengler1f7667b2017-06-06 16:45:51 +0000285 network_grains = _get_grains('ipv4', 'ipv6', 'fqdn')
Adam Tengler2b362622017-06-01 14:23:45 +0000286 for node in graph_data:
Adam Tengler69c7ba92017-06-01 15:59:01 +0000287 if not node.get('relations', []):
288 node['relations'] = []
Adam Tengler2b362622017-06-01 14:23:45 +0000289 for relation in node.get('relations', []):
Adam Tengler12a310d2017-06-05 19:11:29 +0000290 if not relation.get('status', None):
291 relation['status'] = 'unknown'
Adam Tengler2b362622017-06-01 14:23:45 +0000292 if relation.get('host_from_target', None):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000293 host = _guess_host_from_target(network_grains, relation.pop('host_from_target'))
Adam Tengler2b362622017-06-01 14:23:45 +0000294 relation['host'] = host
295 if relation.get('host_external', None):
Adam Tengler69c7ba92017-06-01 15:59:01 +0000296 parsed_host_external = [urlparse(item).netloc
297 for item
298 in relation.get('host_external', '').split(' ')
299 if urlparse(item).netloc]
300 service = parsed_host_external[0] if parsed_host_external else ''
Adam Tengler2b362622017-06-01 14:23:45 +0000301 host = relation.get('service', '')
302 relation['host'] = host
Adam Tengler69c7ba92017-06-01 15:59:01 +0000303 del relation['host_external']
Adam Tengler2b362622017-06-01 14:23:45 +0000304 relation['service'] = service
Adam Tengler69c7ba92017-06-01 15:59:01 +0000305 host_list = [n.get('host', '') for n in graph_data + new_nodes]
306 service_list = [n.get('service', '') for n in graph_data + new_nodes if host in n.get('host', '')]
307 if host not in host_list or (host in host_list and service not in service_list):
Adam Tengler2b362622017-06-01 14:23:45 +0000308 new_node = {
309 'host': host,
310 'service': service,
311 'type': relation.get('type', ''),
312 'relations': []
313 }
314 new_nodes.append(new_node)
315
316 graph_data = graph_data + new_nodes
317
318 return graph_data
319
320
321def _grain_graph_data(*args, **kwargs):
Adam Tengler1f7667b2017-06-06 16:45:51 +0000322 ret = _get_grains('salt:graph')
Adam Tengler2b362622017-06-01 14:23:45 +0000323 graph_data = []
324 for minion_ret in ret.values():
325 if minion_ret.get('retcode', 1) == 0:
326 graph_datum = minion_ret.get('ret', {}).get('salt:graph', [])
327 graph_data = graph_data + graph_datum
328
329 graph_nodes = _interpolate_graph_data(graph_data)
330 graph = {}
331
332 for node in graph_nodes:
333 if node.get('host') not in graph:
334 graph[node.get('host')] = {}
335 graph[node.pop('host')][node.pop('service')] = node
336
337 return {'graph': graph}
338
339
340def _pillar_graph_data(*args, **kwargs):
341 graph = {}
342 nodes = inventory()
343 for node, node_data in nodes.items():
344 for role in node_data.get('roles', []):
345 if node not in graph:
346 graph[node] = {}
347 graph[node][role] = {'relations': []}
348
349 return {'graph': graph}
350
351
352def graph_data(*args, **kwargs):
353 '''
354 Returns graph data for visualization app
355
356 CLI Examples:
357
358 .. code-block:: bash
359
Adam Tengler1f7667b2017-06-06 16:45:51 +0000360 salt-call reclass.graph_data
Adam Tengler2b362622017-06-01 14:23:45 +0000361
362 '''
363 pillar_data = _pillar_graph_data().get('graph')
364 grain_data = _grain_graph_data().get('graph')
365
366 for host, services in pillar_data.items():
367 for service, service_data in services.items():
368 grain_service = grain_data.get(host, {}).get(service, {})
369 service_data.update(grain_service)
370
371 graph = []
372 for host, services in pillar_data.items():
373 for service, service_data in services.items():
374 additional_data = {
375 'host': host,
Adam Tengler12a310d2017-06-05 19:11:29 +0000376 'service': service,
377 'status': 'unknown'
Adam Tengler2b362622017-06-01 14:23:45 +0000378 }
379 service_data.update(additional_data)
380 graph.append(service_data)
381
382 for host, services in grain_data.items():
383 for service, service_data in services.items():
384 additional_data = {
385 'host': host,
Adam Tengler12a310d2017-06-05 19:11:29 +0000386 'service': service,
387 'status': 'success'
Adam Tengler2b362622017-06-01 14:23:45 +0000388 }
389 service_data.update(additional_data)
390 host_list = [g.get('host', '') for g in graph]
391 service_list = [g.get('service', '') for g in graph if g.get('host') == host]
392 if host not in host_list or (host in host_list and service not in service_list):
393 graph.append(service_data)
394
395 return {'graph': graph}
396
397
Ales Komarek166cc672016-07-27 14:17:22 +0200398def node_update(name, classes=None, parameters=None, **connection_args):
399 '''
400 Update a node metadata information, classes and parameters.
401
402 CLI Examples:
403
404 .. code-block:: bash
405
406 salt '*' reclass.node_update name=nodename classes="[clas1, class2]"
407 '''
408 node = node_get(name=name)
409 if not node.has_key('Error'):
Ales Komarek71f94b02016-07-27 14:48:57 +0200410 node = node[name.split("/")[1]]
411 else:
412 return {'Error': 'Error in retrieving node'}
Ales Komareka4a9f572016-12-03 20:15:50 +0100413
414
Adam Tengler23d965f2017-05-16 19:14:51 +0000415def _get_node_classes(node_data, class_mapping_fragment):
416 classes = []
417
418 for value_tmpl_string in class_mapping_fragment.get('value_template', []):
419 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
420 rendered_value = value_tmpl.safe_substitute(node_data)
421 classes.append(rendered_value)
422
423 for value in class_mapping_fragment.get('value', []):
424 classes.append(value)
425
426 return classes
427
428
429def _get_params(node_data, class_mapping_fragment):
430 params = {}
431
432 for param_name, param in class_mapping_fragment.items():
433 value = param.get('value', None)
434 value_tmpl_string = param.get('value_template', None)
435 if value:
436 params.update({param_name: value})
437 elif value_tmpl_string:
438 value_tmpl = Template(value_tmpl_string.replace('<<', '${').replace('>>', '}'))
439 rendered_value = value_tmpl.safe_substitute(node_data)
440 params.update({param_name: rendered_value})
441
442 return params
443
444
445def _validate_condition(node_data, expression_tmpl_string):
446 expression_tmpl = Template(expression_tmpl_string.replace('<<', '${').replace('>>', '}'))
447 expression = expression_tmpl.safe_substitute(node_data)
448
449 if expression and expression == 'all':
450 return True
451 elif expression:
452 val_a = expression.split('__')[0]
453 val_b = expression.split('__')[2]
454 condition = expression.split('__')[1]
455 if condition == 'startswith':
456 return val_a.startswith(val_b)
457 elif condition == 'equals':
458 return val_a == val_b
459
460 return False
461
462
463def node_classify(node_name, node_data={}, class_mapping={}, **kwargs):
464 '''
465 CLassify node by given class_mapping dictionary
466
467 :param node_name: node FQDN
468 :param node_data: dictionary of known informations about the node
469 :param class_mapping: dictionary of classes and parameters, with conditions
470
471 '''
472 # clean node_data
473 node_data = {k: v for (k, v) in node_data.items() if not k.startswith('__')}
474
475 classes = []
476 node_params = {}
477 cluster_params = {}
478 ret = {'node_create': '', 'cluster_param': {}}
479
480 for type_name, node_type in class_mapping.items():
481 valid = _validate_condition(node_data, node_type.get('expression', ''))
482 if valid:
483 gen_classes = _get_node_classes(node_data, node_type.get('node_class', {}))
484 classes = classes + gen_classes
485 gen_node_params = _get_params(node_data, node_type.get('node_param', {}))
486 node_params.update(gen_node_params)
487 gen_cluster_params = _get_params(node_data, node_type.get('cluster_param', {}))
488 cluster_params.update(gen_cluster_params)
489
490 if classes:
491 create_kwargs = {'name': node_name, 'path': '_generated', 'classes': classes, 'parameters': node_params}
492 ret['node_create'] = node_create(**create_kwargs)
493
494 for name, value in cluster_params.items():
495 ret['cluster_param'][name] = cluster_meta_set(name, value)
496
497 return ret
498
499
Adam Tengler2b362622017-06-01 14:23:45 +0000500def node_pillar(node_name, **kwargs):
501 '''
502 Returns pillar data for given minion from reclass inventory.
503
504 :param node_name: target minion ID
505
506 CLI Examples:
507
508 .. code-block:: bash
509
510 salt-call reclass.node_pillar minion_id
511
512 '''
513 defaults = find_and_read_configfile()
514 pillar = ext_pillar(node_name, {}, defaults['storage_type'], defaults['inventory_base_uri'])
515 output = {node_name: pillar}
516
517 return output
518
519
Ales Komareka4a9f572016-12-03 20:15:50 +0100520def inventory(**connection_args):
521 '''
522 Get all nodes in inventory and their associated services/roles classification.
523
524 CLI Examples:
525
526 .. code-block:: bash
527
528 salt '*' reclass.inventory
529 '''
530 defaults = find_and_read_configfile()
531 storage = get_storage(defaults['storage_type'], _get_nodes_dir(), _get_classes_dir())
532 reclass = Core(storage, None)
533 nodes = reclass.inventory()["nodes"]
534 output = {}
535
536 for node in nodes:
537 service_classification = []
538 role_classification = []
539 for service in nodes[node]['parameters']:
540 if service not in ['_param', 'private_keys', 'public_keys', 'known_hosts']:
541 service_classification.append(service)
542 for role in nodes[node]['parameters'][service]:
543 if role not in ['_support', '_orchestrate', 'common']:
544 role_classification.append('%s.%s' % (service, role))
545 output[node] = {
546 'roles': role_classification,
547 'services': service_classification,
548 }
549 return output
Adam Tengler8a1cf402017-05-16 10:59:35 +0000550
551
552def cluster_meta_list(file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000553 '''
554 List all cluster level overrides
555
556 :param file_name: name of the override file, defaults to: overrides.yml
557
558 CLI Examples:
559
560 .. code-block:: bash
561
562 salt-call reclass.cluster_meta_list
563
564 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000565 path = os.path.join(_get_cluster_dir(), cluster, file_name)
566 try:
567 with io.open(path, 'r') as file_handle:
568 meta_yaml = yaml.safe_load(file_handle.read())
569 meta = meta_yaml or {}
570 except Exception as e:
571 msg = "Unable to load cluster metadata YAML %s: %s" % (path, repr(e))
572 LOG.debug(msg)
573 meta = {'Error': msg}
574 return meta
575
576
577def cluster_meta_delete(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000578 '''
579 Delete cluster level override entry
580
581 :param name: name of the override entry (dictionary key)
582 :param file_name: name of the override file, defaults to: overrides.yml
583
584 CLI Examples:
585
586 .. code-block:: bash
587
588 salt-call reclass.cluster_meta_delete foo
589
590 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000591 ret = {}
592 path = os.path.join(_get_cluster_dir(), cluster, file_name)
593 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
594 if 'Error' not in meta:
595 metadata = meta.get('parameters', {}).get('_param', {})
596 if name not in metadata:
597 return ret
598 del metadata[name]
599 try:
600 with io.open(path, 'w') as file_handle:
601 file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
602 except Exception as e:
603 msg = "Unable to save cluster metadata YAML: %s" % repr(e)
604 LOG.error(msg)
605 return {'Error': msg}
606 ret = 'Cluster metadata entry {0} deleted'.format(name)
607 return ret
608
609
610def cluster_meta_set(name, value, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000611 '''
612 Create cluster level override entry
613
614 :param name: name of the override entry (dictionary key)
615 :param value: value of the override entry (dictionary value)
616 :param file_name: name of the override file, defaults to: overrides.yml
617
618 CLI Examples:
619
620 .. code-block:: bash
621
622 salt-call reclass.cluster_meta_set foo bar
623
624 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000625 path = os.path.join(_get_cluster_dir(), cluster, file_name)
626 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
627 if 'Error' not in meta:
628 if not meta:
629 meta = {'parameters': {'_param': {}}}
630 metadata = meta.get('parameters', {}).get('_param', {})
631 if name in metadata and metadata[name] == value:
632 return {name: 'Cluster metadata entry %s already exists and is in correct state' % name}
633 metadata.update({name: value})
634 try:
635 with io.open(path, 'w') as file_handle:
636 file_handle.write(unicode(yaml.dump(meta, default_flow_style=False)))
637 except Exception as e:
638 msg = "Unable to save cluster metadata YAML %s: %s" % (path, repr(e))
639 LOG.error(msg)
640 return {'Error': msg}
641 return cluster_meta_get(name, path, **kwargs)
642 return meta
643
644
645def cluster_meta_get(name, file_name="overrides.yml", cluster="", **kwargs):
Adam Tengler2b362622017-06-01 14:23:45 +0000646 '''
647 Get single cluster level override entry
648
649 :param name: name of the override entry (dictionary key)
650 :param file_name: name of the override file, defaults to: overrides.yml
651
652 CLI Examples:
653
654 .. code-block:: bash
655
656 salt-call reclass.cluster_meta_get foo
657
658 '''
Adam Tengler8a1cf402017-05-16 10:59:35 +0000659 ret = {}
660 path = os.path.join(_get_cluster_dir(), cluster, file_name)
661 meta = __salt__['reclass.cluster_meta_list'](path, **kwargs)
662 metadata = meta.get('parameters', {}).get('_param', {})
663 if 'Error' in meta:
664 ret['Error'] = meta['Error']
665 elif name in metadata:
666 ret[name] = metadata.get(name)
667
668 return ret
669