From 423a4d2d2de6d8970d274fac36a4c904b456a8ac Mon Sep 17 00:00:00 2001 From: DrGraypFroot <33982978+DrGraypFroot@users.noreply.github.com> Date: Thu, 5 Jul 2018 13:10:36 +0200 Subject: [PATCH] Updated Backup (markdown) --- Backup.md | 208 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 207 insertions(+), 1 deletion(-) diff --git a/Backup.md b/Backup.md index c6489eb..fea029c 100644 --- a/Backup.md +++ b/Backup.md @@ -31,4 +31,210 @@ echo "Backup is also archived to .zip file backups/${now}.zip" Download Wekan grain with arrow down download button to .zip file. You can restore it later. -[Export data from Wekan Sandstorm grain .zip file](https://github.com/wekan/wekan/wiki/Export-from-Wekan-Sandstorm-grain-.zip-file) \ No newline at end of file +[Export data from Wekan Sandstorm grain .zip file](https://github.com/wekan/wekan/wiki/Export-from-Wekan-Sandstorm-grain-.zip-file) + +## Python Backup Script for Wekan Docker environment +```py +#!/usr/bin/env python3 + +import os +import sys +import subprocess +import configparser +import time +import datetime +import smtplib +import traceback +import logging +import gzip +import yaml +import abc +import shutil + + +class Config: + __conf = { + "db_name" : '', + "retention" : '', + "dump_path" : '', + "container" : '', + "start_date" : time.strftime('%Y%m%d-%H%M%S'), + "curdate" : time.strftime('%Y-%m-%d %X') + } + + __setters = ["db_name", "retention", "dump_path", "container"] + + @staticmethod + def config(name): + return Config.__conf[name] + + @staticmethod + def set(name, value): + if name in Config.__setters: + Config.__conf[name] = value + else: + raise NameError("Name not accepted in set() method") + + +class Dbms(metaclass=abc.ABCMeta): + def __init__(self, db_name, container, dump_path): + self._database = db_name + self._dumpfile = os.path.join(dump_path, self.getdumpfilename()) + self._container = container + self._compression = CompressionGzip() + self._dump_path = dump_path + + @abc.abstractmethod + def dump(self): + pass + + def getdumpfilename(self): + return 'dump-{}-{}'.format(self._database, Config.config('start_date')) + +class DbmsMongodb(Dbms): + def dump(self): + call = 'docker exec {} bash -c "mongodump -d {} -o /dump/"'.format(self._container, self._database) + try: + output = subprocess.check_output(call, universal_newlines=True, shell=True) + except subprocess.CalledProcessError as e: + raise Exception('Mongodump failed due to the following Error: {}'.format(e)) + call = 'docker cp {}:/dump {}'.format(self._container, self._dump_path) + try: + output = subprocess.check_output(call, universal_newlines=True, shell=True) + except subprocess.CalledProcessError as e: + raise Exception('Pulling dump from container failed due to the following Error: {}'.format(e)) + call = 'tar -C {}/dump -cf {} .'.format(self._dump_path, self._dumpfile + '.tar') + try: + output = subprocess.check_output(call, universal_newlines=True, shell=True) + except subprocess.CalledProcessError as e: + raise Exception('Creating .tar-ball failed due to the following Error: {}'.format(e)) + self._compression.setFilename(self._dumpfile + '.tar') + self._compression.compress() + shutil.rmtree(self._dump_path + '/dump') + + +class Compression(metaclass=abc.ABCMeta): + def __init__(self): + self._filename = '' + + def setFilename(self, filename): + self._filename = filename + + @abc.abstractmethod + def compress(self): + pass + + +class CompressionGzip(Compression): + def compress(self): + call = 'gzip {}'.format(self._filename) + try: + output = subprocess.check_output(call, universal_newlines=True, shell=True) + except subprocess.CalledProcessError as e: + raise Exception('Compression failed due to the following Error: {}'.format(e)) + else: + #print('Successfully created dump: {}'.format(self._filename + '.gz')) + pass + +#DB-Config-File-Checker: checks if the file passed in the function call is accessible, if not, raise exception +def checkcfg(conf): + if(os.path.isfile(conf)): + config_file = conf + else: + raise Exception("Specified Config File doesn't exist or insufficient access rights") + checkpermission(config_file) + return config_file + +def checkpath(path): + if not os.path.exists(path): + os.makedirs(path) + +def checkpermission(cfg): + if (os.stat(cfg).st_uid != 0): + raise Exception("Config file must be owned by user root!") + elif (os.stat(cfg).st_gid != 0): + raise Exception("Config file must be owned by group root!") + else: + accessmask = oct(os.stat(cfg).st_mode)[-3:] + if accessmask == '600' or accessmask == '700': + pass + else: + raise Exception("Root must have read and write access to config file, all other users mustn't be allowed. Current Access Mask: {} but it should be 600 or 700".format(accessmask)) + pass + + + +def parseInput(): + sys.tracebacklimit = None + #check if the script has been called with one argument --> The db-specific config file + if len(sys.argv) != 2: + raise Exception("usage: wekandump.py \n Please specify the path to a configfile") + + #Send the specified db-config file to the Configuration-Checker + config_file = checkcfg(sys.argv[1]) + + #Now that both of the config-files have been checked, finally open them + with open(sys.argv[1], 'r') as cfgfile: + cfg = yaml.safe_load(cfgfile) + + + #Set some vars using data from the config-file + Config.set('db_name', cfg['dumps']['database']) + Config.set('retention', cfg['dumps']['retention']) + Config.set('dump_path', cfg['dumps']['path']) + Config.set('container', cfg['dumps']['container']) + + checkpath(Config.config('dump_path')) + + cfgfile.close + +def dumpcompress(): + dbms = DbmsMongodb(Config.config('db_name'), Config.config('container'), Config.config('dump_path')) + dbms.dump() + + +def getcrtime(item): + call = 'stat -c %y {}'.format(item) + output = subprocess.check_output(call, universal_newlines=True, shell=True) + output = output.rstrip() + crtime = datetime.datetime.fromtimestamp(os.stat(item).st_mtime) + return crtime + +def housekeep(): + #get all filenames located in the dump-directory + call = 'ls {}'.format(Config.config('dump_path')) + output = subprocess.check_output(call, universal_newlines=True, shell=True) + output = output.rstrip() + dumps = output.split('\n') + #now that we have a list with the filenames of the files in the dump-folder, every filename is handled seperately + for item in dumps: + item = os.path.join(Config.config('dump_path'), item) + crtime = getcrtime(item) + curtime = datetime.datetime.strptime(Config.config('curdate'), '%Y-%m-%d %X') + if (curtime-crtime).days >= Config.config('retention'): + try: + os.remove(item) + except: + try: + shutil.rmtree(item) + except: + raise Exception('Housekeep: failed to delete the dump {}'.format(item)) + else: + #print("Housekeep: Deleted dump: {}, it has reached the age of {} days. (Retention is {} days.)".format(item, curtime-crtime, Config.config('retention'))) + pass + else: + #print("Housekeep: Deleted dump: {}, it has reached the age of {} days. (Retention is {} days.)".format(item, curtime-crtime, Config.config('retention'))) + pass + else: + #print("Housekeep: Dump {} was kept since it is only {} hours old. (Retention is {} days.)".format(item, curtime-crtime, Config.config('retention'))) + pass + + +def main(): + parseInput() + dumpcompress() + housekeep() + +if __name__ == "__main__": + main() +``` \ No newline at end of file