├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── protobix ├── __init__.py ├── datacontainer.py ├── sampleprobe.py ├── senderprotocol.py └── zabbixagentconfig.py ├── pytest.ini ├── requirements-test.txt ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── docker-tests.sh ├── test_datacontainer.py ├── test_memory_leak.py ├── test_sampleprobe.py ├── test_senderprotocol.py ├── test_zabbixagentconfig.py ├── tls_ca ├── protobix-ca.cert.pem ├── protobix-client.cert.pem ├── protobix-client.key.pem ├── protobix-client.not-yet-valid.cert.pem ├── protobix-client.not-yet-valid.key.pem ├── protobix-client.revoked.cert.pem ├── protobix-client.revoked.key.pem ├── protobix-zabbix-server.cert.pem ├── protobix-zabbix-server.key.pem ├── protobix.crl ├── rogue-protobix-ca.cert.pem ├── rogue-protobix-client.cert.pem ├── rogue-protobix-client.key.pem ├── rogue-protobix-client.not-yet-valid.cert.pem ├── rogue-protobix-client.not-yet-valid.key.pem ├── rogue-protobix-client.revoked.cert.pem ├── rogue-protobix-client.revoked.key.pem ├── rogue-protobix.crl ├── rogue-zabbix-server.cert.pem └── rogue-zabbix-server.key.pem └── zabbix ├── zabbix_server.conf ├── zabbix_server_mysql.sql └── zabbix_server_sqlite3.sql /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .cache 3 | *.pyc 4 | *.swp 5 | /__pycache__/ 6 | /.eggs/ 7 | MANIFEST 8 | dist/ 9 | *.egg-info 10 | *.xdb 11 | *.db 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "3.5" 6 | # comand to install requirements 7 | install: pip install -r requirements.txt -r requirements-test.txt 8 | # command to run tests 9 | script: python setup.py test 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-protobix 2 | 3 | * `dev` Branch: [![Build Status](https://travis-ci.org/jbfavre/python-protobix.svg?branch=dev)](https://travis-ci.org/jbfavre/python-protobix) 4 | * `upstream` Branch (default): [![Build Status](https://travis-ci.org/jbfavre/python-protobix.svg?branch=upstream)](https://travis-ci.org/jbfavre/python-protobix) 5 | 6 | `python-protobix` is a very simple python module which implements [Zabbix Sender protocol 2.0](https://www.zabbix.org/wiki/Docs/protocols/zabbix_sender/2.0). 7 | It allows to build a list of Zabbix items and send them as `trappers`. 8 | 9 | Currently `python-protobix` supports "classics" `items` as well as [`Low Level Discovery`](https://www.zabbix.com/documentation/2.4/manual/discovery/low_level_discovery) ones. 10 | 11 | Please note that `python-protobix` is developped and tested on Debian GNU/Linux only. 12 | I can't enforce compatibility with other distributions, though it should work on any distribution providing Python 2.7 or Python 3.x. 13 | 14 | Any feedback on this is, of course, welcomed. 15 | 16 | ## Test 17 | 18 | To install all required dependencies and launch test suite 19 | 20 | python setup.py test 21 | 22 | By default, all tests named like `*need_backend*` are disabled, since they need a working Zabbix Server. 23 | 24 | If you want to run theses tests as well, you will need: 25 | * a working Zabbix Server 3.x configuration file like the one in `tests/zabbix/zabbix_server.conf` 26 | * SQL statements in `tests/zabbix/zabbix_server_mysql.sql` with all informations to create testing hosts & items 27 | 28 | You can then start Zabbix Server with `zabbix_server -c tests/zabbix/zabbix_server.conf -f` and launch test suite with 29 | 30 | py.test --cov protobix --cov-report term-missing 31 | 32 | ### Using a docker container 33 | 34 | You can also use docker to run test suite on any Linux distribution of your choice. 35 | You can use provided script `docker-tests.sh` as entrypoint example: 36 | 37 | docker run --volume=$(pwd):/home/python-protobix --entrypoint=/home/python-protobix/tests/docker-tests.sh -ti debian:jessie 38 | 39 | Currently, entrypoint `docker-tests.sh` only supports Debian GNU/Linux. 40 | 41 | __Please note that this docker entrypoint does not provide a way to execute test that need a backend__. 42 | 43 | ## Installation 44 | 45 | With `pip` (stable version): 46 | 47 | pip install protobix 48 | 49 | With `pip` (test version): 50 | 51 | pip install -i https://testpypi.python.org/simple/ protobix 52 | 53 | Python is available as Debian package for Debian GNU/Linux sid and testing. 54 | 55 | ## Usage 56 | 57 | Once module is installed, you can use it either extending `protobix.SampleProbe` or directly using `protobix.Datacontainer`. 58 | 59 | ### Extend `protobix.SampleProbe` 60 | 61 | `python-protobix` provides a convenient sample probe you can extend to fit your own needs. 62 | 63 | Using `protobix.SampleProbe` allows you to concentrate on getting metrics or Low Level Discovery items without taking care of anything related to `protobix` itself. 64 | This is the recommanded way of using `python-protobix`. 65 | 66 | `protobix.SampleProbe` provides a `run` method which take care of everything related to `protobix`. 67 | 68 | Some probes are available from my Github repository [`python-zabbix`](https://github.com/jbfavre/python-zabbix) 69 | 70 | ```python 71 | #!/usr/bin/env python 72 | # -*- coding: utf-8 -*- 73 | ''' Copyright (c) 2013 Jean Baptiste Favre. 74 | Sample Python class which extends protobix.SampleProbe 75 | ''' 76 | import protobix 77 | import argparse 78 | import socket 79 | import sys 80 | 81 | class ExampleProbe(protobix.SampleProbe): 82 | 83 | __version__ = '1.0.2' 84 | # discovery_key is *not* the one declared in Zabbix Agent configuration 85 | # it's the one declared in Zabbix template's "Discovery rules" 86 | discovery_key = "example.probe.llddiscovery" 87 | 88 | def _parse_probe_args(self, parser): 89 | # Parse the script arguments 90 | # parser is an instance of argparse.parser created by SampleProbe._parse_args method 91 | # you *must* return parser to SampleProbe so that your own options are taken into account 92 | example_probe_options = parser.add_argument_group('ExampleProbe configuration') 93 | example_probe_options.add_argument( 94 | "-o", "--option", default="default_value", 95 | help="WTF do this option" 96 | ) 97 | return parser 98 | 99 | def _init_probe(self): 100 | # Whatever you need to initiliaze your probe 101 | # Can be establishing a connection 102 | # Or reading a configuration file 103 | # If you have nothing special to do 104 | # Just do not override this method 105 | # Or use: 106 | pass 107 | 108 | def _get_discovery(self): 109 | # Whatever you need to do to discover LLD items 110 | # this method is mandatory 111 | # If not declared, calling the probe ith --discovery option will resut in a NotimplementedError 112 | # If you get discovery infos for only one node you should return data as follow 113 | return { self.hostname: data } 114 | # If you get discovery infos for many hosts, then you should build data dict by yourself 115 | # and return result as follow 116 | return data 117 | 118 | def _get_metrics(self): 119 | # Whatever you need to do to collect metrics 120 | # this method is mandatory 121 | # If not declared, calling the probe with --update-items option will resut in a NotimplementedError 122 | # If you get metrics for only one node you should return data as follow 123 | return { self.hostname: data } 124 | # If you get metrics for many hosts, then you should build data dict by your self 125 | # and return result as follow 126 | return data 127 | 128 | if __name__ == '__main__': 129 | ret = RedisServer().run() 130 | print ret 131 | sys.exit(ret) 132 | ``` 133 | 134 | Declare your newly created probe as `Zabbix Agent` user parameters: 135 | 136 | UserParameter=example.probe.check,/usr/local/bin/example_probe.py --update-items 137 | UserParameter=example.probe.discovery,/usr/local/bin/example_probe.py --discovery 138 | 139 | You're done. 140 | 141 | The `protobix.SampleProbe` exit code will be sent to Zabbix. 142 | You'll be able to setup triggers if needed. 143 | 144 | __Exit codes mapping__: 145 | * 0: everything went well 146 | * 1: probe failed at step 1 (probe initialization) 147 | * 2: probe failed at step 2 (probe data collection) 148 | * 3: probe failed at step 3 (add data to DataContainer) 149 | * 4: probe failed at step 4 (send data to Zabbix) 150 | 151 | ### Use `protobix.Datacontainer` 152 | 153 | If you don't want or can't use `protobix.SampleProbe`, you can also directly use `protobix.Datacontainer`. 154 | 155 | __How to send items updates__ 156 | 157 | ```python 158 | #!/usr/bin/env python 159 | 160 | ''' import module ''' 161 | import protobix 162 | 163 | DATA = { 164 | "protobix.host1": { 165 | "my.protobix.item.int": 0, 166 | "my.protobix.item.string": "item string" 167 | }, 168 | "protobix.host2": { 169 | "my.protobix.item.int": 0, 170 | "my.protobix.item.string": "item string" 171 | } 172 | } 173 | 174 | zbx_datacontainer = protobix.DataContainer() 175 | zbx_datacontainer.data_type = 'items' 176 | zbx_datacontainer.add(DATA) 177 | zbx_datacontainer.send() 178 | ``` 179 | 180 | __How to send Low Level Discovery__ 181 | 182 | ```python 183 | #!/usr/bin/env python 184 | 185 | ''' import module ''' 186 | import protobix 187 | 188 | DATA = { 189 | 'protobix.host1': { 190 | 'my.protobix.lld_item1': [ 191 | { '{#PBX_LLD_KEY11}': 0, 192 | '{#PBX_LLD_KEY12}': 'lld string' }, 193 | { '{#PBX_LLD_KEY11}': 1, 194 | '{#PBX_LLD_KEY12}': 'another lld string' } 195 | ], 196 | 'my.protobix.lld_item2': [ 197 | { '{#PBX_LLD_KEY21}': 10, 198 | '{#PBX_LLD_KEY21}': 'yet an lld string' }, 199 | { '{#PBX_LLD_KEY21}': 2, 200 | '{#PBX_LLD_KEY21}': 'yet another lld string' } 201 | ] 202 | }, 203 | 'protobix.host2': { 204 | 'my.protobix.lld_item1': [ 205 | { '{#PBX_LLD_KEY11}': 0, 206 | '{#PBX_LLD_KEY12}': 'lld string' }, 207 | { '{#PBX_LLD_KEY11}': 1, 208 | '{#PBX_LLD_KEY12}': 'another lld string' } 209 | ], 210 | 'my.protobix.lld_item2': [ 211 | { '{#PBX_LLD_KEY21}': 10, 212 | '{#PBX_LLD_KEY21}': 'yet an lld string' }, 213 | { '{#PBX_LLD_KEY21}': 2, 214 | '{#PBX_LLD_KEY21}': 'yet another lld string' } 215 | ] 216 | } 217 | } 218 | 219 | zbx_datacontainer = protobix.DataContainer() 220 | zbx_datacontainer.data_type = 'lld' 221 | zbx_datacontainer.add(DATA) 222 | zbx_datacontainer.send() 223 | ``` 224 | 225 | ## Advanced configuration 226 | 227 | `python-protobix` behaviour can be altered in many ways using options. 228 | All configuration options are stored in a `protobix.ZabbixAgentConfig` instance. 229 | 230 | __Protobix specific configuration options__ 231 | 232 | | Option name | Default value | ZabbixAgentConfig property | Command-line option (SampleProbe) | 233 | |--------------|---------------|----------------------------|-----------------------------------| 234 | | `data_type` | `None` | `data_type` | `--update-items` or `--discovery` | 235 | | `dryrun` | `False` | `dryrun` | `-d` or `--dryrun` | 236 | 237 | __Zabbix Agent configuration options__ 238 | 239 | | Option name | Default value | ZabbixAgentConfig property | Command-line option (SampleProbe) | 240 | |------------------------|--------------------------|----------------------------|-----------------------------------| 241 | | `ServerActive` | `127.0.0.1` | `server_active` | `-z` or `--zabbix-server` | 242 | | `ServerPort` | `10051` | `server_port` | `-p` or `--port` | 243 | | `LogType` | `file` | `log_type` | none | 244 | | `LogFile` | `/tmp/zabbix_agentd.log` | `log_file` | none | 245 | | `DebugLevel` | `3` | `debug_level` | `-v` (from none to `-vvvvv`) | 246 | | `Timeout` | `3` | `timeout` | none | 247 | | `Hostname` | `socket.getfqdn()` | `hostname` | none | 248 | | `TLSConnect` | `unencrypted` | `tls_connect` | `--tls-connect` | 249 | | `TLSCAFile` | `None` | `tls_ca_file` | `--tls-ca-file` | 250 | | `TLSCertFile` | `None` | `tls_cert_file` | `--tls-cert-file` | 251 | | `TLSCRLFile` | `None` | `tls_crl_file` | `--tls-crl-file` | 252 | | `TLSKeyFile` | `None` | `tls_key_file` | `--tls-key-file` | 253 | | `TLSServerCertIssuer` | `None` | `tls_server_cert_issuer` | `--tls-server-cert-issuer` | 254 | | `TLSServerCertSubject` | `None` | `tls_server_cert_subject` | `--tls-server-cert-subject` | 255 | 256 | ## How to contribute 257 | 258 | You can contribute to `protobix`: 259 | * fork this repository 260 | * write tests and documentation (tests __must__ pass for both Python 2.7 & 3.x) 261 | * implement the feature you need 262 | * open a pull request against __`upstream`__ branch 263 | -------------------------------------------------------------------------------- /protobix/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Protobix is a simple module which implement Zabbix Sender protocol 3 | It provides a sample probe you can extend to monitor any software with Zabbix 4 | """ 5 | from .datacontainer import DataContainer 6 | from .senderprotocol import SenderProtocol 7 | from .sampleprobe import SampleProbe 8 | from .zabbixagentconfig import ZabbixAgentConfig 9 | -------------------------------------------------------------------------------- /protobix/datacontainer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | try: import simplejson as json 3 | except ImportError: import json # pragma: no cover 4 | 5 | from .zabbixagentconfig import ZabbixAgentConfig 6 | from .senderprotocol import SenderProtocol 7 | 8 | # For both 2.0 & >2.2 Zabbix version 9 | # ? 1.8: Processed 0 Failed 1 Total 1 Seconds spent 0.000057 10 | # 2.0: Processed 0 Failed 1 Total 1 Seconds spent 0.000057 11 | # 2.2: processed: 50; failed: 1000; total: 1050; seconds spent: 0.09957 12 | # 2.4: processed: 50; failed: 1000; total: 1050; seconds spent: 0.09957 13 | ZBX_DBG_SEND_RESULT = "Send result [%s-%s-%s] for key [%s] item [%s]. Server's response is %s" 14 | ZBX_TRAPPER_MAX_VALUE = 250 15 | 16 | class DataContainer(SenderProtocol): 17 | 18 | _items_list = [] 19 | _result = [] 20 | _logger = None 21 | _config = None 22 | socket = None 23 | 24 | def __init__(self, 25 | config=None, 26 | logger=None): 27 | 28 | # Loads config from zabbix_agentd file 29 | # If no file, it uses the default _config as configuration 30 | self._config = config 31 | if config is None: 32 | self._config = ZabbixAgentConfig() 33 | if logger: 34 | self.logger = logger 35 | self._items_list = [] 36 | 37 | def add_item(self, host, key, value, clock=None, state=0): 38 | """ 39 | Add a single item into DataContainer 40 | 41 | :host: hostname to which item will be linked to 42 | :key: item key as defined in Zabbix 43 | :value: item value 44 | :clock: timestemp as integer. If not provided self.clock()) will be used 45 | """ 46 | if clock is None: 47 | clock = self.clock 48 | if self._config.data_type == "items": 49 | item = {"host": host, "key": key, 50 | "value": value, "clock": clock, "state": state} 51 | elif self._config.data_type == "lld": 52 | item = {"host": host, "key": key, "clock": clock, "state": state, 53 | "value": json.dumps({"data": value})} 54 | else: 55 | if self.logger: # pragma: no cover 56 | self.logger.error("Setup data_type before adding data") 57 | raise ValueError('Setup data_type before adding data') 58 | self._items_list.append(item) 59 | 60 | def add(self, data): 61 | """ 62 | Add a list of item into the container 63 | 64 | :data: dict of items & value per hostname 65 | """ 66 | for host in data: 67 | for key in data[host]: 68 | if not data[host][key] == []: 69 | self.add_item(host, key, data[host][key]) 70 | 71 | def send(self): 72 | """ 73 | Entrypoint to send data to Zabbix 74 | If debug is enabled, items are sent one by one 75 | If debug isn't enable, we send items in bulk 76 | Returns a list of results (1 if no debug, as many as items in other case) 77 | """ 78 | if self.logger: # pragma: no cover 79 | self.logger.info("Starting to send %d items" % len(self._items_list)) 80 | try: 81 | # Zabbix trapper send a maximum of 250 items in bulk 82 | # We have to respect that, in case of enforcement on zabbix server side 83 | # Special case if debug is enabled: we need to send items one by one 84 | max_value = ZBX_TRAPPER_MAX_VALUE 85 | if self.debug_level >= 4: 86 | max_value = 1 87 | if self.logger: # pragma: no cover 88 | self.logger.debug("Bulk limit is %d items" % max_value) 89 | else: 90 | if self.logger: # pragma: no cover 91 | self.logger.info("Bulk limit is %d items" % max_value) 92 | # Initialize offsets & counters 93 | max_offset = len(self._items_list) 94 | run = 0 95 | start_offset = 0 96 | stop_offset = min(start_offset + max_value, max_offset) 97 | server_success = server_failure = processed = failed = total = time = 0 98 | while start_offset < stop_offset: 99 | run += 1 100 | if self.logger: # pragma: no cover 101 | self.logger.debug( 102 | 'run %d: start_offset is %d, stop_offset is %d' % 103 | (run, start_offset, stop_offset) 104 | ) 105 | 106 | # Extract items to be send from global item's list' 107 | _items_to_send = self.items_list[start_offset:stop_offset] 108 | 109 | # Send extracted items 110 | run_response, run_processed, run_failed, run_total, run_time = self._send_common(_items_to_send) 111 | 112 | # Update counters 113 | if run_response == 'success': 114 | server_success += 1 115 | elif run_response == 'failed': 116 | server_failure += 1 117 | processed += run_processed 118 | failed += run_failed 119 | total += run_total 120 | time += run_time 121 | if self.logger: # pragma: no cover 122 | self.logger.info("%d items sent during run %d" % (run_total, run)) 123 | self.logger.debug( 124 | 'run %d: processed is %d, failed is %d, total is %d' % 125 | (run, run_processed, run_failed, run_total) 126 | ) 127 | 128 | # Compute next run's offsets 129 | start_offset = stop_offset 130 | stop_offset = min(start_offset + max_value, max_offset) 131 | 132 | # Reset socket, which is likely to be closed by server 133 | self._socket_reset() 134 | except: 135 | self._reset() 136 | self._socket_reset() 137 | raise 138 | if self.logger: # pragma: no cover 139 | self.logger.info('All %d items have been sent in %d runs' % (total, run)) 140 | self.logger.debug( 141 | 'Total run is %d; item processed: %d, failed: %d, total: %d, during %f seconds' % 142 | (run, processed, failed, total, time) 143 | ) 144 | # Everything has been sent. 145 | # Reset DataContainer & return results_list 146 | self._reset() 147 | return server_success, server_failure, processed, failed, total, time 148 | 149 | def _send_common(self, item): 150 | """ 151 | Common part of sending operations 152 | Calls SenderProtocol._send_to_zabbix 153 | Returns result as provided by _handle_response 154 | 155 | :item: either a list or a single item depending on debug_level 156 | """ 157 | total = len(item) 158 | processed = failed = time = 0 159 | if self._config.dryrun is True: 160 | total = len(item) 161 | processed = failed = time = 0 162 | response = 'dryrun' 163 | else: 164 | self._send_to_zabbix(item) 165 | response, processed, failed, total, time = self._read_from_zabbix() 166 | 167 | output_key = '(bulk)' 168 | output_item = '(bulk)' 169 | if self.debug_level >= 4: 170 | output_key = item[0]['key'] 171 | output_item = item[0]['value'] 172 | if self.logger: # pragma: no cover 173 | self.logger.info( 174 | "" + 175 | ZBX_DBG_SEND_RESULT % ( 176 | processed, 177 | failed, 178 | total, 179 | output_key, 180 | output_item, 181 | response 182 | ) 183 | ) 184 | return response, processed, failed, total, time 185 | 186 | def _reset(self): 187 | """ 188 | Reset main DataContainer properties 189 | """ 190 | # Reset DataContainer to default values 191 | # So that it can be reused 192 | if self.logger: # pragma: no cover 193 | self.logger.info("Reset DataContainer") 194 | self._items_list = [] 195 | self._config.data_type = None 196 | 197 | @property 198 | def logger(self): 199 | """ 200 | Returns logger instance 201 | """ 202 | return self._logger 203 | 204 | @logger.setter 205 | def logger(self, value): 206 | """ 207 | Set logger instance for the class 208 | """ 209 | if isinstance(value, logging.Logger): 210 | self._logger = value 211 | else: 212 | if self._logger: # pragma: no cover 213 | self._logger.error("logger requires a logging instance") 214 | raise ValueError('logger requires a logging instance') 215 | 216 | # ZabbixAgentConfig getter & setter 217 | # Avoid using private property _config from outside 218 | @property 219 | def dryrun(self): 220 | """ 221 | Returns dryrun 222 | """ 223 | return self._config.dryrun 224 | 225 | @dryrun.setter 226 | def dryrun(self, value): 227 | """ 228 | Set dryrun 229 | """ 230 | self._config.dryrun = value 231 | 232 | @dryrun.setter 233 | def data_type(self, value): 234 | """ 235 | Set data_type 236 | """ 237 | self._config.data_type = value 238 | -------------------------------------------------------------------------------- /protobix/sampleprobe.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from argparse import RawTextHelpFormatter 3 | import socket 4 | import sys 5 | import traceback 6 | import logging 7 | from logging import handlers 8 | 9 | from .datacontainer import DataContainer 10 | from .zabbixagentconfig import ZabbixAgentConfig 11 | 12 | class SampleProbe(object): 13 | 14 | __version__ = '1.0.2' 15 | # Mapping between zabbix-agent Debug option & logging level 16 | LOG_LEVEL = [ 17 | logging.NOTSET, 18 | logging.CRITICAL, 19 | logging.ERROR, 20 | logging.INFO, 21 | logging.DEBUG, 22 | logging.DEBUG, 23 | ] 24 | logger = None 25 | probe_config = None 26 | hostname = None 27 | options = None 28 | 29 | def _parse_args(self, args): 30 | if self.logger: 31 | self.logger.info( 32 | "Read command line options" 33 | ) 34 | # Parse the script arguments 35 | parser = argparse.ArgumentParser( 36 | usage='%(prog)s [options]', 37 | formatter_class=RawTextHelpFormatter, 38 | epilog='Protobix - copyright 2016 - Jean Baptiste Favre (www.jbfavre.org)' 39 | ) 40 | # Probe operation mode 41 | probe_mode = parser.add_argument_group('Probe commands') 42 | probe_mode.add_argument( 43 | '--update-items', action='store_true', dest='update', 44 | help="Get & send items to Zabbix.\nThis is the default behaviour" 45 | ) 46 | probe_mode.add_argument( 47 | '--discovery', action='store_true', 48 | help="If specified, will perform Zabbix Low Level Discovery." 49 | ) 50 | # Common options 51 | common = parser.add_argument_group('Common options') 52 | common.add_argument( 53 | '-d', '--dryrun', action='store_true', 54 | help="Do not send anything to Zabbix. Usefull to debug with\n" 55 | "--verbose option" 56 | ) 57 | common.add_argument( 58 | '-v', action='count', dest='debug_level', 59 | help="Enable verbose mode. Is used to setup logging level.\n" 60 | "Specifying 4 or more 'v' (-vvvv) enables Debug. Items are then\n" 61 | "sent one after the other instead of bulk" 62 | ) 63 | # Protobix specific options 64 | protobix = parser.add_argument_group('Protobix specific options') 65 | protobix.add_argument( 66 | '-z', '--zabbix-server', dest='server_active', 67 | help="Hostname or IP address of Zabbix server. If a host is\n" 68 | "monitored by a proxy, proxy hostname or IP address\n" 69 | "should be used instead. When used together with\n" 70 | "--config, overrides the first entry of ServerActive\n" 71 | "parameter specified in agentd configuration file." 72 | ) 73 | protobix.add_argument( 74 | '-p', '--port', dest='server_port', 75 | help="Specify port number of Zabbix server trapper running on\n" 76 | "the server. Default is 10051. When used together with \n" 77 | "--config, overrides the port of first entry of\n" 78 | "ServerActive parameter specified in agentd configuration\n" 79 | "file." 80 | ) 81 | protobix.add_argument( 82 | '-c', '--config', dest='config_file', 83 | help="Use config-file. Zabbix sender reads server details from\n" 84 | "the agentd configuration file. By default Protobix reads\n" 85 | "`/etc/zabbix/zabbix_agentd.conf`.\n" 86 | "Absolute path should be specified." 87 | ) 88 | protobix.add_argument( 89 | '--tls-connect', choices=['unencrypted', 'psk', 'cert'], 90 | help="How to connect to server or proxy. Values:\n" 91 | "unencrypted connect without encryption\n" 92 | "psk connect using TLS and a pre-shared key\n" 93 | "cert connect using TLS and a certificate." 94 | ) 95 | protobix.add_argument( 96 | '--tls-ca-file', 97 | help="Full pathname of a file containing the top-level CA(s)\n" 98 | "certificates for peer certificate verification." 99 | ) 100 | protobix.add_argument( 101 | '--tls-cert-file', 102 | help="Full pathname of a file containing the certificate or\n" 103 | "certificate chain." 104 | ) 105 | protobix.add_argument( 106 | '--tls-key-file', 107 | help="Full pathname of a file containing the private key." 108 | ) 109 | protobix.add_argument( 110 | '--tls-crl-file', 111 | help="Full pathname of a file containing revoked certificates." 112 | ) 113 | protobix.add_argument( 114 | '--tls-server-cert-issuer', 115 | help="Allowed server certificate issuer." 116 | ) 117 | protobix.add_argument( 118 | '--tls-server-cert-subject', 119 | help="Allowed server certificate subject." 120 | ) 121 | # TLS PSK is not implemented in Python 122 | # https://bugs.python.org/issue19084 123 | # Following options are not implemented 124 | protobix.add_argument( 125 | '--tls-psk-identity', 126 | help="PSK-identity string." 127 | ) 128 | protobix.add_argument( 129 | '--tls-psk-file', 130 | help="Full pathname of a file containing the pre-shared key." 131 | ) 132 | # Probe specific options 133 | parser = self._parse_probe_args(parser) 134 | # Analyze provided command line options 135 | options = parser.parse_args(args) 136 | 137 | # Check that we don't have both '--update' & '--discovery' options 138 | options.probe_mode = 'update' 139 | if options.update is True and options.discovery is True: 140 | raise ValueError( 141 | 'You can\' use both --update-items & --discovery options' 142 | ) 143 | elif options.discovery is True: 144 | options.probe_mode = 'discovery' 145 | 146 | return options 147 | 148 | def _init_logging(self): 149 | logger = logging.getLogger(self.__class__.__name__) 150 | logger.handlers = [] 151 | logger.setLevel(logging.NOTSET) 152 | self.logger = logger 153 | 154 | def _setup_logging(self, log_type, debug_level, log_file): 155 | if self.logger: 156 | self.logger.info( 157 | "Initialize logging" 158 | ) 159 | # Enable log like Zabbix Agent does 160 | # Though, when we have a tty, it's convenient to use console to log 161 | common_log_format = '[%(name)s:%(levelname)s] %(message)s' 162 | if log_type == 'console' or sys.stdout.isatty(): 163 | console_handler = logging.StreamHandler() 164 | console_formatter = logging.Formatter( 165 | fmt=common_log_format, 166 | datefmt='%Y%m%d:%H%M%S' 167 | ) 168 | console_handler.setFormatter(console_formatter) 169 | self.logger.addHandler(console_handler) 170 | if log_type == 'file': 171 | file_handler = logging.FileHandler(log_file) 172 | # Use same date format as Zabbix: when logging into 173 | # zabbix_agentd log file, it's easier to read & parse 174 | log_formatter = logging.Formatter( 175 | fmt='%(process)d:%(asctime)s.%(msecs)03d ' + common_log_format, 176 | datefmt='%Y%m%d:%H%M%S' 177 | ) 178 | file_handler.setFormatter(log_formatter) 179 | self.logger.addHandler(file_handler) 180 | if log_type == 'system': 181 | # TODO: manage syslog address as command line option 182 | syslog_handler = logging.handlers.SysLogHandler( 183 | address=('localhost', 514), 184 | facility=logging.handlers.SysLogHandler.LOG_DAEMON 185 | ) 186 | # Use same date format as Zabbix does: when logging into 187 | # zabbix_agentd log file, it's easier to read & parse 188 | log_formatter = logging.Formatter( 189 | fmt='%(process)d:%(asctime)s.%(msecs)03d ' + common_log_format, 190 | datefmt='%Y%m%d:%H%M%S' 191 | ) 192 | syslog_handler.setFormatter(log_formatter) 193 | self.logger.addHandler(syslog_handler) 194 | self.logger.setLevel( 195 | self.LOG_LEVEL[debug_level] 196 | ) 197 | 198 | def _init_config(self): 199 | if self.logger: 200 | self.logger.info( 201 | "Get configuration" 202 | ) 203 | # Get config from ZabbixAgentConfig 204 | zbx_config = ZabbixAgentConfig(self.options.config_file) 205 | 206 | # And override it with provided command line options 207 | if self.options.server_active: 208 | zbx_config.server_active = self.options.server_active 209 | 210 | if self.options.server_port: 211 | zbx_config.server_port = int(self.options.server_port) 212 | 213 | # tls_connect 'cert' needed options 214 | if self.options.tls_cert_file: 215 | zbx_config.tls_cert_file = self.options.tls_cert_file 216 | 217 | if self.options.tls_key_file: 218 | zbx_config.tls_key_file = self.options.tls_key_file 219 | 220 | if self.options.tls_ca_file: 221 | zbx_config.tls_ca_file = self.options.tls_ca_file 222 | 223 | if self.options.tls_crl_file: 224 | zbx_config.tls_crl_file = self.options.tls_crl_file 225 | 226 | # tls_connect 'psk' needed options 227 | if self.options.tls_psk_file: 228 | zbx_config.tls_psk_file = self.options.tls_psk_file 229 | 230 | if self.options.tls_psk_identity: 231 | zbx_config.tls_psk_identity = self.options.tls_psk_identity 232 | 233 | if self.options.tls_server_cert_issuer: 234 | zbx_config.tls_server_cert_issuer = self.options.tls_server_cert_issuer 235 | 236 | if self.options.tls_server_cert_subject: 237 | zbx_config.tls_server_cert_subject = self.options.tls_server_cert_subject 238 | 239 | # Set tls_connect last because it'll check above options 240 | # to ensure a coherent config set 241 | if self.options.tls_connect: 242 | zbx_config.tls_connect = self.options.tls_connect 243 | 244 | if self.options.debug_level: 245 | self.options.debug_level = min(4, self.options.debug_level) 246 | zbx_config.debug_level = self.options.debug_level 247 | 248 | zbx_config.dryrun = False 249 | if self.options.dryrun: 250 | zbx_config.dryrun = self.options.dryrun 251 | 252 | return zbx_config 253 | 254 | def _get_metrics(self): 255 | # mandatory method 256 | raise NotImplementedError 257 | 258 | def _get_discovery(self): 259 | # mandatory method 260 | raise NotImplementedError 261 | 262 | def _init_probe(self): 263 | # non mandatory method 264 | pass 265 | 266 | def _parse_probe_args(self, parser): 267 | # non mandatory method 268 | return parser 269 | 270 | def run(self, options=None): 271 | # Init logging with default values since we don't have real config yet 272 | self._init_logging() 273 | 274 | # Parse command line options 275 | args = sys.argv[1:] 276 | if isinstance(options, list): 277 | args = options 278 | self.options = self._parse_args(args) 279 | 280 | # Get configuration 281 | self.zbx_config = self._init_config() 282 | 283 | # Update logger with configuration 284 | self._setup_logging( 285 | self.zbx_config.log_type, 286 | self.zbx_config.debug_level, 287 | self.zbx_config.log_file 288 | ) 289 | 290 | # Datacontainer init 291 | zbx_container = DataContainer( 292 | config = self.zbx_config, 293 | logger=self.logger 294 | ) 295 | # Get back hostname from ZabbixAgentConfig 296 | self.hostname = self.zbx_config.hostname 297 | 298 | # Step 1: read probe configuration 299 | # initialize any needed object or connection 300 | try: 301 | self._init_probe() 302 | except: 303 | if self.logger: 304 | self.logger.critical( 305 | "Step 1 - Read probe configuration failed" 306 | ) 307 | self.logger.debug(traceback.format_exc()) 308 | return 1 309 | 310 | # Step 2: get data 311 | try: 312 | data = {} 313 | if self.options.probe_mode == "update": 314 | zbx_container.data_type = 'items' 315 | data = self._get_metrics() 316 | elif self.options.probe_mode == "discovery": 317 | zbx_container.data_type = 'lld' 318 | data = self._get_discovery() 319 | except NotImplementedError as e: 320 | if self.logger: 321 | self.logger.critical( 322 | "Step 2 - Get Data failed [%s]" % str(e) 323 | ) 324 | self.logger.debug(traceback.format_exc()) 325 | raise 326 | except Exception as e: 327 | if self.logger: 328 | self.logger.critical( 329 | "Step 2 - Get Data failed [%s]" % str(e) 330 | ) 331 | self.logger.debug(traceback.format_exc()) 332 | return 2 333 | 334 | # Step 3: add data to container 335 | try: 336 | zbx_container.add(data) 337 | except Exception as e: 338 | if self.logger: 339 | self.logger.critical( 340 | "Step 3 - Format & add Data failed [%s]" % str(e) 341 | ) 342 | self.logger.debug(traceback.format_exc()) 343 | zbx_container._reset() 344 | return 3 345 | 346 | # Step 4: send data to Zabbix server 347 | try: 348 | zbx_container.send() 349 | except socket.error as e: 350 | if self.logger: 351 | self.logger.critical( 352 | "Step 4 - Sent to Zabbix Server [%s] failed [%s]" % ( 353 | self.zbx_config.server_active, 354 | str(e) 355 | ) 356 | ) 357 | self.logger.debug(traceback.format_exc()) 358 | return 4 359 | except Exception as e: 360 | if self.logger: 361 | self.logger.critical( 362 | "Step 4 - Unknown error [%s]" % str(e) 363 | ) 364 | self.logger.debug(traceback.format_exc()) 365 | return 4 366 | # Everything went fine. Let's return 0 and exit 367 | return 0 368 | -------------------------------------------------------------------------------- /protobix/senderprotocol.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import sys 3 | import time 4 | import re 5 | 6 | import socket 7 | try: import simplejson as json 8 | except ImportError: import json # pragma: no cover 9 | 10 | from .zabbixagentconfig import ZabbixAgentConfig 11 | 12 | if sys.version_info < (3,): # pragma: no cover 13 | def b(x): 14 | return x 15 | else: # pragma: no cover 16 | import codecs 17 | def b(x): 18 | return codecs.utf_8_encode(x)[0] 19 | 20 | HAVE_DECENT_SSL = False 21 | if sys.version_info > (2,7,9): 22 | import ssl 23 | # Zabbix force TLSv1.2 protocol 24 | # in src/libs/zbxcrypto/tls.c function zbx_tls_init_child 25 | ZBX_TLS_PROTOCOL=ssl.PROTOCOL_TLSv1_2 26 | HAVE_DECENT_SSL = True 27 | 28 | ZBX_HDR = "ZBXD\1" 29 | ZBX_HDR_SIZE = 13 30 | ZBX_RESP_REGEX = r'[Pp]rocessed:? (\d+);? [Ff]ailed:? (\d+);? ' + \ 31 | r'[Tt]otal:? (\d+);? [Ss]econds spent:? (\d+\.\d+)' 32 | 33 | class SenderProtocol(object): 34 | 35 | REQUEST = "sender data" 36 | _logger = None 37 | 38 | def __init__(self, logger=None): 39 | self._config = ZabbixAgentConfig() 40 | self._items_list = [] 41 | self.socket = None 42 | if logger: # pragma: no cover 43 | self._logger = logger 44 | 45 | @property 46 | def server_active(self): 47 | return self._config.server_active 48 | 49 | @server_active.setter 50 | def server_active(self, value): 51 | if self._logger: # pragma: no cover 52 | self._logger.debug( 53 | "Replacing server_active '%s' with '%s'" % 54 | (self._config.server_active, value) 55 | ) 56 | self._config.server_active = value 57 | 58 | @property 59 | def server_port(self): 60 | return self._config.server_port 61 | 62 | @server_port.setter 63 | def server_port(self, value): 64 | if self._logger: # pragma: no cover 65 | self._logger.debug( 66 | "Replacing server_port '%s' with '%s'" % 67 | (self._config.server_port, value) 68 | ) 69 | self._config.server_port = value 70 | 71 | @property 72 | def debug_level(self): 73 | return self._config.debug_level 74 | 75 | @debug_level.setter 76 | def debug_level(self, value): 77 | if self._logger: # pragma: no cover 78 | self._logger.debug( 79 | "Replacing debug_level '%s' with '%s'" % 80 | (self._config.debug_level, value) 81 | ) 82 | self._config.debug_level = value 83 | 84 | @property 85 | def items_list(self): 86 | return self._items_list 87 | 88 | @property 89 | def clock(self): 90 | return int(time.time()) 91 | 92 | def _send_to_zabbix(self, item): 93 | if self._logger: # pragma: no cover 94 | self._logger.info( 95 | "Send data to Zabbix Server" 96 | ) 97 | 98 | # Format data to be sent 99 | if self._logger: # pragma: no cover 100 | self._logger.debug( 101 | "Building packet to be sent to Zabbix Server" 102 | ) 103 | payload = json.dumps({"data": item, 104 | "request": self.REQUEST, 105 | "clock": self.clock }) 106 | if self._logger: # pragma: no cover 107 | self._logger.debug('About to send: ' + str(payload)) 108 | data_length = len(payload) 109 | data_header = struct.pack('= 4096: 128 | _buffer = self._socket().recv(4096) 129 | zbx_srv_resp_data += _buffer 130 | recv_length = len(_buffer) 131 | 132 | _buffer = None 133 | recv_length = None 134 | # Check that we have a valid Zabbix header mark 135 | if self._logger: # pragma: no cover 136 | self._logger.debug( 137 | "Checking Zabbix headers" 138 | ) 139 | assert zbx_srv_resp_data[:5] == b(ZBX_HDR) 140 | 141 | # Extract response body length from packet 142 | zbx_srv_resp_body_len = struct.unpack('= 3: # pragma: no cover 160 | zbx_srv_resp_body = zbx_srv_resp_body.decode() 161 | # Analyze Zabbix answer 162 | response, processed, failed, total, time = self._handle_response(zbx_srv_resp_body) 163 | 164 | # Return Zabbix Server answer as JSON 165 | return response, processed, failed, total, time 166 | 167 | def _handle_response(self, zbx_answer): 168 | """ 169 | Analyze Zabbix Server response 170 | Returns a list with number of: 171 | * processed items 172 | * failed items 173 | * total items 174 | * time spent 175 | 176 | :zbx_answer: Zabbix server response as string 177 | """ 178 | zbx_answer = json.loads(zbx_answer) 179 | if self._logger: # pragma: no cover 180 | self._logger.info( 181 | "Anaylizing Zabbix Server's answer" 182 | ) 183 | if zbx_answer: 184 | self._logger.debug("Zabbix Server response is: [%s]" % zbx_answer) 185 | 186 | # Default items number in length of th storage list 187 | nb_item = len(self._items_list) 188 | if self._config.debug_level >= 4: 189 | # If debug enabled, force it to 1 190 | nb_item = 1 191 | 192 | # If dryrun is disabled, we can process answer 193 | response = zbx_answer.get('response') 194 | result = re.findall(ZBX_RESP_REGEX, zbx_answer.get('info')) 195 | processed, failed, total, time = result[0] 196 | 197 | return response, int(processed), int(failed), int(total), float(time) 198 | 199 | def _socket_reset(self): 200 | if self.socket: 201 | if self._logger: # pragma: no cover 202 | self._logger.info( 203 | "Reset socket" 204 | ) 205 | self.socket.close() 206 | self.socket = None 207 | 208 | def _socket(self): 209 | # If socket already exists, use it 210 | if self.socket is not None: 211 | if self._logger: # pragma: no cover 212 | self._logger.debug( 213 | "Using existing socket" 214 | ) 215 | return self.socket 216 | 217 | # If not, we have to create it 218 | if self._logger: # pragma: no cover 219 | self._logger.debug( 220 | "Setting socket options" 221 | ) 222 | socket.setdefaulttimeout(self._config.timeout) 223 | # Connect to Zabbix server or proxy with provided config options 224 | if self._logger: # pragma: no cover 225 | self._logger.info( 226 | "Creating new socket" 227 | ) 228 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 229 | 230 | # TLS is enabled, let's set it up 231 | if self._config.tls_connect != 'unencrypted' and HAVE_DECENT_SSL is True: 232 | if self._logger: # pragma: no cover 233 | self._logger.info( 234 | 'Configuring TLS to %s' % str(self._config.tls_connect) 235 | ) 236 | # Setup TLS context & Wrap socket 237 | self.socket = self._init_tls() 238 | if self._logger: # pragma: no cover 239 | self._logger.info( 240 | 'Network socket initialized with TLS support' 241 | ) 242 | 243 | if self._logger and isinstance(self.socket, socket.socket): # pragma: no cover 244 | self._logger.info( 245 | 'Network socket initialized with no TLS' 246 | ) 247 | # Connect to Zabbix Server 248 | self.socket.connect( 249 | (self._config.server_active, self._config.server_port) 250 | ) 251 | #if isinstance(self.socket, ssl.SSLSocket): 252 | # server_cert = self.socket.getpeercert() 253 | # if self._config.tls_server_cert_issuer: 254 | # print(server_cert['issuer']) 255 | # assert server_cert['issuer'] == self._config.tls_server_cert_issuer 256 | # self._logger.info( 257 | # 'Server certificate issuer is %s' % 258 | # server_cert['issuer'] 259 | # ) 260 | # if self._config.tls_server_cert_subject: 261 | # print(server_cert['subject']) 262 | # assert server_cert['subject'] == self._config.tls_server_cert_subject 263 | # self._logger.info( 264 | # 'Server certificate subject is %s' % 265 | # server_cert['subject'] 266 | # ) 267 | 268 | return self.socket 269 | 270 | """ 271 | Manage TLS context & Wrap socket 272 | Returns ssl.SSLSocket if TLS enabled 273 | socket.socket if TLS disabled 274 | """ 275 | def _init_tls(self): 276 | # Create a SSLContext and configure it 277 | if self._logger: # pragma: no cover 278 | self._logger.info( 279 | "Initialize TLS context" 280 | ) 281 | ssl_context = ssl.SSLContext(ZBX_TLS_PROTOCOL) 282 | if self._logger: # pragma: no cover 283 | self._logger.debug( 284 | 'Setting TLS verify_mode to ssl.CERT_REQUIRED' 285 | ) 286 | ssl_context.verify_mode = ssl.CERT_REQUIRED 287 | 288 | # Avoid CRIME and related attacks 289 | if self._logger: # pragma: no cover 290 | self._logger.debug( 291 | 'Setting TLS option ssl.OP_NO_COMPRESSION' 292 | ) 293 | ssl_context.options |= ssl.OP_NO_COMPRESSION 294 | ssl_context.verify_flags = ssl.VERIFY_X509_STRICT 295 | 296 | # If tls_connect is cert, we must provide client cert file & key 297 | if self._config.tls_connect == 'cert': 298 | if self._logger: # pragma: no cover 299 | self._logger.debug( 300 | "Using provided TLSCertFile %s" % self._config.tls_cert_file 301 | ) 302 | self._logger.debug( 303 | "Using provided TLSKeyFile %s" % self._config.tls_key_file 304 | ) 305 | ssl_context.load_cert_chain( 306 | self._config.tls_cert_file, 307 | self._config.tls_key_file 308 | ) 309 | elif self._config.tls_connect == 'psk': 310 | raise NotImplementedError 311 | 312 | # If provided, use CA file & enforce server certificate chek 313 | if self._config.tls_ca_file: 314 | if self._logger: # pragma: no cover 315 | self._logger.debug( 316 | "Using provided TLSCAFile %s" % self._config.tls_ca_file 317 | ) 318 | ssl_context.load_default_certs(ssl.Purpose.SERVER_AUTH) 319 | ssl_context.load_verify_locations( 320 | cafile=self._config.tls_ca_file 321 | ) 322 | 323 | # If provided, use CRL file & enforce server certificate check 324 | if self._config.tls_crl_file: 325 | if self._logger: # pragma: no cover 326 | self._logger.debug( 327 | "Using provided TLSCRLFile %s" % self._config.tls_crl_file 328 | ) 329 | ssl_context.verify_flags |= ssl.VERIFY_CRL_CHECK_LEAF 330 | ssl_context.load_verify_locations( 331 | cafile=self._config.tls_crl_file 332 | ) 333 | 334 | ## If provided enforce server cert issuer check 335 | #if self._config.tls_server_cert_issuer: 336 | # verify_issuer 337 | 338 | ## If provided enforce server cert subject check 339 | #if self._config.tls_server_cert_issuer: 340 | # verify_issuer 341 | 342 | # Once configuration is done, wrap network socket to TLS context 343 | tls_socket = ssl_context.wrap_socket( 344 | self.socket 345 | ) 346 | assert isinstance(tls_socket, ssl.SSLSocket) 347 | return tls_socket 348 | -------------------------------------------------------------------------------- /protobix/zabbixagentconfig.py: -------------------------------------------------------------------------------- 1 | import configobj 2 | import socket 3 | 4 | class ZabbixAgentConfig(object): 5 | 6 | _logger = None 7 | _default_config_file='/etc/zabbix/zabbix_agentd.conf' 8 | 9 | def __init__(self, config_file=None, logger=None): 10 | if config_file is None: 11 | config_file=self._default_config_file 12 | 13 | if logger: # pragma: no cover 14 | self._logger = logger 15 | 16 | if self._logger: # pragma: no cover 17 | self._logger.info( 18 | "Initializing" 19 | ) 20 | 21 | # Set default config value from sample zabbix_agentd.conf 22 | # Only exception is hostname. While non mandatory, we must have 23 | # This property set. Default goes to server FQDN 24 | # We do *NOT* support HostnameItem except to fake system.hostname 25 | self.config = { 26 | # Protobix specific options 27 | 'data_type': None, 28 | 'dryrun': False, 29 | # Zabbix Agent options 30 | 'ServerActive': '127.0.0.1', 31 | 'ServerPort': 10051, 32 | 'LogType': 'file', 33 | 'LogFile': '/tmp/zabbix_agentd.log', 34 | 'DebugLevel': 3, 35 | 'Timeout': 3, 36 | 'Hostname': socket.getfqdn(), 37 | 'TLSConnect': 'unencrypted', 38 | 'TLSCAFile': None, 39 | 'TLSCertFile': None, 40 | 'TLSCRLFile': None, 41 | 'TLSKeyFile': None, 42 | 'TLSServerCertIssuer': None, 43 | 'TLSServerCertSubject': None, 44 | 'TLSPSKIdentity': None, 45 | 'TLSPSKFile': None, 46 | } 47 | 48 | # list_values=False argument below is needed because of potential 49 | # UserParameter with spaces which breaks ConfigObj 50 | # See 51 | if self._logger: # pragma: no cover 52 | self._logger.debug( 53 | "Reading Zabbix Agent configuration file %s" % 54 | config_file 55 | ) 56 | tmp_config = configobj.ConfigObj(config_file, list_values=False) 57 | 58 | # If not config_file found or provided, 59 | # we should fallback to the default 60 | if tmp_config == {}: 61 | if self._logger: # pragma: no cover 62 | self._logger.warn( 63 | "Not configuration found" 64 | ) 65 | return 66 | 67 | if self._logger: # pragma: no cover 68 | self._logger.debug( 69 | "Setting configuration" 70 | ) 71 | if 'DebugLevel' in tmp_config: 72 | self.debug_level = int(tmp_config['DebugLevel']) 73 | 74 | if 'Timeout' in tmp_config: 75 | self.timeout = int(tmp_config['Timeout']) 76 | 77 | if 'Hostname' in tmp_config: 78 | self.hostname = tmp_config['Hostname'] 79 | 80 | # Process LogType & LogFile & ServerACtive in separate methods 81 | # Due to custom logic 82 | self._process_server_config(tmp_config) 83 | self._process_log_config(tmp_config) 84 | self._process_tls_config(tmp_config) 85 | 86 | def _process_server_config(self, tmp_config): 87 | if self._logger: # pragma: no cover 88 | self._logger.debug( 89 | "Processing server config" 90 | ) 91 | if 'ServerActive' in tmp_config: 92 | # Because of list_values=False above, 93 | # we have to check ServerActive format 94 | # and extract server & port manually 95 | # See https://github.com/jbfavre/python-protobix/issues/16 96 | tmp_server = tmp_config['ServerActive'].split(',')[0] \ 97 | if "," in tmp_config['ServerActive'] else tmp_config['ServerActive'] 98 | self.server_active, server_port = \ 99 | tmp_server.split(':') if ":" in tmp_server else (tmp_server, 10051) 100 | self.server_port = int(server_port) 101 | 102 | def _process_log_config(self, tmp_config): 103 | if self._logger: # pragma: no cover 104 | self._logger.debug( 105 | "Processing log config" 106 | ) 107 | if 'LogType' in tmp_config and tmp_config['LogType'] in ['file', 'system', 'console']: 108 | self.log_type = tmp_config['LogType'] 109 | elif 'LogType' in tmp_config: 110 | raise ValueError('LogType must be one of [file,system,console]') 111 | 112 | # At this point, LogType is one of [file,system,console] 113 | if self.log_type in ['system', 'console']: 114 | # If LogType if console or system, we don't need LogFile 115 | self.log_file = None 116 | elif self.log_type == 'file': 117 | # LogFile will be used 118 | if 'LogFile' in tmp_config and tmp_config['LogFile'] == '-': 119 | # Zabbix 2.4 compatibility 120 | # LogFile to '-' means we want to use syslog 121 | self.log_file = None 122 | self.log_type = 'system' 123 | elif 'LogFile' in tmp_config: 124 | self.log_file = tmp_config['LogFile'] 125 | 126 | def _process_tls_config(self, tmp_config): 127 | if self._logger: # pragma: no cover 128 | self._logger.debug( 129 | "Processing tls config" 130 | ) 131 | if 'TLSConnect' in tmp_config: 132 | self.tls_connect = tmp_config['TLSConnect'] 133 | 134 | if self.tls_connect == 'cert': 135 | if 'TLSCertFile' in tmp_config and \ 136 | 'TLSKeyFile' in tmp_config and \ 137 | 'TLSCAFile' in tmp_config: 138 | self.tls_cert_file = tmp_config['TLSCertFile'] 139 | self.tls_key_file = tmp_config['TLSKeyFile'] 140 | self.tls_ca_file = tmp_config['TLSCAFile'] 141 | else: 142 | raise ValueError('TLSConnect is cert. TLSCertFile, TLSKeyFile and TLSCAFile are mandatory') 143 | if 'TLSCRLFile' in tmp_config: 144 | self.tls_crl_file = tmp_config['TLSCRLFile'] 145 | if 'TLSServerCertIssuer' in tmp_config: 146 | self.tls_server_cert_issuer = tmp_config['TLSServerCertIssuer'] 147 | if 'TLSServerCertSubject' in tmp_config: 148 | self.tls_server_cert_subject = tmp_config['TLSServerCertSubject'] 149 | 150 | if self.tls_connect == 'psk': 151 | if 'TLSPSKIdentity' in tmp_config and 'TLSPSKFile' in tmp_config: 152 | self.tls_psk_identity = tmp_config['TLSPSKIdentity'] 153 | self.tls_psk_file = tmp_config['TLSPSKFile'] 154 | else: 155 | raise ValueError('TLSConnect is psk. TLSPSKIdentity and TLSPSKFile are mandatory') 156 | 157 | @property 158 | def server_active(self): 159 | return self.config['ServerActive'] 160 | 161 | @server_active.setter 162 | def server_active(self, value): 163 | if value: 164 | self.config['ServerActive'] = value 165 | 166 | @property 167 | def server_port(self): 168 | return self.config['ServerPort'] 169 | 170 | @server_port.setter 171 | def server_port(self, value): 172 | # Must between 1024-32767 like ListenPort for Server & Proxy 173 | # https://www.zabbix.com/documentation/3.0/manual/appendix/config/zabbix_server 174 | if isinstance(value, int) and value >= 1024 and value <= 32767: 175 | self.config['ServerPort'] = value 176 | else: 177 | raise ValueError('ServerPort must be between 1024 and 32767') 178 | 179 | @property 180 | def log_type(self): 181 | if 'LogType' in self.config: 182 | return self.config['LogType'] 183 | 184 | @log_type.setter 185 | def log_type(self, value): 186 | if value and value in ['file', 'system', 'console']: 187 | self.config['LogType'] = value 188 | 189 | @property 190 | def log_file(self): 191 | return self.config['LogFile'] 192 | 193 | @log_file.setter 194 | def log_file(self, value): 195 | self.config['LogFile'] = value 196 | 197 | @property 198 | def debug_level(self): 199 | return self.config['DebugLevel'] 200 | 201 | @debug_level.setter 202 | def debug_level(self, value): 203 | # Must be between 0 and 5 204 | # https://www.zabbix.com/documentation/3.0/manual/appendix/config/zabbix_agentd 205 | if isinstance(value, int) and value >= 0 and value <= 5: 206 | self.config['DebugLevel'] = value 207 | else: 208 | raise ValueError('DebugLevel must be between 0 and 5, ' + str(value) + ' provided') 209 | 210 | @property 211 | def timeout(self): 212 | return self.config['Timeout'] 213 | 214 | @timeout.setter 215 | def timeout(self, value): 216 | # Must be between 1 and 30 217 | # https://www.zabbix.com/documentation/3.0/manual/appendix/config/zabbix_agentd 218 | if isinstance(value, int) and value > 0 and value <= 30: 219 | self.config['Timeout'] = value 220 | else: 221 | raise ValueError('Timeout must be between 1 and 30') 222 | 223 | @property 224 | def hostname(self): 225 | return self.config['Hostname'] 226 | 227 | @hostname.setter 228 | def hostname(self, value): 229 | if value: 230 | self.config['Hostname'] = value 231 | 232 | @property 233 | def tls_connect(self): 234 | return self.config['TLSConnect'] 235 | 236 | @tls_connect.setter 237 | def tls_connect(self, value): 238 | if value in ['unencrypted', 'psk', 'cert']: 239 | self.config['TLSConnect'] = value 240 | else: 241 | raise ValueError('TLSConnect must be one of [unencrypted,psk,cert]') 242 | 243 | @property 244 | def tls_ca_file(self): 245 | return self.config['TLSCAFile'] 246 | 247 | @tls_ca_file.setter 248 | def tls_ca_file(self, value): 249 | if value: 250 | self.config['TLSCAFile'] = value 251 | 252 | @property 253 | def tls_cert_file(self): 254 | return self.config['TLSCertFile'] 255 | 256 | @tls_cert_file.setter 257 | def tls_cert_file(self, value): 258 | if value: 259 | self.config['TLSCertFile'] = value 260 | 261 | @property 262 | def tls_crl_file(self): 263 | return self.config['TLSCRLFile'] 264 | 265 | @tls_crl_file.setter 266 | def tls_crl_file(self, value): 267 | if value: 268 | self.config['TLSCRLFile'] = value 269 | 270 | @property 271 | def tls_key_file(self): 272 | return self.config['TLSKeyFile'] 273 | 274 | @tls_key_file.setter 275 | def tls_key_file(self, value): 276 | if value: 277 | self.config['TLSKeyFile'] = value 278 | 279 | @property 280 | def tls_server_cert_issuer(self): 281 | return self.config['TLSServerCertIssuer'] 282 | 283 | @tls_server_cert_issuer.setter 284 | def tls_server_cert_issuer(self, value): 285 | if value: 286 | self.config['TLSServerCertIssuer'] = value 287 | 288 | @property 289 | def tls_server_cert_subject(self): 290 | return self.config['TLSServerCertSubject'] 291 | 292 | @tls_server_cert_subject.setter 293 | def tls_server_cert_subject(self, value): 294 | if value: 295 | self.config['TLSServerCertSubject'] = value 296 | 297 | @property 298 | def tls_psk_identity(self): 299 | return self.config['TLSPSKIdentity'] 300 | 301 | @tls_psk_identity.setter 302 | def tls_psk_identity(self, value): 303 | if value: 304 | self.config['TLSPSKIdentity'] = value 305 | 306 | @property 307 | def tls_psk_file(self): 308 | return self.config['TLSPSKFile'] 309 | 310 | @tls_psk_file.setter 311 | def tls_psk_file(self, value): 312 | if value: 313 | self.config['TLSPSKFile'] = value 314 | 315 | @property 316 | def dryrun(self): 317 | return self.config['dryrun'] 318 | 319 | @dryrun.setter 320 | def dryrun(self, value): 321 | if value in [True, False]: 322 | self.config['dryrun'] = value 323 | else: 324 | raise ValueError('dryrun parameter requires boolean') 325 | 326 | @property 327 | def data_type(self): 328 | return self.config['data_type'] 329 | 330 | @data_type.setter 331 | def data_type(self, value): 332 | if value in ['lld', 'items', None]: 333 | self.config['data_type'] = value 334 | else: 335 | raise ValueError('data_type requires either "items" or "lld"') 336 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts='-k-_need_backend' 3 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | mock 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | configobj 2 | simplejson 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | from setuptools import setup 5 | 6 | setup( 7 | name = 'protobix', 8 | packages = ['protobix'], 9 | version = '1.0.2', 10 | install_requires = [ 11 | 'configobj', 12 | 'simplejson' 13 | ], 14 | tests_require = [ 15 | 'mock', 16 | 'pytest', 17 | ], 18 | test_suite='tests', 19 | description = 'Implementation of Zabbix Sender protocol', 20 | long_description = ( 'This module implements Zabbix Sender Protocol.\n' 21 | 'It allows to build list of items and send ' 22 | 'them as trapper.\n' 23 | 'It currently supports items update as well as ' 24 | 'Low Level Discovery.' ), 25 | author = 'Jean Baptiste Favre', 26 | author_email = 'jean-baptiste.favre@blablacar.com', 27 | license = 'GPL-3+', 28 | url='https://github.com/jbfavre/python-protobix/', 29 | download_url = 'https://github.com/jbfavre/python-protobix/archive/1.0.2.tar.gz', 30 | keywords = ['monitoring','zabbix','trappers'], 31 | classifiers = [], 32 | ) 33 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import pytest 5 | try: 6 | import coverage 7 | coverage_options = ['--cov', 'protobix', '--cov-report', 'term-missing'] 8 | except ImportError: 9 | coverage_options = [] 10 | try: 11 | import pylint 12 | pylint_options = ['--pylint '] 13 | except ImportError: 14 | pylint_options = [] 15 | 16 | pytest_options = ['-v', '-k-_need_backend'] 17 | pytest_options += coverage_options 18 | pytest_options += pylint_options 19 | 20 | pytest.main(pytest_options) 21 | -------------------------------------------------------------------------------- /tests/docker-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION=$(sed 's/\..*//' /etc/debian_version) 4 | case ${VERSION} in 5 | 7) echo 'Debian Wheezy' 6 | packages_list='python2.7 python-setuptools python-configobj python-simplejson python-pytest python-mock adduser' 7 | test_suite_list='python' 8 | ;; 9 | 8) echo 'Debian Jessie' 10 | packages_list='python2.7 python3 python-setuptools python3-setuptools python-configobj python-simplejson python3-configobj python3-simplejson python-pytest python-pytest-cov python-mock python3-pytest python3-pytest-cov python3-mock' 11 | test_suite_list='python python3' 12 | ;; 13 | *) echo 'Debian stretch/sid' 14 | packages_list='python2.7 python3 python-setuptools python3-setuptools python-configobj python-simplejson python3-configobj python3-simplejson python-pytest python-pytest-cov python-mock python3-pytest python3-pytest-cov python3-mock' 15 | test_suite_list='python python3' 16 | ;; 17 | esac 18 | 19 | function cleanup() { 20 | cd /home/python-protobix 21 | find . -name '*.pyc' -exec rm {} \; 2>/dev/null 22 | find . -name '__pycache__' -exec rm -r {} \; 2>/dev/null 23 | } 24 | 25 | # Update package list 26 | apt-get update 27 | 28 | # Install dependencies for both python 2.7 & python 3 29 | apt-get -qq -y install ${packages_list} 30 | 31 | # Create an unprivileged user 32 | addgroup -gid 1000 protobix 33 | adduser --system -uid 1000 -gid 1000 --home /home/python-protobix \ 34 | --shell /bin/bash --no-create-home --disabled-password \ 35 | protobix 36 | 37 | 38 | for test_suite in ${test_suite_list} 39 | do 40 | # Clean existing cache files 41 | cleanup 42 | # Run test suite 43 | su - protobix -s /bin/bash -c "cd /home/python-protobix;${test_suite} setup.py test" 44 | done 45 | 46 | # Clean existing cache files 47 | cleanup 48 | -------------------------------------------------------------------------------- /tests/test_datacontainer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for protobix.SenderProtocol 3 | """ 4 | import configobj 5 | import pytest 6 | import mock 7 | import unittest 8 | import time 9 | try: import simplejson as json 10 | except ImportError: import json 11 | import socket 12 | 13 | import sys 14 | import os 15 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) 16 | import protobix 17 | 18 | DATA = { 19 | 'items': { 20 | "protobix.host1": { 21 | "my.protobix.item.int": 0, 22 | "my.protobix.item.string": "item string" 23 | }, 24 | "protobix.host2": { 25 | "my.protobix.item.int": 0, 26 | "my.protobix.item.string": "item string" 27 | } 28 | }, 29 | 'lld': { 30 | 'protobix.host1': { 31 | 'my.protobix.lld_item1': [ 32 | { '{#PBX_LLD_KEY11}': 0, 33 | '{#PBX_LLD_KEY12}': 'lld string' }, 34 | { '{#PBX_LLD_KEY11}': 1, 35 | '{#PBX_LLD_KEY12}': 'another lld string' } 36 | ], 37 | 'my.protobix.lld_item2': [ 38 | { '{#PBX_LLD_KEY21}': 10, 39 | '{#PBX_LLD_KEY21}': 'yet an lld string' }, 40 | { '{#PBX_LLD_KEY21}': 2, 41 | '{#PBX_LLD_KEY21}': 'yet another lld string' } 42 | ] 43 | 44 | }, 45 | 'protobix.host2': { 46 | 'my.protobix.lld_item1': [ 47 | { '{#PBX_LLD_KEY11}': 0, 48 | '{#PBX_LLD_KEY12}': 'lld string' }, 49 | { '{#PBX_LLD_KEY11}': 1, 50 | '{#PBX_LLD_KEY12}': 'another lld string' } 51 | ], 52 | 'my.protobix.lld_item2': [ 53 | { '{#PBX_LLD_KEY21}': 10, 54 | '{#PBX_LLD_KEY21}': 'yet an lld string' }, 55 | { '{#PBX_LLD_KEY21}': 2, 56 | '{#PBX_LLD_KEY21}': 'yet another lld string' } 57 | ] 58 | } 59 | } 60 | } 61 | 62 | pytest_params = ( 63 | 'items', 64 | 'lld' 65 | ) 66 | def test_invalid_logger(): 67 | """ 68 | Adding data before assigning data_type should raise an Exception 69 | """ 70 | with pytest.raises(ValueError) as err: 71 | zbx_datacontainer = protobix.DataContainer(logger='invalid') 72 | assert str(err.value) == 'logger requires a logging instance' 73 | 74 | @pytest.mark.parametrize('data_type', pytest_params) 75 | def test_items_add_before_set_data_type(data_type): 76 | """ 77 | Adding data before assigning data_type should raise an Exception 78 | """ 79 | zbx_datacontainer = protobix.DataContainer() 80 | assert zbx_datacontainer.items_list == [] 81 | with pytest.raises(ValueError): 82 | zbx_datacontainer.add(DATA[data_type]) 83 | assert zbx_datacontainer.items_list == [] 84 | zbx_datacontainer.data_type = data_type 85 | zbx_datacontainer.add(DATA) 86 | assert len(zbx_datacontainer.items_list) == 4 87 | 88 | @pytest.mark.parametrize('data_type', pytest_params) 89 | def test_debug_no_dryrun_yes(data_type): 90 | """ 91 | debug_level to False 92 | dryrun to True 93 | """ 94 | zbx_datacontainer = protobix.DataContainer() 95 | zbx_datacontainer.data_type = data_type 96 | zbx_datacontainer.dryrun = True 97 | assert zbx_datacontainer.items_list == [] 98 | zbx_datacontainer.add(DATA[data_type]) 99 | assert len(zbx_datacontainer.items_list) == 4 100 | 101 | assert zbx_datacontainer.dryrun is True 102 | assert zbx_datacontainer.debug_level < 4 103 | 104 | ''' Send data to zabbix ''' 105 | srv_success, srv_failure, processed, failed, total, time = zbx_datacontainer.send() 106 | assert srv_success == 0 107 | assert srv_failure == 0 108 | assert processed == 0 109 | assert failed == 0 110 | assert total == 4 111 | assert zbx_datacontainer.items_list == [] 112 | 113 | @pytest.mark.parametrize('data_type', pytest_params) 114 | def test_debug_yes_dryrun_yes(data_type): 115 | """ 116 | debug_level to True 117 | dryrun to True 118 | """ 119 | zbx_datacontainer = protobix.DataContainer() 120 | zbx_datacontainer.data_type = data_type 121 | zbx_datacontainer.dryrun = True 122 | zbx_datacontainer.debug_level = 4 123 | 124 | assert zbx_datacontainer.items_list == [] 125 | zbx_datacontainer.add(DATA[data_type]) 126 | 127 | assert len(zbx_datacontainer.items_list) == 4 128 | 129 | ''' Send data to zabbix ''' 130 | srv_success, srv_failure, processed, failed, total, time = zbx_datacontainer.send() 131 | assert srv_success == 0 132 | assert srv_failure == 0 133 | assert processed == 0 134 | assert failed == 0 135 | assert total == 4 136 | assert zbx_datacontainer.items_list == [] 137 | 138 | @pytest.mark.parametrize('data_type', pytest_params) 139 | def test_debug_no_dryrun_no(data_type): 140 | """ 141 | debug_level to False 142 | dryrun to False 143 | """ 144 | zbx_datacontainer = protobix.DataContainer() 145 | # Force a Zabbix port so that test fails even if backend is present 146 | zbx_datacontainer.server_port = 10060 147 | zbx_datacontainer.data_type = data_type 148 | assert zbx_datacontainer.items_list == [] 149 | zbx_datacontainer.add(DATA[data_type]) 150 | assert len(zbx_datacontainer.items_list) == 4 151 | 152 | assert zbx_datacontainer.dryrun is False 153 | assert zbx_datacontainer.debug_level < 4 154 | 155 | ''' Send data to zabbix ''' 156 | with pytest.raises(socket.error): 157 | results_list = zbx_datacontainer.send() 158 | 159 | @pytest.mark.parametrize('data_type', pytest_params) 160 | def test_debug_yes_dryrun_no(data_type): 161 | """ 162 | debug_level to True 163 | dryrun to False 164 | """ 165 | zbx_datacontainer = protobix.DataContainer() 166 | zbx_datacontainer.debug_level = 4 167 | # Force a Zabbix port so that test fails even if backend is present 168 | zbx_datacontainer.server_port = 10060 169 | zbx_datacontainer.data_type = data_type 170 | assert zbx_datacontainer.items_list == [] 171 | zbx_datacontainer.add(DATA[data_type]) 172 | assert len(zbx_datacontainer.items_list) == 4 173 | 174 | assert zbx_datacontainer.dryrun is False 175 | assert zbx_datacontainer.debug_level >= 4 176 | 177 | ''' Send data to zabbix ''' 178 | with pytest.raises(socket.error): 179 | results_list = zbx_datacontainer.send() 180 | 181 | @pytest.mark.parametrize('data_type', pytest_params) 182 | def test_server_connection_fails(data_type): 183 | """ 184 | Connection to Zabbix Server fails 185 | """ 186 | zbx_datacontainer = protobix.DataContainer() 187 | zbx_datacontainer.server_port = 10060 188 | zbx_datacontainer.data_type = data_type 189 | assert zbx_datacontainer.items_list == [] 190 | assert zbx_datacontainer.server_port == 10060 191 | zbx_datacontainer.add(DATA[data_type]) 192 | with pytest.raises(IOError): 193 | zbx_datacontainer.send() 194 | assert zbx_datacontainer.items_list == [] 195 | 196 | @pytest.mark.parametrize('data_type', pytest_params) 197 | def test_need_backend_debug_no_dryrun_no(data_type): 198 | """ 199 | debug_level to False 200 | dryrun to False 201 | """ 202 | zbx_datacontainer = protobix.DataContainer() 203 | zbx_datacontainer.data_type = data_type 204 | assert zbx_datacontainer.items_list == [] 205 | zbx_datacontainer.add(DATA[data_type]) 206 | assert len(zbx_datacontainer.items_list) == 4 207 | 208 | assert zbx_datacontainer.dryrun is False 209 | assert zbx_datacontainer.debug_level < 4 210 | 211 | ''' Send data to zabbix ''' 212 | srv_success, srv_failure, processed, failed, total, time = zbx_datacontainer.send() 213 | assert srv_success == 1 214 | assert srv_failure == 0 215 | assert processed == 4 216 | assert failed == 0 217 | assert total == 4 218 | assert zbx_datacontainer.items_list == [] 219 | 220 | @pytest.mark.parametrize('data_type', pytest_params) 221 | def test_need_backend_debug_yes_dryrun_no(data_type): 222 | """ 223 | debug_level to True 224 | dryrun to False 225 | """ 226 | zbx_datacontainer = protobix.DataContainer() 227 | zbx_datacontainer.debug_level = 4 228 | zbx_datacontainer.data_type = data_type 229 | assert zbx_datacontainer.items_list == [] 230 | zbx_datacontainer.add(DATA[data_type]) 231 | assert len(zbx_datacontainer.items_list) == 4 232 | 233 | assert zbx_datacontainer.dryrun is False 234 | assert zbx_datacontainer.debug_level >= 4 235 | 236 | ''' Send data to zabbix ''' 237 | srv_success, srv_failure, processed, failed, total, time = zbx_datacontainer.send() 238 | assert srv_success == 4 239 | assert srv_failure == 0 240 | assert processed == 4 241 | assert failed == 0 242 | assert total == 4 243 | assert zbx_datacontainer.items_list == [] 244 | 245 | 246 | @pytest.mark.parametrize('data_type', pytest_params) 247 | def test_need_backend_debug_yes_dryrun_no(data_type): 248 | """ 249 | debug_level to True 250 | dryrun to False 251 | """ 252 | zbx_datacontainer = protobix.DataContainer() 253 | zbx_datacontainer.debug_level = 4 254 | zbx_datacontainer.data_type = data_type 255 | assert zbx_datacontainer.items_list == [] 256 | zbx_datacontainer.add(DATA[data_type]) 257 | assert len(zbx_datacontainer.items_list) == 4 258 | 259 | assert zbx_datacontainer.dryrun is False 260 | assert zbx_datacontainer.debug_level >= 4 261 | 262 | ''' Send data to zabbix ''' 263 | srv_success, srv_failure, processed, failed, total, time = zbx_datacontainer.send() 264 | assert srv_success == 4 265 | assert srv_failure == 0 266 | assert processed == 4 267 | assert failed == 0 268 | assert total == 4 269 | assert zbx_datacontainer.items_list == [] 270 | -------------------------------------------------------------------------------- /tests/test_memory_leak.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test long running process & detect memory leak 3 | """ 4 | import configobj 5 | import pytest 6 | import mock 7 | import unittest 8 | 9 | import resource 10 | import sys 11 | import os 12 | 13 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) 14 | import protobix 15 | 16 | PAYLOAD = { 17 | "items": { 18 | "protobix.host1": { 19 | "my.protobix.item.int": 0, 20 | "my.protobix.item.string": 1 21 | }, 22 | "protobix.host2": { 23 | "my.protobix.item.int": 0, 24 | "my.protobix.item.string": 1 25 | }, 26 | "protobix.host3": { 27 | "my.protobix.item.int": 0, 28 | "my.protobix.item.string": 1 29 | }, 30 | "protobix.host4": { 31 | "my.protobix.item.int": 0, 32 | "my.protobix.item.string": 1 33 | }, 34 | "protobix.host5": { 35 | "my.protobix.item.int": 0, 36 | "my.protobix.item.string": 1 37 | }, 38 | "protobix.host6": { 39 | "my.protobix.item.int": 0, 40 | "my.protobix.item.string": 1 41 | }, 42 | "protobix.host7": { 43 | "my.protobix.item.int": 0, 44 | "my.protobix.item.string": 1 45 | }, 46 | "protobix.host8": { 47 | "my.protobix.item.int": 0, 48 | "my.protobix.item.string": 1 49 | } 50 | }, 51 | "lld": { 52 | 'protobix.host1': { 53 | 'my.protobix.lld_item1': [ 54 | { '{#PBX_LLD_KEY11}': 0, 55 | '{#PBX_LLD_KEY12}': 'lld string' }, 56 | { '{#PBX_LLD_KEY11}': 1, 57 | '{#PBX_LLD_KEY12}': 'another lld string' } 58 | ], 59 | 'my.protobix.lld_item2': [ 60 | { '{#PBX_LLD_KEY21}': 10, 61 | '{#PBX_LLD_KEY21}': 'yet an lld string' }, 62 | { '{#PBX_LLD_KEY21}': 2, 63 | '{#PBX_LLD_KEY21}': 'yet another lld string' } 64 | ] 65 | 66 | }, 67 | 'protobix.host2': { 68 | 'my.protobix.lld_item1': [ 69 | { '{#PBX_LLD_KEY11}': 0, 70 | '{#PBX_LLD_KEY12}': 'lld string' }, 71 | { '{#PBX_LLD_KEY11}': 1, 72 | '{#PBX_LLD_KEY12}': 'another lld string' } 73 | ], 74 | 'my.protobix.lld_item2': [ 75 | { '{#PBX_LLD_KEY21}': 10, 76 | '{#PBX_LLD_KEY21}': 'yet an lld string' }, 77 | { '{#PBX_LLD_KEY21}': 2, 78 | '{#PBX_LLD_KEY21}': 'yet another lld string' } 79 | ] 80 | } 81 | } 82 | } 83 | 84 | def long_run(data_type, debug_level): 85 | """ 86 | Generic long running process simulator 87 | Used by tests below 88 | """ 89 | zbx_container = protobix.DataContainer() 90 | zbx_container.debug_level = debug_level 91 | run=1 92 | max_run=1000 93 | while run <= max_run: 94 | zbx_container.data_type = data_type 95 | zbx_container.add(PAYLOAD[data_type]) 96 | try: 97 | zbx_container.send() 98 | except: 99 | pass 100 | if run % (max_run/10) == 0 or run <=1: 101 | usage=resource.getrusage(resource.RUSAGE_SELF) 102 | display_memory = usage[2]*resource.getpagesize()/1000000.0 103 | if run == 1: 104 | initial_memory = usage[2] 105 | display_initial_memory = usage[2]*resource.getpagesize()/1000000.0 106 | final_memory = usage[2] 107 | print ('Run %i: ru_maxrss=%f mb - initial=%f mb' % ( 108 | run, (display_memory), display_initial_memory 109 | )) 110 | run += 1 111 | return initial_memory, final_memory 112 | 113 | memory_leak_matrix = ( 114 | ('items', 2), 115 | ('items', 4), 116 | ('lld', 2), 117 | ('lld', 4) 118 | ) 119 | 120 | @pytest.mark.parametrize(('data_type','debug_level'), memory_leak_matrix) 121 | def test_long_run_for_memory_leak(data_type, debug_level): 122 | """ 123 | Simulate long running process with and without debug 124 | and control memory usage 125 | """ 126 | initial_memory, final_memory = long_run(data_type, debug_level) 127 | assert initial_memory == final_memory 128 | -------------------------------------------------------------------------------- /tests/test_sampleprobe.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test Protobix sampleprobe 3 | """ 4 | import configobj 5 | import pytest 6 | import mock 7 | import unittest 8 | import socket 9 | 10 | import resource 11 | import time 12 | import sys 13 | import os 14 | 15 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) 16 | import protobix 17 | import logging 18 | import argparse 19 | 20 | # Zabbix force TLSv1.2 protocol 21 | # in src/libs/zbxcrypto/tls.c function zbx_tls_init_child 22 | HAVE_DECENT_SSL = False 23 | if sys.version_info > (2,7,9): 24 | import ssl 25 | HAVE_DECENT_SSL = True 26 | 27 | class ProtobixTestProbe(protobix.SampleProbe): 28 | __version__="1.0.2" 29 | 30 | def _get_metrics(self): 31 | return { 32 | "protobix.host1": { 33 | "my.protobix.item.int": 0, 34 | "my.protobix.item.string": "item string" 35 | }, 36 | "protobix.host2": { 37 | "my.protobix.item.int": 0, 38 | "my.protobix.item.string": "item string" 39 | } 40 | } 41 | 42 | def _get_discovery(self): 43 | return { 44 | 'protobix.host1': { 45 | 'my.protobix.lld_item1': [ 46 | { '{#PBX_LLD_KEY11}': 0, 47 | '{#PBX_LLD_KEY12}': 'lld string' }, 48 | { '{#PBX_LLD_KEY11}': 1, 49 | '{#PBX_LLD_KEY12}': 'another lld string' } 50 | ], 51 | 'my.protobix.lld_item2': [ 52 | { '{#PBX_LLD_KEY21}': 10, 53 | '{#PBX_LLD_KEY21}': 'yet an lld string' }, 54 | { '{#PBX_LLD_KEY21}': 2, 55 | '{#PBX_LLD_KEY21}': 'yet another lld string' } 56 | ] 57 | }, 58 | 'protobix.host2': { 59 | 'my.protobix.lld_item1': [ 60 | { '{#PBX_LLD_KEY11}': 0, 61 | '{#PBX_LLD_KEY12}': 'lld string' }, 62 | { '{#PBX_LLD_KEY11}': 1, 63 | '{#PBX_LLD_KEY12}': 'another lld string' } 64 | ], 65 | 'my.protobix.lld_item2': [ 66 | { '{#PBX_LLD_KEY21}': 10, 67 | '{#PBX_LLD_KEY21}': 'yet an lld string' }, 68 | { '{#PBX_LLD_KEY21}': 2, 69 | '{#PBX_LLD_KEY21}': 'yet another lld string' } 70 | ] 71 | } 72 | } 73 | 74 | class ProtobixTLSTestProbe(ProtobixTestProbe): 75 | 76 | def run(self, options=None): 77 | # Init logging with default values since we don't have real config yet 78 | self._init_logging() 79 | # Parse command line options 80 | args = sys.argv[1:] 81 | if isinstance(options, list): 82 | args = options 83 | self.options = self._parse_args(args) 84 | 85 | # Get configuration 86 | self.zbx_config = self._init_config() 87 | 88 | # Update logger with configuration 89 | self._setup_logging( 90 | self.zbx_config.log_type, 91 | self.zbx_config.debug_level, 92 | self.zbx_config.log_file 93 | ) 94 | 95 | # Datacontainer init 96 | zbx_container = protobix.DataContainer( 97 | config = self.zbx_config, 98 | logger=self.logger 99 | ) 100 | # Get back hostname from ZabbixAgentConfig 101 | self.hostname = self.zbx_config.hostname 102 | 103 | # Step 1: read probe configuration 104 | # initialize any needed object or connection 105 | self._init_probe() 106 | 107 | # Step 2: get data 108 | data = {} 109 | if self.options.probe_mode == "update": 110 | zbx_container.data_type = 'items' 111 | data = self._get_metrics() 112 | elif self.options.probe_mode == "discovery": 113 | zbx_container.data_type = 'lld' 114 | data = self._get_discovery() 115 | 116 | # Step 3: add data to container 117 | zbx_container.add(data) 118 | 119 | # Step 4: send data to Zabbix server 120 | server_success, server_failure, processed, failed, total, time = zbx_container.send() 121 | return server_success, server_failure, processed, failed, total, time 122 | 123 | class ProtobixTestProbe2(protobix.SampleProbe): 124 | __version__="1.0.2" 125 | 126 | """ 127 | Check default configuration of the sample probe 128 | """ 129 | def test_default_configuration(): 130 | pbx_test_probe = ProtobixTestProbe() 131 | pbx_test_probe.options = pbx_test_probe._parse_args([]) 132 | assert pbx_test_probe.options.config_file is None 133 | assert pbx_test_probe.options.debug_level is None 134 | assert pbx_test_probe.options.discovery is False 135 | assert pbx_test_probe.options.dryrun is False 136 | assert pbx_test_probe.options.update is False 137 | assert pbx_test_probe.options.server_port is None 138 | assert pbx_test_probe.options.server_active is None 139 | 140 | """ 141 | Check --update-items argument 142 | """ 143 | def test_command_line_option_update_items(): 144 | pbx_test_probe = ProtobixTestProbe() 145 | pbx_test_probe.options = pbx_test_probe._parse_args(['--update-items']) 146 | pbx_config = pbx_test_probe._init_config() 147 | assert pbx_test_probe.options.discovery is False 148 | assert pbx_test_probe.options.update is True 149 | assert pbx_test_probe.options.probe_mode == 'update' 150 | 151 | """ 152 | Check --discovery argument 153 | """ 154 | def test_command_line_option_discovery(): 155 | pbx_test_probe = ProtobixTestProbe() 156 | pbx_test_probe.options = pbx_test_probe._parse_args(['--discovery']) 157 | pbx_config = pbx_test_probe._init_config() 158 | assert pbx_test_probe.options.discovery is True 159 | assert pbx_test_probe.options.update is False 160 | assert pbx_test_probe.options.probe_mode == 'discovery' 161 | 162 | """ 163 | Check exception when providing both --update-items & --discovery arguments 164 | """ 165 | def test_force_both_discovery_and_update(): 166 | pbx_test_probe = ProtobixTestProbe() 167 | with pytest.raises(ValueError): 168 | result = pbx_test_probe.run(['--discovery', '--update-items']) 169 | 170 | """ 171 | Check -v argument. Used to set logger log level 172 | """ 173 | def test_force_verbosity(): 174 | pbx_test_probe = ProtobixTestProbe() 175 | pbx_test_probe.options = pbx_test_probe._parse_args(['-vvvv']) 176 | pbx_config = pbx_test_probe._init_config() 177 | assert pbx_test_probe.options.debug_level == 4 178 | pbx_test_probe.options = pbx_test_probe._parse_args(['-vv']) 179 | pbx_config = pbx_test_probe._init_config() 180 | assert pbx_test_probe.options.debug_level == 2 181 | pbx_test_probe.options = pbx_test_probe._parse_args(['-vvvvvvvvv']) 182 | assert pbx_test_probe.options.debug_level == 9 183 | pbx_config = pbx_test_probe._init_config() 184 | assert pbx_config.debug_level == 4 185 | 186 | """ 187 | Check -d & --dryrun argument. 188 | """ 189 | def test_force_dryrun(): 190 | pbx_test_probe = ProtobixTestProbe() 191 | pbx_test_probe.options = pbx_test_probe._parse_args(['--dryrun']) 192 | pbx_config = pbx_test_probe._init_config() 193 | assert pbx_test_probe.options.dryrun is True 194 | pbx_test_probe.options = pbx_test_probe._parse_args(['-d']) 195 | pbx_config = pbx_test_probe._init_config() 196 | assert pbx_test_probe.options.dryrun is True 197 | 198 | """ 199 | Check -z & --zabbix-server argument. 200 | """ 201 | def test_command_line_option_zabbix_server(): 202 | pbx_test_probe = ProtobixTestProbe() 203 | pbx_test_probe.options = pbx_test_probe._parse_args(['--zabbix-server', '192.168.0.1']) 204 | pbx_config = pbx_test_probe._init_config() 205 | assert pbx_config.server_active == '192.168.0.1' 206 | pbx_test_probe.options = pbx_test_probe._parse_args(['-z', '192.168.0.2']) 207 | pbx_config = pbx_test_probe._init_config() 208 | assert pbx_config.server_active == '192.168.0.2' 209 | 210 | """ 211 | Check -p & --port argument. 212 | """ 213 | def test_command_line_option_port(): 214 | pbx_test_probe = ProtobixTestProbe() 215 | pbx_test_probe.options = pbx_test_probe._parse_args(['--port', '10052']) 216 | pbx_config = pbx_test_probe._init_config() 217 | assert pbx_config.server_port == 10052 218 | pbx_test_probe.options = pbx_test_probe._parse_args(['-p', '10060']) 219 | pbx_config = pbx_test_probe._init_config() 220 | assert pbx_config.server_port == 10060 221 | 222 | """ 223 | Check --tls-cert-file argument. 224 | """ 225 | def test_command_line_option_tls_cert_file(): 226 | pbx_test_probe = ProtobixTestProbe() 227 | pbx_test_probe.options = pbx_test_probe._parse_args(['--tls-cert-file', '/tmp/test_file.crt']) 228 | pbx_config = pbx_test_probe._init_config() 229 | assert pbx_config.tls_cert_file == '/tmp/test_file.crt' 230 | 231 | """ 232 | Check --tls-key-file argument. 233 | """ 234 | def test_command_line_option_tls_key_file(): 235 | pbx_test_probe = ProtobixTestProbe() 236 | pbx_test_probe.options = pbx_test_probe._parse_args(['--tls-key-file', '/tmp/test_file.key']) 237 | pbx_config = pbx_test_probe._init_config() 238 | assert pbx_config.tls_key_file == '/tmp/test_file.key' 239 | 240 | """ 241 | Check --tls-ca-file argument. 242 | """ 243 | def test_command_line_option_tls_ca_file(): 244 | pbx_test_probe = ProtobixTestProbe() 245 | pbx_test_probe.options = pbx_test_probe._parse_args(['--tls-ca-file', '/tmp/test_ca_file.crt']) 246 | pbx_config = pbx_test_probe._init_config() 247 | assert pbx_config.tls_ca_file == '/tmp/test_ca_file.crt' 248 | 249 | """ 250 | Check --tls-crl-file argument. 251 | """ 252 | def test_command_line_option_tls_crl_file(): 253 | pbx_test_probe = ProtobixTestProbe() 254 | pbx_test_probe.options = pbx_test_probe._parse_args(['--tls-crl-file', '/tmp/test_file.crl']) 255 | pbx_config = pbx_test_probe._init_config() 256 | assert pbx_config.tls_crl_file == '/tmp/test_file.crl' 257 | 258 | """ 259 | Check --tls-psk-file argument. 260 | """ 261 | def test_command_line_option_tls_psk_file(): 262 | pbx_test_probe = ProtobixTestProbe() 263 | pbx_test_probe.options = pbx_test_probe._parse_args(['--tls-psk-file', '/tmp/test_file.psk']) 264 | pbx_config = pbx_test_probe._init_config() 265 | assert pbx_config.tls_psk_file == '/tmp/test_file.psk' 266 | 267 | """ 268 | Check --tls-psk-identity argument. 269 | """ 270 | def test_command_line_option_tls_psk_identity(): 271 | pbx_test_probe = ProtobixTestProbe() 272 | pbx_test_probe.options = pbx_test_probe._parse_args(['--tls-psk-identity', 'Zabbix TLS/PSK identity']) 273 | pbx_config = pbx_test_probe._init_config() 274 | assert pbx_config.tls_psk_identity == 'Zabbix TLS/PSK identity' 275 | 276 | """ 277 | Check --tls-server-cert-issuer argument. 278 | """ 279 | def test_command_line_option_tls_server_cert_issuer(): 280 | pbx_test_probe = ProtobixTestProbe() 281 | pbx_test_probe.options = pbx_test_probe._parse_args(['--tls-server-cert-issuer', 'Zabbix TLS cert issuer']) 282 | pbx_config = pbx_test_probe._init_config() 283 | assert pbx_config.tls_server_cert_issuer == 'Zabbix TLS cert issuer' 284 | 285 | """ 286 | Check --tls-server-cert-subject argument. 287 | """ 288 | def test_command_line_option_tls_server_cert_subject(): 289 | pbx_test_probe = ProtobixTestProbe() 290 | pbx_test_probe.options = pbx_test_probe._parse_args(['--tls-server-cert-subject', 'Zabbix TLS cert subject']) 291 | pbx_config = pbx_test_probe._init_config() 292 | assert pbx_config.tls_server_cert_subject == 'Zabbix TLS cert subject' 293 | 294 | """ 295 | Check --tls-connect argument. 296 | """ 297 | def test_command_line_option_tls_connect(): 298 | pbx_test_probe = ProtobixTestProbe() 299 | pbx_test_probe.options = pbx_test_probe._parse_args(['--tls-connect', 'cert']) 300 | pbx_config = pbx_test_probe._init_config() 301 | assert pbx_config.tls_connect == 'cert' 302 | pbx_test_probe.options = pbx_test_probe._parse_args(['--tls-connect', 'psk']) 303 | pbx_config = pbx_test_probe._init_config() 304 | assert pbx_config.tls_connect == 'psk' 305 | pbx_test_probe.options = pbx_test_probe._parse_args(['--tls-connect', 'unencrypted']) 306 | pbx_config = pbx_test_probe._init_config() 307 | assert pbx_config.tls_connect == 'unencrypted' 308 | 309 | """ 310 | Check logger configuration in console mode 311 | """ 312 | def test_log_console(): 313 | pbx_test_probe = ProtobixTestProbe() 314 | pbx_test_probe._init_logging() 315 | assert isinstance(pbx_test_probe.logger, logging.Logger) 316 | pbx_test_probe._setup_logging('console', 4, '/tmp/log_file') 317 | assert len(pbx_test_probe.logger.handlers) == 1 318 | assert pbx_test_probe.logger.level == logging.DEBUG 319 | assert isinstance(pbx_test_probe.logger.handlers[0], logging.StreamHandler) 320 | 321 | """ 322 | Check logger configuration in file mode & debug 323 | """ 324 | def test_log_file(): 325 | pbx_test_probe = ProtobixTestProbe() 326 | pbx_test_probe._init_logging() 327 | assert isinstance(pbx_test_probe.logger, logging.Logger) 328 | pbx_test_probe._setup_logging('file', 4, '/tmp/log_file') 329 | assert len(pbx_test_probe.logger.handlers) == 1 330 | assert pbx_test_probe.logger.level == logging.DEBUG 331 | assert isinstance(pbx_test_probe.logger.handlers[0], logging.FileHandler) 332 | 333 | """ 334 | Check logger configuration in file mode with invalid file 335 | Here, invalid means that it doesn't exists, or we don't have 336 | permission to write into 337 | """ 338 | def test_log_file_invalid(): 339 | pbx_test_probe = ProtobixTestProbe() 340 | pbx_test_probe._init_logging() 341 | assert isinstance(pbx_test_probe.logger, logging.Logger) 342 | with pytest.raises(IOError): 343 | pbx_test_probe._setup_logging('file', 4, '/do_not_have_permission') 344 | assert pbx_test_probe.logger.level == logging.NOTSET 345 | assert len(pbx_test_probe.logger.handlers) == 0 346 | 347 | """ 348 | Check logger configuration in system (syslog) mode 349 | """ 350 | def test_log_syslog(): 351 | pbx_test_probe = ProtobixTestProbe() 352 | pbx_test_probe._init_logging() 353 | assert isinstance(pbx_test_probe.logger, logging.Logger) 354 | pbx_test_probe._setup_logging('system', 3, None) 355 | assert len(pbx_test_probe.logger.handlers) == 1 356 | assert pbx_test_probe.logger.level == logging.INFO 357 | assert isinstance(pbx_test_probe.logger.handlers[0], logging.handlers.SysLogHandler) 358 | 359 | """ 360 | Check a custom probe without _get_metrics method. 361 | """ 362 | def test_not_implemented_get_metrics(): 363 | pbx_test_probe = ProtobixTestProbe2() 364 | with pytest.raises(NotImplementedError): 365 | pbx_test_probe.run([]) 366 | 367 | """ 368 | Check a custom probe without _get_discovery method. 369 | """ 370 | def test_not_implemented_get_discovery(): 371 | pbx_test_probe = ProtobixTestProbe2() 372 | with pytest.raises(NotImplementedError): 373 | pbx_test_probe.run(['--discovery']) 374 | 375 | """ 376 | Check that sample probe correctly catches exception from _init_probe 377 | """ 378 | def test_init_probe_exception(): 379 | pbx_test_probe = ProtobixTestProbe2() 380 | with mock.patch('protobix.SampleProbe._init_probe') as mock_init_probe: 381 | mock_init_probe.side_effect = Exception('Something went wrong') 382 | result = pbx_test_probe.run([]) 383 | assert result == 1 384 | 385 | """ 386 | Check that sample probe correctly catches exception from _get_metrics 387 | """ 388 | def test_get_metrics_exception(): 389 | pbx_test_probe = ProtobixTestProbe2() 390 | with mock.patch('protobix.SampleProbe._get_metrics') as mock_get_metrics: 391 | mock_get_metrics.side_effect = Exception('Something went wrong in _get_metrics') 392 | result = pbx_test_probe.run([]) 393 | assert result == 2 394 | 395 | """ 396 | Check that sample probe correctly catches exception from _get_discovery 397 | """ 398 | def test_get_discovery_exception(): 399 | pbx_test_probe = ProtobixTestProbe2() 400 | with mock.patch('protobix.SampleProbe._get_discovery') as mock_get_discovery: 401 | mock_get_discovery.side_effect = Exception('Something went wrong in _get_discovery') 402 | result = pbx_test_probe.run(['--discovery']) 403 | assert result == 2 404 | 405 | """ 406 | Check that sample probe correctly catches exception from DataContainer add method 407 | """ 408 | def test_datacontainer_add_exception(): 409 | pbx_test_probe = ProtobixTestProbe() 410 | with mock.patch('protobix.DataContainer.add') as mock_datacontainer_add: 411 | mock_datacontainer_add.side_effect = Exception('Something went wrong in DataContainer.add') 412 | result = pbx_test_probe.run([]) 413 | assert result == 3 414 | 415 | """ 416 | Check that sample probe correctly catches exception from DataContainer send method 417 | """ 418 | def test_datacontainer_send_exception(): 419 | pbx_test_probe = ProtobixTestProbe() 420 | with mock.patch('protobix.DataContainer.send') as mock_datacontainer_send: 421 | mock_datacontainer_send.side_effect = Exception('Another something went wrong') 422 | result = pbx_test_probe.run([]) 423 | assert result == 4 424 | 425 | """ 426 | Check that sample probe correctly catches socket exception from DataContainer send method 427 | """ 428 | def test_datacontainer_send_socket_error(): 429 | pbx_test_probe = ProtobixTestProbe() 430 | with mock.patch('protobix.DataContainer.send') as mock_datacontainer_send: 431 | mock_datacontainer_send.side_effect = socket.error 432 | result = pbx_test_probe.run([]) 433 | assert result == 4 434 | 435 | """ 436 | Check return 0 when everything is fine 437 | """ 438 | def test_everything_runs_fine(): 439 | pbx_test_probe = ProtobixTestProbe() 440 | with mock.patch('protobix.DataContainer.send') as mock_datacontainer_send: 441 | mock_datacontainer_send.side_effect = None 442 | result = pbx_test_probe.run([]) 443 | assert result == 0 444 | 445 | if HAVE_DECENT_SSL is True: 446 | 447 | """ 448 | Check sending data with or without TLS with debug disabled 449 | """ 450 | pytest_matrix = ( 451 | ('items', False, False), 452 | ('lld', False, False), 453 | ('items', True, False), 454 | ('lld', True, False), 455 | ('items', False, True), 456 | ('lld', False, True), 457 | ('items', True, True), 458 | ('lld', True, True), 459 | ) 460 | @pytest.mark.parametrize('data_type,tls_enabled,tls_crl_enabled', pytest_matrix) 461 | def test_need_backend_tls_cert(data_type, tls_enabled, tls_crl_enabled): 462 | params = [] 463 | if tls_enabled: 464 | params = [ 465 | '--tls-connect', 'cert', 466 | '--tls-ca-file', 'tests/tls_ca/rogue-protobix-ca.cert.pem', 467 | '--tls-cert-file', 'tests/tls_ca/rogue-protobix-client.cert.pem', 468 | '--tls-key-file', 'tests/tls_ca/rogue-protobix-client.key.pem', 469 | ] 470 | if tls_crl_enabled: 471 | params.append('--tls-crl-file') 472 | params.append('tests/tls_ca/rogue-protobix.crl') 473 | params.append('--update' if data_type == 'items' else '--discovery') 474 | params.append('-vvv') 475 | pbx_test_probe = ProtobixTLSTestProbe() 476 | server_success, server_failure, processed, failed, total, time = pbx_test_probe.run(params) 477 | if tls_enabled is False: 478 | assert server_success == 1 479 | assert server_failure == 0 480 | assert processed == 4 481 | assert failed == 0 482 | assert total == 4 483 | else: 484 | # protobix.host1 does NOT have TLS enabled 485 | # therefore items sent on behalf of protobix.host1 must fail 486 | assert server_success == 1 487 | assert server_failure == 0 488 | assert processed == 2 489 | assert failed == 2 490 | assert total == 4 491 | 492 | """ 493 | Check sending data with or without TLS with debug enabled 494 | """ 495 | pytest_matrix = ( 496 | ('items', False, False), 497 | ('lld', False, False), 498 | ('items', True, False), 499 | ('lld', True, False), 500 | ('items', False, True), 501 | ('lld', False, True), 502 | ('items', True, True), 503 | ('lld', True, True), 504 | ) 505 | @pytest.mark.parametrize('data_type,tls_enabled,tls_crl_enabled', pytest_matrix) 506 | def test_need_backend_tls_cert_debug(data_type, tls_enabled, tls_crl_enabled): 507 | params = [] 508 | if tls_enabled: 509 | params = [ 510 | '--tls-connect', 'cert', 511 | '--tls-ca-file', 'tests/tls_ca/rogue-protobix-ca.cert.pem', 512 | '--tls-cert-file', 'tests/tls_ca/rogue-protobix-client.cert.pem', 513 | '--tls-key-file', 'tests/tls_ca/rogue-protobix-client.key.pem', 514 | ] 515 | if tls_crl_enabled: 516 | params.append('--tls-crl-file') 517 | params.append('tests/tls_ca/rogue-protobix.crl') 518 | params.append('--update' if data_type == 'items' else '--discovery') 519 | params.append('-vvvvv') 520 | pbx_test_probe = ProtobixTLSTestProbe() 521 | server_success, server_failure, processed, failed, total, time = pbx_test_probe.run(params) 522 | if tls_enabled is False: 523 | assert server_success == 4 524 | assert server_failure == 0 525 | assert processed == 4 526 | assert failed == 0 527 | assert total == 4 528 | else: 529 | # protobix.host1 does NOT have TLS enabled 530 | # therefore items sent on behalf of protobix.host1 must fail 531 | assert server_success == 4 532 | assert server_failure == 0 533 | assert processed == 2 534 | assert failed == 2 535 | assert total == 4 536 | 537 | """ 538 | Check sending data with or without TLS with debug disabled 539 | """ 540 | pytest_matrix = ( 541 | ('items', False, False), 542 | ('lld', False, False), 543 | ('items', True, False), 544 | ('lld', True, False), 545 | ('items', False, True), 546 | ('lld', False, True), 547 | ('items', True, True), 548 | ('lld', True, True), 549 | ) 550 | @pytest.mark.parametrize('data_type,tls_enabled,tls_crl_enabled', pytest_matrix) 551 | def test_need_backend_tls_cert_invalid(data_type, tls_enabled, tls_crl_enabled): 552 | params = [] 553 | if tls_enabled: 554 | params = [ 555 | '--tls-connect', 'cert', 556 | '--tls-ca-file', 'tests/tls_ca/protobix-ca.cert.pem', 557 | '--tls-cert-file', 'tests/tls_ca/protobix-client.cert.pem', 558 | '--tls-key-file', 'tests/tls_ca/protobix-client.key.pem', 559 | ] 560 | if tls_crl_enabled: 561 | params.append('--tls-crl-file') 562 | params.append('tests/tls_ca/protobix.crl') 563 | params.append('--update' if data_type == 'items' else '--discovery') 564 | params.append('-vvv') 565 | pbx_test_probe = ProtobixTLSTestProbe() 566 | if tls_enabled is True: 567 | with pytest.raises(ssl.SSLError) as err: 568 | pbx_test_probe.run(params) 569 | else: 570 | server_success, server_failure, processed, failed, total, time = pbx_test_probe.run(params) 571 | assert server_success == 1 572 | assert server_failure == 0 573 | assert processed == 4 574 | assert failed == 0 575 | assert total == 4 576 | 577 | """ 578 | Check sending data with or without TLS with debug enabled 579 | """ 580 | pytest_matrix = ( 581 | ('items', False, False), 582 | ('lld', False, False), 583 | ('items', True, False), 584 | ('lld', True, False), 585 | ('items', False, True), 586 | ('lld', False, True), 587 | ('items', True, True), 588 | ('lld', True, True), 589 | ) 590 | @pytest.mark.parametrize('data_type,tls_enabled,tls_crl_enabled', pytest_matrix) 591 | def test_need_backend_tls_cert_invalid_debug(data_type, tls_enabled, tls_crl_enabled): 592 | params = [] 593 | if tls_enabled: 594 | params = [ 595 | '--tls-connect', 'cert', 596 | '--tls-ca-file', 'tests/tls_ca/protobix-ca.cert.pem', 597 | '--tls-cert-file', 'tests/tls_ca/protobix-client.cert.pem', 598 | '--tls-key-file', 'tests/tls_ca/protobix-client.key.pem', 599 | ] 600 | if tls_crl_enabled: 601 | params.append('--tls-crl-file') 602 | params.append('tests/tls_ca/protobix.crl') 603 | params.append('--update' if data_type == 'items' else '--discovery') 604 | params.append('-vvvvv') 605 | pbx_test_probe = ProtobixTLSTestProbe() 606 | if tls_enabled is True: 607 | with pytest.raises(ssl.SSLError) as err: 608 | pbx_test_probe.run(params) 609 | else: 610 | server_success, server_failure, processed, failed, total, time = pbx_test_probe.run(params) 611 | assert server_success == 4 612 | assert server_failure == 0 613 | assert processed == 4 614 | assert failed == 0 615 | assert total == 4 616 | -------------------------------------------------------------------------------- /tests/test_senderprotocol.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for protobix.SenderProtocol 3 | """ 4 | import configobj 5 | import pytest 6 | import mock 7 | import unittest 8 | import time 9 | try: import simplejson as json 10 | except ImportError: import json 11 | import socket 12 | 13 | import sys 14 | import os 15 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) 16 | import protobix 17 | 18 | try: import simplejson as json 19 | except ImportError: import json 20 | import struct 21 | if sys.version_info < (3,): 22 | def b(x): 23 | return x 24 | else: 25 | import codecs 26 | def b(x): 27 | return codecs.utf_8_encode(x)[0] 28 | 29 | # Zabbix force TLSv1.2 protocol 30 | # in src/libs/zbxcrypto/tls.c function zbx_tls_init_child 31 | HAVE_DECENT_SSL = False 32 | if sys.version_info > (2,7,9): 33 | import ssl 34 | HAVE_DECENT_SSL = True 35 | 36 | def test_default_params(): 37 | """ 38 | Default configuration 39 | """ 40 | zbx_senderprotocol = protobix.SenderProtocol() 41 | 42 | assert zbx_senderprotocol.server_active == '127.0.0.1' 43 | assert zbx_senderprotocol.server_port == 10051 44 | assert zbx_senderprotocol._config.dryrun is False 45 | assert zbx_senderprotocol.items_list == [] 46 | 47 | def test_server_active_custom(): 48 | """ 49 | Test setting zbx_server with custom value 50 | """ 51 | zbx_senderprotocol = protobix.SenderProtocol() 52 | assert zbx_senderprotocol.server_active == '127.0.0.1' 53 | zbx_senderprotocol.server_active = 'myserver.domain.tld' 54 | assert zbx_senderprotocol.server_active == 'myserver.domain.tld' 55 | 56 | def test_server_port_custom(): 57 | """ 58 | Test setting server_port with custom value 59 | """ 60 | zbx_senderprotocol = protobix.SenderProtocol() 61 | assert zbx_senderprotocol.server_port == 10051 62 | zbx_senderprotocol.server_port = 10052 63 | assert zbx_senderprotocol.server_port == 10052 64 | 65 | def test_server_port_invalid_greater_than_32767(): 66 | """ 67 | Test setting server_port with invalid value 68 | """ 69 | zbx_senderprotocol = protobix.SenderProtocol() 70 | with pytest.raises(ValueError) as err: 71 | zbx_senderprotocol.server_port = 40000 72 | assert str(err.value) == 'ServerPort must be between 1024 and 32767' 73 | assert zbx_senderprotocol.server_port == 10051 74 | 75 | def test_server_port_invalid_lower_than_1024(): 76 | """ 77 | Test setting server_port with invalid value 78 | """ 79 | zbx_senderprotocol = protobix.SenderProtocol() 80 | with pytest.raises(ValueError) as err: 81 | zbx_senderprotocol.server_port = 1000 82 | assert str(err.value) == 'ServerPort must be between 1024 and 32767' 83 | assert zbx_senderprotocol.server_port == 10051 84 | 85 | def test_debug_custom(): 86 | """ 87 | Test setting server_port with custom value 88 | """ 89 | zbx_senderprotocol = protobix.SenderProtocol() 90 | assert zbx_senderprotocol.debug_level == 3 91 | zbx_senderprotocol.debug_level = 4 92 | assert zbx_senderprotocol.debug_level == 4 93 | 94 | def test_debug_invalid_lower_than_0(): 95 | """ 96 | Test setting server_port with invalid value 97 | """ 98 | zbx_senderprotocol = protobix.SenderProtocol() 99 | with pytest.raises(ValueError) as err: 100 | zbx_senderprotocol.debug_level = -1 101 | assert str(err.value) == 'DebugLevel must be between 0 and 5, -1 provided' 102 | assert zbx_senderprotocol.debug_level == 3 103 | 104 | def test_debug_invalid_greater_than_5(): 105 | """ 106 | Test setting server_port with invalid value 107 | """ 108 | zbx_senderprotocol = protobix.SenderProtocol() 109 | with pytest.raises(ValueError) as err: 110 | zbx_senderprotocol.debug_level = 10 111 | assert str(err.value) == 'DebugLevel must be between 0 and 5, 10 provided' 112 | assert zbx_senderprotocol.debug_level == 3 113 | 114 | def test_dryrun_custom(): 115 | """ 116 | Test setting dryrun with custom value 117 | """ 118 | zbx_senderprotocol = protobix.SenderProtocol() 119 | assert zbx_senderprotocol._config.dryrun is False 120 | zbx_senderprotocol._config.dryrun = True 121 | assert zbx_senderprotocol._config.dryrun is True 122 | zbx_senderprotocol._config.dryrun = False 123 | assert zbx_senderprotocol._config.dryrun is False 124 | 125 | def test_dryrun_invalid(): 126 | """ 127 | Test setting dryrun with invalid value 128 | """ 129 | zbx_senderprotocol = protobix.SenderProtocol() 130 | assert zbx_senderprotocol._config.dryrun is False 131 | with pytest.raises(ValueError) as err: 132 | zbx_senderprotocol._config.dryrun = 'invalid' 133 | assert str(err.value) == 'dryrun parameter requires boolean' 134 | assert zbx_senderprotocol._config.dryrun is False 135 | 136 | def test_clock_integer(): 137 | """ 138 | Test clock method 139 | """ 140 | zbx_senderprotocol = protobix.SenderProtocol() 141 | assert isinstance(zbx_senderprotocol.clock, int) is True 142 | 143 | def test_clock_accurate(): 144 | """ 145 | Test clock method 146 | """ 147 | zbx_senderprotocol = protobix.SenderProtocol() 148 | assert zbx_senderprotocol.clock == int(time.time()) 149 | 150 | @mock.patch('socket.socket', return_value=mock.MagicMock(name='socket', spec=socket.socket)) 151 | def test_send_to_zabbix(mock_socket): 152 | """ 153 | Test sending data to Zabbix Server 154 | """ 155 | item = { 'host': 'myhostname', 'key': 'my.item.key', 156 | 'value': 1, 'clock': int(time.time())} 157 | payload = json.dumps({ 158 | "data": [item], 159 | "request": "sender data", 160 | "clock": int(time.time()) 161 | }) 162 | packet = b('ZBXD\1') + struct.pack('= 2.2 176 | 'processed: 1; failed: 2; total: 3; seconds spent: 0.123456', 177 | ) 178 | @pytest.mark.parametrize('zabbix_answer', zabbix_answer_params) 179 | def test_handle_response(zabbix_answer): 180 | """ 181 | Test Zabbix Server/Proxy answer 182 | """ 183 | payload='{"response":"success","info":"'+zabbix_answer+'"}' 184 | zbx_datacontainer = protobix.DataContainer() 185 | srv_response, processed, failed, total, time = zbx_datacontainer._handle_response(payload) 186 | assert srv_response == 'success' 187 | assert processed == 1 188 | assert failed == 2 189 | assert total == 3 190 | assert time == 0.123456 191 | 192 | zabbix_answer_params= ( 193 | # Zabbix Sender protocol <= 2.0 194 | 'Invalid content', 195 | # Zabbix Sender protocol >= 2.2 196 | 'invalid content', 197 | ) 198 | @pytest.mark.parametrize('zabbix_answer', zabbix_answer_params) 199 | def test_handle_response_with_invalid_content(zabbix_answer): 200 | """ 201 | Test Zabbix Server/Proxy answer 202 | """ 203 | payload='{"response":"success","info":"'+zabbix_answer+'"}' 204 | zbx_datacontainer = protobix.DataContainer() 205 | with pytest.raises(IndexError): 206 | zbx_datacontainer._handle_response(payload) 207 | 208 | @mock.patch('socket.socket', return_value=mock.MagicMock(name='socket', spec=socket.socket)) 209 | def test_read_from_zabbix_valid_content(mock_socket): 210 | """ 211 | Test sending data to Zabbix Server 212 | """ 213 | answer_payload = '{"info": "processed: 0; failed: 1; total: 1; seconds spent: 0.000441", "response": "success"}' 214 | answer_packet = b('ZBXD\1') + struct.pack('