~cjwatson/turku-charms/turku-agent-charm-fix-systemd-harder

« back to all changes in this revision

Viewing changes to files/turku-agent/turku-update-config

  • Committer: Ryan Finnie
  • Date: 2015-02-26 05:38:01 UTC
  • Revision ID: ryan.finnie@canonical.com-20150226053801-td8ul1fphnjqkh3h
Initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
 
 
3
from __future__ import print_function
 
4
import uuid
 
5
import string
 
6
import random
 
7
import json
 
8
import os
 
9
import copy
 
10
import subprocess
 
11
import sys
 
12
import platform
 
13
import time
 
14
 
 
15
CONFIG_D = '/etc/turku-agent/config.d'
 
16
SOURCES_D = '/etc/turku-agent/sources.d'
 
17
SOURCES_SECRETS_D = '/etc/turku-agent/sources_secrets.d'
 
18
SSH_PRIVATE_KEY = '/etc/turku-agent/id_rsa'
 
19
SSH_PUBLIC_KEY = '/etc/turku-agent/id_rsa.pub'
 
20
RSYNCD_CONF = '/etc/turku-agent/rsyncd.conf'
 
21
RSYNCD_SECRETS = '/etc/turku-agent/rsyncd.secrets'
 
22
VAR_DIR = '/var/lib/turku-agent'
 
23
RESTORE_DIR = '/var/backups/turku-agent/restore'
 
24
 
 
25
 
 
26
def json_dump_p(obj, f):
 
27
    """Calls json.dump with standard (pretty) formatting"""
 
28
    return json.dump(obj, f, sort_keys=True, indent=4, separators=(',', ': '))
 
29
 
 
30
 
 
31
def json_dumps_p(obj):
 
32
    """Calls json.dumps with standard (pretty) formatting"""
 
33
    return json.dumps(obj, sort_keys=True, indent=4, separators=(',', ': '))
 
34
 
 
35
 
 
36
def dict_merge(s, m):
 
37
    """Recursively merge one dict into another."""
 
38
    if not isinstance(m, dict):
 
39
        return m
 
40
    out = copy.deepcopy(s)
 
41
    for k, v in m.items():
 
42
        if k in out and isinstance(out[k], dict):
 
43
            out[k] = dict_merge(out[k], v)
 
44
        else:
 
45
            out[k] = copy.deepcopy(v)
 
46
    return out
 
47
 
 
48
 
 
49
def parse_args():
 
50
    import argparse
 
