Mini Shell
"""Backup Manager CLI"""
from argparse import ArgumentParser
import getpass
import os
import sys
from itertools import chain
from pathlib import Path, PurePath
from typing import Tuple
from bakmgr.dash.dash_helpers import hash_pw, HASH_PATH
from .api.bakauth import MonError, add_problem
from .api.restic import Restic
from .configs import setup_logging, Conf
def main():
"""Main CLI entry point for /opt/bakmgr/bin/bakmgr"""
if os.getuid() != 0:
sys.exit('This tool must run as root')
args = parse_args()
conf = Conf()
setup_logging(conf, f'bakmgr {args.command}')
if args.command == 'dash-password':
return set_dash_password()
if args.command == 'restore-files':
return restore_files(args, conf)
if args.command == 'restore-mysql':
return restore_db(args, 'mysql', conf)
if args.command == 'restore-pgsql':
return restore_db(args, 'pgsql', conf)
raise NotImplementedError(args.command)
def parse_args():
"""Parse CLI args to /opt/bakmgr/bin/bakmgr"""
parser = ArgumentParser(description=__doc__)
subp = parser.add_subparsers(title='command', dest='command')
add_cmd = lambda cmd, msg: subp.add_parser(cmd, description=msg, help=msg)
add_cmd('dash-password', 'set backup dashboard password')
files = add_cmd('restore-files', 'restore files/folders')
mysql = add_cmd('restore-mysql', 'restore MySQL data')
pgsql = add_cmd('restore-pgsql', 'restore PostgreSQL data')
for bak in (files, mysql, pgsql):
grp = bak.add_mutually_exclusive_group()
for opt in ('latest', 'oldest'):
grp.add_argument(
f'--{opt}',
dest=opt,
action='store_true',
help=f'Automatically pick the {opt} backup found',
)
files.add_argument(
'--target',
metavar='PATH',
default=Path('/'),
type=PurePath,
help='alternate directory to extract data to',
)
files.add_argument(
'paths',
metavar='path',
type=PurePath,
nargs='+',
help='path(s) to restore',
)
for sql in (mysql, pgsql):
sql.add_argument(
'path', type=Path, help='specify a path restore the sql dump to'
)
args = parser.parse_args()
if args.command is None:
parser.print_usage()
sys.exit(1)
return args
def set_dash_password():
try:
cleartext_pw = getpass.getpass()
while invalid_pw(cleartext_pw):
cleartext_pw = getpass.getpass()
except KeyboardInterrupt:
sys.exit("canceled")
hashed = hash_pw(cleartext_pw)
with open(HASH_PATH, 'w', encoding='ascii') as file:
HASH_PATH.chmod(0o600)
file.write(hashed)
print("Password stored.")
def invalid_pw(cleartext_pw: str):
if len(cleartext_pw) < 8:
print("Password is too short.")
return True
return False
def pick_snap(args, conf: Conf, tag: str) -> Tuple[Restic, str]:
"""Return a restic instance and chosen snapshot id to restore from"""
print('Looking up backups...')
try:
restic = Restic(conf)
except Exception as exc:
add_problem(MonError.RESTIC, str(exc))
sys.exit(1)
backups = restic.get_backups()[tag]
if not backups:
sys.exit('No backups found')
if len(backups) == 1 or args.latest:
return restic, backups[0].id
if args.oldest:
return restic, backups[-1].id
print('Index', 'Date', sep=' ' * 4)
print('=' * 41)
for index, snap in enumerate(backups):
print(str(index).rjust(2), snap.datetime, sep=' ' * 7)
print('=' * 41)
try:
idx = int(input('Enter the index of the backup to restore from: '))
snap_id = backups[idx].id
except (IndexError, ValueError):
sys.exit('invalid index')
except (EOFError, KeyboardInterrupt):
sys.exit('canceled')
return restic, snap_id
def restore_files(args, conf: Conf):
"""Restore files/folders"""
restic, snap_id = pick_snap(args, conf, 'files')
incl = chain(*[['--include', str(x)] for x in args.paths])
restic.execv('restore', '--target', str(args.target), *incl, snap_id)
def restore_db(args, dbtype: str, conf: Conf):
"""Restore SQL data"""
restic, snap_id = pick_snap(args, conf, dbtype)
with args.path.open('wb') as handle:
proc = restic.proc(
'dump',
snap_id,
f'/root/{dbtype}_dump.sql',
mon=False,
stdout=handle,
stderr=None,
encoding=None,
)
rcode = proc.complete(check=False).returncode
sys.exit(rcode)
if __name__ == '__main__':
main()
Zerion Mini Shell 1.0