mirror of
https://github.com/wekan/wekan.git
synced 2026-01-29 20:56:09 +01:00
Updated Backup (markdown)
parent
45a77e328f
commit
423a4d2d2d
1 changed files with 207 additions and 1 deletions
208
Backup.md
208
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)
|
||||
[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 <path_to_configfile> \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()
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue