Skip to content

Commit

Permalink
First real revision
Browse files Browse the repository at this point in the history
Signed-off-by: Matthias Büchse <matthias.buechse@cloudandheat.com>
  • Loading branch information
mbuechse committed Jan 26, 2024
1 parent 88c531c commit b1cf8ec
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 125 deletions.
23 changes: 7 additions & 16 deletions Tests/iaas/flavor-naming/check-yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,11 @@

import yaml

from flavor_name_check import parser_v2, SyntaxV1
from flavor_name_check import parser_v2, flavorname_to_dict


REQUIRED_FIELDS = ['name-v1', 'name-v2', 'name', 'cpus', 'ram', 'cpu-type']
DEFAULTS = {'disk0-type': 'network'}
CPUTYPE_KEY = {'L': 'crowded-core', 'V': 'shared-core', 'T': 'dedicated-thread', 'C': 'dedicated-core'}
DISKTYPE_KEY = {'n': 'network', 'h': 'hdd', 's': 'ssd', 'p': 'nvme'}


class Undefined:
Expand Down Expand Up @@ -50,23 +48,15 @@ def check_spec(self, flavor_spec):
return
name = flavor_spec['name']
name_v2 = flavor_spec['name-v2']
flavorname = parser_v2(name_v2)
try:
flavorname = parser_v2(name_v2)
except Exception:
flavorname = None
if not flavorname:
self.emit(f"flavor {name}: name-v2 '{name_v2}' could not be parsed")
return
undefined = Undefined()
expected = {
'cpus': flavorname.cpuram.cpus,
'cpu-type': CPUTYPE_KEY[flavorname.cpuram.cputype],
'ram': flavorname.cpuram.ram,
'name-v1': SyntaxV1.from_v2(name_v2),
'disk': undefined,
}
if flavorname.disk:
if flavorname.disk.nrdisks != 1:
self.emit(f"flavor '{name}': name-v2 using multiple disks")
expected['disk'] = flavorname.disk.disksize
expected['disk0-type'] = DISKTYPE_KEY[flavorname.disk.disktype or 'n']
expected = flavorname_to_dict(flavorname)
for key, exp_val in expected.items():
val = flavor_spec.get(key, DEFAULTS.get(key, undefined))
if val != exp_val:
Expand Down Expand Up @@ -108,4 +98,5 @@ def main(argv):
sys.exit(main(sys.argv))
except Exception as e:
print(f"CRITICAL: {e!s}", file=sys.stderr)
raise
sys.exit(1)
113 changes: 91 additions & 22 deletions Tests/iaas/flavor-naming/cli.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,81 @@
#!/usr/bin/env python3
import click
import logging
import sys

from flavor_name_check import parser_v1, parser_v2
import click
import yaml

from flavor_name_check import parser_v1, parser_v2, parser_v3, inputflavor, outputter, flavorname_to_dict
from flavor_name_describe import prettyname


logger = logging.getLogger(__name__)


class Version:
def __init__(self, parser):
self.parser = parser
class ParsingStrategy:
"""class to model parsing that accepts multiple versions of the syntax in different ways"""

def __init__(self, parsers=(), tolerated_parsers=(), invalid_parsers=()):
self.parsers = parsers
self.tolerated_parsers = tolerated_parsers
self.invalid_parsers = invalid_parsers

def parse(self, namestr):
exc = None
for parser in self.parsers:
try:
return parser(namestr)
except Exception as e:
if exc is None:
exc = e
# at this point, if `self.parsers` is not empty, then `exc` is not `None`
for parser in self.tolerated_parsers:
try:
result = parser(namestr)
except Exception:
pass
else:
logger.warning(f"Name is merely tolerated {parser.vstr}: {namestr}")
return result
for parser in self.invalid_parsers:
try:
result = parser(namestr)
except Exception:
pass
else:
raise ValueError(f"Name is non-tolerable {parser.vstr}")
raise exc


VERSIONS = {
'v1': Version(parser_v1),
'v2': Version(parser_v2),
'v3': Version(parser_v2),
'v1': ParsingStrategy(parsers=(parser_v1, ), invalid_parsers=(parser_v2, )),
'v1/v2': ParsingStrategy(parsers=(parser_v1, ), tolerated_parsers=(parser_v2, )),
'v2/v1': ParsingStrategy(parsers=(parser_v2, ), tolerated_parsers=(parser_v1, )),
'v2': ParsingStrategy(parsers=(parser_v2, ), invalid_parsers=(parser_v1, )),
'v3': ParsingStrategy(parsers=(parser_v3, ), invalid_parsers=(parser_v1, )),
}
VERSIONS['latest'] = VERSIONS['v3']
VERSIONS['latest'] = max(VERSIONS.items())[1]


def noop(*args, **kwargs):
pass


class Config:
def __init__(self):
self.verbose = False

@property
def printv(self):
return print if self.verbose else noop


