Mini Shell
"""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