mirror of https://git.48k.eu/ogclient
343 lines
11 KiB
Python
343 lines
11 KiB
Python
#
|
|
# Copyright (C) 2022 Soleta Networks <info@soleta.eu>
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it under
|
|
# the terms of the GNU Affero General Public License as published by the
|
|
# Free Software Foundation; either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
import shlex
|
|
from src.log import OgError
|
|
|
|
from subprocess import DEVNULL, PIPE, STDOUT
|
|
|
|
import psutil
|
|
|
|
from src.utils.disk import get_partition_device
|
|
|
|
|
|
def find_mountpoint(path):
|
|
"""
|
|
Returns mountpoint of a given path
|
|
"""
|
|
path = os.path.abspath(path)
|
|
while not os.path.ismount(path):
|
|
path = os.path.dirname(path)
|
|
return path
|
|
|
|
|
|
def mount_mkdir(source, target, readonly=False):
|
|
"""
|
|
Mounts and creates the mountpoint directory if it's not present.
|
|
|
|
Return True if mount is sucessful or if target is already a mountpoint.
|
|
"""
|
|
if not os.path.exists(target):
|
|
try:
|
|
os.mkdir(target)
|
|
except OSError as e:
|
|
logging.error(f'mkdir operation failed. Reported {e}')
|
|
return False
|
|
|
|
if not os.path.ismount(target):
|
|
return mount(source, target, readonly)
|
|
|
|
return True
|
|
|
|
|
|
def mount(source, target, readonly):
|
|
"""
|
|
Mounts source into target directoru using mount(8).
|
|
|
|
Return true if exit code is 0. False otherwise.
|
|
"""
|
|
if readonly:
|
|
cmd = f'mount -o ro {source} {target}'
|
|
else:
|
|
cmd = f'mount {source} {target}'
|
|
proc = subprocess.run(cmd.split(), stderr=DEVNULL)
|
|
|
|
return not proc.returncode
|
|
|
|
|
|
def umount(target):
|
|
"""
|
|
Umounts target using umount(8).
|
|
|
|
Return true if exit code is 0. False otherwise.
|
|
"""
|
|
cmd = f'umount {target}'
|
|
proc = subprocess.run(cmd.split(), stderr=DEVNULL)
|
|
|
|
return not proc.returncode
|
|
|
|
|
|
def umount_all():
|
|
"""
|
|
Umounts all mountpoints present in the /mnt folder.
|
|
"""
|
|
for path in ['/mnt/'+child for child in os.listdir('/mnt/')]:
|
|
if os.path.ismount(path):
|
|
umount(path)
|
|
|
|
|
|
def ogReduceFs(disk, part):
|
|
"""
|
|
Shrink filesystem of a partition. Supports ext4 and ntfs partitions.
|
|
Unsupported filesystem or invalid paths don't raise an exception,
|
|
instead this method logs a warning message and does nothing.
|
|
"""
|
|
partdev = get_partition_device(disk, part)
|
|
fstype = get_filesystem_type(partdev)
|
|
if fstype == "unknown":
|
|
return -1
|
|
|
|
umount(partdev)
|
|
if fstype == 'ext4':
|
|
ret = _reduce_resize2fs(partdev)
|
|
elif fstype == 'ntfs':
|
|
ret = _reduce_ntfsresize(partdev)
|
|
elif fstype == 'vfat':
|
|
ret = 0
|
|
else:
|
|
ret = -1
|
|
logging.error(f'Unable to shrink filesystem at {partdev}. '
|
|
f'Unsupported filesystem "{fstype}".')
|
|
|
|
return ret
|
|
|
|
def extend_filesystem(disk, part):
|
|
"""
|
|
Grow filesystem of a partition. Supports ext4 and ntfs partitions.
|
|
Unsupported filesystem or invalid paths don't raise an exception,
|
|
instead this method logs a warning message and does nothing.
|
|
"""
|
|
partdev = get_partition_device(disk, part)
|
|
fstype = get_filesystem_type(partdev)
|
|
if fstype == "unknown":
|
|
return -1
|
|
|
|
umount(partdev)
|
|
if fstype == 'ext4':
|
|
_extend_resize2fs(partdev)
|
|
elif fstype == 'ntfs':
|
|
_extend_ntfsresize(partdev)
|
|
elif fstype == 'vfat':
|
|
pass
|
|
else:
|
|
logging.error(f'Unable to grow filesystem at {partdev}. '
|
|
f'Unsupported filesystem "{fstype}".')
|
|
|
|
|
|
def mkfs(fs, disk, partition, label=None):
|
|
"""
|
|
Install any supported filesystem. Target partition is specified a disk
|
|
number and partition number. This function uses utility functions to
|
|
translate disk and partition number into a partition device path.
|
|
|
|
If filesystem and partition are correct, calls the corresponding mkfs_*
|
|
function with the partition device path. If not, ValueError is raised.
|
|
"""
|
|
logging.debug(f'mkfs({fs}, {disk}, {partition}, {label})')
|
|
fsdict = {
|
|
'ext4': mkfs_ext4,
|
|
'ntfs': mkfs_ntfs,
|
|
'fat32': mkfs_fat32,
|
|
'linux-swap': mkfs_swap,
|
|
}
|
|
|
|
if fs not in fsdict:
|
|
raise OgError(f'mkfs failed, unsupported target filesystem {fs}')
|
|
|
|
try:
|
|
partdev = get_partition_device(disk, partition)
|
|
except ValueError as e:
|
|
raise OgError(f'mkfs aborted: {e}') from e
|
|
|
|
return fsdict[fs](partdev, label)
|
|
|
|
|
|
def mkfs_ext4(partdev, label=None):
|
|
err = -1
|
|
if label:
|
|
cmd = shlex.split(f'mkfs.ext4 -L {label} -F {partdev}')
|
|
else:
|
|
cmd = shlex.split(f'mkfs.ext4 -F {partdev}')
|
|
with open('/tmp/command.log', 'wb', 0) as logfile:
|
|
ret = subprocess.run(cmd,
|
|
stdout=logfile,
|
|
stderr=STDOUT)
|
|
err = ret.returncode
|
|
if ret.returncode != 0:
|
|
logging.error(f'mkfs.ext4 reports return code {ret.returncode} for {partdev}')
|
|
|
|
return err
|
|
|
|
|
|
def mkfs_ntfs(partdev, label=None):
|
|
err = -1
|
|
if label:
|
|
cmd = shlex.split(f'mkfs.ntfs -f -L {label} {partdev}')
|
|
else:
|
|
cmd = shlex.split(f'mkfs.ntfs -f {partdev}')
|
|
with open('/tmp/command.log', 'wb', 0) as logfile:
|
|
ret = subprocess.run(cmd,
|
|
stdout=logfile,
|
|
stderr=STDOUT)
|
|
err = ret.returncode
|
|
if ret.returncode != 0:
|
|
logging.error(f'mkfs.ntfs reports return code {ret.returncode} for {partdev}')
|
|
|
|
return err
|
|
|
|
|
|
def mkfs_fat32(partdev, label=None):
|
|
err = -1
|
|
if label:
|
|
cmd = shlex.split(f'mkfs.vfat -n {label} -F32 {partdev}')
|
|
else:
|
|
cmd = shlex.split(f'mkfs.vfat -F32 {partdev}')
|
|
with open('/tmp/command.log', 'wb', 0) as logfile:
|
|
ret = subprocess.run(cmd,
|
|
stdout=logfile,
|
|
stderr=STDOUT)
|
|
err = ret.returncode
|
|
if ret.returncode != 0:
|
|
logging.error(f'mkfs.vfat reports return code {ret.returncode} for {partdev}')
|
|
|
|
return err
|
|
|
|
|
|
def mkfs_swap(partdev, label=None):
|
|
err = -1
|
|
if label:
|
|
cmd = shlex.split(f'mkswap -f -L {label} {partdev}')
|
|
else:
|
|
cmd = shlex.split(f'mkswap -f {partdev}')
|
|
with open('/tmp/command.log', 'wb', 0) as logfile:
|
|
ret = subprocess.run(cmd,
|
|
stdout=logfile,
|
|
stderr=STDOUT)
|
|
err = ret.returncode
|
|
if ret.returncode != 0:
|
|
logging.error(f'mkswap reports return code {ret.returncode} for {partdev}')
|
|
|
|
return err
|
|
|
|
|
|
def get_filesystem_type(partdev):
|
|
"""
|
|
Get filesystem type from a partition device path.
|
|
Raises RuntimeError when blkid exits with non-zero return code.
|
|
"""
|
|
cmd = shlex.split(f'blkid -o value -s TYPE {partdev}')
|
|
proc = subprocess.run(cmd, stdout=PIPE, encoding='utf-8')
|
|
if proc.returncode != 0:
|
|
logging.error(f'Error getting filesystem from {partdev}')
|
|
return "unknown"
|
|
|
|
return proc.stdout.strip()
|
|
|
|
|
|
def _reduce_resize2fs(partdev):
|
|
cmd = shlex.split(f'resize2fs -fpM {partdev}')
|
|
with open('/tmp/command.log', 'ab', 0) as logfile:
|
|
proc = subprocess.run(cmd, stdout=logfile, stderr=STDOUT)
|
|
if proc.returncode != 0:
|
|
logging.error(f'Failed to resize ext4 filesystem in {partdev}')
|
|
return -1
|
|
|
|
return 0
|
|
|
|
|
|
def _reduce_ntfsresize(partdev):
|
|
cmd_info = shlex.split(f'ntfsresize -Pfi {partdev}')
|
|
proc_info = subprocess.run(cmd_info, stdout=subprocess.PIPE, encoding='utf-8')
|
|
out_info = proc_info.stdout.strip()
|
|
|
|
if out_info.find('ERROR: NTFS is inconsistent. Run chkdsk') != -1:
|
|
logging.error('NTFS is inconsistent. Run chkdsk /f on Windows then reboot TWICE!')
|
|
return -1
|
|
|
|
if proc_info.returncode != 0:
|
|
logging.error(f'nfsresize {partdev} has failed with return code {proc_info.returncode}')
|
|
return -1
|
|
|
|
# Process ntfsresize output directly.
|
|
# The first split operation leaves the wanted data at the second element of
|
|
# the split ([1]). Finally do a second split with ' ' to get the data but
|
|
# nothing else following it.
|
|
def parse_ntfsresize(output_data, pattern):
|
|
data_split = output_data.split(pattern)
|
|
# If we fail to match pattern in the split then data_split will contain [output_data]
|
|
if len(data_split) == 1:
|
|
raise OgError(f'nfsresize: failed to find: {pattern}')
|
|
value_str = data_split[1].split(' ')[0]
|
|
|
|
if not value_str.isdigit() or value_str.startswith('-'):
|
|
raise OgError(f'nfsresize: failed to parse numeric value at {pattern}')
|
|
return int(value_str)
|
|
|
|
try:
|
|
size = parse_ntfsresize(out_info, 'device size: ')
|
|
new_size = parse_ntfsresize(out_info, 'resize at ')
|
|
# Increase by 10%+1K the indicated reduction by which the file system
|
|
# can be resized according to ntfsresize.
|
|
new_size = int(new_size * 1.1 + 1024)
|
|
except ValueError:
|
|
return -1
|
|
|
|
|
|
# Run ntfsresize with -n to to probe for the smallest size, this loop is
|
|
# intentional. Acumulate size until ntfsresize in dry-run mode fails, then
|
|
# use such size.
|
|
while new_size < size:
|
|
cmd_resize_dryrun = shlex.split(f'ntfsresize -Pfns {new_size:.0f} {partdev}')
|
|
proc_resize_dryrun = subprocess.run(cmd_resize_dryrun, stdout=subprocess.PIPE, encoding='utf-8')
|
|
|
|
# valid new size found, stop probing
|
|
if proc_resize_dryrun.returncode == 0:
|
|
break
|
|
|
|
out_resize_dryrun = proc_resize_dryrun.stdout.strip()
|
|
if 'Nothing to do: NTFS volume size is already OK.' in out_resize_dryrun:
|
|
logging.info('ntfsresize reports nothing to do. Is the target filesystem already shrunken?')
|
|
break
|
|
|
|
if out_resize_dryrun.find('Needed relocations : ') == -1:
|
|
break
|
|
|
|
try:
|
|
extra_size = parse_ntfsresize(out_resize_dryrun, 'Needed relocations : ')
|
|
# Add size padding
|
|
extra_size = int(extra_size * 1.1 + 1024)
|
|
new_size += extra_size
|
|
except ValueError:
|
|
return -1
|
|
|
|
if new_size < size:
|
|
cmd_resize = shlex.split(f'ntfsresize -fs {new_size:.0f} {partdev}')
|
|
with open('/tmp/command.log', 'ab', 0) as logfile:
|
|
proc_resize = subprocess.run(cmd_resize, input='y', stderr=STDOUT, encoding='utf-8')
|
|
if proc_resize.returncode != 0:
|
|
logging.error(f'ntfsresize on {partdev} with {new_size:.0f} failed with {proc_resize.returncode}')
|
|
return -1
|
|
|
|
return 0
|
|
|
|
def _extend_resize2fs(partdev):
|
|
cmd = shlex.split(f'resize2fs -f {partdev}')
|
|
proc = subprocess.run(cmd)
|
|
if proc.returncode != 0:
|
|
raise OgError(f'Error growing ext4 filesystem at {partdev}')
|
|
|
|
|
|
def _extend_ntfsresize(partdev):
|
|
cmd = shlex.split(f'ntfsresize -f {partdev}')
|
|
proc = subprocess.run(cmd, input=b'y')
|
|
if proc.returncode != 0:
|
|
raise OgError(f'Error growing ntfs filesystem at {partdev}')
|