Remove native ssl support

As eventlet ssl termination is broken with python 3 and
we won't be supporting python 2.7 anymore we will just
remove ssl termination to glance-api and expect the
termination being handled by something else, like HAProxy.

This patch also removes the broken ssl test job as the
non-existing feature is not broken anymore.

Change-Id: Iaf16dfcfdb3a2c93312dcad1ea1229e6b3c8caaa
This commit is contained in:
Erno Kuvaja 2019-12-06 14:49:39 +00:00
parent ce79c8ed78
commit 06b2465f59
11 changed files with 38 additions and 458 deletions

View File

@ -9,25 +9,6 @@
- openstack/devstack-gate
- openstack/glance
- job:
name: glance-eventlet-ssl-handshake-broken-py3
parent: tox
description: |
See https://bugs.launchpad.net/glance/+bug/1482633
vars:
tox_envlist: broken-py3-ssl-tests
irrelevant-files:
- ^(test-|)requirements.txt$
- ^lower-constraints.txt$
- ^.*\.rst$
- ^api-ref/.*$
- ^doc/.*$
- ^etc/.*$
- ^releasenotes/.*$
- ^setup.cfg$
- ^tox.ini$
- ^\.zuul\.yaml$
- job:
name: glance-code-constants-check
parent: tox
@ -231,8 +212,6 @@
jobs:
- openstack-tox-functional
- openstack-tox-functional-py37
- glance-eventlet-ssl-handshake-broken-py3:
voting: false
- glance-code-constants-check
- devstack-plugin-ceph-tempest:
voting: false

View File

@ -1,2 +0,0 @@
^glance\.tests\.functional\.test_reload\.TestReload\.test_reload$
^glance\.tests\.functional\.test_ssl\.TestSSL\.test_ssl_ok$

View File

@ -31,12 +31,9 @@ from eventlet.green import socket
import functools
import os
import re
import uuid
from OpenSSL import crypto
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import encodeutils
from oslo_utils import excutils
from oslo_utils import netutils
from oslo_utils import strutils
@ -46,7 +43,7 @@ from webob import exc
from glance.common import exception
from glance.common import timeutils
from glance.i18n import _, _LE, _LW
from glance.i18n import _, _LE
CONF = cfg.CONF
@ -440,61 +437,6 @@ def setup_remote_pydev_debug(host, port):
LOG.exception(error_msg)
def validate_key_cert(key_file, cert_file):
try:
error_key_name = "private key"
error_filename = key_file
with open(key_file, 'r') as keyfile:
key_str = keyfile.read()
key = crypto.load_privatekey(crypto.FILETYPE_PEM, key_str)
error_key_name = "certificate"
error_filename = cert_file
with open(cert_file, 'r') as certfile:
cert_str = certfile.read()
cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_str)
except IOError as ioe:
raise RuntimeError(_("There is a problem with your %(error_key_name)s "
"%(error_filename)s. Please verify it."
" Error: %(ioe)s") %
{'error_key_name': error_key_name,
'error_filename': error_filename,
'ioe': ioe})
except crypto.Error as ce:
raise RuntimeError(_("There is a problem with your %(error_key_name)s "
"%(error_filename)s. Please verify it. OpenSSL"
" error: %(ce)s") %
{'error_key_name': error_key_name,
'error_filename': error_filename,
'ce': ce})
try:
data = str(uuid.uuid4())
# On Python 3, explicitly encode to UTF-8 to call crypto.sign() which
# requires bytes. Otherwise, it raises a deprecation warning (and
# will raise an error later).
data = encodeutils.to_utf8(data)
digest = CONF.digest_algorithm
if digest == 'sha1':
LOG.warn(
_LW('The FIPS (FEDERAL INFORMATION PROCESSING STANDARDS)'
' state that the SHA-1 is not suitable for'
' general-purpose digital signature applications (as'
' specified in FIPS 186-3) that require 112 bits of'
' security. The default value is sha1 in Kilo for a'
' smooth upgrade process, and it will be updated'
' with sha256 in next release(L).'))
out = crypto.sign(key, data, digest)
crypto.verify(cert, out, data, digest)
except crypto.Error as ce:
raise RuntimeError(_("There is a problem with your key pair. "
"Please verify that cert %(cert_file)s and "
"key %(key_file)s belong together. OpenSSL "
"error %(ce)s") % {'cert_file': cert_file,
'key_file': key_file,
'ce': ce})
def get_test_suite_socket():
global GLANCE_TEST_SOCKET_FD_STR
if GLANCE_TEST_SOCKET_FD_STR in os.environ:

