2016-09-20 18:05:41 +02:00
'''
The goal of this application is to generate a power diagram that will help system administrators to :
- document the power supply architecture
- easily spot potential power overloads ( for example if the power consumption exceeds the capacity of a cable )
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
'''
import sqlite3
import os
import re
import sys
import pygraphviz
2018-02-02 11:32:54 +01:00
from mysql2sqlite import mysql_to_sqlite
2016-09-21 15:24:19 +02:00
2016-09-20 18:05:41 +02:00
def add_capacity_constraints ( capacity1 , capacity2 ) :
"""
combines 2 capacity constraints ( max amperes ) together
: param float capacity1 : max amperes for the first capacity , None if there are no constraints
: param float capacity2 : max amperes for the second capacity , None if there are no constraints
: return float : max amperes for the combined capacity , None if there are no constraints
"""
if capacity1 is None :
return capacity2
else :
if capacity2 is None :
return capacity1
else :
return min ( capacity1 , capacity2 )
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 . . .
"""
def __init__ ( self , name , power_config ) :
self . name = name
self . input_plugs = { }
self . output_plugs = { }
self . current_capacity_constraint = None # the maximum amperes in this connection
self . power_config = power_config
self . power_consumption = 0.0
def get_plug ( self , plug_name ) :
plugs = { ' i ' : self . input_plugs , ' o ' : self . output_plugs } [ plug_name [ 0 ] ]
if plug_name not in plugs :
plugs [ plug_name ] = Plug ( plug_name , self , self . power_config )
return plugs [ plug_name ]
def get_max_amperes ( self ) :
capacity = None
if len ( self . input_plugs ) > 0 :
capacity = self . input_plugs . values ( ) [ 0 ] . get_max_amperes ( )
capacity = add_capacity_constraints ( capacity , self . current_capacity_constraint )
return capacity
def get_outgoing_connections ( self ) :
outgoing_connections = [ ]
for conn in self . power_config . connections :
if conn . from_plug . machine == self :
outgoing_connections . append ( conn )
return outgoing_connections
def get_power_consumption ( self ) :
power_consumption = self . power_consumption
for conn in self . get_outgoing_connections ( ) :
power_consumption + = conn . get_power_consumption ( )
2016-09-21 15:24:19 +02:00
# print("machine %s : power_consumption += %f" % (self.name, conn.get_power_consumption()))
2016-09-20 18:05:41 +02:00
return power_consumption
class Plug ( object ) :
"""
represents a power plug ( input or output ) of a device
"""
def __init__ ( self , name , machine , power_config ) :
self . name = name
self . machine = machine
self . current_capacity_constraint = None # the maximum amperes in this connection
self . power_config = power_config
def __str__ ( self ) :
return self . machine . name + ' . ' + self . name
def get_incoming_connection ( self ) :
return self . power_config . get_connection_to ( self )
# def get_outgoing_connections(self):
# return self.power_config.get_connections_from(self)
def is_input_plug ( self ) :
return self . name [ 0 ] == ' i '
def set_current_capacity_constraint ( self , max_amps ) :
self . current_capacity_constraint = max_amps
def get_max_amperes ( self ) :
capacity = None
if self . is_input_plug ( ) :
in_con = self . get_incoming_connection ( )
if in_con :
# apply incoming connection amperes limitation
capacity = add_capacity_constraints ( capacity , in_con . get_max_amperes ( ) )
2018-02-01 11:55:08 +01:00
# print(str(self)+ 'after incoming connection amperes limitation, capacity = ' + str(capacity))
2016-09-20 18:05:41 +02:00
else :
# apply the machine containing this plug's amperes limitation
capacity = add_capacity_constraints ( capacity , self . machine . get_max_amperes ( ) )
2018-02-01 11:55:08 +01:00
# print(str(self)+'apply the machine containing this plug s amperes limitation, capacity = ' + str(capacity))
2016-09-20 18:05:41 +02:00
# apply this plug's amperes limitation
capacity = add_capacity_constraints ( capacity , self . current_capacity_constraint )
2018-02-01 11:55:08 +01:00
# print(str(self)+'after apply this plug s amperes limitation, capacity = ' + str(capacity))
2016-09-20 18:05:41 +02:00
return capacity
# def get_power_consumption(self):
# power_consumption = 0.0
# for conn in self.get_outgoing_connections():
# power_consumption += conn.get_power_consumption()
# power_consumption += self.get_power_consumption()
# return self.from_plug.get_power_consumption()
class Connection ( object ) :
"""
a power cable connecting an input power plug to an output power plug
"""
def __init__ ( self , from_plug , to_plug ) :
self . from_plug = from_plug
self . to_plug = to_plug
self . current_capacity_constraint = None # the maximum amperes in this connection
def __str__ ( self ) :
2018-02-01 11:55:08 +01:00
return str ( self . from_plug ) + ' -> ' + str ( self . to_plug ) + ' ( ' + str ( self . from_plug . get_max_amperes ( ) ) + str ( self . get_max_amperes ( ) ) + ' A) '
2016-09-20 18:05:41 +02:00
def get_max_amperes ( self ) :
capacity = self . from_plug . get_max_amperes ( )
capacity = add_capacity_constraints ( capacity , self . to_plug . current_capacity_constraint )
if self . current_capacity_constraint is not None :
capacity = min ( capacity , self . current_capacity_constraint )
return capacity
def is_redundancy_cable ( self ) :
return not ( self . to_plug . name == ' i ' or self . to_plug . name == ' i1 ' )
def get_power_consumption ( self ) :
# at the moment, this program doesn't handle redundant power supplies properly:
# the energy dragged by a power supply depends whether both power supplies are connnected to the same provider or not
if self . is_redundancy_cable ( ) :
return 0.0 # consider secondary power supplies to drag 0 power
else :
return self . to_plug . machine . get_power_consumption ( )
class PowerConfig ( object ) :
"""
the description of how machines are connected together ( in terms of electric power )
"""
def __init__ ( self , simpa_db_sql_file_path ) :
self . machines = { }
self . connections = [ ]
self . _parse_from_simpa_db_sql_file ( simpa_db_sql_file_path )
def _parse_from_simpa_db_sql_file ( self , simpa_db_sql_file_path ) :
def get_table_attr ( cur , table , key_name , key_value , attr_name ) :
attr_value = None
cur . execute ( " SELECT " + attr_name + " FROM " + table + " WHERE " + key_name + " = ' " + key_value + " ' " )
rows = cur . fetchall ( )
if len ( rows ) > 0 :
attr_value = rows [ 0 ] [ 0 ]
return attr_value
def machine_name_toMachine_spec_id ( cur , machine_name ) :
machine_spec_id = get_table_attr ( cur , ' machines ' , ' name ' , machine_name , ' machine_spec_id ' )
return machine_spec_id
def machine_spec_id_to_power_consumption ( cur , machine_spec_id ) :
power_consumption = get_table_attr ( cur , ' machine_spec_to_power_consumption ' , ' machine_spec_id ' , machine_spec_id , ' power_consumption ' )
return power_consumption
def get_plug_type_attr ( cur , plug_type , attr_name ) :
# INSERT INTO `powerplug_type_desc` (`plug_type_id`, `genre`, `max_amps`) VALUES
# ('iec60309_blue_pne6h_32a_m', 'm', 32.0),
attr_value = get_table_attr ( cur , ' powerplug_type_desc ' , ' plug_type_id ' , plug_type , attr_name )
return attr_value
def read_plug_capacity ( cur , plug ) :
plug_capacity = None
machine_spec_id = machine_name_toMachine_spec_id ( cur , plug . machine . name )
if machine_spec_id is not None :
# INSERT INTO `powerplug_desc` (`machine_spec_id`, `powerplug_id`, `plug_type`) VALUES
# ('atos_mpdu_2901382', 'i', 'iec60309_blue_pne6h_32a_m'),
cur . execute ( " SELECT plug_type FROM powerplug_desc WHERE machine_spec_id= ' %s ' AND powerplug_id= ' %s ' " % ( machine_spec_id , plug . name ) )
rows = cur . fetchall ( )
if len ( rows ) > 0 :
plug_type = rows [ 0 ] [ 0 ]
2016-09-21 15:24:19 +02:00
# print('plug_type : %s' % plug_type)
2016-09-20 18:05:41 +02:00
plug_capacity = get_plug_type_attr ( cur , plug_type , ' max_amps ' )
2016-09-21 15:24:19 +02:00
#if plug_capacity:
# print('plug_capacity : %f A' % plug_capacity)
2018-02-01 11:55:08 +01:00
# print("read_plug_capacity : plug capacity for plug.machine.name="+plug.machine.name+" plug="+str(plug)+" : "+ str(plug_capacity)+ "A")
2016-09-20 18:05:41 +02:00
return plug_capacity
2016-09-21 15:24:19 +02:00
sqliteDbPath = ' :memory: ' # sqlite-specific special name for a file stored in memory. We could use something like '/tmp/simpadb.sqlite' here but this would make parsing really slow (1 minute instead of 1s), unless either :
# - proper fix : group of INSERT statements are surrounded by BEGIN and COMMIT (see http://stackoverflow.com/questions/4719836/python-and-sqlite3-adding-thousands-of-rows)
# - the file is stored on a solid state disk
2016-09-20 18:05:41 +02:00
try :
os . remove ( sqliteDbPath )
except :
pass
con = sqlite3 . connect ( sqliteDbPath )
f = open ( simpa_db_sql_file_path , ' r ' )
sql = f . read ( ) # watch out for built-in `str`
#print(sql)
cur = con . cursor ( )
#print(mysql_to_sqlite(sql))
cur . executescript ( mysql_to_sqlite ( sql ) )
cur . execute ( " SELECT * FROM machine_to_power " )
rows = cur . fetchall ( )
for row in rows :
( to_plug_as_str , from_plug_as_str , powercordid ) = row
if 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 ) :
plug_capacity = read_plug_capacity ( cur , plug )
if plug_capacity is not None :
plug . set_current_capacity_constraint ( plug_capacity )
cur . execute ( " SELECT * FROM power_output_specs " )
rows = cur . fetchall ( )
for row in rows :
# print row
# ('ups1_o1', 16.0),
( to_plug_as_str , max_amps_as_str ) = row
to_plug = self . _get_plug ( to_plug_as_str )
to_plug . set_current_capacity_constraint ( float ( max_amps_as_str ) )
for machine in self . machines . values ( ) :
machine_name = machine . name
if re . match ( ' simpatix.._.. ' , machine_name ) :
machine_name = ' _ ' . join ( machine_name . split ( ' _ ' ) [ 0 : - 1 ] )
2016-09-21 15:24:19 +02:00
# print(machine_name)
2016-09-20 18:05:41 +02:00
machine_spec_id = machine_name_toMachine_spec_id ( cur , machine_name )
if machine_spec_id is not None :
power_consumption = machine_spec_id_to_power_consumption ( cur , machine_spec_id )
if power_consumption is not None :
machine . power_consumption = power_consumption
def get_connection_to ( self , to_plug ) :
for connection in self . connections :
if connection . to_plug == to_plug :
return connection
return None
def _get_machine ( self , machine_name ) :
if machine_name not in self . machines :
self . machines [ machine_name ] = Machine ( machine_name , self )
return self . machines [ machine_name ]
def _get_plug ( self , plug_as_str ) :
elements = plug_as_str . split ( ' _ ' )
plug_name = elements [ - 1 ]
machine_name = plug_as_str [ 0 : - ( len ( plug_name ) + 1 ) ]
machine = self . _get_machine ( machine_name )
return machine . get_plug ( plug_name )
def _add_connection ( self , from_plug_as_str , to_plug_as_str ) :
from_plug = self . _get_plug ( from_plug_as_str )
to_plug = self . _get_plug ( to_plug_as_str )
conn = Connection ( from_plug , to_plug )
self . connections . append ( conn )
return conn
def __str__ ( self ) :
s = ' '
for c in self . connections :
s + = str ( c ) + ' \n '
return s
def power_config_to_svg ( power_config , svg_file_path ) :
"""
creates a svg diagram representing the input power configuration
: param PowerConfig power_config : the input power config
"""
graph = pygraphviz . AGraph ( )
graph . graph_attr [ ' overlap ' ] = ' false '
graph . graph_attr [ ' splines ' ] = ' true '
graph . edge_attr [ ' colorscheme ' ] = ' rdylgn9 ' # 'brbg11'
graph . node_attr [ ' shape ' ] = ' box '
graph . node_attr [ ' height ' ] = 0.3 # default 0.5 inches
graph . node_attr [ ' fontname ' ] = ' Helvetica ' # default : Times-Roman
graph . edge_attr [ ' fontsize ' ] = 10 # default : 14 pt
graph . edge_attr [ ' len ' ] = 1.5 # default : 1.0
def hotness_to_hsv_color ( hotness ) :
"""
: param float hotness : temperature of the wire ratio ( 0.0 : cold - > 1.0 : hot )
"""
clamped_hotness = max ( min ( hotness , 1.0 ) , 0.0 )
return " %f , 1.0, 0.8 " % ( ( 1.0 - clamped_hotness ) * 0.33 )
#graph.add_node('a')
#graph.add_node('b')
#graph.add_edge(u'a',u'b',color='blue')
#graph.add_edge(u'b',u'a',color='blue')
for con in power_config . connections :
2016-09-21 15:24:19 +02:00
# print(con.from_plug.machine.name, con.to_plug.machine.name)
2016-09-20 18:05:41 +02:00
if not con . is_redundancy_cable ( ) : # don't display redundancy cables, as they might overlap and hide the main one
power_consumption = con . get_power_consumption ( )
amperes = power_consumption / 220.0
color = ' green '
capacity = con . get_max_amperes ( )
if capacity == None :
label = ' ? '
else :
label = str ( capacity ) + ' A '
if amperes / capacity > 1.0 :
color = ' red '
elif amperes / capacity > 0.75 :
color = ' orange '
else :
color = ' green '
label = " %.1f / %s " % ( amperes , label )
#color='//%d' % int(9.0-amperes/capacity*8)
color = hotness_to_hsv_color ( pow ( amperes / capacity , 4.0 ) )
graph . add_edge ( con . from_plug . machine . name , con . to_plug . machine . name , color = color , label = label , penwidth = capacity * 0.25 )
graph . layout ( prog = ' twopi ' )
graph . draw ( svg_file_path )