-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
The parser for restore-vm is a bit simpler because it only has positional arguments and flags at the beginning like a normal program (no --borg-args shenanigans). It can actually reuse the ArchiveBuilder class for the restore because the bind mounts are not read-only, but this doesn't work if the format of the disk changes.
- Loading branch information
1 parent
48546ce
commit e8ee0a4
Showing
4 changed files
with
161 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import subprocess | ||
import os | ||
|
||
|
||
def yes(question, default=None): | ||
"""Ask the user a yes/no question, optionally with a default answer.""" | ||
while True: | ||
print(question, " (Y/n): " if default else " (y/N): ", end="", file=sys.stderr) | ||
answer = input().upper().rstrip("\n") | ||
if answer in {"Y", "YES", "1"}: | ||
return True | ||
elif answer in {"N", "NO", "0"}: | ||
return False | ||
elif default is not None: | ||
return default | ||
|
||
|
||
def grouper(iterable, n): | ||
"""Collect data into fixed-length chunks or blocks, cutting off remaining elements""" | ||
args = [iter(iterable)] * n | ||
return zip(*args) | ||
|
||
|
||
def list_entries(archive, properties=["type", "size", "health", "bpath"], passphrases=None): | ||
"""Wrapper around 'borg list' that returns dicts with keys from '--format'.""" | ||
env = os.environ.copy() | ||
if isinstance(passphrases, str): | ||
env["BORG_PASSPHRASE"] = passphrases | ||
elif passphrases is not None and archive in passphrases: | ||
env["BORG_PASSPHRASE"] = passphrases[archive] | ||
format_string = "{NUL}".join("{%s}" % p for p in properties) + "{NUL}" | ||
p = subprocess.run(["borg", "list", "--format", format_string, str(archive)], | ||
env=env, stdout=subprocess.PIPE, check=True) | ||
for entry in grouper(p.stdout.split(b"\x00"), len(properties)): | ||
yield {key: entry[i] for i, key in enumerate(properties)} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import os.path | ||
import sys | ||
import libvirt | ||
from . import lock | ||
from . import parse | ||
from . import multi | ||
from . import builder | ||
from . import helpers | ||
from . import snapshot | ||
|
||
|
||
def main(): | ||
args = parse.RestoreArgumentParser() | ||
conn = libvirt.open() | ||
if conn is None: | ||
print("Failed to open connection to libvirt", file=sys.stderr) | ||
sys.exit(1) | ||
try: | ||
dom = conn.lookupByName(args.domain) | ||
except libvirt.libvirtError: | ||
print("Domain '{}' not found".format(args.domain)) | ||
sys.exit(1) | ||
|
||
dom_disks = set(parse.Disk.from_domain(dom)) | ||
if len(dom_disks) == 0: | ||
print("Domain has no disks(!)", file=sys.stderr) | ||
sys.exit(1) | ||
|
||
disks_to_restore = args.disks and {x for x in dom_disks if x.target in args.disks} or dom_disks | ||
targets_to_restore = {x.target for x in disks_to_restore} | ||
if (len(args.disks) > 0 and args.disks != targets_to_restore | ||
and not helpers.yes("Some disks to be restored don't exist on the domain: " + | ||
" ".join(sorted(args.disks - targets_to_restore)) + | ||
"\nDo you want to recreate them on the target domain?", False)): | ||
sys.exit(1) | ||
|
||
archive_disks = {} | ||
passphrases = multi.get_passphrases([args.archive]) if sys.stdout.isatty() else None | ||
for entry in helpers.list_entries(args.archive, passphrases=passphrases): | ||
if entry["type"] == b"-": | ||
name = entry["bpath"].decode("utf-8") | ||
if name.split(".")[0] in targets_to_restore: | ||
if (entry["health"] != b"healthy" | ||
and not helpers.yes("The backup copy of disk {} is marked as broken.".format(name.split(".")[0]) + | ||
"\nDo you still want to replace it with a possibly corrupted version?", False)): | ||
sys.exit(1) | ||
else: | ||
archive_disks[name] = int(entry["size"]) | ||
if len(disks_to_restore) != len(archive_disks): | ||
archive_tgts = {d.split(".")[0] for d in archive_disks.keys()} | ||
print("Some disks to be restored don't exist in the archive:", | ||
*sorted(targets_to_restore - archive_tgts), file=sys.stderr) | ||
sys.exit(1) | ||
# TODO: warn if a disk's logical size differs from backup to target | ||
# this is complicated by non-raw images like qcow2 (which are expandable) | ||
|
||
with lock.DiskLock(disks_to_restore), builder.ArchiveBuilder(disks_to_restore) as archive_dir: | ||
args.archive.extra_args.extend(archive_disks.keys()) | ||
borg_failed = multi.assimilate( | ||
archives=[args.archive], | ||
total_size=args.progress and archive_dir.total_size, | ||
dir_to_archive=None, | ||
passphrases=passphrases, | ||
verb="extract" | ||
) | ||
|
||
# bug in libvirt python wrapper(?): sometimes it tries to delete | ||
# the connection object before the domain, which references it | ||
del dom | ||
del conn | ||
|
||
sys.exit(borg_failed) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters