1515from collections import Counter
1616import getopt
1717import logging
18- from operator import attrgetter
1918import os
2019import re
2120import sys
@@ -83,25 +82,33 @@ def print_usage(file=sys.stderr):
8382
8483
8584def check_image_attributes (images , attributes = IMAGE_ATTRIBUTES ):
86- for image in images :
87- wrong = [f"{ key } ={ value } " for key , value in attributes .items () if image .get (key ) != value ]
88- if wrong :
89- logger .warning (f"Image '{ image .name } ' missing recommended attributes: { ', ' .join (wrong )} " )
85+ candidates = [
86+ (image .name , [f"{ key } ={ value } " for key , value in attributes .items () if image .get (key ) != value ])
87+ for image in images
88+ ]
89+ # drop those candidates that are fine
90+ offenders = [candidate for candidate in candidates if candidate [1 ]]
91+ for name , wrong in offenders :
92+ logger .warning (f"Image '{ name } ' missing recommended attributes: { ', ' .join (wrong )} " )
93+ return not offenders
9094
9195
9296def check_flavor_attributes (flavors , attributes = FLAVOR_ATTRIBUTES , optional = FLAVOR_OPTIONAL ):
97+ offenses = 0
9398 for flavor in flavors :
9499 extra_specs = flavor ['extra_specs' ]
95100 wrong = [f"{ key } ={ value } " for key , value in attributes .items () if extra_specs .get (key ) != value ]
96101 miss_opt = [key for key in optional if extra_specs .get (key ) is None ]
97102 if wrong :
103+ offenses += 1
98104 message = f"Flavor '{ flavor .name } ' missing recommended attributes: { ', ' .join (wrong )} "
99105 # only report missing optional attributes if recommended are missing as well
100106 # reasoning here is that these optional attributes are merely a hint for implementers
101107 # and if the recommended attributes are present, we assume that implementers have done their job already
102108 if miss_opt :
103109 message += f"; additionally, missing optional attributes: { ', ' .join (miss_opt )} "
104110 logger .warning (message )
111+ return not offenses
105112
106113
107114def install_test_requirements (fconn ):
@@ -132,47 +139,60 @@ def install_test_requirements(fconn):
132139 logger .debug ("No package manager worked; proceeding anyway as rng-utils might be present nonetheless." )
133140
134141
135- def check_vm_requirements (fconn , image_name ):
136- try :
137- entropy_avail = fconn .run ('cat /proc/sys/kernel/random/entropy_avail' , hide = True ).stdout .strip ()
138- if entropy_avail != "256" :
139- logger .error (
140- f"VM '{ image_name } ' didn't have a fixed amount of entropy available. "
141- f"Expected 256, got { entropy_avail } ."
142- )
142+ def check_entropy_avail (fconn , image_name ):
143+ entropy_avail = fconn .run ('cat /proc/sys/kernel/random/entropy_avail' , hide = True ).stdout .strip ()
144+ if entropy_avail != "256" :
145+ logger .error (
146+ f"VM '{ image_name } ' didn't have a fixed amount of entropy available. "
147+ f"Expected 256, got { entropy_avail } ."
148+ )
149+ return False
150+ return True
151+
152+
153+ def check_rngd (fconn , image_name ):
154+ result = fconn .run ('sudo systemctl status rngd' , hide = True , warn = True )
155+ if "could not be found" in result .stdout or "could not be found" in result .stderr :
156+ logger .warning (f"VM '{ image_name } ' doesn't provide the recommended service rngd" )
157+ return False
158+ return True
159+
143160
161+ def check_fips_test (fconn , image_name ):
162+ try :
144163 install_test_requirements (fconn )
145164 fips_data = fconn .run ('cat /dev/random | rngtest -c 1000' , hide = True , warn = True ).stderr
146165 failure_re = re .search (r'failures:\s\d+' , fips_data , flags = re .MULTILINE )
147166 if failure_re :
148167 fips_failures = failure_re .string [failure_re .regs [0 ][0 ]:failure_re .regs [0 ][1 ]].split (" " )[1 ]
149- if int (fips_failures ) > 3 :
150- logger .error (
151- f"VM '{ image_name } ' didn't pass the FIPS 140-2 testing. "
152- f"Expected a maximum of 3 failures, got { fips_failures } ."
153- )
168+ if int (fips_failures ) <= 3 :
169+ return True # this is the single 'successful' code path
170+ logger .error (
171+ f"VM '{ image_name } ' didn't pass the FIPS 140-2 testing. "
172+ f"Expected a maximum of 3 failures, got { fips_failures } ."
173+ )
154174 else :
155175 logger .error (f"VM '{ image_name } ': failed to determine fips failures" )
156176 logger .debug (f"stderr following:\n { fips_data } " )
157177 except BaseException :
158178 logger .critical (f"Couldn't check VM '{ image_name } ' requirements" , exc_info = True )
179+ return False # any unsuccessful path should end up here
159180
160181
161- def check_vm_recommends (fconn , image , flavor ):
182+ def check_virtio_rng (fconn , image , flavor ):
162183 try :
163- result = fconn .run ('sudo systemctl status rngd' , hide = True , warn = True )
164- if "could not be found" in result .stdout or "could not be found" in result .stderr :
165- logger .warning (f"VM '{ image .name } ' doesn't provide the recommended service rngd" )
166184 # Check the existence of the HRNG -- can actually be skipped if the flavor
167185 # or the image doesn't have the corresponding attributes anyway!
168186 if image .hw_rng_model != "virtio" or flavor .extra_specs .get ("hw_rng:allowed" ) != "True" :
169187 logger .debug ("Not looking for virtio-rng because required attributes are missing" )
170- else :
171- # `cat` can fail with return code 1 if special file does not exist
172- hw_device = fconn .run ('cat /sys/devices/virtual/misc/hw_random/rng_available' , hide = True , warn = True ).stdout
173- result = fconn .run ("sudo su -c 'od -vAn -N2 -tu2 < /dev/hwrng'" , hide = True , warn = True )
174- if not hw_device .strip () or "No such device" in result .stdout or "No such " in result .stderr :
175- logger .warning (f"VM '{ image .name } ' doesn't provide a hardware device." )
188+ return False
189+ # `cat` can fail with return code 1 if special file does not exist
190+ hw_device = fconn .run ('cat /sys/devices/virtual/misc/hw_random/rng_available' , hide = True , warn = True ).stdout
191+ result = fconn .run ("sudo su -c 'od -vAn -N2 -tu2 < /dev/hwrng'" , hide = True , warn = True )
192+ if not hw_device .strip () or "No such device" in result .stdout or "No such " in result .stderr :
193+ logger .warning (f"VM '{ image .name } ' doesn't provide a hardware device." )
194+ return False
195+ return True
176196 except BaseException :
177197 logger .critical (f"Couldn't check VM '{ image .name } ' recommends" , exc_info = True )
178198
@@ -334,7 +354,12 @@ def create_vm(env, all_flavors, image, server_name=SERVER_NAME):
334354 boot_from_volume = True , terminate_volume = True , volume_size = volume_size ,
335355 )
336356 logger .debug (f"Server '{ server_name } ' ('{ server .id } ') has been created" )
337- return server
357+ # next, do an explicit get_server because, beginning with version 3.2.0, the openstacksdk no longer
358+ # sets the interface attributes such as `public_v4`
359+ # I (mbuechse) consider this a bug in openstacksdk; it was introduced with
360+ # https://opendev.org/openstack/openstacksdk/commit/a8adbadf0c4cdf1539019177fb1be08e04d98e82
361+ # I also consider openstacksdk architecture with the Mixins etc. smelly to say the least
362+ return env .conn .get_server (server .id )
338363
339364
340365def delete_vm (conn , server_name = SERVER_NAME ):
@@ -377,19 +402,56 @@ def handle(self, record):
377402 self .bylevel [record .levelno ] += 1
378403
379404
405+ # the following functions are used to map any OpenStack Image to a pair of integers
406+ # used for sorting the images according to fitness for our test
407+ # - debian take precedence over ubuntu
408+ # - higher versions take precedence over lower ones
409+
410+ # only list stable versions here
411+ DEBIAN_CODENAMES = {
412+ "buster" : 10 ,
413+ "bullseye" : 11 ,
414+ "bookworm" : 12 ,
415+ }
416+
417+
418+ def _deduce_sort_debian (os_version , debian_ver = re .compile (r"\d+\Z" )):
419+ if debian_ver .match (os_version ):
420+ return 2 , int (os_version )
421+ return 2 , DEBIAN_CODENAMES .get (os_version , 0 )
422+
423+
424+ def _deduce_sort_ubuntu (os_version , ubuntu_ver = re .compile (r"\d\d\.\d\d\Z" )):
425+ if ubuntu_ver .match (os_version ):
426+ return 1 , int (os_version .replace ("." , "" ))
427+ return 1 , 0
428+
429+
430+ # map lower-case distro name to version deducing function
431+ DISTROS = {
432+ "ubuntu" : _deduce_sort_ubuntu ,
433+ "debian" : _deduce_sort_debian ,
434+ }
435+
436+
437+ def _deduce_sort (img ):
438+ # avoid private images here
439+ # (note that with SCS, public images MUST have os_distro and os_version, but we check nonetheless)
440+ if img .visibility != 'public' or not img .os_distro or not img .os_version :
441+ return 0 , 0
442+ deducer = DISTROS .get (img .os_distro .strip ().lower ())
443+ if deducer is None :
444+ return 0 , 0
445+ return deducer (img .os_version .strip ().lower ())
446+
447+
380448def select_deb_image (images ):
381- """From a list of OpenStack image objects, select a recent Debian derivative.
382-
383- Try Debian first, then Ubuntu.
384- """
385- for prefix in ("Debian " , "Ubuntu " ):
386- imgs = sorted (
387- [img for img in images if img .name .startswith (prefix )],
388- key = attrgetter ("name" ),
389- )
390- if imgs :
391- return imgs [- 1 ]
392- return None
449+ """From a list of OpenStack image objects, select a recent Debian derivative."""
450+ return max (images , key = _deduce_sort , default = None )
451+
452+
453+ def print_result (check_id , passed ):
454+ print (check_id + ": " + ('FAIL' , 'PASS' )[bool (passed )])
393455
394456
395457def main (argv ):
@@ -448,8 +510,8 @@ def main(argv):
448510 logger .debug (f"Selected image: { images [0 ].name } ({ images [0 ].id } )" )
449511
450512 logger .debug ("Checking images and flavors for recommended attributes" )
451- check_image_attributes (all_images )
452- check_flavor_attributes (all_flavors )
513+ print_result ( 'entropy-check-image-properties' , check_image_attributes (all_images ) )
514+ print_result ( 'entropy-check-flavor-properties' , check_flavor_attributes (all_flavors ) )
453515
454516 logger .debug ("Checking dynamic instance properties" )
455517 with TestEnvironment (conn ) as env :
@@ -467,8 +529,12 @@ def main(argv):
467529 ) as fconn :
468530 # need to retry because it takes time for sshd to come up
469531 retry (fconn .open , exc_type = "NoValidConnectionsError,TimeoutError" )
470- check_vm_recommends (fconn , image , server .flavor )
471- check_vm_requirements (fconn , image .name )
532+ # virtio-rng is not an official test case according to testing notes,
533+ # but for some reason we check it nonetheless (call it informative)
534+ check_virtio_rng (fconn , image , server .flavor )
535+ print_result ('entropy-check-entropy-avail' , check_entropy_avail (fconn , image .name ))
536+ print_result ('entropy-check-rngd' , check_rngd (fconn , image .name ))
537+ print_result ('entropy-check-fips-test' , check_fips_test (fconn , image .name ))
472538 finally :
473539 delete_vm (conn )
474540 except BaseException as e :
@@ -480,6 +546,9 @@ def main(argv):
480546 "Total critical / error / warning: "
481547 f"{ c [logging .CRITICAL ]} / { c [logging .ERROR ]} / { c [logging .WARNING ]} "
482548 )
549+ # include this one for backwards compatibility
550+ if not c [logging .CRITICAL ]:
551+ print ("entropy-check: " + ('PASS' , 'FAIL' )[min (1 , c [logging .ERROR ])])
483552 return min (127 , c [logging .CRITICAL ] + c [logging .ERROR ]) # cap at 127 due to OS restrictions
484553
485554
0 commit comments