concho/concho/dell.py

965 lines
46 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from concho.config import TableBasedConfigurator
from concho.config import Configurator
from concho.config import Module
from concho.config import Option
from concho.config import Config
from concho.config import Chassis
from concho.config import Cpu, Dimm
from abc import abstractmethod
# from xml.dom.minidom import parse
from lxml.html import parse
import re
import copy
def clean_string(string):
single_graphic_character_introducer = '\x99' # found in 'AMD EPYC 7262' after EPYC, god knows why
return string.replace(single_graphic_character_introducer, '')
class DellPowerEdgeC6220(TableBasedConfigurator):
def __init__(self):
super().__init__('c6220', num_cpu_per_server=2, num_servers=4)
def get_empty_price(self):
return 4890.0
def get_guarantee_price(self, guarantee_duration):
assert guarantee_duration <= 5, 'only 5 year guarantee is handled for %s' % self.host_type_id
return 880.0
def get_disk_upgrade_price(self, asked_disk_capacity):
assert 1.9e12 < asked_disk_capacity < 2.1e12, 'only 2To upgrades are handled for %s' % self.host_type_id
return 320.0
class DellPowerEdgeR620(TableBasedConfigurator):
def __init__(self):
super().__init__('r620', num_cpu_per_server=2, num_servers=1)
def get_empty_price(self):
return 860.0
def get_guarantee_price(self, guarantee_duration):
assert guarantee_duration <= 5, 'only 5 year guarantee is handled for %s' % self.host_type_id
return 240.0
def get_disk_upgrade_price(self, asked_disk_capacity):
assert 1.9e12 < asked_disk_capacity < 2.1e12, 'only 2To upgrades are handled for %s' % self.host_type_id
return -20.0 * self.num_servers
class DellPowerEdgeR630(TableBasedConfigurator):
def __init__(self):
super().__init__('r630', num_cpu_per_server=2, num_servers=1)
def get_empty_price(self):
# for r630 on 14/10/2016
# (x: price without procs, p2603: price of e5-2603v4, p2609: price of e5-2609v4)
# we want to know x, given dell's web site, where we can get the price for multiple proc but not 0
# x + p2603 = 948.0
# x + 2 * p2603 = 948.0 + 216
# => p2603 approx= 215.5
# => x = 948. - 215. = 733.0
# verification :
# x + p2609 = 1057.0
# => p2609 = 1057-733=324.0
# x + 2 * p2609 = 1381.0
return 733.0
def get_guarantee_price(self, guarantee_duration):
assert guarantee_duration <= 5, 'only 5 year guarantee is handled for %s' % self.host_type_id
return 240.0
def get_disk_upgrade_price(self, asked_disk_capacity):
assert 1.9e12 < asked_disk_capacity < 2.1e12, 'only 2To upgrades are handled for %s' % self.host_type_id
return 0.0 * self.num_servers
class DellPowerEdgeR730(TableBasedConfigurator):
def __init__(self):
super().__init__('r730', num_cpu_per_server=2, num_servers=1)
def get_empty_price(self):
# for r730 on 06/10/2016
# (x: price without procs, p1 : price of e5-2603v4, p2: price of e5-2609v4)
# we want to know x, given dell's web site, where we can get the price for multiple proc but not 0
# x + p1 = 1014.0
# x + 2 * p1 = 1014.0 + 216
# => p1 approx= 215.5
# => x = 1014. - 215. = 799.0
# x + p2 = 1123.0
# => p2 = 324.0
# x + 2 * p2 = 1447.0
return 799.0
def get_guarantee_price(self, guarantee_duration):
assert guarantee_duration <= 5, 'only 5 year guarantee is handled for %s' % self.host_type_id
return 240.0
def get_disk_upgrade_price(self, asked_disk_capacity):
assert 1.9e12 < asked_disk_capacity < 2.1e12, 'only 2To upgrades are handled for %s' % self.host_type_id
return 0.0 * self.num_servers
class DellPowerEdgeC4130(TableBasedConfigurator):
def __init__(self):
super().__init__('c4130', num_cpu_per_server=2, num_servers=1)
def get_empty_price(self):
# for c4130 on 14/10/2016
# x + 2 x E5-2640v4 + 128G + 2 * K80 + X520 + p5years = 12281€
# x + 2 x E5-2640v4 + 128G + 4 * K80 + X520 + p5years = 19317€
# price of a K80
# >>> (19317.-12281)/2
# 3518.0
# assuming the options cost the same as for R630 (X520=210€, p5years=240€, 128G=1778€, E5-2640v4=951€), the cost of the base system is :
# >>> 12281-951-951-1778-210-240-3518-3518
# 1115
# but if we integrate the X520 card so that we have a 10Gb ethernet in the base, the cost of the base system becomes :
# >>> 1115+210
# 1325
return 1325.0
def get_guarantee_price(self, guarantee_duration):
assert guarantee_duration <= 5, 'only 5 year guarantee is handled for %s' % self.host_type_id
return 240.0
def get_disk_upgrade_price(self, asked_disk_capacity):
assert 1.9e12 < asked_disk_capacity < 2.1e12, 'only 2To upgrades are handled for %s' % self.host_type_id
return 0.0 * self.num_servers
class DellPowerEdgeC6320(TableBasedConfigurator):
def __init__(self):
super().__init__('c6320', num_cpu_per_server=2, num_servers=4)
def get_empty_price(self):
# for 4xc6320 on 14/10/2016
# (x: price without procs, p2603: price of e5-2603v4, p2609: price of e5-2609v4)
# x + 4 x (2 x p2620 + p32G) = 5135 € HT
# x + 4 x (2 x p2640 + p128G + pX520 + p5years) = 15590 € HT
# x + 4 x (2 x p2650 + p128G + pX520 + p5years) = 17340 € HT
# x + 4 x (2 x p2660 + p128G + pX520 + p5years) = 19490 € HT
# by examining this and the price of processors on R630
# - E5-2620v4 : 458€
# - E5-2640v4 : 951€
# - E5-2650v4 : 1209€
# - E5-2660v4 : 1525€
# - E5-2680v4 : 1867€
# - E5-2690v4 : 2261€
# I could work out that :
# - the price of procs on c6320 is the price of procs on r630 * 85%
# - the price of the base c6320 with 32 Go and no proc at all is 2020.6
# - the price of the 32G to 128G upgrade is 6222.6 euros (cheaper price of 16G->128G upgrade on r630 : (1778*4 = 7112))
# details :
# >>> (19490.-17340)/8
# 268.75
# >>> (17340.-15590)/8
# 218.75
# >>> 218.75/258.
# 0.8478682170542635
# >>> 268.75/316
# 0.8504746835443038
# >>> 15590.0+((1209.0-951.0)*0.85)*8
# 17344.4
# >>> 15590.0+((1525.0-951.0)*0.85)*8
# 19493.2
# price of 128G ram upgrade assuming that 5years guarantee costs 880€ (same as c6220),
# >>> 15590.0+((458.0-951.0)*0.85)*8-210.0*4-880.0 - 5135.0
# 6222.6
# >>> 5135.0 - (458.0*0.85)*8
# 2020.6
return 2020.6
def get_guarantee_price(self, guarantee_duration):
assert guarantee_duration <= 5, 'only 5 year guarantee is handled for %s' % self.host_type_id
return 880.0
def get_disk_upgrade_price(self, asked_disk_capacity):
assert 1.9e12 < asked_disk_capacity < 2.1e12, 'only 2To upgrades are handled for %s' % self.host_type_id
return 0.0 * self.num_servers
class DellPowerEdgeR640(TableBasedConfigurator):
def __init__(self):
super().__init__('r640', num_cpu_per_server=2, num_servers=1)
def get_empty_price(self):
# on 29/09/2017
# (x: price without procs, p3106: price of Bronze-3106, p6126: price of Gold6126)
# we want to know x, given dell's web site, where we can get the price for multiple proc but not 0
# x + p3106 = 1067.0
# x + 2 * p3106 = 1067.0 + 320.0
# => p3106 = 320
# => x = 1067.0 - 320.0 = 747.0
# check if x computation is consistent with p6126
# x + p6126 = 2767
# x + 2 * p6126 = 4787.0
# => p6126 = 2020.0
# => x = 747.0 --> yes !
return 747.0
def get_dimm_price(self, dimm_capacity):
return {
8: 80.0,
16: 160.0,
32: 320.0,
64: 640.0
}[dimm_capacity]
def get_guarantee_price(self, guarantee_duration):
if guarantee_duration > 7:
assert False, 'guarantee of more than 7 years is not available on %s' % self.host_type_id
elif guarantee_duration >= 5:
return 270.0 # from dell matinfo4 online quotation
else:
# 5-year guarantee included in base price
return 0.0 * self.num_servers
@abstractmethod
def get_disk_upgrade_price(self, asked_disk_capacity):
assert 1.9e12 < asked_disk_capacity < 2.1e12, 'only 2To upgrades are handled for %s' % self.host_type_id
# Retrait des disques de base (2x600Go 10K SAS 2.5'') : -260.0 €
# Ajout d'un disque dur 1,2 To SAS 10k Tpm 2,5" - hotplug : 165.0 €
base_disks_removal_price = -260.0
disk_1200g_price = 165.0
return (base_disks_removal_price + disk_1200g_price * 2) * self.num_servers
class DellPowerEdgeR940(TableBasedConfigurator):
def __init__(self):
super().__init__('r940', num_cpu_per_server=4, num_servers=1)
def get_empty_price(self):
# price of r940 (with 2x xeon gold 5215 and 32 Go DDR4 @ 2933GHz) on 09/06/2020 : 3784€
# (x: price without procs, p5215: price of gold-5215, p6248: price of Gold6248)
# p6240 = 2684
# p6248 = 3442
# p8280l = 12075
# x + 2 * p5215 = 3784
# x + 4 * p6240 = 11886 => x = 1150
# x + 4 * p6248 = 14918 => x = 1150
# x + 4 * p8280l = 49450 => x = 1150
# => p5215 = 1317 (agrees with proc price on r640)
return 1150.0
def get_dimm_price(self, dimm_capacity):
return {
8: 80.0,
16: 160.0,
32: 320.0,
64: 640.0
}[dimm_capacity]
def get_guarantee_price(self, guarantee_duration):
if guarantee_duration > 7:
assert False, 'guarantee of more than 7 years is not available on %s' % self.host_type_id
elif guarantee_duration >= 5:
return 630.0 # from dell matinfo4 online quotation
else:
# 5-year guarantee included in base price
return 0.0
@abstractmethod
def get_disk_upgrade_price(self, asked_disk_capacity):
assert 1.9e12 < asked_disk_capacity < 2.1e12, 'only 2To upgrades are handled for %s' % self.host_type_id
# Retrait des disques de base (2x600Go 10K SAS 2.5'') : -260.0 €
# Ajout d'un disque dur 1,2 To SAS 10k Tpm 2,5" - hotplug : 165.0 €
base_disks_removal_price = -260.0
disk_1200g_price = 165.0
return (base_disks_removal_price + disk_1200g_price * 2) * self.num_servers
class DellPrecision3630(TableBasedConfigurator):
def __init__(self):
super().__init__('prceision3630', num_cpu_per_server=1, num_servers=1)
def get_empty_price(self):
return 449.0
def get_dimm_price(self, dimm_capacity):
return {
8: 80.0,
16: 160.0,
32: 320.0
}[dimm_capacity]
def get_guarantee_price(self, guarantee_duration):
assert guarantee_duration <= 5, 'only 5 year guarantee is handled for %s' % self.host_type_id
return 0.0
def get_disk_upgrade_price(self, asked_disk_capacity):
assert 1.9e12 < asked_disk_capacity < 2.1e12, 'only 2To upgrades are handled for %s' % self.host_type_id
return 0.0
class DellPowerEdgeC6420(TableBasedConfigurator):
def __init__(self, host_type_id):
super().__init__(host_type_id, num_cpu_per_server=2, num_servers=4)
def get_empty_price(self):
# for 4xc6420 on 19/06/2020 (from excel quotation)
#
#
# (x: price without procs, p2630: price of xeon gold 6230)
# x + 4 x (2 x p4210r + p48g) = 5368 € HT
# x + 4 x (2 x p6230r + p192g) = 27213 € HT
#
# p48g = 3 * 160.0 # the price of a 16G ram is 160.0 €
# p4210r = p4210r=978./2 # from r640 prices
#
# >>> p4210r=978./2
# >>> p6230r_upgrade = 13408.0
# >>> p6230r_for_r640 = 2165.0
# >>> num_servers_per_c6000 = 4
# >>> num_cpu_per_server = 2
# >>> p6230r_for_c6420 = (p6230r_upgrade + p4210r * (num_servers_per_c6000 * num_cpu_per_server))/(num_servers_per_c6000 * num_cpu_per_server)
# >>> p6230r_for_c6420
# 2165.0
# => p4210r seems to be the same on r640 and c6420
#
# pc6000 = 5368 - (p4210r * num_cpu_per_server + p48g) * num_servers_per_c6000
# >>> p16g = 160.0
# >>> p48g = p16g * 3
# >>> pc6000 = 5368 - (p4210r * num_cpu_per_server + p48g) * num_servers_per_c6000
# >>> pc6000
# -464.0
# >>> pc6000 + num_servers_per_c6000 * (p6230r_for_c6420 * num_cpu_per_server + p192g)
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# NameError: name 'p192g' is not defined
# >>> p192g = (192/16)*p16g
# >>> p192g
# 1920.0
# >>> pc6000 + num_servers_per_c6000 * (p6230r_for_c6420 * num_cpu_per_server + p192g)
# 24536.0
# >>> pc6000 + num_servers_per_c6000 * (p6230r_for_c6420 * num_cpu_per_server + p192g) + 1159 + 68 + 350 + 1100
# 27213.0
num_servers_per_c6000 = 4
num_cpu_per_server = 2
ram_price_per_gigabyte = 160.0 / 16 # 16G ram price : 160.0 €
xeon_silver_4210r_price = 978.0 / 2 # from r640 prices
basic_config_price = 5368.0
poweredge_c6000_price = basic_config_price - (xeon_silver_4210r_price * num_cpu_per_server + ram_price_per_gigabyte * 48) * num_servers_per_c6000
return poweredge_c6000_price
def get_dimm_price(self, dimm_capacity):
return {
8: 80.0,
16: 160.0,
32: 320.0,
64: 640.0
}[dimm_capacity]
def get_guarantee_price(self, guarantee_duration):
if guarantee_duration > 7:
assert False, 'guarantee of more than 7 years is not available on %s' % self.host_type_id
elif guarantee_duration >= 5:
return 1100.0 # from c6420-20200716-price
else:
# 5-year guarantee included in base price
return 0.0
def get_disk_upgrade_price(self, asked_disk_capacity):
assert 1.9e12 < asked_disk_capacity < 2.1e12, 'only 2To upgrades are handled for %s' % self.host_type_id
# from c6420-20200716-price
# | Ajout d'un disque dur 1 To SATA 7200 Tpm 3,5'' pour les 4 serveurs | | 4-3-1-14g096 | C6420 | 361 € | | € - |
return 361.0
class DellConfiguratorParser():
def __init__(self):
pass
def _get_module(self, root_element, section_label):
modules_element = root_element.xpath(self.get_xpath_filter('root_to_modules_element'))[0]
# print(modules_element)
for module_root in modules_element.xpath(self.get_xpath_filter('modules_element_to_modules')):
# print(module_root)
# blue modules such as "Processeurs (Passage)"
module_titles = module_root.xpath(self.get_xpath_filter('module_to_blue_title'))
if len(module_titles) > 0:
# print(len(module_title.text))
module_title = module_titles[0]
# print('module_title.text = %s ' % module_title.text)
# print(module_title.text_content())
if module_title.text == section_label:
return module_root
# grey modules such as 'Base'
module_titles = module_root.xpath(self.get_xpath_filter('module_to_grey_title'))
if len(module_titles) > 0:
# print(module_title.text)
# print(len(module_title.text))
module_title = module_titles[0]
# print(module_title.text_content())
if module_title.text == section_label:
return module_root
# assert False, 'failed to find module "%s"' % section_label
@abstractmethod
def price_str_as_float(self, price_as_str):
assert False
@abstractmethod
def get_module_label(self, module_id):
assert False
@abstractmethod
def get_xpath_filter(self, filter_id):
assert False
@abstractmethod
def get_base_price(self, html_root):
assert False
def _parse_proc_change_options(self, html_root):
proc_options = Module('processor-change')
# module_root_element = self._get_module(html_root, 'Processeurs (Passage)')
module_root_element = self._get_module(html_root, self.get_module_label('cpu_change'))
for option_root_element in module_root_element.xpath(self.get_xpath_filter('module_to_options')):
label_elements = option_root_element.xpath(self.get_xpath_filter('option_to_label'))
if len(label_elements) > 0:
label = clean_string(label_elements[0].text_content().replace('\n', ''))
price = self.price_str_as_float(option_root_element.xpath(self.get_xpath_filter('option_to_price'))[0].text_content())
# print(label, price)
num_cpus = 1
# Passage à processeur Intel Xeon Gold 6240L 2.6GHz, 24.75M Cache,10.40GT/s, 2UPI, Turbo, HT,18C/36T (150W) - DDR4-2933
match = re.match(r'^Passage à processeur Intel Xeon (?P<cpu_class>Silver|Gold|Platinium) (?P<cpu_number>[0-9][0-9][0-9][0-9][RLYU]?).*', label)
if match:
cpu_class = match['cpu_class'].lower()
if cpu_class == 'platinium':
cpu_class = 'platinum'
cpu_id = "intel-xeon-%s-%s" % (cpu_class, match['cpu_number'].lower())
if match is None:
# Passage à 2 Processeurs Intel Xeon Gold 6240L 2.6GHz, 24.75M Cache,10.40GT/s, 2UPI, Turbo, HT,18C/36T (150W) - DDR4-2933
match = re.match(r'^passage à 2 processeurs intel xeon (?P<cpu_class>silver|gold|platinium) (?P<cpu_number>[0-9][0-9][0-9][0-9][rly]?).*', label.lower())
if match:
num_cpus = 2
cpu_class = match['cpu_class'].lower()
if cpu_class == 'platinium':
cpu_class = 'platinum'
cpu_id = "intel-xeon-%s-%s" % (cpu_class, match['cpu_number'].lower())
if match is None:
# Passage à 2 processeurs AMD EPYC 7642 2.3GHz, 48C/96T, 256M Cache (225W) DDR4-3200
match = re.match(r'^passage à 2 processeurs amd epyc (?P<cpu_number>[0-9][0-9fh][0-9][0-9]).*', label.lower())
if match:
num_cpus = 2
cpu_id = "amd-epyc-%s" % (match['cpu_number'].lower())
assert match, 'unhandled label : %s' % label
# print(match['cpu_class'], match['cpu_number'])
option = Option(Cpu(cpu_id), price / num_cpus)
# print('_parse_proc_change_options : adding cpu %s (price = %f)' % (cpu_id, price / num_cpus))
proc_options.add_option(option)
return proc_options
def _parse_proc_options(self, html_root):
proc_options = Module('processor')
# module_root_element = self._get_module(html_root, 'Processeurs (Passage)')
module_root_element = self._get_module(html_root, self.get_module_label('additional_cpus'))
if module_root_element is not None:
for option_root_element in module_root_element.xpath(self.get_xpath_filter('module_to_options')):
label_elements = option_root_element.xpath(self.get_xpath_filter('option_to_label'))
if len(label_elements) > 0:
label = label_elements[0].text_content()
price = self.price_str_as_float(option_root_element.xpath(self.get_xpath_filter('option_to_price'))[0].text_content())
# print(label, price)
num_additional_cpus = 1
match = re.match(r'^Processeur additionnel Intel Xeon (?P<cpu_class>Silver|Gold|Platinium) (?P<cpu_number>[0-9][0-9][0-9][0-9][RLY]?).*', label)
if match is None:
# Ajout de 2 Processeurs Intel Xeon Gold 6240L 2.6GHz, 24.75M Cache,10.40GT/s, 2UPI, Turbo, HT,18C/36T (150W) - DDR4-2933
match = re.match(r'^ajout de 2 processeurs intel xeon (?P<cpu_class>silver|gold|platinium) (?P<cpu_number>[0-9][0-9][0-9][0-9][rly]?).*', label.lower())
assert match, 'unhandled label : %s' % label
num_additional_cpus = 2
# print(match['cpu_class'], match['cpu_number'])
cpu_class = match['cpu_class'].lower()
if cpu_class == 'platinium':
cpu_class = 'platinum'
cpu_id = "intel-xeon-%s-%s" % (cpu_class, match['cpu_number'].lower())
option = Option(Cpu(cpu_id), price / num_additional_cpus)
# print('_parse_proc_options : adding cpu %s (price = %f)' % (cpu_id, price / num_additional_cpus))
proc_options.add_option(option)
assert len(proc_options.options) > 0
return proc_options
def _parse_ram_options(self, html_root):
ram_options = Module('ram')
# module_root_element = self._get_module(html_root, 'Processeurs (Passage)')
module_root_element = self._get_module(html_root, self.get_module_label('ram_additions'))
for option_root_element in module_root_element.xpath(self.get_xpath_filter('module_to_options')):
label_elements = option_root_element.xpath(self.get_xpath_filter('option_to_label'))
if len(label_elements) > 0:
label = label_elements[0].text_content()
price = self.price_str_as_float(option_root_element.xpath(self.get_xpath_filter('option_to_price'))[0].text_content())
# print(label, price)
# Ajout d'une barette de 128Go 2667 Mhz LRDIMM
match = re.match(r'^Ajout d\'une barette de (?P<num_gb>[0-9]+)Go (?P<num_mhz>[0-9][0-9][0-9][0-9]) *M[Hh]z (?P<mem_type>LRDIMM|RDIMM)$', label)
if match:
# print(match['num_gb'], match['num_mhz'])
num_gb = int(match['num_gb'])
num_mhz = int(match['num_mhz'])
mem_type = match['mem_type'].lower()
if num_gb == 8 and num_mhz == 2667 and mem_type == 'rdimm':
num_mhz = 2933 # error in r940 configurator : incoherence between 'Mémoire 32 Go DDR4 à 2933MHz (4x8Go)' and 'Ajout d'une barette de 8Go 2667 Mhz RDIMM'
dimm = Dimm(num_gb=num_gb, num_mhz=num_mhz, mem_type=mem_type)
option = Option(dimm, price)
ram_options.add_option(option)
else:
# Optane DC Persistent Memory - 128Go 2666Mhz
match = re.match(r'^Optane DC Persistent Memory - (?P<num_gb>[0-9]+)Go (?P<num_mhz>[0-9][0-9][0-9][0-9])Mhz$', label)
if match:
# print(match['num_gb'], match['num_mhz'])
dimm = Dimm(num_gb=int(match['num_gb']), num_mhz=int(match['num_mhz']), mem_type='pmm') # persistent memory module
option = Option(dimm, price)
ram_options.add_option(option)
else:
assert False, 'unhandled label : %s' % label
assert len(ram_options.options) > 0
return ram_options
@abstractmethod
def _get_module_default_item(self, module_label, html_root):
assert False
def _parse_base_config(self, html_root, configurator):
base_config = Config(configurator)
base_config.num_servers = configurator.chassis.item.max_num_servers
base_config.num_cpu_per_server = 1
# initialize cpu
item_label = self._get_module_default_item('Processeurs (Passage)', html_root)
# Processeur Intel Xeon Silver 4208 2.1GHz,11M Cache,9.60GT/s, 2UPI,No Turbo, HT,8C/16T (85W) - DDR4-2400
match = re.match(r'^Processeur Intel Xeon (?P<cpu_class>Silver|Gold|Platinium) (?P<cpu_number>[0-9][0-9][0-9][0-9][RLYU]?).*', item_label)
if match:
cpu_id = "intel-xeon-%s-%s" % (match['cpu_class'].lower(), match['cpu_number'].lower())
if match is None:
match = re.match(r'^2 processeurs Intel Xeon (?P<cpu_class>Silver|Gold|Platinium) (?P<cpu_number>[0-9][0-9][0-9][0-9][RLYU]?).*', item_label)
if match:
base_config.num_cpu_per_server = 2
cpu_id = "intel-xeon-%s-%s" % (match['cpu_class'].lower(), match['cpu_number'].lower())
if match is None:
print('item_label=%s' % item_label)
# match = re.match(r'^2 Processeurs AMD EPYC (?P<cpu_number>[0-9][0-9][0-9][0-9]).*', item_label)
match = re.match(r'^2 Processeurs AMD EPYC (?P<cpu_number>[0-9][0-9][0-9][0-9]).*', clean_string(item_label))
if match:
base_config.num_cpu_per_server = 2
cpu_id = "amd-epyc-%s" % (match['cpu_number'].lower())
assert match, 'unhandled label : %s' % item_label
# print(match['cpu_class'], match['cpu_number'])
base_config.set_cpu(Cpu(cpu_id))
# initialize the default ram dimms
item_label = self._get_module_default_item(self.get_module_label('ram_change'), html_root)
# Mémoire 16 Go DDR4 à 2933MHz (1x16Go)
match = re.match(r'^Mémoire (?P<num_gb>[0-9]+) Go DDR[\-]?4 à (?P<num_mhz>[0-9]+)MHz \((?P<num_dimms>[0-9]+)x(?P<num_gb_per_dimm>[0-9]+)Go\)', item_label.replace('Mémoire de base : ', '').replace('De base ', ''))
assert match, 'unhandled label : %s' % item_label
dimm = Dimm(num_gb=int(match['num_gb_per_dimm']), num_mhz=int(match['num_mhz']), mem_type='rdimm')
num_dimms = int(match['num_dimms'])
if num_dimms == 1:
assert match['num_gb'] == match['num_gb_per_dimm']
# print(match['cpu_class'], match['cpu_number'])
cpu_slot_index = 0
mem_channel_index = 0
dimm_slot = 0
base_config.cpu_slots_mem[cpu_slot_index].mem_channels[mem_channel_index].dimms[dimm_slot] = dimm
else:
# evenly split dimms on channels
assert (num_dimms % base_config.num_cpu_per_server) == 0
num_dimms_per_cpu = num_dimms // base_config.num_cpu_per_server
for cpu_slot_index in range(base_config.num_cpu_per_server):
cpu_slots_mem = base_config.cpu_slots_mem[cpu_slot_index]
assert len(cpu_slots_mem.mem_channels) >= num_dimms_per_cpu
for channel_index in range(num_dimms_per_cpu):
mem_channel = cpu_slots_mem.mem_channels[channel_index]
dimm_slot = 0
mem_channel.dimms[dimm_slot] = dimm
return base_config
@staticmethod
def _deduce_base_cpu_price(base_cpu, cpu_options, additional_cpu_options):
'''
The price of the base config processor is not always available directly in the section 'additional processors' : as an example, r940's default processors are 2 xeon gold 5215 but it's not possible to add 2 other 5215 (probably because 5215 can only talk to another cpu, not 3). In this case the price of this base cpu can be deduced from the price for other cpus (difference between cpu upgrade and additional cpu)
Args:
base_cpu (Cpu): the cpu of the base configuration
cpu_options (Module): the available cpu options
additional_cpu_options (Module): the available additional cpu options
returns:
float: the estimated price of base_cpu
'''
base_cpu_price = None
for cpu_option in additional_cpu_options.options.values():
cpu = cpu_option.item
# assert cpu.uid in cpu_options.options, "unexpected case : %s is available in additional cpus but not in cpu upgrade options" % cpu.uid
if cpu.uid in cpu_options.options:
cpu_upgrade_option = cpu_options.options[cpu.uid]
deduced_base_cpu_price = cpu_option.price - cpu_upgrade_option.price
print('price of %s estimated from %s : %f (%f-%f)' % (base_cpu.uid, cpu.uid, deduced_base_cpu_price, cpu_option.price, cpu_upgrade_option.price))
if base_cpu_price is None:
base_cpu_price = deduced_base_cpu_price
else:
assert abs(base_cpu_price - deduced_base_cpu_price) <= 0.01
return base_cpu_price
def parse(self, dell_configurator_html_file_path, configurator):
html_root = parse(dell_configurator_html_file_path).getroot()
# print(dir(html_root))
# for e in html_root:
# print(e.tag)
# body = html_root.find('body')
# print(body)
# for e in body:
# print(e.tag, e.attrib)
# div = body.find('div')
# for e in div:
# print(e.tag, e.attrib)
# modules_element = body.xpath("//div[@class='col-md-10']")
module_root_element = self._get_module(html_root, 'Base')
assert module_root_element is not None
# option_root_elements = module_root_element.xpath(".//div[@class='row']")
# assert len(option_root_elements) > 0
# option_root_element = option_root_elements[0]
label_elements = module_root_element.xpath(self.get_xpath_filter('base_module_to_label'))
assert len(label_elements) > 0
label = label_elements[0].text_content().replace('\n', '')
# PowerEdge R640
match = re.match(r'^PowerEdge (?P<chassis_type>[CR][0-9][0-9][0-9][0-9]?).*', label)
assert match, 'unhandled label : %s' % label
# print(match['cpu_class'], match['cpu_number'])
chassis_id = "dell-poweredge-%s" % (match['chassis_type'].lower(), )
configurator.chassis = Option(Chassis(chassis_id), 0.0)
configurator.base_config = self._parse_base_config(html_root, configurator)
proc_change_module = self._parse_proc_change_options(html_root)
configurator.add_module(proc_change_module)
configurator.add_module(self._parse_proc_options(html_root))
configurator.add_module(self._parse_ram_options(html_root))
# compute the price of the base config cpu because for example in r940 configurator, the xeon gold 5215 appears in the basic config but not in additional cpus
base_cpu = configurator.base_config.cpu
if configurator.get_item_price(base_cpu.uid) is None:
base_cpu_price = DellConfiguratorParser._deduce_base_cpu_price(base_cpu, proc_change_module, configurator.modules['processor'])
if base_cpu_price is None:
# in the case of r6525, there was no additional processor module, and therefore we have no way to estimate the price of the base processor (amd-epyc-7262)
# so we fallback to an hardcoded estimated price from wikipedia
base_cpu_price = {
'amd-epyc-7262': 550.0,
}[base_cpu.uid]
configurator.modules['processor'].add_option(Option(base_cpu, base_cpu_price))
assert configurator.get_item_price(base_cpu.uid) is not None, 'failed to find the price of base cpu %s' % base_cpu.uid
# compute the price of the chassis
base_price = self.get_base_price(html_root)
# in case there's no additional processor modules, 'processor' module only has the base processor entry
# So we need to populate it with the processors coming from 'processor-change' module
for proc_change_option in configurator.modules['processor-change'].options.values():
cpu_already_exists = False
for proc_option in configurator.modules['processor'].options.values():
if proc_option.item.uid == proc_change_option.item.uid:
cpu_already_exists = True
break
if not cpu_already_exists:
cpu_price = proc_change_option.price + configurator.modules['processor'].options[base_cpu.uid].price
print('estimated price of cpu %s : %f' % (proc_change_option.item.uid, cpu_price))
configurator.modules['processor'].add_option(Option(Cpu(proc_change_option.item.uid), cpu_price))
# delete the 'processor-change' module as its items ids are the same as the ones in the 'processor' modules but their prices are 'wrong' (upgrade prices rather than item price).
# in a configuration, no item should be found more than once
del configurator.modules['processor-change']
one_cpu_price = configurator.get_item_price(configurator.base_config.cpu.uid)
ram_price = configurator.base_config.ram_price
configurator.chassis.price = base_price - configurator.base_config.num_cpus * one_cpu_price - ram_price
class DellConfiguratorParser2020(DellConfiguratorParser):
def __init__(self):
super().__init__()
def get_module_label(self, module_id):
return {
'cpu_change': 'Processeurs (Passage)',
'additional_cpus': 'Processeurs additionnels',
'ram_change': 'Mémoires (Passage)',
'ram_additions': 'Mémoire: Ajout de barettes additionnelles',
}[module_id]
def get_xpath_filter(self, filter_id):
return {
'root_to_modules_element': ".//div[@class='col-md-10']",
'modules_element_to_modules': ".//div[@class='col-md-12 module']",
'module_to_blue_title': ".//div[@class='col-md-4 module-title color-017EB8']",
'module_to_grey_title': ".//div[@class='col-md-4 module-title color-808080']",
'module_to_options': ".//div[@class='row']",
'option_to_label': ".//div[@class='option-not-selected ']",
'option_to_price': ".//div[@class='col-md-3 text-right option-price ']",
'base_module_to_label': ".//div[@class='option-selected ']",
}[filter_id]
def price_str_as_float(self, price_as_str):
# eg '+ 2,255.00 €'
match = re.match(r'^\s*(?P<sign>[-+]?)\s*(?P<numbers>[0-9.]*)\s*€\s*$', price_as_str.replace(',', ''))
assert match, 'unexpected price string (%s)' % price_as_str
# print(match['sign'], match['numbers'])
price_as_float = float("%s%s" % (match['sign'], match['numbers']))
return price_as_float
def _get_module_default_item(self, module_label, html_root):
module_root_element = self._get_module(html_root, module_label)
assert module_root_element is not None
for option_root_element in module_root_element.xpath(".//div[@class='row']"):
label_elements = option_root_element.xpath(".//div[@class='option-selected ']")
assert label_elements is not None
if len(label_elements) > 0:
label = label_elements[0].text_content().replace('\n', '')
price = self.price_str_as_float(option_root_element.xpath(".//div[@class='col-md-3 text-right option-price option-price-selected']")[0].text_content())
assert price == 0.0, 'default items are expected to have a price of 0.0 € (%s s price is %f)' % (label, price)
return label
assert False, 'failed to find the default item of module %s' % module_label
def get_base_price(self, html_root):
base_price = None
price_preview_element = html_root.xpath(".//div[@class='price-preview']")[0]
assert price_preview_element is not None
for price_element in price_preview_element.xpath(".//div"):
price_label_element = price_element.xpath(".//span[@class='col-md-4']")[0]
# <div class="price-preview">
# <div class="row"><span class="col-md-4">Prix</span><span
# class="col-md-8">1175.00 €</span></div>
# <div class="row"><span class="col-md-4">Options</span><span
# class="col-md-8">0.00 €</span></div>
# <div class="row"><span class="col-md-4">Total</span><span
# class="col-md-8">1175.00 €</span></div>
# </div>
assert price_label_element is not None
label = price_label_element.text_content().replace('\n', '')
if label == 'Prix':
price_value_element = price_element.xpath(".//span[@class='col-md-8']")[0]
assert price_value_element is not None
base_price = self.price_str_as_float(price_value_element.text_content())
assert base_price is not None
return base_price
class DellConfiguratorParser2021(DellConfiguratorParser):
def __init__(self):
super().__init__()
def get_module_label(self, module_id):
return {
'cpu_change': 'Processeurs (Passage)',
'additional_cpus': 'Processeurs additionnels',
'ram_change': 'Mémoires (Passage)',
'ram_additions': 'Mémoire: Ajout de barettes additionnelles',
}[module_id]
def get_xpath_filter(self, filter_id):
return {
'root_to_modules_element': ".//div[@class='modules']",
'modules_element_to_modules': ".//div[@class='product-module-configuration']",
'module_to_blue_title': ".//header",
'module_to_grey_title': ".//div[@class='col-md-4 module-title color-808080']",
'module_to_options': ".//div[@class='product-options-configuration-line']",
'option_to_label': ".//div[@class='option-info']",
'option_to_price': ".//div[@class='option-price']",
'base_module_to_label': ".//div[@class='product-options-configuration-block option-selected']",
}[filter_id]
def price_str_as_float(self, price_as_str):
# eg '+ 2255,00 €' # contains a Narrow No-Break Space (NNBSP) https://www.compart.com/en/unicode/U+202F
nnbsp = ''
match = re.match(r'^\s*(?P<sign>[-+]?)\s*(?P<numbers>[0-9.]*)\s*€\s*$', price_as_str.replace(',', '.').replace(nnbsp, ''))
assert match, 'unexpected price string (%s)' % price_as_str
# print(match['sign'], match['numbers'])
price_as_float = float("%s%s" % (match['sign'], match['numbers']))
return price_as_float
def _get_module_default_item(self, module_label, html_root):
module_root_element = self._get_module(html_root, module_label)
assert module_root_element is not None
if module_label == self.get_module_label('ram_change'):
# <div
# class="product-options-configuration-block option-selected">
# <header>Mémoire 16 Go DDR4 à 3200MHz (1x16Go)<div
# class="option-selector"><i
# class="fas fa-check "></i></div>
# </header>
# <div class="mt-2 option-price">+ 0,00&nbsp;€</div>
# </div>
selected_option_filter = ".//div[@class='product-options-configuration-block option-selected']"
label_filter = ".//header"
price_filter = ".//div[@class='mt-2 option-price']"
else:
selected_option_filter = ".//div[@class='product-options-configuration-line option-selected']"
label_filter = ".//div[@class='option-info']"
price_filter = ".//div[@class='option-price']"
for selected_option_root_element in module_root_element.xpath(selected_option_filter):
label_elements = selected_option_root_element.xpath(label_filter)
assert label_elements is not None
if len(label_elements) > 0:
label = label_elements[0].text_content().replace('\n', '')
price = self.price_str_as_float(selected_option_root_element.xpath(price_filter)[0].text_content())
assert price == 0.0, 'default items are expected to have a price of 0.0 € (%s s price is %f)' % (label, price)
return label
assert False, 'failed to find the default item of module %s' % module_label
def get_base_price(self, html_root):
base_price = None
price_preview_element = html_root.xpath(".//div[@class='product-info']")[0]
assert price_preview_element is not None
for price_element in price_preview_element.xpath(".//div[@class='info']"):
price_label_element = price_element.xpath(".//span[@class='info-label']")[0]
# <div class="info"><span class="info-label">Prix de base</span><span
# class="info-value strong">1175 € HT</span></div>
# <hr>
# <div class="info"><span class="info-label">Avec options</span><span
# class="info-value strong">1175 € HT</span></div>
# <hr>
assert price_label_element is not None
label = price_label_element.text_content().replace('\n', '')
if label == 'Prix de base':
price_value_element = price_element.xpath(".//span[@class='info-value strong']")[0]
assert price_value_element is not None
base_price = self.price_str_as_float(price_value_element.text_content().replace(' HT', ''))
assert base_price is not None
return base_price
class MatinfoConfigurator(Configurator):
'''
a configurator using a server configurator web page from matinfo
'''
def __init__(self, configurator_html_file_path, html_parser):
super().__init__(self)
self.base_config = None
self.chassis = None
html_parser.parse(configurator_html_file_path, self)
def create_config(self):
# config = copy.deepcopy(self.base_config)
config = Config(self)
config.num_servers = self.base_config.num_servers
config.num_cpu_per_server = self.base_config.num_cpu_per_server
config.set_cpu(self.base_config.cpu)
config.cpu_slots_mem = copy.deepcopy(self.base_config.cpu_slots_mem)
return config
class DellMatinfoCsvConfigurator(Configurator):
'''
a configurator using the excel sheet from dell matinfo
eg the excel sheet sent to guillaume.raffy@univ-rennes1.fr on 16/07/2020
'''
def __init__(self, dell_csv_file_path):
super().__init__(self)
self.base_config = None
self.chassis = None
self.parse_csv_configurator(dell_csv_file_path)
def parse_csv_configurator(self, dell_csv_file_path):
COLUMN_LABEL = 0
COLUMN_MODEL = 1
COLUMN_PRICE = 4
with open(dell_csv_file_path, 'rt') as csv_file:
self.base_config = Config(self)
proc_options = Module('processor')
ram_options = Module('ram')
for line in csv_file.readlines():
line_cells = line.split('\t')
label = line_cells[COLUMN_LABEL]
match = re.match(r'^CAT3_Configuration n°1 \(Châssis rempli de serveurs identiques\)$', label)
if match:
match = re.match(r'poweredge (?P<chassis_id>c6[0-9]20)', line_cells[COLUMN_MODEL].lower())
assert match, "failed to recognize the chassis in '%s'" % line_cells[COLUMN_MODEL].lower()
self.chassis = Option(Chassis('dell-poweredge-%s' % match['chassis_id']), DellConfiguratorParser.price_str_as_float(line_cells[COLUMN_PRICE]))
continue
# 2 processeurs Intel Xeon Silver 4210R 2.4GHz, 13.75M Cache,9.60GT/s, 2UPI, Turbo, HT,10C/20T (100W) - DDR4-2400
match = re.match(r'^2 processeurs Intel Xeon (?P<cpu_class>Silver|Gold|Platinium) (?P<cpu_number>[0-9][0-9][0-9][0-9][RLYUM]?) .*$', label)
if match:
cpu_class = match['cpu_class'].lower()
if cpu_class == 'platinium':
cpu_class = 'platinum'
base_cpu_id = "intel-xeon-%s-%s" % (cpu_class, match['cpu_number'].lower())
assert self.chassis is not None
self.base_config.num_servers = self.chassis.item.max_num_servers
self.base_config.num_cpu_per_server = self.chassis.item.num_cpu_slots_per_server
self.base_config.set_cpu(Cpu(base_cpu_id))
continue
match = re.match(r'^Passage à 2 Processeurs Intel Xeon (?P<cpu_class>Silver|Gold|Platinium) (?P<cpu_number>[0-9][0-9][0-9][0-9][RLYUM]?) .*', label)
if match:
price = self.price_str_as_float(line_cells[COLUMN_PRICE])
cpu_class = match['cpu_class'].lower()
if cpu_class == 'platinium':
cpu_class = 'platinum'
cpu_id = "intel-xeon-%s-%s" % (cpu_class, match['cpu_number'].lower())
option = Option(Cpu(cpu_id), price / self.chassis.item.max_num_servers / self.chassis.item.num_cpu_slots_per_server)
proc_options.add_option(option)
continue
# Ajout d'une barette de 8Go 2667 Mhz DDR-4 - Pour les 4 serveurs
match = re.match(r'^Ajout d\'une barette de (?P<num_gb>[0-9]+)Go (?P<num_mhz>[0-9][0-9][0-9][0-9]) Mhz (?P<mem_technology>DDR-4) - Pour les 4 serveurs$', label)
if match:
price_for_four = self.price_str_as_float(line_cells[COLUMN_PRICE])
dimm = Dimm(mem_type='rdimm', num_gb=int(match['num_gb']), num_mhz=int(match['num_mhz']))
option = Option(dimm, price_for_four / 4.0)
ram_options.add_option(option)
continue
assert len(proc_options.options) > 0
assert len(ram_options.options) > 0
self.add_module(proc_options)
self.add_module(ram_options)
def create_config(self):
# config = copy.deepcopy(self.base_config)
config = Config(self)
config.num_servers = self.base_config.num_servers
config.num_cpu_per_server = self.base_config.num_cpu_per_server
config.set_cpu(self.base_config.cpu)
config.cpu_slots_mem = copy.deepcopy(self.base_config.cpu_slots_mem)
return config