#!/usr/bin/env python3

'''Test that backup locking and labelling work in backup and restore.'''

from dnstest.test import Test
from dnstest.utils import *
import random
import os
import shutil

def wait_for_file(file, timeout=10):
    '''Wait for a file, with timeout'''

    for i in range(int(timeout / 0.2)):
        if os.path.isfile(file):
            break
        time.sleep(0.2)

def log_errcode(s):
    a = s.rfind("(")
    b = s.rfind(")")
    if a >= b or a < 0:
        return ""
    return s[a+1:b]

def check_log_err(server, expect_err):
    with open(server.fout) as logf:
        for logline in logf:
            if "error:" in logline:
                last_log = logline
    if log_errcode(last_log) != expect_err:
        set_err("WRONG ERRCODE")
        detail_log("!Unexpected errcode '%s' != '%s'" % (log_errcode(last_log), expect_err))

t = Test()

master = t.server("knot")
backup_dir = os.path.join(master.dir, "backup")
backup_dir_void = os.path.join(master.dir, "backup_void")
backup_dir_keysonly = os.path.join(master.dir, "backup_keysonly")

zones = t.zone_rnd(40, records=80)

t.link(zones, master)

for i in range(0, 3):
    master.dnssec(zones[i]).enable = True

master2 = t.server("knot")
zones2 = t.zone("example1.", file_name="example1.file", storage=".")  \
         + t.zone("example2.", file_name="example2.file", storage=".") \
         + t.zone("example3.", file_name="example3.file", storage=".")
t.link(zones2, master2)
for i in range(2, 9):
    dir_from = os.path.join(t.data_dir, "backup%d" % i)
    dir_to = os.path.join(master2.dir, "backup%d" % i)
    shutil.copytree(dir_from, dir_to)
    globals()["backup%d_dir" % i] = dir_to

if master.valgrind:
    master.ctl_params_append = ["-t", "40"]

lockfile = os.path.join(backup_dir, "lock.knot_backup")
labelfile = os.path.join(backup_dir, "knot_backup.label")

if master.valgrind:
    master.conf_zone(zones).semantic_checks = False
    master2.conf_zone(zones).semantic_checks = False

t.start()
serials_init = master.zones_wait(zones)

for i in range(10):
    for zone in zones:
        master.random_ddns(zone)

master.ctl("zone-backup +journal +backupdir %s" % backup_dir, wait=False)
wait_for_file(lockfile, timeout=5)
if master.valgrind:   # Without Valgrind the backup is too fast for this test-case
    # Attempt to start concurrent backups, expected (requested resource is busy).
    try:
        master.ctl("zone-backup +backupdir %s" % backup_dir, wait=True)
        set_err("CONCURRENT BACKUPS ALLOWED")
    except:
        pass
    check_log_err(master, "requested resource is busy")
    # Attempt to start a restore from a backup in progress, expected (malformed data).
    try:
        master.ctl("zone-restore +backupdir %s" % backup_dir, wait=True)
        set_err("RESTORE FROM A PENDING BACKUP ALLOWED")
    except:
        pass
    check_log_err(master, "malformed data")

wait_for_file(labelfile, timeout=60)

# Attempt to start backup into already existing backup, expected (already exists).
try:
    master.ctl("zone-backup +backupdir %s" % backup_dir, wait=True)
    set_err("BACKUP INTO EXISTING BACKUP ALLOWED")
except:
    pass
check_log_err(master, "already exists")

# Attempt to start backup with the "-f" option into already existing backup, expected OK.
try:
    master.ctl("-f zone-backup +backupdir %s" % backup_dir, wait=True)
except:
    set_err("FORCED BACKUP NOT ALLOWED")

# Attempt to start restore from non-existing backup directory, expected (not exists).
try:
    master.ctl("zone-restore +backupdir %s" % backup_dir_void, wait=True)
    set_err("RESTORE FROM NON-EXISTING DIRECTORY ALLOWED")
except:
    pass
check_log_err(master, "not exists")

# Attempt to start backup to the database storage directory, expected (invalid parameter).
try:
    master.ctl("zone-backup +backupdir %s" % master.dir, wait=True)
    set_err("BACKUP TO THE DB STORAGE DIRECTORY ALLOWED")
except:
    pass
check_log_err(master, "invalid parameter")

# Attempt to start restore from the database storage directory, expected (invalid parameter).
try:
    master.ctl("zone-restore +backupdir %s" % master.dir, wait=True)
    set_err("RESTORE FROM THE DB STORAGE DIRECTORY ALLOWED")
except:
    pass
check_log_err(master, "invalid parameter")

# Attempt to start restore from a non-backup directory, expected (malformed data).
try:
    master.ctl("zone-restore +backupdir %s" % t.out_dir, wait=True)
    set_err("RESTORE FROM A NON-BACKUP DIRECTORY ALLOWED")
except:
    pass
check_log_err(master, "malformed data")

# Do a regular restore, expected OK.
try:
    master.ctl("zone-restore +backupdir %s" % backup_dir, wait=True)
except:
    set_err("RESTORE NOT ALLOWED")

# Do a regular restore with the "-f" option from the current format, expected OK.
try:
    master.ctl("-f zone-restore +backupdir %s" % backup_dir, wait=True)
