blob: 4c3ef04eef2f3aacda0d4c6aafb0802d22cccc82 [file] [log] [blame]
Alex0989ecf2022-03-29 13:43:21 -05001# Author: Alex Savatieiev (osavatieiev@mirantis.com; a.savex@gmail.com)
2# Copyright 2019-2022 Mirantis, Inc.
Alex9a4ad212020-10-01 18:04:25 -05003import functools
savex4448e132018-04-25 15:51:14 +02004import os
5import re
Alex Savatieiev63576832019-02-27 15:46:26 -06006import subprocess
savex4448e132018-04-25 15:51:14 +02007
Alex9a4ad212020-10-01 18:04:25 -05008from cfg_checker.common import logger_cli
9from cfg_checker.common.const import all_salt_roles_map, uknown_code, \
10 truth
Alex Savatieiev5118de02019-02-20 15:50:42 -060011from cfg_checker.common.exception import ConfigException
savex4448e132018-04-25 15:51:14 +020012
Alex Savatieiev5118de02019-02-20 15:50:42 -060013pkg_dir = os.path.dirname(__file__)
14pkg_dir = os.path.join(pkg_dir, os.pardir, os.pardir)
15pkg_dir = os.path.normpath(pkg_dir)
16pkg_dir = os.path.abspath(pkg_dir)
savex4448e132018-04-25 15:51:14 +020017
18
Alex9a4ad212020-10-01 18:04:25 -050019# 'Dirty' and simple way to execute piped cmds
Alexb78191f2021-11-02 16:35:46 -050020def piped_shell(command, code=False):
Alexc4f59622021-08-27 13:42:00 -050021 logger_cli.debug("... cmd:'{}'".format(command))
Alex9a4ad212020-10-01 18:04:25 -050022 _code, _out = subprocess.getstatusoutput(command)
23 if _code:
24 logger_cli.error("Non-zero return code: {}, '{}'".format(_code, _out))
Alexb78191f2021-11-02 16:35:46 -050025 if code:
26 return _code, _out
27 else:
28 return _out
Alex9a4ad212020-10-01 18:04:25 -050029
30
31# 'Proper way to execute shell
Alex Savatieiev63576832019-02-27 15:46:26 -060032def shell(command):
Alexc4f59622021-08-27 13:42:00 -050033 logger_cli.debug("... cmd:'{}'".format(command))
Alex Savatieiev63576832019-02-27 15:46:26 -060034 _ps = subprocess.Popen(
35 command.split(),
Alex9a4ad212020-10-01 18:04:25 -050036 stdout=subprocess.PIPE,
37 universal_newlines=False
Alex Savatieiev63576832019-02-27 15:46:26 -060038 ).communicate()[0].decode()
39
40 return _ps
41
42
Alex26b8a8c2019-10-09 17:09:07 -050043def merge_dict(source, destination):
44 """
45 Dict merger, thanks to vincent
46 http://stackoverflow.com/questions/20656135/python-deep-merge-dictionary-data
47
48 >>> a = { 'first' : { 'all_rows' : { 'pass' : 'dog', 'number' : '1' } } }
49 >>> b = { 'first' : { 'all_rows' : { 'fail' : 'cat', 'number' : '5' } } }
50 >>> merge(b, a) == {
51 'first': {
52 'all_rows': {
53 'pass': 'dog',
54 'fail': 'cat',
55 'number': '5'
56 }
57 }
58 }
59 True
60 """
61 for key, value in source.items():
62 if isinstance(value, dict):
63 # get node or create one
64 node = destination.setdefault(key, {})
65 merge_dict(value, node)
66 else:
67 destination[key] = value
68
69 return destination
70
71
savex4448e132018-04-25 15:51:14 +020072class Utils(object):
73 @staticmethod
74 def validate_name(fqdn, message=False):
75 """
76 Function that tries to validate node name.
77 Checks if code contains letters, has '.' in it,
78 roles map contains code's role
79
80 :param fqdn: node FQDN name to supply for the check
81 :param message: True if validate should return error check message
82 :return: False if checks failed, True if all checks passed
83 """
Alex Savatieievf808cd22019-03-01 13:17:59 -060084 _message = "# Validation passed"
savex4448e132018-04-25 15:51:14 +020085
86 def _result():
87 return (True, _message) if message else True
88
89 # node role code checks
Alexd0391d42019-05-21 18:48:55 -050090 _code = re.findall(r"[a-zA-Z]+", fqdn.split('.')[0])
savex4448e132018-04-25 15:51:14 +020091 if len(_code) > 0:
Alex9a4ad212020-10-01 18:04:25 -050092 if _code[0] in all_salt_roles_map:
savex4448e132018-04-25 15:51:14 +020093 return _result()
94 else:
95 # log warning here
Alex Savatieievf808cd22019-03-01 13:17:59 -060096 _message = "# Node code is unknown, '{}'. " \
97 "# Please, update map".format(_code)
savex4448e132018-04-25 15:51:14 +020098 else:
99 # log warning here
Alex Savatieievf808cd22019-03-01 13:17:59 -0600100 _message = "# Node name is invalid, '{}'".format(fqdn)
savex4448e132018-04-25 15:51:14 +0200101
102 # put other checks here
103
104 # output result
105 return _result()
106
107 @staticmethod
108 def node_string_to_list(node_string):
109 # supplied var should contain node list
110 # if there is no ',' -> use space as a delimiter
111 if node_string is not None:
112 if node_string.find(',') < 0:
113 return node_string.split(' ')
114 else:
115 return node_string.split(',')
116 else:
117 return []
118
119 def get_node_code(self, fqdn):
120 # validate
121 _isvalid, _message = self.validate_name(fqdn, message=True)
Alex2e213b22019-12-05 10:40:29 -0600122 _code = re.findall(
123 r"[a-zA-Z]+?(?=(?:[0-9]+$)|(?:\-+?)|(?:\_+?)|$)",
124 fqdn.split('.')[0]
125 )
savex4448e132018-04-25 15:51:14 +0200126 # check if it is valid and raise if not
127 if _isvalid:
Alexe0c5b9e2019-04-23 18:51:23 -0500128 # try to match it with ones in map
129 _c = _code[0]
Alex9a4ad212020-10-01 18:04:25 -0500130 match = any([r in _c for r in all_salt_roles_map.keys()])
Alexe0c5b9e2019-04-23 18:51:23 -0500131 if match:
132 # no match, try to find it
133 match = False
Alex9a4ad212020-10-01 18:04:25 -0500134 for r in all_salt_roles_map.keys():
Alexe0c5b9e2019-04-23 18:51:23 -0500135 _idx = _c.find(r)
136 if _idx > -1:
137 _c = _c[_idx:]
138 match = True
139 break
140 if match:
141 return _c
142 else:
143 return uknown_code
144 else:
145 return uknown_code
savex4448e132018-04-25 15:51:14 +0200146 else:
147 raise ConfigException(_message)
148
Alexe9908f72020-05-19 16:04:53 -0500149 def get_nodes_list(self, nodes_list, env_sting=None):
savex4448e132018-04-25 15:51:14 +0200150 _list = []
Alexe9908f72020-05-19 16:04:53 -0500151 if env_sting is None:
savex4448e132018-04-25 15:51:14 +0200152 # nothing supplied, use the one in repo
153 try:
Alex Savatieievd48994d2018-12-13 12:13:00 +0100154 if not nodes_list:
155 return []
Alexe9908f72020-05-19 16:04:53 -0500156 with open(nodes_list) as _f:
savex4448e132018-04-25 15:51:14 +0200157 _list.extend(_f.read().splitlines())
158 except IOError as e:
Alex Savatieievf808cd22019-03-01 13:17:59 -0600159 raise ConfigException("# Error while loading file, '{}': "
savex4448e132018-04-25 15:51:14 +0200160 "{}".format(e.filename, e.strerror))
161 else:
Alexe9908f72020-05-19 16:04:53 -0500162 _list.extend(self.node_string_to_list(env_sting))
savex4448e132018-04-25 15:51:14 +0200163
164 # validate names
165 _invalid = []
166 _valid = []
167 for idx in range(len(_list)):
168 _name = _list[idx]
169 if not self.validate_name(_name):
170 _invalid.append(_name)
171 else:
172 _valid.append(_name)
173
Alexe9908f72020-05-19 16:04:53 -0500174 return _valid, _invalid
savex4448e132018-04-25 15:51:14 +0200175
Alex9a4ad212020-10-01 18:04:25 -0500176 @staticmethod
177 def to_bool(value):
178 if value.lower() in truth:
179 return True
180 else:
181 return False
182
183 # helper functions to get nested attrs
184 # https://stackoverflow.com/questions/31174295/getattr-and-setattr-on-nested-subobjects-chained-properties
185 # using wonder's beautiful simplification:
186 # https://stackoverflow.com/questions/31174295/getattr-and-setattr-on-nested-objects/31174427?noredirect=1#comment86638618_31174427
187 def rsetattr(self, obj, attr, val):
188 pre, _, post = attr.rpartition('.')
189 return setattr(self.rgetattr(obj, pre) if pre else obj, post, val)
190
191 def rgetattr(self, obj, attr, *args):
192 def _getattr(obj, attr):
193 return getattr(obj, attr, *args)
194 return functools.reduce(_getattr, [obj] + attr.split('.'))
195
savex4448e132018-04-25 15:51:14 +0200196
197utils = Utils()