Skip to content
This repository has been archived by the owner on Nov 10, 2023. It is now read-only.

Commit

Permalink
Start on tar support
Browse files Browse the repository at this point in the history
  • Loading branch information
richfitz committed Oct 20, 2023
1 parent 8b3793c commit c3570d4
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 1 deletion.
29 changes: 28 additions & 1 deletion src/privateer2/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
privateer2 [options] check [--connection]
privateer2 [options] backup <volume> [--server=NAME]
privateer2 [options] restore <volume> [--server=NAME] [--source=NAME]
privateer2 [options] export <volume> [--to-dir=PATH] [--source=NAME]
privateer2 [options] import <tarfile> <volume>
privateer2 [options] server (start | stop | status)
privateer2 [options] schedule (start | stop | status)
Expand All @@ -19,6 +21,11 @@
or server being acted on; the machine we are generating keys for,
configuring, checking, serving, backing up from or restoring to.
Note that the 'import' subcommand is quite different and does not
interact with the configuration; it will reject options '--as' and
'--path'. If 'volume' exists already, it will fail, so this is
fairly safe.
The server and schedule commands start background containers that
run forever (with the 'start' option). Check in on them with
'status' or stop them with 'stop'.
Expand All @@ -38,6 +45,7 @@
from privateer2.restore import restore
from privateer2.schedule import schedule_start, schedule_status, schedule_stop
from privateer2.server import server_start, server_status, server_stop
from privateer2.tar import export_tar, import_tar


def pull(cfg):
Expand Down Expand Up @@ -114,7 +122,16 @@ def _parse_opts(opts):
return Call(_show_version)

dry_run = opts["--dry-run"]
name = opts["--as"]
if opts["import"]:
_dont_use("--as", opts, "import")
_dont_use("--path", opts, "import")
return Call(
import_tar,
volume=opts["<volume>"],
tarfile=opts["<tarfile>"],
dry_run=dry_run,
)

path_config = _path_config(opts["--path"])
root_config = os.path.dirname(path_config)
cfg = read_config(path_config)
Expand Down Expand Up @@ -159,6 +176,16 @@ def _parse_opts(opts):
source=opts["--source"],
dry_run=dry_run,
)
elif opts["export"]:
return Call(
export_tar,
cfg=cfg,
name=name,
volume=opts["<volume>"],
to_dir=opts["--to-dir"],
source=opts["--source"],
dry_run=dry_run,
)
elif opts["server"]:
if opts["start"]:
return Call(server_start, cfg=cfg, name=name, dry_run=dry_run)
Expand Down
115 changes: 115 additions & 0 deletions src/privateer2/tar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import os

import docker
from privateer2.config import find_source
from privateer2.check import check
from privateer2.util import (
isotimestamp,
mounts_str,
run_docker_command,
take_ownership,
volume_exists,
)


def export_tar(cfg, name, volume, *, to_dir=None, source=None, dry_run=False):
machine = check(cfg, name, quiet=True)
# TODO: we should be able to export a truely local volume too;
# this one probably can run a few ways.
#
# TODO: check here that volume is either local, or that it is a
# backup target for anything.
source = find_source(cfg, volume, source)
image = f"mrcide/privateer-client:{cfg.tag}"
if to_dir is None:
export_path = os.getcwd()
else:
export_path = os.path.abspath(to_dir)
mounts = [
docker.types.Mount("/export", export_path, type="bind"),
docker.types.Mount(
"/privateer", machine.data_volume, type="volume", read_only=True
),
]
tarfile = f"{source}-{volume}-{isotimestamp()}.tar"
working_dir = f"/privateer/{source}/{volume}"
command = ["tar", "-cpvf", f"/export/{tarfile}", "."]
if dry_run:
cmd = [
"docker",
"run",
"--rm",
*mounts_str(mounts),
"-w",
working_dir,
image,
*command,
]
print("Command to manually run export")
print()
print(f" {' '.join(cmd)}")
print()
print("(pay attention to the final '.' in the above command!)")
print()
print(f"This will data from the server '{name}' onto the host")
print(f"machine at '{export_path}' as '{tarfile}'.")
print(f"Data originally from '{source}'")
print()
print("Note that this file will have root ownership after creation")
print(f"You can fix that with 'sudo chown $(whoami) {tarfile}'")
print("or")
print()
cmd_own = take_ownership(tarfile, export_path, command_only=True)
print(f" {' '.join(cmd_own)}")
else:
run_docker_command(
"Export",
image,
command=command,
mounts=mounts,
working_dir=working_dir,
)
print("Taking ownership of file")
take_ownership(tarfile, export_path)
print(f"Tar file ready at '{export_path}/{tarfile}'")


def import_tar(volume, tarfile, *, dry_run=False):
if volume_exists(volume):
msg = f"Volume '{volume}' already exists, please delete first"
raise Exception(msg)
if not os.path.exists(tarfile):
msg = f"Input file '{tarfile}' does not exist"

image = "alpine"
tarfile = os.path.abspath(tarfile)
mounts = [
docker.types.Mount("/src.tar", tarfile, type="bind", read_only=True),
docker.types.Mount("/privateer", volume, type="volume"),
]
working_dir = "/privateer"
command = ["tar", "-xvf", "/src.tar"]
if dry_run:
cmd = [
"docker",
"run",
"--rm",
*mounts_str(mounts),
"-w",
working_dir,
image,
*command,
]
print("Command to manually run import")
print()
print(f" docker volume create {volume}")
print(f" {' '.join(cmd)}")
else:
docker.from_env().volumes.create(volume)
run_docker_command(
"Import",
image,
command=command,
mounts=mounts,
working_dir=working_dir,
)
28 changes: 28 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ def test_can_parse_version():
assert res.kwargs == {}


def test_can_parse_import():
res = _parse_argv(["import", "--dry-run", "f", "v"])
assert res.target == privateer2.cli.import_tar
assert res.kwargs == {"volume": "v", "tarfile": "f", "dry_run": True}
assert not _parse_argv(["import", "f", "v"]).kwargs["dry_run"]
with pytest.raises(Exception, match="Don't use '--path' with 'import'"):
_parse_argv(["--path=privateer.json", "import", "f", "v"])
with pytest.raises(Exception, match="Don't use '--as' with 'import'"):
_parse_argv(["--as=alice", "import", "f", "v"])


def test_can_parse_keygen_all():
res = _parse_argv(["keygen", "--path=example/simple.json", "--all"])
assert res.target == privateer2.cli.keygen_all
Expand Down Expand Up @@ -212,6 +223,23 @@ def test_can_parse_complex_restore(tmp_path):
}


def test_can_parse_export(tmp_path):
shutil.copy("example/simple.json", tmp_path / "privateer.json")
with open(tmp_path / ".privateer_identity", "w") as f:
f.write("alice\n")
with transient_working_directory(tmp_path):
res = _parse_argv(["export", "v"])
assert res.target == privateer2.cli.export_tar
assert res.kwargs == {
"cfg": read_config("example/simple.json"),
"name": "alice",
"volume": "v",
"to_dir": None,
"source": None,
"dry_run": False,
}


def test_error_if_unknown_identity(tmp_path):
shutil.copy("example/simple.json", tmp_path / "privateer.json")
msg = "Can't determine identity; did you forget to configure"
Expand Down

0 comments on commit c3570d4

Please sign in to comment.