~brad-marshall/charms/trusty/apache2-wsgi/fix-haproxy-relations

« back to all changes in this revision

Viewing changes to hooks/lib/charmhelpers/contrib/unison/__init__.py

  • Committer: Robin Winslow
  • Date: 2014-05-27 14:00:44 UTC
  • Revision ID: robin.winslow@canonical.com-20140527140044-8rpmb3wx4djzwa83
Add all files

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Easy file synchronization among peer units using ssh + unison.
 
2
#
 
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.
 
7
#
 
8
# Other hooks are then free to synchronize files and directories using
 
9
# sync_to_peers().
 
10
#
 
11
# For a peer relation named 'cluster', for example:
 
12
#
 
13
# cluster-relation-joined:
 
14
# ...
 
15
# ssh_authorized_peers(peer_interface='cluster',
 
16
#                      user='juju_ssh', group='juju_ssh',
 
17
#                      ensure_user=True)
 
18
# ...
 
19
#
 
20
# cluster-relation-changed:
 
21
# ...
 
22
# ssh_authorized_peers(peer_interface='cluster',
 
23
#                      user='juju_ssh', group='juju_ssh',
 
24
#                      ensure_user=True)
 
25
# ...
 
26
#
 
27
# Hooks are now free to sync files as easily as:
 
28
#
 
29
# files = ['/etc/fstab', '/etc/apt.conf.d/']
 
30
# sync_to_peers(peer_interface='cluster',
 
31
#                user='juju_ssh, paths=[files])
 
32
#
 
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.
 
36
#
 
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)
 
40
 
 
41
import os
 
42
import pwd
 
43
 
 
44
from copy import copy
 
45
from subprocess import check_call, check_output
 
46
 
 
47
from charmhelpers.core.host import (
 
48
    adduser,
 
49
    add_user_to_group,
 
50
)
 
51
 
 
52
from charmhelpers.core.hookenv import (
 
53
    log,
 
54
    hook_name,
 
55
    relation_ids,
 
56
    related_units,
 
57
    relation_set,
 
58
    relation_get,
 
59
    unit_private_ip,
 
60
    ERROR,
 
61
)
 
62
 
 
63
BASE_CMD = ['unison', '-auto', '-batch=true', '-confirmbigdel=false',
 
64
            '-fastcheck=true', '-group=false', '-owner=false',
 
65
            '-prefer=newer', '-times=true']
 
66
 
 
67
 
 
68
def get_homedir(user):
 
69
    try:
 
70
        user = pwd.getpwnam(user)
 
71
        return user.pw_dir
 
72
    except KeyError:
 
73
        log('Could not get homedir for user %s: user exists?', ERROR)
 
74
        raise Exception
 
75
 
 
76
 
 
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',
 
81
               '-f', priv_key_path]
 
82
        check_call(cmd)
 
83
    else:
 
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])
 
87
 
 
88
 
 
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:
 
95
            out.write(p)
 
96
    check_call(['chown', user, pub_key_path])
 
97
 
 
98
 
 
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
 
104
 
 
105
    if not os.path.isdir(ssh_dir):
 
106
        os.mkdir(ssh_dir)
 
107
        check_call(['chown', '-R', user, ssh_dir])
 
108
 
 
109
    create_private_key(user, priv_key)
 
110
    create_public_key(user, priv_key, pub_key)
 
111
 
 
112
    with open(priv_key, 'r') as p:
 
113
        _priv = p.read().strip()
 
114
 
 
115
    with open(pub_key, 'r') as p:
 
116
        _pub = p.read().strip()
 
117
 
 
118
    return (_priv, _pub)
 
119
 
 
120
 
 
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:
 
127
        for k in keys:
 
128
            out.write('%s\n' % k)
 
129
 
 
130
 
 
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')
 
135
    khosts = []
 
136
    for host in 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:
 
142
        for host in khosts:
 
143
            out.write('%s\n' % host)
 
144
 
 
145
 
 
146
def ensure_user(user, group=None):
 
147
    adduser(user)
 
148
    if group:
 
149
        add_user_to_group(user, group)
 
150
 
 
151
 
 
152
def ssh_authorized_peers(peer_interface, user, group=None,
 
153
                         ensure_local_user=False):
 
154
    """
 
155
    Main setup function, should be called from both peer -changed and -joined
 
156
    hooks with the same parameters.
 
157
    """
 
158
    if ensure_local_user:
 
159
        ensure_user(user, group)
 
160
    priv_key, pub_key = get_keypair(user)
 
161
    hook = hook_name()
 
162
    if hook == '%s-relation-joined' % peer_interface:
 
163
        relation_set(ssh_pub_key=pub_key)
 
164
    elif hook == '%s-relation-changed' % peer_interface:
 
165
        hosts = []
 
166
        keys = []
 
167
 
 
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',
 
171
                                           rid=r_id,
 
172
                                           unit=unit)
 
173
                priv_addr = relation_get('private-address',
 
174
                                         rid=r_id,
 
175
                                         unit=unit)
 
176
                if ssh_pub_key:
 
177
                    keys.append(ssh_pub_key)
 
178
                    hosts.append(priv_addr)
 
179
                else:
 
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)
 
186
 
 
187
 
 
188
def _run_as_user(user):
 
189
    try:
 
190
        user = pwd.getpwnam(user)
 
191
    except KeyError:
 
192
        log('Invalid user: %s' % user)
 
193
        raise Exception
 
194
    uid, gid = user.pw_uid, user.pw_gid
 
195
    os.environ['HOME'] = user.pw_dir
 
196
 
 
197
    def _inner():
 
198
        os.setgid(gid)
 
199
        os.setuid(uid)
 
200
    return _inner
 
201
 
 
202
 
 
203
def run_as_user(user, cmd):
 
204
    return check_output(cmd, preexec_fn=_run_as_user(user), cwd='/')
 
205
 
 
206
 
 
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'''
 
210
    hosts = []
 
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',
 
214
                                        rid=r_id, unit=unit)
 
215
            authed_hosts = relation_get('ssh_authorized_hosts',
 
216
                                        rid=r_id, unit=unit)
 
217
 
 
218
            if not authed_hosts:
 
219
                log('Peer %s has not authorized *any* hosts yet, skipping.')
 
220
                continue
 
221
 
 
222
            if unit_private_ip() in authed_hosts.split(':'):
 
223
                hosts.append(private_addr)
 
224
            else:
 
225
                log('Peer %s has not authorized *this* host yet, skipping.')
 
226
 
 
227
    return hosts
 
228
 
 
229
 
 
230
def sync_path_to_host(path, host, user, verbose=False):
 
231
    cmd = copy(BASE_CMD)
 
232
    if not verbose:
 
233
        cmd.append('-silent')
 
234
 
 
235
    # removing trailing slash from directory paths, unison
 
236
    # doesn't like these.
 
237
    if path.endswith('/'):
 
238
        path = path[:(len(path) - 1)]
 
239
 
 
240
    cmd = cmd + [path, 'ssh://%s@%s/%s' % (user, host, path)]
 
241
 
 
242
    try:
 
243
        log('Syncing local path %s to %s@%s:%s' % (path, user, host, path))
 
244
        run_as_user(user, cmd)
 
245
    except:
 
246
        log('Error syncing remote files')
 
247
 
 
248
 
 
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]
 
252
 
 
253
 
 
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)