Skip to content

Commit

Permalink
interface overhaul; still wip
Browse files Browse the repository at this point in the history
  • Loading branch information
eimrek committed Oct 25, 2023
1 parent ec151e2 commit a701374
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 54 deletions.
157 changes: 122 additions & 35 deletions disk_objectstore/backup_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Utilities to back up a container.
"""

import logging
import shutil
import sqlite3
import subprocess
Expand All @@ -11,16 +12,29 @@

from disk_objectstore.container import Container

logger = logging.getLogger(__name__)


def _log(msg, end="\n"):
print(msg, end=end)

Check warning on line 19 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L19

Added line #L19 was not covered by tests


def _is_exe_found(exe) -> bool:
def split_remote_and_path(dest: str):
"""extract remote and path from <remote>:<path>"""
split_dest = dest.split(":")
if len(split_dest) == 1:
return None, Path(dest)
if len(split_dest) == 2:
return split_dest[0], Path(split_dest[1])

Check warning on line 28 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L24-L28

Added lines #L24 - L28 were not covered by tests
# more than 1 colon:
raise ValueError

Check warning on line 30 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L30

Added line #L30 was not covered by tests


def is_exe_found(exe: str) -> bool:
return shutil.which(exe) is not None

Check warning on line 34 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L34

Added line #L34 was not covered by tests


def _run_cmd(args: list, remote: Optional[str] = None, check: bool = True) -> bool:
def run_cmd(args: list, remote: Optional[str] = None, check: bool = True) -> bool:
"""
Run a command locally or remotely.
"""
Expand All @@ -42,22 +56,23 @@ def _run_cmd(args: list, remote: Optional[str] = None, check: bool = True) -> bo
return success

Check warning on line 56 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L56

Added line #L56 was not covered by tests


def _check_if_remote_accessible(remote: str) -> bool:
def check_if_remote_accessible(remote: str) -> bool:
"""Check if remote host is accessible via ssh"""
_log(f"Checking if '{remote}' is accessible...", end="")
success = _run_cmd(["exit"], remote=remote)
success = run_cmd(["exit"], remote=remote)
if not success:
_log(f"Error: Remote '{remote}' is not accessible!")
return False
_log(f"Success! '{remote}' is accessible!")
return True

Check warning on line 67 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L61-L67

Added lines #L61 - L67 were not covered by tests


def _check_path_exists(path: Path, remote: Optional[str] = None) -> bool:
def check_path_exists(path: Path, remote: Optional[str] = None) -> bool:
cmd = ["[", "-e", str(path), "]"]
return _run_cmd(cmd, remote=remote, check=False)
return run_cmd(cmd, remote=remote, check=False)

Check warning on line 72 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L71-L72

Added lines #L71 - L72 were not covered by tests


def _call_rsync( # pylint: disable=too-many-arguments
def call_rsync( # pylint: disable=too-many-arguments
args: list,
src: Path,
dest: Path,
Expand Down Expand Up @@ -121,47 +136,54 @@ def _call_rsync( # pylint: disable=too-many-arguments
return success

Check warning on line 136 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L136

Added line #L136 was not covered by tests


def backup( # pylint: disable=too-many-return-statements, too-many-branches
container: Container,
def validate_inputs(
path: Path,
remote: Optional[str] = None,
prev_backup: Optional[Path] = None,
rsync_exe: str = "rsync",
) -> bool:
"""Create a backup of the disk-objectstore container
It should be done in the following order:
1) loose files;
2) sqlite database;
3) packed files.
"""Validate inputs to the backup cli command
:return:
True is successful and False if unsuccessful.
True if validation passes, False otherwise.
"""

# ------------------
# input validation:
if remote:
if not _check_if_remote_accessible(remote):
if not check_if_remote_accessible(remote):
return False

Check warning on line 151 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L149-L151

Added lines #L149 - L151 were not covered by tests

if not _is_exe_found(rsync_exe):
if not is_exe_found(rsync_exe):
_log(f"Error: {rsync_exe} not accessible.")
return False

Check warning on line 155 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L153-L155

Added lines #L153 - L155 were not covered by tests

path_exists = _check_path_exists(path, remote)
path_exists = check_path_exists(path, remote)

Check warning on line 157 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L157

Added line #L157 was not covered by tests

