Browse Source

Add pre/post

Nathaniel van Diepen 4 years ago
parent
commit
b404b670ec
4 changed files with 95 additions and 58 deletions
  1. 1 1
      README.md
  2. 90 44
      backup/__init__.py
  3. 1 12
      backup/command_line.py
  4. 3 1
      etc/backup.d/sources.d/example.yml

+ 1 - 1
README.md

@@ -25,7 +25,7 @@ For examples see the [etc](etc) folder.
 - Backup dependency chains
 - Multithreaded
 - YAML config files
+- pre/post hooks for each backup job
 
 ## Known deficiencies
 - Infinity loops from bad dependency chains
-- No pre/post backup scripts

+ 90 - 44
backup/__init__.py

@@ -60,9 +60,11 @@ class Job(object):
     logTransitions = False
     READY = 0
     QUEUED = 1
-    RUNNING = 2
-    FAILED = 3
-    SUCCESSFUL = 4
+    STARTING = 2
+    RUNNING = 3
+    ENDING = 4
+    FAILED = 5
+    SUCCESSFUL = 6
 
     @staticmethod
     def initPool():
@@ -91,7 +93,7 @@ class Job(object):
     @staticmethod
     def running():
         return [x for x in Job.pool
-                if x.state is Job.RUNNING]
+                if x.state in (Job.STARTING, Job.RUNNING, Job.ENDING)]
 
     @staticmethod
     def queued():
@@ -115,14 +117,22 @@ class Job(object):
         if self._state is Job.READY and self in Job.pool:
             self.setState(Job.QUEUED)
 
-        elif self._state is Job.RUNNING:
+        elif self._state in (Job.STARTING, Job.RUNNING, Job.ENDING):
             code = self._process.poll() or self._process.returncode
             if code is None and not psutil.pid_exists(self._process.pid):
                 code = -1
 
             if code is not None:
-                self.setState(Job.FAILED if code else Job.SUCCESSFUL)
-                Job.initPool()
+                if code:
+                    self.setState(Job.FAILED)
+                    Job.initPool()
+
+                elif self._state is Job.ENDING:
+                    self.setState(Job.SUCCESSFUL)
+                    Job.initPool()
+
+                else:
+                    self.start()
 
         return self._state
 
@@ -138,7 +148,10 @@ class Job(object):
 
     @property
     def args(self):
-        if not hasattr(self, '_args'):
+        if self._state is Job.STARTING:
+            return shlex.split(self.pre)
+
+        elif self._state is Job.RUNNING:
             if Backup.engine == "rdiff-backup":
                 args = ['rdiff-backup', '-v{}'.format(Backup.verbosity)]
                 if 'filters' in self.config:
@@ -153,13 +166,17 @@ class Job(object):
                             raise BackupException(
                                 '{0} has an invalid filter {1}'.format(self, item))
 
-                self._args = args + [self.fromPath, self.toPath]
+                return args + [self.fromPath, self.toPath]
 
             else:
                 raise StateException(
                     'Invalid backup engine {}'.format(Backup.engine))
 
-        return self._args
+        elif self._state is Job.ENDING:
+            return shlex.split(self.post)
+
+        else:
+            raise StateException('Invalid state {}'.format(self.getState()))
 
     @property
     def logfile(self):
@@ -233,6 +250,20 @@ class Job(object):
 
         return self._index
 
+    @property
+    def pre(self):
+        if not hasattr(self, '_pre'):
+            self._pre = self.config['pre'] if 'pre' in self.config else None
+
+        return self._pre
+
+    @property
+    def post(self):
+        if not hasattr(self, '_post'):
+            self._post = self.config['post'] if 'post' in self.config else None
+
+        return self._post
+
     def log(self, text):
         text = '[Backup {0} Job #{1}] {2}\n'.format(
                 self._backup.name, self.index, text)
@@ -241,15 +272,29 @@ class Job(object):
         self._backup.logfile.flush()
 
     def start(self):
-        if self.state is not Job.QUEUED:
+        if self._state is self.QUEUED:
+            self._backup.setStatus(Backup.RUNNING)
+            self.setState(Job.STARTING)
+            if self.pre is None:
+                self.setState(Job.RUNNING)
+
+        elif self._state is self.STARTING:
+            self.setState(Job.RUNNING)
+
+        elif self._state is self.RUNNING:
+            self.setState(Job.ENDING)
+            if self.post is None:
+                self.setState(Job.SUCCESSFUL)
+                return
+
+        else:
             raise StateException('Invalid state to start {}'.format(self))
 
