Skip to content

Commit db3b68a

Browse files
smk4664gsnider2195
andauthored
Add file_pull option to Cisco IOS devices (#345)
* Add file_pull option to Cisco IOS devices * Add docs, ruff and change fragment. * Pylint fixes Ignore the import position as Ruff is handling that. * Address feedback * Fix pylint commands The find command was too greedy and capturing files ignore by the pylint config in pyproject.toml. This allows pylint to use the pyproject.tom. * Rewrite to make the logic for checking files part of the Device. Not all Devices use netmiko, these changes allow other devices to also implement fix exist validation. * Update ios_device.py * Ruff * Apply suggestions from code review Co-authored-by: Gary Snider <75227981+gsnider2195@users.noreply.github.com> * Apply suggestions from code review. --------- Co-authored-by: Gary Snider <75227981+gsnider2195@users.noreply.github.com>
1 parent 848377c commit db3b68a

7 files changed

Lines changed: 593 additions & 18 deletions

File tree

changes/345.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added the ability to download files from within a Cisco IOS device.

docs/user/lib_getting_started.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,35 @@ interface GigabitEthernet1
250250
>>>
251251
```
252252

253+
#### Remote File Copy (Download to Device)
254+
255+
Some devices support copying files directly from a URL to the device. This is useful for larger files like OS images. To do this, you need to use the `FileCopyModel` data model to specify the source file information and then pass that to the `remote_file_copy` method. Currently only supported on Cisco IOS devices. Tested with ftp, http, https, sftp, and tftp urls.
256+
257+
- `remote_file_copy` method
258+
259+
```python
260+
from pyntc.utils.models import FileCopyModel
261+
262+
>>> source_file = FileCopyModel(
263+
... download_url='sftp://example.com/newconfig.cfg',
264+
... checksum='abc123def456',
265+
... hashing_algorithm='md5',
266+
... file_name='newconfig.cfg',
267+
vrf='Mgmt-vrf'
268+
... )
269+
>>> for device in devices:
270+
... device.remote_file_copy(source_file)
271+
...
272+
>>>
273+
```
274+
275+
Before using this feature you may need to configure a client on the device. For instance, on a Cisco IOS device you would need to set the source interface for the ip http client when using http or https urls. You can do this with the `config` method:
276+
277+
```python
278+
>>> csr1.config('ip http client source-interface GigabitEthernet1')
279+
>>>
280+
```
281+
253282
### Save Configs
254283

255284
- `save` method

pyntc/devices/base_device.py

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""The module contains the base class that all device classes must inherit from."""
22

3+
import hashlib
34
import importlib
45
import warnings
56

67
from pyntc.errors import FeatureNotFoundError, NTCError
8+
from pyntc.utils.models import FileCopyModel
79

810

911
def fix_docs(cls):
@@ -221,7 +223,7 @@ def file_copy(self, src, dest=None, **kwargs):
221223
222224
Keyword Args:
223225
file_system (str): Supported only for IOS and NXOS. The file system for the
224-
remote fle. If no file_system is provided, then the ``get_file_system``
226+
remote file. If no file_system is provided, then the ``get_file_system``
225227
method is used to determine the correct file system to use.
226228
"""
227229
raise NotImplementedError
@@ -241,13 +243,126 @@ def file_copy_remote_exists(self, src, dest=None, **kwargs):
241243
242244
Keyword Args:
243245
file_system (str): Supported only for IOS and NXOS. The file system for the
244-
remote fle. If no file_system is provided, then the ``get_file_system``
246+
remote file. If no file_system is provided, then the ``get_file_system``
245247
method is used to determine the correct file system to use.
246248
247249
Returns:
248250
(bool): True if the remote file exists, False if it doesn't.
249251
"""
250252

253+
def check_file_exists(self, filename, **kwargs):
254+
"""Check if a remote file exists by filename.
255+
256+
Args:
257+
filename (str): The name of the file to check for on the remote device.
258+
kwargs (dict): Additional keyword arguments that may be used by subclasses.
259+
260+
Keyword Args:
261+
file_system (str): Supported only for IOS and NXOS. The file system for the
262+
remote file. If no file_system is provided, then the ``get_file_system``
263+
method is used to determine the correct file system to use.
264+
265+
Returns:
266+
(bool): True if the remote file exists, False if it doesn't.
267+
"""
268+
raise NotImplementedError
269+
270+
def get_remote_checksum(self, filename, hashing_algorithm="md5", **kwargs):
271+
"""Get the checksum of a remote file.
272+
273+
Args:
274+
filename (str): The name of the file to check for on the remote device.
275+
hashing_algorithm (str): The hashing algorithm to use (default: "md5").
276+
kwargs (dict): Additional keyword arguments that may be used by subclasses.
277+
278+
Keyword Args:
279+
file_system (str): Supported only for IOS and NXOS. The file system for the
280+
remote file. If no file_system is provided, then the ``get_file_system``
281+
method is used to determine the correct file system to use.
282+
283+
Returns:
284+
(str): The checksum of the remote file.
285+
"""
286+
raise NotImplementedError
287+
288+
@staticmethod
289+
def get_local_checksum(filepath, hashing_algorithm="md5", add_newline=False):
290+
"""Get the checksum of a local file using a specified algorithm.
291+
292+
Args:
293+
filepath (str): The path to the local file.
294+
hashing_algorithm (str): The hashing algorithm to use (e.g., "md5", "sha256").
295+
add_newline (bool): Whether to append a newline before final hashing (Some devices may require this).
296+
297+
Returns:
298+
(str): The hex digest of the file.
299+
"""
300+
# Initialize the hash object dynamically
301+
file_hash = hashlib.new(hashing_algorithm.lower())
302+
303+
with open(filepath, "rb") as f:
304+
# Read in chunks to handle large firmware files without RAM spikes
305+
for chunk in iter(lambda: f.read(4096), b""):
306+
file_hash.update(chunk)
307+
308+
if add_newline:
309+
file_hash.update(b"\n")
310+
311+
return file_hash.hexdigest()
312+
313+
def compare_file_checksum(self, checksum, filename, hashing_algorithm="md5", **kwargs):
314+
"""Compare the checksum of a local file with a remote file.
315+
316+
Args:
317+
checksum (str): The checksum of the file.
318+
filename (str): The name of the file to check for on the remote device.
319+
hashing_algorithm (str): The hashing algorithm to use (default: "md5").
320+
kwargs (dict): Additional keyword arguments that may be used by subclasses.
321+
322+
Keyword Args:
323+
file_system (str): Supported only for IOS and NXOS. The file system for the
324+
remote file. If no file_system is provided, then the ``get_file_system``
325+
method is used to determine the correct file system to use.
326+
327+
Returns:
328+
(bool): True if the checksums match, False otherwise.
329+
"""
330+
return checksum == self.get_remote_checksum(filename, hashing_algorithm, **kwargs)
331+
332+
def remote_file_copy(self, src: FileCopyModel = None, dest=None, **kwargs):
333+
"""Copy a file to a remote device.
334+
335+
Args:
336+
src (FileCopyModel): The source file model.
337+
dest (str): The destination file path on the remote device.
338+
kwargs (dict): Additional keyword arguments that may be used by subclasses.
339+
340+
Keyword Args:
341+
file_system (str): Supported only for IOS and NXOS. The file system for the
342+
remote file. If no file_system is provided, then the ``get_file_system``
343+
method is used to determine the correct file system to use.
344+
"""
345+
raise NotImplementedError
346+
347+
def verify_file(self, checksum, filename, hashing_algorithm="md5", **kwargs):
348+
"""Verify a file on the remote device by confirming the file exists and validate the checksum.
349+
350+
Args:
351+
checksum (str): The checksum of the file.
352+
filename (str): The name of the file to check for on the remote device.
353+
hashing_algorithm (str): The hashing algorithm to use (default: "md5").
354+
kwargs (dict): Additional keyword arguments that may be used by subclasses.
355+
356+
Keyword Args:
357+
file_system (str): Supported only for IOS and NXOS. The file system for the
358+
remote file. If no file_system is provided, then the ``get_file_system``
359+
method is used to determine the correct file system to use.
360+
361+
Returns:
362+
(bool): True if the file is verified successfully, False otherwise.
363+
"""
364+
raise NotImplementedError
365+
251366
def install_os(self, image_name, **vendor_specifics):
252367
"""Install the OS from specified image_name.
253368

0 commit comments

Comments
 (0)