Reputation: 719
I am writing some functions, using paramiko, to execute commands and create files on a remote host. I would like to write some unit tests for them, but I don't know what would be the simplest way to achieve this? This is what I envisage as being an example outline of my code:
import os
import paramiko
import pytest
def my_function(hostname, relpath='.', **kwargs):
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(hostname, **kwargs)
sftp = ssh.open_sftp()
sftp.chdir(relpath)
stdin, stdout, stderr = ssh.exec_command("echo hallo > test.txt")
@pytest.fixture("module")
def mock_remote_host():
# start a remote host here with a local test path
try:
yield hostname, testpath, {"username":"bob", "password":"1234"}
finally:
# delete the test path
# close the remote host
def test_my_function(mock_remote_host):
hostname, dirpath, kwargs = mock_remote_host
my_function(hostname, **kwargs)
filepath = os.path.join(dirpath, 'test.txt')
assert os.path.exists(filepath)
I have had a look at the paramiko test modules, but they seem quite complex for my use case and I'm not sure how to go about simplifying them.
Upvotes: 6
Views: 13334
Reputation: 23
I didn't find any good solutions to my use-case in any of the threads I could find. This one seemed like the most relevant one, so I will post my solution here. Obviously, it is a bit different since it seems that the thread here only relates to an SSH host, but that should just be simpler. Basically the only difference should be to use a different Docker Image.
I am/was developing an LFTP-wrapper for Python, to facility on-the-fly datastreaming with high-level code (this is just a minimal example I pulled from the project README):
# Set the environment variables (only necessary in a non-interactive setting)
# If you are simply running this as a Python script,
# you can omit these lines and you will be prompted to set them interactively
import os
os.environ["PYREMOTEDATA_REMOTE_USERNAME"] = "username"
os.environ["PYREMOTEDATA_REMOTE_URI"] = "storage.example.com"
os.environ["PYREMOTEDATA_REMOTE_DIRECTORY"] = "/MY_PROJECT/DATASETS"
os.environ["PYREMOTEDATA_AUTO"] = "yes"
from pyremotedata.implicit_mount import IOHandler
handler = IOHandler()
with handler as io:
print(io.ls())
# The configuration is persistent, but can be removed using the following:
from pyremotedata.config import remove_config
remove_config()
However, writing unit tests for this code, was pretty hard, since in order to create the most realistic scenario, I felt I needed to mock an SFTP server. Well long story short, I found the easiest way was to start a Docker Container from atmoz/sftp and provide it with a temporary SSH key generated and removed during testing. If anyone else needs this for their project, it can be done like this:
# SSH setup
mkdir -p /tmp # Ensure /tmp exists, mostly for sanity
ssh-keygen -t rsa -b 4096 -f /tmp/temp_sftp_key -q -N "" # Create a temporary SSH key
eval $(ssh-agent -s) # Start the ssh-agent
ssh-add /tmp/temp_sftp_key # Add the temporary SSH key to the ssh-agent
# echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK" >> $GITHUB_ENV # Needed for GitHub Actions, not for local testing
# Mock SFTP server setup
mkdir -p /tmp/upload # Create a temporary directory which will serve as the root of the mock SFTP server file system
chmod ugo+rwx /tmp/upload # Set flexible permissions to avoid invalid permissions issues, which shouldn't arise with a properly set up production SFTP server
sudo docker run --name mock_sftp_server -p 0.0.0.0:2222:22 -d \ # Basic Docker Container setup
-v /tmp/temp_sftp_key.pub:/home/foo/.ssh/keys/temp_sftp_key.pub:ro \ # Add the temporary key to authorized_keys on the mock SFTP server
-v /tmp/upload:/home/foo/upload \ # Add the temporary SFTP server directory for file manipulation on the mock SFTP server
atmoz/sftp foo::1001 # Get the atmoz/sftp image and add a user "foo" without a password
for i in {1..10}; do # Tries to add the SSH fingerprints to known_hosts a few times, since the Docker Container may need a little time to come up and running properly
ssh-keyscan -p 2222 -H 0.0.0.0 >> ~/.ssh/known_hosts && break
echo "Waiting for SFTP server to be ready..."
sleep 1 # wait for 1 second before retrying
done
# Run your tests here
python3 -m unittest discover tests # or however else you want to run your tests
# Clean the environment
ssh-keygen -R 0.0.0.0 # Remove the SSH fingerprints from known_hosts
sudo docker stop mock_sftp_server # Stop the Docker Container
sudo docker rm mock_sftp_server # Remove the Docker Container
ssh-add -d /tmp/temp_sftp_key # Remove the temporary SSH key from the ssh-agent
rm /tmp/temp_sftp_key /tmp/temp_sftp_key.pub # Delete the temporary SSH key
eval "$(ssh-agent -k)" # Kill the ssh-agent
rm -rf /tmp/upload # Remove the temporary directory
Upvotes: 0
Reputation: 3725
I agree with HraBal, because of "Infrastructure as code". You can treat virtual machine as a block of code.
For example:
target domain 127.0.0.1
target domain
and test what you want.I think it is the benefit that you can do this for all programming languages and not need to reinvent the wheel . In addition, you and your successors will know the detail of the system.
(My English is not very good)
Upvotes: 0
Reputation: 719
To answer my own question, I have created: https://github.com/chrisjsewell/atomic-hpc/tree/master/atomic_hpc/mockssh.
As the readme discusses; it is based on https://github.com/carletes/mock-ssh-server/tree/master/mockssh with additions made (to implement more sftp functions) based on https://github.com/rspivak/sftpserver
The following changes have also been made:
users
parameter, such that either a private_path_key or password can be useddirname
parameter to the Server
context manager, such that the this will be set as the root path for the duration of the context.paramiko.sftp_client.SFTPClient.chdir
to fix its use with relative paths.See test_mockssh.py for example uses.
Upvotes: 2
Reputation: 5709
I think what you really need to mock is paramiko.SSHClient
object. You are unittesting your function my_function
, you can assume paramiko
module works correctly and the only thing you need to unit test is if my_function
calls methods of this paramiko.SSHClient
in correct way.
To mock paramiko.SSH
module you can use unittest.mock and decorate your test_my_function
function with @mock.patch.object(paramiko.SSHClient, sshclientmock)
. You have to define sshclientmock
as some kind of Mock
or MagicMock
first.
Also in python 2.7 there is some equivalent of unittest.mock
but I dont remember where to find it exactly.
EDIT: As @chepner mentioned in comment. For python 2.7 you can find mock
module in pypi and install it using pip install mock
Upvotes: 5
Reputation: 2523
If you want to test remote connectivity, remote filesystem structure and remote path navigation you have to set-up a mock host server (a VM maybe). In other words if you want to test your actions on the host you have to mock the host.
If you want to test your actions with the data of the host the easiest way seems to proceed as running.t said in the other answer.
Upvotes: 0