Updates on error logging and handling
- iterative error log storage
- config like value storage
- updates logging format for improved readablility
Change-Id: I171a1b44452c1225340a7d7b1f7593ab9b8ce7c2
Related-PROD: PROD-28199
diff --git a/.gitignore b/.gitignore
index a951307..54591df 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@
.DS_Store
# project specific
+.cfgerrors
.vscode/*
etc/*.list
*.env
diff --git a/cfg_checker/common/config_file.py b/cfg_checker/common/config_file.py
new file mode 100644
index 0000000..87f4759
--- /dev/null
+++ b/cfg_checker/common/config_file.py
@@ -0,0 +1,72 @@
+import configparser
+import os
+
+from cfg_checker.common import logger_cli
+
+
+class ConfigFile(object):
+ _truth = ['true', '1', 't', 'y', 'yes', 'yeah', 'yup',
+ 'certainly', 'uh-huh']
+ _config = None
+ _section_name = None
+ _config_filepath = None
+
+ def __init__(self, section_name, filepath=None):
+ self._section_name = section_name
+ self._config = configparser.ConfigParser()
+ if filepath is not None:
+ self._config_filepath = self._ensure_abs_path(filepath)
+ self._config.read(self._config_filepath)
+ else:
+ logger_cli.debug("... previous iteration conf not found")
+
+ def force_reload_config(self, path):
+ _path = self._ensure_abs_path(path)
+ self._config.read(_path)
+
+ def save_config(self, filepath=None):
+ if filepath:
+ self._config_filepath = filepath
+ with open(self._config_filepath, "w") as configfile:
+ self._config.write(configfile)
+
+ @staticmethod
+ def _ensure_abs_path(path):
+ if path.startswith('~'):
+ path = os.path.expanduser(path)
+ else:
+ # keep it safe, create var :)
+ path = path
+
+ # make sure it is absolute
+ if not os.path.isabs(path):
+ return os.path.abspath(path)
+ else:
+ return path
+
+ def _ensure_boolean(self, _value):
+ if _value.lower() in self._truth:
+ return True
+ else:
+ return False
+
+ def get_value(self, key, value_type=None):
+ if not value_type:
+ # return str by default
+ return self._config.get(self._section_name, key)
+ elif value_type == int:
+ return self._config.getint(self._section_name, key)
+ elif value_type == bool:
+ return self._config.getboolean(self._section_name, key)
+
+ def set_value(self, key, value):
+ _v = None
+ if not isinstance(value, str):
+ _v = str(value)
+ else:
+ _v = value
+
+ if self._section_name not in self._config.sections():
+ self._config.add_section(self._section_name)
+
+ self._config[self._section_name][key] = _v
diff --git a/cfg_checker/common/file_utils.py b/cfg_checker/common/file_utils.py
new file mode 100644
index 0000000..d508121
--- /dev/null
+++ b/cfg_checker/common/file_utils.py
@@ -0,0 +1,84 @@
+import grp
+import os
+import pwd
+import time
+
+from cfg_checker.common import config
+
+_default_time_format = config.date_format
+
+
+def remove_file(filename):
+ os.remove(filename)
+ # open('filename', 'w').close()
+
+
+def write_str_to_file(filename, _str):
+ with open(filename, 'w') as fo:
+ fo.write(_str)
+
+
+def append_str_to_file(filename, _str):
+ with open(filename, 'a') as fa:
+ fa.write(_str)
+
+
+def write_lines_to_file(filename, source_list):
+ with open(filename, 'w') as fw:
+ fw.write("\n".join(source_list) + "\n")
+
+
+def append_lines_to_file(filename, source_list):
+ _buf = "\n".join(source_list)
+ with open(filename, 'a') as fw:
+ fw.write(_buf + "\n")
+
+
+def append_line_to_file(filename, _str):
+ with open(filename, 'a') as fa:
+ fa.write(_str+'\n')
+
+
+def read_file(filename):
+ _buf = None
+ with open(filename, 'rb') as fr:
+ _buf = fr.read()
+ return _buf
+
+
+def read_file_as_lines(filename):
+ _list = []
+ with open(filename, 'r') as fr:
+ for line in fr:
+ _list.append(line)
+ return _list
+
+
+def get_file_info_fd(fd, time_format=_default_time_format):
+
+ def format_time(unixtime):
+ return time.strftime(
+ time_format,
+ time.gmtime(unixtime)
+ )
+
+ (mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = \
+ os.fstat(fd.fileno())
+
+ _dict = {
+ 'fd': fd.fileno(),
+ 'mode': oct(mode & 0777),
+ 'device': hex(dev),
+ 'inode': ino,
+ 'hard_links': nlink,
+ 'owner_id': uid,
+ 'owner_name': pwd.getpwuid(uid).pw_name,
+ 'owner_group_name': grp.getgrgid(gid).gr_name,
+ 'owner_group_id': gid,
+ 'size': size,
+ 'access_time': format_time(atime),
+ 'modification_time': format_time(mtime),
+ 'creation_time': format_time(ctime)
+ }
+
+ return _dict
diff --git a/cfg_checker/common/salt_utils.py b/cfg_checker/common/salt_utils.py
index 8b1b47f..2927e28 100644
--- a/cfg_checker/common/salt_utils.py
+++ b/cfg_checker/common/salt_utils.py
@@ -45,7 +45,7 @@
_ssh_cmd.append(_salt_cmd)
_ssh_cmd = " ".join(_ssh_cmd)
- logger_cli.debug("...calling salt: '{}'".format(_ssh_cmd))
+ logger_cli.debug("... calling salt: '{}'".format(_ssh_cmd))
_result = shell(_ssh_cmd)
if len(_result) < 1:
raise InvalidReturnException("# Empty value returned for '{}".format(
diff --git a/cfg_checker/helpers/errors.py b/cfg_checker/helpers/errors.py
index b124315..ca8a8da 100644
--- a/cfg_checker/helpers/errors.py
+++ b/cfg_checker/helpers/errors.py
@@ -1,8 +1,21 @@
-from cfg_checker.common import logger
+import os
+
+from cfg_checker.common import file_utils as fu
+from cfg_checker.common import logger, logger_cli
+from cfg_checker.common.config_file import ConfigFile
from cfg_checker.common.exception import ErrorMappingException
+from cfg_checker.common.settings import pkg_dir
class ErrorIndex(object):
+ # logs folder filenames
+ _error_logs_folder_name = ".cfgerrors"
+ _conf_filename = "conf"
+ # config file object
+ conf = None
+ # iteration counter
+ _iteration = 0
+ # local vars for codes
_area_code = ""
_delimiter = ""
_index = 0
@@ -16,6 +29,62 @@
self._delimiter = delimiter
self._index += 1
+ # init the error log storage folder
+ _folder = os.path.join(pkg_dir, self._error_logs_folder_name)
+ self._conf_filename = os.path.join(
+ _folder,
+ self._conf_filename
+ )
+
+ if not os.path.exists(_folder):
+ # it is not exists, create it
+ os.mkdir(_folder)
+ logger_cli.debug(
+ "... error logs folder '{}' created".format(_folder)
+ )
+ else:
+ logger_cli.debug(
+ "... error logs folder is at '{}'".format(_folder)
+ )
+ if not os.path.exists(self._conf_filename):
+ # put file with init values
+ self.conf = ConfigFile(self._area_code.lower())
+ self.conf.set_value('iteration', self._iteration)
+ self.conf.save_config(filepath=self._conf_filename)
+ logger_cli.debug(
+ "... create new config file '{}'".format(
+ self._conf_filename
+ )
+ )
+ else:
+ # it exists, try to load latest run
+ self.conf = ConfigFile(
+ self._area_code.lower(),
+ filepath=self._conf_filename
+ )
+ # it is loaded, update iteration from file
+ self._iteration = self.conf.get_value('iteration', value_type=int)
+ self._iteration += 1
+ logger_cli.debug(" ... starting iteration {}".format(self._iteration))
+
+ def save_iteration_data(self):
+ # save error log
+ _filename = "-".join([self._area_code.lower(), "errors"])
+ _filename += "." + str(self._iteration)
+ _log_filename = os.path.join(
+ pkg_dir,
+ self._error_logs_folder_name,
+ _filename
+ )
+ fu.write_lines_to_file(_log_filename, self.get_errors(as_list=True))
+ fu.append_line_to_file(_log_filename, "")
+ fu.append_lines_to_file(_log_filename, self.get_summary(as_list=True))
+ logger_cli.debug("... saved errors to '{}'".format(_log_filename))
+
+ # save last iteration number
+ self.conf.set_value('iteration', self._iteration)
+ self.conf.save_config()
+
def _format_error_code(self, index):
_t = "{:02d}".format(self._errors[index]['type'])
_i = "{:04d}".format(index)
@@ -27,9 +96,9 @@
_code = self._format_error_code(index)
# prepare data as string list
_d = self._errors[index]['data']
- _data = ["{}: {}".format(_k, _v) for _k, _v in _d.iteritems()]
+ _data = [" {}: {}".format(_k, _v) for _k, _v in _d.iteritems()]
# format message
- _msg = "### {}: {}\n{}".format(
+ _msg = "### {}:\n Description: {}\n{}".format(
_code,
self._get_error_type_text(self._errors[index]['type']),
"\n".join(_data)
@@ -89,9 +158,14 @@
else:
return "Unknown error index of {}".format(index)
- def get_summary(self, print_zeros=True):
+ def get_summary(self, print_zeros=True, as_list=False):
# create summary with counts per error type
- _list = []
+ _list = "\n{:=^8s}\n{:^8s}\n{:=^8s}".format(
+ "=",
+ "Totals",
+ "="
+ ).splitlines()
+
for _type in self._types.keys():
_len = len(
filter(
@@ -112,12 +186,27 @@
)
)
- return "\n".join(_list)
+ _total_errors = self.get_errors_total()
- def get_errors_as_list(self):
- # create list of strings with error messages
- _list = []
- for _idx in range(0, self._index - 1):
- _list.append("{}".format(self.get_error(_idx)))
+ _list.append('-'*20)
+ _list.append("{:5d} total errors found\n".format(_total_errors))
+ if as_list:
+ return _list
+ else:
+ return "\n".join(_list)
- return _list
+ def get_errors(self, as_list=False):
+ _list = ["# Errors"]
+ # Detailed errors
+ if self.get_errors_total() > 0:
+ # create list of strings with error messages
+ for _idx in range(1, self._index - 1):
+ _list.append(self._format_error(_idx))
+ _list.append("\n")
+ else:
+ _list.append("-> No errors")
+
+ if as_list:
+ return _list
+ else:
+ return "\n".join(_list)
diff --git a/cfg_checker/modules/network/__init__.py b/cfg_checker/modules/network/__init__.py
index 8f4a037..78df6c6 100644
--- a/cfg_checker/modules/network/__init__.py
+++ b/cfg_checker/modules/network/__init__.py
@@ -16,8 +16,13 @@
netChecker = _prepare_check()
netChecker.print_network_report()
+ # save what was collected
+ netChecker.errors.save_iteration_data()
+
+ # print a report
netChecker.print_summary()
+ # if set, print details
if args.detailed:
netChecker.print_error_details()
diff --git a/cfg_checker/modules/network/checker.py b/cfg_checker/modules/network/checker.py
index aeb1e61..48e7acb 100644
--- a/cfg_checker/modules/network/checker.py
+++ b/cfg_checker/modules/network/checker.py
@@ -10,7 +10,9 @@
class NetworkChecker(SaltNodes):
def __init__(self):
+ logger_cli.info("# Gathering environment information")
super(NetworkChecker, self).__init__()
+ logger_cli.info("# Initializing error logs folder")
self.errors = NetworkErrors()
# adding net data to tree
@@ -375,27 +377,15 @@
)
def print_summary(self):
- _total_errors = self.errors.get_errors_total()
- # Summary
- logger_cli.info(
- "\n{:=^8s}\n{:^8s}\n{:=^8s}".format(
- "=",
- "Totals",
- "="
- )
- )
logger_cli.info(self.errors.get_summary(print_zeros=False))
- logger_cli.info('-'*20)
- logger_cli.info("{:5d} total errors found\n".format(_total_errors))
def print_error_details(self):
# Detailed errors
- if self.errors.get_errors_total() > 0:
- logger_cli.info("\n# Errors")
- for _msg in self.errors.get_errors_as_list():
- logger_cli.info("{}\n".format(_msg))
- else:
- logger_cli.info("-> No errors\n")
+ logger_cli.info(
+ "\n{}\n".format(
+ self.errors.get_errors()
+ )
+ )
def create_html_report(self, filename):
"""
diff --git a/cfg_checker/nodes.py b/cfg_checker/nodes.py
index 3990cec..5e47447 100644
--- a/cfg_checker/nodes.py
+++ b/cfg_checker/nodes.py
@@ -23,7 +23,7 @@
# Keys for all nodes
# this is not working in scope of 2016.8.3, will overide with list
- logger_cli.debug("...collecting node names existing in the cloud")
+ logger_cli.debug("... collecting node names existing in the cloud")
try:
_keys = self.salt.list_keys()
_str = []
@@ -121,7 +121,7 @@
:return: no return value, data pulished internally
"""
logger_cli.debug(
- "...collecting node pillars for '{}'".format(pillar_path)
+ "... collecting node pillars for '{}'".format(pillar_path)
)
_result = self.salt.pillar_get(self.active_nodes_compound, pillar_path)
self.not_responded = []
@@ -165,7 +165,7 @@
config.salt_file_root, config.salt_scripts_folder
)
logger_cli.debug(
- "...Uploading script {} "
+ "... uploading script {} "
"to master's file cache folder: '{}'".format(
script_filename,
_storage_path
@@ -185,12 +185,12 @@
script_filename
)
- logger_cli.debug("...creating file in cache '{}'".format(_cache_path))
+ logger_cli.debug("... creating file in cache '{}'".format(_cache_path))
self.salt.f_touch_master(_cache_path)
self.salt.f_append_master(_cache_path, _script)
# command salt to copy file to minions
logger_cli.debug(
- "...creating script target folder '{}'".format(
+ "... creating script target folder '{}'".format(
_cache_path
)
)
diff --git a/requirements.txt b/requirements.txt
index 7f6827f..fe8d8d4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,4 +2,5 @@
pyyaml
jinja2
requests
-ipaddress
\ No newline at end of file
+ipaddress
+configparser
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 91b1e80..42ca20f 100644
--- a/setup.py
+++ b/setup.py
@@ -17,7 +17,8 @@
'pyyaml',
'jinja2',
'requests',
- 'ipaddress'
+ 'ipaddress',
+ 'configparser'
]
entry_points = {