483 lines
19 KiB
Python
483 lines
19 KiB
Python
|
|
from mock import call, patch, MagicMock, sentinel
|
|
from testtools import TestCase
|
|
|
|
from tests.helpers import patch_open, FakeRelation
|
|
from charmhelpers.contrib import unison
|
|
|
|
|
|
FAKE_RELATION_ENV = {
|
|
'cluster:0': ['cluster/0', 'cluster/1']
|
|
}
|
|
|
|
|
|
TO_PATCH = [
|
|
'log', 'check_call', 'check_output', 'relation_ids',
|
|
'related_units', 'relation_get', 'relation_set',
|
|
'hook_name', 'unit_private_ip',
|
|
]
|
|
|
|
FAKE_LOCAL_UNIT = 'test_host'
|
|
FAKE_RELATION = {
|
|
'cluster:0': {
|
|
'cluster/0': {
|
|
'private-address': 'cluster0.local',
|
|
'ssh_authorized_hosts': 'someotherhost:test_host'
|
|
},
|
|
'clsuter/1': {
|
|
'private-address': 'cluster1.local',
|
|
'ssh_authorized_hosts': 'someotherhost'
|
|
},
|
|
'clsuter/3': {
|
|
'private-address': 'cluster2.local',
|
|
'ssh_authorized_hosts': 'someotherthirdhost'
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
class UnisonHelperTests(TestCase):
|
|
def setUp(self):
|
|
super(UnisonHelperTests, self).setUp()
|
|
for m in TO_PATCH:
|
|
setattr(self, m, self._patch(m))
|
|
self.fake_relation = FakeRelation(FAKE_RELATION)
|
|
self.unit_private_ip.return_value = FAKE_LOCAL_UNIT
|
|
self.relation_get.side_effect = self.fake_relation.get
|
|
self.relation_ids.side_effect = self.fake_relation.relation_ids
|
|
self.related_units.side_effect = self.fake_relation.related_units
|
|
|
|
def _patch(self, method):
|
|
_m = patch('charmhelpers.contrib.unison.' + method)
|
|
mock = _m.start()
|
|
self.addCleanup(_m.stop)
|
|
return mock
|
|
|
|
@patch('pwd.getpwnam')
|
|
def test_get_homedir(self, pwnam):
|
|
fake_user = MagicMock()
|
|
fake_user.pw_dir = '/home/foo'
|
|
pwnam.return_value = fake_user
|
|
self.assertEquals(unison.get_homedir('foo'),
|
|
'/home/foo')
|
|
|
|
@patch('pwd.getpwnam')
|
|
def test_get_homedir_no_user(self, pwnam):
|
|
e = KeyError
|
|
pwnam.side_effect = e
|
|
self.assertRaises(Exception, unison.get_homedir, user='foo')
|
|
|
|
def _ensure_calls_in(self, calls):
|
|
for _call in calls:
|
|
self.assertIn(call(_call), self.check_call.call_args_list)
|
|
|
|
@patch('os.chmod')
|
|
@patch('os.chown')
|
|
@patch('os.path.isfile')
|
|
@patch('pwd.getpwnam')
|
|
def test_create_private_key_rsa(self, pwnam, isfile, chown, chmod):
|
|
fake_user = MagicMock()
|
|
fake_user.pw_uid = 3133
|
|
pwnam.return_value = fake_user
|
|
create_cmd = [
|
|
'ssh-keygen', '-q', '-N', '', '-t', 'rsa', '-b', '2048',
|
|
'-f', '/home/foo/.ssh/id_rsa']
|
|
|
|
def _ensure_perms():
|
|
chown.assert_called_with('/home/foo/.ssh/id_rsa', 3133, -1)
|
|
chmod.assert_called_with('/home/foo/.ssh/id_rsa', 0o600)
|
|
|
|
isfile.return_value = False
|
|
unison.create_private_key(
|
|
user='foo', priv_key_path='/home/foo/.ssh/id_rsa')
|
|
self.assertIn(call(create_cmd), self.check_call.call_args_list)
|
|
_ensure_perms()
|
|
self.check_call.call_args_list = []
|
|
|
|
chown.reset_mock()
|
|
chmod.reset_mock()
|
|
isfile.return_value = True
|
|
unison.create_private_key(
|
|
user='foo', priv_key_path='/home/foo/.ssh/id_rsa')
|
|
self.assertNotIn(call(create_cmd), self.check_call.call_args_list)
|
|
_ensure_perms()
|
|
|
|
@patch('os.chmod')
|
|
@patch('os.chown')
|
|
@patch('os.path.isfile')
|
|
@patch('pwd.getpwnam')
|
|
def test_create_private_key_ecdsa(self, pwnam, isfile, chown, chmod):
|
|
fake_user = MagicMock()
|
|
fake_user.pw_uid = 3133
|
|
pwnam.return_value = fake_user
|
|
create_cmd = [
|
|
'ssh-keygen', '-q', '-N', '', '-t', 'ecdsa', '-b', '521',
|
|
'-f', '/home/foo/.ssh/id_ecdsa']
|
|
|
|
def _ensure_perms():
|
|
chown.assert_called_with('/home/foo/.ssh/id_ecdsa', 3133, -1)
|
|
chmod.assert_called_with('/home/foo/.ssh/id_ecdsa', 0o600)
|
|
|
|
isfile.return_value = False
|
|
unison.create_private_key(
|
|
user='foo',
|
|
priv_key_path='/home/foo/.ssh/id_ecdsa',
|
|
key_type='ecdsa')
|
|
self.assertIn(call(create_cmd), self.check_call.call_args_list)
|
|
_ensure_perms()
|
|
self.check_call.call_args_list = []
|
|
|
|
chown.reset_mock()
|
|
chmod.reset_mock()
|
|
isfile.return_value = True
|
|
unison.create_private_key(
|
|
user='foo',
|
|
priv_key_path='/home/foo/.ssh/id_ecdsa',
|
|
key_type='ecdsa')
|
|
self.assertNotIn(call(create_cmd), self.check_call.call_args_list)
|
|
_ensure_perms()
|
|
|
|
@patch('os.chown')
|
|
@patch('os.path.isfile')
|
|
@patch('pwd.getpwnam')
|
|
def test_create_public_key(self, pwnam, isfile, chown):
|
|
fake_user = MagicMock()
|
|
fake_user.pw_uid = 3133
|
|
pwnam.return_value = fake_user
|
|
create_cmd = ['ssh-keygen', '-y', '-f', '/home/foo/.ssh/id_rsa']
|
|
|
|
def _ensure_perms():
|
|
chown.assert_called_with('/home/foo/.ssh/id_rsa.pub', 3133, -1)
|
|
|
|
isfile.return_value = True
|
|
unison.create_public_key(
|
|
user='foo', priv_key_path='/home/foo/.ssh/id_rsa',
|
|
pub_key_path='/home/foo/.ssh/id_rsa.pub')
|
|
self.assertNotIn(call(create_cmd), self.check_output.call_args_list)
|
|
_ensure_perms()
|
|
|
|
isfile.return_value = False
|
|
with patch_open() as (_open, _file):
|
|
self.check_output.return_value = b'fookey'
|
|
unison.create_public_key(
|
|
user='foo', priv_key_path='/home/foo/.ssh/id_rsa',
|
|
pub_key_path='/home/foo/.ssh/id_rsa.pub')
|
|
self.assertIn(call(create_cmd), self.check_output.call_args_list)
|
|
_ensure_perms()
|
|
_open.assert_called_with('/home/foo/.ssh/id_rsa.pub', 'wb')
|
|
_file.write.assert_called_with(b'fookey')
|
|
|
|
@patch('os.mkdir')
|
|
@patch('os.path.isdir')
|
|
@patch.object(unison, 'get_homedir')
|
|
@patch.multiple(unison, create_private_key=MagicMock(),
|
|
create_public_key=MagicMock())
|
|
def test_get_keypair(self, get_homedir, isdir, mkdir):
|
|
get_homedir.return_value = '/home/foo'
|
|
isdir.return_value = False
|
|
with patch_open() as (_open, _file):
|
|
_file.read.side_effect = [
|
|
'foopriv', 'foopub'
|
|
]
|
|
priv, pub = unison.get_keypair('adam')
|
|
for f in ['/home/foo/.ssh/id_rsa',
|
|
'/home/foo/.ssh/id_rsa.pub']:
|
|
self.assertIn(call(f, 'r'), _open.call_args_list)
|
|
self.assertEquals(priv, 'foopriv')
|
|
self.assertEquals(pub, 'foopub')
|
|
|
|
@patch.object(unison, 'get_homedir')
|
|
@patch('os.chown')
|
|
@patch('pwd.getpwnam')
|
|
def test_write_auth_keys(self, pwnam, chown, get_homedir):
|
|
fake_user = MagicMock()
|
|
fake_user.pw_uid = 3133
|
|
pwnam.return_value = fake_user
|
|
get_homedir.return_value = '/home/foo'
|
|
keys = [
|
|
'ssh-rsa AAAB3Nz adam',
|
|
'ssh-rsa ALKJFz adam@whereschuck.org',
|
|
]
|
|
|
|
def _ensure_perms():
|
|
chown.assert_called_with('/home/foo/.ssh/authorized_keys', 3133, -1)
|
|
|
|
with patch_open() as (_open, _file):
|
|
unison.write_authorized_keys('foo', keys)
|
|
_open.assert_called_with('/home/foo/.ssh/authorized_keys', 'w')
|
|
for k in keys:
|
|
self.assertIn(call('%s\n' % k), _file.write.call_args_list)
|
|
_ensure_perms()
|
|
|
|
@patch.object(unison, 'get_homedir')
|
|
@patch('os.chown')
|
|
@patch('pwd.getpwnam')
|
|
def test_write_known_hosts(self, pwnam, chown, get_homedir):
|
|
fake_user = MagicMock()
|
|
fake_user.pw_uid = 3133
|
|
pwnam.return_value = fake_user
|
|
get_homedir.return_value = '/home/foo'
|
|
keys = [
|
|
'10.0.0.1 ssh-rsa KJDSJF=',
|
|
'10.0.0.2 ssh-rsa KJDSJF=',
|
|
]
|
|
self.check_output.side_effect = keys
|
|
|
|
def _ensure_perms():
|
|
chown.assert_called_with('/home/foo/.ssh/known_hosts', 3133, -1)
|
|
|
|
with patch_open() as (_open, _file):
|
|
unison.write_known_hosts('foo', ['10.0.0.1', '10.0.0.2'])
|
|
_open.assert_called_with('/home/foo/.ssh/known_hosts', 'w')
|
|
for k in keys:
|
|
self.assertIn(call('%s\n' % k), _file.write.call_args_list)
|
|
_ensure_perms()
|
|
|
|
@patch.object(unison, 'remove_password_expiry')
|
|
@patch.object(unison, 'pwgen')
|
|
@patch.object(unison, 'add_user_to_group')
|
|
@patch.object(unison, 'adduser')
|
|
def test_ensure_user(self, adduser, to_group, pwgen,
|
|
remove_password_expiry):
|
|
pwgen.return_value = sentinel.password
|
|
unison.ensure_user('foo', group='foobar')
|
|
adduser.assert_called_with('foo', sentinel.password)
|
|
to_group.assert_called_with('foo', 'foobar')
|
|
remove_password_expiry.assert_called_with('foo')
|
|
|
|
@patch.object(unison, '_run_as_user')
|
|
def test_run_as_user(self, _run):
|
|
with patch.object(unison, '_run_as_user') as _run:
|
|
fake_preexec = MagicMock()
|
|
_run.return_value = fake_preexec
|
|
unison.run_as_user('foo', ['echo', 'foo'])
|
|
self.check_output.assert_called_with(
|
|
['echo', 'foo'], preexec_fn=fake_preexec, cwd='/')
|
|
|
|
@patch('pwd.getpwnam')
|
|
def test_run_user_not_found(self, getpwnam):
|
|
e = KeyError
|
|
getpwnam.side_effect = e
|
|
self.assertRaises(Exception, unison._run_as_user, 'nouser')
|
|
|
|
@patch('os.setuid')
|
|
@patch('os.setgid')
|
|
@patch('os.environ', spec=dict)
|
|
@patch('pwd.getpwnam')
|
|
def test_run_as_user_preexec(self, pwnam, environ, setgid, setuid):
|
|
fake_env = {'HOME': '/root'}
|
|
environ.__getitem__ = MagicMock()
|
|
environ.__setitem__ = MagicMock()
|
|
environ.__setitem__.side_effect = fake_env.__setitem__
|
|
environ.__getitem__.side_effect = fake_env.__getitem__
|
|
|
|
fake_user = MagicMock()
|
|
fake_user.pw_uid = 1010
|
|
fake_user.pw_gid = 1011
|
|
fake_user.pw_dir = '/home/foo'
|
|
pwnam.return_value = fake_user
|
|
inner = unison._run_as_user('foo')
|
|
self.assertEquals(fake_env['HOME'], '/home/foo')
|
|
inner()
|
|
setgid.assert_called_with(1011)
|
|
setuid.assert_called_with(1010)
|
|
|
|
@patch('os.setuid')
|
|
@patch('os.setgid')
|
|
@patch('os.environ', spec=dict)
|
|
@patch('pwd.getpwnam')
|
|
def test_run_as_user_preexec_with_group(self, pwnam, environ, setgid, setuid):
|
|
fake_env = {'HOME': '/root'}
|
|
environ.__getitem__ = MagicMock()
|
|
environ.__setitem__ = MagicMock()
|
|
environ.__setitem__.side_effect = fake_env.__setitem__
|
|
environ.__getitem__.side_effect = fake_env.__getitem__
|
|
|
|
fake_user = MagicMock()
|
|
fake_user.pw_uid = 1010
|
|
fake_user.pw_gid = 1011
|
|
fake_user.pw_dir = '/home/foo'
|
|
fake_group_id = 2000
|
|
pwnam.return_value = fake_user
|
|
inner = unison._run_as_user('foo', gid=fake_group_id)
|
|
self.assertEquals(fake_env['HOME'], '/home/foo')
|
|
inner()
|
|
setgid.assert_called_with(2000)
|
|
setuid.assert_called_with(1010)
|
|
|
|
@patch.object(unison, 'get_keypair')
|
|
@patch.object(unison, 'ensure_user')
|
|
def test_ssh_auth_peer_joined(self, ensure_user, get_keypair):
|
|
get_keypair.return_value = ('privkey', 'pubkey')
|
|
self.hook_name.return_value = 'cluster-relation-joined'
|
|
unison.ssh_authorized_peers(peer_interface='cluster',
|
|
user='foo', group='foo',
|
|
ensure_local_user=True)
|
|
self.relation_set.assert_called_with(ssh_pub_key='pubkey')
|
|
self.assertFalse(self.relation_get.called)
|
|
ensure_user.assert_called_with('foo', 'foo')
|
|
get_keypair.assert_called_with('foo')
|
|
|
|
@patch.object(unison, 'write_known_hosts')
|
|
@patch.object(unison, 'write_authorized_keys')
|
|
@patch.object(unison, 'get_keypair')
|
|
@patch.object(unison, 'ensure_user')
|
|
def test_ssh_auth_peer_changed(self, ensure_user, get_keypair,
|
|
write_keys, write_hosts):
|
|
get_keypair.return_value = ('privkey', 'pubkey')
|
|
|
|
self.hook_name.return_value = 'cluster-relation-changed'
|
|
|
|
self.relation_get.side_effect = [
|
|
'key1',
|
|
'host1',
|
|
'key2',
|
|
'host2',
|
|
'', ''
|
|
]
|
|
unison.ssh_authorized_peers(peer_interface='cluster',
|
|
user='foo', group='foo',
|
|
ensure_local_user=True)
|
|
|
|
ensure_user.assert_called_with('foo', 'foo')
|
|
get_keypair.assert_called_with('foo')
|
|
write_keys.assert_called_with('foo', ['key1', 'key2'])
|
|
write_hosts.assert_called_with('foo', ['host1', 'host2'])
|
|
self.relation_set.assert_called_with(ssh_authorized_hosts='host1:host2')
|
|
|
|
@patch.object(unison, 'write_known_hosts')
|
|
@patch.object(unison, 'write_authorized_keys')
|
|
@patch.object(unison, 'get_keypair')
|
|
@patch.object(unison, 'ensure_user')
|
|
def test_ssh_auth_peer_departed(self, ensure_user, get_keypair,
|
|
write_keys, write_hosts):
|
|
get_keypair.return_value = ('privkey', 'pubkey')
|
|
|
|
self.hook_name.return_value = 'cluster-relation-departed'
|
|
|
|
self.relation_get.side_effect = [
|
|
'key1',
|
|
'host1',
|
|
'key2',
|
|
'host2',
|
|
'', ''
|
|
]
|
|
unison.ssh_authorized_peers(peer_interface='cluster',
|
|
user='foo', group='foo',
|
|
ensure_local_user=True)
|
|
|
|
ensure_user.assert_called_with('foo', 'foo')
|
|
get_keypair.assert_called_with('foo')
|
|
write_keys.assert_called_with('foo', ['key1', 'key2'])
|
|
write_hosts.assert_called_with('foo', ['host1', 'host2'])
|
|
self.relation_set.assert_called_with(ssh_authorized_hosts='host1:host2')
|
|
|
|
def test_collect_authed_hosts(self):
|
|
# only one of the hosts in fake environment has auth'd
|
|
# the local peer
|
|
hosts = unison.collect_authed_hosts('cluster')
|
|
self.assertEquals(hosts, ['cluster0.local'])
|
|
|
|
def test_collect_authed_hosts_none_authed(self):
|
|
with patch.object(unison, 'relation_get') as relation_get:
|
|
relation_get.return_value = ''
|
|
hosts = unison.collect_authed_hosts('cluster')
|
|
self.assertEquals(hosts, [])
|
|
|
|
@patch.object(unison, 'run_as_user')
|
|
def test_sync_path_to_host(self, run_as_user, verbose=True, gid=None):
|
|
for path in ['/tmp/foo', '/tmp/foo/']:
|
|
unison.sync_path_to_host(path=path, host='clusterhost1',
|
|
user='foo', verbose=verbose, gid=gid)
|
|
ex_cmd = ['unison', '-auto', '-batch=true',
|
|
'-confirmbigdel=false', '-fastcheck=true',
|
|
'-group=false', '-owner=false',
|
|
'-prefer=newer', '-times=true']
|
|
if not verbose:
|
|
ex_cmd.append('-silent')
|
|
ex_cmd += ['/tmp/foo', 'ssh://foo@clusterhost1//tmp/foo']
|
|
run_as_user.assert_called_with('foo', ex_cmd, gid)
|
|
|
|
@patch.object(unison, 'run_as_user')
|
|
def test_sync_path_to_host_error(self, run_as_user):
|
|
for i, path in enumerate(['/tmp/foo', '/tmp/foo/']):
|
|
run_as_user.side_effect = Exception
|
|
if i == 0:
|
|
unison.sync_path_to_host(path=path, host='clusterhost1',
|
|
user='foo', verbose=True, gid=None)
|
|
else:
|
|
self.assertRaises(Exception, unison.sync_path_to_host,
|
|
path=path, host='clusterhost1',
|
|
user='foo', verbose=True, gid=None,
|
|
fatal=True)
|
|
|
|
ex_cmd = ['unison', '-auto', '-batch=true',
|
|
'-confirmbigdel=false', '-fastcheck=true',
|
|
'-group=false', '-owner=false',
|
|
'-prefer=newer', '-times=true',
|
|
'/tmp/foo', 'ssh://foo@clusterhost1//tmp/foo']
|
|
run_as_user.assert_called_with('foo', ex_cmd, None)
|
|
|
|
def test_sync_path_to_host_non_verbose(self):
|
|
return self.test_sync_path_to_host(verbose=False)
|
|
|
|
def test_sync_path_to_host_with_gid(self):
|
|
return self.test_sync_path_to_host(gid=111)
|
|
|
|
@patch.object(unison, 'sync_path_to_host')
|
|
def test_sync_to_peer(self, sync_path_to_host):
|
|
paths = ['/tmp/foo1', '/tmp/foo2']
|
|
host = 'host1'
|
|
unison.sync_to_peer(host, 'foouser', paths, True)
|
|
calls = [call('/tmp/foo1', host, 'foouser', True, None, None, False),
|
|
call('/tmp/foo2', host, 'foouser', True, None, None, False)]
|
|
sync_path_to_host.assert_has_calls(calls)
|
|
|
|
@patch.object(unison, 'sync_path_to_host')
|
|
def test_sync_to_peer_with_gid(self, sync_path_to_host):
|
|
paths = ['/tmp/foo1', '/tmp/foo2']
|
|
host = 'host1'
|
|
unison.sync_to_peer(host, 'foouser', paths, True, gid=111)
|
|
calls = [call('/tmp/foo1', host, 'foouser', True, None, 111, False),
|
|
call('/tmp/foo2', host, 'foouser', True, None, 111, False)]
|
|
sync_path_to_host.assert_has_calls(calls)
|
|
|
|
@patch.object(unison, 'collect_authed_hosts')
|
|
@patch.object(unison, 'sync_to_peer')
|
|
def test_sync_to_peers(self, sync_to_peer, collect_hosts):
|
|
collect_hosts.return_value = ['host1', 'host2', 'host3']
|
|
paths = ['/tmp/foo']
|
|
unison.sync_to_peers(peer_interface='cluster', user='foouser',
|
|
paths=paths, verbose=True)
|
|
calls = [call('host1', 'foouser', ['/tmp/foo'], True, None, None, False),
|
|
call('host2', 'foouser', ['/tmp/foo'], True, None, None, False),
|
|
call('host3', 'foouser', ['/tmp/foo'], True, None, None, False)]
|
|
sync_to_peer.assert_has_calls(calls)
|
|
|
|
@patch.object(unison, 'collect_authed_hosts')
|
|
@patch.object(unison, 'sync_to_peer')
|
|
def test_sync_to_peers_with_gid(self, sync_to_peer, collect_hosts):
|
|
collect_hosts.return_value = ['host1', 'host2', 'host3']
|
|
paths = ['/tmp/foo']
|
|
unison.sync_to_peers(peer_interface='cluster', user='foouser',
|
|
paths=paths, verbose=True, gid=111)
|
|
calls = [call('host1', 'foouser', ['/tmp/foo'], True, None, 111, False),
|
|
call('host2', 'foouser', ['/tmp/foo'], True, None, 111, False),
|
|
call('host3', 'foouser', ['/tmp/foo'], True, None, 111, False)]
|
|
sync_to_peer.assert_has_calls(calls)
|
|
|
|
@patch.object(unison, 'collect_authed_hosts')
|
|
@patch.object(unison, 'sync_to_peer')
|
|
def test_sync_to_peers_with_cmd(self, sync_to_peer, collect_hosts):
|
|
collect_hosts.return_value = ['host1', 'host2', 'host3']
|
|
paths = ['/tmp/foo']
|
|
cmd = ['dummy_cmd']
|
|
unison.sync_to_peers(peer_interface='cluster', user='foouser',
|
|
paths=paths, verbose=True, cmd=cmd, gid=111)
|
|
calls = [call('host1', 'foouser', ['/tmp/foo'], True, cmd, 111, False),
|
|
call('host2', 'foouser', ['/tmp/foo'], True, cmd, 111, False),
|
|
call('host3', 'foouser', ['/tmp/foo'], True, cmd, 111, False)]
|
|
sync_to_peer.assert_has_calls(calls)
|