965 lines
46 KiB
Python
965 lines
46 KiB
Python
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 '+ 2 255,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 €</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
|