Skip to content

Commit

Permalink
Merge pull request #62 from bartfeenstra/test-ssh
Browse files Browse the repository at this point in the history
Test the SSH integration using a Docker container running an SSH server.
  • Loading branch information
bartfeenstra committed May 27, 2018
2 parents 78767a1 + 31e9ac7 commit c277b84
Show file tree
Hide file tree
Showing 11 changed files with 160 additions and 8 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -5,3 +5,6 @@
/dist
/docs-build
*.pyc

### Third-party files.
/bin/wait-for-it
6 changes: 5 additions & 1 deletion .travis.yml
Expand Up @@ -6,12 +6,16 @@ python:
- "3.6"
- "pypy"

cache: pip
services:
- docker

addons:
apt:
packages:
- libnotify-bin
- sshpass

cache: pip

install:
- ./bin/build-dev
Expand Down
19 changes: 15 additions & 4 deletions backuppy/location.py
Expand Up @@ -208,28 +208,32 @@ def snapshot(self, name):
class SshTarget(Target):
"""Provide a target over SSH."""

def __init__(self, notifier, user, host, path, port=22):
def __init__(self, notifier, user, host, path, port=22, identity=None, host_keys=None):
"""Initialize a new instance.
:param user: str
:param host: str
:param path: str
:param port: int
:param identity: Optional[str]
:param host_keys: Optional[str]
"""
self._notifier = notifier
self._user = user
self._host = host
self._port = port
self._path = path
self._identity = identity
self._host_keys = host_keys

