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
1 change: 1 addition & 0 deletions changes/345.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added the ability to download files from within a Cisco IOS device.
29 changes: 29 additions & 0 deletions docs/user/lib_getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,35 @@ interface GigabitEthernet1
>>>
```

#### Remote File Copy (Download to Device)

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.

- `remote_file_copy` method

```python
from pyntc.utils.models import FileCopyModel

>>> source_file = FileCopyModel(
... download_url='sftp://example.com/newconfig.cfg',
... checksum='abc123def456',
... hashing_algorithm='md5',
... file_name='newconfig.cfg',
vrf='Mgmt-vrf'
... )
>>> for device in devices:
... device.remote_file_copy(source_file)
...
>>>
```

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:

```python
>>> csr1.config('ip http client source-interface GigabitEthernet1')
>>>
```

### Save Configs

- `save` method
Expand Down
119 changes: 117 additions & 2 deletions pyntc/devices/base_device.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"""The module contains the base class that all device classes must inherit from."""

import hashlib
import importlib
import warnings

from pyntc.errors import FeatureNotFoundError, NTCError
from pyntc.utils.models import FileCopyModel


def fix_docs(cls):
Expand Down Expand Up @@ -221,7 +223,7 @@ def file_copy(self, src, dest=None, **kwargs):

Keyword Args:
file_system (str): Supported only for IOS and NXOS. The file system for the
remote fle. If no file_system is provided, then the ``get_file_system``
remote file. If no file_system is provided, then the ``get_file_system``
method is used to determine the correct file system to use.
"""
raise NotImplementedError
Expand All @@ -241,13 +243,126 @@ def file_copy_remote_exists(self, src, dest=None, **kwargs):

Keyword Args:
file_system (str): Supported only for IOS and NXOS. The file system for the
remote fle. If no file_system is provided, then the ``get_file_system``
remote file. If no file_system is provided, then the ``get_file_system``
method is used to determine the correct file system to use.

Returns:
(bool): True if the remote file exists, False if it doesn't.
"""

def check_file_exists(self, filename, **kwargs):
"""Check if a remote file exists by filename.

Args:
filename (str): The name of the file to check for on the remote device.
kwargs (dict): Additional keyword arguments that may be used by subclasses.

Keyword Args:
file_system (str): Supported only for IOS and NXOS. The file system for the
remote file. If no file_system is provided, then the ``get_file_system``
method is used to determine the correct file system to use.

Returns:
(bool): True if the remote file exists, False if it doesn't.
"""
raise NotImplementedError

def get_remote_checksum(self, filename, hashing_algorithm="md5", **kwargs):
"""Get the checksum of a remote file.

Args:
filename (str): The name of the file to check for on the remote device.
hashing_algorithm (str): The hashing algorithm to use (default: "md5").
kwargs (dict): Additional keyword arguments that may be used by subclasses.

Keyword Args:
file_system (str): Supported only for IOS and NXOS. The file system for the
remote file. If no file_system is provided, then the ``get_file_system``
method is used to determine the correct file system to use.

Returns:
(str): The checksum of the remote file.
"""
raise NotImplementedError

@staticmethod
def get_local_checksum(filepath, hashing_algorithm="md5", add_newline=False):
"""Get the checksum of a local file using a specified algorithm.

Args:
filepath (str): The path to the local file.
hashing_algorithm (str): The hashing algorithm to use (e.g., "md5", "sha256").
add_newline (bool): Whether to append a newline before final hashing (Some devices may require this).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did this come from netmiko? Seems weird but if it's in netmiko it's probably fixing some strange quirk in a device

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is in Netmiko, which is where it came from, but doesn't seem to be used for IOS. Probably fixing something in some devices.


Returns:
(str): The hex digest of the file.
"""
# Initialize the hash object dynamically
file_hash = hashlib.new(hashing_algorithm.lower())

with open(filepath, "rb") as f:
# Read in chunks to handle large firmware files without RAM spikes
for chunk in iter(lambda: f.read(4096), b""):
file_hash.update(chunk)

if add_newline:
file_hash.update(b"\n")

return file_hash.hexdigest()

def compare_file_checksum(self, checksum, filename, hashing_algorithm="md5", **kwargs):
"""Compare the checksum of a local file with a remote file.

Args:
checksum (str): The checksum of the file.
filename (str): The name of the file to check for on the remote device.
hashing_algorithm (str): The hashing algorithm to use (default: "md5").
kwargs (dict): Additional keyword arguments that may be used by subclasses.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need the file_system keyword arg docstring?


Keyword Args:
file_system (str): Supported only for IOS and NXOS. The file system for the
remote file. If no file_system is provided, then the ``get_file_system``
method is used to determine the correct file system to use.

Returns:
(bool): True if the checksums match, False otherwise.
"""
return checksum == self.get_remote_checksum(filename, hashing_algorithm, **kwargs)

def remote_file_copy(self, src: FileCopyModel = None, dest=None, **kwargs):
"""Copy a file to a remote device.

Args:
src (FileCopyModel): The source file model.
dest (str): The destination file path on the remote device.
kwargs (dict): Additional keyword arguments that may be used by subclasses.

Keyword Args:
file_system (str): Supported only for IOS and NXOS. The file system for the
remote file. If no file_system is provided, then the ``get_file_system``
method is used to determine the correct file system to use.
"""
raise NotImplementedError

def verify_file(self, checksum, filename, hashing_algorithm="md5", **kwargs):
"""Verify a file on the remote device by confirming the file exists and validate the checksum.

Args:
checksum (str): The checksum of the file.
filename (str): The name of the file to check for on the remote device.
hashing_algorithm (str): The hashing algorithm to use (default: "md5").
kwargs (dict): Additional keyword arguments that may be used by subclasses.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need the file_system keyword arg docstring?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it should. I will add.


Keyword Args:
file_system (str): Supported only for IOS and NXOS. The file system for the
remote file. If no file_system is provided, then the ``get_file_system``
method is used to determine the correct file system to use.

Returns:
(bool): True if the file is verified successfully, False otherwise.
"""
raise NotImplementedError

def install_os(self, image_name, **vendor_specifics):
"""Install the OS from specified image_name.

Expand Down
Loading