v1.4.0 - Wizard's New Year
* Added ability to upload crash log to a webdav share * Verified functionailty with Nextcloud 12 * Added missing CBS fix launcher * Rewrote user_data_transfer sections * Added ability to answer "All" to extract items * Fixed issue that caused an infinite recursion involving Windows.old items * Unified code for both image and folder sources by using SourceItem objects * Various bugfixes * Various bugfixes * Improved safety checks for the "Build Linux" script * Updated hw-sensors script to skip all non-temperature sensors * New build-ufd script to combine the three parts of the kit together * See README.md for details * Various bugfixes
This commit is contained in:
commit
f844977d62
27 changed files with 773 additions and 342 deletions
215
.bin/Scripts/build-ufd
Executable file
215
.bin/Scripts/build-ufd
Executable file
|
|
@ -0,0 +1,215 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
## Wizard Kit: UFD Build Tool
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o errtrace
|
||||||
|
set -o nounset
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
DEST_DEV="$1"
|
||||||
|
DEST_PARTITION="${DEST_DEV}1"
|
||||||
|
MAIN_PY="/usr/local/bin/settings/main.py"
|
||||||
|
LOG_FILE="${HOME}/build-ufd_${DEST_DEV##*/}_$(date +%Y-%m-%d_%H%M_%z).log"
|
||||||
|
RSYNC_ARGS="-hrtuvS --modify-window=1 --progress"
|
||||||
|
WD=$(pwd)
|
||||||
|
EXTRA_DIR="${WD}/Extras"
|
||||||
|
MAIN_KIT="$(dirname $(find $WD -type d -name '.bin' || true) 2>/dev/null || true)"
|
||||||
|
LINUX_ISO="$((find $WD -maxdepth 1 -type f -iname '*Linux*iso' 2>/dev/null || echo "__Missing__") | sort -r | head -1)"
|
||||||
|
WINPE_ISO="$((find $WD -maxdepth 1 -type f -iname '*WinPE*amd64*iso' 2>/dev/null || echo "__Missing__") | sort -r | head -1)"
|
||||||
|
if [ "${2:-}" == "--silent" ]; then
|
||||||
|
SILENT="True"
|
||||||
|
else
|
||||||
|
SILENT="False"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# COLORS
|
||||||
|
CLEAR="\e[0m"
|
||||||
|
RED="\e[31m"
|
||||||
|
GREEN="\e[32m"
|
||||||
|
YELLOW="\e[33m"
|
||||||
|
BLUE="\e[34m"
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
function ask() {
|
||||||
|
if [[ "${SILENT}" == "True" ]]; then
|
||||||
|
echo -e "${1:-} Yes ${BLUE}(Silent)${CLEAR}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
while :; do
|
||||||
|
read -p "${1:-} [Y/N] " -r answer
|
||||||
|
if echo "$answer" | egrep -iq '^(y|yes|sure)$'; then
|
||||||
|
return 0
|
||||||
|
elif echo "$answer" | egrep -iq '^(n|no|nope)$'; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo "$@" >&2
|
||||||
|
echo ""
|
||||||
|
read -p "Press Enter to exit... " -r
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load main.py settings
|
||||||
|
if [ ! -f "$MAIN_PY" ]; then
|
||||||
|
echo -e "${RED}ERROR${CLEAR}: $MAIN_PY not found."
|
||||||
|
die "Aborted."
|
||||||
|
fi
|
||||||
|
while read line; do
|
||||||
|
if echo "$line" | egrep -q "^\w+='"; then
|
||||||
|
line="$(echo "$line" | sed -r 's/[\r\n]+//')"
|
||||||
|
eval "$line"
|
||||||
|
fi
|
||||||
|
done < "$MAIN_PY"
|
||||||
|
if [ -z ${KIT_NAME_FULL+x} ]; then
|
||||||
|
# KIT_NAME_FULL is not set, assume main.py missing or malformatted
|
||||||
|
echo -e "${RED}ERROR${CLEAR}: failed to load settings from $MAIN_PY"
|
||||||
|
die "Aborted."
|
||||||
|
fi
|
||||||
|
UFD_LABEL="${KIT_NAME_SHORT}_LINUX"
|
||||||
|
|
||||||
|
# Check if root
|
||||||
|
if [[ "$EUID" -ne 0 ]]; then
|
||||||
|
echo -e "${RED}ERROR${CLEAR}: This script must be run as root."
|
||||||
|
die "Aborted."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if in tmux
|
||||||
|
if ! tmux list-session 2>/dev/null | grep -q "build-ufd"; then
|
||||||
|
# Reload in tmux
|
||||||
|
tmux new-session -s "build-ufd" "${0:-}" $*
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Header
|
||||||
|
echo -e "${GREEN}$KIT_NAME_FULL${CLEAR}: UFD Build Tool"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Dest and Sources Check
|
||||||
|
abort="False"
|
||||||
|
if [ ! -b "$DEST_DEV" ]; then
|
||||||
|
echo -e "${RED}ERROR${CLEAR}: Device $DEST_DEV not found."
|
||||||
|
abort="True"
|
||||||
|
fi
|
||||||
|
if [ ! -f "$LINUX_ISO" ]; then
|
||||||
|
echo -e "${RED}ERROR${CLEAR}: Linux ISO not found."
|
||||||
|
abort="True"
|
||||||
|
fi
|
||||||
|
if [ ! -f "$WINPE_ISO" ]; then
|
||||||
|
echo -e "${RED}ERROR${CLEAR}: WinPE ISO not found."
|
||||||
|
abort="True"
|
||||||
|
fi
|
||||||
|
if [ ! -d "$MAIN_KIT" ]; then
|
||||||
|
echo -e "${RED}ERROR${CLEAR}: Wizard Kit directory not found."
|
||||||
|
abort="True"
|
||||||
|
fi
|
||||||
|
if [ ! -d "$EXTRA_DIR" ]; then
|
||||||
|
# Warn but don't abort
|
||||||
|
echo -e "${YELLOW}WARNING${CLEAR}: $EXTRA_DIR not found."
|
||||||
|
echo ""
|
||||||
|
EXTRA_DIR='__None__'
|
||||||
|
fi
|
||||||
|
if [ "$abort" == "True" ]; then
|
||||||
|
echo ""
|
||||||
|
die "Aborted."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Display Job Settings
|
||||||
|
echo -e "${BLUE}Sources${CLEAR}"
|
||||||
|
echo "Main Kit: $MAIN_KIT"
|
||||||
|
echo "Linux ISO: $LINUX_ISO"
|
||||||
|
echo "WinPE ISO: $WINPE_ISO"
|
||||||
|
echo "Extras: $EXTRA_DIR"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}Destination${CLEAR}"
|
||||||
|
lsblk -n -o NAME,LABEL,SIZE,MODEL,SERIAL $DEST_DEV
|
||||||
|
|
||||||
|
# Ask before starting job
|
||||||
|
echo ""
|
||||||
|
if ask "Is the above information correct?"; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}SAFETY CHECK${CLEAR}"
|
||||||
|
echo "All data will be DELETED from the disk and partition(s) listed above."
|
||||||
|
echo -e "This is irreversible and will lead to ${RED}DATA LOSS.${CLEAR}"
|
||||||
|
if ! ask "Asking again to confirm, is this correct?"; then
|
||||||
|
die "Aborted."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
die "Aborted."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start Build
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}Building Kit${CLEAR}"
|
||||||
|
touch "$LOG_FILE"
|
||||||
|
tmux split-window -dl 10 tail -f "$LOG_FILE"
|
||||||
|
|
||||||
|
# Format
|
||||||
|
echo "Formatting drive..."
|
||||||
|
parted "$DEST_DEV" -s -- mklabel msdos mkpart primary fat32 1MiB -1s >> "$LOG_FILE" 2>&1
|
||||||
|
parted "$DEST_DEV" set 1 boot on >> "$LOG_FILE" 2>&1
|
||||||
|
mkfs.vfat -F 32 -n "$UFD_LABEL" "$DEST_PARTITION" >> "$LOG_FILE" 2>&1
|
||||||
|
|
||||||
|
# Mount sources and dest
|
||||||
|
echo "Mounting sources and destination..."
|
||||||
|
mkdir /mnt/{Dest,Linux,WinPE} -p >> "$LOG_FILE" 2>&1
|
||||||
|
mount $DEST_PARTITION /mnt/Dest >> "$LOG_FILE" 2>&1
|
||||||
|
mount "$LINUX_ISO" /mnt/Linux -r >> "$LOG_FILE" 2>&1
|
||||||
|
mount "$WINPE_ISO" /mnt/WinPE -r >> "$LOG_FILE" 2>&1
|
||||||
|
|
||||||
|
# Copy files
|
||||||
|
echo "Copying Linux files..."
|
||||||
|
rsync ${RSYNC_ARGS} /mnt/Linux/* /mnt/Dest/ >> "$LOG_FILE" 2>&1
|
||||||
|
|
||||||
|
echo "Copying WinPE files..."
|
||||||
|
rsync ${RSYNC_ARGS} /mnt/WinPE/{Boot,bootmgr{,.efi},en-us,sources} /mnt/Dest/ >> "$LOG_FILE" 2>&1
|
||||||
|
rsync ${RSYNC_ARGS} /mnt/WinPE/EFI/Microsoft /mnt/Dest/EFI/ >> "$LOG_FILE" 2>&1
|
||||||
|
rsync ${RSYNC_ARGS} /mnt/WinPE/EFI/Boot/* /mnt/Dest/EFI/Microsoft/ >> "$LOG_FILE" 2>&1
|
||||||
|
rsync ${RSYNC_ARGS} /mnt/WinPE/{Boot/{BCD,boot.sdi},bootmgr} /mnt/Dest/sources/ >> "$LOG_FILE" 2>&1
|
||||||
|
|
||||||
|
echo "Copying Main Kit..."
|
||||||
|
rsync ${RSYNC_ARGS} "$MAIN_KIT/" "/mnt/Dest/$KIT_NAME_FULL/" >> "$LOG_FILE" 2>&1
|
||||||
|
if [ "$EXTRA_DIR" != "__None__" ]; then
|
||||||
|
echo "Copying Extra files..."
|
||||||
|
rsync ${RSYNC_ARGS} "$EXTRA_DIR"/* /mnt/Dest/ >> "$LOG_FILE" 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install syslinux
|
||||||
|
echo "Copying Syslinux files..."
|
||||||
|
rsync ${RSYNC_ARGS} /usr/lib/syslinux/bios/*.c32 /mnt/Dest/arch/boot/syslinux/ >> "$LOG_FILE" 2>&1
|
||||||
|
syslinux --install -d /arch/boot/syslinux/ $DEST_PARTITION >> "$LOG_FILE" 2>&1
|
||||||
|
|
||||||
|
echo "Unmounting destination..."
|
||||||
|
umount /mnt/Dest >> "$LOG_FILE" 2>&1
|
||||||
|
sync
|
||||||
|
|
||||||
|
echo "Installing Syslinux MBR..."
|
||||||
|
dd bs=440 count=1 if=/usr/lib/syslinux/bios/mbr.bin of=$DEST_DEV >> "$LOG_FILE" 2>&1
|
||||||
|
sync
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
echo "Hiding boot files..."
|
||||||
|
echo "drive s: file=\"$DEST_PARTITION\"" > /root/.mtoolsrc
|
||||||
|
echo 'mtools_skip_check=1' >> /root/.mtoolsrc
|
||||||
|
for item in boot{,mgr,mgr.efi} efi en-us images isolinux sources "$KIT_NAME_FULL"/{.bin,.cbin}; do
|
||||||
|
yes | mattrib +h "S:/$item" >> "$LOG_FILE" 2>&1 || true
|
||||||
|
done
|
||||||
|
sync
|
||||||
|
|
||||||
|
# Unmount Sources
|
||||||
|
echo "Unmounting sources..."
|
||||||
|
umount /mnt/{Linux,WinPE} -R >> "$LOG_FILE" 2>&1
|
||||||
|
|
||||||
|
# Close progress pane
|
||||||
|
pkill -f "tail.*$LOG_FILE"
|
||||||
|
|
||||||
|
# Done
|
||||||
|
echo ""
|
||||||
|
echo "Done."
|
||||||
|
echo ""
|
||||||
|
read -p "Press Enter to exit..." -r
|
||||||
|
exit 0
|
||||||
|
|
||||||
|
|
@ -10,7 +10,7 @@ $Host.UI.RawUI.WindowTitle = "Wizard Kit: Windows PE Build Tool"
|
||||||
$WD = $(Split-Path $MyInvocation.MyCommand.Path)
|
$WD = $(Split-Path $MyInvocation.MyCommand.Path)
|
||||||
$Bin = (Get-Item $WD -Force).Parent.FullName
|
$Bin = (Get-Item $WD -Force).Parent.FullName
|
||||||
$Root = (Get-Item $Bin -Force).Parent.FullName
|
$Root = (Get-Item $Bin -Force).Parent.FullName
|
||||||
$Build = "$Root\BUILD"
|
$Build = "$Root\BUILD_PE"
|
||||||
$LogDir = "$Build\Logs"
|
$LogDir = "$Build\Logs"
|
||||||
$Temp = "$Build\Temp"
|
$Temp = "$Build\Temp"
|
||||||
$Date = Get-Date -UFormat "%Y-%m-%d"
|
$Date = Get-Date -UFormat "%Y-%m-%d"
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ if __name__ == '__main__':
|
||||||
|
|
||||||
# Show details
|
# Show details
|
||||||
print_info('{}: CBS Cleanup Tool\n'.format(KIT_NAME_FULL))
|
print_info('{}: CBS Cleanup Tool\n'.format(KIT_NAME_FULL))
|
||||||
show_info('Backup / Temp path:', dest)
|
show_data('Backup / Temp path:', dest)
|
||||||
print_standard('\n')
|
print_standard('\n')
|
||||||
if (not ask('Proceed with CBS cleanup?')):
|
if (not ask('Proceed with CBS cleanup?')):
|
||||||
abort()
|
abort()
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ REGEX_BAD_PATH_NAMES = re.compile(
|
||||||
re.IGNORECASE)
|
re.IGNORECASE)
|
||||||
|
|
||||||
def backup_partition(disk, par):
|
def backup_partition(disk, par):
|
||||||
|
"""Create a backup image of a partition."""
|
||||||
if par.get('Image Exists', False) or par['Number'] in disk['Bad Partitions']:
|
if par.get('Image Exists', False) or par['Number'] in disk['Bad Partitions']:
|
||||||
raise GenericAbort
|
raise GenericAbort
|
||||||
|
|
||||||
|
|
@ -28,9 +29,15 @@ def backup_partition(disk, par):
|
||||||
run_program(cmd)
|
run_program(cmd)
|
||||||
|
|
||||||
def fix_path(path):
|
def fix_path(path):
|
||||||
|
"""Replace invalid filename characters with underscores."""
|
||||||
return REGEX_BAD_PATH_NAMES.sub('_', path)
|
return REGEX_BAD_PATH_NAMES.sub('_', path)
|
||||||
|
|
||||||
def prep_disk_for_backup(destination, disk, ticket_number):
|
def prep_disk_for_backup(destination, disk, ticket_number):
|
||||||
|
"""Gather details about the disk and its partitions.
|
||||||
|
|
||||||
|
This includes partitions that can't be backed up,
|
||||||
|
whether backups already exist on the BACKUP_SERVER,
|
||||||
|
partition names/sizes/used space, etc."""
|
||||||
disk['Clobber Risk'] = []
|
disk['Clobber Risk'] = []
|
||||||
width = len(str(len(disk['Partitions'])))
|
width = len(str(len(disk['Partitions'])))
|
||||||
|
|
||||||
|
|
@ -102,7 +109,7 @@ def prep_disk_for_backup(destination, disk, ticket_number):
|
||||||
disk['Backup Warnings'] = warnings
|
disk['Backup Warnings'] = warnings
|
||||||
|
|
||||||
def select_backup_destination(auto_select=True):
|
def select_backup_destination(auto_select=True):
|
||||||
# Build menu
|
"""Select a backup destination from a menu, returns server dict."""
|
||||||
destinations = [s for s in BACKUP_SERVERS if s['Mounted']]
|
destinations = [s for s in BACKUP_SERVERS if s['Mounted']]
|
||||||
actions = [
|
actions = [
|
||||||
{'Name': 'Main Menu', 'Letter': 'M'},
|
{'Name': 'Main Menu', 'Letter': 'M'},
|
||||||
|
|
@ -136,6 +143,7 @@ def select_backup_destination(auto_select=True):
|
||||||
return destinations[int(selection)-1]
|
return destinations[int(selection)-1]
|
||||||
|
|
||||||
def verify_wim_backup(partition):
|
def verify_wim_backup(partition):
|
||||||
|
"""Verify WIM integrity."""
|
||||||
if not os.path.exists(partition['Image Path']):
|
if not os.path.exists(partition['Image Path']):
|
||||||
raise PathNotFoundError
|
raise PathNotFoundError
|
||||||
cmd = [
|
cmd = [
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,34 @@ def ask(prompt='Kotaero!'):
|
||||||
print_log(message=message)
|
print_log(message=message)
|
||||||
return answer
|
return answer
|
||||||
|
|
||||||
|
def choice(choices, prompt='Kotaero!'):
|
||||||
|
"""Prompt the user with a choice question, log answer, and returns str."""
|
||||||
|
answer = None
|
||||||
|
choices = [str(c) for c in choices]
|
||||||
|
choices_short = {c[:1].upper(): c for c in choices}
|
||||||
|
prompt = '{} [{}]: '.format(prompt, '/'.join(choices))
|
||||||
|
regex = '^({}|{})$'.format(
|
||||||
|
'|'.join([c[:1] for c in choices]),
|
||||||
|
'|'.join(choices))
|
||||||
|
|
||||||
|
# Get user's choice
|
||||||
|
while answer is None:
|
||||||
|
tmp = input(prompt)
|
||||||
|
if re.search(regex, tmp, re.IGNORECASE):
|
||||||
|
answer = tmp
|
||||||
|
|
||||||
|
# Log result
|
||||||
|
message = '{prompt}{answer_text}'.format(
|
||||||
|
prompt = prompt,
|
||||||
|
answer_text = 'Yes' if answer else 'No')
|
||||||
|
print_log(message=message)
|
||||||
|
|
||||||
|
# Fix answer formatting to match provided values
|
||||||
|
answer = choices_short[answer[:1].upper()]
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return answer
|
||||||
|
|
||||||
def clear_screen():
|
def clear_screen():
|
||||||
"""Simple wrapper for cls/clear."""
|
"""Simple wrapper for cls/clear."""
|
||||||
if psutil.WINDOWS:
|
if psutil.WINDOWS:
|
||||||
|
|
@ -227,7 +255,20 @@ def major_exception():
|
||||||
print_warning(SUPPORT_MESSAGE)
|
print_warning(SUPPORT_MESSAGE)
|
||||||
print(traceback.format_exc())
|
print(traceback.format_exc())
|
||||||
print_log(traceback.format_exc())
|
print_log(traceback.format_exc())
|
||||||
sleep(30)
|
try:
|
||||||
|
upload_crash_details()
|
||||||
|
except GenericAbort:
|
||||||
|
# User declined upload
|
||||||
|
print_warning('Upload: Aborted')
|
||||||
|
sleep(30)
|
||||||
|
except GenericError:
|
||||||
|
# No log file or uploading disabled
|
||||||
|
sleep(30)
|
||||||
|
except:
|
||||||
|
print_error('Upload: NS')
|
||||||
|
sleep(30)
|
||||||
|
else:
|
||||||
|
print_success('Upload: CS')
|
||||||
pause('Press Enter to exit...')
|
pause('Press Enter to exit...')
|
||||||
exit_script(1)
|
exit_script(1)
|
||||||
|
|
||||||
|
|
@ -358,6 +399,7 @@ def print_warning(*args, **kwargs):
|
||||||
print_standard(*args, color=COLORS['YELLOW'], **kwargs)
|
print_standard(*args, color=COLORS['YELLOW'], **kwargs)
|
||||||
|
|
||||||
def print_log(message='', end='\n', timestamp=True):
|
def print_log(message='', end='\n', timestamp=True):
|
||||||
|
"""Writes message to a log if LogFile is set."""
|
||||||
time_str = time.strftime("%Y-%m-%d %H%M%z: ") if timestamp else ''
|
time_str = time.strftime("%Y-%m-%d %H%M%z: ") if timestamp else ''
|
||||||
if 'LogFile' in global_vars and global_vars['LogFile']:
|
if 'LogFile' in global_vars and global_vars['LogFile']:
|
||||||
with open(global_vars['LogFile'], 'a', encoding='utf-8') as f:
|
with open(global_vars['LogFile'], 'a', encoding='utf-8') as f:
|
||||||
|
|
@ -406,9 +448,6 @@ def show_data(message='~Some message~', data='~Some data~', indent=8, width=32,
|
||||||
else:
|
else:
|
||||||
print_standard(message)
|
print_standard(message)
|
||||||
|
|
||||||
def show_info(message='~Some message~', info='~Some info~', indent=8, width=32):
|
|
||||||
show_data(message=message, data=info, indent=indent, width=width)
|
|
||||||
|
|
||||||
def sleep(seconds=2):
|
def sleep(seconds=2):
|
||||||
"""Wait for a while."""
|
"""Wait for a while."""
|
||||||
time.sleep(seconds)
|
time.sleep(seconds)
|
||||||
|
|
@ -487,58 +526,43 @@ def try_and_print(message='Trying...',
|
||||||
else:
|
else:
|
||||||
return {'CS': not bool(err), 'Error': err, 'Out': out}
|
return {'CS': not bool(err), 'Error': err, 'Out': out}
|
||||||
|
|
||||||
def upload_data(path, file):
|
def upload_crash_details():
|
||||||
"""Add CLIENT_INFO_SERVER to authorized connections and upload file."""
|
"""Upload log and runtime data to the CRASH_SERVER.
|
||||||
if not ENABLED_UPLOAD_DATA:
|
|
||||||
raise GenericError('Feature disabled.')
|
|
||||||
|
|
||||||
extract_item('PuTTY', filter='wizkit.ppk psftp.exe', silent=True)
|
Intended for uploading to a public Nextcloud share."""
|
||||||
|
|
||||||
# Authorize connection to the server
|
|
||||||
winreg.CreateKey(HKCU, r'Software\SimonTatham\PuTTY\SshHostKeys')
|
|
||||||
with winreg.OpenKey(HKCU, r'Software\SimonTatham\PuTTY\SshHostKeys',
|
|
||||||
access=winreg.KEY_WRITE) as key:
|
|
||||||
winreg.SetValueEx(key,
|
|
||||||
'rsa2@22:{IP}'.format(**CLIENT_INFO_SERVER), 0,
|
|
||||||
winreg.REG_SZ, CLIENT_INFO_SERVER['RegEntry'])
|
|
||||||
|
|
||||||
# Write batch file
|
|
||||||
with open(r'{}\psftp.batch'.format(global_vars['TmpDir']),
|
|
||||||
'w', encoding='ascii') as f:
|
|
||||||
f.write('lcd "{path}"\n'.format(path=path))
|
|
||||||
f.write('cd "{Share}"\n'.format(**CLIENT_INFO_SERVER))
|
|
||||||
f.write('mkdir {TicketNumber}\n'.format(**global_vars))
|
|
||||||
f.write('cd {TicketNumber}\n'.format(**global_vars))
|
|
||||||
f.write('put "{file}"\n'.format(file=file))
|
|
||||||
|
|
||||||
# Upload Info
|
|
||||||
cmd = [
|
|
||||||
global_vars['Tools']['PuTTY-PSFTP'],
|
|
||||||
'-noagent',
|
|
||||||
'-i', r'{BinDir}\PuTTY\wizkit.ppk'.format(**global_vars),
|
|
||||||
'{User}@{IP}'.format(**CLIENT_INFO_SERVER),
|
|
||||||
'-b', r'{TmpDir}\psftp.batch'.format(**global_vars)]
|
|
||||||
run_program(cmd)
|
|
||||||
|
|
||||||
def upload_info():
|
|
||||||
"""Upload compressed Info file to the NAS as set in settings.main.py."""
|
|
||||||
if not ENABLED_UPLOAD_DATA:
|
if not ENABLED_UPLOAD_DATA:
|
||||||
raise GenericError('Feature disabled.')
|
raise GenericError
|
||||||
|
|
||||||
path = '{ClientDir}'.format(**global_vars)
|
|
||||||
file = 'Info_{Date-Time}.7z'.format(**global_vars)
|
|
||||||
upload_data(path, file)
|
|
||||||
|
|
||||||
def compress_info():
|
import requests
|
||||||
"""Compress ClientDir info folders with 7-Zip for upload_info()."""
|
if 'LogFile' in global_vars and global_vars['LogFile']:
|
||||||
path = '{ClientDir}'.format(**global_vars)
|
if ask('Upload crash details to {}?'.format(CRASH_SERVER['Name'])):
|
||||||
file = 'Info_{Date-Time}.7z'.format(**global_vars)
|
with open(global_vars['LogFile']) as f:
|
||||||
_cmd = [
|
data = '''{}
|
||||||
global_vars['Tools']['SevenZip'],
|
#############################
|
||||||
'a', '-t7z', '-mx=9', '-bso0', '-bse0',
|
Runtime Details:
|
||||||
r'{}\{}'.format(path, file),
|
|
||||||
r'{ClientDir}\Info'.format(**global_vars)]
|
sys.argv: {}
|
||||||
run_program(_cmd)
|
|
||||||
|
global_vars: {}'''.format(f.read(), sys.argv, global_vars)
|
||||||
|
filename = global_vars.get('LogFile', 'Unknown')
|
||||||
|
filename = re.sub(r'.*(\\|/)', '', filename)
|
||||||
|
filename += '.txt'
|
||||||
|
url = '{}/Crash_{}__{}'.format(
|
||||||
|
CRASH_SERVER['Url'],
|
||||||
|
global_vars.get('Date-Time', 'Unknown Date-Time'),
|
||||||
|
filename)
|
||||||
|
r = requests.put(url, data=data,
|
||||||
|
headers = {'X-Requested-With': 'XMLHttpRequest'},
|
||||||
|
auth = (CRASH_SERVER['User'], CRASH_SERVER['Pass']))
|
||||||
|
# Raise exception if upload NS
|
||||||
|
if not r.ok:
|
||||||
|
raise Exception
|
||||||
|
else:
|
||||||
|
# User said no
|
||||||
|
raise GenericAbort
|
||||||
|
else:
|
||||||
|
# No LogFile defined (or invalid LogFile)
|
||||||
|
raise GenericError
|
||||||
|
|
||||||
def wait_for_process(name, poll_rate=3):
|
def wait_for_process(name, poll_rate=3):
|
||||||
"""Wait for process by name."""
|
"""Wait for process by name."""
|
||||||
|
|
@ -742,6 +766,9 @@ def set_common_vars():
|
||||||
**global_vars)
|
**global_vars)
|
||||||
|
|
||||||
def set_linux_vars():
|
def set_linux_vars():
|
||||||
|
"""Set common variables in a Linux environment.
|
||||||
|
|
||||||
|
These assume we're running under a WK-Linux build."""
|
||||||
result = run_program(['mktemp', '-d'])
|
result = run_program(['mktemp', '-d'])
|
||||||
global_vars['TmpDir'] = result.stdout.decode().strip()
|
global_vars['TmpDir'] = result.stdout.decode().strip()
|
||||||
global_vars['Date'] = time.strftime("%Y-%m-%d")
|
global_vars['Date'] = time.strftime("%Y-%m-%d")
|
||||||
|
|
@ -749,6 +776,10 @@ def set_linux_vars():
|
||||||
global_vars['Env'] = os.environ.copy()
|
global_vars['Env'] = os.environ.copy()
|
||||||
global_vars['BinDir'] = '/usr/local/bin'
|
global_vars['BinDir'] = '/usr/local/bin'
|
||||||
global_vars['LogDir'] = global_vars['TmpDir']
|
global_vars['LogDir'] = global_vars['TmpDir']
|
||||||
|
global_vars['Tools'] = {
|
||||||
|
'wimlib-imagex': 'wimlib-imagex',
|
||||||
|
'SevenZip': '7z',
|
||||||
|
}
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
print("This file is not meant to be called directly.")
|
print("This file is not meant to be called directly.")
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,11 @@ class LocalDisk():
|
||||||
# Should always be true
|
# Should always be true
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
class SourceItem():
|
||||||
|
def __init__(self, name, path):
|
||||||
|
self.name = name
|
||||||
|
self.path = path
|
||||||
|
|
||||||
# Regex
|
# Regex
|
||||||
REGEX_EXCL_ITEMS = re.compile(
|
REGEX_EXCL_ITEMS = re.compile(
|
||||||
r'^(\.(AppleDB|AppleDesktop|AppleDouble'
|
r'^(\.(AppleDB|AppleDesktop|AppleDouble'
|
||||||
|
|
@ -29,7 +34,7 @@ REGEX_EXCL_ITEMS = re.compile(
|
||||||
r'|Thumbs\.db)$',
|
r'|Thumbs\.db)$',
|
||||||
re.IGNORECASE)
|
re.IGNORECASE)
|
||||||
REGEX_EXCL_ROOT_ITEMS = re.compile(
|
REGEX_EXCL_ROOT_ITEMS = re.compile(
|
||||||
r'^\\?(boot(mgr|nxt)$|Config.msi'
|
r'^(boot(mgr|nxt)$|Config.msi'
|
||||||
r'|(eula|globdata|install|vc_?red)'
|
r'|(eula|globdata|install|vc_?red)'
|
||||||
r'|.*.sys$|System Volume Information|RECYCLER?|\$Recycle\.bin'
|
r'|.*.sys$|System Volume Information|RECYCLER?|\$Recycle\.bin'
|
||||||
r'|\$?Win(dows(.old.*|\.~BT|)$|RE_)|\$GetCurrent|Windows10Upgrade'
|
r'|\$?Win(dows(.old.*|\.~BT|)$|RE_)|\$GetCurrent|Windows10Upgrade'
|
||||||
|
|
@ -37,7 +42,7 @@ REGEX_EXCL_ROOT_ITEMS = re.compile(
|
||||||
r'|.*\.(esd|swm|wim|dd|map|dmg|image)$)',
|
r'|.*\.(esd|swm|wim|dd|map|dmg|image)$)',
|
||||||
re.IGNORECASE)
|
re.IGNORECASE)
|
||||||
REGEX_INCL_ROOT_ITEMS = re.compile(
|
REGEX_INCL_ROOT_ITEMS = re.compile(
|
||||||
r'^\\?(AdwCleaner|(My\s*|)(Doc(uments?( and Settings|)|s?)|Downloads'
|
r'^(AdwCleaner|(My\s*|)(Doc(uments?( and Settings|)|s?)|Downloads'
|
||||||
r'|Media|Music|Pic(ture|)s?|Vid(eo|)s?)'
|
r'|Media|Music|Pic(ture|)s?|Vid(eo|)s?)'
|
||||||
r'|{prefix}(-?Info|-?Transfer|)'
|
r'|{prefix}(-?Info|-?Transfer|)'
|
||||||
r'|(ProgramData|Recovery|Temp.*|Users)$'
|
r'|(ProgramData|Recovery|Temp.*|Users)$'
|
||||||
|
|
@ -48,7 +53,7 @@ REGEX_WIM_FILE = re.compile(
|
||||||
r'\.wim$',
|
r'\.wim$',
|
||||||
re.IGNORECASE)
|
re.IGNORECASE)
|
||||||
REGEX_WINDOWS_OLD = re.compile(
|
REGEX_WINDOWS_OLD = re.compile(
|
||||||
r'^\\Win(dows|)\.old',
|
r'^Win(dows|)\.old',
|
||||||
re.IGNORECASE)
|
re.IGNORECASE)
|
||||||
|
|
||||||
# STATIC VARIABLES
|
# STATIC VARIABLES
|
||||||
|
|
@ -234,7 +239,7 @@ def mount_all_volumes():
|
||||||
line.append(info)
|
line.append(info)
|
||||||
return report
|
return report
|
||||||
|
|
||||||
def mount_backup_shares():
|
def mount_backup_shares(read_write=False):
|
||||||
"""Mount the backup shares unless labeled as already mounted."""
|
"""Mount the backup shares unless labeled as already mounted."""
|
||||||
if psutil.LINUX:
|
if psutil.LINUX:
|
||||||
mounted_data = get_mounted_data()
|
mounted_data = get_mounted_data()
|
||||||
|
|
@ -253,12 +258,22 @@ def mount_backup_shares():
|
||||||
print_warning(mounted_str)
|
print_warning(mounted_str)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
mount_network_share(server)
|
mount_network_share(server, read_write)
|
||||||
|
|
||||||
def mount_network_share(server):
|
def mount_network_share(server, read_write=False):
|
||||||
"""Mount a network share defined by server."""
|
"""Mount a network share defined by server."""
|
||||||
|
if read_write:
|
||||||
|
username = server['RW-User']
|
||||||
|
password = server['RW-Pass']
|
||||||
|
else:
|
||||||
|
username = server['User']
|
||||||
|
password = server['Pass']
|
||||||
if psutil.WINDOWS:
|
if psutil.WINDOWS:
|
||||||
cmd = r'net use \\{IP}\{Share} /user:{User} {Pass}'.format(**server)
|
cmd = r'net use \\{ip}\{share} /user:{username} {password}'.format(
|
||||||
|
ip = server['IP'],
|
||||||
|
share = server['Share'],
|
||||||
|
username = username,
|
||||||
|
password = password)
|
||||||
cmd = cmd.split(' ')
|
cmd = cmd.split(' ')
|
||||||
warning = r'Failed to mount \\{Name}\{Share}, {IP} unreachable.'.format(
|
warning = r'Failed to mount \\{Name}\{Share}, {IP} unreachable.'.format(
|
||||||
**server)
|
**server)
|
||||||
|
|
@ -273,7 +288,10 @@ def mount_network_share(server):
|
||||||
'sudo', 'mount',
|
'sudo', 'mount',
|
||||||
'//{IP}/{Share}'.format(**server),
|
'//{IP}/{Share}'.format(**server),
|
||||||
'/Backups/{Name}'.format(**server),
|
'/Backups/{Name}'.format(**server),
|
||||||
'-o', 'username={User},password={Pass}'.format(**server)]
|
'-o', '{}username={},password={}'.format(
|
||||||
|
'' if read_write else 'ro,',
|
||||||
|
username,
|
||||||
|
password)]
|
||||||
warning = 'Failed to mount /Backups/{Name}, {IP} unreachable.'.format(
|
warning = 'Failed to mount /Backups/{Name}, {IP} unreachable.'.format(
|
||||||
**server)
|
**server)
|
||||||
error = 'Failed to mount /Backups/{Name}'.format(**server)
|
error = 'Failed to mount /Backups/{Name}'.format(**server)
|
||||||
|
|
@ -334,150 +352,163 @@ def run_wimextract(source, items, dest):
|
||||||
'--nullglob']
|
'--nullglob']
|
||||||
run_program(cmd)
|
run_program(cmd)
|
||||||
|
|
||||||
def scan_source(source_obj, dest_path):
|
def list_source_items(source_obj, rel_path=None):
|
||||||
"""Scan source for files/folders to transfer."""
|
"""List items in a dir or WIM, returns a list of SourceItem objects."""
|
||||||
selected_items = []
|
items = []
|
||||||
|
rel_path = '{}{}'.format(os.sep, rel_path) if rel_path else ''
|
||||||
if source_obj.is_dir():
|
if source_obj.is_dir():
|
||||||
# File-Based
|
source_path = '{}{}'.format(source_obj.path, rel_path)
|
||||||
print_standard('Scanning source (folder): {}'.format(source_obj.path))
|
items = [SourceItem(name=item.name, path=item.path)
|
||||||
selected_items = scan_source_path(source_obj.path, dest_path)
|
for item in os.scandir(source_path)]
|
||||||
else:
|
else:
|
||||||
# Image-Based
|
# Prep wimlib-imagex
|
||||||
if REGEX_WIM_FILE.search(source_obj.name):
|
if psutil.WINDOWS:
|
||||||
print_standard('Scanning source (image): {}'.format(
|
extract_item('wimlib', silent=True)
|
||||||
source_obj.path))
|
cmd = [
|
||||||
selected_items = scan_source_wim(source_obj.path, dest_path)
|
global_vars['Tools']['wimlib-imagex'], 'dir',
|
||||||
else:
|
source_obj.path, '1']
|
||||||
print_error('ERROR: Unsupported image: {}'.format(
|
if rel_path:
|
||||||
source_obj.path))
|
cmd.append('--path={}'.format(rel_path))
|
||||||
raise GenericError
|
|
||||||
|
|
||||||
return selected_items
|
|
||||||
|
|
||||||
def scan_source_path(source_path, dest_path, rel_path=None, interactive=True):
|
# Get item list
|
||||||
"""Scan source folder for files/folders to transfer, returns list.
|
try:
|
||||||
|
items = run_program(cmd)
|
||||||
This will scan the root and (recursively) any Windows.old folders."""
|
except subprocess.CalledProcessError:
|
||||||
rel_path = '\\' + rel_path if rel_path else ''
|
print_error('ERROR: Failed to get file list.')
|
||||||
if rel_path:
|
raise
|
||||||
dest_path = dest_path + rel_path
|
|
||||||
selected_items = []
|
|
||||||
win_olds = []
|
|
||||||
|
|
||||||
# Root items
|
# Strip non-root items
|
||||||
root_items = []
|
items = [re.sub(r'(\\|/)', os.sep, i.strip())
|
||||||
for item in os.scandir(source_path):
|
for i in items.stdout.decode('utf-8', 'ignore').splitlines()]
|
||||||
if REGEX_INCL_ROOT_ITEMS.search(item.name):
|
if rel_path:
|
||||||
root_items.append(item.path)
|
items = [i.replace(rel_path, '') for i in items]
|
||||||
elif not REGEX_EXCL_ROOT_ITEMS.search(item.name):
|
items = [i for i in items
|
||||||
if (not interactive
|
if i.count(os.sep) == 1 and i.strip() != os.sep]
|
||||||
or ask('Copy: "{}{}" ?'.format(rel_path, item.name))):
|
items = [SourceItem(name=i[1:], path=rel_path+i) for i in items]
|
||||||
root_items.append(item.path)
|
|
||||||
if REGEX_WINDOWS_OLD.search(item.name):
|
|
||||||
win_olds.append(item)
|
|
||||||
if root_items:
|
|
||||||
selected_items.append({
|
|
||||||
'Message': '{}Root Items...'.format(rel_path),
|
|
||||||
'Items': root_items.copy(),
|
|
||||||
'Destination': dest_path})
|
|
||||||
|
|
||||||
# Fonts
|
|
||||||
if os.path.exists(r'{}\Windows\Fonts'.format(source_path)):
|
|
||||||
selected_items.append({
|
|
||||||
'Message': '{}Fonts...'.format(rel_path),
|
|
||||||
'Items': [r'{}\Windows\Fonts'.format(rel_path)],
|
|
||||||
'Destination': r'{}\Windows'.format(dest_path)})
|
|
||||||
|
|
||||||
# Registry
|
|
||||||
registry_items = []
|
|
||||||
for folder in ['config', 'OEM']:
|
|
||||||
folder = r'Windows\System32\{}'.format(folder)
|
|
||||||
folder = os.path.join(source_path, folder)
|
|
||||||
if os.path.exists(folder):
|
|
||||||
registry_items.append(folder)
|
|
||||||
if registry_items:
|
|
||||||
selected_items.append({
|
|
||||||
'Message': '{}Registry...'.format(rel_path),
|
|
||||||
'Items': registry_items.copy(),
|
|
||||||
'Destination': r'{}\Windows\System32'.format(dest_path)})
|
|
||||||
|
|
||||||
# Windows.old(s)
|
|
||||||
for old in win_olds:
|
|
||||||
selected_items.append(
|
|
||||||
scan_source_path(
|
|
||||||
old.path, dest_path, rel_path=old.name, interactive=False))
|
|
||||||
|
|
||||||
# Done
|
# Done
|
||||||
return selected_items
|
return items
|
||||||
|
|
||||||
def scan_source_wim(source_wim, dest_path, rel_path=None, interactive=True):
|
def scan_source(source_obj, dest_path, rel_path='', interactive=True):
|
||||||
"""Scan source WIM file for files/folders to transfer, returns list.
|
"""Scan source for files/folders to transfer, returns list.
|
||||||
|
|
||||||
This will scan the root and (recursively) any Windows.old folders."""
|
This will scan the root and (recursively) any Windows.old folders."""
|
||||||
rel_path = '\\' + rel_path if rel_path else ''
|
|
||||||
selected_items = []
|
selected_items = []
|
||||||
win_olds = []
|
win_olds = []
|
||||||
|
|
||||||
# Scan source
|
|
||||||
extract_item('wimlib', silent=True)
|
|
||||||
cmd = [
|
|
||||||
global_vars['Tools']['wimlib-imagex'], 'dir',
|
|
||||||
source_wim, '1']
|
|
||||||
try:
|
|
||||||
file_list = run_program(cmd)
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
print_error('ERROR: Failed to get file list.')
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Root Items
|
# Root Items
|
||||||
file_list = [i.strip()
|
|
||||||
for i in file_list.stdout.decode('utf-8', 'ignore').splitlines()
|
|
||||||
if i.count('\\') == 1 and i.strip() != '\\']
|
|
||||||
root_items = []
|
root_items = []
|
||||||
if rel_path:
|
item_list = list_source_items(source_obj, rel_path)
|
||||||
file_list = [i.replace(rel_path, '') for i in file_list]
|
for item in item_list:
|
||||||
for item in file_list:
|
if REGEX_INCL_ROOT_ITEMS.search(item.name):
|
||||||
if REGEX_INCL_ROOT_ITEMS.search(item):
|
print_success('Auto-Selected: {}'.format(item.path))
|
||||||
root_items.append(item)
|
root_items.append('{}'.format(item.path))
|
||||||
elif not REGEX_EXCL_ROOT_ITEMS.search(item):
|
elif not REGEX_EXCL_ROOT_ITEMS.search(item.name):
|
||||||
if (not interactive
|
if not interactive:
|
||||||
or ask('Extract: "{}{}" ?'.format(rel_path, item))):
|
print_success('Auto-Selected: {}'.format(item.path))
|
||||||
root_items.append('{}{}'.format(rel_path, item))
|
root_items.append('{}'.format(item.path))
|
||||||
if REGEX_WINDOWS_OLD.search(item):
|
else:
|
||||||
|
prompt = 'Transfer: "{}{}{}" ?'.format(
|
||||||
|
rel_path,
|
||||||
|
os.sep if rel_path else '',
|
||||||
|
item.name)
|
||||||
|
choices = ['Yes', 'No', 'All', 'Quit']
|
||||||
|
answer = choice(prompt=prompt, choices=choices)
|
||||||
|
if answer == 'Quit':
|
||||||
|
abort()
|
||||||
|
elif answer == 'All':
|
||||||
|
interactive = False
|
||||||
|
if answer in ['Yes', 'All']:
|
||||||
|
root_items.append('{}'.format(item.path))
|
||||||
|
if REGEX_WINDOWS_OLD.search(item.name):
|
||||||
|
item.name = '{}{}{}'.format(
|
||||||
|
rel_path,
|
||||||
|
os.sep if rel_path else '',
|
||||||
|
item.name)
|
||||||
win_olds.append(item)
|
win_olds.append(item)
|
||||||
if root_items:
|
if root_items:
|
||||||
selected_items.append({
|
selected_items.append({
|
||||||
'Message': '{}Root Items...'.format(rel_path),
|
'Message': '{}{}Root Items...'.format(
|
||||||
|
rel_path,
|
||||||
|
' ' if rel_path else ''),
|
||||||
'Items': root_items.copy(),
|
'Items': root_items.copy(),
|
||||||
'Destination': dest_path})
|
'Destination': dest_path})
|
||||||
|
|
||||||
# Fonts
|
# Fonts
|
||||||
if wim_contains(source_wim, r'{}Windows\Fonts'.format(rel_path)):
|
font_obj = get_source_item_obj(source_obj, rel_path, 'Windows/Fonts')
|
||||||
|
if font_obj:
|
||||||
selected_items.append({
|
selected_items.append({
|
||||||
'Message': '{}Fonts...'.format(rel_path),
|
'Message': '{}{}Fonts...'.format(
|
||||||
'Items': [r'{}\Windows\Fonts'.format(rel_path)],
|
rel_path,
|
||||||
|
' ' if rel_path else ''),
|
||||||
|
'Items': [font_obj.path],
|
||||||
'Destination': dest_path})
|
'Destination': dest_path})
|
||||||
|
|
||||||
# Registry
|
# Registry
|
||||||
registry_items = []
|
registry_items = []
|
||||||
for folder in ['config', 'OEM']:
|
for folder in ['config', 'OEM']:
|
||||||
folder = r'{}Windows\System32\{}'.format(rel_path, folder)
|
folder_obj = get_source_item_obj(
|
||||||
if wim_contains(source_wim, folder):
|
source_obj, rel_path, 'Windows/System32/{}'.format(folder))
|
||||||
registry_items.append(folder)
|
if folder_obj:
|
||||||
|
registry_items.append(folder_obj.path)
|
||||||
if registry_items:
|
if registry_items:
|
||||||
selected_items.append({
|
selected_items.append({
|
||||||
'Message': '{}Registry...'.format(rel_path),
|
'Message': '{}{}Registry...'.format(
|
||||||
|
rel_path,
|
||||||
|
' ' if rel_path else ''),
|
||||||
'Items': registry_items.copy(),
|
'Items': registry_items.copy(),
|
||||||
'Destination': dest_path})
|
'Destination': dest_path})
|
||||||
|
|
||||||
# Windows.old(s)
|
# Windows.old(s)
|
||||||
for old in win_olds:
|
for old in win_olds:
|
||||||
scan_source_wim(source_wim, dest_path, rel_path=old, interactive=False)
|
selected_items.extend(scan_source(
|
||||||
|
source_obj,
|
||||||
|
dest_path,
|
||||||
|
rel_path=old.name,
|
||||||
|
interactive=False))
|
||||||
|
|
||||||
# Done
|
# Done
|
||||||
return selected_items
|
return selected_items
|
||||||
|
|
||||||
|
def get_source_item_obj(source_obj, rel_path, item_path):
|
||||||
|
"""Check if the item exists and return a SourceItem object if it does."""
|
||||||
|
item_obj = None
|
||||||
|
item_path = re.sub(r'(\\|/)', os.sep, item_path)
|
||||||
|
if source_obj.is_dir():
|
||||||
|
item_obj = SourceItem(
|
||||||
|
name = item_path,
|
||||||
|
path = '{}{}{}{}{}'.format(
|
||||||
|
source_obj.path,
|
||||||
|
os.sep,
|
||||||
|
rel_path,
|
||||||
|
os.sep if rel_path else '',
|
||||||
|
item_path))
|
||||||
|
if not os.path.exists(item_obj.path):
|
||||||
|
item_obj = None
|
||||||
|
else:
|
||||||
|
# Assuming WIM file
|
||||||
|
if psutil.WINDOWS:
|
||||||
|
extract_item('wimlib', silent=True)
|
||||||
|
cmd = [
|
||||||
|
global_vars['Tools']['wimlib-imagex'], 'dir',
|
||||||
|
source_obj.path, '1',
|
||||||
|
'--path={}'.format(item_path),
|
||||||
|
'--one-file-only']
|
||||||
|
try:
|
||||||
|
run_program(cmd)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
# function will return None below
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
item_obj = SourceItem(
|
||||||
|
name = item_path,
|
||||||
|
path = '{}{}{}{}'.format(
|
||||||
|
os.sep,
|
||||||
|
rel_path,
|
||||||
|
os.sep if rel_path else '',
|
||||||
|
item_path))
|
||||||
|
return item_obj
|
||||||
|
|
||||||
def select_destination(folder_path, prompt='Select destination'):
|
def select_destination(folder_path, prompt='Select destination'):
|
||||||
"""Select destination drive, returns path as string."""
|
"""Select destination drive, returns path as string."""
|
||||||
disk = select_volume(prompt)
|
disk = select_volume(prompt)
|
||||||
|
|
@ -500,7 +531,7 @@ def select_source(ticket_number):
|
||||||
local_sources = []
|
local_sources = []
|
||||||
remote_sources = []
|
remote_sources = []
|
||||||
sources = []
|
sources = []
|
||||||
mount_backup_shares()
|
mount_backup_shares(read_write=False)
|
||||||
|
|
||||||
# Check for ticket folders on servers
|
# Check for ticket folders on servers
|
||||||
for server in BACKUP_SERVERS:
|
for server in BACKUP_SERVERS:
|
||||||
|
|
@ -623,6 +654,14 @@ def select_source(ticket_number):
|
||||||
pause("Press Enter to exit...")
|
pause("Press Enter to exit...")
|
||||||
exit_script()
|
exit_script()
|
||||||
|
|
||||||
|
# Sanity check
|
||||||
|
if selected_source.is_file():
|
||||||
|
# Image-Based
|
||||||
|
if not REGEX_WIM_FILE.search(selected_source.name):
|
||||||
|
print_error('ERROR: Unsupported image: {}'.format(
|
||||||
|
selected_source.path))
|
||||||
|
raise GenericError
|
||||||
|
|
||||||
# Done
|
# Done
|
||||||
return selected_source
|
return selected_source
|
||||||
|
|
||||||
|
|
@ -699,12 +738,12 @@ def transfer_source(source_obj, dest_path, selected_items):
|
||||||
raise GenericError
|
raise GenericError
|
||||||
|
|
||||||
def umount_backup_shares():
|
def umount_backup_shares():
|
||||||
"""Unnount the backup shares regardless of current status."""
|
"""Unmount the backup shares regardless of current status."""
|
||||||
for server in BACKUP_SERVERS:
|
for server in BACKUP_SERVERS:
|
||||||
umount_network_share(server)
|
umount_network_share(server)
|
||||||
|
|
||||||
def umount_network_share(server):
|
def umount_network_share(server):
|
||||||
"""Unnount a network share defined by server."""
|
"""Unmount a network share defined by server."""
|
||||||
cmd = r'net use \\{IP}\{Share} /delete'.format(**server)
|
cmd = r'net use \\{IP}\{Share} /delete'.format(**server)
|
||||||
cmd = cmd.split(' ')
|
cmd = cmd.split(' ')
|
||||||
try:
|
try:
|
||||||
|
|
@ -716,19 +755,5 @@ def umount_network_share(server):
|
||||||
print_info('Umounted {Name}'.format(**server))
|
print_info('Umounted {Name}'.format(**server))
|
||||||
server['Mounted'] = False
|
server['Mounted'] = False
|
||||||
|
|
||||||
def wim_contains(source_path, file_path):
|
|
||||||
"""Check if the WIM contains a file or folder."""
|
|
||||||
_cmd = [
|
|
||||||
global_vars['Tools']['wimlib-imagex'], 'dir',
|
|
||||||
source_path, '1',
|
|
||||||
'--path={}'.format(file_path),
|
|
||||||
'--one-file-only']
|
|
||||||
try:
|
|
||||||
run_program(_cmd)
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
print("This file is not meant to be called directly.")
|
print("This file is not meant to be called directly.")
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ REGEX_DISK_MBR = re.compile(r'Disk ID: [A-Z0-9]+', re.IGNORECASE)
|
||||||
REGEX_DISK_RAW = re.compile(r'Disk ID: 00000000', re.IGNORECASE)
|
REGEX_DISK_RAW = re.compile(r'Disk ID: 00000000', re.IGNORECASE)
|
||||||
|
|
||||||
def assign_volume_letters():
|
def assign_volume_letters():
|
||||||
|
"""Assign a volume letter to all available volumes."""
|
||||||
remove_volume_letters()
|
remove_volume_letters()
|
||||||
|
|
||||||
# Write script
|
# Write script
|
||||||
|
|
@ -24,6 +25,7 @@ def assign_volume_letters():
|
||||||
run_diskpart(script)
|
run_diskpart(script)
|
||||||
|
|
||||||
def get_boot_mode():
|
def get_boot_mode():
|
||||||
|
"""Check if the boot mode was UEFI or legacy."""
|
||||||
boot_mode = 'Legacy'
|
boot_mode = 'Legacy'
|
||||||
try:
|
try:
|
||||||
reg_key = winreg.OpenKey(
|
reg_key = winreg.OpenKey(
|
||||||
|
|
@ -37,6 +39,7 @@ def get_boot_mode():
|
||||||
return boot_mode
|
return boot_mode
|
||||||
|
|
||||||
def get_disk_details(disk):
|
def get_disk_details(disk):
|
||||||
|
"""Get disk details using DiskPart."""
|
||||||
details = {}
|
details = {}
|
||||||
script = [
|
script = [
|
||||||
'select disk {}'.format(disk['Number']),
|
'select disk {}'.format(disk['Number']),
|
||||||
|
|
@ -61,6 +64,7 @@ def get_disk_details(disk):
|
||||||
return details
|
return details
|
||||||
|
|
||||||
def get_disks():
|
def get_disks():
|
||||||
|
"""Get list of attached disks using DiskPart."""
|
||||||
disks = []
|
disks = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -79,6 +83,7 @@ def get_disks():
|
||||||
return disks
|
return disks
|
||||||
|
|
||||||
def get_partition_details(disk, partition):
|
def get_partition_details(disk, partition):
|
||||||
|
"""Get partition details using DiskPart and fsutil."""
|
||||||
details = {}
|
details = {}
|
||||||
script = [
|
script = [
|
||||||
'select disk {}'.format(disk['Number']),
|
'select disk {}'.format(disk['Number']),
|
||||||
|
|
@ -157,6 +162,7 @@ def get_partition_details(disk, partition):
|
||||||
return details
|
return details
|
||||||
|
|
||||||
def get_partitions(disk):
|
def get_partitions(disk):
|
||||||
|
"""Get list of partition using DiskPart."""
|
||||||
partitions = []
|
partitions = []
|
||||||
script = [
|
script = [
|
||||||
'select disk {}'.format(disk['Number']),
|
'select disk {}'.format(disk['Number']),
|
||||||
|
|
@ -179,6 +185,7 @@ def get_partitions(disk):
|
||||||
return partitions
|
return partitions
|
||||||
|
|
||||||
def get_table_type(disk):
|
def get_table_type(disk):
|
||||||
|
"""Get disk partition table type using DiskPart."""
|
||||||
part_type = 'Unknown'
|
part_type = 'Unknown'
|
||||||
script = [
|
script = [
|
||||||
'select disk {}'.format(disk['Number']),
|
'select disk {}'.format(disk['Number']),
|
||||||
|
|
@ -200,6 +207,7 @@ def get_table_type(disk):
|
||||||
return part_type
|
return part_type
|
||||||
|
|
||||||
def get_volumes():
|
def get_volumes():
|
||||||
|
"""Get list of volumes using DiskPart."""
|
||||||
vols = []
|
vols = []
|
||||||
try:
|
try:
|
||||||
result = run_diskpart(['list volume'])
|
result = run_diskpart(['list volume'])
|
||||||
|
|
@ -214,9 +222,11 @@ def get_volumes():
|
||||||
return vols
|
return vols
|
||||||
|
|
||||||
def is_bad_partition(par):
|
def is_bad_partition(par):
|
||||||
|
"""Check if the partition is accessible."""
|
||||||
return 'Letter' not in par or REGEX_BAD_PARTITION.search(par['FileSystem'])
|
return 'Letter' not in par or REGEX_BAD_PARTITION.search(par['FileSystem'])
|
||||||
|
|
||||||
def prep_disk_for_formatting(disk=None):
|
def prep_disk_for_formatting(disk=None):
|
||||||
|
"""Gather details about the disk and its partitions."""
|
||||||
disk['Format Warnings'] = '\n'
|
disk['Format Warnings'] = '\n'
|
||||||
width = len(str(len(disk['Partitions'])))
|
width = len(str(len(disk['Partitions'])))
|
||||||
|
|
||||||
|
|
@ -261,6 +271,7 @@ def prep_disk_for_formatting(disk=None):
|
||||||
partition['Display String'] = display
|
partition['Display String'] = display
|
||||||
|
|
||||||
def reassign_volume_letter(letter, new_letter='I'):
|
def reassign_volume_letter(letter, new_letter='I'):
|
||||||
|
"""Assign a new letter to a volume using DiskPart."""
|
||||||
if not letter:
|
if not letter:
|
||||||
# Ignore
|
# Ignore
|
||||||
return None
|
return None
|
||||||
|
|
@ -276,6 +287,7 @@ def reassign_volume_letter(letter, new_letter='I'):
|
||||||
return new_letter
|
return new_letter
|
||||||
|
|
||||||
def remove_volume_letters(keep=None):
|
def remove_volume_letters(keep=None):
|
||||||
|
"""Remove all assigned volume letters using DiskPart."""
|
||||||
if not keep:
|
if not keep:
|
||||||
keep = ''
|
keep = ''
|
||||||
|
|
||||||
|
|
@ -292,6 +304,7 @@ def remove_volume_letters(keep=None):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def run_diskpart(script):
|
def run_diskpart(script):
|
||||||
|
"""Run DiskPart script."""
|
||||||
tempfile = r'{}\diskpart.script'.format(global_vars['Env']['TMP'])
|
tempfile = r'{}\diskpart.script'.format(global_vars['Env']['TMP'])
|
||||||
|
|
||||||
# Write script
|
# Write script
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ TESTS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_smart_details(dev):
|
def get_smart_details(dev):
|
||||||
|
"""Get SMART data for dev if possible, returns dict."""
|
||||||
cmd = 'sudo smartctl --all --json /dev/{}'.format(dev).split()
|
cmd = 'sudo smartctl --all --json /dev/{}'.format(dev).split()
|
||||||
result = run_program(cmd, check=False)
|
result = run_program(cmd, check=False)
|
||||||
try:
|
try:
|
||||||
|
|
@ -51,6 +52,7 @@ def get_smart_details(dev):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def get_status_color(s):
|
def get_status_color(s):
|
||||||
|
"""Get color based on status, returns str."""
|
||||||
color = COLORS['CLEAR']
|
color = COLORS['CLEAR']
|
||||||
if s in ['Denied', 'NS', 'OVERRIDE', 'Unknown']:
|
if s in ['Denied', 'NS', 'OVERRIDE', 'Unknown']:
|
||||||
color = COLORS['RED']
|
color = COLORS['RED']
|
||||||
|
|
@ -61,6 +63,7 @@ def get_status_color(s):
|
||||||
return color
|
return color
|
||||||
|
|
||||||
def menu_diags(*args):
|
def menu_diags(*args):
|
||||||
|
"""Main HW-Diagnostic menu."""
|
||||||
diag_modes = [
|
diag_modes = [
|
||||||
{'Name': 'All tests',
|
{'Name': 'All tests',
|
||||||
'Tests': ['Prime95', 'NVMe/SMART', 'badblocks']},
|
'Tests': ['Prime95', 'NVMe/SMART', 'badblocks']},
|
||||||
|
|
@ -83,6 +86,13 @@ def menu_diags(*args):
|
||||||
{'Letter': 'Q', 'Name': 'Quit', 'CRLF': True},
|
{'Letter': 'Q', 'Name': 'Quit', 'CRLF': True},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# CLI-mode actions
|
||||||
|
if 'DISPLAY' not in global_vars['Env']:
|
||||||
|
actions.extend([
|
||||||
|
{'Letter': 'R', 'Name': 'Reboot', 'CRLF': True},
|
||||||
|
{'Letter': 'S', 'Name': 'Shutdown'},
|
||||||
|
])
|
||||||
|
|
||||||
# Quick disk check
|
# Quick disk check
|
||||||
if 'quick' in args:
|
if 'quick' in args:
|
||||||
run_tests(['Quick', 'NVMe/SMART'])
|
run_tests(['Quick', 'NVMe/SMART'])
|
||||||
|
|
@ -118,10 +128,15 @@ def menu_diags(*args):
|
||||||
run_program(
|
run_program(
|
||||||
'pipes -t 0 -t 1 -t 2 -t 3 -p 5 -R -r 4000'.split(),
|
'pipes -t 0 -t 1 -t 2 -t 3 -p 5 -R -r 4000'.split(),
|
||||||
check=False, pipe=False)
|
check=False, pipe=False)
|
||||||
|
elif selection == 'R':
|
||||||
|
run_program(['reboot'])
|
||||||
|
elif selection == 'S':
|
||||||
|
run_program(['poweroff'])
|
||||||
elif selection == 'Q':
|
elif selection == 'Q':
|
||||||
break
|
break
|
||||||
|
|
||||||
def run_badblocks():
|
def run_badblocks():
|
||||||
|
"""Run a read-only test for all detected disks."""
|
||||||
aborted = False
|
aborted = False
|
||||||
clear_screen()
|
clear_screen()
|
||||||
print_log('\nStart badblocks test(s)\n')
|
print_log('\nStart badblocks test(s)\n')
|
||||||
|
|
@ -180,6 +195,7 @@ def run_badblocks():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def run_mprime():
|
def run_mprime():
|
||||||
|
"""Run Prime95 for MPRIME_LIMIT minutes while showing the temps."""
|
||||||
aborted = False
|
aborted = False
|
||||||
clear_screen()
|
clear_screen()
|
||||||
print_log('\nStart Prime95 test')
|
print_log('\nStart Prime95 test')
|
||||||
|
|
@ -271,6 +287,7 @@ def run_mprime():
|
||||||
run_program('tmux kill-pane -a'.split())
|
run_program('tmux kill-pane -a'.split())
|
||||||
|
|
||||||
def run_nvme_smart():
|
def run_nvme_smart():
|
||||||
|
"""Run the built-in NVMe or SMART test for all detected disks."""
|
||||||
aborted = False
|
aborted = False
|
||||||
clear_screen()
|
clear_screen()
|
||||||
print_log('\nStart NVMe/SMART test(s)\n')
|
print_log('\nStart NVMe/SMART test(s)\n')
|
||||||
|
|
@ -365,6 +382,7 @@ def run_nvme_smart():
|
||||||
run_program('tmux kill-pane -a'.split(), check=False)
|
run_program('tmux kill-pane -a'.split(), check=False)
|
||||||
|
|
||||||
def run_tests(tests):
|
def run_tests(tests):
|
||||||
|
"""Run selected hardware test(s)."""
|
||||||
print_log('Starting Hardware Diagnostics')
|
print_log('Starting Hardware Diagnostics')
|
||||||
print_log('\nRunning tests: {}'.format(', '.join(tests)))
|
print_log('\nRunning tests: {}'.format(', '.join(tests)))
|
||||||
# Enable selected tests
|
# Enable selected tests
|
||||||
|
|
@ -403,6 +421,7 @@ def run_tests(tests):
|
||||||
pause('Press Enter to exit...')
|
pause('Press Enter to exit...')
|
||||||
|
|
||||||
def scan_disks():
|
def scan_disks():
|
||||||
|
"""Scan for disks eligible for hardware testing."""
|
||||||
clear_screen()
|
clear_screen()
|
||||||
|
|
||||||
# Get eligible disk list
|
# Get eligible disk list
|
||||||
|
|
@ -410,10 +429,18 @@ def scan_disks():
|
||||||
json_data = json.loads(result.stdout.decode())
|
json_data = json.loads(result.stdout.decode())
|
||||||
devs = {}
|
devs = {}
|
||||||
for d in json_data.get('blockdevices', []):
|
for d in json_data.get('blockdevices', []):
|
||||||
if d['type'] == 'disk' and d['hotplug'] == '0':
|
if d['type'] == 'disk':
|
||||||
devs[d['name']] = {'lsblk': d}
|
if d['hotplug'] == '0':
|
||||||
TESTS['NVMe/SMART']['Status'][d['name']] = 'Pending'
|
devs[d['name']] = {'lsblk': d}
|
||||||
TESTS['badblocks']['Status'][d['name']] = 'Pending'
|
TESTS['NVMe/SMART']['Status'][d['name']] = 'Pending'
|
||||||
|
TESTS['badblocks']['Status'][d['name']] = 'Pending'
|
||||||
|
else:
|
||||||
|
# Skip WizardKit devices
|
||||||
|
wk_label = '{}_LINUX'.format(KIT_NAME_SHORT)
|
||||||
|
if wk_label not in [c.get('label', '') for c in d['children']]:
|
||||||
|
devs[d['name']] = {'lsblk': d}
|
||||||
|
TESTS['NVMe/SMART']['Status'][d['name']] = 'Pending'
|
||||||
|
TESTS['badblocks']['Status'][d['name']] = 'Pending'
|
||||||
|
|
||||||
for dev, data in devs.items():
|
for dev, data in devs.items():
|
||||||
# Get SMART attributes
|
# Get SMART attributes
|
||||||
|
|
@ -470,6 +497,7 @@ def scan_disks():
|
||||||
TESTS['badblocks']['Devices'] = devs
|
TESTS['badblocks']['Devices'] = devs
|
||||||
|
|
||||||
def show_disk_details(dev):
|
def show_disk_details(dev):
|
||||||
|
"""Display disk details."""
|
||||||
dev_name = dev['lsblk']['name']
|
dev_name = dev['lsblk']['name']
|
||||||
# Device description
|
# Device description
|
||||||
print_info('Device: /dev/{}'.format(dev['lsblk']['name']))
|
print_info('Device: /dev/{}'.format(dev['lsblk']['name']))
|
||||||
|
|
@ -547,6 +575,7 @@ def show_disk_details(dev):
|
||||||
print_success(raw_str, timestamp=False)
|
print_success(raw_str, timestamp=False)
|
||||||
|
|
||||||
def show_results():
|
def show_results():
|
||||||
|
"""Show results for selected test(s)."""
|
||||||
clear_screen()
|
clear_screen()
|
||||||
print_log('\n───────────────────────────')
|
print_log('\n───────────────────────────')
|
||||||
print_standard('Hardware Diagnostic Results')
|
print_standard('Hardware Diagnostic Results')
|
||||||
|
|
@ -610,6 +639,7 @@ def show_results():
|
||||||
run_program('tmux kill-pane -a'.split())
|
run_program('tmux kill-pane -a'.split())
|
||||||
|
|
||||||
def update_progress():
|
def update_progress():
|
||||||
|
"""Update progress file."""
|
||||||
if 'Progress Out' not in TESTS:
|
if 'Progress Out' not in TESTS:
|
||||||
TESTS['Progress Out'] = '{}/progress.out'.format(global_vars['LogDir'])
|
TESTS['Progress Out'] = '{}/progress.out'.format(global_vars['LogDir'])
|
||||||
output = []
|
output = []
|
||||||
|
|
|
||||||
|
|
@ -460,8 +460,8 @@ def show_user_data_summary(indent=8, width=32):
|
||||||
indent = ' ' * indent,
|
indent = ' ' * indent,
|
||||||
width = width,
|
width = width,
|
||||||
folder = folder,
|
folder = folder,
|
||||||
size = folders[folder]['Size'],
|
size = folders[folder].get('Size', 'Unknown'),
|
||||||
path = folders[folder]['Path']))
|
path = folders[folder].get('Path', 'Unknown')))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
print("This file is not meant to be called directly.")
|
print("This file is not meant to be called directly.")
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ def is_connected():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def show_valid_addresses():
|
def show_valid_addresses():
|
||||||
|
"""Show all valid private IP addresses assigned to the system."""
|
||||||
devs = psutil.net_if_addrs()
|
devs = psutil.net_if_addrs()
|
||||||
for dev, families in sorted(devs.items()):
|
for dev, families in sorted(devs.items()):
|
||||||
for family in families:
|
for family in families:
|
||||||
|
|
@ -62,6 +63,7 @@ def show_valid_addresses():
|
||||||
show_data(message=dev, data=family.address)
|
show_data(message=dev, data=family.address)
|
||||||
|
|
||||||
def speedtest():
|
def speedtest():
|
||||||
|
"""Run a network speedtest using speedtest-cli."""
|
||||||
result = run_program(['speedtest-cli', '--simple'])
|
result = run_program(['speedtest-cli', '--simple'])
|
||||||
output = [line.strip() for line in result.stdout.decode().splitlines()
|
output = [line.strip() for line in result.stdout.decode().splitlines()
|
||||||
if line.strip()]
|
if line.strip()]
|
||||||
|
|
|
||||||
|
|
@ -192,7 +192,7 @@ def config_explorer_user():
|
||||||
|
|
||||||
def update_clock():
|
def update_clock():
|
||||||
"""Set Timezone and sync clock."""
|
"""Set Timezone and sync clock."""
|
||||||
run_program(['tzutil' ,'/s', TIME_ZONE], check=False)
|
run_program(['tzutil' ,'/s', WINDOWS_TIME_ZONE], check=False)
|
||||||
run_program(['net', 'stop', 'w32ime'], check=False)
|
run_program(['net', 'stop', 'w32ime'], check=False)
|
||||||
run_program(
|
run_program(
|
||||||
['w32tm', '/config', '/syncfromflags:manual',
|
['w32tm', '/config', '/syncfromflags:manual',
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from settings.music import *
|
||||||
from settings.sources import *
|
from settings.sources import *
|
||||||
|
|
||||||
def compress_and_remove_item(item):
|
def compress_and_remove_item(item):
|
||||||
|
"""Compress and delete an item unless an error is encountered."""
|
||||||
try:
|
try:
|
||||||
compress_item(item)
|
compress_item(item)
|
||||||
except:
|
except:
|
||||||
|
|
@ -17,6 +18,7 @@ def compress_and_remove_item(item):
|
||||||
remove_item(item.path)
|
remove_item(item.path)
|
||||||
|
|
||||||
def compress_item(item):
|
def compress_item(item):
|
||||||
|
"""Compress an item in a 7-Zip archive using the ARCHIVE_PASSWORD."""
|
||||||
# Prep
|
# Prep
|
||||||
prev_dir = os.getcwd()
|
prev_dir = os.getcwd()
|
||||||
dest = '{}.7z'.format(item.path)
|
dest = '{}.7z'.format(item.path)
|
||||||
|
|
@ -58,9 +60,11 @@ def download_generic(out_dir, out_name, source_url):
|
||||||
raise GenericError('Failed to download file.')
|
raise GenericError('Failed to download file.')
|
||||||
|
|
||||||
def download_to_temp(out_name, source_url):
|
def download_to_temp(out_name, source_url):
|
||||||
|
"""Download a file to the TmpDir."""
|
||||||
download_generic(global_vars['TmpDir'], out_name, source_url)
|
download_generic(global_vars['TmpDir'], out_name, source_url)
|
||||||
|
|
||||||
def extract_generic(source, dest, mode='x', sz_args=[]):
|
def extract_generic(source, dest, mode='x', sz_args=[]):
|
||||||
|
"""Extract a file to a destination."""
|
||||||
cmd = [
|
cmd = [
|
||||||
global_vars['Tools']['SevenZip'],
|
global_vars['Tools']['SevenZip'],
|
||||||
mode, source, r'-o{}'.format(dest),
|
mode, source, r'-o{}'.format(dest),
|
||||||
|
|
@ -70,11 +74,13 @@ def extract_generic(source, dest, mode='x', sz_args=[]):
|
||||||
run_program(cmd)
|
run_program(cmd)
|
||||||
|
|
||||||
def extract_temp_to_bin(source, item, mode='x', sz_args=[]):
|
def extract_temp_to_bin(source, item, mode='x', sz_args=[]):
|
||||||
|
"""Extract a file to the .bin folder."""
|
||||||
source = r'{}\{}'.format(global_vars['TmpDir'], source)
|
source = r'{}\{}'.format(global_vars['TmpDir'], source)
|
||||||
dest = r'{}\{}'.format(global_vars['BinDir'], item)
|
dest = r'{}\{}'.format(global_vars['BinDir'], item)
|
||||||
extract_generic(source, dest, mode, sz_args)
|
extract_generic(source, dest, mode, sz_args)
|
||||||
|
|
||||||
def extract_temp_to_cbin(source, item, mode='x', sz_args=[]):
|
def extract_temp_to_cbin(source, item, mode='x', sz_args=[]):
|
||||||
|
"""Extract a file to the .cbin folder."""
|
||||||
source = r'{}\{}'.format(global_vars['TmpDir'], source)
|
source = r'{}\{}'.format(global_vars['TmpDir'], source)
|
||||||
dest = r'{}\{}'.format(global_vars['CBinDir'], item)
|
dest = r'{}\{}'.format(global_vars['CBinDir'], item)
|
||||||
include_path = r'{}\_include\{}'.format(global_vars['CBinDir'], item)
|
include_path = r'{}\_include\{}'.format(global_vars['CBinDir'], item)
|
||||||
|
|
@ -83,6 +89,7 @@ def extract_temp_to_cbin(source, item, mode='x', sz_args=[]):
|
||||||
extract_generic(source, dest, mode, sz_args)
|
extract_generic(source, dest, mode, sz_args)
|
||||||
|
|
||||||
def generate_launcher(section, name, options):
|
def generate_launcher(section, name, options):
|
||||||
|
"""Generate a launcher script."""
|
||||||
# Prep
|
# Prep
|
||||||
dest = r'{}\{}'.format(global_vars['BaseDir'], section)
|
dest = r'{}\{}'.format(global_vars['BaseDir'], section)
|
||||||
if section == '(Root)':
|
if section == '(Root)':
|
||||||
|
|
@ -119,6 +126,7 @@ def generate_launcher(section, name, options):
|
||||||
f.write('\n'.join(out_text))
|
f.write('\n'.join(out_text))
|
||||||
|
|
||||||
def remove_item(item_path):
|
def remove_item(item_path):
|
||||||
|
"""Delete a file or folder."""
|
||||||
if os.path.exists(item_path):
|
if os.path.exists(item_path):
|
||||||
if os.path.isdir(item_path):
|
if os.path.isdir(item_path):
|
||||||
shutil.rmtree(item_path, ignore_errors=True)
|
shutil.rmtree(item_path, ignore_errors=True)
|
||||||
|
|
@ -126,6 +134,7 @@ def remove_item(item_path):
|
||||||
os.remove(item_path)
|
os.remove(item_path)
|
||||||
|
|
||||||
def remove_from_kit(item):
|
def remove_from_kit(item):
|
||||||
|
"""Delete a file or folder from the .bin/.cbin folders."""
|
||||||
item_locations = []
|
item_locations = []
|
||||||
for p in [global_vars['BinDir'], global_vars['CBinDir']]:
|
for p in [global_vars['BinDir'], global_vars['CBinDir']]:
|
||||||
item_locations.append(r'{}\{}'.format(p, item))
|
item_locations.append(r'{}\{}'.format(p, item))
|
||||||
|
|
@ -134,6 +143,7 @@ def remove_from_kit(item):
|
||||||
remove_item(item_path)
|
remove_item(item_path)
|
||||||
|
|
||||||
def remove_from_temp(item):
|
def remove_from_temp(item):
|
||||||
|
"""Delete a file or folder from the TmpDir folder."""
|
||||||
item_path = r'{}\{}'.format(global_vars['TmpDir'], item)
|
item_path = r'{}\{}'.format(global_vars['TmpDir'], item)
|
||||||
remove_item(item_path)
|
remove_item(item_path)
|
||||||
|
|
||||||
|
|
@ -159,6 +169,7 @@ def resolve_dynamic_url(source_url, regex):
|
||||||
return url
|
return url
|
||||||
|
|
||||||
def scan_for_net_installers(server, family_name, min_year):
|
def scan_for_net_installers(server, family_name, min_year):
|
||||||
|
"""Scan network shares for installers."""
|
||||||
if not server['Mounted']:
|
if not server['Mounted']:
|
||||||
mount_network_share(server)
|
mount_network_share(server)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -154,11 +154,15 @@ def format_mbr(disk):
|
||||||
run_diskpart(script)
|
run_diskpart(script)
|
||||||
|
|
||||||
def mount_windows_share():
|
def mount_windows_share():
|
||||||
"""Mount the Windows images share unless labeled as already mounted."""
|
"""Mount the Windows images share unless labeled as already mounted."""
|
||||||
if not WINDOWS_SERVER['Mounted']:
|
if not WINDOWS_SERVER['Mounted']:
|
||||||
mount_network_share(WINDOWS_SERVER)
|
# Mounting read-write in case a backup was done in the same session
|
||||||
|
# and the server was left mounted read-write. This avoids throwing an
|
||||||
|
# error by trying to mount the same server with multiple credentials.
|
||||||
|
mount_network_share(WINDOWS_SERVER, read_write=True)
|
||||||
|
|
||||||
def select_windows_version():
|
def select_windows_version():
|
||||||
|
"""Select Windows version from a menu, returns dict."""
|
||||||
actions = [
|
actions = [
|
||||||
{'Name': 'Main Menu', 'Letter': 'M'},
|
{'Name': 'Main Menu', 'Letter': 'M'},
|
||||||
]
|
]
|
||||||
|
|
@ -175,6 +179,7 @@ def select_windows_version():
|
||||||
raise GenericAbort
|
raise GenericAbort
|
||||||
|
|
||||||
def setup_windows(windows_image, windows_version):
|
def setup_windows(windows_image, windows_version):
|
||||||
|
"""Apply a Windows image to W:"""
|
||||||
cmd = [
|
cmd = [
|
||||||
global_vars['Tools']['wimlib-imagex'],
|
global_vars['Tools']['wimlib-imagex'],
|
||||||
'apply',
|
'apply',
|
||||||
|
|
@ -186,6 +191,7 @@ def setup_windows(windows_image, windows_version):
|
||||||
run_program(cmd)
|
run_program(cmd)
|
||||||
|
|
||||||
def setup_windows_re(windows_version, windows_letter='W', tools_letter='T'):
|
def setup_windows_re(windows_version, windows_letter='W', tools_letter='T'):
|
||||||
|
"""Setup the WinRE partition."""
|
||||||
win = r'{}:\Windows'.format(windows_letter)
|
win = r'{}:\Windows'.format(windows_letter)
|
||||||
winre = r'{}\System32\Recovery\WinRE.wim'.format(win)
|
winre = r'{}\System32\Recovery\WinRE.wim'.format(win)
|
||||||
dest = r'{}:\Recovery\WindowsRE'.format(tools_letter)
|
dest = r'{}:\Recovery\WindowsRE'.format(tools_letter)
|
||||||
|
|
@ -203,6 +209,7 @@ def setup_windows_re(windows_version, windows_letter='W', tools_letter='T'):
|
||||||
run_program(cmd)
|
run_program(cmd)
|
||||||
|
|
||||||
def update_boot_partition(system_letter='S', windows_letter='W', mode='ALL'):
|
def update_boot_partition(system_letter='S', windows_letter='W', mode='ALL'):
|
||||||
|
"""Setup the Windows boot partition."""
|
||||||
cmd = [
|
cmd = [
|
||||||
r'{}\Windows\System32\bcdboot.exe'.format(
|
r'{}\Windows\System32\bcdboot.exe'.format(
|
||||||
global_vars['Env']['SYSTEMDRIVE']),
|
global_vars['Env']['SYSTEMDRIVE']),
|
||||||
|
|
@ -212,6 +219,7 @@ def update_boot_partition(system_letter='S', windows_letter='W', mode='ALL'):
|
||||||
run_program(cmd)
|
run_program(cmd)
|
||||||
|
|
||||||
def wim_contains_image(filename, imagename):
|
def wim_contains_image(filename, imagename):
|
||||||
|
"""Check if an ESD/WIM contains the specified image, returns bool."""
|
||||||
cmd = [
|
cmd = [
|
||||||
global_vars['Tools']['wimlib-imagex'],
|
global_vars['Tools']['wimlib-imagex'],
|
||||||
'info',
|
'info',
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ PE_TOOLS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
def check_pe_tools():
|
def check_pe_tools():
|
||||||
|
"""Fix tool paths for WinPE layout."""
|
||||||
for k in PE_TOOLS.keys():
|
for k in PE_TOOLS.keys():
|
||||||
PE_TOOLS[k]['Path'] = r'{}\{}'.format(
|
PE_TOOLS[k]['Path'] = r'{}\{}'.format(
|
||||||
global_vars['BinDir'], PE_TOOLS[k]['Path'])
|
global_vars['BinDir'], PE_TOOLS[k]['Path'])
|
||||||
|
|
@ -80,7 +81,7 @@ def menu_backup():
|
||||||
ticket_number = get_ticket_number()
|
ticket_number = get_ticket_number()
|
||||||
|
|
||||||
# Mount backup shares
|
# Mount backup shares
|
||||||
mount_backup_shares()
|
mount_backup_shares(read_write=True)
|
||||||
|
|
||||||
# Select destination
|
# Select destination
|
||||||
destination = select_backup_destination(auto_select=False)
|
destination = select_backup_destination(auto_select=False)
|
||||||
|
|
@ -203,6 +204,7 @@ def menu_backup():
|
||||||
pause('\nPress Enter to return to main menu... ')
|
pause('\nPress Enter to return to main menu... ')
|
||||||
|
|
||||||
def menu_root():
|
def menu_root():
|
||||||
|
"""Main WinPE menu."""
|
||||||
check_pe_tools()
|
check_pe_tools()
|
||||||
menus = [
|
menus = [
|
||||||
{'Name': 'Create Backups', 'Menu': menu_backup},
|
{'Name': 'Create Backups', 'Menu': menu_backup},
|
||||||
|
|
@ -381,6 +383,7 @@ def menu_setup():
|
||||||
pause('\nPress Enter to return to main menu... ')
|
pause('\nPress Enter to return to main menu... ')
|
||||||
|
|
||||||
def menu_tools():
|
def menu_tools():
|
||||||
|
"""Tool launcher menu."""
|
||||||
tools = [{'Name': k} for k in sorted(PE_TOOLS.keys())]
|
tools = [{'Name': k} for k in sorted(PE_TOOLS.keys())]
|
||||||
actions = [{'Name': 'Main Menu', 'Letter': 'M'},]
|
actions = [{'Name': 'Main Menu', 'Letter': 'M'},]
|
||||||
set_title(KIT_NAME_FULL)
|
set_title(KIT_NAME_FULL)
|
||||||
|
|
@ -409,6 +412,7 @@ def menu_tools():
|
||||||
break
|
break
|
||||||
|
|
||||||
def select_minidump_path():
|
def select_minidump_path():
|
||||||
|
"""Select BSOD minidump path from a menu."""
|
||||||
dumps = []
|
dumps = []
|
||||||
|
|
||||||
# Assign volume letters first
|
# Assign volume letters first
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ def color_temp(temp):
|
||||||
color = COLORS['CLEAR']
|
color = COLORS['CLEAR']
|
||||||
return '{color}{prefix}{temp:2.0f}°C{CLEAR}'.format(
|
return '{color}{prefix}{temp:2.0f}°C{CLEAR}'.format(
|
||||||
color = color,
|
color = color,
|
||||||
prefix = '+' if temp>0 else '-',
|
prefix = '-' if temp < 0 else '',
|
||||||
temp = temp,
|
temp = temp,
|
||||||
**COLORS)
|
**COLORS)
|
||||||
|
|
||||||
|
|
@ -61,12 +61,9 @@ def get_feature_string(chip, feature):
|
||||||
skipname = len(feature.name)+1 # skip common prefix
|
skipname = len(feature.name)+1 # skip common prefix
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
if feature.type == sensors.feature.INTRUSION:
|
if feature.type != sensors.feature.TEMP:
|
||||||
vals = [sensors.get_value(chip, sf.number) for sf in sfs]
|
# Skip non-temperature sensors
|
||||||
# short path for INTRUSION to demonstrate type usage
|
return None
|
||||||
status = "alarm" if int(vals[0]) == 1 else "normal"
|
|
||||||
print_standard(' {:18} {}'.format(label, status))
|
|
||||||
return
|
|
||||||
|
|
||||||
for sf in sfs:
|
for sf in sfs:
|
||||||
name = sf.name[skipname:].decode("utf-8").strip()
|
name = sf.name[skipname:].decode("utf-8").strip()
|
||||||
|
|
@ -81,7 +78,7 @@ def get_feature_string(chip, feature):
|
||||||
data[name] = ' {}°C'.format(val)
|
data[name] = ' {}°C'.format(val)
|
||||||
else:
|
else:
|
||||||
data[name] = '{}{:2.0f}°C'.format(
|
data[name] = '{}{:2.0f}°C'.format(
|
||||||
'+' if temp>0 else '-',
|
'-' if temp < 0 else '',
|
||||||
temp)
|
temp)
|
||||||
else:
|
else:
|
||||||
data[name] = color_temp(val)
|
data[name] = color_temp(val)
|
||||||
|
|
@ -94,7 +91,7 @@ def get_feature_string(chip, feature):
|
||||||
list_data.append('{}: {}'.format(item, data.pop(item)))
|
list_data.append('{}: {}'.format(item, data.pop(item)))
|
||||||
list_data.extend(
|
list_data.extend(
|
||||||
['{}: {}'.format(k, v) for k, v in sorted(data.items())])
|
['{}: {}'.format(k, v) for k, v in sorted(data.items())])
|
||||||
data_str = '{:18} {} ({})'.format(
|
data_str = '{:18} {} {}'.format(
|
||||||
label, main_temp, ', '.join(list_data))
|
label, main_temp, ', '.join(list_data))
|
||||||
else:
|
else:
|
||||||
list_data.extend(sorted(data.items()))
|
list_data.extend(sorted(data.items()))
|
||||||
|
|
@ -119,10 +116,13 @@ if __name__ == '__main__':
|
||||||
chip_name = '{} ({})'.format(
|
chip_name = '{} ({})'.format(
|
||||||
sensors.chip_snprintf_name(chip),
|
sensors.chip_snprintf_name(chip),
|
||||||
sensors.get_adapter_name(chip.bus))
|
sensors.get_adapter_name(chip.bus))
|
||||||
chip_temps[chip_name] = [chip_name]
|
chip_feats = [get_feature_string(chip, feature)
|
||||||
for feature in sensors.FeatureIterator(chip):
|
for feature in sensors.FeatureIterator(chip)]
|
||||||
chip_temps[chip_name].append(get_feature_string(chip, feature))
|
# Strip empty/None items
|
||||||
chip_temps[chip_name].append('')
|
chip_feats = [f for f in chip_feats if f]
|
||||||
|
|
||||||
|
if chip_feats:
|
||||||
|
chip_temps[chip_name] = [chip_name, *chip_feats, '']
|
||||||
|
|
||||||
# Sort chips
|
# Sort chips
|
||||||
sensor_temps = []
|
sensor_temps = []
|
||||||
|
|
|
||||||
|
|
@ -441,6 +441,12 @@ LAUNCHERS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
r'Misc': {
|
r'Misc': {
|
||||||
|
'Cleanup CBS Temp Files': {
|
||||||
|
'L_TYPE': 'PyScript',
|
||||||
|
'L_PATH': 'Scripts',
|
||||||
|
'L_ITEM': 'cbs_fix.py',
|
||||||
|
'L_ELEV': 'True',
|
||||||
|
},
|
||||||
'ConEmu (as ADMIN)': {
|
'ConEmu (as ADMIN)': {
|
||||||
'L_TYPE': 'Executable',
|
'L_TYPE': 'Executable',
|
||||||
'L_PATH': 'ConEmu',
|
'L_PATH': 'ConEmu',
|
||||||
|
|
|
||||||
|
|
@ -1,83 +1,89 @@
|
||||||
# Wizard Kit: Settings - Main / Branding
|
# Wizard Kit: Settings - Main / Branding
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
ENABLED_UPLOAD_DATA = False
|
ENABLED_UPLOAD_DATA = False
|
||||||
|
|
||||||
# STATIC VARIABLES (also used by BASH and BATCH files)
|
# STATIC VARIABLES (also used by BASH and BATCH files)
|
||||||
## NOTE: There are no spaces around the = for easier parsing in BASH and BATCH
|
## NOTE: There are no spaces around the = for easier parsing in BASH and BATCH
|
||||||
# Main Kit
|
# Main Kit
|
||||||
ARCHIVE_PASSWORD='Abracadabra'
|
ARCHIVE_PASSWORD='Abracadabra'
|
||||||
KIT_NAME_FULL='Wizard Kit'
|
KIT_NAME_FULL='Wizard Kit'
|
||||||
KIT_NAME_SHORT='WK'
|
KIT_NAME_SHORT='WK'
|
||||||
SUPPORT_MESSAGE='Please let 2Shirt know by opening an issue on GitHub'
|
SUPPORT_MESSAGE='Please let 2Shirt know by opening an issue on GitHub'
|
||||||
# Live Linux
|
# Live Linux
|
||||||
DIAG_SHARE='/srv/ClientInfo'
|
MPRIME_LIMIT='7' # of minutes to run Prime95 during hw-diags
|
||||||
DIAG_USER='wkdiag'
|
ROOT_PASSWORD='Abracadabra'
|
||||||
MPRIME_LIMIT='7' # of minutes to run Prime95 during hw-diags
|
TECH_PASSWORD='Abracadabra'
|
||||||
ROOT_PASSWORD='Abracadabra'
|
# Server IP addresses
|
||||||
SKIP_UPLOAD='False'
|
OFFICE_SERVER_IP='10.0.0.10'
|
||||||
TECH_PASSWORD='Abracadabra'
|
QUICKBOOKS_SERVER_IP='10.0.0.10'
|
||||||
# Server IP addresses
|
# Time Zones
|
||||||
DIAG_SERVER='10.0.0.10'
|
LINUX_TIME_ZONE='America/Los_Angeles' # See 'timedatectl list-timezones' for valid values
|
||||||
OFFICE_SERVER_IP='10.0.0.10'
|
WINDOWS_TIME_ZONE='Pacific Standard Time' # See 'tzutil /l' for valid values
|
||||||
QUICKBOOKS_SERVER_IP='10.0.0.10'
|
# WiFi
|
||||||
# Time Zones
|
WIFI_SSID='SomeWifi'
|
||||||
LINUX_TIME_ZONE='America/Los_Angeles' # See 'timedatectl list-timezones' for valid values
|
WIFI_PASSWORD='Abracadabra'
|
||||||
WINDOWS_TIME_ZONE='Pacific Standard Time' # See 'tzutil /l' for valid values
|
|
||||||
# WiFi
|
# SERVER VARIABLES
|
||||||
WIFI_SSID='SomeWifi'
|
## NOTE: Windows can only use one user per server. This means that if
|
||||||
WIFI_PASSWORD='Abracadabra'
|
## one server serves multiple shares then you have to use the same
|
||||||
|
## user/password for all of those shares.
|
||||||
# SERVER VARIABLES
|
BACKUP_SERVERS = [
|
||||||
## NOTE: Windows can only use one user per server. This means that if
|
{ 'IP': '10.0.0.10',
|
||||||
## one server serves multiple shares then you have to use the same
|
'Name': 'ServerOne',
|
||||||
## user/password for all of those shares.
|
'Mounted': False,
|
||||||
BACKUP_SERVERS = [
|
'Share': 'Backups',
|
||||||
{ 'IP': '10.0.0.10',
|
'User': 'restore',
|
||||||
'Name': 'ServerOne',
|
'Pass': 'Abracadabra',
|
||||||
'Mounted': False,
|
'RW-User': 'backup',
|
||||||
'Share': 'Backups',
|
'RW-Pass': 'Abracadabra',
|
||||||
'User': 'restore',
|
},
|
||||||
'Pass': 'Abracadabra',
|
{ 'IP': '10.0.0.11',
|
||||||
},
|
'Name': 'ServerTwo',
|
||||||
{ 'IP': '10.0.0.11',
|
'Mounted': False,
|
||||||
'Name': 'ServerTwo',
|
'Share': 'Backups',
|
||||||
'Mounted': False,
|
'User': 'restore',
|
||||||
'Share': 'Backups',
|
'Pass': 'Abracadabra',
|
||||||
'User': 'restore',
|
'RW-User': 'backup',
|
||||||
'Pass': 'Abracadabra',
|
'RW-Pass': 'Abracadabra',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
CLIENT_INFO_SERVER = {
|
CRASH_SERVER = {
|
||||||
'IP': '10.0.0.10',
|
'Name': 'CrashServer',
|
||||||
'RegEntry': r'0x10001,0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
|
'Url': '',
|
||||||
'Share': '/srv/ClientInfo',
|
'User': '',
|
||||||
'User': 'upload',
|
'Pass': '',
|
||||||
}
|
}
|
||||||
OFFICE_SERVER = {
|
OFFICE_SERVER = {
|
||||||
'IP': OFFICE_SERVER_IP,
|
'IP': OFFICE_SERVER_IP,
|
||||||
'Name': 'ServerOne',
|
'Name': 'ServerOne',
|
||||||
'Mounted': False,
|
'Mounted': False,
|
||||||
'Share': 'Office',
|
'Share': 'Office',
|
||||||
'User': 'restore',
|
'User': 'restore',
|
||||||
'Pass': 'Abracadabra',
|
'Pass': 'Abracadabra',
|
||||||
}
|
'RW-User': 'backup',
|
||||||
QUICKBOOKS_SERVER = {
|
'RW-Pass': 'Abracadabra',
|
||||||
'IP': QUICKBOOKS_SERVER_IP,
|
}
|
||||||
'Name': 'ServerOne',
|
QUICKBOOKS_SERVER = {
|
||||||
'Mounted': False,
|
'IP': QUICKBOOKS_SERVER_IP,
|
||||||
'Share': 'QuickBooks',
|
'Name': 'ServerOne',
|
||||||
'User': 'restore',
|
'Mounted': False,
|
||||||
'Pass': 'Abracadabra',
|
'Share': 'QuickBooks',
|
||||||
}
|
'User': 'restore',
|
||||||
WINDOWS_SERVER = {
|
'Pass': 'Abracadabra',
|
||||||
'IP': '10.0.0.10',
|
'RW-User': 'backup',
|
||||||
'Name': 'ServerOne',
|
'RW-Pass': 'Abracadabra',
|
||||||
'Mounted': False,
|
}
|
||||||
'Share': 'Windows',
|
WINDOWS_SERVER = {
|
||||||
'User': 'restore',
|
'IP': '10.0.0.10',
|
||||||
'Pass': 'Abracadabra',
|
'Name': 'ServerOne',
|
||||||
}
|
'Mounted': False,
|
||||||
|
'Share': 'Windows',
|
||||||
if __name__ == '__main__':
|
'User': 'restore',
|
||||||
print("This file is not meant to be called directly.")
|
'Pass': 'Abracadabra',
|
||||||
|
'RW-User': 'backup',
|
||||||
|
'RW-Pass': 'Abracadabra',
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,6 @@ TOOLS = {
|
||||||
'ProduKey': {
|
'ProduKey': {
|
||||||
'32': r'ProduKey\ProduKey.exe',
|
'32': r'ProduKey\ProduKey.exe',
|
||||||
'64': r'ProduKey\ProduKey64.exe'},
|
'64': r'ProduKey\ProduKey64.exe'},
|
||||||
'PuTTY-PSFTP': {
|
|
||||||
'32': r'PuTTY\PSFTP.EXE'},
|
|
||||||
'RKill': {
|
'RKill': {
|
||||||
'32': r'RKill\RKill.exe'},
|
'32': r'RKill\RKill.exe'},
|
||||||
'SevenZip': {
|
'SevenZip': {
|
||||||
|
|
|
||||||
|
|
@ -80,14 +80,6 @@ if __name__ == '__main__':
|
||||||
try_and_print(message='Installed RAM:',
|
try_and_print(message='Installed RAM:',
|
||||||
function=show_installed_ram, ns='Unknown', silent_function=False)
|
function=show_installed_ram, ns='Unknown', silent_function=False)
|
||||||
|
|
||||||
# Upload info
|
|
||||||
if ENABLED_UPLOAD_DATA:
|
|
||||||
print_info('Finalizing')
|
|
||||||
try_and_print(message='Compressing Info...',
|
|
||||||
function=compress_info, cs='Done')
|
|
||||||
try_and_print(message='Uploading to NAS...',
|
|
||||||
function=upload_info, cs='Done')
|
|
||||||
|
|
||||||
# Play audio, show devices, open Windows updates, and open Activation
|
# Play audio, show devices, open Windows updates, and open Activation
|
||||||
popen_program(['mmc', 'devmgmt.msc'])
|
popen_program(['mmc', 'devmgmt.msc'])
|
||||||
run_hwinfo_sensors()
|
run_hwinfo_sensors()
|
||||||
|
|
|
||||||
|
|
@ -106,14 +106,6 @@ if __name__ == '__main__':
|
||||||
except Exception:
|
except Exception:
|
||||||
print_error(' Unknown error.')
|
print_error(' Unknown error.')
|
||||||
|
|
||||||
# Upload info
|
|
||||||
if ENABLED_UPLOAD_DATA:
|
|
||||||
print_info('Finalizing')
|
|
||||||
try_and_print(message='Compressing Info...',
|
|
||||||
function=compress_info, cs='Done')
|
|
||||||
try_and_print(message='Uploading to NAS...',
|
|
||||||
function=upload_info, cs='Done')
|
|
||||||
|
|
||||||
# Done
|
# Done
|
||||||
print_standard('\nDone.')
|
print_standard('\nDone.')
|
||||||
pause('Press Enter to exit...')
|
pause('Press Enter to exit...')
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,9 @@ if __name__ == '__main__':
|
||||||
# Transfer
|
# Transfer
|
||||||
clear_screen()
|
clear_screen()
|
||||||
print_info('Transfer Details:\n')
|
print_info('Transfer Details:\n')
|
||||||
show_info('Ticket:', ticket_number)
|
show_data('Ticket:', ticket_number)
|
||||||
show_info('Source:', source.path)
|
show_data('Source:', source.path)
|
||||||
show_info('Destination:', dest)
|
show_data('Destination:', dest)
|
||||||
|
|
||||||
if (not ask('Proceed with transfer?')):
|
if (not ask('Proceed with transfer?')):
|
||||||
umount_backup_shares()
|
umount_backup_shares()
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ order += "tztime local"
|
||||||
#order += "tztime utc"
|
#order += "tztime utc"
|
||||||
|
|
||||||
cpu_usage {
|
cpu_usage {
|
||||||
format = ". %usage"
|
format = " %usage"
|
||||||
max_threshold = 90
|
max_threshold = 90
|
||||||
#format_above_threshold = " %usage"
|
#format_above_threshold = " %usage"
|
||||||
degraded_threshold = 75
|
degraded_threshold = 75
|
||||||
|
|
@ -29,19 +29,19 @@ cpu_usage {
|
||||||
}
|
}
|
||||||
|
|
||||||
wireless _first_ {
|
wireless _first_ {
|
||||||
format_up = ". (%quality at %essid) %ip"
|
format_up = " (%quality at %essid) %ip"
|
||||||
format_down = ". Down"
|
format_down = " Down"
|
||||||
}
|
}
|
||||||
|
|
||||||
ethernet _first_ {
|
ethernet _first_ {
|
||||||
# if you use %speed, i3status requires root privileges
|
# if you use %speed, i3status requires root privileges
|
||||||
format_up = ". %ip"
|
format_up = " %ip"
|
||||||
format_down = ". Down"
|
format_down = " Down"
|
||||||
}
|
}
|
||||||
|
|
||||||
battery all {
|
battery all {
|
||||||
integer_battery_capacity = true
|
integer_battery_capacity = true
|
||||||
format = "%status. %percentage"
|
format = "%status %percentage"
|
||||||
format_down = ""
|
format_down = ""
|
||||||
status_chr = ""
|
status_chr = ""
|
||||||
status_bat = ""
|
status_bat = ""
|
||||||
|
|
@ -53,7 +53,7 @@ battery all {
|
||||||
}
|
}
|
||||||
|
|
||||||
volume master {
|
volume master {
|
||||||
format = ". %volume"
|
format = " %volume"
|
||||||
format_muted = " muted"
|
format_muted = " muted"
|
||||||
device = "pulse"
|
device = "pulse"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,13 @@ aic94xx-firmware
|
||||||
bash-pipes
|
bash-pipes
|
||||||
gtk-theme-arc-git
|
gtk-theme-arc-git
|
||||||
hfsprogs
|
hfsprogs
|
||||||
i3-gaps
|
|
||||||
i3lock-fancy-git
|
i3lock-fancy-git
|
||||||
mprime-bin
|
mprime
|
||||||
nvme-cli
|
nvme-cli
|
||||||
openbox-patched
|
openbox-patched
|
||||||
papirus-icon-theme
|
papirus-icon-theme
|
||||||
pasystray
|
pasystray
|
||||||
smartmontools-svn
|
smartmontools-svn
|
||||||
testdisk-wip
|
testdisk-wip
|
||||||
ttf-font-awesome
|
ttf-font-awesome-4
|
||||||
wd719x-firmware
|
wd719x-firmware
|
||||||
wimlib
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,11 @@ base-devel
|
||||||
curl
|
curl
|
||||||
dos2unix
|
dos2unix
|
||||||
git
|
git
|
||||||
|
hwloc
|
||||||
libewf
|
libewf
|
||||||
openssh
|
openssh
|
||||||
p7zip
|
p7zip
|
||||||
progsreiserfs
|
progsreiserfs
|
||||||
refind-efi
|
refind-efi
|
||||||
rsync
|
rsync
|
||||||
|
syslinux
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ mdadm
|
||||||
mediainfo
|
mediainfo
|
||||||
mesa-demos
|
mesa-demos
|
||||||
mkvtoolnix-cli
|
mkvtoolnix-cli
|
||||||
mprime-bin
|
mprime
|
||||||
mpv
|
mpv
|
||||||
mupdf
|
mupdf
|
||||||
ncdu
|
ncdu
|
||||||
|
|
@ -50,6 +50,7 @@ networkmanager
|
||||||
nvme-cli
|
nvme-cli
|
||||||
oblogout
|
oblogout
|
||||||
openbox-patched
|
openbox-patched
|
||||||
|
otf-font-awesome-4
|
||||||
p7zip
|
p7zip
|
||||||
papirus-icon-theme
|
papirus-icon-theme
|
||||||
pasystray
|
pasystray
|
||||||
|
|
@ -75,7 +76,7 @@ tint2
|
||||||
tk
|
tk
|
||||||
tmux
|
tmux
|
||||||
tree
|
tree
|
||||||
ttf-font-awesome
|
ttf-font-awesome-4
|
||||||
ttf-inconsolata
|
ttf-inconsolata
|
||||||
udevil
|
udevil
|
||||||
udisks2
|
udisks2
|
||||||
|
|
|
||||||
34
Build Linux
34
Build Linux
|
|
@ -2,6 +2,11 @@
|
||||||
#
|
#
|
||||||
## Wizard Kit: Live Linux Build Tool
|
## Wizard Kit: Live Linux Build Tool
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o errtrace
|
||||||
|
set -o nounset
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
# Prep
|
# Prep
|
||||||
DATE="$(date +%F)"
|
DATE="$(date +%F)"
|
||||||
DATETIME="$(date +%F_%H%M)"
|
DATETIME="$(date +%F_%H%M)"
|
||||||
|
|
@ -21,7 +26,7 @@ elif which vim >/dev/null 2>&1; then
|
||||||
else
|
else
|
||||||
EDITOR=vi
|
EDITOR=vi
|
||||||
fi
|
fi
|
||||||
if which sudo >/dev/null 2>&1; then
|
if [ ! -z ${SUDO_USER+x} ]; then
|
||||||
REAL_USER="$SUDO_USER"
|
REAL_USER="$SUDO_USER"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -110,7 +115,7 @@ function copy_live_env() {
|
||||||
rsync -aI "$ROOT_DIR/.linux_items/include/" "$LIVE_DIR/"
|
rsync -aI "$ROOT_DIR/.linux_items/include/" "$LIVE_DIR/"
|
||||||
mkdir -p "$LIVE_DIR/airootfs/usr/local/bin"
|
mkdir -p "$LIVE_DIR/airootfs/usr/local/bin"
|
||||||
rsync -aI "$ROOT_DIR/.bin/Scripts/" "$LIVE_DIR/airootfs/usr/local/bin/"
|
rsync -aI "$ROOT_DIR/.bin/Scripts/" "$LIVE_DIR/airootfs/usr/local/bin/"
|
||||||
cp -a "$BUILD_DIR/main.py" "$LIVE_DIR/airootfs/usr/local/bin/"
|
cp -a "$BUILD_DIR/main.py" "$LIVE_DIR/airootfs/usr/local/bin/settings/"
|
||||||
}
|
}
|
||||||
|
|
||||||
function run_elevated() {
|
function run_elevated() {
|
||||||
|
|
@ -184,12 +189,12 @@ function update_live_env() {
|
||||||
sed -i -r "s/_+/$KIT_NAME_FULL Linux Environment/" "$LIVE_DIR/airootfs/etc/motd"
|
sed -i -r "s/_+/$KIT_NAME_FULL Linux Environment/" "$LIVE_DIR/airootfs/etc/motd"
|
||||||
|
|
||||||
# Oh My ZSH
|
# Oh My ZSH
|
||||||
git clone --depth=1 git://github.com/robbyrussell/oh-my-zsh.git "$SKEL_DIR/.oh-my-zsh"
|
git clone --depth=1 https://github.com/robbyrussell/oh-my-zsh.git "$SKEL_DIR/.oh-my-zsh"
|
||||||
rm -Rf "$SKEL_DIR/.oh-my-zsh/.git"
|
rm -Rf "$SKEL_DIR/.oh-my-zsh/.git"
|
||||||
curl -o "$SKEL_DIR/.oh-my-zsh/themes/lean.zsh-theme" https://raw.githubusercontent.com/miekg/lean/master/prompt_lean_setup
|
curl -o "$SKEL_DIR/.oh-my-zsh/themes/lean.zsh-theme" https://raw.githubusercontent.com/miekg/lean/master/prompt_lean_setup
|
||||||
|
|
||||||
# Openbox theme
|
# Openbox theme
|
||||||
git clone --depth=1 git@github.com:addy-dclxvi/Openbox-Theme-Collections.git "$TEMP_DIR/ob-themes"
|
git clone --depth=1 https://github.com/addy-dclxvi/Openbox-Theme-Collections.git "$TEMP_DIR/ob-themes"
|
||||||
mkdir -p "$LIVE_DIR/airootfs/usr/share/themes"
|
mkdir -p "$LIVE_DIR/airootfs/usr/share/themes"
|
||||||
cp -a "$TEMP_DIR/ob-themes/Triste-Orange" "$LIVE_DIR/airootfs/usr/share/themes/"
|
cp -a "$TEMP_DIR/ob-themes/Triste-Orange" "$LIVE_DIR/airootfs/usr/share/themes/"
|
||||||
|
|
||||||
|
|
@ -243,15 +248,15 @@ function update_repo() {
|
||||||
|
|
||||||
# Archive current files
|
# Archive current files
|
||||||
if [[ -d "$REPO_DIR" ]]; then
|
if [[ -d "$REPO_DIR" ]]; then
|
||||||
mkdir "$BUILD_DIR/Archive" 2>/dev/null
|
mkdir -p "$BUILD_DIR/Archive" 2>/dev/null
|
||||||
archive="$BUILD_DIR/Archive/$(date "+%F_%H%M%S")"
|
archive="$BUILD_DIR/Archive/$(date "+%F_%H%M%S")"
|
||||||
mv -bv "$REPO_DIR" "$archive"
|
mv -bv "$REPO_DIR" "$archive"
|
||||||
fi
|
fi
|
||||||
sleep 1s
|
sleep 1s
|
||||||
|
|
||||||
# Build custom repo packages
|
# Build custom repo packages
|
||||||
mkdir "$REPO_DIR" 2>/dev/null
|
mkdir -p "$REPO_DIR" 2>/dev/null
|
||||||
mkdir "$TEMP_DIR" 2>/dev/null
|
mkdir -p "$TEMP_DIR" 2>/dev/null
|
||||||
pushd "$TEMP_DIR" >/dev/null
|
pushd "$TEMP_DIR" >/dev/null
|
||||||
while read -r p; do
|
while read -r p; do
|
||||||
echo "Building: $p"
|
echo "Building: $p"
|
||||||
|
|
@ -291,6 +296,13 @@ function build_iso() {
|
||||||
chown root:root "$LIVE_DIR" -R
|
chown root:root "$LIVE_DIR" -R
|
||||||
chmod 700 "$LIVE_DIR/airootfs/etc/skel/.ssh"
|
chmod 700 "$LIVE_DIR/airootfs/etc/skel/.ssh"
|
||||||
chmod 600 "$LIVE_DIR/airootfs/etc/skel/.ssh/id_rsa"
|
chmod 600 "$LIVE_DIR/airootfs/etc/skel/.ssh/id_rsa"
|
||||||
|
|
||||||
|
# Removing cached (and possibly outdated) custom repo packages
|
||||||
|
for package in $(cat "$ROOT_DIR/.linux_items/packages/aur"); do
|
||||||
|
if [[ -f /var/cache/pacman/pkg/${package}* ]]; then
|
||||||
|
rm /var/cache/pacman/pkg/${package}*
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
# Build ISO
|
# Build ISO
|
||||||
prefix="${KIT_NAME_SHORT}-Linux"
|
prefix="${KIT_NAME_SHORT}-Linux"
|
||||||
|
|
@ -325,32 +337,38 @@ function build_full() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check input
|
# Check input
|
||||||
case $1 in
|
case ${1:-} in
|
||||||
-b|--build-full)
|
-b|--build-full)
|
||||||
build_full
|
build_full
|
||||||
|
echo Done
|
||||||
;;
|
;;
|
||||||
|
|
||||||
-f|--fix-perms)
|
-f|--fix-perms)
|
||||||
fix_kit_permissions
|
fix_kit_permissions
|
||||||
|
echo Done
|
||||||
;;
|
;;
|
||||||
|
|
||||||
-i|--install-deps)
|
-i|--install-deps)
|
||||||
install_deps
|
install_deps
|
||||||
|
echo Done
|
||||||
;;
|
;;
|
||||||
|
|
||||||
-o|--build-iso)
|
-o|--build-iso)
|
||||||
load_settings
|
load_settings
|
||||||
build_iso
|
build_iso
|
||||||
|
echo Done
|
||||||
;;
|
;;
|
||||||
|
|
||||||
-p|--prep-live-env)
|
-p|--prep-live-env)
|
||||||
load_settings
|
load_settings
|
||||||
copy_live_env
|
copy_live_env
|
||||||
update_live_env
|
update_live_env
|
||||||
|
echo Done
|
||||||
;;
|
;;
|
||||||
|
|
||||||
-u|--update-repo)
|
-u|--update-repo)
|
||||||
update_repo
|
update_repo
|
||||||
|
echo Done
|
||||||
;;
|
;;
|
||||||
|
|
||||||
*)
|
*)
|
||||||
|
|
|
||||||
85
README.md
85
README.md
|
|
@ -7,7 +7,7 @@ A collection of scripts to help technicians service Windows systems.
|
||||||
### Build Requirements ###
|
### Build Requirements ###
|
||||||
|
|
||||||
* PowerShell 3.0 or newer<sup>1</sup>
|
* PowerShell 3.0 or newer<sup>1</sup>
|
||||||
* 6 Gb disk space
|
* 10 Gb disk space
|
||||||
|
|
||||||
### Initial Setup ###
|
### Initial Setup ###
|
||||||
|
|
||||||
|
|
@ -49,19 +49,39 @@ A collection of scripts to help technicians service Windows systems.
|
||||||
### Initial Setup ###
|
### Initial Setup ###
|
||||||
|
|
||||||
* Replace artwork as desired
|
* Replace artwork as desired
|
||||||
* Run `Build_Linux` which will do the following:
|
* Install Arch Linux in a virtual machine ([VirtualBox](https://www.virtualbox.org/) is a good option for Windows systems).
|
||||||
* Install missing dependancies with pacman
|
* See the [installation guide](https://wiki.archlinux.org/index.php/Installation_guide) for details.
|
||||||
* Open `main.py` in nano for configuration
|
* Add a standard user to the Arch Linux installation.
|
||||||
* Build the local repo for the AUR packages
|
* See the [wiki page](https://wiki.archlinux.org/index.php/Users_and_groups#User_management) for details.
|
||||||
* Build the live Linux environment (exported as an ISO file)
|
* Install git # `pacman -Syu git`
|
||||||
|
* _(Recommended)_ Install and configure `sudo`
|
||||||
|
* See the [wiki page](https://wiki.archlinux.org/index.php/Sudo) for details.
|
||||||
|
* Login to the user added above
|
||||||
|
* Download the Github repo $ `git clone https://github.com/2Shirt/WizardKit.git`
|
||||||
|
* Run the build script
|
||||||
|
* $ `cd WizardKit`
|
||||||
|
* $ `./Build\ Linux -b`
|
||||||
|
* The build script does the following:
|
||||||
|
* Installs missing dependencies via `pacman`
|
||||||
|
* Opens `main.py` in `nano` for configuration
|
||||||
|
* Downloads, builds, and adds AUR packages to a local repo
|
||||||
|
* Builds the Live Linux ISO
|
||||||
|
|
||||||
|
### Notes ###
|
||||||
|
|
||||||
|
* The WinPE boot options require files to be copied from a completed WinPE build.
|
||||||
|
* This is done below for the Combined UFD
|
||||||
|
|
||||||
## Windows PE ##
|
## Windows PE ##
|
||||||
|
|
||||||
### Build Requirements ###
|
### Build Requirements ###
|
||||||
|
|
||||||
* Windows Assessment and Deployment Kit for Windows 10
|
* Windows Assessment and Deployment Kit for Windows 10
|
||||||
|
* Deployment Tools
|
||||||
|
* Windows Preinstallation Environment (Windows PE)
|
||||||
|
* _All other features are not required_
|
||||||
* PowerShell 3.0 or newer
|
* PowerShell 3.0 or newer
|
||||||
* 2 Gb disk space
|
* 8 Gb disk space
|
||||||
|
|
||||||
### Initial Setup ###
|
### Initial Setup ###
|
||||||
|
|
||||||
|
|
@ -72,5 +92,56 @@ A collection of scripts to help technicians service Windows systems.
|
||||||
* Download all tools
|
* Download all tools
|
||||||
* Build both 32-bit & 64-bit PE images (exported as ISO files)
|
* Build both 32-bit & 64-bit PE images (exported as ISO files)
|
||||||
|
|
||||||
|
## Combined Wizard Kit ##
|
||||||
|
|
||||||
|
### Build Requirements ###
|
||||||
|
|
||||||
|
* 64-bit system or virtual machine
|
||||||
|
* 4 Gb RAM
|
||||||
|
* 8 Gb USB flash drive _(16 Gb or larger recommended)_
|
||||||
|
|
||||||
|
### Overview ###
|
||||||
|
|
||||||
|
There's a `build-ufd` script which does the following:
|
||||||
|
|
||||||
|
* Checks for the presence if the Linux ISO and the (64-bit) WinPE ISO.
|
||||||
|
* Formats the selected UFD using FAT32.
|
||||||
|
* All data will be deleted from the UFD resulting in **DATA LOSS**.
|
||||||
|
* Copies the required files from the Linux ISO, WinPE ISO, and Main Kit folder to the UFD.
|
||||||
|
* Installs Syslinux to the UFD making it bootable on legacy systems.
|
||||||
|
* Sets the boot files/folders to be hidden under Windows.
|
||||||
|
|
||||||
|
### Setup ###
|
||||||
|
|
||||||
|
* Boot to a Live Linux ISO built following the instructions above.
|
||||||
|
* You can apply it to a UFD using [rufus](https://rufus.akeo.ie/) for physical systems.
|
||||||
|
* Virtual machines should be able to use the Linux ISO directly.
|
||||||
|
* Put the Linux ISO, the WinPE ISO, and the Main Kit folder _(usually "OUT_KIT")_ in the same directory.
|
||||||
|
* "OUT_KIT" will be renamed on the UFD using `$KIT_NAME_FULL`
|
||||||
|
* `$KIT_NAME_FULL` defaults to "Wizard Kit" but can be changed in `main.py`
|
||||||
|
* "OUT_KIT" can be renamed in the source folder.
|
||||||
|
* The script searched for the ".bin" folder and uses it's parent folder as the Main Kit source.
|
||||||
|
* Additional files/folders can be included by putting them in a folder named "Extras".
|
||||||
|
* These files/folders will be copied to the root of the UFD.
|
||||||
|
* To include images for the WinPE Setup section, put the files in "Extras/images".
|
||||||
|
* WinPE Setup will recognize ESD, WIM, and SWM<sup>2</sup> images.
|
||||||
|
* The filenames should be "Win7", "Win8", or "Win10"
|
||||||
|
* The final layout should be similar to this: _(assuming it's mounted to "/Sources")_
|
||||||
|
* **(Required)** `/Sources/OUT_KIT`
|
||||||
|
* **(Required)** `/Sources/WK-Linux-2018-01-01-x86_64.iso`
|
||||||
|
* **(Required)** `/Sources/WK-WinPE-2018-01-01-amd64.iso`
|
||||||
|
* _(Optional)_ `/Sources/Extras/Essential Windows Updates`
|
||||||
|
* _(Optional)_ `/Sources/Extras/images/Win7.wim`
|
||||||
|
* _(Optional)_ `/Sources/Extras/images/Win8.wim`
|
||||||
|
* _(Optional)_ `/Sources/Extras/images/Win10.esd`
|
||||||
|
* Connect the UFD but don't mount it.
|
||||||
|
* Mount the device, or connect to the share, with the ISOs and Main Kit folder.
|
||||||
|
* $ `cd /Sources` _(replace with real path to source files)_
|
||||||
|
* Get the device name of the UFD.
|
||||||
|
* You can use $ `lsblk --fs` or $ `inxi -Dxx` to help.
|
||||||
|
* $ `sudo build-ufd /dev/sdX` _(replace `/dev/sdX` with the desired device)_
|
||||||
|
* **2nd Warning**: All data will be erased from the UFD resulting in **DATA LOSS**.
|
||||||
|
|
||||||
## Notes ##
|
## Notes ##
|
||||||
1. PowerShell 6.0 on Windows 7 is not supported by the build script.
|
1. PowerShell 6.0 on Windows 7 is not supported by the build script.
|
||||||
|
2. See [wimlib-imagex](https://wimlib.net/) for details about split WIM images.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue