Skip to content

Commit be1848c

Browse files
Merge branch 'main' into feat/secure-communication
2 parents 36780c6 + 4d46088 commit be1848c

File tree

17 files changed

+1329
-858
lines changed

17 files changed

+1329
-858
lines changed

Standards/scs-0003-v1-sovereign-cloud-standards-yaml.md

Lines changed: 158 additions & 100 deletions
Large diffs are not rendered by default.

Standards/scs-0103-v1-standard-flavors.md

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,19 @@ description: |
1414

1515
## Introduction
1616

17+
This is v1.1 of the standard, which lifts the following restriction regarding the property `scs:name-vN`:
18+
this property may now be used on any flavor, rather than standard flavors only. In addition, the "vN" is
19+
now interpreted as "name variant N" instead of "version N of the naming standard". Note that this change
20+
indeed preserves compliance, i.e., compliance with v1.0 implies compliance with v1.1.
21+
22+
## Terminology
23+
24+
extra_specs
25+
Additional properties on an OpenStack flavor, see
26+
[OpenStack Nova user documentation](https://docs.openstack.org/nova/2024.1/user/flavors.html#extra-specs)
27+
and
28+
[OpenStack Nova configuration documentation](https://docs.openstack.org/nova/2024.1/configuration/extra-specs.html).
29+
1730
## Motivation
1831

1932
In OpenStack environments there is a need to define different flavors for instances.
@@ -23,13 +36,13 @@ OpenStack providers thus typically offer a large selection of flavors.
2336
While flavors can be discovered (`openstack flavor list`), it is helpful for users (DevOps teams),
2437
to have a guaranteed set of flavors available on all SCS clouds, so these need not be discovered.
2538

26-
## Properties (extra specs)
39+
## Properties (extra_specs)
2740

28-
The following extra specs are recognized, together with the respective semantics:
41+
The following extra_specs are recognized, together with the respective semantics:
2942

30-
- `scs:name-vN=NAME` (where `N` is `1` or `2`, and `NAME` is some string) means that the
31-
flavor is one of the
32-
standard SCS flavors, and the requirements of Section "Standard SCS flavors" below apply.
43+
- `scs:name-vN=NAME` (where `N` is a positive integer, and `NAME` is some string) means that
44+
`NAME` is a valid name for this flavor according to any major version of the [SCS standard on
45+
flavor naming](https://docs.scs.community/standards/iaas/scs-0100).
3346
- `scs:cpu-type=shared-core` means that _at least 20% of a core in >99% of the time_,
3447
measured over the course of one month (1% is 7,2 h/month). The `cpu-type=shared-core`
3548
corresponds to the `V` cpu modifier in the [flavor-naming spec](./scs-0100-v3-flavor-naming.md),
@@ -43,6 +56,24 @@ The following extra specs are recognized, together with the respective semantics
4356

4457
Whenever ANY of these are present on ANY flavor, the corresponding semantics must be satisfied.
4558

59+
The extra_spec `scs:name-vN` is to be interpreted as "name variant N". This name scheme is designed to be
60+
backwards compatible with v1.0 of this standard, where `scs:name-vN` is interpreted as
61+
"name according to naming standard vN". We abandon this former interpretation for two reasons:
62+
63+
1. the naming standards admit multiple (even many) names for the same flavor, and we want to provide a means
64+
of advertising more than one of them (said standards recommend using two: a short one and a long one),
65+
2. the same flavor name may be valid according to multiple versions at the same time, which would lead to
66+
a pollution of the extra_specs with redundant properties; for instance, the name
67+
`SCS-4V-16` is valid for both [scs-0100-v2](scs-0100-v2-flavor-naming.md) and
68+
[scs-0100-v3](scs-0100-v3-flavor-naming.md), and, since it does not use any extension, it will be valid
69+
for any future version that only changes the extensions, such as the GPU vendor and architecture.
70+
71+
Note that it is not required to use consecutive numbers to number the name variants.
72+
This way, it becomes easier to remove a single variant (no "closing the gap" required).
73+
74+
If extra_specs of the form `scs:name-vN` are used to specify SCS flavor names, it is RECOMMENDED to include
75+
names for the latest stable major version of the standard on flavor naming.
76+
4677
## Standard SCS flavors
4778

4879
Following are flavors that must exist on standard SCS clouds (x86-64).
@@ -127,14 +158,19 @@ instance life cycle.)
127158

128159
## Conformance Tests
129160

130-
The script `flavors-openstack.py` will read the lists of mandatory and recommended flavors
161+
The script [`flavors-openstack.py`](https://github.com/SovereignCloudStack/standards/blob/main/Tests/iaas/standard-flavors/flavors-openstack.py)
162+
will read the lists of mandatory and recommended flavors
131163
from a yaml file provided as command-line argument, connect to an OpenStack installation,
132-
and check whether the flavors are present and their extra specs are correct. Missing
133-
flavors will be reported on various logging channels: error for mandatory, info for
134-
recommended flavors. Incorrect extra specs will be reported as error in any case.
164+
and check whether the flavors are present and their extra_specs are correct.
165+
166+
Missing flavors will be reported on various logging channels: error for mandatory, warning for
167+
recommended flavors. Incorrect extra_specs will be reported as error in any case.
135168
The return code will be non-zero if the test could not be performed or if any error was
136169
reported.
137170

171+
The script does not check whether a name given via the extra_spec `scs:name-vN` is indeed valid according
172+
to any major version of the SCS standard on flavor naming.
173+
138174
## Operational tooling
139175

140176
The [openstack-flavor-manager](https://github.com/osism/openstack-flavor-manager) is able to

Tests/README.md

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
# Testsuite for SCS standards
22

33
The tool `scs-compliance-check.py` parses a
4-
[compliance definition file](https://github.com/SovereignCloudStack/standards/blob/main/Standards/scs-0003-v1-sovereign-cloud-standards-yaml.md)
5-
and executes the test executables referenced in there for
6-
the specified layer (`iaas` or `kaas`).
4+
[certificate scope specification](https://github.com/SovereignCloudStack/standards/blob/main/Standards/scs-0003-v1-sovereign-cloud-standards-yaml.md)
5+
and executes the test executables referenced in there.
76

87
## Local execution (Linux, BSD, ...)
98

@@ -26,7 +25,7 @@ With a cloud environment configured in your `~/.config/openstack/clouds.yaml`
2625
and `secure.yaml`, then run
2726

2827
```shell
29-
./scs-compliance-check.py -a os_cloud=CLOUDNAME -s CLOUDNAME scs-compatible-iaas.yaml
28+
./scs-compliance-check.py -s CLOUDNAME -a os_cloud=CLOUDNAME scs-compatible-iaas.yaml
3029
```
3130

3231
Replace `CLOUDNAME` with the name of your cloud environment as
@@ -47,17 +46,23 @@ fail if it isn't set.
4746
## Usage information (help output)
4847

4948
```text
50-
Usage: scs-compliance-check.py [options] compliance-spec.yaml
51-
Options: -v/--verbose: More verbose output
52-
-q/--quiet: Don't output anything but errors
53-
-d/--date YYYY-MM-DD: Check standards valid on specified date instead of today
54-
-V/--version VERS: Force version VERS of the standard (instead of deriving from date)
55-
-s/--subject SUBJECT: Name of the subject (cloud) under test, for the report
56-
-S/--sections SECTION_LIST: comma-separated list of sections to test (default: all sections)
57-
-t/--tests REGEX: regular expression to select individual tests
58-
-o/--output REPORT_PATH: Generate yaml report of compliance check under given path
59-
-C/--critical-only: Only return critical errors in return code
60-
-a/--assign KEY=VALUE: assign variable to be used for the run (as required by yaml file)
49+
Usage: scs-compliance-check.py [options] SPEC_YAML
50+
51+
Arguments:
52+
SPEC_YAML: yaml file specifying the certificate scope
53+
54+
Options:
55+
-v/--verbose: More verbose output
56+
-q/--quiet: Don't output anything but errors
57+
--debug: enables DEBUG logging channel
58+
-d/--date YYYY-MM-DD: Check standards valid on specified date instead of today
59+
-V/--version VERS: Force version VERS of the standard (instead of deriving from date)
60+
-s/--subject SUBJECT: Name of the subject (cloud) under test, for the report
61+
-S/--sections SECTION_LIST: comma-separated list of sections to test (default: all sections)
62+
-t/--tests REGEX: regular expression to select individual testcases based on their ids
63+
-o/--output REPORT_PATH: Generate yaml report of compliance check under given path
64+
-C/--critical-only: Only return critical errors in return code
65+
-a/--assign KEY=VALUE: assign variable to be used for the run (as required by yaml file)
6166
6267
With -C, the return code will be nonzero precisely when the tests couldn't be run to completion.
6368
```

Tests/iaas/entropy/entropy-check.py

Lines changed: 114 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
from collections import Counter
1616
import getopt
1717
import logging
18-
from operator import attrgetter
1918
import os
2019
import re
2120
import sys
@@ -83,25 +82,33 @@ def print_usage(file=sys.stderr):
8382

8483

8584
def 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

9296
def 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

107114
def 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

340365
def 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+
380448
def 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

395457
def 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

Comments
 (0)