~niedbalski/charms/trusty/keystone/fix-lp-1308557

« back to all changes in this revision

Viewing changes to hooks/lib/unison.py

  • Committer: Ante Karamatic
  • Date: 2014-02-25 12:29:18 UTC
  • Revision ID: ivoks@ubuntu.com-20140225122918-sq2n9p4k7366r56e
Revert to last stable version. Last three commits were not ready to be merged.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/python
 
2
#
 
3
# Easy file synchronization among peer units using ssh + unison.
 
4
#
 
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.
 
9
#
 
10
# Other hooks are then free to synchronize files and directories using
 
11
# sync_to_peers().
 
12
#
 
13
# For a peer relation named 'cluster', for example:
 
14
#
 
15
# cluster-relation-joined:
 
16
# ...
 
17
# ssh_authorized_peers(peer_interface='cluster',
 
18
#                      user='juju_ssh', group='juju_ssh',
 
19
#                      ensure_user=True)
 
20
# ...
 
21
#
 
22
# cluster-relation-changed:
 
23
# ...
 
24
# ssh_authorized_peers(peer_interface='cluster',
 
25
#                      user='juju_ssh', group='juju_ssh',
 
26
#                      ensure_user=True)
 
27
# ...
 
28
#
 
29
# Hooks are now free to sync files as easily as:
 
30
#
 
31
# files = ['/etc/fstab', '/etc/apt.conf.d/']
 
32
# sync_to_peers(peer_interface='cluster',
 
33
#                user='juju_ssh, paths=[files])
 
34
#
 
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.
 
38
#
 
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.
 
42
 
 
43
import os
 
44
import sys
 
45
import lib.utils as utils
 
46
import subprocess
 
47
import grp
 
48
import pwd
 
49
 
 
50
 
 
51
def get_homedir(user):
 
52
    try:
 
53
        user = pwd.getpwnam(user)
 
54
        return user.pw_dir
 
55
    except KeyError:
 
56
        utils.juju_log('INFO',
 
57
                       'Could not get homedir for user %s: user exists?')
 
58
        sys.exit(1)
 
59
 
 
60
 
 
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):
 
65
        os.mkdir(ssh_dir)
 
66
 
 
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',
 
71
               '-f', priv_key]
 
72
        subprocess.check_call(cmd)
 
73
 
 
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.' % \
 
77
                       pub_key)
 
78
        cmd = ['ssh-keygen', '-y', '-f', priv_key]
 
79
        p = subprocess.check_output(cmd).strip()
 
80
        with open(pub_key, 'wb') as out:
 
81
            out.write(p)
 
82
    subprocess.check_call(['chown', '-R', user, ssh_dir])
 
83
    return open(priv_key, 'r').read().strip(), \
 
84
           open(pub_key, 'r').read().strip()
 
85
 
 
86
 
 
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:
 
93
        for k in keys:
 
94
            out.write('%s\n' % k)
 
95
 
 
96
 
 
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')
 
101
    khosts = []
 
102
    for host in 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:
 
108
        for host in khosts:
 
109
            out.write('%s\n' % host)
 
110
 
 
111
 
 
112
def ensure_user(user, group=None):
 
113
    # need to ensure a bash shell'd user exists.
 
114
    try:
 
115
        pwd.getpwnam(user)
 
116
    except KeyError:
 
117
        utils.juju_log('INFO', 'Creating new user %s.%s.' % (user, group))
 
118
        cmd = ['adduser', '--system', '--shell', '/bin/bash', user]
 
119
        if group:
 
120
            try:
 
121
                grp.getgrnam(group)
 
122
            except KeyError:
 
123
                subprocess.check_call(['addgroup', group])
 
124
            cmd += ['--ingroup', group]
 
125
        subprocess.check_call(cmd)
 
126
 
 
127
 
 
128
def ssh_authorized_peers(peer_interface, user, group=None, ensure_local_user=False):
 
129
    """
 
130
    Main setup function, should be called from both peer -changed and -joined
 
131
    hooks with the same parameters.
 
132
    """
 
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)
 
139
        print 'joined'
 
140
    elif hook == '%s-relation-changed' % peer_interface:
 
141
        hosts = []
 
142
        keys = []
 
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,
 
146
                                                   remote_unit=unit)
 
147
                if 'ssh_pub_key' in settings:
 
148
                    keys.append(settings['ssh_pub_key'])
 
149
                    hosts.append(settings['private-address'])
 
150
                else:
 
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)
 
158
 
 
159
 
 
160
def _run_as_user(user):
 
161
    try:
 
162
        user = pwd.getpwnam(user)
 
163
    except KeyError:
 
164
        utils.juju_log('INFO', 'Invalid user: %s' % user)
 
165
        sys.exit(1)
 
166
    uid, gid = user.pw_uid, user.pw_gid
 
167
    os.environ['HOME'] = user.pw_dir
 
168
 
 
169
    def _inner():
 
170
        os.setgid(gid)
 
171
        os.setuid(uid)
 
172
    return _inner
 
173
 
 
174
 
 
175
def run_as_user(user, cmd):
 
176
    return subprocess.check_output(cmd, preexec_fn=_run_as_user(user), cwd='/')
 
177
 
 
178
 
 
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']
 
183
    if not verbose:
 
184
        base_cmd.append('-silent')
 
185
 
 
186
    hosts = []
 
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,
 
190
                                               remote_unit=unit)
 
191
            try:
 
192
                authed_hosts = settings['ssh_authorized_hosts'].split(':')
 
193
            except KeyError:
 
194
                print 'unison sync_to_peers: peer has not authorized *any* '\
 
195
                      'hosts yet.'
 
196
                return
 
197
 
 
198
            unit_hostname = utils.unit_get('private-address')
 
199
            add_host = None
 
200
            for authed_host in authed_hosts:
 
201
                if unit_hostname == authed_host:
 
202
                    add_host = settings['private-address']
 
203
            if add_host:
 
204
                hosts.append(settings['private-address'])
 
205
            else:
 
206
                print 'unison sync_to_peers: peer (%s) has not authorized '\
 
207
                      '*this* host yet, skipping.' %\
 
208
                       settings['private-address']
 
209
 
 
210
    for path in paths:
 
211
        # removing trailing slash from directory paths, unison
 
212
        # doesn't like these.
 
213
        if path.endswith('/'):
 
214
            path = path[:(len(path) - 1)]
 
215
        for host in hosts:
 
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))
 
219
            print ' '.join(cmd)
 
220
            run_as_user(user, cmd)