diff --git a/ChangeLog.md b/ChangeLog.md index 3b03662..dad6d51 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,8 @@ # PyPowerStore Change Log +## Version 3.4.0.0 - released on 29/11/24 +- Added the Support for SNMP server object. + ## Version 3.3.0.0 - released on 31/05/24 - Added the Support for ACL in SMB Share object. diff --git a/ProgrammersGuideExamples/snmp_server.py b/ProgrammersGuideExamples/snmp_server.py new file mode 100644 index 0000000..25508ac --- /dev/null +++ b/ProgrammersGuideExamples/snmp_server.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2024, Dell Technologies + +""" SNMP server Operations""" +from PyPowerStore import powerstore_conn + +CONN = powerstore_conn.PowerStoreConn(username="", + password="", + server_ip="", + verify=False, + application_type="", + timeout=180.0) + +print(CONN) + +MODIFY_PARAMS = { + "ip_address": "10.**.**.**", + "port": 162, + "trap_community": "community", + "alert_severity": "Info" +} + +CREATE_PARAMS = { + "ip_address": "10.**.**.**", + "port": 162, + "version": "V2c", + "alert_severity": "Info", + "trap_community": "public" +} + +# create SNMP server +SNMP_SERVER = CONN.snmp_server.create_snmp_server(CREATE_PARAMS) +print(SNMP_SERVER) + +# Get SNMP server list +SNMP_SERVERS = CONN.snmp_server.get_snmp_server_list(all_pages=True) +print(SNMP_SERVERS) + +# get SNMP server details by ID +SNMP_SERVER = CONN.snmp_server.get_snmp_server_details(SNMP_SERVER['id']) +print(SNMP_SERVER) + +# modify SNMP server +MODIFY_SNMP_SERVER = CONN.snmp_server.modify_snmp_server(SNMP_SERVERS[0]['id'], + MODIFY_PARAMS) +print(MODIFY_SNMP_SERVER) + +# delete SNMP server +DELETE_SNMP_SERVER = CONN.snmp_server.delete_snmp_server(SNMP_SERVERS[0]['id']) +print(DELETE_SNMP_SERVER) diff --git a/PyPowerStore/objects/snmp_server.py b/PyPowerStore/objects/snmp_server.py new file mode 100644 index 0000000..6a54cb9 --- /dev/null +++ b/PyPowerStore/objects/snmp_server.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2024, Dell Technologies + +"""Collection of SNMP related functions for PowerStore""" + +from PyPowerStore.client import Client +from PyPowerStore.utils import constants, helpers + +# TODO: kept LOG as global for now will improve it to avoid overriding +LOG = helpers.get_logger(__name__) + +SELECT_ALL_SNMP = {"select": "id, ip_address, port, version, trap_community," + "alert_severity, user_name, auth_protocol, privacy_protocol"} + +# SNMP server endpoints +GET_SNMP_LIST_URL = 'https://{0}/api/rest/snmp_server' +GET_SNMP_DETAILS_URL = 'https://{0}/api/rest/snmp_server/{1}' +GET_SNMP_DETAILS_BY_NAS_SERVER_URL = GET_SNMP_LIST_URL +MODIFY_SNMP_URL = GET_SNMP_DETAILS_URL +CREATE_SNMP_URL = GET_SNMP_LIST_URL +DELETE_SNMP_URL = GET_SNMP_DETAILS_URL + +class SNMPServer: + """Provisioning related functionality for PowerStore.""" + def __init__(self, provisioning, enable_log=False): + """ Initializes ProtectionFunctions Class. + + :param provisioning: Provisioning class object + :type provisioning: Provisioning + :param enable_log: (optional) Whether to enable log or not + :type enable_log: bool + """ + global LOG + self.provisioning = provisioning + self.server_ip = provisioning.server_ip + self.snmp_server_client = provisioning.client + LOG = helpers.get_logger(__name__, enable_log=enable_log) + + # SNMP server methods begin + def get_snmp_server_list(self, filter_dict=None, all_pages=False): + """Get a list of SNMP servers. + + :param filter_dict: (optional) Filter detail + :type filter_dict: dict + :param all_pages: (optional) Indicates whether to return all element + or not + :type all_pages: bool + :returns: SNMP servers + :rtype: list of dict + """ + LOG.info("Getting SNMP servers with filter: '%s' and all_pages: %s" + % (filter_dict, all_pages)) + querystring = helpers.prepare_querystring(SELECT_ALL_SNMP, filter_dict) + LOG.info("Querystring: '%s'" % querystring) + return self.snmp_server_client.request(constants.GET, + GET_SNMP_LIST_URL.format + (self.server_ip), payload=None, + querystring=querystring, + all_pages=all_pages) + + def get_snmp_server_details(self, snmp_server_id): + """Details of a SNMP server. + + :param snmp_server_id: The SNMP server ID + :type snmp_server_id: str + :return:SNMP server details + :rtype: dict + """ + querystring = SELECT_ALL_SNMP + + LOG.info("Getting SNMP server details by ID: '%s'" % snmp_server_id) + return self.snmp_server_client.request( + constants.GET, + GET_SNMP_DETAILS_URL.format(self.server_ip, + snmp_server_id), + payload=None, + querystring=querystring) + + def create_snmp_server(self, payload): + """Create an SNMP server. + + :param payload: The payload to create the SNMP server + :type payload: dict + :return: SNMP server ID on success else raise exception + :rtype: dict + """ + LOG.info("Creating SNMP server") + return self.snmp_server_client.request( + constants.POST, + CREATE_SNMP_URL.format(self.server_ip), + payload=payload) + + def modify_snmp_server(self, snmp_server_id, modify_parameters): + """Modify SNMP server attributes. + + :param snmp_server_id: The ID of the SNMP server + :type snmp_server_id: str + :param modify_parameters: Attributes to be modified + :type modify_parameters: dict + :return: None if success else raise exception + :rtype: None + """ + LOG.info("Modifying SNMP server: '%s'" % snmp_server_id) + if modify_parameters: + payload = dict() + for key, value in modify_parameters.items(): + if value is not None: + payload[key] = value + + if payload: + return self.snmp_server_client.request( + constants.PATCH, + MODIFY_SNMP_URL.format( + self.server_ip, snmp_server_id), + payload=payload) + + raise ValueError("Nothing to modify") + + def delete_snmp_server(self, snmp_server_id): + """Delete an SNMP server. + + :param snmp_server_id: The ID of the SNMP server to delete + :type snmp_server_id: str + :return: None on success else raise exception + :rtype: None + """ + LOG.info("Deleting SNMP server: '%s'" % snmp_server_id) + return self.snmp_server_client.request( + constants.DELETE, + DELETE_SNMP_URL.format(self.server_ip, snmp_server_id)) + + # SNMP server methods end diff --git a/PyPowerStore/powerstore_conn.py b/PyPowerStore/powerstore_conn.py index d752348..79d01af 100644 --- a/PyPowerStore/powerstore_conn.py +++ b/PyPowerStore/powerstore_conn.py @@ -11,6 +11,7 @@ from PyPowerStore.objects.nfs_server import NFSServer from PyPowerStore.objects.file_dns import FileDNS from PyPowerStore.objects.file_nis import FileNIS +from PyPowerStore.objects.snmp_server import SNMPServer class PowerStoreConn(): """Class for establishing connection with PowerStore""" @@ -55,3 +56,5 @@ def __init__(self, username, password, server_ip, verify=False, enable_log=enable_log) self.file_nis = FileNIS(self.provisioning, enable_log=enable_log) + self.snmp_server = SNMPServer(self.provisioning, + enable_log=enable_log) diff --git a/PyPowerStore/tests/unit_tests/base_test.py b/PyPowerStore/tests/unit_tests/base_test.py index ae89020..7fa2da5 100644 --- a/PyPowerStore/tests/unit_tests/base_test.py +++ b/PyPowerStore/tests/unit_tests/base_test.py @@ -27,6 +27,7 @@ from PyPowerStore.tests.unit_tests.data.nfs_server_data import NFSServerData from PyPowerStore.tests.unit_tests.data.file_dns_data import FileDNSData from PyPowerStore.tests.unit_tests.data.file_nis_data import FileNISData +from PyPowerStore.tests.unit_tests.data.snmp_server_data import SNMPServerData from unittest import mock class TestBase(TestCase): @@ -57,6 +58,7 @@ def setUp(self): self.nfs_server_data = NFSServerData() self.file_dns_data = FileDNSData() self.file_nis_data = FileNISData() + self.snmp_server_data = SNMPServerData() self.conf = PowerStoreConfig() self.mock_client = mock.patch('PyPowerStore.provisioning.Client', new=MockClient) @@ -74,3 +76,4 @@ def setUp(self): self.file_nis = self.conn.file_nis self.smb_server = self.conn.smb_server self.nfs_server = self.conn.nfs_server + self.snmp_server = self.conn.snmp_server diff --git a/PyPowerStore/tests/unit_tests/data/snmp_server_data.py b/PyPowerStore/tests/unit_tests/data/snmp_server_data.py new file mode 100644 index 0000000..e7af914 --- /dev/null +++ b/PyPowerStore/tests/unit_tests/data/snmp_server_data.py @@ -0,0 +1,75 @@ +class SNMPServerData(): + + snmp_server_id = "snmp_server_id_1" + + snmp_server_list = [ + { + "id": "2bf5709d-0466-437a-a28f-9d31f2fdfcc5", + "ip_address": "127.0.0.1", + "port": 162, + "version": "V2c", + "trap_community": "commnity", + "alert_severity": "Info", + "user_name": None, + "auth_protocol": None, + "privacy_protocol": None + }, + { + "id": "54261519-c5c2-446a-ad76-5f4ca63581df", + "ip_address": "100.96.32.85", + "port": 162, + "version": "V2c", + "trap_community": "public", + "alert_severity": "Info", + "user_name": None, + "auth_protocol": None, + "privacy_protocol": None + }, + { + "id": "789f4c09-9e15-4b44-a9f3-baf716172140", + "ip_address": "10.250.230.45", + "port": 162, + "version": "V3", + "trap_community": None, + "alert_severity": "Info", + "user_name": "test", + "auth_protocol": "None", + "privacy_protocol": "None" + }] + + snmp_server_detail = { + "id": "789f4c09-9e15-4b44-a9f3-baf716172140", + "ip_address": "10.250.230.45", + "port": 162, + "version": "V3", + "trap_community": None, + "alert_severity": "Info", + "user_name": "test", + "auth_protocol": "None", + "privacy_protocol": "None" + } + + snmp_server_valid_param_list = [ + "ip_address", "port", "version", "alert_severity", "trap_community"] + + snmp_server_id_not_exist = "5f4a3017-0bad-899e-e1eb-c6f547282e66" + snmp_server_error = { + 400: {'messages': [{'arguments': ['Object instance has properties ' + 'which are not allowed by the ' + 'schema.'], + 'code': '0xE04040030001', + 'message_l10n': 'Validation failed: Object ' + 'instance has properties which ' + 'are not allowed by the schema.', + 'severity': 'Error'}]}, + 422: {"messages": [{ + "code": "0xE0F0101D0024", + "severity": "Error", + "message_l10n": "Server Record Not Found, id: c5fdeb93-42ed-4ec9-988e-daec2974f2fk", + "arguments": [ + "c5fdeb93-42ed-4ec9-988e-daec2974f2fk" + ] + } + ] +} + } diff --git a/PyPowerStore/tests/unit_tests/entity/snmp_server.py b/PyPowerStore/tests/unit_tests/entity/snmp_server.py new file mode 100644 index 0000000..c9d577f --- /dev/null +++ b/PyPowerStore/tests/unit_tests/entity/snmp_server.py @@ -0,0 +1,53 @@ +from PyPowerStore.tests.unit_tests.entity.base_abstract import Entity +from PyPowerStore.tests.unit_tests.data.snmp_server_data import SNMPServerData +from PyPowerStore.utils import constants +from PyPowerStore.objects import snmp_server + +class SNMPServerResponse(Entity): + + def __init__(self, method, url, **kwargs): + self.method = method + self.url = url + self.kwargs = kwargs + self.snmp_server_data = SNMPServerData() + self.status_code = 200 + + def get_api_name(self): + if self.method == 'GET': + if self.url.endswith('/snmp_server'): + return self.get_snmp_server_list + else: + return self.get_snmp_server_details + elif self.method == 'PATCH': + return self.modify_snmp_server + elif self.method == 'POST': + return self.create_snmp_server + elif self.method == 'DELETE': + return self.delete_snmp_server + + def execute_api(self, api_name): + status_code, response = api_name() + return status_code, response + + def get_snmp_server_list(self): + return self.status_code, self.snmp_server_data.snmp_server_list + + def get_snmp_server_details(self): + if self.url.endswith('/snmp_server/{0}'.format( + self.snmp_server_data.snmp_server_id_not_exist)): + return 422, self.snmp_server_data.snmp_server_error[422] + return 200, self.snmp_server_data.snmp_server_detail + + def modify_snmp_server(self): + data = self.kwargs.get('data', {}) + param = list(data.keys()) + if set(param) - set(self.snmp_server_data.snmp_server_valid_param_list): + # invalid param given + return 400, self.snmp_server_data.snmp_server_error[400] + return 204, None + + def create_snmp_server(self): + return 201, self.snmp_server_data.snmp_server_id + + def delete_snmp_server(self): + return 204, None diff --git a/PyPowerStore/tests/unit_tests/myrequests.py b/PyPowerStore/tests/unit_tests/myrequests.py index 6e1b4fa..4582aae 100644 --- a/PyPowerStore/tests/unit_tests/myrequests.py +++ b/PyPowerStore/tests/unit_tests/myrequests.py @@ -49,6 +49,7 @@ from PyPowerStore.tests.unit_tests.entity.nfs_server import NFSServerResponse from PyPowerStore.tests.unit_tests.entity.file_dns import FileDNSResponse from PyPowerStore.tests.unit_tests.entity.file_nis import FileNISResponse +from PyPowerStore.tests.unit_tests.entity.snmp_server import SNMPServerResponse import json # map the entity class name with the url resource name @@ -103,7 +104,8 @@ 'smb_server': SMBServerResponse, 'nfs_server': NFSServerResponse, 'file_dns': FileDNSResponse, - 'file_nis': FileNISResponse + 'file_nis': FileNISResponse, + 'snmp_server': SNMPServerResponse } diff --git a/PyPowerStore/tests/unit_tests/test_snmp_server.py b/PyPowerStore/tests/unit_tests/test_snmp_server.py new file mode 100644 index 0000000..6cb5717 --- /dev/null +++ b/PyPowerStore/tests/unit_tests/test_snmp_server.py @@ -0,0 +1,80 @@ +from PyPowerStore.utils import constants +from PyPowerStore.tests.unit_tests.base_test import TestBase +from PyPowerStore.utils.exception import PowerStoreException +from PyPowerStore.objects import snmp_server +import copy +from unittest import mock + + +class TestSNMPServer(TestBase): + + def test_get_snmp_servers(self): + snmp_server_list = self.snmp_server.get_snmp_server_list() + self.assertListEqual(snmp_server_list, self.snmp_server_data.snmp_server_list) + + def test_get_snmp_server_with_filter(self): + querystring = {"version": "V2c"} + querystring.update(snmp_server.SELECT_ALL_SNMP) + with mock.patch.object(self.snmp_server.snmp_server_client, + 'request') as mock_request: + self.snmp_server.get_snmp_server_list(filter_dict=querystring, + all_pages=True) + mock_request.assert_called_with( + constants.GET, + snmp_server.GET_SNMP_LIST_URL.format( + self.provisioning.server_ip), + all_pages=True, + payload=None, + querystring=querystring) + + def test_get_snmp_server_details(self): + snmp_server_detail = self.snmp_server.get_snmp_server_details( + self.snmp_server_data.snmp_server_id) + self.assertEqual(snmp_server_detail, self.snmp_server_data.snmp_server_detail) + + def test_get_invalid_snmp_server_details(self): + self.assertRaisesRegex( + PowerStoreException, + "Server Record Not Found", + self.snmp_server.get_snmp_server_details, + self.snmp_server_data.snmp_server_id_not_exist) + + def test_modify_snmp_server(self): + param = { + "ip_address": "127.0.0.8", + "port": 162, + "version": "V2c", + "alert_severity": "Info", + "trap_community": "public" + } + resp = self.snmp_server.modify_snmp_server(self.snmp_server_data.snmp_server_id, param) + self.assertIsNone(resp) + + def test_modify_snmp_server_with_invalid_param(self): + invalid_param = {'invalid_key': 'invalid_value'} + self.assertRaisesRegex( + PowerStoreException, + "HTTP code: 400, Bad Request", + self.snmp_server.modify_snmp_server, + self.snmp_server_data.snmp_server_id, + invalid_param) + + def test_modify_snmp_server_with_empty_param(self): + self.assertRaises( + ValueError, self.snmp_server.modify_snmp_server, + self.snmp_server_data.snmp_server_id, {}) + + def test_create_snmp_server(self): + payload = { + "ip_address": "127.0.0.8", + "port": 162, + "version": "V2c", + "alert_severity": "Info", + "trap_community": "public" + } + snmp_server_id = self.snmp_server.create_snmp_server(payload) + self.assertEqual(snmp_server_id, self.snmp_server_data.snmp_server_id) + + def test_delete_snmp_server(self): + resp = self.snmp_server.delete_snmp_server(self.snmp_server_data.snmp_server_id) + self.assertIsNone(resp) diff --git a/README.md b/README.md index 575b5bf..a669351 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ The library docs are available under 'docs' folder. This library uses python's "requests" library. -PyPowerStore officially supports Python 3.9, 3.10 and 3.11. +PyPowerStore officially supports Python 3.10, 3.11 and 3.12. ## Support diff --git a/setup.py b/setup.py index 289e2e3..d67b586 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup(name='PyPowerStore', - version='3.3.0.0', + version='3.4.0.0', description='Python Library for Dell PowerStore', author='Ansible Team at Dell', author_email='ansible.team@dell.com',