Commit 9fb170cf authored by Waleed Akbar's avatar Waleed Akbar
Browse files

Refactor pluggable service and tests for improved readability and consistency

parent f7da5a8d
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -18,8 +18,8 @@ cd $PROJECTDIR/src
RCFILE=$PROJECTDIR/coverage/.coveragerc

# to run integration test: -m integration
# python3 -m pytest --log-level=info --log-cli-level=info --verbose -m "not integration" \
#     pluggables/tests/test_pluggables_with_SBI.py
python3 -m pytest --log-level=info --log-cli-level=info --verbose -m "not integration" \
    pluggables/tests/test_pluggables_with_SBI.py
python3 -m pytest --log-level=info --log-cli-level=info --verbose -m "not integration"\
    pluggables/tests/test_pluggables.py

+1 −1
Original line number Diff line number Diff line
@@ -34,7 +34,7 @@ paramiko==2.11.*
pyang==2.6.*
git+https://github.com/robshakir/pyangbind.git
python-json-logger==2.0.2
#pytz==2021.3
pytz==2021.3
#redis==4.1.2
requests==2.27.1
requests-mock==1.9.3
+46 −31
Original line number Diff line number Diff line
@@ -13,18 +13,19 @@
# limitations under the License.

import json, logging, grpc
from .config_translator                       import translate_pluggable_config_to_netconf, create_config_rule_from_dict
from common.method_wrappers.Decorator         import MetricsPool, safe_and_metered_rpc_method
from common.method_wrappers.ServiceExceptions import (
        NotFoundException, AlreadyExistsException, InvalidArgumentException, OperationFailedException)
from common.proto.context_pb2                 import Empty, Device, DeviceId
from common.proto.pluggables_pb2_grpc import PluggablesServiceServicer
from common.proto.pluggables_pb2              import (
    Pluggable, CreatePluggableRequest, ListPluggablesRequest, ListPluggablesResponse, 
        Pluggable, PluggableConfig, CreatePluggableRequest, ListPluggablesRequest, ListPluggablesResponse,
        GetPluggableRequest, DeletePluggableRequest, ConfigurePluggableRequest) 
from common.method_wrappers.ServiceExceptions import (
    NotFoundException, AlreadyExistsException, InvalidArgumentException, OperationFailedException)
from common.proto.pluggables_pb2_grpc         import PluggablesServiceServicer
from common.tools.object_factory.ConfigRule   import json_config_rule_set
from context.client.ContextClient             import ContextClient
from device.client.DeviceClient               import DeviceClient
from .config_translator import translate_pluggable_config_to_netconf, create_config_rule_from_dict


LOGGER = logging.getLogger(__name__)
METRICS_POOL = MetricsPool('Pluggables', 'ServicegRPC')
@@ -56,9 +57,9 @@ class PluggablesServiceServicerImpl(PluggablesServiceServicer):
        
        # Translate pluggable config to NETCONF format
        component_name = f"channel-{pluggable_index}"
        netconf_config = translate_pluggable_config_to_netconf(pluggable_config, component_name=component_name)
        config = translate_pluggable_config_to_netconf(pluggable_config, component_name=component_name)
        
        LOGGER.info(f"Translated pluggable config to NETCONF format: {netconf_config}")
        LOGGER.info(f"Translated pluggable config to NETCONF format: {config}")
        
        # Step 2: Create configuration rule with generic pluggable template
        # Use template index 1 for standard pluggable configuration
@@ -66,11 +67,12 @@ class PluggablesServiceServicerImpl(PluggablesServiceServicer):
        resource_key   = f"/pluggable/{template_index}/config"
        
        # Create config rule dict and convert to protobuf
        config_json = json.dumps(netconf_config)
        config_json      = json.dumps(config)
        config_rule_dict = json_config_rule_set(resource_key, config_json)
        config_rule      = create_config_rule_from_dict(config_rule_dict)
        
        # Step 3: Create a minimal Device object with only the DSCM config rule
        # TODO: In future, if device config merging is needed, fetch existing device config first
        config_device = Device()
        config_device.device_id.device_uuid.uuid = device_uuid  # type: ignore
        config_device.device_config.config_rules.append(config_rule)  # type: ignore
@@ -195,25 +197,38 @@ class PluggablesServiceServicerImpl(PluggablesServiceServicer):
            LOGGER.info(f'No matching pluggable found: device={device_uuid}, index={pluggable_index}')
            raise NotFoundException('Pluggable', pluggable_key)
        
        # Remove pluggable config from device
        # TODO: Verify deletion works with actual hub and leaf devices
        # 
        # Remove pluggable config from device before deleting from memory
        # Use empty config to signal deletion to the device driver
        try:
            pluggable = self.pluggables[pluggable_key]
            # Create empty config to trigger deletion
            from common.proto.pluggables_pb2 import PluggableConfig
            empty_config = PluggableConfig()
            empty_config.id.device.device_uuid.uuid = device_uuid
            empty_config.id.pluggable_index = pluggable_index
            del_config = PluggableConfig()
            del_config.id.device.device_uuid.uuid = device_uuid
            del_config.id.pluggable_index         = pluggable_index
            del_config.operational_mode           = 0            # Operational mode
            del_config.channel_name               = "channel-1"  # Channel name for component

            group_1 = del_config.dsc_groups.add()
            group_1.id.pluggable.device.device_uuid.uuid = device_uuid
            group_1.id.pluggable.pluggable_index         = pluggable_index
            group_1.id.group_index                       = 0

            subcarrier_1 = group_1.subcarriers.add()
            subcarrier_1.id.group.pluggable.device.device_uuid.uuid = device_uuid
            subcarrier_1.id.group.pluggable.pluggable_index         = pluggable_index
            subcarrier_1.id.group.group_index                       = 0
            subcarrier_1.id.subcarrier_index                        = 0
            subcarrier_1.active                                     = False

            # ADD MORE SUBCARRIERS IF NEEDED
            
            LOGGER.info(f"Removing configuration from device {device_uuid}")
            self._push_config_to_device(device_uuid, pluggable_index, empty_config)
        except Exception as e:
            LOGGER.error(f"Failed to remove config from device: {e}")
            self._push_config_to_device(device_uuid, pluggable_index, del_config)
        except (grpc.RpcError, InvalidArgumentException, OperationFailedException) as e:
            LOGGER.warning(f"Failed to remove config from device (continuing with memory deletion): {e}")
            # Continue with deletion from memory even if device config removal fails
        
        # Always delete from memory (even if device operation failed)
        del self.pluggables[pluggable_key]
        LOGGER.info(f"Deleted pluggable: device={device_uuid}, index={pluggable_index}")
        LOGGER.info(f"Deleted pluggable from memory: device={device_uuid}, index={pluggable_index}")
        
        return Empty()
    
+4 −5
Original line number Diff line number Diff line
@@ -33,18 +33,17 @@ def translate_pluggable_config_to_netconf(
    component_name: str = "channel-1"           # Fallback if channel_name not provided (channel-1 for HUB and channel-1/3/5 for LEAF)
) -> Dict[str, Any]:
    """
    Translate PluggableConfig protobuf message to the format expected by NetConfDriver.
    Translate PluggableConfig protobuf message to the format expected by OpenConfig.
    Args:
        pluggable_config: PluggableConfig message containing DSC groups and subcarriers
        component_name: Fallback name if channel_name is not specified in config (default: "channel-1")
    Returns:
        Dictionary in the format expected by NetConfDriver templates:
        Dictionary in the format expected by OpenConfig templates:
    """
      
    if not pluggable_config or not pluggable_config.dsc_groups:
        LOGGER.warning("Empty pluggable config provided")
        return {
            "name": channel_name,
            "operation": "delete"
        }
    if hasattr(pluggable_config, 'channel_name') and pluggable_config.channel_name:
