diff --git a/.github/scripts/additional_api_tests.py b/.github/scripts/additional_api_tests.py index 1ffe373..2ad8661 100644 --- a/.github/scripts/additional_api_tests.py +++ b/.github/scripts/additional_api_tests.py @@ -6,7 +6,6 @@ import sys import ssl -import base64 import unittest from aiohttp import ClientSession, TCPConnector @@ -18,77 +17,6 @@ ZABBIX_URL = 'https://127.0.0.1:443' ZABBIX_USER = 'Admin' ZABBIX_PASSWORD = 'zabbix' -HTTP_USER = 'http_user' -HTTP_PASSWORD = 'http_pass' - - -class IntegrationAPITest(unittest.TestCase): - """Test working with a real Zabbix API instance synchronously""" - - def setUp(self): - self.user = ZABBIX_USER - self.password = ZABBIX_PASSWORD - self.url = ZABBIX_URL + '/http_auth/' - self.api = ZabbixAPI( - url=self.url, - user=self.user, - password=self.password, - skip_version_check=True, - validate_certs=False, - http_user=HTTP_USER, - http_password=HTTP_PASSWORD - ) - - def tearDown(self): - if self.api: - self.api.logout() - - def test_login(self): - """Tests login function works properly""" - - self.assertEqual( - type(self.api), ZabbixAPI, "Login was going wrong") - self.assertEqual( - type(self.api.api_version()), APIVersion, "Version getting was going wrong") - - def test_basic_auth(self): - """Tests __basic_auth function works properly""" - - self.assertEqual( - self.api._ZabbixAPI__basic_cred, base64.b64encode( - "http_user:http_pass".encode() - ).decode(), "Basic auth credentials generation was going wrong") - - def test_version_get(self): - """Tests getting version info works properly""" - - version = None - if self.api: - version = self.api.apiinfo.version() - self.assertEqual( - version, str(self.api.api_version()), "Request apiinfo.version was going wrong") - - def test_check_auth(self): - """Tests checking authentication state works properly""" - - resp = None - if self.api: - if self.api._ZabbixAPI__session_id == self.api._ZabbixAPI__token: - resp = self.api.user.checkAuthentication(token=self.api._ZabbixAPI__session_id) - else: - resp = self.api.user.checkAuthentication(sessionid=self.api._ZabbixAPI__session_id) - self.assertEqual( - type(resp), dict, "Request user.checkAuthentication was going wrong") - - def test_user_get(self): - """Tests getting users info works properly""" - - users = None - if self.api: - users = self.api.user.get( - output=['userid', 'name'] - ) - self.assertEqual(type(users), list, "Request user.get was going wrong") class CustomCertAPITest(unittest.TestCase): @@ -154,80 +82,6 @@ def test_user_get(self): self.assertEqual(type(users), list, "Request user.get was going wrong") -class IntegrationAsyncAPITest(unittest.IsolatedAsyncioTestCase): - """Test working with a real Zabbix API instance asynchronously""" - - async def asyncSetUp(self): - self.user = ZABBIX_USER - self.password = ZABBIX_PASSWORD - self.url = ZABBIX_URL + '/http_auth/' - self.api = AsyncZabbixAPI( - url=self.url, - skip_version_check=True, - validate_certs=False, - http_user=HTTP_USER, - http_password=HTTP_PASSWORD - ) - await self.api.login( - user=self.user, - password=self.password - ) - - async def asyncTearDown(self): - if self.api: - await self.api.logout() - - async def test_login(self): - """Tests login function works properly""" - - self.assertEqual( - type(self.api), AsyncZabbixAPI, "Login was going wrong") - self.assertEqual( - type(self.api.api_version()), APIVersion, "Version getting was going wrong") - - async def test_basic_auth(self): - """Tests __basic_auth function works properly""" - - basic_auth = self.api.client_session._default_auth - - self.assertEqual( - base64.b64encode(f"{basic_auth.login}:{basic_auth.password}".encode()).decode(), - base64.b64encode(f"{HTTP_USER}:{HTTP_PASSWORD}".encode()).decode(), - "Basic auth credentials generation was going wrong" - ) - - async def test_version_get(self): - """Tests getting version info works properly""" - - version = None - if self.api: - version = await self.api.apiinfo.version() - self.assertEqual( - version, str(self.api.api_version()), "Request apiinfo.version was going wrong") - - async def test_check_auth(self): - """Tests checking authentication state works properly""" - - resp = None - if self.api: - if self.api._AsyncZabbixAPI__session_id == self.api._AsyncZabbixAPI__token: - resp = await self.api.user.checkAuthentication(token=(self.api._AsyncZabbixAPI__session_id or '')) - else: - resp = await self.api.user.checkAuthentication(sessionid=(self.api._AsyncZabbixAPI__session_id or '')) - self.assertEqual( - type(resp), dict, "Request user.checkAuthentication was going wrong") - - async def test_user_get(self): - """Tests getting users info works properly""" - - users = None - if self.api: - users = await self.api.user.get( - output=['userid', 'name'] - ) - self.assertEqual(type(users), list, "Request user.get was going wrong") - - class CustomCertAsyncAPITest(unittest.IsolatedAsyncioTestCase): """Test working with a real Zabbix API instance asynchronously""" @@ -238,14 +92,14 @@ async def asyncSetUp(self): context = ssl.create_default_context() context.load_verify_locations('/etc/nginx/ssl/nginx.crt') - session = ClientSession( + self.session = ClientSession( connector=TCPConnector(ssl=context) ) self.api = AsyncZabbixAPI( url=self.url, skip_version_check=True, - client_session=session + client_session=self.session ) await self.api.login( user=self.user, @@ -255,6 +109,8 @@ async def asyncSetUp(self): async def asyncTearDown(self): if self.api: await self.api.logout() + if not self.session.closed: + await self.session.close() async def test_login(self): """Tests login function works properly""" diff --git a/.github/scripts/compatibility_api_test_5.py b/.github/scripts/compatibility_api_test_5.py index b9ef881..009e2a1 100644 --- a/.github/scripts/compatibility_api_test_5.py +++ b/.github/scripts/compatibility_api_test_5.py @@ -147,8 +147,6 @@ def prepare_items(self): value_type=3 )['itemids'][0] - time.sleep(2) - self.assertIsNotNone(hostid, "Creating test item was going wrong") zapi.logout() @@ -156,6 +154,8 @@ def prepare_items(self): def test_send_values(self): """Tests sending item values""" + time.sleep(10) + items = [ ItemValue(self.hostname, self.itemkey, 10), ItemValue(self.hostname, self.itemkey, 'test message'), @@ -337,7 +337,7 @@ async def prepare_items(self): async def test_send_values(self): """Tests sending item values""" - time.sleep(2) + time.sleep(10) items = [ ItemValue(self.hostname, self.itemkey, 10), diff --git a/.github/scripts/compatibility_api_test_6.py b/.github/scripts/compatibility_api_test_6.py index 2219ef8..5808851 100644 --- a/.github/scripts/compatibility_api_test_6.py +++ b/.github/scripts/compatibility_api_test_6.py @@ -179,8 +179,6 @@ def prepare_items(self): value_type=3 )['itemids'][0] - time.sleep(2) - self.assertIsNotNone(hostid, "Creating test item was going wrong") zapi.logout() @@ -188,6 +186,8 @@ def prepare_items(self): def test_send_values(self): """Tests sending item values""" + time.sleep(10) + items = [ ItemValue(self.hostname, self.itemkey, 10), ItemValue(self.hostname, self.itemkey, 'test message'), @@ -403,7 +403,7 @@ async def prepare_items(self): async def test_send_values(self): """Tests sending item values""" - time.sleep(2) + time.sleep(10) items = [ ItemValue(self.hostname, self.itemkey, 10), diff --git a/.github/scripts/compatibility_api_test_7.py b/.github/scripts/compatibility_api_test_7.py index ba85b4e..cb944ee 100644 --- a/.github/scripts/compatibility_api_test_7.py +++ b/.github/scripts/compatibility_api_test_7.py @@ -21,10 +21,11 @@ ZABBIX_URL = '127.0.0.1' ZABBIX_USER = 'Admin' ZABBIX_PASSWORD = 'zabbix' +ZABBIX_PROXY_ADDR = '127.0.0.1' class CompatibilityAPITest(unittest.TestCase): - """Compatibility synchronous test with Zabbix API version 7.0""" + """Compatibility synchronous test with Zabbix API version 7.0, 7.2""" def setUp(self): self.url = ZABBIX_URL @@ -110,7 +111,7 @@ def test_token_auth(self): class CompatibilitySenderTest(unittest.TestCase): - """Compatibility synchronous test with Zabbix sender version 7.0""" + """Compatibility synchronous test with Zabbix sender version 7.0, 7.2""" def setUp(self): self.ip = ZABBIX_URL @@ -121,32 +122,42 @@ def setUp(self): port=self.port, chunk_size=self.chunk_size ) + self.zapi = None + self.hostid = None + self.proxy_groupid = None + self.proxy_ip = ZABBIX_PROXY_ADDR self.hostname = f"{self.__class__.__name__}_host" self.itemname = f"{self.__class__.__name__}_item" self.itemkey = f"{self.__class__.__name__}" - self.prepare_items() + self.pgroupname = "CompatibilitySenderTest_group" + self.proxy = "CompatibilitySenderTest_proxy" + self.proxyids = [] + self.prepare_instance() - def prepare_items(self): - """Creates host and items for sending values later""" + def tearDown(self): + if self.zapi: + self.zapi.logout() + + def prepare_instance(self): + """Creates required entities for sending values later""" - zapi = ZabbixAPI( + self.zapi = ZabbixAPI( url=ZABBIX_URL, user=ZABBIX_USER, password=ZABBIX_PASSWORD, skip_version_check=True ) - hosts = zapi.host.get( + hosts = self.zapi.host.get( filter={'host': self.hostname}, output=['hostid'] ) - hostid = None if len(hosts) > 0: - hostid = hosts[0].get('hostid') + self.hostid = hosts[0].get('hostid') - if not hostid: - hostid = zapi.host.create( + if not self.hostid: + self.hostid = self.zapi.host.create( host=self.hostname, interfaces=[{ "type": 1, @@ -159,9 +170,9 @@ def prepare_items(self): groups=[{"groupid": "2"}] )['hostids'][0] - self.assertIsNotNone(hostid, "Creating test host was going wrong") + self.assertIsNotNone(self.hostid, "Creating test host was going wrong") - items = zapi.item.get( + items = self.zapi.item.get( filter={'key_': self.itemkey}, output=['itemid'] ) @@ -171,23 +182,68 @@ def prepare_items(self): itemid = items[0].get('itemid') if not itemid: - itemid = zapi.item.create( + itemid = self.zapi.item.create( name=self.itemname, key_=self.itemkey, - hostid=hostid, + hostid=self.hostid, type=2, value_type=3 )['itemids'][0] time.sleep(2) - self.assertIsNotNone(hostid, "Creating test item was going wrong") + self.assertIsNotNone(itemid, "Creating test item was going wrong") + + groups = self.zapi.proxygroup.get( + filter={'name': self.pgroupname}, + output=['proxy_groupid'] + ) + + if len(groups) > 0: + self.proxy_groupid = groups[0].get('proxy_groupid') - zapi.logout() + if not self.proxy_groupid: + self.proxy_groupid = self.zapi.proxygroup.create( + name=self.pgroupname, + failover_delay="10s", + min_online="1" + )['proxy_groupids'][0] + + self.assertIsNotNone(self.proxy_groupid, "Creating test proxy group was going wrong") + + time.sleep(10) + + proxies = self.zapi.proxy.get( + search={'name': self.proxy}, + output=['proxyid'] + ) + if len(proxies) > 0: + self.zapi.proxy.delete(*[p['proxyid'] for p in proxies]) + + self.proxyids += self.zapi.proxy.create( + name=self.proxy + "1", + operating_mode="0", + local_address=self.proxy_ip, + local_port=10061, + proxy_groupid=self.proxy_groupid + )['proxyids'] + self.proxyids += self.zapi.proxy.create( + name=self.proxy + "2", + operating_mode="0", + local_address=self.proxy_ip, + local_port=10062, + proxy_groupid=self.proxy_groupid + )['proxyids'] + + self.assertTrue(bool(self.proxyids), "Creating test proxy group was going wrong") def test_send_values(self): """Tests sending item values""" + self.zapi.host.update( + hostid=self.hostid, + monitored_by="0" + ) items = [ ItemValue(self.hostname, self.itemkey, 10), ItemValue(self.hostname, self.itemkey, 'test message'), @@ -208,9 +264,29 @@ def test_send_values(self): self.assertEqual(first_chunk.processed, 4, "Number of the processed values is unexpected") self.assertEqual(first_chunk.failed, (first_chunk.total - first_chunk.processed), "Number of the failed values is unexpected") + for port in [10061, 10062]: + self.sender = Sender( + server=self.proxy_ip, + port=port, + chunk_size=self.chunk_size + ) + self.zapi.host.update( + hostid=self.hostid, + monitored_by="2", + proxy_groupid=self.proxy_groupid + ) + resp = self.sender.send_value(self.hostname, self.itemkey, 10) + + self.assertEqual(type(resp), TrapperResponse, "Sending item values was going wrong") + self.assertEqual(resp.total, 1, "Total number of the sent values is unexpected") + + first_chunk = list(resp.details.values())[0][0] + self.assertEqual(type(first_chunk), TrapperResponse, "Sending item values was going wrong") + self.assertEqual(first_chunk.total, 1, "Total number of the sent values is unexpected") + class CompatibilityGetTest(unittest.TestCase): - """Compatibility synchronous test with Zabbix get version 7.0""" + """Compatibility synchronous test with Zabbix get version 7.0, 7.2""" def setUp(self): self.host = ZABBIX_URL @@ -231,7 +307,7 @@ def test_get_values(self): class CompatibilityAsyncAPITest(unittest.IsolatedAsyncioTestCase): - """Compatibility asynchronous test with Zabbix API version 7.0""" + """Compatibility asynchronous test with Zabbix API version 7.0, 7.2""" async def asyncSetUp(self): self.url = ZABBIX_URL @@ -323,7 +399,7 @@ async def test_token_auth(self): class CompatibilityAsyncSenderTest(unittest.IsolatedAsyncioTestCase): - """Compatibility asynchronous test with Zabbix sender version 7.0""" + """Compatibility asynchronous test with Zabbix sender version 7.0, 7.2""" async def asyncSetUp(self): self.ip = ZABBIX_URL @@ -334,34 +410,42 @@ async def asyncSetUp(self): port=self.port, chunk_size=self.chunk_size ) + self.zapi = None + self.hostid = None + self.proxy_groupid = None + self.proxy_ip = ZABBIX_PROXY_ADDR self.hostname = f"{self.__class__.__name__}_host" self.itemname = f"{self.__class__.__name__}_item" self.itemkey = f"{self.__class__.__name__}" + self.pgroupname = "CompatibilitySenderTest_group" + self.proxy = "CompatibilitySenderTest_proxy" + self.proxyids = [] await self.prepare_items() + async def asyncTearDown(self): + if self.zapi: + self.zapi.logout() + async def prepare_items(self): - """Creates host and items for sending values later""" + """Creates required entities for sending values later""" - zapi = AsyncZabbixAPI( + self.zapi = ZabbixAPI( url=ZABBIX_URL, - skip_version_check=True - ) - await zapi.login( user=ZABBIX_USER, - password=ZABBIX_PASSWORD + password=ZABBIX_PASSWORD, + skip_version_check=True ) - hosts = await zapi.host.get( + hosts = self.zapi.host.get( filter={'host': self.hostname}, output=['hostid'] ) - hostid = None if len(hosts) > 0: - hostid = hosts[0].get('hostid') + self.hostid = hosts[0].get('hostid') - if not hostid: - created_host = await zapi.host.create( + if not self.hostid: + created_host = self.zapi.host.create( host=self.hostname, interfaces=[{ "type": 1, @@ -373,11 +457,11 @@ async def prepare_items(self): }], groups=[{"groupid": "2"}] ) - hostid = created_host['hostids'][0] + self.hostid = created_host['hostids'][0] - self.assertIsNotNone(hostid, "Creating test host was going wrong") + self.assertIsNotNone(self.hostid, "Creating test host was going wrong") - items = await zapi.item.get( + items = self.zapi.item.get( filter={'key_': self.itemkey}, output=['itemid'] ) @@ -387,23 +471,72 @@ async def prepare_items(self): itemid = items[0].get('itemid') if not itemid: - created_item = await zapi.item.create( + created_item = self.zapi.item.create( name=self.itemname, key_=self.itemkey, - hostid=hostid, + hostid=self.hostid, type=2, value_type=3 ) itemid = created_item['itemids'][0] - self.assertIsNotNone(hostid, "Creating test item was going wrong") + self.assertIsNotNone(itemid, "Creating test item was going wrong") + + groups = self.zapi.proxygroup.get( + filter={'name': self.pgroupname}, + output=['proxy_groupid'] + ) + + if len(groups) > 0: + self.proxy_groupid = groups[0].get('proxy_groupid') - await zapi.logout() + if not self.proxy_groupid: + created_proxy_group = self.zapi.proxygroup.create( + name=self.pgroupname, + failover_delay="10s", + min_online="1" + ) + self.proxy_groupid = created_proxy_group['proxy_groupids'][0] + + self.assertIsNotNone(self.proxy_groupid, "Creating test proxy group was going wrong") + + time.sleep(10) + + proxies = self.zapi.proxy.get( + search={'name': self.proxy}, + output=['proxyid'] + ) + if len(proxies) > 0: + self.zapi.proxy.delete(*[p['proxyid'] for p in proxies]) + + created_proxy = self.zapi.proxy.create( + name=self.proxy + "1", + operating_mode="0", + local_address=self.proxy_ip, + local_port=10061, + proxy_groupid=self.proxy_groupid + ) + self.proxyids += created_proxy['proxyids'] + created_proxy = self.zapi.proxy.create( + name=self.proxy + "2", + operating_mode="0", + local_address=self.proxy_ip, + local_port=10062, + proxy_groupid=self.proxy_groupid + ) + self.proxyids += created_proxy['proxyids'] + + self.assertTrue(bool(self.proxyids), "Creating test proxy group was going wrong") async def test_send_values(self): """Tests sending item values""" - time.sleep(2) + self.zapi.host.update( + hostid=self.hostid, + monitored_by="0" + ) + + time.sleep(5) items = [ ItemValue(self.hostname, self.itemkey, 10), @@ -425,9 +558,29 @@ async def test_send_values(self): self.assertEqual(first_chunk.processed, 4, "Number of the processed values is unexpected") self.assertEqual(first_chunk.failed, (first_chunk.total - first_chunk.processed), "Number of the failed values is unexpected") + for port in [10061, 10062]: + self.sender = AsyncSender( + server=self.proxy_ip, + port=port, + chunk_size=self.chunk_size + ) + self.zapi.host.update( + hostid=self.hostid, + monitored_by="2", + proxy_groupid=self.proxy_groupid + ) + resp = await self.sender.send_value(self.hostname, self.itemkey, 10) + + self.assertEqual(type(resp), TrapperResponse, "Sending item values was going wrong") + self.assertEqual(resp.total, 1, "Total number of the sent values is unexpected") + + first_chunk = list(resp.details.values())[0][0] + self.assertEqual(type(first_chunk), TrapperResponse, "Sending item values was going wrong") + self.assertEqual(first_chunk.total, 1, "Total number of the sent values is unexpected") + class CompatibilityAsyncGetTest(unittest.IsolatedAsyncioTestCase): - """Compatibility asynchronous test with Zabbix get version 7.0""" + """Compatibility asynchronous test with Zabbix get version 7.0, 7.2""" async def asyncSetUp(self): self.host = ZABBIX_URL diff --git a/.github/scripts/compatibility_api_test_latest.py b/.github/scripts/compatibility_api_test_latest.py index 05f5fcb..daad262 100644 --- a/.github/scripts/compatibility_api_test_latest.py +++ b/.github/scripts/compatibility_api_test_latest.py @@ -21,6 +21,7 @@ ZABBIX_URL = '127.0.0.1' ZABBIX_USER = 'Admin' ZABBIX_PASSWORD = 'zabbix' +ZABBIX_PROXY_ADDR = '127.0.0.1' class CompatibilityAPITest(unittest.TestCase): @@ -122,32 +123,42 @@ def setUp(self): port=self.port, chunk_size=self.chunk_size ) + self.zapi = None + self.hostid = None + self.proxy_groupid = None + self.proxy_ip = ZABBIX_PROXY_ADDR self.hostname = f"{self.__class__.__name__}_host" self.itemname = f"{self.__class__.__name__}_item" self.itemkey = f"{self.__class__.__name__}" - self.prepare_items() + self.pgroupname = "CompatibilitySenderTest_group" + self.proxy = "CompatibilitySenderTest_proxy" + self.proxyids = [] + self.prepare_instance() - def prepare_items(self): - """Creates host and items for sending values later""" + def tearDown(self): + if self.zapi: + self.zapi.logout() + + def prepare_instance(self): + """Creates required entities for sending values later""" - zapi = ZabbixAPI( + self.zapi = ZabbixAPI( url=ZABBIX_URL, user=ZABBIX_USER, password=ZABBIX_PASSWORD, skip_version_check=True ) - hosts = zapi.host.get( + hosts = self.zapi.host.get( filter={'host': self.hostname}, output=['hostid'] ) - hostid = None if len(hosts) > 0: - hostid = hosts[0].get('hostid') + self.hostid = hosts[0].get('hostid') - if not hostid: - hostid = zapi.host.create( + if not self.hostid: + self.hostid = self.zapi.host.create( host=self.hostname, interfaces=[{ "type": 1, @@ -160,9 +171,9 @@ def prepare_items(self): groups=[{"groupid": "2"}] )['hostids'][0] - self.assertIsNotNone(hostid, "Creating test host was going wrong") + self.assertIsNotNone(self.hostid, "Creating test host was going wrong") - items = zapi.item.get( + items = self.zapi.item.get( filter={'key_': self.itemkey}, output=['itemid'] ) @@ -172,23 +183,68 @@ def prepare_items(self): itemid = items[0].get('itemid') if not itemid: - itemid = zapi.item.create( + itemid = self.zapi.item.create( name=self.itemname, key_=self.itemkey, - hostid=hostid, + hostid=self.hostid, type=2, value_type=3 )['itemids'][0] - time.sleep(2) + time.sleep(2) + + self.assertIsNotNone(itemid, "Creating test item was going wrong") - self.assertIsNotNone(hostid, "Creating test item was going wrong") + groups = self.zapi.proxygroup.get( + filter={'name': self.pgroupname}, + output=['proxy_groupid'] + ) - zapi.logout() + if len(groups) > 0: + self.proxy_groupid = groups[0].get('proxy_groupid') + + if not self.proxy_groupid: + self.proxy_groupid = self.zapi.proxygroup.create( + name=self.pgroupname, + failover_delay="10s", + min_online="1" + )['proxy_groupids'][0] + + self.assertIsNotNone(self.proxy_groupid, "Creating test proxy group was going wrong") + + time.sleep(10) + + proxies = self.zapi.proxy.get( + search={'name': self.proxy}, + output=['proxyid'] + ) + if len(proxies) > 0: + self.zapi.proxy.delete(*[p['proxyid'] for p in proxies]) + + self.proxyids += self.zapi.proxy.create( + name=self.proxy + "1", + operating_mode="0", + local_address=self.proxy_ip, + local_port=10061, + proxy_groupid=self.proxy_groupid + )['proxyids'] + self.proxyids += self.zapi.proxy.create( + name=self.proxy + "2", + operating_mode="0", + local_address=self.proxy_ip, + local_port=10062, + proxy_groupid=self.proxy_groupid + )['proxyids'] + + self.assertTrue(bool(self.proxyids), "Creating test proxy group was going wrong") def test_send_values(self): """Tests sending item values""" + self.zapi.host.update( + hostid=self.hostid, + monitored_by="0" + ) items = [ ItemValue(self.hostname, self.itemkey, 10), ItemValue(self.hostname, self.itemkey, 'test message'), @@ -209,6 +265,26 @@ def test_send_values(self): self.assertEqual(first_chunk.processed, 4, "Number of the processed values is unexpected") self.assertEqual(first_chunk.failed, (first_chunk.total - first_chunk.processed), "Number of the failed values is unexpected") + for port in [10061, 10062]: + self.sender = Sender( + server=self.proxy_ip, + port=port, + chunk_size=self.chunk_size + ) + self.zapi.host.update( + hostid=self.hostid, + monitored_by="2", + proxy_groupid=self.proxy_groupid + ) + resp = self.sender.send_value(self.hostname, self.itemkey, 10) + + self.assertEqual(type(resp), TrapperResponse, "Sending item values was going wrong") + self.assertEqual(resp.total, 1, "Total number of the sent values is unexpected") + + first_chunk = list(resp.details.values())[0][0] + self.assertEqual(type(first_chunk), TrapperResponse, "Sending item values was going wrong") + self.assertEqual(first_chunk.total, 1, "Total number of the sent values is unexpected") + class CompatibilityGetTest(unittest.TestCase): """Compatibility synchronous test with the latest Zabbix get version""" @@ -336,34 +412,42 @@ async def asyncSetUp(self): port=self.port, chunk_size=self.chunk_size ) + self.zapi = None + self.hostid = None + self.proxy_groupid = None + self.proxy_ip = ZABBIX_PROXY_ADDR self.hostname = f"{self.__class__.__name__}_host" self.itemname = f"{self.__class__.__name__}_item" self.itemkey = f"{self.__class__.__name__}" + self.pgroupname = "CompatibilitySenderTest_group" + self.proxy = "CompatibilitySenderTest_proxy" + self.proxyids = [] await self.prepare_items() + async def asyncTearDown(self): + if self.zapi: + self.zapi.logout() + async def prepare_items(self): - """Creates host and items for sending values later""" + """Creates required entities for sending values later""" - zapi = AsyncZabbixAPI( + self.zapi = ZabbixAPI( url=ZABBIX_URL, - skip_version_check=True - ) - await zapi.login( user=ZABBIX_USER, - password=ZABBIX_PASSWORD + password=ZABBIX_PASSWORD, + skip_version_check=True ) - hosts = await zapi.host.get( + hosts = self.zapi.host.get( filter={'host': self.hostname}, output=['hostid'] ) - hostid = None if len(hosts) > 0: - hostid = hosts[0].get('hostid') + self.hostid = hosts[0].get('hostid') - if not hostid: - created_host = await zapi.host.create( + if not self.hostid: + created_host = self.zapi.host.create( host=self.hostname, interfaces=[{ "type": 1, @@ -375,11 +459,11 @@ async def prepare_items(self): }], groups=[{"groupid": "2"}] ) - hostid = created_host['hostids'][0] + self.hostid = created_host['hostids'][0] - self.assertIsNotNone(hostid, "Creating test host was going wrong") + self.assertIsNotNone(self.hostid, "Creating test host was going wrong") - items = await zapi.item.get( + items = self.zapi.item.get( filter={'key_': self.itemkey}, output=['itemid'] ) @@ -389,23 +473,72 @@ async def prepare_items(self): itemid = items[0].get('itemid') if not itemid: - created_item = await zapi.item.create( + created_item = self.zapi.item.create( name=self.itemname, key_=self.itemkey, - hostid=hostid, + hostid=self.hostid, type=2, value_type=3 ) itemid = created_item['itemids'][0] - self.assertIsNotNone(hostid, "Creating test item was going wrong") + self.assertIsNotNone(itemid, "Creating test item was going wrong") + + groups = self.zapi.proxygroup.get( + filter={'name': self.pgroupname}, + output=['proxy_groupid'] + ) + + if len(groups) > 0: + self.proxy_groupid = groups[0].get('proxy_groupid') + + if not self.proxy_groupid: + created_proxy_group = self.zapi.proxygroup.create( + name=self.pgroupname, + failover_delay="10s", + min_online="1" + ) + self.proxy_groupid = created_proxy_group['proxy_groupids'][0] + + self.assertIsNotNone(self.proxy_groupid, "Creating test proxy group was going wrong") + + time.sleep(10) + + proxies = self.zapi.proxy.get( + search={'name': self.proxy}, + output=['proxyid'] + ) + if len(proxies) > 0: + self.zapi.proxy.delete(*[p['proxyid'] for p in proxies]) + + created_proxy = self.zapi.proxy.create( + name=self.proxy + "1", + operating_mode="0", + local_address=self.proxy_ip, + local_port=10061, + proxy_groupid=self.proxy_groupid + ) + self.proxyids += created_proxy['proxyids'] + created_proxy = self.zapi.proxy.create( + name=self.proxy + "2", + operating_mode="0", + local_address=self.proxy_ip, + local_port=10062, + proxy_groupid=self.proxy_groupid + ) + self.proxyids += created_proxy['proxyids'] - await zapi.logout() + self.assertTrue(bool(self.proxyids), "Creating test proxy group was going wrong") async def test_send_values(self): """Tests sending item values""" - time.sleep(2) + self.zapi.host.update( + hostid=self.hostid, + monitored_by="0" + ) + + time.sleep(5) items = [ ItemValue(self.hostname, self.itemkey, 10), @@ -427,6 +560,26 @@ async def test_send_values(self): self.assertEqual(first_chunk.processed, 4, "Number of the processed values is unexpected") self.assertEqual(first_chunk.failed, (first_chunk.total - first_chunk.processed), "Number of the failed values is unexpected") + for port in [10061, 10062]: + self.sender = AsyncSender( + server=self.proxy_ip, + port=port, + chunk_size=self.chunk_size + ) + self.zapi.host.update( + hostid=self.hostid, + monitored_by="2", + proxy_groupid=self.proxy_groupid + ) + resp = await self.sender.send_value(self.hostname, self.itemkey, 10) + + self.assertEqual(type(resp), TrapperResponse, "Sending item values was going wrong") + self.assertEqual(resp.total, 1, "Total number of the sent values is unexpected") + + first_chunk = list(resp.details.values())[0][0] + self.assertEqual(type(first_chunk), TrapperResponse, "Sending item values was going wrong") + self.assertEqual(first_chunk.total, 1, "Total number of the sent values is unexpected") + class CompatibilityAsyncGetTest(unittest.IsolatedAsyncioTestCase): """Compatibility asynchronous test with the latest Zabbix get version""" diff --git a/.github/scripts/depricated_tests.py b/.github/scripts/depricated_tests.py new file mode 100644 index 0000000..2113cc3 --- /dev/null +++ b/.github/scripts/depricated_tests.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python +# Copyright (C) 2001-2023 Zabbix SIA +# +# Zabbix SIA licenses this file under the MIT License. +# See the LICENSE file in the project root for more information. + +import sys +import base64 +import unittest + +sys.path.append('.') +from zabbix_utils.api import ZabbixAPI +from zabbix_utils.types import APIVersion +from zabbix_utils.aioapi import AsyncZabbixAPI + +ZABBIX_URL = 'https://127.0.0.1:443' +ZABBIX_USER = 'Admin' +ZABBIX_PASSWORD = 'zabbix' +HTTP_USER = 'http_user' +HTTP_PASSWORD = 'http_pass' + + +class BasicAuthAPITest(unittest.TestCase): + """Test working with a real Zabbix API instance using Basic auth synchronously + + Should be removed after: `June 30, 2029` + """ + + def setUp(self): + self.user = ZABBIX_USER + self.password = ZABBIX_PASSWORD + self.url = ZABBIX_URL + '/http_auth/' + self.api = ZabbixAPI( + url=self.url, + user=self.user, + password=self.password, + validate_certs=False, + http_user=HTTP_USER, + http_password=HTTP_PASSWORD + ) + + def tearDown(self): + if self.api: + self.api.logout() + + def test_login(self): + """Tests login function works properly""" + + self.assertEqual( + type(self.api), ZabbixAPI, "Login was going wrong") + self.assertEqual( + type(self.api.api_version()), APIVersion, "Version getting was going wrong") + + def test_basic_auth(self): + """Tests __basic_auth function works properly""" + + self.assertEqual( + self.api._ZabbixAPI__basic_cred, base64.b64encode( + "http_user:http_pass".encode() + ).decode(), "Basic auth credentials generation was going wrong") + + def test_version_get(self): + """Tests getting version info works properly""" + + version = None + if self.api: + version = self.api.apiinfo.version() + self.assertEqual( + version, str(self.api.api_version()), "Request apiinfo.version was going wrong") + + def test_check_auth(self): + """Tests checking authentication state works properly""" + + resp = None + if self.api: + if self.api._ZabbixAPI__session_id == self.api._ZabbixAPI__token: + resp = self.api.user.checkAuthentication(token=self.api._ZabbixAPI__session_id) + else: + resp = self.api.user.checkAuthentication(sessionid=self.api._ZabbixAPI__session_id) + self.assertEqual( + type(resp), dict, "Request user.checkAuthentication was going wrong") + + def test_user_get(self): + """Tests getting users info works properly""" + + users = None + if self.api: + users = self.api.user.get( + output=['userid', 'name'] + ) + self.assertEqual(type(users), list, "Request user.get was going wrong") + + +class BasicAuthAsyncAPITest(unittest.IsolatedAsyncioTestCase): + """Test working with a real Zabbix API instance using Basic auth asynchronously + + Should be removed after: `June 30, 2029` + """ + + async def asyncSetUp(self): + self.user = ZABBIX_USER + self.password = ZABBIX_PASSWORD + self.url = ZABBIX_URL + '/http_auth/' + self.api = AsyncZabbixAPI( + url=self.url, + validate_certs=False, + http_user=HTTP_USER, + http_password=HTTP_PASSWORD + ) + await self.api.login( + user=self.user, + password=self.password + ) + + async def asyncTearDown(self): + if self.api: + await self.api.logout() + + async def test_login(self): + """Tests login function works properly""" + + self.assertEqual( + type(self.api), AsyncZabbixAPI, "Login was going wrong") + self.assertEqual( + type(self.api.api_version()), APIVersion, "Version getting was going wrong") + + async def test_basic_auth(self): + """Tests __basic_auth function works properly""" + + basic_auth = self.api.client_session._default_auth + + self.assertEqual( + base64.b64encode(f"{basic_auth.login}:{basic_auth.password}".encode()).decode(), + base64.b64encode(f"{HTTP_USER}:{HTTP_PASSWORD}".encode()).decode(), + "Basic auth credentials generation was going wrong" + ) + + async def test_version_get(self): + """Tests getting version info works properly""" + + version = None + if self.api: + version = await self.api.apiinfo.version() + self.assertEqual( + version, str(self.api.api_version()), "Request apiinfo.version was going wrong") + + async def test_check_auth(self): + """Tests checking authentication state works properly""" + + resp = None + if self.api: + if self.api._AsyncZabbixAPI__session_id == self.api._AsyncZabbixAPI__token: + resp = await self.api.user.checkAuthentication(token=(self.api._AsyncZabbixAPI__session_id or '')) + else: + resp = await self.api.user.checkAuthentication(sessionid=(self.api._AsyncZabbixAPI__session_id or '')) + self.assertEqual( + type(resp), dict, "Request user.checkAuthentication was going wrong") + + async def test_user_get(self): + """Tests getting users info works properly""" + + users = None + if self.api: + users = await self.api.user.get( + output=['userid', 'name'] + ) + self.assertEqual(type(users), list, "Request user.get was going wrong") + + +if __name__ == '__main__': + unittest.main() diff --git a/.github/scripts/release_notification.py b/.github/scripts/release_notification.py new file mode 100644 index 0000000..65971af --- /dev/null +++ b/.github/scripts/release_notification.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# coding: utf-8 + +import os +import json +import smtplib +import markdown + +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart + +# Repository constants +LIBRARY_VERSION = os.environ['LIBRARY_VERSION'] +REPOSITORY = os.environ['REPOSITORY'] + +# Mail server variables +mail_port = int(os.environ.get('MAIL_PORT', '465')) +mail_server = os.environ['MAIL_SERVER'] +auth_user = os.environ['MAIL_USER'] +auth_pass = os.environ['MAIL_PASS'] + +# Mail variables +mail_from = '"zabbix_utils" <' + auth_user + '>' +mail_to = json.loads(os.environ['RELEASE_RECIPIENT_LIST']) +mail_subject = f"[GitHub] A new version {LIBRARY_VERSION} of the zabbix_utils library has been released" +mail_text = f""" + A new version of the zabbix_utils library has been released: + v{LIBRARY_VERSION} + +

+""" + +# Reading release notes +with open("RELEASE_NOTES.md", "r", encoding="utf-8") as fh: + release_notes = markdown.markdown("\n".join(fh.readlines()[1:])) + +# Preparing mail data +msg = MIMEMultipart('mixed') +msg['Subject'] = mail_subject +msg['From'] = mail_from +msg['To'] = ', '.join(mail_to) + +# Adding message text +msg.attach(MIMEText(mail_text + release_notes, 'html')) + +# Connection to the mail server +server = smtplib.SMTP_SSL(mail_server, mail_port) +server.login(auth_user, auth_pass) + +# Sending email +server.sendmail(mail_from, mail_to, msg.as_string()) + +# Closing connection +server.quit() diff --git a/.github/workflows/additional_tests.yaml b/.github/workflows/additional_tests.yaml index be6bc60..1478992 100644 --- a/.github/workflows/additional_tests.yaml +++ b/.github/workflows/additional_tests.yaml @@ -52,7 +52,7 @@ jobs: TBOT_CHAT: ${{ vars.TBOT_CHAT }} SUBJECT: Importing tests with requirements FAIL run: | - bash ./.github/scripts/library_import_tests.sh "async" "AsyncZabbixAPI" "aiohttp.client.ClientSession" > /tmp/importing.log + bash ./.github/scripts/library_import_tests.sh "async" "AsyncZabbixAPI" "Unable to connect to" > /tmp/importing.log - name: Raise an exception run: | test $(cat /tmp/importing.log | wc -l) -eq 0 || exit 1 @@ -116,7 +116,7 @@ jobs: run: | sudo apt-get install -y python3 python3-pip python-is-python3 pip install -r ./requirements.txt - - name: Additional tests + - name: Run tests continue-on-error: true run: | sleep 5 diff --git a/.github/workflows/compatibility_70.yaml b/.github/workflows/compatibility_70.yaml index 9955570..ad14899 100644 --- a/.github/workflows/compatibility_70.yaml +++ b/.github/workflows/compatibility_70.yaml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v4 - name: Install packages run: | - sudo apt update && sudo apt install -y git sudo gcc make automake pkg-config postgresql-14 libpostgresql-ocaml-dev libxml2-dev libpcre3-dev libevent-dev apache2 libapache2-mod-php php8.1-pgsql php8.1-bcmath php8.1-xml php8.1-gd php8.1-ldap php8.1-mbstring libzip-dev + sudo apt update && sudo apt install -y git sudo gcc make automake pkg-config postgresql-14 libpostgresql-ocaml-dev libxml2-dev libpcre3-dev libevent-dev apache2 libapache2-mod-php php8.1-pgsql php8.1-bcmath php8.1-xml php8.1-gd php8.1-ldap php8.1-mbstring libzip-dev zabbix-sender - name: Build from sources run: | WORKDIR=$(pwd) @@ -35,6 +35,26 @@ jobs: sudo make dbschema_postgresql sudo make echo -e "CacheUpdateFrequency=1\n" >> ./conf/zabbix_server.conf + ./configure --enable-proxy --with-sqlite3 + sudo make + mkdir /tmp/zabbix_proxy1/ + mkdir /tmp/zabbix_proxy2/ + cp ./conf/zabbix_proxy.conf ./conf/zabbix_proxy1.conf + mv ./conf/zabbix_proxy.conf ./conf/zabbix_proxy2.conf + sed -i "s/Hostname=Zabbix proxy/Hostname=CompatibilitySenderTest_proxy1/g" ./conf/zabbix_proxy1.conf + sed -i "s/Hostname=Zabbix proxy/Hostname=CompatibilitySenderTest_proxy2/g" ./conf/zabbix_proxy2.conf + sed -i "s#LogFile=/tmp/zabbix_proxy.log#LogFile=/tmp/zabbix_proxy1.log#g" ./conf/zabbix_proxy1.conf + sed -i "s#LogFile=/tmp/zabbix_proxy.log#LogFile=/tmp/zabbix_proxy2.log#g" ./conf/zabbix_proxy2.conf + sed -i 's#DBName=zabbix_proxy#DBName=/tmp/proxy1.db#' ./conf/zabbix_proxy1.conf + sed -i 's#DBName=zabbix_proxy#DBName=/tmp/proxy2.db#' ./conf/zabbix_proxy2.conf + echo -e "PidFile=/tmp/zabbix_proxy1/zabbix_proxy1.pid\n" >> ./conf/zabbix_proxy1.conf + echo -e "PidFile=/tmp/zabbix_proxy2/zabbix_proxy2.pid\n" >> ./conf/zabbix_proxy2.conf + echo -e "SocketDir=/tmp/zabbix_proxy1\n" >> ./conf/zabbix_proxy1.conf + echo -e "SocketDir=/tmp/zabbix_proxy2\n" >> ./conf/zabbix_proxy2.conf + echo -e "ListenPort=10061\n" >> ./conf/zabbix_proxy1.conf + echo -e "ListenPort=10062\n" >> ./conf/zabbix_proxy2.conf + sudo chown -R zabbix:zabbix /tmp/zabbix_proxy1/ + sudo chown -R zabbix:zabbix /tmp/zabbix_proxy2/ cd ui sudo rm /var/www/html/index.html sudo cp -a . /var/www/html/ @@ -66,6 +86,12 @@ jobs: run: | cd /tmp/zabbix-branch sudo ./src/zabbix_server/zabbix_server -c ./conf/zabbix_server.conf + - name: Start Zabbix proxies + continue-on-error: true + run: | + cd /tmp/zabbix-branch + sudo ./src/zabbix_proxy/zabbix_proxy -c ./conf/zabbix_proxy1.conf + sudo ./src/zabbix_proxy/zabbix_proxy -c ./conf/zabbix_proxy2.conf - name: Start Zabbix agent run: | cd /tmp/zabbix-branch diff --git a/.github/workflows/compatibility_64.yaml b/.github/workflows/compatibility_72.yaml similarity index 66% rename from .github/workflows/compatibility_64.yaml rename to .github/workflows/compatibility_72.yaml index e26b192..73ef9d3 100644 --- a/.github/workflows/compatibility_64.yaml +++ b/.github/workflows/compatibility_72.yaml @@ -1,5 +1,5 @@ -name: zabbix_64 -run-name: Compatibility with Zabbix 6.4 test +name: zabbix_72 +run-name: Compatibility with Zabbix 7.2 test on: push: @@ -9,10 +9,10 @@ on: workflow_dispatch: env: - ZABBIX_VERSION: '6.4' + ZABBIX_VERSION: '7.2' ZABBIX_BRANCH: release/$ZABBIX_VERSION CONFIG_PATH: .github/configs/ - TEST_FILE: compatibility_api_test_6.py + TEST_FILE: compatibility_api_test_7.py jobs: compatibility: @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v4 - name: Install packages run: | - sudo apt update && sudo apt install -y git sudo gcc make automake pkg-config postgresql-14 libpostgresql-ocaml-dev libxml2-dev libpcre3-dev libevent-dev apache2 libapache2-mod-php php8.1-pgsql php8.1-bcmath php8.1-xml php8.1-gd php8.1-ldap php8.1-mbstring libzip-dev + sudo apt update && sudo apt install -y git sudo gcc make automake pkg-config postgresql-14 libpostgresql-ocaml-dev libxml2-dev libpcre3-dev libevent-dev apache2 libapache2-mod-php php8.1-pgsql php8.1-bcmath php8.1-xml php8.1-gd php8.1-ldap php8.1-mbstring libzip-dev zabbix-sender - name: Build from sources run: | WORKDIR=$(pwd) @@ -35,6 +35,26 @@ jobs: sudo make dbschema_postgresql sudo make echo -e "CacheUpdateFrequency=1\n" >> ./conf/zabbix_server.conf + ./configure --enable-proxy --with-sqlite3 + sudo make + mkdir /tmp/zabbix_proxy1/ + mkdir /tmp/zabbix_proxy2/ + cp ./conf/zabbix_proxy.conf ./conf/zabbix_proxy1.conf + mv ./conf/zabbix_proxy.conf ./conf/zabbix_proxy2.conf + sed -i "s/Hostname=Zabbix proxy/Hostname=CompatibilitySenderTest_proxy1/g" ./conf/zabbix_proxy1.conf + sed -i "s/Hostname=Zabbix proxy/Hostname=CompatibilitySenderTest_proxy2/g" ./conf/zabbix_proxy2.conf + sed -i "s#LogFile=/tmp/zabbix_proxy.log#LogFile=/tmp/zabbix_proxy1.log#g" ./conf/zabbix_proxy1.conf + sed -i "s#LogFile=/tmp/zabbix_proxy.log#LogFile=/tmp/zabbix_proxy2.log#g" ./conf/zabbix_proxy2.conf + sed -i 's#DBName=zabbix_proxy#DBName=/tmp/proxy1.db#' ./conf/zabbix_proxy1.conf + sed -i 's#DBName=zabbix_proxy#DBName=/tmp/proxy2.db#' ./conf/zabbix_proxy2.conf + echo -e "PidFile=/tmp/zabbix_proxy1/zabbix_proxy1.pid\n" >> ./conf/zabbix_proxy1.conf + echo -e "PidFile=/tmp/zabbix_proxy2/zabbix_proxy2.pid\n" >> ./conf/zabbix_proxy2.conf + echo -e "SocketDir=/tmp/zabbix_proxy1\n" >> ./conf/zabbix_proxy1.conf + echo -e "SocketDir=/tmp/zabbix_proxy2\n" >> ./conf/zabbix_proxy2.conf + echo -e "ListenPort=10061\n" >> ./conf/zabbix_proxy1.conf + echo -e "ListenPort=10062\n" >> ./conf/zabbix_proxy2.conf + sudo chown -R zabbix:zabbix /tmp/zabbix_proxy1/ + sudo chown -R zabbix:zabbix /tmp/zabbix_proxy2/ cd ui sudo rm /var/www/html/index.html sudo cp -a . /var/www/html/ @@ -66,6 +86,12 @@ jobs: run: | cd /tmp/zabbix-branch sudo ./src/zabbix_server/zabbix_server -c ./conf/zabbix_server.conf + - name: Start Zabbix proxies + continue-on-error: true + run: | + cd /tmp/zabbix-branch + sudo ./src/zabbix_proxy/zabbix_proxy -c ./conf/zabbix_proxy1.conf + sudo ./src/zabbix_proxy/zabbix_proxy -c ./conf/zabbix_proxy2.conf - name: Start Zabbix agent run: | cd /tmp/zabbix-branch diff --git a/.github/workflows/compatibility_latest.yaml b/.github/workflows/compatibility_latest.yaml index 2cc3135..7eea525 100644 --- a/.github/workflows/compatibility_latest.yaml +++ b/.github/workflows/compatibility_latest.yaml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v4 - name: Install packages run: | - sudo apt update && sudo apt install -y git sudo gcc make automake pkg-config postgresql-14 libpostgresql-ocaml-dev libxml2-dev libpcre3-dev libevent-dev apache2 libapache2-mod-php php8.1-pgsql php8.1-bcmath php8.1-xml php8.1-gd php8.1-ldap php8.1-mbstring libzip-dev + sudo apt update && sudo apt install -y git sudo gcc make automake pkg-config postgresql-14 libpostgresql-ocaml-dev libxml2-dev libpcre3-dev libevent-dev apache2 libapache2-mod-php php8.1-pgsql php8.1-bcmath php8.1-xml php8.1-gd php8.1-ldap php8.1-mbstring libzip-dev zabbix-sender - name: Build from sources run: | WORKDIR=$(pwd) @@ -32,6 +32,26 @@ jobs: sudo make dbschema_postgresql sudo make echo -e "CacheUpdateFrequency=1\n" >> ./conf/zabbix_server.conf + ./configure --enable-proxy --with-sqlite3 + sudo make + mkdir /tmp/zabbix_proxy1/ + mkdir /tmp/zabbix_proxy2/ + cp ./conf/zabbix_proxy.conf ./conf/zabbix_proxy1.conf + mv ./conf/zabbix_proxy.conf ./conf/zabbix_proxy2.conf + sed -i "s/Hostname=Zabbix proxy/Hostname=CompatibilitySenderTest_proxy1/g" ./conf/zabbix_proxy1.conf + sed -i "s/Hostname=Zabbix proxy/Hostname=CompatibilitySenderTest_proxy2/g" ./conf/zabbix_proxy2.conf + sed -i "s#LogFile=/tmp/zabbix_proxy.log#LogFile=/tmp/zabbix_proxy1.log#g" ./conf/zabbix_proxy1.conf + sed -i "s#LogFile=/tmp/zabbix_proxy.log#LogFile=/tmp/zabbix_proxy2.log#g" ./conf/zabbix_proxy2.conf + sed -i 's#DBName=zabbix_proxy#DBName=/tmp/proxy1.db#' ./conf/zabbix_proxy1.conf + sed -i 's#DBName=zabbix_proxy#DBName=/tmp/proxy2.db#' ./conf/zabbix_proxy2.conf + echo -e "PidFile=/tmp/zabbix_proxy1/zabbix_proxy1.pid\n" >> ./conf/zabbix_proxy1.conf + echo -e "PidFile=/tmp/zabbix_proxy2/zabbix_proxy2.pid\n" >> ./conf/zabbix_proxy2.conf + echo -e "SocketDir=/tmp/zabbix_proxy1\n" >> ./conf/zabbix_proxy1.conf + echo -e "SocketDir=/tmp/zabbix_proxy2\n" >> ./conf/zabbix_proxy2.conf + echo -e "ListenPort=10061\n" >> ./conf/zabbix_proxy1.conf + echo -e "ListenPort=10062\n" >> ./conf/zabbix_proxy2.conf + sudo chown -R zabbix:zabbix /tmp/zabbix_proxy1/ + sudo chown -R zabbix:zabbix /tmp/zabbix_proxy2/ cd ui sudo rm /var/www/html/index.html sudo cp -a . /var/www/html/ @@ -63,6 +83,12 @@ jobs: run: | cd /tmp/zabbix-branch sudo ./src/zabbix_server/zabbix_server -c ./conf/zabbix_server.conf + - name: Start Zabbix proxies + continue-on-error: true + run: | + cd /tmp/zabbix-branch + sudo ./src/zabbix_proxy/zabbix_proxy -c ./conf/zabbix_proxy1.conf + sudo ./src/zabbix_proxy/zabbix_proxy -c ./conf/zabbix_proxy2.conf - name: Start Zabbix agent run: | cd /tmp/zabbix-branch diff --git a/.github/workflows/depricated_tests.yaml b/.github/workflows/depricated_tests.yaml new file mode 100644 index 0000000..f76ef83 --- /dev/null +++ b/.github/workflows/depricated_tests.yaml @@ -0,0 +1,89 @@ +name: depricated_tests +run-name: Tests for deprecated features + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +env: + ZABBIX_VERSION: '7.0' + ZABBIX_BRANCH: release/$ZABBIX_VERSION + CONFIG_PATH: .github/configs/ + TEST_FILE: depricated_tests.py + +jobs: + depricated-tests: + name: Depricated tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Install packages + run: | + sudo apt update && sudo apt install -y git sudo nginx gcc make automake pkg-config postgresql-14 libpostgresql-ocaml-dev libxml2-dev libpcre3-dev libevent-dev apache2 libapache2-mod-php php8.1-pgsql php8.1-bcmath php8.1-xml php8.1-gd php8.1-ldap php8.1-mbstring libzip-dev + - name: Build from sources + run: | + WORKDIR=$(pwd) + cd /tmp/ + git -c advice.detachedHead=false clone https://git.zabbix.com/scm/zbx/zabbix.git --branch ${{ env.ZABBIX_BRANCH }} --depth 1 --single-branch /tmp/zabbix-branch + cd /tmp/zabbix-branch + ./bootstrap.sh + ./configure --enable-server --with-postgresql + sudo make dbschema_postgresql + echo -e "CacheUpdateFrequency=1\n" >> ./conf/zabbix_server.conf + sudo mkdir -p /etc/nginx/ssl/ + sudo cp $WORKDIR/${{ env.CONFIG_PATH }}/.htpasswd /etc/nginx/.htpasswd + sudo cp $WORKDIR/${{ env.CONFIG_PATH }}/default.conf /etc/nginx/sites-enabled/default + sudo openssl req -x509 -nodes -days 1 -newkey rsa:2048 -keyout /etc/nginx/ssl/nginx.key -out /etc/nginx/ssl/nginx.crt -config $WORKDIR/${{ env.CONFIG_PATH }}/nginx.cnf + sudo chown -R www-data:www-data /etc/nginx/ + cd ui + sudo rm /var/www/html/index.html + sudo cp -a . /var/www/html/ + sudo cp $WORKDIR/${{ env.CONFIG_PATH }}/zabbix.conf.php /var/www/html/conf/ + sudo cp $WORKDIR/${{ env.CONFIG_PATH }}/pg_hba.conf /etc/postgresql/14/main/pg_hba.conf + sudo chown -R www-data:www-data /var/www/html/ + sudo sed -i "s/post_max_size = 8M/post_max_size = 16M/g" /etc/php/8.1/apache2/php.ini + sudo sed -i "s/max_execution_time = 30/max_execution_time = 300/g" /etc/php/8.1/apache2/php.ini + sudo sed -i "s/max_input_time = 60/max_input_time = 300/g" /etc/php/8.1/apache2/php.ini + sudo sed -i "s/Listen 80/Listen 8080/g" /etc/apache2/ports.conf + sudo sed -i "s///g" /etc/apache2/sites-enabled/000-default.conf + sudo locale-gen en_US.UTF-8 + sudo update-locale + - name: Prepare environment + run: | + sudo addgroup --system --quiet zabbix + sudo adduser --quiet --system --disabled-login --ingroup zabbix --home /var/lib/zabbix --no-create-home zabbix + sudo mkdir -p /var/run/postgresql/14-main.pg_stat_tmp + sudo touch /var/run/postgresql/14-main.pg_stat_tmp/global.tmp + sudo chmod 0777 /var/run/postgresql/14-main.pg_stat_tmp/global.tmp + (sudo -u postgres /usr/lib/postgresql/14/bin/postgres -D /var/lib/postgresql/14/main -c config_file=/etc/postgresql/14/main/postgresql.conf)& + sleep 5 + cd /tmp/zabbix-branch/database/postgresql + sudo -u postgres createuser zabbix + sudo -u postgres createdb -O zabbix -E Unicode -T template0 zabbix + cat schema.sql | sudo -u zabbix psql zabbix + cat images.sql | sudo -u zabbix psql zabbix + cat data.sql | sudo -u zabbix psql zabbix + - name: Start Apache & Nginx + run: | + sudo apache2ctl start + sudo nginx -g "daemon on; master_process on;" + - name: Install python3 + run: | + sudo apt-get install -y python3 python3-pip python-is-python3 + pip install -r ./requirements.txt + - name: Run tests + continue-on-error: true + run: | + sleep 5 + python ./.github/scripts/$TEST_FILE 2>/tmp/depricated.log >/dev/null + - name: Send report + env: + TBOT_TOKEN: ${{ secrets.TBOT_TOKEN }} + TBOT_CHAT: ${{ vars.TBOT_CHAT }} + SUBJECT: Zabbix API depricated tests FAIL + run: | + tail -n1 /tmp/depricated.log | grep "OK" 1>/dev/null || tail /tmp/depricated.log | python ./.github/scripts/telegram_msg.py | exit 1 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 2a26376..72e1b88 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -68,3 +68,15 @@ jobs: tag: "v${{ env.LIBRARY_VERSION }}" bodyFile: RELEASE_NOTES.md artifacts: dist/* + - name: Send notification + run: | + python ./.github/scripts/release_notification.py + working-directory: ./ + env: + MAIL_SERVER: ${{ secrets.MAIL_SERVER }} + MAIL_PORT: ${{ secrets.MAIL_PORT }} + MAIL_USER: ${{ secrets.MAIL_USER }} + MAIL_PASS: ${{ secrets.MAIL_PASS }} + RELEASE_RECIPIENT_LIST: ${{ secrets.RELEASE_RECIPIENT_LIST }} + LIBRARY_VERSION: ${{ env.LIBRARY_VERSION }} + REPOSITORY: ${{ github.repository }} diff --git a/CHANGELOG.md b/CHANGELOG.md index a068122..6fa06b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +## [2.0.2](https://github.com/zabbix/python-zabbix-utils/compare/v2.0.1...v2.0.2) (2024-12-12) + +### Features: + +- added support for Zabbix 7.2 +- added support of proxy groups for Sender and AsyncSender + +### Changes: + +- discontinued support for HTTP authentication for Zabbix 7.2 and newer +- discontinued support for Zabbix 6.4 +- added examples of deleting items +- added examples of how to clear item history +- added examples of how to pass get request parameters + +### Bug fixes: + +- fixed issue [#21](https://github.com/zabbix/python-zabbix-utils/issues/21) with non-obvious format of ID array passing +- fixed issue [#26](https://github.com/zabbix/python-zabbix-utils/issues/26) with Sender and AsyncSender working with proxy groups +- fixed small bugs and flaws + ## [2.0.1](https://github.com/zabbix/python-zabbix-utils/compare/v2.0.0...v2.0.1) (2024-09-18) ### Features: diff --git a/README.md b/README.md index d891ad0..58a61c1 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ [![Zabbix 5.0](https://github.com/zabbix/python-zabbix-utils/actions/workflows/compatibility_50.yaml/badge.svg)](https://github.com/zabbix/python-zabbix-utils/actions/workflows/compatibility_50.yaml) [![Zabbix 6.0](https://github.com/zabbix/python-zabbix-utils/actions/workflows/compatibility_60.yaml/badge.svg)](https://github.com/zabbix/python-zabbix-utils/actions/workflows/compatibility_60.yaml) -[![Zabbix 6.4](https://github.com/zabbix/python-zabbix-utils/actions/workflows/compatibility_64.yaml/badge.svg)](https://github.com/zabbix/python-zabbix-utils/actions/workflows/compatibility_64.yaml) [![Zabbix 7.0](https://github.com/zabbix/python-zabbix-utils/actions/workflows/compatibility_70.yaml/badge.svg)](https://github.com/zabbix/python-zabbix-utils/actions/workflows/compatibility_70.yaml) +[![Zabbix 7.2](https://github.com/zabbix/python-zabbix-utils/actions/workflows/compatibility_72.yaml/badge.svg)](https://github.com/zabbix/python-zabbix-utils/actions/workflows/compatibility_72.yaml) **zabbix_utils** is a Python library for working with [Zabbix API](https://www.zabbix.com/documentation/current/manual/api/reference) as well as with [Zabbix sender](https://www.zabbix.com/documentation/current/manpages/zabbix_sender) and [Zabbix get](https://www.zabbix.com/documentation/current/manpages/zabbix_get) protocols. @@ -29,7 +29,7 @@ Supported versions: Tested on: -* Zabbix 5.0, 6.0, 6.4 and 7.0 +* Zabbix 5.0, 6.0, 7.0 and 7.2 * Python 3.8, 3.9, 3.10, 3.11 and 3.12 Dependencies: diff --git a/examples/api/asynchronous/check_auth_state.py b/examples/api/asynchronous/check_auth_state.py index 42f8c46..cf7e3df 100644 --- a/examples/api/asynchronous/check_auth_state.py +++ b/examples/api/asynchronous/check_auth_state.py @@ -25,7 +25,7 @@ async def main(): # Create an instance of the AsyncZabbixAPI class api = AsyncZabbixAPI(ZABBIX_SERVER) - # Authenticating with Zabbix API using the provided token. + # Authenticating with Zabbix API using the provided username and password. await api.login(**ZABBIX_AUTH) # Some actions when your session can be released diff --git a/examples/api/asynchronous/clear_history.py b/examples/api/asynchronous/clear_history.py new file mode 100644 index 0000000..c973d6e --- /dev/null +++ b/examples/api/asynchronous/clear_history.py @@ -0,0 +1,46 @@ +# Copyright (C) 2001-2023 Zabbix SIA +# +# Zabbix SIA licenses this file to you under the MIT License. +# See the LICENSE file in the project root for more information. + +import asyncio +from zabbix_utils import AsyncZabbixAPI, APIRequestError + +# Zabbix server URL or IP address +ZABBIX_SERVER = "127.0.0.1" + +# Zabbix server authentication credentials +ZABBIX_AUTH = { + "user": "Admin", # Zabbix user name for authentication + "password": "zabbix" # Zabbix user password for authentication +} + +# IDs of items for which the history should be cleared +ITEM_IDS = [70060] + + +async def main(): + """ + The main function to perform asynchronous tasks. + """ + + # Create an instance of the AsyncZabbixAPI class + api = AsyncZabbixAPI(ZABBIX_SERVER) + + # Authenticating with Zabbix API using the provided username and password. + await api.login(**ZABBIX_AUTH) + + # Clear history for items with specified IDs + try: + await api.history.clear(*ITEM_IDS) + + # Alternative way to do the same (since v2.0.2): + # await api.history.clear(ITEM_IDS) + except APIRequestError as e: + print(f"An error occurred when attempting to delete items: {e}") + else: + # Logout to release the Zabbix API session + await api.logout() + +# Run the main coroutine +asyncio.run(main()) diff --git a/examples/api/asynchronous/custom_client_session.py b/examples/api/asynchronous/custom_client_session.py index 20a570b..e3bec44 100644 --- a/examples/api/asynchronous/custom_client_session.py +++ b/examples/api/asynchronous/custom_client_session.py @@ -34,7 +34,7 @@ async def main(): client_session=client_session ) - # Authenticating with Zabbix API using the provided token. + # Authenticating with Zabbix API using the provided username and password. await api.login(**ZABBIX_AUTH) # Retrieve a list of hosts from the Zabbix server, including their host ID and name diff --git a/examples/api/asynchronous/custom_ssl_context.py b/examples/api/asynchronous/custom_ssl_context.py index 70cddfc..cd44eda 100644 --- a/examples/api/asynchronous/custom_ssl_context.py +++ b/examples/api/asynchronous/custom_ssl_context.py @@ -40,7 +40,7 @@ async def main(): client_session=client_session ) - # Authenticating with Zabbix API using the provided token. + # Authenticating with Zabbix API using the provided username and password. await api.login(**ZABBIX_AUTH) # Retrieve a list of hosts from the Zabbix server, including their host ID and name diff --git a/examples/api/asynchronous/delete_items.py b/examples/api/asynchronous/delete_items.py new file mode 100644 index 0000000..ceca435 --- /dev/null +++ b/examples/api/asynchronous/delete_items.py @@ -0,0 +1,46 @@ +# Copyright (C) 2001-2023 Zabbix SIA +# +# Zabbix SIA licenses this file to you under the MIT License. +# See the LICENSE file in the project root for more information. + +import asyncio +from zabbix_utils import AsyncZabbixAPI, APIRequestError + +# Zabbix server URL or IP address +ZABBIX_SERVER = "127.0.0.1" + +# Zabbix server authentication credentials +ZABBIX_AUTH = { + "user": "Admin", # Zabbix user name for authentication + "password": "zabbix" # Zabbix user password for authentication +} + +# Item IDs to be deleted +ITEM_IDS = [70060] + + +async def main(): + """ + The main function to perform asynchronous tasks. + """ + + # Create an instance of the AsyncZabbixAPI class + api = AsyncZabbixAPI(ZABBIX_SERVER) + + # Authenticating with Zabbix API using the provided username and password. + await api.login(**ZABBIX_AUTH) + + # Delete items with specified IDs + try: + await api.item.delete(*ITEM_IDS) + + # Alternative way to do the same (since v2.0.2): + # await api.item.delete(ITEM_IDS) + except APIRequestError as e: + print(f"An error occurred when attempting to delete items: {e}") + else: + # Logout to release the Zabbix API session + await api.logout() + +# Run the main coroutine +asyncio.run(main()) diff --git a/examples/api/asynchronous/disabling_validate_certs.py b/examples/api/asynchronous/disabling_validate_certs.py index 145e56e..745f55d 100644 --- a/examples/api/asynchronous/disabling_validate_certs.py +++ b/examples/api/asynchronous/disabling_validate_certs.py @@ -31,7 +31,7 @@ async def main(): # Note: Ignoring SSL certificate validation may expose the connection to security risks. api = AsyncZabbixAPI(**ZABBIX_PARAMS) - # Authenticating with Zabbix API using the provided token. + # Authenticating with Zabbix API using the provided username and password. await api.login(**ZABBIX_AUTH) # Retrieve a list of users from the Zabbix server, including their user ID and name. diff --git a/examples/api/asynchronous/get_request_parameters.py b/examples/api/asynchronous/get_request_parameters.py new file mode 100644 index 0000000..ad7ee6f --- /dev/null +++ b/examples/api/asynchronous/get_request_parameters.py @@ -0,0 +1,51 @@ +# Copyright (C) 2001-2023 Zabbix SIA +# +# Zabbix SIA licenses this file to you under the MIT License. +# See the LICENSE file in the project root for more information. + +import asyncio +from zabbix_utils import AsyncZabbixAPI + +# Zabbix server URL or IP address +ZABBIX_SERVER = "127.0.0.1" + +# Zabbix server authentication credentials +ZABBIX_AUTH = { + "user": "Admin", # Zabbix user name for authentication + "password": "zabbix" # Zabbix user password for authentication +} + + +async def main(): + """ + The main function to perform asynchronous tasks. + """ + + # Create an instance of the AsyncZabbixAPI class + api = AsyncZabbixAPI(ZABBIX_SERVER) + + # Authenticating with Zabbix API using the provided username and password. + await api.login(**ZABBIX_AUTH) + + # There are only three ways to pass parameters of type dictionary: + # + # 1. Specifying values directly with their keys: + problems = await api.problem.get(tags=[{"tag": "scope", "value": "notice", "operator": "0"}]) + # + # 2. Unpacking dictionary keys and values using `**`: + # request_params = {"tags": [{"tag": "scope", "value": "notice", "operator": "0"}]} + # problems = await api.problem.get(**request_params) + # + # 3. Passing the dictionary directly as an argument (since v2.0.2): + # request_params = {"tags": [{"tag": "scope", "value": "notice", "operator": "0"}]} + # problems = await api.problem.get(request_params) + + # Print the names of the retrieved users + for problem in problems: + print(problem['name']) + + # Logout to release the Zabbix API session + await api.logout() + +# Run the main coroutine +asyncio.run(main()) diff --git a/examples/api/synchronous/clear_history.py b/examples/api/synchronous/clear_history.py new file mode 100644 index 0000000..abaa54b --- /dev/null +++ b/examples/api/synchronous/clear_history.py @@ -0,0 +1,31 @@ +# Copyright (C) 2001-2023 Zabbix SIA +# +# Zabbix SIA licenses this file to you under the MIT License. +# See the LICENSE file in the project root for more information. + +from zabbix_utils import ZabbixAPI, APIRequestError + +# Zabbix server details and authentication credentials +ZABBIX_AUTH = { + "url": "127.0.0.1", # Zabbix server URL or IP address + "user": "Admin", # Zabbix user name for authentication + "password": "zabbix" # Zabbix user password for authentication +} + +# IDs of items for which the history should be cleared +ITEM_IDS = [70060] + +# Create an instance of the ZabbixAPI class with the specified authentication details +api = ZabbixAPI(**ZABBIX_AUTH) + +# Clear history for items with specified IDs +try: + api.history.clear(*ITEM_IDS) + + # Alternative way to do the same (since v2.0.2): + # api.history.clear(*ITEM_IDS) +except APIRequestError as e: + print(f"An error occurred when attempting to clear items' history: {e}") + +# Logout to release the Zabbix API session +api.logout() diff --git a/examples/api/synchronous/delete_items.py b/examples/api/synchronous/delete_items.py new file mode 100644 index 0000000..3d77f6b --- /dev/null +++ b/examples/api/synchronous/delete_items.py @@ -0,0 +1,31 @@ +# Copyright (C) 2001-2023 Zabbix SIA +# +# Zabbix SIA licenses this file to you under the MIT License. +# See the LICENSE file in the project root for more information. + +from zabbix_utils import ZabbixAPI, APIRequestError + +# Zabbix server details and authentication credentials +ZABBIX_AUTH = { + "url": "127.0.0.1", # Zabbix server URL or IP address + "user": "Admin", # Zabbix user name for authentication + "password": "zabbix" # Zabbix user password for authentication +} + +# Item IDs to be deleted +ITEM_IDS = [70060] + +# Create an instance of the ZabbixAPI class with the specified authentication details +api = ZabbixAPI(**ZABBIX_AUTH) + +# Delete items with specified IDs +try: + api.item.delete(*ITEM_IDS) + + # Alternative way to do the same (since v2.0.2): + # api.item.delete(ITEM_IDS) +except APIRequestError as e: + print(f"An error occurred when attempting to delete items: {e}") + +# Logout to release the Zabbix API session +api.logout() diff --git a/examples/api/synchronous/get_request_parameters.py b/examples/api/synchronous/get_request_parameters.py new file mode 100644 index 0000000..ec6036c --- /dev/null +++ b/examples/api/synchronous/get_request_parameters.py @@ -0,0 +1,36 @@ +# Copyright (C) 2001-2023 Zabbix SIA +# +# Zabbix SIA licenses this file to you under the MIT License. +# See the LICENSE file in the project root for more information. + +from zabbix_utils import ZabbixAPI + +# Zabbix server details and authentication credentials +ZABBIX_AUTH = { + "url": "127.0.0.1", # Zabbix server URL or IP address + "user": "Admin", # Zabbix user name for authentication + "password": "zabbix" # Zabbix user password for authentication +} + +# Create an instance of the ZabbixAPI class with the specified authentication details +api = ZabbixAPI(**ZABBIX_AUTH) + +# There are only three ways to pass parameters of type dictionary: +# +# 1. Specifying values directly with their keys: +problems = api.problem.get(tags=[{"tag": "scope", "value": "notice", "operator": "0"}]) +# +# 2. Unpacking dictionary keys and values using `**`: +# request_params = {"tags": [{"tag": "scope", "value": "notice", "operator": "0"}]} +# problems = api.problem.get(**request_params) +# +# 3. Passing the dictionary directly as an argument (since v2.0.2): +# request_params = {"tags": [{"tag": "scope", "value": "notice", "operator": "0"}]} +# problems = api.problem.get(request_params) + +# Print the names of the retrieved users +for problem in problems: + print(problem['name']) + +# Logout to release the Zabbix API session +api.logout() diff --git a/tests/common.py b/tests/common.py index cab5887..e8e66ec 100644 --- a/tests/common.py +++ b/tests/common.py @@ -81,6 +81,9 @@ class MockBasicAuth(): class MockSessionConn(): def __init__(self): self._ssl = None + self.closed = False + def close(self): + self.closed = True class MockSession(): def __init__(self, exception=None): diff --git a/tests/test_zabbix_aioapi.py b/tests/test_zabbix_aioapi.py index 2423d49..96f1417 100644 --- a/tests/test_zabbix_aioapi.py +++ b/tests/test_zabbix_aioapi.py @@ -289,8 +289,10 @@ async def test__prepare_request(self): params={'user': DEFAULT_VALUES['user'], 'password': DEFAULT_VALUES['password']}, need_auth=True ) - self.assertEqual(req.get('auth'), DEFAULT_VALUES['token'], - "unexpected auth request parameter, must be: " + DEFAULT_VALUES['token']) + self.assertEqual(req.get('auth'), None, + "unexpected auth request parameter, must be: None") + self.assertEqual(headers.get('Authorization'), 'Bearer ' + DEFAULT_VALUES['token'], + "unexpected Authorization header, must be: Bearer " + DEFAULT_VALUES['token']) self.zapi.client_session.del_auth() await self.zapi.logout() diff --git a/tests/test_zabbix_api.py b/tests/test_zabbix_api.py index 49cfa43..63ec234 100644 --- a/tests/test_zabbix_api.py +++ b/tests/test_zabbix_api.py @@ -50,10 +50,13 @@ def mock_urlopen(*args, **kwargs): ul, urlopen=mock_urlopen): - zapi = ZabbixAPI( - http_user=DEFAULT_VALUES['user'], - http_password=DEFAULT_VALUES['password'] - ) + with self.assertRaises(APINotSupported, + msg="expected APINotSupported exception hasn't been raised"): + ZabbixAPI( + http_user=DEFAULT_VALUES['user'], + http_password=DEFAULT_VALUES['password'] + ) + zapi = ZabbixAPI() with self.assertRaises(ProcessingError, msg="expected ProcessingError exception hasn't been raised"): zapi.hosts.get() @@ -160,7 +163,9 @@ def test_login(self): ZabbixAPI, send_api_request=common.mock_send_sync_request): - zapi = ZabbixAPI(http_user=DEFAULT_VALUES['user'], http_password=DEFAULT_VALUES['password']) + with self.assertRaises(APINotSupported, msg="expected APINotSupported exception hasn't been raised"): + ZabbixAPI(http_user=DEFAULT_VALUES['user'], http_password=DEFAULT_VALUES['password']) + zapi = ZabbixAPI() with self.assertRaises(TypeError, msg="expected TypeError exception hasn't been raised"): zapi = ZabbixAPI() diff --git a/zabbix_utils/aioapi.py b/zabbix_utils/aioapi.py index 97290ad..ccf411d 100644 --- a/zabbix_utils/aioapi.py +++ b/zabbix_utils/aioapi.py @@ -84,13 +84,17 @@ async def func(*args: Any, **kwargs: Any) -> Any: # Support '_' suffix to avoid conflicts with python keywords method = removesuffix(self.object, '_') + "." + removesuffix(name, '_') + # Support passing list of ids and params as a dict + params = kwargs or ( + (args[0] if type(args[0]) in (list, dict,) else list(args)) if args else None) + log.debug("Executing %s method", method) need_auth = method not in ModuleUtils.UNAUTH_METHODS response = await self.parent.send_async_request( method, - args or kwargs, + params, need_auth ) return response.get('result') @@ -133,6 +137,7 @@ def __init__(self, url: Optional[str] = None, client_params["connector"] = aiohttp.TCPConnector( ssl=self.validate_certs ) + # HTTP Auth unsupported since Zabbix 7.2 if http_user and http_password: client_params["auth"] = aiohttp.BasicAuth( login=http_user, @@ -149,6 +154,10 @@ def __init__(self, url: Optional[str] = None, self.__check_version(skip_version_check) + if self.version > 7.0 and http_user and http_password: + self.__close_session() + raise APINotSupported("HTTP authentication unsupported since Zabbix 7.2.") + def __getattr__(self, name: str) -> Callable: """Dynamic creation of an API object. @@ -167,14 +176,18 @@ async def __aenter__(self) -> Callable: async def __aexit__(self, *args) -> None: await self.logout() - async def __close_session(self) -> None: + async def __aclose_session(self) -> None: if self.__internal_client: await self.__internal_client.close() async def __exception(self, exc) -> None: - await self.__close_session() + await self.__aclose_session() raise exc from exc + def __close_session(self) -> None: + if self.__internal_client: + self.__internal_client._connector.close() + def api_version(self) -> APIVersion: """Return object of Zabbix API version. @@ -257,13 +270,13 @@ async def logout(self) -> None: if self.__use_token: self.__session_id = None self.__use_token = False - await self.__close_session() + await self.__aclose_session() return log.debug("Logout from Zabbix API") await self.user.logout() self.__session_id = None - await self.__close_session() + await self.__aclose_session() else: log.debug("You're not logged in Zabbix API") @@ -305,7 +318,9 @@ def __prepare_request(self, method: str, params: Optional[dict] = None, if need_auth: if not self.__session_id: raise ProcessingError("You're not logged in Zabbix API") - if self.version < 6.4 or self.client_session._default_auth is not None: + if self.version < 6.4: + request['auth'] = self.__session_id + elif self.version <= 7.0 and self.client_session._default_auth is not None: request['auth'] = self.__session_id else: headers["Authorization"] = f"Bearer {self.__session_id}" @@ -401,6 +416,7 @@ def send_sync_request(self, method: str, params: Optional[dict] = None, request_json, headers = self.__prepare_request(method, params, need_auth) + # HTTP Auth unsupported since Zabbix 7.2 basic_auth = self.client_session._default_auth if basic_auth is not None: headers["Authorization"] = "Basic " + base64.b64encode( @@ -429,9 +445,14 @@ def send_sync_request(self, method: str, params: Optional[dict] = None, resp = ul.urlopen(req, context=ctx) resp_json = json.loads(resp.read().decode('utf-8')) except URLError as err: + self.__close_session() raise ProcessingError(f"Unable to connect to {self.url}:", err) from None except ValueError as err: + self.__close_session() raise ProcessingError("Unable to parse json:", err) from None + except Exception as err: + self.__close_session() + raise ProcessingError(err) from None return self.__check_response(method, resp_json) diff --git a/zabbix_utils/aiosender.py b/zabbix_utils/aiosender.py index 7748c56..6e51d4e 100644 --- a/zabbix_utils/aiosender.py +++ b/zabbix_utils/aiosender.py @@ -29,12 +29,12 @@ import logging import configparser -from typing import Callable, Union, Optional +from typing import Callable, Union, Optional, Tuple from .logger import EmptyHandler from .common import ZabbixProtocol from .exceptions import ProcessingError -from .types import TrapperResponse, ItemValue, Cluster +from .types import TrapperResponse, ItemValue, Cluster, Node log = logging.getLogger(__name__) log.addHandler(EmptyHandler()) @@ -138,99 +138,115 @@ def __create_request(self, items: list) -> dict: "data": [i.to_json() for i in items] } - async def __chunk_send(self, items: list) -> dict: - responses = {} - - packet = ZabbixProtocol.create_packet(self.__create_request(items), log, self.compression) - - for cluster in self.clusters: - active_node = None + async def __send_to_cluster(self, cluster: Cluster, packet: bytes) -> Optional[Tuple[Node, dict]]: + active_node = None + active_node_idx = 0 + for i, node in enumerate(cluster.nodes): - for i, node in enumerate(cluster.nodes): + log.debug('Trying to send data to %s', node) - log.debug('Trying to send data to %s', node) + connection_params = { + "host": node.address, + "port": node.port + } - connection_params = { - "host": node.address, - "port": node.port - } + if self.source_ip: + connection_params['local_addr'] = (self.source_ip, 0) - if self.source_ip: - connection_params['local_addr'] = (self.source_ip, 0) + if self.ssl_context is not None: + connection_params['ssl'] = self.ssl_context(self.tls) + if not isinstance(connection_params['ssl'], ssl.SSLContext): + raise TypeError( + 'Function "ssl_context" must return "ssl.SSLContext".') from None - if self.ssl_context is not None: - connection_params['ssl'] = self.ssl_context(self.tls) - if not isinstance(connection_params['ssl'], ssl.SSLContext): - raise TypeError( - 'Function "ssl_context" must return "ssl.SSLContext".') from None - - connection = asyncio.open_connection(**connection_params) - - try: - reader, writer = await asyncio.wait_for(connection, timeout=self.timeout) - except asyncio.TimeoutError: - log.debug( - 'The connection to %s timed out after %d seconds', - node, - self.timeout - ) - except (ConnectionRefusedError, socket.gaierror) as err: - log.debug( - 'An error occurred while trying to connect to %s: %s', - node, - getattr(err, 'msg', str(err)) - ) - else: - if i > 0: - cluster.nodes[0], cluster.nodes[i] = cluster.nodes[i], cluster.nodes[0] - active_node = node - break - - if active_node is None: - log.error( - 'Couldn\'t connect to all of cluster nodes: %s', - str(list(cluster.nodes)) - ) - raise ProcessingError( - f"Couldn't connect to all of cluster nodes: {list(cluster.nodes)}" - ) + connection = asyncio.open_connection(**connection_params) try: - writer.write(packet) - send_data = writer.drain() - await asyncio.wait_for(send_data, timeout=self.timeout) - except (asyncio.TimeoutError, socket.timeout) as err: - log.error( - 'The connection to %s timed out after %d seconds while trying to send', - active_node, + reader, writer = await asyncio.wait_for(connection, timeout=self.timeout) + except asyncio.TimeoutError: + log.debug( + 'The connection to %s timed out after %d seconds', + node, self.timeout ) - writer.close() - await writer.wait_closed() - raise err - except (OSError, socket.error) as err: - log.warning( - 'An error occurred while trying to send to %s: %s', - active_node, + except (ConnectionRefusedError, socket.gaierror) as err: + log.debug( + 'An error occurred while trying to connect to %s: %s', + node, getattr(err, 'msg', str(err)) ) - writer.close() - await writer.wait_closed() - raise err - try: - response = await self.__get_response(reader) - except (ConnectionResetError, asyncio.exceptions.IncompleteReadError) as err: - log.debug('Get value error: %s', err) - raise err - log.debug('Response from %s: %s', active_node, response) + else: + active_node_idx = i + if i > 0: + cluster.nodes[0], cluster.nodes[i] = cluster.nodes[i], cluster.nodes[0] + active_node_idx = 0 + active_node = node + break + + if active_node is None: + log.error( + 'Couldn\'t connect to all of cluster nodes: %s', + str(list(cluster.nodes)) + ) + raise ProcessingError( + f"Couldn't connect to all of cluster nodes: {list(cluster.nodes)}" + ) - if response and response.get('response') != 'success': + try: + writer.write(packet) + send_data = writer.drain() + await asyncio.wait_for(send_data, timeout=self.timeout) + except (asyncio.TimeoutError, socket.timeout) as err: + log.error( + 'The connection to %s timed out after %d seconds while trying to send', + active_node, + self.timeout + ) + writer.close() + await writer.wait_closed() + raise err + except (OSError, socket.error) as err: + log.warning( + 'An error occurred while trying to send to %s: %s', + active_node, + getattr(err, 'msg', str(err)) + ) + writer.close() + await writer.wait_closed() + raise err + try: + response = await self.__get_response(reader) + except (ConnectionResetError, asyncio.exceptions.IncompleteReadError) as err: + log.debug('Get value error: %s', err) + raise err + log.debug('Response from %s: %s', active_node, response) + + if response and response.get('response') != 'success': + if response.get('redirect'): + log.debug( + 'Packet was redirected from %s to %s. Proxy group revision: %s.', + active_node, + response['redirect']['address'], + response['redirect']['revision'] + ) + cluster.nodes[active_node_idx] = Node(*response['redirect']['address'].split(':')) + active_node, response = await self.__send_to_cluster(cluster, packet) + else: raise ProcessingError(response) from None - responses[active_node] = response + writer.close() + await writer.wait_closed() - writer.close() - await writer.wait_closed() + return active_node, response + + async def __chunk_send(self, items: list) -> dict: + responses = {} + + packet = ZabbixProtocol.create_packet(self.__create_request(items), log, self.compression) + + for cluster in self.clusters: + active_node, response = await self.__send_to_cluster(cluster, packet) + responses[active_node] = response return responses diff --git a/zabbix_utils/api.py b/zabbix_utils/api.py index 1039757..c3ae9ad 100644 --- a/zabbix_utils/api.py +++ b/zabbix_utils/api.py @@ -83,13 +83,17 @@ def func(*args: Any, **kwargs: Any) -> Any: # Support '_' suffix to avoid conflicts with python keywords method = removesuffix(self.object, '_') + "." + removesuffix(name, '_') + # Support passing list of ids and params as a dict + params = kwargs or ( + (args[0] if type(args[0]) in (list, dict,) else list(args)) if args else None) + log.debug("Executing %s method", method) need_auth = method not in ModuleUtils.UNAUTH_METHODS return self.parent.send_api_request( method, - args or kwargs, + params, need_auth ).get('result') @@ -131,16 +135,21 @@ def __init__(self, url: Optional[str] = None, token: Optional[str] = None, self.validate_certs = validate_certs self.timeout = timeout + # HTTP Auth unsupported since Zabbix 7.2 if http_user and http_password: self.__basic_auth(http_user, http_password) if ssl_context is not None: if not isinstance(ssl_context, ssl.SSLContext): - raise TypeError('Function "ssl_context" must return "ssl.SSLContext".') from None + raise TypeError( + 'Parameter "ssl_context" must be an "ssl.SSLContext".') from None self.ssl_context = ssl_context self.__check_version(skip_version_check) + if self.version > 7.0 and http_user and http_password: + raise APINotSupported("HTTP authentication unsupported since Zabbix 7.2.") + if token or user or password: self.login(token, user, password) @@ -316,7 +325,9 @@ def send_api_request(self, method: str, params: Optional[dict] = None, if need_auth: if not self.__session_id: raise ProcessingError("You're not logged in Zabbix API") - if self.version < 6.4 or self.__basic_cred is not None: + if self.version < 6.4: + request_json['auth'] = self.__session_id + elif self.version <= 7.0 and self.__basic_cred is not None: request_json['auth'] = self.__session_id else: headers["Authorization"] = f"Bearer {self.__session_id}" diff --git a/zabbix_utils/sender.py b/zabbix_utils/sender.py index ec21b6f..cb34ca8 100644 --- a/zabbix_utils/sender.py +++ b/zabbix_utils/sender.py @@ -27,12 +27,12 @@ import logging import configparser -from typing import Callable, Optional, Union +from typing import Callable, Optional, Union, Tuple from .logger import EmptyHandler from .common import ZabbixProtocol from .exceptions import ProcessingError -from .types import TrapperResponse, ItemValue, Cluster +from .types import TrapperResponse, ItemValue, Cluster, Node log = logging.getLogger(__name__) log.addHandler(EmptyHandler()) @@ -116,7 +116,7 @@ def __load_config(self, filepath: str) -> None: config.read_string('[root]\n' + cfg.read()) self.__read_config(config['root']) - def __get_response(self, conn: socket) -> Optional[str]: + def __get_response(self, conn: socket) -> Optional[dict]: try: result = json.loads( ZabbixProtocol.parse_sync_packet(conn, log, ProcessingError) @@ -135,99 +135,116 @@ def __create_request(self, items: list) -> dict: "data": [i.to_json() for i in items] } - def __chunk_send(self, items: list) -> dict: - responses = {} - - packet = ZabbixProtocol.create_packet(self.__create_request(items), log, self.compression) - - for cluster in self.clusters: - active_node = None + def __send_to_cluster(self, cluster: Cluster, packet: bytes) -> Optional[Tuple[Node, dict]]: + active_node = None + active_node_idx = 0 + for i, node in enumerate(cluster.nodes): - for i, node in enumerate(cluster.nodes): + log.debug('Trying to send data to %s', node) - log.debug('Trying to send data to %s', node) - - try: - if self.use_ipv6: - connection = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) - else: - connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - except socket.error: - raise ProcessingError(f"Error creating socket for {node}") from None - - connection.settimeout(self.timeout) - - if self.source_ip: - connection.bind((self.source_ip, 0,)) - - try: - connection.connect((node.address, node.port)) - except (TimeoutError, socket.timeout): - log.debug( - 'The connection to %s timed out after %d seconds', - node, - self.timeout - ) - except (ConnectionRefusedError, socket.gaierror) as err: - log.debug( - 'An error occurred while trying to connect to %s: %s', - node, - getattr(err, 'msg', str(err)) - ) + try: + if self.use_ipv6: + connection = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) else: - if i > 0: - cluster.nodes[0], cluster.nodes[i] = cluster.nodes[i], cluster.nodes[0] - active_node = node - break - - if active_node is None: - log.error( - 'Couldn\'t connect to all of cluster nodes: %s', - str(list(cluster.nodes)) - ) - connection.close() - raise ProcessingError( - f"Couldn't connect to all of cluster nodes: {list(cluster.nodes)}" - ) + connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + except socket.error: + raise ProcessingError(f"Error creating socket for {node}") from None + + connection.settimeout(self.timeout) - if self.socket_wrapper is not None: - connection = self.socket_wrapper(connection, self.tls) + if self.source_ip: + connection.bind((self.source_ip, 0,)) try: - connection.sendall(packet) - except (TimeoutError, socket.timeout) as err: - log.error( - 'The connection to %s timed out after %d seconds while trying to send', - active_node, + connection.connect((node.address, node.port)) + except (TimeoutError, socket.timeout): + log.debug( + 'The connection to %s timed out after %d seconds', + node, self.timeout ) - connection.close() - raise err - except (OSError, socket.error) as err: - log.warning( - 'An error occurred while trying to send to %s: %s', - active_node, + except (ConnectionRefusedError, socket.gaierror) as err: + log.debug( + 'An error occurred while trying to connect to %s: %s', + node, getattr(err, 'msg', str(err)) ) - connection.close() - raise err + else: + active_node_idx = i + if i > 0: + cluster.nodes[0], cluster.nodes[i] = cluster.nodes[i], cluster.nodes[0] + active_node_idx = 0 + active_node = node + break + + if active_node is None: + log.error( + 'Couldn\'t connect to all of cluster nodes: %s', + str(list(cluster.nodes)) + ) + connection.close() + raise ProcessingError( + f"Couldn't connect to all of cluster nodes: {list(cluster.nodes)}" + ) - try: - response = self.__get_response(connection) - except ConnectionResetError as err: - log.debug('Get value error: %s', err) - raise err - log.debug('Response from %s: %s', active_node, response) + if self.socket_wrapper is not None: + connection = self.socket_wrapper(connection, self.tls) - if response and response.get('response') != 'success': + try: + connection.sendall(packet) + except (TimeoutError, socket.timeout) as err: + log.error( + 'The connection to %s timed out after %d seconds while trying to send', + active_node, + self.timeout + ) + connection.close() + raise err + except (OSError, socket.error) as err: + log.warning( + 'An error occurred while trying to send to %s: %s', + active_node, + getattr(err, 'msg', str(err)) + ) + connection.close() + raise err + + try: + response = self.__get_response(connection) + except ConnectionResetError as err: + log.debug('Get value error: %s', err) + raise err + log.debug('Response from %s: %s', active_node, response) + + if response and response.get('response') != 'success': + if response.get('redirect'): + print(response) + log.debug( + 'Packet was redirected from %s to %s. Proxy group revision: %s.', + active_node, + response['redirect']['address'], + response['redirect']['revision'] + ) + cluster.nodes[active_node_idx] = Node(*response['redirect']['address'].split(':')) + active_node, response = self.__send_to_cluster(cluster, packet) + else: raise socket.error(response) - responses[active_node] = response + try: + connection.close() + except socket.error: + pass - try: - connection.close() - except socket.error: - pass + return active_node, response + + def __chunk_send(self, items: list) -> dict: + responses = {} + + packet = ZabbixProtocol.create_packet(self.__create_request(items), log, self.compression) + + for cluster in self.clusters: + active_node, response = self.__send_to_cluster(cluster, packet) + responses[active_node] = response return responses diff --git a/zabbix_utils/types.py b/zabbix_utils/types.py index 2a4274f..60dc6a5 100644 --- a/zabbix_utils/types.py +++ b/zabbix_utils/types.py @@ -349,7 +349,7 @@ class Cluster(): """ def __init__(self, addr: list): - self.__nodes = self.__parse_ha_node(addr) + self.nodes = self.__parse_ha_node(addr) def __parse_ha_node(self, node_list: list) -> list: nodes = [] @@ -363,21 +363,11 @@ def __parse_ha_node(self, node_list: list) -> list: return nodes def __str__(self) -> str: - return json.dumps([(node.address, node.port) for node in self.__nodes]) + return json.dumps([(node.address, node.port) for node in self.nodes]) def __repr__(self) -> str: return self.__str__() - @property - def nodes(self) -> list: - """Returns list of Node objects. - - Returns: - list: List of Node objects - """ - - return self.__nodes - class AgentResponse: """Contains response from Zabbix agent/agent2. diff --git a/zabbix_utils/version.py b/zabbix_utils/version.py index 7a3edd3..15c3e95 100644 --- a/zabbix_utils/version.py +++ b/zabbix_utils/version.py @@ -22,7 +22,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. -__version__ = "2.0.1" +__version__ = "2.0.2" __min_supported__ = 5.0 -__max_supported__ = 7.0 +__max_supported__ = 7.2