~fo0bar/turku/turku-agent-encoding

« back to all changes in this revision

Viewing changes to turku_agent/utils.py

  • Committer: Ryan Finnie
  • Date: 2015-03-16 16:31:48 UTC
  • Revision ID: ryan.finnie@canonical.com-20150316163148-cuv8aee048e63shy
Begin separating functionality into modules

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
 
 
3
# Turku backups - client agent
 
4
# Copyright 2015 Canonical Ltd.
 
5
#
 
6
# This program is free software: you can redistribute it and/or modify it
 
7
# under the terms of the GNU General Public License version 3, as published by
 
8
# the Free Software Foundation.
 
9
#
 
10
# This program is distributed in the hope that it will be useful, but WITHOUT
 
11
# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
 
12
# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 
13
# General Public License for more details.
 
14
#
 
15
# You should have received a copy of the GNU General Public License along with
 
16
# this program.  If not, see <http://www.gnu.org/licenses/>.
 
17
 
 
18
from __future__ import print_function
 
19
import uuid
 
20
import string
 
21
import random
 
22
import json
 
23
import os
 
24
import copy
 
25
import subprocess
 
26
import sys
 
27
import platform
 
28
import urlparse
 
29
import httplib
 
30
 
 
31
 
 
32
CONFIG_D = '/etc/turku-agent/config.d'
 
33
SOURCES_D = '/etc/turku-agent/sources.d'
 
34
SOURCES_SECRETS_D = '/etc/turku-agent/sources_secrets.d'
 
35
SSH_PRIVATE_KEY = '/etc/turku-agent/id_rsa'
 
36
SSH_PUBLIC_KEY = '/etc/turku-agent/id_rsa.pub'
 
37
RSYNCD_CONF = '/etc/turku-agent/rsyncd.conf'
 
38
RSYNCD_SECRETS = '/etc/turku-agent/rsyncd.secrets'
 
39
VAR_DIR = '/var/lib/turku-agent'
 
40
RESTORE_DIR = '/var/backups/turku-agent/restore'
 
41
 
 
42
 
 
43
def json_dump_p(obj, f):
 
44
    """Calls json.dump with standard (pretty) formatting"""
 
45
    return json.dump(obj, f, sort_keys=True, indent=4, separators=(',', ': '))
 
46
 
 
47
 
 
48
def json_dumps_p(obj):
 
49
    """Calls json.dumps with standard (pretty) formatting"""
 
50
    return json.dumps(obj, sort_keys=True, indent=4, separators=(',', ': '))
 
51
 
 
52
 
 
53
def dict_merge(s, m):
 
54
    """Recursively merge one dict into another."""
 
55
    if not isinstance(m, dict):
 
56
        return m
 
57
    out = copy.deepcopy(s)
 
58
    for k, v in m.items():
 
59
        if k in out and isinstance(out[k], dict):
 
60
            out[k] = dict_merge(out[k], v)
 
61
        else:
 
62
            out[k] = copy.deepcopy(v)
 
63
    return out
 
64
 
 
65
 
 
66
def load_config():
 
67
    for d in (CONFIG_D, SOURCES_D, VAR_DIR):
 
68
        if not os.path.isdir(d):
 
69
            os.makedirs(d)
 
70
    for d in (SOURCES_SECRETS_D,):
 
71
        if not os.path.isdir(d):
 
72
            os.makedirs(d)
 
73
            os.chmod(d, 0o700)
 
74
    for f in (SSH_PRIVATE_KEY, SSH_PUBLIC_KEY, RSYNCD_CONF, RSYNCD_SECRETS):
 
75
        d = os.path.dirname(f)
 
76
        if not os.path.isdir(d):
 
77
            os.makedirs(d)
 
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
    sources_config = {}
 
145
    # Merge in sources.d/*.json to the sources dict
 
146
    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)]
 
147
    sources_files.sort()
 
148
    for file in sources_files:
 
149
        with open(file) as f:
 
150
            j = json.load(f)
 
151
        for s in j.keys():
 
152
            # Ignore incomplete source entries
 
153
            if not 'path' in j[s]:
 
154
                print('WARNING: Path not found for "%s", not using.' % s, file=sys.stderr)
 
155
                del j[s]
 
156
        sources_config = dict_merge(sources_config, j)
 
157
 
 
158
    for s in sources_config:
 
159
        # Check for missing usernames/passwords
 
160
        if not ('username' in sources_config[s] or 'password' in sources_config[s]):
 
161
            # If they're in sources_secrets.d, use them
 
162
            if os.path.isfile(os.path.join(SOURCES_SECRETS_D, s + '.json')):
 
163
                with open(os.path.join(SOURCES_SECRETS_D, s + '.json')) as f:
 
164
                    j = json.load(f)
 
165
                sources_config = dict_merge(sources_config, {s: j})
 
166
        # Check again and generate sources_secrets.d if still not found
 
167
        if not ('username' in sources_config[s] or 'password' in sources_config[s]):
 
168
            if not 'username' in sources_config[s]:
 
169
                sources_config[s]['username'] = str(uuid.uuid4())
 
170
            if not 'password' in sources_config[s]:
 
171
                sources_config[s]['password'] = ''.join(random.choice(string.ascii_letters + string.digits) for i in range(30))
 
172
            with open(os.path.join(SOURCES_SECRETS_D, s + '.json'), 'w') as f:
 
173
                json_dump_p({'username': sources_config[s]['username'], 'password': sources_config[s]['password']}, f)
 
174
 
 
175
    root_config['sources'] = sources_config
 
176
 
 
177
    return root_config
 
178
 
 
179
 
 
180
def api_call(api_url, cmd, post_data, timeout=5):
 
181
    url = urlparse.urlparse(api_url)
 
182
    if url.scheme == 'https':
 
183
        h = httplib.HTTPSConnection(url.netloc, timeout=timeout)
 
184
    else:
 
185
        h = httplib.HTTPConnection(url.netloc, timeout=timeout)
 
186
    out = json.dumps(post_data)
 
187
    h.putrequest('POST', '%s/%s' % (url.path, cmd))
 
188
    h.putheader('Content-Type', 'application/json')
 
189
    h.putheader('Content-Length', len(out))
 
190
    h.putheader('Accept', 'application/json')
 
191
    h.endheaders()
 
192
    h.send(out)
 
193
 
 
194
    res = h.getresponse()
 
195
    if not res.status == httplib.OK:
 
196
        raise Exception('Received error %d (%s) from API server' % (res.status, res.reason))
 
197
    if not res.getheader('content-type') == 'application/json':
 
198
        raise Exception('Received invalid reply from API server')
 
199
    try:
 
200
        return json.load(res)
 
201
    except ValueError:
 
202
        raise Exception('Received invalid reply from API server')