mirror of https://git.48k.eu/ogclient
256 lines
7.9 KiB
Python
256 lines
7.9 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 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):
|
|
"""
|
|
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):
|
|
os.mkdir(target)
|
|
|
|
if not os.path.ismount(target):
|
|
return mount(source, target)
|
|
else:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def mount(source, target):
|
|
"""
|
|
Mounts source into target directoru using mount(8).
|
|
|
|
Return true if exit code is 0. False otherwise.
|
|
"""
|
|
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 get_usedperc(mountpoint):
|
|
"""
|
|
Returns percetage of used filesystem as decimal number.
|
|
"""
|
|
try:
|
|
total, used, free, perc = psutil.disk_usage(mountpoint)
|
|
except FileNotFoundError:
|
|
return '0'
|
|
return str(perc)
|
|
|
|
|
|
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)
|
|
|
|
umount(partdev)
|
|
if fstype == 'ext4':
|
|
_reduce_resize2fs(partdev)
|
|
elif fstype == 'ntfs':
|
|
_reduce_ntfsresize(partdev)
|
|
else:
|
|
logging.warn(f'Unable to shrink filesystem at {partdev}. '
|
|
f'Unsupported filesystem "{fstype}".')
|
|
|
|
|
|
def ogExtendFs(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)
|
|
|
|
umount(partdev)
|
|
if fstype == 'ext4':
|
|
_extend_resize2fs(partdev)
|
|
elif fstype == 'ntfs':
|
|
_extend_ntfsresize(partdev)
|
|
else:
|
|
logging.warn(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,
|
|
}
|
|
|
|
if fs not in fsdict:
|
|
logging.warn(f'mkfs aborted, invalid target filesystem.')
|
|
raise ValueError('Invalid target filesystem')
|
|
|
|
try:
|
|
partdev = get_partition_device(disk, partition)
|
|
except ValueError as e:
|
|
logging.warn(f'mkfs aborted, invalid partition.')
|
|
raise e
|
|
|
|
fsdict[fs](partdev, label)
|
|
|
|
|
|
def mkfs_ext4(partdev, label=None):
|
|
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:
|
|
subprocess.run(cmd,
|
|
stdout=logfile,
|
|
stderr=STDOUT)
|
|
|
|
|
|
def mkfs_ntfs(partdev, label=None):
|
|
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:
|
|
subprocess.run(cmd,
|
|
stdout=logfile,
|
|
stderr=STDOUT)
|
|
|
|
|
|
def mkfs_fat32(partdev, label=None):
|
|
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:
|
|
subprocess.run(cmd,
|
|
stdout=logfile,
|
|
stderr=STDOUT)
|
|
|
|
|
|
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:
|
|
raise RuntimeError(f'Error getting filesystem from {partdev}')
|
|
return proc.stdout.strip()
|
|
|
|
|
|
def _reduce_resize2fs(partdev):
|
|
cmd = shlex.split(f'resize2fs -fpM {partdev}')
|
|
with open('/tmp/command.log', 'ab', 0) as logfile:
|
|
subprocess.run(cmd, stdout=logfile, stderr=STDOUT)
|
|
|
|
|
|
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()
|
|
|
|
# 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.
|
|
#
|
|
# In addition, increase by 10%+1K the reported shrink location on which the
|
|
# filesystem can be resized according to ntfsresize.
|
|
size = int(out_info.split('device size: ')[1].split(' ')[0])
|
|
new_size = int(int(out_info.split('resize at ')[1].split(' ')[0])*1.1+1024)
|
|
|
|
# Dry-run loop to test if resizing is actually possible. This is required by ntfsresize.
|
|
returncode = 1
|
|
while new_size < size and returncode != 0:
|
|
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')
|
|
returncode = proc_resize_dryrun.returncode
|
|
out_resize_dryrun = proc_resize_dryrun.stdout.strip()
|
|
if 'Nothing to do: NTFS volume size is already OK.' in out_resize_dryrun:
|
|
logging.warn('ntfsresize reports nothing to do. Is the target filesystem already shrunken?')
|
|
break
|
|
extra_size = int(out_resize_dryrun.split('Needed relocations : ')[1].split(' ')[0])*1.1+1024
|
|
new_size += int(extra_size)
|
|
|
|
if new_size < size:
|
|
cmd_resize = shlex.split(f'ntfsresize -fs {new_size:.0f} {partdev}')
|
|
with open('/tmp/command.log', 'ab', 0) as logfile:
|
|
subprocess.run(cmd_resize, input='y', stderr=STDOUT, encoding='utf-8')
|
|
|
|
|
|
def _extend_resize2fs(partdev):
|
|
cmd = shlex.split(f'resize2fs -f {partdev}')
|
|
proc = subprocess.run(cmd)
|
|
if proc.returncode != 0:
|
|
raise RuntimeError(f'Error growing ext4 filesystem at {partdev}')
|
|
|
|
|
|
def _extend_ntfsresize(partdev):
|
|
cmd = shlex.split(f'ntfsresize -f {partdev}')
|
|
proc = subprocess.run(cmd, input='y')
|
|
if proc.returncode != 0:
|
|
raise RuntimeError(f'Error growing ntfs filesystem at {partdev}')
|