@click.group()
@click.argument('version', type=click.Choice(list(VERSIONS), case_sensitive=False))
@click.pass_context
def cli(ctx, version):
vobj = VERSIONS.get(version)
ctx.obj['version'] = vobj
@click.option('-d', '--debug', 'debug', is_flag=True)
@click.option('-v', '--verbose', 'verbose', is_flag=True)
@click.pass_obj
def cli(cfg, debug=False, verbose=False):
cfg.verbose = verbose


@cli.result_callback()
Expand All @@ -32,24 +84,41 @@ def process_pipeline(rc, *args, **kwargs):


@cli.command()
@click.argument('version', type=click.Choice(list(VERSIONS), case_sensitive=False))
@click.argument('namestr', nargs=-1)
@click.pass_context
def check(ctx, namestr):
version = ctx.obj['version']
@click.option('-o', '--output', 'output', type=click.Choice(['name', 'description', 'yaml']))
@click.pass_obj
def parse(cfg, version, namestr, output='name'):
version = VERSIONS.get(version)
printv = cfg.printv
errors = 0
for name in namestr:
try:
flavorname = version.parser(name)
flavorname = version.parse(name)
except ValueError as exc:
print(f"{name}: ERROR {exc}")
print(f"{exc}: {name}")
errors += 1
else:
if flavorname is None:
print(f" {name}: NOT an SCS flavor")
print(f"NOT an SCS flavor: {name}")
elif output == 'description':
printv(name, end=': ')
print(f"{prettyname(flavorname)}")
elif output == 'yaml':
print(yaml.dump(flavorname_to_dict(flavorname), explicit_start=True))
else:
print(f" {name}: OK")
printv(f"OK: {name}")
return errors


@cli.command()
@click.option('-p', '--pretty', 'pretty', is_flag=True)
def input(pretty=False):
flavorname = inputflavor()
outfn = prettyname if pretty else outputter
print(outfn(flavorname))


if __name__ == '__main__':
cli(obj={})
logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.INFO)
cli(obj=Config())
89 changes: 7 additions & 82 deletions Tests/iaas/flavor-naming/flavor-name-check.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import sys

from flavor_name_check import CompatLayer, Attr, Main, Disk, Hype, HWVirt, CPUBrand, GPU, IB, Flavorname
from flavor_name_check import CompatLayer, inputflavor
from flavor_name_describe import prettyname


Expand All @@ -27,86 +27,6 @@ def usage():
sys.exit(2)


class Inputter:
"""Auxiliary class for interactive input of flavor names."""

@staticmethod
def to_bool(s):
"""interpret string input as bool"""
s = s.upper()
if s == "" or s == "0" or s[0] == "N" or s[0] == "F":
return False
if s == "1" or s[0] == "Y" or s[0] == "T":
return True
raise ValueError

def input_component(self, targetcls):
target = targetcls()
print(targetcls.type)
attrs = [att for att in targetcls.__dict__.values() if isinstance(att, Attr)]
for i, attr in enumerate(attrs):
fdesc = attr.name
tbl = attr.get_tbl(target)
if tbl:
print(f" {fdesc} Options:")
for key, v in tbl.items():
print(f" {'' if key is None else key}: {v}")
while True:
print(f" {fdesc}: ", end="")
val = input()
try:
if not val and i == 0 and not issubclass(targetcls, (Main, Disk)):
# BAIL: if you don't want an extension, supply empty first attr
return
if fdesc[0] == "?":
val = self.to_bool(val)
elif fdesc[0:2] == "##":
val = float(val)
elif fdesc[0] == "#":
if fdesc[1] == "." and not val:
val = attr.default
break
oval = val
val = int(val)
if str(val) != oval:
raise ValueError(val)
elif tbl:
if val in tbl:
break
if val.upper() in tbl:
val = val.upper()
elif val.lower() in tbl:
val = val.lower()
if val not in tbl:
raise ValueError(val)
except BaseException as exc:
print(exc)
print(" INVALID!")
else:
break
attr.__set__(target, val)
return target

def __call__(self):
flavorname = Flavorname()
flavorname.cpuram = self.input_component(Main)
flavorname.disk = self.input_component(Disk)
if flavorname.disk and not (flavorname.disk.nrdisks and flavorname.disk.disksize):
# special case...
flavorname.disk = None
flavorname.hype = self.input_component(Hype)
flavorname.hvirt = self.input_component(HWVirt)
flavorname.cpubrand = self.input_component(CPUBrand)
flavorname.gpu = self.input_component(GPU)
flavorname.ib = self.input_component(IB)
return flavorname


def inputflavor(inputter=Inputter()):
"""Interactively input a flavor"""
return inputter()


def main(argv):
"""Entry point when used as selfstanding tool"""
_fnmck = CompatLayer()
Expand Down Expand Up @@ -175,7 +95,12 @@ def main(argv):
for name in flavorlist:
if not name:
continue
ret = _fnmck.parsename(name)
try:
ret = _fnmck.parsename(name)
except Exception as e:
error += 1
print(f"ERROR parsing {name}: {e}")
continue
if not ret:
nonscs += 1
continue
Expand Down
Loading

0 comments on commit b1cf8ec

Please sign in to comment.