Skip to content

Commit 803d114

Browse files
Add NXOS pre-transfer get free space check (#375)
* Added _get_free_space for nxos * Update 375.added Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --------- Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com>
1 parent 10b818d commit 803d114

3 files changed

Lines changed: 89 additions & 3 deletions

File tree

changes/375.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added free space validation for file copy operations on NXOS devices.

pyntc/devices/nxos_device.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ def file_copy(self, src, dest=None, file_system="bootflash:"):
279279
"""
280280
if not self.file_copy_remote_exists(src, dest, file_system):
281281
dest = dest or os.path.basename(src)
282+
self._check_free_space(os.path.getsize(src), file_system=file_system)
282283
try:
283284
file_copy = self.native.file_copy( # pylint: disable=assignment-from-no-return
284285
src, dest, file_system=file_system
@@ -337,6 +338,22 @@ def _get_file_system(self):
337338
log.debug("Host %s: File system %s.", self.host, file_system)
338339
return file_system
339340

341+
def _get_free_space(self, file_system=None):
342+
"""Return free bytes on ``file_system`` as reported by NXOS ``dir`` output."""
343+
if file_system is None:
344+
file_system = self._get_file_system()
345+
346+
raw_data = self.show(f"dir {file_system}", raw_text=True)
347+
# Example NXOS dir output: 47171194880 bytes free
348+
match = re.search(r"(\d+)\s+bytes\s+free", raw_data)
349+
if match is None:
350+
log.error("Host %s: could not parse free space from '%s'.", self.host, f"dir {file_system}")
351+
raise CommandError(command=f"dir {file_system}", message="Unable to parse free space from dir output.")
352+
353+
free_bytes = int(match.group(1))
354+
log.debug("Host %s: %s bytes free on %s.", self.host, free_bytes, file_system)
355+
return free_bytes
356+
340357
@staticmethod
341358
def _netloc(src: FileCopyModel) -> str:
342359
"""Return host:port or just host from a FileCopyModel."""
@@ -511,6 +528,7 @@ def remote_file_copy(self, src: FileCopyModel, dest=None, file_system=None, **kw
511528
file_system,
512529
)
513530
if not self.verify_file(src.checksum, dest, hashing_algorithm=src.hashing_algorithm, file_system=file_system):
531+
self._pre_transfer_space_check(src, file_system)
514532
current_prompt = self.native_ssh.find_prompt()
515533

516534
# Define prompt mapping for expected prompts during file copy

tests/unit/test_devices/test_nxos_device.py

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
CommandListError,
1111
FileSystemNotFoundError,
1212
FileTransferError,
13+
NotEnoughFreeSpaceError,
1314
NTCFileNotFoundError,
1415
)
1516
from pyntc.utils.models import FileCopyModel
@@ -139,14 +140,18 @@ def test_file_copy_remote_exists_failure(self):
139140
"source_file", "dest_file", file_system=FILE_SYSTEM
140141
)
141142

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

151+
@mock.patch.object(NXOSDevice, "_get_free_space", return_value=1024 * 1024 * 1024)
152+
@mock.patch("pyntc.devices.nxos_device.os.path.getsize", return_value=1024)
148153
@mock.patch.object(NXOSDevice, "file_copy_remote_exists", side_effect=[False, True])
149-
def test_file_copy_no_dest(self, mock_fcre):
154+
def test_file_copy_no_dest(self, mock_fcre, mock_getsize, mock_get_free_space):
150155
self.device.file_copy("source_file")
151156
self.device.native.file_copy.assert_called_with("source_file", "source_file", file_system=FILE_SYSTEM)
152157
self.device.native.file_copy.assert_called()
@@ -156,12 +161,22 @@ def test_file_copy_file_exists(self, mock_fcre):
156161
self.device.file_copy("source_file", "dest_file")
157162
self.device.native.file_copy.assert_not_called()
158163

164+
@mock.patch.object(NXOSDevice, "_get_free_space", return_value=1024 * 1024 * 1024)
165+
@mock.patch("pyntc.devices.nxos_device.os.path.getsize", return_value=1024)
159166
@mock.patch.object(NXOSDevice, "file_copy_remote_exists", side_effect=[False, False])
160-
def test_file_copy_fail(self, mock_fcre):
167+
def test_file_copy_fail(self, mock_fcre, mock_getsize, mock_get_free_space):
161168
with self.assertRaises(FileTransferError):
162169
self.device.file_copy("source_file")
163170
self.device.native.file_copy.assert_called()
164171

172+
@mock.patch.object(NXOSDevice, "_get_free_space", return_value=1024) # Only 1KB free
173+
@mock.patch("pyntc.devices.nxos_device.os.path.getsize", return_value=1024 * 1024) # Trying to copy 1MB
174+
@mock.patch.object(NXOSDevice, "file_copy_remote_exists", side_effect=[False])
175+
def test_file_copy_raises_not_enough_free_space(self, mock_fcre, mock_getsize, mock_get_free_space):
176+
"""Test file_copy raises NotEnoughFreeSpaceError when insufficient space."""
177+
with self.assertRaises(NotEnoughFreeSpaceError):
178+
self.device.file_copy("source_file")
179+
165180
def test_reboot(self):
166181
self.device.reboot()
167182
self.device.native.show_list.assert_called_with(["terminal dont-ask", "reload"])
@@ -278,6 +293,41 @@ def test_get_file_system_not_found(self, mock_show):
278293
self.device._get_file_system()
279294
mock_show.assert_called_with("dir", raw_text=True)
280295

296+
@mock.patch.object(NXOSDevice, "show")
297+
def test_get_free_space(self, mock_show):
298+
"""Test _get_free_space parses NXOS dir output correctly."""
299+
# NXOS dir output format with free space at the end
300+
mock_show.return_value = """Directory of bootflash:/
301+
4096 Mar 03 22:47:15 2026 .rpmstore/
302+
4733329408 bytes used
303+
47171194880 bytes free
304+
51904524288 bytes total
305+
306+
"""
307+
result = self.device._get_free_space()
308+
self.assertEqual(result, 47171194880)
309+
mock_show.assert_called_with("dir bootflash:", raw_text=True)
310+
311+
@mock.patch.object(NXOSDevice, "show")
312+
def test_get_free_space_with_custom_filesystem(self, mock_show):
313+
"""Test _get_free_space uses custom file system when provided."""
314+
mock_show.return_value = """Directory of disk0:/
315+
1000000 bytes used
316+
2000000 bytes free
317+
3000000 bytes total
318+
319+
"""
320+
result = self.device._get_free_space("disk0:")
321+
self.assertEqual(result, 2000000)
322+
mock_show.assert_called_with("dir disk0:", raw_text=True)
323+
324+
@mock.patch.object(NXOSDevice, "show")
325+
def test_get_free_space_raises_on_parse_error(self, mock_show):
326+
"""Test _get_free_space raises CommandError when output can't be parsed."""
327+
mock_show.return_value = "Directory of bootflash:/\nNo free space info here\n"
328+
with self.assertRaises(CommandError):
329+
self.device._get_free_space()
330+
281331
def test_check_file_exists_true(self):
282332
self.device.native_ssh.send_command.return_value = "12345 bootflash:/nxos.bin"
283333
result = self.device.check_file_exists("nxos.bin", file_system="bootflash:")
@@ -362,6 +412,23 @@ def test_remote_file_copy_transfer_fails_verification(self):
362412
with self.assertRaises(FileTransferError):
363413
self.device.remote_file_copy(src, file_system="bootflash:")
364414

415+
@mock.patch.object(NXOSDevice, "verify_file", return_value=False)
416+
@mock.patch.object(NXOSDevice, "_get_free_space", return_value=1024) # Only 1KB free
417+
def test_remote_file_copy_raises_not_enough_free_space(self, mock_get_free_space, mock_verify):
418+
"""Test remote_file_copy raises NotEnoughFreeSpaceError when insufficient space."""
419+
src = FileCopyModel(
420+
download_url="http://example.com/nxos.bin",
421+
checksum="abc123",
422+
file_name="nxos.bin",
423+
hashing_algorithm="md5",
424+
timeout=30,
425+
file_size=1024 * 1024, # Trying to copy 1MB
426+
)
427+
self.device.native_ssh.find_prompt.return_value = "host#"
428+
with self.assertRaises(NotEnoughFreeSpaceError):
429+
self.device.remote_file_copy(src, file_system="bootflash:")
430+
self.device.native_ssh.send_command.assert_not_called()
431+
365432
def test_remote_file_copy_invalid_scheme(self):
366433
src = FileCopyModel(
367434
download_url="smtp://example.com/nxos.bin",

0 commit comments

Comments
 (0)