Mini Shell

Direktori : /opt/bakmgr/lib/python3.6/site-packages/bakmgr/dash/routing/
Upload File :
Current File : //opt/bakmgr/lib/python3.6/site-packages/bakmgr/dash/routing/restore.py

import json
from datetime import datetime, timedelta
from os import kill
from pathlib import Path
import signal
from subprocess import PIPE, CalledProcessError, Popen
import sys
import time
import traceback
import multiprocessing
from itertools import chain
from threading import Thread
from typing import List
from flask import current_app as app
from flask import render_template, request, jsonify, abort
from bakmgr.api.restic import Restic, ResticError
from bakmgr.api.bakauth import BakAuthError
from bakmgr.configs import Conf
from bakmgr.dash.dash_helpers import get_reg, iter_dir, login

RUN_DIR = Path('/run/bakmgr/restores')
RESTORE_DIR = Path('/var/lib/bakmgr/restores')
AUTO_DISMISS = timedelta(days=14).total_seconds()


@app.route('/restore', methods=['GET', 'POST', 'DELETE'])
@login
def restore_page():
    if request.method == 'GET':
        return render_template("restore.html.jinja", page='restore')
    if request.method == 'DELETE':
        if 'name' in request.form:
            # dismissing a completed restore
            try:
                float(request.form['name'])
            except ValueError:
                app.logger.error(
                    "Invalid restore name: %s", request.form['name']
                )
                abort(400)
            path = RESTORE_DIR / f"{request.form['name']}.json"
            path.unlink(missing_ok=True)
        else:
            # killing a pending restore
            try:
                pid = int(request.form['pid'])
            except ValueError:
                app.logger.error("Invalid restore pid: %s", request.form['pid'])
            path = RUN_DIR / str(pid)
            if not path.is_file():
                return 'OK'  # already ended
            app.logger.warning('killing restore PID %s', pid)
            kill(pid, signal.SIGUSR1)
        return 'OK'
    # requesting a restore
    if request.form['task'] not in ('files', 'pgsql', 'mysql'):
        abort(400)
    try:
        if request.form['task'] == 'files':
            proc = FileRestoreProc(
                snapshot=request.form['snapshot'],
                include=[Path(x) for x in request.form['include'].splitlines()],
                dest=Path(request.form['dest']),
                date=request.form['date'],
            )
        else:
            proc = SqlRestoreProc(
                task_name=request.form['task'],
                snapshot=request.form['snapshot'],
                compress=int(request.form['compress']) == 1,
                dest=Path(request.form['dest']),
                date=request.form['date'],
            )
        Thread(target=proc.join, daemon=True).start()
    except (BakAuthError, ValueError) as exc:
        return jsonify(success=False, text=str(exc))
    return jsonify(success=True, text="Started")


@app.route('/list_backups')
@login
def list_backups():
    try:
        restic = Restic(Conf(), reg=get_reg())
        backups = {k: v for k, v in restic.get_backups().items() if v}
    except (ResticError, BakAuthError) as exc:
        app.logger.error(exc)
        return render_template("error_div.html.jinja", error=str(exc))
    except Exception as exc:
        app.logger.error(traceback.format_exc())
        return render_template(
            "error_div.html.jinja", error=f"{type(exc).__name__}: {exc}"
        )
    return render_template("list_backups.html.jinja", backups=backups)


@app.route('/list_restores')
@login
def list_restores():
    pids = []
    for path in iter_dir(RUN_DIR):
        try:
            pid = int(path.name)
        except ValueError:
            path.unlink()
            continue
        try:
            kill(pid, 0)
        except OSError:
            path.unlink()
            continue
        pids.append(pid)
    finished = {}
    dismiss = time.time() - AUTO_DISMISS
    for path in iter_dir(RESTORE_DIR):
        if not path.name.endswith('.json'):
            path.unlink(missing_ok=True)
            continue
        try:
            fin_time = float(path.name[:-5])
            if fin_time < dismiss:
                path.unlink(missing_ok=True)
                continue
            finished[fin_time] = json.loads(path.read_text('utf-8'))
        except ValueError:
            path.unlink(missing_ok=True)
            continue
    return render_template(
        'list_restores.html.jinja', pids=pids, finished=finished
    )


