blob: eac81c18317fb3b1ebbdf590572ea074076760a5 [file] [log] [blame]
Alex Savatieiev5118de02019-02-20 15:50:42 -06001import os
Alex9a4ad212020-10-01 18:04:25 -05002import json
3import pwd
Alexe9908f72020-05-19 16:04:53 -05004import sys
Alexc4f59622021-08-27 13:42:00 -05005import yaml
Alex Savatieiev5118de02019-02-20 15:50:42 -06006
Alex359e5752021-08-16 17:28:30 -05007from cfg_checker.common.const import ENV_TYPE_GLOB, ENV_TYPE_SALT
8from cfg_checker.common.const import ENV_TYPE_KUBE, ENV_TYPE_LINUX, ENV_LOCAL
9from cfg_checker.common.const import supported_envs
10
Alex3bc95f62020-03-05 17:00:04 -060011from cfg_checker.common.exception import ConfigException
Alex3bc95f62020-03-05 17:00:04 -060012from cfg_checker.common.log import logger_cli
Alex3ebc5632019-04-18 16:47:18 -050013
Alex9a4ad212020-10-01 18:04:25 -050014from cfg_checker.common.other import utils, shell
15from cfg_checker.common.ssh_utils import ssh_shell_p
16
17from cfg_checker.clients import get_kube_remote
Alex Savatieiev5118de02019-02-20 15:50:42 -060018
19pkg_dir = os.path.dirname(__file__)
20pkg_dir = os.path.join(pkg_dir, os.pardir, os.pardir)
21pkg_dir = os.path.normpath(pkg_dir)
22pkg_dir = os.path.abspath(pkg_dir)
23
24_default_work_folder = os.path.normpath(pkg_dir)
25
Alex9a4ad212020-10-01 18:04:25 -050026
27def _extract_salt_return(_raw):
28 if not isinstance(_raw, str):
29 _json = _raw
Alexc4f59622021-08-27 13:42:00 -050030 logger_cli.debug("... ambigious return detected")
Alex9a4ad212020-10-01 18:04:25 -050031 else:
32 try:
33 _json = json.loads(_raw)
34 except ValueError:
35 _json = _raw
36 logger_cli.debug(
Alexc4f59622021-08-27 13:42:00 -050037 "... return value is not a json: '{}'".format(_raw)
Alex9a4ad212020-10-01 18:04:25 -050038 )
39
40 return _json
41
Alex Savatieiev5118de02019-02-20 15:50:42 -060042
43class CheckerConfiguration(object):
Alexe9908f72020-05-19 16:04:53 -050044 @staticmethod
Alex Savatieiev9df93a92019-02-27 17:40:16 -060045 def load_nodes_list():
Alexe9908f72020-05-19 16:04:53 -050046 _file = os.environ.get('SALT_NODE_LIST_FILE', None)
47 if _file:
48 _v, _ = utils.get_nodes_list(
49 os.path.join(pkg_dir, _file),
50 env_sting=os.environ.get('CFG_ALL_NODES', None)
51 )
52 return _v
53 else:
54 return None
Alex Savatieiev9df93a92019-02-27 17:40:16 -060055
Alex9a4ad212020-10-01 18:04:25 -050056 def _detect(self, _type):
Alexc4f59622021-08-27 13:42:00 -050057 logger_cli.debug("... detecting '{}'".format(_type))
Alex9a4ad212020-10-01 18:04:25 -050058 if _type is None:
59 raise ConfigException("# Unexpected supported env type")
60 elif _type == ENV_TYPE_SALT:
61 # Detect salt env
62 _detect_cmd = ["curl", "-s"]
63 _detect_cmd.append(
64 "http://" + self.mcp_host + ':' + self.salt_port
65 )
66 # Try to call salt API on target host
67 _r = None
Alexc4f59622021-08-27 13:42:00 -050068 logger_cli.debug("... detecting env type '{}'".format(_type))
Alex9a4ad212020-10-01 18:04:25 -050069 if self.env_name == ENV_LOCAL:
70 _r = shell(" ".join(_detect_cmd))
71 else:
72 _r = ssh_shell_p(
73 " ".join(_detect_cmd),
74 self.ssh_host,
75 username=self.ssh_user,
76 keypath=self.ssh_key,
77 piped=False,
78 use_sudo=self.ssh_uses_sudo,
79 silent=True
80 )
81 # Parse return
82 _r = _extract_salt_return(_r)
83
84 if len(_r) < 1:
85 return False
86 elif _r["return"] == "Welcome":
87 return True
88 else:
89 return False
90 elif _type == ENV_TYPE_KUBE:
91 _kube = get_kube_remote(self)
Alex359e5752021-08-16 17:28:30 -050092 if not _kube.initialized:
93 logger_cli.debug(
Alexc4f59622021-08-27 13:42:00 -050094 "... failed to init kube using '{}'".format(
Alex359e5752021-08-16 17:28:30 -050095 _kube.kConfigPath
96 )
97 )
98 return False
99 else:
100 logger_cli.debug(
101 "... config loaded from '{}'".format(
102 _kube.kConfigPath
103 )
104 )
Alex9a4ad212020-10-01 18:04:25 -0500105 try:
106 _vApi = _kube.get_versions_api()
107 _v = _vApi.get_code()
108 if hasattr(_v, "platform") and \
109 hasattr(_v, "major") and \
110 hasattr(_v, "minor"):
Alex9a4ad212020-10-01 18:04:25 -0500111 logger_cli.info(
112 "# Kube server found: {}:{} on '{}'".format(
113 _v.major,
114 _v.minor,
Alexc4f59622021-08-27 13:42:00 -0500115 _kube.kConfigPath
Alex9a4ad212020-10-01 18:04:25 -0500116 )
117 )
118 return True
119 else:
120 return False
121 except Exception as e:
Alex359e5752021-08-16 17:28:30 -0500122 logger_cli.debug(
123 "... kube env error: '{}' ".format(
Alex9a4ad212020-10-01 18:04:25 -0500124 str(e)
125 )
126 )
127 return False
128 elif _type == ENV_TYPE_LINUX:
129 # Detect Linux env
130 from platform import system, release
131 _s = system()
132 _r = release()
Alexc4f59622021-08-27 13:42:00 -0500133 logger_cli.debug("... running on {} {}".format(_s, _r))
Alex9a4ad212020-10-01 18:04:25 -0500134 if _s in ['Linux', 'Darwin']:
135 return True
136 else:
137 return False
138 else:
139 raise ConfigException(
140 "# Env type of '{}' is not supported".format(
141 _type
142 )
143 )
144
145 def _detect_types(self):
146 """Try to detect env type based on the name
147 """
148 self.detected_envs = []
149 logger_cli.info('# Detecting env types')
150 for _env in supported_envs:
151 if self._detect(_env):
152 logger_cli.info("# '{}' found".format(_env))
153 self.detected_envs.append(_env)
154 else:
155 logger_cli.info("# '{}' not found".format(_env))
156
157 return
158
159 def _init_mcp_values(self):
Alex Savatieiev5118de02019-02-20 15:50:42 -0600160 """Load values from environment variables or put default ones
161 """
Alex9a4ad212020-10-01 18:04:25 -0500162 # filter vars and preload if needed
163 self.salt_vars = []
164 self.kube_vars = []
165 for _key, _value in self.vars:
Alexc4f59622021-08-27 13:42:00 -0500166 if _key in os.environ:
167 logger_cli.info(
168 "-> Using env var '{}={}'".format(_key, os.environ[_key])
169 )
Alex9a4ad212020-10-01 18:04:25 -0500170 if _key.startswith(ENV_TYPE_GLOB):
171 os.environ[_key] = _value
172 elif _key.startswith(ENV_TYPE_SALT):
173 self.salt_vars.append([_key, _value])
174 elif _key.startswith(ENV_TYPE_KUBE):
175 self.kube_vars.append([_key, _value])
176 else:
177 logger_cli.warn(
178 "Unsupported config variable: '{}={}'".format(
179 _key,
180 _value
181 )
182 )
Alex Savatieiev5118de02019-02-20 15:50:42 -0600183 self.name = "CheckerConfig"
184 self.working_folder = os.environ.get(
185 'CFG_TESTS_WORK_DIR',
186 _default_work_folder
187 )
188 self.date_format = "%Y-%m-%d %H:%M:%S.%f%z"
189 self.default_tz = "UTC"
190
Alex41485522019-04-12 17:26:18 -0500191 self.pkg_versions_map = 'versions_map.csv'
192
Alex359e5752021-08-16 17:28:30 -0500193 # self.ssh_uses_sudo = False
Alex9a4ad212020-10-01 18:04:25 -0500194 self.ssh_key = os.environ.get('MCP_SSH_KEY', None)
195 self.ssh_user = os.environ.get('MCP_SSH_USER', None)
196 self.ssh_host = os.environ.get('MCP_SSH_HOST', None)
Alex1f90e7b2021-09-03 15:31:28 -0500197 self.ssh_connect_timeout = int(
198 os.environ.get('MCP_SSH_TIMEOUT', "15")
199 )
Alex Savatieiev63576832019-02-27 15:46:26 -0600200
Alex9a4ad212020-10-01 18:04:25 -0500201 self.mcp_host = os.environ.get('MCP_ENV_HOST', None)
202 self.salt_port = os.environ.get('MCP_SALT_PORT', '6969')
203 self.threads = int(os.environ.get('MCP_THREADS', "5"))
Alex1f90e7b2021-09-03 15:31:28 -0500204 self.script_execution_timeout = int(
205 os.environ.get('MCP_SCRIPT_RUN_TIMEOUT', "300")
206 )
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600207
Alex Savatieiev5118de02019-02-20 15:50:42 -0600208 self.skip_nodes = utils.node_string_to_list(os.environ.get(
209 'CFG_SKIP_NODES',
210 None
211 ))
Alex9a4ad212020-10-01 18:04:25 -0500212 # prebuild user data and folder path
213 self.pw_user = pwd.getpwuid(os.getuid())
214 if self.env_name == "local":
215 pass
216 else:
217 if not self.ssh_key and not self.force_no_key:
218 raise ConfigException(
219 "Please, supply a key for the cluster's master node. "
220 "Use MCP_SSH_KEY, see 'etc/example.env'"
221 )
222
223 def _init_env_values(self):
224 if ENV_TYPE_SALT in self.detected_envs:
225 for _key, _value in self.salt_vars:
Alexc4f59622021-08-27 13:42:00 -0500226 if _key not in os.environ:
227 os.environ[_key] = _value
Alex9a4ad212020-10-01 18:04:25 -0500228
229 self.salt_user = os.environ.get('SALT_USER', 'salt')
230 self.salt_timeout = os.environ.get('SALT_TIMEOUT', 30)
Alexc4f59622021-08-27 13:42:00 -0500231 self.salt_file_root = os.environ.get(
232 'SALT_FILE_ROOT',
233 "/usr/share/salt-formulas/env/"
234 )
Alex9a4ad212020-10-01 18:04:25 -0500235 self.salt_scripts_folder = os.environ.get(
236 'SALT_SCRIPTS_FOLDER',
237 'cfg_checker_scripts'
238 )
239 elif ENV_TYPE_KUBE in self.detected_envs:
240 for _key, _value in self.kube_vars:
Alexc4f59622021-08-27 13:42:00 -0500241 if _key not in os.environ:
242 os.environ[_key] = _value
Alex9a4ad212020-10-01 18:04:25 -0500243
Alexc4f59622021-08-27 13:42:00 -0500244 self.kube_config_root = os.environ.get('KUBE_CONFIG_ROOT', "/root")
Alex9a4ad212020-10-01 18:04:25 -0500245 self.kube_scripts_folder = os.environ.get(
246 'KUBE_SCRIPTS_FOLDER',
Alexc4f59622021-08-27 13:42:00 -0500247 "cfg-checker-scripts"
Alex9a4ad212020-10-01 18:04:25 -0500248 )
249 self.kube_node_user = os.environ.get(
250 'KUBE_NODE_USER',
251 'ubuntu'
252 )
253 self.kube_node_keypath = os.environ.get(
254 'KUBE_NODE_KEYPATH',
255 None
256 )
257 # Warn user only if Kube env is detected locally
Alexccb72e02021-01-20 16:38:03 -0600258 if self.env_name == ENV_LOCAL:
Alex9a4ad212020-10-01 18:04:25 -0500259 if not os.path.exists(self.kube_config_path):
260 logger_cli.warn(
261 "Kube config path not found on local env: '{}'".format(
262 self.kube_config_path
263 )
264 )
265 # On local envs, KUBE_NODE_KEYPATH is mandatory and is
266 # provided to give cfg-checker access to kube nodes
267 if not self.kube_node_keypath and not self.force_no_key:
268 raise ConfigException(
269 "Please, supply a key for the cluster nodes. "
270 "Use KUBE_NODE_KEYPATH, see 'etc/example.env'. "
271 "Consider checking KUBE_NODE_USER as well"
272 )
Alex36308942020-11-09 17:13:38 -0600273 self.kube_node_homepath = os.path.join(
274 '/home',
275 self.kube_node_user
276 )
Alex9a4ad212020-10-01 18:04:25 -0500277 else:
Alex205546c2020-12-30 19:22:30 -0600278 # Init key for nodes
279 # KUBE_NODE_KEYPATH is provided in case of node keys would be
280 # different to master node key, which is supplied
Alex9a4ad212020-10-01 18:04:25 -0500281 # using MCP_SSH_KEY (mandatory) and, for the most cases,
282 # should be the same for remote envs
283 if not self.kube_node_keypath and not self.force_no_key:
284 logger_cli.debug(
285 "... using MCP_SSH_KEY as node keys. "
286 "Supply KUBE_NODE_KEYPATH to update."
287 )
288 self.kube_node_keypath = self.ssh_key
Alex36308942020-11-09 17:13:38 -0600289 self.kube_node_homepath = self.homepath
Alex Savatieiev5118de02019-02-20 15:50:42 -0600290
Alexccb72e02021-01-20 16:38:03 -0600291 def _init_env(self, config_path, env_name=None):
Alex Savatieiev63576832019-02-27 15:46:26 -0600292 """Inits the environment vars from the env file
293 Uses simple validation for the values and names
Alex Savatieiev5118de02019-02-20 15:50:42 -0600294
295 Keyword Arguments:
296 env_name {str} -- environment name to search configuration
297 files in etc/<env_name>.env (default: {None})
Alex9a4ad212020-10-01 18:04:25 -0500298 env_type {str} -- environment type to use: salt/kube
Alex Savatieiev5118de02019-02-20 15:50:42 -0600299
300 Raises:
301 ConfigException -- on IO error when loading env file
302 ConfigException -- on env file failed validation
303 """
Alexc4f59622021-08-27 13:42:00 -0500304 # detect kubeconfig placement
305 _env_kubeconf_path = os.environ.get('KUBECONFIG', None)
306 if not os.path.exists(self.kube_config_path):
307 logger_cli.debug(
308 "... kubeconfig not detected at '{}'".format(
309 self.kube_config_path
310 )
311 )
312 # not exists, get KUBECONFIG var
313 if _env_kubeconf_path:
314 # get the env var path
315 self.kube_config_path = _env_kubeconf_path
316 logger_cli.debug(
317 "... KUBECONFIG var points to '{}'".format(
318 self.kube_config_path
319 )
320 )
321 self.kube_config_detected = True
322 else:
323 logger_cli.debug("... KUBECONFIG env var not found")
324 self.kube_config_path = None
325 self.kube_config_detected = False
326 else:
327 logger_cli.debug(
328 "... kubeconfig detected at '{}'".format(
329 self.kube_config_path
330 )
331 )
332 self.kube_config_detected = True
333
334 # try to load values from KUBECONF
335 _kube_conf = None
336 if self.kube_config_path:
337 with open(self.kube_config_path) as kF:
338 _kube_conf = yaml.load(kF, Loader=yaml.SafeLoader)
339 # extract host ip
340 try:
341 _server = _kube_conf["clusters"][0]["cluster"]["server"]
342 except KeyError as e:
343 logger_cli.debug(
344 "... failed to extract server ip: "
345 "no '{}' key in 'clusters/[0]/cluster/server".format(e)
346 )
347 except IndexError:
348 logger_cli.debug(
349 "... failed to extract server ip: empty cluster list"
350 )
351 _ip = _server.split(':')
352 self.remote_ip = _ip[1].replace('/', '')
353 logger_cli.debug("... detected ip: '{}'".format(self.remote_ip))
354
Alex Savatieiev5118de02019-02-20 15:50:42 -0600355 # load env file as init os.environment with its values
Alexccb72e02021-01-20 16:38:03 -0600356 if os.path.isfile(config_path):
357 with open(config_path) as _f:
Alex Savatieiev5118de02019-02-20 15:50:42 -0600358 _list = _f.read().splitlines()
Alex3ebc5632019-04-18 16:47:18 -0500359 logger_cli.info(
360 "# Loading env vars from '{}'".format(
Alexccb72e02021-01-20 16:38:03 -0600361 config_path
Alex3ebc5632019-04-18 16:47:18 -0500362 )
363 )
Alex Savatieiev5118de02019-02-20 15:50:42 -0600364 else:
365 raise ConfigException(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600366 "# Failed to load enviroment vars from '{}'".format(
Alexccb72e02021-01-20 16:38:03 -0600367 config_path
Alex Savatieiev5118de02019-02-20 15:50:42 -0600368 )
369 )
Alex9a4ad212020-10-01 18:04:25 -0500370 self.vars = []
Alex Savatieiev5118de02019-02-20 15:50:42 -0600371 for index in range(len(_list)):
372 _line = _list[index]
373 # skip comments
374 if _line.strip().startswith('#'):
375 continue
376 # validate
377 _errors = []
Alex9a4ad212020-10-01 18:04:25 -0500378 if len(_line) < 1:
379 _errors.append("Line {}: empty".format(index))
380 elif _line.find('=') < 0 or _line.count('=') > 1:
Alex Savatieiev5118de02019-02-20 15:50:42 -0600381 _errors.append("Line {}: {}".format(index, _line))
382 else:
383 # save values
384 _t = _line.split('=')
Alex9a4ad212020-10-01 18:04:25 -0500385 self.vars.append([_t[0], _t[1]])
Alex Savatieiev5118de02019-02-20 15:50:42 -0600386 # if there was errors, report them
387 if _errors:
388 raise ConfigException(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600389 "# Environment file failed validation in lines: {}".format(
Alex Savatieiev5118de02019-02-20 15:50:42 -0600390 "\n".join(_errors)
391 )
392 )
393 else:
Alex3ebc5632019-04-18 16:47:18 -0500394 logger_cli.debug(
Alexc4f59622021-08-27 13:42:00 -0500395 "... loaded total of '{}' vars".format(
Alex3ebc5632019-04-18 16:47:18 -0500396 len(_list)
397 )
398 )
Alexccb72e02021-01-20 16:38:03 -0600399 self.env_name = env_name
Alex Savatieiev5118de02019-02-20 15:50:42 -0600400
Alex9a4ad212020-10-01 18:04:25 -0500401 def __init__(self, args):
Alex Savatieiev5118de02019-02-20 15:50:42 -0600402 """Base configuration class. Only values that are common for all scripts
403 """
Alex9a4ad212020-10-01 18:04:25 -0500404 self.ssh_uses_sudo = args.sudo
Alexeffa0682021-06-04 12:18:33 -0500405 self.ssh_direct = args.ssh_direct
Alexccb72e02021-01-20 16:38:03 -0600406 self.kube_config_path = args.kube_config
Alex9a4ad212020-10-01 18:04:25 -0500407 self.debug = args.debug
Alex33747812021-04-07 10:11:39 -0500408 self.insecure = args.insecure
Alex9a4ad212020-10-01 18:04:25 -0500409 self.force_no_key = args.force_no_key
Alexe9908f72020-05-19 16:04:53 -0500410 # Make sure we running on Python 3
411 if sys.version_info[0] < 3 and sys.version_info[1] < 5:
412 logger_cli.error("# ERROR: Python 3.5+ is required")
413 sys.exit(1)
414 else:
415 logger_cli.debug("### Python version is {}.{}".format(
416 sys.version_info[0],
417 sys.version_info[1]
418 ))
419
Alexccb72e02021-01-20 16:38:03 -0600420 # if env name is default, check var too
421 if args.env_name == ENV_LOCAL:
422 _env = os.getenv('MCP_ENV', None)
423 _env = _env if _env else args.env_name
Alex359e5752021-08-16 17:28:30 -0500424 _env_config_path = os.path.join(pkg_dir, 'etc', _env + '.env')
Alexccb72e02021-01-20 16:38:03 -0600425 else:
426 _env = args.env_name
Alex359e5752021-08-16 17:28:30 -0500427 _env_config_path = args.env_config
Alex9a4ad212020-10-01 18:04:25 -0500428
429 # Init environment variables from file, validate
Alex359e5752021-08-16 17:28:30 -0500430 self._init_env(_env_config_path, env_name=_env)
Alex9a4ad212020-10-01 18:04:25 -0500431 # Load Common vars for any type of the env
432 self._init_mcp_values()
433 # Detect env types present
434 self._detect_types()
435 # handle forced env type var
436 _forced_type = os.getenv('MCP_TYPE_FORCE', None)
437 if _forced_type in supported_envs:
438 self.detected_envs.append(_forced_type)
439 elif _forced_type is not None:
440 logger_cli.warn(
441 "Unsupported forced type of '{}'".format(
442 _forced_type
443 )
444 )
445 # Check if any of the envs detected
446 if len(self.detected_envs) < 1:
447 if _env is None:
448 raise ConfigException("No environment types detected locally")
449 else:
450 raise ConfigException(
451 "No environment types detected at '{}'".format(
452 self.mcp_host
453 )
454 )
Alex9a4ad212020-10-01 18:04:25 -0500455 # initialize path to folders
Alexccb72e02021-01-20 16:38:03 -0600456 if self.env_name == ENV_LOCAL:
Alex9a4ad212020-10-01 18:04:25 -0500457 # names and folders
458 self.user = self.pw_user.pw_name
459 self.homepath = self.pw_user.pw_dir
Alex9a4ad212020-10-01 18:04:25 -0500460 else:
461 # names and folders in case of remote env
462 self.user = self.ssh_user
463 self.homepath = os.path.join('/home', self.ssh_user)
Alexccb72e02021-01-20 16:38:03 -0600464
465 # Init vars that is specific to detected envs only
466 self._init_env_values()