diff --git a/rsd_lib/resources/v2_1/system/network_interface.py b/rsd_lib/resources/v2_1/system/network_interface.py new file mode 100644 index 0000000..acd78dd --- /dev/null +++ b/rsd_lib/resources/v2_1/system/network_interface.py @@ -0,0 +1,156 @@ +# Copyright 2018 99cloud, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sushy.resources import base + +from rsd_lib import utils as rsd_lib_utils + + +class StatusField(base.CompositeField): + state = base.Field('State') + health = base.Field('Health') + health_rollup = base.Field('HealthRollup') + + +class IPv4AddressesField(base.ListField): + address = base.Field('Address') + subnet_mask = base.Field('SubnetMask') + address_origin = base.Field('AddressOrigin') + gateway = base.Field('Gateway') + + +class IPv6AddressesField(base.ListField): + address = base.Field('Address') + prefix_length = base.Field('PrefixLength') + address_origin = base.Field('AddressOrigin') + address_state = base.Field('AddressState') + + +class IPv6StaticAddressesField(base.ListField): + address = base.Field('Address') + prefix_length = base.Field('PrefixLength') + + +class VLANField(base.CompositeField): + vlan_enable = base.Field('VLANEnable', adapter=bool) + vlan_id = base.Field('VLANId', + adapter=rsd_lib_utils.int_or_none) + + +class NetworkInterface(base.ResourceBase): + + name = base.Field('Name') + """The network interface name""" + + identity = base.Field('Id') + """The network interface identity""" + + description = base.Field('Description') + """The network interface description""" + + status = StatusField('Status') + """The network interface status""" + + interface_enabled = base.Field('InterfaceEnabled', adapter=bool) + """The boolean indicate this network interface is enabled or not""" + + permanent_mac_address = base.Field('PermanentMACAddress') + """The network interface permanent mac address""" + + mac_address = base.Field('MACAddress') + """The network interface mac address""" + + speed_mbps = base.Field('SpeedMbps') + """The network interface speed""" + + auto_neg = base.Field('AutoNeg', adapter=bool) + """Indicates if the speed and duplex is automatically configured + by the NIC + """ + + full_duplex = base.Field('FullDuplex', adapter=bool) + """Indicates if the NIC is in Full Duplex mode or not""" + + mtu_size = base.Field('MTUSize', + adapter=rsd_lib_utils.int_or_none) + """The network interface mtu size""" + + host_name = base.Field('HostName') + """The network interface host name""" + + fqdn = base.Field('FQDN') + """Fully qualified domain name obtained by DNS for this interface""" + + ipv6_default_gateway = base.Field('IPv6DefaultGateway') + """Default gateway address that is currently in use on this interface""" + + max_ipv6_static_addresses = base.Field('MaxIPv6StaticAddresses', + adapter=rsd_lib_utils.int_or_none) + """Indicates the maximum number of Static IPv6 addresses that can be + configured on this interface + """ + + name_servers = base.Field('NameServers', adapter=list) + """The network interface nameserver""" + + ipv4_addresses = IPv4AddressesField('IPv4Addresses') + """The network interface ipv4 address""" + + ipv6_addresses = IPv6AddressesField('IPv6Addresses') + """The network interface ipv6 address""" + + ipv6_static_addresses = IPv6StaticAddressesField('IPv6StaticAddresses') + """The network interface ipv6 static address""" + + vlan = VLANField('VLAN') + """The network interface vlan collection""" + + oem = base.Field('oem') + """The network interface oem field""" + + links = base.Field('links') + """The network interface links field""" + + def __init__(self, connector, identity, redfish_version=None): + """A class representing a Network Interface + + :param connector: A Connector instance + :param identity: The identity of the Network Interface + :param redfish_version: The version of RedFish. Used to construct + the object according to schema of the given version. + """ + super(NetworkInterface, self).__init__(connector, + identity, + redfish_version) + + +class NetworkInterfaceCollection(base.ResourceCollectionBase): + + @property + def _resource_type(self): + return NetworkInterface + + def __init__(self, connector, path, redfish_version=None): + """A class representing a NetworkInterfaceCollection + + :param connector: A Connector instance + :param path: The canonical path to the network interface collection + resource + :param redfish_version: The version of RedFish. Used to construct + the object according to schema of the given version. + """ + super(NetworkInterfaceCollection, self).__init__(connector, + path, + redfish_version) diff --git a/rsd_lib/resources/v2_1/system/system.py b/rsd_lib/resources/v2_1/system/system.py index 4d86381..3f94e44 100644 --- a/rsd_lib/resources/v2_1/system/system.py +++ b/rsd_lib/resources/v2_1/system/system.py @@ -17,6 +17,7 @@ from sushy import exceptions from sushy.resources.system import system from rsd_lib.resources.v2_1.system import memory +from rsd_lib.resources.v2_1.system import network_interface from rsd_lib.resources.v2_1.system import storage_subsystem from rsd_lib import utils @@ -25,6 +26,7 @@ class System(system.System): _memory = None # ref to System memory collection instance _storage_subsystem = None # ref to storage subsystem collection instance + _network_interface = None # ref to network interface collection instance def _get_memory_collection_path(self): """Helper function to find the memory path""" @@ -71,10 +73,36 @@ class System(system.System): redfish_version=self.redfish_version) return self._storage_subsystem + def _get_network_interface_collection_path(self): + """Helper function to find the network interface path""" + network_interface_col = self.json.get('EthernetInterfaces') + if not network_interface_col: + raise exceptions.MissingAttributeError( + attribute='NetworkInterface', + resource=self._path + ) + return utils.get_resource_identity(network_interface_col) + + @property + def network_interface(self): + """Property to provide reference to `NetworkInterface` instance + + It is calculated once the first time it is queried. On refresh, + this property is reset. + """ + if self._network_interface is None: + self._network_interface = network_interface.\ + NetworkInterfaceCollection( + self._conn, self._get_network_interface_collection_path(), + redfish_version=self.redfish_version + ) + return self._network_interface + def refresh(self): super(System, self).refresh() self._memory = None self._storage_subsystem = None + self._network_interface = None class SystemCollection(system.SystemCollection): diff --git a/rsd_lib/tests/unit/json_samples/v2_1/system_network_interface.json b/rsd_lib/tests/unit/json_samples/v2_1/system_network_interface.json new file mode 100644 index 0000000..6b4ebe5 --- /dev/null +++ b/rsd_lib/tests/unit/json_samples/v2_1/system_network_interface.json @@ -0,0 +1,56 @@ +{ + "@odata.context": "/redfish/v1/$metadata#EthernetInterface.EthernetInterface", + "@odata.id": "/redfish/v1/Systems/System1/EthernetInterfaces/LAN1", + "@odata.type": "#EthernetInterface.v1_1_0.EthernetInterface", + "Id": "LAN1", + "Name": "Ethernet Interface", + "Description": "System NIC 1", + "Status": { + "State": "Enabled", + "Health": "OK", + "HealthRollup": "OK" + }, + "InterfaceEnabled": true, + "PermanentMACAddress": "AA:BB:CC:DD:EE:FF", + "MACAddress": "AA:BB:CC:DD:EE:FF", + "SpeedMbps": 100, + "AutoNeg": true, + "FullDuplex": true, + "MTUSize": 1500, + "HostName": "web483", + "FQDN": "web483.redfishspecification.org", + "IPv6DefaultGateway": "fe80::3ed9:2bff:fe34:600", + "MaxIPv6StaticAddresses": null, + "NameServers": [ + "names.redfishspecification.org" + ], + "IPv4Addresses": [ + { + "Address": "192.168.0.10", + "SubnetMask": "255.255.252.0", + "AddressOrigin": "Static", + "Gateway": "192.168.0.1" + } + ], + "IPv6Addresses": [ + { + "Address": "fe80::1ec1:deff:fe6f:1e24", + "PrefixLength": 64, + "AddressOrigin": "Static", + "AddressState": "Preferred" + } + ], + "IPv6StaticAddresses": [], + "VLAN": null, + "Oem": {}, + "Links": { + "Oem": { + "Intel_RackScale": { + "@odata.type": "#Intel.Oem.EthernetInterface", + "NeighborPort": { + "@odata.id": "/redfish/v1/EthernetSwitches/1/Ports/1" + } + } + } + } +} diff --git a/rsd_lib/tests/unit/json_samples/v2_1/system_network_interface_collection.json b/rsd_lib/tests/unit/json_samples/v2_1/system_network_interface_collection.json new file mode 100644 index 0000000..66e8309 --- /dev/null +++ b/rsd_lib/tests/unit/json_samples/v2_1/system_network_interface_collection.json @@ -0,0 +1,12 @@ +{ + "@odata.context": "/redfish/v1/$metadata#Systems/Members/1/EthernetInterfaces/$entity", + "@odata.type": "#EthernetInterfaceCollection.EthernetInterfaceCollection", + "@odata.id": "/redfish/v1/Systems/System1/EthernetInterfaces", + "Name": "Ethernet Interface Collection", + "Members@odata.count": 1, + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/System1/EthernetInterfaces/LAN1" + } + ] +} diff --git a/rsd_lib/tests/unit/resources/v2_1/system/test_network_interface.py b/rsd_lib/tests/unit/resources/v2_1/system/test_network_interface.py new file mode 100644 index 0000000..5dc85d1 --- /dev/null +++ b/rsd_lib/tests/unit/resources/v2_1/system/test_network_interface.py @@ -0,0 +1,130 @@ +# Copyright 2018 99cloud, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import mock +import testtools + +from rsd_lib.resources.v2_1.system import network_interface + + +class NetworkInterfaceTestCase(testtools.TestCase): + + def setUp(self): + super(NetworkInterfaceTestCase, self).setUp() + self.conn = mock.Mock() + with open('rsd_lib/tests/unit/json_samples/v2_1/' + 'system_network_interface.json', 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + + self.network_interface_inst = network_interface.NetworkInterface( + self.conn, '/redfish/v1/Systems/System1/EthernetInterfaces/LAN1', + redfish_version='1.1.0') + + def test__parse_attributes(self): + self.network_interface_inst._parse_attributes() + self.assertEqual('Ethernet Interface', + self.network_interface_inst.name) + self.assertEqual('LAN1', self.network_interface_inst.identity) + self.assertEqual('System NIC 1', + self.network_interface_inst.description) + self.assertEqual('Enabled', self.network_interface_inst.status.state) + self.assertEqual('OK', self.network_interface_inst.status.health) + self.assertEqual('OK', + self.network_interface_inst.status.health_rollup) + self.assertEqual(True, self.network_interface_inst.interface_enabled) + self.assertEqual('AA:BB:CC:DD:EE:FF', + self.network_interface_inst.permanent_mac_address) + self.assertEqual('AA:BB:CC:DD:EE:FF', + self.network_interface_inst.mac_address) + self.assertEqual(100, self.network_interface_inst.speed_mbps) + self.assertEqual(True, self.network_interface_inst.auto_neg) + self.assertEqual(True, self.network_interface_inst.full_duplex) + self.assertEqual(1500, self.network_interface_inst.mtu_size) + self.assertEqual('web483', self.network_interface_inst.host_name) + self.assertEqual('web483.redfishspecification.org', + self.network_interface_inst.fqdn) + self.assertEqual('fe80::3ed9:2bff:fe34:600', + self.network_interface_inst.ipv6_default_gateway) + self.assertEqual(None, + self.network_interface_inst.max_ipv6_static_addresses) + self.assertEqual(['names.redfishspecification.org'], + self.network_interface_inst.name_servers) + self.assertEqual('192.168.0.10', + self.network_interface_inst.ipv4_addresses[0].address) + self.assertEqual('255.255.252.0', + self.network_interface_inst.ipv4_addresses[0]. + subnet_mask) + self.assertEqual('192.168.0.1', + self.network_interface_inst.ipv4_addresses[0].gateway) + self.assertEqual('fe80::1ec1:deff:fe6f:1e24', + self.network_interface_inst.ipv6_addresses[0].address) + self.assertEqual(64, + self.network_interface_inst.ipv6_addresses[0]. + prefix_length) + self.assertEqual('Static', + self.network_interface_inst.ipv6_addresses[0]. + address_origin) + self.assertEqual('Preferred', + self.network_interface_inst.ipv6_addresses[0]. + address_state) + self.assertEqual([], self.network_interface_inst.ipv6_static_addresses) + self.assertEqual(None, self.network_interface_inst.vlan) + + +class NetworkInterfaceCollectionTestCase(testtools.TestCase): + + def setUp(self): + super(NetworkInterfaceCollectionTestCase, self).setUp() + self.conn = mock.Mock() + with open('rsd_lib/tests/unit/json_samples/v2_1/' + 'system_network_interface_collection.json', 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + self.network_interface_col = network_interface.\ + NetworkInterfaceCollection( + self.conn, + '/redfish/v1/Systems/System1/EthernetInterfaces', + redfish_version='1.1.0' + ) + + def test__parse_attributes(self): + self.network_interface_col._parse_attributes() + self.assertEqual('1.1.0', self.network_interface_col.redfish_version) + self.assertEqual( + ('/redfish/v1/Systems/System1/EthernetInterfaces/LAN1',), + self.network_interface_col.members_identities) + + @mock.patch.object(network_interface, 'NetworkInterface', autospec=True) + def test_get_member(self, mock_network_interface): + self.network_interface_col.get_member( + '/redfish/v1/Systems/System1/EthernetInterfaces/LAN1') + mock_network_interface.assert_called_once_with( + self.network_interface_col._conn, + '/redfish/v1/Systems/System1/EthernetInterfaces/LAN1', + redfish_version=self.network_interface_col.redfish_version + ) + + @mock.patch.object(network_interface, 'NetworkInterface', autospec=True) + def test_get_members(self, mock_network_interface): + members = self.network_interface_col.get_members() + calls = [ + mock.call(self.network_interface_col._conn, + '/redfish/v1/Systems/System1/EthernetInterfaces/LAN1', + redfish_version=self.network_interface_col. + redfish_version) + ] + mock_network_interface.assert_has_calls(calls) + self.assertIsInstance(members, list) + self.assertEqual(1, len(members)) diff --git a/rsd_lib/tests/unit/resources/v2_1/system/test_system.py b/rsd_lib/tests/unit/resources/v2_1/system/test_system.py index ff2c6a7..0cfd31c 100644 --- a/rsd_lib/tests/unit/resources/v2_1/system/test_system.py +++ b/rsd_lib/tests/unit/resources/v2_1/system/test_system.py @@ -21,6 +21,7 @@ from sushy import exceptions from sushy.resources.system import system as sushy_system from rsd_lib.resources.v2_1.system import memory +from rsd_lib.resources.v2_1.system import network_interface from rsd_lib.resources.v2_1.system import storage_subsystem from rsd_lib.resources.v2_1.system import system @@ -159,6 +160,65 @@ class SystemTestCase(testtools.TestCase): self.assertIsInstance(self.system_inst.storage_subsystem, storage_subsystem.StorageSubsystemCollection) + def test__get_network_interface_collection_path(self): + self.assertEqual( + '/redfish/v1/Systems/437XR1138R2/EthernetInterfaces', + self.system_inst._get_network_interface_collection_path()) + + def test__get_network_interface_collection_path_missing_systems_attr(self): + self.system_inst._json.pop('EthernetInterfaces') + with self.assertRaisesRegex( + exceptions.MissingAttributeError, 'attribute NetworkInterface'): + self.system_inst._get_network_interface_collection_path() + + def test_network_interface(self): + # check for the underneath variable value + self.assertIsNone(self.system_inst._network_interface) + # | GIVEN | + self.conn.get.return_value.json.reset_mock() + with open('rsd_lib/tests/unit/json_samples/v2_1/' + 'system_network_interface_collection.json', 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + # | WHEN | + actual_network_interface_col = self.system_inst.network_interface + # | THEN | + self.assertIsInstance(actual_network_interface_col, + network_interface.NetworkInterfaceCollection) + self.conn.get.return_value.json.assert_called_once_with() + + # reset mock + self.conn.get.return_value.json.reset_mock() + # | WHEN & THEN | + # tests for same object on invoking subsequently + self.assertIs(actual_network_interface_col, + self.system_inst.network_interface) + self.conn.get.return_value.json.assert_not_called() + + def test_network_interface_on_refresh(self): + # | GIVEN | + with open('rsd_lib/tests/unit/json_samples/v2_1/' + 'system_network_interface_collection.json', 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + # | WHEN & THEN | + self.assertIsInstance(self.system_inst.network_interface, + network_interface.NetworkInterfaceCollection) + + # on refreshing the system instance... + with open('rsd_lib/tests/unit/json_samples/v2_1/system.json', + 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + self.system_inst.refresh() + # | WHEN & THEN | + self.assertIsNone(self.system_inst._network_interface) + + # | GIVEN | + with open('rsd_lib/tests/unit/json_samples/v2_1/' + 'system_network_interface_collection.json', 'r') as f: + self.conn.get.return_value.son.return_value = json.loads(f.read()) + # | WHEN & THEN | + self.assertIsInstance(self.system_inst.network_interface, + network_interface.NetworkInterfaceCollection) + class SystemCollectionTestCase(testtools.TestCase):