-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Matthias Büchse <[email protected]>
- Loading branch information
Showing
1 changed file
with
337 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,337 @@ | ||
#!/usr/bin/env python3 | ||
import re | ||
|
||
|
||
class TypeCheck: | ||
def __call__(self, attr, value): | ||
raise ValueError(f"{attr} can not be set to {value}") | ||
|
||
|
||
class OptionalCheck(TypeCheck): | ||
def __init__(self, check): | ||
self.check = check | ||
|
||
def __call__(self, attr, value): | ||
if value is None: | ||
return | ||
self.check(attr, value) | ||
|
||
|
||
class TblCheck(TypeCheck): | ||
def __init__(self, tbl): | ||
self.tbl = tbl | ||
|
||
def __call__(self, attr, value): | ||
if value not in self.tbl: | ||
raise ValueError(f"{attr} can not be set to {value}; must be one of {tuple(self.tbl)}") | ||
|
||
|
||
class BoolCheck(TypeCheck): | ||
def __call__(self, attr, value): | ||
if not isinstance(value, bool): | ||
raise ValueError(f"{attr} can not be set to {value}; must be boolean") | ||
|
||
|
||
class IntCheck(TypeCheck): | ||
def __call__(self, attr, value): | ||
if not isinstance(value, int) or value <= 0: | ||
raise ValueError(f"{attr} can not be set to {value}; must be positive integer") | ||
|
||
|
||
class FloatCheck(TypeCheck): | ||
def __call__(self, attr, value): | ||
if not isinstance(value, float) or value <= 0 or int(2 * value) != 2 * value: | ||
raise ValueError(f"{attr} can not be set to {value}; must be positive multiple of 0.5") | ||
|
||
|
||
class Attr: | ||
typ = None | ||
default = None | ||
|
||
def __init__(self, name, default=None): | ||
self.name = name | ||
if default != self.default: | ||
self.default = default # instance attribute will override class attibute | ||
# the following will be set via __set_name__ | ||
self.attr = None | ||
self._attr = None | ||
|
||
def validate(self, val): | ||
if self.typ is None: | ||
return | ||
self.typ(self.attr, val) | ||
|
||
def __set_name__(self, owner, name): | ||
self.attr = name | ||
self._attr = '_' + name | ||
|
||
def __get__(self, obj, objclass=None): | ||
if obj is None: | ||
return self | ||
return getattr(obj, self._attr, self.default) | ||
|
||
def __set__(self, obj, value): | ||
self.validate(value) | ||
setattr(obj, self._attr, value) | ||
|
||
|
||
class IntAttr(Attr): | ||
typ = IntCheck() | ||
|
||
|
||
class OptIntAttr(Attr): | ||
typ = OptionalCheck(IntCheck()) | ||
|
||
|
||
class FloatAttr(Attr): | ||
typ = FloatCheck() | ||
|
||
|
||
class BoolAttr(Attr): | ||
typ = BoolCheck() | ||
|
||
|
||
class TblAttr(Attr): | ||
def __init__(self, name, tbl, default=None): | ||
super().__init__(name, default) | ||
self.typ = TblCheck(tbl) | ||
|
||
|
||
class DepTblAttr(Attr): | ||
def __init__(self, name, key_attr, deptbl, default=None): | ||
super().__init__(name, default) | ||
self.key_attr = key_attr | ||
self.typs = {key: TblCheck(tbl) for key, tbl in deptbl.items()} | ||
|
||
def __set__(self, obj, value): | ||
self.typs[self.key_attr.__get__(obj)](self.attr, value) | ||
super().__set__(obj, value) | ||
|
||
|
||
class Main: | ||
"""Class representing the first part (CPU+RAM)""" | ||
type = "CPU-RAM" | ||
cpus = IntAttr("#vCPUs") | ||
cputype = TblAttr("CPU type", {"L": "LowPerf vCPUs", "V": "vCPUs", "T": "SMT Threads", "C": "Dedicated Cores"}) | ||
cpuinsecure = BoolAttr("?Insec SMT") | ||
ram = FloatAttr("##GiB RAM") | ||
raminsecure = BoolAttr("?no ECC") | ||
ramoversubscribed = BoolAttr("?RAM Over") | ||
|
||
|
||
class Disk: | ||
"""Class representing the disk part""" | ||
type = "Disk" | ||
nrdisks = IntAttr("#:NrDisks") | ||
nrdisks.default = 1 | ||
disksize = OptIntAttr("#.GB Disk") | ||
disktype = TblAttr(".Disk type", {"n": "Networked", "h": "Local HDD", "s": "SSD", "p": "HiPerf NVMe"}) | ||
|
||
|
||
class Hype: | ||
"""Class repesenting Hypervisor""" | ||
type = "Hypervisor" | ||
hype = TblAttr(".Hypervisor", {None: None, "kvm": "KVM", "xen": "Xen", "hyv": "Hyper-V", "vmw": "VMware", "bms": "Bare Metal System"}) | ||
|
||
|
||
class HWVirt: | ||
"""Class repesenting support for hardware virtualization""" | ||
type = "Hardware/NestedVirtualization" | ||
hwvirt = BoolAttr("?HardwareVirt") | ||
|
||
|
||
class CPUBrand: | ||
"""Class repesenting CPU brand""" | ||
type = "CPUBrand" | ||
cpuvendor = TblAttr(".CPU Vendor", {"i": "Intel", "z": "AMD", "a": "ARM", "r": "RISC-V"}) | ||
cpugen = DepTblAttr("#.CPU Gen", cpuvendor, { | ||
"i": {0: "Unspec/Pre-Skylake", 1: "Skylake", 2: "Cascade Lake", 3: "Ice Lake", 4: "Sapphire Rapids"}, | ||
"z": {0: "Unspec/Pre-Zen", 1: "Zen 1", 2: "Zen 2", 3: "Zen 3", 4: "Zen 4"}, | ||
"a": {0: "Unspec/Pre-A76", 1: "A76/NeoN1", 2: "A78/X1/NeoV1", 3: "A710/NeoN2"}, | ||
}) | ||
perf = TblAttr("Performance", {"": "Std Perf", "h": "High Perf", "hh": "Very High Perf", "hhh": "Very Very High Perf"}) | ||
|
||
|
||
class GPU: | ||
"""Class repesenting GPU support""" | ||
type = "GPU" | ||
gputype = TblAttr(".Type", {"g": "vGPU", "G": "Pass-Through GPU"}) | ||
brand = TblAttr(".Brand", {"N": "nVidia", "A": "AMD", "I": "Intel"}) | ||
gen = DepTblAttr(".Gen", brand, { | ||
"N": {"f": "Fermi", "k": "Kepler", "m": "Maxwell", "p": "Pascal", | ||
"v": "Volta", "t": "Turing", "a": "Ampere", "l": "AdaLovelace"}, | ||
"A": {"0.4": "GCN4.0/Polaris", "0.5": "GCN5.0/Vega", "1": "RDNA1/Navi1x", "2": "RDNA2/Navi2x", "3": "RDNA3/Navi3x"}, | ||
"I": {"0.9": "Gen9/Skylake", "0.95": "Gen9.5/KabyLake", "1": "Xe1/Gen12.1", "2": "Xe2"}, | ||
}) | ||
cu = OptIntAttr("#.CU/EU/SM") | ||
perf = TblAttr("Performance", {"": "Std Perf", "h": "High Perf", "hh": "Very High Perf", "hhh": "Very Very High Perf"}) | ||
|
||
|
||
class IB: | ||
"""Class representing Infiniband""" | ||
type = "Infiniband" | ||
ib = BoolAttr("?IB") | ||
|
||
|
||
class Flavorname: | ||
def __init__(self): | ||
self.cpuram = None | ||
self.disk = None | ||
self.hype = None | ||
self.hwvirt = None | ||
self.cpubrand = None | ||
self.gpu = None | ||
self.ib = None | ||
|
||
|
||
class ComponentParser: | ||
def __init__(self, parsestr, targetcls): | ||
self.parsestr = parsestr | ||
self.targetcls = targetcls | ||
|
||
def parse(self, s, pos): | ||
m = self.parsestr.match(s, pos) | ||
if m is None: | ||
return None, pos | ||
match_attr = [att for att in self.targetcls.__dict__.values() if isinstance(att, Attr)] | ||
groups = m.groups() | ||
if len(groups) != len(match_attr): | ||
raise ValueError(f"unexpected number of matching groups: {match_attr} vs {groups}") | ||
t = self.targetcls() | ||
for attr, group in zip(match_attr, groups): | ||
if attr.name[:2] == "##": | ||
attr.__set__(t, float(group)) | ||
elif attr.name[:1] == "#": | ||
attr.__set__(t, int(group) if group else attr.default) | ||
elif attr.name[:1] == "?": | ||
attr.__set__(t, bool(group)) | ||
else: | ||
attr.__set__(t, group) | ||
return t, pos + len(m.group(0)) | ||
|
||
|
||
class Output: | ||
prefix = "SCS-" | ||
cpuram = "%i%s%?i-%f%?u%?o" | ||
disk = "-%1x%0i%s" | ||
hype = "_%s" | ||
hwvirt = "_%?hwv" | ||
cpubrand = "_%s%0i%s" | ||
gpu = "_%s%s%s%:i%s" | ||
ib = "_%?ib" | ||
|
||
def output_component(self, pattern, component, parts): | ||
if component is None: | ||
return | ||
attr_iter = iter([att for att in component.__class__.__dict__.values() if isinstance(att, Attr)]) | ||
i = 0 | ||
while i < len(pattern): | ||
j = i | ||
while i < len(pattern) and pattern[i] != "%": | ||
i += 1 | ||
if i > j: | ||
parts.append(pattern[j:i]) | ||
if i == len(pattern): | ||
break | ||
i += 1 # skip % | ||
attr = next(attr_iter) | ||
value = attr.__get__(component) | ||
if pattern[i] == "?": | ||
j = i + 1 | ||
while j < len(pattern) and pattern[j].isalnum(): | ||
j += 1 | ||
if value: | ||
parts.append(pattern[i + 1:j]) | ||
i = j - 1 | ||
elif pattern[i] == "s": | ||
parts.append(str(value)) | ||
elif pattern[i] == "i": | ||
parts.append(str(value)) | ||
elif pattern[i:i+2] == "0i": | ||
if value != int(pattern[i]): | ||
parts.append(str(value)) | ||
i += 1 | ||
elif pattern[i:i+2] == ":i": | ||
if value: | ||
parts.append(f"-{value}") | ||
i += 1 | ||
elif pattern[i:i+2] == "1x": | ||
if value != int(pattern[i]): | ||
parts.append(f"{value}x") | ||
i += 1 | ||
elif pattern[i] == "f": | ||
if value == int(value): | ||
parts.append(f"{value:.0f}") | ||
else: | ||
parts.append(f"{value:.1f}") | ||
else: | ||
print(pattern, i) | ||
raise RuntimeError("Pattern problem") | ||
i += 1 | ||
|
||
def output(self, flavorname): | ||
parts = [self.prefix] | ||
self.output_component(self.cpuram, flavorname.cpuram, parts) | ||
self.output_component(self.disk, flavorname.disk, parts) | ||
self.output_component(self.hype, flavorname.hype, parts) | ||
self.output_component(self.hwvirt, flavorname.hwvirt, parts) | ||
self.output_component(self.cpubrand, flavorname.cpubrand, parts) | ||
self.output_component(self.gpu, flavorname.gpu, parts) | ||
self.output_component(self.ib, flavorname.ib, parts) | ||
return "".join(parts) | ||
|
||
|
||
class SyntaxV1: | ||
prefix = "SCS-" | ||
cpuram = re.compile(r"([0-9]*)([LVTC])(i|):([0-9\.]*)(u|)(o|)") | ||
disk = re.compile(r":(?:([0-9]*)x|)([0-9]*)([nhsp]|)") | ||
hype = re.compile(r"\-(kvm|xen|vmw|hyv|bms)") | ||
hwvirt = re.compile(r"\-(hwv)") | ||
cpubrand = re.compile(r"\-([izar])([0-9]*)(h*)") | ||
gpu = re.compile(r"\-([gG])([NAI])([^:-]*)(?::([0-9]*)|)(h*)") | ||
ib = re.compile(r"\-(ib)") | ||
|
||
|
||
class SyntaxV2: | ||
prefix = "SCS-" | ||
cpuram = re.compile(r"([0-9]*)([LVTC])(i|)\-([0-9\.]*)(u|)(o|)") | ||
disk = re.compile(r"\-(?:([0-9]*)x|)([0-9]*)([nhsp]|)") | ||
hype = re.compile(r"_(kvm|xen|vmw|hyv|bms)") | ||
hwvirt = re.compile(r"_(hwv)") | ||
cpubrand = re.compile(r"_([izar])([0-9]*)(h*)") | ||
gpu = re.compile(r"_([gG])([NAI])([^-]*)(?:\-([0-9]*)|)(h*)") | ||
ib = re.compile(r"_(ib)") | ||
|
||
|
||
class Parser: | ||
def __init__(self, syntax): | ||
self.prefix = syntax.prefix | ||
self.cpuram = ComponentParser(syntax.cpuram, Main) | ||
self.disk = ComponentParser(syntax.disk, Disk) | ||
self.hype = ComponentParser(syntax.hype, Hype) | ||
self.hwvirt = ComponentParser(syntax.hwvirt, HWVirt) | ||
self.cpubrand = ComponentParser(syntax.cpubrand, CPUBrand) | ||
self.gpu = ComponentParser(syntax.gpu, GPU) | ||
self.ib = ComponentParser(syntax.ib, IB) | ||
|
||
def parse(self, s, pos=0): | ||
if not s[pos:].startswith(self.prefix): | ||
return | ||
pos += len(self.prefix) | ||
flavorname = Flavorname() | ||
for key, p in self.__dict__.items(): | ||
if not isinstance(p, ComponentParser): | ||
continue | ||
t, pos = p.parse(s, pos) | ||
setattr(flavorname, key, t) | ||
if flavorname.cpuram is None: | ||
raise ValueError(f"Error 10: Failed to parse main part of {s}") | ||
if pos != len(s): | ||
raise ValueError(f"Failed to parse name {s} to completion (after {pos})") | ||
return flavorname | ||
|
||
if __name__ == "__main__": | ||
namestr = "SCS-16T-64-3x10s_GNa-64_ib" | ||
print(namestr) | ||
print(Output().output(Parser(SyntaxV1).parse("SCS-16T:64:3x10s-GNa:64-ib"))) | ||
print(namestr == Output().output(Parser(SyntaxV2).parse(namestr))) |