diff --git a/pyzabbix/__init__.py b/pyzabbix/__init__.py index eabd5d1..883040b 100644 --- a/pyzabbix/__init__.py +++ b/pyzabbix/__init__.py @@ -1,4 +1,5 @@ from .api import ZabbixAPI, ZabbixAPIException, ssl_context_compat -from .sender import ZabbixMetric, ZabbixSender, ZabbixResponse +from .sender import (ZabbixMetric, ZabbixSender, ZabbixResponse, + ZabbixActiveChecksResponse, ZabbixCheck) __version__ = '1.1.3' diff --git a/pyzabbix/sender.py b/pyzabbix/sender.py index 196f073..1aeb208 100644 --- a/pyzabbix/sender.py +++ b/pyzabbix/sender.py @@ -131,6 +131,70 @@ def __repr__(self): return result +class ZabbixActiveChecksResponse(object): + """The :class:`ZabbixActiveChecksResponse` + contains the parsed response from Zabbix. + """ + def __init__(self): + self.checks = None + + def __repr__(self): + """Represent detailed ZabbixActiveChecksResponse view.""" + if self.checks is None: + return '' + else: + return "[%s]" % ", ".join([ch.__repr__() for ch in self.checks]) + + def parse(self, response): + """Parse zabbix response for active checks.""" + self.checks = [] + data = response.get('data') + for o in data: + # lastlogsize is optional in 2.2; required in 2.4, 3.0, 3.2, 3.4 + # mtime is unused in 3.2; required in 2.4, 3.0, 3.2, 3.4 + ch = ZabbixCheck( + o.get('key'), + o.get('delay'), + o.get('lastlogsize'), + o.get('mtime')) + self.checks.append(ch) + + +class ZabbixCheck(object): + """The :class:`ZabbixCheck` contains one active check + the Zabbix server would like. + + :type key: str + :param key: Key by which you will identify this metric. + + :type delay: int + :param delay: Period (in seconds) to collect this metric. + + :type lastlogsize: int + :param lastlogsize: Point in the log file already known by the server. + + :type mtime: int + :param mtime: Last time the server heard about this metric. + + >>> from pyzabbix import ZabbixCheck + >>> ZabbixCheck('cpu[usage]', 60, 0, 0) + """ + + def __init__(self, key, delay, lastlogsize, mtime): + self.key = str(key) + self.delay = delay + self.lastlogsize = lastlogsize + self.mtime = mtime + + def __repr__(self): + """Represent detailed ZabbixCheck view.""" + + result = json.dumps(self.__dict__) + logger.debug('%s: %s', self.__class__.__name__, result) + + return result + + class ZabbixSender(object): """The :class:`ZabbixSender` send metrics to Zabbix server. @@ -385,3 +449,57 @@ def send(self, metrics): for m in range(0, len(metrics), self.chunk_size): result.parse(self._chunk_send(metrics[m:m + self.chunk_size])) return result + + def _create_active_checks_request(self, host): + """Create a formatted request to zabbix from a host name. + + :type host: str + :param host: The host to ask about + + :rtype: str + :return: Formatted zabbix request + """ + request = '{"request":"active checks","host":"%s"}' % host + request = request.encode("utf-8") + logger.debug('Request: %s', request) + return request + + def send_active_checks(self, host): + """Get active checks from the zabbix server. + + :type host: str + :param host: host to ask about + + :rtype: :class:`pyzabbix.sender.ZabbixActiveChecksResponse` + :return: Parsed response from Zabbix Server + """ + request = self._create_active_checks_request(host) + packet = self._create_packet(request) + + for host_addr in self.zabbix_uri: + logger.debug('Sending data to %s', host_addr) + + # create socket object + connection = socket.socket() + + # server and port must be tuple + connection.connect(host_addr) + + try: + connection.sendall(packet) + except Exception as err: + # In case of error we should close connection, + # otherwise we will close it after data will be received. + connection.close() + raise Exception(err) + + response = self._get_response(connection) + logger.debug('%s response: %s', host_addr, response) + + if response and response.get('response') != 'success': + logger.debug('Response error: %s}', response) + raise Exception(response) + + result = ZabbixActiveChecksResponse() + result.parse(response) + return result diff --git a/tests/test_sender.py b/tests/test_sender.py index 32962ba..64cada2 100644 --- a/tests/test_sender.py +++ b/tests/test_sender.py @@ -12,7 +12,8 @@ from unittest.mock import patch, call, mock_open autospec = True -from pyzabbix import ZabbixMetric, ZabbixSender, ZabbixResponse +from pyzabbix import (ZabbixMetric, ZabbixSender, ZabbixResponse, + ZabbixActiveChecksResponse, ZabbixCheck) class TestZabbixResponse(TestCase): @@ -51,11 +52,42 @@ def test_repr(self): self.assertEqual(zm_repr, zm.__dict__) +class TestZabbixActiveChecksResponse(TestCase): + def test_init(self): + zr = ZabbixActiveChecksResponse() + self.assertIsNone(zr.checks) + + def test_repr(self): + zr = ZabbixActiveChecksResponse() + self.assertEqual(zr.__repr__(), "") + + def test_parse(self): + zr = ZabbixActiveChecksResponse() + zr.parse({'data': [ + {'key': 'cpu[usage]', 'delay': 60, 'lastlogsize': 5, 'mtime': 9}, + {'key': 'disk[io]', 'delay': 120, 'lastlogsize': 8, 'mtime': 4}]}) + self.assertEqual(zr.checks[0].key, "cpu[usage]") + self.assertEqual(zr.checks[0].delay, 60) + self.assertEqual(zr.checks[0].lastlogsize, 5) + self.assertEqual(zr.checks[0].mtime, 9) + self.assertEqual(zr.checks[1].key, "disk[io]") + self.assertEqual(zr.checks[1].delay, 120) + self.assertEqual(zr.checks[1].lastlogsize, 8) + self.assertEqual(zr.checks[1].mtime, 4) + + class TestsZabbixSender(TestCase): def setUp(self): self.resp_header = b'ZBXD\x01\\\x00\x00\x00\x00\x00\x00\x00' self.resp_body = b'''{"response":"success","info":"processed: 0; \ failed: 10; total: 10; seconds spent: 0.000078"} +''' + + self.resp_header_active_checks = \ + b'ZBXD\x01\\\x00\x00\x00\x00\x00\x00\x00' + self.resp_body_active_checks = b'''{"response":"success","data":[\ +{"key":"cpu[usage]","delay":60,"lastlogsize":6,"mtime":4},\ +{"key":"disk[io]","delay":1200,"lastlogsize":9,"mtime":7}]} ''' def test_init(self): @@ -185,6 +217,19 @@ def test_get_response_fail_s_close(self, mock_socket): result = zs._get_response(mock_socket) self.assertFalse(result) + @patch('pyzabbix.sender.socket.socket', autospec=autospec) + def test_get_response_to_active_checks(self, mock_socket): + mock_socket.recv.side_effect = ( + self.resp_header_active_checks, + self.resp_body_active_checks + ) + + zs = ZabbixSender() + result = zs._get_response(mock_socket) + mock_socket.recv.assert_has_calls([call(92)]) + self.assertEqual(result['response'], 'success') + self.assertIsNotNone(result['data']) + @patch('pyzabbix.sender.socket.socket', autospec=autospec) def test_send(self, mock_socket): mock_data = b'\x01\\\x00\x00\x00\x00\x00\x00\x00'