class RestoreProc(multiprocessing.Process):
    def __init__(self, snapshot: str, dest: Path, task_name: str, date: str):
        signal.signal(signal.SIGUSR1, self.kill_restore)
        self.restic_pids = set()
        self.date = date
        self.task_name = task_name
        self.snapshot = snapshot
        self.restic = Restic(Conf(), reg=get_reg())
        self._log = []
        if not dest.is_absolute():
            raise ValueError(f"{dest} is not an absolute path")
        self.dest = dest
        # TODO: evaluate if daemon=True or False is better here
        super().__init__(target=self.restore_task, daemon=True)
        self.start()

    def kill_restore(self, *_):
        self.log("killing restore process")
        for pid in self.restic_pids:
            try:
                kill(pid, signal.SIGINT)
            except OSError:
                pass

    def restore_task(self):
        RUN_DIR.mkdir(mode=0o755, parents=True, exist_ok=True)
        pid_path = RUN_DIR / str(self.pid)
        pid_path.touch(mode=0o644, exist_ok=True)
        try:
            success = self.task()
        except ResticError as exc:
            success = False
            self.log(str(exc))
        except Exception as exc:
            success = False
            print(traceback.format_exc(), file=sys.stderr)
            self.log(f"Restore crashed with {type(exc).__name__}: {exc}")
        if success:
            self.log("Success")
        RESTORE_DIR.mkdir(mode=0o755, parents=True, exist_ok=True)
        path = RESTORE_DIR / f"{time.time()}.json"
        data = {
            'success': success,
            'log': self._log,
            'task': self.task_name,
            'date': self.date,
        }
        with open(path, 'w', encoding='utf-8') as file:
            json.dump(data, file, indent=4)

    def log(self, msg: str):
        when = datetime.now().strftime("%d/%m/%y %I%p")
        line = f"{when}: {msg}"
        self._log.append(line)
        print(line, file=sys.stderr)

    def task(self) -> bool:
        raise NotImplementedError


class FileRestoreProc(RestoreProc):
    def __init__(
        self, *, snapshot: str, include: List[Path], dest: Path, date: str
    ):
        for path in include:
            if not path.is_absolute():
                raise ValueError(f"{path} is not an absolute path")
        self.include = include
        super().__init__(snapshot, dest, 'files', date)

    def task(self) -> bool:
        incl = chain(*[['--include', str(x)] for x in self.include])
        proc = self.restic.proc(
            'restore',
            '--target',
            str(self.dest),
            *incl,
            self.snapshot,
            mon=False,
        )
        self.restic_pids.add(proc.pid)
        try:
            proc.complete(check=True)
        except CalledProcessError as exc:
            raise ResticError(exc, self.restic) from exc
        return True


class SqlRestoreProc(RestoreProc):
    def __init__(
        self,
        *,
        task_name: str,
        snapshot: str,
        compress: bool,
        dest: Path,
        date: str,
    ):
        self.compress = compress
        super().__init__(snapshot, dest, task_name, date)

    def task(self) -> bool:
        args = ('dump', self.snapshot, f'/root/{self.task_name}_dump.sql')
        kwargs = dict(mon=False, encoding=None)
        with self.dest.open('wb') as file:
            try:
                if self.compress:
                    with Popen(
                        ['gzip', '--stdout'], stdout=file, stdin=PIPE
                    ) as gzip:
                        proc = self.restic.proc(
                            *args, **kwargs, stdout=gzip.stdin
                        )
                        self.restic_pids.add(proc.pid)
                        proc.complete(check=True)
                else:
                    proc = self.restic.proc(*args, **kwargs, stdout=file)
                    proc.complete(check=True)
            except CalledProcessError as exc:
                raise ResticError(exc, self.restic) from exc
        return True

Zerion Mini Shell 1.0