Skip to content

Commit f836da5

Browse files
committed
Restrict container names (closes #15), format sourcecode
1 parent cec9869 commit f836da5

File tree

7 files changed

+227
-179
lines changed

7 files changed

+227
-179
lines changed

main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
while True:
2828
# Scheduling next run
2929
next_run = schedule.get_next()
30-
if next_run != None:
30+
if next_run is not None:
3131
logging.info(
3232
f"Scheduled next run at {next_run.strftime('%Y-%m-%d %H:%M:%S')}.."
3333
)
@@ -37,4 +37,4 @@
3737
logging.info("Exiting backup service")
3838
sys.exit()
3939

40-
backup.run()
40+
backup.run()

src/backup.py

Lines changed: 96 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
import datetime
99
import pyAesCrypt
1010
import humanize
11+
import re
1112

1213

1314
class Backup:
14-
DUMP_DIR = '/dump'
15+
DUMP_DIR = "/dump"
16+
DUMP_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_.-]*$")
1517

1618
def __init__(self, config, global_labels, docker, healthcheck):
1719
self._config = config
@@ -24,20 +26,28 @@ def run(self):
2426
self._healthcheck.start("Starting backup cycle.")
2527

2628
# Find available database containers
27-
containers = self._docker.get_targets(f"{settings.LABEL_PREFIX}enable=true")
29+
containers = self._docker.get_targets(
30+
f"{settings.LABEL_PREFIX}enable=true")
2831

2932
container_count = len(containers)
3033
successful_count = 0
3134

3235
if container_count:
33-
logging.info(f"Starting backup cycle with {len(containers)} container(s)..")
36+
logging.info(
37+
f"Starting backup cycle with {len(containers)} container(s)..")
3438

3539
self._docker.create_backup_network()
3640

3741
for i, container in enumerate(containers):
3842
database = Database(container, self._global_labels)
39-
dump_name_part = database.dump_name if database.dump_name != None else container.name
40-
dump_timestamp_part = (datetime.datetime.now()).strftime("_%Y-%m-%d_%H-%M-%S") if database.dump_timestamp else ''
43+
dump_name_part = (
44+
database.dump_name if database.dump_name is not None else container.name
45+
)
46+
dump_timestamp_part = (
47+
(datetime.datetime.now()).strftime("_%Y-%m-%d_%H-%M-%S")
48+
if database.dump_timestamp
49+
else ""
50+
)
4151
dump_file = f"{self.DUMP_DIR}/{dump_name_part}{dump_timestamp_part}.sql"
4252
failed = False
4353

@@ -51,86 +61,95 @@ def run(self):
5161
)
5262
)
5363

54-
if database.type == DatabaseType.unknown:
64+
if not self.DUMP_NAME_PATTERN.match(dump_name_part):
5565
logging.error(
56-
"> FAILED: Cannot read database type. Please specify via label."
66+
f"> FAILED: Invalid dump name. Name must match '{self.DUMP_NAME_PATTERN.pattern}'."
5767
)
5868
failed = True
5969

60-
logging.debug(
61-
"> Login {}@host:{} using Password: {}".format(
62-
database.username,
63-
database.port,
64-
"YES" if len(database.password) > 0 else "NO",
65-
)
66-
)
67-
68-
# Create dump
69-
self._docker.connect_target(container)
70-
71-
try:
72-
env = os.environ.copy()
73-
74-
if (
75-
database.type == DatabaseType.mysql
76-
or database.type == DatabaseType.mariadb
77-
):
78-
subprocess.run(
79-
(
80-
f"mysqldump"
81-
f' --host="{self._config.docker_target_name}"'
82-
f' --user="{database.username}"'
83-
f' --password="{database.password}"'
84-
f" --all-databases"
85-
f" --ignore-database=mysql"
86-
f" --ignore-database=information_schema"
87-
f" --ignore-database=performance_schema"
88-
f' > "{dump_file}"'
89-
),
90-
shell=True,
91-
text=True,
92-
capture_output=True,
93-
env=env,
94-
).check_returncode()
95-
elif database.type == DatabaseType.postgres:
96-
env["PGPASSWORD"] = database.password
97-
subprocess.run(
98-
(
99-
f"pg_dumpall"
100-
f' --host="{self._config.docker_target_name}"'
101-
f' --username="{database.username}"'
102-
f' > "{dump_file}"'
103-
),
104-
shell=True,
105-
text=True,
106-
capture_output=True,
107-
env=env,
108-
).check_returncode()
109-
except subprocess.CalledProcessError as e:
110-
error_text = f"\n{e.stderr.strip()}".replace("\n", "\n> ").strip()
70+
if not failed and database.type == DatabaseType.unknown:
11171
logging.error(
112-
f"> FAILED. Error while crating dump. Return Code: {e.returncode}; Error Output:"
72+
"> FAILED: Cannot read database type. Please specify via label."
11373
)
114-
logging.error(f"{error_text}")
11574
failed = True
11675

