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
=== added file '.bzrignore'
--- .bzrignore 1970-01-01 00:00:00 +0000
+++ .bzrignore 2022-06-22 14:48:04 +0000
@@ -0,0 +1,61 @@
1MANIFEST
2.pybuild/
3.pytest_cache/
4
5# Byte-compiled / optimized / DLL files
6__pycache__/
7*.py[cod]
8
9# C extensions
10*.so
11
12# Distribution / packaging
13.Python
14env/
15build/
16develop-eggs/
17dist/
18downloads/
19eggs/
20.eggs/
21lib/
22lib64/
23parts/
24sdist/
25var/
26*.egg-info/
27.installed.cfg
28*.egg
29
30# PyInstaller
31# Usually these files are written by a python script from a template
32# before PyInstaller builds the exe, so as to inject date/other infos into it.
33*.manifest
34*.spec
35
36# Installer logs
37pip-log.txt
38pip-delete-this-directory.txt
39
40# Unit test / coverage reports
41htmlcov/
42.tox/
43.coverage
44.coverage.*
45.cache
46nosetests.xml
47coverage.xml
48*,cover
49
50# Translations
51*.mo
52*.pot
53
54# Django stuff:
55*.log
56
57# Sphinx documentation
58docs/_build/
59
60# PyBuilder
61target/
062
=== renamed file '.bzrignore' => '.bzrignore.moved'
=== added file 'MANIFEST.in'
--- MANIFEST.in 1970-01-01 00:00:00 +0000
+++ MANIFEST.in 2022-06-22 14:48:04 +0000
@@ -0,0 +1,16 @@
1include Makefile
2include README
3include requirements.txt
4include tests/*.py
5include tox.ini
6include turku-agent.cron
7include turku-agent-ping
8include turku-agent-ping.service
9include turku-agent-ping.timer
10include turku-agent-rsyncd.conf
11include turku-agent-rsyncd.init-debian
12include turku-agent-rsyncd.service
13include turku-agent-rsyncd-wrapper
14include turku-update-config
15include turku-update-config.service
16include turku-update-config.timer
017
=== renamed file 'MANIFEST.in' => 'MANIFEST.in.moved'
=== added file 'Makefile'
--- Makefile 1970-01-01 00:00:00 +0000
+++ Makefile 2022-06-22 14:48:04 +0000
@@ -0,0 +1,28 @@
1PYTHON := python3
2
3all: build
4
5build:
6 $(PYTHON) setup.py build
7
8lint:
9 $(PYTHON) -mtox -e flake8
10
11test:
12 $(PYTHON) -mtox
13
14test-quick:
15 $(PYTHON) -mtox -e black,flake8,pytest-quick
16
17black-check:
18 $(PYTHON) -mtox -e black
19
20black:
21 $(PYTHON) -mblack $(CURDIR)
22
23install: build
24 $(PYTHON) setup.py install
25
26clean:
27 $(PYTHON) setup.py clean
28 $(RM) -r build MANIFEST
029
=== renamed file 'Makefile' => 'Makefile.moved'
=== added file 'README'
--- README 1970-01-01 00:00:00 +0000
+++ README 2022-06-22 14:48:04 +0000
@@ -0,0 +1,4 @@
1Turku backups - client agent
2Copyright 2015 Canonical Ltd.
3
4https://launchpad.net/turku
05
=== renamed file 'README' => 'README.moved'
=== added directory 'debian'
=== added file 'debian/changelog'
--- debian/changelog 1970-01-01 00:00:00 +0000
+++ debian/changelog 2022-06-22 14:48:04 +0000
@@ -0,0 +1,31 @@
1turku-agent (0.1.0~bzr43) precise-cat; urgency=medium
2
3 * turku-agent r43
4
5 -- Laurent Sesques <laurent.sesques@canonical.com> Mon, 22 May 2017 11:33:17 +0200
6
7turku-agent (0.1.0~bzr40-1~0.IS.12.04) precise-cat; urgency=low
8
9 * precise-cat rebuild (no changes)
10
11 -- Ryan Finnie <ryan.finnie@canonical.com> Sun, 29 Mar 2015 17:25:09 +0000
12
13turku-agent (0.1.0~bzr40-1) lucid; urgency=low
14
15 * turku-agent r40
16 * upstart file is not (yet) updated to turku-agent-rsyncd-wrapper, to
17 assist in a mass migration.
18
19 -- Ryan Finnie <ryan.finnie@canonical.com> Sun, 29 Mar 2015 08:19:25 +0000
20
21turku-agent (0.0.20150318-1) lucid; urgency=low
22
23 * turku-agent r26
24
25 -- Ryan Finnie <ryan.finnie@canonical.com> Wed, 18 Mar 2015 05:48:26 +0000
26
27turku-agent (0.0.20150225-1) trusty; urgency=medium
28
29 * Initial packaging.
30
31 -- Ryan Finnie <ryan.finnie@canonical.com> Fri, 06 Mar 2015 05:52:34 +0000
032
=== added file 'debian/compat'
--- debian/compat 1970-01-01 00:00:00 +0000
+++ debian/compat 2022-06-22 14:48:04 +0000
@@ -0,0 +1,1 @@
17
02
=== added file 'debian/control'
--- debian/control 1970-01-01 00:00:00 +0000
+++ debian/control 2022-06-22 14:48:04 +0000
@@ -0,0 +1,14 @@
1Source: turku-agent
2Priority: extra
3Maintainer: Ryan Finnie <ryan.finnie@canonical.com>
4Build-Depends: debhelper (>= 7.0.50~), python3-all, dh-python
5Standards-Version: 3.9.2
6Section: admin
7
8Package: turku-agent
9Section: admin
10Architecture: all
11Depends: ${shlibs:Depends}, ${misc:Depends}, ${python3:Depends}, rsync
12Recommends: upstart
13Description: Turku backups (agent)
14 This package contains the Turku backups agent.
015
=== added file 'debian/copyright'
--- debian/copyright 1970-01-01 00:00:00 +0000
+++ debian/copyright 2022-06-22 14:48:04 +0000
@@ -0,0 +1,13 @@
1Copyright 2015 Canonical Ltd.
2
3This program is free software: you can redistribute it and/or modify it
4under the terms of the GNU General Public License version 3, as published by
5the Free Software Foundation.
6
7This program is distributed in the hope that it will be useful, but WITHOUT
8ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
9SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
10General Public License for more details.
11
12You should have received a copy of the GNU General Public License along with
13this program. If not, see /usr/share/common-licenses/GPL-3
014
=== added file 'debian/dirs'
--- debian/dirs 1970-01-01 00:00:00 +0000
+++ debian/dirs 2022-06-22 14:48:04 +0000
@@ -0,0 +1,2 @@
1etc/turku-agent/config.d
2etc/turku-agent/sources.d
03
=== added file 'debian/install'
--- debian/install 1970-01-01 00:00:00 +0000
+++ debian/install 2022-06-22 14:48:04 +0000
@@ -0,0 +1,2 @@
1debian/turku-agent-rsyncd.conf etc/init
2debian/turku-agent-rsyncd.service lib/systemd/system
03
=== added file 'debian/rules'
--- debian/rules 1970-01-01 00:00:00 +0000
+++ debian/rules 2022-06-22 14:48:04 +0000
@@ -0,0 +1,14 @@
1#!/usr/bin/make -f
2# -*- makefile -*-
3# Sample debian/rules that uses debhelper.
4# This file was originally written by Joey Hess and Craig Small.
5# As a special exception, when this file is copied by dh-make into a
6# dh-make output file, you may use that output file without restriction.
7# This special exception was added by Craig Small in version 0.37 of dh-make.
8
9# Uncomment this to turn on verbose mode.
10export DH_VERBOSE=1
11export PYBUILD_NAME=turku-agent
12
13%:
14 dh $@ --with python3 --buildsystem=pybuild
015
=== added directory 'debian/source'
=== added file 'debian/source/format'
--- debian/source/format 1970-01-01 00:00:00 +0000
+++ debian/source/format 2022-06-22 14:48:04 +0000
@@ -0,0 +1,1 @@
11.0
02
=== added file 'debian/turku-agent-rsyncd.conf'
--- debian/turku-agent-rsyncd.conf 1970-01-01 00:00:00 +0000
+++ debian/turku-agent-rsyncd.conf 2022-06-22 14:48:04 +0000
@@ -0,0 +1,8 @@
1description "turku rsync daemon"
2
3start on runlevel [2345]
4stop on runlevel [!2345]
5
6respawn
7
8exec /usr/bin/rsync --no-detach --daemon --config=/var/lib/turku-agent/rsyncd.conf
09
=== added file 'debian/turku-agent-rsyncd.service'
--- debian/turku-agent-rsyncd.service 1970-01-01 00:00:00 +0000
+++ debian/turku-agent-rsyncd.service 2022-06-22 14:48:04 +0000
@@ -0,0 +1,10 @@
1[Unit]
2Description=turku rsyncd daemon
3ConditionPathExists=/var/lib/turku-agent/rsyncd.conf
4
5[Service]
6ExecStart=/usr/bin/env turku-agent-rsyncd-wrapper
7Restart=always
8
9[Install]
10WantedBy=multi-user.target
011
=== added file 'debian/turku-agent.cron.d'
--- debian/turku-agent.cron.d 1970-01-01 00:00:00 +0000
+++ debian/turku-agent.cron.d 2022-06-22 14:48:04 +0000
@@ -0,0 +1,3 @@
1PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
2*/5 * * * * root turku-agent-ping --wait=300 >/dev/null 2>/dev/null
30 0,12 * * * root turku-update-config --wait=7200 >/dev/null 2>/dev/null
04
=== added file 'requirements.txt'
=== renamed file 'requirements.txt' => 'requirements.txt.moved'
=== added file 'setup.py'
--- setup.py 1970-01-01 00:00:00 +0000
+++ setup.py 2022-06-22 14:48:04 +0000
@@ -0,0 +1,39 @@
1#!/usr/bin/env python3
2
3# Turku backups - client agent
4# Copyright 2015 Canonical Ltd.
5#
6# This program is free software: you can redistribute it and/or modify it
7# under the terms of the GNU General Public License version 3, as published by
8# the Free Software Foundation.
9#
10# This program is distributed in the hope that it will be useful, but WITHOUT
11# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
12# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13# General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License along with
16# this program. If not, see <http://www.gnu.org/licenses/>.
17
18import sys
19from setuptools import setup
20
21assert sys.version_info > (3, 4)
22
23
24setup(
25 name="turku_agent",
26 description="Turku backups - client agent",
27 version="0.2.0",
28 author="Ryan Finnie",
29 author_email="ryan.finnie@canonical.com",
30 url="https://launchpad.net/turku",
31 packages=["turku_agent"],
32 entry_points={
33 "console_scripts": [
34 "turku-agent-ping = turku_agent.ping:main",
35 "turku-agent-rsyncd-wrapper = turku_agent.rsyncd_wrapper:main",
36 "turku-update-config = turku_agent.update_config:main",
37 ]
38 },
39)
040
=== renamed file 'setup.py' => 'setup.py.moved'
=== added directory 'tests'
=== renamed directory 'tests' => 'tests.moved'
=== added file 'tests/__init__.py'
=== added file 'tests/test_stub.py'
--- tests/test_stub.py 1970-01-01 00:00:00 +0000
+++ tests/test_stub.py 2022-06-22 14:48:04 +0000
@@ -0,0 +1,8 @@
1import unittest
2import warnings
3
4
5class TestStub(unittest.TestCase):
6 def test_stub(self):
7 # pytest doesn't like a tests/ with no tests
8 warnings.warn("Remove this file once unit tests are added")
09
=== added file 'tox.ini'
--- tox.ini 1970-01-01 00:00:00 +0000
+++ tox.ini 2022-06-22 14:48:04 +0000
@@ -0,0 +1,38 @@
1[tox]
2envlist = black, flake8, pytest
3
4[testenv]
5basepython = python
6
7[testenv:black]
8commands = python -mblack --check .
9deps = black
10
11[testenv:flake8]
12commands = python -mflake8
13deps = flake8
14
15[testenv:pytest]
16commands = python -mpytest --cov=turku_agent --cov-report=term-missing
17deps = pytest
18 pytest-cov
19 -r{toxinidir}/requirements.txt
20
21[testenv:pytest-quick]
22commands = python -mpytest -m "not slow"
23deps = pytest
24 -r{toxinidir}/requirements.txt
25
26[flake8]
27exclude =
28 .git,
29 __pycache__,
30 .tox,
31# TODO: remove C901 once complexity is reduced
32ignore = C901,E203,E231,W503
33max-line-length = 120
34max-complexity = 10
35
36[pytest]
37markers =
38 slow
039
=== renamed file 'tox.ini' => 'tox.ini.moved'
=== added file 'turku-agent-ping'
--- turku-agent-ping 1970-01-01 00:00:00 +0000
+++ turku-agent-ping 2022-06-22 14:48:04 +0000
@@ -0,0 +1,20 @@
1#!/usr/bin/env python3
2
3# Turku backups - client agent
4# Copyright 2015 Canonical Ltd.
5#
6# This program is free software: you can redistribute it and/or modify it
7# under the terms of the GNU General Public License version 3, as published by
8# the Free Software Foundation.
9#
10# This program is distributed in the hope that it will be useful, but WITHOUT
11# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
12# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13# General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License along with
16# this program. If not, see <http://www.gnu.org/licenses/>.
17
18import sys
19from turku_agent import ping
20sys.exit(ping.main())
021
=== added file 'turku-agent-ping.service'
--- turku-agent-ping.service 1970-01-01 00:00:00 +0000
+++ turku-agent-ping.service 2022-06-22 14:48:04 +0000
@@ -0,0 +1,6 @@
1[Unit]
2Description=turku-agent-ping
3
4[Service]
5Type=oneshot
6ExecStart=/usr/bin/env turku-agent-ping
07
=== added file 'turku-agent-ping.timer'
--- turku-agent-ping.timer 1970-01-01 00:00:00 +0000
+++ turku-agent-ping.timer 2022-06-22 14:48:04 +0000
@@ -0,0 +1,10 @@
1[Unit]
2Description=turku-agent-ping
3
4[Timer]
5OnUnitActiveSec=5m
6RandomizedDelaySec=5m
7OnStartupSec=15m
8
9[Install]
10WantedBy=timers.target
011
=== added file 'turku-agent-rsyncd-wrapper'
--- turku-agent-rsyncd-wrapper 1970-01-01 00:00:00 +0000
+++ turku-agent-rsyncd-wrapper 2022-06-22 14:48:04 +0000
@@ -0,0 +1,20 @@
1#!/usr/bin/env python3
2
3# Turku backups - client agent
4# Copyright 2015 Canonical Ltd.
5#
6# This program is free software: you can redistribute it and/or modify it
7# under the terms of the GNU General Public License version 3, as published by
8# the Free Software Foundation.
9#
10# This program is distributed in the hope that it will be useful, but WITHOUT
11# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
12# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13# General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License along with
16# this program. If not, see <http://www.gnu.org/licenses/>.
17
18import sys
19from turku_agent import rsyncd_wrapper
20sys.exit(rsyncd_wrapper.main())
021
=== added file 'turku-agent-rsyncd.conf'
--- turku-agent-rsyncd.conf 1970-01-01 00:00:00 +0000
+++ turku-agent-rsyncd.conf 2022-06-22 14:48:04 +0000
@@ -0,0 +1,8 @@
1description "turku rsync daemon"
2
3start on runlevel [2345]
4stop on runlevel [!2345]
5
6respawn
7
8exec /usr/bin/env turku-agent-rsyncd-wrapper
09
=== added file 'turku-agent-rsyncd.init-debian'
--- turku-agent-rsyncd.init-debian 1970-01-01 00:00:00 +0000
+++ turku-agent-rsyncd.init-debian 2022-06-22 14:48:04 +0000
@@ -0,0 +1,55 @@
1#! /bin/sh
2
3### BEGIN INIT INFO
4# Provides: turku-agent-rsyncd
5# Required-Start: $remote_fs $syslog
6# Required-Stop: $remote_fs $syslog
7# Should-Start: $named
8# Default-Start: 2 3 4 5
9# Default-Stop:
10# Short-Description: turku rsync daemon
11# Description: turku rsync daemon
12### END INIT INFO
13
14set -e
15
16PID_FILE=/var/run/turku-agent-rsyncd.pid
17
18. /lib/lsb/init-functions
19
20export PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
21
22case "$1" in
23 start)
24 log_daemon_msg "Starting turku rsync daemon" "turku-agent-rsyncd"
25 start-stop-daemon --start --quiet --background --make-pidfile \
26 --pidfile $PID_FILE --startas /usr/bin/env -- turku-agent-rsyncd-wrapper
27 log_end_msg $?
28 ;;
29 stop)
30 log_daemon_msg "Stopping turku rsync daemon" "turku-agent-rsyncd"
31 start-stop-daemon --stop --quiet --oknodo --pidfile $PID_FILE --retry 5
32 log_end_msg $?
33 rm -f $PID_FILE
34 ;;
35
36 reload|force-reload)
37 log_daemon_msg "Reloading turku rsync daemon" "turku-agent-rsyncd"
38 log_end_msg 0
39 ;;
40
41 restart)
42 "$0" stop
43 "$0" start
44 ;;
45
46 status)
47 status_of_proc -p $PID_FILE rsync turku-agent-rsyncd
48 exit $? # notreached due to set -e
49 ;;
50 *)
51 echo "Usage: /etc/init.d/turku-agent-rsyncd {start|stop|reload|force-reload|restart|status}"
52 exit 1
53esac
54
55exit 0
056
=== added file 'turku-agent-rsyncd.service'
--- turku-agent-rsyncd.service 1970-01-01 00:00:00 +0000
+++ turku-agent-rsyncd.service 2022-06-22 14:48:04 +0000
@@ -0,0 +1,10 @@
1[Unit]
2Description=turku rsyncd daemon
3ConditionPathExists=/var/lib/turku-agent/rsyncd.conf
4
5[Service]
6ExecStart=/usr/bin/env turku-agent-rsyncd-wrapper
7Restart=always
8
9[Install]
10WantedBy=multi-user.target
011
=== added file 'turku-agent.cron'
--- turku-agent.cron 1970-01-01 00:00:00 +0000
+++ turku-agent.cron 2022-06-22 14:48:04 +0000
@@ -0,0 +1,3 @@
1PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
2*/5 * * * * root sh -c 'systemctl is-active basic.target 2>/dev/null >/dev/null || turku-agent-ping --wait=300 >/dev/null 2>/dev/null'
30 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'
04
=== added file 'turku-update-config'
--- turku-update-config 1970-01-01 00:00:00 +0000
+++ turku-update-config 2022-06-22 14:48:04 +0000
@@ -0,0 +1,20 @@
1#!/usr/bin/env python3
2
3# Turku backups - client agent
4# Copyright 2015 Canonical Ltd.
5#
6# This program is free software: you can redistribute it and/or modify it
7# under the terms of the GNU General Public License version 3, as published by
8# the Free Software Foundation.
9#
10# This program is distributed in the hope that it will be useful, but WITHOUT
11# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
12# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13# General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License along with
16# this program. If not, see <http://www.gnu.org/licenses/>.
17
18import sys
19from turku_agent import update_config
20sys.exit(update_config.main())
021
=== added file 'turku-update-config.service'
--- turku-update-config.service 1970-01-01 00:00:00 +0000
+++ turku-update-config.service 2022-06-22 14:48:04 +0000
@@ -0,0 +1,6 @@
1[Unit]
2Description=turku-update-config
3
4[Service]
5Type=oneshot
6ExecStart=/usr/bin/env turku-update-config
07
=== added file 'turku-update-config.timer'
--- turku-update-config.timer 1970-01-01 00:00:00 +0000
+++ turku-update-config.timer 2022-06-22 14:48:04 +0000
@@ -0,0 +1,10 @@
1[Unit]
2Description=turku-update-config
3
4[Timer]
5OnUnitActiveSec=12h
6RandomizedDelaySec=12h
7OnStartupSec=1h
8
9[Install]
10WantedBy=timers.target
011
=== added directory 'turku_agent'
=== added file 'turku_agent/__init__.py'
=== added file 'turku_agent/ping.py'
--- turku_agent/ping.py 1970-01-01 00:00:00 +0000
+++ turku_agent/ping.py 2022-06-22 14:48:04 +0000
@@ -0,0 +1,242 @@
1# Turku backups - client agent
2# Copyright 2015 Canonical Ltd.
3#
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU General Public License version 3, as published by
6# the Free Software Foundation.
7#
8# This program is distributed in the hope that it will be useful, but WITHOUT
9# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11# General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License along with
14# this program. If not, see <http://www.gnu.org/licenses/>.
15
16
17import json
18import os
19import random
20import shlex
21import subprocess
22import tempfile
23import time
24
25from .utils import load_config, acquire_lock, api_call
26
27
28def parse_args():
29 import argparse
30
31 parser = argparse.ArgumentParser(
32 formatter_class=argparse.ArgumentDefaultsHelpFormatter
33 )
34 parser.add_argument("--config-dir", "-c", type=str, default="/etc/turku-agent")
35 parser.add_argument("--wait", "-w", type=float)
36 parser.add_argument("--restore", action="store_true")
37 parser.add_argument("--restore-storage", type=str, default=None)
38 parser.add_argument(
39 "--gonogo-program",
40 type=str,
41 default=None,
42 help="Go/no-go program run each time to determine whether to ping",
43 )
44 return parser.parse_args()
45
46
47def call_ssh(config, storage, ssh_req):
48 # Write the server host public key
49 t = tempfile.NamedTemporaryFile(mode="w+", encoding="UTF-8")
50 for key in storage["ssh_ping_host_keys"]:
51 t.write("%s %s\n" % (storage["ssh_ping_host"], key))
52 t.flush()
53
54 # Call ssh
55 ssh_command = config["ssh_command"]
56 ssh_command += [
57 "-T",
58 "-o",
59 "BatchMode=yes",
60 "-o",
61 "UserKnownHostsFile=%s" % t.name,
62 "-o",
63 "StrictHostKeyChecking=yes",
64 "-o",
65 "CheckHostIP=no",
66 "-i",
67 config["ssh_private_key_file"],
68 "-R",
69 "%d:%s:%d"
70 % (
71 ssh_req["port"],
72 config["rsyncd_local_address"],
73 config["rsyncd_local_port"],
74 ),
75 "-p",
76 str(storage["ssh_ping_port"]),
77 "-l",
78 storage["ssh_ping_user"],
79 storage["ssh_ping_host"],
80 "turku-ping-remote",
81 ]
82 p = subprocess.Popen(ssh_command, stdin=subprocess.PIPE)
83
84 # Write the ssh request
85 p.stdin.write((json.dumps(ssh_req) + "\n.\n").encode("UTF-8"))
86 p.stdin.flush()
87
88 # Wait for the server to close the SSH connection
89 try:
90 p.wait()
91 except KeyboardInterrupt:
92 pass
93
94 # Cleanup
95 t.close()
96
97
98def main():
99 args = parse_args()
100
101 # Sleep a random amount of time if requested
102 if args.wait:
103 time.sleep(random.uniform(0, args.wait))
104
105 config = load_config(args.config_dir)
106
107 # Basic checks
108 for i in ("ssh_private_key_file", "machine_uuid", "machine_secret", "api_url"):
109 if i not in config:
110 return
111 if not os.path.isfile(config["ssh_private_key_file"]):
112 return
113
114 # If a go/no-go program is defined, run it and only go if it exits 0.
115 # Example: prevent backups during high-load for sensitive systems:
116 # ['check_load', '-c', '1,5,15']
117 gonogo_program = (
118 args.gonogo_program if args.gonogo_program else config["gonogo_program"]
119 )
120 if isinstance(gonogo_program, (list, tuple)):
121 # List, program name first, optional arguments after
122 gonogo_program_and_args = list(gonogo_program)
123 elif isinstance(gonogo_program, str):
124 # String, shlex split it
125 gonogo_program_and_args = shlex.split(gonogo_program)
126 else:
127 # None
128 gonogo_program_and_args = []
129 if gonogo_program_and_args:
130 try:
131 subprocess.check_call(gonogo_program_and_args)
132 except (subprocess.CalledProcessError, OSError):
133 return
134
135 lock = acquire_lock(os.path.join(config["lock_dir"], "turku-agent-ping.lock"))
136
137 restore_mode = args.restore
138
139 # Check with the API server
140 api_out = {}
141
142 machine_merge_map = (("machine_uuid", "uuid"), ("machine_secret", "secret"))
143 api_out["machine"] = {}
144 for a, b in machine_merge_map:
145 if a in config:
146 api_out["machine"][b] = config[a]
147
148 if restore_mode:
149 print("Entering restore mode.")
150 print()
151 api_reply = api_call(config["api_url"], "agent_ping_restore", api_out)
152
153 sources_by_storage = {}
154 for source_name in api_reply["machine"]["sources"]:
155 source = api_reply["machine"]["sources"][source_name]
156 if source_name not in config["sources"]:
157 continue
158 if "storage" not in source:
159 continue
160 if source["storage"]["name"] not in sources_by_storage:
161 sources_by_storage[source["storage"]["name"]] = {}
162 sources_by_storage[source["storage"]["name"]][source_name] = source
163
164 if len(sources_by_storage) == 0:
165 print("Cannot find any appropraite sources.")
166 return
167 print("This machine's sources are on the following storage units:")
168 for storage_name in sources_by_storage:
169 print(" %s" % storage_name)
170 for source_name in sources_by_storage[storage_name]:
171 print(" %s" % source_name)
172 print()
173 if len(sources_by_storage) == 1:
174 storage = list(list(sources_by_storage.values())[0].values())[0]["storage"]
175 elif args.restore_storage:
176 if args.restore_storage in sources_by_storage:
177 storage = sources_by_storage[args.restore_storage]["storage"]
178 else:
179 print('Cannot find appropriate storage "%s"' % args.restore_storage)
180 return
181 else:
182 print(
183 "Multiple storages found. Please use --restore-storage to specify one."
184 )
185 return
186
187 ssh_req = {
188 "verbose": True,
189 "action": "restore",
190 "port": random.randint(49152, 65535),
191 }
192 print("Storage unit: %s" % storage["name"])
193 if "restore_path" in config:
194 print("Local destination path: %s" % config["restore_path"])
195 print("Sample restore usage from storage unit:")
196 print(
197 " RSYNC_PASSWORD=%s rsync -avzP --numeric-ids ${P?}/ rsync://%s@127.0.0.1:%s/%s/"
198 % (
199 config["restore_password"],
200 config["restore_username"],
201 ssh_req["port"],
202 config["restore_module"],
203 )
204 )
205 print()
206 call_ssh(config, storage, ssh_req)
207 else:
208 api_reply = api_call(config["api_url"], "agent_ping_checkin", api_out)
209
210 if "scheduled_sources" not in api_reply:
211 return
212 sources_by_storage = {}
213 for source_name in api_reply["machine"]["scheduled_sources"]:
214 source = api_reply["machine"]["scheduled_sources"][source_name]
215 if source_name not in config["sources"]:
216 continue
217 if "storage" not in source:
218 continue
219 if source["storage"]["name"] not in sources_by_storage:
220 sources_by_storage[source["storage"]["name"]] = {}
221 sources_by_storage[source["storage"]["name"]][source_name] = source
222
223 for storage_name in sources_by_storage:
224 ssh_req = {
225 "verbose": True,
226 "action": "checkin",
227 "port": random.randint(49152, 65535),
228 "sources": {},
229 }
230 for source in sources_by_storage[storage_name]:
231 ssh_req["sources"][source] = {
232 "username": config["sources"][source]["username"],
233 "password": config["sources"][source]["password"],
234 }
235 call_ssh(
236 config,
237 list(sources_by_storage[storage_name].values())[0]["storage"],
238 ssh_req,
239 )
240
241 # Cleanup
242 lock.close()
0243
=== added file 'turku_agent/rsyncd_wrapper.py'
--- turku_agent/rsyncd_wrapper.py 1970-01-01 00:00:00 +0000
+++ turku_agent/rsyncd_wrapper.py 2022-06-22 14:48:04 +0000
@@ -0,0 +1,46 @@
1#!/usr/bin/env python3
2
3# Turku backups - client agent
4# Copyright 2015 Canonical Ltd.
5#
6# This program is free software: you can redistribute it and/or modify it
7# under the terms of the GNU General Public License version 3, as published by
8# the Free Software Foundation.
9#
10# This program is distributed in the hope that it will be useful, but WITHOUT
11# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
12# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13# General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License along with
16# this program. If not, see <http://www.gnu.org/licenses/>.
17
18import os
19
20from .utils import load_config
21
22
23def parse_args():
24 import argparse
25
26 parser = argparse.ArgumentParser(
27 formatter_class=argparse.ArgumentDefaultsHelpFormatter
28 )
29 parser.add_argument("--config-dir", "-c", type=str, default="/etc/turku-agent")
30 parser.add_argument("--detach", action="store_true")
31 return parser.parse_known_args()
32
33
34def main():
35 args, rest = parse_args()
36
37 config = load_config(args.config_dir)
38 rsyncd_command = config["rsyncd_command"]
39 if not args.detach:
40 rsyncd_command.append("--no-detach")
41 rsyncd_command.append("--daemon")
42 rsyncd_command.append(
43 "--config=%s" % os.path.join(config["var_dir"], "rsyncd.conf")
44 )
45 rsyncd_command += rest
46 os.execvp(rsyncd_command[0], rsyncd_command)
047
=== added file 'turku_agent/update_config.py'
--- turku_agent/update_config.py 1970-01-01 00:00:00 +0000
+++ turku_agent/update_config.py 2022-06-22 14:48:04 +0000
@@ -0,0 +1,181 @@
1# Turku backups - client agent
2# Copyright 2015 Canonical Ltd.
3#
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU General Public License version 3, as published by
6# the Free Software Foundation.
7#
8# This program is distributed in the hope that it will be useful, but WITHOUT
9# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11# General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License along with
14# this program. If not, see <http://www.gnu.org/licenses/>.
15
16import logging
17import os
18import random
19import subprocess
20import time
21
22from .utils import json_dumps_p, load_config, fill_config, acquire_lock, api_call
23
24
25class IncompleteConfigError(Exception):
26 pass
27
28
29def parse_args():
30 import argparse
31
32 parser = argparse.ArgumentParser(
33 formatter_class=argparse.ArgumentDefaultsHelpFormatter
34 )
35 parser.add_argument("--config-dir", "-c", type=str, default="/etc/turku-agent")
36 parser.add_argument("--wait", "-w", type=float)
37 parser.add_argument("--debug", action="store_true")
38 return parser.parse_args()
39
40
41def write_conf_files(config):
42 # Build rsyncd.conf
43 built_rsyncd_conf = (
44 "address = %s\n" % config["rsyncd_local_address"]
45 + "port = %d\n" % config["rsyncd_local_port"]
46 + "log file = /dev/stdout\n"
47 + "uid = root\n"
48 + "gid = root\n"
49 + "list = false\n\n"
50 )
51 rsyncd_secrets = []
52 rsyncd_secrets.append((config["restore_username"], config["restore_password"]))
53 built_rsyncd_conf += (
54 "[%s]\n"
55 + " path = %s\n"
56 + " auth users = %s\n"
57 + " secrets file = %s\n"
58 + " read only = false\n\n"
59 ) % (
60 config["restore_module"],
61 config["restore_path"],
62 config["restore_username"],
63 os.path.join(config["var_dir"], "rsyncd.secrets"),
64 )
65 for s in config["sources"]:
66 sd = config["sources"][s]
67 rsyncd_secrets.append((sd["username"], sd["password"]))
68 built_rsyncd_conf += (
69 "[%s]\n"
70 + " path = %s\n"
71 + " auth users = %s\n"
72 + " secrets file = %s\n"
73 + " read only = true\n\n"
74 ) % (
75 s,
76 sd["path"],
77 sd["username"],
78 os.path.join(config["var_dir"], "rsyncd.secrets"),
79 )
80 with open(os.path.join(config["var_dir"], "rsyncd.conf"), "w") as f:
81 f.write(built_rsyncd_conf)
82
83 # Build rsyncd.secrets
84 built_rsyncd_secrets = ""
85 for (username, password) in rsyncd_secrets:
86 built_rsyncd_secrets += username + ":" + password + "\n"
87 with open(os.path.join(config["var_dir"], "rsyncd.secrets"), "w") as f:
88 os.fchmod(f.fileno(), 0o600)
89 f.write(built_rsyncd_secrets)
90
91
92def init_is_upstart():
93 try:
94 return "upstart" in subprocess.check_output(
95 ["initctl", "version"], stderr=subprocess.DEVNULL, universal_newlines=True
96 )
97 except (FileNotFoundError, subprocess.CalledProcessError):
98 return False
99
100
101def start_services():
102 # Start rsyncd if it isn't already running.
103 # Note that we do *not* need to reload rsyncd when changing rsyncd.conf,
104 # as it rereads it on every client connection; but we may need to start
105 # it as it won't start if its configuration file doesn't exist.
106 if init_is_upstart():
107 # With Upstart, start will fail if the service is already running,
108 # so we need to check for that first.
109 try:
110 if "start/running" in subprocess.check_output(
111 ["status", "turku-agent-rsyncd"],
112 stderr=subprocess.STDOUT,
113 universal_newlines=True,
114 ):
115 return
116 except subprocess.CalledProcessError:
117 pass
118 subprocess.check_call(["service", "turku-agent-rsyncd", "start"])
119
120
121def send_config(config):
122 required_keys = ["api_url"]
123 if "api_auth" not in config:
124 required_keys += ["api_auth_name", "api_auth_secret"]
125 for k in required_keys:
126 if k not in config:
127 raise IncompleteConfigError('Required config "%s" not found.' % k)
128
129 api_out = {}
130 if ("api_auth_name" in config) and ("api_auth_secret" in config):
131 # name/secret style
132 api_out["auth"] = {
133 "name": config["api_auth_name"],
134 "secret": config["api_auth_secret"],
135 }
136 else:
137 # nameless secret style
138 api_out["auth"] = config["api_auth"]
139
140 # Merge the following options into the machine section
141 machine_merge_map = (
142 ("machine_uuid", "uuid"),
143 ("machine_secret", "secret"),
144 ("environment_name", "environment_name"),
145 ("service_name", "service_name"),
146 ("unit_name", "unit_name"),
147 ("ssh_public_key", "ssh_public_key"),
148 ("published", "published"),
149 )
150 api_out["machine"] = {}
151 for a, b in machine_merge_map:
152 if a in config:
153 api_out["machine"][b] = config[a]
154
155 api_out["machine"]["sources"] = config["sources"]
156
157 api_call(config["api_url"], "update_config", api_out)
158
159
160def main():
161 args = parse_args()
162 # Sleep a random amount of time if requested
163 if args.wait:
164 time.sleep(random.uniform(0, args.wait))
165
166 config = load_config(args.config_dir)
167 lock = acquire_lock(os.path.join(config["lock_dir"], "turku-update-config.lock"))
168 fill_config(config)
169 if args.debug:
170 print(json_dumps_p(config))
171 write_conf_files(config)
172 try:
173 send_config(config)
174 except Exception as e:
175 if args.debug:
176 raise
177 logging.exception(e)
178 return 1
179 start_services()
180
181 lock.close()
0182
=== added file 'turku_agent/utils.py'
--- turku_agent/utils.py 1970-01-01 00:00:00 +0000
+++ turku_agent/utils.py 2022-06-22 14:48:04 +0000
@@ -0,0 +1,350 @@
1# Turku backups - client agent
2# Copyright 2015 Canonical Ltd.
3#
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU General Public License version 3, as published by
6# the Free Software Foundation.
7#
8# This program is distributed in the hope that it will be useful, but WITHOUT
9# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11# General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License along with
14# this program. If not, see <http://www.gnu.org/licenses/>.
15
16import copy
17import http.client
18import json
19import os
20import platform
21import random
22import string
23import subprocess
24import urllib.parse
25import uuid
26
27
28class RuntimeLock:
29 name = None
30 file = None
31
32 def __init__(self, name):
33 import fcntl
34
35 file = open(name, "w")
36 try:
37 fcntl.lockf(file, fcntl.LOCK_EX | fcntl.LOCK_NB)
38 except IOError as e:
39 import errno
40
41 if e.errno in (errno.EACCES, errno.EAGAIN):
42 raise
43 file.write("%10s\n" % os.getpid())
44 file.flush()
45 file.seek(0)
46 self.name = name
47 self.file = file
48
49 def close(self):
50 if self.file:
51 self.file.close()
52 self.file = None
53 os.unlink(self.name)
54
55 def __del__(self):
56 self.close()
57
58 def __enter__(self):
59 self.file.__enter__()
60 return self
61
62 def __exit__(self, exc, value, tb):
63 result = self.file.__exit__(exc, value, tb)
64 self.close()
65 return result
66
67
68def acquire_lock(name):
69 return RuntimeLock(name)
70
71
72def json_dump_p(obj, f):
73 """Calls json.dump with standard (pretty) formatting"""
74 return json.dump(obj, f, sort_keys=True, indent=4, separators=(",", ": "))
75
76
77def json_dumps_p(obj):
78 """Calls json.dumps with standard (pretty) formatting"""
79 return json.dumps(obj, sort_keys=True, indent=4, separators=(",", ": "))
80
81
82def json_load_file(file):
83 with open(file) as f:
84 try:
85 return json.load(f)
86 except ValueError as e:
87 e.args += (file,)
88 raise
89
90
91def dict_merge(s, m):
92 """Recursively merge one dict into another."""
93 if not isinstance(m, dict):
94 return m
95 out = copy.deepcopy(s)
96 for k, v in list(m.items()):
97 if k in out and isinstance(out[k], dict):
98 out[k] = dict_merge(out[k], v)
99 else:
100 out[k] = copy.deepcopy(v)
101 return out
102
103
104def load_config(config_dir):
105 config = {}
106 config["config_dir"] = config_dir
107
108 config_d = os.path.join(config["config_dir"], "config.d")
109 sources_d = os.path.join(config["config_dir"], "sources.d")
110
111 # Merge in config.d/*.json to the root level
112 config_files = []
113 if os.path.isdir(config_d):
114 config_files = [
115 os.path.join(config_d, fn)
116 for fn in os.listdir(config_d)
117 if fn.endswith(".json")
118 and os.path.isfile(os.path.join(config_d, fn))
119 and os.access(os.path.join(config_d, fn), os.R_OK)
120 ]
121 config_files.sort()
122 for file in config_files:
123 config = dict_merge(config, json_load_file(file))
124
125 if "var_dir" not in config:
126 config["var_dir"] = "/var/lib/turku-agent"
127
128 var_config_d = os.path.join(config["var_dir"], "config.d")
129
130 # Load /var config.d files
131 var_config = {}
132 var_config_files = []
133 if os.path.isdir(var_config_d):
134 var_config_files = [
135 os.path.join(var_config_d, fn)
136 for fn in os.listdir(var_config_d)
137 if fn.endswith(".json")
138 and os.path.isfile(os.path.join(var_config_d, fn))
139 and os.access(os.path.join(var_config_d, fn), os.R_OK)
140 ]
141 var_config_files.sort()
142 for file in var_config_files:
143 var_config = dict_merge(var_config, json_load_file(file))
144 # /etc gets priority over /var
145 var_config = dict_merge(var_config, config)
146 config = var_config
147
148 if "lock_dir" not in config:
149 config["lock_dir"] = "/var/lock"
150
151 if "rsyncd_command" not in config:
152 config["rsyncd_command"] = ["rsync"]
153
154 if "rsyncd_local_address" not in config:
155 config["rsyncd_local_address"] = "127.0.0.1"
156
157 if "rsyncd_local_port" not in config:
158 config["rsyncd_local_port"] = 27873
159
160 if "ssh_command" not in config:
161 config["ssh_command"] = ["ssh"]
162
163 # If a go/no-go program is defined, run it and only go if it exits 0.
164 # Type: String (program with no args) or list (program first, optional arguments after)
165 if "gonogo_program" not in config:
166 config["gonogo_program"] = None
167
168 var_sources_d = os.path.join(config["var_dir"], "sources.d")
169
170 # Validate the unit name
171 if "unit_name" not in config:
172 config["unit_name"] = platform.node()
173 # If this isn't in the on-disk config, don't write it; just
174 # generate it every time
175
176 # Pull the SSH public key
177 if os.path.isfile(os.path.join(config["var_dir"], "ssh_key.pub")):
178 with open(os.path.join(config["var_dir"], "ssh_key.pub")) as f:
179 config["ssh_public_key"] = f.read().rstrip()
180 config["ssh_public_key_file"] = os.path.join(config["var_dir"], "ssh_key.pub")
181 config["ssh_private_key_file"] = os.path.join(config["var_dir"], "ssh_key")
182
183 sources_config = {}
184 # Merge in sources.d/*.json to the sources dict
185 sources_files = []
186 if os.path.isdir(sources_d):
187 sources_files = [
188 os.path.join(sources_d, fn)
189 for fn in os.listdir(sources_d)
190 if fn.endswith(".json")
191 and os.path.isfile(os.path.join(sources_d, fn))
192 and os.access(os.path.join(sources_d, fn), os.R_OK)
193 ]
194 sources_files.sort()
195 var_sources_files = []
196 if os.path.isdir(var_sources_d):
197 var_sources_files = [
198 os.path.join(var_sources_d, fn)
199 for fn in os.listdir(var_sources_d)
200 if fn.endswith(".json")
201 and os.path.isfile(os.path.join(var_sources_d, fn))
202 and os.access(os.path.join(var_sources_d, fn), os.R_OK)
203 ]
204 var_sources_files.sort()
205 sources_files += var_sources_files
206 for file in sources_files:
207 sources_config = dict_merge(sources_config, json_load_file(file))
208
209 # Check for required sources options
210 for s in list(sources_config.keys()):
211 if "path" not in sources_config[s]:
212 del sources_config[s]
213
214 config["sources"] = sources_config
215
216 return config
217
218
219def fill_config(config):
220 config_d = os.path.join(config["config_dir"], "config.d")
221 sources_d = os.path.join(config["config_dir"], "sources.d")
222 var_config_d = os.path.join(config["var_dir"], "config.d")
223 var_sources_d = os.path.join(config["var_dir"], "sources.d")
224
225 # Create required directories
226 for d in (config_d, sources_d, var_config_d, var_sources_d):
227 if not os.path.isdir(d):
228 os.makedirs(d)
229
230 # Validate the machine UUID/secret
231 write_uuid_data = False
232 if "machine_uuid" not in config:
233 config["machine_uuid"] = str(uuid.uuid4())
234 write_uuid_data = True
235 if "machine_secret" not in config:
236 config["machine_secret"] = "".join(
237 random.choice(string.ascii_letters + string.digits) for i in range(30)
238 )
239 write_uuid_data = True
240 # Write out the machine UUID/secret if needed
241 if write_uuid_data:
242 with open(os.path.join(var_config_d, "10-machine_uuid.json"), "w") as f:
243 os.fchmod(f.fileno(), 0o600)
244 json_dump_p(
245 {
246 "machine_uuid": config["machine_uuid"],
247 "machine_secret": config["machine_secret"],
248 },
249 f,
250 )
251
252 # Restoration configuration
253 write_restore_data = False
254 if "restore_path" not in config:
255 config["restore_path"] = "/var/backups/turku-agent/restore"
256 write_restore_data = True
257 if "restore_module" not in config:
258 config["restore_module"] = "turku-restore"
259 write_restore_data = True
260 if "restore_username" not in config:
261 config["restore_username"] = str(uuid.uuid4())
262 write_restore_data = True
263 if "restore_password" not in config:
264 config["restore_password"] = "".join(
265 random.choice(string.ascii_letters + string.digits) for i in range(30)
266 )
267 write_restore_data = True
268 if write_restore_data:
269 with open(os.path.join(var_config_d, "10-restore.json"), "w") as f:
270 os.fchmod(f.fileno(), 0o600)
271 restore_out = {
272 "restore_path": config["restore_path"],
273 "restore_module": config["restore_module"],
274 "restore_username": config["restore_username"],
275 "restore_password": config["restore_password"],
276 }
277 json_dump_p(restore_out, f)
278 if not os.path.isdir(config["restore_path"]):
279 os.makedirs(config["restore_path"])
280
281 # Generate the SSH keypair if it doesn't exist
282 if "ssh_private_key_file" not in config:
283 subprocess.check_call(
284 [
285 "ssh-keygen",
286 "-t",
287 "rsa",
288 "-N",
289 "",
290 "-C",
291 "turku",
292 "-f",
293 os.path.join(config["var_dir"], "ssh_key"),
294 ]
295 )
296 with open(os.path.join(config["var_dir"], "ssh_key.pub")) as f:
297 config["ssh_public_key"] = f.read().rstrip()
298 config["ssh_public_key_file"] = os.path.join(config["var_dir"], "ssh_key.pub")
299 config["ssh_private_key_file"] = os.path.join(config["var_dir"], "ssh_key")
300
301 for s in config["sources"]:
302 # Check for missing usernames/passwords
303 if not (
304 "username" in config["sources"][s] or "password" in config["sources"][s]
305 ):
306 if "username" not in config["sources"][s]:
307 config["sources"][s]["username"] = str(uuid.uuid4())
308 if "password" not in config["sources"][s]:
309 config["sources"][s]["password"] = "".join(
310 random.choice(string.ascii_letters + string.digits)
311 for i in range(30)
312 )
313 with open(os.path.join(var_sources_d, "10-" + s + ".json"), "w") as f:
314 os.fchmod(f.fileno(), 0o600)
315 json_dump_p(
316 {
317 s: {
318 "username": config["sources"][s]["username"],
319 "password": config["sources"][s]["password"],
320 }
321 },
322 f,
323 )
324
325
326def api_call(api_url, cmd, post_data, timeout=5):
327 url = urllib.parse.urlparse(api_url)
328 if url.scheme == "https":
329 h = http.client.HTTPSConnection(url.netloc, timeout=timeout)
330 else:
331 h = http.client.HTTPConnection(url.netloc, timeout=timeout)
332 out = json.dumps(post_data)
333 h.putrequest("POST", "%s/%s" % (url.path, cmd))
334 h.putheader("Content-Type", "application/json")
335 h.putheader("Content-Length", len(out))
336 h.putheader("Accept", "application/json")
337 h.endheaders()
338 h.send(out.encode("UTF-8"))
339
340 res = h.getresponse()
341 if not res.status == http.client.OK:
342 raise Exception(
343 "Received error %d (%s) from API server" % (res.status, res.reason)
344 )
345 if not res.getheader("content-type") == "application/json":
346 raise Exception("Received invalid reply from API server")
347 try:
348 return json.loads(res.read().decode("UTF-8"))
349 except ValueError:
350 raise Exception("Received invalid reply from API server")

Subscribers

People subscribed via source and target branches

to all changes: