📜 ⬆️ ⬇️

Monitor client PCs in Microsoft AD with Zabbix. Part 2 - Template, Scripts and LLD

Zabbix has quite large capabilities out of the box, but sometimes this may not be enough, and in this case it is possible to use a third-party script for event handling (Report problems to script). We’ll go back to the script itself a bit later, while I’ll describe only the main idea, so that it is clear what and why we add triggers to the description. The script parses the body of the letter and searches for the line MYparsBLOCK: funcname: if it finds it, it executes funcname (), if it does not find it, it simply sends an alert. It’s more reasonable to add this to the trigger description, so you need to add the default message - {TRIGGER.DESCRIPTION} to the Actions-Event source –Triggers-operations.

Cooking template


You need to create a monitoring template that will be attached to new hosts using the auto-registration rule. (Action-> Event Source-> Auto registration-> Link to templates: Win_monitor) I took as a basis the standard zabbix template for windows, as well as the APC Smart UPS Monitoring from somewhere in the open spaces, threw out of them everything superfluous and added what need me

Items
Agent ping
agent.ping
Average disk queue length
perf_counter [\ 234 (_Total) \ 1400]
Average disk read queue length
perf_counter [\ 234 (_Total) \ 1402]
Average disk write queue length
perf_counter [\ 234 (_Total) \ 1404]
CPU Model
wmi.get [ROOT \ cimv2, SELECT Name FROM Win32_Processor]
CPU utilization
perf_counter [\ 238 (_Total) \ 6]
APC Smart UPS Monitoring: Driver Caption
wmi.get [ROOT \ cimv2, SELECT Caption FROM Win32_PNPEntity WHERE PNPDeviceID LIKE '% VID_051D & PID_0002%' OR Service LIKE '% hidbatt%']
Free disk space on C:
vfs.fs.size [c:, free]
Free disk space on C: (percentage)
vfs.fs.size [c:, pfree]
Free memory
vm.memory.size [free]
Host name of zabbix_agentd running
agent.hostname
Mainboard Model
wmi.get [ROOT \ cimv2, SELECT Product FROM Win32_BaseBoard]
System information
system.uname
System uptime
system.uptime
Total disk space on C:
vfs.fs.size [c:, total]
Total memory
vm.memory.size [total]
Used disk space on C:
vfs.fs.size [c:, used]
APC Smart UPS Monitoring: Battery Life
battery.runtime
APC Smart UPS Monitoring: Battery Replacement Date
battery.mfr.date
APC Smart UPS Monitoring: Battery Charge
battery.charge
APC Smart UPS Monitoring: UPS Model
ups.model
APC Smart UPS Monitoring: Load
ups.load
APC Smart UPS Monitoring: Voltage (input)
input.voltage
APC Smart UPS Monitoring: Voltage (output)
output.voltage
APC Smart UPS Monitoring: UPS Status
ups.status
APC Smart UPS Monitoring: Beeper Status
ups.beeper.status
APC Smart UPS Monitoring: Battery Temperature
battery.temperature

