Commit 9c59c5fc authored by Xavier Thompson's avatar Xavier Thompson

software/theia: Fix resilient signature checking

parent 5763522b
...@@ -35,15 +35,15 @@ md5sum = d78a9f885bdebf6720197209e0c21aa0 ...@@ -35,15 +35,15 @@ md5sum = d78a9f885bdebf6720197209e0c21aa0
[theia-common] [theia-common]
_update_hash_filename_ = theia_common.py _update_hash_filename_ = theia_common.py
md5sum = 38eba0fb605953677b5a2a2e686a66a2 md5sum = 6a25c6a7f1beb27232a3c9acd8a76500
[theia-export] [theia-export]
_update_hash_filename_ = theia_export.py _update_hash_filename_ = theia_export.py
md5sum = d338b2d3ba1dcd7a919c0baf51dd11da md5sum = e2f6c483cce09f87ab1e63ae8be0daf4
[theia-import] [theia-import]
_update_hash_filename_ = theia_import.py _update_hash_filename_ = theia_import.py
md5sum = 3580f1eec1099bebd550ba1eacd9c116 md5sum = 5dea99b0106cccba65f8ae90d110e111
[yarn.lock] [yarn.lock]
_update_hash_filename_ = yarn.lock _update_hash_filename_ = yarn.lock
......
...@@ -81,7 +81,6 @@ def remove(path): ...@@ -81,7 +81,6 @@ def remove(path):
def parse_installed(partition): def parse_installed(partition):
paths = [] paths = []
custom_script = os.path.join(partition, 'srv', '.backup_identity_script')
for cfg in glob.glob(os.path.join(partition, '.installed*.cfg')): for cfg in glob.glob(os.path.join(partition, '.installed*.cfg')):
try: try:
with open(cfg) as f: with open(cfg) as f:
...@@ -93,7 +92,7 @@ def parse_installed(partition): ...@@ -93,7 +92,7 @@ def parse_installed(partition):
for section in six.itervalues(installed_cfg): for section in six.itervalues(installed_cfg):
for p in section.get('__buildout_installed__', '').splitlines(): for p in section.get('__buildout_installed__', '').splitlines():
p = p.strip() p = p.strip()
if p and p != custom_script: if p:
paths.append(p) paths.append(p)
return paths return paths
...@@ -108,31 +107,44 @@ def sha256sum(file_path, chunk_size=1024 * 1024): ...@@ -108,31 +107,44 @@ def sha256sum(file_path, chunk_size=1024 * 1024):
return sha256.hexdigest() return sha256.hexdigest()
def hashwalk(backup_dir, mirror_partitions): def fast_hashwalk(root_dir):
scripts = {} for dirpath, dirnames, filenames in os.walk(root_dir):
for p in mirror_partitions:
script_path = os.path.join(p, 'srv', '.backup_identity_script')
if os.path.exists(script_path):
scripts[os.path.abspath(p)] = script_path
for dirpath, dirnames, filenames in os.walk(backup_dir):
filenames.sort()
for f in filenames: for f in filenames:
filepath = os.path.join(dirpath, f) filepath = os.path.join(dirpath, f)
if os.path.isfile(filepath): if os.path.isfile(filepath):
displaypath = os.path.relpath(filepath, start=backup_dir) displaypath = os.path.relpath(filepath, start=root_dir)
yield '%s %s' % (sha256sum(filepath), displaypath) yield '%s %s' % (sha256sum(filepath), displaypath)
remaining_dirnames = []
for subdir in dirnames:
subdirpath = os.path.abspath(os.path.join(dirpath, subdir)) def exclude_hashwalk(root_dir, instance_dir):
custom_hashscript = scripts.get(subdirpath) root_dir = os.path.abspath(root_dir)
if custom_hashscript: instance_dir = os.path.abspath(instance_dir)
print('Using custom signature script %s' % custom_hashscript) for dirpath, dirnames, filenames in os.walk(root_dir):
for s in hashcustom(subdirpath, backup_dir, custom_hashscript): for f in filenames:
yield s filepath = os.path.join(dirpath, f)
else: if os.path.isfile(filepath):
remaining_dirnames.append(subdir) displaypath = os.path.relpath(filepath, start=root_dir)
remaining_dirnames.sort() yield '%s %s' % (sha256sum(filepath), displaypath)
dirnames[:] = remaining_dirnames if dirpath == instance_dir:
remaining_dirs = []
for d in dirnames:
if not d.startswith('slappart'):
remaining_dirs.append(d)
dirnames[:] = remaining_dirs
def hashwalk(root_dir, instance_dir=None):
if instance_dir and not os.path.relpath(
instance_dir, start=root_dir).startswith(os.pardir):
return exclude_hashwalk(root_dir, instance_dir)
return fast_hashwalk(root_dir)
def hashscript(partition):
script = os.path.join(partition, 'srv', '.backup_identity_script')
if os.path.exists(script):
return script
return None
@contextlib.contextmanager @contextlib.contextmanager
...@@ -145,10 +157,11 @@ def cwd(path): ...@@ -145,10 +157,11 @@ def cwd(path):
os.chdir(old_path) os.chdir(old_path)
def hashcustom(mirrordir, backup_dir, custom_hashscript): def hashcustom(partition, script):
workingdir = os.path.join(mirrordir, os.pardir, os.pardir, os.pardir) workingdir = os.path.join(partition, os.pardir, os.pardir, os.pardir)
with cwd(os.path.abspath(workingdir)): with cwd(os.path.abspath(workingdir)):
for dirpath, _, filenames in os.walk(mirrordir): for dirpath, dirnames, filenames in os.walk(partition):
dirnames.sort()
filepaths = [] filepaths = []
for f in filenames: for f in filenames:
path = os.path.join(dirpath, f) path = os.path.join(dirpath, f)
...@@ -157,16 +170,16 @@ def hashcustom(mirrordir, backup_dir, custom_hashscript): ...@@ -157,16 +170,16 @@ def hashcustom(mirrordir, backup_dir, custom_hashscript):
if not filepaths: if not filepaths:
continue continue
hashprocess = sp.Popen( hashprocess = sp.Popen(
custom_hashscript, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.PIPE) script, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.PIPE)
out, err = hashprocess.communicate(str2bytes('\0'.join(filepaths))) out, err = hashprocess.communicate(str2bytes('\0'.join(filepaths)))
if hashprocess.returncode != 0: if hashprocess.returncode != 0:
template = "Custom signature script %s failed on inputs:\n%s" template = "Custom signature script %s failed on inputs:\n%s"
msg = template % (custom_hashscript, '\n'.join(filepaths)) msg = template % (script, '\n'.join(filepaths))
msg += "\nwith stdout:\n%s" % bytes2str(out) msg += "\nwith stdout:\n%s" % bytes2str(out)
msg += "\nand stderr:\n%s" % bytes2str(err) msg += "\nand stderr:\n%s" % bytes2str(err)
raise Exception(msg) raise Exception(msg)
signatures = bytes2str(out).strip('\n').split('\n') signatures = bytes2str(out).strip('\n').split('\n')
signatures.sort() signatures.sort()
displaypath = os.path.relpath(dirpath, start=backup_dir) displaypath = os.path.relpath(dirpath, start=partition)
for s in signatures: for s in signatures:
yield '%s %s/ (custom)' % (s, displaypath) yield '%s %s' % (s, displaypath)
...@@ -55,49 +55,74 @@ class TheiaExport(object): ...@@ -55,49 +55,74 @@ class TheiaExport(object):
self.copytree_partitions_args = {} self.copytree_partitions_args = {}
self.logs = [] self.logs = []
def mirrorpath(self, src): def mirror_path(self, src):
return os.path.abspath(os.path.join( return os.path.abspath(os.path.join(
self.backup_dir, os.path.relpath(src, start=self.root_dir))) self.backup_dir, os.path.relpath(src, start=self.root_dir)))
def backuptree(self, src, exclude=(), extrargs=(), verbosity='-v'): def backup_tree(self, src):
dst = self.mirrorpath(src) return copytree(self.rsync_bin, src, self.mirror_path(src))
return copytree(self.rsync_bin, src, dst, exclude, extrargs, verbosity)
def backupfile(self, src): def backup_file(self, src):
dst = self.mirrorpath(src) return copyfile(src, self.mirror_path(src))
return copyfile(src, dst)
def backupdb(self): def backup_db(self):
copydb(self.sqlite3_bin, self.proxy_db, self.mirrorpath(self.proxy_db)) copydb(self.sqlite3_bin, self.proxy_db, self.mirror_path(self.proxy_db))
def backuppartition(self, partition): def backup_partition(self, partition):
installed = parse_installed(partition) installed = parse_installed(partition)
rules = os.path.join(partition, 'srv', 'exporter.exclude') rules = os.path.join(partition, 'srv', 'exporter.exclude')
extrargs = ('--filter=.-/ ' + rules,) if os.path.exists(rules) else () extrargs = ('--filter=.-/ ' + rules,) if os.path.exists(rules) else ()
self.backuptree(partition, exclude=installed, extrargs=extrargs) dst = self.mirror_path(partition)
self.copytree_partitions_args[partition] = (installed, extrargs) copytree(self.rsync_bin, partition, dst, installed, extrargs)
self.copytree_partitions_args[partition] = (dst, installed, extrargs)
def sign(self, signaturefile): def sign(self, signaturefile, signatures):
remove(signaturefile) remove(signaturefile)
pardir = os.path.abspath(os.path.join(self.backup_dir, os.pardir)) pardir = os.path.abspath(os.path.join(self.backup_dir, os.pardir))
tmpfile = os.path.join(pardir, 'backup.signature.tmp') tmpfile = os.path.join(pardir, os.path.basename(signaturefile) + '.tmp')
mirror_partitions = [self.mirrorpath(p) for p in self.partition_dirs]
with open(tmpfile, 'w') as f: with open(tmpfile, 'w') as f:
for s in hashwalk(self.backup_dir, mirror_partitions): for s in signatures:
f.write(s + '\n') f.write(s + '\n')
os.rename(tmpfile, signaturefile) os.rename(tmpfile, signaturefile)
def checkpartition(self, partition, pattern='/srv/backup/'): def sign_root(self):
installed, extrargs = self.copytree_partitions_args[partition] signaturefile = os.path.join(self.backup_dir, 'backup.signature')
output = self.backuptree( signatures = hashwalk(self.backup_dir, self.mirror_path(self.instance_dir))
self.sign(signaturefile, signatures)
def sign_partition(self, partition):
dst = self.mirror_path(partition)
filename = os.path.basename(partition) + '.backup.signature'
signaturefile = os.path.join(self.backup_dir, filename)
script = hashscript(partition)
if script:
signaturefile += '.custom'
self.sign(signaturefile, hashcustom(dst, script))
else:
self.sign(signaturefile, hashwalk(dst))
def remove_signatures(self):
pattern = os.path.join(self.backup_dir, '*backup.signature*')
signature_files = glob.glob(pattern)
for f in signature_files:
try:
os.remove(f)
except OSError:
pass
def check_partition(self, partition, pattern='/srv/backup/'):
dst, installed, extrargs = self.copytree_partitions_args[partition]
output = copytree(
self.rsync_bin,
partition, partition,
dst,
exclude=installed, exclude=installed,
extrargs=extrargs + ('--dry-run', '--update'), extrargs=extrargs + ('--dry-run', '--update'),
verbosity='--out-format=%n', verbosity='--out-format=%n',
) )
return [path for path in output.splitlines() if pattern in path] return [path for path in output.splitlines() if pattern in path]
def loginfo(self, msg): def log(self, msg):
print(msg) print(msg)
self.logs.append(msg) self.logs.append(msg)
...@@ -126,36 +151,42 @@ class TheiaExport(object): ...@@ -126,36 +151,42 @@ class TheiaExport(object):
with open(timestamp, 'w') as f: with open(timestamp, 'w') as f:
f.write(str(export_start_date)) f.write(str(export_start_date))
self.loginfo('Backup resilient timestamp ' + timestamp) self.remove_signatures()
self.backupfile(timestamp)
self.log('Backup resilient timestamp ' + timestamp)
self.backup_file(timestamp)
for d in self.dirs: for d in self.dirs:
self.loginfo('Backup directory ' + d) self.log('Backup directory ' + d)
self.backuptree(d) self.backup_tree(d)
self.loginfo('Backup slapproxy database') self.log('Backup slapproxy database')
self.backupdb() self.backup_db()
self.loginfo('Backup partitions') self.log('Backup partitions')
for p in self.partition_dirs: for p in self.partition_dirs:
self.backuppartition(p) self.backup_partition(p)
self.loginfo('Compute backup signature') self.log('Compute root backup signature')
self.sign(os.path.join(self.backup_dir, 'backup.signature')) self.sign_root()
self.log('Compute partitions backup signatures')
for p in self.partition_dirs:
self.sign_partition(p)
time.sleep(10) time.sleep(10)
self.loginfo('Check partitions') self.log('Check partitions')
modified = list(itertools.chain.from_iterable( modified = list(itertools.chain.from_iterable(
self.checkpartition(p) for p in self.partition_dirs)) self.check_partition(p) for p in self.partition_dirs))
if modified: if modified:
msg = 'Some files have been modified since the backup started' msg = 'Some files have been modified since the backup started'
self.loginfo(msg + ':') self.log(msg + ':')
self.loginfo('\n'.join(modified)) self.log('\n'.join(modified))
self.loginfo("Let's wait %d minutes and try again" % BACKUP_WAIT) self.log("Let's wait %d minutes and try again" % BACKUP_WAIT)
time.sleep(BACKUP_WAIT * 60) time.sleep(BACKUP_WAIT * 60)
raise Exception(msg) raise Exception(msg)
self.loginfo('Done') self.log('Done')
if __name__ == '__main__': if __name__ == '__main__':
......
...@@ -57,32 +57,32 @@ class TheiaImport(object): ...@@ -57,32 +57,32 @@ class TheiaImport(object):
configp.read(cfg) configp.read(cfg)
self.proxy_db = configp.get('slapproxy', 'database_uri') self.proxy_db = configp.get('slapproxy', 'database_uri')
self.instance_dir = configp.get('slapos', 'instance_root') self.instance_dir = configp.get('slapos', 'instance_root')
mirror_dir = self.mirrorpath(self.instance_dir) mirror_dir = self.mirror_path(self.instance_dir)
partitions = glob.glob(os.path.join(mirror_dir, 'slappart*')) partitions = glob.glob(os.path.join(mirror_dir, 'slappart*'))
self.mirror_partition_dirs = [p for p in partitions if os.path.isdir(p)] self.mirror_partition_dirs = [p for p in partitions if os.path.isdir(p)]
self.logs = [] self.logs = []
def mirrorpath(self, dst): def mirror_path(self, dst):
return os.path.abspath(os.path.join( return os.path.abspath(os.path.join(
self.backup_dir, os.path.relpath(dst, start=self.root_dir))) self.backup_dir, os.path.relpath(dst, start=self.root_dir)))
def dstpath(self, src): def dst_path(self, src):
return os.path.abspath(os.path.join( return os.path.abspath(os.path.join(
self.root_dir, os.path.relpath(src, start=self.backup_dir))) self.root_dir, os.path.relpath(src, start=self.backup_dir)))
def restoretree(self, dst, exclude=(), extrargs=(), verbosity='-v'): def restore_tree(self, dst, exclude=(), extrargs=(), verbosity='-v'):
src = self.mirrorpath(dst) src = self.mirror_path(dst)
return copytree(self.rsync_bin, src, dst, exclude, extrargs, verbosity) return copytree(self.rsync_bin, src, dst, exclude, extrargs, verbosity)
def restorefile(self, dst): def restore_file(self, dst):
src = self.mirrorpath(dst) src = self.mirror_path(dst)
return copyfile(src, dst) return copyfile(src, dst)
def restoredb(self): def restore_db(self):
copydb(self.sqlite3_bin, self.mirrorpath(self.proxy_db), self.proxy_db) copydb(self.sqlite3_bin, self.mirror_path(self.proxy_db), self.proxy_db)
def restorepartition(self, mirror_partition): def restore_partition(self, mirror_partition):
p = self.dstpath(mirror_partition) p = self.dst_path(mirror_partition)
installed = parse_installed(p) if os.path.exists(p) else [] installed = parse_installed(p) if os.path.exists(p) else []
copytree(self.rsync_bin, mirror_partition, p, exclude=installed) copytree(self.rsync_bin, mirror_partition, p, exclude=installed)
...@@ -97,31 +97,60 @@ class TheiaImport(object): ...@@ -97,31 +97,60 @@ class TheiaImport(object):
print(' '.join(command)) print(' '.join(command))
sp.check_call(command) sp.check_call(command)
def verify(self, signaturefile): def sign(self, signaturefile, root_dir):
pardir = os.path.abspath(os.path.join(self.backup_dir, os.pardir)) with open(signaturefile, 'r') as f:
moved = os.path.join(pardir, 'backup.signature.moved') for line in f:
proof = os.path.join(pardir, 'backup.signature.proof') try:
if os.path.exists(signaturefile): _, relpath = line.strip().split(None, 1)
os.rename(signaturefile, moved) except ValueError:
if not os.path.exists(moved): yield 'Could not parse: %s' % line
msg = 'ERROR the backup signature file is missing' continue
print(msg) filepath = os.path.join(root_dir, relpath)
try:
signature = sha256sum(filepath)
except IOError:
yield 'Could not read: %s' % filepath
continue
yield '%s %s' % (signature, relpath)
def sign_custom(self, root_dir):
partition = self.dst_path(root_dir)
script = hashscript(partition)
if not script:
msg = 'ERROR: missing custom signature script for partition ' + partition
raise Exception(msg) raise Exception(msg)
return hashcustom(root_dir, script)
def find_signature_file(self, partition):
filename = os.path.basename(partition) + '.backup.signature'
signaturefile = os.path.join(self.backup_dir, filename)
if os.path.exists(signaturefile):
return signaturefile, False
signaturefile += '.custom'
if os.path.exists(signaturefile):
return signaturefile, True
raise Exception('ERROR: missing signature file for partition ' + partition)
def verify(self, signaturefile, root_dir, custom=False):
proof = signaturefile + '.proof'
if custom:
signatures = self.sign_custom(root_dir)
else:
signatures = self.sign(signaturefile, root_dir)
with open(proof, 'w') as f: with open(proof, 'w') as f:
for s in hashwalk(self.backup_dir, self.mirror_partition_dirs): for s in signatures:
f.write(s + '\n') f.write(s + '\n')
diffcommand = ('diff', moved, proof) diffcommand = ('diff', signaturefile, proof)
print(' '.join(diffcommand))
try: try:
sp.check_output( sp.check_output(
diffcommand, stderr=sp.STDOUT, universal_newlines=True) diffcommand, stderr=sp.STDOUT, universal_newlines=True)
except sp.CalledProcessError as e: except sp.CalledProcessError as e:
template = 'ERROR the backup signatures do not match\n\n%s' template = 'ERROR the backup signatures do not match\n\n%s\n%s'
msg = template % e.output msg = template % (' '.join(diffcommand), e.output)
print(msg) print(msg)
raise Exception(msg) raise Exception(msg)
def loginfo(self, msg): def log(self, msg):
print(msg) print(msg)
self.logs.append(msg) self.logs.append(msg)
...@@ -144,44 +173,54 @@ class TheiaImport(object): ...@@ -144,44 +173,54 @@ class TheiaImport(object):
sys.exit(exitcode) sys.exit(exitcode)
def restore(self): def restore(self):
self.loginfo('Verify backup signature') self.log('Verify main backup signature')
self.verify(os.path.join(self.backup_dir, 'backup.signature')) signaturefile = os.path.join(self.backup_dir, 'backup.signature')
self.verify(signaturefile, self.backup_dir)
custom_partition_signatures = []
for m in self.mirror_partition_dirs:
signaturefile, custom = self.find_signature_file(m)
if custom:
custom_partition_signatures.append((signaturefile, m))
else:
self.log('Verify backup signature for ' + m)
self.verify(signaturefile, m)
self.loginfo('Stop slapproxy') self.log('Stop slapproxy')
self.supervisorctl('stop', 'slapos-proxy') self.supervisorctl('stop', 'slapos-proxy')
self.loginfo('Restore partitions') self.log('Restore partitions')
for m in self.mirror_partition_dirs: for m in self.mirror_partition_dirs:
self.restorepartition(m) self.restore_partition(m)
for d in self.dirs: for d in self.dirs:
self.loginfo('Restore directory ' + d) self.log('Restore directory ' + d)
self.restoretree(d) self.restore_tree(d)
self.loginfo('Restore slapproxy database') self.log('Restore slapproxy database')
self.restoredb() self.restore_db()
timestamp = os.path.join(self.root_dir, 'etc', '.resilient_timestamp') timestamp = os.path.join(self.root_dir, 'etc', '.resilient_timestamp')
self.loginfo('Restore resilient timestamp ' + timestamp) self.log('Restore resilient timestamp ' + timestamp)
self.restorefile(timestamp) self.restore_file(timestamp)
custom_script = os.path.join(self.root_dir, 'srv', 'runner-import-restore') custom_script = os.path.join(self.root_dir, 'srv', 'runner-import-restore')
if os.path.exists(custom_script): if os.path.exists(custom_script):
self.loginfo('Run custom restore script %s' % custom_script) self.log('Run custom restore script %s' % custom_script)
sp.check_call(custom_script) sp.check_call(custom_script)
self.loginfo('Start slapproxy again') self.log('Start slapproxy again')
self.supervisorctl('start', 'slapos-proxy') self.supervisorctl('start', 'slapos-proxy')
self.loginfo('Reformat partitions') self.log('Reformat partitions')
self.slapos('node', 'format', '--now') self.slapos('node', 'format', '--now')
self.loginfo('Remove old supervisord configuration files') self.log('Remove old supervisord configuration files')
conf_dir = os.path.join(self.instance_dir, 'etc', 'supervisor.conf.d') conf_dir = os.path.join(self.instance_dir, 'etc', 'supervisor.conf.d')
for f in glob.glob(os.path.join(conf_dir, '*')): for f in glob.glob(os.path.join(conf_dir, '*')):
os.remove(f) os.remove(f)
self.loginfo('Build Software Releases') self.log('Build Software Releases')
for i in range(3): for i in range(3):
try: try:
self.slapos('node', 'software', '--all', '--logfile', self.sr_log) self.slapos('node', 'software', '--all', '--logfile', self.sr_log)
...@@ -191,18 +230,18 @@ class TheiaImport(object): ...@@ -191,18 +230,18 @@ class TheiaImport(object):
else: else:
break break
self.loginfo('Remove old custom instance scripts') self.log('Remove old custom instance scripts')
partitions_glob = os.path.join(self.instance_dir, 'slappart*') partitions_glob = os.path.join(self.instance_dir, 'slappart*')
scripts = os.path.join(partitions_glob, 'srv', 'runner-import-restore') scripts = os.path.join(partitions_glob, 'srv', 'runner-import-restore')
for f in glob.glob(scripts): for f in glob.glob(scripts):
remove(f) remove(f)
self.loginfo('Remove partition timestamps') self.log('Remove partition timestamps')
timestamps = os.path.join(partitions_glob, '.timestamp') timestamps = os.path.join(partitions_glob, '.timestamp')
for f in glob.glob(timestamps): for f in glob.glob(timestamps):
remove(f) remove(f)
self.loginfo('Build Instances') self.log('Build Instances')
cp_log = self.cp_log cp_log = self.cp_log
for i in range(3): for i in range(3):
try: try:
...@@ -213,11 +252,15 @@ class TheiaImport(object): ...@@ -213,11 +252,15 @@ class TheiaImport(object):
else: else:
break break
self.log('Verify custom backup signatures')
for signaturefile, m in custom_partition_signatures:
self.verify(signaturefile, m, True)
for custom_script in glob.glob(scripts): for custom_script in glob.glob(scripts):
self.loginfo('Running custom instance script %s' % custom_script) self.log('Running custom instance script %s' % custom_script)
sp.check_call(custom_script) sp.check_call(custom_script)
self.loginfo('Done') self.log('Done')
if __name__ == '__main__': if __name__ == '__main__':
......
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