Skip nodes functionality for Package and Network modules
Added to main entrypoint
- Skip nodes using simple argument with '*' as a trailing wildcard
- Skip nodes using file list
Usability improovement
- Node list preview in status line
- Node stats alignment in net report
Minor fixes:
- Python version detection (3.5+)
- Node counter for each status
- Proper node skip handling
Change-Id: I086ef501bc06f0e739df25349257f1c63a2e2fcf
Related-PROD: PROD-35009
diff --git a/cfg_checker/cfg_check.py b/cfg_checker/cfg_check.py
index 5c986a2..2ce37eb 100644
--- a/cfg_checker/cfg_check.py
+++ b/cfg_checker/cfg_check.py
@@ -32,6 +32,20 @@
action='store_true', default=True,
help="Use sudo for getting salt creds"
)
+
+ parser.add_argument(
+ '--skip-nodes',
+ metavar='skip_string', default=None,
+ help="String with nodes to skip. Only trailing '*' supported!"
+ " Example: 'cmp*,ctl01'"
+ )
+
+ parser.add_argument(
+ '--skip-nodes-file',
+ metavar='skip_nodes_file', default=None,
+ help="Filename with nodes to skip. Note: use fqdn node names."
+ )
+
subparsers = parser.add_subparsers(dest='command')
# create parsers
diff --git a/cfg_checker/common/const.py b/cfg_checker/common/const.py
index 685c79a..3b17099 100644
--- a/cfg_checker/common/const.py
+++ b/cfg_checker/common/const.py
@@ -8,6 +8,7 @@
_cnt = itertools.count()
NODE_DOWN = next(_cnt)
NODE_UP = next(_cnt)
+NODE_SKIP = next(_cnt)
# version const order is important!
# biggest get shown in report top row
@@ -42,12 +43,13 @@
VERSION_DOWN: "downgraded",
VERSION_WARN: "warning",
VERSION_ERR: "error",
- VERSION_NA: "no status"
+ VERSION_NA: "nostatus"
}
node_status = {
NODE_UP: "up",
- NODE_DOWN: "down"
+ NODE_DOWN: "down",
+ NODE_SKIP: "skip"
}
uknown_code = "unk"
diff --git a/cfg_checker/common/other.py b/cfg_checker/common/other.py
index a385b90..5a4c552 100644
--- a/cfg_checker/common/other.py
+++ b/cfg_checker/common/other.py
@@ -126,20 +126,20 @@
else:
raise ConfigException(_message)
- def get_nodes_list(self, env, nodes_list):
+ def get_nodes_list(self, nodes_list, env_sting=None):
_list = []
- if env is None:
+ if env_sting is None:
# nothing supplied, use the one in repo
try:
if not nodes_list:
return []
- with open(os.path.join(pkg_dir, nodes_list)) as _f:
+ with open(nodes_list) as _f:
_list.extend(_f.read().splitlines())
except IOError as e:
raise ConfigException("# Error while loading file, '{}': "
"{}".format(e.filename, e.strerror))
else:
- _list.extend(self.node_string_to_list(env))
+ _list.extend(self.node_string_to_list(env_sting))
# validate names
_invalid = []
@@ -151,7 +151,7 @@
else:
_valid.append(_name)
- return _valid
+ return _valid, _invalid
utils = Utils()
diff --git a/cfg_checker/common/settings.py b/cfg_checker/common/settings.py
index 92c17a5..cca5142 100644
--- a/cfg_checker/common/settings.py
+++ b/cfg_checker/common/settings.py
@@ -1,4 +1,5 @@
import os
+import sys
from cfg_checker.common.exception import ConfigException
@@ -15,11 +16,17 @@
class CheckerConfiguration(object):
+ @staticmethod
def load_nodes_list():
- return utils.get_nodes_list(
- os.environ.get('CFG_ALL_NODES', None),
- os.environ.get('SALT_NODE_LIST_FILE', None)
- )
+ _file = os.environ.get('SALT_NODE_LIST_FILE', None)
+ if _file:
+ _v, _ = utils.get_nodes_list(
+ os.path.join(pkg_dir, _file),
+ env_sting=os.environ.get('CFG_ALL_NODES', None)
+ )
+ return _v
+ else:
+ return None
def _init_values(self):
"""Load values from environment variables or put default ones
@@ -119,6 +126,16 @@
def __init__(self):
"""Base configuration class. Only values that are common for all scripts
"""
+ # Make sure we running on Python 3
+ if sys.version_info[0] < 3 and sys.version_info[1] < 5:
+ logger_cli.error("# ERROR: Python 3.5+ is required")
+ sys.exit(1)
+ else:
+ logger_cli.debug("### Python version is {}.{}".format(
+ sys.version_info[0],
+ sys.version_info[1]
+ ))
+
_env = os.getenv('SALT_ENV', None)
self._init_env(_env)
self._init_values()
diff --git a/cfg_checker/helpers/args_utils.py b/cfg_checker/helpers/args_utils.py
index 726c20e..498ed30 100644
--- a/cfg_checker/helpers/args_utils.py
+++ b/cfg_checker/helpers/args_utils.py
@@ -11,6 +11,20 @@
self.print_help()
+def get_skip_args(args):
+ if hasattr(args, "skip_nodes"):
+ _skip = getattr(args, "skip_nodes")
+ if _skip:
+ _skip = _skip.split(',')
+ else:
+ _skip = None
+ if hasattr(args, "skip_nodes_file"):
+ _skip_file = getattr(args, "skip_nodes_file")
+ else:
+ _skip_file = None
+ return _skip, _skip_file
+
+
def get_arg(args, str_arg):
_attr = getattr(args, str_arg)
if _attr:
diff --git a/cfg_checker/modules/network/__init__.py b/cfg_checker/modules/network/__init__.py
index 71df82d..28d08c4 100644
--- a/cfg_checker/modules/network/__init__.py
+++ b/cfg_checker/modules/network/__init__.py
@@ -71,7 +71,11 @@
# should not print map, etc...
# Just bare summary and errors
logger_cli.info("# Network check to console")
- netChecker = checker.NetworkChecker()
+ _skip, _skip_file = args_utils.get_skip_args(args)
+ netChecker = checker.NetworkChecker(
+ skip_list=_skip,
+ skip_list_file=_skip_file
+ )
netChecker.check_networks()
# save what was collected
@@ -93,8 +97,11 @@
logger_cli.info("# Network report (check, node map")
_filename = args_utils.get_arg(args, 'html')
-
- netChecker = checker.NetworkChecker()
+ _skip, _skip_file = args_utils.get_skip_args(args)
+ netChecker = checker.NetworkChecker(
+ skip_list=_skip,
+ skip_list_file=_skip_file
+ )
netChecker.check_networks(map=False)
# save what was collected
@@ -108,8 +115,11 @@
# Network Map
# Should generate network map to console or HTML
logger_cli.info("# Network report")
-
- networkMap = mapper.NetworkMapper()
+ _skip, _skip_file = args_utils.get_skip_args(args)
+ networkMap = mapper.NetworkMapper(
+ skip_list=_skip,
+ skip_list_file=_skip_file
+ )
networkMap.prepare_all_maps()
networkMap.create_map()
networkMap.print_map()
@@ -120,7 +130,11 @@
def do_list(args):
# Network List
# Should generate network map to console or HTML
- _map = mapper.NetworkMapper()
+ _skip, _skip_file = args_utils.get_skip_args(args)
+ _map = mapper.NetworkMapper(
+ skip_list=_skip,
+ skip_list_file=_skip_file
+ )
reclass = _map.map_network(_map.RECLASS)
runtime = _map.map_network(_map.RUNTIME)
@@ -141,7 +155,13 @@
if not args.cidr:
logger_cli.error("\n# Use mcp-check network list to get list of CIDRs")
_cidr = args_utils.get_arg(args, "cidr")
- _pinger = pinger.NetworkPinger(mtu=args.mtu, detailed=args.detailed)
+ _skip, _skip_file = args_utils.get_skip_args(args)
+ _pinger = pinger.NetworkPinger(
+ mtu=args.mtu,
+ detailed=args.detailed,
+ skip_list=_skip,
+ skip_list_file=_skip_file
+ )
_ret = _pinger.ping_nodes(_cidr)
diff --git a/cfg_checker/modules/network/checker.py b/cfg_checker/modules/network/checker.py
index acd3bb1..c590d13 100644
--- a/cfg_checker/modules/network/checker.py
+++ b/cfg_checker/modules/network/checker.py
@@ -5,10 +5,18 @@
class NetworkChecker(object):
- def __init__(self):
+ def __init__(
+ self,
+ skip_list=None,
+ skip_list_file=None
+ ):
logger_cli.debug("... init error logs folder")
self.errors = NetworkErrors()
- self.mapper = NetworkMapper(self.errors)
+ self.mapper = NetworkMapper(
+ self.errors,
+ skip_list=skip_list,
+ skip_list_file=skip_list_file
+ )
def check_networks(self, map=True):
self.mapper.map_network(self.mapper.RECLASS)
@@ -41,6 +49,7 @@
filename
)
_report({
+ "domain": self.mapper.domain,
"nodes": self.mapper.nodes,
"map": self.mapper.map,
"mcp_release": self.mapper.cluster['mcp_release'],
diff --git a/cfg_checker/modules/network/mapper.py b/cfg_checker/modules/network/mapper.py
index 483c11f..51f52bb 100644
--- a/cfg_checker/modules/network/mapper.py
+++ b/cfg_checker/modules/network/mapper.py
@@ -31,12 +31,21 @@
CONFIG = "config"
RUNTIME = "runtime"
- def __init__(self, errors_class=None):
+ def __init__(
+ self,
+ errors_class=None,
+ skip_list=None,
+ skip_list_file=None
+ ):
logger_cli.info("# Initializing mapper")
# init networks and nodes
self.networks = {}
- self.nodes = salt_master.get_nodes()
+ self.nodes = salt_master.get_nodes(
+ skip_list=skip_list,
+ skip_list_file=skip_list_file
+ )
self.cluster = salt_master.get_info()
+ self.domain = salt_master.domain
# init and pre-populate interfaces
self.interfaces = {k: {} for k in self.nodes}
# Init errors class
diff --git a/cfg_checker/modules/network/pinger.py b/cfg_checker/modules/network/pinger.py
index 0500284..5b12a94 100644
--- a/cfg_checker/modules/network/pinger.py
+++ b/cfg_checker/modules/network/pinger.py
@@ -10,10 +10,21 @@
# This is independent class with a salt.nodes input
class NetworkPinger(object):
- def __init__(self, mtu=None, detailed=False, errors_class=None):
+ def __init__(
+ self,
+ mtu=None,
+ detailed=False,
+ errors_class=None,
+ skip_list=None,
+ skip_list_file=None
+ ):
logger_cli.info("# Initializing")
# all active nodes in the cloud
- self.target_nodes = salt_master.get_nodes()
+ self.target_nodes = salt_master.get_nodes(
+ skip_list=skip_list,
+ skip_list_file=skip_list_file
+ )
+
# default MTU value
self.target_mtu = mtu if mtu else 64
# only data
diff --git a/cfg_checker/modules/packages/__init__.py b/cfg_checker/modules/packages/__init__.py
index 41dfca1..2d0cc79 100644
--- a/cfg_checker/modules/packages/__init__.py
+++ b/cfg_checker/modules/packages/__init__.py
@@ -112,9 +112,12 @@
_kw = [args.exclude_keywords]
# init connection to salt and collect minion data
+ _skip, _skip_file = args_utils.get_skip_args(args)
pChecker = checker.CloudPackageChecker(
force_tag=args.force_tag,
- exclude_keywords=_kw
+ exclude_keywords=_kw,
+ skip_list=_skip,
+ skip_list_file=_skip_file
)
# collect data on installed packages
pChecker.collect_installed_packages()
diff --git a/cfg_checker/modules/packages/checker.py b/cfg_checker/modules/packages/checker.py
index 8f30f3c..fb02db2 100644
--- a/cfg_checker/modules/packages/checker.py
+++ b/cfg_checker/modules/packages/checker.py
@@ -12,10 +12,19 @@
class CloudPackageChecker(object):
- def __init__(self, force_tag=None, exclude_keywords=[]):
+ def __init__(
+ self,
+ force_tag=None,
+ exclude_keywords=[],
+ skip_list=None,
+ skip_list_file=None
+ ):
# Init salt master info
if not salt_master.nodes:
- salt_master.nodes = salt_master.get_nodes()
+ salt_master.nodes = salt_master.get_nodes(
+ skip_list=skip_list,
+ skip_list_file=skip_list_file
+ )
# check that this env tag is present in Manager
self.rm = RepoManager()
@@ -57,6 +66,7 @@
_data['status_err'] = const.VERSION_ERR
_data['status_warn'] = const.VERSION_WARN
_data['status_down'] = const.VERSION_DOWN
+ _data['status_skip'] = const.VERSION_NA
# Presort packages
_data['critical'] = {}
diff --git a/cfg_checker/modules/reclass/comparer.py b/cfg_checker/modules/reclass/comparer.py
index 47e4baf..3c308e0 100644
--- a/cfg_checker/modules/reclass/comparer.py
+++ b/cfg_checker/modules/reclass/comparer.py
@@ -151,11 +151,15 @@
# ignore _source key
if k == "_source":
continue
- # ignore secrets
+ # ignore secrets and other secure stuff
if isinstance(k, str) and k == "secrets.yml":
continue
if isinstance(k, str) and k.find("_password") > 0:
continue
+ if isinstance(k, str) and k.find("_key") > 0:
+ continue
+ if isinstance(k, str) and k.find("_token") > 0:
+ continue
# check if this is an env name cluster entry
if dict2 is not None and \
k == self.model_name_1 and \
diff --git a/cfg_checker/nodes.py b/cfg_checker/nodes.py
index c261752..13ab3e7 100644
--- a/cfg_checker/nodes.py
+++ b/cfg_checker/nodes.py
@@ -3,7 +3,9 @@
from copy import deepcopy
from cfg_checker.clients import get_salt_remote, salt
-from cfg_checker.common import config, const
+from cfg_checker.common import config
+from cfg_checker.common.const import all_roles_map
+from cfg_checker.common.const import NODE_UP, NODE_DOWN, NODE_SKIP
from cfg_checker.common import logger, logger_cli
from cfg_checker.common import utils
from cfg_checker.common.exception import SaltException
@@ -12,7 +14,7 @@
node_tmpl = {
'role': '',
'node_group': '',
- 'status': const.NODE_DOWN,
+ 'status': NODE_DOWN,
'pillars': {},
'grains': {}
}
@@ -25,7 +27,7 @@
self.salt = salt
self.nodes = None
- def gather_node_info(self):
+ def gather_node_info(self, skip_list, skip_list_file):
# 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")
@@ -61,33 +63,86 @@
else:
_minions = self.node_keys['minions']
+ # Skip nodes if needed
+ _skipped_minions = []
+ # skip list file
+ if skip_list_file:
+ _valid, _invalid = utils.get_nodes_list(skip_list_file)
+ logger_cli.info(
+ "\n# WARNING: Detected invalid entries "
+ "in nodes skip list:\n".format(
+ "\n".join(_invalid)
+ )
+ )
+ _skipped_minions.extend(_valid)
+ # process wildcard, create node list out of mask
+ if skip_list:
+ _list = []
+ _invalid = []
+ for _item in skip_list:
+ if '*' in _item:
+ _str = _item[:_item.index('*')]
+ _nodes = [_m for _m in _minions if _m.startswith(_str)]
+ if not _nodes:
+ logger_cli.warn(
+ "# WARNING: No nodes found for {}".format(_item)
+ )
+ _list.extend(_nodes)
+ else:
+ if _item in _minions:
+ _list += _item
+ else:
+ logger_cli.warn(
+ "# WARNING: No node found for {}".format(_item)
+ )
+ # removing duplicates
+ _list = list(set(_list))
+ _skipped_minions.extend(_list)
+
# in case API not listed minions, we need all that answer ping
_active = self.salt.get_active_nodes()
logger_cli.info("-> nodes responded: {}".format(len(_active)))
# iterate through all accepted nodes and create a dict for it
self.nodes = {}
self.skip_list = []
+ _domains = set()
for _name in _minions:
_nc = utils.get_node_code(_name)
- _rmap = const.all_roles_map
+ _rmap = all_roles_map
_role = _rmap[_nc] if _nc in _rmap else 'unknown'
- _status = const.NODE_UP if _name in _active else const.NODE_DOWN
- if _status == const.NODE_DOWN:
+ if _name in _skipped_minions:
+ _status = NODE_SKIP
self.skip_list.append(_name)
- logger_cli.info("-> '{}' is down, marked to skip".format(
- _name
- ))
+ else:
+ _status = NODE_UP if _name in _active else NODE_DOWN
+ if _status == NODE_DOWN:
+ self.skip_list.append(_name)
+ logger_cli.info(
+ "-> '{}' is down, "
+ "added to skip list".format(
+ _name
+ )
+ )
self.nodes[_name] = deepcopy(node_tmpl)
+ self.nodes[_name]['shortname'] = _name.split(".", 1)[0]
+ _domains.add(_name.split(".", 1)[1])
self.nodes[_name]['node_group'] = _nc
self.nodes[_name]['role'] = _role
self.nodes[_name]['status'] = _status
+ _domains = list(_domains)
+ if len(_domains) > 1:
+ logger_cli.warning(
+ "Multiple domains detected: {}".format(",".join(_domains))
+ )
+ else:
+ self.domain = _domains[0]
logger_cli.info("-> {} nodes inactive".format(len(self.skip_list)))
logger_cli.info("-> {} nodes collected".format(len(self.nodes)))
# form an all nodes compound string to use in salt
self.active_nodes_compound = self.salt.compound_string_from_list(
filter(
- lambda nd: self.nodes[nd]['status'] == const.NODE_UP,
+ lambda nd: self.nodes[nd]['status'] == NODE_UP,
self.nodes
)
)
@@ -96,7 +151,7 @@
# lambda nd: self.nodes[nd]['role'] == const.all_roles_map['cfg'],
# self.nodes
# )
- _role = const.all_roles_map['cfg']
+ _role = all_roles_map['cfg']
_filtered = [n for n, v in self.nodes.items() if v['role'] == _role]
if len(_filtered) < 1:
raise SaltException(
@@ -137,9 +192,12 @@
else:
return False
- def get_nodes(self):
+ def get_nodes(self, skip_list=None, skip_list_file=None):
if not self.nodes:
- self.gather_node_info()
+ if not skip_list and config.skip_nodes:
+ self.gather_node_info(config.skip_nodes, skip_list_file)
+ else:
+ self.gather_node_info(skip_list, skip_list_file)
return self.nodes
def get_info(self):
@@ -177,7 +235,7 @@
if target_key not in data:
data[target_key] = None
# Save data
- if data['status'] == const.NODE_DOWN:
+ if data['status'] in [NODE_DOWN, NODE_SKIP]:
data[target_key] = None
elif node not in _result:
continue
@@ -219,7 +277,7 @@
if _key not in _data:
_data[_key] = {}
_data = _data[_key]
- if data['status'] == const.NODE_DOWN:
+ if data['status'] in [NODE_DOWN, NODE_SKIP]:
_data[_pillar_keys[-1]] = None
elif not _result[node]:
logger_cli.debug(
diff --git a/cfg_checker/reports/reporter.py b/cfg_checker/reports/reporter.py
index 3d3ede3..e0a746e 100644
--- a/cfg_checker/reports/reporter.py
+++ b/cfg_checker/reports/reporter.py
@@ -25,6 +25,7 @@
UP = const.NODE_UP
DOWN = const.NODE_DOWN
+SKIP = const.NODE_SKIP
def line_breaks(text):
@@ -265,7 +266,7 @@
# parse them and put into dict
for node, dt in _dict.items():
dt[_key] = {}
- if dt['status'] == DOWN:
+ if dt['status'] == DOWN or dt['status'] == SKIP:
continue
if not dt[_key_r]:
# no stats collected, put negatives
@@ -295,7 +296,7 @@
# parse them and put into dict
for node, dt in _dict.items():
dt[_key] = {}
- if dt['status'] == DOWN:
+ if dt['status'] == DOWN or dt['status'] == SKIP:
continue
if not dt[_key_r]:
# no stats collected, put negatives
@@ -327,7 +328,7 @@
_f_cmd(_cmd, _key_r, target_dict=_dict)
for node, dt in _dict.items():
dt[_key] = {}
- if dt['status'] == DOWN:
+ if dt['status'] == DOWN or dt['status'] == SKIP:
continue
if not dt[_key_r]:
# no stats collected, put negatives
@@ -359,7 +360,7 @@
for node in _kvm:
dt = _dict[node]
dt[_key] = {}
- if dt['status'] == DOWN:
+ if dt['status'] == DOWN or dt['status'] == SKIP:
continue
if not dt[_key_r]:
# no stats collected, put negatives
@@ -394,7 +395,7 @@
# totals for start mark
_ts = [0, 0, 0, 0]
# skip if node is down
- if dt['status'] == DOWN:
+ if dt['status'] == DOWN or dt['status'] == SKIP:
dt[_key] = {
"total": [-1, -1, -1, -1]
}
@@ -492,6 +493,10 @@
dt["disk"]["unknown"] = {}
dt["disk_max_dev"] = "unknown"
continue
+ if dt['status'] == SKIP:
+ dt["disk"]["skipped"] = {}
+ dt["disk_max_dev"] = "skipped"
+ continue
if not dt[_key_r]:
# no stats collected, put negatives
dt.pop(_key_r)