Added clone load/save and add block pair sections
This commit is contained in:
parent
0f2007f5dc
commit
6d6380dc6a
1 changed files with 142 additions and 5 deletions
|
|
@ -3,6 +3,7 @@
|
||||||
# vim: sts=2 sw=2 ts=2
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
import atexit
|
import atexit
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
@ -30,6 +31,15 @@ Usage:
|
||||||
Options:
|
Options:
|
||||||
-h --help Show this page
|
-h --help Show this page
|
||||||
'''
|
'''
|
||||||
|
CLONE_SETTINGS = {
|
||||||
|
'Source': None,
|
||||||
|
'Destination': None,
|
||||||
|
'Needs Format': False,
|
||||||
|
'Table Type': None,
|
||||||
|
'Partition Mapping': [
|
||||||
|
# (5, 1) ## Clone source partition #5 to destination partition #1
|
||||||
|
],
|
||||||
|
}
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
MENU_ACTIONS = (
|
MENU_ACTIONS = (
|
||||||
'Start',
|
'Start',
|
||||||
|
|
@ -78,6 +88,101 @@ class State():
|
||||||
self.init_tmux()
|
self.init_tmux()
|
||||||
exe.start_thread(self.fix_tmux_layout_loop)
|
exe.start_thread(self.fix_tmux_layout_loop)
|
||||||
|
|
||||||
|
def add_clone_block_pairs(self, source_parts, working_dir):
|
||||||
|
"""Add device to device block pairs and set settings if necessary."""
|
||||||
|
part_prefix = ''
|
||||||
|
if re.search(r'(loop|mmc|nvme)', self.destination.path.name):
|
||||||
|
part_prefix = 'p'
|
||||||
|
settings = {}
|
||||||
|
|
||||||
|
def _check_settings(settings):
|
||||||
|
"""Check settings for issues and update as necessary."""
|
||||||
|
if settings:
|
||||||
|
bail = False
|
||||||
|
for key in ('model', 'serial'):
|
||||||
|
if settings['Source'][key] != self.source.details[key]:
|
||||||
|
std.print_error(f"Clone settings don't match source {key}")
|
||||||
|
bail = True
|
||||||
|
if settings['Destination'][key] != self.destination.details[key]:
|
||||||
|
std.print_error(f"Clone settings don't match destination {key}")
|
||||||
|
bail = True
|
||||||
|
if bail:
|
||||||
|
raise std.GenericAbort()
|
||||||
|
|
||||||
|
# Update settings
|
||||||
|
if not settings:
|
||||||
|
settings = CLONE_SETTINGS.copy()
|
||||||
|
if not settings['Source']:
|
||||||
|
settings['Source'] = {
|
||||||
|
'model': self.source.details['model'],
|
||||||
|
'serial': self.source.details['serial'],
|
||||||
|
}
|
||||||
|
if not settings['Destination']:
|
||||||
|
settings['Destination'] = {
|
||||||
|
'model': self.destination.details['model'],
|
||||||
|
'serial': self.destination.details['serial'],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return settings
|
||||||
|
|
||||||
|
# Clone settings
|
||||||
|
settings = self.load_settings(working_dir)
|
||||||
|
settings = _check_settings(settings)
|
||||||
|
|
||||||
|
# Add pairs
|
||||||
|
if not self.source.path.samefile(source_parts[0].path):
|
||||||
|
# One or more partitions selected for cloning
|
||||||
|
if settings['Partition Mapping']:
|
||||||
|
for part_map in settings['Partition Mapping']:
|
||||||
|
bp_source = pathlib.Path(
|
||||||
|
f'{self.source.path}{part_prefix}{part_map[0]}',
|
||||||
|
)
|
||||||
|
bp_dest = pathlib.Path(
|
||||||
|
f'{self.destination.path}{part_prefix}{part_map[1]}',
|
||||||
|
)
|
||||||
|
# TODO: add bp(bp_source, bp_dest, map_dir=working_dir)
|
||||||
|
else:
|
||||||
|
# New run and new settings
|
||||||
|
offset = 0
|
||||||
|
if (std.ask('Does the source disk contain an OS?')
|
||||||
|
and std.ask('Create an empty boot partition on the clone?')):
|
||||||
|
offset = 2
|
||||||
|
settings['Needs Format'] = True
|
||||||
|
settings['Table Type'] = 'GPT'
|
||||||
|
if std.choice(['G', 'M'], 'GPT or MBR partition table?') == 'M':
|
||||||
|
offset = 1
|
||||||
|
settings['Table Type'] = 'MBR'
|
||||||
|
|
||||||
|
# Add pairs
|
||||||
|
for dest_num, part in enumerate(source_parts):
|
||||||
|
dest_num += offset + 1
|
||||||
|
bp_source = part.path
|
||||||
|
bp_dest = pathlib.Path(
|
||||||
|
f'{self.destination.path}{part_prefix}{dest_num}',
|
||||||
|
)
|
||||||
|
# TODO: add bp(bp_source, bp_dest, map_dir=working_dir)
|
||||||
|
|
||||||
|
# Add to settings file
|
||||||
|
source_num = re.sub(r'^.*?(\d+)$', r'\1', part.path.name)
|
||||||
|
settings['Partition Mapping'].append([source_num, dest_num])
|
||||||
|
|
||||||
|
# Save settings
|
||||||
|
self.save_settings(settings, working_dir)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Whole device or forced single partition selected, skip settings
|
||||||
|
bp_source = self.source.path
|
||||||
|
bp_dest = self.destination.path
|
||||||
|
# TODO: add bp(bp_source, bp_dest, map_dir=working_dir)
|
||||||
|
|
||||||
|
def add_image_block_pairs(self, source_parts, working_dir):
|
||||||
|
"""Add device to image file block pairs."""
|
||||||
|
for part in source_parts:
|
||||||
|
bp_source = part.path
|
||||||
|
bp_dest = pathlib.Path(f'{self.destination.path}/{part_TODO}.dd')
|
||||||
|
# TODO: add bp(bp_source, bp_dest, map_dir=working_dir)
|
||||||
|
|
||||||
def confirm_selections(self, mode, prompt, map_dir=None):
|
def confirm_selections(self, mode, prompt, map_dir=None):
|
||||||
"""Show selection details and prompt for confirmation."""
|
"""Show selection details and prompt for confirmation."""
|
||||||
report = []
|
report = []
|
||||||
|
|
@ -232,11 +337,10 @@ class State():
|
||||||
working_dir = pathlib.Path(os.getcwd())
|
working_dir = pathlib.Path(os.getcwd())
|
||||||
|
|
||||||
# Add block pairs
|
# Add block pairs
|
||||||
# NOTE: Destination is not updated
|
if mode == 'Clone':
|
||||||
# Load settings/maps
|
self.add_clone_block_pairs(source_parts, working_dir)
|
||||||
# Ask about boot partition
|
else:
|
||||||
# Create pairs using paths
|
self.add_image_block_pairs(source_parts, working_dir)
|
||||||
# TODO
|
|
||||||
|
|
||||||
# Confirmation #2
|
# Confirmation #2
|
||||||
self.confirm_selections(mode, 'Start recovery?', map_dir=working_dir)
|
self.confirm_selections(mode, 'Start recovery?', map_dir=working_dir)
|
||||||
|
|
@ -273,6 +377,25 @@ class State():
|
||||||
# Source / Dest
|
# Source / Dest
|
||||||
self.update_top_panes()
|
self.update_top_panes()
|
||||||
|
|
||||||
|
def load_settings(self, working_dir):
|
||||||
|
# pylint: disable=no-self-use
|
||||||
|
"""Load settings from previous run, returns dict."""
|
||||||
|
settings = {}
|
||||||
|
settings_file = pathlib.Path(f'{working_dir}/clone.json')
|
||||||
|
|
||||||
|
# Try loading JSON data
|
||||||
|
if settings_file.exists():
|
||||||
|
with open(settings_file, 'r') as _f:
|
||||||
|
try:
|
||||||
|
settings = json.loads(_f.read())
|
||||||
|
except (OSError, json.JSONDecodeError):
|
||||||
|
LOG.error('Failed to load clone settings')
|
||||||
|
std.print_error('Invalid clone settings detected.')
|
||||||
|
raise std.GenericAbort()
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return settings
|
||||||
|
|
||||||
def save_debug_reports(self):
|
def save_debug_reports(self):
|
||||||
"""Save debug reports to disk."""
|
"""Save debug reports to disk."""
|
||||||
LOG.info('Saving debug reports')
|
LOG.info('Saving debug reports')
|
||||||
|
|
@ -289,6 +412,19 @@ class State():
|
||||||
with open(f'{debug_dir}/bp_part#.report', 'a') as _f:
|
with open(f'{debug_dir}/bp_part#.report', 'a') as _f:
|
||||||
_f.write('\n'.join(debug.generate_object_report(_bp)))
|
_f.write('\n'.join(debug.generate_object_report(_bp)))
|
||||||
|
|
||||||
|
def save_settings(self, settings, working_dir):
|
||||||
|
# pylint: disable=no-self-use
|
||||||
|
"""Save settings for future runs."""
|
||||||
|
settings_file = pathlib.Path(f'{working_dir}/clone.json')
|
||||||
|
|
||||||
|
# Try saving JSON data
|
||||||
|
try:
|
||||||
|
with open(settings_file, 'w') as _f:
|
||||||
|
json.dump(settings, _f)
|
||||||
|
except OSError:
|
||||||
|
std.print_error('Failed to save clone settings')
|
||||||
|
raise std.GenericAbort()
|
||||||
|
|
||||||
def update_progress_pane(self):
|
def update_progress_pane(self):
|
||||||
"""Update progress pane."""
|
"""Update progress pane."""
|
||||||
report = []
|
report = []
|
||||||
|
|
@ -609,6 +745,7 @@ def get_working_dir(mode, destination):
|
||||||
"""Get working directory using mode and destination, returns path."""
|
"""Get working directory using mode and destination, returns path."""
|
||||||
working_dir = None
|
working_dir = None
|
||||||
if mode == 'Clone':
|
if mode == 'Clone':
|
||||||
|
std.print_info('Mounting backup shares...')
|
||||||
net.mount_backup_shares(read_write=True)
|
net.mount_backup_shares(read_write=True)
|
||||||
for server in cfg.net.BACKUP_SERVERS:
|
for server in cfg.net.BACKUP_SERVERS:
|
||||||
path = pathlib.Path(f'/Backups/{server}')
|
path = pathlib.Path(f'/Backups/{server}')
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue