Merge lp:~ballot/turku/turku-agent into lp:turku
- turku-agent
- Merge into turku-storage
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Turku | Pending | ||
Review via email: mp+425240@code.launchpad.net |
Commit message
Description of the change
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-subproces s-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
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") |