|
|
|
#!/usr/bin/python
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
# @Time : 2019-10-09
|
|
|
|
# @Author : lework
|
|
|
|
# @Desc : 使用ansible的facts数据生成linux资源报告
|
|
|
|
|
|
|
|
|
|
|
|
import datetime
|
|
|
|
import smtplib
|
|
|
|
import os
|
|
|
|
import json
|
|
|
|
import codecs
|
|
|
|
import copy
|
|
|
|
from email.header import Header
|
|
|
|
from email.mime.text import MIMEText
|
|
|
|
import urllib.request
|
|
|
|
from jinja2 import FileSystemLoader, Environment
|
|
|
|
|
|
|
|
|
|
|
|
def deepupdate(target, src, overwrite=True):
|
|
|
|
"""Deep update target list, dict or set or other iterable with src
|
|
|
|
For each k,v in src: if k doesn't exist in target, it is deep copied from
|
|
|
|
src to target. Otherwise, if v is a list, target[k] is extended with
|
|
|
|
src[k]. If v is a set, target[k] is updated with v, If v is a dict,
|
|
|
|
recursively deep-update it. If `overwrite` is False, existing values in
|
|
|
|
target will not be overwritten.
|
|
|
|
Examples:
|
|
|
|
\>>> t = {'name': 'Ferry', 'hobbies': ['programming', 'sci-fi']}
|
|
|
|
\>>> deepupdate(t, {'hobbies': ['gaming']})
|
|
|
|
\>>> print t
|
|
|
|
{'name': 'Ferry', 'hobbies': ['programming', 'sci-fi', 'gaming']}
|
|
|
|
"""
|
|
|
|
for k, v in src.items():
|
|
|
|
if type(v) == list:
|
|
|
|
if not k in target:
|
|
|
|
target[k] = copy.deepcopy(v)
|
|
|
|
elif overwrite is True:
|
|
|
|
target[k].extend(v)
|
|
|
|
elif type(v) == dict:
|
|
|
|
if not k in target:
|
|
|
|
target[k] = copy.deepcopy(v)
|
|
|
|
else:
|
|
|
|
deepupdate(target[k], v, overwrite=overwrite)
|
|
|
|
elif type(v) == set:
|
|
|
|
if not k in target:
|
|
|
|
target[k] = v.copy()
|
|
|
|
elif overwrite is True:
|
|
|
|
if type(target[k]) == list:
|
|
|
|
target[k].extend(v)
|
|
|
|
elif type(target[k]) == set:
|
|
|
|
target[k].update(v)
|
|
|
|
else:
|
|
|
|
raise TypeError("Cannot update {} with {}".format(type(target[k]), type(v)))
|
|
|
|
else:
|
|
|
|
if k not in target or overwrite is True:
|
|
|
|
target[k] = copy.copy(v)
|
|
|
|
|
|
|
|
|
|
|
|
def send_mail(mail_config, to_list, subject, content):
|
|
|
|
"""
|
|
|
|
发送HTML类型的邮件
|
|
|
|
:param mail_config: dict
|
|
|
|
:param to_list: list
|
|
|
|
:param subject: str
|
|
|
|
:param content: str
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
|
|
|
|
mail_port = mail_config.get('mail_port', '')
|
|
|
|
mail_host = mail_config.get('mail_host', '')
|
|
|
|
mail_user = mail_config.get('mail_user', '')
|
|
|
|
mail_pass = mail_config.get('mail_pass', '')
|
|
|
|
|
|
|
|
me = mail_user
|
|
|
|
msg = MIMEText(content, _subtype='html', _charset='utf-8')
|
|
|
|
msg['Subject'] = Header(subject, 'utf-8')
|
|
|
|
msg['From'] = me
|
|
|
|
msg['to'] = ",".join(to_list)
|
|
|
|
try:
|
|
|
|
s = smtplib.SMTP_SSL(mail_host, mail_port)
|
|
|
|
s.login(mail_user, mail_pass)
|
|
|
|
s.sendmail(me, to_list, msg.as_string())
|
|
|
|
s.quit()
|
|
|
|
print("[Send mail] success.")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
|
|
print("[Send mail] error. %s" % e)
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
class Ansible(object):
|
|
|
|
"""
|
|
|
|
生成主机信息
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, fact_dirs, fact_cache=False):
|
|
|
|
|
|
|
|
self.fact_dirs = fact_dirs
|
|
|
|
self.fact_cache = fact_cache
|
|
|
|
self.host_data = {}
|
|
|
|
self.remote_timestamp = 0
|
|
|
|
|
|
|
|
# 条件
|
|
|
|
self.bad_threshold = 80
|
|
|
|
self.critical_threshold = 90
|
|
|
|
self.time_threshold = 10 * 60
|
|
|
|
|
|
|
|
self.default_host_info = [
|
|
|
|
'ansible_hostname',
|
|
|
|
'ansible_default_ipv4',
|
|
|
|
'ansible_distribution',
|
|
|
|
'ansible_distribution_version',
|
|
|
|
'ansible_kernel',
|
|
|
|
'ansible_dns',
|
|
|
|
'ansible_uptime_seconds',
|
|
|
|
'ansible_date_time',
|
|
|
|
'ansible_memory_mb',
|
|
|
|
'ansible_memfree_mb',
|
|
|
|
'ansible_memtotal_mb',
|
|
|
|
'ansible_mounts',
|
|
|
|
'ansible_swaptotal_mb',
|
|
|
|
'ansible_swapfree_mb'
|
|
|
|
]
|
|
|
|
|
|
|
|
self.check_result = {
|
|
|
|
'time': '',
|
|
|
|
'summary': {
|
|
|
|
'ok': 0,
|
|
|
|
'bad': 0,
|
|
|
|
'critical': 0,
|
|
|
|
'total': 0,
|
|
|
|
'error': 0
|
|
|
|
},
|
|
|
|
'ok': [],
|
|
|
|
'bad': [],
|
|
|
|
'critical': [],
|
|
|
|
'ok_item': {},
|
|
|
|
'bad_item': {},
|
|
|
|
'critical_item': {},
|
|
|
|
'error_item': {}
|
|
|
|
}
|
|
|
|
|
|
|
|
for fact_dir in self.fact_dirs:
|
|
|
|
self._parse_fact_dir(fact_dir, self.fact_cache)
|
|
|
|
|
|
|
|
self._set_remote_timestamp()
|
|
|
|
|
|
|
|
def _parse_fact_dir(self, fact_dir, fact_cache=False):
|
|
|
|
if not os.path.isdir(fact_dir):
|
|
|
|
raise IOError("Not a directory: '{0}'".format(fact_dir))
|
|
|
|
|
|
|
|
flist = []
|
|
|
|
for (dirpath, dirnames, filenames) in os.walk(fact_dir):
|
|
|
|
flist.extend(filenames)
|
|
|
|
break
|
|
|
|
|
|
|
|
for fname in flist:
|
|
|
|
if fname.startswith('.'):
|
|
|
|
continue
|
|
|
|
hostname = fname
|
|
|
|
|
|
|
|
fd = codecs.open(os.path.join(fact_dir, fname), 'r', encoding='utf8')
|
|
|
|
s = fd.readlines()
|
|
|
|
fd.close()
|
|
|
|
try:
|
|
|
|
x = json.loads(''.join(s))
|
|
|
|
# for compatibility with fact_caching=jsonfile
|
|
|
|
# which omits the "ansible_facts" parent key added by the setup module
|
|
|
|
if self.fact_cache:
|
|
|
|
x = json.loads('{ "ansible_facts": ' + ''.join(s) + ' }')
|
|
|
|
self.update_host(hostname, x)
|
|
|
|
self.update_host(hostname, {'name': hostname})
|
|
|
|
except ValueError as e:
|
|
|
|
# Ignore non-JSON files (and bonus errors)
|
|
|
|
print("Error parsing: %s: %s" % (fname, e))
|
|
|
|
|
|
|
|
def _set_remote_timestamp(self):
|
|
|
|
time_api = "http://api.m.taobao.com/rest/api3.do?api=mtop.common.getTimestamp"
|
|
|
|
try:
|
|
|
|
response = urllib.request.urlopen(time_api)
|
|
|
|
result = json.loads(response.read().decode('utf-8'))
|
|
|
|
self.remote_timestamp = int(result['data'].get('t', '0'))
|
|
|
|
except Exception as e:
|
|
|
|
self.remote_timestamp = datetime.datetime.timestamp(datetime.datetime.now())
|
|
|
|
|
|
|
|
def check_time(self, host, item, now):
|
|
|
|
now_timestamp = datetime.datetime.timestamp(datetime.datetime.strptime(now, "%Y-%m-%dT%H:%M:%SZ"))
|
|
|
|
time_zone = 8 * 60 * 60
|
|
|
|
|
|
|
|
if abs(self.remote_timestamp - now_timestamp - time_zone) >= self.time_threshold:
|
|
|
|
self.check_result['critical'].append(host)
|
|
|
|
if host not in self.check_result['critical_item']:
|
|
|
|
self.check_result['critical_item'][host] = {'critical': [], 'bad': []}
|
|
|
|
self.check_result['critical_item'][host]['critical'].append(item)
|
|
|
|
|
|
|
|
def check_usedutilization(self, host, item, now):
|
|
|
|
if now >= self.critical_threshold:
|
|
|
|
self.check_result['critical'].append(host)
|
|
|
|
if host not in self.check_result['critical_item']:
|
|
|
|
self.check_result['critical_item'][host] = {'critical': [], 'bad': []}
|
|
|
|
# 将bad项目放置到critical中显示
|
|
|
|
if host in self.check_result['bad']:
|
|
|
|
self.check_result['critical_item'][host]['bad'].extend(self.check_result['bad_item'][host]['bad'])
|
|
|
|
del self.check_result['bad_item'][host]
|
|
|
|
self.check_result['critical_item'][host]['critical'].append(item)
|
|
|
|
|
|
|
|
elif now >= self.bad_threshold:
|
|
|
|
if host in self.check_result['critical']:
|
|
|
|
self.check_result['critical_item'][host]['bad'].append(item)
|
|
|
|
return
|
|
|
|
self.check_result['bad'].append(host)
|
|
|
|
if host not in self.check_result['bad_item']:
|
|
|
|
self.check_result['bad_item'][host] = {'bad': []}
|
|
|
|
self.check_result['bad_item'][host]['bad'].append(item)
|
|
|
|
else:
|
|
|
|
if host in self.check_result['critical'] or host in self.check_result['bad']:
|
|
|
|
return
|
|
|
|
self.check_result['ok'].append(host)
|
|
|
|
|
|
|
|
def update_host(self, hostname, key_values, overwrite=True):
|
|
|
|
"""
|
|
|
|
Update a hosts information. This is called by various collectors such
|
|
|
|
as the ansible setup module output and the hosts parser to add
|
|
|
|
informatio to a host. It does some deep inspection to make sure nested
|
|
|
|
information can be updated.
|
|
|
|
"""
|
|
|
|
default_empty_host = {
|
|
|
|
'name': hostname,
|
|
|
|
}
|
|
|
|
host_info = self.host_data.get(hostname, default_empty_host)
|
|
|
|
deepupdate(host_info, key_values, overwrite=overwrite)
|
|
|
|
self.host_data[hostname] = host_info
|
|
|
|
|
|
|
|
def get_check_result(self):
|
|
|
|
for key, host in self.host_data.items():
|
|
|
|
print('[Check] %s' % key)
|
|
|
|
if 'ansible_facts' not in host:
|
|
|
|
self.check_result['summary']['error'] += 1
|
|
|
|
self.check_result['error_item'][key] = {'msg': host['msg']}
|
|
|
|
continue
|
|
|
|
|
|
|
|
usedutilization = {
|
|
|
|
'os_time': '',
|
|
|
|
'mem': '',
|
|
|
|
'swap': '',
|
|
|
|
'disk': []
|
|
|
|
}
|
|
|
|
|
|
|
|
iso8601 = host['ansible_facts']['ansible_date_time'].get('iso8601', None)
|
|
|
|
ansible_memtotal_mb = host['ansible_facts'].get('ansible_memtotal_mb', 0)
|
|
|
|
ansible_memfree_mb = host['ansible_facts'].get('ansible_memfree_mb', 0)
|
|
|
|
ansible_swaptotal_mb = host['ansible_facts'].get('ansible_swaptotal_mb', 0)
|
|
|
|
ansible_swapfree_mb = host['ansible_facts'].get('ansible_swapfree_mb', 0)
|
|
|
|
|
|
|
|
if ansible_memtotal_mb != 0:
|
|
|
|
usedutilization['mem'] = int(
|
|
|
|
(ansible_memtotal_mb - ansible_memfree_mb) / ansible_memtotal_mb * 10000) / 100
|
|
|
|
else:
|
|
|
|
usedutilization['mem'] = 0
|
|
|
|
|
|
|
|
if ansible_swaptotal_mb != 0:
|
|
|
|
usedutilization['swap'] = int(
|
|
|
|
(ansible_swaptotal_mb - ansible_swapfree_mb) / ansible_swaptotal_mb * 10000) / 100
|
|
|
|
else:
|
|
|
|
usedutilization['swap'] = 0
|
|
|
|
|
|
|
|
usedutilization['os_time'] = iso8601
|
|
|
|
|
|
|
|
for disk in host['ansible_facts'].get('ansible_mounts', []):
|
|
|
|
mount = disk.get('mount', '')
|
|
|
|
fstype = disk.get('fstype', '')
|
|
|
|
if 'containers' in mount or 'iso9660' in fstype:
|
|
|
|
continue
|
|
|
|
size_total = disk.get('size_total', 0)
|
|
|
|
size_available = disk.get('size_available', 0)
|
|
|
|
block_used = disk.get('block_used', 0)
|
|
|
|
block_total = disk.get('block_total', 0)
|
|
|
|
inode_total = disk.get('inode_total', 0)
|
|
|
|
inode_used = disk.get('inode_used', 0)
|
|
|
|
|
|
|
|
size_usedutilization = 0
|
|
|
|
block_usedutilization = 0
|
|
|
|
inode_usedutilization = 0
|
|
|
|
|
|
|
|
if size_total != 0:
|
|
|
|
size_usedutilization = int((size_total - size_available) / size_total * 10000) / 100
|
|
|
|
|
|
|
|
if block_total != 0:
|
|
|
|
block_usedutilization = int(block_used / block_total * 10000) / 100
|
|
|
|
|
|
|
|
if inode_total != 0:
|
|
|
|
inode_usedutilization = int(inode_used / inode_total * 10000) / 100
|
|
|
|
|
|
|
|
usedutilization['disk'].append(
|
|
|
|
{'mount': mount, 'size': size_usedutilization, 'block': block_usedutilization,
|
|
|
|
'inode': inode_usedutilization})
|
|
|
|
|
|
|
|
self.check_usedutilization(key, 'mem', usedutilization['mem'])
|
|
|
|
self.check_usedutilization(key, 'swap', usedutilization['swap'])
|
|
|
|
|
|
|
|
for du in usedutilization['disk']:
|
|
|
|
self.check_usedutilization(key, 'mount_size_' + du['mount'], du['size'])
|
|
|
|
self.check_usedutilization(key, 'mount_block_' + du['mount'], du['block'])
|
|
|
|
self.check_usedutilization(key, 'mount_inode_' + du['mount'], du['inode'])
|
|
|
|
|
|
|
|
# self.check_time(key, 'time', iso8601)
|
|
|
|
self.host_data[key]['usedutilization'] = usedutilization
|
|
|
|
|
|
|
|
self.check_result['ok'] = sorted(
|
|
|
|
list(set(self.check_result['ok']).difference(set(self.check_result['critical']))))
|
|
|
|
self.check_result['ok'] = sorted(list(set(self.check_result['ok']).difference(set(self.check_result['bad']))))
|
|
|
|
self.check_result['bad'] = sorted(
|
|
|
|
list(set(self.check_result['bad']).difference(set(self.check_result['critical']))))
|
|
|
|
self.check_result['critical'] = sorted(set(self.check_result['critical']))
|
|
|
|
self.check_result['total'] = len(self.host_data.values())
|
|
|
|
|
|
|
|
self.check_result['summary']['ok'] = len(self.check_result['ok'])
|
|
|
|
self.check_result['summary']['bad'] = len(self.check_result['bad'])
|
|
|
|
self.check_result['summary']['critical'] = len(self.check_result['critical'])
|
|
|
|
self.check_result['summary']['total'] = len(self.host_data.values())
|
|
|
|
|
|
|
|
self.check_result['time'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
|
|
|
|
for ok_item in self.check_result['ok']:
|
|
|
|
self.check_result['ok_item'][ok_item] = {}
|
|
|
|
for info in self.default_host_info:
|
|
|
|
self.check_result['ok_item'][ok_item][info] = self.host_data[ok_item]['ansible_facts'][info]
|
|
|
|
self.check_result['ok_item'][ok_item]['usedutilization'] = self.host_data[ok_item]['usedutilization']
|
|
|
|
|
|
|
|
for bad_item in self.check_result['bad']:
|
|
|
|
for info in self.default_host_info:
|
|
|
|
self.check_result['bad_item'][bad_item][info] = self.host_data[bad_item]['ansible_facts'][info]
|
|
|
|
self.check_result['bad_item'][bad_item]['usedutilization'] = self.host_data[bad_item]['usedutilization']
|
|
|
|
|
|
|
|
for critical_item in self.check_result['critical']:
|
|
|
|
for info in self.default_host_info:
|
|
|
|
self.check_result['critical_item'][critical_item][info] = \
|
|
|
|
self.host_data[critical_item]['ansible_facts'][
|
|
|
|
info]
|
|
|
|
self.check_result['critical_item'][critical_item]['usedutilization'] = self.host_data[critical_item][
|
|
|
|
'usedutilization']
|
|
|
|
|
|
|
|
return self.check_result
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
# 定义基础数据
|
|
|
|
print('[Init] Set configuration')
|
|
|
|
current_path = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
now_date = datetime.datetime.now().strftime('%Y-%m-%d')
|
|
|
|
report_path = os.path.join(current_path, 'report', 'report-%s.html' % now_date)
|
|
|
|
template_path = os.path.join(current_path, 'templates')
|
|
|
|
# template_file = 'report.html'
|
|
|
|
template_file = 'report_cssinline.html'
|
|
|
|
|
|
|
|
# 设置fact目录
|
|
|
|
fact_dirs = [os.path.join(current_path, 'facts')]
|
|
|
|
|
|
|
|
# 获取检查结果
|
|
|
|
print('[Check] Get Result')
|
|
|
|
ansible = Ansible(fact_dirs=fact_dirs)
|
|
|
|
check_result = ansible.get_check_result()
|
|
|
|
|
|
|
|
# 生成报告
|
|
|
|
print('[Check] Generate report')
|
|
|
|
TemplateLoader = FileSystemLoader(searchpath=template_path)
|
|
|
|
TemplateEnv = Environment(loader=TemplateLoader)
|
|
|
|
template = TemplateEnv.get_template(template_file)
|
|
|
|
html = template.render(data=check_result)
|
|
|
|
|
|
|
|
# 存储报告
|
|
|
|
print('[Check] Save report')
|
|
|
|
with open(report_path, 'w', encoding='utf-8') as f:
|
|
|
|
f.write(html)
|
|
|
|
|
|
|
|
# 发送邮件
|
|
|
|
subject = 'System Check Report [%s]' % now_date
|
|
|
|
to_list = ['lework@ops.com']
|
|
|
|
|
|
|
|
mail_config = {
|
|
|
|
'mail_host': 'smtp.lework.com',
|
|
|
|
'mail_port': '465',
|
|
|
|
'mail_user': 'ops@lework.com',
|
|
|
|
'mail_pass': '123123'
|
|
|
|
}
|
|
|
|
|
|
|
|
send_mail(mail_config, to_list, subject, html)
|