117-
self._docker.disconnect_target(container)
76+
if not failed:
77+
logging.debug(
78+
"> Login {}@host:{} using Password: {}".format(
79+
database.username,
80+
database.port,
81+
"YES" if len(database.password) > 0 else "NO",
82+
)
83+
)
84+
85+
# Create dump
86+
self._docker.connect_target(container)
87+
88+
try:
89+
env = os.environ.copy()
90+
91+
if (
92+
database.type == DatabaseType.mysql
93+
or database.type == DatabaseType.mariadb
94+
):
95+
subprocess.run(
96+
(
97+
f"mysqldump"
98+
f' --host="{self._config.docker_target_name}"'
99+
f' --user="{database.username}"'
100+
f' --password="{database.password}"'
101+
f" --all-databases"
102+
f" --ignore-database=mysql"
103+
f" --ignore-database=information_schema"
104+
f" --ignore-database=performance_schema"
105+
f' > "{dump_file}"'
106+
),
107+
shell=True,
108+
text=True,
109+
capture_output=True,
110+
env=env,
111+
).check_returncode()
112+
elif database.type == DatabaseType.postgres:
113+
env["PGPASSWORD"] = database.password
114+
subprocess.run(
115+
(
116+
f"pg_dumpall"
117+
f' --host="{self._config.docker_target_name}"'
118+
f' --username="{database.username}"'
119+
f' > "{dump_file}"'
120+
),
121+
shell=True,
122+
text=True,
123+
capture_output=True,
124+
env=env,
125+
).check_returncode()
126+
except subprocess.CalledProcessError as e:
127+
error_text = f"\n{e.stderr.strip()}".replace(
128+
"\n", "\n> "
129+
).strip()
130+
logging.error(
131+
f"> FAILED. Error while crating dump. Return Code: {e.returncode}; Error Output:"
132+
)
133+
logging.error(f"{error_text}")
134+
failed = True
135+
136+
self._docker.disconnect_target(container)
118137

119138
if not failed and (not os.path.exists(dump_file)):
120139
logging.error(
121-
f"> FAILED: Dump cannot be created due to an unknown error!"
140+
"> FAILED: Dump cannot be created due to an unknown error!"
122141
)
123142
failed = True
124143

125144
dump_size = os.path.getsize(dump_file)
126145
if not failed and dump_size == 0:
127-
logging.error(f"> FAILED: Dump file is empty!")
146+
logging.error("> FAILED: Dump file is empty!")
128147
failed = True
129148

130149
# Compress pump
131150
if not failed and database.compress:
132151
logging.debug(
133-
f"> Compressing dump (level: {database.compression_level})"
152+
"> Compressing dump (level: {database.compression_level})"
134153
)
135154
compressed_dump_file = f"{dump_file}.gz"
136155

@@ -143,19 +162,21 @@ def run(self):
143162
shell=True,
144163
)
145164
except Exception as e:
146-
logging.error(f"> FAILED: Error while compressing: {e}")
165+
logging.error(
166+
f"> FAILED: Error while compressing: {e}")
147167
failed = True
148168

149169
processed_dump_size = os.path.getsize(compressed_dump_file)
150170
dump_file = compressed_dump_file
151171

152172
# Encrypt dump
153173
if not failed and database.encrypt and dump_size > 0:
154-
logging.debug(f"> Encrypting dump")
174+
logging.debug("> Encrypting dump")
155175
encrypted_dump_file = f"{dump_file}.aes"
156176

157-
if database.encryption_key == None:
158-
logging.error(f"> FAILED: No encryption key specified!")
177+
if database.encryption_key is None:
178+
logging.error(
179+
"> FAILED: No encryption key specified!")
159180
failed = True
160181
else:
161182
try:
@@ -167,10 +188,12 @@ def run(self):
167188
)
168189
os.remove(dump_file)
169190
except Exception as e:
170-
logging.error(f"> FAILED: Error while encrypting: {e}")
191+
logging.error(
192+
f"> FAILED: Error while encrypting: {e}")
171193
failed = True
172194

173-
processed_dump_size = os.path.getsize(encrypted_dump_file)
195+
processed_dump_size = os.path.getsize(
196+
encrypted_dump_file)
174197
dump_file = encrypted_dump_file
175198

176199
if not failed:
@@ -196,12 +219,12 @@ def run(self):
196219
# Cleanup
197220
files = sorted(glob.glob(f"{self.DUMP_DIR}/{dump_name_part}_*.*"))
198221
if len(files) > 1:
199-
#todo: retention policy for keeping multiple dumps
222+
# todo: retention policy for keeping multiple dumps
200223
deleted_files = 0
201224
for i in range(len(files) - 1):
202225
deleted_files += 1
203226
os.remove(files[i])
204-
227+
205228
if deleted_files > 0:
206229
logging.info(f"> Deleted {deleted_files} old dump files")
207230

@@ -217,4 +240,4 @@ def run(self):
217240
if full_success:
218241
self._healthcheck.success(message)
219242
else:
220-
self._healthcheck.fail(message)
243+
self._healthcheck.fail(message)

0 commit comments

Comments
 (0)