added handler to made concho work with current hpe's website
This is not finished, there's still to : - remove hardcoded base chassis id and price - add handler for amd configurations (only intel configs are currently handled) - also refatored Dimm class to contain more details (will be useful to estimate ram throughput) - also added type hinting to dell.py work related to [https://bugzilla.ipr.univ-rennes.fr/show_bug.cgi?id=4015]
This commit is contained in:
parent
e1df4f6501
commit
3b002ee17b
|
@ -194,6 +194,44 @@ intel-xeon-platinum-8362 2.8 32 2 265 0 0
|
|||
intel-xeon-platinum-8368 2.4 38 2 270 0 0
|
||||
intel-xeon-platinum-8380 2.3 40 2 270 0 0
|
||||
|
||||
intel-xeon-bronze-3408u 1.8 8 2 125 0 0
|
||||
intel-xeon-silver-4410y 2.0 12 2 150 0 0
|
||||
intel-xeon-silver-4416+ 2.0 20 2 165 0 0
|
||||
intel-xeon-gold-5411n 1.9 24 2 165 0 0
|
||||
intel-xeon-gold-5415+ 2.9 8 2 150 0 0
|
||||
intel-xeon-gold-5416s 2.0 16 2 150 0 0
|
||||
intel-xeon-gold-5418n 1.8 24 2 165 0 0
|
||||
intel-xeon-gold-5418y 2.0 24 2 185 0 0
|
||||
intel-xeon-gold-5420+ 2.0 28 2 205 0 0
|
||||
intel-xeon-gold-6414u 2.0 32 1 250 0 0
|
||||
intel-xeon-gold-6416h 2.2 18 2 165 0 0
|
||||
intel-xeon-gold-6418h 2.1 24 2 185 0 0
|
||||
intel-xeon-gold-6421n 1.8 32 2 185 0 0
|
||||
intel-xeon-gold-6426y 2.5 16 2 185 0 0
|
||||
intel-xeon-gold-6430 2.1 32 2 270 0 0
|
||||
intel-xeon-gold-6434 3.7 8 2 195 0 0
|
||||
intel-xeon-gold-6438n 2.0 32 2 205 0 0
|
||||
intel-xeon-gold-6438y+ 2.0 32 2 205 0 0
|
||||
intel-xeon-gold-6442y 2.6 24 2 225 0 0
|
||||
intel-xeon-gold-6444y 3.6 16 2 270 0 0
|
||||
intel-xeon-gold-6448y 2.1 32 2 225 0 0
|
||||
intel-xeon-gold-6454s 2.2 32 2 270 0 0
|
||||
intel-xeon-gold-6458q 3.1 32 2 350 0 0
|
||||
intel-xeon-platinum-8444h 2.9 16 2 270 0 0
|
||||
intel-xeon-platinum-8452y 2.0 36 2 300 0 0
|
||||
intel-xeon-platinum-8458p 2.7 44 2 350 0 0
|
||||
intel-xeon-platinum-8458q 3.1 32 2 350 0 0
|
||||
intel-xeon-platinum-8460y+ 2.0 40 2 300 0 0
|
||||
intel-xeon-platinum-8462y+ 2.8 32 2 300 0 0
|
||||
intel-xeon-platinum-8468 2.1 48 2 350 0 0
|
||||
intel-xeon-platinum-8468v 2.4 48 2 330 0 0
|
||||
intel-xeon-platinum-8470 2.0 52 2 350 0 0
|
||||
intel-xeon-platinum-8470n 1.7 52 2 300 0 0
|
||||
intel-xeon-platinum-8470q 2.1 52 2 350 0 0
|
||||
intel-xeon-platinum-8480+ 2.0 56 2 350 0 0
|
||||
intel-xeon-platinum-8490h 1.9 60 2 350 0 0
|
||||
intel-xeon-platinum-9462 2.7 32 2 350 0 0
|
||||
|
||||
amd-epyc-7262 3.2 8 2 155 0 0
|
||||
amd-epyc-7272 2.9 12 2 120 0 0
|
||||
amd-epyc-7282 2.8 16 2 120 0 0
|
||||
|
|
|
|
@ -43,17 +43,50 @@ class Chassis(Item):
|
|||
self.num_dimm_slots_per_channel = 2
|
||||
|
||||
|
||||
class SdramChip():
|
||||
# eg 'sdr66', 'ddr-400', 'ddr4-2666', 'ddr5-4800', see https://en.wikipedia.org/wiki/DIMM
|
||||
chip_type: str # 'sdr' or 'ddr' (single or dual data rate)
|
||||
generation: Optional[int]
|
||||
transfer_rate: int # in mega transfer per second
|
||||
|
||||
def __init__(self, chip_type: str, generation: Optional[int], transfer_rate: int):
|
||||
self.chip_type = chip_type
|
||||
self.generation = generation
|
||||
self.transfer_rate = transfer_rate
|
||||
|
||||
|
||||
class DimmCas():
|
||||
# https://en.wikipedia.org/wiki/CAS_latency
|
||||
cas1: int
|
||||
cas2: int
|
||||
cas3: int
|
||||
|
||||
def __init__(self, cas1: int, cas2: int, cas3: int):
|
||||
self.cas1 = cas1
|
||||
self.cas2 = cas2
|
||||
self.cas3 = cas3
|
||||
|
||||
|
||||
class Dimm(Item):
|
||||
num_gb: int
|
||||
num_mhz: int
|
||||
sdram_chip: SdramChip
|
||||
mem_type: MemType
|
||||
cas: Optional[DimmCas]
|
||||
|
||||
def __init__(self, num_gb, num_mhz, mem_type):
|
||||
uid = "%s-%s-%s" % (mem_type, num_gb, num_mhz)
|
||||
super().__init__(uid)
|
||||
def __init__(self, num_gb: int, sdram_chip: SdramChip, cas: Optional[DimmCas], mem_type: str):
|
||||
'''
|
||||
mem_type: 'rdimm', 'pmm'
|
||||
'''
|
||||
self.num_gb = num_gb
|
||||
self.num_mhz = num_mhz
|
||||
self.sdram_chip = sdram_chip
|
||||
self.cas = cas
|
||||
self.mem_type = mem_type
|
||||
uid = "%s-%s-%s" % (mem_type, num_gb, self.num_mhz)
|
||||
super().__init__(uid)
|
||||
|
||||
@property
|
||||
def num_mhz(self):
|
||||
return self.sdram_chip.transfer_rate
|
||||
|
||||
|
||||
class Cpu(Item):
|
||||
|
@ -83,6 +116,14 @@ class Cpu(Item):
|
|||
proc_id = self.uid
|
||||
if re.match('intel-core-i[357]-8[0-9][0-9][0-9][ktbuh]', proc_id):
|
||||
return 'coffeelake'
|
||||
elif re.match('intel-xeon-bronze-[0-9]4[0-9][0-9]', proc_id):
|
||||
return 'sapphire rapids'
|
||||
elif re.match('intel-xeon-silver-[0-9]4[0-9][0-9]', proc_id):
|
||||
return 'sapphire rapids'
|
||||
elif re.match('intel-xeon-gold-[0-9]4[0-9][0-9]', proc_id):
|
||||
return 'sapphire rapids'
|
||||
elif re.match('intel-xeon-platinum-[0-9]4[0-9][0-9]', proc_id):
|
||||
return 'icelake'
|
||||
elif re.match('intel-xeon-silver-[0-9]3[0-9][0-9]', proc_id):
|
||||
return 'icelake'
|
||||
elif re.match('intel-xeon-gold-[0-9]3[0-9][0-9]', proc_id):
|
||||
|
@ -163,6 +204,10 @@ class Cpu(Item):
|
|||
# https://www.intel.com/content/www/us/en/products/sku/215269/intel-xeon-silver-4314-processor-24m-cache-2-40-ghz/specifications.html shows that even xeon silver 4314 has 2 AVX 512 fma units
|
||||
num_simd_per_core = 2
|
||||
|
||||
if proc_arch in ['sapphire rapids']:
|
||||
num_simd_per_core = 2
|
||||
# cpus_may2023_v3.pdf
|
||||
|
||||
if proc_arch == 'rome':
|
||||
num_simd_per_core = 1
|
||||
|
||||
|
@ -177,6 +222,7 @@ class Cpu(Item):
|
|||
'coffeelake': 6,
|
||||
'cascadelake': 6,
|
||||
'icelake': 8,
|
||||
'sapphire rapids': 8,
|
||||
'rome': 8,
|
||||
'milan': 8
|
||||
}[self.architecture]
|
||||
|
@ -252,6 +298,7 @@ def get_simd_id(proc_arch: CpuArchitecture) -> SimdId:
|
|||
'skylake': 'avx-512',
|
||||
'cascadelake': 'avx-512',
|
||||
'icelake': 'avx-512',
|
||||
'sapphire rapids': 'avx-512',
|
||||
'coffeelake': 'avx2',
|
||||
# from https://www.microway.com/knowledge-center-articles/detailed-specifications-of-the-amd-epyc-rome-cpus/:
|
||||
# - Full support for 256-bit AVX2 instructions with two 256-bit FMA units per CPU core. The previous “Naples” architecture split 256-bit instructions into two separate 128-bit operations
|
||||
|
@ -308,7 +355,7 @@ class Config():
|
|||
cpu: Optional[Cpu]
|
||||
cpu_slots_mem: List[CpuSlotMem]
|
||||
|
||||
def __init__(self, configurator):
|
||||
def __init__(self, configurator: 'Configurator'):
|
||||
self.configurator = configurator
|
||||
self.num_servers = 0
|
||||
self._num_cpu_per_server = 0
|
||||
|
@ -321,7 +368,7 @@ class Config():
|
|||
|
||||
@staticmethod
|
||||
def _find_dimm_combination(num_dimm_slots_per_channel: int, min_ram_per_channel: int, available_dimms: List['Option']) -> List[Dimm]:
|
||||
available_dimms.append(Option(Dimm(0, 0, 'dummy'), 0.0)) # fake dimm to represent empty slot
|
||||
available_dimms.append(Option(Dimm(num_gb=0, sdram_chip=SdramChip('ddr', generation=None, transfer_rate=66), cas=None, mem_type='dummy'), 0.0)) # fake dimm to represent empty slot
|
||||
slot_options = []
|
||||
|
||||
# try all combinations of dimms
|
||||
|
@ -446,6 +493,8 @@ class Config():
|
|||
dynamic_frequency_scaling = xeon_6248_avx512_base_freq / xeon_6248_base_freq
|
||||
elif self.cpu.architecture == 'icelake':
|
||||
dynamic_frequency_scaling = 0.9 # 0.9 is a guesstimate based on a web page that I found in january 2023 (I can't find it anymore) which showed that the frequence was less lowered on ice lake... if we could find actual figures, it would be great
|
||||
elif self.cpu.architecture == 'saphhire rapids':
|
||||
dynamic_frequency_scaling = 1.0 # sapphire rapids seem to get closer to theoretical speed, see /home/graffy/work/concho/cpus_may2023_v3.pdf
|
||||
cpu_clock_when_computing = self.cpu.clock * dynamic_frequency_scaling
|
||||
flops = self.cpu.num_dp_flop_per_cycle * cpu_clock_when_computing * 1.e9 * self.cpu.num_cores * self.num_cpu_per_server * self.num_servers
|
||||
return flops
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from typing import List
|
||||
from pathlib import Path
|
||||
from concho.config import TableBasedConfigurator
|
||||
from concho.config import Configurator
|
||||
|
@ -5,12 +6,13 @@ 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 concho.config import Cpu, Dimm, Price, MemSizeGb, MemSize, CpuId
|
||||
from concho.config import IHtmlConfiguratorParser
|
||||
|
||||
from abc import abstractmethod
|
||||
# from xml.dom.minidom import parse
|
||||
from lxml.html import parse
|
||||
from lxml.html import parse as html_parse
|
||||
from lxml.html import HtmlElement
|
||||
import re
|
||||
import copy
|
||||
|
||||
|
@ -242,7 +244,7 @@ class DellPowerEdgeR940(TableBasedConfigurator):
|
|||
def __init__(self):
|
||||
super().__init__('r940', num_cpu_per_server=4, num_servers=1)
|
||||
|
||||
def get_empty_price(self):
|
||||
def get_empty_price(self) -> Price:
|
||||
# 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
|
||||
|
@ -255,7 +257,7 @@ class DellPowerEdgeR940(TableBasedConfigurator):
|
|||
# => p5215 = 1317 (agrees with proc price on r640)
|
||||
return 1150.0
|
||||
|
||||
def get_dimm_price(self, dimm_capacity):
|
||||
def get_dimm_price(self, dimm_capacity: MemSizeGb) -> Price:
|
||||
return {
|
||||
8: 80.0,
|
||||
16: 160.0,
|
||||
|
@ -263,7 +265,7 @@ class DellPowerEdgeR940(TableBasedConfigurator):
|
|||
64: 640.0
|
||||
}[dimm_capacity]
|
||||
|
||||
def get_guarantee_price(self, guarantee_duration):
|
||||
def get_guarantee_price(self, guarantee_duration: int) -> Price:
|
||||
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:
|
||||
|
@ -273,7 +275,7 @@ class DellPowerEdgeR940(TableBasedConfigurator):
|
|||
return 0.0
|
||||
|
||||
@abstractmethod
|
||||
def get_disk_upgrade_price(self, asked_disk_capacity):
|
||||
def get_disk_upgrade_price(self, asked_disk_capacity: MemSize) -> Price:
|
||||
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 €
|
||||
|
@ -287,21 +289,21 @@ class DellPrecision3630(TableBasedConfigurator):
|
|||
def __init__(self):
|
||||
super().__init__('prceision3630', num_cpu_per_server=1, num_servers=1)
|
||||
|
||||
def get_empty_price(self):
|
||||
def get_empty_price(self) -> Price:
|
||||
return 449.0
|
||||
|
||||
def get_dimm_price(self, dimm_capacity):
|
||||
def get_dimm_price(self, dimm_capacity: MemSizeGb) -> Price:
|
||||
return {
|
||||
8: 80.0,
|
||||
16: 160.0,
|
||||
32: 320.0
|
||||
}[dimm_capacity]
|
||||
|
||||
def get_guarantee_price(self, guarantee_duration):
|
||||
def get_guarantee_price(self, guarantee_duration) -> Price:
|
||||
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):
|
||||
def get_disk_upgrade_price(self, asked_disk_capacity) -> Price:
|
||||
assert 1.9e12 < asked_disk_capacity < 2.1e12, 'only 2To upgrades are handled for %s' % self.host_type_id
|
||||
return 0.0
|
||||
|
||||
|
@ -311,7 +313,7 @@ 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):
|
||||
def get_empty_price(self) -> Price:
|
||||
# for 4xc6420 on 19/06/2020 (from excel quotation)
|
||||
#
|
||||
#
|
||||
|
@ -359,7 +361,7 @@ class DellPowerEdgeC6420(TableBasedConfigurator):
|
|||
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):
|
||||
def get_dimm_price(self, dimm_capacity) -> Price:
|
||||
return {
|
||||
8: 80.0,
|
||||
16: 160.0,
|
||||
|
@ -367,7 +369,7 @@ class DellPowerEdgeC6420(TableBasedConfigurator):
|
|||
64: 640.0
|
||||
}[dimm_capacity]
|
||||
|
||||
def get_guarantee_price(self, guarantee_duration):
|
||||
def get_guarantee_price(self, guarantee_duration) -> Price:
|
||||
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:
|
||||
|
@ -376,7 +378,7 @@ class DellPowerEdgeC6420(TableBasedConfigurator):
|
|||
# 5-year guarantee included in base price
|
||||
return 0.0
|
||||
|
||||
def get_disk_upgrade_price(self, asked_disk_capacity):
|
||||
def get_disk_upgrade_price(self, asked_disk_capacity) -> Price:
|
||||
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 € | | € - |
|
||||
|
@ -388,7 +390,7 @@ class DellConfiguratorParser(IHtmlConfiguratorParser):
|
|||
def __init__(self):
|
||||
pass
|
||||
|
||||
def _get_module(self, root_element, section_label):
|
||||
def _get_module(self, root_element: HtmlElement, section_label: str) -> HtmlElement:
|
||||
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')):
|
||||
|
@ -426,10 +428,10 @@ class DellConfiguratorParser(IHtmlConfiguratorParser):
|
|||
assert False
|
||||
|
||||
@abstractmethod
|
||||
def get_base_price(self, html_root):
|
||||
def get_base_price(self, html_root: HtmlElement) -> Price:
|
||||
assert False
|
||||
|
||||
def _parse_proc_change_options(self, html_root):
|
||||
def _parse_proc_change_options(self, html_root: HtmlElement) -> Module:
|
||||
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'))
|
||||
|
@ -469,7 +471,7 @@ class DellConfiguratorParser(IHtmlConfiguratorParser):
|
|||
proc_options.add_option(option)
|
||||
return proc_options
|
||||
|
||||
def _parse_proc_options(self, html_root):
|
||||
def _parse_proc_options(self, html_root: HtmlElement) -> Module:
|
||||
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'))
|
||||
|
@ -498,7 +500,7 @@ class DellConfiguratorParser(IHtmlConfiguratorParser):
|
|||
assert len(proc_options.options) > 0
|
||||
return proc_options
|
||||
|
||||
def _parse_ram_options(self, html_root):
|
||||
def _parse_ram_options(self, html_root: HtmlElement) -> Module:
|
||||
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'))
|
||||
|
@ -535,10 +537,10 @@ class DellConfiguratorParser(IHtmlConfiguratorParser):
|
|||
return ram_options
|
||||
|
||||
@abstractmethod
|
||||
def _get_module_default_item(self, module_label, html_root):
|
||||
def _get_module_default_item(self, module_label, html_root: HtmlElement):
|
||||
assert False
|
||||
|
||||
def _parse_base_config(self, html_root, configurator):
|
||||
def _parse_base_config(self, html_root: HtmlElement, configurator: Configurator) -> Config:
|
||||
base_config = Config(configurator)
|
||||
base_config.num_servers = configurator.chassis.item.max_num_servers
|
||||
base_config.num_cpu_per_server = 1
|
||||
|
@ -596,7 +598,7 @@ class DellConfiguratorParser(IHtmlConfiguratorParser):
|
|||
return base_config
|
||||
|
||||
@staticmethod
|
||||
def _deduce_base_cpu_price(base_cpu, cpu_options, additional_cpu_options):
|
||||
def _deduce_base_cpu_price(base_cpu: CpuId, cpu_options: List[Option], additional_cpu_options: List[Option]) -> Price:
|
||||
'''
|
||||
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)
|
||||
|
||||
|
@ -624,9 +626,9 @@ class DellConfiguratorParser(IHtmlConfiguratorParser):
|
|||
|
||||
return base_cpu_price
|
||||
|
||||
def parse(self, dell_configurator_html_file_path, configurator):
|
||||
def parse(self, dell_configurator_html_file_path: Path, configurator: Configurator):
|
||||
|
||||
html_root = parse(dell_configurator_html_file_path).getroot()
|
||||
html_root = html_parse(dell_configurator_html_file_path).getroot()
|
||||
# print(dir(html_root))
|
||||
# for e in html_root:
|
||||
# print(e.tag)
|
||||
|
|
|
@ -0,0 +1,294 @@
|
|||
from typing import List, Tuple
|
||||
from concho.config import IHtmlConfiguratorParser, Configurator, Module, Option, Cpu, Price, Dimm, SdramChip, DimmCas, Config, Chassis
|
||||
from pathlib import Path
|
||||
from lxml.html import HtmlElement, parse as parse_html
|
||||
import re
|
||||
import pandas as pd
|
||||
import json
|
||||
|
||||
|
||||
def parse_price(price_as_str: str) -> Price:
|
||||
# 'EUR 1,092.65'
|
||||
return Price(price_as_str.replace('EUR', '').replace(',', ''))
|
||||
|
||||
|
||||
class Quantity():
|
||||
num_selected: int # selected quantity
|
||||
choices: List[int] # choice of quantities
|
||||
|
||||
def __init__(self, quantity_details: str):
|
||||
'''
|
||||
quantity_details: eg '2/[0, 2, 4, 6, 8, 12, 16]'
|
||||
'''
|
||||
parts = quantity_details.split('/')
|
||||
self.num_selected = int(parts[0])
|
||||
self.choices = json.loads(parts[1])
|
||||
|
||||
|
||||
class HpeV2ConfiguratorParser(IHtmlConfiguratorParser):
|
||||
# <div id="ProcessorSection_AdditionalProcessorsChoice" class="choice_container choice_type_multi_select" choicename="Processors">
|
||||
# <div class="section_container">
|
||||
# <span id="section_title">Processors</span>
|
||||
# <div class="choicecontent FE-Carbon-Footprint__choicecontent FE-Occ-Configurator__choicecontent">
|
||||
# <table id="ProcessorSection_AdditionalProcessorsChoice_table" class="tablemulti no_extended">
|
||||
# <thead>
|
||||
# <tr>
|
||||
# <th class="tableheader description" width="300">Description</th>
|
||||
# <th class="tableheader partNumber" width="120">Part Number</th>
|
||||
# <th class="tableheader pcf">CO2e estimé</th>
|
||||
# <th class="tableheader price">Price</th>
|
||||
# <th class="tableheader quantity">Qty</th>
|
||||
# ...
|
||||
# </tr>
|
||||
# </thead>
|
||||
# <tbody>
|
||||
# <tr id="P49597-B21" itemid="P49597-B21" class="qtyZero P49597-B21" style="">
|
||||
# <td class="tabledetail radio-btn">
|
||||
# ...
|
||||
# </td>
|
||||
|
||||
# <td class="tabledetail ">
|
||||
# <span id="item_description_ProcessorSection_AdditionalProcessorsChoice_P49597-B21" class="item_description pull-left">
|
||||
# Intel Xeon-Gold 5415+ 2.9GHz 8-core 150W Processor for HPE
|
||||
# </span>
|
||||
|
||||
# <span id="item_info_ProcessorSection_AdditionalProcessorsChoice_P49597-B21" class="hpenew-circle-information item_info_icon pull-left eocs_item_tooltip">
|
||||
# <span class="eocs_item_tooltiptext">
|
||||
# <li class="eocs_msg_text">Mixing of Processors is not allowed</li>
|
||||
# </span>
|
||||
# </span>
|
||||
# <br>
|
||||
# </td>
|
||||
|
||||
# <td class="tabledetail ">
|
||||
# P49597-B21
|
||||
# </td>
|
||||
|
||||
# <td class="tabledetail column_hpe_preferred FE-Carbon-Footprint__hide">
|
||||
# </td>
|
||||
|
||||
# <td class="tabledetail column_recommended FE-Carbon-Footprint__hide">
|
||||
# <span class="icon-checkmark lightgraycolor greencolor">
|
||||
# </span>
|
||||
# <br>
|
||||
# </td>
|
||||
|
||||
# <td id="leadtime_P49597-B21" class="tabledetail thinLeadTime ">
|
||||
# <div>
|
||||
# <div style="background:Green; width:15px;height:15px;float:left;">
|
||||
# </div>
|
||||
# <div style="padding-left: 10px;float:left;">4D
|
||||
# </div>
|
||||
# </div>
|
||||
# </td>
|
||||
|
||||
# <td class="tabledetail endDate ">09/30/2025</td>
|
||||
|
||||
# <td class="tabledetail pcf">
|
||||
# <div class="FE-Carbon-Footprint__cf-row" title="CO2e / each">133 kg CO2e</div>
|
||||
# </td>
|
||||
|
||||
# <td id="item_price_ProcessorSection_AdditionalProcessorsChoice_P49597-B21" class="tabledetail price ">
|
||||
# <span>EUR 1,092.65</span>
|
||||
# </td>
|
||||
|
||||
# <td class="tabledetail">
|
||||
# <select id="item_dropdown_ProcessorSection_AdditionalProcessorsChoice_P49597-B21" class="tabledetailqtyselect" style="border:none;appearance:none;" disabled="disabled">
|
||||
# <option value="0" selected="selected">0</option>
|
||||
# <option value="1">1</option>
|
||||
# <option value="2">2</option>
|
||||
# </select>
|
||||
|
||||
# </td>
|
||||
# </tr>
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def get_module_label(self, module_id):
|
||||
return {
|
||||
'cpu_change': 'Processeurs (Passage)',
|
||||
'additional_cpus': 'ProcessorSection_AdditionalProcessorsChoice', # <div id="ProcessorSection_AdditionalProcessorsChoice" class="choice_container choice_type_multi_select" choicename="Processors">
|
||||
'ram': 'memory_memorySlotsChoice',
|
||||
}[module_id]
|
||||
|
||||
def get_xpath_filter(self, filter_id):
|
||||
return {
|
||||
'root_to_modules_element': ".//div[@class='choice_section_div']", # <div id="choice_section_div" class="choice_section_div">
|
||||
'modules_element_to_modules': ".//div[@class='choice_container choice_type_multi_select']", # <div id="ProcessorSection_AdditionalProcessorsChoice" class="choice_container choice_type_multi_select" choicename="Processors">
|
||||
'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]
|
||||
|
||||
@staticmethod
|
||||
def _parse_html_table(table_root_element: HtmlElement) -> pd.DataFrame:
|
||||
table_as_dict = {}
|
||||
thead_element = table_root_element.xpath(".//thead")[0]
|
||||
col_labels = []
|
||||
|
||||
ignored_classes = {
|
||||
'tableheader',
|
||||
'FE-Carbon-Footprint__hide'
|
||||
}
|
||||
|
||||
for th_element in thead_element.xpath(".//th"):
|
||||
classes = th_element.get('class')
|
||||
col_label_found = False
|
||||
for cl in classes.split(' '):
|
||||
if cl not in ignored_classes:
|
||||
col_labels.append(cl)
|
||||
table_as_dict[cl] = []
|
||||
col_label_found = True
|
||||
break
|
||||
assert col_label_found, f'failed to find a valid column label in {classes}'
|
||||
print(col_labels)
|
||||
|
||||
tbody_element = table_root_element.xpath(".//tbody")[0]
|
||||
for tr_element in tbody_element.xpath(".//tr"):
|
||||
td_elements = tr_element.xpath(".//td")
|
||||
assert len(td_elements) == len(col_labels)
|
||||
icol = 0
|
||||
for td_element in td_elements:
|
||||
col_label = col_labels[icol]
|
||||
if col_label == 'quantity':
|
||||
# <select id="item_dropdown_memory_memorySlotsChoice_P43328-B21" class="tabledetailqtyselect">
|
||||
# <option value="0">0</option>
|
||||
# <option value="2" selected="selected">2</option>
|
||||
# <option value="4">4</option>
|
||||
# <option value="6">6</option>
|
||||
# <option value="8">8</option>
|
||||
# <option value="12">12</option>
|
||||
# <option value="16">16</option>
|
||||
# </select>
|
||||
options = []
|
||||
selected_quantity = None
|
||||
for option_element in td_element.xpath(".//option"):
|
||||
quantity_choice = int(option_element.get('value'))
|
||||
options.append(quantity_choice)
|
||||
selected_as_str = option_element.get('selected')
|
||||
print(selected_as_str)
|
||||
if selected_as_str:
|
||||
selected_quantity = quantity_choice
|
||||
cell_value = f'{selected_quantity}/{options}'
|
||||
else:
|
||||
cell_value = ''.join(td_element.itertext()).replace('\t', '').replace('\n', ' ')
|
||||
table_as_dict[col_label].append(cell_value)
|
||||
icol += 1
|
||||
# print(table_as_dict)
|
||||
table = pd.DataFrame(table_as_dict)
|
||||
print(table)
|
||||
return table
|
||||
|
||||
def _get_module(self, root_element: HtmlElement, module_id: str) -> HtmlElement:
|
||||
'''
|
||||
'''
|
||||
modules_element = root_element.xpath(self.get_xpath_filter('root_to_modules_element'))[0]
|
||||
# print(modules_element)
|
||||
module_label = self.get_module_label(module_id) # eg ProcessorSection_AdditionalProcessorsChoice
|
||||
print(f'module label: {module_label}')
|
||||
module_root = modules_element.xpath(f".//div[@id='{module_label}']")[0]
|
||||
return module_root
|
||||
|
||||
def _parse_module_html_table(self, html_root: HtmlElement, module_id: str) -> pd.DataFrame:
|
||||
'''
|
||||
module_id: eg 'additional_cpus'
|
||||
'''
|
||||
module_root_element = self._get_module(html_root, module_id)
|
||||
assert module_root_element is not None
|
||||
table_root = module_root_element.xpath(".//table")[0]
|
||||
table = HpeV2ConfiguratorParser._parse_html_table(table_root)
|
||||
return table
|
||||
|
||||
def _parse_proc_options(self, proc_module_table: pd.DataFrame) -> Tuple[Module, List[Cpu]]:
|
||||
|
||||
proc_options = Module('processor')
|
||||
selected_procs = []
|
||||
# module_root_element = self._get_module(html_root, 'Processeurs (Passage)')
|
||||
for row_index, row in proc_module_table.iterrows():
|
||||
print(f'row = {row}')
|
||||
label = row['description']
|
||||
cpu_price = parse_price(row['price'])
|
||||
match = re.match(r'^ *Intel Xeon-(?P<cpu_class>Bronze|Silver|Gold|Platinum) (?P<cpu_number>[0-9][0-9][0-9][0-9][HNPQRSLUVY]?[+]?).*', label)
|
||||
assert match, 'unhandled label : %s' % label
|
||||
# print(match['cpu_class'], match['cpu_number'])
|
||||
cpu_class = match['cpu_class'].lower()
|
||||
cpu_id = "intel-xeon-%s-%s" % (cpu_class, match['cpu_number'].lower())
|
||||
cpu = Cpu(cpu_id)
|
||||
option = Option(cpu, cpu_price)
|
||||
for selected_item in range(Quantity(row['quantity']).num_selected):
|
||||
selected_procs.append(cpu)
|
||||
|
||||
proc_options.add_option(option)
|
||||
|
||||
assert len(proc_options.options) > 0
|
||||
return proc_options, selected_procs
|
||||
|
||||
def _parse_ram_options(self, ram_module_table: pd.DataFrame) -> Tuple[Module, List[Dimm]]:
|
||||
|
||||
ram_options = Module('ram')
|
||||
selected_dimms = []
|
||||
|
||||
# module_root_element = self._get_module(html_root, 'Processeurs (Passage)')
|
||||
for row_index, row in ram_module_table.iterrows():
|
||||
print(f'row = {row}')
|
||||
label = row['description'] # eg 'HPE 32GB (1x32GB) Dual Rank x8 DDR5-4800 CAS-40-39-39 EC8 Registered Smart Memory Kit'
|
||||
match = re.match(r'^ *HPE (?P<total_num_gb>[0-9]+)GB \((?P<num_dimms>[1-9]+)x(?P<num_gb_per_dimm>[0-9]+)GB\) (?P<dimm_rank>Single|Dual|Quad|Octal) +Rank +x(?P<by>[48]) +DDR(?P<ddr_generation>[0-9]+)-(?P<mega_transfers_per_sec>[0-9]+) +CAS-(?P<cas1>[0-9]+)-(?P<cas2>[0-9]+)-(?P<cas3>[0-9]+) EC8 Registered.*', label)
|
||||
assert match, 'unhandled label : %s' % label
|
||||
assert int(match['num_dimms']) == 1
|
||||
|
||||
dimm_price = parse_price(row['price'])
|
||||
sdram_chip = SdramChip('ddr', int(match['ddr_generation']), int(match['mega_transfers_per_sec']))
|
||||
cas = DimmCas(int(match['cas1']), int(match['cas2']), int(match['cas3']))
|
||||
dimm = Dimm(num_gb=int(match['num_gb_per_dimm']), sdram_chip=sdram_chip, cas=cas, mem_type='rdimm')
|
||||
option = Option(dimm, dimm_price)
|
||||
for selected_item in range(Quantity(row['quantity']).num_selected):
|
||||
selected_dimms.append(dimm)
|
||||
|
||||
ram_options.add_option(option)
|
||||
|
||||
assert len(ram_options.options) > 0
|
||||
return ram_options, selected_dimms
|
||||
|
||||
def parse(self, hpe_configurator_html_file_path: Path, configurator: Configurator):
|
||||
'''
|
||||
hpe_configurator_html_file_path : eg '/home/graffy/work/concho/catalogs/hpev2/20250314-cat2-conf16-hpe-dl380-gen11.html'
|
||||
'''
|
||||
hybris_file_path = hpe_configurator_html_file_path.parent / Path(str(hpe_configurator_html_file_path.stem) + '_files') / 'HybrisIntegrationLogin.html' # eg /home/graffy/work/concho/catalogs/hpev2/20250314-cat2-conf16-hpe-dl380-gen11_files/HybrisIntegrationLogin.html
|
||||
# print(hybris_file_path)
|
||||
html_root: HtmlElement = parse_html(str(hybris_file_path)).getroot()
|
||||
# print(type(html_root))
|
||||
|
||||
# configurator.base_config = self._parse_base_config()
|
||||
chassis_id = "hpe-proliant-dl380-gen11"
|
||||
configurator.chassis = Option(Chassis(chassis_id), 1000.0) # TODO: compute the chassis price
|
||||
|
||||
# configurator.base_config = self._parse_base_config(html_root, configurator)
|
||||
|
||||
configurator.base_config = Config(configurator)
|
||||
configurator.base_config.num_servers = 1
|
||||
configurator.base_config.num_cpu_per_server = 2
|
||||
configurator.base_config.get_price
|
||||
|
||||
proc_table = self._parse_module_html_table(html_root, 'additional_cpus')
|
||||
proc_module, selected_procs = self._parse_proc_options(proc_table)
|
||||
configurator.add_module(proc_module)
|
||||
assert len(selected_procs) == 1
|
||||
configurator.base_config.set_cpu(selected_procs[0])
|
||||
|
||||
ram_table = self._parse_module_html_table(html_root, 'ram')
|
||||
ram_module, selected_dimms = self._parse_ram_options(ram_table)
|
||||
configurator.add_module(ram_module)
|
||||
channel_index = 0
|
||||
for dimm in selected_dimms:
|
||||
configurator.base_config.cpu_slots_mem[0].mem_channels[channel_index].dimms.append(dimm)
|
||||
channel_index += 1
|
||||
# configurator.add_module(self._parse_ram_options(html_root))
|
||||
|
||||
# script_elements = html_root.xpath(".//script[@type='text/javascript']")
|
||||
# print('number of javascript scripts:', len(script_elements))
|
||||
# # script type="text/javascript"
|
||||
# db_jscript = None
|
|
@ -141,7 +141,8 @@ def plot_configs(configs, xaxis_def, yaxis_def, plot_title):
|
|||
'skylake': 0.4,
|
||||
'coffeelake': 0.6,
|
||||
'cascadelake': 0.8,
|
||||
'icelake': 1.0,
|
||||
'icelake': 0.9,
|
||||
'sapphire rapids': 1.0,
|
||||
'rome': 0.8,
|
||||
'milan': 1.0,
|
||||
}[Cpu(proc_id).architecture]
|
||||
|
@ -172,7 +173,8 @@ def plot_configs(configs, xaxis_def, yaxis_def, plot_title):
|
|||
'hpe-proliant-dl360-gen10': 0.3,
|
||||
'hpe-proliant-dl360-gen10+': 0.55,
|
||||
'hpe-proliant-dl385-gen10': 0.0,
|
||||
'hpe-proliant-dl385-gen10+': 0.0
|
||||
'hpe-proliant-dl385-gen10+': 0.0,
|
||||
'hpe-proliant-dl380-gen11': 0.1,
|
||||
}[model]
|
||||
value = 0.9
|
||||
return matplotlib.colors.hsv_to_rgb((hue, saturation, value))
|
||||
|
|
6
setup.py
6
setup.py
|
@ -2,6 +2,6 @@ from setuptools import setup, find_packages
|
|||
|
||||
|
||||
setup(name='concho',
|
||||
version='1.0',
|
||||
packages=find_packages(),
|
||||
install_requires=['lxml', 'numpy', 'matplotlib'])
|
||||
version='1.0',
|
||||
packages=find_packages(),
|
||||
install_requires=['lxml', 'numpy', 'matplotlib', 'pandas'])
|
|
@ -4,6 +4,7 @@ from concho.config import HtmlConfigurator
|
|||
from concho.dell import DellConfiguratorParser2020
|
||||
from concho.dell import DellConfiguratorParser2021
|
||||
from concho.hpe import HpeConfiguratorParser, HpeCpuChoiceConfiguratorParser
|
||||
from concho.hpev2 import HpeV2ConfiguratorParser
|
||||
from concho.procs_chooser import plot_configurators
|
||||
from concho.procs_chooser import ConfigPrice
|
||||
# from concho.procs_chooser import ConfigFlops
|
||||
|
@ -89,5 +90,17 @@ def test_ur1_presents_2023_configs():
|
|||
# plot_configurators(configurators=configurators, ram_per_core=4.0e9, xaxis_def=ConfigPrice(), yaxis_def=ConfigFlops(), plot_title='physmol/ts credit 2023 configs', config_filter=config_filter)
|
||||
|
||||
|
||||
def test_hpe_bpu11_configs():
|
||||
configurators = [
|
||||
# HtmlConfigurator('20210407 - Cat2 Conf4 PowerEdge R640 - Dell.html', DellConfiguratorParser2021()),
|
||||
HtmlConfigurator(Path('catalogs/hpev2/20250314-cat2-conf16-hpe-dl380-gen11.html'), HpeV2ConfiguratorParser()),
|
||||
]
|
||||
|
||||
def config_filter(config):
|
||||
return True # config.get_price() < 40000.0
|
||||
|
||||
plot_configurators(configurators=configurators, ram_per_core=4.0e9, xaxis_def=ConfigPrice(), yaxis_def=ConfigFlopsPerEuro(), plot_title='physmol/ts credit 2023 configs', config_filter=config_filter)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_ur1_presents_2023_configs()
|
||||
test_hpe_bpu11_configs()
|
||||
|
|
Loading…
Reference in New Issue