Merge lp:~ballot/turku/turku-agent into lp:turku

Proposed by Benjamin Allot
Status: Superseded
Proposed branch: lp:~ballot/turku/turku-agent
Merge into: lp:turku
Diff against target: 1459 lines (+1280/-0) (has conflicts)
33 files modified
.bzrignore (+61/-0)
MANIFEST.in (+16/-0)
Makefile (+28/-0)
README (+4/-0)
debian/changelog (+31/-0)
debian/compat (+1/-0)
debian/control (+14/-0)
debian/copyright (+13/-0)
debian/dirs (+2/-0)
debian/install (+2/-0)
debian/rules (+14/-0)
debian/source/format (+1/-0)
debian/turku-agent-rsyncd.conf (+8/-0)
debian/turku-agent-rsyncd.service (+10/-0)
debian/turku-agent.cron.d (+3/-0)
setup.py (+39/-0)
tests/test_stub.py (+8/-0)
tox.ini (+38/-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 (+242/-0)
turku_agent/rsyncd_wrapper.py (+46/-0)
turku_agent/update_config.py (+181/-0)
turku_agent/utils.py (+350/-0)
Conflict adding file .bzrignore.  Moved existing file to .bzrignore.moved.
Conflict adding file MANIFEST.in.  Moved existing file to MANIFEST.in.moved.
Conflict adding file Makefile.  Moved existing file to Makefile.moved.
Conflict adding file README.  Moved existing file to README.moved.
Conflict adding file requirements.txt.  Moved existing file to requirements.txt.moved.
Conflict adding file setup.py.  Moved existing file to setup.py.moved.
Conflict adding file tests.  Moved existing file to tests.moved.
Conflict adding file tox.ini.  Moved existing file to tox.ini.moved.
To merge this branch: bzr merge lp:~ballot/turku/turku-agent
Reviewer Review Type Date Requested Status
Turku Pending
Review via email: mp+425240@code.launchpad.net
To post a comment you must log in.

Unmerged revisions

58. By Benjamin Allot

Add debian directory for packaging.

Lintian errors are still there

57. By Ryan Finnie

Mega-noop cleanup

Reviewed-on: https://code.launchpad.net/~fo0bar/turku/turku-agent-cleanup/+merge/386145
Reviewed-by: Barry Price <email address hidden>
Reviewed-by: Stuart Bishop <email address hidden>

56. By Ryan Finnie

Switch timers from OnCalendar (+ dependency hacks) to monotonic OnUnitActiveSec/OnStartupSec

Reviewed-on: https://code.launchpad.net/~fo0bar/turku/turku-agent-timers/+merge/381281
Reviewed-by: Joel Sing <email address hidden>

55. By Ryan Finnie

Revert subprocess portion of revno 53

Reviewed-on: https://code.launchpad.net/~fo0bar/turku/turku-agent-subprocess-encoding/+merge/381217
Reviewed-by: Joel Sing <email address hidden>

54. By Ryan Finnie

Run timers after network-online.target / time-sync.target

Reviewed-on: https://code.launchpad.net/~fo0bar/turku/turku-agent-timers-network/+merge/381073
Reviewed-by: Haw Loeung <email address hidden>
Reviewed-by: Stuart Bishop <email address hidden>

53. By Ryan Finnie

Move encoding from writes to opens

Reviewed-on: https://code.launchpad.net/~fo0bar/turku/turku-agent-encoding/+merge/381075
Reviewed-by: Haw Loeung <email address hidden>
Reviewed-by: Stuart Bishop <email address hidden>

52. By Ryan Finnie

Allow gonogo_program to be stored in config, allow --gonogo-program to be shlex-split

Reviewed-on: https://code.launchpad.net/~fo0bar/turku/turku-agent-gonogo/+merge/368854
Reviewed-by: Tom Haddon <email address hidden>

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file '.bzrignore'
2--- .bzrignore 1970-01-01 00:00:00 +0000
3+++ .bzrignore 2022-06-22 14:48:04 +0000
4@@ -0,0 +1,61 @@
5+MANIFEST
6+.pybuild/
7+.pytest_cache/
8+
9+# Byte-compiled / optimized / DLL files
10+__pycache__/
11+*.py[cod]
12+
13+# C extensions
14+*.so
15+
16+# Distribution / packaging
17+.Python
18+env/
19+build/
20+develop-eggs/
21+dist/
22+downloads/
23+eggs/
24+.eggs/
25+lib/
26+lib64/
27+parts/
28+sdist/
29+var/
30+*.egg-info/
31+.installed.cfg
32+*.egg
33+
34+# PyInstaller
35+# Usually these files are written by a python script from a template
36+# before PyInstaller builds the exe, so as to inject date/other infos into it.
37+*.manifest
38+*.spec
39+
40+# Installer logs
41+pip-log.txt
42+pip-delete-this-directory.txt
43+
44+# Unit test / coverage reports
45+htmlcov/
46+.tox/
47+.coverage
48+.coverage.*
49+.cache
50+nosetests.xml
51+coverage.xml
52+*,cover
53+
54+# Translations
55+*.mo
56+*.pot
57+
58+# Django stuff:
59+*.log
60+
61+# Sphinx documentation
62+docs/_build/
63+
64+# PyBuilder
65+target/
66
67=== renamed file '.bzrignore' => '.bzrignore.moved'
68=== added file 'MANIFEST.in'
69--- MANIFEST.in 1970-01-01 00:00:00 +0000
70+++ MANIFEST.in 2022-06-22 14:48:04 +0000
71@@ -0,0 +1,16 @@
72+include Makefile
73+include README
74+include requirements.txt
75+include tests/*.py
76+include tox.ini
77+include turku-agent.cron
78+include turku-agent-ping
79+include turku-agent-ping.service
80+include turku-agent-ping.timer
81+include turku-agent-rsyncd.conf
82+include turku-agent-rsyncd.init-debian
83+include turku-agent-rsyncd.service
84+include turku-agent-rsyncd-wrapper
85+include turku-update-config
86+include turku-update-config.service
87+include turku-update-config.timer
88
89=== renamed file 'MANIFEST.in' => 'MANIFEST.in.moved'
90=== added file 'Makefile'
91--- Makefile 1970-01-01 00:00:00 +0000
92+++ Makefile 2022-06-22 14:48:04 +0000
93@@ -0,0 +1,28 @@
94+PYTHON := python3
95+
96+all: build
97+
98+build:
99+ $(PYTHON) setup.py build
100+
101+lint:
102+ $(PYTHON) -mtox -e flake8
103+
104+test:
105+ $(PYTHON) -mtox
106+
107+test-quick:
108+ $(PYTHON) -mtox -e black,flake8,pytest-quick
109+
110+black-check:
111+ $(PYTHON) -mtox -e black
112+
113+black:
114+ $(PYTHON) -mblack $(CURDIR)
115+
116+install: build
117+ $(PYTHON) setup.py install
118+
119+clean:
120+ $(PYTHON) setup.py clean
121+ $(RM) -r build MANIFEST
122
123=== renamed file 'Makefile' => 'Makefile.moved'
124=== added file 'README'
125--- README 1970-01-01 00:00:00 +0000
126+++ README 2022-06-22 14:48:04 +0000
127@@ -0,0 +1,4 @@
128+Turku backups - client agent
129+Copyright 2015 Canonical Ltd.
130+
131+https://launchpad.net/turku
132
133=== renamed file 'README' => 'README.moved'
134=== added directory 'debian'
135=== added file 'debian/changelog'
136--- debian/changelog 1970-01-01 00:00:00 +0000
137+++ debian/changelog 2022-06-22 14:48:04 +0000
138@@ -0,0 +1,31 @@
139+turku-agent (0.1.0~bzr43) precise-cat; urgency=medium
140+
141+ * turku-agent r43
142+
143+ -- Laurent Sesques <laurent.sesques@canonical.com> Mon, 22 May 2017 11:33:17 +0200
144+
145+turku-agent (0.1.0~bzr40-1~0.IS.12.04) precise-cat; urgency=low
146+
147+ * precise-cat rebuild (no changes)
148+
149+ -- Ryan Finnie <ryan.finnie@canonical.com> Sun, 29 Mar 2015 17:25:09 +0000
150+
151+turku-agent (0.1.0~bzr40-1) lucid; urgency=low
152+
153+ * turku-agent r40
154+ * upstart file is not (yet) updated to turku-agent-rsyncd-wrapper, to
155+ assist in a mass migration.
156+
157+ -- Ryan Finnie <ryan.finnie@canonical.com> Sun, 29 Mar 2015 08:19:25 +0000
158+
159+turku-agent (0.0.20150318-1) lucid; urgency=low
160+
161+ * turku-agent r26
162+
163+ -- Ryan Finnie <ryan.finnie@canonical.com> Wed, 18 Mar 2015 05:48:26 +0000
164+
165+turku-agent (0.0.20150225-1) trusty; urgency=medium
166+
167+ * Initial packaging.
168+
169+ -- Ryan Finnie <ryan.finnie@canonical.com> Fri, 06 Mar 2015 05:52:34 +0000
170
171=== added file 'debian/compat'
172--- debian/compat 1970-01-01 00:00:00 +0000
173+++ debian/compat 2022-06-22 14:48:04 +0000
174@@ -0,0 +1,1 @@
175+7
176
177=== added file 'debian/control'
178--- debian/control 1970-01-01 00:00:00 +0000
179+++ debian/control 2022-06-22 14:48:04 +0000
180@@ -0,0 +1,14 @@
181+Source: turku-agent
182+Priority: extra
183+Maintainer: Ryan Finnie <ryan.finnie@canonical.com>
184+Build-Depends: debhelper (>= 7.0.50~), python3-all, dh-python
185+Standards-Version: 3.9.2
186+Section: admin
187+
188+Package: turku-agent
189+Section: admin
190+Architecture: all
191+Depends: ${shlibs:Depends}, ${misc:Depends}, ${python3:Depends}, rsync
192+Recommends: upstart
193+Description: Turku backups (agent)
194+ This package contains the Turku backups agent.
195
196=== added file 'debian/copyright'
197--- debian/copyright 1970-01-01 00:00:00 +0000
198+++ debian/copyright 2022-06-22 14:48:04 +0000
199@@ -0,0 +1,13 @@
200+Copyright 2015 Canonical Ltd.
201+
202+This program is free software: you can redistribute it and/or modify it
203+under the terms of the GNU General Public License version 3, as published by
204+the Free Software Foundation.
205+
206+This program is distributed in the hope that it will be useful, but WITHOUT
207+ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
208+SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
209+General Public License for more details.
210+
211+You should have received a copy of the GNU General Public License along with
212+this program. If not, see /usr/share/common-licenses/GPL-3
213
214=== added file 'debian/dirs'
215--- debian/dirs 1970-01-01 00:00:00 +0000
216+++ debian/dirs 2022-06-22 14:48:04 +0000
217@@ -0,0 +1,2 @@
218+etc/turku-agent/config.d
219+etc/turku-agent/sources.d
220
221=== added file 'debian/install'
222--- debian/install 1970-01-01 00:00:00 +0000
223+++ debian/install 2022-06-22 14:48:04 +0000
224@@ -0,0 +1,2 @@
225+debian/turku-agent-rsyncd.conf etc/init
226+debian/turku-agent-rsyncd.service lib/systemd/system
227
228=== added file 'debian/rules'
229--- debian/rules 1970-01-01 00:00:00 +0000
230+++ debian/rules 2022-06-22 14:48:04 +0000
231@@ -0,0 +1,14 @@
232+#!/usr/bin/make -f
233+# -*- makefile -*-
234+# Sample debian/rules that uses debhelper.
235+# This file was originally written by Joey Hess and Craig Small.
236+# As a special exception, when this file is copied by dh-make into a
237+# dh-make output file, you may use that output file without restriction.
238+# This special exception was added by Craig Small in version 0.37 of dh-make.
239+
240+# Uncomment this to turn on verbose mode.
241+export DH_VERBOSE=1
242+export PYBUILD_NAME=turku-agent
243+
244+%:
245+ dh $@ --with python3 --buildsystem=pybuild
246
247=== added directory 'debian/source'
248=== added file 'debian/source/format'
249--- debian/source/format 1970-01-01 00:00:00 +0000
250+++ debian/source/format 2022-06-22 14:48:04 +0000
251@@ -0,0 +1,1 @@
252+1.0
253
254=== added file 'debian/turku-agent-rsyncd.conf'
255--- debian/turku-agent-rsyncd.conf 1970-01-01 00:00:00 +0000
256+++ debian/turku-agent-rsyncd.conf 2022-06-22 14:48:04 +0000
257@@ -0,0 +1,8 @@
258+description "turku rsync daemon"
259+
260+start on runlevel [2345]
261+stop on runlevel [!2345]
262+
263+respawn
264+
265+exec /usr/bin/rsync --no-detach --daemon --config=/var/lib/turku-agent/rsyncd.conf
266
267=== added file 'debian/turku-agent-rsyncd.service'
268--- debian/turku-agent-rsyncd.service 1970-01-01 00:00:00 +0000
269+++ debian/turku-agent-rsyncd.service 2022-06-22 14:48:04 +0000
270@@ -0,0 +1,10 @@
271+[Unit]
272+Description=turku rsyncd daemon
273+ConditionPathExists=/var/lib/turku-agent/rsyncd.conf
274+
275+[Service]
276+ExecStart=/usr/bin/env turku-agent-rsyncd-wrapper
277+Restart=always
278+
279+[Install]
280+WantedBy=multi-user.target
281
282=== added file 'debian/turku-agent.cron.d'
283--- debian/turku-agent.cron.d 1970-01-01 00:00:00 +0000
284+++ debian/turku-agent.cron.d 2022-06-22 14:48:04 +0000
285@@ -0,0 +1,3 @@
286+PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
287+*/5 * * * * root turku-agent-ping --wait=300 >/dev/null 2>/dev/null
288+0 0,12 * * * root turku-update-config --wait=7200 >/dev/null 2>/dev/null
289
290=== added file 'requirements.txt'
291=== renamed file 'requirements.txt' => 'requirements.txt.moved'
292=== added file 'setup.py'
293--- setup.py 1970-01-01 00:00:00 +0000
294+++ setup.py 2022-06-22 14:48:04 +0000
295@@ -0,0 +1,39 @@
296+#!/usr/bin/env python3
297+
298+# Turku backups - client agent
299+# Copyright 2015 Canonical Ltd.
300+#
301+# This program is free software: you can redistribute it and/or modify it
302+# under the terms of the GNU General Public License version 3, as published by
303+# the Free Software Foundation.
304+#
305+# This program is distributed in the hope that it will be useful, but WITHOUT
306+# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
307+# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
308+# General Public License for more details.
309+#
310+# You should have received a copy of the GNU General Public License along with
311+# this program. If not, see <http://www.gnu.org/licenses/>.
312+
313+import sys
314+from setuptools import setup
315+
316+assert sys.version_info > (3, 4)
317+
318+
319+setup(
320+ name="turku_agent",
321+ description="Turku backups - client agent",
322+ version="0.2.0",
323+ author="Ryan Finnie",
324+ author_email="ryan.finnie@canonical.com",
325+ url="https://launchpad.net/turku",
326+ packages=["turku_agent"],
327+ entry_points={
328+ "console_scripts": [
329+ "turku-agent-ping = turku_agent.ping:main",
330+ "turku-agent-rsyncd-wrapper = turku_agent.rsyncd_wrapper:main",
331+ "turku-update-config = turku_agent.update_config:main",
332+ ]
333+ },
334+)
335
336=== renamed file 'setup.py' => 'setup.py.moved'
337=== added directory 'tests'
338=== renamed directory 'tests' => 'tests.moved'
339=== added file 'tests/__init__.py'
340=== added file 'tests/test_stub.py'
341--- tests/test_stub.py 1970-01-01 00:00:00 +0000
342+++ tests/test_stub.py 2022-06-22 14:48:04 +0000
343@@ -0,0 +1,8 @@
344+import unittest
345+import warnings
346+
347+
348+class TestStub(unittest.TestCase):
349+ def test_stub(self):
350+ # pytest doesn't like a tests/ with no tests
351+ warnings.warn("Remove this file once unit tests are added")
352
353=== added file 'tox.ini'
354--- tox.ini 1970-01-01 00:00:00 +0000
355+++ tox.ini 2022-06-22 14:48:04 +0000
356@@ -0,0 +1,38 @@
357+[tox]
358+envlist = black, flake8, pytest
359+
360+[testenv]
361+basepython = python
362+
363+[testenv:black]
364+commands = python -mblack --check .
365+deps = black
366+
367+[testenv:flake8]
368+commands = python -mflake8
369+deps = flake8
370+
371+[testenv:pytest]
372+commands = python -mpytest --cov=turku_agent --cov-report=term-missing
373+deps = pytest
374+ pytest-cov
375+ -r{toxinidir}/requirements.txt
376+
377+[testenv:pytest-quick]
378+commands = python -mpytest -m "not slow"
379+deps = pytest
380+ -r{toxinidir}/requirements.txt
381+
382+[flake8]
383+exclude =
384+ .git,
385+ __pycache__,
386+ .tox,
387+# TODO: remove C901 once complexity is reduced
388+ignore = C901,E203,E231,W503
389+max-line-length = 120
390+max-complexity = 10
391+
392+[pytest]
393+markers =
394+ slow
395
396=== renamed file 'tox.ini' => 'tox.ini.moved'
397=== added file 'turku-agent-ping'
398--- turku-agent-ping 1970-01-01 00:00:00 +0000
399+++ turku-agent-ping 2022-06-22 14:48:04 +0000
400@@ -0,0 +1,20 @@
401+#!/usr/bin/env python3
402+
403+# Turku backups - client agent
404+# Copyright 2015 Canonical Ltd.
405+#
406+# This program is free software: you can redistribute it and/or modify it
407+# under the terms of the GNU General Public License version 3, as published by
408+# the Free Software Foundation.
409+#
410+# This program is distributed in the hope that it will be useful, but WITHOUT
411+# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
412+# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
413+# General Public License for more details.
414+#
415+# You should have received a copy of the GNU General Public License along with
416+# this program. If not, see <http://www.gnu.org/licenses/>.
417+
418+import sys
419+from turku_agent import ping
420+sys.exit(ping.main())
421
422=== added file 'turku-agent-ping.service'
423--- turku-agent-ping.service 1970-01-01 00:00:00 +0000
424+++ turku-agent-ping.service 2022-06-22 14:48:04 +0000
425@@ -0,0 +1,6 @@
426+[Unit]
427+Description=turku-agent-ping
428+
429+[Service]
430+Type=oneshot
431+ExecStart=/usr/bin/env turku-agent-ping
432
433=== added file 'turku-agent-ping.timer'
434--- turku-agent-ping.timer 1970-01-01 00:00:00 +0000
435+++ turku-agent-ping.timer 2022-06-22 14:48:04 +0000
436@@ -0,0 +1,10 @@
437+[Unit]
438+Description=turku-agent-ping
439+
440+[Timer]
441+OnUnitActiveSec=5m
442+RandomizedDelaySec=5m
443+OnStartupSec=15m
444+
445+[Install]
446+WantedBy=timers.target
447
448=== added file 'turku-agent-rsyncd-wrapper'
449--- turku-agent-rsyncd-wrapper 1970-01-01 00:00:00 +0000
450+++ turku-agent-rsyncd-wrapper 2022-06-22 14:48:04 +0000
451@@ -0,0 +1,20 @@
452+#!/usr/bin/env python3
453+
454+# Turku backups - client agent
455+# Copyright 2015 Canonical Ltd.
456+#
457+# This program is free software: you can redistribute it and/or modify it
458+# under the terms of the GNU General Public License version 3, as published by
459+# the Free Software Foundation.
460+#
461+# This program is distributed in the hope that it will be useful, but WITHOUT
462+# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
463+# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
464+# General Public License for more details.
465+#
466+# You should have received a copy of the GNU General Public License along with
467+# this program. If not, see <http://www.gnu.org/licenses/>.
468+
469+import sys
470+from turku_agent import rsyncd_wrapper
471+sys.exit(rsyncd_wrapper.main())
472
473=== added file 'turku-agent-rsyncd.conf'
474--- turku-agent-rsyncd.conf 1970-01-01 00:00:00 +0000
475+++ turku-agent-rsyncd.conf 2022-06-22 14:48:04 +0000
476@@ -0,0 +1,8 @@
477+description "turku rsync daemon"
478+
479+start on runlevel [2345]
480+stop on runlevel [!2345]
481+
482+respawn
483+
484+exec /usr/bin/env turku-agent-rsyncd-wrapper
485
486=== added file 'turku-agent-rsyncd.init-debian'
487--- turku-agent-rsyncd.init-debian 1970-01-01 00:00:00 +0000
488+++ turku-agent-rsyncd.init-debian 2022-06-22 14:48:04 +0000
489@@ -0,0 +1,55 @@
490+#! /bin/sh
491+
492+### BEGIN INIT INFO
493+# Provides: turku-agent-rsyncd
494+# Required-Start: $remote_fs $syslog
495+# Required-Stop: $remote_fs $syslog
496+# Should-Start: $named
497+# Default-Start: 2 3 4 5
498+# Default-Stop:
499+# Short-Description: turku rsync daemon
500+# Description: turku rsync daemon
501+### END INIT INFO
502+
503+set -e
504+
505+PID_FILE=/var/run/turku-agent-rsyncd.pid
506+
507+. /lib/lsb/init-functions
508+
509+export PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
510+
511+case "$1" in
512+ start)
513+ log_daemon_msg "Starting turku rsync daemon" "turku-agent-rsyncd"
514+ start-stop-daemon --start --quiet --background --make-pidfile \
515+ --pidfile $PID_FILE --startas /usr/bin/env -- turku-agent-rsyncd-wrapper
516+ log_end_msg $?
517+ ;;
518+ stop)
519+ log_daemon_msg "Stopping turku rsync daemon" "turku-agent-rsyncd"
520+ start-stop-daemon --stop --quiet --oknodo --pidfile $PID_FILE --retry 5
521+ log_end_msg $?
522+ rm -f $PID_FILE
523+ ;;
524+
525+ reload|force-reload)
526+ log_daemon_msg "Reloading turku rsync daemon" "turku-agent-rsyncd"
527+ log_end_msg 0
528+ ;;
529+
530+ restart)
531+ "$0" stop
532+ "$0" start
533+ ;;
534+
535+ status)
536+ status_of_proc -p $PID_FILE rsync turku-agent-rsyncd
537+ exit $? # notreached due to set -e
538+ ;;
539+ *)
540+ echo "Usage: /etc/init.d/turku-agent-rsyncd {start|stop|reload|force-reload|restart|status}"
541+ exit 1
542+esac
543+
544+exit 0
545
546=== added file 'turku-agent-rsyncd.service'
547--- turku-agent-rsyncd.service 1970-01-01 00:00:00 +0000
548+++ turku-agent-rsyncd.service 2022-06-22 14:48:04 +0000
549@@ -0,0 +1,10 @@
550+[Unit]
551+Description=turku rsyncd daemon
552+ConditionPathExists=/var/lib/turku-agent/rsyncd.conf
553+
554+[Service]
555+ExecStart=/usr/bin/env turku-agent-rsyncd-wrapper
556+Restart=always
557+
558+[Install]
559+WantedBy=multi-user.target
560
561=== added file 'turku-agent.cron'
562--- turku-agent.cron 1970-01-01 00:00:00 +0000
563+++ turku-agent.cron 2022-06-22 14:48:04 +0000
564@@ -0,0 +1,3 @@
565+PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
566+*/5 * * * * root sh -c 'systemctl is-active basic.target 2>/dev/null >/dev/null || turku-agent-ping --wait=300 >/dev/null 2>/dev/null'
567+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'
568
569=== added file 'turku-update-config'
570--- turku-update-config 1970-01-01 00:00:00 +0000
571+++ turku-update-config 2022-06-22 14:48:04 +0000
572@@ -0,0 +1,20 @@
573+#!/usr/bin/env python3
574+
575+# Turku backups - client agent
576+# Copyright 2015 Canonical Ltd.
577+#
578+# This program is free software: you can redistribute it and/or modify it
579+# under the terms of the GNU General Public License version 3, as published by
580+# the Free Software Foundation.
581+#
582+# This program is distributed in the hope that it will be useful, but WITHOUT
583+# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
584+# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
585+# General Public License for more details.
586+#
587+# You should have received a copy of the GNU General Public License along with
588+# this program. If not, see <http://www.gnu.org/licenses/>.
589+
590+import sys
591+from turku_agent import update_config
592+sys.exit(update_config.main())
593
594=== added file 'turku-update-config.service'
595--- turku-update-config.service 1970-01-01 00:00:00 +0000
596+++ turku-update-config.service 2022-06-22 14:48:04 +0000
597@@ -0,0 +1,6 @@
598+[Unit]
599+Description=turku-update-config
600+
601+[Service]
602+Type=oneshot
603+ExecStart=/usr/bin/env turku-update-config
604
605=== added file 'turku-update-config.timer'
606--- turku-update-config.timer 1970-01-01 00:00:00 +0000
607+++ turku-update-config.timer 2022-06-22 14:48:04 +0000
608@@ -0,0 +1,10 @@
609+[Unit]
610+Description=turku-update-config
611+
612+[Timer]
613+OnUnitActiveSec=12h
614+RandomizedDelaySec=12h
615+OnStartupSec=1h
616+
617+[Install]
618+WantedBy=timers.target
619
620=== added directory 'turku_agent'
621=== added file 'turku_agent/__init__.py'
622=== added file 'turku_agent/ping.py'
623--- turku_agent/ping.py 1970-01-01 00:00:00 +0000
624+++ turku_agent/ping.py 2022-06-22 14:48:04 +0000
625@@ -0,0 +1,242 @@
626+# Turku backups - client agent
627+# Copyright 2015 Canonical Ltd.
628+#
629+# This program is free software: you can redistribute it and/or modify it
630+# under the terms of the GNU General Public License version 3, as published by
631+# the Free Software Foundation.
632+#
633+# This program is distributed in the hope that it will be useful, but WITHOUT
634+# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
635+# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
636+# General Public License for more details.
637+#
638+# You should have received a copy of the GNU General Public License along with
639+# this program. If not, see <http://www.gnu.org/licenses/>.
640+
641+
642+import json
643+import os
644+import random
645+import shlex
646+import subprocess
647+import tempfile
648+import time
649+
650+from .utils import load_config, acquire_lock, api_call
651+
652+
653+def parse_args():
654+ import argparse
655+
656+ parser = argparse.ArgumentParser(
657+ formatter_class=argparse.ArgumentDefaultsHelpFormatter
658+ )
659+ parser.add_argument("--config-dir", "-c", type=str, default="/etc/turku-agent")
660+ parser.add_argument("--wait", "-w", type=float)
661+ parser.add_argument("--restore", action="store_true")
662+ parser.add_argument("--restore-storage", type=str, default=None)
663+ parser.add_argument(
664+ "--gonogo-program",
665+ type=str,
666+ default=None,
667+ help="Go/no-go program run each time to determine whether to ping",
668+ )
669+ return parser.parse_args()
670+
671+
672+def call_ssh(config, storage, ssh_req):
673+ # Write the server host public key
674+ t = tempfile.NamedTemporaryFile(mode="w+", encoding="UTF-8")
675+ for key in storage["ssh_ping_host_keys"]:
676+ t.write("%s %s\n" % (storage["ssh_ping_host"], key))
677+ t.flush()
678+
679+ # Call ssh
680+ ssh_command = config["ssh_command"]
681+ ssh_command += [
682+ "-T",
683+ "-o",
684+ "BatchMode=yes",
685+ "-o",
686+ "UserKnownHostsFile=%s" % t.name,
687+ "-o",
688+ "StrictHostKeyChecking=yes",
689+ "-o",
690+ "CheckHostIP=no",
691+ "-i",
692+ config["ssh_private_key_file"],
693+ "-R",
694+ "%d:%s:%d"
695+ % (
696+ ssh_req["port"],
697+ config["rsyncd_local_address"],
698+ config["rsyncd_local_port"],
699+ ),
700+ "-p",
701+ str(storage["ssh_ping_port"]),
702+ "-l",
703+ storage["ssh_ping_user"],
704+ storage["ssh_ping_host"],
705+ "turku-ping-remote",
706+ ]
707+ p = subprocess.Popen(ssh_command, stdin=subprocess.PIPE)
708+
709+ # Write the ssh request
710+ p.stdin.write((json.dumps(ssh_req) + "\n.\n").encode("UTF-8"))
711+ p.stdin.flush()
712+
713+ # Wait for the server to close the SSH connection
714+ try:
715+ p.wait()
716+ except KeyboardInterrupt:
717+ pass
718+
719+ # Cleanup
720+ t.close()
721+
722+
723+def main():
724+ args = parse_args()
725+
726+ # Sleep a random amount of time if requested
727+ if args.wait:
728+ time.sleep(random.uniform(0, args.wait))
729+
730+ config = load_config(args.config_dir)
731+
732+ # Basic checks
733+ for i in ("ssh_private_key_file", "machine_uuid", "machine_secret", "api_url"):
734+ if i not in config:
735+ return
736+ if not os.path.isfile(config["ssh_private_key_file"]):
737+ return
738+
739+ # If a go/no-go program is defined, run it and only go if it exits 0.
740+ # Example: prevent backups during high-load for sensitive systems:
741+ # ['check_load', '-c', '1,5,15']
742+ gonogo_program = (
743+ args.gonogo_program if args.gonogo_program else config["gonogo_program"]
744+ )
745+ if isinstance(gonogo_program, (list, tuple)):
746+ # List, program name first, optional arguments after
747+ gonogo_program_and_args = list(gonogo_program)
748+ elif isinstance(gonogo_program, str):
749+ # String, shlex split it
750+ gonogo_program_and_args = shlex.split(gonogo_program)
751+ else:
752+ # None
753+ gonogo_program_and_args = []
754+ if gonogo_program_and_args:
755+ try:
756+ subprocess.check_call(gonogo_program_and_args)
757+ except (subprocess.CalledProcessError, OSError):
758+ return
759+
760+ lock = acquire_lock(os.path.join(config["lock_dir"], "turku-agent-ping.lock"))
761+
762+ restore_mode = args.restore
763+
764+ # Check with the API server
765+ api_out = {}
766+
767+ machine_merge_map = (("machine_uuid", "uuid"), ("machine_secret", "secret"))
768+ api_out["machine"] = {}
769+ for a, b in machine_merge_map:
770+ if a in config:
771+ api_out["machine"][b] = config[a]
772+
773+ if restore_mode:
774+ print("Entering restore mode.")
775+ print()
776+ api_reply = api_call(config["api_url"], "agent_ping_restore", api_out)
777+
778+ sources_by_storage = {}
779+ for source_name in api_reply["machine"]["sources"]:
780+ source = api_reply["machine"]["sources"][source_name]
781+ if source_name not in config["sources"]:
782+ continue
783+ if "storage" not in source:
784+ continue
785+ if source["storage"]["name"] not in sources_by_storage:
786+ sources_by_storage[source["storage"]["name"]] = {}
787+ sources_by_storage[source["storage"]["name"]][source_name] = source
788+
789+ if len(sources_by_storage) == 0:
790+ print("Cannot find any appropraite sources.")
791+ return
792+ print("This machine's sources are on the following storage units:")
793+ for storage_name in sources_by_storage:
794+ print(" %s" % storage_name)
795+ for source_name in sources_by_storage[storage_name]:
796+ print(" %s" % source_name)
797+ print()
798+ if len(sources_by_storage) == 1:
799+ storage = list(list(sources_by_storage.values())[0].values())[0]["storage"]
800+ elif args.restore_storage:
801+ if args.restore_storage in sources_by_storage:
802+ storage = sources_by_storage[args.restore_storage]["storage"]
803+ else:
804+ print('Cannot find appropriate storage "%s"' % args.restore_storage)
805+ return
806+ else:
807+ print(
808+ "Multiple storages found. Please use --restore-storage to specify one."
809+ )
810+ return
811+
812+ ssh_req = {
813+ "verbose": True,
814+ "action": "restore",
815+ "port": random.randint(49152, 65535),
816+ }
817+ print("Storage unit: %s" % storage["name"])
818+ if "restore_path" in config:
819+ print("Local destination path: %s" % config["restore_path"])
820+ print("Sample restore usage from storage unit:")
821+ print(
822+ " RSYNC_PASSWORD=%s rsync -avzP --numeric-ids ${P?}/ rsync://%s@127.0.0.1:%s/%s/"
823+ % (
824+ config["restore_password"],
825+ config["restore_username"],
826+ ssh_req["port"],
827+ config["restore_module"],
828+ )
829+ )
830+ print()
831+ call_ssh(config, storage, ssh_req)
832+ else:
833+ api_reply = api_call(config["api_url"], "agent_ping_checkin", api_out)
834+
835+ if "scheduled_sources" not in api_reply:
836+ return
837+ sources_by_storage = {}
838+ for source_name in api_reply["machine"]["scheduled_sources"]:
839+ source = api_reply["machine"]["scheduled_sources"][source_name]
840+ if source_name not in config["sources"]:
841+ continue
842+ if "storage" not in source:
843+ continue
844+ if source["storage"]["name"] not in sources_by_storage:
845+ sources_by_storage[source["storage"]["name"]] = {}
846+ sources_by_storage[source["storage"]["name"]][source_name] = source
847+
848+ for storage_name in sources_by_storage:
849+ ssh_req = {
850+ "verbose": True,
851+ "action": "checkin",
852+ "port": random.randint(49152, 65535),
853+ "sources": {},
854+ }
855+ for source in sources_by_storage[storage_name]:
856+ ssh_req["sources"][source] = {
857+ "username": config["sources"][source]["username"],
858+ "password": config["sources"][source]["password"],
859+ }
860+ call_ssh(
861+ config,
862+ list(sources_by_storage[storage_name].values())[0]["storage"],
863+ ssh_req,
864+ )
865+
866+ # Cleanup
867+ lock.close()
868
869=== added file 'turku_agent/rsyncd_wrapper.py'
870--- turku_agent/rsyncd_wrapper.py 1970-01-01 00:00:00 +0000
871+++ turku_agent/rsyncd_wrapper.py 2022-06-22 14:48:04 +0000
872@@ -0,0 +1,46 @@
873+#!/usr/bin/env python3
874+
875+# Turku backups - client agent
876+# Copyright 2015 Canonical Ltd.
877+#
878+# This program is free software: you can redistribute it and/or modify it
879+# under the terms of the GNU General Public License version 3, as published by
880+# the Free Software Foundation.
881+#
882+# This program is distributed in the hope that it will be useful, but WITHOUT
883+# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
884+# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
885+# General Public License for more details.
886+#
887+# You should have received a copy of the GNU General Public License along with
888+# this program. If not, see <http://www.gnu.org/licenses/>.
889+
890+import os
891+
892+from .utils import load_config
893+
894+
895+def parse_args():
896+ import argparse
897+
898+ parser = argparse.ArgumentParser(
899+ formatter_class=argparse.ArgumentDefaultsHelpFormatter
900+ )
901+ parser.add_argument("--config-dir", "-c", type=str, default="/etc/turku-agent")
902+ parser.add_argument("--detach", action="store_true")
903+ return parser.parse_known_args()
904+
905+
906+def main():
907+ args, rest = parse_args()
908+
909+ config = load_config(args.config_dir)
910+ rsyncd_command = config["rsyncd_command"]
911+ if not args.detach:
912+ rsyncd_command.append("--no-detach")
913+ rsyncd_command.append("--daemon")
914+ rsyncd_command.append(
915+ "--config=%s" % os.path.join(config["var_dir"], "rsyncd.conf")
916+ )
917+ rsyncd_command += rest
918+ os.execvp(rsyncd_command[0], rsyncd_command)
919
920=== added file 'turku_agent/update_config.py'
921--- turku_agent/update_config.py 1970-01-01 00:00:00 +0000
922+++ turku_agent/update_config.py 2022-06-22 14:48:04 +0000
923@@ -0,0 +1,181 @@
924+# Turku backups - client agent
925+# Copyright 2015 Canonical Ltd.
926+#
927+# This program is free software: you can redistribute it and/or modify it
928+# under the terms of the GNU General Public License version 3, as published by
929+# the Free Software Foundation.
930+#
931+# This program is distributed in the hope that it will be useful, but WITHOUT
932+# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
933+# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
934+# General Public License for more details.
935+#
936+# You should have received a copy of the GNU General Public License along with
937+# this program. If not, see <http://www.gnu.org/licenses/>.
938+
939+import logging
940+import os
941+import random
942+import subprocess
943+import time
944+
945+from .utils import json_dumps_p, load_config, fill_config, acquire_lock, api_call
946+
947+
948+class IncompleteConfigError(Exception):
949+ pass
950+
951+
952+def parse_args():
953+ import argparse
954+
955+ parser = argparse.ArgumentParser(
956+ formatter_class=argparse.ArgumentDefaultsHelpFormatter
957+ )
958+ parser.add_argument("--config-dir", "-c", type=str, default="/etc/turku-agent")
959+ parser.add_argument("--wait", "-w", type=float)
960+ parser.add_argument("--debug", action="store_true")
961+ return parser.parse_args()
962+
963+
964+def write_conf_files(config):
965+ # Build rsyncd.conf
966+ built_rsyncd_conf = (
967+ "address = %s\n" % config["rsyncd_local_address"]
968+ + "port = %d\n" % config["rsyncd_local_port"]
969+ + "log file = /dev/stdout\n"
970+ + "uid = root\n"
971+ + "gid = root\n"
972+ + "list = false\n\n"
973+ )
974+ rsyncd_secrets = []
975+ rsyncd_secrets.append((config["restore_username"], config["restore_password"]))
976+ built_rsyncd_conf += (
977+ "[%s]\n"
978+ + " path = %s\n"
979+ + " auth users = %s\n"
980+ + " secrets file = %s\n"
981+ + " read only = false\n\n"
982+ ) % (
983+ config["restore_module"],
984+ config["restore_path"],
985+ config["restore_username"],
986+ os.path.join(config["var_dir"], "rsyncd.secrets"),
987+ )
988+ for s in config["sources"]:
989+ sd = config["sources"][s]
990+ rsyncd_secrets.append((sd["username"], sd["password"]))
991+ built_rsyncd_conf += (
992+ "[%s]\n"
993+ + " path = %s\n"
994+ + " auth users = %s\n"
995+ + " secrets file = %s\n"
996+ + " read only = true\n\n"
997+ ) % (
998+ s,
999+ sd["path"],
1000+ sd["username"],
1001+ os.path.join(config["var_dir"], "rsyncd.secrets"),
1002+ )
1003+ with open(os.path.join(config["var_dir"], "rsyncd.conf"), "w") as f:
1004+ f.write(built_rsyncd_conf)
1005+
1006+ # Build rsyncd.secrets
1007+ built_rsyncd_secrets = ""
1008+ for (username, password) in rsyncd_secrets:
1009+ built_rsyncd_secrets += username + ":" + password + "\n"
1010+ with open(os.path.join(config["var_dir"], "rsyncd.secrets"), "w") as f:
1011+ os.fchmod(f.fileno(), 0o600)
1012+ f.write(built_rsyncd_secrets)
1013+
1014+
1015+def init_is_upstart():
1016+ try:
1017+ return "upstart" in subprocess.check_output(
1018+ ["initctl", "version"], stderr=subprocess.DEVNULL, universal_newlines=True
1019+ )
1020+ except (FileNotFoundError, subprocess.CalledProcessError):
1021+ return False
1022+
1023+
1024+def start_services():
1025+ # Start rsyncd if it isn't already running.
1026+ # Note that we do *not* need to reload rsyncd when changing rsyncd.conf,
1027+ # as it rereads it on every client connection; but we may need to start
1028+ # it as it won't start if its configuration file doesn't exist.
1029+ if init_is_upstart():
1030+ # With Upstart, start will fail if the service is already running,
1031+ # so we need to check for that first.
1032+ try:
1033+ if "start/running" in subprocess.check_output(
1034+ ["status", "turku-agent-rsyncd"],
1035+ stderr=subprocess.STDOUT,
1036+ universal_newlines=True,
1037+ ):
1038+ return
1039+ except subprocess.CalledProcessError:
1040+ pass
1041+ subprocess.check_call(["service", "turku-agent-rsyncd", "start"])
1042+
1043+
1044+def send_config(config):
1045+ required_keys = ["api_url"]
1046+ if "api_auth" not in config:
1047+ required_keys += ["api_auth_name", "api_auth_secret"]
1048+ for k in required_keys:
1049+ if k not in config:
1050+ raise IncompleteConfigError('Required config "%s" not found.' % k)
1051+
1052+ api_out = {}
1053+ if ("api_auth_name" in config) and ("api_auth_secret" in config):
1054+ # name/secret style
1055+ api_out["auth"] = {
1056+ "name": config["api_auth_name"],
1057+ "secret": config["api_auth_secret"],
1058+ }
1059+ else:
1060+ # nameless secret style
1061+ api_out["auth"] = config["api_auth"]
1062+
1063+ # Merge the following options into the machine section
1064+ machine_merge_map = (
1065+ ("machine_uuid", "uuid"),
1066+ ("machine_secret", "secret"),
1067+ ("environment_name", "environment_name"),
1068+ ("service_name", "service_name"),
1069+ ("unit_name", "unit_name"),
1070+ ("ssh_public_key", "ssh_public_key"),
1071+ ("published", "published"),
1072+ )
1073+ api_out["machine"] = {}
1074+ for a, b in machine_merge_map:
1075+ if a in config:
1076+ api_out["machine"][b] = config[a]
1077+
1078+ api_out["machine"]["sources"] = config["sources"]
1079+
1080+ api_call(config["api_url"], "update_config", api_out)
1081+
1082+
1083+def main():
1084+ args = parse_args()
1085+ # Sleep a random amount of time if requested
1086+ if args.wait:
1087+ time.sleep(random.uniform(0, args.wait))
1088+
1089+ config = load_config(args.config_dir)
1090+ lock = acquire_lock(os.path.join(config["lock_dir"], "turku-update-config.lock"))
1091+ fill_config(config)
1092+ if args.debug:
1093+ print(json_dumps_p(config))
1094+ write_conf_files(config)
1095+ try:
1096+ send_config(config)
1097+ except Exception as e:
1098+ if args.debug:
1099+ raise
1100+ logging.exception(e)
1101+ return 1
1102+ start_services()
1103+
1104+ lock.close()
1105
1106=== added file 'turku_agent/utils.py'
1107--- turku_agent/utils.py 1970-01-01 00:00:00 +0000
1108+++ turku_agent/utils.py 2022-06-22 14:48:04 +0000
1109@@ -0,0 +1,350 @@
1110+# Turku backups - client agent
1111+# Copyright 2015 Canonical Ltd.
1112+#
1113+# This program is free software: you can redistribute it and/or modify it
1114+# under the terms of the GNU General Public License version 3, as published by
1115+# the Free Software Foundation.
1116+#
1117+# This program is distributed in the hope that it will be useful, but WITHOUT
1118+# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
1119+# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1120+# General Public License for more details.
1121+#
1122+# You should have received a copy of the GNU General Public License along with
1123+# this program. If not, see <http://www.gnu.org/licenses/>.
1124+
1125+import copy
1126+import http.client
1127+import json
1128+import os
1129+import platform
1130+import random
1131+import string
1132+import subprocess
1133+import urllib.parse
1134+import uuid
1135+
1136+
1137+class RuntimeLock:
1138+ name = None
1139+ file = None
1140+
1141+ def __init__(self, name):
1142+ import fcntl
1143+
1144+ file = open(name, "w")
1145+ try:
1146+ fcntl.lockf(file, fcntl.LOCK_EX | fcntl.LOCK_NB)
1147+ except IOError as e:
1148+ import errno
1149+
1150+ if e.errno in (errno.EACCES, errno.EAGAIN):
1151+ raise
1152+ file.write("%10s\n" % os.getpid())
1153+ file.flush()
1154+ file.seek(0)
1155+ self.name = name
1156+ self.file = file
1157+
1158+ def close(self):
1159+ if self.file:
1160+ self.file.close()
1161+ self.file = None
1162+ os.unlink(self.name)
1163+
1164+ def __del__(self):
1165+ self.close()
1166+
1167+ def __enter__(self):
1168+ self.file.__enter__()
1169+ return self
1170+
1171+ def __exit__(self, exc, value, tb):
1172+ result = self.file.__exit__(exc, value, tb)
1173+ self.close()
1174+ return result
1175+
1176+
1177+def acquire_lock(name):
1178+ return RuntimeLock(name)
1179+
1180+
1181+def json_dump_p(obj, f):
1182+ """Calls json.dump with standard (pretty) formatting"""
1183+ return json.dump(obj, f, sort_keys=True, indent=4, separators=(",", ": "))
1184+
1185+
1186+def json_dumps_p(obj):
1187+ """Calls json.dumps with standard (pretty) formatting"""
1188+ return json.dumps(obj, sort_keys=True, indent=4, separators=(",", ": "))
1189+
1190+
1191+def json_load_file(file):
1192+ with open(file) as f:
1193+ try:
1194+ return json.load(f)
1195+ except ValueError as e:
1196+ e.args += (file,)
1197+ raise
1198+
1199+
1200+def dict_merge(s, m):
1201+ """Recursively merge one dict into another."""
1202+ if not isinstance(m, dict):
1203+ return m
1204+ out = copy.deepcopy(s)
1205+ for k, v in list(m.items()):
1206+ if k in out and isinstance(out[k], dict):
1207+ out[k] = dict_merge(out[k], v)
1208+ else:
1209+ out[k] = copy.deepcopy(v)
1210+ return out
1211+
1212+
1213+def load_config(config_dir):
1214+ config = {}
1215+ config["config_dir"] = config_dir
1216+
1217+ config_d = os.path.join(config["config_dir"], "config.d")
1218+ sources_d = os.path.join(config["config_dir"], "sources.d")
1219+
1220+ # Merge in config.d/*.json to the root level
1221+ config_files = []
1222+ if os.path.isdir(config_d):
1223+ config_files = [
1224+ os.path.join(config_d, fn)
1225+ for fn in os.listdir(config_d)
1226+ if fn.endswith(".json")
1227+ and os.path.isfile(os.path.join(config_d, fn))
1228+ and os.access(os.path.join(config_d, fn), os.R_OK)
1229+ ]
1230+ config_files.sort()
1231+ for file in config_files:
1232+ config = dict_merge(config, json_load_file(file))
1233+
1234+ if "var_dir" not in config:
1235+ config["var_dir"] = "/var/lib/turku-agent"
1236+
1237+ var_config_d = os.path.join(config["var_dir"], "config.d")
1238+
1239+ # Load /var config.d files
1240+ var_config = {}
1241+ var_config_files = []
1242+ if os.path.isdir(var_config_d):
1243+ var_config_files = [
1244+ os.path.join(var_config_d, fn)
1245+ for fn in os.listdir(var_config_d)
1246+ if fn.endswith(".json")
1247+ and os.path.isfile(os.path.join(var_config_d, fn))
1248+ and os.access(os.path.join(var_config_d, fn), os.R_OK)
1249+ ]
1250+ var_config_files.sort()
1251+ for file in var_config_files:
1252+ var_config = dict_merge(var_config, json_load_file(file))
1253+ # /etc gets priority over /var
1254+ var_config = dict_merge(var_config, config)
1255+ config = var_config
1256+
1257+ if "lock_dir" not in config:
1258+ config["lock_dir"] = "/var/lock"
1259+
1260+ if "rsyncd_command" not in config:
1261+ config["rsyncd_command"] = ["rsync"]
1262+
1263+ if "rsyncd_local_address" not in config:
1264+ config["rsyncd_local_address"] = "127.0.0.1"
1265+
1266+ if "rsyncd_local_port" not in config:
1267+ config["rsyncd_local_port"] = 27873
1268+
1269+ if "ssh_command" not in config:
1270+ config["ssh_command"] = ["ssh"]
1271+
1272+ # If a go/no-go program is defined, run it and only go if it exits 0.
1273+ # Type: String (program with no args) or list (program first, optional arguments after)
1274+ if "gonogo_program" not in config:
1275+ config["gonogo_program"] = None
1276+
1277+ var_sources_d = os.path.join(config["var_dir"], "sources.d")
1278+
1279+ # Validate the unit name
1280+ if "unit_name" not in config:
1281+ config["unit_name"] = platform.node()
1282+ # If this isn't in the on-disk config, don't write it; just
1283+ # generate it every time
1284+
1285+ # Pull the SSH public key
1286+ if os.path.isfile(os.path.join(config["var_dir"], "ssh_key.pub")):
1287+ with open(os.path.join(config["var_dir"], "ssh_key.pub")) as f:
1288+ config["ssh_public_key"] = f.read().rstrip()
1289+ config["ssh_public_key_file"] = os.path.join(config["var_dir"], "ssh_key.pub")
1290+ config["ssh_private_key_file"] = os.path.join(config["var_dir"], "ssh_key")
1291+
1292+ sources_config = {}
1293+ # Merge in sources.d/*.json to the sources dict
1294+ sources_files = []
1295+ if os.path.isdir(sources_d):
1296+ sources_files = [
1297+ os.path.join(sources_d, fn)
1298+ for fn in os.listdir(sources_d)
1299+ if fn.endswith(".json")
1300+ and os.path.isfile(os.path.join(sources_d, fn))
1301+ and os.access(os.path.join(sources_d, fn), os.R_OK)
1302+ ]
1303+ sources_files.sort()
1304+ var_sources_files = []
1305+ if os.path.isdir(var_sources_d):
1306+ var_sources_files = [
1307+ os.path.join(var_sources_d, fn)
1308+ for fn in os.listdir(var_sources_d)
1309+ if fn.endswith(".json")
1310+ and os.path.isfile(os.path.join(var_sources_d, fn))
1311+ and os.access(os.path.join(var_sources_d, fn), os.R_OK)
1312+ ]
1313+ var_sources_files.sort()
1314+ sources_files += var_sources_files
1315+ for file in sources_files:
1316+ sources_config = dict_merge(sources_config, json_load_file(file))
1317+
1318+ # Check for required sources options
1319+ for s in list(sources_config.keys()):
1320+ if "path" not in sources_config[s]:
1321+ del sources_config[s]
1322+
1323+ config["sources"] = sources_config
1324+
1325+ return config
1326+
1327+
1328+def fill_config(config):
1329+ config_d = os.path.join(config["config_dir"], "config.d")
1330+ sources_d = os.path.join(config["config_dir"], "sources.d")
1331+ var_config_d = os.path.join(config["var_dir"], "config.d")
1332+ var_sources_d = os.path.join(config["var_dir"], "sources.d")
1333+
1334+ # Create required directories
1335+ for d in (config_d, sources_d, var_config_d, var_sources_d):
1336+ if not os.path.isdir(d):
1337+ os.makedirs(d)
1338+
1339+ # Validate the machine UUID/secret
1340+ write_uuid_data = False
1341+ if "machine_uuid" not in config:
1342+ config["machine_uuid"] = str(uuid.uuid4())
1343+ write_uuid_data = True
1344+ if "machine_secret" not in config:
1345+ config["machine_secret"] = "".join(
1346+ random.choice(string.ascii_letters + string.digits) for i in range(30)
1347+ )
1348+ write_uuid_data = True
1349+ # Write out the machine UUID/secret if needed
1350+ if write_uuid_data:
1351+ with open(os.path.join(var_config_d, "10-machine_uuid.json"), "w") as f:
1352+ os.fchmod(f.fileno(), 0o600)
1353+ json_dump_p(
1354+ {
1355+ "machine_uuid": config["machine_uuid"],
1356+ "machine_secret": config["machine_secret"],
1357+ },
1358+ f,
1359+ )
1360+
1361+ # Restoration configuration
1362+ write_restore_data = False
1363+ if "restore_path" not in config:
1364+ config["restore_path"] = "/var/backups/turku-agent/restore"
1365+ write_restore_data = True
1366+ if "restore_module" not in config:
1367+ config["restore_module"] = "turku-restore"
1368+ write_restore_data = True
1369+ if "restore_username" not in config:
1370+ config["restore_username"] = str(uuid.uuid4())
1371+ write_restore_data = True
1372+ if "restore_password" not in config:
1373+ config["restore_password"] = "".join(
1374+ random.choice(string.ascii_letters + string.digits) for i in range(30)
1375+ )
1376+ write_restore_data = True
1377+ if write_restore_data:
1378+ with open(os.path.join(var_config_d, "10-restore.json"), "w") as f:
1379+ os.fchmod(f.fileno(), 0o600)
1380+ restore_out = {
1381+ "restore_path": config["restore_path"],
1382+ "restore_module": config["restore_module"],
1383+ "restore_username": config["restore_username"],
1384+ "restore_password": config["restore_password"],
1385+ }
1386+ json_dump_p(restore_out, f)
1387+ if not os.path.isdir(config["restore_path"]):
1388+ os.makedirs(config["restore_path"])
1389+
1390+ # Generate the SSH keypair if it doesn't exist
1391+ if "ssh_private_key_file" not in config:
1392+ subprocess.check_call(
1393+ [
1394+ "ssh-keygen",
1395+ "-t",
1396+ "rsa",
1397+ "-N",
1398+ "",
1399+ "-C",
1400+ "turku",
1401+ "-f",
1402+ os.path.join(config["var_dir"], "ssh_key"),
1403+ ]
1404+ )
1405+ with open(os.path.join(config["var_dir"], "ssh_key.pub")) as f:
1406+ config["ssh_public_key"] = f.read().rstrip()
1407+ config["ssh_public_key_file"] = os.path.join(config["var_dir"], "ssh_key.pub")
1408+ config["ssh_private_key_file"] = os.path.join(config["var_dir"], "ssh_key")
1409+
1410+ for s in config["sources"]:
1411+ # Check for missing usernames/passwords
1412+ if not (
1413+ "username" in config["sources"][s] or "password" in config["sources"][s]
1414+ ):
1415+ if "username" not in config["sources"][s]:
1416+ config["sources"][s]["username"] = str(uuid.uuid4())
1417+ if "password" not in config["sources"][s]:
1418+ config["sources"][s]["password"] = "".join(
1419+ random.choice(string.ascii_letters + string.digits)
1420+ for i in range(30)
1421+ )
1422+ with open(os.path.join(var_sources_d, "10-" + s + ".json"), "w") as f:
1423+ os.fchmod(f.fileno(), 0o600)
1424+ json_dump_p(
1425+ {
1426+ s: {
1427+ "username": config["sources"][s]["username"],
1428+ "password": config["sources"][s]["password"],
1429+ }
1430+ },
1431+ f,
1432+ )
1433+
1434+
1435+def api_call(api_url, cmd, post_data, timeout=5):
1436+ url = urllib.parse.urlparse(api_url)
1437+ if url.scheme == "https":
1438+ h = http.client.HTTPSConnection(url.netloc, timeout=timeout)
1439+ else:
1440+ h = http.client.HTTPConnection(url.netloc, timeout=timeout)
1441+ out = json.dumps(post_data)
1442+ h.putrequest("POST", "%s/%s" % (url.path, cmd))
1443+ h.putheader("Content-Type", "application/json")
1444+ h.putheader("Content-Length", len(out))
1445+ h.putheader("Accept", "application/json")
1446+ h.endheaders()
1447+ h.send(out.encode("UTF-8"))
1448+
1449+ res = h.getresponse()
1450+ if not res.status == http.client.OK:
1451+ raise Exception(
1452+ "Received error %d (%s) from API server" % (res.status, res.reason)
1453+ )
1454+ if not res.getheader("content-type") == "application/json":
1455+ raise Exception("Received invalid reply from API server")
1456+ try:
1457+ return json.loads(res.read().decode("UTF-8"))
1458+ except ValueError:
1459+ raise Exception("Received invalid reply from API server")

Subscribers

People subscribed via source and target branches

to all changes: