3
from __future__ import print_function
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'
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=(',', ': '))
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=(',', ': '))
37
"""Recursively merge one dict into another."""
38
if not isinstance(m, dict):
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)
45
out[k] = copy.deepcopy(v)
52
parser = argparse.ArgumentParser(
53
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
54
parser.add_argument('--wait', '-w', type=float)
55
return parser.parse_args()
58
def check_directories():
59
for d in (CONFIG_D, SOURCES_D, VAR_DIR):
60
if not os.path.isdir(d):
62
for d in (SOURCES_SECRETS_D,):
63
if not os.path.isdir(d):
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):
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)]
84
for file in config_files:
87
root_config = dict_merge(root_config, j)
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
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
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)
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)
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'],
132
json_dump_p(restore_out, f)
133
if not os.path.isdir(root_config['restore_path']):
134
os.makedirs(root_config['restore_path'])
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])
140
# Pull the SSH public key
141
with open(SSH_PUBLIC_KEY) as f:
142
root_config['ssh_public_key'] = f.read().rstrip()
144
# Merge the following options into the api_config
146
('api_url', 'api_url'),
149
for a, b in api_merge_map:
151
api_merge[b] = root_config[a]
152
api_config = dict_merge(api_config, api_merge)
154
# Merge the following options into the root
156
('api_auth', 'auth'),
159
for a, b in root_merge_map:
161
root_merge[b] = root_config[a]
162
built_config = dict_merge(built_config, root_merge)
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'),
174
for a, b in machine_merge_map:
176
machine_merge[b] = root_config[a]
177
built_config = dict_merge(built_config, {'machine': machine_merge})
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)]
182
for file in sources_files:
183
with open(file) as f:
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)
190
built_config = dict_merge(built_config, {'sources': j})
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:
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)
209
#print(json_dumps_p(built_config))
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'],
218
return((built_config, api_config))
221
def write_conf_files(built_config):
223
built_rsyncd_conf = 'address = 127.0.0.1\nport = 27873\nlog file = /dev/stdout\nuid = root\ngid = root\nlist = false\n\n'
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)
234
#print(built_rsyncd_conf)
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)
244
#print(built_rsyncd_secrets)
247
def restart_services():
249
if not subprocess.call(['service', 'turku-agent-rsyncd', 'restart']) == 0:
250
subprocess.check_call(['service', 'turku-agent-rsyncd', 'start'])
253
def send_config(built_config, api_config):
254
if not 'api_url' in api_config:
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)
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')
274
# Read/validate the response
275
res = h.getresponse()
276
if not res.status == httplib.OK:
278
if not res.getheader('content-type') == 'application/json':
281
server_config = json.load(res)
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)
293
# Sleep a random amount of time if requested
295
time.sleep(random.uniform(0, args.wait))
298
(built_config, api_config) = parse_config()
299
write_conf_files(built_config)
300
send_config(built_config, api_config)
304
if __name__ == '__main__':
305
sys.exit(main(sys.argv[1:]))