Skip to content
Merged
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
1 change: 1 addition & 0 deletions changes/375.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added free space validation for file copy operations on NXOS devices.
18 changes: 18 additions & 0 deletions pyntc/devices/nxos_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ def file_copy(self, src, dest=None, file_system="bootflash:"):
"""
if not self.file_copy_remote_exists(src, dest, file_system):
dest = dest or os.path.basename(src)
self._check_free_space(os.path.getsize(src), file_system=file_system)
try:
file_copy = self.native.file_copy( # pylint: disable=assignment-from-no-return
src, dest, file_system=file_system
Expand Down Expand Up @@ -337,6 +338,22 @@ def _get_file_system(self):
log.debug("Host %s: File system %s.", self.host, file_system)
return file_system

def _get_free_space(self, file_system=None):
"""Return free bytes on ``file_system`` as reported by NXOS ``dir`` output."""
if file_system is None:
file_system = self._get_file_system()

raw_data = self.show(f"dir {file_system}", raw_text=True)
# Example NXOS dir output: 47171194880 bytes free
match = re.search(r"(\d+)\s+bytes\s+free", raw_data)
if match is None:
log.error("Host %s: could not parse free space from '%s'.", self.host, f"dir {file_system}")
raise CommandError(command=f"dir {file_system}", message="Unable to parse free space from dir output.")

free_bytes = int(match.group(1))
log.debug("Host %s: %s bytes free on %s.", self.host, free_bytes, file_system)
return free_bytes

@staticmethod
def _netloc(src: FileCopyModel) -> str:
"""Return host:port or just host from a FileCopyModel."""
Expand Down Expand Up @@ -511,6 +528,7 @@ def remote_file_copy(self, src: FileCopyModel, dest=None, file_system=None, **kw
file_system,
)
if not self.verify_file(src.checksum, dest, hashing_algorithm=src.hashing_algorithm, file_system=file_system):
self._pre_transfer_space_check(src, file_system)
current_prompt = self.native_ssh.find_prompt()

# Define prompt mapping for expected prompts during file copy
Expand Down
73 changes: 70 additions & 3 deletions tests/unit/test_devices/test_nxos_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
CommandListError,
FileSystemNotFoundError,
FileTransferError,
NotEnoughFreeSpaceError,
NTCFileNotFoundError,
)
from pyntc.utils.models import FileCopyModel
Expand Down Expand Up @@ -139,14 +140,18 @@ def test_file_copy_remote_exists_failure(self):
"source_file", "dest_file", file_system=FILE_SYSTEM
)

@mock.patch.object(NXOSDevice, "_get_free_space", return_value=1024 * 1024 * 1024)
@mock.patch("pyntc.devices.nxos_device.os.path.getsize", return_value=1024)
@mock.patch.object(NXOSDevice, "file_copy_remote_exists", side_effect=[False, True])
def test_file_copy(self, mock_fcre):
def test_file_copy(self, mock_fcre, mock_getsize, mock_get_free_space):
self.device.file_copy("source_file", "dest_file")
self.device.native.file_copy.assert_called_with("source_file", "dest_file", file_system=FILE_SYSTEM)
self.device.native.file_copy.assert_called()

@mock.patch.object(NXOSDevice, "_get_free_space", return_value=1024 * 1024 * 1024)
@mock.patch("pyntc.devices.nxos_device.os.path.getsize", return_value=1024)
@mock.patch.object(NXOSDevice, "file_copy_remote_exists", side_effect=[False, True])
def test_file_copy_no_dest(self, mock_fcre):
def test_file_copy_no_dest(self, mock_fcre, mock_getsize, mock_get_free_space):
self.device.file_copy("source_file")
self.device.native.file_copy.assert_called_with("source_file", "source_file", file_system=FILE_SYSTEM)
self.device.native.file_copy.assert_called()
Expand All @@ -156,12 +161,22 @@ def test_file_copy_file_exists(self, mock_fcre):
self.device.file_copy("source_file", "dest_file")
self.device.native.file_copy.assert_not_called()

@mock.patch.object(NXOSDevice, "_get_free_space", return_value=1024 * 1024 * 1024)
@mock.patch("pyntc.devices.nxos_device.os.path.getsize", return_value=1024)
@mock.patch.object(NXOSDevice, "file_copy_remote_exists", side_effect=[False, False])
def test_file_copy_fail(self, mock_fcre):
def test_file_copy_fail(self, mock_fcre, mock_getsize, mock_get_free_space):
with self.assertRaises(FileTransferError):
self.device.file_copy("source_file")
self.device.native.file_copy.assert_called()

@mock.patch.object(NXOSDevice, "_get_free_space", return_value=1024) # Only 1KB free
@mock.patch("pyntc.devices.nxos_device.os.path.getsize", return_value=1024 * 1024) # Trying to copy 1MB
@mock.patch.object(NXOSDevice, "file_copy_remote_exists", side_effect=[False])
def test_file_copy_raises_not_enough_free_space(self, mock_fcre, mock_getsize, mock_get_free_space):
"""Test file_copy raises NotEnoughFreeSpaceError when insufficient space."""
with self.assertRaises(NotEnoughFreeSpaceError):
self.device.file_copy("source_file")

def test_reboot(self):
self.device.reboot()
self.device.native.show_list.assert_called_with(["terminal dont-ask", "reload"])
Expand Down Expand Up @@ -278,6 +293,41 @@ def test_get_file_system_not_found(self, mock_show):
self.device._get_file_system()
mock_show.assert_called_with("dir", raw_text=True)

@mock.patch.object(NXOSDevice, "show")
def test_get_free_space(self, mock_show):
"""Test _get_free_space parses NXOS dir output correctly."""
# NXOS dir output format with free space at the end
mock_show.return_value = """Directory of bootflash:/
4096 Mar 03 22:47:15 2026 .rpmstore/
4733329408 bytes used
47171194880 bytes free
51904524288 bytes total