51
 
 
52
    parser = argparse.ArgumentParser(
 
53
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
 
54
    parser.add_argument('--wait', '-w', type=float)
 
55
    return parser.parse_args()
 
56
 
 
57
 
 
58
def check_directories():
 
59
    for d in (CONFIG_D, SOURCES_D, VAR_DIR):
 
60
        if not os.path.isdir(d):
 
61
            os.makedirs(d)
 
62
    for d in (SOURCES_SECRETS_D,):
 
63
        if not os.path.isdir(d):
 
64
            os.makedirs(d)
 
65
            os.chmod(d, 0o700)
 
66
    for f in (SSH_PRIVATE_KEY, SSH_PUBLIC_KEY, RSYNCD_CONF, RSYNCD_SECRETS):
 
67
        d = os.path.dirname(f)
 
68
        if not os.path.isdir(d):
 
69
            os.makedirs(d)
 
70
 
 
71
 
 
72
def parse_config():
 
73
    api_config = {}
 
74
    built_config = {
 
75
        'machine': {},
 
76
        'sources': {}
 
77
    }
 
78
 
 
79
    root_config = {}
 
80
 
 
81
    # Merge in config.d/*.json to the root level
 
82
    config_files = [os.path.join(CONFIG_D, fn) for fn in os.listdir(CONFIG_D) if fn.endswith('.json') and os.path.isfile(os.path.join(CONFIG_D, fn)) and os.access(os.path.join(CONFIG_D, fn), os.R_OK)]
 
83
    config_files.sort()
 
84
    for file in config_files:
 
85
        with open(file) as f:
 
86
            j = json.load(f)
 
87
        root_config = dict_merge(root_config, j)
 
88
 
 
89
    # Validate the unit name
 
90
    if not 'unit_name' in root_config:
 
91
        root_config['unit_name'] = platform.node()
 
92
        # If this isn't in the on-disk config, don't write it; just
 
93
        # generate it every time
 
94
 
 
95
    # Validate the machine UUID/secret
 
96
    write_uuid_data = False
 
97
    if not 'machine_uuid' in root_config:
 
98
        root_config['machine_uuid'] = str(uuid.uuid4())
 
99
        write_uuid_data = True
 
100
    if not 'machine_secret' in root_config:
 
101
        root_config['machine_secret'] = ''.join(random.choice(string.ascii_letters + string.digits) for i in range(30))
 
102
        write_uuid_data = True
 
103
    # Write out the machine UUID/secret if needed
 
104
    if write_uuid_data:
 
105
        with open(os.path.join(CONFIG_D, '10-machine_uuid.json'), 'w') as f:
 
106
            os.chmod(os.path.join(CONFIG_D, '10-machine_uuid.json'), 0o600)
 
107
            json_dump_p({'machine_uuid': root_config['machine_uuid'], 'machine_secret': root_config['machine_secret']}, f)
 
108
 
 
109
    # Restoration configuration
 
110
    write_restore_data = False
 
111
    if not 'restore_path' in root_config:
 
112
        root_config['restore_path'] = RESTORE_DIR
 
113
        write_restore_data = True
 
114
    if not 'restore_module' in root_config:
 
115
        root_config['restore_module'] = 'turku-restore'
 
116
        write_restore_data = True
 
117
    if not 'restore_username' in root_config:
 
118
        root_config['restore_username'] = str(uuid.uuid4())
 
119
        write_restore_data = True
 
120
    if not 'restore_password' in root_config:
 
121
        root_config['restore_password'] = ''.join(random.choice(string.ascii_letters + string.digits) for i in range(30))
 
122
        write_restore_data = True
 
123
    if write_restore_data:
 
124
        with open(os.path.join(CONFIG_D, '10-restore.json'), 'w') as f:
 
125
            os.chmod(os.path.join(CONFIG_D, '10-restore.json'), 0o600)
 
126
            restore_out = {
 
127
                'restore_path': root_config['restore_path'],
 
128
                'restore_module': root_config['restore_module'],
 
129
                'restore_username': root_config['restore_username'],
 
130
                'restore_password': root_config['restore_password'],
 
131
            }
 
132
            json_dump_p(restore_out, f)
 
133
    if not os.path.isdir(root_config['restore_path']):
 
134
        os.makedirs(root_config['restore_path'])
 
135
 
 
136
    # Generate the SSH keypair if it doesn't exist
 
137
    if not os.path.isfile(SSH_PUBLIC_KEY):
 
138
        subprocess.check_call(['ssh-keygen', '-t', 'rsa', '-N', '', '-C', 'turku', '-f', SSH_PRIVATE_KEY])
 
139
 
 
140
    # Pull the SSH public key
 
141
    with open(SSH_PUBLIC_KEY) as f:
 
142
        root_config['ssh_public_key'] = f.read().rstrip()
 
143
 
 
144
    # Merge the following options into the api_config
 
145
    api_merge_map = (
 
146
        ('api_url', 'api_url'),
 
147
    )
 
148
    api_merge = {}
 
149
    for a, b in api_merge_map:
 
150
        if a in root_config:
 
151
            api_merge[b] = root_config[a]
 
152
    api_config = dict_merge(api_config, api_merge)
 
153
 
 
154
    # Merge the following options into the root
 
155
    root_merge_map = (
 
156
        ('api_auth', 'auth'),
 
157
    )
 
158
    root_merge = {}
 
159
    for a, b in root_merge_map:
 
160
        if a in root_config:
 
161
            root_merge[b] = root_config[a]
 
162
    built_config = dict_merge(built_config, root_merge)
 
163
 
 
164
    # Merge the following options into the machine section
 
165
    machine_merge_map = (
 
166
        ('machine_uuid', 'uuid'),
 
167
        ('machine_secret', 'secret'),
 
168
        ('environment_name', 'environment_name'),
 
169
        ('service_name', 'service_name'),
 
170
        ('unit_name', 'unit_name'),
 
171
        ('ssh_public_key', 'ssh_public_key'),
 
172
    )
 
173
    machine_merge = {}
 
174
    for a, b in machine_merge_map:
 
175
        if a in root_config:
 
176
            machine_merge[b] = root_config[a]
 
177
    built_config = dict_merge(built_config, {'machine': machine_merge})
 
178
 
 
179
    # Merge in sources.d/*.json to the sources dict
 
180
    sources_files = [os.path.join(SOURCES_D, fn) for fn in os.listdir(SOURCES_D) if fn.endswith('.json') and os.path.isfile(os.path.join(SOURCES_D, fn)) and os.access(os.path.join(SOURCES_D, fn), os.R_OK)]
 
181
    sources_files.sort()
 
182
    for file in sources_files:
 
183
        with open(file) as f:
 
184
            j = json.load(f)
 
185
        for s in j.keys():
 
186
            # Ignore incomplete source entries
 
187
            if not 'path' in j[s]:
 
188
                print('WARNING: Path not found for "%s", not using.' % s, file=sys.stderr)
 
189
                del j[s]
 
190
        built_config = dict_merge(built_config, {'sources': j})
 
191
 
 
192
    for s in built_config['sources']:
 
193
        # Check for missing usernames/passwords
 
194
        if not ('username' in built_config['sources'][s] or 'password' in built_config['sources'][s]):
 
195
            # If they're in sources_secrets.d, use them
 
196
            if os.path.isfile(os.path.join(SOURCES_SECRETS_D, s + '.json')):
 
197
                with open(os.path.join(SOURCES_SECRETS_D, s + '.json')) as f:
 
198
                    j = json.load(f)
 
199
                built_config = dict_merge(built_config, {'sources': {s: j}})
 
200
        # Check again and generate sources_secrets.d if still not found
 
201
        if not ('username' in built_config['sources'][s] or 'password' in built_config['sources'][s]):
 
202
            if not 'username' in built_config['sources'][s]:
 
203
                built_config['sources'][s]['username'] = str(uuid.uuid4())
 
204
            if not 'password' in built_config['sources'][s]:
 
205
                built_config['sources'][s]['password'] = ''.join(random.choice(string.ascii_letters + string.digits) for i in range(30))
 
206
            with open(os.path.join(SOURCES_SECRETS_D, s + '.json'), 'w') as f:
 
207
                json_dump_p({'username': built_config['sources'][s]['username'], 'password': built_config['sources'][s]['password']}, f)
 
208
 
 
209
    #print(json_dumps_p(built_config))
 
210
 
 
211
    built_config['restore'] = {
 
212
        'path': root_config['restore_path'],
 
213
        'module': root_config['restore_module'],
 
214
        'username': root_config['restore_username'],
 
215
        'password': root_config['restore_password'],
 
216
    }
 
217
 
 
218
    return((built_config, api_config))
 
219
 
 
220
 
 
221
def write_conf_files(built_config):
 
222
    # Build rsyncd.conf
 
223
    built_rsyncd_conf = 'address = 127.0.0.1\nport = 27873\nlog file = /dev/stdout\nuid = root\ngid = root\nlist = false\n\n'
 
224
    rsyncd_secrets = []
 
225
    rsyncd_secrets.append((built_config['restore']['username'], built_config['restore']['password']))
 
226
    built_rsyncd_conf += '[%s]\n    path = %s\n    auth users = %s\n    secrets file = %s\n    read only = false\n\n' % (built_config['restore']['module'], built_config['restore']['path'], built_config['restore']['username'], RSYNCD_SECRETS)
 
227
    for s in built_config['sources']:
 
228
        sd = built_config['sources'][s]
 
229
        rsyncd_secrets.append((sd['username'], sd['password']))
 
230
        built_rsyncd_conf += '[%s]\n    path = %s\n    auth users = %s\n    secrets file = %s\n    read only = true\n\n' % (s, sd['path'], sd['username'], RSYNCD_SECRETS)
 
231
    with open(RSYNCD_CONF, 'w') as f:
 
232
        f.write(built_rsyncd_conf)
 
233
 
 
234
    #print(built_rsyncd_conf)
 
235
 
 
236
    # Build rsyncd.secrets
 
237
    built_rsyncd_secrets = ''
 
238
    for (username, password) in rsyncd_secrets:
 
239
        built_rsyncd_secrets += username + ':' + password + '\n'
 
240
    with open(RSYNCD_SECRETS, 'w') as f:
 
241
        os.chmod(RSYNCD_SECRETS, 0o600)
 
242
        f.write(built_rsyncd_secrets)
 
243
 
 
244
    #print(built_rsyncd_secrets)
 
245
 
 
246
 
 
247
def restart_services():
 
248
    # Restart rsyncd
 
249
    if not subprocess.call(['service', 'turku-agent-rsyncd', 'restart']) == 0:
 
250
        subprocess.check_call(['service', 'turku-agent-rsyncd', 'start'])
 
251
 
 
252
 
 
253
def send_config(built_config, api_config):
 
254
    if not 'api_url' in api_config:
 
255
        return
 
256
 
 
257
    import httplib
 
258
    import urlparse
 
259
 
 
260
    # Send the completed config to the API server
 
261
    url = urlparse.urlparse(api_config['api_url'])
 
262
    if url.scheme == 'https':
 
263
        h = httplib.HTTPSConnection(url.netloc, timeout=5)
 
264
    else:
 
265
        h = httplib.HTTPConnection(url.netloc, timeout=5)
 
266
    dumped_json = json.dumps(built_config)
 
267
    h.putrequest('POST', '%s/update_config' % url.path)
 
268
    h.putheader('Content-Type', 'application/json')
 
269
    h.putheader('Content-Length', len(dumped_json))
 
270
    h.putheader('Accept', 'application/json')
 
271
    h.endheaders()
 
272
    h.send(dumped_json)
 
273
 
 
274
    # Read/validate the response
 
275
    res = h.getresponse()
 
276
    if not res.status == httplib.OK:
 
277
        return
 
278
    if not res.getheader('content-type') == 'application/json':
 
279
        return
 
280
    try:
 
281
        server_config = json.load(res)
 
282
    except ValueError:
 
283
        return
 
284
 
 
285
    # Write the response
 
286
    with open(os.path.join(VAR_DIR, 'server_config.json'), 'w') as f:
 
287
        os.chmod(os.path.join(VAR_DIR, 'server_config.json'), 0o600)
 
288
        json_dump_p(server_config, f)
 
289
 
 
290
 
 
291
def main(argv):
 
292
    args = parse_args()
 
293
    # Sleep a random amount of time if requested
 
294
    if args.wait:
 
295
        time.sleep(random.uniform(0, args.wait))
 
296
 
 
297
    check_directories()
 
298
    (built_config, api_config) = parse_config()
 
299
    write_conf_files(built_config)
 
300
    send_config(built_config, api_config)
 
301
    restart_services()
 
302
 
 
303
 
 
304
if __name__ == '__main__':
 
305
    sys.exit(main(sys.argv[1:]))