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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy