diff --git a/rsd_lib/resources/v2_1/ethernet_switch/acl_rule.py b/rsd_lib/resources/v2_1/ethernet_switch/acl_rule.py index bd47feb..3f3d9f4 100644 --- a/rsd_lib/resources/v2_1/ethernet_switch/acl_rule.py +++ b/rsd_lib/resources/v2_1/ethernet_switch/acl_rule.py @@ -13,11 +13,17 @@ # License for the specific language governing permissions and limitations # under the License. +import logging + +from jsonschema import validate from sushy.resources import base from sushy import utils +from rsd_lib.resources.v2_1.ethernet_switch import schemas as acl_rule_schema from rsd_lib import utils as rsd_lib_utils +LOG = logging.getLogger(__name__) + class IPSourceField(base.CompositeField): ipv4_address = base.Field('IPv4Address') @@ -117,3 +123,16 @@ class ACLRuleCollection(base.ResourceCollectionBase): super(ACLRuleCollection, self).__init__(connector, path, redfish_version) + + def add_acl_rule(self, acl_rule_req): + """Add a acl rule + + :param acl_rule: JSON for acl_rule + :returns: The location of the acl rule + """ + target_uri = self._path + validate(acl_rule_req, acl_rule_schema.acl_rule_req_schema) + resp = self._conn.post(target_uri, data=acl_rule_req) + acl_rule_url = resp.headers['Location'] + LOG.info("ACL Rule add at %s", acl_rule_url) + return acl_rule_url[acl_rule_url.find(self._path):] diff --git a/rsd_lib/resources/v2_1/ethernet_switch/schemas.py b/rsd_lib/resources/v2_1/ethernet_switch/schemas.py index 20fb36d..16da8e8 100644 --- a/rsd_lib/resources/v2_1/ethernet_switch/schemas.py +++ b/rsd_lib/resources/v2_1/ethernet_switch/schemas.py @@ -39,3 +39,123 @@ vlan_network_interface_req_schema = { ], 'additionalProperties': False } + +acl_rule_req_schema = { + 'type': 'object', + 'oneOf': [ + { + 'properties': { + 'Action': {'enum': ['Forward']} + }, + 'required': ['ForwardMirrorInterface'] + }, + { + 'properties': { + 'Action': {'enum': ['Mirror']} + }, + 'required': ['ForwardMirrorInterface', + 'MirrorPortRegion', 'MirrorType'] + }, + { + 'properties': { + 'Action': {'enum': ['Permit', 'Deny']} + } + } + ], + 'properties': { + 'RuleId': {'type': 'number'}, + 'Action': { + 'type': 'string', + 'enum': ['Permit', 'Deny', 'Forward', 'Mirror'] + }, + 'ForwardMirrorInterface': { + 'type': 'object', + 'properties': { + '@odata.id': { + 'type': 'string' + } + }, + 'required': ['@odata.id'] + }, + 'MirrorPortRegion': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + '@odata.id': { + 'type': 'string' + } + }, + 'required': ['@odata.id'] + } + }, + 'MirrorType': { + 'type': 'string', + 'enum': ['Egress', 'Ingress', 'Bidirectional', 'Redirect'] + }, + 'Condition': { + 'type': 'object', + 'properties': { + 'IPSource': { + 'type': 'object', + 'properties': { + 'IPv4Addresses': {'type': 'string'}, + 'Mask': {'type': ['string', 'null']} + }, + 'required': ['IPv4Address'] + }, + 'IPDestination': { + 'type': 'object', + 'properties': { + 'IPv4Address': {'type': 'string'}, + 'Mask': {'type': ['string', 'null']} + }, + 'required': ['IPv4Address'] + }, + 'MACSource': { + 'type': 'object', + 'properties': { + 'MACAddress': {'type': 'string'}, + 'Mask': {'type': ['string', 'null']} + }, + 'required': ['MACAddress'] + }, + 'MACDestination': { + 'type': 'object', + 'properties': { + 'MACAddress': {'type': 'string'}, + 'Mask': {'type': ['string', 'null']} + }, + 'required': ['MACAddress'] + }, + 'VLANId': { + 'type': 'object', + 'properties': { + 'Id': {'type': 'number'}, + 'Mask': {'type': ['number', 'null']} + }, + 'required': ['Id'] + }, + 'L4SourcePort': { + 'type': 'object', + 'properties': { + 'Port': {'type': 'number'}, + 'Mask': {'type': ['number', 'null']} + }, + 'required': ['Port'] + }, + 'L4DestinationPort': { + 'type': 'object', + 'properties': { + 'Port': {'type': 'number'}, + 'Mask': {'type': ['number', 'null']} + }, + 'required': ['Port'] + }, + 'L4Protocol': {'type': ['number', 'null']} + } + }, + }, + 'required': ['Action', 'Condition'], + 'additionalProperties': False +} diff --git a/rsd_lib/tests/unit/resources/v2_1/ethernet_switch/test_acl_rule.py b/rsd_lib/tests/unit/resources/v2_1/ethernet_switch/test_acl_rule.py index 78a6bdd..eb5438a 100644 --- a/rsd_lib/tests/unit/resources/v2_1/ethernet_switch/test_acl_rule.py +++ b/rsd_lib/tests/unit/resources/v2_1/ethernet_switch/test_acl_rule.py @@ -14,11 +14,13 @@ # under the License. import json +import jsonschema import mock import testtools from rsd_lib.resources.v2_1.ethernet_switch import acl_rule +from rsd_lib.tests.unit.fakes import request_fakes class ACLRuleTestCase(testtools.TestCase): @@ -79,6 +81,12 @@ class ACLRuleCollectionTestCase(testtools.TestCase): with open('rsd_lib/tests/unit/json_samples/v2_1/' 'acl_rule_collection.json', 'r') as f: self.conn.get.return_value.json.return_value = json.loads(f.read()) + self.conn.post.return_value = \ + request_fakes.fake_request_post( + None, + headers={"Location": "https://localhost:8443/redfish/v1/" + "EthernetSwitches/Switch1/ACLs/ACL1/" + "Rules/Rule1"}) self.acl_rule_col = acl_rule.ACLRuleCollection( self.conn, @@ -99,16 +107,151 @@ class ACLRuleCollectionTestCase(testtools.TestCase): def test_get_member(self, mock_acl_rule): self.acl_rule_col.get_member( '/redfish/v1/EthernetSwitches/Switch1/ACLs/ACL1/Rules/Rule1') - mock_acl_rule.assert_called_once_with( self.acl_rule_col._conn, '/redfish/v1/EthernetSwitches/Switch1/ACLs/ACL1/Rules/Rule1', - redfish_version=self.acl_rule_col.redfish_version - ) + redfish_version=self.acl_rule_col.redfish_version) @mock.patch.object(acl_rule, 'ACLRule', autospec=True) def test_get_members(self, mock_acl_rule): members = self.acl_rule_col.get_members() + calls = [ + mock.call(self.acl_rule_col._conn, + '/redfish/v1/EthernetSwitches/Switch1/ACLs/ACL1/' + 'Rules/Rule1', + redfish_version=self.acl_rule_col.redfish_version) + ] + mock_acl_rule.assert_has_calls(calls) self.assertEqual(mock_acl_rule.call_count, 1) self.assertIsInstance(members, list) self.assertEqual(1, len(members)) + + def test_add_acl_rule_reqs(self): + reqs = { + 'RuleId': 1, + 'Action': 'Mirror', + 'ForwardMirrorInterface': { + '@odata.id': '/redfish/v1/EthernetSwitches/Switch1/Ports/Port9' + }, + 'MirrorPortRegion': [ + { + '@odata.id': '/redfish/v1/EthernetSwitches/Switch1/Ports/' + 'Port1' + } + ], + 'MirrorType': 'Bidirectional', + 'Condition': { + 'IPSource': { + 'IPv4Address': '192.168.8.0', + 'Mask': '0.0.0.255' + }, + 'IPDestination': { + 'IPv4Address': '192.168.1.0' + }, + 'MACSource': { + 'MACAddress': '00:11:22:33:44:55', + }, + 'MACDestination': { + 'MACAddress': '55:44:33:22:11:00' + }, + 'VLANid': { + 'Id': 1088, + 'Mask': 4095 + }, + 'L4SourcePort': { + 'Port': 22, + 'Mask': 255 + }, + 'L4DestinationPort': { + 'Port': 22, + 'Mask': 255 + }, + 'L4Protocol': 1 + } + } + result = self.acl_rule_col.add_acl_rule(reqs) + self.acl_rule_col._conn.post.assert_called_once_with( + '/redfish/v1/EthernetSwitches/Switch1/ACLs/ACL1/Rules', + data=reqs) + self.assertEqual(result, + '/redfish/v1/EthernetSwitches/Switch1/ACLs/ACL1/' + 'Rules/Rule1') + + def test_add_acl_rule_invalid_reqs(self): + reqs = { + 'RuleId': 1, + 'Action': 'Mirror', + 'ForwardMirrorInterface': { + '@odata.id': '/redfish/v1/EthernetSwitches/Switch1/Ports/Port9' + }, + 'MirrorPortRegion': [ + { + '@odata.id': '/redfish/v1/EthernetSwitches/Switch1/Ports/' + 'Port1' + } + ], + 'MirrorType': 'Bidirectional', + 'Condition': { + 'IPSource': { + 'IPv4Address': '192.168.8.0', + 'Mask': '0.0.0.255' + }, + 'IPDestination': { + 'IPv4Address': '192.168.1.0' + }, + 'MACSource': { + 'MACAddress': '00:11:22:33:44:55', + }, + 'MACDestination': { + 'MACAddress': '55:44:33:22:11:00' + }, + 'VLANid': { + 'Id': 1088, + 'Mask': 4095 + }, + 'L4SourcePort': { + 'Port': 22, + 'Mask': 255 + }, + 'L4DestinationPort': { + 'Port': 22, + 'Mask': 255 + }, + 'L4Protocol': 1 + } + } + + # Missing field + acl_rule_req = reqs.copy() + acl_rule_req.pop('Action') + self.assertRaises(jsonschema.exceptions.ValidationError, + self.acl_rule_col.add_acl_rule, + acl_rule_req) + + # Wrong format + acl_rule_req = reqs.copy() + acl_rule_req.update({'RuleId': 'WrongFormat'}) + self.assertRaises(jsonschema.exceptions.ValidationError, + self.acl_rule_col.add_acl_rule, + acl_rule_req) + + # Wrong additional fields + acl_rule_req = reqs.copy() + acl_rule_req['Additional'] = 'AdditionalField' + self.assertRaises(jsonschema.exceptions.ValidationError, + self.acl_rule_col.add_acl_rule, + acl_rule_req) + + # Wrong enum + acl_rule_req = reqs.copy() + acl_rule_req['MirrorType'] = 'WrongEnum' + self.assertRaises(jsonschema.exceptions.ValidationError, + self.acl_rule_col.add_acl_rule, + acl_rule_req) + + # Wrong dependency + acl_rule_req = reqs.copy() + acl_rule_req.pop('ForwardMirrorInterface') + self.assertRaises(jsonschema.exceptions.ValidationError, + self.acl_rule_col.add_acl_rule, + acl_rule_req)