Triggers
Free disk space is less than 1GB on volume C: {HOST.NAME}
{Win_monitor: vfs.fs.size [c:, free] .last (0)} <1073741824
Lack of free memory on {HOST.NAME}
{Win_monitor: vm.memory.size [free] .avg (30m)} <10000000
APC Smart UPS Monitoring: robot_ Data does not come from the UPS {HOST.NAME}
{Win_monitor: ups.status.str (Error)} = 1 and {Win_monitor: wmi.get [ROOT \ .imv2, SELECT Caption FROM Win32_PNPEntity WHERE PNPDeviceID LIKE '% VID_051D & PID_0002%' OR LIKE Service% Hidbatt% 'OR LIKE% HIDBatt% ") )}> 1
Zabbix agent on {HOST.NAME} is unreachable for 7 days
{Win_monitor: agent.ping.nodata (7d)} = 1
APC Smart UPS Monitoring: Battery not charging on {HOST.NAME}
{Win_monitor: battery.charge.max (# 120)} <90
APC Smart UPS Monitoring: Beeper off on {HOST.NAME}
{Win_monitor: ups.beeper.status.str (disabled)} = 1
APC Smart UPS Monitoring: Low battery life at {HOST.NAME}
{Win_monitor: battery.runtime.last (0)} <5 and {Win_monitor: ups.model.str (Smart)} = 1

I already wrote that the implementation of the ups monitoring was not as smooth as we would like. Oops constantly fall off, restarting the driver with the help of the devcon utility helps, therefore we add to the trigger (in the description) “robot_ No data comes from the UPS {HOST.NAME}” our block with the nutp function. Well, no one needs dead hosts in monitoring, because in the trigger “Zabbix agent on {HOST.NAME} is unreachable for 7 days” we add the function remove_offline, which will remove hosts from zabbix:

MYparsBLOCK:nutpt: HIP:{HOST.DNS} MYparsBLOCK:remove_offline: HID:{HOST.NAME} 

Low-level discovery


As for smarts, ordinary Items and Triggers will not suit us, since there may be different numbers of smarts on different machines. In zabbix, it is possible to make item and trigger prototype, which will be created for the list of objects obtained using low-level discovery rules, you can read more here . In order for the rule to work, we need to write a script / application that will issue a list of hard drives in a special JSON format at startup. At first I made a script on powershell, but on the part of the machines the script periodically did not have time to execute in 30 seconds, due to the fact that powershell itself was initialized for a very long time. I had to give up powershell and make an exe application in c # (I don't know it, but it seemed simple enough to rewrite the script). An application using smartctl gets a list of hdd, removes duplicate (by serial number) and displays in the format we need.
')
hddscan.cs
 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Diagnostics; using System.Text.RegularExpressions; namespace hdd_scan { class Program { static string[] smartctl(string arg) { Process p = new Process(); p.StartInfo.FileName = "C:\\Program Files\\Zabbix\\extra\\smart\\smartctl.exe"; p.StartInfo.Arguments = arg; p.StartInfo.UseShellExecute = false; p.StartInfo.RedirectStandardOutput = true; p.Start(); string output = p.StandardOutput.ReadToEnd(); string[] list = output.Split('\n'); p.WaitForExit(); return list; } static void Main(string[] args) { try { string[] hddlist = smartctl("--scan"); Dictionary<string, string> psarr = new Dictionary<string, string>(); string pattern = @"^(?<1>\/[\w]+)\/(?<xer>[\S]+)\s"; foreach (string hdd in hddlist) { var match = Regex.Match(hdd, pattern); if (match.Success) { string shdd = match.Groups["xer"].Value; string[] tmp = smartctl("-a " + shdd); foreach (string line in tmp) { if (line.Contains("Serial") == true) { string[] serials = Regex.Split(line, @"^Serial\sNumber\:\s+"); if (serials.Length < 2) continue; string serial = serials[1]; if (!psarr.ContainsValue(serial)) { psarr.Add(shdd, serial); } } } } } //Starting output int cnt = 0; Console.WriteLine("{\n"); Console.WriteLine("\t\"data\":[\n\n"); foreach (KeyValuePair<string, string> kvp in psarr) { string[] flist = smartctl("-a "+kvp.Key); string checkstring = "A mandatory SMART command failed: exiting. To continue, add one or more '-T permissive' options."; // bool test= false; for (int i = 0; i < flist.Length; i++) { if (flist[i].Contains(checkstring)) { test = true; } } if (!test) { cnt++; if (cnt > 1) { Console.WriteLine("\t,\n"); } Console.WriteLine("\t{\n"); Console.WriteLine("\t\t\"{{#HDDNAME}}\":\"{0}\"\n", kvp.Key); Console.WriteLine("\t}\n"); } } Console.WriteLine("\n\t]\n"); Console.WriteLine("}\n"); } catch (Exception ex) { Console.WriteLine(ex.Message); } } } } 


Yes, smart machines may be disabled on some machines, so we will add another firstrun function, and place it in the message of the autoregistration rule Action-> Event Source-> Auto registration, add the item Send message to ... via script, place in the body:

 MYparsBLOCK:firstrun: HIP:{HOST.IP} 

In the template, create a discovey rule - smart.discovery.

Item prototypes
smart _ {# HDDNAME} _CRC_Error_Count
smart [{# HDDNAME}, crc]
smart _ {# HDDNAME} _Current_Pending_Sector
smart [{# HDDNAME}, pend]
smart _ {# HDDNAME} _Health_Status
smart [{# HDDNAME}, health]
smart _ {# HDDNAME} _Model
smart [{# HDDNAME}, model]
smart _ {# HDDNAME} _Reallocated_Sector_Ct
smart [{# HDDNAME}, realloc]
smart _ {# HDDNAME} _Temperature
smart [{# HDDNAME}, temp]

Triggers prototypes
HDD: Current_Pending_Sector on {#HDDNAME} {HOST.NAME} over 5
{Win_monitor: smart [{# HDDNAME}, pend] .last ()}> 5
HDD: Reallocated_Sector_Ct to {#HDDNAME} {HOST.NAME} is greater than 5
{Win_monitor: smart [{# HDDNAME}, realloc] .last ()}> 5
HDD: HDD temperature is above 55 degrees at {#HDDNAME} {HOST.NAME}
{Win_monitor: smart [{# HDDNAME}, temp] .last ()}> 55
HDD: CRC_Error_Count growth has been registered to {#HDDNAME} {HOST.NAME}
{Win_monitor: smart [{# HDDNAME}, crc] .change ()}> 0
HDD: Current_Pending_Sector growth is fixed at {#HDDNAME} {HOST.NAME}
{Win_monitor: smart [{# HDDNAME}, pend] .change ()}> 0 and {Win_monitor: smart [{# HDDNAME}, pend] .last ()}> 6
HDD: Reallocated_Sector_Ct growth was recorded at {#HDDNAME} {HOST.NAME}
{Win_monitor: smart [{# HDDNAME}, realloc] .change ()}> 0 and {Win_monitor: smart [{# HDDNAME}, realloc] .last ()}> 6

In the description of prototypes we add the hddsmart function, it will add the HDD model to the body of the trigger message, so that it is clear what kind of hard disk it is, because smartctl uses sda, sdb, etc. as the name

 MYparsBLOCK:hddsmart: HIP:{HOST.DNS}:KKEY:smart[{#HDDNAME},model] 

The agent does not understand most of the parameters, so it is imperative to register all UserParameter in the client configuration.

UserParameter
UserParameter=battery.charge,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost battery.charge
UserParameter=battery.charge.low,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost battery.charge.low
UserParameter=battery.charge.warning,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost battery.charge.warning
UserParameter=battery.mfr.date,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost battery.mfr.date
UserParameter=battery.runtime,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost battery.runtime
UserParameter=battery.runtime.low,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost battery.runtime.low
UserParameter=battery.temperature,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost battery.temperature
UserParameter=battery.type,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost battery.type
UserParameter=battery.voltage,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost battery.voltage
UserParameter=battery.voltage.nominal,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost battery.voltage.nominal
UserParameter=input.sensitivity,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost input.sensitivity
UserParameter=input.transfer.high,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost input.transfer.high
UserParameter=input.transfer.low,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost input.transfer.low
UserParameter=input.voltage,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost input.voltage
UserParameter=output.current,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost output.current
UserParameter=output.frequency,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost output.frequency
UserParameter=output.voltage,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost output.voltage
UserParameter=output.voltage.nominal,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost output.voltage.nominal
UserParameter=ups.beeper.status,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost ups.beeper.status
UserParameter=ups.delay.shutdown,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost ups.delay.shutdown
UserParameter=ups.delay.start,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost ups.delay.start
UserParameter=ups.firmware,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost ups.firmware
UserParameter=ups.firmware.aux,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost ups.firmware.aux
UserParameter=ups.load,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost ups.load
UserParameter=ups.mfr,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost ups.mfr
UserParameter=ups.mfr.date,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost ups.mfr.date
UserParameter=ups.model,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost ups.model
UserParameter=ups.productid,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost ups.productid
UserParameter=ups.serial,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost ups.serial
UserParameter=ups.status,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost ups.status
UserParameter=ups.test.result,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost ups.test.result
UserParameter=ups.timer.reboot,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost ups.timer.reboot
UserParameter=ups.timer.shutdown,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost ups.timer.shutdown
UserParameter=ups.timer.start,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost ups.timer.start
UserParameter=ups.vendorid,"c:\Program Files (x86)\NUT\bin\upsc.exe" primary@localhost ups.timer.vendorid
UserParameter=smart[*],"C:\Program Files\Zabbix\cmd\smart.cmd" "$1" $2
UserParameter=smart.discovery, "C:\Program Files\Zabbix\cmd\hdd_scan.exe"

smart.cmd
 @echo off rem use smart.cmd <disk> < parameter> smart.cmd sda health cd "C:\Program Files\Zabbix\cmd" if %2==health ("C:\Program Files\Zabbix\extra\smart\smartctl.exe" -H %1 | grep result | awk "{print $6}") if %2==model ("C:\Program Files\Zabbix\extra\smart\smartctl.exe" -i %1 | grep "Device Model" | awk -F"Device Model:" "{print $2}") if %2==realloc ("C:\Program Files\Zabbix\extra\smart\smartctl.exe" --attributes %1 | grep Reallocated_S | awk "{print $10}") if %2==crc ("C:\Program Files\Zabbix\extra\smart\smartctl.exe" --attributes %1 | grep CRC | awk "{print $10}") if %2==pend ("C:\Program Files\Zabbix\extra\smart\smartctl.exe" --attributes %1 | grep Pend | awk "{print $10}") if %2==temp ("C:\Program Files\Zabbix\extra\smart\smartctl.exe" --attributes %1 | grep Temperature_Celsius | awk "{print $10}") 


Alert script


Actually the script itself, which will send letters and perform our functions:

 #!/usr/bin/env python # -*- coding: utf-8 -*- #/var/lib/zabbixsrv/alertscripts/mail.py import string import re import subprocess import sys import time import os #    ,  ,    def send_mail(recipient, subject, body): import smtplib from email.MIMEText import MIMEText from email.Header import Header from email.Utils import formatdate encoding='utf-8' SMTP_SERVER = 'smtp' SENDER_NAME = u'Zabbix Alert' session = None msg = MIMEText(body, 'plain', encoding) msg['Subject'] = Header(subject, encoding) msg['From'] = Header(SENDER_NAME, encoding) msg['To'] = recipient msg['Date'] = formatdate() try: session = smtplib.SMTP(SMTP_SERVER) session.sendmail(SENDER_NAME, recipient, msg.as_string()) except Exception as e: raise e finally: # close session if session: session.quit() # Zabbix     ,   ,     .(  ,   ) def daemonize (stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): try: pid = os.fork() if pid > 0: sys.exit(0) except OSError, e: sys.stderr.write("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror)) sys.exit(1) os.chdir("/") os.umask(0) os.setsid() try: pid = os.fork() if pid > 0: sys.exit(0) except OSError, e: sys.stderr.write("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror)) sys.exit(1) for f in sys.stdout, sys.stderr: f.flush() si = file(stdin, 'r') so = file(stdout, 'a+') se = file(stderr, 'a+', 0) os.dup2(si.fileno(), sys.stdin.fileno()) os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(se.fileno(), sys.stderr.fileno()) #     def hddsmart(): m=re.search('MYparsBLOCK\:\S+\:\s+HIP\:(?P<hostip>\S+)\:KKEY\:(?P<kkey>\S+)',a3) hostip,kkey= m.group('hostip'),m.group('kkey') p = subprocess.Popen('zabbix_get -s '+hostip+' -k '+kkey, shell=True,stdout=subprocess.PIPE) bb = a3[0:string.find(a3,'MYparsBLOCK')] + 'HDD: ' + p.stdout.read() send_mail(sys.argv[1],a2,bb) #    .    api    def remove_offline(): if 'PROBLEM:' in a2: m=re.search('MYparsBLOCK\:\S+\:\s+HID\:(?P<hostid>\S+)',a3) hostid = m.group('hostid') + '\n' hidf=open('/var/log/zabbixsrv/2del_ids', 'a') hidf.write(hostid) hidf.close send_mail(sys.argv[1],a2,a3[0:string.find(a3,'MYparsBLOCK')]) # ,       .      microsoft devcon. def nutpt(): if 'PROBLEM:' in a2: m=re.search('MYparsBLOCK\:\S+\:\s+HIP\:(?P<hostip>\S+)',a3) hostip = m.group('hostip') log = '' i = 0 while i < 5: p = subprocess.Popen("""zabbix_get -s %s -k 'system.run[net stop "Network UPS Tools"]'"""%(hostip), shell=True,stdout=subprocess.PIPE) log +=p.stdout.read() time.sleep(10) p = subprocess.Popen("""zabbix_get -s %s -k system.run['cd "C:\Program Files\Zabbix\cmd\"&devcon.exe restart USB\VID_051D*']"""%(hostip), shell=True,stdout=subprocess.PIPE) log +=p.stdout.read() time.sleep(30) p = subprocess.Popen("""zabbix_get -s %s -k 'system.run[net start "Network UPS Tools"]'"""%(hostip), shell=True,stdout=subprocess.PIPE) log +=p.stdout.read() i += 1 p = subprocess.Popen("""zabbix_get -s %s -k 'ups.status'"""%(hostip), shell=True,stdout=subprocess.PIPE) if 'Error' not in p.stdout.read(): i = 8 if i <> 8: send_mail(sys.argv[1],a2,log) #    .     smart   smartctl.exe --scan-open def firstrun(): m=re.search('MYparsBLOCK\:\S+\:\s+HIP\:(?P<hostip>\S+)',a3) hostip = m.group('hostip') p = subprocess.Popen("""zabbix_get -s %s -k system.run['cd "C:\Program Files\Zabbix\extra\smart\"&smartctl.exe --scan-open']"""%(hostip), shell=True,stdout=subprocess.PIPE) log = p.stdout.read() send_mail(sys.argv[1],a2,log) daemonize(stdout='/var/log/zabbixsrv/script_out.log', stderr='/var/log/zabbixsrv/script_err.log') try: a1,a2,a3 = sys.argv[1],sys.argv[2],sys.argv[3] #debug(      ) #os.system('echo "' + a1+' '+a2+' '+a3 +'" >> /var/log/zabbixsrv/script_dbg.log') if 'MYparsBLOCK' in a3: eval(re.search('MYparsBLOCK\:(?P<myfunc>\S+)\:',a3).group('myfunc'))() #      else: send_mail(sys.argv[1],a2,a3) except: #print sys.exc_info() send_mail('admin@domain.local', 'Error in script', str(sys.exc_info())) 

Keep in mind that if there are several recipients, then the function will be executed several times. For some reason this is relevant (for example, the HDD model), and for some reason it can even be harmful, so this must be taken into account when setting up Actions.

Script to remove inactive hosts


 #!/usr/bin/python # import os from pyzabbix import ZabbixAPI, ZabbixAPIException try: os.rename ('/var/log/zabbixsrv/2del_ids','/var/log/zabbixsrv/klist_pr') except: pass user='apirobot' pwd='*******' url = 'https://127.0.0.1/zabbix/' zh = ZabbixAPI(url) zh.session.verify = False zh.login(user=user, password=pwd) f = open('/var/log/zabbixsrv/klist_pr') for hnm in f: try: hid = zh.host.get(filter={"host":hnm.replace('\n','')},output=['hostid'])[0]['hostid'] #zh.host.delete(hostid = hid) - API change zh.host.delete(int(hid)) except: pass f.close() os.remove('/var/log/zabbixsrv/klist_pr') 

To use the script, you need an account with rights to delete machines. I run the script on the crown under a different account, because here the password is stored in clear text.

We connect agents by dns-name


By default, agents are registered so that zabbix connects to them via ip. It does not suit me, so we are writing a script that will fix it, and at the same time it will report lookup problems. I took some script from pyzabbix examples for the basis and redid it a bit.

use_fqdn.py
 #!/usr/bin/python # # -*- coding: utf-8 -*- import socket from getpass import getpass from pyzabbix import ZabbixAPI, ZabbixAPIException zapi = ZabbixAPI(server='https://127.0.0.1/zabbix/') zapi.session.verify = False zapi.login('apirobot', '*******') body = '' err = '' def send_mail(recipient, subject, body): import smtplib from email.MIMEText import MIMEText from email.Header import Header from email.Utils import formatdate encoding='utf-8' SMTP_SERVER = 'smtp' SENDER_NAME = u'zabbix@domain.local' MAIL_ACCOUNT = 'zabbix@domain.local' session = None msg = MIMEText(body, 'plain', encoding) msg['Subject'] = Header(subject, encoding) msg['From'] = Header(SENDER_NAME, encoding) msg['To'] = recipient msg['Date'] = formatdate() try: session = smtplib.SMTP(SMTP_SERVER) session.sendmail(MAIL_ACCOUNT, recipient, msg.as_string()) except Exception as e: raise e finally: # close session if session: session.quit() # Loop through all hosts interfaces, getting only "main" interfaces of type "agent" for h in zapi.hostinterface.get(output=["dns","ip","useip"],selectHosts=["host"],filter={"main":1,"type":1}): #print h # Make sure the hosts are named according to their FQDN # if len(h['dns']) == 0: try: zapi.hostinterface.update(interfaceid=h['interfaceid'], dns = socket.gethostbyaddr(h['hosts'][0]['host'])[0]) except: body += ('FQDN_UPD_ERR: ' + h['hosts'][0]['host']) + '\n' try: a = socket.gethostbyaddr(h['hosts'][0]['host'])[2][0] b = socket.gethostbyaddr(h['dns'])[2][0] if (a != b): body += ('Warning: %s has dns "%s"' % (h['hosts'][0]['host'], h['dns'])) + '\n' except: body += ('DNS_LOOKUP_ERR: ' + h['hosts'][0]['host']) + '\n' # Make sure they are using hostnames to connect rather than IPs (could be also filtered in the get request) if h['useip'] == '1': body += ('%s is using IP instead of hostname. Fixing.' % h['hosts'][0]['host']) + '\n' try: zapi.hostinterface.update(interfaceid=h['interfaceid'], useip=0) except ZabbixAPIException as e: #print(e) err += str(e)+'\n' err += '\n' continue body += '\nZabbix Errors:' + err if len(body) > 16: send_mail('admin@domain.local','check agents',body) 

P.S


»All scripts, template and other necessary files are posted on github .

» Monitor client PCs in Microsoft AD with Zabbix. Part 1 - Auto Install

Source: https://habr.com/ru/post/312146/


All Articles