88import datetime
99import pyAesCrypt
1010import humanize
11+ import re
1112
1213
1314class 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