added type hinting to PowerDiagram

work related to [https://bugzilla.ipr.univ-rennes.fr/show_bug.cgi?id=3790]
This commit is contained in:
Guillaume Raffy 2024-03-12 18:15:04 +01:00
parent e58f06e14a
commit 5e402a2e45
1 changed files with 57 additions and 34 deletions

View File

@ -6,11 +6,16 @@
This application takes its input from a database, currently in the form of an sql dump, but it could easily be adapted to read directly from a mysql database This application takes its input from a database, currently in the form of an sql dump, but it could easily be adapted to read directly from a mysql database
''' '''
from typing import Dict, List
from pathlib import Path
import re import re
import pygraphviz # port install py-pygraphviz import pygraphviz # port install py-pygraphviz
from inventory import Inventory, MachineSpecIdNotFound from .inventory import Inventory, MachineSpecIdNotFound
from SimpaDbUtil import SqlFile, SqlDatabaseReader, TableAttrNotFound from .SimpaDbUtil import SqlFile, SqlDatabaseReader, TableAttrNotFound
# from Lib import SimpaDbUtil
MachineName = str
PlugName = str
RackId = str
def add_capacity_constraints(capacity1, capacity2): def add_capacity_constraints(capacity1, capacity2):
@ -34,7 +39,15 @@ class Machine(object):
""" """
represents a device with input and output power plugs. It could represent a power consumer such as a server (in which case, it has no output plug), or a power distrubtion unit (in which case, it has one input plug and more than one output plugs), or even something else... represents a device with input and output power plugs. It could represent a power consumer such as a server (in which case, it has no output plug), or a power distrubtion unit (in which case, it has one input plug and more than one output plugs), or even something else...
""" """
def __init__(self, name, power_config): name: MachineName
input_plugs: Dict[PlugName, 'Plug']
output_plugs: Dict[PlugName, 'Plug']
current_capacity_constraint: float
power_config: 'PowerConfig'
power_consumption: float
rack_id: RackId
def __init__(self, name: MachineName, power_config: 'PowerConfig'):
self.name = name self.name = name
self.input_plugs = {} self.input_plugs = {}
self.output_plugs = {} self.output_plugs = {}
@ -43,27 +56,27 @@ class Machine(object):
self.power_consumption = 0.0 self.power_consumption = 0.0
self.rack_id = None self.rack_id = None
def get_plug(self, plug_name): def get_plug(self, plug_name: PlugName) -> 'Plug':
plugs = {'i': self.input_plugs, 'o': self.output_plugs}[plug_name[0]] plugs = {'i': self.input_plugs, 'o': self.output_plugs}[plug_name[0]]
if plug_name not in plugs: if plug_name not in plugs:
plugs[plug_name] = Plug(plug_name, self, self.power_config) plugs[plug_name] = Plug(plug_name, self, self.power_config)
return plugs[plug_name] return plugs[plug_name]
def get_max_amperes(self): def get_max_amperes(self) -> float:
capacity = None capacity = None
if len(self.input_plugs) > 0: if len(self.input_plugs) > 0:
capacity = list(self.input_plugs.values())[0].get_max_amperes() capacity = list(self.input_plugs.values())[0].get_max_amperes()
capacity = add_capacity_constraints(capacity, self.current_capacity_constraint) capacity = add_capacity_constraints(capacity, self.current_capacity_constraint)
return capacity return capacity
def get_outgoing_connections(self): def get_outgoing_connections(self) -> List['Connection']:
outgoing_connections = [] outgoing_connections = []
for conn in self.power_config.connections: for conn in self.power_config.connections:
if conn.from_plug.machine == self: if conn.from_plug.machine == self:
outgoing_connections.append(conn) outgoing_connections.append(conn)
return outgoing_connections return outgoing_connections
def get_powered_machines(self): def get_powered_machines(self) -> Dict[MachineName, 'Machine']:
powered_machines = {} powered_machines = {}
powered_machines[self.name] = self powered_machines[self.name] = self
for conn in self.get_outgoing_connections(): for conn in self.get_outgoing_connections():
@ -73,7 +86,7 @@ class Machine(object):
powered_machines[machine.name] = machine powered_machines[machine.name] = machine
return powered_machines return powered_machines
def get_power_consumption(self, worst_case_scenario=False): def get_power_consumption(self, worst_case_scenario: bool = False) -> float:
""" """
Returns the number of watts going through this 'machine' (which could just be a powerstrip) Returns the number of watts going through this 'machine' (which could just be a powerstrip)
@ -99,7 +112,12 @@ class Plug(object):
""" """
represents a power plug (input or output) of a device represents a power plug (input or output) of a device
""" """
def __init__(self, name, machine, power_config): name: PlugName
machine: Machine
current_capacity_constraint: float
power_config: 'PowerConfig'
def __init__(self, name: PlugName, machine: Machine, power_config: 'PowerConfig'):
self.name = name self.name = name
self.machine = machine self.machine = machine
self.current_capacity_constraint = None # the maximum amperes in this connection self.current_capacity_constraint = None # the maximum amperes in this connection
@ -108,19 +126,19 @@ class Plug(object):
def __str__(self): def __str__(self):
return self.machine.name + '.' + self.name return self.machine.name + '.' + self.name
def get_incoming_connection(self): def get_incoming_connection(self) -> 'Connection':
return self.power_config.get_connection_to(self) return self.power_config.get_connection_to(self)
# def get_outgoing_connections(self): # def get_outgoing_connections(self):
# return self.power_config.get_connections_from(self) # return self.power_config.get_connections_from(self)
def is_input_plug(self): def is_input_plug(self) -> bool:
return self.name[0] == 'i' return self.name[0] == 'i'
def set_current_capacity_constraint(self, max_amps): def set_current_capacity_constraint(self, max_amps: float):
self.current_capacity_constraint = max_amps self.current_capacity_constraint = max_amps
def get_max_amperes(self): def get_max_amperes(self) -> float:
capacity = None capacity = None
debug = False debug = False
@ -149,16 +167,19 @@ class Connection(object):
""" """
a power cable connecting an input power plug to an output power plug a power cable connecting an input power plug to an output power plug
""" """
from_plug: Plug
to_plug: Plug
current_capacity_constraint: float # the maximum amperes in this connection
def __init__(self, from_plug, to_plug): def __init__(self, from_plug: Plug, to_plug: Plug):
self.from_plug = from_plug self.from_plug = from_plug
self.to_plug = to_plug self.to_plug = to_plug
self.current_capacity_constraint = None # the maximum amperes in this connection self.current_capacity_constraint = None
def __str__(self): def __str__(self):
return str(self.from_plug) + ' -> ' + str(self.to_plug) + ' (' + str(self.from_plug.get_max_amperes()) + ' A, ' + str(self.get_max_amperes()) + ' A)' return str(self.from_plug) + ' -> ' + str(self.to_plug) + ' (' + str(self.from_plug.get_max_amperes()) + ' A, ' + str(self.get_max_amperes()) + ' A)'
def get_max_amperes(self): def get_max_amperes(self) -> float:
# gLogger.debug('%s (%s A) -> %s (%s A): ' % (str(self.from_plug), str(self.from_plug.get_max_amperes()), str(self.to_plug), str(self.to_plug.current_capacity_constraint))) # gLogger.debug('%s (%s A) -> %s (%s A): ' % (str(self.from_plug), str(self.from_plug.get_max_amperes()), str(self.to_plug), str(self.to_plug.current_capacity_constraint)))
capacity = self.from_plug.get_max_amperes() capacity = self.from_plug.get_max_amperes()
capacity = add_capacity_constraints(capacity, self.to_plug.current_capacity_constraint) capacity = add_capacity_constraints(capacity, self.to_plug.current_capacity_constraint)
@ -166,7 +187,7 @@ class Connection(object):
capacity = min(capacity, self.current_capacity_constraint) capacity = min(capacity, self.current_capacity_constraint)
return capacity return capacity
def is_redundancy_cable(self): def is_redundancy_cable(self) -> bool:
to_machine = self.to_plug.machine to_machine = self.to_plug.machine
my_power_provider = self.get_power_provider() my_power_provider = self.get_power_provider()
# find the first sibling cable that has the same provider as self # find the first sibling cable that has the same provider as self
@ -185,7 +206,7 @@ class Connection(object):
# for each provider, all cable but the 1st one are considered as redundant # for each provider, all cable but the 1st one are considered as redundant
return True return True
def get_power_provider(self): def get_power_provider(self) -> Machine:
from_machine = self.from_plug.machine from_machine = self.from_plug.machine
if from_machine.is_power_provider(): if from_machine.is_power_provider():
return from_machine return from_machine
@ -194,7 +215,7 @@ class Connection(object):
input_plug = from_machine.input_plugs[input_plug_names[0]] input_plug = from_machine.input_plugs[input_plug_names[0]]
return input_plug.get_incoming_connection().get_power_provider() return input_plug.get_incoming_connection().get_power_provider()
def get_power_consumption(self, worst_case_scenario=False): def get_power_consumption(self, worst_case_scenario: bool = False) -> float:
""" """
Returns the number of watts going through this cable Returns the number of watts going through this cable
@ -222,8 +243,10 @@ class PowerConfig(object):
""" """
the description of how machines are connected together (in terms of electric power) the description of how machines are connected together (in terms of electric power)
""" """
machines: Dict[MachineName, Machine]
connections: List[Connection]
def __init__(self, simpa_db_sql_file_path): def __init__(self, simpa_db_sql_file_path: Path):
self.machines = {} self.machines = {}
self.connections = [] self.connections = []
@ -232,7 +255,7 @@ class PowerConfig(object):
inventory = Inventory(sql_reader) inventory = Inventory(sql_reader)
self._parse_from_inventory(inventory) self._parse_from_inventory(inventory)
def _parse_from_inventory(self, inventory): def _parse_from_inventory(self, inventory: Inventory):
""" """
:param Inventory inventory: :param Inventory inventory:
""" """
@ -240,7 +263,7 @@ class PowerConfig(object):
rows = inventory.query("SELECT * FROM machine_to_power") rows = inventory.query("SELECT * FROM machine_to_power")
for row in rows: for row in rows:
(to_plug_as_str, from_plug_as_str, powercordid) = row (to_plug_as_str, from_plug_as_str, _powercordid) = row
if to_plug_as_str != '': if to_plug_as_str != '':
conn = self._add_connection(from_plug_as_str, to_plug_as_str) conn = self._add_connection(from_plug_as_str, to_plug_as_str)
for plug in (conn.from_plug, conn.to_plug): for plug in (conn.from_plug, conn.to_plug):
@ -302,7 +325,7 @@ class PowerConfig(object):
try: try:
cpu_power_consumption = inventory.get_num_cpus(machine_name) * inventory.get_cpu_tdp(machine_name) cpu_power_consumption = inventory.get_num_cpus(machine_name) * inventory.get_cpu_tdp(machine_name)
estimated_max_power_consumption += cpu_power_consumption estimated_max_power_consumption += cpu_power_consumption
except TableAttrNotFound as e: # noqa: F841 except TableAttrNotFound as _e: # noqa: F841
pass # could happen for machines that have no cpu (eg simpa-switch-cisco-2) pass # could happen for machines that have no cpu (eg simpa-switch-cisco-2)
if measured_max_power_consumption is not None: if measured_max_power_consumption is not None:
machine.power_consumption = num_machines * measured_max_power_consumption machine.power_consumption = num_machines * measured_max_power_consumption
@ -312,25 +335,25 @@ class PowerConfig(object):
elif estimated_max_power_consumption is not None: elif estimated_max_power_consumption is not None:
machine.power_consumption = num_machines * estimated_max_power_consumption machine.power_consumption = num_machines * estimated_max_power_consumption
def get_connection_to(self, to_plug): def get_connection_to(self, to_plug: Plug) -> Connection:
for connection in self.connections: for connection in self.connections:
if connection.to_plug == to_plug: if connection.to_plug == to_plug:
return connection return connection
return None return None
def _get_machine(self, machine_name): def _get_machine(self, machine_name: MachineName) -> Machine:
if machine_name not in self.machines: if machine_name not in self.machines:
self.machines[machine_name] = Machine(machine_name, self) self.machines[machine_name] = Machine(machine_name, self)
return self.machines[machine_name] return self.machines[machine_name]
def _get_plug(self, plug_as_str): def _get_plug(self, plug_as_str: str) -> Plug:
elements = plug_as_str.split('_') elements = plug_as_str.split('_')
plug_name = elements[-1] plug_name = elements[-1]
machine_name = plug_as_str[0:-(len(plug_name) + 1)] machine_name = plug_as_str[0:-(len(plug_name) + 1)]
machine = self._get_machine(machine_name) machine = self._get_machine(machine_name)
return machine.get_plug(plug_name) return machine.get_plug(plug_name)
def _add_connection(self, from_plug_as_str, to_plug_as_str): def _add_connection(self, from_plug_as_str: str, to_plug_as_str: str):
from_plug = self._get_plug(from_plug_as_str) from_plug = self._get_plug(from_plug_as_str)
to_plug = self._get_plug(to_plug_as_str) to_plug = self._get_plug(to_plug_as_str)
conn = Connection(from_plug, to_plug) conn = Connection(from_plug, to_plug)
@ -346,7 +369,7 @@ class PowerConfig(object):
class CableColorer(object): class CableColorer(object):
def get_cable_color(self, cable, worst_case_scenario): def get_cable_color(self, cable: Connection, worst_case_scenario: bool):
""" """
:param Connection cable: :param Connection cable:
""" """
@ -355,7 +378,7 @@ class CableColorer(object):
class SimpleColorer(CableColorer): class SimpleColorer(CableColorer):
def get_cable_color(self, cable, worst_case_scenario): def get_cable_color(self, cable: Connection, worst_case_scenario: bool):
""" """
:param Connection cable: :param Connection cable:
""" """
@ -377,14 +400,14 @@ class SimpleColorer(CableColorer):
class RampColorer(CableColorer): class RampColorer(CableColorer):
@staticmethod @staticmethod
def hotness_to_hsv_color(hotness): def hotness_to_hsv_color(hotness: float):
""" """
:param float hotness: temperature of the wire ratio (0.0 : cold -> 1.0 : hot) :param float hotness: temperature of the wire ratio (0.0 : cold -> 1.0 : hot)
""" """
clamped_hotness = max(min(hotness, 1.0), 0.0) clamped_hotness = max(min(hotness, 1.0), 0.0)
return "%f, 1.0, 0.8" % ((clamped_hotness) * 0.1 + 0.23) return "%f, 1.0, 0.8" % ((clamped_hotness) * 0.1 + 0.23)
def get_cable_color(self, cable, worst_case_scenario): def get_cable_color(self, cable: Connection, worst_case_scenario: bool):
""" """
:param Connection cable: :param Connection cable:
""" """
@ -410,7 +433,7 @@ class RampColorer(CableColorer):
return color return color
def power_config_to_svg(power_config, svg_file_path, worst_case_scenario=True): def power_config_to_svg(power_config: PowerConfig, svg_file_path: Path, worst_case_scenario: bool = True):
""" """
creates a svg diagram representing the input power configuration creates a svg diagram representing the input power configuration
@ -506,7 +529,7 @@ def power_config_to_svg(power_config, svg_file_path, worst_case_scenario=True):
# sub.graph_attr['rank']='same' # sub.graph_attr['rank']='same'
# assert False # assert False
# graph.layout(prog='twopi') # graph.layout(prog='twopi')
with open('./toto.dot', 'w') as f: with open(svg_file_path.with_suffix('.dot'), 'w', encoding='utf8') as f:
f.write(graph.string()) f.write(graph.string())
graph.layout(prog='dot') graph.layout(prog='dot')