borgmatic.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765
  1. import collections
  2. import json
  3. import logging
  4. import os
  5. import sys
  6. import time
  7. from queue import Queue
  8. from subprocess import CalledProcessError
  9. import colorama
  10. try:
  11. import importlib_metadata
  12. except ModuleNotFoundError: # pragma: nocover
  13. import importlib.metadata as importlib_metadata
  14. import borgmatic.actions.borg
  15. import borgmatic.actions.break_lock
  16. import borgmatic.actions.check
  17. import borgmatic.actions.compact
  18. import borgmatic.actions.create
  19. import borgmatic.actions.export_tar
  20. import borgmatic.actions.extract
  21. import borgmatic.actions.info
  22. import borgmatic.actions.list
  23. import borgmatic.actions.mount
  24. import borgmatic.actions.prune
  25. import borgmatic.actions.rcreate
  26. import borgmatic.actions.restore
  27. import borgmatic.actions.rinfo
  28. import borgmatic.actions.rlist
  29. import borgmatic.actions.transfer
  30. import borgmatic.commands.completion
  31. from borgmatic.borg import umount as borg_umount
  32. from borgmatic.borg import version as borg_version
  33. from borgmatic.commands.arguments import parse_arguments
  34. from borgmatic.config import checks, collect, convert, validate
  35. from borgmatic.hooks import command, dispatch, monitor
  36. from borgmatic.logger import add_custom_log_levels, configure_logging, should_do_markup
  37. from borgmatic.signals import configure_signals
  38. from borgmatic.verbosity import verbosity_to_log_level
  39. logger = logging.getLogger(__name__)
  40. LEGACY_CONFIG_PATH = '/etc/borgmatic/config'
  41. def run_configuration(config_filename, config, arguments):
  42. '''
  43. Given a config filename, the corresponding parsed config dict, and command-line arguments as a
  44. dict from subparser name to a namespace of parsed arguments, execute the defined create, prune,
  45. compact, check, and/or other actions.
  46. Yield a combination of:
  47. * JSON output strings from successfully executing any actions that produce JSON
  48. * logging.LogRecord instances containing errors from any actions or backup hooks that fail
  49. '''
  50. (location, storage, retention, consistency, hooks) = (
  51. config.get(section_name, {})
  52. for section_name in ('location', 'storage', 'retention', 'consistency', 'hooks')
  53. )
  54. global_arguments = arguments['global']
  55. local_path = location.get('local_path', 'borg')
  56. remote_path = location.get('remote_path')
  57. retries = storage.get('retries', 0)
  58. retry_wait = storage.get('retry_wait', 0)
  59. encountered_error = None
  60. error_repository = ''
  61. using_primary_action = {'create', 'prune', 'compact', 'check'}.intersection(arguments)
  62. monitoring_log_level = verbosity_to_log_level(global_arguments.monitoring_verbosity)
  63. try:
  64. local_borg_version = borg_version.local_borg_version(storage, local_path)
  65. except (OSError, CalledProcessError, ValueError) as error:
  66. yield from log_error_records(f'{config_filename}: Error getting local Borg version', error)
  67. return
  68. try:
  69. if using_primary_action:
  70. dispatch.call_hooks(
  71. 'initialize_monitor',
  72. hooks,
  73. config_filename,
  74. monitor.MONITOR_HOOK_NAMES,
  75. monitoring_log_level,
  76. global_arguments.dry_run,
  77. )
  78. if using_primary_action:
  79. dispatch.call_hooks(
  80. 'ping_monitor',
  81. hooks,
  82. config_filename,
  83. monitor.MONITOR_HOOK_NAMES,
  84. monitor.State.START,
  85. monitoring_log_level,
  86. global_arguments.dry_run,
  87. )
  88. except (OSError, CalledProcessError) as error:
  89. if command.considered_soft_failure(config_filename, error):
  90. return
  91. encountered_error = error
  92. yield from log_error_records(f'{config_filename}: Error pinging monitor', error)
  93. if not encountered_error:
  94. repo_queue = Queue()
  95. for repo in location['repositories']:
  96. repo_queue.put(
  97. (repo, 0),
  98. )
  99. while not repo_queue.empty():
  100. repository, retry_num = repo_queue.get()
  101. logger.debug(f'{repository["path"]}: Running actions for repository')
  102. timeout = retry_num * retry_wait
  103. if timeout:
  104. logger.warning(f'{config_filename}: Sleeping {timeout}s before next retry')
  105. time.sleep(timeout)
  106. try:
  107. yield from run_actions(
  108. arguments=arguments,
  109. config_filename=config_filename,
  110. location=location,
  111. storage=storage,
  112. retention=retention,
  113. consistency=consistency,
  114. hooks=hooks,
  115. local_path=local_path,
  116. remote_path=remote_path,
  117. local_borg_version=local_borg_version,
  118. repository=repository,
  119. )
  120. except (OSError, CalledProcessError, ValueError) as error:
  121. if retry_num < retries:
  122. repo_queue.put(
  123. (repository, retry_num + 1),
  124. )
  125. tuple( # Consume the generator so as to trigger logging.
  126. log_error_records(
  127. f'{repository["path"]}: Error running actions for repository',
  128. error,
  129. levelno=logging.WARNING,
  130. log_command_error_output=True,
  131. )
  132. )
  133. logger.warning(
  134. f'{config_filename}: Retrying... attempt {retry_num + 1}/{retries}'
  135. )
  136. continue
  137. if command.considered_soft_failure(config_filename, error):
  138. return
  139. yield from log_error_records(
  140. f'{repository["path"]}: Error running actions for repository', error
  141. )
  142. encountered_error = error
  143. error_repository = repository['path']
  144. try:
  145. if using_primary_action:
  146. # send logs irrespective of error
  147. dispatch.call_hooks(
  148. 'ping_monitor',
  149. hooks,
  150. config_filename,
  151. monitor.MONITOR_HOOK_NAMES,
  152. monitor.State.LOG,
  153. monitoring_log_level,
  154. global_arguments.dry_run,
  155. )
  156. except (OSError, CalledProcessError) as error:
  157. if command.considered_soft_failure(config_filename, error):
  158. return
  159. encountered_error = error
  160. yield from log_error_records(f'{repository["path"]}: Error pinging monitor', error)
  161. if not encountered_error:
  162. try:
  163. if using_primary_action:
  164. dispatch.call_hooks(
  165. 'ping_monitor',
  166. hooks,
  167. config_filename,
  168. monitor.MONITOR_HOOK_NAMES,
  169. monitor.State.FINISH,
  170. monitoring_log_level,
  171. global_arguments.dry_run,
  172. )
  173. dispatch.call_hooks(
  174. 'destroy_monitor',
  175. hooks,
  176. config_filename,
  177. monitor.MONITOR_HOOK_NAMES,
  178. monitoring_log_level,
  179. global_arguments.dry_run,
  180. )
  181. except (OSError, CalledProcessError) as error:
  182. if command.considered_soft_failure(config_filename, error):
  183. return
  184. encountered_error = error
  185. yield from log_error_records(f'{config_filename}: Error pinging monitor', error)
  186. if encountered_error and using_primary_action:
  187. try:
  188. command.execute_hook(
  189. hooks.get('on_error'),
  190. hooks.get('umask'),
  191. config_filename,
  192. 'on-error',
  193. global_arguments.dry_run,
  194. repository=error_repository,
  195. error=encountered_error,
  196. output=getattr(encountered_error, 'output', ''),
  197. )
  198. dispatch.call_hooks(
  199. 'ping_monitor',
  200. hooks,
  201. config_filename,
  202. monitor.MONITOR_HOOK_NAMES,
  203. monitor.State.FAIL,
  204. monitoring_log_level,
  205. global_arguments.dry_run,
  206. )
  207. dispatch.call_hooks(
  208. 'destroy_monitor',
  209. hooks,
  210. config_filename,
  211. monitor.MONITOR_HOOK_NAMES,
  212. monitoring_log_level,
  213. global_arguments.dry_run,
  214. )
  215. except (OSError, CalledProcessError) as error:
  216. if command.considered_soft_failure(config_filename, error):
  217. return
  218. yield from log_error_records(f'{config_filename}: Error running on-error hook', error)
  219. def run_actions(
  220. *,
  221. arguments,
  222. config_filename,
  223. location,
  224. storage,
  225. retention,
  226. consistency,
  227. hooks,
  228. local_path,
  229. remote_path,
  230. local_borg_version,
  231. repository,
  232. ):
  233. '''
  234. Given parsed command-line arguments as an argparse.ArgumentParser instance, the configuration
  235. filename, several different configuration dicts, local and remote paths to Borg, a local Borg
  236. version string, and a repository name, run all actions from the command-line arguments on the
  237. given repository.
  238. Yield JSON output strings from executing any actions that produce JSON.
  239. Raise OSError or subprocess.CalledProcessError if an error occurs running a command for an
  240. action or a hook. Raise ValueError if the arguments or configuration passed to action are
  241. invalid.
  242. '''
  243. add_custom_log_levels()
  244. repository_path = os.path.expanduser(repository['path'])
  245. global_arguments = arguments['global']
  246. dry_run_label = ' (dry run; not making any changes)' if global_arguments.dry_run else ''
  247. hook_context = {
  248. 'repository': repository_path,
  249. # Deprecated: For backwards compatibility with borgmatic < 1.6.0.
  250. 'repositories': ','.join([repo['path'] for repo in location['repositories']]),
  251. 'log_file': global_arguments.log_file if global_arguments.log_file else '',
  252. }
  253. command.execute_hook(
  254. hooks.get('before_actions'),
  255. hooks.get('umask'),
  256. config_filename,
  257. 'pre-actions',
  258. global_arguments.dry_run,
  259. **hook_context,
  260. )
  261. for action_name, action_arguments in arguments.items():
  262. if action_name == 'rcreate':
  263. borgmatic.actions.rcreate.run_rcreate(
  264. repository,
  265. storage,
  266. local_borg_version,
  267. action_arguments,
  268. global_arguments,
  269. local_path,
  270. remote_path,
  271. )
  272. elif action_name == 'transfer':
  273. borgmatic.actions.transfer.run_transfer(
  274. repository,
  275. storage,
  276. local_borg_version,
  277. action_arguments,
  278. global_arguments,
  279. local_path,
  280. remote_path,
  281. )
  282. elif action_name == 'create':
  283. yield from borgmatic.actions.create.run_create(
  284. config_filename,
  285. repository,
  286. location,
  287. storage,
  288. hooks,
  289. hook_context,
  290. local_borg_version,
  291. action_arguments,
  292. global_arguments,
  293. dry_run_label,
  294. local_path,
  295. remote_path,
  296. )
  297. elif action_name == 'prune':
  298. borgmatic.actions.prune.run_prune(
  299. config_filename,
  300. repository,
  301. storage,
  302. retention,
  303. hooks,
  304. hook_context,
  305. local_borg_version,
  306. action_arguments,
  307. global_arguments,
  308. dry_run_label,
  309. local_path,
  310. remote_path,
  311. )
  312. elif action_name == 'compact':
  313. borgmatic.actions.compact.run_compact(
  314. config_filename,
  315. repository,
  316. storage,
  317. retention,
  318. hooks,
  319. hook_context,
  320. local_borg_version,
  321. action_arguments,
  322. global_arguments,
  323. dry_run_label,
  324. local_path,
  325. remote_path,
  326. )
  327. elif action_name == 'check':
  328. if checks.repository_enabled_for_checks(repository, consistency):
  329. borgmatic.actions.check.run_check(
  330. config_filename,
  331. repository,
  332. location,
  333. storage,
  334. consistency,
  335. hooks,
  336. hook_context,
  337. local_borg_version,
  338. action_arguments,
  339. global_arguments,
  340. local_path,
  341. remote_path,
  342. )
  343. elif action_name == 'extract':
  344. borgmatic.actions.extract.run_extract(
  345. config_filename,
  346. repository,
  347. location,
  348. storage,
  349. hooks,
  350. hook_context,
  351. local_borg_version,
  352. action_arguments,
  353. global_arguments,
  354. local_path,
  355. remote_path,
  356. )
  357. elif action_name == 'export-tar':
  358. borgmatic.actions.export_tar.run_export_tar(
  359. repository,
  360. storage,
  361. local_borg_version,
  362. action_arguments,
  363. global_arguments,
  364. local_path,
  365. remote_path,
  366. )
  367. elif action_name == 'mount':
  368. borgmatic.actions.mount.run_mount(
  369. repository,
  370. storage,
  371. local_borg_version,
  372. arguments['mount'],
  373. local_path,
  374. remote_path,
  375. )
  376. elif action_name == 'restore':
  377. borgmatic.actions.restore.run_restore(
  378. repository,
  379. location,
  380. storage,
  381. hooks,
  382. local_borg_version,
  383. action_arguments,
  384. global_arguments,
  385. local_path,
  386. remote_path,
  387. )
  388. elif action_name == 'rlist':
  389. yield from borgmatic.actions.rlist.run_rlist(
  390. repository,
  391. storage,
  392. local_borg_version,
  393. action_arguments,
  394. local_path,
  395. remote_path,
  396. )
  397. elif action_name == 'list':
  398. yield from borgmatic.actions.list.run_list(
  399. repository,
  400. storage,
  401. local_borg_version,
  402. action_arguments,
  403. local_path,
  404. remote_path,
  405. )
  406. elif action_name == 'rinfo':
  407. yield from borgmatic.actions.rinfo.run_rinfo(
  408. repository,
  409. storage,
  410. local_borg_version,
  411. action_arguments,
  412. local_path,
  413. remote_path,
  414. )
  415. elif action_name == 'info':
  416. yield from borgmatic.actions.info.run_info(
  417. repository,
  418. storage,
  419. local_borg_version,
  420. action_arguments,
  421. local_path,
  422. remote_path,
  423. )
  424. elif action_name == 'break-lock':
  425. borgmatic.actions.break_lock.run_break_lock(
  426. repository,
  427. storage,
  428. local_borg_version,
  429. arguments['break-lock'],
  430. local_path,
  431. remote_path,
  432. )
  433. elif action_name == 'borg':
  434. borgmatic.actions.borg.run_borg(
  435. repository,
  436. storage,
  437. local_borg_version,
  438. action_arguments,
  439. local_path,
  440. remote_path,
  441. )
  442. command.execute_hook(
  443. hooks.get('after_actions'),
  444. hooks.get('umask'),
  445. config_filename,
  446. 'post-actions',
  447. global_arguments.dry_run,
  448. **hook_context,
  449. )
  450. def load_configurations(config_filenames, overrides=None, resolve_env=True):
  451. '''
  452. Given a sequence of configuration filenames, load and validate each configuration file. Return
  453. the results as a tuple of: dict of configuration filename to corresponding parsed configuration,
  454. and sequence of logging.LogRecord instances containing any parse errors.
  455. '''
  456. # Dict mapping from config filename to corresponding parsed config dict.
  457. configs = collections.OrderedDict()
  458. logs = []
  459. # Parse and load each configuration file.
  460. for config_filename in config_filenames:
  461. try:
  462. configs[config_filename], parse_logs = validate.parse_configuration(
  463. config_filename, validate.schema_filename(), overrides, resolve_env
  464. )
  465. logs.extend(parse_logs)
  466. except PermissionError:
  467. logs.extend(
  468. [
  469. logging.makeLogRecord(
  470. dict(
  471. levelno=logging.WARNING,
  472. levelname='WARNING',
  473. msg=f'{config_filename}: Insufficient permissions to read configuration file',
  474. )
  475. ),
  476. ]
  477. )
  478. except (ValueError, OSError, validate.Validation_error) as error:
  479. logs.extend(
  480. [
  481. logging.makeLogRecord(
  482. dict(
  483. levelno=logging.CRITICAL,
  484. levelname='CRITICAL',
  485. msg=f'{config_filename}: Error parsing configuration file',
  486. )
  487. ),
  488. logging.makeLogRecord(
  489. dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error)
  490. ),
  491. ]
  492. )
  493. return (configs, logs)
  494. def log_record(suppress_log=False, **kwargs):
  495. '''
  496. Create a log record based on the given makeLogRecord() arguments, one of which must be
  497. named "levelno". Log the record (unless suppress log is set) and return it.
  498. '''
  499. record = logging.makeLogRecord(kwargs)
  500. if suppress_log:
  501. return record
  502. logger.handle(record)
  503. return record
  504. def log_error_records(
  505. message, error=None, levelno=logging.CRITICAL, log_command_error_output=False
  506. ):
  507. '''
  508. Given error message text, an optional exception object, an optional log level, and whether to
  509. log the error output of a CalledProcessError (if any), log error summary information and also
  510. yield it as a series of logging.LogRecord instances.
  511. Note that because the logs are yielded as a generator, logs won't get logged unless you consume
  512. the generator output.
  513. '''
  514. level_name = logging._levelToName[levelno]
  515. if not error:
  516. yield log_record(levelno=levelno, levelname=level_name, msg=message)
  517. return
  518. try:
  519. raise error
  520. except CalledProcessError as error:
  521. yield log_record(levelno=levelno, levelname=level_name, msg=message)
  522. if error.output:
  523. # Suppress these logs for now and save full error output for the log summary at the end.
  524. yield log_record(
  525. levelno=levelno,
  526. levelname=level_name,
  527. msg=error.output,
  528. suppress_log=not log_command_error_output,
  529. )
  530. yield log_record(levelno=levelno, levelname=level_name, msg=error)
  531. except (ValueError, OSError) as error:
  532. yield log_record(levelno=levelno, levelname=level_name, msg=message)
  533. yield log_record(levelno=levelno, levelname=level_name, msg=error)
  534. except: # noqa: E722
  535. # Raising above only as a means of determining the error type. Swallow the exception here
  536. # because we don't want the exception to propagate out of this function.
  537. pass
  538. def get_local_path(configs):
  539. '''
  540. Arbitrarily return the local path from the first configuration dict. Default to "borg" if not
  541. set.
  542. '''
  543. return next(iter(configs.values())).get('location', {}).get('local_path', 'borg')
  544. def collect_configuration_run_summary_logs(configs, arguments):
  545. '''
  546. Given a dict of configuration filename to corresponding parsed configuration, and parsed
  547. command-line arguments as a dict from subparser name to a parsed namespace of arguments, run
  548. each configuration file and yield a series of logging.LogRecord instances containing summary
  549. information about each run.
  550. As a side effect of running through these configuration files, output their JSON results, if
  551. any, to stdout.
  552. '''
  553. # Run cross-file validation checks.
  554. repository = None
  555. for action_name, action_arguments in arguments.items():
  556. if hasattr(action_arguments, 'repository'):
  557. repository = getattr(action_arguments, 'repository')
  558. break
  559. try:
  560. if 'extract' in arguments or 'mount' in arguments:
  561. validate.guard_single_repository_selected(repository, configs)
  562. validate.guard_configuration_contains_repository(repository, configs)
  563. except ValueError as error:
  564. yield from log_error_records(str(error))
  565. return
  566. if not configs:
  567. yield from log_error_records(
  568. f"{' '.join(arguments['global'].config_paths)}: No valid configuration files found",
  569. )
  570. return
  571. if 'create' in arguments:
  572. try:
  573. for config_filename, config in configs.items():
  574. hooks = config.get('hooks', {})
  575. command.execute_hook(
  576. hooks.get('before_everything'),
  577. hooks.get('umask'),
  578. config_filename,
  579. 'pre-everything',
  580. arguments['global'].dry_run,
  581. )
  582. except (CalledProcessError, ValueError, OSError) as error:
  583. yield from log_error_records('Error running pre-everything hook', error)
  584. return
  585. # Execute the actions corresponding to each configuration file.
  586. json_results = []
  587. for config_filename, config in configs.items():
  588. results = list(run_configuration(config_filename, config, arguments))
  589. error_logs = tuple(result for result in results if isinstance(result, logging.LogRecord))
  590. if error_logs:
  591. yield from log_error_records(f'{config_filename}: An error occurred')
  592. yield from error_logs
  593. else:
  594. yield logging.makeLogRecord(
  595. dict(
  596. levelno=logging.INFO,
  597. levelname='INFO',
  598. msg=f'{config_filename}: Successfully ran configuration file',
  599. )
  600. )
  601. if results:
  602. json_results.extend(results)
  603. if 'umount' in arguments:
  604. logger.info(f"Unmounting mount point {arguments['umount'].mount_point}")
  605. try:
  606. borg_umount.unmount_archive(
  607. mount_point=arguments['umount'].mount_point,
  608. local_path=get_local_path(configs),
  609. )
  610. except (CalledProcessError, OSError) as error:
  611. yield from log_error_records('Error unmounting mount point', error)
  612. if json_results:
  613. sys.stdout.write(json.dumps(json_results))
  614. if 'create' in arguments:
  615. try:
  616. for config_filename, config in configs.items():
  617. hooks = config.get('hooks', {})
  618. command.execute_hook(
  619. hooks.get('after_everything'),
  620. hooks.get('umask'),
  621. config_filename,
  622. 'post-everything',
  623. arguments['global'].dry_run,
  624. )
  625. except (CalledProcessError, ValueError, OSError) as error:
  626. yield from log_error_records('Error running post-everything hook', error)
  627. def exit_with_help_link(): # pragma: no cover
  628. '''
  629. Display a link to get help and exit with an error code.
  630. '''
  631. logger.critical('')
  632. logger.critical('Need some help? https://torsion.org/borgmatic/#issues')
  633. sys.exit(1)
  634. def main(): # pragma: no cover
  635. configure_signals()
  636. try:
  637. arguments = parse_arguments(*sys.argv[1:])
  638. except ValueError as error:
  639. configure_logging(logging.CRITICAL)
  640. logger.critical(error)
  641. exit_with_help_link()
  642. except SystemExit as error:
  643. if error.code == 0:
  644. raise error
  645. configure_logging(logging.CRITICAL)
  646. logger.critical(f"Error parsing arguments: {' '.join(sys.argv)}")
  647. exit_with_help_link()
  648. global_arguments = arguments['global']
  649. if global_arguments.version:
  650. print(importlib_metadata.version('borgmatic'))
  651. sys.exit(0)
  652. if global_arguments.bash_completion:
  653. print(borgmatic.commands.completion.bash_completion())
  654. sys.exit(0)
  655. if global_arguments.fish_completion:
  656. print(borgmatic.commands.completion.fish_completion())
  657. sys.exit(0)
  658. config_filenames = tuple(collect.collect_config_filenames(global_arguments.config_paths))
  659. configs, parse_logs = load_configurations(
  660. config_filenames, global_arguments.overrides, global_arguments.resolve_env
  661. )
  662. any_json_flags = any(
  663. getattr(sub_arguments, 'json', False) for sub_arguments in arguments.values()
  664. )
  665. colorama.init(
  666. autoreset=True,
  667. strip=not should_do_markup(global_arguments.no_color or any_json_flags, configs),
  668. )
  669. try:
  670. configure_logging(
  671. verbosity_to_log_level(global_arguments.verbosity),
  672. verbosity_to_log_level(global_arguments.syslog_verbosity),
  673. verbosity_to_log_level(global_arguments.log_file_verbosity),
  674. verbosity_to_log_level(global_arguments.monitoring_verbosity),
  675. global_arguments.log_file,
  676. global_arguments.log_file_format,
  677. )
  678. except (FileNotFoundError, PermissionError) as error:
  679. configure_logging(logging.CRITICAL)
  680. logger.critical(f'Error configuring logging: {error}')
  681. exit_with_help_link()
  682. logger.debug('Ensuring legacy configuration is upgraded')
  683. convert.guard_configuration_upgraded(LEGACY_CONFIG_PATH, config_filenames)
  684. summary_logs = parse_logs + list(collect_configuration_run_summary_logs(configs, arguments))
  685. summary_logs_max_level = max(log.levelno for log in summary_logs)
  686. for message in ('', 'summary:'):
  687. log_record(
  688. levelno=summary_logs_max_level,
  689. levelname=logging.getLevelName(summary_logs_max_level),
  690. msg=message,
  691. )
  692. for log in summary_logs:
  693. logger.handle(log)
  694. if summary_logs_max_level >= logging.CRITICAL:
  695. exit_with_help_link()