177 lines
5.9 KiB
Python
177 lines
5.9 KiB
Python
"""
|
|
.. moduleauthor:: Eric Torres
|
|
.. module:: rbackup.plugins.packagemanager
|
|
:synopsis: Module for package manager plugins.
|
|
"""
|
|
import logging
|
|
import subprocess
|
|
import tarfile
|
|
|
|
from pathlib import Path
|
|
from tempfile import NamedTemporaryFile
|
|
|
|
# ========== Constants ==========
|
|
LOCKFILE_MODE = 0o0000
|
|
VALID_DB_COMPRESS_MODES = [None, "bzip2", "gz", "lzma", "xz"]
|
|
|
|
# ========== Logging Setup ==========
|
|
syslog = logging.getLogger(__name__)
|
|
|
|
|
|
# ========== Classes ==========
|
|
class PackageManager:
|
|
"""Class for abstracting package manager-based operations.
|
|
|
|
The package manager can be used in conjunction with a ``Snapshot`` for backups.
|
|
|
|
Lockfile Management
|
|
^^^^^^^^^^^^^^^^^^^
|
|
|
|
This class can be used as a context manager for creating a lockfile for the
|
|
specific package manager. This is to prevent transactions from occurring during
|
|
backup operations which would most likely leave the package manager's database in
|
|
an inconsistent state on the backup.
|
|
|
|
.. note:: Subclasses can override the context manager and implement i.e. blocking until
|
|
the process is complete with a timeout.
|
|
"""
|
|
|
|
def __init__(self, cachedir=None, db_path=None, lockfile=None, pkglist_cmd=None):
|
|
"""Default constructor for the PackageManager class.
|
|
|
|
:param cachedir: path to the package manager cache directory
|
|
:type cachedir: str or path-like object
|
|
:param db_path: path to the package manager database
|
|
:type db_path: str or path-like object
|
|
:param lockfile: path to this package manager's lockfile
|
|
:type lockfile: str
|
|
:param pkglist_cmd: command to list installed packages to stdout
|
|
:type pkglist_cmd: str or iterable of str
|
|
"""
|
|
self._cachedir = Path(cachedir)
|
|
self._db_path = Path(db_path)
|
|
self._lockfile = Path(lockfile)
|
|
self._pkglist_cmd = pkglist_cmd
|
|
|
|
def __enter__(self):
|
|
"""Create the package manager's lockfile. This prevents transactions
|
|
from occurring during the backup which could leave the database backup
|
|
in an inconsistent state.
|
|
|
|
The existence of this lockfile is an error, and its meaning is up to
|
|
the package manager. For example, pacman's db.lck indicates
|
|
either there is an ongoing transaction in progress or a previous transaction
|
|
failed and the database is in an inconsistent state.
|
|
|
|
:returns: self
|
|
:rtype: ``PackageManager`` object
|
|
:raises FileExistsError: if lockfile exists when this method is called
|
|
"""
|
|
self._lockfile.touch(mode=0o000)
|
|
yield self
|
|
|
|
def __exit__(self):
|
|
"""Remove the package manager's lockfile. After this lockfile is closed,
|
|
the package manager this class abstracts can perform transactions once again.
|
|
"""
|
|
self._lockfile.unlink()
|
|
|
|
def gen_pkglist(self):
|
|
"""Generate a text file listing installed packages
|
|
on the system and return the path to that file.
|
|
|
|
If there is an error in the package listing command, then
|
|
it is to be assumed that no file was created, therefore there
|
|
is no file to cleanup.
|
|
|
|
.. note:: This method is internal and is meant to be called from
|
|
a subclass in a separate module.
|
|
|
|
:returns: path to temporary file
|
|
:rtype: path-like object
|
|
"""
|
|
syslog.info("Creating a package list")
|
|
|
|
try:
|
|
process = subprocess.run(self._pkglist_cmd, capture_output=True)
|
|
except subprocess.CalledProcessError as e:
|
|
syslog.error(e)
|
|
else:
|
|
with NamedTemporaryFile(mode="wb", delete=False) as pkglist:
|
|
pkglist.write(process.stdout)
|
|
|
|
syslog.info("Package list generation complete")
|
|
return Path(pkglist.name)
|
|
|
|
def gen_db_archive(self, compress=None):
|
|
"""Generate a database archive for this package manager.
|
|
|
|
All arguments and keyword-only arguments are passed directly
|
|
to the PackageManager object.
|
|
|
|
.. note:: This method is internal and is meant to be called from
|
|
a subclass in a separate module.
|
|
|
|
:param compress: compression mode
|
|
:type compress: str
|
|
:returns: the path to the created file
|
|
:rtype: path-like object
|
|
:raises ValueError: if compress is not in packagemanager.VALID_DB_COMPRESS_MODES
|
|
"""
|
|
if compress is not None and compress not in VALID_DB_COMPRESS_MODES:
|
|
raise ValueError(f"{compress} is not a valid compress mode")
|
|
|
|
syslog.info("Creating a database archive")
|
|
|
|
archivemode = "w" if compress is None else f"w:{compress}"
|
|
archivesuffix = ".tar" if compress is None else f".tar.{compress}"
|
|
|
|
with NamedTemporaryFile(delete=False, suffix=archivesuffix) as tmpfile:
|
|
archive_path = Path(tmpfile.name)
|
|
|
|
with tarfile.open(name=archive_path, mode=archivemode) as db_archive:
|
|
db_archive.add(self.database_path)
|
|
|
|
syslog.info("Database archive generation complete")
|
|
|
|
return archive_path
|
|
|
|
@property
|
|
def cache_directory(self):
|
|
"""
|
|
:returns: the cache directory of this package manager.
|
|
:rtype: path-like object
|
|
"""
|
|
return self._cachedir
|
|
|
|
@property
|
|
def database_path(self):
|
|
"""
|
|
:returns: the database path of this package manager.
|
|
:rtype: path-like object
|
|
"""
|
|
return self._db_path
|
|
|
|
@property
|
|
def lockfile(self):
|
|
"""
|
|
:returns: the lockfile path of this package manager
|
|
:rtype: path-like object
|
|
"""
|
|
return self._lockfile
|
|
|
|
@property
|
|
def pkglist_cmd(self):
|
|
"""
|
|
:returns: the package listing command of this package manager.
|
|
:rtype: iterable or str
|
|
"""
|
|
return self._pkglist_cmd
|
|
|
|
|
|
# ========== Functions ==========
|
|
if __name__ == "__main__":
|
|
import doctest
|
|
|
|
doctest.testmod()
|