Mini Shell

Direktori : /proc/self/root/opt/bakmgr/lib/python3.6/site-packages/bakmgr/api/
Upload File :
Current File : //proc/self/root/opt/bakmgr/lib/python3.6/site-packages/bakmgr/api/bakauth.py

"""Functions for communicating with backup authority"""
from pathlib import Path
import random
import json
import enum
import sys
import os
import logging
from typing import List, Tuple, Union
from urllib3.util.retry import Retry
import distro
import requests
from requests.adapters import HTTPAdapter

from bakmgr.inst.update import get_bakauth_version

BAKAUTH1 = 'ash-sys-pro-bakauth1.imhadmin.net'  # prod main
BAKAUTH3 = 'lax-sys-pro-bakauth3.imhadmin.net'  # prod replicant
RETRIES = 2
TIMEOUT = 30.0


class MonError(enum.Enum):
    """Filenames for monitored errors"""

    RESTIC = 'restic_error'
    BAKAUTH = 'bakauth_error'
    MYSQL = 'mysql_backup_failed'
    PGSQL = 'pgsql_backup_failed'
    FILES = 'file_backup_failed'
    CRON_CRASH = 'cron_crashed'
    UPDATES = 'updates'


class BakAuthError(Exception):
    """Any error from backup authority"""

    def __init__(self, status: int, msg: str):
        super().__init__(msg)
        self.status = status


def mon_update() -> None:
    """Updates bakauth with client server status

    Raises:
        BakAuthError: any error updating monitoring status
    """
    try:
        errors = os.listdir('/opt/bakmgr/var/monitoring')
    except OSError:
        errors = []
    version = get_bakauth_version()
    py_version = sys.version_info
    post(
        bakauths=[BAKAUTH1],
        uri='/monitoring/update',
        data={
            'version': version,
            'errors': json.dumps(errors),
            'os_info': json.dumps(distro.info()),
            'py_info': f'{py_version[0]}.{py_version[1]}',
        },
    )


def add_problem(error: MonError, msg: str):
    """Place a monitoring file to be picked up by mon_update() to notify
    bakauth1 / systems of a problem"""
    logging.error(msg)
    path = Path('/opt/bakmgr/var/monitoring') / error.value
    with path.open('w', encoding='utf-8') as handle:
        handle.write(msg)
        handle.write('\n')


def clear_problem(*errors: MonError):
    """Mark a monitored system as OK by removing its monitoring file"""
    for error in errors:
        try:
            Path('/opt/bakmgr/var/monitoring').joinpath(error.value).unlink()
        except FileNotFoundError:
            pass


def post_vded_size(
    usage: int,
    *,
    notify: bool = False,
    reset: bool = False,
) -> None:
    """Post a v/ded server's size for AMP to display

    Args:
        usage (int): disk usage (in MiB)
        notify (bool, optional): notify as over quota. Defaults to False.
        reset (bool, optional): reset notify counter. Defaults to False.

    Raises:
        BakAuthError: any error updating usage
    """
    post(
        bakauths=[BAKAUTH1],
        uri='/usage/set/vded',
        data={'usage': usage, 'notify': notify, 'reset': reset},
    )


def get_vded_quota(*, nocache: bool = False) -> int:
    """Get this server's quota as an int in MiB

    Args:
        nocache (bool): Instruct bakauth to skip cache when
        looking up quota information. Defaults to False.

    Raises:
        BakAuthError: any error requesting the server's quota

    Returns:
        int: this v/ded server's quota as an int in MiB
    """
    bakauths = [BAKAUTH1, BAKAUTH3]
    random.shuffle(bakauths)
    quota_gb = post(
        bakauths=bakauths, uri='/buckets/vded_quota', data={'nocache': nocache}
    )
    return quota_gb * 1024


def get_reg_details():
    bakauths = [BAKAUTH1, BAKAUTH3]
    random.shuffle(bakauths)
    return post(bakauths=bakauths, uri='/buckets/reg_details', data={})


class LoggedRetry(Retry):
    """Logs on HTTP retries"""

    def increment(
        self,
        method=None,
        url=None,
        response=None,
        error=None,
        _pool=None,
        _stacktrace=None,
    ):
        if error is not None:
            logging.debug('bakauth::%s: %s', url, error)
        return super().increment(
            method, url, response, error, _pool, _stacktrace
        )


class BakAuthSession(requests.Session):
    """Custom requests.Session for bakauth requests"""

    def __init__(self, retries: int):
        super().__init__()
        self.mount(
            'https://',
            HTTPAdapter(
                max_retries=LoggedRetry(
                    total=retries,
                    read=retries,
                    connect=retries,
                    status=retries,
                    status_forcelist=[500],
                    backoff_factor=1.0,
                )
            ),
        )


def _read_auth() -> Tuple[str, str]:
    try:
        with open('/etc/bakmgr/.auth.json', encoding='ascii') as handle:
            data = json.load(handle)
    except FileNotFoundError as exc:
        raise BakAuthError(
            -1,
            '/etc/bakmgr/.auth.json was not found. '
            'Please run /opt/bakmgr/bin/bakmgr-setup --host HOSTNAME',
        ) from exc
    return (data['apiuser'], data['authkey'])


def post(*, bakauths: List[str], uri: str, data: dict) -> Union[str, int, dict]:
    """Performs a https post request to backup authority"""
    for index, bakauth_host in enumerate(bakauths):
        try:
            with BakAuthSession(RETRIES) as session:
                raw = session.post(
                    url=f"https://{bakauth_host}:4002/{uri.lstrip('/')}",
                    auth=_read_auth(),
                    timeout=TIMEOUT,
                    data=data,
                )
        except requests.exceptions.RequestException as exc:
            if index + 1 == len(bakauths):
                raise BakAuthError(-1, str(exc)) from exc
            continue
        try:
            ret = raw.json()
        except ValueError as exc:
            if index + 1 == len(bakauths):
                raise BakAuthError(
                    -1, f'Invalid JSON from auth server: {raw}'
                ) from exc
            continue
    if ret['status'] != 0:
        raise BakAuthError(ret['status'], ret['data'])
    return ret['data']

Zerion Mini Shell 1.0