3
# Easy file synchronization among peer units using ssh + unison.
5
# From *both* peer relation -joined and -changed, add a call to
6
# ssh_authorized_peers() describing the peer relation and the desired
7
# user + group. After all peer relations have settled, all hosts should
8
# be able to connect to on another via key auth'd ssh as the specified user.
10
# Other hooks are then free to synchronize files and directories using
13
# For a peer relation named 'cluster', for example:
15
# cluster-relation-joined:
17
# ssh_authorized_peers(peer_interface='cluster',
18
# user='juju_ssh', group='juju_ssh',
22
# cluster-relation-changed:
24
# ssh_authorized_peers(peer_interface='cluster',
25
# user='juju_ssh', group='juju_ssh',
29
# Hooks are now free to sync files as easily as:
31
# files = ['/etc/fstab', '/etc/apt.conf.d/']
32
# sync_to_peers(peer_interface='cluster',
33
# user='juju_ssh, paths=[files])
35
# It is assumed the charm itself has setup permissions on each unit
36
# such that 'juju_ssh' has read + write permissions. Also assumed
37
# that the calling charm takes care of leader delegation.
39
# TODO: Currently depends on the utils.py shipped with the keystone charm.
40
# Either copy required functionality to this library or depend on
41
# something more generic.
45
import lib.utils as utils
51
def get_homedir(user):
53
user = pwd.getpwnam(user)
56
utils.juju_log('INFO',
57
'Could not get homedir for user %s: user exists?')
61
def get_keypair(user):
62
home_dir = get_homedir(user)
63
ssh_dir = os.path.join(home_dir, '.ssh')
64
if not os.path.isdir(ssh_dir):
67
priv_key = os.path.join(ssh_dir, 'id_rsa')
68
if not os.path.isfile(priv_key):
69
utils.juju_log('INFO', 'Generating new ssh key for user %s.' % user)
70
cmd = ['ssh-keygen', '-q', '-N', '', '-t', 'rsa', '-b', '2048',
72
subprocess.check_call(cmd)
74
pub_key = '%s.pub' % priv_key
75
if not os.path.isfile(pub_key):
76
utils.juju_log('INFO', 'Generatring missing ssh public key @ %s.' % \
78
cmd = ['ssh-keygen', '-y', '-f', priv_key]
79
p = subprocess.check_output(cmd).strip()
80
with open(pub_key, 'wb') as out:
82
subprocess.check_call(['chown', '-R', user, ssh_dir])
83
return open(priv_key, 'r').read().strip(), \
84
open(pub_key, 'r').read().strip()
87
def write_authorized_keys(user, keys):
88
home_dir = get_homedir(user)
89
ssh_dir = os.path.join(home_dir, '.ssh')
90
auth_keys = os.path.join(ssh_dir, 'authorized_keys')
91
utils.juju_log('INFO', 'Syncing authorized_keys @ %s.' % auth_keys)
92
with open(auth_keys, 'wb') as out:
97
def write_known_hosts(user, hosts):
98
home_dir = get_homedir(user)
99
ssh_dir = os.path.join(home_dir, '.ssh')
100
known_hosts = os.path.join(ssh_dir, 'known_hosts')
103
cmd = ['ssh-keyscan', '-H', '-t', 'rsa', host]
104
remote_key = subprocess.check_output(cmd).strip()
105
khosts.append(remote_key)
106
utils.juju_log('INFO', 'Syncing known_hosts @ %s.' % known_hosts)
107
with open(known_hosts, 'wb') as out:
109
out.write('%s\n' % host)
112
def ensure_user(user, group=None):
113
# need to ensure a bash shell'd user exists.
117
utils.juju_log('INFO', 'Creating new user %s.%s.' % (user, group))
118
cmd = ['adduser', '--system', '--shell', '/bin/bash', user]
123
subprocess.check_call(['addgroup', group])
124
cmd += ['--ingroup', group]
125
subprocess.check_call(cmd)
128
def ssh_authorized_peers(peer_interface, user, group=None, ensure_local_user=False):
130
Main setup function, should be called from both peer -changed and -joined
131
hooks with the same parameters.
133
if ensure_local_user:
134
ensure_user(user, group)
135
priv_key, pub_key = get_keypair(user)
136
hook = os.path.basename(sys.argv[0])
137
if hook == '%s-relation-joined' % peer_interface:
138
utils.relation_set(ssh_pub_key=pub_key)
140
elif hook == '%s-relation-changed' % peer_interface:
143
for r_id in utils.relation_ids(peer_interface):
144
for unit in utils.relation_list(r_id):
145
settings = utils.relation_get_dict(relation_id=r_id,
147
if 'ssh_pub_key' in settings:
148
keys.append(settings['ssh_pub_key'])
149
hosts.append(settings['private-address'])
151
utils.juju_log('INFO',
152
'ssh_authorized_peers(): ssh_pub_key '\
153
'missing for unit %s, skipping.' % unit)
154
write_authorized_keys(user, keys)
155
write_known_hosts(user, hosts)
156
authed_hosts = ':'.join(hosts)
157
utils.relation_set(ssh_authorized_hosts=authed_hosts)
160
def _run_as_user(user):
162
user = pwd.getpwnam(user)
164
utils.juju_log('INFO', 'Invalid user: %s' % user)
166
uid, gid = user.pw_uid, user.pw_gid
167
os.environ['HOME'] = user.pw_dir
175
def run_as_user(user, cmd):
176
return subprocess.check_output(cmd, preexec_fn=_run_as_user(user), cwd='/')
179
def sync_to_peers(peer_interface, user, paths=[], verbose=False):
180
base_cmd = ['unison', '-auto', '-batch=true', '-confirmbigdel=false',
181
'-fastcheck=true', '-group=false', '-owner=false',
182
'-prefer=newer', '-times=true']
184
base_cmd.append('-silent')
187
for r_id in (utils.relation_ids(peer_interface) or []):
188
for unit in utils.relation_list(r_id):
189
settings = utils.relation_get_dict(relation_id=r_id,
192
authed_hosts = settings['ssh_authorized_hosts'].split(':')
194
print 'unison sync_to_peers: peer has not authorized *any* '\
198
unit_hostname = utils.unit_get('private-address')
200
for authed_host in authed_hosts:
201
if unit_hostname == authed_host:
202
add_host = settings['private-address']
204
hosts.append(settings['private-address'])
206
print 'unison sync_to_peers: peer (%s) has not authorized '\
207
'*this* host yet, skipping.' %\
208
settings['private-address']
211
# removing trailing slash from directory paths, unison
212
# doesn't like these.
213
if path.endswith('/'):
214
path = path[:(len(path) - 1)]
216
cmd = base_cmd + [path, 'ssh://%s@%s/%s' % (user, host, path)]
217
utils.juju_log('INFO', 'Syncing local path %s to %s@%s:%s' %\
218
(path, user, host, path))
220
run_as_user(user, cmd)