except:
    set_err("FORCED RESTORE NOT ALLOWED")

# Tests with +keysonly filter ##############################################

# Do a keysonly restore from a default ("+kaspdb") backup, expected OK.
try:
    master.ctl("zone-restore +keysonly +backupdir %s" % backup_dir, wait=True)
except:
    set_err("KEYSONLY RESTORE FROM KASPDB NOT ALLOWED")

# Do a new standard backup (for '+keysonly' testing), expected OK.
try:
    master.ctl("zone-backup +backupdir %s" % backup_dir_keysonly, wait=True)
except:
    set_err("NEW BACKUP NOT ALLOWED")

# Do a forced keysonly backup into already existing backup, expected OK.
try:
    master.ctl("-f zone-backup +keysonly +backupdir %s" % backup_dir_keysonly, wait=True)
except:
    set_err("FORCED KEYSONLY BACKUP NOT ALLOWED")

# Do a keysonly restore from a keysonly backup, expected OK.
try:
    master.ctl("zone-restore +keysonly +backupdir %s" % backup_dir_keysonly, wait=True)
except:
    set_err("KEYSONLY RESTORE NOT ALLOWED")

# Attempt to start restore of various additional data from a keysonly backup, expected (requested data not in backup).
test_filters = ["", \
                "  +zonefile +nojournal +notimers +nokaspdb +nocatalog +noquic", \
                "+nozonefile   +journal +notimers +nokaspdb +nocatalog +noquic", \
                "+nozonefile +nojournal   +timers +nokaspdb +nocatalog +noquic", \
                "+nozonefile +nojournal +notimers   +kaspdb +nocatalog +noquic", \
                "+nozonefile +nojournal +notimers +nokaspdb   +catalog +noquic", \
                "+nozonefile +nojournal +notimers +nokaspdb +nocatalog   +quic"]

for filters in test_filters:
    try:
        master.ctl("zone-restore %s +backupdir %s" % (filters, backup_dir_keysonly), wait=True)
        set_err("DATA RESTORE FROM KEYSONLY BACKUP ALLOWED")
    except:
        pass
    check_log_err(master, "requested data not in backup")

# Tests with preconfigured backups, server master2. #############################

# Attempt to restore without the "-f" option from the format 1 backup, expected (malformed data).
try:
    master2.ctl("zone-restore +backupdir %s" % backup2_dir, wait=True)
    set_err("RESTORE FROM OBSOLETE FORMAT ALLOWED")
except:
    pass
check_log_err(master2, "malformed data")

# Do a restore with the "-f" option from a format 1 backup, expected OK.
try:
    master2.ctl("-f zone-restore +backupdir %s" % backup2_dir, wait=True)
except:
    set_err("FORCED RESTORE FROM OBSOLETE FORMAT NOT ALLOWED")

# Attempt to restore from a labelled, but locked backup, expected (requested resource is busy).
try:
    master2.ctl("zone-restore +backupdir %s" % backup3_dir, wait=True)
    set_err("RESTORE FROM A LOCKED BACKUP ALLOWED")
except:
    pass
check_log_err(master2, "requested resource is busy")

# Attempt to restore from an unlabelled backup, expected (malformed data).
try:
    master2.ctl("zone-restore +backupdir %s" % backup4_dir, wait=True)
    set_err("RESTORE FROM AN UNLABELLED BACKUP ALLOWED")
except:
    pass
check_log_err(master2, "malformed data")

# Attempt to restore from a backup with corrupted label, expected (malformed data).
try:
    master2.ctl("-f zone-restore +backupdir %s" % backup5_dir, wait=True)
    set_err("RESTORE FROM BACKUP WITH A CORRUPT LABEL ALLOWED")
except:
    pass
check_log_err(master2, "malformed data")

# Attempt to restore from unsupported backup format number, expected (operation not supported).
try:
    master2.ctl("-f zone-restore +backupdir %s" % backup6_dir, wait=True)
    set_err("RESTORE FROM UNSUPPORTED BACKUP FORMAT VERSION ALLOWED")
except:
    pass
check_log_err(master2, "operation not supported")

# Attempt to restore from non-existant backup format number, expected (malformed data).
try:
    master2.ctl("-f zone-restore +backupdir %s" % backup7_dir, wait=True)
    set_err("RESTORE FROM NON-EXISTANT BACKUP FORMAT VERSION ALLOWED")
except:
    pass
check_log_err(master2, "malformed data")

# Attempt to restore from a backup made by incompatible CPU architecture (incompatible CPU architecture).
try:
    master2.ctl("zone-restore +backupdir %s" % backup8_dir, wait=True)
    set_err("RESTORE FROM INCOMPATIBLE ARCHITECTURE ALLOWED")
except:
    pass
check_log_err(master2, "incompatible CPU architecture")

# Attempt to restore a zonefile from a backup made by incompatible CPU architecture, expected OK.
try:
    master2.ctl("zone-restore +zonefile +notimers +nokaspdb +nojournal +nocatalog +backupdir %s" % backup8_dir, wait=True)
except:
    set_err("ZONEFILE RESTORE FROM INCOMPATIBLE CPU FAILED")

t.stop()
