Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@

VPD_DATA_FILE = "/var/run/hw-management/eeprom/vpd_data"
REVISION = "REV"
VPD_DATA_WAIT_TIMEOUT = 60 # Timeout in seconds for waiting for VPD data file

HWMGMT_SYSTEM_ROOT = '/var/run/hw-management/system/'

Expand Down Expand Up @@ -1066,7 +1067,10 @@ def _parse_vpd_data(self, filename):
result = {}
try:
if not os.access(filename, os.R_OK):
return result
logger.log_info("VPD data file {} not accessible, waiting for creation".format(filename))
if not utils.wait_for_file_creation(filename, VPD_DATA_WAIT_TIMEOUT):
logger.log_error("VPD data file {} not available after timeout".format(filename))
return result

result = utils.read_key_value_file(filename, delimeter=": ")

Expand Down
53 changes: 53 additions & 0 deletions platform/mellanox/mlnx-platform-api/sonic_platform/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@
import threading
import time
import os

# Inotify causes an exception when DEBUG env variable is set to a non-integer convertible
# https://github.com/dsoprea/PyInotify/blob/0.2.10/inotify/adapters.py#L37
os.environ['DEBUG'] = '0'
import inotify.adapters
import inotify.constants
from sonic_py_common import device_info
from sonic_py_common.logger import Logger

Expand Down Expand Up @@ -299,6 +305,53 @@ def extract_cpo_ports_index(num_of_asics=1):
return _extract_ports_index_by_type(CPO_PORT_TYPE, num_of_asics)


# Use this function only for files that have user read permission.
def wait_for_file_creation(file_path, timeout):
"""
Wait for a file to be created using inotify
Args:
file_path: Path to the file to wait for
timeout: Timeout in seconds
Returns:
True if file was created/copied from a temporary file, and is readable, False otherwise
"""
# If file already exists and is readable, return immediately
if os.access(file_path, os.R_OK):
return True

dir_path = os.path.dirname(file_path)
file_name = os.path.basename(file_path)

if not os.path.exists(dir_path):
logger.log_debug("Directory {} does not exist".format(dir_path))
return False

try:
notifier = inotify.adapters.Inotify()
notifier.add_watch(dir_path,
mask=(inotify.constants.IN_CREATE
| inotify.constants.IN_CLOSE_WRITE
| inotify.constants.IN_MOVED_TO))

for event in notifier.event_gen(timeout_s=timeout, yield_nones=False):
(_, type_names, path, filename) = event
if filename == file_name:
if "IN_CREATE" in type_names or "IN_CLOSE_WRITE" in type_names or "IN_MOVED_TO" in type_names:
if os.access(file_path, os.R_OK):
logger.log_info("File {} created and readable".format(file_path))
return True

except Exception as e:
logger.log_error("Inotify error while waiting for {}: {}".format(file_path, repr(e)))

if os.access(file_path, os.R_OK):
return True

return False


def extract_asic_id_map(num_of_asics=1):
asic_id_map = {}

Expand Down
55 changes: 54 additions & 1 deletion platform/mellanox/mlnx-platform-api/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#
# SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES
# Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
Expand Down Expand Up @@ -327,6 +327,59 @@ def test_wait_until_conditions(self):
conditions = [lambda: False]
assert not utils.wait_until_conditions(conditions, 1)

@mock.patch('sonic_platform.utils.inotify.adapters.Inotify')
@mock.patch('os.access')
def test_wait_for_file_creation_immediate(self, mock_access, mock_inotify):
mock_access.return_value = True
assert utils.wait_for_file_creation('/tmp/test.file', timeout=1)
mock_inotify.assert_not_called()

@mock.patch('sonic_platform.utils.logger.log_debug')
@mock.patch('os.path.exists')
@mock.patch('os.access')
def test_wait_for_file_creation_dir_missing(self, mock_access, mock_exists, mock_log_debug):
"""When directory does not exist, return False immediately (no wait)."""
mock_access.return_value = False
mock_exists.return_value = False
assert not utils.wait_for_file_creation('/tmp/test.file', timeout=1)
mock_log_debug.assert_called_once()

@pytest.mark.parametrize("event_type", [
"IN_CREATE",
"IN_CLOSE_WRITE",
"IN_MOVED_TO",
])
@mock.patch('sonic_platform.utils.logger.log_info')
@mock.patch('os.path.exists')
@mock.patch('os.access')
def test_wait_for_file_creation_inotify_event(self, mock_access, mock_exists, mock_log_info, event_type):
"""All inotify event types that indicate file creation are handled."""
# First os.access: file not readable at start; second: readable when inotify event is processed
mock_access.side_effect = [False, True]
mock_exists.return_value = True

mock_notifier = mock.MagicMock()
mock_notifier.event_gen.return_value = [
(None, [event_type], "/tmp", "test.file")
]

with mock.patch('sonic_platform.utils.inotify.adapters.Inotify', return_value=mock_notifier):
assert utils.wait_for_file_creation('/tmp/test.file', timeout=1)

mock_log_info.assert_called_once()

@mock.patch('sonic_platform.utils.logger.log_error')
@mock.patch('os.path.exists')
@mock.patch('os.access')
def test_wait_for_file_creation_inotify_error(self, mock_access, mock_exists, mock_log_error):
mock_access.side_effect = [False, False]
mock_exists.return_value = True

with mock.patch('sonic_platform.utils.inotify.adapters.Inotify', side_effect=Exception('boom')):
assert not utils.wait_for_file_creation('/tmp/test.file', timeout=1)

mock_log_error.assert_called_once()

def test_timer(self):
timer = utils.Timer()
timer.start()
Expand Down
Loading