@@ -54,11 +53,11 @@ def translate_pluggable_config_to_netconf(
        channel_name = component_name
        LOGGER.debug(f"Using fallback component_name: {channel_name}")
    
    if not hasattr(pluggable_config, 'center_frequency_mhz') or pluggable_config.center_frequency_mhz <= 0:
    if not hasattr(pluggable_config, 'center_frequency_mhz'):
        raise ValueError("center_frequency_mhz is required and must be greater than 0 in PluggableConfig")
    center_frequency_mhz = int(pluggable_config.center_frequency_mhz)
    
    if not hasattr(pluggable_config, 'operational_mode') or pluggable_config.operational_mode <= 0:
    if not hasattr(pluggable_config, 'operational_mode'):
        raise ValueError("operational_mode is required and must be greater than 0 in PluggableConfig")
    operational_mode = pluggable_config.operational_mode
    
+23 −23
Original line number Diff line number Diff line
@@ -83,14 +83,14 @@ def test_create_pluggable_hub_with_config(pluggables_client: PluggablesClient):
    _pluggable = pluggables_client.CreatePluggable(_request)
    
    assert isinstance(_pluggable, Pluggable)
    assert _pluggable.id.device.device_uuid.uuid == DEVICE_HUB_UUID
    assert _pluggable.id.pluggable_index == 2
    assert len(_pluggable.config.dsc_groups) == 1  # Should be 1, not 2 (check testmessages.py)
    assert _pluggable.id.device.device_uuid.uuid == DEVICE_HUB_UUID                         # pyright: ignore[reportAttributeAccessIssue]
    assert _pluggable.id.pluggable_index == 2                                               # pyright: ignore[reportAttributeAccessIssue]
    assert len(_pluggable.config.dsc_groups) == 1  # Should be 1, not 2 (check testmessages.py)  # pyright: ignore[reportAttributeAccessIssue]
    
    # Verify DSC group configuration
    dsc_group = _pluggable.config.dsc_groups[0]
    assert dsc_group.group_size == 4  # From testmessages.py
    assert len(dsc_group.subcarriers) == 2
    dsc_group = _pluggable.config.dsc_groups[0]                                             # pyright: ignore[reportAttributeAccessIssue]
    assert dsc_group.group_size == 4  # From testmessages.py                                # pyright: ignore[reportAttributeAccessIssue]
    assert len(dsc_group.subcarriers) == 2                                                  # pyright: ignore[reportAttributeAccessIssue]
    
    LOGGER.info(f'Created Pluggable on Hub with {len(dsc_group.subcarriers)} subcarriers')

@@ -112,9 +112,9 @@ def test_create_pluggable_leaf_with_config(pluggables_client: PluggablesClient):
    _pluggable = pluggables_client.CreatePluggable(_request)
    
    assert isinstance(_pluggable, Pluggable)
    assert _pluggable.id.device.device_uuid.uuid == DEVICE_LEAF_UUID
    assert _pluggable.id.pluggable_index == 1  # Should be 1, not 0
    assert len(_pluggable.config.dsc_groups) == 1
    assert _pluggable.id.device.device_uuid.uuid == DEVICE_LEAF_UUID                        # pyright: ignore[reportAttributeAccessIssue]
    assert _pluggable.id.pluggable_index == 1  # Should be 1, not 0                         # pyright: ignore[reportAttributeAccessIssue]
    assert len(_pluggable.config.dsc_groups) == 1                                           # pyright: ignore[reportAttributeAccessIssue]
    
    LOGGER.info(f'Created Pluggable on Leaf: {_pluggable.id}')

@@ -131,7 +131,7 @@ def test_configure_pluggable_hub(pluggables_client: PluggablesClient):
        with_initial_config=False
    )
    _created = pluggables_client.CreatePluggable(_create_request)
    assert _created.id.pluggable_index == 3
    assert _created.id.pluggable_index == 3                                                 # pyright: ignore[reportAttributeAccessIssue]
    
    # Now configure it
    _config_request = create_configure_pluggable_request(
@@ -143,14 +143,14 @@ def test_configure_pluggable_hub(pluggables_client: PluggablesClient):
    _configured = pluggables_client.ConfigurePluggable(_config_request)
    
    assert isinstance(_configured, Pluggable)
    assert _configured.id.device.device_uuid.uuid == DEVICE_HUB_UUID
    assert _configured.id.pluggable_index == 3
    assert len(_configured.config.dsc_groups) == 1
    assert _configured.id.device.device_uuid.uuid == DEVICE_HUB_UUID                        # pyright: ignore[reportAttributeAccessIssue]
    assert _configured.id.pluggable_index == 3                                              # pyright: ignore[reportAttributeAccessIssue]
    assert len(_configured.config.dsc_groups) == 1                                          # pyright: ignore[reportAttributeAccessIssue]
    
    # Verify configuration was applied
    dsc_group = _configured.config.dsc_groups[0]
    assert dsc_group.group_size == 2
    assert len(dsc_group.subcarriers) == 2
    dsc_group = _configured.config.dsc_groups[0]                                            # pyright: ignore[reportAttributeAccessIssue]
    assert dsc_group.group_size == 2                                                        # pyright: ignore[reportAttributeAccessIssue]
    assert len(dsc_group.subcarriers) == 2                                                  # pyright: ignore[reportAttributeAccessIssue]
    
    LOGGER.info(f'Configured Pluggable on Hub with {len(dsc_group.subcarriers)} subcarriers')

@@ -181,9 +181,9 @@ def test_get_pluggable(pluggables_client: PluggablesClient):
    _retrieved = pluggables_client.GetPluggable(_get_request)
    
    assert isinstance(_retrieved, Pluggable)
    assert _retrieved.id.device.device_uuid.uuid == DEVICE_HUB_UUID
    assert _retrieved.id.pluggable_index == 4
    assert len(_retrieved.config.dsc_groups) == len(_created.config.dsc_groups)
    assert _retrieved.id.device.device_uuid.uuid == DEVICE_HUB_UUID                         # pyright: ignore[reportAttributeAccessIssue]
    assert _retrieved.id.pluggable_index == 4                                               # pyright: ignore[reportAttributeAccessIssue]
    assert len(_retrieved.config.dsc_groups) == len(_created.config.dsc_groups)             # pyright: ignore[reportAttributeAccessIssue]
    
    LOGGER.info(f'Retrieved Pluggable: {_retrieved.id}')

@@ -203,8 +203,8 @@ def test_list_pluggables(pluggables_client: PluggablesClient):
    assert len(_response.pluggables) >= 1  # At least one from previous tests
    
    for pluggable in _response.pluggables:
        assert pluggable.id.device.device_uuid.uuid == DEVICE_HUB_UUID
        LOGGER.info(f'Found Pluggable: index={pluggable.id.pluggable_index}')
        assert pluggable.id.device.device_uuid.uuid == DEVICE_HUB_UUID                      # pyright: ignore[reportAttributeAccessIssue]
        LOGGER.info(f'Found Pluggable: index={pluggable.id.pluggable_index}')              # pyright: ignore[reportAttributeAccessIssue]

# Number 7.
@pytest.mark.integration
@@ -222,7 +222,7 @@ def test_delete_pluggable(pluggables_client: PluggablesClient):
        with_initial_config=True
    )
    _created = pluggables_client.CreatePluggable(_create_request)
    assert _created.id.pluggable_index == 2
    assert _created.id.pluggable_index == 2                                                 # pyright: ignore[reportAttributeAccessIssue]
    
    # Delete it
    _delete_request = create_delete_pluggable_request(
@@ -257,7 +257,7 @@ def test_pluggable_already_exists_error(pluggables_client: PluggablesClient):
    
    # Create first time - should succeed
    _pluggable = pluggables_client.CreatePluggable(_request)
    assert _pluggable.id.pluggable_index == 3  # Should be 3, not 5
    assert _pluggable.id.pluggable_index == 3  # Should be 3, not 5                         # pyright: ignore[reportAttributeAccessIssue]
    
    # Try to create again - should fail
    with pytest.raises(grpc.RpcError) as e:
Loading