1
# Easy file synchronization among peer units using ssh + unison.
3
# From *both* peer relation -joined and -changed, add a call to
4
# ssh_authorized_peers() describing the peer relation and the desired
5
# user + group. After all peer relations have settled, all hosts should
6
# be able to connect to on another via key auth'd ssh as the specified user.
8
# Other hooks are then free to synchronize files and directories using
11
# For a peer relation named 'cluster', for example:
13
# cluster-relation-joined:
15
# ssh_authorized_peers(peer_interface='cluster',
16
# user='juju_ssh', group='juju_ssh',
20
# cluster-relation-changed:
22
# ssh_authorized_peers(peer_interface='cluster',
23
# user='juju_ssh', group='juju_ssh',
27
# Hooks are now free to sync files as easily as:
29
# files = ['/etc/fstab', '/etc/apt.conf.d/']
30
# sync_to_peers(peer_interface='cluster',
31
# user='juju_ssh, paths=[files])
33
# It is assumed the charm itself has setup permissions on each unit
34
# such that 'juju_ssh' has read + write permissions. Also assumed
35
# that the calling charm takes care of leader delegation.
37
# Additionally files can be synchronized only to an specific unit:
38
# sync_to_peer(slave_address, user='juju_ssh',
39
# paths=[files], verbose=False)
45
from subprocess import check_call, check_output
47
from charmhelpers.core.host import (
52
from charmhelpers.core.hookenv import (
63
BASE_CMD = ['unison', '-auto', '-batch=true', '-confirmbigdel=false',
64
'-fastcheck=true', '-group=false', '-owner=false',
65
'-prefer=newer', '-times=true']
68
def get_homedir(user):
70
user = pwd.getpwnam(user)
73
log('Could not get homedir for user %s: user exists?', ERROR)
77
def create_private_key(user, priv_key_path):
78
if not os.path.isfile(priv_key_path):
79
log('Generating new SSH key for user %s.' % user)
80
cmd = ['ssh-keygen', '-q', '-N', '', '-t', 'rsa', '-b', '2048',
84
log('SSH key already exists at %s.' % priv_key_path)
85
check_call(['chown', user, priv_key_path])
86
check_call(['chmod', '0600', priv_key_path])
89
def create_public_key(user, priv_key_path, pub_key_path):
90
if not os.path.isfile(pub_key_path):
91
log('Generating missing ssh public key @ %s.' % pub_key_path)
92
cmd = ['ssh-keygen', '-y', '-f', priv_key_path]
93
p = check_output(cmd).strip()
94
with open(pub_key_path, 'wb') as out:
96
check_call(['chown', user, pub_key_path])
99
def get_keypair(user):
100
home_dir = get_homedir(user)
101
ssh_dir = os.path.join(home_dir, '.ssh')
102
priv_key = os.path.join(ssh_dir, 'id_rsa')
103
pub_key = '%s.pub' % priv_key
105
if not os.path.isdir(ssh_dir):
107
check_call(['chown', '-R', user, ssh_dir])
109
create_private_key(user, priv_key)
110
create_public_key(user, priv_key, pub_key)
112
with open(priv_key, 'r') as p:
113
_priv = p.read().strip()
115
with open(pub_key, 'r') as p:
116
_pub = p.read().strip()
121
def write_authorized_keys(user, keys):
122
home_dir = get_homedir(user)
123
ssh_dir = os.path.join(home_dir, '.ssh')
124
auth_keys = os.path.join(ssh_dir, 'authorized_keys')
125
log('Syncing authorized_keys @ %s.' % auth_keys)
126
with open(auth_keys, 'wb') as out:
128
out.write('%s\n' % k)
131
def write_known_hosts(user, hosts):
132
home_dir = get_homedir(user)
133
ssh_dir = os.path.join(home_dir, '.ssh')
134
known_hosts = os.path.join(ssh_dir, 'known_hosts')
137
cmd = ['ssh-keyscan', '-H', '-t', 'rsa', host]
138
remote_key = check_output(cmd).strip()
139
khosts.append(remote_key)
140
log('Syncing known_hosts @ %s.' % known_hosts)
141
with open(known_hosts, 'wb') as out:
143
out.write('%s\n' % host)
146
def ensure_user(user, group=None):
149
add_user_to_group(user, group)
152
def ssh_authorized_peers(peer_interface, user, group=None,
153
ensure_local_user=False):
155
Main setup function, should be called from both peer -changed and -joined
156
hooks with the same parameters.
158
if ensure_local_user:
159
ensure_user(user, group)
160
priv_key, pub_key = get_keypair(user)
162
if hook == '%s-relation-joined' % peer_interface:
163
relation_set(ssh_pub_key=pub_key)
164
elif hook == '%s-relation-changed' % peer_interface:
168
for r_id in relation_ids(peer_interface):
169
for unit in related_units(r_id):
170
ssh_pub_key = relation_get('ssh_pub_key',
173
priv_addr = relation_get('private-address',
177
keys.append(ssh_pub_key)
178
hosts.append(priv_addr)
180
log('ssh_authorized_peers(): ssh_pub_key '
181
'missing for unit %s, skipping.' % unit)
182
write_authorized_keys(user, keys)
183
write_known_hosts(user, hosts)
184
authed_hosts = ':'.join(hosts)
185
relation_set(ssh_authorized_hosts=authed_hosts)
188
def _run_as_user(user):
190
user = pwd.getpwnam(user)
192
log('Invalid user: %s' % user)
194
uid, gid = user.pw_uid, user.pw_gid
195
os.environ['HOME'] = user.pw_dir
203
def run_as_user(user, cmd):
204
return check_output(cmd, preexec_fn=_run_as_user(user), cwd='/')
207
def collect_authed_hosts(peer_interface):
208
'''Iterate through the units on peer interface to find all that
209
have the calling host in its authorized hosts list'''
211
for r_id in (relation_ids(peer_interface) or []):
212
for unit in related_units(r_id):
213
private_addr = relation_get('private-address',
215
authed_hosts = relation_get('ssh_authorized_hosts',
219
log('Peer %s has not authorized *any* hosts yet, skipping.')
222
if unit_private_ip() in authed_hosts.split(':'):
223
hosts.append(private_addr)
225
log('Peer %s has not authorized *this* host yet, skipping.')
230
def sync_path_to_host(path, host, user, verbose=False):
233
cmd.append('-silent')
235
# removing trailing slash from directory paths, unison
236
# doesn't like these.
237
if path.endswith('/'):
238
path = path[:(len(path) - 1)]
240
cmd = cmd + [path, 'ssh://%s@%s/%s' % (user, host, path)]
243
log('Syncing local path %s to %s@%s:%s' % (path, user, host, path))
244
run_as_user(user, cmd)
246
log('Error syncing remote files')
249
def sync_to_peer(host, user, paths=[], verbose=False):
250
'''Sync paths to an specific host'''
251
[sync_path_to_host(p, host, user, verbose) for p in paths]
254
def sync_to_peers(peer_interface, user, paths=[], verbose=False):
255
'''Sync all hosts to an specific path'''
256
for host in collect_authed_hosts(peer_interface):
257
sync_to_peer(host, user, paths, verbose)