View File

@ -34,7 +34,6 @@ import threading
import time
from eventlet.green import socket
from eventlet.green import ssl
import eventlet.greenio
import eventlet.wsgi
import glance_store
@ -143,67 +142,6 @@ Possible values:
Related options:
* None
""")),
cfg.StrOpt('ca_file',
sample_default='/etc/ssl/cafile',
help=_("""
Absolute path to the CA file.
Provide a string value representing a valid absolute path to
the Certificate Authority file to use for client authentication.
A CA file typically contains necessary trusted certificates to
use for the client authentication. This is essential to ensure
that a secure connection is established to the server via the
internet.
Possible values:
* Valid absolute path to the CA file
Related options:
* None
""")),
cfg.StrOpt('cert_file',
sample_default='/etc/ssl/certs',
help=_("""
Absolute path to the certificate file.
Provide a string value representing a valid absolute path to the
certificate file which is required to start the API service
securely.
A certificate file typically is a public key container and includes
the server's public key, server name, server information and the
signature which was a result of the verification process using the
CA certificate. This is required for a secure connection
establishment.
Possible values:
* Valid absolute path to the certificate file
Related options:
* None
""")),
cfg.StrOpt('key_file',
sample_default='/etc/ssl/key/key-file.pem',
help=_("""
Absolute path to a private key file.
Provide a string value representing a valid absolute path to a
private key file which is required to establish the client-server
connection.
Possible values:
* Absolute path to the private key file
Related options:
* None
""")),
]
@ -402,30 +340,6 @@ def get_bind_addr(default_port=None):
return (CONF.bind_host, CONF.bind_port or default_port)
def ssl_wrap_socket(sock):
"""
Wrap an existing socket in SSL
:param sock: non-SSL socket to wrap
:returns: An SSL wrapped socket
"""
utils.validate_key_cert(CONF.key_file, CONF.cert_file)
ssl_kwargs = {
'server_side': True,
'certfile': CONF.cert_file,
'keyfile': CONF.key_file,
'cert_reqs': ssl.CERT_NONE,
}
if CONF.ca_file:
ssl_kwargs['ca_certs'] = CONF.ca_file
ssl_kwargs['cert_reqs'] = ssl.CERT_REQUIRED
return ssl.wrap_socket(sock, **ssl_kwargs)
def get_socket(default_port):
"""
Bind socket to bind ip:port in conf
@ -434,8 +348,7 @@ def get_socket(default_port):
:param default_port: port to bind to if none is specified in conf
:returns: a socket object as returned from socket.listen or
ssl.wrap_socket if conf specifies cert_file
:returns: a socket object as returned from socket.listen
"""
bind_addr = get_bind_addr(default_port)
@ -450,12 +363,6 @@ def get_socket(default_port):
if addr[0] in (socket.AF_INET, socket.AF_INET6)
][0]
use_ssl = CONF.key_file or CONF.cert_file
if use_ssl and (not CONF.key_file or not CONF.cert_file):
raise RuntimeError(_("When running server in SSL mode, you must "
"specify both a cert_file and key_file "
"option value in your configuration file"))
sock = utils.get_test_suite_socket()
retry_until = time.time() + 30
@ -701,16 +608,6 @@ class BaseServer(object):
new_sock = (old_conf is None or (
has_changed('bind_host') or
has_changed('bind_port')))
# Will we be using https?
use_ssl = not (not CONF.cert_file or not CONF.key_file)
# Were we using https before?
old_use_ssl = (old_conf is not None and not (
not old_conf.get('key_file') or
not old_conf.get('cert_file')))
# Do we now need to perform an SSL wrap on the socket?
wrap_sock = use_ssl is True and (old_use_ssl is False or new_sock)
# Do we now need to perform an SSL unwrap on the socket?
unwrap_sock = use_ssl is False and old_use_ssl is True
if new_sock:
self._sock = None
@ -722,25 +619,7 @@ class BaseServer(object):
# sockets can hang around forever without keepalive
_sock.setsockopt(socket.SOL_SOCKET,
socket.SO_KEEPALIVE, 1)
self._sock = _sock
if wrap_sock:
self.sock = ssl_wrap_socket(self._sock)
if unwrap_sock:
self.sock = self._sock
if new_sock and not use_ssl:
self.sock = self._sock
# Pick up newly deployed certs
if old_conf is not None and use_ssl is True and old_use_ssl is True:
if has_changed('cert_file') or has_changed('key_file'):
utils.validate_key_cert(CONF.key_file, CONF.cert_file)
if has_changed('cert_file'):
self.sock.certfile = CONF.cert_file
if has_changed('key_file'):
self.sock.keyfile = CONF.key_file
self.sock = _sock
if new_sock or (old_conf is not None and has_changed('tcp_keepidle')):
# This option isn't available in the OS X version of eventlet
@ -982,19 +861,13 @@ class Win32Server(BaseServer):
def configure_socket(self, old_conf=None, has_changed=None):
fresh_start = not (old_conf or has_changed)
use_ssl = CONF.cert_file or CONF.key_file
pipe_handle = getattr(CONF, 'pipe_handle', None)
if not (fresh_start and pipe_handle):
return super(Win32Server, self).configure_socket(
old_conf, has_changed)
self._sock = self._get_sock_from_parent()
if use_ssl:
self.sock = ssl_wrap_socket(self._sock)
else:
self.sock = self._sock
self.sock = self._get_sock_from_parent()
if hasattr(socket, 'TCP_KEEPIDLE'):
# This was introduced in WS 2016 RS3

View File

@ -379,8 +379,6 @@ class ApiServer(Server):
self.default_store = kwargs.get("default_store", "file")
self.bind_host = "127.0.0.1"
self.registry_host = "127.0.0.1"
self.key_file = ""
self.cert_file = ""
self.metadata_encryption_key = "012345678901234567890123456789ab"
self.image_dir = os.path.join(self.test_dir, "images")
self.pid_file = pid_file or os.path.join(self.test_dir, "api.pid")
@ -421,8 +419,6 @@ debug = %(debug)s
default_log_levels = eventlet.wsgi.server=DEBUG
bind_host = %(bind_host)s
bind_port = %(bind_port)s
key_file = %(key_file)s
cert_file = %(cert_file)s
metadata_encryption_key = %(metadata_encryption_key)s
registry_host = %(registry_host)s
registry_port = %(registry_port)s
@ -560,8 +556,6 @@ class ApiServerForMultipleBackend(Server):
self.default_backend = kwargs.get("default_backend", "file1")
self.bind_host = "127.0.0.1"
self.registry_host = "127.0.0.1"
self.key_file = ""
self.cert_file = ""
self.metadata_encryption_key = "012345678901234567890123456789ab"
self.image_dir_backend_1 = os.path.join(self.test_dir, "images_1")
self.image_dir_backend_2 = os.path.join(self.test_dir, "images_2")
@ -605,8 +599,6 @@ debug = %(debug)s
default_log_levels = eventlet.wsgi.server=DEBUG
bind_host = %(bind_host)s
bind_port = %(bind_port)s
key_file = %(key_file)s
cert_file = %(cert_file)s
metadata_encryption_key = %(metadata_encryption_key)s
registry_host = %(registry_host)s
registry_port = %(registry_port)s

View File

@ -23,6 +23,7 @@ from six.moves import http_client as http
from glance.tests import functional
from glance.tests.utils import execute
from glance.tests.utils import skip_if_disabled
TEST_VAR_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
'../', 'var'))
@ -44,11 +45,16 @@ class TestReload(functional.FunctionalTest):
"""Test configuration reload"""
def setUp(self):
self.workers = 1
super(TestReload, self).setUp()
self.cleanup()
self.workers = 1
self.include_scrubber = False
self.disabled = True
self.disabled_message = "Reload is broken, Bug 1855708"
def tearDown(self):
self.stop_servers()
if not self.disabled:
self.stop_servers()
super(TestReload, self).tearDown()
def ticker(self, message, seconds=60, tick=0.01):
@ -101,6 +107,7 @@ class TestReload(functional.FunctionalTest):
def _url(self, protocol, path):
return '%s://127.0.0.1:%d%s' % (protocol, self.api_port, path)
@skip_if_disabled
def test_reload(self):
"""Test SIGHUP picks up new config values"""
def check_pids(pre, post=None, workers=2):
@ -121,6 +128,13 @@ class TestReload(functional.FunctionalTest):
pre_pids = {}
post_pids = {}
path = self._url('http', '/')
response = requests.get(path)
self.assertEqual(http.MULTIPLE_CHOICES, response.status_code)
del response # close socket so that process audit is reliable
pre_pids['api'] = self._get_children('api')
# Test changing the workers value creates all new children
# This recycles the existing socket
msg = 'Start timeout'
@ -145,88 +159,6 @@ class TestReload(functional.FunctionalTest):
if check_pids(pre_pids['api'], post_pids['api']):
break
# Test changing from http to https
# This recycles the existing socket
path = self._url('http', '/')
response = requests.get(path)
self.assertEqual(http.MULTIPLE_CHOICES, response.status_code)
del response # close socket so that process audit is reliable
pre_pids['api'] = self._get_children('api')
key_file = os.path.join(TEST_VAR_DIR, 'privatekey.key')
set_config_value(self._conffile('api'), 'key_file', key_file)
cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
set_config_value(self._conffile('api'), 'cert_file', cert_file)
cmd = "kill -HUP %s" % self._get_parent('api')
execute(cmd, raise_error=True)
msg = 'http to https timeout'
for _ in self.ticker(msg):
post_pids['api'] = self._get_children('api')
if check_pids(pre_pids['api'], post_pids['api']):
break
ca_file = os.path.join(TEST_VAR_DIR, 'ca.crt')
path = self._url('https', '/')
response = requests.get(path, verify=ca_file)
self.assertEqual(http.MULTIPLE_CHOICES, response.status_code)
del response
# Test https restart
# This recycles the existing socket
pre_pids['api'] = self._get_children('api')
cmd = "kill -HUP %s" % self._get_parent('api')
execute(cmd, raise_error=True)
msg = 'https restart timeout'
for _ in self.ticker(msg):
post_pids['api'] = self._get_children('api')
if check_pids(pre_pids['api'], post_pids['api']):
break
ca_file = os.path.join(TEST_VAR_DIR, 'ca.crt')
path = self._url('https', '/')
response = requests.get(path, verify=ca_file)
self.assertEqual(http.MULTIPLE_CHOICES, response.status_code)
del response
# Test changing the https bind_host
# This requires a new socket
pre_pids['api'] = self._get_children('api')
set_config_value(self._conffile('api'), 'bind_host', '127.0.0.1')
cmd = "kill -HUP %s" % self._get_parent('api')
execute(cmd, raise_error=True)
msg = 'https bind_host timeout'
for _ in self.ticker(msg):
post_pids['api'] = self._get_children('api')
if check_pids(pre_pids['api'], post_pids['api']):
break
path = self._url('https', '/')
response = requests.get(path, verify=ca_file)
self.assertEqual(http.MULTIPLE_CHOICES, response.status_code)
del response
# Test https -> http
# This recycles the existing socket
pre_pids['api'] = self._get_children('api')
set_config_value(self._conffile('api'), 'key_file', '')
set_config_value(self._conffile('api'), 'cert_file', '')
cmd = "kill -HUP %s" % self._get_parent('api')
execute(cmd, raise_error=True)
msg = 'https to http timeout'
for _ in self.ticker(msg):
post_pids['api'] = self._get_children('api')
if check_pids(pre_pids['api'], post_pids['api']):
break
path = self._url('http', '/')
response = requests.get(path)
self.assertEqual(http.MULTIPLE_CHOICES, response.status_code)
del response
# Test changing the http bind_host
# This requires a new socket
pre_pids['api'] = self._get_children('api')

View File

@ -1,83 +0,0 @@
# Copyright 2015 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
import httplib2
from six.moves import http_client as http
from glance.tests import functional
TEST_VAR_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', 'var'))
class TestSSL(functional.FunctionalTest):
"""Functional tests verifying SSL communication"""
def setUp(self):
super(TestSSL, self).setUp()
if getattr(self, 'inited', False):
return
self.inited = False
self.disabled = True
# NOTE (stevelle): Test key/cert/CA file created as per:
# http://nrocco.github.io/2013/01/25/
# self-signed-ssl-certificate-chains.html
# For these tests certificate.crt must be created with 'Common Name'
# set to 127.0.0.1
self.key_file = os.path.join(TEST_VAR_DIR, 'privatekey.key')
if not os.path.exists(self.key_file):
self.disabled_message = ("Could not find private key file %s" %
self.key_file)
self.inited = True
return
self.cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
if not os.path.exists(self.cert_file):
self.disabled_message = ("Could not find certificate file %s" %
self.cert_file)
self.inited = True
return
self.ca_file = os.path.join(TEST_VAR_DIR, 'ca.crt')
if not os.path.exists(self.ca_file):
self.disabled_message = ("Could not find CA file %s" %
self.ca_file)
self.inited = True
return
self.inited = True
self.disabled = False
def tearDown(self):
super(TestSSL, self).tearDown()
if getattr(self, 'inited', False):
return
def test_ssl_ok(self):
"""Make sure the public API works with HTTPS."""
self.cleanup()
self.start_servers(**self.__dict__.copy())
path = "https://%s:%d/versions" % ("127.0.0.1", self.api_port)
https = httplib2.Http(ca_certs=self.ca_file)
response, content = https.request(path, 'GET')
self.assertEqual(http.OK, response.status)

View File

@ -15,7 +15,6 @@
# under the License.
import mock
import os
import tempfile
from oslo_log import log as logging
@ -326,46 +325,6 @@ class TestUtils(test_utils.BaseTestCase):
result = utils.mutating(fake_function)
self.assertEqual("test passed", result(req, Fake()))
def test_validate_key_cert_key(self):
self.config(digest_algorithm='sha256')
var_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),
'../../', 'var'))
keyfile = os.path.join(var_dir, 'privatekey.key')
certfile = os.path.join(var_dir, 'certificate.crt')
utils.validate_key_cert(keyfile, certfile)
def test_validate_key_cert_no_private_key(self):
with tempfile.NamedTemporaryFile('w+') as tmpf:
self.assertRaises(RuntimeError,
utils.validate_key_cert,
"/not/a/file", tmpf.name)
def test_validate_key_cert_cert_cant_read(self):
with tempfile.NamedTemporaryFile('w+') as keyf:
with tempfile.NamedTemporaryFile('w+') as certf:
os.chmod(certf.name, 0)
self.assertRaises(RuntimeError,
utils.validate_key_cert,
keyf.name, certf.name)
def test_validate_key_cert_key_cant_read(self):
with tempfile.NamedTemporaryFile('w+') as keyf:
with tempfile.NamedTemporaryFile('w+') as certf:
os.chmod(keyf.name, 0)
self.assertRaises(RuntimeError,
utils.validate_key_cert,
keyf.name, certf.name)
def test_invalid_digest_algorithm(self):
self.config(digest_algorithm='fake_algorithm')
var_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),
'../../', 'var'))
keyfile = os.path.join(var_dir, 'privatekey.key')
certfile = os.path.join(var_dir, 'certificate.crt')
self.assertRaises(ValueError,
utils.validate_key_cert,
keyfile, certfile)
def test_valid_hostname(self):
valid_inputs = ['localhost',
'glance04-a'

View File

@ -705,17 +705,11 @@ class GetSocketTestCase(test_utils.BaseTestCase):
self.useFixture(fixtures.MonkeyPatch(
"glance.common.wsgi.utils.validate_key_cert",
lambda *x: None))
wsgi.CONF.cert_file = '/etc/ssl/cert'
wsgi.CONF.key_file = '/etc/ssl/key'
wsgi.CONF.ca_file = '/etc/ssl/ca_cert'
wsgi.CONF.tcp_keepidle = 600
@mock.patch.object(prefetcher, 'Prefetcher')
def test_correct_configure_socket(self, mock_prefetcher):
mock_socket = mock.Mock()
self.useFixture(fixtures.MonkeyPatch(
'glance.common.wsgi.ssl.wrap_socket',
mock_socket))
self.useFixture(fixtures.MonkeyPatch(
'glance.common.wsgi.eventlet.listen',
lambda *x, **y: mock_socket))
@ -731,23 +725,16 @@ class GetSocketTestCase(test_utils.BaseTestCase):
socket.SO_KEEPALIVE,
1), mock_socket.mock_calls)
if hasattr(socket, 'TCP_KEEPIDLE'):
self.assertIn(mock.call().setsockopt(
self.assertIn(mock.call.setsockopt(
socket.IPPROTO_TCP,
socket.TCP_KEEPIDLE,
wsgi.CONF.tcp_keepidle), mock_socket.mock_calls)
def test_get_socket_without_all_ssl_reqs(self):
wsgi.CONF.key_file = None
self.assertRaises(RuntimeError, wsgi.get_socket, 1234)
def test_get_socket_with_bind_problems(self):
self.useFixture(fixtures.MonkeyPatch(
'glance.common.wsgi.eventlet.listen',
mock.Mock(side_effect=(
[wsgi.socket.error(socket.errno.EADDRINUSE)] * 3 + [None]))))
self.useFixture(fixtures.MonkeyPatch(
'glance.common.wsgi.ssl.wrap_socket',
lambda *x, **y: None))
self.assertRaises(RuntimeError, wsgi.get_socket, 1234)
@ -755,9 +742,6 @@ class GetSocketTestCase(test_utils.BaseTestCase):
self.useFixture(fixtures.MonkeyPatch(
'glance.common.wsgi.eventlet.listen',
mock.Mock(side_effect=wsgi.socket.error(socket.errno.ENOMEM))))
self.useFixture(fixtures.MonkeyPatch(
'glance.common.wsgi.ssl.wrap_socket',
lambda *x, **y: None))
self.assertRaises(wsgi.socket.error, wsgi.get_socket, 1234)

View File

@ -0,0 +1,15 @@
---
upgrade:
- |
If upgrade is conducted from PY27 where ssl connections has been terminated
into glance-api, the termination needs to happen externally from now on.
security:
- |
The ssl support from Glance has been removed as it worked only under PY27
which is not anymore supported environment. Termination of encrypted
connections needs to happen externally as soon as move to PY3 happens. Any
deployment needing end to end encryption would need to put either reverse
proxy (using fully blown http server like Apache or Nginx will cause
significant performance hit and we advice using something more simple that
does not break the http protocol) in front of the service or utilize
ssl tunneling (like stunnel) between loadbalancers and glance-api.

15
tox.ini
View File

@ -57,7 +57,7 @@ ignore_errors = True
whitelist_externals =
bash
commands =
stestr run --blacklist-file ./broken-functional-py3-ssl-tests.txt {posargs}
stestr run {posargs}
[testenv:functional-py37]
basepython = python3.7
@ -67,18 +67,7 @@ ignore_errors = True
whitelist_externals =
bash
commands =
stestr run --blacklist-file ./broken-functional-py3-ssl-tests.txt {posargs}
[testenv:broken-py3-ssl-tests]
# NOTE(rosmaita): these tests were being skipped due to bug #1482633, but we
# want it to be obvious that Glance is affected by the eventlet ssl-handshake
# problem under py3. (When this testenv is removed, don't forget to adjust
# the blacklist generation in the functional-py3 testenv.)
basepython = python3
setenv =
TEST_PATH = ./glance/tests/functional
commands =
stestr run --whitelist-file ./broken-functional-py3-ssl-tests.txt {posargs}
stestr run {posargs}
[testenv:gateonly]
# NOTE(rosmaita): these tests catch configuration problems for some code