├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── configuration-question.md │ └── feature_request.md ├── .gitignore ├── BSD ├── mini_ipmi_bsdcpu.py ├── sudoers.d │ └── zabbix └── zabbix_agentd.conf.d │ └── userparameter_mini-ipmi2.conf ├── KEYS.md ├── Linux ├── mini_ipmi_lmsensors.py ├── sudoers.d │ └── zabbix └── zabbix_agentd.d │ └── userparameter_mini-ipmi2.conf ├── README.md ├── Template_mini-IPMI_v2.xml ├── UNLICENSE ├── Win ├── mini_ipmi_ohmr.py └── zabbix_agentd.d │ └── userparameter_mini-ipmi2.conf ├── mini_ipmi_smartctl.py ├── screenshots ├── mini-IPMI-graph.png ├── mini-IPMI-triggers-config.png ├── mini-IPMI-triggers-cpu.png ├── mini-IPMI-triggers-disk.png ├── python-installation1.png └── python-installation2.png └── sender_wrapper.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Provide all outputs described in [Testing](https://github.com/nobodysu/zabbix-mini-IPMI#testing) step** 23 | Serial numbers should be replaced with X_SERIAL_X. 24 | 25 | **Please complete the following information:** 26 | - OS: [e.g. Debian] 27 | - Zabbix server version: [e.g. 3.0] 28 | - Active or passive check: [e.g. Passive] 29 | - Using zabbix proxy: [e.g. No proxy] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/configuration-question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Configuration question 3 | about: Ask a question about configuration problem 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the problem** 11 | A clear and concise description of what the problem is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Provide all outputs described in [Testing](https://github.com/nobodysu/zabbix-mini-IPMI#testing) step** 23 | Serial numbers should be replaced with X_SERIAL_X. 24 | 25 | **Please complete the following information:** 26 | - OS: [e.g. Debian] 27 | - Zabbix server version: [e.g. 3.0] 28 | - Active or passive check: [e.g. Passive] 29 | - Using zabbix proxy: [e.g. No proxy] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | -------------------------------------------------------------------------------- /BSD/mini_ipmi_bsdcpu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Installation instructions: https://github.com/nobodysu/zabbix-mini-IPMI ## 4 | 5 | BIN_PATH = r'/sbin/sysctl' 6 | 7 | # path to second send script 8 | SENDER_WRAPPER_PATH = r'/usr/local/etc/zabbix/scripts/sender_wrapper.py' 9 | 10 | # path to zabbix agent configuration file 11 | AGENT_CONF_PATH = r'/usr/local/etc/zabbix3/zabbix_agentd.conf' 12 | 13 | SENDER_PATH = r'zabbix_sender' 14 | #SENDER_PATH = r'/usr/bin/zabbix_sender' 15 | 16 | DELAY = '50' # how long the script must wait between LLD and sending, increase if data received late (does not affect windows) 17 | # this setting MUST be lower than 'Update interval' in discovery rule 18 | TJMAX = '70' 19 | 20 | ## End of configuration ## 21 | 22 | import sys 23 | import subprocess 24 | import re 25 | from sender_wrapper import (readConfig, processData, fail_ifNot_Py3) 26 | 27 | HOST = sys.argv[2] 28 | 29 | 30 | def getOutput(binPath_): 31 | 32 | p = None 33 | try: 34 | p = subprocess.check_output([binPath_, 'dev.cpu'], universal_newlines=True) 35 | except OSError as e: 36 | if e.args[0] == 2: 37 | error = 'OS_NOCMD' 38 | else: 39 | error = 'OS_ERROR' 40 | if sys.argv[1] == 'getverb': 41 | raise 42 | except Exception as e: 43 | error = 'UNKNOWN_EXC_ERROR' 44 | 45 | if sys.argv[1] == 'getverb': 46 | raise 47 | 48 | try: 49 | p = e.output 50 | except: 51 | pass 52 | else: 53 | error = 'CONFIGURED' 54 | 55 | return error, p 56 | 57 | 58 | def getCpuData(pOut_): 59 | '''Can work unexpectedly with multiple CPUs.''' 60 | sender = [] 61 | json = [] 62 | 63 | tempRe = re.findall(r'dev\.cpu\.(\d+)\.temperature:\s+(\d+)', pOut_, re.I) 64 | if tempRe: 65 | error = None 66 | json.append({'{#CPU}':'0'}) 67 | 68 | allTemps = [] 69 | for num, val in tempRe: 70 | allTemps.append(val) 71 | sender.append('"%s" mini.cpu.temp[cpu0,core%s] "%s"' % (HOST, num, val)) 72 | json.append({'{#CPUC}':'0', '{#CORE}':num}) 73 | 74 | sender.append('"%s" mini.cpu.info[cpu0,TjMax] "%s"' % (HOST, TJMAX)) 75 | sender.append('"%s" mini.cpu.temp[cpu0,MAX] "%s"' % (HOST, max(allTemps))) 76 | sender.append('"%s" mini.cpu.temp[MAX] "%s"' % (HOST, max(allTemps))) 77 | 78 | else: 79 | error = 'NOCPUTEMPS' 80 | 81 | return sender, json, error 82 | 83 | 84 | if __name__ == '__main__': 85 | 86 | fail_ifNot_Py3() 87 | 88 | senderData = [] 89 | jsonData = [] 90 | 91 | p_Output = getOutput(BIN_PATH) 92 | pRunStatus = p_Output[0] 93 | pOut = p_Output[1] 94 | 95 | errors = None 96 | if pOut: 97 | getCpuData_Out = getCpuData(pOut) 98 | cpuErrors = getCpuData_Out[2] 99 | senderData.extend(getCpuData_Out[0]) 100 | jsonData.extend(getCpuData_Out[1]) 101 | if cpuErrors: 102 | errors = 'cpu_err' 103 | senderData.append('"%s" mini.cpu.info[ConfigStatus] "%s"' % (HOST, cpuErrors)) 104 | 105 | if not errors: 106 | senderData.append('"%s" mini.cpu.info[ConfigStatus] "%s"' % (HOST, pRunStatus)) # OS_NOCMD, OS_ERROR, UNKNOWN_EXC_ERROR, CONFIGURED 107 | 108 | link = r'https://github.com/nobodysu/zabbix-mini-IPMI/issues' 109 | sendStatusKey = 'mini.cpu.info[SendStatus]' 110 | processData(senderData, jsonData, AGENT_CONF_PATH, SENDER_WRAPPER_PATH, SENDER_PATH, DELAY, HOST, link, sendStatusKey) 111 | 112 | -------------------------------------------------------------------------------- /BSD/sudoers.d/zabbix: -------------------------------------------------------------------------------- 1 | #Defaults:zabbix !requiretty # Older sudo 2 | 3 | zabbix ALL=NOPASSWD: /usr/sbin/smartctl, /usr/bin/smartctl, /usr/local/sbin/smartctl, /sbin/smartctl, /bin/smartctl 4 | 5 | -------------------------------------------------------------------------------- /BSD/zabbix_agentd.conf.d/userparameter_mini-ipmi2.conf: -------------------------------------------------------------------------------- 1 | UserParameter=mini.disktemp.discovery[*], PATH=/usr/local/sbin:/usr/local/bin "/usr/local/etc/zabbix/scripts/mini_ipmi_smartctl.py" "$1" "$2" 2 | UserParameter=mini.cputemp.discovery[*], PATH=/usr/local/sbin:/usr/local/bin "/usr/local/etc/zabbix/scripts/mini_ipmi_bsdcpu.py" "$1" "$2" 3 | -------------------------------------------------------------------------------- /KEYS.md: -------------------------------------------------------------------------------- 1 | | Key | Supported in | 2 | | ------------------------------------------------------------ | ------------------- | 3 | | mini.brd.info[BIOSvendor] | mini_ipmi_ohmr.py | 4 | | mini.brd.info[BIOSversion] | mini_ipmi_ohmr.py | 5 | |mini.brd.info[MainboardManufacturer]|mini_ipmi_ohmr.py| 6 | |mini.brd.info[MainboardName]|mini_ipmi_ohmr.py| 7 | |mini.brd.info[MainboardVersion]|mini_ipmi_ohmr.py| 8 | |mini.brd.info[SMBIOSversion]|mini_ipmi_ohmr.py| 9 | |mini.brd.info[vcoreMax]|mini_ipmi_ohmr.py, mini_ipmi_lmsensors.py| 10 | |mini.brd.info[vttMax]|mini_ipmi_ohmr.py, mini_ipmi_lmsensors.py| 11 | |mini.brd.temp[MAX]|mini_ipmi_ohmr.py, mini_ipmi_lmsensors.py| 12 | |mini.brd.vlt[_N_]|mini_ipmi_ohmr.py, mini_ipmi_lmsensors.py| 13 | |mini.cpu.info[ConfigStatus]| mini_ipmi_ohmr.py, mini_ipmi_lmsensors.py, mini_ipmi_bsdcpu.py| 14 | |mini.cpu.temp[MAX]|mini_ipmi_ohmr.py, mini_ipmi_lmsensors.py, mini_ipmi_bsdcpu.py| 15 | |mini.gpu.temp[MAX]|mini_ipmi_ohmr.py, mini_ipmi_lmsensors.py| 16 | |mini.info[OHMRver]| mini_ipmi_ohmr.py| 17 | |mini.brd.fan[{#BRDFANNUM},rpm]|mini_ipmi_ohmr.py, mini_ipmi_lmsensors.py| 18 | |mini.brd.temp[{#BRDTEMPNUM}]|mini_ipmi_ohmr.py, mini_ipmi_lmsensors.py| 19 | |mini.brd.vlt[{#P5V}]|mini_ipmi_lmsensors.py| 20 | |mini.brd.vlt[{#P12V}]|mini_ipmi_lmsensors.py| 21 | |mini.brd.vlt[{#P33V}]|mini_ipmi_lmsensors.py| 22 | |mini.brd.vlt[{#VAVCC}]|mini_ipmi_ohmr.py, mini_ipmi_lmsensors.py| 23 | |mini.brd.vlt[{#VBAT}]|mini_ipmi_ohmr.py, mini_ipmi_lmsensors.py| | 24 | |mini.brd.vlt[{#VCC3V}]|mini_ipmi_ohmr.py, mini_ipmi_lmsensors.py| 25 | |mini.brd.vlt[{#VCORE}]|mini_ipmi_ohmr.py, mini_ipmi_lmsensors.py| 26 | |mini.brd.vlt[{#VSB3V}]|mini_ipmi_ohmr.py, mini_ipmi_lmsensors.py| 27 | |mini.brd.vlt[{#VTT}]|mini_ipmi_ohmr.py, mini_ipmi_lmsensors.py| 28 | |mini.cpu.info[cpu{#CPU},CPUstatus]|mini_ipmi_ohmr.py| 29 | |mini.cpu.info[cpu{#CPU},ID]|mini_ipmi_ohmr.py, mini_ipmi_lmsensors.py| 30 | | mini.cpu.info[cpu{#CPU},TjMax]|mini_ipmi_ohmr.py, mini_ipmi_lmsensors.py| 31 | |mini.cpu.temp[cpu{#CPUC},core{#CORE}]|mini_ipmi_ohmr.py, mini_ipmi_lmsensors.py, mini_ipmi_bsdcpu.py| 32 | |mini.cpu.temp[cpu{#CPU},MAX]|mini_ipmi_ohmr.py, mini_ipmi_lmsensors.py, mini_ipmi_bsdcpu.py| 33 | | mini.gpu.fan[gpu{#GPUFAN},rpm]|mini_ipmi_ohmr.py| 34 | |mini.gpu.info[gpu{#GPU},GPUstatus]|mini_ipmi_ohmr.py| 35 | |mini.gpu.info[gpu{#GPU},ID]|mini_ipmi_ohmr.py, mini_ipmi_lmsensors.py| 36 | |mini.gpu.memory[gpu{#GPUMEM},free]|mini_ipmi_ohmr.py| 37 | |mini.gpu.memory[gpu{#GPUMEM},total]|mini_ipmi_ohmr.py| 38 | |mini.gpu.memory[gpu{#GPUMEM},used]|mini_ipmi_ohmr.py| 39 | |mini.gpu.temp[gpu{#GPUTEMP}]|mini_ipmi_ohmr.py, mini_ipmi_lmsensors.py| 40 | |mini.disk.info[ConfigStatus]|mini_ipmi_smartctl.py| 41 | |mini.disk.info[{#DISK},DriveStatus]|mini_ipmi_smartctl.py| 42 | |mini.disk.temp[{#DISK}]|mini_ipmi_smartctl.py| 43 | |mini.disk.temp[MAX]|mini_ipmi_smartctl.py| 44 | -------------------------------------------------------------------------------- /Linux/mini_ipmi_lmsensors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | BIN_PATH = r'sensors' # -u 4 | #BIN_PATH = r'/usr/bin/sensors' # if 'sensors' isn't in PATH 5 | 6 | # path to second send script 7 | SENDER_WRAPPER_PATH = r'/etc/zabbix/scripts/sender_wrapper.py' 8 | 9 | # path to zabbix agent configuration file 10 | AGENT_CONF_PATH = r'/etc/zabbix/zabbix_agentd.conf' 11 | 12 | SENDER_PATH = r'zabbix_sender' 13 | #SENDER_PATH = r'/usr/bin/zabbix_sender' 14 | 15 | FALLBACK_TJMAX = '70' 16 | 17 | # Following settings brings (almost) no overhead. True or False 18 | GATHER_VOLTAGES = True 19 | GATHER_BOARD_FANS = True 20 | GATHER_BOARD_TEMPS = True 21 | GATHER_GPU_DATA = True 22 | GATHER_CPU_DATA = True 23 | 24 | VOLTAGE_REGEXPS_KEYS_AND_JSONS = ( 25 | ('Vcore', 'cpuVcore', '{#VCORE}'), 26 | ('VBAT', 'VBat', '{#VBAT}'), 27 | ('3VSB|VSB3V|Standby \+3\.3V|3V_SB', 'VSB3V', '{#VSB3V}'), 28 | ('3VCC|VCC3V', 'VCC3V', '{#VCC3V}'), 29 | ('AVCC', 'AVCC', '{#AVCC}'), 30 | ('VTT', 'VTT', '{#VTT}'), 31 | ('\+3\.3 Voltage', 'p3.3V', '{#p3.3V}'), 32 | ('\+5 Voltage', 'p5V', '{#p5V}'), 33 | ('\+12 Voltage', 'p12V', '{#p12V}'), 34 | ) 35 | 36 | # re.I | re.M 37 | CORES_REGEXPS = ( 38 | ('Core(?:\s+)?(\d+):\n\s+temp\d+_input:\s+(\d+)'), 39 | ('Core(\d+)\s+Temp:\n\s+temp\d+_input:\s+(\d+)'), 40 | ('Tdie:\n\s+temp(\d+)_input:\s+(\d+)'), 41 | ('Tccd(\d+):\n\s+temp\d+_input:\s+(\d+)'), 42 | ('k\d+temp-pci-\w+\nAdapter:\s+PCI\s+adapter\ntemp(\d+):\n\s+temp\d+_input:\s+(\d+)'), 43 | ) 44 | 45 | IGNORED_SENSORS = ( 46 | ('nct6791-isa-0290', 'AUXTIN3'), # ignore 'AUXTIN3' on 'nct6791-isa-0290' 47 | ) 48 | 49 | DELAY = '50' # how long the script must wait between LLD and sending, increase if data received late (does not affect windows) 50 | # this setting MUST be lower than 'Update interval' in discovery rule 51 | 52 | ## End of configuration ## 53 | 54 | import sys 55 | import subprocess 56 | import re 57 | from sender_wrapper import (readConfig, processData, fail_ifNot_Py3, removeQuotes) 58 | 59 | HOST = sys.argv[2] 60 | 61 | 62 | def getOutput(binPath_): 63 | try: 64 | from subprocess import DEVNULL # for python versions greater than 3.3, inclusive 65 | except: 66 | import os 67 | DEVNULL = open(os.devnull, 'w') # for 3.0-3.2, inclusive 68 | 69 | p = None 70 | try: 71 | p = subprocess.check_output([binPath_, '-u'], universal_newlines=True, stderr=DEVNULL) 72 | except OSError as e: 73 | if e.args[0] == 2: 74 | error = 'OS_NOCMD' 75 | else: 76 | error = 'OS_ERROR' 77 | if sys.argv[1] == 'getverb': 78 | raise 79 | except Exception as e: 80 | error = 'UNKNOWN_EXC_ERROR' 81 | 82 | if sys.argv[1] == 'getverb': 83 | raise 84 | 85 | try: 86 | p = e.output 87 | except: 88 | pass 89 | else: 90 | error = 'CONFIGURED' 91 | 92 | if p: 93 | p = p.strip() 94 | p = p.split('\n\n') 95 | 96 | return error, p 97 | 98 | 99 | def getVoltages(pOut_): 100 | sender = [] 101 | json = [] 102 | 103 | for block in pOut_: 104 | if 'Adapter: PCI adapter' in block: # we dont need GPU voltages 105 | continue 106 | 107 | voltagesRe = re.findall(r'(.+):\n(?:\s+)?in(\d+)_input:\s+(\d+\.\d+)', block, re.I) 108 | if voltagesRe: 109 | for name, num, val in voltagesRe: 110 | 111 | for regexp, key, jsn in VOLTAGE_REGEXPS_KEYS_AND_JSONS: 112 | if re.search(regexp, name, re.I): 113 | sender.append('"%s" mini.brd.vlt[%s] "%s"' % (HOST, key, removeQuotes(val))) 114 | json.append({jsn:key}) 115 | 116 | sender.append('"%s" mini.brd.vlt[%s] "%s"' % (HOST, num, removeQuotes(val))) # static items for graph, could be duplicate 117 | 118 | break # as safe as possible 119 | 120 | return sender, json 121 | 122 | 123 | def getBoardFans(pOut_): 124 | 125 | sender = [] 126 | json = [] 127 | 128 | for i in pOut_: 129 | if 'Adapter: PCI adapter' in i: # we dont need GPU fans 130 | continue 131 | 132 | fans = re.findall(r'(.+):\n(?:\s+)?fan(\d+)_input:\s+(\d+)', i, re.I) 133 | if fans: 134 | for name, num, val in fans: 135 | # only create LLD when speed is not zero, BUT always send values including zero (prevents false triggering) 136 | sender.append('"%s" mini.brd.fan[%s,rpm] "%s"' % (HOST, num, val)) 137 | if val != '0': 138 | json.append({'{#BRDFANNAME}':name.strip(), '{#BRDFANNUM}':num}) 139 | 140 | break 141 | 142 | return sender, json 143 | 144 | 145 | def getBoardTemps(pOut_): 146 | 147 | sender = [] 148 | json = [] 149 | 150 | for i in pOut_: 151 | if 'Adapter: PCI adapter' in i: # we dont need GPU temps 152 | continue 153 | 154 | blockIdentRe = re.search(r'^.+', i) 155 | if blockIdentRe: 156 | blockIdent = blockIdentRe.group(0).strip() 157 | else: 158 | blockIdent = None 159 | 160 | temps = re.findall(r'((?:CPU|GPU|MB|M/B|AUX|Ambient|Other|SYS|Processor).+):\n(?:\s+)?temp(\d+)_input:\s+(\d+)', i, re.I) 161 | if temps: 162 | for name, num, val in temps: 163 | if isIgnoredMbSensor(blockIdent, name): 164 | continue 165 | 166 | sender.append('"%s" mini.brd.temp[%s] "%s"' % (HOST, num, val)) 167 | json.append({'{#BRDTEMPNAME}':name.strip(), '{#BRDTEMPNUM}':num}) 168 | 169 | break # unrelated blocks 170 | 171 | return sender, json 172 | 173 | 174 | def isIgnoredMbSensor(ident_, sensor_): 175 | 176 | result = False 177 | for ident, sensor in IGNORED_SENSORS: 178 | if (ident_ == ident and 179 | sensor_ == sensor): 180 | 181 | result = True 182 | break 183 | 184 | return result 185 | 186 | 187 | def getGpuData(pOut_): 188 | 189 | sender = [] 190 | json = [] 191 | 192 | gpuBlocks = -1 193 | allTemps = [] 194 | for i in pOut_: 195 | temp = re.search(r'(nouveau.+|nvidia.+|radeon.+)\n.+\n.+\n(?:\s+)?temp\d+_input:\s+(\d+)', i, re.I) 196 | if temp: 197 | gpuid = temp.group(1) 198 | val = temp.group(2) 199 | 200 | gpuBlocks += 1 201 | allTemps.append(int(val)) 202 | 203 | json.append({'{#GPU}':gpuBlocks}) 204 | sender.append('"%s" mini.gpu.info[gpu%s,ID] "%s"' % (HOST, gpuBlocks, removeQuotes(gpuid))) 205 | 206 | json.append({'{#GPUTEMP}':gpuBlocks}) 207 | sender.append('"%s" mini.gpu.temp[gpu%s] "%s"' % (HOST, gpuBlocks, val)) 208 | 209 | if gpuBlocks != -1: 210 | if allTemps: 211 | error = None 212 | sender.append('"%s" mini.gpu.temp[MAX] "%s"' % (HOST, max(allTemps))) 213 | else: 214 | error = 'NOGPUTEMPS' # unreachable 215 | else: 216 | error = 'NOGPUS' 217 | 218 | return sender, json, error 219 | 220 | 221 | def chooseCpuRegexp(block_): 222 | 223 | result = '' 224 | for regexp in CORES_REGEXPS: 225 | match = re.search(regexp, block_, re.I | re.M) 226 | if match: 227 | result = regexp 228 | 229 | return result 230 | 231 | 232 | def getCpuData(pOut_): 233 | '''Note: certain cores can pose as different blocks making them separate cpus in zabbix.''' 234 | sender = [] 235 | json = [] 236 | 237 | cpuBlocks = -1 # first cpu will be '0' 238 | allTemps = [] 239 | for block in pOut_: 240 | regexp = chooseCpuRegexp(block) 241 | 242 | coreTempsRe = re.findall(regexp, block, re.I | re.M) 243 | 244 | if (coreTempsRe and 245 | regexp): 246 | 247 | cpuBlocks += 1 # you need to be creative to parse lmsensors 248 | 249 | json.append({'{#CPU}':cpuBlocks}) 250 | sender.append('"%s" mini.cpu.info[cpu%s,ID] "%s"' % (HOST, cpuBlocks, removeQuotes(block.splitlines()[0]))) 251 | 252 | tempCrit = re.search(r'_crit:\s+(\d+)\.\d+', block, re.I) 253 | if tempCrit: 254 | tjMax = tempCrit.group(1) 255 | else: 256 | tjMax = FALLBACK_TJMAX 257 | 258 | sender.append('"%s" mini.cpu.info[cpu%s,TjMax] "%s"' % (HOST, cpuBlocks, tjMax)) 259 | 260 | cpuTemps = [] 261 | previousCore = None 262 | for num, val in coreTempsRe: 263 | if previousCore == num: 264 | continue # some cores have same number - ignore them 265 | previousCore = num 266 | 267 | cpuTemps.append(int(val)) 268 | allTemps.append(int(val)) 269 | sender.append('"%s" mini.cpu.temp[cpu%s,core%s] "%s"' % (HOST, cpuBlocks, num, val)) 270 | json.append({'{#CPUC}':cpuBlocks, '{#CORE}':num}) 271 | 272 | sender.append('"%s" mini.cpu.temp[cpu%s,MAX] "%s"' % (HOST, cpuBlocks, max(cpuTemps))) 273 | 274 | if cpuBlocks != -1: 275 | if allTemps: 276 | error = None 277 | sender.append('"%s" mini.cpu.temp[MAX] "%s"' % (HOST, max(allTemps))) 278 | else: 279 | error = 'NOCPUTEMPS' 280 | else: 281 | error = 'NOCPUS' 282 | 283 | return sender, json, error 284 | 285 | 286 | if __name__ == '__main__': 287 | 288 | fail_ifNot_Py3() 289 | 290 | senderData = [] 291 | jsonData = [] 292 | statusErrors = [] 293 | 294 | p_Output = getOutput(BIN_PATH) 295 | pRunStatus = p_Output[0] 296 | pOut = p_Output[1] 297 | 298 | if pOut: 299 | if GATHER_VOLTAGES: 300 | getVoltages_Out = getVoltages(pOut) 301 | senderData.extend(getVoltages_Out[0]) 302 | jsonData.extend(getVoltages_Out[1]) 303 | 304 | if GATHER_BOARD_FANS: 305 | getBoardFans_Out = getBoardFans(pOut) 306 | senderData.extend(getBoardFans_Out[0]) 307 | jsonData.extend(getBoardFans_Out[1]) 308 | 309 | if GATHER_BOARD_TEMPS: 310 | getBoardTemps_Out = getBoardTemps(pOut) 311 | senderData.extend(getBoardTemps_Out[0]) 312 | jsonData.extend(getBoardTemps_Out[1]) 313 | 314 | if GATHER_GPU_DATA: 315 | getGpuData_Out = getGpuData(pOut) 316 | gpuErrors = getGpuData_Out[2] 317 | senderData.extend(getGpuData_Out[0]) 318 | jsonData.extend(getGpuData_Out[1]) 319 | if gpuErrors: 320 | statusErrors.append(gpuErrors) # NOGPUS, NOGPUTEMPS 321 | 322 | if GATHER_CPU_DATA: 323 | getCpuData_Out = getCpuData(pOut) 324 | cpuErrors = getCpuData_Out[2] 325 | senderData.extend(getCpuData_Out[0]) 326 | jsonData.extend(getCpuData_Out[1]) 327 | if cpuErrors: 328 | statusErrors.append(cpuErrors) # NOCPUS, NOCPUTEMPS 329 | 330 | if statusErrors: 331 | errorsString = ', '.join(statusErrors).strip() 332 | senderData.append('"%s" mini.cpu.info[ConfigStatus] "%s"' % (HOST, errorsString)) 333 | else: 334 | senderData.append('"%s" mini.cpu.info[ConfigStatus] "%s"' % (HOST, pRunStatus)) # OS_NOCMD, OS_ERROR, UNKNOWN_EXC_ERROR, CONFIGURED 335 | 336 | link = r'https://github.com/nobody43/zabbix-mini-IPMI/issues' 337 | sendStatusKey = 'mini.cpu.info[SendStatus]' 338 | processData(senderData, jsonData, AGENT_CONF_PATH, SENDER_WRAPPER_PATH, SENDER_PATH, DELAY, HOST, link, sendStatusKey) 339 | 340 | -------------------------------------------------------------------------------- /Linux/sudoers.d/zabbix: -------------------------------------------------------------------------------- 1 | #Defaults:zabbix !requiretty # Older sudo 2 | 3 | zabbix ALL=NOPASSWD: /usr/sbin/smartctl, /usr/bin/smartctl, /usr/local/sbin/smartctl, /sbin/smartctl, /bin/smartctl 4 | 5 | -------------------------------------------------------------------------------- /Linux/zabbix_agentd.d/userparameter_mini-ipmi2.conf: -------------------------------------------------------------------------------- 1 | UserParameter=mini.disktemp.discovery[*], "/etc/zabbix/scripts/mini_ipmi_smartctl.py" "$1" "$2" 2 | UserParameter=mini.cputemp.discovery[*], "/etc/zabbix/scripts/mini_ipmi_lmsensors.py" "$1" "$2" 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zabbix-mini-IPMI 2 | CPU and disk temperature monitoring scripts for zabbix. Also support voltage and fan speed monitoring on certain configurations. Uses `lm-sensors`, `smartmontools` and `OpenHardwareMonitorReport`. For Linux, BSD and Windows. 3 | 4 | ## Features 5 | 6 | - Multi-CPU, disk and GPU solution 7 | - Low-Level Discovery 8 | - Bulk item upload with zabbix-sender 9 | - No unnecessary processes are spawned 10 | - Does not spin idle drives 11 | - RAID passthrough (manual) 12 | 13 | ![Temperature graph](https://raw.githubusercontent.com/nobody43/zabbix-mini-IPMI/master/screenshots/mini-IPMI-graph.png) 14 | 15 | [More screenshots.](https://github.com/nobody43/zabbix-mini-IPMI/tree/master/screenshots) 16 | 17 | ### Choosing OHMR version 18 | Only custom param-capable versions are supported on Windows 7+: 19 | #### [0.9.6.0](https://github.com/openhardwaremonitor/openhardwaremonitor/pull/1115#issuecomment-1189362017) 20 | #### [0.9.2.0](https://github.com/openhardwaremonitor/openhardwaremonitor/pull/1115#issuecomment-616230088) 21 | #### [0.8.0.5](https://github.com/openhardwaremonitor/openhardwaremonitor/pull/1115#issuecomment-462141642) 22 | Version for Windows XP: 23 | #### [0.3.2.0](https://github.com/openhardwaremonitor/openhardwaremonitor/issues/230#issue-102662845) 24 | 25 | ## Installation 26 | As prerequisites you need `python3`, `lm-sensors`, `smartmontools`, `sudo` and `zabbix-sender` packages. For testing `zabbix-get` is also required.
27 | Take a look at scripts first lines and provide paths if needed. If you have custom RAID configuration, also provide that manually. Import `Template_mini-IPMI_v2.xml` in zabbix web interface. 28 | ### Debian / Ubuntu 29 | ```bash 30 | client# apt install python3 sudo zabbix-agent zabbix-sender smartmontools lm-sensors 31 | server# apt install zabbix-get 32 | ``` 33 | ### Centos 34 | ```bash 35 | client# yum install python3 sudo zabbix-agent zabbix-sender smartmontools lm_sensors 36 | server# yum install zabbix-get 37 | ``` 38 | ### First step 39 | > **Note**: Your include directory may be either `zabbix_agentd.d` or `zabbix_agentd.conf.d` dependent on the distribution. 40 | #### Linux 41 | ```bash 42 | client# mv mini_ipmi_smartctl.py Linux/mini_ipmi_lmsensors.py sender_wrapper.py /etc/zabbix/scripts/ 43 | client# mv Linux/sudoers.d/zabbix /etc/sudoers.d/ # place sudoers include for smartctl sudo access 44 | client# mv Linux/zabbix_agentd.d/userparameter_mini-ipmi2.conf /etc/zabbix/zabbix_agentd.d/ 45 | ``` 46 | 47 | #### FreeBSD 48 | ```bash 49 | client# mv mini_ipmi_smartctl.py BSD/mini_ipmi_bsdcpu.py sender_wrapper.py /etc/zabbix/scripts/ 50 | client# mv BSD/sudoers.d/zabbix /usr/local/etc/sudoers.d/ 51 | client# mv BSD/zabbix_agentd.d/userparameter_mini-ipmi2.conf /usr/local/etc/zabbix/zabbix_agentd.d/ 52 | ``` 53 | Then, for Intel processor you need to add `coretemp_load="YES"` to `/boot/loader.conf`. For AMD it will be `amdtemp_load="YES"`. Reboot or manual `kldload` is required to take effect. 54 | 55 | #### Windows 56 | ```cmd 57 | client> move mini_ipmi_smartctl.py "C:\Program Files\Zabbix Agent\scripts\" 58 | client> move mini_ipmi_ohmr.py "C:\Program Files\Zabbix Agent\scripts\" 59 | client> move sender_wrapper.py "C:\Program Files\Zabbix Agent\scripts\" 60 | client> move userparameter_mini-ipmi2.conf "C:\Program Files\Zabbix Agent\zabbix_agentd.d\" 61 | ``` 62 | Install [python3](https://www.python.org/downloads/windows/), [adding it to](https://github.com/nobody43/zabbix-mini-IPMI/blob/master/screenshots/python-installation1.png) `PATH` during installation for [all users](https://github.com/nobody43/zabbix-mini-IPMI/blob/master/screenshots/python-installation2.png). Install [smartmontools](https://www.smartmontools.org/wiki/Download#InstalltheWindowspackage) and add its bin folder to `PATH` in environment variables. `OpenHardwareMonitorReport` `0.8.0.5+` requires `.NET Framework 4`. `0.3.2.0` requires `.NET Framework 3`. 63 | 64 | ### Second step 65 | Dependent on the distribution, you may need to include your zabbix conf folder in `zabbix_agentd.conf`, like this: 66 | ```conf 67 | Include=/usr/local/etc/zabbix/zabbix_agentd.d/ 68 | ``` 69 | Its recomended to add at least `Timeout=10` to server and client config files to allow drives spun up and OHMR execution. 70 | 71 | Thats all for Windows. For others run the following to finish configuration: 72 | ```bash 73 | client# chmod 755 scripts/mini_ipmi*.py scripts/sender_wrapper.py # apply necessary permissions 74 | client# chown root:zabbix scripts/mini_ipmi*.py scripts/sender_wrapper.py 75 | client# chmod 644 userparameter_mini-ipmi2.conf 76 | client# chown root:zabbix userparameter_mini-ipmi2.conf 77 | client# chmod 400 sudoers.d/zabbix 78 | client# chown root sudoers.d/zabbix 79 | client# visudo # test sudoers configuration, type :q! to exit 80 | ``` 81 | 82 | ## Testing 83 | ```bash 84 | server$ zabbix_get -s 192.0.2.1 -k mini.cputemp.discovery[get,"Example host"] 85 | server$ zabbix_get -s 192.0.2.1 -k mini.disktemp.discovery[get,"Example host"] 86 | ``` 87 | or locally: 88 | ```bash 89 | client$ /etc/zabbix/scripts/mini_ipmi_lmsensors.py get "Example host" 90 | client$ /etc/zabbix/scripts/mini_ipmi_smartctl.py get "Example host" 91 | ``` 92 | Default operation mode. Displays json that server should get, detaches, then waits and sends data with zabbix-sender. `Example host` is your `Host name` field in zabbix. You might want to use nonexistent name for testing to avoid unnecessary database pollution (client introduces itself with this name and false names will be ignored). 93 |

94 | 95 | ```bash 96 | server$ zabbix_get -s 192.0.2.1 -k mini.cputemp.discovery[getverb,"Example host"] 97 | server$ zabbix_get -s 192.0.2.1 -k mini.disktemp.discovery[getverb,"Example host"] 98 | ``` 99 | or locally: 100 | ```mixed 101 | client$ /etc/zabbix/scripts/mini_ipmi_lmsensors.py getverb "Example host" 102 | client_admin!_console> python "C:\Program Files\Zabbix Agent\scripts\mini_ipmi_ohmr.py" getverb "Example host" 103 | ``` 104 | Verbose mode. Does not detaches or prints LLD. Lists all items sent to zabbix-sender, also it is possible to see sender output in this mode. 105 |

106 | 107 | These scripts were tested to work with following configurations: 108 | - Debian 11 / Server (5.0, 6.0) / Agent 4.0 / Python 3.9 109 | - Ubuntu 22.04 / Server (5.0, 6.0) / Agent 5.0 / Python 3.10 110 | - Windows Server 2012 / Server 6.0 / Agent 4.0 / Python (3.7, 3.11) 111 | - Windows 10 / Server 6.0 / Agent 4.0 / Python (3.10, 3.11) 112 | - Windows 7 / Server 6.0 / Agent 4.0 / Python (3.4, 3.7, 3.8) 113 | - Centos 7 / Zabbix 3.0 / Python 3.6 114 | - FreeBSD 10.3 / Zabbix 3.0 / Python 3.6 115 | - Windows XP / Zabbix 3.0 / Python 3.4 116 | 117 | ## Updating 118 | Overwrite scripts and UserParameters. If UserParameters were changed - agent restart is required. If template had changed from previous version - update it in zabbix web interface [marking](https://github.com/nobody43/zabbix-smartmontools/blob/main/screenshots/template-updating.png) all `Delete missing` checkboxes. 119 | 120 | > **Note**: low values in php settings `/etc/httpd/conf.d/zabbix.conf` may result in request failure. Especially `php_value memory_limit`. 121 | 122 | ## Known issues 123 | - Zabbix web panel displays an error on json discovery, but everything works fine ([#18](https://github.com/nobody43/zabbix-mini-IPMI/issues/18)) 124 | - Windows version does not detaches, and data will only be gathered on second pass 125 | 126 | ## Links 127 | - https://www.smartmontools.org 128 | - https://wiki.archlinux.org/index.php/Lm_sensors 129 | - https://github.com/openhardwaremonitor/openhardwaremonitor 130 | - https://unlicense.org 131 | - [Disk SMART monitoring solution](https://github.com/nobody43/zabbix-smartmontools) 132 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Win/mini_ipmi_ohmr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | #BIN_PATH = r'OpenHardwareMonitorReport.exe' 4 | BIN_PATH = r'C:\Program Files\OpenHardwareMonitorReport\OpenHardwareMonitorReport.exe' # if OHMR isn't in PATH 5 | 6 | # path to send script 7 | SENDER_WRAPPER_PATH = r'C:\Program Files\Zabbix Agent\scripts\sender_wrapper.py' 8 | 9 | # path to zabbix agent configuration file 10 | AGENT_CONF_PATH = r'C:\Program Files\Zabbix Agent\zabbix_agentd.conf' 11 | 12 | #SENDER_PATH = r'zabbix_sender' 13 | SENDER_PATH = r'C:\Program Files\Zabbix Agent\zabbix_sender.exe' 14 | 15 | PARAMS = 'reporttoconsole --IgnoreMonitorHDD --IgnoreMonitorRAM' 16 | # Possible params: 17 | # --IgnoreMonitorCPU 18 | # --IgnoreMonitorFanController 19 | # --IgnoreMonitorGPU 20 | # --IgnoreMonitorHDD 21 | # --IgnoreMonitorMainboard 22 | # --IgnoreMonitorRAM 23 | 24 | SKIP_PARAMS_ON_WINXP = True # True or False 25 | 26 | # Advanced configuration 27 | 28 | # Supply absent Tjmax 29 | MANUAL_TJMAXES = ( 30 | ('AMD Athlon 64 X2 Dual Core Processor 5200+', '72'), 31 | ) 32 | 33 | # Ignore specific temperature by name on specific board 34 | IGNORED_SENSORS = ( 35 | ('H110M-R', 'Temperature #6'), # ignore 'Temperature #6' on board 'H110M-R' 36 | # ('EXAMPLE_BOARDNAME', 'EXAMPLE_TEMPERATURENAME'), 37 | # ('Pull requests', 'are welcome'), 38 | ) 39 | 40 | # These CPUs will not produce NO_TEMP problem 41 | CPUS_WITHOUT_SENSOR = ( 42 | 'Intel Pentium 4 3.00GHz', 43 | ) 44 | 45 | GPUS_WITHOUT_SENSOR = ( 46 | 'AMD Radeon Vega 8 Graphics', 47 | 'AMD Radeon Vega 10 Graphics', 48 | ) 49 | 50 | BOARD_REGEXPS_AND_KEYS = ( 51 | ('^SMBIOS\s+Version:\s+(.+)$', 'mini.brd.info[SMBIOSversion]'), 52 | ('^BIOS\s+Vendor:\s+(.+)$', 'mini.brd.info[BIOSvendor]'), 53 | ('^BIOS\s+Version:\s+(.+)$', 'mini.brd.info[BIOSversion]'), 54 | ('^Mainboard\s+Manufacturer:\s+(.+)$', 'mini.brd.info[MainboardManufacturer]'), 55 | ('^Mainboard\s+Name:\s+(.+)$', 'mini.brd.info[MainboardName]'), 56 | ('^Mainboard\s+Version:\s+(.+)$', 'mini.brd.info[MainboardVersion]'), 57 | ) 58 | 59 | VOLTAGE_REGEXPS_KEYS_AND_JSONS = ( 60 | ('^VCore$', 'cpuVcore', '{#VCORE}'), 61 | ('^VBAT$', 'VBat', '{#VBAT}'), 62 | ('(^3VSB$|^VSB3V$|^Standby \+3\.3V$)', 'VSB3V', '{#VSB3V}'), 63 | ('(^3VCC$|^VCC3V$)', 'VCC3V', '{#VCC3V}'), 64 | ('^AVCC$', 'AVCC', '{#VAVCC}'), 65 | ('^VTT$', 'VTT', '{#VTT}'), 66 | ) 67 | 68 | DELAY = '50' # how long the script must wait between LLD and sending, increase if data received late (does not affect windows) 69 | # this setting MUST be lower than 'Update interval' in the discovery rule 70 | 71 | ## End of configuration ## 72 | 73 | import sys 74 | import subprocess 75 | import re 76 | import platform 77 | from sender_wrapper import (readConfig, processData, fail_ifNot_Py3, removeQuotes) 78 | 79 | HOST = sys.argv[2] 80 | 81 | 82 | def chooseCmd(binPath_, params_): 83 | 84 | cmd = '%s %s' % (binPath_, params_) 85 | 86 | if not SKIP_PARAMS_ON_WINXP: 87 | if platform.release() == "XP": 88 | cmd = binPath_ 89 | 90 | return cmd 91 | 92 | 93 | def getOutput(cmd_): 94 | 95 | p = None 96 | try: 97 | p = subprocess.check_output(cmd_, universal_newlines=True) 98 | except OSError as e: 99 | if e.args[0] == 2: 100 | status = 'OS_NOCMD' 101 | else: 102 | status = 'OS_ERROR' 103 | if sys.argv[1] == 'getverb': 104 | raise 105 | except Exception as e: 106 | status = 'UNKNOWN_EXC_ERROR' 107 | 108 | if sys.argv[1] == 'getverb': 109 | raise 110 | 111 | try: 112 | p = e.output 113 | except: 114 | pass 115 | else: 116 | status = 'CONFIGURED' 117 | 118 | # Prevent empty results 119 | if p: 120 | m0 = 'Status: Extracting driver failed' 121 | m1 = 'First Exception: OpenSCManager returned zero.' 122 | m2 = 'Second Exception: OpenSCManager returned zero.' 123 | if (m0 in p or 124 | m1 in p or 125 | m2 in p): 126 | 127 | print('OHMR failed. Try again.') 128 | sys.exit(1) 129 | 130 | return status, p 131 | 132 | 133 | def getOHMRversion(pOut_): 134 | 135 | OHMRver = re.search(r'^Version:\s+(.+)$', pOut_, re.I | re.M) 136 | if OHMRver: 137 | version = OHMRver.group(1).strip() 138 | else: 139 | version = None 140 | 141 | sender = ['"%s" mini.info[OHMRversion] "%s"' % (HOST, removeQuotes(version))] 142 | 143 | return sender 144 | 145 | 146 | def getBoardInfo(pOut_): 147 | 148 | sender = [] 149 | 150 | for regexp, key in BOARD_REGEXPS_AND_KEYS: 151 | reMatch = re.search(regexp, pOut_, re.I | re.M) 152 | if reMatch: 153 | sender.append('"%s" %s "%s"' % (HOST, key, removeQuotes(reMatch.group(1).strip()))) 154 | 155 | return sender 156 | 157 | 158 | def getBoardName(pOut_): 159 | 160 | boardRe = re.search(r'^Mainboard\s+Name:\s+(.+)$', pOut_, re.I | re.M) 161 | if boardRe: 162 | board = boardRe.group(1).strip() 163 | else: 164 | board = None 165 | 166 | return board 167 | 168 | 169 | def getTjmax(pOut_, cpuID_, cpuName_): 170 | 171 | tjMaxRe = re.search(r'\(\/[\w-]+cpu\/%s\/temperature\/\d+\)\s+\|\s+\|\s+\+\-\s+TjMax\s+\[\S+\]\s+:\s+(\d+)' % cpuID_, pOut_, re.I | re.M) 172 | if tjMaxRe: 173 | tjmax = tjMaxRe.group(1) 174 | else: 175 | tjmax = None 176 | for name, val in MANUAL_TJMAXES: 177 | if name == cpuName_: 178 | tjmax = val 179 | break 180 | 181 | return tjmax 182 | 183 | 184 | def isCpuWithoutSensor(cpuname_): 185 | 186 | if cpuname_ in CPUS_WITHOUT_SENSOR: 187 | result = True 188 | else: 189 | result = False 190 | 191 | return result 192 | 193 | 194 | def isCpuSensorPresent(pOut_): 195 | 196 | coreTempsRe = re.search(r'Core.+:\s+\d+.+\(\/[\w-]+cpu\/\d+\/temperature\/\d+\)', pOut_, re.I) 197 | if coreTempsRe: 198 | result = True 199 | else: 200 | result = False 201 | 202 | return result 203 | 204 | 205 | def isGpuWithoutSensor(gpuname_): 206 | 207 | if gpuname_ in GPUS_WITHOUT_SENSOR: 208 | result = True 209 | else: 210 | result = False 211 | 212 | return result 213 | 214 | 215 | def isParamIgnored(param_): 216 | 217 | if param_ in PARAMS: 218 | result = True 219 | else: 220 | result = False 221 | 222 | return result 223 | 224 | 225 | def getCpusData(pOut_): 226 | 227 | sender = [] 228 | json = [] 229 | 230 | # determine available CPUs 231 | cpusRe = re.findall(r'\+\-\s+(.+)\s+\(\/[\w-]+cpu\/(\d+)\)', pOut_, re.I) 232 | cpus = set(cpusRe) 233 | #print(cpusRe) 234 | 235 | allTemps = [] 236 | for name, id in cpus: 237 | # Processor model 238 | sender.append('"%s" mini.cpu.info[cpu%s,ID] "%s"' % (HOST, id, removeQuotes(name.strip()))) 239 | json.append({'{#CPU}':id}) 240 | 241 | gotTjmax = getTjmax(pOut_, id, name) 242 | if gotTjmax: 243 | sender.append('"%s" mini.cpu.info[cpu%s,TjMax] "%s"' % (HOST, id, gotTjmax)) 244 | 245 | # All core temperatures for given CPU 246 | coreTempsRe = re.findall(r'Core.+:\s+(\d+).+\(\/[\w-]+cpu\/%s\/temperature\/(\d+)\)' % id, pOut_, re.I) 247 | if coreTempsRe: 248 | sender.append('"%s" mini.cpu.info[cpu%s,CPUstatus] "PROCESSED"' % (HOST, id)) 249 | cpuTemps = [] 250 | for coretemp, coreid in coreTempsRe: 251 | cpuTemps.append(int(coretemp)) 252 | allTemps.append(int(coretemp)) 253 | sender.append('"%s" mini.cpu.temp[cpu%s,core%s] "%s"' % (HOST, id, coreid, coretemp)) 254 | json.append({'{#CPUC}':id, '{#CORE}':coreid}) 255 | 256 | sender.append('"%s" mini.cpu.temp[cpu%s,MAX] "%s"' % (HOST, id, str(max(cpuTemps)))) 257 | 258 | elif isCpuWithoutSensor(name): 259 | sender.append('"%s" mini.cpu.info[cpu%s,CPUstatus] "NO_SENSOR"' % (HOST, id)) 260 | else: 261 | packageTempsRe = re.findall(r'Package.+:\s+(\d+).+\(\/[\w-]+cpu\/%s\/temperature\/(\d+)\)' % id, pOut_, re.I) 262 | if packageTempsRe: 263 | sender.append('"%s" mini.cpu.info[cpu%s,CPUstatus] "PROCESSED"' % (HOST, id)) 264 | for packagetemp, packageid in packageTempsRe: 265 | allTemps.append(int(packagetemp)) 266 | 267 | else: 268 | sender.append('"%s" mini.cpu.info[cpu%s,CPUstatus] "NO_TEMP"' % (HOST, id)) 269 | 270 | if cpus: 271 | if allTemps: 272 | error = None 273 | sender.append('"%s" mini.cpu.temp[MAX] "%s"' % (HOST , str(max(allTemps)))) 274 | else: 275 | error = 'NOCPUTEMPS' 276 | else: 277 | error = 'NOCPUS' 278 | 279 | return sender, json, error 280 | 281 | 282 | def getVoltages(pOut_): 283 | 284 | sender = [] 285 | json = [] 286 | 287 | voltagesRe = re.findall(r'\+\-\s+(.+)\s+:\s+(\d+\.\d+|\d+)\s+.+\(\/lpc\/[\w-]+\/voltage\/(\d+)\)', pOut_, re.I) 288 | for name, val, id in voltagesRe: 289 | name = name.strip() 290 | 291 | for regexp, key, jsn in VOLTAGE_REGEXPS_KEYS_AND_JSONS: 292 | if re.search(regexp, name, re.I): 293 | sender.append('"%s" mini.brd.vlt[%s] "%s"' % (HOST, key, removeQuotes(val))) 294 | json.append({jsn:key}) 295 | 296 | sender.append('"%s" mini.brd.vlt[%s] "%s"' % (HOST, id, removeQuotes(val))) # static items for graph, could be duplicate 297 | 298 | return sender, json 299 | 300 | 301 | def getBoardFans(pOut_): 302 | 303 | sender = [] 304 | json = [] 305 | 306 | fansRe = re.findall(r'\+\-\s+(.+)\s+:\s+(\d+).+\(\/lpc\/[\w-]+\/fan\/(\d+)\)', pOut_, re.I) 307 | for name, val, num in fansRe: 308 | name = name.strip() 309 | 310 | # Only create LLD when speed is not zero, BUT always send zero values (hides phantom fans) 311 | sender.append('"%s" mini.brd.fan[%s,rpm] "%s"' % (HOST, num, val)) 312 | if val != '0': 313 | json.append({'{#BRDFANNAME}':name, '{#BRDFANNUM}':num}) 314 | 315 | return sender, json 316 | 317 | 318 | def getBoardTemps(pOut_): 319 | 320 | sender = [] 321 | json = [] 322 | 323 | board = getBoardName(pOut_) 324 | 325 | tempsRe = re.findall(r'\+\-\s+(.+)\s+:\s+(\d+).+\(\/lpc\/[\w-]+\/temperature\/(\d+)\)', pOut_, re.I) 326 | #print(tempsRe) 327 | 328 | allTemps = [] 329 | for name, val, id in tempsRe: 330 | name = name.strip() 331 | 332 | if (isCpuSensorPresent(pOut_) and 333 | re.match('^CPU Core$|^CPU$', name)): 334 | 335 | continue 336 | 337 | ignoredSensor = False 338 | if board: 339 | for boardReference, ignoredTempName in IGNORED_SENSORS: 340 | if (boardReference == board and 341 | ignoredTempName == name): 342 | 343 | ignoredSensor = True 344 | 345 | if ignoredSensor: 346 | continue 347 | 348 | allTemps.append(int(val)) 349 | 350 | sender.append('"%s" mini.brd.temp[%s] "%s"' % (HOST, id, val)) 351 | json.append({'{#BRDTEMPNAME}':name, '{#BRDTEMPNUM}':id}) 352 | 353 | if allTemps: 354 | sender.append('"%s" mini.brd.temp[MAX] "%s"' % (HOST, str(max(allTemps)))) 355 | 356 | return sender, json 357 | 358 | 359 | def getGpusData(pOut_): 360 | 361 | sender = [] 362 | json = [] 363 | 364 | # Determine available GPUs 365 | gpusRe = re.findall(r'\+\-\s+(.+)\s+\(\/[\w-]+gpu\/(\d+)\)', pOut_, re.I) 366 | gpus = set(gpusRe) 367 | 368 | allTemps = [] 369 | for name, num in gpus: 370 | errors = [] 371 | sender.append('"%s" mini.gpu.info[gpu%s,ID] "%s"' % (HOST, num, name.strip())) 372 | json.append({'{#GPU}':num}) 373 | 374 | temp = re.search(r':\s+(\d+).+\(\/[\w-]+gpu\/%s\/temperature\/0\)' % num, pOut_, re.I) 375 | if temp: 376 | json.append({'{#GPUTEMP}':num}) 377 | allTemps.append(int(temp.group(1))) 378 | sender.append('"%s" mini.gpu.temp[gpu%s] "%s"' % (HOST, num, temp.group(1))) 379 | elif isGpuWithoutSensor(name): 380 | errors.append('NO_SENSOR') 381 | else: 382 | errors.append('NO_TEMP') 383 | 384 | fanspeed = re.search(r':\s+(\d+).+\(\/[\w-]+gpu\/%s\/fan\/0\)' % num, pOut_, re.I) 385 | if fanspeed: 386 | sender.append('"%s" mini.gpu.fan[gpu%s,rpm] "%s"' % (HOST, num, fanspeed.group(1))) 387 | if fanspeed.group(1) != '0': 388 | json.append({'{#GPUFAN}':num}) 389 | else: 390 | errors.append('NO_FAN') 391 | 392 | memory = re.findall(r'\+\-\s+(GPU\s+Memory\s+Free|GPU\s+Memory\s+Used|GPU\s+Memory\s+Total)\s+:\s+(\d+).+\(\/[\w-]+gpu\/%s\/smalldata\/\d+\)' % num, pOut_, re.I) 393 | if memory: 394 | json.append({'{#GPUMEM}':num}) 395 | for memname, memval in memory: 396 | if 'Free' in memname: 397 | sender.append('"%s" mini.gpu.memory[gpu%s,free] "%s"' % (HOST, num, memval)) 398 | elif 'Used' in memname: 399 | sender.append('"%s" mini.gpu.memory[gpu%s,used] "%s"' % (HOST, num, memval)) 400 | elif 'Total' in memname: 401 | sender.append('"%s" mini.gpu.memory[gpu%s,total] "%s"' % (HOST, num, memval)) 402 | 403 | if errors: 404 | for e in errors: 405 | sender.append('"%s" mini.gpu.info[gpu%s,GPUstatus] "%s"' % (HOST, num, e)) # NO_TEMP, NO_FAN, NO_SENSOR 406 | else: 407 | sender.append('"%s" mini.gpu.info[gpu%s,GPUstatus] "PROCESSED"' % (HOST, num)) 408 | 409 | if gpus: 410 | statusError = None 411 | if allTemps: 412 | sender.append('"%s" mini.gpu.temp[MAX] "%s"' % (HOST, str(max(allTemps)))) 413 | else: 414 | statusError = 'NOGPUS' 415 | 416 | return sender, json, statusError 417 | 418 | 419 | if __name__ == '__main__': 420 | 421 | fail_ifNot_Py3() 422 | 423 | senderData = [] 424 | jsonData = [] 425 | statusErrors = [] 426 | 427 | cmd = chooseCmd(BIN_PATH, PARAMS) 428 | 429 | p_Output = getOutput(cmd) 430 | pRunStatus = p_Output[0] 431 | pOut = p_Output[1] 432 | 433 | if pOut: 434 | senderData.extend(getOHMRversion(pOut)) 435 | 436 | senderData.extend(getBoardInfo(pOut)) 437 | 438 | if not isParamIgnored('--IgnoreMonitorCPU'): 439 | cpuData_Out = getCpusData(pOut) 440 | if cpuData_Out: 441 | cpuSender = cpuData_Out[0] 442 | cpuJson = cpuData_Out[1] 443 | cpuError = cpuData_Out[2] 444 | 445 | senderData.extend(cpuSender) 446 | jsonData.extend(cpuJson) 447 | 448 | if cpuError: 449 | statusErrors.append(cpuError) 450 | 451 | if not isParamIgnored('--IgnoreMonitorMainboard'): 452 | boardTemps_Out = getBoardTemps(pOut) 453 | if boardTemps_Out: 454 | boardSender = boardTemps_Out[0] 455 | boardJson = boardTemps_Out[1] 456 | 457 | senderData.extend(boardSender) 458 | jsonData.extend(boardJson) 459 | 460 | voltages_Out = getVoltages(pOut) 461 | if voltages_Out: 462 | voltagesSender = voltages_Out[0] 463 | voltagesJson = voltages_Out[1] 464 | 465 | senderData.extend(voltagesSender) 466 | jsonData.extend(voltagesJson) 467 | 468 | if not isParamIgnored('--IgnoreMonitorFanController'): 469 | boardFans_Out = getBoardFans(pOut) 470 | if boardFans_Out: 471 | senderData.extend(boardFans_Out[0]) 472 | jsonData.extend(boardFans_Out[1]) 473 | 474 | if not isParamIgnored('--IgnoreMonitorGPU'): 475 | gpuData_Out = getGpusData(pOut) 476 | if gpuData_Out: 477 | gpuSender = gpuData_Out[0] 478 | gpuJson = gpuData_Out[1] 479 | gpuError = gpuData_Out[2] 480 | 481 | senderData.extend(gpuSender) 482 | jsonData.extend(gpuJson) 483 | 484 | if gpuError: 485 | statusErrors.append(gpuError) 486 | 487 | if statusErrors: 488 | errorsString = ', '.join(statusErrors).strip() 489 | senderData.append('"%s" mini.cpu.info[ConfigStatus] "%s"' % (HOST, errorsString)) 490 | elif pRunStatus: 491 | senderData.append('"%s" mini.cpu.info[ConfigStatus] "%s"' % (HOST, pRunStatus)) 492 | 493 | link = r'https://github.com/nobody43/zabbix-mini-IPMI/issues' 494 | sendStatusKey = 'mini.cpu.info[SendStatus]' 495 | processData(senderData, jsonData, AGENT_CONF_PATH, SENDER_WRAPPER_PATH, SENDER_PATH, DELAY, HOST, link, sendStatusKey) 496 | -------------------------------------------------------------------------------- /Win/zabbix_agentd.d/userparameter_mini-ipmi2.conf: -------------------------------------------------------------------------------- 1 | UserParameter=mini.disktemp.discovery[*], python "C:\Program Files\Zabbix Agent\scripts\mini_ipmi_smartctl.py" "$1" "$2" 2 | UserParameter=mini.cputemp.discovery[*], python "C:\Program Files\Zabbix Agent\scripts\mini_ipmi_ohmr.py" "$1" "$2" 3 | -------------------------------------------------------------------------------- /mini_ipmi_smartctl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Only one out of three system-specific setting is used, PATH considered. 4 | binPath_LINUX = r'smartctl' 5 | binPath_WIN = r'C:\Program Files\smartmontools\bin\smartctl.exe' 6 | binPath_OTHER = r'/usr/local/sbin/smartctl' 7 | 8 | # path to zabbix agent configuration file 9 | agentConf_LINUX = r'/etc/zabbix/zabbix_agentd.conf' 10 | agentConf_WIN = r'C:\Program Files\Zabbix Agent\zabbix_agentd.conf' 11 | agentConf_OTHER = r'/usr/local/etc/zabbix3/zabbix_agentd.conf' 12 | 13 | senderPath_LINUX = r'zabbix_sender' 14 | senderPath_WIN = r'C:\Program Files\Zabbix Agent\zabbix_sender.exe' 15 | senderPath_OTHER = r'/usr/local/bin/zabbix_sender' 16 | 17 | # path to second send script 18 | senderPyPath_LINUX = r'/etc/zabbix/scripts/sender_wrapper.py' 19 | senderPyPath_WIN = r'C:\Program Files\Zabbix Agent\scripts\sender_wrapper.py' 20 | senderPyPath_OTHER = r'/usr/local/etc/zabbix/scripts/sender_wrapper.py' 21 | 22 | 23 | ## Advanced configuration ## 24 | # 'True' or 'False' 25 | isCheckNVMe = False # Additional overhead. Should be disabled if smartmontools is >= 7 or NVMe is absent. 26 | 27 | isCheckSAS = False # Use '-a' instead of '-A', which may produce ERR_CODE_*. Slight overhead. 28 | 29 | isIgnoreDuplicates = True 30 | 31 | isHeavyDebug = False 32 | 33 | # type, min, max, critical 34 | thresholds = ( 35 | ('hdd', 25, 45, 60), 36 | ('ssd', 5, 55, 70), 37 | ) 38 | 39 | perDiskTimeout = 3 # Single disk query can not exceed this value. Python33 or above required. 40 | 41 | delay = '50' # How long the script must wait between LLD and sending, increase if data received late (does not affect windows). 42 | # This setting MUST be lower than 'Update interval' in the discovery rule. 43 | 44 | # Manually provide disk list or RAID configuration if needed. 45 | diskDevsManual = [] 46 | # like this: 47 | #diskDevsManual = ['/dev/bus/0 -d megaraid,4', '/dev/bus/0 -d megaraid,5'] 48 | # more info: https://www.smartmontools.org/wiki/Supported_RAID-Controllers 49 | 50 | # These models will not produce 'NOTEMP' warning. Pull requests are welcome. 51 | noTemperatureSensorModels = ( 52 | 'INTEL SSDSC2CW060A3', 53 | 'AXXROMBSASMR', 54 | 'PLEXTOR PX-256M6Pro', 55 | ) 56 | 57 | # re.IGNORECASE | re.MULTILINE 58 | modelPatterns = ( 59 | '^Device Model:\s+(.+)$', 60 | '^Device:\s+(.+)$', 61 | '^Product:\s+(.+)$', 62 | '^Model Number:\s+(.+)$', 63 | ) 64 | 65 | # First match returned right away; re.IGNORECASE | re.MULTILINE 66 | temperaturePatterns = ( 67 | '^(?:\s+)?\d+\s+Temperature_Celsius\s+[\w-]+\s+\d{3}\s+[\w-]+\s+[\w-]+\s+[\w-]+\s+[\w-]+\s+[\w-]+\s+(\d+)', 68 | '^(?:\s+)?\d+\s+Temperature_Internal\s+[\w-]+\s+\d{3}\s+[\w-]+\s+[\w-]+\s+[\w-]+\s+[\w-]+\s+[\w-]+\s+(\d+)', 69 | '^(?:\s+)?\d+\s+Temperature_Case\s+[\w-]+\s+\d{3}\s+[\w-]+\s+[\w-]+\s+[\w-]+\s+[\w-]+\s+[\w-]+\s+(\d+)', 70 | '^(?:\s+)?Current\s+Drive\s+Temperature:\s+(\d+)\s+', 71 | '^(?:\s+)?Temperature:\s+(\d+)\s+C', 72 | '^(?:\s+)?\d+\s+Airflow_Temperature_Cel\s+[\w-]+\s+\d{3}\s+[\w-]+\s+[\w-]+\s+[\w-]+\s+[\w-]+\s+[\w-]+\s+(\d+)', 73 | ) 74 | 75 | ## End of configuration ## 76 | 77 | 78 | import sys 79 | import subprocess 80 | import re 81 | import shlex 82 | from sender_wrapper import (fail_ifNot_Py3, sanitizeStr, clearDiskTypeStr, processData) 83 | 84 | HOST = sys.argv[2] 85 | 86 | 87 | def scanDisks(mode): 88 | '''Determines available disks. Can be skipped.''' 89 | if mode == 'NOTYPE': 90 | cmd = addSudoIfNix([binPath, '--scan']) 91 | elif mode == 'NVME': 92 | cmd = addSudoIfNix([binPath, '--scan', '-d', 'nvme']) 93 | else: 94 | print('Invalid type %s. Terminating.' % mode) 95 | sys.exit(1) 96 | 97 | try: 98 | p = subprocess.check_output(cmd, universal_newlines=True) 99 | error = '' 100 | except OSError as e: 101 | p = '' 102 | 103 | if e.args[0] == 2: 104 | error = 'SCAN_OS_NOCMD_%s' % mode 105 | else: 106 | error = 'SCAN_OS_ERROR_%s' % mode 107 | 108 | except Exception as e: 109 | try: 110 | p = e.output 111 | except: 112 | p = '' 113 | 114 | error = 'SCAN_UNKNOWN_ERROR_%s' % mode 115 | if sys.argv[1] == 'getverb': 116 | raise 117 | 118 | # TESTING 119 | #if mode == 'NVME': p = '''/dev/nvme0 -d nvme # /dev/nvme0, NVMe device\n/dev/bus/0 -d megaraid,4 # /dev/bus/0 [megaraid_disk_04], SCSI device''' 120 | 121 | # Determine full device names and types 122 | disks = re.findall(r'^(/dev/[^#]+)', p, re.M) 123 | 124 | return error, disks 125 | 126 | 127 | def moveCsmiToBegining(disks): 128 | 129 | csmis = [] 130 | others = [] 131 | 132 | for i in disks: 133 | if re.search(r'\/csmi\d+\,\d+', i, re.I): 134 | csmis.append(i) 135 | else: 136 | others.append(i) 137 | 138 | result = csmis + others 139 | 140 | return result 141 | 142 | 143 | def listDisks(): 144 | 145 | errors = [] 146 | 147 | if not diskDevsManual: 148 | scanDisks_Out = scanDisks('NOTYPE') 149 | errors.append(scanDisks_Out[0]) # SCAN_OS_NOCMD_*, SCAN_OS_ERROR_*, SCAN_UNKNOWN_ERROR_* 150 | 151 | disks = scanDisks_Out[1] 152 | 153 | if isCheckNVMe: 154 | scanDisksNVMe_Out = scanDisks('NVME') 155 | errors.append(scanDisksNVMe_Out[0]) 156 | 157 | disks.extend(scanDisksNVMe_Out[1]) 158 | else: 159 | errors.append('') 160 | 161 | else: 162 | disks = diskDevsManual 163 | 164 | # Remove duplicates preserving order 165 | diskResult = [] 166 | for i in disks: 167 | if i not in diskResult: 168 | diskResult.append(i) 169 | 170 | diskResult = moveCsmiToBegining(diskResult) 171 | 172 | return errors, diskResult 173 | 174 | 175 | def findErrorsAndOuts(cD): 176 | 177 | err = None 178 | p = '' 179 | 180 | try: 181 | if isCheckSAS: 182 | cmd = addSudoIfNix([binPath, '-a', '-i', '-n', 'standby']) + shlex.split(cD) 183 | else: 184 | cmd = addSudoIfNix([binPath, '-A', '-i', '-n', 'standby']) + shlex.split(cD) 185 | 186 | if (sys.version_info.major == 3 and 187 | sys.version_info.minor <= 2): 188 | 189 | p = subprocess.check_output(cmd, universal_newlines=True) 190 | 191 | err = 'OLD_PYTHON32_OR_LESS' 192 | else: 193 | p = subprocess.check_output(cmd, universal_newlines=True, timeout=perDiskTimeout) 194 | 195 | except OSError as e: 196 | if e.args[0] == 2: 197 | err = 'D_OS_NOCMD' 198 | else: 199 | err = 'D_OS_ERROR' 200 | if sys.argv[1] == 'getverb': raise 201 | 202 | except subprocess.CalledProcessError as e: 203 | p = e.output 204 | 205 | if 'Device is in STANDBY (OS)' in p: 206 | err = 'STANDBY_OS' 207 | elif 'Device is in STANDBY' in p: 208 | err = 'STANDBY' 209 | elif 'Device is in SLEEP' in p: 210 | err = 'SLEEP' 211 | elif 'Unknown USB bridge' in p: 212 | err = 'UNK_USB_BRIDGE' 213 | elif r"Packet Interface Devices [this device: CD/DVD] don't support ATA SMART" in p: 214 | err = 'CD_DVD_DRIVE' 215 | 216 | elif (sys.version_info.major == 3 and 217 | sys.version_info.minor <= 1): 218 | 219 | err = 'UNK_OLD_PYTHON31_OR_LESS' 220 | 221 | elif e.args: 222 | err = 'ERR_CODE_%s' % str(e.args[0]) 223 | else: 224 | err = 'UNKNOWN_RESPONSE' 225 | 226 | except subprocess.TimeoutExpired: 227 | err = 'TIMEOUT' 228 | 229 | except Exception as e: 230 | err = 'UNKNOWN_EXC_ERROR' 231 | if sys.argv[1] == 'getverb': raise 232 | 233 | try: 234 | p = e.output 235 | except: 236 | p = '' 237 | 238 | return (err, p) 239 | 240 | 241 | def findDiskTemp(p): 242 | 243 | result = None 244 | for i in temperaturePatterns: 245 | temperatureRe = re.search(i, p, re.I | re.M) 246 | if temperatureRe: 247 | result = temperatureRe.group(1) 248 | break 249 | 250 | return result 251 | 252 | 253 | def findIdent(p): 254 | 255 | identPatterns = ( 256 | '^Serial Number:\s+(.+)$', 257 | '^LU WWN Device Id:\s+(.+)$', 258 | '^Logical Unit id:\s+(.+)$', 259 | '^Product:\s+(.+)$', 260 | '^Device Model:\s+(.+)$', 261 | ) 262 | 263 | result = None 264 | for i in identPatterns: 265 | identRe = re.search(i, p, re.I | re.M) 266 | if identRe: 267 | result = identRe.group(1) 268 | break 269 | 270 | return result 271 | 272 | 273 | def chooseSystemSpecificPaths(): 274 | 275 | if sys.platform.startswith('linux'): 276 | binPath_ = binPath_LINUX 277 | agentConf_ = agentConf_LINUX 278 | senderPath_ = senderPath_LINUX 279 | senderPyPath_ = senderPyPath_LINUX 280 | 281 | elif sys.platform == 'win32': 282 | binPath_ = binPath_WIN 283 | agentConf_ = agentConf_WIN 284 | senderPath_ = senderPath_WIN 285 | senderPyPath_ = senderPyPath_WIN 286 | 287 | else: 288 | binPath_ = binPath_OTHER 289 | agentConf_ = agentConf_OTHER 290 | senderPath_ = senderPath_OTHER 291 | senderPyPath_ = senderPyPath_OTHER 292 | 293 | if sys.argv[1] == 'getverb': 294 | print(' Path guess: %s\n' % sys.platform) 295 | 296 | return (binPath_, agentConf_, senderPath_, senderPyPath_) 297 | 298 | 299 | def isModelWithoutSensor(p): 300 | 301 | result = False 302 | for i in modelPatterns: 303 | modelRe = re.search(i, p, re.I | re.M) 304 | if modelRe: 305 | model = modelRe.group(1).strip() 306 | 307 | if model in noTemperatureSensorModels: 308 | result = True 309 | break 310 | 311 | return result 312 | 313 | 314 | def isDummyNVMe(p): 315 | 316 | subsystemRe = re.search(r'Subsystem ID:\s+0x0000', p, re.I) 317 | ouiRe = re.search(r'IEEE OUI Identifier:\s+0x000000', p, re.I) 318 | 319 | if (subsystemRe and 320 | ouiRe): 321 | 322 | result = True 323 | else: 324 | result = False 325 | 326 | return result 327 | 328 | 329 | def addSudoIfNix(cmd): 330 | 331 | result = cmd 332 | if not sys.platform == 'win32': 333 | result = ['sudo'] + cmd 334 | 335 | return result 336 | 337 | 338 | def isSSD(p): 339 | 340 | ssdRe = re.search('^Rotation Rate:\s+Solid State Device', p, re.I | re.M) 341 | 342 | if ssdRe: 343 | result = True 344 | else: 345 | result = False 346 | 347 | return result 348 | 349 | 350 | def doHeavyDebug(diskError_, driveStatus_, diskPout_): 351 | 352 | if (diskError_ and 353 | driveStatus_ != 'DUPLICATE_IGNORE'): 354 | 355 | heavyOut = repr(diskPout_.strip()) 356 | heavyOut = heavyOut.strip().strip('"').strip("'").strip() 357 | heavyOut = heavyOut.replace("'", r"\'").replace('"', r'\"') 358 | 359 | debugData = '"%s" mini.disk.HeavyDebug "%s"' % (HOST, heavyOut) 360 | senderData.append(debugData) 361 | 362 | 363 | if __name__ == '__main__': 364 | 365 | fail_ifNot_Py3() 366 | 367 | paths_Out = chooseSystemSpecificPaths() 368 | binPath = paths_Out[0] 369 | agentConf = paths_Out[1] 370 | senderPath = paths_Out[2] 371 | senderPyPath = paths_Out[3] 372 | 373 | senderData = [] 374 | jsonData = [] 375 | 376 | listDisks_Out = listDisks() 377 | scanErrors = listDisks_Out[0] 378 | diskDevs = listDisks_Out[1] 379 | 380 | if scanErrors: 381 | scanErrorNotype = scanErrors[0] 382 | scanErrorNvme = scanErrors[1] 383 | else: 384 | scanErrorNotype = None 385 | scanErrorNvme = None 386 | 387 | sessionSerials = [] 388 | allTemps = [] 389 | diskError_NOCMD = False 390 | for d in diskDevs: 391 | clearedD = clearDiskTypeStr(d) 392 | sanitizedD = sanitizeStr(clearedD) 393 | jsonData.append({'{#DISK}':sanitizedD}) # always discover to prevent flapping 394 | 395 | disk_Out = findErrorsAndOuts(clearedD) 396 | diskError = disk_Out[0] 397 | diskPout = disk_Out[1] 398 | if diskError: 399 | if 'D_OS_' in diskError: 400 | diskError_NOCMD = diskError 401 | break # [v] fatal; json of other disks is discarded 402 | 403 | isDuplicate = False 404 | ident = findIdent(diskPout) 405 | if ident in sessionSerials: 406 | isDuplicate = True 407 | elif ident: 408 | sessionSerials.append(ident) 409 | 410 | temp = findDiskTemp(diskPout) 411 | if isDuplicate: 412 | if isIgnoreDuplicates: 413 | driveStatus = 'DUPLICATE_IGNORE' 414 | else: 415 | driveStatus = 'DUPLICATE_MENTION' 416 | elif isModelWithoutSensor(diskPout): 417 | driveStatus = 'NOSENSOR' 418 | elif isDummyNVMe(diskPout): 419 | driveStatus = 'DUMMY_NVME' 420 | elif diskError: 421 | driveStatus = diskError 422 | elif not temp: # !!BUG!! needs more complex conditionals 423 | driveStatus = 'NOTEMP' 424 | else: 425 | driveStatus = 'PROCESSED' 426 | senderData.append('"%s" mini.disk.info[%s,DriveStatus] "%s"' % (HOST, sanitizedD, driveStatus)) 427 | 428 | if (temp and 429 | not driveStatus == 'NOSENSOR' and 430 | not driveStatus == 'DUPLICATE_IGNORE'): 431 | 432 | senderData.append('"%s" mini.disk.temp[%s] "%s"' % (HOST, sanitizedD, temp)) 433 | allTemps.append(temp) 434 | 435 | if isSSD(diskPout): 436 | threshMin = thresholds[1][1] 437 | threshMax = thresholds[1][2] 438 | threshCrit = thresholds[1][3] 439 | else: 440 | threshMin = thresholds[0][1] 441 | threshMax = thresholds[0][2] 442 | threshCrit = thresholds[0][3] 443 | 444 | senderData.append('"%s" mini.disk.tempMin[%s] "%s"' % (HOST, sanitizedD, threshMin)) 445 | senderData.append('"%s" mini.disk.tempMax[%s] "%s"' % (HOST, sanitizedD, threshMax)) 446 | senderData.append('"%s" mini.disk.tempCrit[%s] "%s"' % (HOST, sanitizedD, threshCrit)) 447 | 448 | if isHeavyDebug: 449 | doHeavyDebug(diskError, driveStatus, diskPout) 450 | 451 | if scanErrorNotype: 452 | configStatus = scanErrorNotype 453 | elif diskError_NOCMD: 454 | configStatus = diskError_NOCMD 455 | elif not diskDevs: 456 | configStatus = 'NODISKS' 457 | elif not allTemps: 458 | configStatus = 'NODISKTEMPS' 459 | else: 460 | configStatus = 'CONFIGURED' 461 | senderData.append('"%s" mini.disk.info[ConfigStatus] "%s"' % (HOST, configStatus)) 462 | 463 | if allTemps: 464 | senderData.append('"%s" mini.disk.temp[MAX] "%s"' % (HOST, str(max(allTemps)))) 465 | 466 | link = r'https://github.com/nobody43/zabbix-mini-IPMI/issues' 467 | sendStatusKey = 'mini.disk.info[SendStatus]' 468 | processData(senderData, jsonData, agentConf, senderPyPath, senderPath, delay, HOST, link, sendStatusKey) 469 | 470 | -------------------------------------------------------------------------------- /screenshots/mini-IPMI-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nobody43/zabbix-mini-IPMI/7fe920de4a9e95f6159fe88839112a832a638281/screenshots/mini-IPMI-graph.png -------------------------------------------------------------------------------- /screenshots/mini-IPMI-triggers-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nobody43/zabbix-mini-IPMI/7fe920de4a9e95f6159fe88839112a832a638281/screenshots/mini-IPMI-triggers-config.png -------------------------------------------------------------------------------- /screenshots/mini-IPMI-triggers-cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nobody43/zabbix-mini-IPMI/7fe920de4a9e95f6159fe88839112a832a638281/screenshots/mini-IPMI-triggers-cpu.png -------------------------------------------------------------------------------- /screenshots/mini-IPMI-triggers-disk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nobody43/zabbix-mini-IPMI/7fe920de4a9e95f6159fe88839112a832a638281/screenshots/mini-IPMI-triggers-disk.png -------------------------------------------------------------------------------- /screenshots/python-installation1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nobody43/zabbix-mini-IPMI/7fe920de4a9e95f6159fe88839112a832a638281/screenshots/python-installation1.png -------------------------------------------------------------------------------- /screenshots/python-installation2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nobody43/zabbix-mini-IPMI/7fe920de4a9e95f6159fe88839112a832a638281/screenshots/python-installation2.png -------------------------------------------------------------------------------- /sender_wrapper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import subprocess 5 | import re 6 | from time import sleep 7 | from json import dumps 8 | 9 | 10 | def isWindows(): 11 | if sys.platform == 'win32': 12 | return True 13 | else: 14 | return False 15 | 16 | 17 | def send(): 18 | 19 | if fetchMode == 'get': 20 | sleep(timeout) # wait for LLD to be processed by server 21 | senderProc = subprocess.Popen([senderPath, '-c', agentConf, '-i', '-'], 22 | stdin=subprocess.PIPE, universal_newlines=True, close_fds=(not isWindows())) 23 | 24 | elif fetchMode == 'getverb': 25 | print('\n Note: the sender will fail if server did not gather LLD previously.') 26 | print('\n Data sent to zabbix sender:') 27 | print('\n') 28 | print(senderDataNStr) 29 | senderProc = subprocess.Popen([senderPath, '-vv', '-c', agentConf, '-i', '-'], 30 | stdin=subprocess.PIPE, universal_newlines=True, close_fds=(not isWindows())) 31 | 32 | else: 33 | print(sys.argv[0] + " : Not supported. Use 'get' or 'getverb'.") 34 | sys.exit(1) 35 | 36 | senderProc.communicate(input=senderDataNStr) 37 | 38 | 39 | if __name__ == '__main__': 40 | fetchMode = sys.argv[1] 41 | 42 | agentConf = sys.argv[2] 43 | senderPath = sys.argv[3] 44 | timeout = int(sys.argv[4]) 45 | senderDataNStr = sys.argv[5] 46 | 47 | if isWindows(): 48 | timeout = 0 49 | 50 | send() 51 | 52 | 53 | # External 54 | def fail_ifNot_Py3(): 55 | '''Terminate if not using python3.''' 56 | if sys.version_info.major != 3: 57 | sys.stdout.write(sys.argv[0] + ': Python3 is required.') 58 | sys.exit(1) 59 | 60 | 61 | def oldPythonMsg(): 62 | if (sys.version_info.major == 3 and 63 | sys.version_info.minor <= 2): 64 | 65 | print("python32 or less is detected. It's advisable to use python33 or above for timeout guards support.") 66 | 67 | 68 | def displayVersions(config, senderPath_): 69 | '''Display python and sender versions.''' 70 | print(' Python version:\n', sys.version) 71 | 72 | oldPythonMsg() 73 | 74 | try: 75 | print('\n Sender version:\n', subprocess.check_output([senderPath_, '-V']).decode()) 76 | except: 77 | print('Could not run zabbix_sender.') 78 | 79 | print() 80 | 81 | 82 | def readConfig(config): 83 | '''Read and display important config values for debug.''' 84 | try: 85 | f = open(config, 'r') 86 | text = f.read() 87 | f.close() 88 | 89 | print(" Config's main settings:") 90 | server = re.search(r'^(?:\s+)?(Server(?:\s+)?\=(?:\s+)?.+)$', text, re.M) 91 | if server: 92 | print(server.group(1)) 93 | else: 94 | print("Could not find 'Server' setting in config!") 95 | 96 | serverActive = re.search(r'^(?:\s+)?(ServerActive(?:\s+)?\=(?:\s+)?.+)$', text, re.M) 97 | if serverActive: 98 | print(serverActive.group(1)) 99 | else: 100 | print("Could not find 'ServerActive' setting in config!") 101 | 102 | timeout = re.search(r'^(?:\s+)?(Timeout(?:\s+)?\=(?:\s+)?(\d+))(?:\s+)?$', text, re.M) 103 | if timeout: 104 | print(timeout.group(1)) 105 | 106 | if int(timeout.group(2)) < 10: 107 | print("'Timeout' setting is too low for this script!") 108 | else: 109 | print("Could not find 'Timeout' manual setting in config!\nDefault value is too low for this script.") 110 | 111 | except: 112 | print(' Could not process config file:\n' + config) 113 | finally: 114 | print() 115 | 116 | 117 | def chooseDevnull(): 118 | try: 119 | from subprocess import DEVNULL # for python versions greater than 3.3, inclusive 120 | except: 121 | import os 122 | DEVNULL = open(os.devnull, 'w') # for 3.0-3.2, inclusive 123 | 124 | return DEVNULL 125 | 126 | 127 | def processData(senderData_, jsonData_, agentConf_, senderPyPath_, senderPath_, 128 | timeout_, host_, issuesLink_, sendStatusKey_='UNKNOWN'): 129 | '''Compose data and try to send it.''' 130 | DEVNULL = chooseDevnull() 131 | 132 | fetchMode_ = sys.argv[1] 133 | senderDataNStr = '\n'.join(senderData_) # items for zabbix sender separated by newlines 134 | 135 | # pass senderDataNStr to sender_wrapper.py: 136 | if fetchMode_ == 'get': 137 | print(dumps({"data": jsonData_}, indent=4)) # print data gathered for LLD 138 | 139 | # spawn new process and regain shell control immediately (on Win 'sender_wrapper.py' will not wait) 140 | try: 141 | cmd = [sys.executable, senderPyPath_, fetchMode_, agentConf_, senderPath_, timeout_, senderDataNStr] 142 | 143 | subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=DEVNULL, stderr=DEVNULL, close_fds=(not isWindows())) 144 | 145 | except OSError as e: 146 | if e.args[0] == 7: 147 | subprocess.call([senderPath_, '-c', agentConf_, '-s', host_, '-k', sendStatusKey_, '-o', 'HUGEDATA']) 148 | else: 149 | subprocess.call([senderPath_, '-c', agentConf_, '-s', host_, '-k', sendStatusKey_, '-o', 'SEND_OS_ERROR']) 150 | 151 | except: 152 | subprocess.call( [senderPath_, '-c', agentConf_, '-s', host_, '-k', sendStatusKey_, '-o', 'UNKNOWN_SEND_ERROR']) 153 | 154 | elif fetchMode_ == 'getverb': 155 | displayVersions(agentConf_, senderPath_) 156 | readConfig(agentConf_) 157 | 158 | #for i in range(135000): senderDataNStr = senderDataNStr + '0' # HUGEDATA testing 159 | try: 160 | # do not detach if in verbose mode, also skips timeout in 'sender_wrapper.py' 161 | cmd = [sys.executable, senderPyPath_, 'getverb', agentConf_, senderPath_, timeout_, senderDataNStr] 162 | 163 | subprocess.Popen(cmd, stdin=subprocess.PIPE, close_fds=(not isWindows())) 164 | 165 | except OSError as e: 166 | if e.args[0] == 7: # almost unreachable in case of this script 167 | print(sys.argv[0] + ': Could not send anything. Argument list or filepath too long. (HUGEDATA)') # FileNotFoundError: [WinError 206] 168 | else: 169 | print(sys.argv[0] + ': Something went wrong. (SEND_OS_ERROR)') 170 | 171 | raise 172 | 173 | except: 174 | print(sys.argv[0] + ': Something went wrong. (UNKNOWN_SEND_ERROR)') 175 | raise 176 | 177 | finally: 178 | print(' Please report any issues or missing features to:\n%s\n' % issuesLink_) 179 | 180 | else: 181 | print(sys.argv[0] + ": Not supported. Use 'get' or 'getverb'.") 182 | 183 | 184 | def clearDiskTypeStr(s): 185 | stopWords = ( 186 | (' -d atacam'), (' -d scsi'), (' -d ata'), (' -d sat'), (' -d nvme'), 187 | (' -d sas'), (' -d csmi'), (' -d usb'), (' -d pd'), (' -d auto'), 188 | ) 189 | 190 | for i in stopWords: 191 | s = s.replace(i, '') 192 | 193 | s = s.strip() 194 | 195 | return s 196 | 197 | 198 | def removeQuotes(s): 199 | quotes = ('\'', '"') 200 | 201 | for i in quotes: 202 | s = s.replace(i, '') 203 | 204 | return s 205 | 206 | 207 | def sanitizeStr(s): 208 | '''Sanitizes provided string in sequential order.''' 209 | stopChars = ( 210 | ('/dev/', ''), (' -d', ''), 211 | ('!', '_'), (',', '_'), ('[', '_'), ('~', '_'), (' ', '_'), 212 | (']', '_'), ('+', '_'), ('/', '_'), ('\\', '_'), ('\'', '_'), 213 | ('`', '_'), ('@', '_'), ('#', '_'), ('$', '_'), ('%', '_'), 214 | ('^', '_'), ('&', '_'), ('*', '_'), ('(', '_'), (')', '_'), 215 | ('{', '_'), ('}', '_'), ('=', '_'), (':', '_'), (';', '_'), 216 | ('"', '_'), ('?', '_'), ('<', '_'), ('>', '_'), (' ', '_'), 217 | ) 218 | 219 | for i, j in stopChars: 220 | s = s.replace(i, j) 221 | 222 | s = s.strip() 223 | 224 | return s 225 | --------------------------------------------------------------------------------