import os from shutil import disk_usage import time import argparse BS = 1024**2 def set_utilization(fs, utilization_percent=0.6, baloon_filename='baloon', hysteresis_percent=0.01, quiet=False): fs = os.path.realpath(fs) du = disk_usage(fs) if utilization_percent < 0.1 or utilization_percent > 0.9: raise ValueError('utilization_percent must be between 0.1 and 0.9') if hysteresis_percent < 0 or hysteresis_percent > 0.1: raise ValueError('hysteresis_percent must be between 0 and 0.1') hysteresis = int(du.total * hysteresis_percent) if hysteresis < BS: hysteresis = BS to_be_wasted = int(((du.used+du.free) * utilization_percent) - ((du.used+du.free) - du.free)) baloon_filename = os.path.join(fs, baloon_filename) if not quiet: print(f'hysteresis: {hysteresis}bytes') print(f'total: {round(du.total/ 2**30, 3)}GB, used: {round(du.used/ 2**30, 3)}GB, free: {round(du.free/ 2**30, 3)}GB usage: {round(du.used * 100 / (du.used+du.free), 3)}%') print(f'to_be_wasted: {round(to_be_wasted/ 2**30, 3)}GB ') if to_be_wasted < -hysteresis: to_be_trimmed = abs(to_be_wasted) baloon_size = os.path.getsize(baloon_filename) if baloon_size > to_be_trimmed: os.truncate(baloon_filename, baloon_size - to_be_trimmed) if not quiet: print(f'trimmed {to_be_trimmed} Bytes') else: if os.path.exists(baloon_filename): os.remove(baloon_filename) if not quiet and to_be_trimmed > baloon_size: print(f'can\'t trim {to_be_trimmed - baloon_size} Bytes') elif to_be_wasted > hysteresis: with open(baloon_filename , 'ab') as f: last = 0 writed = to_be_wasted while to_be_wasted > 0: if time.time() - last > 1: if not quiet: print(f'left {int(to_be_wasted/ 2**30)}GB \r', end='') last = time.time() write_size = min(to_be_wasted, BS) f.write(b'x' * write_size) to_be_wasted -= write_size if not quiet: print(f'Done, added {writed} bytes') if not quiet: du = disk_usage(fs) print(f'total: {round(du.total/ 2**30, 3)}GB, used: {round(du.used/ 2**30, 3)}GB, free: {round(du.free/ 2**30, 3)}GB usage: {round(du.used * 100 / (du.used+du.free), 3)}%') def hysteresis_type(arg): MAX_VAL = 10 MIN_VAL = 0 try: f = float(arg) except ValueError: raise argparse.ArgumentTypeError("Must be a floating point number") if f < MIN_VAL or f > MAX_VAL: raise argparse.ArgumentTypeError("Argument must be < " + str(MAX_VAL) + " and > " + str(MIN_VAL)) return f parser = argparse.ArgumentParser(description='Baloon utilize your disk space') parser.add_argument('-v', '--version', action='version', version='%(prog)s 1.0') parser.add_argument('-u', '--utilization', type=int, metavar="[10-90]", choices=range(10, 91), help='percentage of utilization', default=60) parser.add_argument('-b', '--baloon', type=str, metavar="baloon", help='name of the baloon file', default='baloon') parser.add_argument('-s', '--hysteresis', type=hysteresis_type, metavar="[0-10]", help='hysteresis, in percentage of disk size', default=1) parser.add_argument('-f', '--fs', type=str, action='append', metavar="fs", help='path to the filesystem', required=True) parser.add_argument('-d', '--daemon', action='store_true', help='daemon mode') parser.add_argument('-i', '--interval', type=int, help='interval in seconds to work in daemon mode', default=60*60) args = parser.parse_args() fss = [] baloon_filename = args.baloon hysteresis_percent = args.hysteresis/100.0 utilization_percent = args.utilization/100.0 daemon_mode = args.daemon daemon_mode_interval = args.interval for fs in args.fs: fs = os.path.realpath(fs) if not os.path.isdir(fs): raise ValueError(f'{fs} is not a directory') fss.append(fs) if not daemon_mode: for p in vars(args): print(f'{p}: {getattr(args, p)}') for fs in fss: set_utilization(fs, utilization_percent, baloon_filename, hysteresis_percent) else: while True: for fs in fss: if os.path.isdir(fs): try: set_utilization(fs=fs, utilization_percent=utilization_percent, baloon_filename=baloon_filename, hysteresis_percent=hysteresis_percent, quiet=True) except Exception as e: pass time.sleep(daemon_mode_interval)