Source code for exec_helpers.ssh

#    Copyright 2018 - 2023 Aleksei Stepanov aka penguinolog.

#    Copyright 2013 - 2016 Mirantis, Inc.

#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at

#         http://www.apache.org/licenses/LICENSE-2.0

#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

"""SSH client helper based on Paramiko. Extended API helpers."""

from __future__ import annotations

import os
import pathlib
import typing

from . import _ssh_base

if typing.TYPE_CHECKING:
    from ._ssh_base import SupportPathT

__all__ = ("SSHClient",)


[docs] class SSHClient(_ssh_base.SSHClientBase): """SSH Client helper.""" __slots__ = () @staticmethod def _path_esc(path: str) -> str: """Escape space character in the path. :param path: path to be escaped :type path: str :return: path with escaped spaces :rtype: str """ return path.replace(" ", r"\ ")
[docs] def mkdir(self, path: SupportPathT) -> None: """Run 'mkdir -p path' on remote. :param path: path to create :type path: str | pathlib.PurePath """ if self.exists(path): return # noinspection PyTypeChecker self.execute(f"mkdir -p {self._path_esc(pathlib.PurePath(path).as_posix())}\n")
[docs] def rm_rf(self, path: SupportPathT) -> None: """Run 'rm -rf path' on remote. :param path: path to remove :type path: str | pathlib.PurePath """ # noinspection PyTypeChecker self.execute(f"rm -rf {self._path_esc(pathlib.PurePath(path).as_posix())}")
[docs] def upload(self, source: SupportPathT, target: SupportPathT) -> None: """Upload file(s) from source to target using SFTP session. :param source: local path :type source: str | pathlib.PurePath :param target: remote path :type target: str | pathlib.PurePath """ self.logger.debug(f"Copying '{source}' -> '{target}'") source_orig = pathlib.Path(source).expanduser() tgt = pathlib.PurePath(target) # Remote -> No FS access, system agnostic if self.isdir(target): tgt = tgt.joinpath(source_orig.name) src = source_orig.resolve() if not src.is_dir(): self._sftp.put(src.as_posix(), tgt.as_posix(), confirm=True) return for pth in src.glob("**/*"): relative = pth.relative_to(src).as_posix() destination: str = os.path.normpath(tgt.joinpath(relative).as_posix()).replace("\\", "/") if pth.is_dir(): self.mkdir(destination) continue if self.exists(destination): self._sftp.unlink(destination) self._sftp.put(pth.as_posix(), destination, confirm=True)
[docs] def download(self, destination: SupportPathT, target: SupportPathT) -> bool: """Download file(s) to target from destination. :param destination: remote path :type destination: str | pathlib.PurePath :param target: local path :type target: str | pathlib.PurePath :return: downloaded file present on local filesystem :rtype: bool """ self.logger.debug(f"Copying '{destination}' -> '{target}' from remote to local host") tgt = pathlib.Path(target).expanduser().resolve() dst = pathlib.PurePath(destination) if tgt.is_dir(): tgt = tgt.joinpath(dst.name) if not self.isdir(destination): if self.exists(destination): self._sftp.get(dst.as_posix(), tgt.as_posix()) else: self.logger.debug(f"Can't download {destination} because it does not exist") else: self.logger.debug(f"Can't download {destination} because it is a directory") return tgt.exists()