Merge lp:~fo0bar/turku/turku-agent-gonogo into lp:turku
- turku-agent-gonogo
- Merge into turku-storage
Status: | Superseded |
---|---|
Proposed branch: | lp:~fo0bar/turku/turku-agent-gonogo |
Merge into: | lp:turku |
Diff against target: |
1081 lines (+983/-0) (has conflicts) 19 files modified
.bzrignore (+5/-0) MANIFEST.in (+3/-0) README (+4/-0) setup.py (+39/-0) turku-agent-ping (+20/-0) turku-agent-ping.service (+6/-0) turku-agent-ping.timer (+10/-0) turku-agent-rsyncd-wrapper (+20/-0) turku-agent-rsyncd.conf (+8/-0) turku-agent-rsyncd.init-debian (+55/-0) turku-agent-rsyncd.service (+10/-0) turku-agent.cron (+3/-0) turku-update-config (+20/-0) turku-update-config.service (+6/-0) turku-update-config.timer (+10/-0) turku_agent/ping.py (+211/-0) turku_agent/rsyncd_wrapper.py (+42/-0) turku_agent/update_config.py (+179/-0) turku_agent/utils.py (+332/-0) Conflict adding file .bzrignore. Moved existing file to .bzrignore.moved. Conflict adding file setup.py. Moved existing file to setup.py.moved. |
To merge this branch: | bzr merge lp:~fo0bar/turku/turku-agent-gonogo |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Canonical IS Reviewers | Pending | ||
Review via email:
|
Commit message
Description of the change
- 52. By Ryan Finnie
-
Allow gonogo_program to be stored in config, allow --gonogo-program to be shlex-split
Unmerged revisions
- 52. By Ryan Finnie
-
Allow gonogo_program to be stored in config, allow --gonogo-program to be shlex-split
- 51. By Colin Watson
-
On Upstart, check if turku-agent-rsyncd is already running before starting it.
Reviewed-on: https:/
/code.launchpad .net/~cjwatson/ turku/turku- agent-fix- rsyncd- interaction/ +merge/ 367847
Reviewed-by: Haw Loeung <email address hidden> - 50. By Colin Watson
-
Don't restart rsyncd when updating its configuration.
Reviewed-on: https:/
/code.launchpad .net/~cjwatson/ turku/turku- agent-fix- rsyncd- interaction/ +merge/ 367418
Reviewed-by: Haw Loeung <email address hidden> - 49. By Ryan Finnie
-
Fix Python 3 .values conversion with --restore
- 48. By Ryan Finnie
-
Remove early legacy config migrations/
mitigations - 47. By Ryan Finnie
-
Convert to Python 3
Program functionality is identical to previous, but will now need
to be installed with python3. - 46. By Ryan Finnie
-
turku-agent-ping: Add --gonogo-program
- 45. By Ryan Finnie
-
Fix incorrect description turku-update-
config. timer - 44. By Ryan Finnie
-
Add systemd timers for turku-agent-ping and turku-update-config
- 43. By Ryan Finnie
-
[jacekn] Make sure update_config errors are not ignored. This allows for hooks to fail on bad configuration.
Preview Diff
1 | === added file '.bzrignore' |
2 | --- .bzrignore 1970-01-01 00:00:00 +0000 |
3 | +++ .bzrignore 2019-06-15 00:58:25 +0000 |
4 | @@ -0,0 +1,5 @@ |
5 | +*.pyc |
6 | +./build/ |
7 | +./dist/ |
8 | +./MANIFEST |
9 | +./*.egg-info/ |
10 | |
11 | === renamed file '.bzrignore' => '.bzrignore.moved' |
12 | === added file 'MANIFEST.in' |
13 | --- MANIFEST.in 1970-01-01 00:00:00 +0000 |
14 | +++ MANIFEST.in 2019-06-15 00:58:25 +0000 |
15 | @@ -0,0 +1,3 @@ |
16 | +include turku-agent.cron |
17 | +include turku-agent-rsyncd.conf |
18 | +include turku-agent-rsyncd.service |
19 | |
20 | === added file 'README' |
21 | --- README 1970-01-01 00:00:00 +0000 |
22 | +++ README 2019-06-15 00:58:25 +0000 |
23 | @@ -0,0 +1,4 @@ |
24 | +Turku backups - client agent |
25 | +Copyright 2015 Canonical Ltd. |
26 | + |
27 | +https://launchpad.net/turku |
28 | |
29 | === added file 'setup.py' |
30 | --- setup.py 1970-01-01 00:00:00 +0000 |
31 | +++ setup.py 2019-06-15 00:58:25 +0000 |
32 | @@ -0,0 +1,39 @@ |
33 | +#!/usr/bin/env python3 |
34 | + |
35 | +# Turku backups - client agent |
36 | +# Copyright 2015 Canonical Ltd. |
37 | +# |
38 | +# This program is free software: you can redistribute it and/or modify it |
39 | +# under the terms of the GNU General Public License version 3, as published by |
40 | +# the Free Software Foundation. |
41 | +# |
42 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
43 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
44 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
45 | +# General Public License for more details. |
46 | +# |
47 | +# You should have received a copy of the GNU General Public License along with |
48 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
49 | + |
50 | +import sys |
51 | +from setuptools import setup |
52 | + |
53 | +assert(sys.version_info > (3, 4)) |
54 | + |
55 | + |
56 | +setup( |
57 | + name='turku_agent', |
58 | + description='Turku backups - client agent', |
59 | + version='0.2.0', |
60 | + author='Ryan Finnie', |
61 | + author_email='ryan.finnie@canonical.com', |
62 | + url='https://launchpad.net/turku', |
63 | + packages=['turku_agent'], |
64 | + entry_points={ |
65 | + 'console_scripts': [ |
66 | + 'turku-agent-ping = turku_agent.ping:main', |
67 | + 'turku-agent-rsyncd-wrapper = turku_agent.rsyncd_wrapper:main', |
68 | + 'turku-update-config = turku_agent.update_config:main', |
69 | + ], |
70 | + }, |
71 | +) |
72 | |
73 | === renamed file 'setup.py' => 'setup.py.moved' |
74 | === added file 'turku-agent-ping' |
75 | --- turku-agent-ping 1970-01-01 00:00:00 +0000 |
76 | +++ turku-agent-ping 2019-06-15 00:58:25 +0000 |
77 | @@ -0,0 +1,20 @@ |
78 | +#!/usr/bin/env python3 |
79 | + |
80 | +# Turku backups - client agent |
81 | +# Copyright 2015 Canonical Ltd. |
82 | +# |
83 | +# This program is free software: you can redistribute it and/or modify it |
84 | +# under the terms of the GNU General Public License version 3, as published by |
85 | +# the Free Software Foundation. |
86 | +# |
87 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
88 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
89 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
90 | +# General Public License for more details. |
91 | +# |
92 | +# You should have received a copy of the GNU General Public License along with |
93 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
94 | + |
95 | +import sys |
96 | +from turku_agent import ping |
97 | +sys.exit(ping.main()) |
98 | |
99 | === added file 'turku-agent-ping.service' |
100 | --- turku-agent-ping.service 1970-01-01 00:00:00 +0000 |
101 | +++ turku-agent-ping.service 2019-06-15 00:58:25 +0000 |
102 | @@ -0,0 +1,6 @@ |
103 | +[Unit] |
104 | +Description=turku-agent-ping |
105 | + |
106 | +[Service] |
107 | +Type=oneshot |
108 | +ExecStart=/usr/bin/env turku-agent-ping |
109 | |
110 | === added file 'turku-agent-ping.timer' |
111 | --- turku-agent-ping.timer 1970-01-01 00:00:00 +0000 |
112 | +++ turku-agent-ping.timer 2019-06-15 00:58:25 +0000 |
113 | @@ -0,0 +1,10 @@ |
114 | +[Unit] |
115 | +Description=turku-agent-ping |
116 | + |
117 | +[Timer] |
118 | +OnCalendar=*:0/5 |
119 | +RandomizedDelaySec=5m |
120 | +Persistent=true |
121 | + |
122 | +[Install] |
123 | +WantedBy=timers.target |
124 | |
125 | === added file 'turku-agent-rsyncd-wrapper' |
126 | --- turku-agent-rsyncd-wrapper 1970-01-01 00:00:00 +0000 |
127 | +++ turku-agent-rsyncd-wrapper 2019-06-15 00:58:25 +0000 |
128 | @@ -0,0 +1,20 @@ |
129 | +#!/usr/bin/env python3 |
130 | + |
131 | +# Turku backups - client agent |
132 | +# Copyright 2015 Canonical Ltd. |
133 | +# |
134 | +# This program is free software: you can redistribute it and/or modify it |
135 | +# under the terms of the GNU General Public License version 3, as published by |
136 | +# the Free Software Foundation. |
137 | +# |
138 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
139 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
140 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
141 | +# General Public License for more details. |
142 | +# |
143 | +# You should have received a copy of the GNU General Public License along with |
144 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
145 | + |
146 | +import sys |
147 | +from turku_agent import rsyncd_wrapper |
148 | +sys.exit(rsyncd_wrapper.main()) |
149 | |
150 | === added file 'turku-agent-rsyncd.conf' |
151 | --- turku-agent-rsyncd.conf 1970-01-01 00:00:00 +0000 |
152 | +++ turku-agent-rsyncd.conf 2019-06-15 00:58:25 +0000 |
153 | @@ -0,0 +1,8 @@ |
154 | +description "turku rsync daemon" |
155 | + |
156 | +start on runlevel [2345] |
157 | +stop on runlevel [!2345] |
158 | + |
159 | +respawn |
160 | + |
161 | +exec /usr/bin/env turku-agent-rsyncd-wrapper |
162 | |
163 | === added file 'turku-agent-rsyncd.init-debian' |
164 | --- turku-agent-rsyncd.init-debian 1970-01-01 00:00:00 +0000 |
165 | +++ turku-agent-rsyncd.init-debian 2019-06-15 00:58:25 +0000 |
166 | @@ -0,0 +1,55 @@ |
167 | +#! /bin/sh |
168 | + |
169 | +### BEGIN INIT INFO |
170 | +# Provides: turku-agent-rsyncd |
171 | +# Required-Start: $remote_fs $syslog |
172 | +# Required-Stop: $remote_fs $syslog |
173 | +# Should-Start: $named |
174 | +# Default-Start: 2 3 4 5 |
175 | +# Default-Stop: |
176 | +# Short-Description: turku rsync daemon |
177 | +# Description: turku rsync daemon |
178 | +### END INIT INFO |
179 | + |
180 | +set -e |
181 | + |
182 | +PID_FILE=/var/run/turku-agent-rsyncd.pid |
183 | + |
184 | +. /lib/lsb/init-functions |
185 | + |
186 | +export PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin |
187 | + |
188 | +case "$1" in |
189 | + start) |
190 | + log_daemon_msg "Starting turku rsync daemon" "turku-agent-rsyncd" |
191 | + start-stop-daemon --start --quiet --background --make-pidfile \ |
192 | + --pidfile $PID_FILE --startas /usr/bin/env -- turku-agent-rsyncd-wrapper |
193 | + log_end_msg $? |
194 | + ;; |
195 | + stop) |
196 | + log_daemon_msg "Stopping turku rsync daemon" "turku-agent-rsyncd" |
197 | + start-stop-daemon --stop --quiet --oknodo --pidfile $PID_FILE --retry 5 |
198 | + log_end_msg $? |
199 | + rm -f $PID_FILE |
200 | + ;; |
201 | + |
202 | + reload|force-reload) |
203 | + log_daemon_msg "Reloading turku rsync daemon" "turku-agent-rsyncd" |
204 | + log_end_msg 0 |
205 | + ;; |
206 | + |
207 | + restart) |
208 | + "$0" stop |
209 | + "$0" start |
210 | + ;; |
211 | + |
212 | + status) |
213 | + status_of_proc -p $PID_FILE rsync turku-agent-rsyncd |
214 | + exit $? # notreached due to set -e |
215 | + ;; |
216 | + *) |
217 | + echo "Usage: /etc/init.d/turku-agent-rsyncd {start|stop|reload|force-reload|restart|status}" |
218 | + exit 1 |
219 | +esac |
220 | + |
221 | +exit 0 |
222 | |
223 | === added file 'turku-agent-rsyncd.service' |
224 | --- turku-agent-rsyncd.service 1970-01-01 00:00:00 +0000 |
225 | +++ turku-agent-rsyncd.service 2019-06-15 00:58:25 +0000 |
226 | @@ -0,0 +1,10 @@ |
227 | +[Unit] |
228 | +Description=turku rsyncd daemon |
229 | +ConditionPathExists=/var/lib/turku-agent/rsyncd.conf |
230 | + |
231 | +[Service] |
232 | +ExecStart=/usr/bin/env turku-agent-rsyncd-wrapper |
233 | +Restart=always |
234 | + |
235 | +[Install] |
236 | +WantedBy=multi-user.target |
237 | |
238 | === added file 'turku-agent.cron' |
239 | --- turku-agent.cron 1970-01-01 00:00:00 +0000 |
240 | +++ turku-agent.cron 2019-06-15 00:58:25 +0000 |
241 | @@ -0,0 +1,3 @@ |
242 | +PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin |
243 | +*/5 * * * * root sh -c 'systemctl is-active basic.target 2>/dev/null >/dev/null || turku-agent-ping --wait=300 >/dev/null 2>/dev/null' |
244 | +0 0,12 * * * root sh -c 'systemctl is-active basic.target 2>/dev/null >/dev/null || turku-update-config --wait=7200 >/dev/null 2>/dev/null' |
245 | |
246 | === added file 'turku-update-config' |
247 | --- turku-update-config 1970-01-01 00:00:00 +0000 |
248 | +++ turku-update-config 2019-06-15 00:58:25 +0000 |
249 | @@ -0,0 +1,20 @@ |
250 | +#!/usr/bin/env python3 |
251 | + |
252 | +# Turku backups - client agent |
253 | +# Copyright 2015 Canonical Ltd. |
254 | +# |
255 | +# This program is free software: you can redistribute it and/or modify it |
256 | +# under the terms of the GNU General Public License version 3, as published by |
257 | +# the Free Software Foundation. |
258 | +# |
259 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
260 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
261 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
262 | +# General Public License for more details. |
263 | +# |
264 | +# You should have received a copy of the GNU General Public License along with |
265 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
266 | + |
267 | +import sys |
268 | +from turku_agent import update_config |
269 | +sys.exit(update_config.main()) |
270 | |
271 | === added file 'turku-update-config.service' |
272 | --- turku-update-config.service 1970-01-01 00:00:00 +0000 |
273 | +++ turku-update-config.service 2019-06-15 00:58:25 +0000 |
274 | @@ -0,0 +1,6 @@ |
275 | +[Unit] |
276 | +Description=turku-update-config |
277 | + |
278 | +[Service] |
279 | +Type=oneshot |
280 | +ExecStart=/usr/bin/env turku-update-config |
281 | |
282 | === added file 'turku-update-config.timer' |
283 | --- turku-update-config.timer 1970-01-01 00:00:00 +0000 |
284 | +++ turku-update-config.timer 2019-06-15 00:58:25 +0000 |
285 | @@ -0,0 +1,10 @@ |
286 | +[Unit] |
287 | +Description=turku-update-config |
288 | + |
289 | +[Timer] |
290 | +OnCalendar=0,12:0 |
291 | +RandomizedDelaySec=12h |
292 | +Persistent=true |
293 | + |
294 | +[Install] |
295 | +WantedBy=timers.target |
296 | |
297 | === added directory 'turku_agent' |
298 | === added file 'turku_agent/__init__.py' |
299 | === added file 'turku_agent/ping.py' |
300 | --- turku_agent/ping.py 1970-01-01 00:00:00 +0000 |
301 | +++ turku_agent/ping.py 2019-06-15 00:58:25 +0000 |
302 | @@ -0,0 +1,211 @@ |
303 | +#!/usr/bin/env python3 |
304 | + |
305 | +# Turku backups - client agent |
306 | +# Copyright 2015 Canonical Ltd. |
307 | +# |
308 | +# This program is free software: you can redistribute it and/or modify it |
309 | +# under the terms of the GNU General Public License version 3, as published by |
310 | +# the Free Software Foundation. |
311 | +# |
312 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
313 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
314 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
315 | +# General Public License for more details. |
316 | +# |
317 | +# You should have received a copy of the GNU General Public License along with |
318 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
319 | + |
320 | + |
321 | +import os |
322 | +import json |
323 | +import random |
324 | +import shlex |
325 | +import subprocess |
326 | +import tempfile |
327 | +import time |
328 | +from .utils import load_config, acquire_lock, api_call |
329 | + |
330 | + |
331 | +def parse_args(): |
332 | + import argparse |
333 | + |
334 | + parser = argparse.ArgumentParser( |
335 | + formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
336 | + parser.add_argument('--config-dir', '-c', type=str, default='/etc/turku-agent') |
337 | + parser.add_argument('--wait', '-w', type=float) |
338 | + parser.add_argument('--restore', action='store_true') |
339 | + parser.add_argument('--restore-storage', type=str, default=None) |
340 | + parser.add_argument('--gonogo-program', type=str, default=None) |
341 | + return parser.parse_args() |
342 | + |
343 | + |
344 | +def call_ssh(config, storage, ssh_req): |
345 | + # Write the server host public key |
346 | + t = tempfile.NamedTemporaryFile() |
347 | + for key in storage['ssh_ping_host_keys']: |
348 | + t.write(('%s %s\n' % (storage['ssh_ping_host'], key)).encode('UTF-8')) |
349 | + t.flush() |
350 | + |
351 | + # Call ssh |
352 | + ssh_command = config['ssh_command'] |
353 | + ssh_command += [ |
354 | + '-T', |
355 | + '-o', 'BatchMode=yes', |
356 | + '-o', 'UserKnownHostsFile=%s' % t.name, |
357 | + '-o', 'StrictHostKeyChecking=yes', |
358 | + '-o', 'CheckHostIP=no', |
359 | + '-i', config['ssh_private_key_file'], |
360 | + '-R', '%d:%s:%d' % (ssh_req['port'], config['rsyncd_local_address'], config['rsyncd_local_port']), |
361 | + '-p', str(storage['ssh_ping_port']), |
362 | + '-l', storage['ssh_ping_user'], |
363 | + storage['ssh_ping_host'], |
364 | + 'turku-ping-remote', |
365 | + ] |
366 | + p = subprocess.Popen(ssh_command, stdin=subprocess.PIPE) |
367 | + |
368 | + # Write the ssh request |
369 | + p.stdin.write((json.dumps(ssh_req) + '\n.\n').encode('UTF-8')) |
370 | + p.stdin.flush() |
371 | + |
372 | + # Wait for the server to close the SSH connection |
373 | + try: |
374 | + p.wait() |
375 | + except KeyboardInterrupt: |
376 | + pass |
377 | + |
378 | + # Cleanup |
379 | + t.close() |
380 | + |
381 | + |
382 | +def main(): |
383 | + args = parse_args() |
384 | + |
385 | + # Sleep a random amount of time if requested |
386 | + if args.wait: |
387 | + time.sleep(random.uniform(0, args.wait)) |
388 | + |
389 | + config = load_config(args.config_dir) |
390 | + |
391 | + # Basic checks |
392 | + for i in ('ssh_private_key_file', 'machine_uuid', 'machine_secret', 'api_url'): |
393 | + if i not in config: |
394 | + return |
395 | + if not os.path.isfile(config['ssh_private_key_file']): |
396 | + return |
397 | + |
398 | + # If a go/no-go program is defined, run it and only go if it exits 0. |
399 | + if args.gonogo_program: |
400 | + gonogo_program = shlex.split(args.gonogo_program) |
401 | + elif config['gonogo_program']: |
402 | + gonogo_program = config['gonogo_program'] |
403 | + else: |
404 | + gonogo_program = [] |
405 | + if gonogo_program: |
406 | + try: |
407 | + subprocess.check_call(gonogo_program) |
408 | + except (subprocess.CalledProcessError, OSError): |
409 | + return |
410 | + |
411 | + lock = acquire_lock(os.path.join(config['lock_dir'], 'turku-agent-ping.lock')) |
412 | + |
413 | + restore_mode = args.restore |
414 | + |
415 | + # Check with the API server |
416 | + api_out = {} |
417 | + |
418 | + machine_merge_map = ( |
419 | + ('machine_uuid', 'uuid'), |
420 | + ('machine_secret', 'secret'), |
421 | + ) |
422 | + api_out['machine'] = {} |
423 | + for a, b in machine_merge_map: |
424 | + if a in config: |
425 | + api_out['machine'][b] = config[a] |
426 | + |
427 | + if restore_mode: |
428 | + print('Entering restore mode.') |
429 | + print() |
430 | + api_reply = api_call(config['api_url'], 'agent_ping_restore', api_out) |
431 | + |
432 | + sources_by_storage = {} |
433 | + for source_name in api_reply['machine']['sources']: |
434 | + source = api_reply['machine']['sources'][source_name] |
435 | + if source_name not in config['sources']: |
436 | + continue |
437 | + if 'storage' not in source: |
438 | + continue |
439 | + if source['storage']['name'] not in sources_by_storage: |
440 | + sources_by_storage[source['storage']['name']] = {} |
441 | + sources_by_storage[source['storage']['name']][source_name] = source |
442 | + |
443 | + if len(sources_by_storage) == 0: |
444 | + print('Cannot find any appropraite sources.') |
445 | + return |
446 | + print('This machine\'s sources are on the following storage units:') |
447 | + for storage_name in sources_by_storage: |
448 | + print(' %s' % storage_name) |
449 | + for source_name in sources_by_storage[storage_name]: |
450 | + print(' %s' % source_name) |
451 | + print() |
452 | + if len(sources_by_storage) == 1: |
453 | + storage = list(list(sources_by_storage.values())[0].values())[0]['storage'] |
454 | + elif args.restore_storage: |
455 | + if args.restore_storage in sources_by_storage: |
456 | + storage = sources_by_storage[args.restore_storage]['storage'] |
457 | + else: |
458 | + print('Cannot find appropriate storage "%s"' % args.restore_storage) |
459 | + return |
460 | + else: |
461 | + print('Multiple storages found. Please use --restore-storage to specify one.') |
462 | + return |
463 | + |
464 | + ssh_req = { |
465 | + 'verbose': True, |
466 | + 'action': 'restore', |
467 | + 'port': random.randint(49152, 65535), |
468 | + } |
469 | + print('Storage unit: %s' % storage['name']) |
470 | + if 'restore_path' in config: |
471 | + print('Local destination path: %s' % config['restore_path']) |
472 | + print('Sample restore usage from storage unit:') |
473 | + print( |
474 | + ' RSYNC_PASSWORD=%s rsync -avzP --numeric-ids ${P?}/ rsync://%s@127.0.0.1:%s/%s/' % ( |
475 | + config['restore_password'], |
476 | + config['restore_username'], |
477 | + ssh_req['port'], config['restore_module'] |
478 | + ) |
479 | + ) |
480 | + print() |
481 | + call_ssh(config, storage, ssh_req) |
482 | + else: |
483 | + api_reply = api_call(config['api_url'], 'agent_ping_checkin', api_out) |
484 | + |
485 | + if 'scheduled_sources' not in api_reply: |
486 | + return |
487 | + sources_by_storage = {} |
488 | + for source_name in api_reply['machine']['scheduled_sources']: |
489 | + source = api_reply['machine']['scheduled_sources'][source_name] |
490 | + if source_name not in config['sources']: |
491 | + continue |
492 | + if 'storage' not in source: |
493 | + continue |
494 | + if source['storage']['name'] not in sources_by_storage: |
495 | + sources_by_storage[source['storage']['name']] = {} |
496 | + sources_by_storage[source['storage']['name']][source_name] = source |
497 | + |
498 | + for storage_name in sources_by_storage: |
499 | + ssh_req = { |
500 | + 'verbose': True, |
501 | + 'action': 'checkin', |
502 | + 'port': random.randint(49152, 65535), |
503 | + 'sources': {}, |
504 | + } |
505 | + for source in sources_by_storage[storage_name]: |
506 | + ssh_req['sources'][source] = { |
507 | + 'username': config['sources'][source]['username'], |
508 | + 'password': config['sources'][source]['password'], |
509 | + } |
510 | + call_ssh(config, list(sources_by_storage[storage_name].values())[0]['storage'], ssh_req) |
511 | + |
512 | + # Cleanup |
513 | + lock.close() |
514 | |
515 | === added file 'turku_agent/rsyncd_wrapper.py' |
516 | --- turku_agent/rsyncd_wrapper.py 1970-01-01 00:00:00 +0000 |
517 | +++ turku_agent/rsyncd_wrapper.py 2019-06-15 00:58:25 +0000 |
518 | @@ -0,0 +1,42 @@ |
519 | +#!/usr/bin/env python3 |
520 | + |
521 | +# Turku backups - client agent |
522 | +# Copyright 2015 Canonical Ltd. |
523 | +# |
524 | +# This program is free software: you can redistribute it and/or modify it |
525 | +# under the terms of the GNU General Public License version 3, as published by |
526 | +# the Free Software Foundation. |
527 | +# |
528 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
529 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
530 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
531 | +# General Public License for more details. |
532 | +# |
533 | +# You should have received a copy of the GNU General Public License along with |
534 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
535 | + |
536 | +import os |
537 | +from .utils import load_config |
538 | + |
539 | + |
540 | +def parse_args(): |
541 | + import argparse |
542 | + |
543 | + parser = argparse.ArgumentParser( |
544 | + formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
545 | + parser.add_argument('--config-dir', '-c', type=str, default='/etc/turku-agent') |
546 | + parser.add_argument('--detach', action='store_true') |
547 | + return parser.parse_known_args() |
548 | + |
549 | + |
550 | +def main(): |
551 | + args, rest = parse_args() |
552 | + |
553 | + config = load_config(args.config_dir) |
554 | + rsyncd_command = config['rsyncd_command'] |
555 | + if not args.detach: |
556 | + rsyncd_command.append('--no-detach') |
557 | + rsyncd_command.append('--daemon') |
558 | + rsyncd_command.append('--config=%s' % os.path.join(config['var_dir'], 'rsyncd.conf')) |
559 | + rsyncd_command += rest |
560 | + os.execvp(rsyncd_command[0], rsyncd_command) |
561 | |
562 | === added file 'turku_agent/update_config.py' |
563 | --- turku_agent/update_config.py 1970-01-01 00:00:00 +0000 |
564 | +++ turku_agent/update_config.py 2019-06-15 00:58:25 +0000 |
565 | @@ -0,0 +1,179 @@ |
566 | +#!/usr/bin/env python3 |
567 | + |
568 | +# Turku backups - client agent |
569 | +# Copyright 2015 Canonical Ltd. |
570 | +# |
571 | +# This program is free software: you can redistribute it and/or modify it |
572 | +# under the terms of the GNU General Public License version 3, as published by |
573 | +# the Free Software Foundation. |
574 | +# |
575 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
576 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
577 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
578 | +# General Public License for more details. |
579 | +# |
580 | +# You should have received a copy of the GNU General Public License along with |
581 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
582 | + |
583 | +import random |
584 | +import os |
585 | +import subprocess |
586 | +import time |
587 | +import logging |
588 | +from .utils import json_dump_p, json_dumps_p, load_config, fill_config, acquire_lock, api_call |
589 | + |
590 | + |
591 | +class IncompleteConfigError(Exception): |
592 | + pass |
593 | + |
594 | + |
595 | +def parse_args(): |
596 | + import argparse |
597 | + |
598 | + parser = argparse.ArgumentParser( |
599 | + formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
600 | + parser.add_argument('--config-dir', '-c', type=str, default='/etc/turku-agent') |
601 | + parser.add_argument('--wait', '-w', type=float) |
602 | + parser.add_argument('--debug', action='store_true') |
603 | + return parser.parse_args() |
604 | + |
605 | + |
606 | +def write_conf_files(config): |
607 | + # Build rsyncd.conf |
608 | + built_rsyncd_conf = ( |
609 | + 'address = %s\n' % config['rsyncd_local_address'] + |
610 | + 'port = %d\n' % config['rsyncd_local_port'] + |
611 | + 'log file = /dev/stdout\n' + |
612 | + 'uid = root\n' + |
613 | + 'gid = root\n' + |
614 | + 'list = false\n\n' |
615 | + ) |
616 | + rsyncd_secrets = [] |
617 | + rsyncd_secrets.append((config['restore_username'], config['restore_password'])) |
618 | + built_rsyncd_conf += ( |
619 | + '[%s]\n' + |
620 | + ' path = %s\n' + |
621 | + ' auth users = %s\n' + |
622 | + ' secrets file = %s\n' + |
623 | + ' read only = false\n\n' |
624 | + ) % ( |
625 | + config['restore_module'], |
626 | + config['restore_path'], |
627 | + config['restore_username'], |
628 | + os.path.join(config['var_dir'], 'rsyncd.secrets'), |
629 | + ) |
630 | + for s in config['sources']: |
631 | + sd = config['sources'][s] |
632 | + rsyncd_secrets.append((sd['username'], sd['password'])) |
633 | + built_rsyncd_conf += ( |
634 | + '[%s]\n' + |
635 | + ' path = %s\n' + |
636 | + ' auth users = %s\n' + |
637 | + ' secrets file = %s\n' + |
638 | + ' read only = true\n\n' |
639 | + ) % ( |
640 | + s, |
641 | + sd['path'], |
642 | + sd['username'], |
643 | + os.path.join(config['var_dir'], 'rsyncd.secrets'), |
644 | + ) |
645 | + with open(os.path.join(config['var_dir'], 'rsyncd.conf'), 'w') as f: |
646 | + f.write(built_rsyncd_conf) |
647 | + |
648 | + # Build rsyncd.secrets |
649 | + built_rsyncd_secrets = '' |
650 | + for (username, password) in rsyncd_secrets: |
651 | + built_rsyncd_secrets += username + ':' + password + '\n' |
652 | + with open(os.path.join(config['var_dir'], 'rsyncd.secrets'), 'w') as f: |
653 | + os.fchmod(f.fileno(), 0o600) |
654 | + f.write(built_rsyncd_secrets) |
655 | + |
656 | + |
657 | +def init_is_upstart(): |
658 | + try: |
659 | + return 'upstart' in subprocess.check_output( |
660 | + ['initctl', 'version'], |
661 | + stderr=subprocess.DEVNULL, universal_newlines=True) |
662 | + except (FileNotFoundError, subprocess.CalledProcessError): |
663 | + return False |
664 | + |
665 | + |
666 | +def start_services(): |
667 | + # Start rsyncd if it isn't already running. |
668 | + # Note that we do *not* need to reload rsyncd when changing rsyncd.conf, |
669 | + # as it rereads it on every client connection; but we may need to start |
670 | + # it as it won't start if its configuration file doesn't exist. |
671 | + if init_is_upstart(): |
672 | + # With Upstart, start will fail if the service is already running, |
673 | + # so we need to check for that first. |
674 | + try: |
675 | + if 'start/running' in subprocess.check_output( |
676 | + ['status', 'turku-agent-rsyncd'], |
677 | + stderr=subprocess.STDOUT, universal_newlines=True): |
678 | + return |
679 | + except subprocess.CalledProcessError: |
680 | + pass |
681 | + subprocess.check_call(['service', 'turku-agent-rsyncd', 'start']) |
682 | + |
683 | + |
684 | +def send_config(config): |
685 | + required_keys = ['api_url'] |
686 | + if 'api_auth' not in config: |
687 | + required_keys += ['api_auth_name', 'api_auth_secret'] |
688 | + for k in required_keys: |
689 | + if k not in config: |
690 | + raise IncompleteConfigError('Required config "%s" not found.' % k) |
691 | + |
692 | + api_out = {} |
693 | + if ('api_auth_name' in config) and ('api_auth_secret' in config): |
694 | + # name/secret style |
695 | + api_out['auth'] = { |
696 | + 'name': config['api_auth_name'], |
697 | + 'secret': config['api_auth_secret'], |
698 | + } |
699 | + else: |
700 | + # nameless secret style |
701 | + api_out['auth'] = config['api_auth'] |
702 | + |
703 | + # Merge the following options into the machine section |
704 | + machine_merge_map = ( |
705 | + ('machine_uuid', 'uuid'), |
706 | + ('machine_secret', 'secret'), |
707 | + ('environment_name', 'environment_name'), |
708 | + ('service_name', 'service_name'), |
709 | + ('unit_name', 'unit_name'), |
710 | + ('ssh_public_key', 'ssh_public_key'), |
711 | + ('published', 'published'), |
712 | + ) |
713 | + api_out['machine'] = {} |
714 | + for a, b in machine_merge_map: |
715 | + if a in config: |
716 | + api_out['machine'][b] = config[a] |
717 | + |
718 | + api_out['machine']['sources'] = config['sources'] |
719 | + |
720 | + api_reply = api_call(config['api_url'], 'update_config', api_out) |
721 | + |
722 | + |
723 | +def main(): |
724 | + args = parse_args() |
725 | + # Sleep a random amount of time if requested |
726 | + if args.wait: |
727 | + time.sleep(random.uniform(0, args.wait)) |
728 | + |
729 | + config = load_config(args.config_dir) |
730 | + lock = acquire_lock(os.path.join(config['lock_dir'], 'turku-update-config.lock')) |
731 | + fill_config(config) |
732 | + if args.debug: |
733 | + print(json_dumps_p(config)) |
734 | + write_conf_files(config) |
735 | + try: |
736 | + send_config(config) |
737 | + except Exception as e: |
738 | + if args.debug: |
739 | + raise |
740 | + logging.exception(e) |
741 | + return 1 |
742 | + start_services() |
743 | + |
744 | + lock.close() |
745 | |
746 | === added file 'turku_agent/utils.py' |
747 | --- turku_agent/utils.py 1970-01-01 00:00:00 +0000 |
748 | +++ turku_agent/utils.py 2019-06-15 00:58:25 +0000 |
749 | @@ -0,0 +1,332 @@ |
750 | +#!/usr/bin/env python3 |
751 | + |
752 | +# Turku backups - client agent |
753 | +# Copyright 2015 Canonical Ltd. |
754 | +# |
755 | +# This program is free software: you can redistribute it and/or modify it |
756 | +# under the terms of the GNU General Public License version 3, as published by |
757 | +# the Free Software Foundation. |
758 | +# |
759 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
760 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
761 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
762 | +# General Public License for more details. |
763 | +# |
764 | +# You should have received a copy of the GNU General Public License along with |
765 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
766 | + |
767 | +import uuid |
768 | +import string |
769 | +import random |
770 | +import json |
771 | +import os |
772 | +import copy |
773 | +import subprocess |
774 | +import platform |
775 | +import urllib.parse |
776 | +import http.client |
777 | + |
778 | + |
779 | +class RuntimeLock(): |
780 | + name = None |
781 | + file = None |
782 | + |
783 | + def __init__(self, name): |
784 | + import fcntl |
785 | + file = open(name, 'w') |
786 | + try: |
787 | + fcntl.lockf(file, fcntl.LOCK_EX | fcntl.LOCK_NB) |
788 | + except IOError as e: |
789 | + import errno |
790 | + if e.errno in (errno.EACCES, errno.EAGAIN): |
791 | + raise |
792 | + file.write('%10s\n' % os.getpid()) |
793 | + file.flush() |
794 | + file.seek(0) |
795 | + self.name = name |
796 | + self.file = file |
797 | + |
798 | + def close(self): |
799 | + if self.file: |
800 | + self.file.close() |
801 | + self.file = None |
802 | + os.unlink(self.name) |
803 | + |
804 | + def __del__(self): |
805 | + self.close() |
806 | + |
807 | + def __enter__(self): |
808 | + self.file.__enter__() |
809 | + return self |
810 | + |
811 | + def __exit__(self, exc, value, tb): |
812 | + result = self.file.__exit__(exc, value, tb) |
813 | + self.close() |
814 | + return result |
815 | + |
816 | + |
817 | +def acquire_lock(name): |
818 | + return RuntimeLock(name) |
819 | + |
820 | + |
821 | +def json_dump_p(obj, f): |
822 | + """Calls json.dump with standard (pretty) formatting""" |
823 | + return json.dump(obj, f, sort_keys=True, indent=4, separators=(',', ': ')) |
824 | + |
825 | + |
826 | +def json_dumps_p(obj): |
827 | + """Calls json.dumps with standard (pretty) formatting""" |
828 | + return json.dumps(obj, sort_keys=True, indent=4, separators=(',', ': ')) |
829 | + |
830 | + |
831 | +def json_load_file(file): |
832 | + with open(file) as f: |
833 | + try: |
834 | + return json.load(f) |
835 | + except ValueError as e: |
836 | + e.args += (file,) |
837 | + raise |
838 | + |
839 | + |
840 | +def dict_merge(s, m): |
841 | + """Recursively merge one dict into another.""" |
842 | + if not isinstance(m, dict): |
843 | + return m |
844 | + out = copy.deepcopy(s) |
845 | + for k, v in list(m.items()): |
846 | + if k in out and isinstance(out[k], dict): |
847 | + out[k] = dict_merge(out[k], v) |
848 | + else: |
849 | + out[k] = copy.deepcopy(v) |
850 | + return out |
851 | + |
852 | + |
853 | +def load_config(config_dir): |
854 | + config = {} |
855 | + config['config_dir'] = config_dir |
856 | + |
857 | + config_d = os.path.join(config['config_dir'], 'config.d') |
858 | + sources_d = os.path.join(config['config_dir'], 'sources.d') |
859 | + |
860 | + # Merge in config.d/*.json to the root level |
861 | + config_files = [] |
862 | + if os.path.isdir(config_d): |
863 | + config_files = [ |
864 | + os.path.join(config_d, fn) |
865 | + for fn in os.listdir(config_d) |
866 | + if fn.endswith('.json') |
867 | + and os.path.isfile(os.path.join(config_d, fn)) |
868 | + and os.access(os.path.join(config_d, fn), os.R_OK) |
869 | + ] |
870 | + config_files.sort() |
871 | + for file in config_files: |
872 | + config = dict_merge(config, json_load_file(file)) |
873 | + |
874 | + if 'var_dir' not in config: |
875 | + config['var_dir'] = '/var/lib/turku-agent' |
876 | + |
877 | + var_config_d = os.path.join(config['var_dir'], 'config.d') |
878 | + |
879 | + # Load /var config.d files |
880 | + var_config = {} |
881 | + var_config_files = [] |
882 | + if os.path.isdir(var_config_d): |
883 | + var_config_files = [ |
884 | + os.path.join(var_config_d, fn) |
885 | + for fn in os.listdir(var_config_d) |
886 | + if fn.endswith('.json') |
887 | + and os.path.isfile(os.path.join(var_config_d, fn)) |
888 | + and os.access(os.path.join(var_config_d, fn), os.R_OK) |
889 | + ] |
890 | + var_config_files.sort() |
891 | + for file in var_config_files: |
892 | + var_config = dict_merge(var_config, json_load_file(file)) |
893 | + # /etc gets priority over /var |
894 | + var_config = dict_merge(var_config, config) |
895 | + config = var_config |
896 | + |
897 | + if 'lock_dir' not in config: |
898 | + config['lock_dir'] = '/var/lock' |
899 | + |
900 | + if 'rsyncd_command' not in config: |
901 | + config['rsyncd_command'] = ['rsync'] |
902 | + |
903 | + if 'rsyncd_local_address' not in config: |
904 | + config['rsyncd_local_address'] = '127.0.0.1' |
905 | + |
906 | + if 'rsyncd_local_port' not in config: |
907 | + config['rsyncd_local_port'] = 27873 |
908 | + |
909 | + if 'ssh_command' not in config: |
910 | + config['ssh_command'] = ['ssh'] |
911 | + |
912 | + if 'gonogo_program' not in config: |
913 | + config['gonogo_program'] = [] |
914 | + |
915 | + var_sources_d = os.path.join(config['var_dir'], 'sources.d') |
916 | + |
917 | + # Validate the unit name |
918 | + if 'unit_name' not in config: |
919 | + config['unit_name'] = platform.node() |
920 | + # If this isn't in the on-disk config, don't write it; just |
921 | + # generate it every time |
922 | + |
923 | + # Pull the SSH public key |
924 | + if os.path.isfile(os.path.join(config['var_dir'], 'ssh_key.pub')): |
925 | + with open(os.path.join(config['var_dir'], 'ssh_key.pub')) as f: |
926 | + config['ssh_public_key'] = f.read().rstrip() |
927 | + config['ssh_public_key_file'] = os.path.join(config['var_dir'], 'ssh_key.pub') |
928 | + config['ssh_private_key_file'] = os.path.join(config['var_dir'], 'ssh_key') |
929 | + |
930 | + sources_config = {} |
931 | + # Merge in sources.d/*.json to the sources dict |
932 | + sources_files = [] |
933 | + if os.path.isdir(sources_d): |
934 | + sources_files = [ |
935 | + os.path.join(sources_d, fn) |
936 | + for fn in os.listdir(sources_d) |
937 | + if fn.endswith('.json') |
938 | + and os.path.isfile(os.path.join(sources_d, fn)) |
939 | + and os.access(os.path.join(sources_d, fn), os.R_OK) |
940 | + ] |
941 | + sources_files.sort() |
942 | + var_sources_files = [] |
943 | + if os.path.isdir(var_sources_d): |
944 | + var_sources_files = [ |
945 | + os.path.join(var_sources_d, fn) |
946 | + for fn in os.listdir(var_sources_d) |
947 | + if fn.endswith('.json') |
948 | + and os.path.isfile(os.path.join(var_sources_d, fn)) |
949 | + and os.access(os.path.join(var_sources_d, fn), os.R_OK) |
950 | + ] |
951 | + var_sources_files.sort() |
952 | + sources_files += var_sources_files |
953 | + for file in sources_files: |
954 | + sources_config = dict_merge(sources_config, json_load_file(file)) |
955 | + |
956 | + # Check for required sources options |
957 | + for s in list(sources_config.keys()): |
958 | + if 'path' not in sources_config[s]: |
959 | + del sources_config[s] |
960 | + |
961 | + config['sources'] = sources_config |
962 | + |
963 | + return config |
964 | + |
965 | + |
966 | +def fill_config(config): |
967 | + config_d = os.path.join(config['config_dir'], 'config.d') |
968 | + sources_d = os.path.join(config['config_dir'], 'sources.d') |
969 | + var_config_d = os.path.join(config['var_dir'], 'config.d') |
970 | + var_sources_d = os.path.join(config['var_dir'], 'sources.d') |
971 | + |
972 | + # Create required directories |
973 | + for d in (config_d, sources_d, var_config_d, var_sources_d): |
974 | + if not os.path.isdir(d): |
975 | + os.makedirs(d) |
976 | + |
977 | + # Validate the machine UUID/secret |
978 | + write_uuid_data = False |
979 | + if 'machine_uuid' not in config: |
980 | + config['machine_uuid'] = str(uuid.uuid4()) |
981 | + write_uuid_data = True |
982 | + if 'machine_secret' not in config: |
983 | + config['machine_secret'] = ''.join( |
984 | + random.choice(string.ascii_letters + string.digits) |
985 | + for i in range(30) |
986 | + ) |
987 | + write_uuid_data = True |
988 | + # Write out the machine UUID/secret if needed |
989 | + if write_uuid_data: |
990 | + with open(os.path.join(var_config_d, '10-machine_uuid.json'), 'w') as f: |
991 | + os.fchmod(f.fileno(), 0o600) |
992 | + json_dump_p({ |
993 | + 'machine_uuid': config['machine_uuid'], |
994 | + 'machine_secret': config['machine_secret'], |
995 | + }, f) |
996 | + |
997 | + # Restoration configuration |
998 | + write_restore_data = False |
999 | + if 'restore_path' not in config: |
1000 | + config['restore_path'] = '/var/backups/turku-agent/restore' |
1001 | + write_restore_data = True |
1002 | + if 'restore_module' not in config: |
1003 | + config['restore_module'] = 'turku-restore' |
1004 | + write_restore_data = True |
1005 | + if 'restore_username' not in config: |
1006 | + config['restore_username'] = str(uuid.uuid4()) |
1007 | + write_restore_data = True |
1008 | + if 'restore_password' not in config: |
1009 | + config['restore_password'] = ''.join( |
1010 | + random.choice(string.ascii_letters + string.digits) |
1011 | + for i in range(30) |
1012 | + ) |
1013 | + write_restore_data = True |
1014 | + if write_restore_data: |
1015 | + with open(os.path.join(var_config_d, '10-restore.json'), 'w') as f: |
1016 | + os.fchmod(f.fileno(), 0o600) |
1017 | + restore_out = { |
1018 | + 'restore_path': config['restore_path'], |
1019 | + 'restore_module': config['restore_module'], |
1020 | + 'restore_username': config['restore_username'], |
1021 | + 'restore_password': config['restore_password'], |
1022 | + } |
1023 | + json_dump_p(restore_out, f) |
1024 | + if not os.path.isdir(config['restore_path']): |
1025 | + os.makedirs(config['restore_path']) |
1026 | + |
1027 | + # Generate the SSH keypair if it doesn't exist |
1028 | + if 'ssh_private_key_file' not in config: |
1029 | + subprocess.check_call([ |
1030 | + 'ssh-keygen', '-t', 'rsa', '-N', '', '-C', 'turku', |
1031 | + '-f', os.path.join(config['var_dir'], 'ssh_key') |
1032 | + ]) |
1033 | + with open(os.path.join(config['var_dir'], 'ssh_key.pub')) as f: |
1034 | + config['ssh_public_key'] = f.read().rstrip() |
1035 | + config['ssh_public_key_file'] = os.path.join(config['var_dir'], 'ssh_key.pub') |
1036 | + config['ssh_private_key_file'] = os.path.join(config['var_dir'], 'ssh_key') |
1037 | + |
1038 | + for s in config['sources']: |
1039 | + # Check for missing usernames/passwords |
1040 | + if not ('username' in config['sources'][s] or 'password' in config['sources'][s]): |
1041 | + sources_secrets_d = os.path.join(config['config_dir'], 'sources_secrets.d') |
1042 | + if 'username' not in config['sources'][s]: |
1043 | + config['sources'][s]['username'] = str(uuid.uuid4()) |
1044 | + if 'password' not in config['sources'][s]: |
1045 | + config['sources'][s]['password'] = ''.join( |
1046 | + random.choice(string.ascii_letters + string.digits) |
1047 | + for i in range(30) |
1048 | + ) |
1049 | + with open(os.path.join(var_sources_d, '10-' + s + '.json'), 'w') as f: |
1050 | + os.fchmod(f.fileno(), 0o600) |
1051 | + json_dump_p({ |
1052 | + s: { |
1053 | + 'username': config['sources'][s]['username'], |
1054 | + 'password': config['sources'][s]['password'], |
1055 | + } |
1056 | + }, f) |
1057 | + |
1058 | + |
1059 | +def api_call(api_url, cmd, post_data, timeout=5): |
1060 | + url = urllib.parse.urlparse(api_url) |
1061 | + if url.scheme == 'https': |
1062 | + h = http.client.HTTPSConnection(url.netloc, timeout=timeout) |
1063 | + else: |
1064 | + h = http.client.HTTPConnection(url.netloc, timeout=timeout) |
1065 | + out = json.dumps(post_data) |
1066 | + h.putrequest('POST', '%s/%s' % (url.path, cmd)) |
1067 | + h.putheader('Content-Type', 'application/json') |
1068 | + h.putheader('Content-Length', len(out)) |
1069 | + h.putheader('Accept', 'application/json') |
1070 | + h.endheaders() |
1071 | + h.send(out.encode('UTF-8')) |
1072 | + |
1073 | + res = h.getresponse() |
1074 | + if not res.status == http.client.OK: |
1075 | + raise Exception('Received error %d (%s) from API server' % (res.status, res.reason)) |
1076 | + if not res.getheader('content-type') == 'application/json': |
1077 | + raise Exception('Received invalid reply from API server') |
1078 | + try: |
1079 | + return json.loads(res.read().decode('UTF-8')) |
1080 | + except ValueError: |
1081 | + raise Exception('Received invalid reply from API server') |