def is_available(self):
"""Check if the target is available.
:return: bool
"""
try:
self._connect()
return True
with self._connect():
return True
except SSHException:
self._notifier.alert(
'Could not establish an SSH connection to the remote.')
Expand All @@ -254,8 +258,15 @@ def _connect(self):
"""
client = paramiko.SSHClient()
client.load_system_host_keys()
if self._host_keys:
client.load_host_keys(self._host_keys)
client.set_missing_host_key_policy(RejectPolicy())
client.connect(self._host, self._port, self._user, timeout=9)
connect_args = {}
if self._identity:
connect_args['look_for_keys'] = False
connect_args['key_filename'] = self._identity
client.connect(self._host, self._port, self._user,
timeout=9, **connect_args)
return client

@property
Expand Down
6 changes: 4 additions & 2 deletions backuppy/tests/__init__.py
@@ -1,4 +1,6 @@
import os

CONFIGURATION_PATH = '/'.join(
(os.path.dirname(os.path.abspath(__file__)), 'resources', 'configuration'))
RESOURCE_PATH = '/'.join(
(os.path.dirname(os.path.abspath(__file__)), 'resources'))

CONFIGURATION_PATH = '/'.join((RESOURCE_PATH, 'configuration'))
27 changes: 27 additions & 0 deletions backuppy/tests/resources/id_rsa
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAzyLkx0ipceD5nCCYd9tAoeBKE1vAjBitJ7x6e1DMSUh6Ypgz
QQz/So+gb8/2Kq3SyHGSX3+i3jshOCePOssf0hHtrT11SdrlT69puGed1hFPgRB2
NZsInFV6zwjqHadWoRhgSKrnSbOhw/KEByfYRF8ywyG754l6zCKO2YIVDkqZ9qRp
NF0AvOzLFOA97++1v5Kyy8Fk4bm1cdmvJoguX2+TyM4W3IgoTgduteqEKhfwW3RK
Gj0cyLMov6E7OF87Tz8CQ+9kgSBulSRr7QF+SV5JsdM15EFLati6KCA7T7n3nJWj
cJ2Sv7H7m2YWP32b2R79pkhd1zNCFt+6xrl45wIDAQABAoIBABdriJaHvrDjkRV4
EgUrQymKZJW/wAuXFqzxgJ/EyPRYP446S0FFqO/TQX6i8uBsevKy9KvbkJyz7tOc
lXM+WEC2SjtWQZayK09RNBDnlk8H8gdTxynUd6rFd3dFOMNVklPwn6JXwILyzo2L
NCZi+O7yHJk3jTlmr/24wpIRRTQyQxYRYnYPVqQqiG/XNgxFiv9DP9+AlJq+vKOr
M+FwouySCNEt+nPttT+5fMv6BJW8QbHVk95l7VHmEZFIgrIRT9ZpJdDTOcTUeEQV
UjHYMmP9UJaRp1uDyOWbzl8dvOtRsbF4H/CncTr3zis0oHab/cVv9sQ9vVK1F6jk
LeCMl/ECgYEA6Z7xF6celwsYFPpzgucEOazAFpcjy8R+ssJOOdxjSd5gzgibdfUg
UhffdY/rSK0v1oh1gV3cZNXXmuatW9r4icxCgjeGASZ+RgX70EPd3vGl78jrersy
rNqx1i4YOQjrxvdal2dikH510NRVXXp0HXOM50rY7468p1GQcYhUg70CgYEA4vp5
VDJSFLlSYDRHWoaeiYT14P+93LaTroNUMnOnIClF3OzOYhlbOHxDUTHsJHzO+D2Q
7pnn8uS20EVce/wu3gTaOIkGSMAOzuw+tYgHWqeslcAzDE6+dDpy5Yi12k18jYoD
YE5ZABlDCZOha3pULFSmvWx8BXMDEE2kLaY6p3MCgYEA076DtnR6fVxIz3rRB2xr
n/q7f74ta5sFWvBSBo+CTomIJDYY8ajjSoTovJ1dG9oc6c1083QnNh279WHu7rph
WkQQZAX/JzvEZ6M0wWdBybgsNlFdXTgejh0J4p6Uxd0YFpZLPb8uzthP4J8TYE6E
v8zjgR+N0FMHGoAK81wlfeECgYEA3Vxf9ZA54sI2J2L078F4Xi/QyEVCOj3JW5nz
BF0scb4ux14fjSajgzwVPtuMLK2YABuy/DAXORh7fjUXFEgGwTpERHzGJy8/geh+
4/WtDmmWdFmEr40gSyQxp8+jYyrMvRELZ+IhBGqeqXlUJQihjLZmAzkI3xuiskS2
SFrkuycCgYBJExulQbJqKUXEjsFDdigBemDbVc/OMraewpks0wsZ6uuvS5ck/3Yu
Eg5ofbsScFWSm7LXE0v+8n0clKPdzK00d5KdRlSQnOOr6epntEeS62LrbVvIljLQ
A/eIs2580w3B+PQkxnoPAwGW3q0zP/ysIpKZMhuacS0e4K1DYWT+ZA==
-----END RSA PRIVATE KEY-----
1 change: 1 addition & 0 deletions backuppy/tests/resources/id_rsa.pub
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDPIuTHSKlx4PmcIJh320Ch4EoTW8CMGK0nvHp7UMxJSHpimDNBDP9Kj6Bvz/YqrdLIcZJff6LeOyE4J486yx/SEe2tPXVJ2uVPr2m4Z53WEU+BEHY1mwicVXrPCOodp1ahGGBIqudJs6HD8oQHJ9hEXzLDIbvniXrMIo7ZghUOSpn2pGk0XQC87MsU4D3v77W/krLLwWThubVx2a8miC5fb5PIzhbciChOB2616oQqF/BbdEoaPRzIsyi/oTs4XztPPwJD72SBIG6VJGvtAX5JXkmx0zXkQUtq2LooIDtPufeclaNwnZK/sfubZhY/fZvZHv2mSF3XM0IW37rGuXjn bart@Graystone
91 changes: 91 additions & 0 deletions backuppy/tests/test_location.py
Expand Up @@ -8,12 +8,15 @@

from backuppy.location import PathLocation, SshTarget, FirstAvailableTarget, _new_snapshot_args, PathTarget
from backuppy.notifier import Notifier
from backuppy.tests import RESOURCE_PATH

try:
from unittest.mock import Mock, patch
except ImportError:
from mock import Mock, patch

from tempfile import NamedTemporaryFile

try:
from tempfile import TemporaryDirectory
except ImportError:
Expand Down Expand Up @@ -151,6 +154,94 @@ def test_snapshot_without_name(self, m):
m.return_value.__enter__().exec_command.assert_any_call(' '.join(args))


class Container(object):
NAME = 'backuppy_test'
PORT = 22
USERNAME = 'root'
PASSWORD = 'root'
IDENTITY = os.path.join(RESOURCE_PATH, 'id_rsa')

def __init__(self):
self._started = False
self._ip = None
self._fingerprint = None

def _ensure_started(self):
if not self._started:
raise RuntimeError('This container has not been started yet.')

def start(self):
self.stop()
subprocess.call(['docker', 'run', '-d', '--name',
self.NAME, 'rastasheep/ubuntu-sshd:18.04'])
self._started = True
self.await()
with self.known_hosts() as f:
subprocess.call(['sshpass', '-p', self.PASSWORD, 'scp', '-o', 'UserKnownHostsFile=%s' %
f.name, '%s.pub' % self.IDENTITY, '%s@%s:~/.ssh/authorized_keys' % (self.USERNAME, self.ip)])

def stop(self):
self._started = False
subprocess.call(['docker', 'stop', self.NAME])
subprocess.call(['docker', 'container', 'rm', self.NAME])

@property
def ip(self):
self._ensure_started()

if not self._ip:
self._ip = str(subprocess.check_output(
['docker', 'inspect', '-f', '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}',
self.NAME]).strip().decode('utf-8'))

return self._ip

@property
def fingerprint(self):
self._ensure_started()

if not self._fingerprint:
self._fingerprint = str(subprocess.check_output(
['ssh-keyscan', '-t', 'rsa', self.ip]).decode('utf-8'))

return self._fingerprint

def known_hosts(self):
f = NamedTemporaryFile(mode='r+')
f.write(self.fingerprint)
f.flush()
return f

def await(self):
subprocess.call(['./bin/wait-for-it', '%s:%d' % (self.ip, self.PORT)])


class SshTargetIntegrationTest(TestCase):
def setUp(self):
self._container = Container()
self._container.start()

def tearDown(self):
self._container.stop()

def test_is_available(self):
notifier = Mock(Notifier)
path = '/var/cache'
with self._container.known_hosts() as f:
sut = SshTarget(notifier, self._container.USERNAME, self._container.ip, path,
self._container.PORT, identity=self._container.IDENTITY, host_keys=f.name)
self.assertTrue(sut.is_available())

def test_snapshot_without_name(self):
snapshot_name = 'foo_bar'
notifier = Mock(Notifier)
path = '/var/cache'
with self._container.known_hosts() as f:
sut = SshTarget(notifier, self._container.USERNAME, self._container.ip, path,
self._container.PORT, identity=self._container.IDENTITY, host_keys=f.name)
sut.snapshot(snapshot_name)


class FirstAvailableTargetTest(TestCase):
def test_to_rsync(self):
logger = getLogger(__name__)
Expand Down
9 changes: 9 additions & 0 deletions bin/.build-common
@@ -0,0 +1,9 @@
#!/usr/bin/env bash

cd `dirname "$0"`/..

# Set up wait-for-it.
if [ ! -f ./bin/wait-for-it ]; then
curl https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh > ./bin/wait-for-it
chmod ugo+rx ./bin/wait-for-it
fi
1 change: 1 addition & 0 deletions bin/build
@@ -1,4 +1,5 @@
#!/usr/bin/env bash

cd `dirname "$0"`/..
./bin/.build-common
pip install .
1 change: 1 addition & 0 deletions bin/build-dev
@@ -1,5 +1,6 @@
#!/usr/bin/env bash

cd `dirname "$0"`/..
./bin/.build-common
pip install -e .
pip install -r requirements-dev.txt
4 changes: 3 additions & 1 deletion requirements-dev.txt
Expand Up @@ -3,11 +3,13 @@ autopep8 ~= 1.3
backports.tempfile ~= 1.0; python_version < "3.4"
coverage ~= 4.4.1
coveralls ~= 1.2.0
flake8 ~= 3.3.0
flake8 ~= 3.5.0
m2r ~= 0.1.13
mock ~= 2.0.0
nose2 ~= 0.6.5
parameterized ~= 0.6.1
# Depend on an older version, until https://gitlab.com/pycqa/flake8/merge_requests/230 has been release in flake8 > 3.5.0.
pycodestyle ~= 2.3.1
pydocstyle ~= 2.1.1
recommonmark ~= 0.4.0
sphinx ~= 1.7.1
Expand Down

0 comments on commit c277b84

Please sign in to comment.