|
|
|
#!/usr/bin/env python3
|
|
|
|
""" A class you can use to add SSH features to you plugin. Useful for vm_controller/machinery classes """
|
|
|
|
import os.path
|
|
|
|
import socket
|
|
|
|
import time
|
|
|
|
import paramiko
|
|
|
|
|
|
|
|
from fabric import Connection
|
|
|
|
from invoke.exceptions import UnexpectedExit
|
|
|
|
from app.exceptions import NetworkError
|
|
|
|
from plugins.base.plugin_base import BasePlugin
|
|
|
|
|
|
|
|
|
|
|
|
class SSHFeatures(BasePlugin):
|
|
|
|
""" A Mixin class to add SSH features to all kind of VM machinery """
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.config = None
|
|
|
|
super().__init__()
|
|
|
|
self.connection = None
|
|
|
|
|
|
|
|
def get_ip(self):
|
|
|
|
""" Get the IP of a machine, must be overwritten in the machinery class """
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def connect(self):
|
|
|
|
""" Connect to a machine """
|
|
|
|
|
|
|
|
if self.connection is not None:
|
|
|
|
return self.connection
|
|
|
|
|
|
|
|
retries = 10
|
|
|
|
retry_sleep = 10
|
|
|
|
timeout = 30
|
|
|
|
while retries:
|
|
|
|
try:
|
|
|
|
if self.config.os() == "linux":
|
|
|
|
uhp = self.get_ip()
|
|
|
|
self.vprint(f"Connecting to {uhp}", 3)
|
|
|
|
self.connection = Connection(uhp, connect_timeout=timeout)
|
|
|
|
|
|
|
|
if self.config.os() == "windows":
|
|
|
|
args = {}
|
|
|
|
# args = {"key_filename": self.config.ssh_keyfile() or self.v.keyfile(vm_name=self.config.vmname())}
|
|
|
|
if self.config.ssh_keyfile():
|
|
|
|
args["key_filename"] = self.config.ssh_keyfile()
|
|
|
|
if self.config.ssh_password():
|
|
|
|
args["password"] = self.config.ssh_password()
|
|
|
|
self.vprint(args, 3)
|
|
|
|
uhp = self.get_ip()
|
|
|
|
self.vprint(f"IP to connect to: {uhp}", 3)
|
|
|
|
self.connection = Connection(uhp, connect_timeout=timeout, user=self.config.ssh_user(), connect_kwargs=args)
|
|
|
|
except (paramiko.ssh_exception.SSHException, socket.timeout):
|
|
|
|
self.vprint(f"Failed to connect, will retry {retries} times. Timeout: {timeout}", 0)
|
|
|
|
retries -= 1
|
|
|
|
timeout += 10
|
|
|
|
time.sleep(retry_sleep)
|
|
|
|
else:
|
|
|
|
self.vprint(f"Connection: {self.connection}", 3)
|
|
|
|
return self.connection
|
|
|
|
|
|
|
|
self.vprint("SSH network error", 0)
|
|
|
|
raise NetworkError
|
|
|
|
|
|
|
|
def remote_run(self, cmd: str, disown: bool = False):
|
|
|
|
""" Connects to the machine and runs a command there
|
|
|
|
|
|
|
|
@param cmd: The command to execute
|
|
|
|
@param disown: Send the connection into background
|
|
|
|
"""
|
|
|
|
|
|
|
|
if cmd is None:
|
|
|
|
return ""
|
|
|
|
|
|
|
|
self.connect()
|
|
|
|
cmd = cmd.strip()
|
|
|
|
|
|
|
|
self.vprint("Running SSH remote run: " + cmd, 3)
|
|
|
|
self.vprint("Disown: " + str(disown), 3)
|
|
|
|
# self.vprint("Connection: " + self.connection, 1)
|
|
|
|
result = None
|
|
|
|
retry = 2
|
|
|
|
while retry > 0:
|
|
|
|
do_retry = False
|
|
|
|
try:
|
|
|
|
result = self.connection.run(cmd, disown=disown)
|
|
|
|
print(result)
|
|
|
|
# paramiko.ssh_exception.SSHException in the next line is needed for windows openssh
|
|
|
|
except (paramiko.ssh_exception.NoValidConnectionsError, UnexpectedExit, paramiko.ssh_exception.SSHException) as error:
|
|
|
|
if retry <= 0:
|
|
|
|
raise NetworkError from error
|
|
|
|
do_retry = True
|
|
|
|
if do_retry:
|
|
|
|
self.disconnect()
|
|
|
|
self.connect()
|
|
|
|
retry -= 1
|
|
|
|
self.vprint("Got some SSH errors. Retrying", 2)
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
|
|
|
|
if result and result.stderr:
|
|
|
|
self.vprint("Debug: Stderr: " + str(result.stderr.strip()), 0)
|
|
|
|
|
|
|
|
if result:
|
|
|
|
return result.stdout.strip()
|
|
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
def put(self, src: str, dst: str):
|
|
|
|
""" Send a file to a machine
|
|
|
|
|
|
|
|
@param src: source dir
|
|
|
|
@param dst: destination
|
|
|
|
"""
|
|
|
|
self.connect()
|
|
|
|
|
|
|
|
self.vprint(f"PUT {src} -> {dst}", 3)
|
|
|
|
|
|
|
|
res = ""
|
|
|
|
retries = 10
|
|
|
|
retry_sleep = 10
|
|
|
|
timeout = 30
|
|
|
|
while retries > 0:
|
|
|
|
do_retry = False
|
|
|
|
try:
|
|
|
|
res = self.connection.put(src, dst)
|
|
|
|
except (paramiko.ssh_exception.SSHException, socket.timeout, UnexpectedExit):
|
|
|
|
self.vprint("SSH PUT: Failed to connect", 1)
|
|
|
|
do_retry = True
|
|
|
|
except paramiko.ssh_exception.NoValidConnectionsError as error:
|
|
|
|
self.vprint(f"SSH PUT: No valid connection. Errors: {error.errors}", 1)
|
|
|
|
do_retry = True
|
|
|
|
except FileNotFoundError as error:
|
|
|
|
self.vprint(f"SSH PUT: File not found: {error}", 0)
|
|
|
|
break
|
|
|
|
except OSError:
|
|
|
|
self.vprint("SSH PUT: Obscure OSError, ignoring (file should have been copied)", 1)
|
|
|
|
# do_retry = True
|
|
|
|
# breakpoint()
|
|
|
|
|
|
|
|
if do_retry:
|
|
|
|
self.vprint(f"SSH PUT: Will retry {retries} times. Timeout: {timeout}", 3)
|
|
|
|
retries -= 1
|
|
|
|
timeout += 10
|
|
|
|
time.sleep(retry_sleep)
|
|
|
|
self.disconnect()
|
|
|
|
self.connect()
|
|
|
|
else:
|
|
|
|
return res
|
|
|
|
self.vprint("SSH network error on PUT command", 0)
|
|
|
|
raise NetworkError
|
|
|
|
|
|
|
|
def get(self, src: str, dst: str):
|
|
|
|
""" Get a file to a machine
|
|
|
|
|
|
|
|
@param src: source dir
|
|
|
|
@param dst: destination
|
|
|
|
"""
|
|
|
|
self.connect()
|
|
|
|
res = None
|
|
|
|
|
|
|
|
if os.path.isdir(dst):
|
|
|
|
dst = os.path.join(dst, os.path.basename(src))
|
|
|
|
|
|
|
|
retry = 2
|
|
|
|
while retry > 0:
|
|
|
|
do_retry = False
|
|
|
|
try:
|
|
|
|
res = self.connection.get(src, dst)
|
|
|
|
except (UnexpectedExit) as error:
|
|
|
|
if retry <= 0:
|
|
|
|
raise NetworkError from error
|
|
|
|
do_retry = True
|
|
|
|
except paramiko.ssh_exception.NoValidConnectionsError as error:
|
|
|
|
self.vprint(f"SSH GET: No valid connection. Errors: {error.errors}", 1)
|
|
|
|
do_retry = True
|
|
|
|
except FileNotFoundError as error:
|
|
|
|
self.vprint(error, 0)
|
|
|
|
break
|
|
|
|
except OSError:
|
|
|
|
self.vprint("SSH GET: Obscure OSError, ignoring (file should have been copied)", 1)
|
|
|
|
# do_retry = True
|
|
|
|
if do_retry:
|
|
|
|
self.disconnect()
|
|
|
|
self.connect()
|
|
|
|
retry -= 1
|
|
|
|
self.vprint("Got some SSH errors. Retrying", 2)
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
def disconnect(self):
|
|
|
|
""" Disconnect from a machine """
|
|
|
|
if self.connection:
|
|
|
|
self.connection.close()
|
|
|
|
self.connection = None
|