Mini Shell

Direktori : /opt/bakmgr/lib/python3.6/site-packages/bakmgr/api/
Upload File :
Current File : //opt/bakmgr/lib/python3.6/site-packages/bakmgr/api/quotas.py

"""Backup size estimation functions"""
from datetime import datetime
from subprocess import DEVNULL, PIPE, run
import time
import json
import sys
from pathlib import Path
import logging
from typing import Dict, List, Union, TYPE_CHECKING
from .bakauth import BakAuthError, get_vded_quota, post_vded_size

if TYPE_CHECKING:
    from ..configs import Conf


class SizeCache:
    """Calculates and caches backup usage in MiB"""

    def __init__(self, conf: 'Conf', quota: int):
        self.quota = quota
        self.conf = conf
        self.path = Path('/opt/bakmgr/var/.sizes_cache.json')
        try:
            self.stamp = self.path.stat().st_mtime
            with self.path.open(encoding='ascii') as handle:
                data = json.load(handle)
            if conf.files.enabled and data['files'] is not None:
                self.files = float(data['files'])
            else:
                self.files = None
            if conf.mysql.enabled and data['mysql'] is not None:
                self.mysql = float(data['mysql'])
            else:
                self.mysql = None
            if conf.pgsql.enabled and data['pgsql'] is not None:
                self.pgsql = float(data['pgsql'])
            else:
                self.pgsql = None
        except (ValueError, KeyError, FileNotFoundError):
            self.stamp = 0.0
            self.mysql = 0.0
            self.files = 0.0
            self.pgsql = 0.0

    @property
    def time_str(self) -> str:
        if self.stamp > 0.0:
            return datetime.fromtimestamp(self.stamp).strftime("%d/%m/%y %I%p")
        raise AttributeError

    @property
    def total(self):
        return sum([x for x in (self.mysql, self.files, self.pgsql) if x])

    @property
    def retain(self) -> int:
        """Number of backups to retain"""
        if self.total < self.quota:
            return 2
        try:
            val = int(self.quota / self.total)
        except ZeroDivisionError:
            return 10
        return min(max(2, val), 10)  # between 2 and 10, inclusive

    @property
    def __dict__(self):
        return {
            'files': self.files,
            'mysql': self.mysql,
            'pgsql': self.pgsql,
        }

    def get(self) -> int:
        """Get total backup usage in MiB"""
        age_hrs = abs(time.time() - self.stamp) / 3600
        if age_hrs < 24 and self.total < self.quota:
            # accept cache: cache < 1day old, under quota
            return self.total
        if age_hrs < 2 and self.total > self.quota:
            # accept cache: cache < 2h old, over quota
            return self.total
        self._recalc()
        return self.total

    def _recalc(self):
        """Recalculate backup usage"""
        if self.conf.files.enabled:
            self.files = du_mb(self.conf.files.include, self.conf.files.exclude)
        else:
            self.files = None
        if self.conf.mysql.enabled:
            self.mysql = du_mb(['/var/lib/mysql'], [])
        else:
            self.mysql = None
        if self.conf.pgsql.enabled:
            self.pgsql = du_mb(['/var/lib/pgsql'], [])
        else:
            self.pgsql = None
        with self.path.open('w', encoding='ascii') as handle:
            json.dump(vars(self), handle, indent=4)


def check_quota(conf: 'Conf') -> int:
    """pre-backup quota validation"""
    quota = get_vded_quota()
    if quota <= 0:
        logging.error('No backup subscription found')
        sys.exit(1)
    cache = SizeCache(conf, quota)
    total = int(cache.get())
    try:
        post_vded_size(
            total,
            notify=total > quota,
            reset=total <= quota,
        )
    except BakAuthError as exc:
        logging.warning(exc)
    if total <= quota:
        logging.info('Usage: %dM/%dM', total, quota)
        return cache.retain
    logging.error('Usage: %dM/%dM', total, quota)
    sys.exit(3)


def du_mb(
    paths: Union[List[str], List[Path]],
    exclude: Union[List[str], List[Path]],
) -> Dict[str, int]:
    """Get total of a list of paths in MiB

    Args:
        paths (list[str]): paths to calculate size of
        exclude (list[str]): exclude these subpaths from above

    Raises:
        ValueError: unexpected output from du
        IndexError: unexpected output from du

    Returns:
        dict[str, int]: paths and sizes in MiB
    """
    if not paths:
        return {}
    kwargs = {}
    cmd = ['du', '-0sB', '1M']
    if exclude:
        cmd.append('--exclude-from=-')
        kwargs['input'] = '\n'.join([str(x) for x in exclude]) + '\n'
    cmd.extend([str(x) for x in paths if Path(x).exists()])
    # check=False becasue running du against a list where one item does not
    # exist will exit 1 and for our purposes that's fine
    stdout = run(
        cmd,
        check=False,
        stdout=PIPE,
        stderr=DEVNULL,
        encoding='UTF-8',
        **kwargs,
    ).stdout
    # parse the result
    total = 0
    for row in stdout.strip('\0').split('\0'):
        if not row:
            continue
        size, _ = row.split('\t', 1)
        total += int(size)
    return total

Zerion Mini Shell 1.0