WizardKit/.bin/Scripts/functions/ddrescue.py
2018-07-16 18:07:12 -06:00

240 lines
7.2 KiB
Python

# Wizard Kit: Functions - ddrescue
import json
import pathlib
import re
from functions.common import *
from operator import itemgetter
# STATIC VARIABLES
USAGE=""" {script_name} clone [source [destination]]
{script_name} image [source [destination]]
(e.g. {script_name} clone /dev/sda /dev/sdb)
"""
# Functions
def get_device_details(dev_path):
"""Get device details via lsblk, returns JSON dict."""
try:
cmd = (
'lsblk',
'--json',
'--output-all',
'--paths',
dev_path)
result = run_program(cmd)
except CalledProcessError:
print_error('Failed to get device details for {}'.format(dev_path))
abort()
json_data = json.loads(result.stdout.decode())
# Just return the first device (there should only be one)
return json_data['blockdevices'][0]
def menu_clone(source_path, dest_path):
"""ddrescue cloning menu."""
source_is_image = False
source_dev_path = None
print_success('GNU ddrescue: Cloning Menu')
# Set devices
source = select_device('source', source_path)
dest = select_device('destination', dest_path,
skip_device = source['Details'], allow_image_file = False)
# Show selection details
clear_screen()
print_success('Source device')
if source['Is Image']:
print_standard('Using image file: {}'.format(source['Path']))
print_standard(' (via loopback device: {})'.format(
source['Dev Path']))
show_device_details(source['Dev Path'])
print_standard(' ')
print_success('Destination device ', end='')
print_error('(ALL DATA WILL BE DELETED)', timestamp=False)
show_device_details(dest['Dev Path'])
print_standard(' ')
# Confirm
if not ask('Proceed with clone?'):
abort()
# Build outer panes
clear_screen()
#TODO
def menu_ddrescue(*args):
"""Main ddrescue loop/menu."""
args = list(args)
script_name = os.path.basename(args.pop(0))
run_mode = ''
source_path = None
dest_path = None
# Parse args
try:
run_mode = args.pop(0)
source_path = args.pop(0)
dest_path = args.pop(0)
except IndexError:
# We'll set the missing paths later
pass
# Show proper menu or exit
if run_mode == 'clone':
menu_clone(source_path, dest_path)
elif run_mode == 'image':
menu_image(source_path, dest_path)
else:
if not re.search(r'(^$|help|-h|\?)', run_mode, re.IGNORECASE):
print_error('Invalid mode.')
show_usage(script_name)
exit_script()
def menu_image(source_path, dest_path):
"""ddrescue imaging menu."""
print_success('GNU ddrescue: Imaging Menu')
pass
def menu_select_device(title='Which device?', skip_device={}):
"""Select block device via a menu, returns dev_path as str."""
skip_names = [
skip_device.get('name', None), skip_device.get('pkname', None)]
skip_names = [n for n in skip_names if n]
try:
cmd = (
'lsblk',
'--json',
'--nodeps',
'--output-all',
'--paths')
result = run_program(cmd)
json_data = json.loads(result.stdout.decode())
except CalledProcessError:
print_error('Failed to get device details for {}'.format(dev_path))
abort()
# Build menu
dev_options = []
for dev in json_data['blockdevices']:
# Skip if dev is in skip_names
if dev['name'] in skip_names or dev['pkname'] in skip_names:
continue
# Append non-matching devices
dev_options.append({
'Name': '{name:12} {tran:5} {size:6} {model} {serial}'.format(
name = dev['name'],
tran = dev['tran'] if dev['tran'] else '',
size = dev['size'] if dev['size'] else '',
model = dev['model'] if dev['model'] else '',
serial = dev['serial'] if dev['serial'] else ''),
'Path': dev['name']})
dev_options = sorted(dev_options, key=itemgetter('Name'))
if not dev_options:
print_error('No devices available.')
abort()
# Show Menu
actions = [{'Name': 'Quit', 'Letter': 'Q'}]
selection = menu_select(
title = title,
main_entries = dev_options,
action_entries = actions)
if selection.isnumeric():
return dev_options[int(selection)-1]['Path']
elif selection == 'Q':
abort()
def select_device(description='device', provided_path=None,
skip_device={}, allow_image_file=True):
"""Select device via provided path or menu, return dev as dict."""
dev = {'Is Image': False}
# Set path
if provided_path:
dev['Path'] = provided_path
else:
dev['Path'] = menu_select_device(
title='Please select a {}'.format(description),
skip_device=skip_device)
dev['Path'] = os.path.realpath(dev['Path'])
# Check path
if pathlib.Path(dev['Path']).is_block_device():
dev['Dev Path'] = dev['Path']
elif allow_image_file and pathlib.Path(dev['Path']).is_file():
dev['Dev Path'] = setup_loopback_device(dev['Path'])
dev['Is Image'] = True
else:
print_error('Invalid {} "{}".'.format(description, dev['Path']))
abort()
# Get device details
dev['Details'] = get_device_details(dev['Dev Path'])
# Check for parent device(s)
while dev['Details']['pkname']:
print_warning('{} "{}" is a child device.'.format(
description.title(), dev['Dev Path']))
if ask('Use parent device "{}" instead?'.format(
dev['Details']['pkname'])):
# Update dev with parent info
dev['Dev Path'] = dev['Details']['pkname']
dev['Details'] = get_device_details(dev['Dev Path'])
else:
# Leave alone
break
return dev
def setup_loopback_device(source_path):
"""Setup a loopback device for source_path, returns dev_path as str."""
cmd = (
'losetup',
'--find',
'--partscan',
'--show',
source_path)
try:
out = run_program(cmd, check=True)
dev_path = out.stdout.decode().strip()
sleep(1)
except CalledProcessError:
print_error('Failed to setup loopback device for source.')
abort()
else:
return dev_path
def show_device_details(dev_path):
"""Display device details on screen."""
cmd = (
'lsblk', '--nodeps',
'--output', 'NAME,TRAN,TYPE,SIZE,VENDOR,MODEL,SERIAL',
dev_path)
result = run_program(cmd)
output = result.stdout.decode().splitlines()
print_info(output.pop(0))
for line in output:
print_standard(line)
# Children info
cmd = ('lsblk', '--output', 'NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT', dev_path)
result = run_program(cmd)
output = result.stdout.decode().splitlines()
print_info(output.pop(0))
for line in output:
print_standard(line)
def show_usage(script_name):
print_info('Usage:')
print_standard(USAGE.format(script_name=script_name))
if __name__ == '__main__':
print("This file is not meant to be called directly.")
# vim: sts=4 sw=4 ts=4