blob: aa399dfc367154ccfd463c0d308d21df8f55d7ca [file] [log] [blame]
import abc
import os
import re
import time
from cfg_checker.common import const
from cfg_checker.common import logger_cli
from cfg_checker.common.file_utils import read_file_as_lines
from cfg_checker.nodes import salt_master
import jinja2
import six
pkg_dir = os.path.dirname(__file__)
pkg_dir = os.path.join(pkg_dir, os.pardir, os.pardir)
pkg_dir = os.path.normpath(pkg_dir)
# % threshhold values
_disk_warn = 80
_disk_critical = 90
_ram_warn = 5
_ram_critical = 3
_softnet_interval = 5
UP = const.NODE_UP
DOWN = const.NODE_DOWN
def line_breaks(text):
# replace python linebreaks with html breaks
return text.replace("\n", "<br />")
def get_sorted_keys(td):
# detect if we can sort by desc
# Yes, this is slow, but bullet-proof from empty desc
_desc = all([bool(td[k]['desc']) for k in td.keys()])
# Get sorted list
if not _desc:
return sorted(td.keys())
else:
return sorted(
td.keys(),
key=lambda k: (
td[k]['desc']['section'],
td[k]['desc']['app'],
k
)
)
def get_max(_list):
return sorted(_list)[-1]
def make_pkg_action_label(act):
_act_labels = {
const.ACT_UPGRADE: "Upgrade possible",
const.ACT_NEED_UP: "Needs upgrade",
const.ACT_NEED_DOWN: "Needs downgrade",
const.ACT_REPO: "Repo update",
const.ACT_NA: ""
}
return _act_labels[act]
def make_pkg_action_class(act):
_act_classes = {
const.ACT_UPGRADE: "possible",
const.ACT_NEED_UP: "needs_up",
const.ACT_NEED_DOWN: "needs_down",
const.ACT_REPO: "needs_repo",
const.ACT_NA: ""
}
return _act_classes[act]
def make_pkg_status_label(sts):
_status_labels = {
const.VERSION_OK: "OK",
const.VERSION_UP: "Upgraded",
const.VERSION_DOWN: "Downgraded",
const.VERSION_ERR: "ERROR",
const.VERSION_NA: "N/A"
}
return _status_labels[sts]
def make_pkg_status_class(sts):
return const.all_pkg_statuses[sts]
def make_node_status(sts):
return const.node_status[sts]
def make_repo_info(repos):
_text = ""
for r in repos:
# tag
_text += r['tag'] + ": "
# repo header
_text += " ".join([
r['subset'],
r['release'],
r['ubuntu-release'],
r['type'],
r['arch']
]) + ", "
# maintainer w/o email
_m = r['maintainer'][:r['maintainer'].find('<')-1]
_m_ascii = _m.encode('ascii', errors="xmlcharrefreplace")
_text += _m_ascii
# newline
_text += "<br />"
return _text
@six.add_metaclass(abc.ABCMeta)
class _Base(object):
def __init__(self):
self.jinja2_env = self.init_jinja2_env()
@abc.abstractmethod
def __call__(self, payload):
pass
@staticmethod
def init_jinja2_env():
return jinja2.Environment(
loader=jinja2.FileSystemLoader(os.path.join(pkg_dir, 'templates')),
trim_blocks=True,
lstrip_blocks=True)
class _TMPLBase(_Base):
@abc.abstractproperty
def tmpl(self):
pass
@staticmethod
def _count_totals(data):
data['counters']['total_nodes'] = len(data['nodes'])
def __call__(self, payload):
# init data structures
data = self.common_data()
# payload should have pre-sorted structure according to report called
# nodes, openstack_release, mcp_release, etc...
data.update(payload)
# add template specific data
self._extend_data(data)
# do counts global
self._count_totals(data)
# specific filters
self.jinja2_env.filters['linebreaks'] = line_breaks
self.jinja2_env.filters['get_max'] = get_max
self.jinja2_env.filters['get_sorted_keys'] = get_sorted_keys
self.jinja2_env.filters['pkg_status_label'] = make_pkg_status_label
self.jinja2_env.filters['pkg_status_class'] = make_pkg_status_class
self.jinja2_env.filters['pkg_action_label'] = make_pkg_action_label
self.jinja2_env.filters['pkg_action_class'] = make_pkg_action_class
self.jinja2_env.filters['node_status_class'] = make_node_status
self.jinja2_env.filters['pkg_repo_info'] = make_repo_info
# render!
logger_cli.info("-> Using template: {}".format(self.tmpl))
tmpl = self.jinja2_env.get_template(self.tmpl)
logger_cli.info("-> Rendering")
return tmpl.render(data)
def common_data(self):
return {
'counters': {},
'salt_info': {},
'gen_date': time.strftime("%m/%d/%Y %H:%M:%S")
}
def _extend_data(self, data):
pass
# HTML Package versions report
class CSVAllPackages(_TMPLBase):
tmpl = "pkg_versions_csv.j2"
# HTML Package versions report
class HTMLPackageCandidates(_TMPLBase):
tmpl = "pkg_versions_html.j2"
# Package versions report
class HTMLModelCompare(_TMPLBase):
tmpl = "model_tree_cmp_tmpl.j2"
def _extend_data(self, data):
# move names into separate place
data["names"] = data["diffs"].pop("diff_names")
data["tabs"] = data.pop("diffs")
# counters - mdl_diff
for _tab in data["tabs"].keys():
data['counters'][_tab] = len(data["tabs"][_tab]["diffs"].keys())
class HTMLNetworkReport(_TMPLBase):
tmpl = "network_check_tmpl.j2"
def _extend_data(self, data):
def get_bytes(value):
if value[-1] == 'G':
return int(float(value[:-1]) * 1024 * 1024 * 1024)
elif value[-1] == 'M':
return int(float(value[:-1]) * 1024 * 1024)
elif value[-1] == 'K':
return int(float(value[:-1]) * 1024)
else:
return int(value)
def _lscpu(_dict):
_key = "lscpu"
_key_r = "lscpu_raw"
# get all of the values
_f_cmd = salt_master.get_cmd_for_nodes
_cmd = "lscpu | sed -n '/\\:/s/ \\+/ /gp'"
_f_cmd(_cmd, _key_r, target_dict=_dict)
# parse them and put into dict
for node, dt in _dict.iteritems():
dt[_key] = {}
if dt['status'] == DOWN:
continue
lines = dt[_key_r].splitlines()
for line in lines:
li = line.split(':')
_var_name = li[0].lower()
_var_name = re.sub(' ', '_', _var_name)
_var_name = re.sub('|'.join(['\\(', '\\)']), '', _var_name)
_var_value = li[1].strip()
dt[_key][_var_name] = _var_value
dt.pop(_key_r)
# detect virtual nodes
if "hypervisor_vendor" in dt[_key]:
dt['node_type'] = "virtual"
else:
dt['node_type'] = "physical"
def _free(_dict):
_key = "ram"
_key_r = "ram_raw"
_f_cmd = salt_master.get_cmd_for_nodes
_cmd = "free -h | sed -n '/Mem/s/ \\+/ /gp'"
_f_cmd(_cmd, _key_r, target_dict=_dict)
# parse them and put into dict
for node, dt in _dict.iteritems():
dt[_key] = {}
if dt['status'] == DOWN:
continue
li = dt[_key_r].split()
dt[_key]['total'] = li[1]
dt[_key]['used'] = li[2]
dt[_key]['free'] = li[3]
dt[_key]['shared'] = li[4]
dt[_key]['cache'] = li[5]
dt[_key]['available'] = li[6]
_total = get_bytes(li[1])
_avail = get_bytes(li[6])
_m = _avail * 100.0 / _total
if _m < _ram_critical:
dt[_key]["status"] = "fail"
elif _m < _ram_warn:
dt[_key]["status"] = "warn"
else:
dt[_key]["status"] = ""
def _services(_dict):
_key = "services"
_key_r = "services_raw"
_f_cmd = salt_master.get_cmd_for_nodes
_cmd = "service --status-all"
_f_cmd(_cmd, _key_r, target_dict=_dict)
for node, dt in _dict.iteritems():
dt[_key] = {}
if dt['status'] == DOWN:
continue
lines = dt[_key_r].splitlines()
for line in lines:
li = line.split()
_status = li[1]
_name = li[3]
if _status == '-':
dt[_key][_name] = False
elif _status == '+':
dt[_key][_name] = True
else:
dt[_key][_name] = None
dt.pop(_key_r)
def _vcp_status(_dict):
_key = "virsh"
_key_r = "virsh_raw"
salt_master.get_cmd_for_nodes(
"virsh list --all | sed -n -e '/[0-9]/s/ \\+/ /gp'",
_key_r,
target_dict=_dict,
nodes="kvm*"
)
_kvm = filter(lambda x: x.find("kvm") >= 0, _dict.keys())
for node in _kvm:
dt = _dict[node]
dt[_key] = {}
if dt['status'] == DOWN:
continue
lines = dt[_key_r].splitlines()
for line in lines:
li = line.split()
_id = li[0]
_name = li[1]
_status = li[2]
dt[_key][_name] = {
'id': _id,
'status': _status
}
dt.pop(_key_r)
# query per-cpu and count totals
# total (0), dropped(1), squeezed (2), collision (7)
def _soft_net_stats(_dict):
_key = "net_stats"
_key_r = "net_stats_raw"
_f_cmd = salt_master.get_cmd_for_nodes
_cmd = "cat /proc/net/softnet_stat; echo \\#; " \
"sleep {}; cat /proc/net/softnet_stat".format(
_softnet_interval
)
_f_cmd(_cmd, _key_r, target_dict=_dict)
for node, dt in _dict.iteritems():
_cpuindex = 1
_add_mode = True
# final totals
dt[_key] = {
"total": [0, 0, 0, 0]
}
# totals for start mark
_ts = [0, 0, 0, 0]
# skip if node is down
if dt['status'] == DOWN:
continue
lines = dt[_key_r].splitlines()
for line in lines:
if line.startswith("#"):
_add_mode = False
_cpuindex = 1
continue
li = line.split()
_c = [
int(li[0], 16),
int(li[1], 16),
int(li[2], 16),
int(li[7], 16)
]
_id = "cpu{:02}".format(_cpuindex)
if _id not in dt[_key]:
dt[_key][_id] = []
_dc = dt[_key][_id]
if _add_mode:
# saving values and adding totals
dt[_key][_id] = _c
# save start totals
_ts = [_ts[i]+_c[i] for i in range(0, len(_c))]
else:
# this is second measurement
# subtract all values
for i in range(len(_c)):
dt[_key][_id][i] = _c[i] - _dc[i]
dt[_key]["total"][i] += _c[i]
_cpuindex += 1
# finally, subtract initial totals
for k, v in dt[_key].iteritems():
if k != "total":
dt[_key][k] = [v[i] / 5. for i in range(len(v))]
else:
dt[_key][k] = [(v[i]-_ts[i])/5. for i in range(len(v))]
dt.pop(_key_r)
# prepare yellow and red marker values
data["const"] = {
"net_interval": _softnet_interval,
"ram_warn": _ram_warn,
"ram_critical": _ram_critical,
"disk_warn": _disk_warn,
"disk_critical": _disk_critical,
"services": read_file_as_lines(
os.path.join(
pkg_dir,
'etc',
'services.list'
)
)
}
# get kernel version
salt_master.get_cmd_for_nodes(
"uname -r",
"kernel",
target_dict=data["nodes"]
)
# process lscpu data
_lscpu(data["nodes"])
# free ram
# sample: 16425392 14883144 220196
_free(data["nodes"])
# disk space
# sample: /dev/vda1 78G 33G 45G 43%
salt_master.get_cmd_for_nodes(
"df -h | sed -n '/^\\/dev/s/ \\+/ /gp' | cut -d\" \" -f 1-5",
"disk_raw",
target_dict=data["nodes"]
)
for _data in data["nodes"].itervalues():
_data["disk"] = {}
# show first device row by default
_data["disk_max_dev"] = None
_d = _data["disk"]
_r = _data["disk_raw"]
_r = _r.splitlines()
_max = -1
for idx in range(0, len(_r)):
_t = _r[idx].split()
_d[_t[0]] = {}
_d[_t[0]]['v'] = _t[1:]
_chk = int(_t[-1].split('%')[0])
if _chk > _max:
_data["disk_max_dev"] = _t[0]
_max = _chk
if _chk > _disk_critical:
_d[_t[0]]['f'] = "fail"
elif _chk > _disk_warn:
_d[_t[0]]['f'] = "warn"
else:
_d[_t[0]]['f'] = ""
# prepare networks data for report
for net, net_v in data['map'].iteritems():
for node, ifs in net_v.iteritems():
for d in ifs:
_err = "fail"
d['interface_error'] = _err if d['interface_error'] else ""
d['mtu_error'] = _err if d['mtu_error'] else ""
d['status_error'] = _err if d['status_error'] else ""
d['subnet_gateway_error'] = \
_err if d['subnet_gateway_error'] else ""
_services(data["nodes"])
# vcp status
# query virsh and prepare for report
_vcp_status(data["nodes"])
# soft net stats
_soft_net_stats(data["nodes"])
class ReportToFile(object):
def __init__(self, report, target):
self.report = report
self.target = target
def __call__(self, payload):
payload = self.report(payload)
if isinstance(self.target, six.string_types):
self._wrapped_dump(payload)
else:
self._dump(payload, self.target)
def _wrapped_dump(self, payload):
with open(self.target, 'wt') as target:
self._dump(payload, target)
@staticmethod
def _dump(payload, target):
target.write(payload)