Browse Source

Support multiple configured systemd service directories (RuntimeDirectory, StateDirectory)

Simon Pilkington 2 weeks ago
parent
commit
68864395b5
2 changed files with 105 additions and 12 deletions
  1. 26 2
      borgmatic/config/paths.py
  2. 79 10
      tests/unit/config/test_paths.py

+ 26 - 2
borgmatic/config/paths.py

@@ -2,10 +2,19 @@ import contextlib
 import logging
 import logging
 import os
 import os
 import tempfile
 import tempfile
+from enum import Enum
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 
 
+class Systemd_directories(Enum):
+    RUNTIME_DIRECTORY = 0
+    STATE_DIRECTORY = 1
+    CACHE_DIRECTORY = 2
+    LOGS_DIRECTORY = 3
+    CONFIGURATION_DIRECTORY = 4
+
+
 def expand_user_in_path(path):
 def expand_user_in_path(path):
     '''
     '''
     Given a directory path, expand any tildes in it.
     Given a directory path, expand any tildes in it.
@@ -16,6 +25,17 @@ def expand_user_in_path(path):
         return None
         return None
 
 
 
 
+def resolve_systemd_directory(directory):
+    '''
+    Given a systemd directory environment variable enum, read the value if set and return the first
+    configured directory.
+    '''
+    separator = ':'
+
+    paths = os.environ.get(directory.name)
+    return paths.split(separator)[0] if paths else None
+
+
 def get_working_directory(config):  # pragma: no cover
 def get_working_directory(config):  # pragma: no cover
     '''
     '''
     Given a configuration dict, get the working directory from it, expanding any tildes.
     Given a configuration dict, get the working directory from it, expanding any tildes.
@@ -96,7 +116,9 @@ class Runtime_directory:
         runtime_directory = (
         runtime_directory = (
             config.get('user_runtime_directory')
             config.get('user_runtime_directory')
             or os.environ.get('XDG_RUNTIME_DIR')  # Set by PAM on Linux.
             or os.environ.get('XDG_RUNTIME_DIR')  # Set by PAM on Linux.
-            or os.environ.get('RUNTIME_DIRECTORY')  # Set by systemd if configured.
+            or resolve_systemd_directory(
+                Systemd_directories.RUNTIME_DIRECTORY
+            )  # Set by systemd if configured.
         )
         )
 
 
         if runtime_directory:
         if runtime_directory:
@@ -174,7 +196,9 @@ def get_borgmatic_state_directory(config):
         os.path.join(
         os.path.join(
             config.get('user_state_directory')
             config.get('user_state_directory')
             or os.environ.get('XDG_STATE_HOME')
             or os.environ.get('XDG_STATE_HOME')
-            or os.environ.get('STATE_DIRECTORY')  # Set by systemd if configured.
+            or resolve_systemd_directory(
+                Systemd_directories.STATE_DIRECTORY
+            )  # Set by systemd if configured.
             or '~/.local/state',
             or '~/.local/state',
             'borgmatic',
             'borgmatic',
         ),
         ),

+ 79 - 10
tests/unit/config/test_paths.py

@@ -121,7 +121,9 @@ def test_runtime_directory_with_relative_xdg_runtime_dir_errors():
 def test_runtime_directory_falls_back_to_runtime_directory():
 def test_runtime_directory_falls_back_to_runtime_directory():
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
     flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
     flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
-    flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
+    flexmock(module).should_receive('resolve_systemd_directory').with_args(
+        module.Systemd_directories.RUNTIME_DIRECTORY
+    ).and_return(
         '/run',
         '/run',
     )
     )
     flexmock(module.os).should_receive('makedirs')
     flexmock(module.os).should_receive('makedirs')
@@ -133,7 +135,9 @@ def test_runtime_directory_falls_back_to_runtime_directory():
 def test_runtime_directory_falls_back_to_runtime_directory_without_adding_duplicate_borgmatic_subdirectory():
 def test_runtime_directory_falls_back_to_runtime_directory_without_adding_duplicate_borgmatic_subdirectory():
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
     flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
     flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
-    flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
+    flexmock(module).should_receive('resolve_systemd_directory').with_args(
+        module.Systemd_directories.RUNTIME_DIRECTORY
+    ).and_return(
         '/run/borgmatic',
         '/run/borgmatic',
     )
     )
     flexmock(module.os).should_receive('makedirs')
     flexmock(module.os).should_receive('makedirs')
@@ -144,7 +148,9 @@ def test_runtime_directory_falls_back_to_runtime_directory_without_adding_duplic
 
 
 def test_runtime_directory_with_relative_runtime_directory_errors():
 def test_runtime_directory_with_relative_runtime_directory_errors():
     flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
     flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
-    flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
+    flexmock(module).should_receive('resolve_systemd_directory').with_args(
+        module.Systemd_directories.RUNTIME_DIRECTORY
+    ).and_return(
         'run',
         'run',
     )
     )
     flexmock(module.os).should_receive('makedirs').never()
     flexmock(module.os).should_receive('makedirs').never()
@@ -156,7 +162,9 @@ def test_runtime_directory_with_relative_runtime_directory_errors():
 def test_runtime_directory_falls_back_to_tmpdir_and_adds_temporary_subdirectory_that_get_cleaned_up():
 def test_runtime_directory_falls_back_to_tmpdir_and_adds_temporary_subdirectory_that_get_cleaned_up():
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
     flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
     flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
-    flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
+    flexmock(module).should_receive('resolve_systemd_directory').with_args(
+        module.Systemd_directories.RUNTIME_DIRECTORY
+    ).and_return(
         None,
         None,
     )
     )
     flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return('/run')
     flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return('/run')
@@ -174,7 +182,9 @@ def test_runtime_directory_falls_back_to_tmpdir_and_adds_temporary_subdirectory_
 
 
 def test_runtime_directory_with_relative_tmpdir_errors():
 def test_runtime_directory_with_relative_tmpdir_errors():
     flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
     flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
-    flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
+    flexmock(module).should_receive('resolve_systemd_directory').with_args(
+        module.Systemd_directories.RUNTIME_DIRECTORY
+    ).and_return(
         None,
         None,
     )
     )
     flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return('run')
     flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return('run')
@@ -188,7 +198,9 @@ def test_runtime_directory_with_relative_tmpdir_errors():
 def test_runtime_directory_falls_back_to_temp_and_adds_temporary_subdirectory_that_get_cleaned_up():
 def test_runtime_directory_falls_back_to_temp_and_adds_temporary_subdirectory_that_get_cleaned_up():
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
     flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
     flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
-    flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
+    flexmock(module).should_receive('resolve_systemd_directory').with_args(
+        module.Systemd_directories.RUNTIME_DIRECTORY
+    ).and_return(
         None,
         None,
     )
     )
     flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
     flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
@@ -207,7 +219,9 @@ def test_runtime_directory_falls_back_to_temp_and_adds_temporary_subdirectory_th
 
 
 def test_runtime_directory_with_relative_temp_errors():
 def test_runtime_directory_with_relative_temp_errors():
     flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
     flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
-    flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
+    flexmock(module).should_receive('resolve_systemd_directory').with_args(
+        module.Systemd_directories.RUNTIME_DIRECTORY
+    ).and_return(
         None,
         None,
     )
     )
     flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
     flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
@@ -222,7 +236,9 @@ def test_runtime_directory_with_relative_temp_errors():
 def test_runtime_directory_falls_back_to_hard_coded_tmp_path_and_adds_temporary_subdirectory_that_get_cleaned_up():
 def test_runtime_directory_falls_back_to_hard_coded_tmp_path_and_adds_temporary_subdirectory_that_get_cleaned_up():
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
     flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
     flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
-    flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
+    flexmock(module).should_receive('resolve_systemd_directory').with_args(
+        module.Systemd_directories.RUNTIME_DIRECTORY
+    ).and_return(
         None,
         None,
     )
     )
     flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
     flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
@@ -242,7 +258,9 @@ def test_runtime_directory_falls_back_to_hard_coded_tmp_path_and_adds_temporary_
 def test_runtime_directory_with_erroring_cleanup_does_not_raise():
 def test_runtime_directory_with_erroring_cleanup_does_not_raise():
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
     flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
     flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
-    flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
+    flexmock(module).should_receive('resolve_systemd_directory').with_args(
+        module.Systemd_directories.RUNTIME_DIRECTORY
+    ).and_return(
         None,
         None,
     )
     )
     flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
     flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
@@ -293,7 +311,9 @@ def test_get_borgmatic_state_directory_falls_back_to_xdg_state_home():
 def test_get_borgmatic_state_directory_falls_back_to_state_directory():
 def test_get_borgmatic_state_directory_falls_back_to_state_directory():
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
     flexmock(module.os.environ).should_receive('get').with_args('XDG_STATE_HOME').and_return(None)
     flexmock(module.os.environ).should_receive('get').with_args('XDG_STATE_HOME').and_return(None)
-    flexmock(module.os.environ).should_receive('get').with_args('STATE_DIRECTORY').and_return(
+    flexmock(module).should_receive('resolve_systemd_directory').with_args(
+        module.Systemd_directories.STATE_DIRECTORY
+    ).and_return(
         '/tmp',
         '/tmp',
     )
     )
 
 
@@ -304,3 +324,52 @@ def test_get_borgmatic_state_directory_defaults_to_hard_coded_path():
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
     flexmock(module.os.environ).should_receive('get').and_return(None)
     flexmock(module.os.environ).should_receive('get').and_return(None)
     assert module.get_borgmatic_state_directory({}) == '~/.local/state/borgmatic'
     assert module.get_borgmatic_state_directory({}) == '~/.local/state/borgmatic'
+
+
+def test_resolve_systemd_directory_none():
+    flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
+        None
+    )
+    flexmock(module.os.environ).should_receive('get').with_args('STATE_DIRECTORY').and_return(None)
+
+    assert module.resolve_systemd_directory(module.Systemd_directories.RUNTIME_DIRECTORY) is None
+    assert module.resolve_systemd_directory(module.Systemd_directories.STATE_DIRECTORY) is None
+
+
+def test_resolve_systemd_directory_single():
+    runtime_dir = '/run/borgmatic'
+    state_dir = '/var/lib/borgmatic'
+
+    flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
+        runtime_dir
+    )
+    flexmock(module.os.environ).should_receive('get').with_args('STATE_DIRECTORY').and_return(
+        state_dir
+    )
+
+    assert (
+        module.resolve_systemd_directory(module.Systemd_directories.RUNTIME_DIRECTORY)
+        == runtime_dir
+    )
+    assert module.resolve_systemd_directory(module.Systemd_directories.STATE_DIRECTORY) == state_dir
+
+
+def test_resolve_systemd_directory_multiple():
+    runtime_dirs = '/run/borgmatic:/run/second:/run/third'
+    state_dirs = '/var/lib/borgmatic:/var/lib/second:/var/lib/third'
+
+    flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
+        runtime_dirs
+    )
+    flexmock(module.os.environ).should_receive('get').with_args('STATE_DIRECTORY').and_return(
+        state_dirs
+    )
+
+    assert (
+        module.resolve_systemd_directory(module.Systemd_directories.RUNTIME_DIRECTORY)
+        == '/run/borgmatic'
+    )
+    assert (
+        module.resolve_systemd_directory(module.Systemd_directories.STATE_DIRECTORY)
+        == '/var/lib/borgmatic'
+    )