-        self._backup.setStatus(Backup.RUNNING)
-        self.setState(Job.RUNNING)
-        self.logfile.write(' '.join([shlex.quote(x) for x in self.args]) + '\n')
+        args = self.args
+        self.logfile.write(' '.join([shlex.quote(x) for x in args]) + '\n')
         self.logfile.flush()
         self._process = subprocess.Popen(
-                self.args, stdout=self.logfile, stderr=subprocess.STDOUT,
+                args, stdout=self.logfile, stderr=subprocess.STDOUT,
                 stdin=subprocess.DEVNULL, universal_newlines=True, bufsize=1)
 
     def setState(self, state):
@@ -264,7 +309,9 @@ class Job(object):
         return {
             Job.READY: 'ready',
             Job.QUEUED: 'queued',
+            Job.STARTING: 'starting',
             Job.RUNNING: 'running',
+            Job.ENDING: 'ending',
             Job.FAILED: 'failed',
             Job.SUCCESSFUL: 'successful',
         }[self.state if state is None else state]
@@ -520,43 +567,38 @@ def sources():
 
 
 def main(args):
-    config._root = args[0] if len(args) else '/etc/backup.d'
-    if not os.path.exists(config._root):
-        raise BackupException(
-            'Configuration files missing from {}'.format(config._root))
-
-    if 'engine' in config():
-        engine = config()["engine"]
-        if engine not in ("rdiff-backup"):
-            raise BackupException('Unknown backup engine: {}'.format(engine))
-
-        Backup.engine = engine
-
-    if 'logdir' in config():
-        logdir = config()['logdir']
-        os.makedirs(logdir, exist_ok=True)
-        if not os.path.exists(logdir):
+    try:
+        config._root = args[0] if len(args) else '/etc/backup.d'
+        if not os.path.exists(config._root):
             raise BackupException(
-                'Unable to create logging directory: {}'.format(logdir))
+                'Configuration files missing from {}'.format(config._root))
 
-        Backup.logdir = logdir
+        if 'engine' in config():
+            engine = config()["engine"]
+            if engine not in ("rdiff-backup"):
+                raise BackupException('Unknown backup engine: {}'.format(engine))
 
-    if 'maxthreads' in config():
-        Job.maxthreads = config()['maxthreads']
+            Backup.engine = engine
 
-    if 'verbosity' in config():
-        Backup.verbosity = config()['verbosity']
+        if 'logdir' in config():
+            logdir = config()['logdir']
+            os.makedirs(logdir, exist_ok=True)
+            if not os.path.exists(logdir):
+                raise BackupException(
+                    'Unable to create logging directory: {}'.format(logdir))
 
-    Backup.logTransitions = Job.logTransitions = True
-    Backup.load(sources().keys())
-    Backup.start()
-    Backup.wait()
+            Backup.logdir = logdir
 
+        if 'maxthreads' in config():
+            Job.maxthreads = config()['maxthreads']
 
-if __name__ == '__main__':
-    try:
-        main(sys.argv[1:])
+        if 'verbosity' in config():
+            Backup.verbosity = config()['verbosity']
 
+        Backup.logTransitions = Job.logTransitions = True
+        Backup.load(sources().keys())
+        Backup.start()
+        Backup.wait()
     except BackupException as ex:
         print(ex)
         sys.exit(1)
@@ -566,3 +608,7 @@ if __name__ == '__main__':
         msg = "Error encountered:\n" + format_exc().strip()
         print(msg)
         sys.exit(1)
+
+
+if __name__ == '__main__':
+    main(sys.argv[1:])

+ 1 - 12
backup/command_line.py

@@ -4,15 +4,4 @@ import sys
 
 
 def main():
-    try:
-        backup.main(sys.argv[1:])
-
-    except backup.BackupException as ex:
-        print(ex)
-        sys.exit(1)
-
-    except Exception:
-        from traceback import format_exc
-        msg = "Error encountered:\n" + format_exc().strip()
-        print(msg)
-        sys.exit(1)
+    backup.main(sys.argv[1:])

+ 3 - 1
etc/backup.d/sources.d/example.yml

@@ -6,5 +6,7 @@ roots:
   from: "ssh.host::"
   to: /backups
 jobs:
-  - from: /var/www
+  - pre: echo "Starting backup"
+    from: /var/www
     to: www
+    post: echo "Finished"