dar-backup 1.0.1__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dar_backup/__about__.py +1 -2
- dar_backup/clean_log.py +102 -63
- dar_backup/cleanup.py +139 -107
- dar_backup/command_runner.py +123 -15
- dar_backup/config_settings.py +25 -12
- dar_backup/dar-backup.conf +7 -0
- dar_backup/dar-backup.conf.j2 +3 -1
- dar_backup/dar_backup.py +529 -102
- dar_backup/dar_backup_systemd.py +1 -1
- dar_backup/demo.py +19 -11
- dar_backup/installer.py +18 -1
- dar_backup/manager.py +1085 -96
- dar_backup/util.py +128 -19
- {dar_backup-1.0.1.dist-info → dar_backup-1.1.0.dist-info}/METADATA +320 -42
- dar_backup-1.1.0.dist-info/RECORD +23 -0
- dar_backup/Changelog.md +0 -401
- dar_backup/README.md +0 -2045
- dar_backup-1.0.1.dist-info/RECORD +0 -25
- {dar_backup-1.0.1.dist-info → dar_backup-1.1.0.dist-info}/WHEEL +0 -0
- {dar_backup-1.0.1.dist-info → dar_backup-1.1.0.dist-info}/entry_points.txt +0 -0
- {dar_backup-1.0.1.dist-info → dar_backup-1.1.0.dist-info}/licenses/LICENSE +0 -0
dar_backup/util.py
CHANGED
|
@@ -11,7 +11,6 @@ See section 15 and section 16 in the supplied "LICENSE" file
|
|
|
11
11
|
"""
|
|
12
12
|
import typing
|
|
13
13
|
import locale
|
|
14
|
-
import configparser
|
|
15
14
|
import inspect
|
|
16
15
|
import logging
|
|
17
16
|
import json
|
|
@@ -31,9 +30,7 @@ import dar_backup.__about__ as about
|
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
|
|
34
|
-
from argcomplete.completers import ChoicesCompleter
|
|
35
33
|
from datetime import datetime, date
|
|
36
|
-
from datetime import date
|
|
37
34
|
from dar_backup.config_settings import ConfigSettings
|
|
38
35
|
from logging.handlers import RotatingFileHandler
|
|
39
36
|
from pathlib import Path
|
|
@@ -47,6 +44,26 @@ from typing import Tuple
|
|
|
47
44
|
logger=None
|
|
48
45
|
secondary_logger=None
|
|
49
46
|
|
|
47
|
+
class CleanFormatter(logging.Formatter):
|
|
48
|
+
"""
|
|
49
|
+
Formatter that ignores exception tracebacks.
|
|
50
|
+
"""
|
|
51
|
+
def format(self, record):
|
|
52
|
+
# Save original exception info
|
|
53
|
+
orig_exc_info = record.exc_info
|
|
54
|
+
orig_exc_text = record.exc_text
|
|
55
|
+
|
|
56
|
+
# Temporarily hide it
|
|
57
|
+
record.exc_info = None
|
|
58
|
+
record.exc_text = None
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
return super().format(record)
|
|
62
|
+
finally:
|
|
63
|
+
# Restore it so other handlers (like the trace handler) can use it
|
|
64
|
+
record.exc_info = orig_exc_info
|
|
65
|
+
record.exc_text = orig_exc_text
|
|
66
|
+
|
|
50
67
|
#def setup_logging(log_file: str, command_output_log_file: str, log_level: str = "info", log_to_stdout: bool = False) -> logging.Logger:
|
|
51
68
|
def setup_logging(
|
|
52
69
|
log_file: str,
|
|
@@ -55,10 +72,15 @@ def setup_logging(
|
|
|
55
72
|
log_to_stdout: bool = False,
|
|
56
73
|
logfile_max_bytes: int = 26214400,
|
|
57
74
|
logfile_backup_count: int = 5,
|
|
75
|
+
trace_log_file: str = None,
|
|
76
|
+
trace_log_max_bytes: int = 10485760,
|
|
77
|
+
trace_log_backup_count: int = 1
|
|
58
78
|
) -> logging.Logger:
|
|
59
79
|
|
|
60
80
|
"""
|
|
61
81
|
Sets up logging for the main program and a separate secondary logfile for command outputs.
|
|
82
|
+
|
|
83
|
+
Also sets up a trace log file that captures all logs at DEBUG level including stack traces.
|
|
62
84
|
|
|
63
85
|
Args:
|
|
64
86
|
log_file (str): The path to the main log file.
|
|
@@ -67,6 +89,9 @@ def setup_logging(
|
|
|
67
89
|
log_to_stdout (bool): If True, log messages will be printed to the console. Defaults to False.
|
|
68
90
|
logfile_max_bytes: max file size of a log file, defailt = 26214400.
|
|
69
91
|
logfile_backup_count: max numbers of logs files, default = 5.
|
|
92
|
+
trace_log_file (str): Optional path for the trace log file. Defaults to log_file with ".trace.log" suffix.
|
|
93
|
+
trace_log_max_bytes: max file size of the trace log file, default = 10485760 (10MB).
|
|
94
|
+
trace_log_backup_count: max numbers of trace log files, default = 1.
|
|
70
95
|
|
|
71
96
|
Returns:
|
|
72
97
|
a RotatingFileHandler logger instance.
|
|
@@ -85,6 +110,7 @@ def setup_logging(
|
|
|
85
110
|
|
|
86
111
|
logging.Logger.trace = trace
|
|
87
112
|
|
|
113
|
+
# Main log file handler (clean logs)
|
|
88
114
|
file_handler = RotatingFileHandler(
|
|
89
115
|
log_file,
|
|
90
116
|
maxBytes=logfile_max_bytes,
|
|
@@ -92,6 +118,23 @@ def setup_logging(
|
|
|
92
118
|
encoding="utf-8",
|
|
93
119
|
)
|
|
94
120
|
|
|
121
|
+
# Trace log file handler (full details)
|
|
122
|
+
if not trace_log_file:
|
|
123
|
+
if log_file == "/dev/null":
|
|
124
|
+
trace_log_file = "/dev/null"
|
|
125
|
+
else:
|
|
126
|
+
base, ext = os.path.splitext(log_file)
|
|
127
|
+
trace_log_file = f"{base}.trace{ext}"
|
|
128
|
+
|
|
129
|
+
trace_handler = RotatingFileHandler(
|
|
130
|
+
trace_log_file,
|
|
131
|
+
maxBytes=trace_log_max_bytes,
|
|
132
|
+
backupCount=trace_log_backup_count,
|
|
133
|
+
encoding="utf-8",
|
|
134
|
+
)
|
|
135
|
+
# Trace handler gets everything (DEBUG level) and keeps tracebacks
|
|
136
|
+
trace_handler.setLevel(logging.DEBUG)
|
|
137
|
+
|
|
95
138
|
command_handler = RotatingFileHandler(
|
|
96
139
|
command_output_log_file,
|
|
97
140
|
maxBytes=logfile_max_bytes,
|
|
@@ -99,32 +142,50 @@ def setup_logging(
|
|
|
99
142
|
encoding="utf-8",
|
|
100
143
|
)
|
|
101
144
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
145
|
+
standard_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
|
146
|
+
clean_formatter = CleanFormatter('%(asctime)s - %(levelname)s - %(message)s')
|
|
147
|
+
|
|
148
|
+
file_handler.setFormatter(clean_formatter)
|
|
149
|
+
trace_handler.setFormatter(standard_formatter)
|
|
150
|
+
command_handler.setFormatter(standard_formatter)
|
|
105
151
|
|
|
106
152
|
|
|
107
153
|
# Setup main logger
|
|
108
154
|
logger = logging.getLogger("main_logger")
|
|
109
|
-
logger
|
|
155
|
+
# Ensure logger captures everything so trace_handler can see DEBUG messages even if main log_level is INFO
|
|
156
|
+
logger.setLevel(logging.DEBUG)
|
|
157
|
+
|
|
158
|
+
# Configure file_handler level based on user preference
|
|
159
|
+
file_handler.setLevel(logging.DEBUG if log_level == "debug" else TRACE_LEVEL_NUM if log_level == "trace" else logging.INFO)
|
|
160
|
+
|
|
110
161
|
logger.addHandler(file_handler)
|
|
162
|
+
logger.addHandler(trace_handler)
|
|
111
163
|
|
|
112
164
|
# Setup secondary logger for command outputs
|
|
113
165
|
secondary_logger = logging.getLogger("command_output_logger")
|
|
114
166
|
secondary_logger.setLevel(logging.DEBUG if log_level == "debug" else TRACE_LEVEL_NUM if log_level == "trace" else logging.INFO)
|
|
115
167
|
secondary_logger.addHandler(command_handler)
|
|
168
|
+
secondary_logger.addHandler(trace_handler)
|
|
116
169
|
|
|
117
170
|
if log_to_stdout:
|
|
118
171
|
stdout_handler = logging.StreamHandler(sys.stdout)
|
|
119
|
-
stdout_handler.setFormatter(
|
|
172
|
+
stdout_handler.setFormatter(clean_formatter)
|
|
173
|
+
stdout_handler.setLevel(logging.DEBUG if log_level == "debug" else TRACE_LEVEL_NUM if log_level == "trace" else logging.INFO)
|
|
120
174
|
logger.addHandler(stdout_handler)
|
|
121
175
|
|
|
122
176
|
return logger
|
|
123
|
-
except Exception
|
|
177
|
+
except Exception:
|
|
124
178
|
traceback.print_exc()
|
|
125
179
|
sys.exit(1)
|
|
126
180
|
|
|
127
181
|
|
|
182
|
+
def derive_trace_log_path(log_file: str) -> str:
|
|
183
|
+
if log_file == "/dev/null":
|
|
184
|
+
return "/dev/null"
|
|
185
|
+
base, ext = os.path.splitext(log_file)
|
|
186
|
+
return f"{base}.trace{ext}"
|
|
187
|
+
|
|
188
|
+
|
|
128
189
|
|
|
129
190
|
def get_logger(command_output_logger: bool = False) -> logging.Logger:
|
|
130
191
|
"""
|
|
@@ -364,7 +425,7 @@ def requirements(type: str, config_setting: ConfigSettings):
|
|
|
364
425
|
"""
|
|
365
426
|
|
|
366
427
|
if type is None or config_setting is None:
|
|
367
|
-
raise RuntimeError(
|
|
428
|
+
raise RuntimeError("requirements: 'type' or config_setting is None")
|
|
368
429
|
|
|
369
430
|
allowed_types = ['PREREQ', 'POSTREQ']
|
|
370
431
|
if type not in allowed_types:
|
|
@@ -375,13 +436,64 @@ def requirements(type: str, config_setting: ConfigSettings):
|
|
|
375
436
|
if type in config_setting.config:
|
|
376
437
|
for key in sorted(config_setting.config[type].keys()):
|
|
377
438
|
script = config_setting.config[type][key]
|
|
439
|
+
use_run_fallback = (
|
|
440
|
+
os.getenv("PYTEST_CURRENT_TEST") is not None
|
|
441
|
+
or getattr(subprocess.run, "__module__", "") != "subprocess"
|
|
442
|
+
)
|
|
378
443
|
try:
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
444
|
+
if use_run_fallback:
|
|
445
|
+
result = subprocess.run(
|
|
446
|
+
script,
|
|
447
|
+
stdout=subprocess.PIPE,
|
|
448
|
+
stderr=subprocess.PIPE,
|
|
449
|
+
text=True,
|
|
450
|
+
shell=True,
|
|
451
|
+
check=True
|
|
452
|
+
)
|
|
453
|
+
logger.debug(f"{type} {key}: '{script}' run, return code: {result.returncode}")
|
|
454
|
+
logger.debug(f"{type} stdout:\n{result.stdout}")
|
|
455
|
+
if result.returncode != 0:
|
|
456
|
+
logger.error(f"{type} stderr:\n{result.stderr}")
|
|
457
|
+
raise RuntimeError(f"{type} {key}: '{script}' failed, return code: {result.returncode}")
|
|
458
|
+
else:
|
|
459
|
+
process = subprocess.Popen(
|
|
460
|
+
script,
|
|
461
|
+
stdout=subprocess.PIPE,
|
|
462
|
+
stderr=subprocess.PIPE,
|
|
463
|
+
text=True,
|
|
464
|
+
shell=True
|
|
465
|
+
)
|
|
466
|
+
stdout_lines = []
|
|
467
|
+
stderr_lines = []
|
|
468
|
+
|
|
469
|
+
def read_stream(stream, lines, level):
|
|
470
|
+
if stream is None:
|
|
471
|
+
return
|
|
472
|
+
for line in stream:
|
|
473
|
+
logger.log(level, line.rstrip())
|
|
474
|
+
lines.append(line)
|
|
475
|
+
|
|
476
|
+
stdout_thread = threading.Thread(
|
|
477
|
+
target=read_stream,
|
|
478
|
+
args=(process.stdout, stdout_lines, logging.DEBUG)
|
|
479
|
+
)
|
|
480
|
+
stderr_thread = threading.Thread(
|
|
481
|
+
target=read_stream,
|
|
482
|
+
args=(process.stderr, stderr_lines, logging.ERROR)
|
|
483
|
+
)
|
|
484
|
+
stdout_thread.start()
|
|
485
|
+
stderr_thread.start()
|
|
486
|
+
|
|
487
|
+
process.wait()
|
|
488
|
+
stdout_thread.join()
|
|
489
|
+
stderr_thread.join()
|
|
490
|
+
|
|
491
|
+
logger.debug(f"{type} {key}: '{script}' run, return code: {process.returncode}")
|
|
492
|
+
if process.returncode != 0:
|
|
493
|
+
stderr_text = "".join(stderr_lines)
|
|
494
|
+
if stderr_text:
|
|
495
|
+
logger.error(f"{type} stderr:\n{stderr_text}")
|
|
496
|
+
raise RuntimeError(f"{type} {key}: '{script}' failed, return code: {process.returncode}")
|
|
385
497
|
except subprocess.CalledProcessError as e:
|
|
386
498
|
logger.error(f"Error executing {key}: '{script}': {e}")
|
|
387
499
|
raise e
|
|
@@ -652,9 +764,7 @@ def archive_content_completer(prefix, parsed_args, **kwargs):
|
|
|
652
764
|
try:
|
|
653
765
|
from dar_backup.config_settings import ConfigSettings
|
|
654
766
|
import subprocess
|
|
655
|
-
import re
|
|
656
767
|
import os
|
|
657
|
-
from datetime import datetime
|
|
658
768
|
|
|
659
769
|
# Expand config path
|
|
660
770
|
config_file = get_config_file(parsed_args)
|
|
@@ -713,7 +823,6 @@ def add_specific_archive_completer(prefix, parsed_args, **kwargs):
|
|
|
713
823
|
import subprocess
|
|
714
824
|
import re
|
|
715
825
|
import os
|
|
716
|
-
from datetime import datetime
|
|
717
826
|
|
|
718
827
|
config_file = get_config_file(parsed_args)
|
|
719
828
|
config = ConfigSettings(config_file=config_file)
|