Skip to content

Add Arista EOS remote file copy#365

Merged
itdependsnetworks merged 5 commits intodevelopfrom
u/mattmiller/NAPPS-1056/add_remote_file_copy
Apr 13, 2026
Merged

Add Arista EOS remote file copy#365
itdependsnetworks merged 5 commits intodevelopfrom
u/mattmiller/NAPPS-1056/add_remote_file_copy

Conversation

@mattmiller87
Copy link
Copy Markdown
Contributor

@mattmiller87 mattmiller87 commented Apr 10, 2026

New Pull Request

Have you:

  • Updated the docs if necessary?
  • Updated any configuration settings?
  • Written a unit test?

Change Notes

Added the remote file copy feature to Arista EOS devices.
Added unittests for remote file copy on Arista EOS devices. (with help from Claude)

Justification

This allows an Arista device to run the copy command directly.

@mattmiller87 mattmiller87 force-pushed the u/mattmiller/NAPPS-1056/add_remote_file_copy branch from 8ec398a to d34fe08 Compare April 11, 2026 00:01
@mattmiller87 mattmiller87 force-pushed the u/mattmiller/NAPPS-1056/add_remote_file_copy branch 4 times, most recently from 44f96c6 to 6a32a97 Compare April 13, 2026 13:29
@mattmiller87 mattmiller87 force-pushed the u/mattmiller/NAPPS-1056/add_remote_file_copy branch from 6a32a97 to 3fc3736 Compare April 13, 2026 13:44
Comment thread docs/user/lib_getting_started.md Outdated
Comment thread pyntc/devices/eos_device.py
Comment thread pyntc/devices/eos_device.py Outdated
Comment thread pyntc/devices/eos_device.py Outdated
Comment thread pyntc/devices/eos_device.py Outdated
@mattmiller87
Copy link
Copy Markdown
Contributor Author

@jeffkala, I appreciate the comments. I've applied your suggestions.

Copy link
Copy Markdown
Contributor

@jeffkala jeffkala left a comment

Choose a reason for hiding this comment

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

I'm good with it, thanks for updating my first set of reviews.

@itdependsnetworks itdependsnetworks merged commit 78b734f into develop Apr 13, 2026
10 checks passed
@mattmiller87 mattmiller87 deleted the u/mattmiller/NAPPS-1056/add_remote_file_copy branch April 13, 2026 21:18
"""Get the checksum of a remote file on Arista EOS device using netmiko SSH.

Uses Arista's 'verify' command via SSH to compute file checksums.
Note, Netmiko FileTransfer only supports `verify /md5`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What does this mean?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was hoping the netmiko FileTransfer would solve this problem, but there is an issue where it is limited to md5.

https://ktbyers.github.io/netmiko/docs/netmiko/arista/index.html > Expand Source Code

def remote_md5(
        self, base_cmd: str = "verify /md5", remote_file: Optional[str] = None
    ) -> str:
        if remote_file is None:
            if self.direction == "put":
                remote_file = self.dest_file
            elif self.direction == "get":
                remote_file = self.source_file
        remote_md5_cmd = f"{base_cmd} file:{self.file_system}/{remote_file}"
        dest_md5 = self.ssh_ctl_chan._send_command_str(remote_md5_cmd, read_timeout=600)
        dest_md5 = self.process_md5(dest_md5)
        return dest_md5


# Parse the checksum from the output
# Expected format: verify /sha512 (flash:nautobot.png) = <checksum>
match = re.search(r"=\s*([a-fA-F0-9]+)", result)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't like how imprecise this regex is. Can we just do if remote_checksum.lower() in result.lower():?


# Use Arista's verify command to get the checksum
# Example: verify /sha512 flash:nautobot.png
command = f"verify /{hashing_algorithm} {path}"
Copy link
Copy Markdown
Contributor

@gsnider2195 gsnider2195 Apr 13, 2026

Choose a reason for hiding this comment

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

We should handle invalid hashing_algorithm values and raise the correct exception so the user knows that the checksum type is invalid instead of just saying that it failed to parse. If they select SHA in their SoftwareImageFile they may never understand that their checksum type is wrong and will just assume that the file transfer is failing since % Invalid input is the only message they'll see:

Arista7050TX-64-R#verify /asdf flash:/EOS-4.15.4F.swi
% Invalid input


def _build_url_copy_command_simple(self, src, file_system):
"""Build copy command for simple URL-based transfers (TFTP, HTTP, HTTPS without credentials)."""
return f"copy {src.download_url} {file_system}", False
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should include the filename in the destination or it will default to the filename in the url which may not be correct. For example the filename in sftpgo's web server defaults to download because the url is http://10.1.100.220:8080/web/client/pubshares/QVuvQMzugSegd2e7krDwr7/download


def _build_url_copy_command_with_creds(self, src, file_system):
"""Build copy command for URL-based transfers with credentials (HTTP/HTTPS/SCP/FTP/SFTP)."""
parsed = urlparse(src.download_url)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We probably need to fail if parsed.query is not blank since a url with a query contains a ? which doesn't work in the EOS command line.

raise ValueError(f"Unsupported scheme: {src.scheme}")

# Build command based on scheme and credentials
command_builders = {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If include_username is not required we would probably want to do something like this:

if src.scheme == "tftp" or src.username is None:
    command, detect_prompt = self._build_url_copy_command_simple(src, file_system)
else:
    command, detect_prompt = self._build_url_copy_command_with_creds(src, file_system)

If they didn't provide credentials and the copy fails, we'll bubble up that failure to the user.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

May also need to account for src.username is None and src.token is not None for URLs like https://token@example.com/file.bin

device_checksum = (
self.get_remote_checksum(filename, hashing_algorithm=hashing_algorithm, **kwargs) if exists else None
)
if checksum == device_checksum:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
if checksum == device_checksum:
if checksum.lower() == device_checksum.lower():

Returns:
(bool): True if the file is verified successfully, False otherwise.
"""
exists = self.check_file_exists(filename, **kwargs)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
exists = self.check_file_exists(filename, **kwargs)
if not self.check_file_exists(filename, **kwargs):
log.debug("...") # File not found log message here
return False

Comment on lines +478 to +481
with pytest.raises(TypeError) as exc_info:
device.remote_file_copy(src)

assert "src must be an instance of FileCopyModel" in str(exc_info.value)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

99% of the apps ecosystem is unittest and self.assertRaises. Lets switch this over to those patterns for consistency.

Comment on lines +442 to +462
try:
from hypothesis import given
from hypothesis import strategies as st
except ImportError:
# Create dummy decorators if hypothesis is not available
def given(*args, **kwargs):
def decorator(func):
return func

return decorator

class _ST:
@staticmethod
def just(value):
return value

@staticmethod
def one_of(*args):
return args[0]

st = _ST()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should stick with patterns that the rest of the team maintaining this is familiar with.

jtdub added a commit to jtdub/pyntc that referenced this pull request Apr 14, 2026
itdependsnetworks pushed a commit that referenced this pull request Apr 14, 2026
* Updates to the EOS driver based on PR comments from #365

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants