├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── VERSION.txt ├── install.sh ├── miniprobe ├── __init__.py ├── miniprobe.py ├── probe.py ├── scripts │ ├── probe.tpl │ └── rotate.tpl ├── sensors │ ├── __init__.py │ ├── adns.py │ ├── apt.py │ ├── blacklist.py │ ├── cpuload.py │ ├── cputemp.py │ ├── diskspace.py │ ├── ds18b20.py │ ├── externalip.py │ ├── http.py │ ├── mdadm.py │ ├── memory.py │ ├── nmap.py │ ├── ping.py │ ├── port.py │ ├── portrange.py │ ├── postfix.py │ ├── probehealth.py │ ├── sensor.py │ ├── snmpcustom.py │ ├── snmpcustomstring.py │ ├── snmpdisk.py │ ├── snmpload.py │ ├── snmpmemory.py │ ├── snmpprocess.py │ └── snmptraffic.py └── tests │ ├── __init__.py │ ├── test_miniprobe.py │ └── test_sensors.py ├── requirements.txt ├── requirements3.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.log 3 | *.gz 4 | *.conf 5 | sensors/__init__.py 6 | dist 7 | *.egg-info 8 | build 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.2" 5 | - "3.3" 6 | - "3.4" 7 | - "3.5" 8 | os: 9 | - linux 10 | matrix: 11 | allow_failures: 12 | - python: "3.2" 13 | - python: "3.3" 14 | - python: "3.4" 15 | - python: "3.5" 16 | branches: 17 | only: 18 | - master 19 | - development 20 | install: 21 | - pip install --upgrade pip 22 | - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install -r requirements.txt; fi 23 | - if [[ $TRAVIS_PYTHON_VERSION == '3.2' ]]; then pip install -r requirements3.txt; fi 24 | - if [[ $TRAVIS_PYTHON_VERSION == '3.3' ]]; then pip install -r requirements3.txt; fi 25 | - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then pip install -r requirements3.txt; fi 26 | - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then pip install -r requirements3.txt; fi 27 | 28 | script: 29 | - nosetests -v 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Paessler AG 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | 14 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Important 2 | **From now on this project is completely deprecated and only here for reference!** 3 | Some time ago the Python Mini Probe project was rebooted and moved over to Gitlab. [Come over and check it out](https://gitlab.com/paessler-labs/prtg-pyprobe)! - Konstantin Wolff, Paessler AG 23/11/2020 4 | 5 | PythonMiniProbe 6 | =============== 7 | 8 | With the current version of PRTG, you can use the Mini Probe interface with your custom code to implement solutions to special scenarios that you might have in your network. Please note that there are major changes planned to the underlying API in PRTG. Therefore, any code you write now will likely need to be changed later, so it can be used for future versions of PRTG. 9 | Further news about changing interfaces will be provided here as soon as there are more detailed plans available. 10 | 11 | Current Status: BETA 12 | 13 | MiniProbe POC for PRTG Network Monitor written in Python which accesses the MiniProbe Interface on the PRTG Core Server. 14 | 15 | 16 | 17 | Build Status 18 | ------------ 19 | [![Build Status](https://travis-ci.org/PaesslerAG/PythonMiniProbe.svg?branch=development)](https://travis-ci.org/PaesslerAG/PythonMiniProbe) 20 | 21 | IMPORTANT: Major changes in this branch. Please see Migration Howto below! 22 | Installation 23 | ------------ 24 | - set up your PRTG server to use HTTPS (other connection methods not allowed at the moment) 25 | - allow MiniProbes to connect (Setup -> Probes -> Allow MiniProbes to connect) 26 | - make sure you can reach the PRTG web interface from the machine the mini probe should run on (e.g. wget https://YOUR_PRTG_SERVER) 27 | - This is tested during the setup 28 | - Install python-dev and build-essential (or at least gcc) packages 29 | - Install pip as outlined here https://pip.pypa.io/en/latest/installing.html (pre Python 2.7.9) 30 | - Download either zip or clone repository 31 | - run command 'sudo python setup.py install' which will install all necessary packages 32 | - run command 'sudo python setup.py configure' which will bring up the configuration dialogue 33 | 34 | The miniprobe should now be started. You should also be able to start/stop the same using the command 35 | 36 | sudo service prtgprobe start 37 | 38 | or 39 | 40 | sudo service prtgprobe stop 41 | 42 | 43 | Migration Guide 44 | --------------- 45 | - Copy away file probe.conf 46 | - Stop the mini probe process 47 | - Delete the files /etc/init.d/probe and /etc/logrotate.d/probe (filenames might be prtgprobe as well) 48 | - Remove the /probe folder 49 | - Install python-dev and build-essential (or at least gcc) packages 50 | - Install pip as outlined here https://pip.pypa.io/en/latest/installing.html (pre Python 2.7.9) 51 | - Download either zip or clone repository 52 | - Run command 'sudo python setup.py install' which will install all necessary packages and will run the configuration 53 | - [optional] Run 'install.sh' to skip the 4 steps above 54 | - Copy the gid line from your old probe.conf to the new probe.conf at /probe/miniprobe/probe.conf 55 | - Start the mini probe process, the mini probe will connect with the previous GID and continue monitoring 56 | IMPORTANT: If replacing the new probe.conf with the old one, make sure the line 'subprocs:10' is present in the file! 57 | 58 | 59 | Prerequisites 60 | ----------------- 61 | Debian based system (tested on Ubuntu, Debian, Raspbian) 62 | Python 2.7+ (preferred 2.9) 63 | Needed modules are installed using the setup.py install phase: 64 | Module list deprecated as pip will resolve dependencies! 65 | - pyasn1 (https://pypi.python.org/pypi/pyasn1/0.1.7) 66 | - pysnmp (https://pypi.python.org/pypi/pysnmp/4.2.5) 67 | - requests (https://pypi.python.org/pypi/requests/2.5.3) 68 | - dnspython (https://pypi.python.org/pypi/dnspython/1.12.0) 69 | 70 | 71 | Installation of DS18B20 72 | ---------------------- 73 | Requirements: 74 | - DS18B20 75 | - 4.7K Ohm resistor 76 | 77 | Setup: 78 | - Solder the resister between pin 2 and 3 of the DS18B20. If the flat part of the DS18B20 is facing you, then pin 2 and 3 are located at the right hand side starting from the middle. 79 | - Place pin 1 on pin 6 on the Raspberry. 80 | - Place pin 2 on pin 7 on the Raspberry. 81 | - Place pin 3 on pin 1 on the Raspberry. 82 | - Run the installscript of the probe and answer Yes to the question if you want to use the Raspberry Pi temperature sensor. 83 | - The installscript will now make a change to the raspberry boot process to include a special library and will then reboot the Raspberry. After the reboot, run the installer again and answer the same question again. It will now, if everything is correct, detect your DS18B20 using its own unique serial number. Just confirm that this is correct by pressing any key on your keyboard. 84 | 85 | Current available sensors 86 | ------------------------- 87 | - CPU Load (for probe only) 88 | - CPU Temperature (for probe only) 89 | - Disk Space (for probe only) 90 | - DNS for the following records: A, AAAA, CNAME, SRV, SOA, NS, MX, PTR 91 | - External IP to get the external and internal ip for the probe 92 | - HTTP 93 | - Linux Updates to check for the number of available updates (for probe only) 94 | - Memory (for probe only) 95 | - NMAP to get available systems to monitor (currently using ping only) 96 | - Ping to check if a host/system is up 97 | - Port to check if a specific port is available 98 | - Probe Health for overal probe health, combines several other sensors into 1 99 | - SNMP Custom to monitor a system using SNMP OID 100 | - SNMP Traffic to monitor traffic on the probe 101 | 102 | Debugging 103 | --------- 104 | 105 | To enable debugging, open the file probe.conf and replace the line 106 | 107 | debug: 108 | 109 | with 110 | 111 | debug:True 112 | 113 | This will enable detailed logging to folder "logs" which is as sub folder of the miniprobe folder. For further debugging, please stop the miniprobe process as outlined above. Navigate to the folder the file "probe.py" can be found then run following command "python probe.py". On major errors you will get the details and traceback directly on the console. 114 | 115 | Changelog 116 | ========= 117 | 118 | ======= 119 | 120 | 12-02-16 121 | -------- 122 | - Status update on future of Mini Probe API 123 | 124 | 23-07-2015 125 | ---------- 126 | MAJOR CHANGES: 127 | - added Python 3 compatibility 128 | 129 | MINOR CHANGES: 130 | - code cleanup in preparation for further capsuling 131 | - adjusted travis config to run nose tests for python 3 132 | 133 | 11-06-2015 134 | ---------- 135 | MAJOR CHANGES: 136 | - restructuring project layout accoriding to pip compliance 137 | - removed included modules 138 | - added support for pip/reqirements.txt 139 | - for installation, see above "Installation for this branch" 140 | - added tests folder for future unit tests 141 | - dropped probe_installer.py, code moved to setup.py 142 | - added release 143 | 144 | 07-05-2015 145 | ---------- 146 | - Finished the DNS Sensor for all dns types currently available in a Windows Probe 147 | - Added an APT sensor to check for available updates on the system 148 | 149 | 04-05-2015 150 | ---------- 151 | - Added dns sensor with support for A MX and SOA Records 152 | - Set the log message "Running Sensor: ..." to a debug message 153 | - Added the dnspython module for the dns sensor 154 | 155 | 10-03-2015 156 | ---------- 157 | - Added support for internal QA 158 | 159 | 24-02-2015 160 | ---------- 161 | - Added support for multiprocessing, now sensors are spawned as subprocesses (merged branch experimental with master) 162 | - fixed some indentation stuff (tabs instead of whitespaces) 163 | - fixed function get_config_key in probe_installer.py as key was set to None type all the time, fixed some sensors to put return data in the queue rather than simply returning it. 164 | - other minor fixes resp. code cleanup 165 | - preparation for a workflow (no direct commits to master any more) 166 | 167 | 14-02-2015 168 | ---------- 169 | - Added full support for the DS18B20 and a lot of cleanup and fixes 170 | - Also added the boot/config.txt fix for the DS18B20 that is needed on the RPi 171 | - Removed the no longer needed W1ThermSensor module from the repo 172 | as the Raspbian Image for raspberry already includes everything needed 173 | 174 | 02-02-2015 175 | ---------- 176 | - Installer cleanup and preparation for reading current config 177 | - Fix typo :( 178 | - Installer cleanup continued, added uninstall option to the installer, debug option added during installation 179 | - added W1ThermSensor module to the repo 180 | 181 | 26-01-2015 182 | ---------- 183 | - Merge pull request #2 from eagle00789/master 184 | -- Fixed Update-rd.d command 185 | -- Removed duplicate defined baseinterval check 186 | -- Fixed a bug in the installer that created the config file before any values where asked 187 | -- Added ping check for PRTG Server availability with possibility to still continue 188 | -- Added several checks and moved some code around to a function. 189 | 190 | 19-01-2015 191 | ---------- 192 | - added optional debug information 193 | 194 | 08-01-2015 195 | ---------- 196 | - fix for issue 1 197 | 198 | 05-11-2014 199 | ---------- 200 | - updated module requests 201 | 202 | 10-10-2014 203 | ---------- 204 | - dropped own logging 205 | -- now using python built in logging function (you might delete the file logger.py if existant) 206 | -- created file miniprobe.py which offers base functionality, probe.py now only does the work 207 | -- various changes and code cleanup 208 | -- added script templates for the probe_installer.py in folder /scripts 209 | -- changed version number to reflect YY.QQ.Release 210 | 211 | 28-08-2014 212 | ---------- 213 | - added module retry 214 | 215 | 26-08-2014 216 | ---------- 217 | - Updated module requests 218 | -- from now it is not necessary to use weakened security in the PRTG web server. There will be a one time warning if you are using a self signed certificate which can be ignored. 219 | - added VERSION.txt file 220 | -- the version number is built up from Year.Quarter.Buildnumber 221 | - moved Python version check to the beginning of the script 222 | - big code cleanup 223 | - applied PEP 8 rules to the code, some other refactoring). To be continued... (Probably tomorrow) 224 | 225 | 17-07-2014 226 | ---------- 227 | - Changed readme file, adjusted setup.py 228 | 229 | 07-07-2014 230 | ---------- 231 | - Fixed typos 232 | - Added check for logs folder 233 | 234 | 27-06-2014 235 | ---------- 236 | - Updated documentation 237 | - Merge Remote-tracking branch 238 | 239 | 26-06-2014 240 | ---------- 241 | - Updated Readme 242 | - Changed line separators 243 | - Initial Commit 244 | - Changed readme file 245 | - deleted readme 246 | 247 | 25-06-2014 248 | ---------- 249 | - Initial commit 250 | 251 | -------------------------------------------------------------------------------- /VERSION.txt: -------------------------------------------------------------------------------- 1 | 16.1.22 -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DOWNLOADED=false 4 | if [ "$(id -u)" != "0" ]; then 5 | echo "Sorry, you are not root." 6 | exit 1 7 | fi 8 | 9 | echo "This script will guide you to install the PythonMiniProbe" 10 | 11 | echo "installing python-dev and build-essentials" 12 | apt-get -y install python-dev build-essential 2>&1 >> /tmp/probe_install.log 13 | 14 | case "$(python --version 2>&1)" in 15 | *" 2.7.9"*) 16 | echo "Correct python version!" 17 | ;; 18 | *" 3."*) 19 | echo "Correct python version!" 20 | ;; 21 | *) 22 | echo "Installing PIP!" 23 | apt-get -y install python-pip 2>&1 >> /tmp/probe_install.log 24 | ;; 25 | esac 26 | 27 | if [ ! -f ./README.md ] 28 | then 29 | read -p "Use git to install the miniprobe (y|n)? " -n 1 -r 30 | echo # (optional) move to a new line 31 | if [[ $REPLY =~ ^[Yy]$ ]] 32 | then 33 | git clone https://github.com/PaesslerAG/PythonMiniProbe.git /PythonMiniProbe 34 | cd /PythonMiniProbe 35 | DOWNLOADED=true 36 | else 37 | read -p "Use wget to install the miniprobe (y|n)? " -n 1 -r 38 | echo # (optional) move to a new line 39 | if [[ $REPLY =~ ^[Yy]$ ]] 40 | then 41 | wget -O /tmp/probe.zip https://github.com/PaesslerAG/PythonMiniProbe/archive/master.zip 42 | unzip /tmp/probe.zip -d /tmp 43 | mv /tmp/PythonMiniProbe-master /PythonMiniProbe 44 | cd /PythonMiniProbe 45 | DOWNLOADED=true 46 | fi 47 | fi 48 | else 49 | DOWNLOADED=true 50 | fi 51 | 52 | if [ "$DOWNLOADED" = true ] 53 | then 54 | echo "Starting to install the miniprobe and requirements" 55 | python setup.py install 56 | fi 57 | -------------------------------------------------------------------------------- /miniprobe/__init__.py: -------------------------------------------------------------------------------- 1 | #Copyright (c) 2014, Paessler AG 2 | #All rights reserved. 3 | #Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 4 | # following conditions are met: 5 | #1. Redistributions of source code must retain the above copyright notice, this list of conditions 6 | # and the following disclaimer. 7 | #2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 8 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | #3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 10 | # or promote products derived from this software without specific prior written permission. 11 | 12 | #THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 13 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 14 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 15 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 16 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 17 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 18 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 19 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 20 | -------------------------------------------------------------------------------- /miniprobe/miniprobe.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | 23 | # PRTG Python Miniprobe 24 | # Miniprobe needs at least Python 2.7 because of "importlib" 25 | # If older python version is used you will have to install "importlib" 26 | 27 | # import general modules 28 | import sys 29 | import hashlib 30 | import importlib 31 | import gc 32 | import logging 33 | import subprocess 34 | import os 35 | import requests 36 | import warnings 37 | 38 | from requests.packages.urllib3 import exceptions 39 | 40 | # import own modules 41 | sys.path.append('./') 42 | 43 | try: 44 | import sensors 45 | except Exception as e: 46 | print(e) 47 | 48 | 49 | class MiniProbe(object): 50 | """ 51 | Main class for the Python Mini Probe 52 | """ 53 | def __init__(self, http): 54 | gc.enable() 55 | self.http = http 56 | logging.basicConfig( 57 | filename="./logs/probe.log", 58 | filemode="a", 59 | level=logging.INFO, 60 | format="%(asctime)s - %(levelname)s - %(message)s", 61 | datefmt='%m/%d/%Y %H:%M:%S' 62 | ) 63 | 64 | def get_import_sensors(self): 65 | """ 66 | import available sensor modules and return list of sensor objects 67 | """ 68 | sensor_objects = [] 69 | for mod in sensors.__all__: 70 | try: 71 | sensor_objects.append(self.load_class("sensors.%s.%s" % (mod.lower(), mod))) 72 | except Exception as import_error: 73 | logging.error("Sensor Import Error! Error message: %s" % import_error) 74 | return sensor_objects 75 | 76 | @staticmethod 77 | def load_class(full_class_string): 78 | """ 79 | dynamically load a class from a string 80 | """ 81 | class_data = full_class_string.split(".") 82 | module_path = ".".join(class_data[:-1]) 83 | class_str = class_data[-1] 84 | module = importlib.import_module(module_path) 85 | return getattr(module, class_str) 86 | 87 | def read_config(self, path): 88 | """ 89 | read configuration file and write data to dict 90 | """ 91 | config = {} 92 | try: 93 | conf_file = open(path) 94 | for line in conf_file: 95 | if not (line == '\n'): 96 | if not (line.startswith('#')): 97 | config[line.split(':')[0]] = line.split(':')[1].rstrip() 98 | conf_file.close() 99 | return config 100 | except Exception as read_error: 101 | logging.error("No config found! Error Message: %s Exiting!" % read_error) 102 | sys.exit() 103 | 104 | @staticmethod 105 | def hash_access_key(key): 106 | """ 107 | create hash of probes access key 108 | """ 109 | key = key.encode('utf-8') 110 | return hashlib.sha1(key).hexdigest() 111 | 112 | def create_parameters(self, config, jsondata, i=None): 113 | """ 114 | create URL parameters for announce, task and data requests 115 | """ 116 | if i == 'announce': 117 | return {'gid': config['gid'], 'key': self.hash_access_key(config['key']), 'protocol': config['protocol'], 118 | 'name': config['name'], 'baseinterval': config['baseinterval'], 'sensors': jsondata} 119 | else: 120 | return {'gid': config['gid'], 'key': self.hash_access_key(config['key']), 'protocol': config['protocol']} 121 | 122 | def create_url(self, config, i=None, http=False): 123 | """ 124 | creating the actual URL 125 | """ 126 | prefix = "https" 127 | if http: 128 | prefix = "http" 129 | 130 | if not (i is None) and (i != "data"): 131 | return "%s://%s:%s/probe/%s" % ( 132 | prefix, config['server'], config['port'], i) 133 | elif i == "data": 134 | return "%s://%s:%s/probe/%s?gid=%s&protocol=%s&key=%s" % (prefix, config['server'], config['port'], i, 135 | config['gid'], config['protocol'], 136 | self.hash_access_key(config['key'])) 137 | pass 138 | else: 139 | return "No method given" 140 | 141 | def build_announce(self, sensor_list): 142 | """ 143 | build json for announce request 144 | """ 145 | sensors_avail = [] 146 | for sensor in sensor_list: 147 | if not sensor.get_sensordef() == "": 148 | sensors_avail.append(sensor.get_sensordef()) 149 | return sensors_avail 150 | 151 | def build_task(self, config): 152 | """ 153 | build data payload for task request. 154 | """ 155 | task = { 156 | 'gid': config['gid'], 157 | 'protocol': config['protocol'], 158 | 'key': self.hash_access_key(config['key']) 159 | } 160 | return task 161 | 162 | def request_to_core(self, req_type, data, config): 163 | """ 164 | perform different request types to the core 165 | """ 166 | url = self.create_url(config, req_type, self.http) 167 | try: 168 | with warnings.catch_warnings(): 169 | warnings.simplefilter("ignore", exceptions.InsecureRequestWarning) 170 | request_to_core = requests.post(url, data=data, verify=False, timeout=30) 171 | logging.info("%s request successfully sent to PRTG Core Server at %s:%s." 172 | % (req_type, config["server"], config["port"])) 173 | logging.debug("Connecting to %s:%s" % (config["server"], config["port"])) 174 | logging.debug("Status Code: %s | Message: %s" % (request_to_core.status_code, request_to_core.text)) 175 | return request_to_core 176 | except requests.exceptions.Timeout: 177 | logging.error("%s Timeout: %s" % (req_type, str(data))) 178 | raise 179 | except Exception as req_except: 180 | logging.error("Exception %s!" % req_except) 181 | raise 182 | 183 | def split_json_response(self, json_response, size=None): 184 | """ 185 | split up response from task request into predefined chunk sizes 186 | """ 187 | if not size: 188 | size = "10" 189 | return [json_response[i:i + int(size)] for i in range(0, len(json_response), int(size))] 190 | 191 | @staticmethod 192 | def clean_mem(): 193 | """Ugly brute force method to clean up Mem""" 194 | subprocess.call("sync", shell=False) 195 | os.popen("sysctl vm.drop_caches=1") 196 | os.popen("sysctl vm.drop_caches=2") 197 | os.popen("sysctl vm.drop_caches=3") 198 | -------------------------------------------------------------------------------- /miniprobe/probe.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | # PRTG Python Miniprobe 23 | # Miniprobe needs at least Python 2.7 because of "importlib" 24 | # If older python version is used you will have to install "importlib" 25 | 26 | # import general modules 27 | import sys 28 | import json 29 | import time 30 | import gc 31 | import logging 32 | import socket 33 | 34 | # import own modules 35 | sys.path.append('./') 36 | try: 37 | from miniprobe import MiniProbe 38 | import sensors 39 | import requests 40 | import multiprocessing 41 | except Exception as e: 42 | print(e) 43 | 44 | # Implemented for internal testing only. Not for public usage! 45 | http = False 46 | if sys.argv[1:] and sys.argv[1] == "http": 47 | http = True 48 | 49 | 50 | class Probe(object): 51 | def __init__(self): 52 | gc.enable() 53 | self.mini_probe = MiniProbe(http) 54 | self.config = self.mini_probe.read_config('./probe.conf') 55 | self.probe_stop = False 56 | self.announce = False 57 | self.task = False 58 | self.has_json_content = False 59 | self.data_request_payload_json = [] 60 | self.task_request_response_json = [] 61 | self.key_sha1 = self.mini_probe.hash_access_key(self.config['key']) 62 | self.out_queue = multiprocessing.Queue() 63 | self.sensor_list = self.mini_probe.get_import_sensors() 64 | self.announce_json = json.dumps(self.mini_probe.build_announce(self.sensor_list)) 65 | self.announce_data = self.mini_probe.create_parameters(self.config, self.announce_json, 'announce') 66 | self.url_announce = self.mini_probe.create_url(self.config, 'announce', http) 67 | self.url_task = self.mini_probe.create_url(self.config, 'tasks', http) 68 | self.task_data = self.mini_probe.build_task(self.config) 69 | self.url_data = self.mini_probe.create_url(self.config, 'data', http) 70 | self.procs = [] 71 | # Set up debug logging 72 | self.logger = logging.getLogger("") 73 | if self.config['debug'] == "True": 74 | self.config['debug'] = True 75 | self.logger.setLevel(logging.DEBUG) 76 | logging.warning("DEBUG LOGGING HAS BEEN TURNED ON!!") 77 | logging.getLogger("requests").setLevel(logging.INFO) 78 | else: 79 | self.config['debug'] = False 80 | self.logger.setLevel(logging.INFO) 81 | logging.info("Debug logging has been turned off!!") 82 | logging.getLogger("requests").setLevel(logging.WARNING) 83 | if self.config['cleanmem'] == "True": 84 | self.config['cleanmem'] = True 85 | else: 86 | self.config['cleanmem'] = False 87 | 88 | def send_announce(self): 89 | """ 90 | send announce request to core 91 | """ 92 | try: 93 | announce_request = self.mini_probe.request_to_core("announce", self.announce_data, self.config) 94 | if announce_request.status_code == requests.codes.ok: 95 | self.announce = True 96 | logging.info("Announce success.") 97 | logging.debug("Announce success. Details: HTTP Status %s, Message: %s" 98 | % (announce_request.status_code, announce_request.text)) 99 | else: 100 | logging.info("Announce pending. Trying again in %s seconds" 101 | % str(int(self.config['baseinterval']) / 2)) 102 | logging.debug("Announce pending. Details: HTTP Status %s, Message: %s" 103 | % (announce_request.status_code, announce_request.text)) 104 | time.sleep(int(self.config['baseinterval']) / 2) 105 | except Exception as request_exception: 106 | logging.error(request_exception) 107 | time.sleep(int(self.config['baseinterval']) / 2) 108 | 109 | def get_tasks(self): 110 | """ 111 | get tasks from core 112 | """ 113 | self.data_request_payload_json = [] 114 | self.has_json_content = False 115 | try: 116 | task_request = self.mini_probe.request_to_core("tasks", self.task_data, self.config) 117 | try: 118 | if str(task_request.json()) != "[]": 119 | self.has_json_content = True 120 | self.task = True 121 | logging.info("Task success.") 122 | logging.debug("Task success. HTTP Status %s, Message: %s" 123 | % (task_request.status_code, task_request.text)) 124 | return task_request 125 | else: 126 | logging.info("Task has no JSON content. Trying again in %s seconds" 127 | % (int(self.config['baseinterval']) / 2)) 128 | logging.debug("Task has no JSON content. Details: HTTP Status %s, Message: %s" 129 | % (task_request.status_code, task_request.text)) 130 | return None 131 | except Exception as json_exception: 132 | logging.info(json_exception) 133 | logging.info("No JSON. HTTP Status: %s, Message: %s" % (task_request.status_code, task_request.text)) 134 | return None 135 | except Exception as request_exception: 136 | logging.error(request_exception) 137 | logging.error("Exception. Trying again in %s seconds." % str(int(self.config['baseinterval']) / 3)) 138 | return None 139 | 140 | def send_data(self): 141 | """ 142 | send processed data to the core 143 | """ 144 | try: 145 | data_request = self.mini_probe.request_to_core("data", json.dumps(self.data_request_payload_json), 146 | self.config) 147 | if data_request.status_code == requests.codes.ok: 148 | logging.info("Data success.") 149 | logging.debug("Data success. Details: HTTP Status %s, Message: %s" 150 | % (data_request.status_code, data_request.text)) 151 | self.data_request_payload_json = [] 152 | else: 153 | logging.info("Data issue. Current data might be dropped, please turn on debug logging") 154 | logging.debug("Data issue. Details: HTTP Status %s, Message: %s" 155 | % (data_request.status_code, data_request.text)) 156 | except Exception as request_exception: 157 | logging.error(request_exception) 158 | 159 | def kill_procs(self): 160 | """ 161 | killing processes in worker pool when finished 162 | """ 163 | for p in self.procs: 164 | if not p.is_alive(): 165 | p.join() 166 | p.terminate() 167 | del p 168 | 169 | def main(self): 170 | """ 171 | Main routine for MiniProbe (Python) 172 | """ 173 | # Doing some startup logging 174 | logging.info("PRTG Small Probe '%s' starting on '%s'" % (self.config['name'], socket.gethostname())) 175 | logging.info("Connecting to PRTG Core Server at %s:%s" % (self.config['server'], self.config['port'])) 176 | while not self.announce: 177 | self.send_announce() 178 | 179 | while not self.probe_stop: 180 | self.task = False 181 | while not self.task: 182 | task_request = self.get_tasks() 183 | if not task_request: 184 | time.sleep(int(self.config['baseinterval']) / 2) 185 | 186 | gc.collect() 187 | if task_request.status_code == requests.codes.ok and self.has_json_content: 188 | self.task_request_response_json = task_request.json() 189 | logging.debug("JSON response: %s" % self.task_request_response_json) 190 | if self.config['subprocs']: 191 | json_response_chunks = self.mini_probe.split_json_response(self.task_request_response_json, 192 | self.config['subprocs']) 193 | else: 194 | json_response_chunks = self.mini_probe.split_json_response(self.task_request_response_json) 195 | for element in json_response_chunks: 196 | for part in element: 197 | logging.debug(part) 198 | for sensor in self.sensor_list: 199 | if part['kind'] == sensor.get_kind(): 200 | p = multiprocessing.Process(target=sensor.get_data, args=(part, self.out_queue), 201 | name=part['kind']) 202 | self.procs.append(p) 203 | logging.debug("Spawning sensor %s." % p.name) 204 | p.start() 205 | else: 206 | pass 207 | gc.collect() 208 | try: 209 | while len(self.data_request_payload_json) < len(element): 210 | out = self.out_queue.get() 211 | self.data_request_payload_json.append(out) 212 | except Exception as data_queue_exception: 213 | logging.error(data_queue_exception) 214 | pass 215 | 216 | self.send_data() 217 | 218 | if len(self.task_request_response_json) > 10: 219 | time.sleep((int(self.config['baseinterval']) * (9 / len(self.task_request_response_json)))) 220 | else: 221 | time.sleep(int(self.config['baseinterval']) / 2) 222 | elif task_request.status_code != requests.codes.ok: 223 | logging.info("Task issue. Request returning incorrect status code. Turn on debugging for details") 224 | logging.debug("Task issue. Details: HTTP Status %s, Message: %s" 225 | % (task_request.status_code, task_request.text)) 226 | else: 227 | logging.info("Task has no JSON content. Nothing to do. Waiting for %s seconds." 228 | % str(int(self.config['baseinterval']) / 3)) 229 | time.sleep(int(self.config['baseinterval']) / 3) 230 | 231 | self.kill_procs() 232 | gc.collect() 233 | 234 | if self.config['cleanmem']: 235 | # checking if clean memory option has been chosen during install then call the method to flush mem 236 | self.mini_probe.clean_mem() 237 | sys.exit() 238 | 239 | if __name__ == "__main__": 240 | probe = Probe() 241 | probe.main() 242 | -------------------------------------------------------------------------------- /miniprobe/scripts/probe.tpl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ### BEGIN INIT INFO 4 | # Provides: prtgprobe 5 | # Required-Start: $remote_fs $syslog 6 | # Required-Stop: $remote_fs $syslog 7 | # Default-Start: 2 3 4 5 8 | # Default-Stop: 0 1 6 9 | # Short-Description: will run the MiniProbe as service 10 | # Description: This will run the PRTG Python Miniprobe as a service 11 | ### END INIT INFO 12 | 13 | # Change the next 3 lines to suit where you install your script and what you want to call it 14 | DIR=%s 15 | DAEMON=$DIR/probe.py 16 | DAEMON_NAME=prtgprobe 17 | 18 | # This next line determines what user the script runs as. 19 | # Root generally not recommended but necessary if you are using the Raspberry Pi GPIO from Python. 20 | DAEMON_USER=%s 21 | 22 | # The process ID of the script when it runs is stored here: 23 | PIDFILE=/var/run/$DAEMON_NAME.pid 24 | 25 | . /lib/lsb/init-functions 26 | 27 | do_start () { 28 | log_daemon_msg "Starting system $DAEMON_NAME daemon" 29 | start-stop-daemon --start --background --pidfile $PIDFILE --make-pidfile --chdir $DIR --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON 30 | log_end_msg $? 31 | } 32 | do_stop () { 33 | log_daemon_msg "Stopping system $DAEMON_NAME daemon" 34 | start-stop-daemon --stop --pidfile $PIDFILE --retry 10 35 | log_end_msg $? 36 | } 37 | 38 | case "$1" in 39 | 40 | start|stop) 41 | do_${1} 42 | ;; 43 | 44 | restart|reload|force-reload) 45 | do_stop 46 | do_start 47 | ;; 48 | 49 | status) 50 | status_of_proc -p $PIDFILE "$DAEMON" "$DAEMON_NAME" && exit 0 || exit $? 51 | ;; 52 | *) 53 | echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}" 54 | exit 1 55 | ;; 56 | 57 | esac 58 | exit 0 59 | -------------------------------------------------------------------------------- /miniprobe/scripts/rotate.tpl: -------------------------------------------------------------------------------- 1 | %s/logs/probe.log { 2 | rotate 4 3 | daily 4 | dateext 5 | compress 6 | missingok 7 | copytruncate 8 | } 9 | -------------------------------------------------------------------------------- /miniprobe/sensors/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Paessler AG 2 | # All rights reserved. 3 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 4 | # following conditions are met: 5 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 6 | # and the following disclaimer. 7 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 8 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 10 | # or promote products derived from this software without specific prior written permission. 11 | 12 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 13 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 14 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 15 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 16 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 17 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 18 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 19 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 20 | 21 | # Announce modules available in this package 22 | # Just extend this list for your modules and they will be automatically imported during runtime and 23 | # are announced to the PRTG Core 24 | __all__ = ['Ping', 'HTTP', 'Port', 'SNMPCustom', 'SNMPCustomString', 'SNMPLoad', 'SNMPMemory', 'SNMPDisk', 'SNMPProcess', 'CPULoad', 'Memory', 'Diskspace', 'SNMPTraffic', 'CPUTemp', 'Probehealth', 'ExternalIP', 'ADNS', 'APT', 'NMAP', 'MDADM', 'Postfix'] 25 | -------------------------------------------------------------------------------- /miniprobe/sensors/adns.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import gc 23 | import logging 24 | import timeit 25 | try: 26 | import dns.resolver 27 | import dns.reversename 28 | dnscheck = True 29 | except Exception as e: 30 | logging.error("PyDNS could not be imported. DNS Sensor won't work.Error: %s" % e) 31 | dnscheck = False 32 | pass 33 | 34 | 35 | class ADNS(object): 36 | def __init__(self): 37 | gc.enable() 38 | 39 | @staticmethod 40 | def get_kind(): 41 | """ 42 | return sensor kind 43 | """ 44 | return "mpdns" 45 | 46 | @staticmethod 47 | def get_sensordef(): 48 | """ 49 | Definition of the sensor and data to be shown in the PRTG WebGUI 50 | """ 51 | sensordefinition = { 52 | "kind": ADNS.get_kind(), 53 | "name": "DNS", 54 | "description": "Monitors a DNS server (Domain Name Service), " 55 | "resolves a domain name, and compares it to an IP address", 56 | "help": "The DNS sensor monitors a Domain Name Service (DNS) server. " 57 | "It resolves a domain name and compares it to a given IP address.", 58 | "tag": "mpdnssensor", 59 | "groups": [ 60 | { 61 | "name": "DNS Specific", 62 | "caption": "DNS Specific", 63 | "fields": [ 64 | { 65 | "type": "integer", 66 | "name": "timeout", 67 | "caption": "Timeout (in s)", 68 | "required": "1", 69 | "default": 5, 70 | "minimum": 1, 71 | "maximum": 900, 72 | "help": "Timeout in seconds. A maximum value of 900 is allowed." 73 | }, 74 | { 75 | "type": "integer", 76 | "name": "port", 77 | "caption": "Port", 78 | "required": "1", 79 | "default": 53, 80 | "minimum": 1, 81 | "maximum": 65535, 82 | "help": "Enter the port on which the DNS service of the parent device is running." 83 | }, 84 | { 85 | "type": "edit", 86 | "name": "domain", 87 | "caption": "Domain", 88 | "required": "1", 89 | "help": "Enter a DNS name or IP address to resolve." 90 | }, 91 | { 92 | "type": "radio", 93 | "name": "type", 94 | "caption": "Query Type", 95 | "required": "1", 96 | "help": "Specify the type of query that the sensor will send to the DNS server.", 97 | "options": { 98 | "A": "Host address IPv4 (A)", 99 | "AAAA": "Host address IPv6 (AAAA)", 100 | "CNAME": "Canonical name for an alias (CNAME)", 101 | "MX": "Mail exchange (MX)", 102 | "NS": "Authoritative name server (NS)", 103 | "PTR": "Domain name pointer (PTR)", 104 | "SOA": "Start of a zone of authority marker (SOA)", 105 | "SRV": "Service Record" 106 | }, 107 | "default": "A", 108 | }, 109 | ] 110 | } 111 | ] 112 | } 113 | if not dnscheck: 114 | sensordefinition = "" 115 | return sensordefinition 116 | 117 | @staticmethod 118 | def get_data(data, out_queue): 119 | adns = ADNS() 120 | logging.debug("Running sensor: %s" % adns.get_kind()) 121 | try: 122 | start_time = timeit.default_timer() 123 | result = adns.get_record(data['timeout'], data['port'], data['domain'], data['type'], data['host']) 124 | timed = timeit.default_timer() - start_time 125 | logging.debug("DNS: %s" % result) 126 | except Exception as ex: 127 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (adns.get_kind(), 128 | data['sensorid'], ex)) 129 | data_r = { 130 | "sensorid": int(data['sensorid']), 131 | "error": "Exception", 132 | "code": 1, 133 | "message": "DNS sensor failed. See log for details" 134 | } 135 | out_queue.put(data_r) 136 | return 1 137 | dns_channel = adns.get_dns(int(timed * 1000)) 138 | addressdata = [] 139 | for element in dns_channel: 140 | addressdata.append(element) 141 | if result[0:9] == "DNS Error": 142 | data = { 143 | "sensorid": int(data['sensorid']), 144 | "error": "Exception", 145 | "code": 1, 146 | "message": result 147 | } 148 | else: 149 | data = { 150 | "sensorid": int(data['sensorid']), 151 | "message": data['type'] + ": " + result, 152 | "channel": addressdata 153 | } 154 | del result 155 | gc.collect() 156 | out_queue.put(data) 157 | return 0 158 | 159 | @staticmethod 160 | def get_dns(time): 161 | channel_list = [{"name": "Response Time", 162 | "ShowChart": 0, 163 | "ShowTable": 0, 164 | "mode": "integer", 165 | "kind": "Custom", 166 | "customunit": "ms", 167 | "value": time}] 168 | return channel_list 169 | 170 | @staticmethod 171 | def get_record(timeout, port, domain, type, host): 172 | result = domain + ": " 173 | try: 174 | resolver = dns.resolver.Resolver(configure=False) 175 | resolver.nameservers = [host] 176 | resolver.port = port 177 | if type == 'PTR': 178 | addr = dns.reversename.from_address(domain) 179 | answers = dns.resolver.query(addr, type) 180 | else: 181 | answers = dns.resolver.query(domain, type) 182 | if (type == 'A') or (type == 'AAAA'): 183 | for rdata in answers: 184 | result = result + str(rdata.address) + ", " 185 | elif type == 'MX': 186 | for rdata in answers: 187 | result = result + rdata.preference + ": " + rdata.exchange + ", " 188 | elif type == 'SOA': 189 | for rdata in answers: 190 | result = result + "NS: " + str(rdata.mname) + ", TECH: " + str(rdata.rname) + ", S/N: " + str(rdata.serial) + ", Refresh: " + str(rdata.refresh / 60) + " min, Expire: " \ 191 | + str(rdata.expire / 60) + " min " 192 | elif (type == 'CNAME') or (type == 'NS') or (type == 'PTR'): 193 | for rdata in answers: 194 | result = result + str(rdata.target) + ", " 195 | except dns.resolver.NoAnswer: 196 | result = "DNS Error while getting %s record. " \ 197 | "This could be the result of a misconfiguration in the sensor settings" % type 198 | return result[:-2] 199 | -------------------------------------------------------------------------------- /miniprobe/sensors/apt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import os 23 | import gc 24 | import logging 25 | 26 | 27 | class APT(object): 28 | def __init__(self): 29 | gc.enable() 30 | 31 | @staticmethod 32 | def get_kind(): 33 | """ 34 | return sensor kind 35 | """ 36 | return "mpapt" 37 | 38 | @staticmethod 39 | def get_sensordef(): 40 | """ 41 | Definition of the sensor and data to be shown in the PRTG WebGUI 42 | """ 43 | sensordefinition = { 44 | "kind": APT.get_kind(), 45 | "name": "Linux Updates", 46 | "description": "Monitors the available updates for the linux system", 47 | "help": "Monitors the available updates for the linux system using apt-get/yum", 48 | "tag": "mpaptsensor", 49 | "fields": [], 50 | "groups": [] 51 | } 52 | return sensordefinition 53 | 54 | def check(self): 55 | locale = os.getenv('LANG') 56 | upgrade = 0 57 | install = 0 58 | remove = 0 59 | ret = os.popen("LC_ALL=C apt-get -s dist-upgrade | grep 'newly inst'") 60 | updatedata = ret.readlines() 61 | ret.close() 62 | for line in updatedata: 63 | upgrade = int(line.split(" ")[0].lstrip()) 64 | install = int(line.split(" ")[2].lstrip()) 65 | remove = int(line.split(" ")[5].lstrip()) 66 | total = upgrade + install + remove 67 | channel_list = [ 68 | { 69 | "name": "Total updates available", 70 | "mode": "integer", 71 | "unit": "Count", 72 | "limitmaxwarning": 1, 73 | "limitmode": 1, 74 | "value": total 75 | }, 76 | { 77 | "name": "Upgrades", 78 | "mode": "integer", 79 | "unit": "Count", 80 | "value": upgrade 81 | }, 82 | { 83 | "name": "New to install", 84 | "mode": "integer", 85 | "unit": "Count", 86 | "value": install 87 | }, 88 | { 89 | "name": "Old to remove", 90 | "mode": "integer", 91 | "unit": "Count", 92 | "limitmaxwarning": 1, 93 | "LimitMode": 1, 94 | "value": remove 95 | }, 96 | ] 97 | return channel_list 98 | 99 | @staticmethod 100 | def get_data(data, out_queue): 101 | apt = APT() 102 | try: 103 | aptdata = apt.check() 104 | data_r = { 105 | "sensorid": int(data['sensorid']), 106 | "message": "OK", 107 | "channel": aptdata 108 | } 109 | logging.debug("Running sensor: %s" % apt.get_kind()) 110 | except Exception as e: 111 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (apt.get_kind(), 112 | data['sensorid'], e)) 113 | data_r = { 114 | "sensorid": int(data['sensorid']), 115 | "error": "Exception", 116 | "code": 1, 117 | "message": "APT failed. %s" % e 118 | } 119 | out_queue.put(data_r) 120 | return 1 121 | del apt 122 | gc.collect() 123 | out_queue.put(data_r) 124 | return 0 125 | -------------------------------------------------------------------------------- /miniprobe/sensors/blacklist.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import gc 23 | import logging 24 | try: 25 | import dns.resolver 26 | import dns.reversename 27 | dnscheck = True 28 | except Exception as e: 29 | logging.error("PyDNS could not be imported. DNS Sensor won't work.Error: %s" % e) 30 | dnscheck = False 31 | pass 32 | 33 | class Blacklist(object): 34 | blacklists = [] 35 | blacklists.append("zen.spamhaus.org") 36 | blacklists.append("spam.abuse.ch") 37 | blacklists.append("cbl.abuseat.org") 38 | blacklists.append("virbl.dnsbl.bit.nl") 39 | blacklists.append("dnsbl.inps.de") 40 | blacklists.append("ix.dnsbl.manitu.net") 41 | blacklists.append("dnsbl.sorbs.net") 42 | blacklists.append("bl.spamcannibal.org") 43 | blacklists.append("bl.spamcop.net") 44 | blacklists.append("xbl.spamhaus.org") 45 | blacklists.append("pbl.spamhaus.org") 46 | blacklists.append("dnsbl-1.uceprotect.net") 47 | blacklists.append("dnsbl-2.uceprotect.net") 48 | blacklists.append("dnsbl-3.uceprotect.net") 49 | blacklists.append("db.wpbl.info") 50 | blacklists.append("bsb.emtpy.us") 51 | blacklists.append("dnsbl.anticaptcha.net") 52 | blacklists.append("aspews.ext.sorbs.net") 53 | blacklists.append("ips.backscatterer.org") 54 | blacklists.append("b.barracudacentral.org") 55 | blacklists.append("bl.blocklist.de") 56 | blacklists.append("dnsbl.burnt-tech.com") 57 | blacklists.append("cblless.anti-spam.org.cn") 58 | blacklists.append("bogons.cymru.com") 59 | blacklists.append("fullbogons.cymru.com") 60 | blacklists.append("tor.dan.me.uk") 61 | blacklists.append("torexit.dan.me.uk") 62 | blacklists.append("rbl.dns-servicios.com") 63 | blacklists.append("bl.drmx.org") 64 | blacklists.append("dnsbl.dronebl.org") 65 | blacklists.append("rbl.efnetrbl.org") 66 | 67 | def __init__(self): 68 | gc.enable() 69 | 70 | @staticmethod 71 | def get_kind(): 72 | """ 73 | return sensor kind 74 | """ 75 | return "mpblacklist" 76 | 77 | @staticmethod 78 | def get_sensordef(): 79 | """ 80 | Definition of the sensor and data to be shown in the PRTG WebGUI 81 | """ 82 | sensordefinition = { 83 | "kind": Blacklist.get_kind(), 84 | "name": "Blacklist", 85 | "description": "Monitors a server for blacklisting", 86 | "help": "The Blacklist sensor monitors a server for blacklisting", 87 | "tag": "mpblacklist", 88 | "groups": [ 89 | { 90 | "name": "Blacklist Specific", 91 | "caption": "Blacklist Specific", 92 | "fields": [ 93 | { 94 | "type": "integer", 95 | "name": "timeout", 96 | "caption": "Timeout (in s)", 97 | "required": "1", 98 | "default": 5, 99 | "minimum": 1, 100 | "maximum": 900, 101 | "help": "Timeout in seconds. A maximum value of 900 is allowed." 102 | }, 103 | { 104 | "type": "edit", 105 | "name": "domain", 106 | "caption": "Domain", 107 | "required": "1", 108 | "help": "Enter a DNS name or IP address to check for blacklisting." 109 | } 110 | ] 111 | } 112 | ] 113 | } 114 | if not dnscheck: 115 | sensordefinition = "" 116 | return sensordefinition 117 | 118 | @staticmethod 119 | def get_data(data, out_queue): 120 | blacklist = Blacklist() 121 | logging.debug("Running sensor: %s" % blacklist.get_kind()) 122 | try: 123 | result = blacklist.get_record(data['timeout'], data['domain']) 124 | logging.debug("Blacklist: %s" % result) 125 | except Exception as ex: 126 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (blacklist.get_kind(), 127 | data['sensorid'], ex)) 128 | data_r = { 129 | "sensorid": int(data['sensorid']), 130 | "error": "Exception", 131 | "code": 1, 132 | "message": "Blacklist sensor failed. See log for details" 133 | } 134 | out_queue.put(data_r) 135 | return 1 136 | dns_channel = blacklist.get_blacklist(result) 137 | addressdata = [] 138 | for element in dns_channel: 139 | addressdata.append(element) 140 | if result[0:9] == "DNS Error": 141 | data = { 142 | "sensorid": int(data['sensorid']), 143 | "error": "Exception", 144 | "code": 1, 145 | "message": result 146 | } 147 | else: 148 | data = { 149 | "sensorid": int(data['sensorid']), 150 | "message": result[0], 151 | "channel": addressdata 152 | } 153 | del result 154 | gc.collect() 155 | out_queue.put(data) 156 | return 0 157 | 158 | @staticmethod 159 | def get_blacklist(result): 160 | channel_list = [{"name": "Listed Count", 161 | "ShowChart": 0, 162 | "ShowTable": 0, 163 | "mode": "integer", 164 | "kind": "Custom", 165 | "customunit": "", 166 | "limitmaxerror": 0, 167 | "limitmode": 1, 168 | "value": result[1]}, 169 | {"name": "Not Listed Count", 170 | "ShowChart": 0, 171 | "ShowTable": 0, 172 | "mode": "integer", 173 | "kind": "Custom", 174 | "customunit": "", 175 | "value": result[2]}, 176 | {"name": "No Answer Count", 177 | "ShowChart": 0, 178 | "ShowTable": 0, 179 | "mode": "integer", 180 | "kind": "Custom", 181 | "customunit": "", 182 | "limitmaxwarning": 0, 183 | "limitmode": 1, 184 | "value": result[3]}] 185 | return channel_list 186 | 187 | @staticmethod 188 | def get_record(timeout, domain): 189 | blacklist = Blacklist() 190 | noanswer = 0 191 | notlisted = 0 192 | listed = 0 193 | msg = "" 194 | for bl in blacklist.blacklists: 195 | try: 196 | resolver = dns.resolver.Resolver() 197 | query = '.'.join(reversed(str(domain).split("."))) + "." + bl 198 | answers = resolver.query(query, "A") 199 | answer_txt = resolver.query(query, "TXT")[0] 200 | answer_txt = str(answer_txt).strip('"') 201 | msg = msg + str(answer_txt) + ", " 202 | listed += 1 203 | except dns.resolver.NXDOMAIN: 204 | notlisted += 1 205 | except dns.resolver.NoAnswer: 206 | noanswer += 1 207 | return [msg[:-2], listed, notlisted, noanswer] 208 | 209 | -------------------------------------------------------------------------------- /miniprobe/sensors/cpuload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import gc 23 | import logging 24 | 25 | 26 | class CPULoad(object): 27 | def __init__(self): 28 | gc.enable() 29 | 30 | @staticmethod 31 | def get_kind(): 32 | """ 33 | return sensor kind 34 | """ 35 | return "mpcpuload" 36 | 37 | @staticmethod 38 | def get_sensordef(): 39 | """ 40 | Definition of the sensor and data to be shown in the PRTG WebGUI 41 | """ 42 | sensordefinition = { 43 | "kind": CPULoad.get_kind(), 44 | "name": "CPU Load", 45 | "description": "Monitors CPU load avg on the system the mini probe is running on", 46 | "default": "yes", 47 | "help": "Monitors CPU load avg on the system the mini probe is running on", 48 | "tag": "mpcpuloadsensor", 49 | "fields": [], 50 | "groups": [] 51 | } 52 | return sensordefinition 53 | 54 | @staticmethod 55 | def get_data(data, out_queue): 56 | cpuload = CPULoad() 57 | logging.debug("Running sensor: %s" % cpuload.get_kind()) 58 | try: 59 | cpu = cpuload.read_cpu('/proc/loadavg') 60 | except Exception as e: 61 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (cpuload.get_kind(), 62 | data['sensorid'], e)) 63 | data = { 64 | "sensorid": int(data['sensorid']), 65 | "error": "Exception", 66 | "code": 1, 67 | "message": "CPU load sensor failed. See log for details" 68 | } 69 | out_queue.put(data) 70 | return 1 71 | cpudata = [] 72 | for element in cpu: 73 | cpudata.append(element) 74 | data = { 75 | "sensorid": int(data['sensorid']), 76 | "message": "OK", 77 | "channel": cpudata 78 | } 79 | del cpuload 80 | gc.collect() 81 | out_queue.put(data) 82 | return 0 83 | 84 | @staticmethod 85 | def read_cpu(path): 86 | cpu = open(path, "r") 87 | data = [] 88 | for line in cpu: 89 | for element in line.split(" "): 90 | data.append(element) 91 | channel_list = [ 92 | { 93 | "name": "Load Average 1min", 94 | "mode": "float", 95 | "kind": "Custom", 96 | "customunit": "", 97 | "value": float(data[0]) 98 | }, 99 | { 100 | "name": "Load Average 5min", 101 | "mode": "float", 102 | "kind": "Custom", 103 | "customunit": "", 104 | "value": float(data[1]) 105 | }, 106 | { 107 | "name": "Load Average 10min", 108 | "mode": "float", 109 | "kind": "Custom", 110 | "customunit": "", 111 | "value": float(data[2]) 112 | }] 113 | cpu.close() 114 | return channel_list 115 | -------------------------------------------------------------------------------- /miniprobe/sensors/cputemp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import gc 23 | import logging 24 | import os.path 25 | temp = True 26 | if not os.path.exists("/sys/class/thermal/thermal_zone0/temp"): 27 | temp = False 28 | 29 | 30 | class CPUTemp(object): 31 | def __init__(self): 32 | gc.enable() 33 | 34 | @staticmethod 35 | def get_kind(): 36 | """ 37 | return sensor kind 38 | """ 39 | return "mpcputemp" 40 | 41 | @staticmethod 42 | def get_sensordef(testing=False): 43 | """ 44 | Definition of the sensor and data to be shown in the PRTG WebGUI 45 | """ 46 | sensordefinition = { 47 | "kind": CPUTemp.get_kind(), 48 | "name": "CPU Temperature", 49 | "description": "Returns the CPU temperature", 50 | "default": "yes", 51 | "help": "Returns the CPU temperature", 52 | "tag": "mpcputempsensor", 53 | "groups": [ 54 | { 55 | "name": "Group", 56 | "caption": "Temperature settings", 57 | "fields": [ 58 | { 59 | "type": "radio", 60 | "name": "celfar", 61 | "caption": "Choose between Celsius or Fahrenheit display", 62 | "help": "Choose wether you want to return the value in Celsius or Fahrenheit", 63 | "options": { 64 | "C": "Celsius", 65 | "F": "Fahrenheit" 66 | }, 67 | "default": "C" 68 | }, 69 | ] 70 | } 71 | ] 72 | } 73 | if not temp and not testing: 74 | sensordefinition = "" 75 | return sensordefinition 76 | 77 | @staticmethod 78 | def get_data(data, out_queue): 79 | temperature = CPUTemp() 80 | logging.debug("Running sensor: %s" % temperature.get_kind()) 81 | try: 82 | tmp = temperature.read_temp(data) 83 | except Exception as e: 84 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (temperature.get_kind(), 85 | data['sensorid'], e)) 86 | data = { 87 | "sensorid": int(data['sensorid']), 88 | "error": "Exception", 89 | "code": 1, 90 | "message": "CPUTemp sensor failed. See log for details" 91 | } 92 | out_queue.put(data) 93 | return 1 94 | tempdata = [] 95 | for element in tmp: 96 | tempdata.append(element) 97 | data = { 98 | "sensorid": int(data['sensorid']), 99 | "message": "OK", 100 | "channel": tempdata 101 | } 102 | del temperature 103 | gc.collect() 104 | out_queue.put(data) 105 | return 1 106 | 107 | @staticmethod 108 | def read_temp(config): 109 | data = [] 110 | chandata = [] 111 | tmp = open("/sys/class/thermal/thermal_zone0/temp", "r") 112 | lines = tmp.readlines() 113 | tmp.close() 114 | temp_string = lines[0] 115 | logging.debug("CPUTemp Debug message: Temperature from file: %s" % temp_string) 116 | temp_c = float(temp_string) / 1000.0 117 | temp_f = temp_c * 9.0 / 5.0 + 32.0 118 | logging.debug("CPUTemp Debug message: Temperature after calculations:: %s" % temp_c) 119 | if config['celfar'] == "C": 120 | data.append(temp_c) 121 | else: 122 | data.append(temp_f) 123 | for i in range(len(data)): 124 | chandata.append({"name": "CPU Temperature", 125 | "mode": "float", 126 | "unit": "Custom", 127 | "customunit": config['celfar'], 128 | "LimitMode": 1, 129 | "LimitMaxError": 40, 130 | "LimitMaxWarning": 35, 131 | "value": float(data[i])}) 132 | return chandata 133 | -------------------------------------------------------------------------------- /miniprobe/sensors/diskspace.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import os 23 | import gc 24 | import logging 25 | 26 | 27 | class Diskspace(object): 28 | def __init__(self): 29 | gc.enable() 30 | 31 | @staticmethod 32 | def get_kind(): 33 | """ 34 | return sensor kind 35 | """ 36 | return "mpdiskspace" 37 | 38 | @staticmethod 39 | def get_sensordef(): 40 | """ 41 | Definition of the sensor and data to be shown in the PRTG WebGUI 42 | """ 43 | sensordefinition = { 44 | "kind": Diskspace.get_kind(), 45 | "name": "Disk space", 46 | "description": "Monitors disk space on the system the mini probe is running on", 47 | "default": "yes", 48 | "help": "Monitors disk space on the system the mini probe is running on", 49 | "tag": "spdiskspacesensor", 50 | "fields": [], 51 | "groups": [] 52 | } 53 | return sensordefinition 54 | 55 | @staticmethod 56 | def get_data(data, out_queue): 57 | diskspace = Diskspace() 58 | try: 59 | disk = diskspace.read_disk() 60 | logging.debug("Running sensor: %s" % diskspace.get_kind()) 61 | except Exception as e: 62 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (diskspace.get_kind(), 63 | data['sensorid'], e)) 64 | data = { 65 | "sensorid": int(data['sensorid']), 66 | "error": "Exception", 67 | "code": 1, 68 | "message": "Disk Space Sensor failed. See log for details" 69 | } 70 | out_queue.put(data) 71 | return 1 72 | channels = disk 73 | data = { 74 | "sensorid": int(data['sensorid']), 75 | "message": "OK", 76 | "channel": channels 77 | } 78 | del diskspace 79 | gc.collect() 80 | out_queue.put(data) 81 | return 0 82 | 83 | def read_disk(self): 84 | disks = [] 85 | channel_list = [] 86 | for line in os.popen("df -k -xtmpfs -xdevtmpfs"): 87 | if not line.startswith("Filesystem"): 88 | disks.append(line.rstrip().split()) 89 | for line in disks: 90 | channel1 = {"name": "Total Bytes " + str(line[5]), 91 | "mode": "integer", 92 | "kind": "BytesDisk", 93 | "value": int(line[1]) * 1024} 94 | channel2 = {"name": "Used Bytes " + str(line[5]), 95 | "mode": "integer", 96 | "kind": "BytesDisk", 97 | "value": int(line[2]) * 1024} 98 | channel3 = {"name": "Free Bytes " + str(line[5]), 99 | "mode": "integer", 100 | "kind": "BytesDisk", 101 | "value": int(line[3]) * 1024} 102 | total = float(line[2]) + float(line[3]) 103 | used = float(line[2]) / total 104 | free = float(line[3]) / total 105 | 106 | channel4 = {"name": "Free Space " + str(line[5]), 107 | "mode": "float", 108 | "kind": "Percent", 109 | "value": free * 100} 110 | channel5 = {"name": "Used Space " + str(line[5]), 111 | "mode": "float", 112 | "kind": "Percent", 113 | "value": used * 100} 114 | channel_list.append(channel1) 115 | channel_list.append(channel2) 116 | channel_list.append(channel3) 117 | channel_list.append(channel4) 118 | channel_list.append(channel5) 119 | return channel_list 120 | -------------------------------------------------------------------------------- /miniprobe/sensors/ds18b20.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import gc 23 | import os 24 | import logging 25 | import time 26 | #import __init__ 27 | dev = True 28 | if not os.path.isdir("/sys/bus/w1/devices"): 29 | dev = False 30 | 31 | 32 | class DS18B20(object): 33 | def __init__(self): 34 | gc.enable() 35 | 36 | @staticmethod 37 | def get_kind(): 38 | """ 39 | return sensor kind 40 | """ 41 | return "mpds18b20" 42 | 43 | @staticmethod 44 | def get_sensordef(testing=False): 45 | """ 46 | Definition of the sensor and data to be shown in the PRTG WebGUI 47 | """ 48 | if os.path.isdir("/sys/bus/w1/devices"): 49 | default = "yes" 50 | else: 51 | default = "no" 52 | sensordefinition = { 53 | "kind": DS18B20.get_kind(), 54 | "name": "DS18B20 Temperature", 55 | "description": "Returns the temperature measured by an attached DS18B20 temperature sensor on pin 4", 56 | "default": default, 57 | "help": "Returns the temperature measured by an attached DS18B20 temperature sensor on pin 4", 58 | "tag": "mpds18b20sensor", 59 | "groups": [ 60 | { 61 | "name": "Group", 62 | "caption": "Temperature settings", 63 | "fields": [ 64 | { 65 | "type": "radio", 66 | "name": "celfar", 67 | "caption": "Choose between Celsius or Fahrenheit display", 68 | "help": "Choose wether you want to return the value in Celsius or Fahrenheit", 69 | "options": { 70 | "C": "Celsius", 71 | "F": "Fahrenheit" 72 | }, 73 | "default": "C" 74 | }, 75 | ] 76 | } 77 | ] 78 | } 79 | if not dev and not testing: 80 | sensordefinition = "" 81 | return sensordefinition 82 | 83 | @staticmethod 84 | def get_data(data, out_queue): 85 | temperature = DS18B20() 86 | logging.debug("Running sensor: %s" % temperature.get_kind()) 87 | try: 88 | temp = temperature.read_temp(data) 89 | except Exception as e: 90 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (temperature.get_kind(), 91 | data['sensorid'], e)) 92 | data = { 93 | "sensorid": int(data['sensorid']), 94 | "error": "Exception", 95 | "code": 1, 96 | "message": "DS18B20 sensor failed. See log for details" 97 | } 98 | out_queue.put(data) 99 | return 1 100 | tempdata = [] 101 | for element in temp: 102 | tempdata.append(element) 103 | data = { 104 | "sensorid": int(data['sensorid']), 105 | "message": "OK", 106 | "channel": tempdata 107 | } 108 | del temperature 109 | gc.collect() 110 | out_queue.put(data) 111 | return 0 112 | 113 | @staticmethod 114 | def read_temp(config): 115 | data = [] 116 | sens = [] 117 | chandata = [] 118 | for sensor in __init__.DS18B20_sensors: 119 | sens.append(sensor) 120 | temp = open("/sys/bus/w1/devices/28-" + sensor + "/w1_slave", "r") 121 | lines = temp.readlines() 122 | temp.close() 123 | while lines[0].strip()[-3:] != 'YES': 124 | time.sleep(0.2) 125 | equals_pos = lines[1].find('t=') 126 | if equals_pos != -1: 127 | temp_string = lines[1][equals_pos + 2:] 128 | logging.debug("DS18B20 Debug message: Temperature from file: %s" % temp_string) 129 | temp_c = float(temp_string) / 1000.0 130 | temp_f = 1.8 * temp_c + 32.0 131 | if config['celfar'] == "C": 132 | data.append(temp_c) 133 | logging.debug("DS18B20 Debug message: Temperature after calculations:: %s %s" % 134 | (temp_c, config['celfar'])) 135 | else: 136 | data.append(temp_f) 137 | logging.debug("DS18B20 Debug message: Temperature after calculations:: %s %s" % 138 | (temp_f, config['celfar'])) 139 | temp.close() 140 | for i in range(len(data)): 141 | chandata.append({"name": "Sensor: " + sens[i], 142 | "mode": "float", 143 | "unit": "Custom", 144 | "customunit": config['celfar'], 145 | "LimitMode": 1, 146 | "LimitMaxError": 40, 147 | "LimitMaxWarning": 35, 148 | "value": float(data[i])}) 149 | return chandata 150 | -------------------------------------------------------------------------------- /miniprobe/sensors/externalip.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import gc 23 | import logging 24 | import requests 25 | import socket 26 | import fcntl 27 | import struct 28 | server = "http://icanhazip.com" 29 | 30 | 31 | class ExternalIP(object): 32 | def __init__(self): 33 | gc.enable() 34 | 35 | @staticmethod 36 | def get_kind(): 37 | """ 38 | return sensor kind 39 | """ 40 | return "mpexternalip" 41 | 42 | @staticmethod 43 | def get_sensordef(): 44 | """ 45 | Definition of the sensor and data to be shown in the PRTG WebGUI 46 | """ 47 | sensordefinition = { 48 | "kind": ExternalIP.get_kind(), 49 | "name": "External IP", 50 | "description": "Returns the external ip address of the probe", 51 | "default": "yes", 52 | "help": "Returns the external ip address of the probe using the website icanhasip.com", 53 | "tag": "mpexternalipsensor", 54 | "fields": [], 55 | "groups": [] 56 | } 57 | return sensordefinition 58 | 59 | @staticmethod 60 | def get_data(data, out_queue): 61 | ip = ExternalIP() 62 | # address = "" 63 | logging.debug("Running sensor: %s" % ip.get_kind()) 64 | try: 65 | address = ip.get_ip(server) 66 | logging.debug("IP-Address: %s" % address) 67 | localip = ip.local_ip('eth0') 68 | remoteip = ip.remote_ip(server) 69 | except Exception as e: 70 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (ip.get_kind(), 71 | data['sensorid'], e)) 72 | data = { 73 | "sensorid": int(data['sensorid']), 74 | "error": "Exception", 75 | "code": 1, 76 | "message": "External IP sensor failed. See log for details" 77 | } 78 | out_queue.put(data) 79 | return 1 80 | addressdata = [] 81 | for element in address: 82 | addressdata.append(element) 83 | data = { 84 | "sensorid": int(data['sensorid']), 85 | "message": "External IP: " + remoteip + " Internal IP: " + localip, 86 | "channel": addressdata 87 | } 88 | del address 89 | gc.collect() 90 | out_queue.put(data) 91 | return 0 92 | 93 | @staticmethod 94 | def get_ip(url): 95 | channel_list = [ 96 | { 97 | "name": "IP-Address", 98 | "ShowChart": 0, 99 | "ShowTable": 0, 100 | "mode": "integer", 101 | "kind": "Custom", 102 | "customunit": "", 103 | "value": 1 104 | }] 105 | return channel_list 106 | 107 | @staticmethod 108 | def remote_ip(url): 109 | ip = requests.get(url, timeout=30) 110 | address = str(ip.text[0:-1]) 111 | ip.close 112 | return address 113 | 114 | def local_ip(self, ifname): 115 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 116 | return socket.inet_ntoa(fcntl.ioctl( 117 | s.fileno(), 118 | 0x8915, # SIOCGIFADDR 119 | struct.pack('256s', ifname[:15]) 120 | )[20:24]) 121 | -------------------------------------------------------------------------------- /miniprobe/sensors/http.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import gc 23 | import requests 24 | import logging 25 | 26 | 27 | class HTTP(object): 28 | 29 | def __init__(self): 30 | gc.enable() 31 | 32 | @staticmethod 33 | def get_kind(): 34 | """ 35 | return sensor kind 36 | """ 37 | return "mphttp" 38 | 39 | @staticmethod 40 | def get_sensordef(): 41 | """ 42 | Definition of the sensor and data to be shown in the PRTG WebGUI 43 | """ 44 | sensordefinition = { 45 | "kind": HTTP.get_kind(), 46 | "name": "HTTP", 47 | "description": "Monitors a web server using HTTP", 48 | "help": "Monitors a web server using HTTP", 49 | "tag": "mphttpsensor", 50 | "groups": [ 51 | { 52 | "name": "HTTP Specific", 53 | "caption": "HTTP Specific", 54 | "fields": [ 55 | { 56 | "type": "integer", 57 | "name": "timeout", 58 | "caption": "Timeout (in s)", 59 | "required": "1", 60 | "default": 60, 61 | "minimum": 1, 62 | "maximum": 900, 63 | "help": "Timeout in seconds. A maximum value of 900 is allowed." 64 | }, 65 | { 66 | "type": "edit", 67 | "name": "url", 68 | "caption": "URL", 69 | "required": "1", 70 | "default": "http://", 71 | "help": "Enter a valid URL to monitor. The server part (e.g. www.paessler.com) " 72 | "may be different from the 'DNS Name' property in the settings of the " 73 | "associated server." 74 | }, 75 | { 76 | "type": "radio", 77 | "name": "http_method", 78 | "caption": "Request Method", 79 | "required": "1", 80 | "help": "Choose the type of the HTTP request", 81 | "options": { 82 | "1": "GET", 83 | "2": "POST", 84 | "3": "HEAD" 85 | }, 86 | "default": 1 87 | }, 88 | { 89 | "type": "edit", 90 | "name": "post_data", 91 | "caption": "POST data", 92 | "help": "Data in this field will only be used when request type is POST" 93 | } 94 | 95 | ] 96 | }, 97 | { 98 | "name": "Authentication", 99 | "caption": "Authentication", 100 | "fields": [ 101 | { 102 | "type": "radio", 103 | "name": "auth_method", 104 | "caption": "Authentication Method", 105 | "required": "1", 106 | "help": "Choose the type of authentication used", 107 | "options": { 108 | "1": "No authentication", 109 | "2": "Basic" 110 | }, 111 | "default": 1 112 | }, 113 | { 114 | "type": "edit", 115 | "name": "username", 116 | "caption": "Username", 117 | "help": "Provide username here if target requires authentication" 118 | }, 119 | { 120 | "type": "password", 121 | "name": "password", 122 | "caption": "Password", 123 | "help": "Provide password here if target requires authentication" 124 | } 125 | ] 126 | } 127 | ] 128 | } 129 | 130 | return sensordefinition 131 | 132 | def request(self, url, request_method=None, auth_method=None, timeout=None, post_data=None, 133 | user=None, password=None): 134 | timeout = float(timeout) 135 | try: 136 | if request_method == "1": 137 | # GET 138 | if (user or password) and auth_method == "2": 139 | req = requests.get(url, auth=(user, password), timeout=timeout, verify=False) 140 | else: 141 | req = requests.get(url, timeout=timeout, verify=False) 142 | elif request_method == "2": 143 | # POST 144 | if (user or password) and auth_method == "2": 145 | req = requests.post(url, data=post_data, auth=(user, password), timeout=timeout, verify=False) 146 | else: 147 | req = requests.post(url, data=post_data, timeout=timeout, verify=False) 148 | elif request_method == "3": 149 | # HEAD 150 | if (user or password) and auth_method == "2": 151 | req = requests.head(url, auth=(user, password), timeout=timeout, verify=False) 152 | else: 153 | req = requests.head(url, timeout=timeout, verify=False) 154 | time = req.elapsed 155 | except Exception as e: 156 | logging.error(e) 157 | raise 158 | try: 159 | code = req.status_code 160 | response_time = time.microseconds / 1000 161 | except Exception as e: 162 | logging.error(e) 163 | raise 164 | data = [int(code), float(response_time)] 165 | return data 166 | 167 | @staticmethod 168 | def get_data(data, out_queue): 169 | http = HTTP() 170 | try: 171 | http_data = http.request(data['url'], request_method=data["http_method"], auth_method=data["auth_method"], 172 | user=data["username"], password=data["password"], 173 | post_data=data["post_data"], timeout=data["timeout"]) 174 | logging.debug("Running sensor: %s" % http.get_kind()) 175 | except Exception as e: 176 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" 177 | % (http.get_kind(), data['sensorid'], e)) 178 | data = { 179 | "sensorid": int(data['sensorid']), 180 | "error": "Exception", 181 | "code": 1, 182 | "message": "HTTP Request failed. See log for details" 183 | } 184 | out_queue.put(data) 185 | return 1 186 | 187 | data = { 188 | "sensorid": int(data['sensorid']), 189 | "message": "OK Status Code: %s" % http_data[0], 190 | "channel": [ 191 | { 192 | "name": "Response Time", 193 | "mode": "float", 194 | "kind": "TimeResponse", 195 | "value": http_data[1] 196 | }] 197 | } 198 | del http 199 | gc.collect() 200 | out_queue.put(data) 201 | return 0 202 | -------------------------------------------------------------------------------- /miniprobe/sensors/mdadm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # With nice greetings from Dynam-IT GbR 3 | # Suggestion for a mdadm-raid sensor 4 | # Thanks to Paessler for providing the free PRTG Python Probe and the API 5 | 6 | import os 7 | # import re for use of re.split; explained below 8 | import re 9 | import gc 10 | import logging 11 | 12 | 13 | class MDADM(object): 14 | def __init__(self): 15 | gc.enable() 16 | 17 | @staticmethod 18 | def get_kind(): 19 | """ 20 | return sensor kind 21 | """ 22 | return "mpmdadm" 23 | 24 | @staticmethod 25 | def get_sensordef(): 26 | """ 27 | Definition of the sensor and data to be shown in the PRTG WebGUI 28 | """ 29 | sensordefinition = { 30 | "kind": MDADM.get_kind(), 31 | "name": "MDADM RAID Status", 32 | "description": "Monitors status of RAID arrays provided by MDADM", 33 | "help": "Count total number of Arrays, count arrays with missing drives,count arrays resyncing, recovering and checking", 34 | "tag": "mpmdadmsensor", 35 | "fields": [], 36 | "groups": [] 37 | 38 | } 39 | return sensordefinition 40 | 41 | def check(self): 42 | 43 | # Get the standard information output of mdadm and count 44 | numberOfArrays = os.popen("grep ^md -c /proc/mdstat").read() 45 | arraysMissingDrives = 0 46 | arraysRecovering = 0 47 | arraysResyncing = 0 48 | arraysChecking = 0 49 | 50 | ret = os.popen("cat /proc/mdstat").readlines() 51 | mdstat = ' '.join([line.strip() for line in ret]) 52 | # Using re.split to split output instead of split to prevent cutting the leading "md" 53 | raidArrayList = re.split(' (?=md)', mdstat) 54 | 55 | for index in range(len(raidArrayList)): 56 | # Searching for list entries beginning with "md" 57 | if re.match('^md', raidArrayList[index]): 58 | print(raidArrayList[index]) 59 | if '_' in raidArrayList[index]: 60 | # adrive missing can also be a defect drive. Defect or missing drives are marked with an "_" instead of an "U" in the drive list 61 | arraysMissingDrives += 1 62 | if 'recovering' in raidArrayList[index]: 63 | arraysRecovering += 1 64 | if 'resync' in raidArrayList[index]: 65 | arraysResyncing += 1 66 | if 'check' in raidArrayList[index]: 67 | arraysChecking += 1 68 | 69 | channel_list = [ 70 | { 71 | "name": "Total count of RAID arrays", 72 | "mode": "integer", 73 | "unit": "count", 74 | "limitmaxwarning": 0, 75 | "limitmode": 0, 76 | "value": numberOfArrays 77 | }, 78 | { 79 | "name": "RAID arrays with missing drives", 80 | "mode": "integer", 81 | "unit": "count", 82 | # setting this channel to error if arraysMissingDrives is > 0 83 | "limitmaxerror": 0, 84 | "limitmode": 1, 85 | "value": arraysMissingDrives 86 | }, 87 | { 88 | "name": "RAID arrays recovering", 89 | "mode": "integer", 90 | "unit": "count", 91 | "limitmaxwarning": 0, 92 | "limitmode": 1, 93 | "value": arraysRecovering 94 | }, 95 | { 96 | "name": "RAID arrays resyncing", 97 | "mode": "integer", 98 | "unit": "count", 99 | "limitmaxwarning": 0, 100 | "limitmode": 1, 101 | "value": arraysResyncing 102 | }, 103 | { 104 | "name": "RAID arrays in automatic checking task", 105 | "mode": "integer", 106 | "unit": "count", 107 | # In many linux distributions an array check is performed automatically once a day or week. So we should not warn about this 108 | "limitmaxwarning": 0, 109 | "limitmode": 0, 110 | "value": arraysChecking 111 | }, 112 | ] 113 | return channel_list 114 | 115 | @staticmethod 116 | def get_data(data, out_queue): 117 | mdadm = MDADM() 118 | try: 119 | mdadmdata = mdadm.check() 120 | data_r = { 121 | "sensorid": int(data['sensorid']), 122 | "message": "OK", 123 | "channel": mdadmdata 124 | } 125 | logging.debug("Running sensor: %s" % mdadm.get_kind()) 126 | except Exception as e: 127 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (mdadm.get_kind(), 128 | data['sensorid'], e)) 129 | data_r = { 130 | "sensorid": int(data['sensorid']), 131 | "error": "Exception", 132 | "code": 1, 133 | "message": "MDADM Sensor failed. %s" % e 134 | } 135 | out_queue.put(data_r) 136 | return 1 137 | del mdadm 138 | gc.collect() 139 | out_queue.put(data_r) 140 | return 0 141 | -------------------------------------------------------------------------------- /miniprobe/sensors/memory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import gc 23 | import logging 24 | 25 | 26 | class Memory(object): 27 | def __init__(self): 28 | gc.enable() 29 | 30 | @staticmethod 31 | def get_kind(): 32 | """ 33 | return sensor kind 34 | """ 35 | return "mpmemory" 36 | 37 | @staticmethod 38 | def get_sensordef(): 39 | """ 40 | Definition of the sensor and data to be shown in the PRTG WebGUI 41 | """ 42 | sensordefinition = { 43 | "kind": Memory.get_kind(), 44 | "name": "Memory", 45 | "description": "Monitors memory on the system the mini probe is running on", 46 | "default": "yes", 47 | "help": "Monitors memory on the system the mini probe is running on", 48 | "tag": "mpmemorysensor", 49 | "fields": [], 50 | "groups": [] 51 | } 52 | return sensordefinition 53 | 54 | @staticmethod 55 | def get_data(data, out_queue): 56 | memory = Memory() 57 | try: 58 | mem = memory.read_memory('/proc/meminfo') 59 | logging.debug("Running sensor: %s" % memory.get_kind()) 60 | except Exception as e: 61 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (memory.get_kind(), 62 | data['sensorid'], e)) 63 | data = { 64 | "sensorid": int(data['sensorid']), 65 | "error": "Exception", 66 | "code": 1, 67 | "message": "Memory sensor failed. See log for details" 68 | } 69 | out_queue.put(data) 70 | return 1 71 | memorydata = [] 72 | for element in mem: 73 | memorydata.append(element) 74 | data = { 75 | "sensorid": int(data['sensorid']), 76 | "message": "OK", 77 | "channel": memorydata 78 | } 79 | del memory 80 | gc.collect() 81 | out_queue.put(data) 82 | return 0 83 | 84 | def read_memory(self, path): 85 | mem = open(path, "r") 86 | data = {} 87 | for line in mem: 88 | tmp = line.split(":")[1].lstrip() 89 | data[line.split(":")[0].rstrip()] = tmp.split(" ")[0].rstrip() 90 | channel_list = [ 91 | { 92 | "name": "Memory Total", 93 | "mode": "integer", 94 | "kind": "BytesMemory", 95 | "value": int(data['MemTotal']) * 1024 96 | }, 97 | { 98 | "name": "Memory Free", 99 | "mode": "integer", 100 | "kind": "BytesMemory", 101 | "value": int(data['MemFree']) * 1024 102 | }, 103 | { 104 | "name": "Swap Total", 105 | "mode": "integer", 106 | "kind": "BytesMemory", 107 | "value": int(data['SwapTotal']) * 1024 108 | }, 109 | { 110 | "name": "Swap Free", 111 | "mode": "integer", 112 | "kind": "BytesMemory", 113 | "value": int(data['SwapFree']) * 1024 114 | }] 115 | mem.close() 116 | return channel_list 117 | -------------------------------------------------------------------------------- /miniprobe/sensors/nmap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #Copyright (c) 2014, Paessler AG 3 | #All rights reserved. 4 | #Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | #1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | #2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | #3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | #THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import time 23 | import socket 24 | import logging 25 | import gc 26 | import re 27 | import os 28 | import sys 29 | import struct 30 | import select 31 | from itertools import islice 32 | 33 | class NMAP(object): 34 | ICMP_ECHO_REQUEST = 8 35 | 36 | def __init__(self): 37 | pass 38 | 39 | @staticmethod 40 | def get_kind(): 41 | """ 42 | return sensor kind 43 | """ 44 | return "mpnmap" 45 | 46 | @staticmethod 47 | def get_sensordef(): 48 | """ 49 | Definition of the sensor and data to be shown in the PRTG WebGUI 50 | """ 51 | sensordefinition = { 52 | "kind": NMAP.get_kind(), 53 | "name": "NMAP", 54 | "description": "Checks the availability of systems.", 55 | "help": "Checks the availability of systems on a network and logs this to a separate logfile on the miniprobe.", 56 | "tag": "mpnmapsensor", 57 | "groups": [ 58 | { 59 | "name": "nmapspecific", 60 | "caption": "NMAP specific", 61 | "fields": [ 62 | { 63 | "type": "integer", 64 | "name": "timeout", 65 | "caption": "Timeout (in ms)", 66 | "required": "1", 67 | "default": 50, 68 | "minimum": 10, 69 | "maximum": 1000, 70 | "help": "If the reply takes longer than this value the request is aborted " 71 | "and an error message is triggered. Max. value is 1000 ms. (=1 sec.)" 72 | }, 73 | { 74 | "type": "edit", 75 | "name": "ip", 76 | "caption": "IP-Address(es)", 77 | "required": "1", 78 | "default": "", 79 | "help": "Specify the ip-address or a range of addresses using one of the following notations:[br]Single: 192.168.1.1[br]CIDR: 192.168.1.0/24[br]- separated: 192.168.1.1-192.168.1.100" 80 | } 81 | ] 82 | } 83 | ] 84 | } 85 | return sensordefinition 86 | 87 | @staticmethod 88 | def get_data(data, out_queue): 89 | nmap = NMAP() 90 | error = False 91 | tmpMessage = "" 92 | cidr = "" 93 | alive_str = "" 94 | alive_cnt = 0 95 | try: 96 | logging.debug("Running sensor: %s" % nmap.get_kind()) 97 | if '/' in data['ip']: 98 | validCIDR = nmap.validateCIDRBlock(data['ip']) 99 | if validCIDR: 100 | tmpMessage = "True" 101 | cidr = nmap.returnCIDR(data['ip']) 102 | for ip in islice(cidr, 1, len(cidr)-1): 103 | result = nmap.do_one_ping(ip, float(data['timeout'])/1000) 104 | if not result == None: 105 | alive_str = alive_str + ip + ": " + str(int(result * 1000)) + " ms, " 106 | alive_cnt = alive_cnt + 1 107 | else: 108 | tmpMessage = validCIDR 109 | error = True 110 | channel_list = [ 111 | { 112 | "name": "Hosts alive", 113 | "mode": "Integer", 114 | "kind": "count", 115 | "value": alive_cnt 116 | }] 117 | except Exception as e: 118 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (nmap.get_kind(), 119 | data['sensorid'], e)) 120 | sensor_data = { 121 | "sensorid": int(data['sensorid']), 122 | "error": "Exception", 123 | "code": 1, 124 | "message": "Port check failed or ports closed. See log for details" 125 | } 126 | out_queue.put(sensor_data) 127 | return 1 128 | sensor_data = { 129 | "sensorid": int(data['sensorid']), 130 | "message": alive_str[:-2], 131 | "channel": channel_list 132 | } 133 | del nmap 134 | gc.collect() 135 | out_queue.put(sensor_data) 136 | return 0 137 | 138 | def ip2bin(self,ip): 139 | b = "" 140 | nmap = NMAP() 141 | inQuads = ip.split(".") 142 | outQuads = 4 143 | for q in inQuads: 144 | if q != "": 145 | b += nmap.dec2bin(int(q),8) 146 | outQuads -= 1 147 | while outQuads > 0: 148 | b += "00000000" 149 | outQuads -= 1 150 | return b 151 | 152 | def dec2bin(self,n,d=None): 153 | s = "" 154 | while n>0: 155 | if n&1: 156 | s = "1"+s 157 | else: 158 | s = "0"+s 159 | n >>= 1 160 | if d is not None: 161 | while len(s) 255): 183 | return "Error: quad "+str(q)+" wrong size." 184 | # subnet is an appropriate value (1-32) 185 | if (int(subnet) < 1) or (int(subnet) > 32): 186 | return "Error: subnet "+str(subnet)+" wrong size." 187 | # passed all checks -> return True 188 | return True 189 | 190 | def returnCIDR(self,c): 191 | nmap = NMAP() 192 | ips = [] 193 | parts = c.split("/") 194 | baseIP = nmap.ip2bin(parts[0]) 195 | subnet = int(parts[1]) 196 | # Python string-slicing weirdness: 197 | # "myString"[:-1] -> "myStrin" but "myString"[:0] -> "" 198 | # if a subnet of 32 was specified simply print the single IP 199 | if subnet == 32: 200 | return nmap.bin2ip(baseIP) 201 | # for any other size subnet, print a list of IP addresses by concatenating 202 | # the prefix with each of the suffixes in the subnet 203 | else: 204 | ipPrefix = baseIP[:-(32-subnet)] 205 | for i in range(2**(32-subnet)): 206 | ips.append(nmap.bin2ip(ipPrefix+nmap.dec2bin(i, (32-subnet)))) 207 | return ips 208 | 209 | def checksum(self, source_string): 210 | """ 211 | I'm not too confident that this is right but testing seems 212 | to suggest that it gives the same answers as in_cksum in ping.c 213 | """ 214 | sum = 0 215 | countTo = (len(source_string)/2)*2 216 | count = 0 217 | while count> 16) + (sum & 0xffff) 226 | sum = sum + (sum >> 16) 227 | answer = ~sum 228 | answer = answer & 0xffff 229 | # Swap bytes. Bugger me if I know why. 230 | answer = answer >> 8 | (answer << 8 & 0xff00) 231 | return answer 232 | 233 | def receive_one_ping(self, my_socket, ID, timeout): 234 | """ 235 | receive the ping from the socket. 236 | """ 237 | timeLeft = float(timeout) 238 | while True: 239 | startedSelect = time.time() 240 | whatReady = select.select([my_socket], [], [], timeLeft) 241 | howLongInSelect = (time.time() - startedSelect) 242 | if whatReady[0] == []: # Timeout 243 | return 244 | timeReceived = time.time() 245 | recPacket, addr = my_socket.recvfrom(1024) 246 | icmpHeader = recPacket[20:28] 247 | type, code, checksum, packetID, sequence = struct.unpack( 248 | "bbHHh", icmpHeader 249 | ) 250 | if packetID == ID: 251 | bytesInDouble = struct.calcsize("d") 252 | timeSent = struct.unpack("d", recPacket[28:28 + bytesInDouble])[0] 253 | return timeReceived - timeSent 254 | timeLeft = timeLeft - howLongInSelect 255 | if timeLeft <= 0: 256 | return 257 | 258 | def send_one_ping(self, my_socket, dest_addr, ID): 259 | """ 260 | Send one ping to the given >dest_addr<. 261 | """ 262 | dest_addr = socket.gethostbyname(dest_addr) 263 | # Header is type (8), code (8), checksum (16), id (16), sequence (16) 264 | my_checksum = 0 265 | # Make a dummy heder with a 0 checksum. 266 | header = struct.pack("bbHHh", self.ICMP_ECHO_REQUEST, 0, my_checksum, ID, 1) 267 | bytesInDouble = struct.calcsize("d") 268 | data = (192 - bytesInDouble) * "Q" 269 | data = struct.pack("d", time.time()) + data 270 | # Calculate the checksum on the data and the dummy header. 271 | my_checksum = self.checksum(header + data) 272 | # Now that we have the right checksum, we put that in. It's just easier 273 | # to make up a new header than to stuff it into the dummy. 274 | header = struct.pack( 275 | "bbHHh", self.ICMP_ECHO_REQUEST, 0, socket.htons(my_checksum), ID, 1 276 | ) 277 | packet = header + data 278 | my_socket.sendto(packet, (dest_addr, 1)) # Don't know about the 1 279 | 280 | def do_one_ping(self, dest_addr, timeout): 281 | """ 282 | Returns either the delay (in seconds) or none on timeout. 283 | """ 284 | icmp = socket.getprotobyname("icmp") 285 | try: 286 | my_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp) 287 | except socket.error as serr: 288 | if serr.errno == 1: 289 | # Operation not permitted 290 | serr.msg += ( 291 | " - Note that ICMP messages can only be sent from processes" 292 | " running as root." 293 | ) 294 | raise socket.error(serr.msg) 295 | raise # raise the original error 296 | my_ID = os.getpid() & 0xFFFF 297 | self.send_one_ping(my_socket, dest_addr, my_ID) 298 | delay = self.receive_one_ping(my_socket, my_ID, timeout) 299 | my_socket.close() 300 | return delay 301 | -------------------------------------------------------------------------------- /miniprobe/sensors/ping.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import os 23 | import gc 24 | import logging 25 | 26 | 27 | class Ping(object): 28 | def __init__(self): 29 | gc.enable() 30 | 31 | @staticmethod 32 | def get_kind(): 33 | """ 34 | return sensor kind 35 | """ 36 | return "mpping" 37 | 38 | @staticmethod 39 | def get_sensordef(): 40 | """ 41 | Definition of the sensor and data to be shown in the PRTG WebGUI 42 | """ 43 | sensordefinition = { 44 | "kind": Ping.get_kind(), 45 | "name": "Ping", 46 | "description": "Monitors the availability of a target using ICMP", 47 | "help": "Monitors the availability of a target using ICMP", 48 | "tag": "mppingsensor", 49 | "groups": [ 50 | { 51 | "name": " Ping Settings", 52 | "caption": "Ping Settings", 53 | "fields": [ 54 | { 55 | "type": "integer", 56 | "name": "timeout", 57 | "caption": "Timeout (in s)", 58 | "required": "1", 59 | "default": 5, 60 | "minimum": 1, 61 | "maximum": 300, 62 | "help": "Timeout in seconds. A maximum value of 300 is allowed." 63 | }, 64 | { 65 | "type": "integer", 66 | "name": "packsize", 67 | "caption": "Packetsize (Bytes)", 68 | "required": "1", 69 | "default": 32, 70 | "minimum": 1, 71 | "maximum": 10000, 72 | "help": "The default packet size for Ping requests is 32 bytes, " 73 | "but you can choose any other packet size between 1 and 10,000 bytes." 74 | }, 75 | { 76 | "type": "integer", 77 | "name": "pingcount", 78 | "caption": "Ping Count", 79 | "required": "1", 80 | "default": 1, 81 | "minimum": 1, 82 | "maximum": 20, 83 | "help": "Enter the count of Ping requests PRTG will send to the device during an interval" 84 | } 85 | ] 86 | } 87 | ] 88 | } 89 | return sensordefinition 90 | 91 | def ping(self, target, count, timeout, packetsize): 92 | ping = "" 93 | ret = os.popen("/bin/ping -c %s -s %s -W %s %s" % (str(count), str(packetsize), str(timeout), str(target))) 94 | pingdata = ret.readlines() 95 | ret.close() 96 | for line in pingdata: 97 | if line.startswith("r"): 98 | ping = line.split("=")[1].lstrip() 99 | if line.find("packet") > 0: 100 | pack_loss = line.split(",")[2].lstrip() 101 | pack_loss = pack_loss.split(' ')[0].lstrip() 102 | pack_loss = pack_loss[:-1] 103 | if ping == '': 104 | return "Not reachable!" 105 | values = ping.split("/") + [pack_loss] 106 | channel_list = [ 107 | { 108 | "name": "Ping Time Min", 109 | "mode": "float", 110 | "kind": "TimeResponse", 111 | "value": float(values[0]) 112 | }, 113 | { 114 | "name": "Ping Time Avg", 115 | "mode": "float", 116 | "kind": "TimeResponse", 117 | "value": float(values[1]) 118 | }, 119 | { 120 | "name": "Ping Time Max", 121 | "mode": "float", 122 | "kind": "TimeResponse", 123 | "value": float(values[2]) 124 | }, 125 | { 126 | "name": "Ping Time MDEV", 127 | "mode": "float", 128 | "kind": "TimeResponse", 129 | "value": float(values[3].split(' ')[0]) 130 | }, 131 | { 132 | "name": "Packet Loss", 133 | "mode": "integer", 134 | "kind": "Percent", 135 | "value": int(values[4]) 136 | } 137 | ] 138 | return channel_list 139 | 140 | @staticmethod 141 | def get_data(data, out_queue): 142 | ping = Ping() 143 | try: 144 | pingdata = ping.ping(data['host'], data['pingcount'], data['timeout'], data['packsize']) 145 | if pingdata == "Not reachable!": 146 | data_r = { 147 | "sensorid": int(data['sensorid']), 148 | "error": "Exception", 149 | "code": 1, 150 | "message": data['host'] + " is " + pingdata 151 | } 152 | else: 153 | data_r = { 154 | "sensorid": int(data['sensorid']), 155 | "message": "OK", 156 | "channel": pingdata 157 | } 158 | logging.debug("Running sensor: %s" % ping.get_kind()) 159 | logging.debug("Host: %s Pingcount: %s timeout: %s packetsize: %s" % (data['host'], data['pingcount'], 160 | data['timeout'], data['packsize'])) 161 | except Exception as e: 162 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (ping.get_kind(), 163 | data['sensorid'], e)) 164 | data_r = { 165 | "sensorid": int(data['sensorid']), 166 | "error": "Exception", 167 | "code": 1, 168 | "message": "Ping failed." 169 | } 170 | out_queue.put(data_r) 171 | return 1 172 | del ping 173 | gc.collect() 174 | out_queue.put(data_r) 175 | return 0 176 | -------------------------------------------------------------------------------- /miniprobe/sensors/port.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import time 23 | import socket 24 | import gc 25 | import logging 26 | 27 | 28 | class Port(object): 29 | def __init__(self): 30 | gc.enable() 31 | 32 | def __del__(self): 33 | gc.collect() 34 | 35 | @staticmethod 36 | def get_kind(): 37 | """ 38 | return sensor kind 39 | """ 40 | return "mpport" 41 | 42 | @staticmethod 43 | def get_sensordef(): 44 | """ 45 | Definition of the sensor and data to be shown in the PRTG WebGUI 46 | """ 47 | sensordefinition = { 48 | "kind": Port.get_kind(), 49 | "name": "Port", 50 | "description": "Monitors the availability of a port on a target system", 51 | "help": "Monitors the availability of a port on a target system", 52 | "tag": "mpportsensor", 53 | "groups": [ 54 | { 55 | "name": " portspecific", 56 | "caption": "Port specific", 57 | "fields": [ 58 | { 59 | "type": "integer", 60 | "name": "timeout", 61 | "caption": "Timeout (in s)", 62 | "required": "1", 63 | "default": 60, 64 | "minimum": 1, 65 | "maximum": 900, 66 | "help": "If the reply takes longer than this value the request is aborted " 67 | "and an error message is triggered. Max. value is 900 sec. (=15 min.)" 68 | }, 69 | { 70 | "type": "integer", 71 | "name": "targetport", 72 | "caption": "Port", 73 | "required": "1", 74 | "default": 110, 75 | "minimum": 1, 76 | "maximum": 65534, 77 | "help": "" 78 | } 79 | ] 80 | } 81 | ] 82 | } 83 | return sensordefinition 84 | 85 | def port(self, target, timeout, port): 86 | remote_server = socket.gethostbyname(target) 87 | response_time = 0.0 88 | try: 89 | start_time = time.time() 90 | conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 91 | conn.settimeout(float(timeout)) 92 | conn.connect((remote_server, int(port))) 93 | conn.close() 94 | end_time = time.time() 95 | response_time = (end_time - start_time) * 1000 96 | except socket.gaierror as e: 97 | logging.error(e) 98 | raise 99 | except socket.timeout as e: 100 | logging.error(e) 101 | raise 102 | except Exception as e: 103 | logging.error(e) 104 | raise 105 | 106 | channel_list = [ 107 | { 108 | "name": "Available", 109 | "mode": "float", 110 | "kind": "TimeResponse", 111 | "value": float(response_time) 112 | } 113 | ] 114 | return channel_list 115 | 116 | @staticmethod 117 | def get_data(data, out_queue): 118 | port = Port() 119 | try: 120 | port_data = port.port(data['host'], data['timeout'], data['targetport']) 121 | logging.debug("Running sensor: %s" % port.get_kind()) 122 | except Exception as e: 123 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (port.get_kind(), 124 | data['sensorid'], e)) 125 | data = { 126 | "sensorid": int(data['sensorid']), 127 | "error": "Exception", 128 | "code": 1, 129 | "message": "Port check failed. See log for details" 130 | } 131 | out_queue.put(data) 132 | return 1 133 | data = { 134 | "sensorid": int(data['sensorid']), 135 | "message": "OK Port %s available" % data['targetport'], 136 | "channel": port_data 137 | } 138 | del port 139 | gc.collect() 140 | out_queue.put(data) 141 | return 0 142 | -------------------------------------------------------------------------------- /miniprobe/sensors/portrange.py: -------------------------------------------------------------------------------- 1 | """ 2 | Not stable therefore removed in the Beta. Might come back later. 3 | """ 4 | #!/usr/bin/env python 5 | #Copyright (c) 2014, Paessler AG 6 | #All rights reserved. 7 | #Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 8 | # following conditions are met: 9 | #1. Redistributions of source code must retain the above copyright notice, this list of conditions 10 | # and the following disclaimer. 11 | #2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 12 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 13 | #3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 14 | # or promote products derived from this software without specific prior written permission. 15 | 16 | #THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 17 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 19 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 20 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 23 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | import time 26 | import socket 27 | import logging 28 | import gc 29 | 30 | 31 | class Portrange(object): 32 | def __init__(self): 33 | pass 34 | 35 | @staticmethod 36 | def get_kind(): 37 | """ 38 | return sensor kind 39 | """ 40 | return "mpportrange" 41 | 42 | @staticmethod 43 | def get_sensordef(): 44 | """ 45 | Definition of the sensor and data to be shown in the PRTG WebGUI 46 | """ 47 | sensordefinition = { 48 | "kind": Portrange.get_kind(), 49 | "name": "Port Range", 50 | "description": "Checks the availability of a port range on a target system", 51 | "help": "Checks the availability of a port range on a target system", 52 | "tag": "mpportrangesensor", 53 | "groups": [ 54 | { 55 | "name": " portspecific", 56 | "caption": "Port specific", 57 | "fields": [ 58 | { 59 | "type": "integer", 60 | "name": "timeout", 61 | "caption": "Timeout (in s)", 62 | "required": "1", 63 | "default": 60, 64 | "minimum": 1, 65 | "maximum": 900, 66 | "help": "If the reply takes longer than this value the request is aborted " 67 | "and an error message is triggered. Max. value is 900 sec. (=15 min.)" 68 | }, 69 | { 70 | "type": "integer", 71 | "name": "startport", 72 | "caption": "Port", 73 | "required": "1", 74 | "default": 110, 75 | "minimum": 1, 76 | "maximum": 65534, 77 | "help": "Specify the port ranges starting port" 78 | }, 79 | { 80 | "type": "integer", 81 | "name": "endport", 82 | "caption": "Port", 83 | "required": "1", 84 | "default": 110, 85 | "minimum": 1, 86 | "maximum": 65534, 87 | "help": "Specify the port ranges end port" 88 | } 89 | ] 90 | } 91 | ] 92 | } 93 | return sensordefinition 94 | 95 | def portrange(self, target, timeout, start, end): 96 | remote_server = socket.gethostbyname(target) 97 | numberofports = int(end) - int(start) 98 | result = 1234 99 | a = 0 100 | start_time = time.time() 101 | for port in range(int(start), int(end)): 102 | try: 103 | conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 104 | conn.settimeout(float(timeout)) 105 | result = conn.connect_ex((remote_server, int(port))) 106 | conn.close() 107 | except socket.gaierror as e: 108 | logging.error(e) 109 | except socket.timeout as e: 110 | logging.error(e) 111 | except Exception as e: 112 | logging.error(e) 113 | if result == 0: 114 | a += 1 115 | else: 116 | raise Exception('port %s not open' % port) 117 | 118 | end_time = time.time() 119 | response_time = (end_time - start_time) * 1000 120 | if a == numberofports: 121 | channel_list = [ 122 | { 123 | "name": "Available", 124 | "mode": "float", 125 | "kind": "TimeResponse", 126 | "value": float(response_time) 127 | } 128 | ] 129 | return channel_list 130 | else: 131 | raise Exception 132 | 133 | @staticmethod 134 | def get_data(data): 135 | port = Portrange() 136 | try: 137 | port_data = port.portrange(data['host'], data['timeout'], data['startport'], data['endport']) 138 | logging.debug("Running sensor: %s" % port.get_kind()) 139 | except Exception as e: 140 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (port.get_kind(), 141 | data['sensorid'], e)) 142 | sensor_data = { 143 | "sensorid": int(data['sensorid']), 144 | "error": "Exception", 145 | "code": 1, 146 | "message": "Port check failed or ports closed. See log for details" 147 | } 148 | return sensor_data 149 | sensor_data = { 150 | "sensorid": int(data['sensorid']), 151 | "message": "OK Ports open", 152 | "channel": port_data 153 | } 154 | del port 155 | gc.collect() 156 | return sensor_data 157 | -------------------------------------------------------------------------------- /miniprobe/sensors/postfix.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import os 23 | import gc 24 | import logging 25 | 26 | 27 | class Postfix(object): 28 | def __init__(self): 29 | gc.enable() 30 | 31 | @staticmethod 32 | def get_kind(): 33 | """ 34 | return sensor kind 35 | """ 36 | return "mppostfix" 37 | 38 | @staticmethod 39 | def get_sensordef(): 40 | """ 41 | Definition of the sensor and data to be shown in the PRTG WebGUI 42 | """ 43 | sensordefinition = { 44 | "kind": Postfix.get_kind(), 45 | "name": "Postfix Mailqueue", 46 | "description": "Monitors the mailqueue of a postfix server", 47 | "help": "Monitors the mailqueue of a postfix server for active, deferred, hold or corrupt mail", 48 | "tag": "mppostfixsensor", 49 | "fields": [], 50 | "groups": [] 51 | } 52 | return sensordefinition 53 | 54 | def check(self): 55 | spoolcmd = os.popen("postconf -h queue_directory") 56 | spooldir = spoolcmd.readline().replace('\n', '') 57 | spoolcmd.close() 58 | deferredcmd = os.popen("test -d %s/deferred && find %s/deferred -type f | wc -l" % (spooldir, spooldir)) 59 | deferredcnt = deferredcmd.readline().replace('\n', '') 60 | deferredcmd.close() 61 | activecmd = os.popen("test -d %s/active && find %s/active -type f | wc -l" % (spooldir, spooldir)) 62 | activecnt = activecmd.readline().replace('\n', '') 63 | activecmd.close() 64 | holdcmd = os.popen("test -d %s/hold && find %s/hold -type f | wc -l" % (spooldir, spooldir)) 65 | holdcnt = holdcmd.readline().replace('\n', '') 66 | holdcmd.close() 67 | corruptcmd = os.popen("test -d %s/corrupt && find %s/corrupt -type f | wc -l" % (spooldir, spooldir)) 68 | corruptcnt = corruptcmd.readline().replace('\n', '') 69 | corruptcmd.close() 70 | channel_list = [ 71 | { 72 | "name": "Deferred mails", 73 | "mode": "integer", 74 | "unit": "Count", 75 | "limitmaxwarning": 40, 76 | "limitmaxerror": 50, 77 | "limitmode": 1, 78 | "value": deferredcnt 79 | }, 80 | { 81 | "name": "Active mails", 82 | "mode": "integer", 83 | "unit": "Count", 84 | "value": activecnt 85 | }, 86 | { 87 | "name": "Hold mails", 88 | "mode": "integer", 89 | "unit": "Count", 90 | "value": holdcnt 91 | }, 92 | { 93 | "name": "Corrupt mails", 94 | "mode": "integer", 95 | "unit": "Count", 96 | "value": corruptcnt 97 | }, 98 | ] 99 | return channel_list 100 | 101 | @staticmethod 102 | def get_data(data, out_queue): 103 | postfix = Postfix() 104 | try: 105 | postfixdata = postfix.check() 106 | data_r = { 107 | "sensorid": int(data['sensorid']), 108 | "message": "OK", 109 | "channel": postfixdata 110 | } 111 | logging.debug("Running sensor: %s" % postfix.get_kind()) 112 | except Exception as e: 113 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (postfix.get_kind(), 114 | data['sensorid'], e)) 115 | data_r = { 116 | "sensorid": int(data['sensorid']), 117 | "error": "Exception", 118 | "code": 1, 119 | "message": "Postfix failed. %s" % e 120 | } 121 | out_queue.put(data_r) 122 | return 1 123 | del postfix 124 | gc.collect() 125 | out_queue.put(data_r) 126 | return 0 127 | -------------------------------------------------------------------------------- /miniprobe/sensors/probehealth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import os 23 | import logging 24 | 25 | 26 | class Probehealth(object): 27 | def __init__(self): 28 | pass 29 | 30 | @staticmethod 31 | def get_kind(): 32 | """ 33 | return sensor kind 34 | """ 35 | return "mpprobehealth" 36 | 37 | @staticmethod 38 | def get_sensordef(): 39 | """ 40 | Definition of the sensor and data to be shown in the PRTG WebGUI 41 | """ 42 | sensordefinition = { 43 | "kind": Probehealth.get_kind(), 44 | "name": "Probe Health", 45 | "description": "Internal sensor used to monitor the health of a PRTG probe on the system the mini probe " 46 | "is running on", 47 | "default": "yes", 48 | "help": "Internal sensor used to monitor the health of a PRTG probe on the system the mini probe " 49 | "is running on", 50 | "tag": "mpprobehealthsensor", 51 | "groups": [ 52 | { 53 | "name": "Group", 54 | "caption": "Temperature settings", 55 | "fields": [ 56 | { 57 | "type": "radio", 58 | "name": "celfar", 59 | "caption": "Choose between Celsius or Fahrenheit display", 60 | "help": "Choose wether you want to return the value in Celsius or Fahrenheit", 61 | "options": { 62 | "C": "Celsius", 63 | "F": "Fahrenheit" 64 | }, 65 | "default": "C" 66 | }, 67 | { 68 | "type": "integer", 69 | "name": "maxtemp", 70 | "caption": "Error temperature", 71 | "required": "1", 72 | "minimum": 20, 73 | "maximum": 75, 74 | "help": "Set the maximum temperature above which the temperature sensor will " 75 | "provide a error (not below 20 or above 75)", 76 | "default": 45 77 | }, 78 | ] 79 | } 80 | ] 81 | } 82 | return sensordefinition 83 | 84 | @staticmethod 85 | def get_data(data, out_queue): 86 | probehealth = Probehealth() 87 | try: 88 | mem = probehealth.read_memory('/proc/meminfo') 89 | cpu = probehealth.read_cpu('/proc/loadavg') 90 | temperature = probehealth.read_temp() 91 | disk = probehealth.read_disk() 92 | health = probehealth.read_probe_health(data) 93 | logging.debug("Running sensor: %s" % probehealth.get_kind()) 94 | except Exception as e: 95 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (probehealth.get_kind(), 96 | data['sensorid'], e)) 97 | data = { 98 | "sensorid": int(data['sensorid']), 99 | "error": "Exception", 100 | "code": 1, 101 | "message": "Probe Health sensor failed. See log for details" 102 | } 103 | out_queue.put(data) 104 | return 1 105 | probedata = [] 106 | for element in health: 107 | probedata.append(element) 108 | for element in mem: 109 | probedata.append(element) 110 | for element in temperature: 111 | probedata.append(element) 112 | for element in cpu: 113 | probedata.append(element) 114 | for element in disk: 115 | probedata.append(element) 116 | data = { 117 | "sensorid": int(data['sensorid']), 118 | "message": "OK", 119 | "channel": probedata 120 | } 121 | out_queue.put(data) 122 | return 0 123 | 124 | def read_memory(self, path): 125 | mem = open(path, "r") 126 | data = {} 127 | for line in mem: 128 | tmp = line.split(":")[1].lstrip() 129 | data[line.split(":")[0].rstrip()] = tmp.split(" ")[0].rstrip() 130 | channel_list = [{"name": "Memory Total", 131 | "mode": "integer", 132 | "kind": "BytesMemory", 133 | "value": int(data['MemTotal']) * 1024}, 134 | {"name": "Memory Free", 135 | "mode": "integer", 136 | "kind": "BytesMemory", 137 | "value": int(data['MemFree']) * 1024}, 138 | {"name": "Swap Total", 139 | "mode": "integer", 140 | "kind": "BytesMemory", 141 | "value": int(data['SwapTotal']) * 1024}, 142 | {"name": "Swap Free", 143 | "mode": "integer", 144 | "kind": "BytesMemory", 145 | "value": int(data['SwapFree']) * 1024}] 146 | mem.close() 147 | return channel_list 148 | 149 | def read_cpu(self, path): 150 | cpu = open(path, "r") 151 | data = [] 152 | for line in cpu: 153 | for element in line.split(" "): 154 | data.append(element) 155 | channel_list = [{"name": "Load Average 1min", 156 | "mode": "float", 157 | "kind": "Custom", 158 | "customunit": "", 159 | "value": float(data[0])}, 160 | {"name": "Load Average 5min", 161 | "mode": "float", 162 | "kind": "Custom", 163 | "customunit": "", 164 | "value": float(data[1])}, 165 | {"name": "Load Average 10min", 166 | "mode": "float", 167 | "kind": "Custom", 168 | "customunit": "", 169 | "value": float(data[2])}] 170 | cpu.close() 171 | return channel_list 172 | 173 | def read_disk(self): 174 | disks = [] 175 | channel_list = [] 176 | for line in os.popen("df -k"): 177 | if line.startswith("/"): 178 | disks.append(line.rstrip().split()) 179 | for line in disks: 180 | channel1 = {"name": "Total Bytes " + str(line[0]), 181 | "mode": "integer", 182 | "kind": "BytesDisk", 183 | "value": int(line[1]) * 1024} 184 | channel2 = {"name": "Used Bytes" + str(line[0]), 185 | "mode": "integer", 186 | "kind": "BytesDisk", 187 | "value": int(line[2]) * 1024} 188 | channel3 = {"name": "Free Bytes " + str(line[0]), 189 | "mode": "integer", 190 | "kind": "BytesDisk", 191 | "value": int(line[3]) * 1024} 192 | total = float(line[2]) + float(line[3]) 193 | used = float(line[2]) / total 194 | free = float(line[3]) / total 195 | 196 | channel4 = {"name": "Free Space " + str(line[0]), 197 | "mode": "float", 198 | "kind": "Percent", 199 | "value": free * 100} 200 | channel5 = {"name": "Used Space" + str(line[0]), 201 | "mode": "float", 202 | "kind": "Percent", 203 | "value": used * 100} 204 | channel_list.append(channel1) 205 | channel_list.append(channel2) 206 | channel_list.append(channel3) 207 | channel_list.append(channel4) 208 | channel_list.append(channel5) 209 | return channel_list 210 | 211 | def read_temp(self): 212 | data = [] 213 | chandata = [] 214 | try: 215 | if os.path.exists("/sys/class/thermal/thermal_zone0/temp"): 216 | temp = open("/sys/class/thermal/thermal_zone0/temp", "r") 217 | lines = temp.readlines() 218 | temp.close() 219 | temp_string = lines[0] 220 | else: 221 | return chandata 222 | except OSError: 223 | logging.debug("Could not read temp file, no data will be returned") 224 | return chandata 225 | logging.debug("CPUTemp Debug message: Temperature from file: %s" % temp_string) 226 | temp_c = float(temp_string) / 1000.0 227 | logging.debug("CPUTemp Debug message: Temperature after calculations:: %s" % temp_c) 228 | data.append(temp_c) 229 | for i in range(len(data)): 230 | chandata.append({"name": "CPU Temperature", 231 | "mode": "float", 232 | "unit": "Custom", 233 | "customunit": "C", 234 | "LimitMode": 1, 235 | "LimitMaxError": 40, 236 | "LimitMaxWarning": 35, 237 | "value": float(data[i])}) 238 | return chandata 239 | 240 | def read_probe_health(self, config): 241 | health = 100 242 | logging.debug("Current Health: %s percent" % health) 243 | data = [] 244 | chandata = [] 245 | try: 246 | if os.path.exists("/sys/class/thermal/thermal_zone0/temp"): 247 | temp = open("/sys/class/thermal/thermal_zone0/temp", "r") 248 | lines = temp.readlines() 249 | temp.close() 250 | temp_float = float(lines[0]) / 1000.0 251 | if temp_float > config['maxtemp']: 252 | health -= 25 253 | logging.debug("Current Health: %s percent" % health) 254 | else: 255 | return chandata 256 | except OSError: 257 | logging.debug("Health not changed, no temperature available") 258 | pass 259 | disks = [] 260 | for line in os.popen("df -k"): 261 | if line.startswith("/"): 262 | disks.append(line.rstrip().split()) 263 | tmphealth = 25 / len(disks) 264 | for line in disks: 265 | free = (float(line[3]) / (float(line[2]) + float(line[3]))) * 100 266 | if free < 10: 267 | health -= tmphealth 268 | logging.debug("Current Health: %s percent" % health) 269 | cpu = open('/proc/loadavg', "r") 270 | for line in cpu: 271 | for element in line.split(" "): 272 | data.append(element) 273 | if float(data[1]) > 0.70: 274 | health -= 25 275 | logging.debug("Current Health: %s percent" % health) 276 | chandata.append({"name": "Overall Probe Health", 277 | "mode": "integer", 278 | "unit": "percent", 279 | "value": health}) 280 | return chandata 281 | -------------------------------------------------------------------------------- /miniprobe/sensors/sensor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #Copyright (c) 2014, Paessler AG 3 | #All rights reserved. 4 | #Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | #1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | #2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | #3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | #THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | # base class for all sensors 23 | # providing base functionality 24 | 25 | """ 26 | Not used at the moment. 27 | """ 28 | 29 | 30 | class Sensor(object): 31 | def __init__(self): 32 | pass 33 | -------------------------------------------------------------------------------- /miniprobe/sensors/snmpcustom.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import sys 23 | import gc 24 | import logging 25 | 26 | try: 27 | from pysnmp.entity.rfc3413.oneliner import cmdgen 28 | snmp = True 29 | except Exception as e: 30 | logging.error("PySNMP could not be imported. SNMP Sensors won't work.Error: %s" % e) 31 | snmp = False 32 | pass 33 | 34 | 35 | class SNMPCustom(object): 36 | 37 | def __init__(self): 38 | gc.enable() 39 | 40 | @staticmethod 41 | def get_kind(): 42 | """ 43 | return sensor kind 44 | """ 45 | return "mpsnmpcustom" 46 | 47 | @staticmethod 48 | def get_sensordef(): 49 | """ 50 | Definition of the sensor and data to be shown in the PRTG WebGUI 51 | """ 52 | sensordefinition = { 53 | "kind": SNMPCustom.get_kind(), 54 | "name": "SNMP Custom", 55 | "description": "Monitors a numerical value returned by a specific OID using SNMP", 56 | "help": "Monitors a numerical value returned by a specific OID using SNMP", 57 | "tag": "mpsnmpcustomsensor", 58 | "groups": [ 59 | { 60 | "name": "OID values", 61 | "caption": "OID values", 62 | "fields": [ 63 | { 64 | "type": "edit", 65 | "name": "oid", 66 | "caption": "OID Value", 67 | "required": "1", 68 | "help": "Please enter the OID value." 69 | }, 70 | { 71 | "type": "edit", 72 | "name": "unit", 73 | "caption": "Unit String", 74 | "default": "#", 75 | "help": "Enter a 'unit' string, e.g. 'ms', 'Kbyte' (for display purposes only)." 76 | }, 77 | 78 | { 79 | "type": "radio", 80 | "name": "value_type", 81 | "caption": "Value Type", 82 | "required": "1", 83 | "help": "Select 'Gauge' if you want to see absolute values (e.g. for temperature value) " 84 | "or 'Delta' for counter differences divided by time period " 85 | "(e.g. for bandwidth values)", 86 | "options": { 87 | "1": "Gauge", 88 | "2": "Delta" 89 | }, 90 | "default": 1 91 | }, 92 | { 93 | "type": "integer", 94 | "name": "multiplication", 95 | "caption": "Multiplication", 96 | "required": "1", 97 | "default": 1, 98 | "help": "Provide a value the raw SNMP value is to be multiplied by." 99 | }, 100 | { 101 | "type": "integer", 102 | "name": "division", 103 | "caption": "Division", 104 | "required": "1", 105 | "default": 1, 106 | "help": "Provide a value the raw SNMP value is divided by." 107 | }, 108 | { 109 | "type": "radio", 110 | "name": "snmp_version", 111 | "caption": "SNMP Version", 112 | "required": "1", 113 | "help": "Choose your SNMP Version", 114 | "options": { 115 | "1": "V1", 116 | "2": "V2c", 117 | "3": "V3" 118 | }, 119 | "default": 2 120 | }, 121 | { 122 | "type": "edit", 123 | "name": "community", 124 | "caption": "Community String", 125 | "required": "1", 126 | "help": "Please enter the community string." 127 | }, 128 | { 129 | "type": "integer", 130 | "name": "port", 131 | "caption": "Port", 132 | "required": "1", 133 | "default": 161, 134 | "help": "Provide the SNMP port" 135 | } 136 | ] 137 | } 138 | ] 139 | } 140 | if not snmp: 141 | sensordefinition = "" 142 | return sensordefinition 143 | 144 | def snmp_get(self, oid, target, snmp_type, community, port, unit, multiplication=1, division=1): 145 | try: 146 | sys.path.append('./') 147 | from pysnmp.entity.rfc3413.oneliner import cmdgen 148 | snmpget = cmdgen.CommandGenerator() 149 | error_indication, error_status, error_index, var_binding = snmpget.getCmd( 150 | cmdgen.CommunityData(community), cmdgen.UdpTransportTarget((target, port)), oid) 151 | except Exception as import_error: 152 | logging.error(import_error) 153 | raise 154 | 155 | if snmp_type == "1": 156 | channellist = [ 157 | { 158 | "name": "Value", 159 | "mode": "integer", 160 | "kind": "custom", 161 | "customunit": "", 162 | "value": (int(var_binding[0][1]) * int(multiplication)) / int(division) 163 | } 164 | ] 165 | else: 166 | channellist = [ 167 | { 168 | "name": "Value", 169 | "mode": "counter", 170 | "kind": "custom", 171 | "customunit": "%s" % unit, 172 | "value": (int(var_binding[0][1]) * int(multiplication)) / int(division) 173 | } 174 | ] 175 | return channellist 176 | 177 | @staticmethod 178 | def get_data(data, out_queue): 179 | snmpcustom = SNMPCustom() 180 | try: 181 | snmp_data = snmpcustom.snmp_get(str(data['oid']), data['host'], data['value_type'], 182 | data['community'], int(data['port']), data['unit'], 183 | int(data['multiplication']), int(data['division'])) 184 | logging.debug("Running sensor: %s" % snmpcustom.get_kind()) 185 | except Exception as get_data_error: 186 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (snmpcustom.get_kind(), 187 | data['sensorid'], 188 | get_data_error)) 189 | data = { 190 | "sensorid": int(data['sensorid']), 191 | "error": "Exception", 192 | "code": 1, 193 | "message": "SNMP Request failed. See log for details" 194 | } 195 | out_queue.put(data) 196 | return 1 197 | 198 | data = { 199 | "sensorid": int(data['sensorid']), 200 | "message": "OK", 201 | "channel": snmp_data 202 | } 203 | del snmpcustom 204 | gc.collect() 205 | out_queue.put(data) 206 | return 0 207 | -------------------------------------------------------------------------------- /miniprobe/sensors/snmpcustomstring.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import sys 23 | import gc 24 | import logging 25 | import time 26 | 27 | try: 28 | from pysnmp.entity.rfc3413.oneliner import cmdgen 29 | snmp = True 30 | except Exception as e: 31 | logging.error("PySNMP could not be imported. SNMP Sensors won't work.Error: %s" % e) 32 | snmp = False 33 | pass 34 | 35 | 36 | class SNMPCustomString(object): 37 | 38 | def __init__(self): 39 | gc.enable() 40 | 41 | @staticmethod 42 | def get_kind(): 43 | """ 44 | return sensor kind 45 | """ 46 | return "mpsnmpcustomstring" 47 | 48 | @staticmethod 49 | def get_sensordef(): 50 | """ 51 | Definition of the sensor and data to be shown in the PRTG WebGUI 52 | """ 53 | sensordefinition = { 54 | "kind": SNMPCustomString.get_kind(), 55 | "name": "SNMP Custom String", 56 | "description": "Monitors a string value returned by a specific OID using SNMP", 57 | "help": "Monitors a string value returned by a specific OID using SNMP", 58 | "tag": "mpsnmpcustomstringsensor", 59 | "groups": [ 60 | { 61 | "name": "OID values", 62 | "caption": "OID values", 63 | "fields": [ 64 | { 65 | "type": "edit", 66 | "name": "oid", 67 | "caption": "OID Value", 68 | "required": "1", 69 | "help": "Please enter the OID value." 70 | }, 71 | { 72 | "type": "radio", 73 | "name": "snmp_version", 74 | "caption": "SNMP Version", 75 | "required": "1", 76 | "help": "Choose your SNMP Version", 77 | "options": { 78 | "1": "V1", 79 | "2": "V2c", 80 | "3": "V3" 81 | }, 82 | "default": 2 83 | }, 84 | { 85 | "type": "edit", 86 | "name": "community", 87 | "caption": "Community String", 88 | "required": "1", 89 | "help": "Please enter the community string." 90 | }, 91 | { 92 | "type": "integer", 93 | "name": "port", 94 | "caption": "Port", 95 | "required": "1", 96 | "default": 161, 97 | "help": "Provide the SNMP port" 98 | } 99 | ] 100 | } 101 | ] 102 | } 103 | if not snmp: 104 | sensordefinition = "" 105 | return sensordefinition 106 | 107 | def snmp_get(self, oid, target, snmp_type, community, port, unit): 108 | try: 109 | sys.path.append('./') 110 | from pysnmp.entity.rfc3413.oneliner import cmdgen 111 | start = time.clock() 112 | snmpget = cmdgen.CommandGenerator() 113 | error_indication, error_status, error_index, var_binding = snmpget.getCmd( 114 | cmdgen.CommunityData(community), cmdgen.UdpTransportTarget((target, port)), oid) 115 | end = time.clock() 116 | delta = (end - start) * 1000 117 | except Exception as import_error: 118 | logging.error(import_error) 119 | raise 120 | 121 | channel_list = [ 122 | { 123 | "name": "Response Time", 124 | "mode": "float", 125 | "kind": "TimeResponse", 126 | "value": float(delta) 127 | } 128 | ] 129 | return ( 130 | str(var_binding[0][1]), 131 | channel_list 132 | ) 133 | 134 | @staticmethod 135 | def get_data(data, out_queue): 136 | snmpcustom = SNMPCustomString() 137 | try: 138 | snmp_data, channel = snmpcustom.snmp_get(str(data['oid']), data['host'], 'string', 139 | data['community'], int(data['port']), '') 140 | logging.debug("Running sensor: %s" % snmpcustom.get_kind()) 141 | except Exception as get_data_error: 142 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (snmpcustom.get_kind(), 143 | data['sensorid'], 144 | get_data_error)) 145 | data = { 146 | "sensorid": int(data['sensorid']), 147 | "error": "Exception", 148 | "code": 1, 149 | "message": "SNMP Request failed. See log for details" 150 | } 151 | out_queue.put(data) 152 | return 1 153 | 154 | data = { 155 | "sensorid": int(data['sensorid']), 156 | "message": snmp_data, 157 | "channel": channel 158 | } 159 | del snmpcustom 160 | gc.collect() 161 | out_queue.put(data) 162 | return 0 163 | -------------------------------------------------------------------------------- /miniprobe/sensors/snmpdisk.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import sys 23 | import gc 24 | import logging 25 | 26 | try: 27 | from pysnmp.entity.rfc3413.oneliner import cmdgen 28 | snmp = True 29 | except Exception as e: 30 | logging.error("PySNMP could not be imported. SNMP Sensors won't work.Error: %s" % e) 31 | snmp = False 32 | pass 33 | 34 | 35 | class SNMPDisk(object): 36 | 37 | def __init__(self): 38 | gc.enable() 39 | 40 | @staticmethod 41 | def get_kind(): 42 | """ 43 | return sensor kind 44 | """ 45 | return "mpsnmpdisk" 46 | 47 | @staticmethod 48 | def get_sensordef(): 49 | """ 50 | Definition of the sensor and data to be shown in the PRTG WebGUI 51 | """ 52 | sensordefinition = { 53 | "kind": SNMPDisk.get_kind(), 54 | "name": "SNMP Disk", 55 | "description": "Monitors disk usage using SNMP", 56 | "help": "Monitors disk usage using SNMP", 57 | "tag": "mpsnmpdisksensor", 58 | "groups": [ 59 | { 60 | "name": "OID values", 61 | "caption": "OID values", 62 | "fields": [ 63 | { 64 | "type": "edit", 65 | "name": "mount_point", 66 | "caption": "Disk mount point", 67 | "required": "1", 68 | "help": "Provide the disk mount point" 69 | }, 70 | { 71 | "type": "radio", 72 | "name": "snmp_version", 73 | "caption": "SNMP Version", 74 | "required": "1", 75 | "help": "Choose your SNMP Version", 76 | "options": { 77 | "1": "V1", 78 | "2": "V2c", 79 | "3": "V3" 80 | }, 81 | "default": 2 82 | }, 83 | { 84 | "type": "edit", 85 | "name": "community", 86 | "caption": "Community String", 87 | "required": "1", 88 | "help": "Please enter the community string." 89 | }, 90 | { 91 | "type": "integer", 92 | "name": "port", 93 | "caption": "Port", 94 | "required": "1", 95 | "default": 161, 96 | "help": "Provide the SNMP port" 97 | } 98 | ] 99 | } 100 | ] 101 | } 102 | if not snmp: 103 | sensordefinition = "" 104 | return sensordefinition 105 | 106 | def snmp_get(self, target, community, port, mount_point): 107 | try: 108 | sys.path.append('./') 109 | from pysnmp.entity.rfc3413.oneliner import cmdgen 110 | snmpget = cmdgen.CommandGenerator() 111 | error_indication, error_status, error_index, var_bind_table = snmpget.bulkCmd( 112 | cmdgen.CommunityData(community), cmdgen.UdpTransportTarget((target, port)), 113 | 0, 114 | 25, 115 | '.1.3.6.1.4.1.2021.9.1.2' 116 | ) 117 | 118 | index = -1 119 | 120 | for var_bind_table_row in var_bind_table: 121 | for name, val in var_bind_table_row: 122 | if val == mount_point: 123 | index = name[len(name) - 1] 124 | break 125 | 126 | if index == -1: 127 | raise Exception('Mount point not found') 128 | else: 129 | data = [ 130 | ".1.3.6.1.4.1.2021.9.1.6.%s" % str(index), 131 | ".1.3.6.1.4.1.2021.9.1.7.%s" % str(index) 132 | ] 133 | 134 | snmpget = cmdgen.CommandGenerator() 135 | error_indication, error_status, error_index, var_binding = snmpget.getCmd( 136 | cmdgen.CommunityData(community), cmdgen.UdpTransportTarget((target, port)), *data) 137 | if error_indication: 138 | raise Exception(error_indication) 139 | 140 | total = int(var_binding[0][1]) * 1024 141 | free = int(var_binding[1][1]) * 1024 142 | used = total - free 143 | free_percentage = float((float(free) / float(total)) * 100) 144 | used_percentage = float(100 - free_percentage) 145 | 146 | except Exception as import_error: 147 | logging.error(import_error) 148 | raise 149 | 150 | channellist = [ 151 | { 152 | "name": "Used %", 153 | "mode": "float", 154 | "kind": "Percent", 155 | "value": used_percentage 156 | }, 157 | { 158 | "name": "Free %", 159 | "mode": "float", 160 | "kind": "Percent", 161 | "value": free_percentage 162 | }, 163 | { 164 | "name": "Total", 165 | "mode": "integer", 166 | "unit": "BytesMemory", 167 | "value": total 168 | }, 169 | { 170 | "name": "Used", 171 | "mode": "integer", 172 | "unit": "BytesMemory", 173 | "value": used 174 | }, 175 | { 176 | "name": "Free", 177 | "mode": "integer", 178 | "unit": "BytesMemory", 179 | "value": free 180 | } 181 | ] 182 | return channellist 183 | 184 | @staticmethod 185 | def get_data(data, out_queue): 186 | snmpcustom = SNMPDisk() 187 | try: 188 | snmp_data = snmpcustom.snmp_get(data['host'], data['community'], int(data['port']), str(data['mount_point'])) 189 | logging.debug("Running sensor: %s" % snmpcustom.get_kind()) 190 | except Exception as get_data_error: 191 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (snmpcustom.get_kind(), 192 | data['sensorid'], 193 | get_data_error)) 194 | data = { 195 | "sensorid": int(data['sensorid']), 196 | "error": "Exception", 197 | "code": 1, 198 | "message": "SNMP Request failed. See log for details" 199 | } 200 | out_queue.put(data) 201 | return 1 202 | 203 | data = { 204 | "sensorid": int(data['sensorid']), 205 | "message": "OK", 206 | "channel": snmp_data 207 | } 208 | del snmpcustom 209 | gc.collect() 210 | out_queue.put(data) 211 | return 0 212 | -------------------------------------------------------------------------------- /miniprobe/sensors/snmpload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import sys 23 | import gc 24 | import logging 25 | 26 | try: 27 | from pysnmp.entity.rfc3413.oneliner import cmdgen 28 | snmp = True 29 | except Exception as e: 30 | logging.error("PySNMP could not be imported. SNMP Sensors won't work.Error: %s" % e) 31 | snmp = False 32 | pass 33 | 34 | 35 | class SNMPLoad(object): 36 | 37 | def __init__(self): 38 | gc.enable() 39 | 40 | @staticmethod 41 | def get_kind(): 42 | """ 43 | return sensor kind 44 | """ 45 | return "mpsnmpload" 46 | 47 | @staticmethod 48 | def get_sensordef(): 49 | """ 50 | Definition of the sensor and data to be shown in the PRTG WebGUI 51 | """ 52 | sensordefinition = { 53 | "kind": SNMPLoad.get_kind(), 54 | "name": "SNMP Load", 55 | "description": "Monitors a numerical value returned by a specific OID using SNMP", 56 | "help": "Monitors a numerical value returned by a specific OID using SNMP", 57 | "tag": "mpsnmploadsensor", 58 | "groups": [ 59 | { 60 | "name": "OID values", 61 | "caption": "OID values", 62 | "fields": [ 63 | { 64 | "type": "radio", 65 | "name": "snmp_version", 66 | "caption": "SNMP Version", 67 | "required": "1", 68 | "help": "Choose your SNMP Version", 69 | "options": { 70 | "1": "V1", 71 | "2": "V2c", 72 | "3": "V3" 73 | }, 74 | "default": 2 75 | }, 76 | { 77 | "type": "edit", 78 | "name": "community", 79 | "caption": "Community String", 80 | "required": "1", 81 | "help": "Please enter the community string." 82 | }, 83 | { 84 | "type": "integer", 85 | "name": "port", 86 | "caption": "Port", 87 | "required": "1", 88 | "default": 161, 89 | "help": "Provide the SNMP port" 90 | } 91 | ] 92 | } 93 | ] 94 | } 95 | if not snmp: 96 | sensordefinition = "" 97 | return sensordefinition 98 | 99 | def snmp_get(self, target, community, port): 100 | try: 101 | sys.path.append('./') 102 | from pysnmp.entity.rfc3413.oneliner import cmdgen 103 | 104 | data = ['.1.3.6.1.4.1.2021.10.1.3.1','.1.3.6.1.4.1.2021.10.1.3.2','.1.3.6.1.4.1.2021.10.1.3.3'] 105 | 106 | snmpget = cmdgen.CommandGenerator() 107 | error_indication, error_status, error_index, var_binding = snmpget.getCmd( 108 | cmdgen.CommunityData(community), cmdgen.UdpTransportTarget((target, port)), *data 109 | ) 110 | except Exception as import_error: 111 | logging.error(import_error) 112 | raise 113 | 114 | channel_list = [ 115 | { 116 | "name": "Load Average 1min", 117 | "mode": "float", 118 | "kind": "Custom", 119 | "customunit": "", 120 | "value": float(var_binding[0][1]) 121 | }, 122 | { 123 | "name": "Load Average 5min", 124 | "mode": "float", 125 | "kind": "Custom", 126 | "customunit": "", 127 | "value": float(var_binding[1][1]) 128 | }, 129 | { 130 | "name": "Load Average 10min", 131 | "mode": "float", 132 | "kind": "Custom", 133 | "customunit": "", 134 | "value": float(var_binding[2][1]) 135 | } 136 | ] 137 | return channel_list 138 | 139 | @staticmethod 140 | def get_data(data, out_queue): 141 | snmpcustom = SNMPLoad() 142 | try: 143 | snmp_data = snmpcustom.snmp_get( 144 | data['host'], 145 | data['community'], 146 | int(data['port']) 147 | ) 148 | logging.debug("Running sensor: %s" % snmpcustom.get_kind()) 149 | except Exception as get_data_error: 150 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (snmpcustom.get_kind(), 151 | data['sensorid'], 152 | get_data_error)) 153 | data = { 154 | "sensorid": int(data['sensorid']), 155 | "error": "Exception", 156 | "code": 1, 157 | "message": "SNMP Request failed. See log for details" 158 | } 159 | out_queue.put(data) 160 | return 1 161 | 162 | data = { 163 | "sensorid": int(data['sensorid']), 164 | "message": "OK", 165 | "channel": snmp_data 166 | } 167 | del snmpcustom 168 | gc.collect() 169 | out_queue.put(data) 170 | return 0 171 | -------------------------------------------------------------------------------- /miniprobe/sensors/snmpmemory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import gc 23 | import logging 24 | 25 | try: 26 | from pysnmp.entity.rfc3413.oneliner import cmdgen 27 | snmp = True 28 | except Exception as e: 29 | logging.error("PySNMP could not be imported. SNMP Sensors won't work.Error: %s" % e) 30 | snmp = False 31 | pass 32 | 33 | 34 | class SNMPMemory(object): 35 | 36 | def __init__(self): 37 | gc.enable() 38 | 39 | @staticmethod 40 | def get_kind(): 41 | """ 42 | return sensor kind 43 | """ 44 | return "mpsnmpmemory" 45 | 46 | @staticmethod 47 | def get_sensordef(): 48 | """ 49 | Definition of the sensor and data to be shown in the PRTG WebGUI 50 | """ 51 | sensordefinition = { 52 | "kind": SNMPMemory.get_kind(), 53 | "name": "SNMP Memory", 54 | "description": "Monitors Memory or SWAP using SNMP", 55 | "help": "Monitors Memory or SWAP using SNMP", 56 | "tag": "mpsnmpmemorysensor", 57 | "groups": [ 58 | { 59 | "name": "SNMP Settings", 60 | "caption": "SNMP Settings", 61 | "fields": [ 62 | { 63 | "type": "radio", 64 | "name": "memory_type", 65 | "caption": "Memory Type", 66 | "required": "1", 67 | "help": "Choose memory type to monitor.", 68 | "options": { 69 | "1": "Memory", 70 | "2": "SWAP" 71 | }, 72 | "default": 1 73 | }, 74 | { 75 | "type": "radio", 76 | "name": "snmp_version", 77 | "caption": "SNMP Version", 78 | "required": "1", 79 | "help": "Choose your SNMP Version", 80 | "options": { 81 | "1": "V1", 82 | "2": "V2c", 83 | "3": "V3" 84 | }, 85 | "default": 2 86 | }, 87 | { 88 | "type": "edit", 89 | "name": "community", 90 | "caption": "Community String", 91 | "required": "1", 92 | "help": "Please enter the community string." 93 | }, 94 | { 95 | "type": "integer", 96 | "name": "port", 97 | "caption": "Port", 98 | "required": "1", 99 | "default": 161, 100 | "help": "Provide the SNMP port" 101 | } 102 | ] 103 | } 104 | ], 105 | "fields": [] 106 | } 107 | if not snmp: 108 | sensordefinition = "" 109 | return sensordefinition 110 | 111 | def snmp_get(self, target, community, port, memory_type): 112 | if memory_type == 1: 113 | data = ['.1.3.6.1.4.1.2021.4.5.0', '.1.3.6.1.4.1.2021.4.6.0'] 114 | else: 115 | data = ['.1.3.6.1.4.1.2021.4.3.0', '.1.3.6.1.4.1.2021.4.4.0'] 116 | snmpget = cmdgen.CommandGenerator() 117 | error_indication, error_status, error_index, var_binding = snmpget.getCmd( 118 | cmdgen.CommunityData(community), cmdgen.UdpTransportTarget((target, port)), *data) 119 | if error_indication: 120 | raise Exception(error_indication) 121 | 122 | total = int(var_binding[0][1]) * 1024 123 | free = int(var_binding[1][1]) * 1024 124 | used = total - free 125 | free_percentage = float((float(free) / float(total)) * 100) 126 | used_percentage = float(100 - free_percentage) 127 | 128 | channellist = [ 129 | { 130 | "name": "Used %", 131 | "mode": "float", 132 | "kind": "Percent", 133 | "value": used_percentage 134 | }, 135 | { 136 | "name": "Free %", 137 | "mode": "float", 138 | "kind": "Percent", 139 | "value": free_percentage 140 | }, 141 | { 142 | "name": "Total", 143 | "mode": "integer", 144 | "unit": "BytesMemory", 145 | "value": total 146 | }, 147 | { 148 | "name": "Used", 149 | "mode": "integer", 150 | "unit": "BytesMemory", 151 | "value": used 152 | }, 153 | { 154 | "name": "Free", 155 | "mode": "integer", 156 | "unit": "BytesMemory", 157 | "value": free 158 | } 159 | ] 160 | return channellist 161 | 162 | @staticmethod 163 | def get_data(data, out_queue): 164 | snmpmemory = SNMPMemory() 165 | try: 166 | snmp_data = snmpmemory.snmp_get( 167 | data['host'], 168 | data['community'], 169 | int(data['port']), 170 | int(data['memory_type']) 171 | ) 172 | logging.debug("Running sensor: %s" % snmpmemory.get_kind()) 173 | except Exception as get_data_error: 174 | print get_data_error 175 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (snmpmemory.get_kind(), 176 | data['sensorid'], 177 | get_data_error)) 178 | data = { 179 | "sensorid": int(data['sensorid']), 180 | "error": "Exception", 181 | "code": 1, 182 | "message": "SNMP Request failed. See log for details" 183 | } 184 | out_queue.put(data) 185 | return 1 186 | 187 | data = { 188 | "sensorid": int(data['sensorid']), 189 | "message": "OK", 190 | "channel": snmp_data 191 | } 192 | del snmpmemory 193 | gc.collect() 194 | out_queue.put(data) 195 | return 0 196 | -------------------------------------------------------------------------------- /miniprobe/sensors/snmpprocess.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import sys 23 | import gc 24 | import logging 25 | 26 | try: 27 | from pysnmp.entity.rfc3413.oneliner import cmdgen 28 | snmp = True 29 | except Exception as e: 30 | logging.error("PySNMP could not be imported. SNMP Sensors won't work.Error: %s" % e) 31 | snmp = False 32 | pass 33 | 34 | 35 | class SNMPProcess(object): 36 | 37 | def __init__(self): 38 | gc.enable() 39 | 40 | @staticmethod 41 | def get_kind(): 42 | """ 43 | return sensor kind 44 | """ 45 | return "mpsnmpprocess" 46 | 47 | @staticmethod 48 | def get_sensordef(): 49 | """ 50 | Definition of the sensor and data to be shown in the PRTG WebGUI 51 | """ 52 | sensordefinition = { 53 | "kind": SNMPProcess.get_kind(), 54 | "name": "SNMP Process", 55 | "description": "Monitors process count using SNMP", 56 | "help": "Monitors process count using SNMP", 57 | "tag": "mpsnmpprocesssensor", 58 | "groups": [ 59 | { 60 | "name": "OID values", 61 | "caption": "OID values", 62 | "fields": [ 63 | { 64 | "type": "edit", 65 | "name": "process_name", 66 | "caption": "Process Name", 67 | "required": "1", 68 | "help": "Provide the process name" 69 | }, 70 | { 71 | "type": "radio", 72 | "name": "snmp_version", 73 | "caption": "SNMP Version", 74 | "required": "1", 75 | "help": "Choose your SNMP Version", 76 | "options": { 77 | "1": "V1", 78 | "2": "V2c", 79 | "3": "V3" 80 | }, 81 | "default": 2 82 | }, 83 | { 84 | "type": "edit", 85 | "name": "community", 86 | "caption": "Community String", 87 | "required": "1", 88 | "help": "Please enter the community string." 89 | }, 90 | { 91 | "type": "integer", 92 | "name": "port", 93 | "caption": "Port", 94 | "required": "1", 95 | "default": 161, 96 | "help": "Provide the SNMP port" 97 | } 98 | ] 99 | } 100 | ] 101 | } 102 | if not snmp: 103 | sensordefinition = "" 104 | return sensordefinition 105 | 106 | def snmp_get(self, target, community, port, process_name): 107 | try: 108 | sys.path.append('./') 109 | from pysnmp.entity.rfc3413.oneliner import cmdgen 110 | snmpget = cmdgen.CommandGenerator() 111 | error_indication, error_status, error_index, var_bind_table = snmpget.bulkCmd( 112 | cmdgen.CommunityData(community), cmdgen.UdpTransportTarget((target, port)), 113 | 0, 114 | 25, 115 | '.1.3.6.1.4.1.2021.2.1.2' 116 | ) 117 | 118 | index = -1 119 | 120 | for var_bind_table_row in var_bind_table: 121 | for name, val in var_bind_table_row: 122 | if val == process_name: 123 | index = name[len(name) - 1] 124 | break 125 | 126 | if index == -1: 127 | raise Exception('Process not found') 128 | else: 129 | snmpget = cmdgen.CommandGenerator() 130 | error_indication, error_status, error_index, var_binding = snmpget.getCmd( 131 | cmdgen.CommunityData(community), cmdgen.UdpTransportTarget((target, port)), 132 | ".1.3.6.1.4.1.2021.2.1.5.%d" % index 133 | ) 134 | except Exception as import_error: 135 | logging.error(import_error) 136 | raise 137 | 138 | channellist = [ 139 | { 140 | "name": "Process Count", 141 | "mode": "integer", 142 | "kind": "Custom", 143 | "customunit": "", 144 | "value": int(var_binding[0][1]) 145 | } 146 | ] 147 | return channellist 148 | 149 | @staticmethod 150 | def get_data(data, out_queue): 151 | snmpcustom = SNMPProcess() 152 | try: 153 | snmp_data = snmpcustom.snmp_get(data['host'], data['community'], int(data['port']), str(data['process_name'])) 154 | logging.debug("Running sensor: %s" % snmpcustom.get_kind()) 155 | except Exception as get_data_error: 156 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (snmpcustom.get_kind(), 157 | data['sensorid'], 158 | get_data_error)) 159 | data = { 160 | "sensorid": int(data['sensorid']), 161 | "error": "Exception", 162 | "code": 1, 163 | "message": "SNMP Request failed. See log for details" 164 | } 165 | out_queue.put(data) 166 | return 1 167 | 168 | data = { 169 | "sensorid": int(data['sensorid']), 170 | "message": "OK", 171 | "channel": snmp_data 172 | } 173 | del snmpcustom 174 | gc.collect() 175 | out_queue.put(data) 176 | return 0 177 | -------------------------------------------------------------------------------- /miniprobe/sensors/snmptraffic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014, Paessler AG 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | # following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | # and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 9 | # and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 11 | # or promote products derived from this software without specific prior written permission. 12 | 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 15 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 17 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 18 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 19 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | import gc 23 | import logging 24 | 25 | try: 26 | from pysnmp.entity.rfc3413.oneliner import cmdgen 27 | snmp = True 28 | except Exception as e: 29 | logging.error("PySNMP could not be imported. SNMP Sensors won't work.Error: %s" % e) 30 | snmp = False 31 | pass 32 | 33 | 34 | class SNMPTraffic(object): 35 | 36 | def __init__(self): 37 | gc.enable() 38 | 39 | @staticmethod 40 | def get_kind(): 41 | """ 42 | return sensor kind 43 | """ 44 | return "mpsnmptraffic" 45 | 46 | @staticmethod 47 | def get_sensordef(): 48 | """ 49 | Definition of the sensor and data to be shown in the PRTG WebGUI 50 | """ 51 | sensordefinition = { 52 | "kind": SNMPTraffic.get_kind(), 53 | "name": "SNMP Traffic", 54 | "description": "Monitors Traffic on provided interface using SNMP", 55 | "help": "Monitors Traffic on provided interface using SNMP", 56 | "tag": "mpsnmptrafficsensor", 57 | "groups": [ 58 | { 59 | "name": "Interface Definition", 60 | "caption": "Interface Definition", 61 | "fields": [ 62 | { 63 | "type": "edit", 64 | "name": "ifindex", 65 | "caption": "Interface Index (ifIndex)", 66 | "required": "1", 67 | "help": "Please enter the ifIndex of the interface to be monitored." 68 | } 69 | 70 | ] 71 | }, 72 | { 73 | "name": "SNMP Settings", 74 | "caption": "SNMP Settings", 75 | "fields": [ 76 | { 77 | "type": "radio", 78 | "name": "snmp_version", 79 | "caption": "SNMP Version", 80 | "required": "1", 81 | "help": "Choose your SNMP Version", 82 | "options": { 83 | "1": "V1", 84 | "2": "V2c", 85 | "3": "V3" 86 | }, 87 | "default": 2 88 | }, 89 | { 90 | "type": "edit", 91 | "name": "community", 92 | "caption": "Community String", 93 | "required": "1", 94 | "help": "Please enter the community string." 95 | }, 96 | { 97 | "type": "integer", 98 | "name": "port", 99 | "caption": "Port", 100 | "required": "1", 101 | "default": 161, 102 | "help": "Provide the SNMP port" 103 | }, 104 | { 105 | "type": "radio", 106 | "name": "snmp_counter", 107 | "caption": "SNMP Counter Type", 108 | "required": "1", 109 | "help": "Choose the Counter Type to be used", 110 | "options": { 111 | "1": "32 bit", 112 | "2": "64 bit" 113 | }, 114 | "default": 2 115 | } 116 | ] 117 | } 118 | ], 119 | "fields": [] 120 | } 121 | if not snmp: 122 | sensordefinition = "" 123 | return sensordefinition 124 | 125 | def snmp_get(self, target, countertype, community, port, ifindex): 126 | if countertype == "1": 127 | data = ["1.3.6.1.2.1.2.2.1.10.%s" % str(ifindex), "1.3.6.1.2.1.2.2.1.16.%s" % str(ifindex)] 128 | else: 129 | data = ["1.3.6.1.2.1.31.1.1.1.6.%s" % str(ifindex), "1.3.6.1.2.1.31.1.1.1.10.%s" % str(ifindex)] 130 | snmpget = cmdgen.CommandGenerator() 131 | error_indication, error_status, error_index, var_binding = snmpget.getCmd( 132 | cmdgen.CommunityData(community), cmdgen.UdpTransportTarget((target, port)), *data) 133 | if error_indication: 134 | raise Exception(error_indication) 135 | if countertype == "1": 136 | traffic_in = str(long(var_binding[0][1])) 137 | traffic_out = str(long(var_binding[1][1])) 138 | traffic_total = str(long(var_binding[0][1]) + long(var_binding[1][1])) 139 | else: 140 | traffic_in = str(long(var_binding[0][1])) 141 | traffic_out = str(long(var_binding[1][1])) 142 | traffic_total = str(long(var_binding[0][1]) + long(var_binding[1][1])) 143 | 144 | channellist = [ 145 | { 146 | "name": "Traffic Total", 147 | "mode": "counter", 148 | "unit": "BytesBandwidth", 149 | "value": traffic_total 150 | }, 151 | { 152 | "name": "Traffic In", 153 | "mode": "counter", 154 | "unit": "BytesBandwidth", 155 | "value": traffic_in 156 | }, 157 | { 158 | "name": "Traffic Out", 159 | "mode": "counter", 160 | "unit": "BytesBandwidth", 161 | "value": traffic_out 162 | } 163 | ] 164 | return channellist 165 | 166 | @staticmethod 167 | def get_data(data, out_queue): 168 | snmptraffic = SNMPTraffic() 169 | try: 170 | snmp_data = snmptraffic.snmp_get(data['host'], data['snmp_counter'], 171 | data['community'], int(data['port']), data['ifindex']) 172 | logging.debug("Running sensor: %s" % snmptraffic.get_kind()) 173 | except Exception as get_data_error: 174 | logging.error(get_data_error) 175 | logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (snmptraffic.get_kind(), 176 | data['sensorid'], 177 | get_data_error)) 178 | data = { 179 | "sensorid": int(data['sensorid']), 180 | "error": "Exception", 181 | "code": 1, 182 | "message": "SNMP Request failed. See log for details" 183 | } 184 | out_queue.put(data) 185 | return 1 186 | 187 | data = { 188 | "sensorid": int(data['sensorid']), 189 | "message": "OK", 190 | "channel": snmp_data 191 | } 192 | del snmptraffic 193 | gc.collect() 194 | out_queue.put(data) 195 | return 0 196 | -------------------------------------------------------------------------------- /miniprobe/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PRTG/PythonMiniProbe/110b53756d988a5cd2bead8cfbcaa315f665c226/miniprobe/tests/__init__.py -------------------------------------------------------------------------------- /miniprobe/tests/test_miniprobe.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from nose.tools import * 4 | from miniprobe import miniprobe 5 | 6 | class TestMiniProbe(): 7 | @classmethod 8 | def setup_class(cls): 9 | cls.http = False 10 | cls.config = {} 11 | cls.config['gid'] = 'testgid' 12 | cls.config['key'] = 'testkey' 13 | cls.config['protocol'] = 'testprotocol' 14 | cls.config['name'] = 'testname' 15 | cls.config['baseinterval'] = 'testbaseinterval' 16 | cls.config['server'] = 'testserver' 17 | cls.config['port'] = 'testport' 18 | cls.mp = miniprobe.MiniProbe(cls.http) 19 | 20 | def test_miniprobe_hash_access_key(self): 21 | """miniprobe returns the correct hash_access_key""" 22 | test_hash_access_key = self.mp 23 | assert_equal(test_hash_access_key.hash_access_key(self.config['key']), '913a73b565c8e2c8ed94497580f619397709b8b6') 24 | 25 | def test_miniprobe_create_parameters(self): 26 | """miniprobe returns the correct create_parameters""" 27 | test_create_parameters = self.mp 28 | assert_equal(test_create_parameters.create_parameters(self.config, '{}'), {'gid': 'testgid', 'protocol': 'testprotocol', 'key': '913a73b565c8e2c8ed94497580f619397709b8b6'}) 29 | assert_equal(test_create_parameters.create_parameters(self.config, '{}', 'announce'), {'protocol': 'testprotocol', 'name': 'testname', 'gid': 'testgid', 'baseinterval': 'testbaseinterval', 'key': '913a73b565c8e2c8ed94497580f619397709b8b6', 'sensors': '{}'}) 30 | 31 | def test_miniprobe_create_url(self): 32 | """miniprobe returns the correct create_url""" 33 | test_create_url = self.mp 34 | assert_equal(test_create_url.create_url(self.config), 'No method given') 35 | assert_equal(test_create_url.create_url(self.config, 'test'), 'https://testserver:testport/probe/test') 36 | assert_equal(test_create_url.create_url(self.config, 'data'), 'https://testserver:testport/probe/data?gid=testgid&protocol=testprotocol&key=913a73b565c8e2c8ed94497580f619397709b8b6') 37 | 38 | def test_build_task(self): 39 | """miniprobe returns correct task payload""" 40 | test_build_task = self.mp 41 | assert_equal(test_build_task.build_task(self.config), {'gid': 'testgid', 'protocol': 'testprotocol', 'key': '913a73b565c8e2c8ed94497580f619397709b8b6'}) 42 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.5.3 2 | pyasn1>=0.1.7 3 | pysnmp>=4.2.5 4 | dnspython>=1.12 5 | 6 | -------------------------------------------------------------------------------- /requirements3.txt: -------------------------------------------------------------------------------- 1 | requests==2.5.3 2 | pyasn1>=0.1.7 3 | pysnmp>=4.2.5 4 | dnspython3>=1.12 --------------------------------------------------------------------------------