Skip to content

Commit

Permalink
Feat/generator form (#408)
Browse files Browse the repository at this point in the history
* Generate form for flavor name generation.
* 5 columns is enough, add NN to optional tbls.
* Use distinct field names by prefixing type.
  Also implement stripping ':' and '.' in descriptors.
  Add Generate button, though not yet effective.
* Start parsing generate form.
* Parsing of generator form mostly works.
  TODO: Handle booleans.
* Do a bit more input validation. Set parsed.
  ... to create output for extensions.
* Fix handling empty (optional) fields. Add short nm output.
* Work well without any input by filling in 0.
* Add a second button, typewrite for flavor name.
* Mention v3, link diskless flavor blog article.
* Check table values.
* Add table lookup debugging.
* Also remove dynamic tables again. Robustness ag/ missing attrs.
* Add dynamic table generation and output.
* Default to empty.
* Catch exceptions and display them as error message.
* Make flake8 happy and pylint happier.
* Better output formatting. Colors.
* Handle invalid (also empty) gpu.gen entries.
* Fix shebang, handle 1x0 disk properly.
* Handle case without generation (GPU, CPUvendor).
* Also handle cpugen and initialize to 0 if unset.
* Output letters that indicate table values / flags.
* Output full value (till next delimiter) as flag.

Signed-off-by: Kurt Garloff <kurt@garloff.de>
  • Loading branch information
garloff authored Dec 19, 2023
1 parent 63b61de commit a3b3568
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 24 deletions.
260 changes: 249 additions & 11 deletions Tests/iaas/flavor-naming/flavor-form.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def parse_name(fnm):
try:
FLAVOR_SPEC = fnmck.parsename(fnm)
except (TypeError, NameError, KeyError) as exc:
ERROR = f"\tERROR<br/>\n\t{exc}"
ERROR = f"\tERROR:\n\t{exc}"
return ()
ERROR = ""
return FLAVOR_SPEC
Expand All @@ -42,25 +42,260 @@ def parse_name(fnm):
def output_parse():
"output pretty description from SCS flavor name"
fnmd = importlib.import_module("flavor-name-describe")
print('\tInput an SCS flavor name such as e.g. SCS-2V-8 ...')
print('\t<br/>\n\t<FORM ACTION="/cgi-bin/flavor-form.py" METHOD="GET">')
print(f'\t Flavor name: <INPUT TYPE="text" NAME="flavor" SIZE=24 VALUE="{html.escape(FLAVOR_NAME, quote=True)}"/>')
print('\t <INPUT TYPE="submit" VALUE="Submit"/>')
print('\t <label for="flavor"?Flavor name:</label>')
print(f'\t <INPUT TYPE="text" ID="flavor" NAME="flavor" SIZE=24 VALUE="{html.escape(FLAVOR_NAME, quote=True)}"/>')
print('\t <INPUT TYPE="submit" VALUE="Parse"/>')
# print(' <INPUT TYPE="reset" VALUE="Clear"/>\n</FORM>')
print('\t</FORM>')
print('\t</FORM><br/>')
if FLAVOR_NAME:
print(f"\t<br/><b>Flavor {html.escape(FLAVOR_NAME, quote=True)}:</b>")
print(f"\t<br/><font size=+1 color=blue><b>Flavor <tt>{html.escape(FLAVOR_NAME, quote=True)}</tt>:</b></font>")
if FLAVOR_SPEC:
print(f"\t{html.escape(fnmd.prettyname(FLAVOR_SPEC), quote=True)}")
print(f"\t<font color=green>{html.escape(fnmd.prettyname(FLAVOR_SPEC), quote=True)}</font>")
else:
print("\tNot an SCS flavor")
print("\t<font color=brown>Not an SCS flavor</font>")
if ERROR:
print(f"\t<br/>{html.escape(ERROR, quote=True)})")
print(f"\t<br/><font color=red>{html.escape(ERROR, quote=True)}</font>")


def find_spec(lst, key):
"Find index of class name key in lst, -1 means not found"
for i, val in enumerate(lst):
if type(val).type == key:
return i
return -1


def find_attr(cls, key):
"Find index of attribute in object cls, -1 means not found"
for i, val in enumerate(cls.pattrs):
if val == key:
return i
return -1


def generate_name(form):
"Parse submitted form with flavor properties"
global ERROR, FLAVOR_SPEC, FLAVOR_NAME
ERROR = ""
FLAVOR_NAME = ""
FLAVOR_SPEC = (fnmck.Main("0L-0"), fnmck.Disk(""), fnmck.Hype(""), fnmck.HWVirt(""),
fnmck.CPUBrand(""), fnmck.GPU(""), fnmck.IB(""))
for key, val in form.items():
val = val[0]
print(f'{key}={val}', file=sys.stderr)
keypair = key.split(':')
idx = find_spec(FLAVOR_SPEC, keypair[0])
if idx < 0:
ERROR = f"ERROR: Unknown key {keypair[0]}"
return None
spec = FLAVOR_SPEC[idx]
idx2 = find_attr(spec, keypair[1])
if idx2 < 0:
ERROR = f"ERROR: Can not find attribute {keypair[1]} in {keypair[1]}"
return None
fdesc = spec.pnames[idx2]
if val == "NN":
val = ""
if val and val != "" and val != "0" and not (val == "1" and fdesc[0:2] == "#:"):
spec.parsed += 1
# Now parse fdesc to get the right value
if fdesc[0:2] == '##':
setattr(spec, keypair[1], float(val))
elif fdesc[0] == '#':
if fdesc[1] != '.' and int(val) <= 0:
ERROR = f"ERROR: {key} must be > 0, found {val}"
return None
if fdesc[1] == ':' and not int(val):
val = '1'
setattr(spec, keypair[1], int(val))
elif fdesc[0] == '?':
setattr(spec, keypair[1], bool(val))
elif hasattr(spec, f"tbl_{keypair[1]}"):
tbl = getattr(spec, f"tbl_{keypair[1]}")
# print(f'tbl_{keypair[1]}: {tbl}: Search for {val}', file=sys.stderr)
if val not in tbl and (val or fdesc[0] != '.'):
ERROR = f'ERROR: Invalid key {val} for tbl_{keypair[1]}'
return None
setattr(spec, keypair[1], val)
spec.create_dep_tbl(idx2, val)
# if idx2 < len(spec.pattrs)-1 and hasattr(spec, f"tbl_{spec.pattrs[idx2+1]}"):
# print(f"Dynamically set tbl_{spec.pattrs[idx2+1]} to tbl_{spec.pattrs[idx2]}_{val}_{spec.pattrs[idx2+1]}", file=sys.stderr)
else:
setattr(FLAVOR_SPEC[idx], keypair[1], val)
# Eliminate empty features
for spec in FLAVOR_SPEC:
if spec.pnames[0][0] == '.' and not getattr(spec, spec.pattrs[0]):
spec.parsed = 0
if "perf" in spec.pattrs:
setattr(spec, "perf", "")
if "gen" in spec.pattrs:
setattr(spec, "gen", "")
elif "gen" in spec.pattrs and not hasattr(spec, "gen"):
setattr(spec, "gen", "")
print(f'{spec.type}:gen=""', file=sys.stderr)
elif "cpugen" in spec.pattrs and not hasattr(spec, "cpugen"):
setattr(spec, "cpugen", 0)
print(f'{spec.type}:cpugen=0', file=sys.stderr)

# Debugging
print(*FLAVOR_SPEC, file=sys.stderr, sep='\n')
try:
FLAVOR_NAME = fnmck.outname(*FLAVOR_SPEC)
except (TypeError, NameError, KeyError) as exc:
ERROR = f"\tERROR:\n\t{exc}"
# return None
return FLAVOR_SPEC


def is_checked(flag):
"Checked attribute string"
if flag:
return "checked"
return ""


def keystr(key):
"Empty string gets converted to NN"
if key == "":
return "NN"
return key


def flagstr(fstr):
"Return string fstr till next delimiter %_-"
for i in range(0, len(fstr)):
if fstr[i] == "_" or fstr[i] == "-" or fstr[i] == "%":
return fstr[:i]
return fstr


def find_letter(idx, outstr):
"Find letter in output template outstr with idx i that indicates a flag"
found = 0
for ltri in range(0, len(outstr)):
ltr = outstr[ltri]
if ltr == '%':
if idx == found:
if outstr[ltri+1] == '?':
return flagstr(outstr[ltri+2:])
else:
found += 1
return ""


def form_attr(attr):
"""This mirrors flavor-name-check.py input(), but instead generates a web form.
Defaults come from attr, the form is constructed from the attr's class
attributes (like the mentioned input function). tblopt indicates whether
chosing a value in a table is optional."""
spec = type(attr)
# pct = min(20, int(100/len(spec.pnames)))
pct = 20
# print(attr, spec)
if ERROR:
print(f'\tERROR: {html.escape(ERROR, quote=True)}<br/>')
print(f'\t <fieldset><legend>{spec.type}</legend><br/>')
print('\t <div id="the-whole-thing" style="position: relative; overflow: hidden;">')
for i, fname in enumerate(spec.pattrs):
tbl = None
fdesc = spec.pnames[i]
if fdesc[0] != "?" or i == 0 or spec.pnames[i-1][0] != "?":
print(f'\t <div id="column" style="position: relative; width: {pct}%; float: left;">')
# print(fname, fdesc)
value = ""
try:
value = getattr(attr, fname)
except AttributeError:
pass
# Table => READIO
if hasattr(attr, f"tbl_{fname}"):
tbl = getattr(attr, f"tbl_{fname}")
if tbl:
tblopt = False
if fdesc[0] == '.':
tblopt = True
fdesc = fdesc[1:]
# print(f'\t <label for="{fname}">{fname[0].upper()+fname[1:]}:</label><br/>')
print(f'\t <label for="{fname}">{fdesc}:</label><br/>')
value_set = False
for key in tbl.keys():
ischk = value == key or (not key and not value)
value_set = value_set or ischk
print(f'\t <input type="radio" id="{fname}:{key}" name="{spec.type}:{fname}" value="{keystr(key)}" {is_checked(ischk)}/>')
print(f'\t <label for="{fname}:{key}">{tbl[key]} (<tt>{key}</tt>)</label><br/>')
if tblopt:
print(f'\t <input type="radio" id="{fname}:NN" name="{spec.type}:{fname}" value="NN" {is_checked(not value_set)}/>')
print(f'\t <label for="{fname}:NN">NN ()</label><br/>')
attr.create_dep_tbl(i, value)
# if i < len(attr.pattrs)-1 and hasattr(attr, f"tbl_{spec.pattrs[i+1]}"):
# print(f" Dynamically set tbl_{attr.pattrs[i+1]} to tbl_{attr.pattrs[i]}_{value}_{attr.pattrs[i+1]}", file=sys.stderr)
elif fdesc[0:2] == "##":
# Float number => NUMBER
print(f'\t <label for="{fname}">{fdesc[2:]}:</label><br/>')
print(f'\t <input type="number" name="{spec.type}:{fname}" id="{fname}" min=0 value="{value}" size=5/>')
elif fdesc[0] == "#":
# Float number => NUMBER
# Handle : and .
if fdesc[1] == ":":
if not value:
value = 1
fdesc = fdesc[1:]
elif fdesc[1] == '.':
fdesc = fdesc[1:]
print(f'\t <label for="{fname}">{fdesc[1:]}:</label><br/>')
print(f'\t <input type="number" name="{spec.type}:{fname}" id="{fname}" min=0 step=1 value="{value}" size=4/>')
elif fdesc[0] == "?":
# Bool => Checkbox
letter = find_letter(i, spec.outstr)
print(f'\t <input type="checkbox" name="{spec.type}:{fname}" id="{fname}" {is_checked(value)}/>')
print(f'\t <label for="{fname}">{fdesc[1:]} (<tt>{letter}</tt>)</label>')
else:
if fdesc[0] == '.':
fdesc = fdesc[1:]
print(f'\t <label for="{fname}">{fdesc}:</label><br/>')
print(f'\t <input type="text" name="{spec.type}:{fname}" id="{fname}" value="{value}" size=4/>')
if fdesc[0] != "?" or i == len(spec.pnames)-1 or spec.pnames[i+1][0] != "?":
print('\t </div>')
else:
print('\t <br/>')

print('\t </div>')
print('\t </fieldset>')


def output_generate():
"input details to generate SCS flavor name"
print("\tNot implemented yet as webform, use")
print('\t<tt><a href="https://github.com/SovereignCloudStack/standards/blob/main/Tests/iaas/flavor-naming/flavor-name-check.py">flavor-name-check.py</a> -i</tt>')
global FLAVOR_SPEC
if not FLAVOR_SPEC:
print(f'\tERROR: {html.escape(ERROR, quote=True)}')
print('\t<br/>Starting with empty template ...')
# return
FLAVOR_SPEC = (fnmck.Main("0L-0"), fnmck.Disk(""), fnmck.Hype(""), fnmck.HWVirt(""),
fnmck.CPUBrand(""), fnmck.GPU(""), fnmck.IB(""))
cpu, disk, hype, hvirt, cpubrand, gpu, ibd = FLAVOR_SPEC
print('\t<br/>\n\t<FORM ACTION="/cgi-bin/flavor-form.py" METHOD="GET">')
form_attr(cpu)
print('\t<INPUT TYPE="submit" VALUE="Generate"/><br/>')
print('\t<br/>The following settings are all optional and (except for disk) meant for highly specialized / differentiated offerings.<br/>')
print('\t<font size=-1>')
form_attr(disk)
form_attr(hype)
form_attr(hvirt)
form_attr(cpubrand)
form_attr(gpu)
form_attr(ibd)
print('\t</font><br/>')
print('\tRemember that you are allowed to understate performance.<br/>')
print('\t<INPUT TYPE="submit" VALUE="Generate"/><br/>')
print('\t</FORM>')
if FLAVOR_NAME:
print(f"\t<br/><font size=+1 color=blue><b>SCS flavor name: <tt>{html.escape(FLAVOR_NAME, quote=True)}</tt></b>")
altname = fnmck.outname(cpu, disk, None, None, None, gpu, ibd)
print(f"\t<br/><b>Short SCS flavor name: <tt>{html.escape(altname, quote=True)}</tt></b></font>")
else:
print(f'\t<font color=red>ERROR: {html.escape(ERROR, quote=True)}</font>')


def main(argv):
Expand All @@ -69,13 +304,16 @@ def main(argv):
form = {"flavor": [""]}
if 'QUERY_STRING' in os.environ:
form = urllib.parse.parse_qs(os.environ['QUERY_STRING'])
print(f'QUERY_STRING: {os.environ["QUERY_STRING"]}', file=sys.stderr)
# For testing
if len(argv) > 0:
form = {"flavor": [argv[0],]}
form = {"flavor": [argv[0], ]}
find_parse = re.compile(r'^[ \t]*<!\-\-FLAVOR\-FORM: PARSE\-\->[ \t]*$')
find_generate = re.compile(r'^[ \t]*<!\-\-FLAVOR\-FORM: GENERATE\-\->[ \t]*$')
if "flavor" in form:
parse_name(form["flavor"][0])
if "CPU-RAM:cpus" in form:
generate_name(form)
with open("page/index.html", "r", encoding='utf-8') as infile:
for line in infile:
print(line, end='')
Expand Down
29 changes: 19 additions & 10 deletions Tests/iaas/flavor-naming/flavor-name-check.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# vim: set ts=4 sw=4 et:
#
"""Flavor naming checker
https://github.com/SovereignCloudStack/Operational-Docs/
https://github.com/SovereignCloudStack/standards/Test/iaas/flavor-naming/
Return codes:
0: Matching
Expand Down Expand Up @@ -216,7 +216,9 @@ def out(self):
ostr += self.outstr[i]
i += 1
continue
att = self.__getattribute__(self.pattrs[par])
att = None
if hasattr(self, self.pattrs[par]):
att = self.__getattribute__(self.pattrs[par])
if self.outstr[i+1] == ".":
ostr += self.outstr[i:i+2]
if int(att) == att:
Expand Down Expand Up @@ -283,11 +285,13 @@ def create_dep_tbl(self, idx, val):
dtbl = f"tbl_{fname}_{val}_{self.pattrs[idx+1]}"
else:
return False
if hasattr(self, ntbl):
return True
# if hasattr(self, ntbl):
# return True
if hasattr(self, dtbl):
self.__setattr__(ntbl, self.__getattribute__(dtbl))
return True
elif hasattr(self, ntbl) and not hasattr(type(self), ntbl):
delattr(self, ntbl)
return False

def std_validator(self):
Expand Down Expand Up @@ -420,6 +424,11 @@ def __init__(self, string, forceold=False):
except AttributeError:
self.nrdisks = 1

def input(self):
super().input()
if not self.nrdisks or not self.disksize:
self.parsed = 0


class Hype(Prop):
"Class repesenting Hypervisor"
Expand Down Expand Up @@ -492,17 +501,17 @@ def outname(cpuram, disk, hype, hvirt, cpubrand, gpu, ibd):
"Return name constructed from tuple"
# TODO SCSx: Differentiate b/w SCS- and SCSx-
out = "SCS-" + cpuram.out()
if disk.parsed:
if disk and disk.parsed:
out += "-" + disk.out()
if hype.parsed:
if hype and hype.parsed:
out += "_" + hype.out()
if hvirt.parsed:
if hvirt and hvirt.parsed:
out += "_" + hvirt.out()
if cpubrand.parsed:
if cpubrand and cpubrand.parsed:
out += "_" + cpubrand.out()
if gpu.parsed:
if gpu and gpu.parsed:
out += "_" + gpu.out()
if ibd.parsed:
if ibd and ibd.parsed:
out += "_" + ibd.out()
return out

Expand Down
7 changes: 5 additions & 2 deletions Tests/iaas/flavor-naming/flavor-name-describe.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,11 @@ def prettyname(item_list, prefix=""):
stg += "and " + tbl_out(gpu, "gputype")
stg += tbl_out(gpu, "brand")
stg += tbl_out(gpu, "perf", True)
stg += gpu.__getattribute__(f"tbl_brand_{gpu.brand}_gen")[gpu.gen] + " "
if gpu.cu:
try:
stg += gpu.__getattribute__(f"tbl_brand_{gpu.brand}_gen")[gpu.gen] + " "
except KeyError:
pass
if hasattr(gpu, "cu") and gpu.cu:
stg += f"(w/ {gpu.cu} CU/EU/SM) "
# IB
if ibd.parsed:
Expand Down
3 changes: 2 additions & 1 deletion Tests/iaas/flavor-naming/page/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<link rel="icon" type="image/png" sizes="16x16" href="/assets/scs-favicon-16.png" />
</head>
<body>
<h1>SCS flavor name parser and generator</h1>
<h1>SCS flavor name (v3) parser and generator</h1>
<h2>SCS flavor name parser</h2>
<!--FLAVOR-FORM: PARSE-->
<h2>SCS flavor name generator</h2>
Expand All @@ -26,6 +26,7 @@ <h2>References</h2>
<li><a href="https://docs.scs.community/standards/iaas/scs-0100">SCS flavor naming standard</a></li>
<li><a href="https://github.com/SovereignCloudStack/standards/blob/main/Drafts/flavor-naming-strategy.md">SCS flavor naming rationale</a></li>
<li><a href="https://github.com/SovereignCloudStack/standards/tree/main/Tests/iaas/flavor-naming">SCS flavor naming tools</a></li>
<li><a href="https://scs.community/2023/08/21/diskless-flavors/">SCS diskless flavors blog article</a></li>
</ul>
</body>
</html>

0 comments on commit a3b3568

Please sign in to comment.