"""
result = self.device._get_free_space()
self.assertEqual(result, 47171194880)
mock_show.assert_called_with("dir bootflash:", raw_text=True)

@mock.patch.object(NXOSDevice, "show")
def test_get_free_space_with_custom_filesystem(self, mock_show):
"""Test _get_free_space uses custom file system when provided."""
mock_show.return_value = """Directory of disk0:/
1000000 bytes used
2000000 bytes free
3000000 bytes total

"""
result = self.device._get_free_space("disk0:")
self.assertEqual(result, 2000000)
mock_show.assert_called_with("dir disk0:", raw_text=True)

@mock.patch.object(NXOSDevice, "show")
def test_get_free_space_raises_on_parse_error(self, mock_show):
"""Test _get_free_space raises CommandError when output can't be parsed."""
mock_show.return_value = "Directory of bootflash:/\nNo free space info here\n"
with self.assertRaises(CommandError):
self.device._get_free_space()

def test_check_file_exists_true(self):
self.device.native_ssh.send_command.return_value = "12345 bootflash:/nxos.bin"
result = self.device.check_file_exists("nxos.bin", file_system="bootflash:")
Expand Down Expand Up @@ -362,6 +412,23 @@ def test_remote_file_copy_transfer_fails_verification(self):
with self.assertRaises(FileTransferError):
self.device.remote_file_copy(src, file_system="bootflash:")

@mock.patch.object(NXOSDevice, "verify_file", return_value=False)
@mock.patch.object(NXOSDevice, "_get_free_space", return_value=1024) # Only 1KB free
def test_remote_file_copy_raises_not_enough_free_space(self, mock_get_free_space, mock_verify):
"""Test remote_file_copy raises NotEnoughFreeSpaceError when insufficient space."""
src = FileCopyModel(
download_url="http://example.com/nxos.bin",
checksum="abc123",
file_name="nxos.bin",
hashing_algorithm="md5",
timeout=30,
file_size=1024 * 1024, # Trying to copy 1MB
)
self.device.native_ssh.find_prompt.return_value = "host#"
with self.assertRaises(NotEnoughFreeSpaceError):
self.device.remote_file_copy(src, file_system="bootflash:")
self.device.native_ssh.send_command.assert_not_called()

def test_remote_file_copy_invalid_scheme(self):
src = FileCopyModel(
download_url="smtp://example.com/nxos.bin",
Expand Down
Loading