Source code for reemote.operations.scp.copy

# Copyright (c) 2025 Kim Jarvis TPF Software Services S.A. kim.jarvis@tpfsystems.com 
# This software is licensed under the MIT License. See the LICENSE file for details.
#
import asyncssh
from reemote.command import Command
from typing import Union, List, Optional
import tempfile
import os


[docs] class Copy: """ A class to handle secure builtin copying between remote hosts using SCP (Secure Copy Protocol). This class provides functionality to copy builtin or directories between remote hosts, from source hosts to destination hosts. It supports various SCP options including preserving builtin attributes and recursive directory copying. Attributes: srcpaths (Union[str, List[str]]): Source builtin or directory path(s). Can be a single string path or a list of paths. Supports host:path format for remote sources. dstpath (str): Destination path where builtin will be copied. Supports host:path format for remote destinations. src_hosts (List[str], optional): List of source host identifiers. If None or empty, operation will attempt to use all available hosts as sources. dst_hosts (List[str], optional): List of destination host identifiers. If None or empty, operation will attempt to use all available hosts as destinations. preserve (bool): If True, preserves builtin modification times, access times, and modes from the original builtin. Defaults to False. recurse (bool): If True, recursively copies entire directories. Defaults to False. block_size (int): Block size used for builtin transfers in bytes. Defaults to 16384. port (int): SSH port to use for connections. Defaults to 22. **Examples:** .. code:: python # Copy builtin from one host to another yield Copy( srcpaths='/home/user/*.txt', dstpath='/home/user/', src_hosts=["10.156.135.16"], dst_hosts=["10.156.135.17"], recurse=True ) # Copy multiple builtin between hosts yield Copy( srcpaths=['/var/log/app.log', '/tmp/debug.log'], dstpath='backup-server:/backup/logs/', src_hosts=["host1", "host2"], dst_hosts=["backup-server"] ) Note: - The copy operation requires proper SSH credentials and permissions on both source and destination hosts - Wildcard patterns are supported in source paths - For host-to-host copies, the operation will be executed from the destination host """ def __init__(self, srcpaths: Union[str, List[str]], dstpath: str, src_hosts: List[str] = None, dst_hosts: List[str] = None, preserve: bool = False, recurse: bool = False, block_size: int = 16384, port: int = 22): self.srcpaths = srcpaths self.dstpath = dstpath self.src_hosts = src_hosts self.dst_hosts = dst_hosts self.preserve = preserve self.recurse = recurse self.block_size = block_size self.port = port def __repr__(self): return f"Copy(srcpaths={self.srcpaths!r}, dstpath={self.dstpath!r}, src_hosts={self.src_hosts!r}, dst_hosts={self.dst_hosts!r}, preserve={self.preserve}, recurse={self.recurse})" @staticmethod async def _copy_callback(host_info, global_info, command, cp, caller): """Static callback method for builtin copy between hosts""" # Validate host_info (matching Download error handling) required_keys = ['host', 'username', 'password'] for key in required_keys: if key not in host_info or host_info[key] is None: raise ValueError(f"Missing or invalid value for '{key}' in host_info.") # Validate caller attributes (matching Download error handling) if caller.srcpaths is None: raise ValueError("The 'srcpaths' attribute of the caller cannot be None.") if caller.dstpath is None: raise ValueError("The 'dstpath' attribute of the caller cannot be None.") # This operation should run on destination hosts to pull builtin from source hosts is_dest_host = (caller.dst_hosts is None or not caller.dst_hosts or host_info["host"] in caller.dst_hosts) if not is_dest_host: return # Only run on destination hosts try: # Create connection parameters for destination host connect_kwargs = { 'host': host_info['host'], 'username': host_info.get('username'), 'password': host_info.get('password'), 'client_keys': host_info.get('client_keys'), 'known_hosts': host_info.get('known_hosts') } # Remove None values connect_kwargs = {k: v for k, v in connect_kwargs.items() if v is not None} # Set port if specified and different from default if caller.port != 22: connect_kwargs['port'] = caller.port print(f"Connecting to destination host {host_info['host']} for copy operation") async with asyncssh.connect(**connect_kwargs) as conn: print(f"Connected successfully to destination host {host_info['host']}") # Prepare source paths with proper host prefixes if isinstance(caller.srcpaths, list): srcpaths = [] for srcpath in caller.srcpaths: # Add source host prefix if not already present if ':' not in srcpath and caller.src_hosts: # Use the first source host if multiple specified source_host = caller.src_hosts[0] srcpaths.append(f"{source_host}:{srcpath}") else: srcpaths.append(srcpath) else: if ':' not in caller.srcpaths and caller.src_hosts: source_host = caller.src_hosts[0] srcpaths = f"{source_host}:{caller.srcpaths}" else: srcpaths = caller.srcpaths # Prepare destination path (local to destination host) if ':' in caller.dstpath: # Extract path if destination has host prefix dstpath = caller.dstpath.split(':', 1)[1] else: dstpath = caller.dstpath print(f"Copying from source: {srcpaths} -> destination: {dstpath} on {host_info['host']}") # Perform the SCP copy operation (pulling from source to destination) await asyncssh.scp( srcpaths, dstpath, preserve=caller.preserve, recurse=caller.recurse, block_size=caller.block_size ) print(f"Copy completed successfully to destination host {host_info['host']}") except (OSError, asyncssh.Error) as exc: print(f'SCP copy failed on destination host {host_info["host"]}: {str(exc)}') # Provide more detailed error information if "Permission denied" in str(exc): print(f"Check that user {host_info.get('username', 'current user')} has:") print(f" - Read permission on source paths: {caller.srcpaths}") print(f" - Write permission on destination path: {caller.dstpath}") print(f" - Proper SSH key authentication configured") raise def execute(self): """ Execute the copy operation between hosts. Yields: Operation: An Operation object that handles the asynchronous copy process. Returns: Command: The operation object with executed and changed flags set to True. """ r = yield Command(f"{self}", local=True, callback=self._copy_callback, caller=self) r.executed = True r.changed = True return r