|
@@ -1,11 +1,36 @@
|
|
import contextlib
|
|
import contextlib
|
|
import os
|
|
import os
|
|
-import yaml
|
|
|
|
|
|
+import platform
|
|
|
|
+import subprocess
|
|
import sys
|
|
import sys
|
|
|
|
+import time
|
|
|
|
+import yaml
|
|
|
|
|
|
from glob import glob
|
|
from glob import glob
|
|
|
|
|
|
|
|
|
|
|
|
+def term():
|
|
|
|
+ """Get the Terminal reference to make output pretty
|
|
|
|
+
|
|
|
|
+ Returns:
|
|
|
|
+ (blessings.Terminal): Returns
|
|
|
|
+ a `blessings <https://blessings.readthedocs.io/en/latest>`_ terminal
|
|
|
|
+ instance. If running in windows and not cygwin it will return an
|
|
|
|
+ `intercessions <https://pypi.org/project/intercessions>`_ terminal
|
|
|
|
+ instance instead
|
|
|
|
+ """
|
|
|
|
+ if not hasattr(term, '_handle'):
|
|
|
|
+ if sys.platform != "cygwin" and platform.system() == 'Windows':
|
|
|
|
+ from intercessions import Terminal
|
|
|
|
+
|
|
|
|
+ else:
|
|
|
|
+ from blessings import Terminal
|
|
|
|
+
|
|
|
|
+ term._handle = Terminal()
|
|
|
|
+
|
|
|
|
+ return term._handle
|
|
|
|
+
|
|
|
|
+
|
|
@contextlib.contextmanager
|
|
@contextlib.contextmanager
|
|
def pushd(newDir):
|
|
def pushd(newDir):
|
|
previousDir = os.getcwd()
|
|
previousDir = os.getcwd()
|
|
@@ -25,10 +50,8 @@ class BackupException(Exception):
|
|
pass
|
|
pass
|
|
|
|
|
|
|
|
|
|
-def deptree(sources, deps=None):
|
|
|
|
- if deps is None:
|
|
|
|
- deps = []
|
|
|
|
-
|
|
|
|
|
|
+def deptree(sources):
|
|
|
|
+ deps = []
|
|
deferred = []
|
|
deferred = []
|
|
for name in sources.keys():
|
|
for name in sources.keys():
|
|
source = sources[name]
|
|
source = sources[name]
|
|
@@ -69,32 +92,55 @@ def status(name, value=None):
|
|
status._handle[name] = value
|
|
status._handle[name] = value
|
|
|
|
|
|
|
|
|
|
-def backup(name):
|
|
|
|
- source = main.sources[name]
|
|
|
|
|
|
+def backup(source):
|
|
failed = [x for x in source["depends"] if not status(x)]
|
|
failed = [x for x in source["depends"] if not status(x)]
|
|
|
|
+ path = source['path']
|
|
if failed:
|
|
if failed:
|
|
- raise BackupException(
|
|
|
|
- "Unable to backup {0} due to ncomplete backups: {1}".format(
|
|
|
|
- name, failed))
|
|
|
|
-
|
|
|
|
- # TODO - handle explicit failures with false
|
|
|
|
- # status(name, False)
|
|
|
|
- status(name, True)
|
|
|
|
-
|
|
|
|
|
|
+ process = BackupException(
|
|
|
|
+ "Unable to backup {0} due to incomplete backups: {1}".format(
|
|
|
|
+ path, ', '.join(failed)))
|
|
|
|
+ active = False
|
|
|
|
+
|
|
|
|
+ else:
|
|
|
|
+ args = []
|
|
|
|
+ process = subprocess.Popen(
|
|
|
|
+ ['rdiff-backup'] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
|
|
+ stdin=subprocess.DEVNULL, universal_newlines=True)
|
|
|
|
+ active = True
|
|
|
|
+
|
|
|
|
+ return {
|
|
|
|
+ 'active': active,
|
|
|
|
+ 'name': os.path.basename(path),
|
|
|
|
+ 'path': path,
|
|
|
|
+ 'process': process
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def config():
|
|
|
|
+ if hasattr(config, '_handle'):
|
|
|
|
+ return config._handle
|
|
|
|
|
|
-def main(args):
|
|
|
|
with pushd('etc/backup.d'):
|
|
with pushd('etc/backup.d'):
|
|
with open("backup.yml") as f:
|
|
with open("backup.yml") as f:
|
|
- main.config = yaml.load(f)
|
|
|
|
|
|
+ config._handle = yaml.load(f)
|
|
|
|
+
|
|
|
|
+ return config._handle
|
|
|
|
+
|
|
|
|
|
|
- sources = {}
|
|
|
|
- for source in main.config['sources']:
|
|
|
|
|
|
+def sources():
|
|
|
|
+ if hasattr(sources, '_handle'):
|
|
|
|
+ return sources._handle
|
|
|
|
+
|
|
|
|
+ sources._handle = {}
|
|
|
|
+ with pushd('etc/backup.d'):
|
|
|
|
+ for source in config()['sources']:
|
|
source = os.path.realpath(source)
|
|
source = os.path.realpath(source)
|
|
for path in glob('{}/*.yml'.format(source)):
|
|
for path in glob('{}/*.yml'.format(source)):
|
|
path = os.path.realpath(path)
|
|
path = os.path.realpath(path)
|
|
with pushd(os.path.dirname(path)), open(path) as f:
|
|
with pushd(os.path.dirname(path)), open(path) as f:
|
|
data = yaml.load(f)
|
|
data = yaml.load(f)
|
|
if "active" in data and data["active"]:
|
|
if "active" in data and data["active"]:
|
|
|
|
+ data['path'] = path
|
|
if "depends" not in data:
|
|
if "depends" not in data:
|
|
data["depends"] = []
|
|
data["depends"] = []
|
|
|
|
|
|
@@ -102,19 +148,69 @@ def main(args):
|
|
data["depends"][i] = os.path.realpath(
|
|
data["depends"][i] = os.path.realpath(
|
|
'{}.yml'.format(data["depends"][i]))
|
|
'{}.yml'.format(data["depends"][i]))
|
|
|
|
|
|
- sources[path] = data
|
|
|
|
|
|
+ sources._handle[path] = data
|
|
|
|
+
|
|
|
|
+ return sources._handle
|
|
|
|
+
|
|
|
|
|
|
- main.sources = sources
|
|
|
|
- main.deptree = deptree(sources)
|
|
|
|
|
|
+def logStatus(tasks):
|
|
errors = []
|
|
errors = []
|
|
- for name in main.deptree:
|
|
|
|
- try:
|
|
|
|
- backup(name)
|
|
|
|
|
|
+ t = term()
|
|
|
|
+ for task in list(tasks):
|
|
|
|
+ print(task['name'], end=': ')
|
|
|
|
+ code = 1 if isinstance(
|
|
|
|
+ task['process'], BackupException) else task['process'].poll()
|
|
|
|
+
|
|
|
|
+ if code is None:
|
|
|
|
+ print(t.blue('...'))
|
|
|
|
+ continue
|
|
|
|
+
|
|
|
|
+ if code:
|
|
|
|
+ print(t.red('fail'))
|
|
|
|
+
|
|
|
|
+ else:
|
|
|
|
+ print(t.green('ok'))
|
|
|
|
+
|
|
|
|
+ if task['active']:
|
|
|
|
+ if code:
|
|
|
|
+ errors.append(task['name'])
|
|
|
|
+
|
|
|
|
+ task['active'] = False
|
|
|
|
+ status(task['path'], not code)
|
|
|
|
+
|
|
|
|
+ return errors
|
|
|
|
|
|
- except BackupException as ex:
|
|
|
|
- print(ex)
|
|
|
|
- errors.append(ex)
|
|
|
|
|
|
|
|
|
|
+def main(args):
|
|
|
|
+ engine = config()["engine"]
|
|
|
|
+ if engine not in ("rdiff-backup"):
|
|
|
|
+ raise BackupException('Unknown backup engine: {}'.format(engine))
|
|
|
|
+
|
|
|
|
+ tasks = []
|
|
|
|
+ errors = []
|
|
|
|
+ maxconcurrent = config()["maxconcurrent"]
|
|
|
|
+ tree = deptree(sources())
|
|
|
|
+ t = term()
|
|
|
|
+ with t.fullscreen(), t.hidden_cursor():
|
|
|
|
+ while len(tree):
|
|
|
|
+ if len([x for x in tasks if x['active']]) < maxconcurrent:
|
|
|
|
+ path = tree.pop(0)
|
|
|
|
+ source = sources()[path]
|
|
|
|
+ if not [x for x in source["depends"] if status(x) is None]:
|
|
|
|
+ tasks.append(backup(source))
|
|
|
|
+
|
|
|
|
+ else:
|
|
|
|
+ tree.append(path)
|
|
|
|
+
|
|
|
|
+ print(t.clear + t.move(0, 0), end='')
|
|
|
|
+ errors += logStatus(tasks)
|
|
|
|
+
|
|
|
|
+ while len([x for x in tasks if x['active']]):
|
|
|
|
+ print(t.clear + t.move(0, 0), end='')
|
|
|
|
+ errors += logStatus(tasks)
|
|
|
|
+ time.sleep(1)
|
|
|
|
+
|
|
|
|
+ errors += logStatus(tasks)
|
|
if errors:
|
|
if errors:
|
|
raise BackupException("At least one backup failed")
|
|
raise BackupException("At least one backup failed")
|
|
|
|
|