Commit 76b5e25f authored by Nicolas Wavrant's avatar Nicolas Wavrant

resilient: port the export script of the webrunner to python

parent c4d2977f
......@@ -53,6 +53,7 @@ setup(name=name,
'dnspython',
'requests',
'jsonschema',
'zc.buildout',
] + additional_install_requires,
extras_require = {
'lampconfigure': ["mysqlclient"], #needed for MySQL Database access
......@@ -108,6 +109,7 @@ setup(name=name,
'pubsubserver = slapos.pubsub:main',
'qemu-qmp-client = slapos.qemuqmpclient:main',
'rdiffbackup.genstatrss = slapos.resilient.rdiffBackupStat2RSS:main',
'runner-exporter = slapos.resilient.runner_exporter:runExport',
'securedelete = slapos.securedelete:main',
'slapos-kill = slapos.systool:kill',
'slaprunnertest = slapos.runner.runnertest:main',
......
from __future__ import print_function
import argparse
import errno
import glob
import os
import re
import shutil
import subprocess
import sys
import time
from contextlib import contextmanager
from datetime import datetime
from hashlib import sha256
from zc.buildout.configparser import parse
os.environ['LC_ALL'] = 'C'
os.umask(0o77)
def read_file_by_chunk(path, chunk_size=1024 * 1024):
with open(path, 'rb') as f:
chunk = f.read(chunk_size)
while chunk:
yield chunk
chunk = f.read(chunk_size)
@contextmanager
def CwdContextManager(path):
"""
Context Manager for executing code in a given directory.
There is no need to provide fallback or basic checks
in this code, as these checkes should exist in the code
invoking this Context Manager.
If someone needs to add checks here, I'm pretty sure
it means that they are trying to hide legitimate errors.
See tests to see examples of invokation
"""
old_path = os.getcwd()
try:
os.chdir(path)
yield
finally:
os.chdir(old_path)
def parseArgumentList():
parser = argparse.ArgumentParser()
parser.add_argument('--srv-path', required=True)
parser.add_argument('--backup-path', required=True)
parser.add_argument('--etc-path', required=True)
parser.add_argument('--rsync-binary', default='rsync')
parser.add_argument('--backup-wait-time', type=int, required=True)
parser.add_argument('-n', action='store_true', dest='dry', default=False)
return parser.parse_args()
def rsync(rsync_binary, source, destination, extra_args=None, dry=False):
arg_list = [
rsync_binary,
'-rlptgov',
'--stats',
'--safe-links',
'--ignore-missing-args',
'--delete',
'--delete-excluded'
]
if isinstance(extra_args, list):
arg_list.extend(extra_args)
if isinstance(source, list):
arg_list.extend(source)
else:
arg_list.append(source)
arg_list.append(destination)
if dry:
print('DEBUG:', arg_list)
return
try:
print(subprocess.check_output(arg_list))
except subprocess.CalledProcessError as e:
# All rsync errors are not to be considered as errors
allowed_error_message_list = \
'^(file has vanished: |rsync warning: some files vanished before they could be transferred)'
if e.returncode != 24 or \
re.search(allowed_error_message_regex, e.output, re.M) is None:
raise
def getExcludePathList(path):
excluded_path_list = [
"*.sock",
"*.socket",
"*.pid",
".installed*.cfg",
]
def append_relative(path_list):
for p in path_list:
p = p.strip()
if p:
excluded_path_list.append(os.path.relpath(p, path))
for partition in glob.glob(os.path.join(path, "instance", "slappart*")):
if not os.path.isdir(partition):
continue
with CwdContextManager(partition):
try:
with open("srv/exporter.exclude") as f:
exclude = f.readlines()
except IOError as e:
if e.errno != errno.ENOENT:
raise
else:
append_relative(exclude)
for installed in glob.glob(".installed*.cfg"):
try:
with open(installed) as f:
installed = parse(f, installed)
except IOError as e:
if e.errno != errno.ENOENT:
raise
else:
for section in installed.itervalues():
append_relative(section.get(
'__buildout_installed__', '').splitlines())
return excluded_path_list
def getSha256Sum(file_path):
hash_sum = sha256()
for chunk in read_file_by_chunk(file_path):
hash_sum.update(chunk)
return hash_sum.hexdigest()
def synchroniseRunnerConfigurationDirectory(config, backup_path):
if not os.path.exists(backup_path):
os.makedirs(backup_path)
file_list = ['config.json']
for hidden_file in os.listdir('.'):
if hidden_file[0] == '.':
file_list.append(hidden_file)
rsync(config.rsync_binary, file_list, backup_path, dry=config.dry)
def synchroniseRunnerWorkingDirectory(config, backup_path):
file_list = []
exclude_list = []
if os.path.isdir('instance'):
file_list.append('instance')
exclude_list = getExcludePathList('.')
# XXX: proxy.db should be properly dumped to leverage its
# atomic properties
for file in ('project', 'public', 'proxy.db'):
if os.path.exists(file):
file_list.append(file)
if file_list:
rsync(
config.rsync_binary, file_list, backup_path,
["--exclude={}".format(x) for x in exclude_list],
dry=config.dry
)
def getSlappartSignatureMethodDict():
slappart_signature_method_dict = {}
for partition in glob.glob("./instance/slappart*"):
if os.path.isdir(partition):
script_path = os.path.join(partition, 'srv', '.backup_identity_script')
if os.path.exists(script_path):
slappart_signature_method_dict[partition] = script_path
return slappart_signature_method_dict
def writeSignatureFile(slappart_signature_method_dict, runner_working_path, signature_file_path='backup.signature'):
special_slappart_list = slappart_signature_method_dict.keys()
signature_list = []
for dirpath, dirname_list, filename_list in os.walk('.'):
if dirpath == '.':
continue
# Find if special signature function should be applied
for special_slappart in special_slappart_list:
if dirpath.startswith(special_slappart):
signature_function = lambda x: subprocess.check_output([
os.path.join(runner_working_path, slappart_signature_method_dict[special_slappart]),
x
])
break
else:
signature_function = getSha256Sum
# Calculate all signatures
for filename in filename_list:
file_path = os.path.join(dirpath, filename)
signature_list.append("%s %s" % (signature_function(file_path), file_path))
# Write the signatures in file
with open(signature_file_path, 'w+') as signature_file:
signature_file.write("\n".join(sorted(signature_list)))
def backupFilesWereModifiedDuringExport(export_start_date):
export_time = time.time() - export_start_date
return bool(
subprocess.check_output((
'find', '-cmin', str(export_time / 60.), '-type', 'f', '-path', '*/srv/backup/*'
))
)
def runExport():
export_start_date = int(time.time())
print(datetime.fromtimestamp(export_start_date).isoformat())
args = parseArgumentList()
def _rsync(*params):
return rsync(args.rsync_binary, *params, dry=args.dry)
runner_working_path = os.path.join(args.srv_path, 'runner')
backup_runner_path = os.path.join(args.backup_path, 'runner')
# Synchronise runner's etc directory
with CwdContextManager(args.etc_path):
with open('.resilient-timestamp', 'w') as f:
f.write(str(export_start_date))
# "+ '/'" is mandatory otherwise rsyncing the etc directory
# will create in the backup_etc_path only a file called etc
backup_etc_path = os.path.join(args.backup_path, 'etc') + '/'
synchroniseRunnerConfigurationDirectory(args, backup_etc_path)
# Synchronise runner's working directory
# and aggregate signature functions as we are here
with CwdContextManager(runner_working_path):
synchroniseRunnerWorkingDirectory(args, backup_runner_path)
slappart_signature_method_dict = getSlappartSignatureMethodDict()
# Calculate signature of synchronised files
with CwdContextManager(args.backup_path):
writeSignatureFile(slappart_signature_method_dict, runner_working_path)
# BBB: clean software folder if it was synchronized
# in an old instance
backup_software_path = os.path.join(backup_runner_path, 'software')
if os.path.isdir(backup_software_path):
shutil.rmtree(backup_software_path)
# Wait a little to increase the probability to detect an ongoing backup.
time.sleep(10)
# Check that export didn't happen during backup of instances
with CwdContextManager(backup_runner_path):
if backupFilesWereModifiedDuringExport(export_start_date):
print("ERROR: Some backups are not consistent, exporter should be re-run."
" Let's sleep %s minutes, to let the backup end..." % backup_wait_time)
time.sleep(backup_wait_time * 60)
system.exit(1)
\ No newline at end of file
import mock
import os
import shutil
import time
import unittest
from slapos.resilient import runner_exporter
from StringIO import StringIO
tested_instance_cfg = """[buildout]
installed_develop_eggs =
parts = folders hello-nicolas hello-rafael exclude
[folders]
__buildout_installed__ =
__buildout_signature__ = wcwidth-0.1.7-py2.7.egg contextlib2-0.5.5-py2.7.egg ...
etc = /some/prefix/slappart18/test/etc
home = /srv/slapgrid/slappart18/test
recipe = slapos.cookbook:mkdirectory
srv = /some/prefix/slappart18/test/srv
[hello-nicolas]
__buildout_installed__ = ./instance/slappart0/etc/nicolas.txt
__buildout_signature__ = MarkupSafe-1.0-py2.7-linux-x86_64.egg Jinja2-2.10-py2.7.egg zc.buildout-2.12.2-py2.7.egg slapos.recipe.template-4.3-py2.7.egg setuptools-40.4.3-py2.7.egg
mode = 0644
name = Nicolas
output = /some/prefix/slappart18/test/etc/nicolas.txt
recipe = slapos.recipe.template
[hello-rafael]
__buildout_installed__ = ./instance/slappart0/etc//rafael.txt
__buildout_signature__ = MarkupSafe-1.0-py2.7-linux-x86_64.egg Jinja2-2.10-py2.7.egg zc.buildout-2.12.2-py2.7.egg slapos.recipe.template-4.3-py2.7.egg setuptools-40.4.3-py2.7.egg
name = Rafael
output = /some/prefix/slappart18/test/etc/rafael.txt
recipe = slapos.recipe.template
[exclude]
__buildout_installed__ = srv/exporter.exclude
__buildout_signature__ = MarkupSafe-1.0-py2.7-linux-x86_64.egg Jinja2-2.10-py2.7.egg zc.buildout-2.12.2-py2.7.egg slapos.recipe.template-4.3-py2.7.egg setuptools-40.4.3-py2.7.egg
recipe = slapos.recipe.template:jinja2
rendered = /some/prefix/slappart18/test/srv/exporter.exclude
template = inline:
srv/backup/**"""
class Config():
pass
class TestRunnerExporter(unittest.TestCase):
def setUp(self):
if not os.path.exists('test_folder'):
os.mkdir('test_folder')
os.chdir('test_folder')
def tearDown(self):
if os.path.basename(os.getcwd()) == 'test_folder':
os.chdir('..')
shutil.rmtree('test_folder')
elif 'test_folder' in os.listdir('.'):
shutil.rmtree('test_folder')
def _createFile(self, path, content=''):
with open(path, 'w') as f:
f.write(content)
def _createExecutableFile(self, path, content=''):
self._createFile(path, content)
os.chmod(path, 0700)
def _setUpFakeInstanceFolder(self):
self._createFile('proxy.db')
os.makedirs('project')
os.makedirs('public')
"""Create data mirroring tested_instance_cfg"""
os.makedirs('instance/slappart0/etc')
os.makedirs('instance/slappart0/srv/backup')
os.makedirs('instance/slappart1/etc')
os.makedirs('instance/slappart1/srv/backup')
self._createFile('instance/slappart0/.installed.cfg', tested_instance_cfg)
self._createFile('instance/slappart0/srv/backup/data.dat',
'all my fortune lays on this secret !')
self._createFile('instance/slappart0/srv/exporter.exclude',
'srv/backup/**')
self._createFile('instance/slappart0/etc/config.json')
self._createFile('instance/slappart0/etc/.parameters.xml')
self._createFile('instance/slappart0/etc/.project',
'workspace/slapos-dev/software/erp5')
self._createExecutableFile(
'instance/slappart1/srv/.backup_identity_script',
'#!/bin/bash\nmd5sum $1 | cut -d " " -f 1 | tr -d "\n"'
)
def test_CwdContextManager(self):
os.makedirs('a/b')
with runner_exporter.CwdContextManager('a'):
self.assertEqual(os.listdir('.'), ['b'])
os.mkdir('c')
self.assertEqual(os.listdir('.'), ['a'])
self.assertEqual(sorted(os.listdir('a')), ['b', 'c'])
def test_getExcludePathList(self):
self._setUpFakeInstanceFolder()
self.assertEqual(
sorted(runner_exporter.getExcludePathList('.')),
[
'*.pid',
'*.sock',
'*.socket',
'.installed*.cfg',
'instance/slappart0/etc/nicolas.txt',
'instance/slappart0/etc/rafael.txt',
'srv/backup/**',
'srv/exporter.exclude',
]
)
@mock.patch('subprocess.check_output')
def test_synchroniseRunnerConfigurationDirectory(self, check_output_mock):
self._setUpFakeInstanceFolder()
config = Config()
config.rsync_binary = 'rsync'
config.dry = False
with runner_exporter.CwdContextManager('instance/slappart0/etc'):
runner_exporter.synchroniseRunnerConfigurationDirectory(
config, 'backup/runner/etc/'
)
self.assertEqual(check_output_mock.call_count, 1)
check_output_mock.assert_any_call(
['rsync', '-rlptgov', '--stats', '--safe-links', '--ignore-missing-args', '--delete', '--delete-excluded', 'config.json', '.parameters.xml', '.project', 'backup/runner/etc/']
)
@mock.patch('subprocess.check_output')
def test_synchroniseRunnerWorkingDirectory(self, check_output_mock):
self._setUpFakeInstanceFolder()
config = Config()
config.rsync_binary = 'rsync'
config.dry = False
with runner_exporter.CwdContextManager('.'):
runner_exporter.synchroniseRunnerWorkingDirectory(
config, 'backup/runner/runner'
)
self.assertEqual(check_output_mock.call_count, 1)
check_output_mock.assert_any_call(
['rsync', '-rlptgov', '--stats', '--safe-links', '--ignore-missing-args', '--delete', '--delete-excluded', '--exclude=*.sock', '--exclude=*.socket', '--exclude=*.pid', '--exclude=.installed*.cfg', '--exclude=srv/backup/**', '--exclude=instance/slappart0/etc/nicolas.txt', '--exclude=instance/slappart0/etc/rafael.txt', '--exclude=srv/exporter.exclude', 'instance', 'project', 'public', 'proxy.db', 'backup/runner/runner']
)
def test_getSlappartSignatureMethodDict(self):
self._setUpFakeInstanceFolder()
slappart_signature_method_dict = runner_exporter.getSlappartSignatureMethodDict()
self.assertEqual(
slappart_signature_method_dict,
{
'./instance/slappart1': './instance/slappart1/srv/.backup_identity_script',
}
)
def test_writeSignatureFile(self):
self._setUpFakeInstanceFolder()
os.makedirs('backup/instance/etc')
os.makedirs('backup/instance/slappart0')
os.makedirs('backup/instance/slappart1')
self._createFile('backup/instance/etc/.project', 'workspace/slapos-dev/software/erp5')
self._createFile('backup/instance/slappart0/data', 'hello')
self._createFile('backup/instance/slappart1/data', 'world')
slappart_signature_method_dict = {
'./instance/slappart1': './instance/slappart1/srv/.backup_identity_script',
}
with runner_exporter.CwdContextManager('backup'):
runner_exporter.writeSignatureFile(slappart_signature_method_dict, '..', signature_file_path='backup.signature')
with open('backup.signature', 'r') as f:
signature_file_content = f.read()
# Slappart1 is using md5sum as signature, others are using sha256sum (default)
self.assertEqual(signature_file_content, """2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 ./instance/slappart0/data
49b74873d57ff0307b7c9364e2fe2a3876d8722fbe7ce3a6f1438d47647a86f4 ./instance/etc/.project
7d793037a0760186574b0282f2f435e7 ./instance/slappart1/data""")
def test_backupFilesWereModifiedDuringExport(self):
self._setUpFakeInstanceFolder()
with runner_exporter.CwdContextManager('instance'):
self.assertTrue(runner_exporter.backupFilesWereModifiedDuringExport(time.time() - 5))
time.sleep(2)
self.assertFalse(runner_exporter.backupFilesWereModifiedDuringExport(time.time() - 1))
self._createFile('slappart1/srv/backup/bakckup.data', 'my backup')
self.assertTrue(runner_exporter.backupFilesWereModifiedDuringExport(time.time() - 1))
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment