diff --git a/Sausage b/Sausage index 2ab77ae..1723348 100755 --- a/Sausage +++ b/Sausage @@ -1,347 +1,347 @@ #!/usr/bin/python3 # © All rights reserved. ECOLE POLYTECHNIQUE FEDERALE DE LAUSANNE, # Switzerland # SCITAS - Scientific IT and Application Support, 2021 # See the LICENSE.txt file for more details. import grp import os import configparser import requests import json import argparse import getpass from datetime import date from datetime import datetime def valid_date(date): try: validate = datetime.strptime(date, "%Y-%m-%d") return validate except ValueError: msg = "Not a valid date: '{0}', YYYY-MM-DD expected.".format(date) raise argparse.ArgumentTypeError(msg) def valid_period(date): try: validate = datetime.strptime(date, "%Y-%m") return validate except ValueError: msg = "Not a valid period: '{0}', YYYY-MM expected.".format(date) raise argparse.ArgumentTypeError(msg) class AppArgs(object): def __init__(self, conf): self.response = {} self.parser = argparse.ArgumentParser( prog='Sausage', description='SCITAS Account Usage.', formatter_class=argparse.ArgumentDefaultsHelpFormatter) self.cfg_parser = configparser.ConfigParser() self.cfg_parser.read(conf) self.billinggrp = self.cfg_parser.get("permissions", "billing") self.add_args() def add_args(self): self.parser.add_argument( '-u', '--user', help='If not provided whoami is considered') self.parser.add_argument( '-a', '--all', help='all users from an account are printed', action='store_true') self.parser.add_argument( '-A', '--account', help='Prints account consumption per cluster') self.parser.add_argument( '-s', '--start', help='Start date - format YYYY-MM-DD', type=valid_date) self.parser.add_argument( '-e', '--end', help='End date - format YYYY-MM-DD', type=valid_date) self.parser.add_argument( '-c', '--co2', help='Prints the co2 footprint per cluster', action='store_true') self.parser.add_argument( '-b', '--billing', help='Displays the billing period - format YYYY-MM', type=valid_period) args = self.parser.parse_args() if args.billing: listofgroups = [grp.getgrgid(g).gr_name for g in os.getgroups()] if self.billinggrp not in listofgroups: self.parser.error( "--billing is only available for users in " + self.billinggrp + " group") if (args.user or args.account or args.all or args.start or args.end or args.co2): self.parser.error( "--billing is not compatible with any other option") if args.start and args.end is None: self.parser.error("range requires both dates (--start and --end)") if args.end: if args.start is None: self.parser.error( "range requires both dates (--start and --end)") if args.end < args.start: self.parser.error("start date must be earlier than end date") if args.all and args.account is None: self.parser.error( "the option --all requires a valid account (--all and --account)") if args.all and args.user: self.parser.error( "--all option is not compatible with --user option") self.response = { "user": args.user, "account": args.account, "all": args.all, "start": args.start, "end": args.end, "co2": args.co2, "billing": args.billing } class GetData(object): def __init__(self, conf, args): self.cfg_parser = configparser.ConfigParser() self.cfg_parser.read(conf) self.server = self.cfg_parser.get( "server", "url") + ":" + self.cfg_parser.get("server", "port") self.unit = self.cfg_parser.get("cluster", "calc_unit") self.co2 = args["co2"] self.message = [] self.vseparator = '|' self.hseparator = '-' * 57 if args["start"]: self.firstday = str(args["start"].date()) else: self.firstday = str(date.today().replace(day=1)) if args["end"]: self.lastday = str(args["end"].date()) else: self.lastday = str(date.today()) self.request(args) def formatheader(self, a, b, c, d): if self.format == 4: self.message.append(f"{a:^30}" + self.vseparator + f"{b:^1}" + f"{c:^12}" + self.vseparator + f"{d:^11}") elif self.format == 1: self.message.append(f"{a:^19}" + self.vseparator + f"{b:^12}" + f"{c:^12}" + self.vseparator + f"{d:^11}") else: self.message.append(f"{a:^19}" + self.vseparator + f"{b:^12}" + self.vseparator + f"{c:^12}" + self.vseparator + f"{d:^11}") def formatline(self, a, b, c, d): if self.format == 4: self.message.append(f" {a:<29}" + self.vseparator + f" {b:<0}" + f"{c:>11} " + self.vseparator + f"{d:>10} ") elif self.format == 1: self.message.append(f" {a:<18}" + self.vseparator + f" {b:<11}" + f"{c:>11} " + self.vseparator + f"{d:>10} ") else: self.message.append(f" {a:<18}" + self.vseparator + f" {b:<11}" + self.vseparator + f"{c:>11} " + self.vseparator + f"{d:>10} ") def formatfooter(self, a, b, c): self.message.append(f" {a:<28}" + f"{b:>13} " + f"{c:<12}") def printbox(self): # Init internal variables data = self.response.json() if self.format == 4: element = "" else: element = data["name"] carbon = 0 money = 0 walltime = 0 # begin print headers # Default values title = "ACCOUNT: " head_b = "Cluster" # By case values if self.format == 1: head_a = "Cluster" head_b = "" elif self.format == 2: head_a = "Account" title = "USER: " elif self.format == 3: head_a = "Username" elif self.format == 4: head_a = "Account" head_b = "" title = "Billing table" head_c = self.unit + "-hrs" if self.co2: head_d = "kg.eCO²" else: head_d = "CHF" # Append headers self.message.append(self.hseparator) self.message.append(title + element) self.message.append("Global usage from " + self.firstday + " to " + self.lastday) self.message.append(self.hseparator) self.formatheader(head_a, head_b, head_c, head_d) self.message.append(self.hseparator) # end print headers # Begin print body for key, value in sorted(data.items()): if isinstance(value, dict): head_a = key # Format 1 used in cases 3 and 6 # Format 4 used in case 7 if self.format == 1 or self.format == 4: # if money is less than 0.01 CHF, print '-' if value['chf'] > 0.00999: chf = "{:.2f}".format(value['chf']) + money += value['chf'] else: chf = "-" # if time is less than 1 second, print '-' if value['time'] > 0.00999: time = "{:.2f}".format(value['time']) + walltime += value['time'] else: time = "-" # Convert co2 in ekg if value['co2'] > 9.999: co2 = "{:.2f}".format(value['co2'] / 1000) + carbon += value['co2'] else: co2 = "-" - carbon += value['co2'] - money += value['chf'] - walltime += value['time'] head_c = str(time) if self.co2: head_d = str(co2) else: head_d = str(chf) self.formatline(head_a, head_b, head_c, head_d) # Format 2 used in cases 1, 2, 4 and 6 # Format 3 used in case 5 elif self.format == 2 or self.format == 3: for k, v in value.items(): head_b = k # Convert co2 in ekg if v['co2'] > 9.999: co2 = "{:.2f}".format(v['co2'] / 1000) + carbon += v['co2'] else: co2 = "-" # if money is less than 0.01 CHF, print '-' if v['chf'] > 0.00999: chf = "{:.2f}".format(v['chf']) money += v['chf'] else: chf = "-" # if time is less than 1 second, print '-' if v['time'] > 0.00999: time = "{:.2f}".format(v['time']) + walltime += v['time'] else: time = "-" - carbon += v['co2'] - walltime += v['time'] head_c = str(time) if self.co2: head_d = str(co2) else: head_d = str(chf) self.formatline(head_a, head_b, head_c, head_d) # End print body # Begin print footer carbon = "{:.2f}".format(carbon / 1000) money = "{:.2f}".format(money) walltime = "{:.2f}".format(walltime) self.message.append(self.hseparator) self.formatfooter("Total costs:", str(money), "CHF") self.formatfooter("Total walltime:", str(walltime), self.unit + "-hrs") self.formatfooter("Estimated carbon footprint:", str(carbon), "kg. eCO²") self.message.append(self.hseparator) # End print footer def request(self, args): # First case : without arguments if all(v == None for v in [args["user"], args["account"], args["start"], args["end"], args["billing"]]) and not args["all"]: self.response = requests.get( self.server + '/user/' + getpass.getuser()) self.format = 2 # Second case : only with user elif args["user"] and all(v == None for v in [args["account"], args["start"], args["end"], args["billing"]]) and not args["all"]: self.response = requests.get(self.server + '/user/' + args["user"]) self.format = 2 # Third case : only with account elif args["account"] and all(v == None for v in [args["user"], args["start"], args["end"], args["billing"]]) and not args["all"]: self.response = requests.get( self.server + '/account/' + args["account"]) self.format = 1 # Fourth case : with user and account (without range) elif all(v != None for v in [args["user"], args["account"]]) and all(v == None for v in [args["start"], args["end"], args["billing"]]) and not args["all"]: self.response = requests.get( self.server + '/account/' + args["account"] + '/' + args["user"]) self.format = 2 # Fifth case : with account and all elif args["account"] and all(v == None for v in [args["user"], args["start"], args["end"], args["billing"]]) and args["all"]: self.response = requests.get( self.server + '/account/' + args["account"] + '/' + "all") self.format = 3 # Sixth case : with range elif all(v != None for v in [args["start"], args["end"]]) and (args["user"] or args["account"]): if args["account"]: if args["all"]: self.response = requests.get( self.server + '/range/all/' + self.firstday + '/' + self.lastday + '/' + args["account"]) self.format = 3 else: self.response = requests.get( self.server + '/range/account/' + self.firstday + '/' + self.lastday + '/' + args["account"]) self.format = 1 elif args["user"]: self.response = requests.get( self.server + '/range/user/' + self.firstday + '/' + self.lastday + '/' + args["user"]) self.format = 2 # Seventh case : with billing elif args["billing"] is not None: secure_file = "/etc/sausage/sausage.secure" self.secure_parser = configparser.ConfigParser() self.secure_parser.read(secure_file) self.token = self.secure_parser.get("security", "token") args["billing"] = str(args["billing"]) self.response = requests.get( self.server + '/billing/' + self.token + '/' + args["billing"].split(sep='-')[0] + '/' + args["billing"].split(sep='-')[1]) self.format = 4 if self.response.status_code == 200: self.printbox() conf_file = "/etc/sausage/sausage.cfg" options = AppArgs(conf_file) getdata = GetData(conf_file, options.response) for item in getdata.message: print("#" + "{0:^57}".format(item) + "#")