1616SPDX-License-Identifier: CC-BY-SA 4.0
1717"""
1818
19+ import logging
1920import os
2021import sys
22+ import typing
2123import getopt
22- import yaml
2324import openstack
2425
2526import flavor_names
2627
2728
29+ logger = logging .getLogger (__name__ )
30+
31+
2832def usage (rcode = 1 ):
2933 "help output"
3034 print ("Usage: flavor-names-openstack.py [options]" , file = sys .stderr )
@@ -41,11 +45,160 @@ def usage(rcode=1):
4145 sys .exit (rcode )
4246
4347
48+ TESTCASES = ('scs-0100-syntax-check' , 'scs-0100-semantics-check' , 'flavor-name-check' )
49+ STRATEGY = flavor_names .ParsingStrategy (
50+ vstr = 'v3' ,
51+ parsers = (flavor_names .parser_v3 , ),
52+ tolerated_parsers = (flavor_names .parser_v2 , flavor_names .parser_v1 ),
53+ )
54+ ACC_DISK = (0 , 5 , 10 , 20 , 50 , 100 , 200 , 500 , 1000 , 2000 , 5000 , 10000 , 20000 , 50000 , 100000 )
55+
56+
57+ def compute_scs_flavors (flavors : typing .List [openstack .compute .v2 .flavor .Flavor ], parser = STRATEGY ) -> list :
58+ result = []
59+ for flv in flavors :
60+ if not flv .name or flv .name [:4 ] != 'SCS-' :
61+ continue # not an SCS flavor; none of our business
62+ try :
63+ flavorname = parser (flv .name )
64+ except ValueError as exc :
65+ logger .info (f"error parsing { flv .name } : { exc } " )
66+ flavorname = None
67+ result .append ((flv , flavorname ))
68+ return result
69+
70+
71+ def compute_scs_0100_syntax_check (scs_flavors : list ) -> bool :
72+ problems = [flv .name for flv , flavorname in scs_flavors if not flavorname ]
73+ if problems :
74+ logger .error (f"scs-100-syntax-check: flavor(s) failed: { ', ' .join (sorted (problems ))} " )
75+ return not problems
76+
77+
78+ def compute_scs_0100_semantics_check (scs_flavors : list ) -> bool :
79+ problems = set ()
80+ for flv , flavorname in scs_flavors :
81+ if not flavorname :
82+ continue # this case is handled by syntax check
83+ cpuram = flavorname .cpuram
84+ if flv .vcpus < cpuram .cpus :
85+ logger .info (f"Flavor { flv .name } CPU overpromise: { flv .vcpus } < { cpuram .cpus } " )
86+ problems .add (flv .name )
87+ elif flv .vcpus > cpuram .cpus :
88+ logger .info (f"Flavor { flv .name } CPU underpromise: { flv .vcpus } > { cpuram .cpus } " )
89+ # RAM
90+ flvram = int ((flv .ram + 51 ) / 102.4 ) / 10
91+ # Warn for strange sizes (want integer numbers, half allowed for < 10GiB)
92+ if flvram >= 10 and flvram != int (flvram ) or flvram * 2 != int (flvram * 2 ):
93+ logger .info (f"Flavor { flv .name } uses discouraged uneven size of memory { flvram :.1f} GiB" )
94+ if flvram < cpuram .ram :
95+ logger .info (f"Flavor { flv .name } RAM overpromise { flvram :.1f} < { cpuram .ram :.1f} " )
96+ problems .add (flv .name )
97+ elif flvram > cpuram .ram :
98+ logger .info (f"Flavor { flv .name } RAM underpromise { flvram :.1f} > { cpuram .ram :.1f} " )
99+ # Disk could have been omitted
100+ disksize = flavorname .disk .disksize if flavorname .disk else 0
101+ # We have a recommendation for disk size steps
102+ if disksize not in ACC_DISK :
103+ logger .info (f"Flavor { flv .name } non-standard disk size { disksize } , should have (5, 10, 20, 50, 100, 200, ...)" )
104+ if flv .disk < disksize :
105+ logger .info (f"Flavor { flv .name } disk overpromise { flv .disk } < { disksize } " )
106+ problems .add (flv .name )
107+ elif flv .disk > disksize :
108+ logger .info (f"Flavor { flv .name } disk underpromise { flv .disk } > { disksize } " )
109+ if problems :
110+ logger .error (f"scs-100-semantics-check: flavor(s) failed: { ', ' .join (sorted (problems ))} " )
111+ return not problems
112+
113+
114+ def compute_flavor_name_check (syntax_check_result , semantics_check_result ):
115+ return syntax_check_result and semantics_check_result
116+
117+
118+ # TODO see comment in main function about moving to another module
119+
120+ class Container :
121+ """
122+ This class does lazy evaluation and memoization. You register any potential value either
123+ by giving the value directly, using `add_value`, or
124+ by specifying how it is computed using `add_function`,
125+ which expects a function that takes a container (so other values may be referred to).
126+ In each case, you have to give the value a name.
127+
128+ The value will be available as a normal member variable under this name.
129+ If given via a function, this function will only be evaluated when the value is accessed,
130+ and the value will be memoized, so the function won't be called twice.
131+ If the function raises an exception, then this will be memoized just as well.
132+
133+ For instance,
134+
135+ >>>> container = Container()
136+ >>>> container.add_function('pi', lambda _: 22/7)
137+ >>>> container.add_function('pi_squared', lambda c: c.pi * c.pi)
138+ >>>> assert c.pi_squared == 22/7 * 22/7
139+ """
140+ def __init__ (self ):
141+ self ._values = {}
142+ self ._functions = {}
143+
144+ def __getattr__ (self , key ):
145+ val = self ._values .get (key )
146+ if val is None :
147+ try :
148+ ret = self ._functions [key ](self )
149+ except BaseException as e :
150+ val = (True , e )
151+ else :
152+ val = (False , ret )
153+ self ._values [key ] = val
154+ error , ret = val
155+ if error :
156+ raise ret
157+ return ret
158+
159+ def add_function (self , name , fn ):
160+ if name in self ._functions :
161+ raise RuntimeError (f"fn { name } already registered" )
162+ self ._functions [name ] = fn
163+
164+ def add_value (self , name , value ):
165+ if name in self ._values :
166+ raise RuntimeError (f"value { name } already registered" )
167+ self ._values [name ] = value
168+
169+
170+ # TODO see comment in main function about moving to another module
171+
172+ def harness (name , * check_fns ):
173+ """Harness for evaluating testcase `name`.
174+
175+ Logs beginning of computation.
176+ Calls each fn in `check_fns`.
177+ Prints (to stdout) 'name: RESULT', where RESULT is one of
178+
179+ - 'ABORT' if an exception occurs during the function calls
180+ - 'FAIL' if one of the functions has a falsy result
181+ - 'PASS' otherwise
182+ """
183+ logger .debug (f'** { name } ' )
184+ try :
185+ result = all (check_fn () for check_fn in check_fns )
186+ except BaseException :
187+ logger .debug ('exception during check' , exc_info = True )
188+ result = 'ABORT'
189+ else :
190+ result = ['FAIL' , 'PASS' ][min (1 , result )]
191+ # this is quite redundant
192+ # logger.debug(f'** computation end for {name}')
193+ print (f"{ name } : { result } " )
194+
195+
44196def main (argv ):
45197 """Entry point -- main loop going over flavors"""
46- fnmck = flavor_names .CompatLayer ()
198+ # configure logging, disable verbose library logging
199+ logging .basicConfig (format = '%(levelname)s: %(message)s' , level = logging .DEBUG )
200+ openstack .enable_logging (debug = False )
47201 cloud = None
48- verbose = False
49202
50203 try :
51204 cloud = os .environ ["OS_CLOUD" ]
@@ -70,15 +223,15 @@ def main(argv):
70223 # fnmck.disallow_old = True
71224 print (f'ignoring obsolete argument: { opt [0 ]} ' , file = sys .stderr )
72225 elif opt [0 ] == "-2" or opt [0 ] == "--v2plus" :
73- fnmck . disallow_old = True
226+ print ( f'ignoring obsolete argument: { opt [ 0 ] } ' , file = sys . stderr )
74227 elif opt [0 ] == "-1" or opt [0 ] == "--v1prefer" :
75228 print (f'ignoring obsolete argument: { opt [0 ]} ' , file = sys .stderr )
76229 elif opt [0 ] == "-o" or opt [0 ] == "--accept-old-mandatory" :
77230 print (f'ignoring obsolete argument: { opt [0 ]} ' , file = sys .stderr )
78231 elif opt [0 ] == "-v" or opt [0 ] == "--verbose" :
79- verbose = True
232+ print ( f'ignoring obsolete argument: { opt [ 0 ] } ' , file = sys . stderr )
80233 elif opt [0 ] == "-q" or opt [0 ] == "--quiet" :
81- fnmck . quiet = True
234+ print ( f'ignoring obsolete argument: { opt [ 0 ] } ' , file = sys . stderr )
82235 else :
83236 usage (2 )
84237 if len (args ) > 0 :
@@ -88,123 +241,21 @@ def main(argv):
88241 if not cloud :
89242 print ("CRITICAL: You need to have OS_CLOUD set or pass --os-cloud=CLOUD." , file = sys .stderr )
90243 sys .exit (1 )
91- conn = openstack .connect (cloud = cloud , timeout = 32 )
92- flavors = conn .compute .flavors ()
93-
94- # Lists of flavors: mandatory, good-SCS, bad-SCS, non-SCS, with-warnings
95- SCSFlv = []
96- wrongFlv = []
97- nonSCSFlv = []
98- warnFlv = []
99- errors = 0
100- for flv in flavors :
101- # Skip non-SCS flavors
102- if flv .name and flv .name [:4 ] != "SCS-" : # and flv.name[:4] != "SCSx"
103- nonSCSFlv .append (flv .name )
104- continue
105- try :
106- ret = fnmck .parsename (flv .name )
107- assert ret
108- # Parser error
109- except ValueError as exc :
110- errors += 1
111- wrongFlv .append (flv .name )
112- print (f"ERROR: Wrong flavor \" { flv .name } \" : { exc } " , file = sys .stderr )
113- continue
114- # We have a successfully parsed SCS- name now
115- # See if the OpenStack provided data fulfills what we
116- # expect from the flavor based on its name
117- err = 0
118- warn = 0
119- # Split list for readability
120- cpuram = ret .cpuram
121- # next qwould be hype, hwvirt, cpubrand, gpu, ib
122- # see flavor-name-check.py: parsename()
123- # vCPUS
124- if flv .vcpus < cpuram .cpus :
125- print (f"ERROR: Flavor { flv .name } has only { flv .vcpus } vCPUs, "
126- f"should have >= { cpuram .cpus } " , file = sys .stderr )
127- err += 1
128- elif flv .vcpus > cpuram .cpus :
129- print (f"WARNING: Flavor { flv .name } has { flv .vcpus } vCPUs, "
130- f"only needs { cpuram .cpus } " , file = sys .stderr )
131- warn += 1
132- # RAM
133- flvram = int ((flv .ram + 51 ) / 102.4 ) / 10
134- # Warn for strange sizes (want integer numbers, half allowed for < 10GiB)
135- if flvram >= 10 and flvram != int (flvram ) or flvram * 2 != int (flvram * 2 ):
136- print (f"WARNING: Flavor { flv .name } uses discouraged uneven size "
137- f"of memory { flvram :.1f} GiB" , file = sys .stderr )
138- if flvram < cpuram .ram :
139- print (f"ERROR: Flavor { flv .name } has only { flvram :.1f} GiB RAM, "
140- f"should have >= { cpuram .ram :.1f} GiB" , file = sys .stderr )
141- err += 1
142- elif flvram > cpuram .ram :
143- print (f"WARNING: Flavor { flv .name } has { flvram :.1f} GiB RAM, "
144- f"only needs { cpuram .ram :.1f} GiB" , file = sys .stderr )
145- warn += 1
146- # DISK
147- accdisk = (0 , 5 , 10 , 20 , 50 , 100 , 200 , 500 , 1000 , 2000 , 5000 , 10000 , 20000 , 50000 , 100000 )
148- # Disk could have been omitted
149- disksize = ret .disk .disksize if ret .disk else 0
150- # We have a recommendation for disk size steps
151- if disksize not in accdisk :
152- print (f"WARNING: Flavor { flv .name } advertizes disk size { disksize } , "
153- f"should have (5, 10, 20, 50, 100, 200, ...)" , file = sys .stderr )
154- warn += 1
155- if flv .disk < disksize :
156- print (f"ERROR: Flavor { flv .name } has only { flv .disk } GB root disk, "
157- f"should have >= { disksize } GB" , file = sys .stderr )
158- err += 1
159- elif flv .disk > disksize :
160- print (f"WARNING: Flavor { flv .name } has { flv .disk } GB root disk, "
161- f"only needs { disksize } GB" , file = sys .stderr )
162- warn += 1
163- # Ev'thing checked, react to errors by putting the bad flavors in the bad bucket
164- if err :
165- wrongFlv .append (flv .name )
166- errors += 1
167- else :
168- SCSFlv .append (flv .name )
169- if warn :
170- warnFlv .append (flv .name )
171- # This makes the output more readable
172- SCSFlv .sort ()
173- nonSCSFlv .sort ()
174- wrongFlv .sort ()
175- warnFlv .sort ()
176- # Produce dicts for YAML reporting
177- flvSCSList = {
178- "SCSFlavorsValid" : SCSFlv ,
179- "SCSFlavorsWrong" : wrongFlv ,
180- "FlavorsWithWarnings" : warnFlv ,
181- }
182- flvOthList = {
183- "OtherFlavors" : nonSCSFlv
184- }
185- flvSCSRep = {
186- "TotalAmount" : len (SCSFlv ) + len (wrongFlv ),
187- "FlavorsValid" : len (SCSFlv ),
188- "FlavorsWrong" : len (wrongFlv ),
189- "FlavorsWithWarnings" : len (warnFlv ),
190- }
191- flvOthRep = {
192- "TotalAmount" : len (nonSCSFlv ),
193- }
194- totSummary = {
195- "Errors" : errors ,
196- "Warnings" : len (warnFlv ),
197- }
198- Report = {cloud : {"TotalSummary" : totSummary }}
199- if not fnmck .quiet :
200- Report [cloud ]["SCSFlavorSummary" ] = flvSCSRep
201- Report [cloud ]["OtherFlavorSummary" ] = flvOthRep
202- if verbose :
203- Report [cloud ]["SCSFlavorReport" ] = flvSCSList
204- Report [cloud ]["OtherFlavorReport" ] = flvOthList
205- print (f"{ yaml .dump (Report , default_flow_style = False )} " )
206- print ("flavor-name-check: " + ('PASS' , 'FAIL' )[min (1 , errors )])
207- return errors
244+
245+ # TODO in the future, the remainder should be moved to a central module `scs_compatible_iaas.py`,
246+ # which would import the test logic (i.e., the functions called compute_XYZ) from here.
247+ # Then this module wouldn't need to know about containers, and the central module can handle
248+ # information sharing as well as running precisely the requested set of testcases.
249+ c = Container ()
250+ c .add_function ('conn' , lambda _ : openstack .connect (cloud = cloud , timeout = 32 ))
251+ c .add_function ('flavors' , lambda c : list (c .conn .compute .flavors ()))
252+ c .add_function ('scs_flavors' , lambda c : compute_scs_flavors (c .flavors ))
253+ c .add_function ('scs_0100_syntax_check' , lambda c : compute_scs_0100_syntax_check (c .scs_flavors ))
254+ c .add_function ('scs_0100_semantics_check' , lambda c : compute_scs_0100_semantics_check (c .scs_flavors ))
255+ c .add_function ('flavor_name_check' , lambda c : compute_flavor_name_check (c .scs_0100_syntax_check , c .scs_0100_semantics_check ))
256+ for testcase in TESTCASES :
257+ harness (testcase , lambda : getattr (c , testcase .replace ('-' , '_' )))
258+ return 0
208259
209260
210261if __name__ == "__main__" :
0 commit comments