if not path_exists:
success = _run_cmd(["mkdir", str(path)], remote=remote)
success = run_cmd(["mkdir", str(path)], remote=remote)
if not success:
_log(f"Error: Couldn't access/create '{str(path)}'!")
return False

Check warning on line 163 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L159-L163

Added lines #L159 - L163 were not covered by tests

if prev_backup:
if not _check_path_exists(prev_backup, remote):
_log(f"Error: {str(prev_backup)} not found.")
return False
# ------------------
return True

Check warning on line 165 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L165

Added line #L165 was not covered by tests


def backup_container( # pylint: disable=too-many-return-statements, too-many-branches
container: Container,
path: Path,
remote: Optional[str] = None,
prev_backup: Optional[Path] = None,
rsync_exe: str = "rsync",
) -> bool:
"""Create a backup of the disk-objectstore container
This is safe to perform when the container is being used.
It should be done in the following order:
1) loose files;
2) sqlite database;
3) packed files.
:return:
True if successful and False if unsuccessful.
"""

# subprocess arguments shared by all rsync calls:
rsync_args = [rsync_exe, "-azh", "-vv", "--no-whole-file"]

Check warning on line 189 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L189

Added line #L189 was not covered by tests
Expand All @@ -174,7 +196,7 @@ def backup( # pylint: disable=too-many-return-statements, too-many-branches
# step 1: back up loose files
loose_path_rel = loose_path.relative_to(container_root_path)
prev_backup_loose = prev_backup / loose_path_rel if prev_backup else None
success = _call_rsync(
success = call_rsync(

Check warning on line 199 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L197-L199

Added lines #L197 - L199 were not covered by tests
rsync_args, loose_path, path, remote=remote, link_dest=prev_backup_loose
)
if not success:
Expand Down Expand Up @@ -202,23 +224,22 @@ def backup( # pylint: disable=too-many-return-statements, too-many-branches
return False

Check warning on line 224 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L223-L224

Added lines #L223 - L224 were not covered by tests

# step 3: transfer the SQLITE database file
success = _call_rsync(
success = call_rsync(

Check warning on line 227 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L227

Added line #L227 was not covered by tests
rsync_args, sqlite_temp_loc, path, remote=remote, link_dest=prev_backup
)
if not success:
return False

Check warning on line 231 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L230-L231

Added lines #L230 - L231 were not covered by tests

# step 4: transfer the packed files
packs_path_rel = packs_path.relative_to(container_root_path)
prev_backup_packs = prev_backup / packs_path_rel if prev_backup else None
success = _call_rsync(
rsync_args, packs_path, path, remote=remote, link_dest=prev_backup_packs
success = call_rsync(

Check warning on line 235 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L234-L235

Added lines #L234 - L235 were not covered by tests
rsync_args, packs_path, path, remote=remote, link_dest=prev_backup
)
if not success:
return False

Check warning on line 239 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L238-L239

Added lines #L238 - L239 were not covered by tests

# step 5: transfer anything else in the container folder
success = _call_rsync(
success = call_rsync(

Check warning on line 242 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L242

Added line #L242 was not covered by tests
rsync_args
+ [
"--exclude",
Expand All @@ -238,3 +259,69 @@ def backup( # pylint: disable=too-many-return-statements, too-many-branches
return False

Check warning on line 259 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L258-L259

Added lines #L258 - L259 were not covered by tests

return True

Check warning on line 261 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L261

Added line #L261 was not covered by tests


def backup_auto_folders(
container: Container,
path: Path,
remote: Optional[str] = None,
rsync_exe: str = "rsync",
):
"""Create a backup, managing live and previous backup folders automatically
The running backup is done to `<path>/live-backup`. When it completes, it is moved to
the final path: `<path>/last-backup`. This done so that the last backup wouldn't be
corrupted, in case the live one crashes or gets interrupted. Rsync `link-dest` is used between
the two folders to keep the backups incremental and performant.
:param path:
Path to where the backup will be created. If 'remote' is specified, must be an absolute path,
otherwise can be relative.
:param remote:
Remote host of the backup location. 'ssh' executable is called via subprocess and therefore remote
hosts configured for it are supported (e.g. via .ssh/config file).
:param kwargs:
* Executable paths if not default, e.g. 'rsync'
:return:
True is successful and False if unsuccessful.
"""

live_folder = path / "live-backup"
last_folder = path / "last-backup"

Check warning on line 293 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L292-L293

Added lines #L292 - L293 were not covered by tests

prev_exists = check_path_exists(last_folder, remote)

Check warning on line 295 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L295

Added line #L295 was not covered by tests

success = backup_container(

Check warning on line 297 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L297

Added line #L297 was not covered by tests
container,
live_folder,
remote=remote,
prev_backup=last_folder if prev_exists else None,
rsync_exe=rsync_exe,
)
if not success:
return False

Check warning on line 305 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L304-L305

Added lines #L304 - L305 were not covered by tests

# move live-backup -> last-backup in a safe manner
# (such that if the process stops at any point, that we wouldn't lose data)
# step 1: last-backup -> last-backup-old
if prev_exists:
success = run_cmd(

Check warning on line 311 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L310-L311

Added lines #L310 - L311 were not covered by tests
["mv", str(last_folder), str(last_folder) + "-old"], remote=remote
)
if not success:
return False

Check warning on line 315 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L314-L315

Added lines #L314 - L315 were not covered by tests
# step 2: live-backup -> last-backup
success = run_cmd(["mv", str(live_folder), str(last_folder)], remote=remote)
if not success:
return False

Check warning on line 319 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L317-L319

Added lines #L317 - L319 were not covered by tests
# step 3: remote last-backup-old
if prev_exists:
success = run_cmd(["rm", "-rf", str(last_folder) + "-old"], remote=remote)
if not success:
return False

Check warning on line 324 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L321-L324

Added lines #L321 - L324 were not covered by tests

_log(f"Backup moved from '{str(live_folder)}' to '{str(last_folder)}'.")
return True

Check warning on line 327 in disk_objectstore/backup_utils.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/backup_utils.py#L326-L327

Added lines #L326 - L327 were not covered by tests
53 changes: 34 additions & 19 deletions disk_objectstore/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,37 +186,52 @@ def optimize(


@main.command("backup")
@click.argument("path", nargs=1, type=click.Path())
@click.option(
"--remote",
default=None,
help="ssh remote of the backup location.",
)
@click.option(
"--prev_backup",
default=None,
help="Previous backup location for rsync link-dest.",
)
@click.argument("dest", nargs=1, type=click.Path())
# @click.option(
# "--keep",
# default=1,
# help="Number of previous backups to keep in the destination.",
# )
@click.option(
"--rsync_exe",
default="rsync",
help="Specify the 'rsync' executable, if not in PATH.",
help="Specify the 'rsync' executable, if not in PATH. Used for both local and remote destinations.",
)
@pass_dostore
def backup(
dostore: ContainerContext,
path: str,
remote: Optional[str],
prev_backup: Optional[str],
dest: str,
rsync_exe: str,
):
"""Create a backup of the container"""
"""Create a backup of the container to a subfolder `last-backup` of the destination location DEST.
NOTE: This is safe to run while the container is being used.
Destination (DEST) can either be a local path, or a remote destination (reachable via ssh).
In the latter case, remote destination needs to have the following syntax:
[<remote_user>@]<remote_host>:<path>
i.e., contain the remote host name and the remote path, separated by a colon (and optionally the
remote user separated by an @ symbol). You can tune SSH parameters using the standard options given
by OpenSSH, such as adding configuration options to ~/.ssh/config (e.g. to allow for passwordless
login - recommended, since this script might ask multiple times for the password).
NOTE: 'rsync' and other UNIX-specific commands are called, thus the command will not work on
non-UNIX environments.
"""

try:
remote, path = backup_utils.split_remote_and_path(dest)
except ValueError:
click.echo("Unsupported destination.")
return False

Check warning on line 227 in disk_objectstore/cli.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/cli.py#L223-L227

Added lines #L223 - L227 were not covered by tests

backup_utils.validate_inputs(path, remote=remote, rsync_exe=rsync_exe)

Check warning on line 229 in disk_objectstore/cli.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/cli.py#L229

Added line #L229 was not covered by tests

with dostore.container as container:
backup_utils.backup(
return backup_utils.backup_auto_folders(

Check warning on line 232 in disk_objectstore/cli.py

View check run for this annotation

Codecov / codecov/patch

disk_objectstore/cli.py#L231-L232

Added lines #L231 - L232 were not covered by tests
container,
Path(path),
path,
remote=remote,
prev_backup=Path(prev_backup) if prev_backup else None,
rsync_exe=rsync_exe,
)

0 comments on commit a701374

Please sign in to comment.