diff --git a/Backup.md b/Backup.md index 9ff8764..7e5bb4d 100644 --- a/Backup.md +++ b/Backup.md @@ -34,240 +34,5 @@ Download Wekan grain with arrow down download button to .zip file. You can resto [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 -This backup script is meant to be executed via cronjob. -Example crontab (Backup daily at 18:30): -```sh -30 18 * * * /usr/local/sbin/wekandump/wekandump.py /usr/local/sbin/wekandump/wekandump.yml > /dev/null 2>&1 -``` -The script automatically deletes backups that are older then the specified value of retention in the yaml file. - -The output of the script contains the result of the mongodump as you would see it when dumping manually. Otherwise it only outputs something in case of failure. To enable informative output even in case of success, remove all the # before the lines containing "print ...." - -```py -#!/usr/bin/env python3 - - -# vim: set fileencoding=utf-8 : -#various imports -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 - -#define config class with all required config-parameters -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') - } - - #define the parameters that can be set through config file - __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") - -#define db class and assign vars -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 - - #function to define filename of the backup-archive - def getdumpfilename(self): - return 'dump-{}-{}'.format(self._database, Config.config('start_date')) - -#class for the mongodb backup -class DbmsMongodb(Dbms): - def dump(self): - #command for creating the backup - 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)) - #command for copying the backup to the host system - 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)) - #tar the backup-folder - 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 for compressing the backup with gzip (this can be interchanged with xz, bzip etc.) -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) - -#this checks the permissions of the config file (you can leave this part out, just required because of corporate environment) -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 the config-file have been checked, finally open it - 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() - - -#created by DrGraypFroot -``` - -Yaml Config file (Specify the database name, retention in days, backup target path and name of your mongodb-docker-container: - -```yml -dumps: - database: wekan #name of the database - retention: 14 #number of days of retention - path: /var/lib/wekandump/ #name of the target directory for dumps - container: wekan-db #name of the docker-container -``` \ No newline at end of file +[Use Python to backup your Mongo Data, with automatic cleanup](https://github.com/wekan/wekan/wiki/Python-Backup-Script-for-Wekan-Docker-environment) \ No newline at end of file