Skip to content

Commit

Permalink
Add keystone audit middleware API logging
Browse files Browse the repository at this point in the history
This commit adds Keystone audit middleware API logging to the Cinder
charm in versions Yoga and newer to allow users to configure their
environment for CADF compliance. This feature can be enabled/disabled
and is set to 'disabled' by default to avoid bloat in log files.
The logging output is configured to /var/log/apache2/cinder_error.log.
This commit builds on previous discussions:
juju/charm-helpers#808.

Related-Pr: juju/charm-helpers#893
func-test-pr: openstack-charmers/zaza-openstack-tests#1200
Closes-Bug: 1856555
Change-Id: Ia7dbd6af2305e92eaa9a65890644c4a324ab2c65
  • Loading branch information
MylesJP committed Apr 23, 2024
1 parent 6697f41 commit e25b5d3
Show file tree
Hide file tree
Showing 13 changed files with 344 additions and 30 deletions.
48 changes: 43 additions & 5 deletions charmhelpers/contrib/network/ip.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import re
import subprocess
import socket
import ssl

from functools import partial

Expand Down Expand Up @@ -527,19 +528,56 @@ def get_hostname(address, fqdn=True):
return result.split('.')[0]


def port_has_listener(address, port):
class SSLPortCheckInfo(object):

def __init__(self, key, cert, ca_cert, check_hostname=False):
self.key = key
self.cert = cert
self.ca_cert = ca_cert
# NOTE: by default we do not check hostname since the port check is
# typically performed using 0.0.0.0 which will not match the
# certificate. Hence the default for this is False.
self.check_hostname = check_hostname

@property
def ssl_context(self):
context = ssl.create_default_context()
context.check_hostname = self.check_hostname
context.load_cert_chain(self.cert, self.key)
context.load_verify_locations(self.ca_cert)
return context


def port_has_listener(address, port, sslinfo=None):
"""
Returns True if the address:port is open and being listened to,
else False.
else False. By default uses netcat to check ports but if sslinfo is
provided will use an SSL connection instead.
@param address: an IP address or hostname
@param port: integer port
@param sslinfo: optional SSLPortCheckInfo object.
If provided, the check is performed using an ssl
connection.
Note calls 'zc' via a subprocess shell
"""
cmd = ['nc', '-z', address, str(port)]
result = subprocess.call(cmd)
return not (bool(result))
if not sslinfo:
cmd = ['nc', '-z', address, str(port)]
result = subprocess.call(cmd)
return not (bool(result))

try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
ssock = sslinfo.ssl_context.wrap_socket(sock,
server_hostname=address)
ssock.connect((address, port))
# this bit is crucial to ensure tls close_notify is sent
ssock.unwrap()

return True
except ConnectionRefusedError:
return False


def assert_charm_supports_ipv6():
Expand Down
15 changes: 15 additions & 0 deletions charmhelpers/contrib/openstack/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,21 @@ def get_related(self):
return self.related


class KeystoneAuditMiddleware(OSContextGenerator):
def __init__(self, service: str) -> None:
self.service_name = service

def __call__(self):
"""Return context dictionary containing configuration status of
audit-middleware and the charm service name.
"""
ctxt = {
'audit_middleware': config('audit-middleware') or False,
'service_name': self.service_name
}
return ctxt


class SharedDBContext(OSContextGenerator):
interfaces = ['shared-db']

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% if audit_middleware -%}
[audit_middleware_notifications]
driver = log
{% endif -%}
6 changes: 6 additions & 0 deletions charmhelpers/contrib/openstack/templates/section-filter-audit
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{% if audit_middleware and service_name -%}
[filter:audit]
paste.filter_factory = keystonemiddleware.audit:filter_factory
audit_map_file = /etc/{{ service_name }}/api_audit_map.conf
service_name = {{ service_name }}
{% endif -%}
24 changes: 17 additions & 7 deletions charmhelpers/contrib/openstack/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1207,12 +1207,14 @@ def _ows_check_services_running(services, ports):
return ows_check_services_running(services, ports)


def ows_check_services_running(services, ports):
def ows_check_services_running(services, ports, ssl_check_info=None):
"""Check that the services that should be running are actually running
and that any ports specified are being listened to.
@param services: list of strings OR dictionary specifying services/ports
@param ports: list of ports
@param ssl_check_info: SSLPortCheckInfo object. If provided, port checks
will be done using an SSL connection.
@returns state, message: strings or None, None
"""
messages = []
Expand All @@ -1228,7 +1230,7 @@ def ows_check_services_running(services, ports):
# also verify that the ports that should be open are open
# NB, that ServiceManager objects only OPTIONALLY have ports
map_not_open, ports_open = (
_check_listening_on_services_ports(services))
_check_listening_on_services_ports(services, ssl_check_info))
if not all(ports_open):
# find which service has missing ports. They are in service
# order which makes it a bit easier.
Expand All @@ -1243,7 +1245,8 @@ def ows_check_services_running(services, ports):

if ports is not None:
# and we can also check ports which we don't know the service for
ports_open, ports_open_bools = _check_listening_on_ports_list(ports)
ports_open, ports_open_bools = \
_check_listening_on_ports_list(ports, ssl_check_info)
if not all(ports_open_bools):
messages.append(
"Ports which should be open, but are not: {}"
Expand Down Expand Up @@ -1302,7 +1305,8 @@ def _check_running_services(services):
return list(zip(services, services_running)), services_running


def _check_listening_on_services_ports(services, test=False):
def _check_listening_on_services_ports(services, test=False,
ssl_check_info=None):
"""Check that the unit is actually listening (has the port open) on the
ports that the service specifies are open. If test is True then the
function returns the services with ports that are open rather than
Expand All @@ -1312,11 +1316,14 @@ def _check_listening_on_services_ports(services, test=False):
@param services: OrderedDict(service: [port, ...], ...)
@param test: default=False, if False, test for closed, otherwise open.
@param ssl_check_info: SSLPortCheckInfo object. If provided, port checks
will be done using an SSL connection.
@returns OrderedDict(service: [port-not-open, ...]...), [boolean]
"""
test = not (not (test)) # ensure test is True or False
all_ports = list(itertools.chain(*services.values()))
ports_states = [port_has_listener('0.0.0.0', p) for p in all_ports]
ports_states = [port_has_listener('0.0.0.0', p, ssl_check_info)
for p in all_ports]
map_ports = OrderedDict()
matched_ports = [p for p, opened in zip(all_ports, ports_states)
if opened == test] # essentially opened xor test
Expand All @@ -1327,16 +1334,19 @@ def _check_listening_on_services_ports(services, test=False):
return map_ports, ports_states


def _check_listening_on_ports_list(ports):
def _check_listening_on_ports_list(ports, ssl_check_info=None):
"""Check that the ports list given are being listened to
Returns a list of ports being listened to and a list of the
booleans.
@param ssl_check_info: SSLPortCheckInfo object. If provided, port checks
will be done using an SSL connection.
@param ports: LIST of port numbers.
@returns [(port_num, boolean), ...], [boolean]
"""
ports_open = [port_has_listener('0.0.0.0', p) for p in ports]
ports_open = [port_has_listener('0.0.0.0', p, ssl_check_info)
for p in ports]
return zip(ports, ports_open), ports_open


Expand Down
6 changes: 1 addition & 5 deletions charmhelpers/contrib/storage/linux/lvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
CalledProcessError,
check_call,
check_output,
Popen,
PIPE,
)


Expand Down Expand Up @@ -58,9 +56,7 @@ def remove_lvm_physical_volume(block_device):
:param block_device: str: Full path of block device to scrub.
'''
p = Popen(['pvremove', '-ff', block_device],
stdin=PIPE)
p.communicate(input='y\n')
check_call(['pvremove', '-ff', '--yes', block_device])


def list_lvm_volume_group(block_device):
Expand Down
7 changes: 5 additions & 2 deletions charmhelpers/core/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,11 @@ def service_resume(service_name, init_dir="/etc/init",
upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
sysv_file = os.path.join(initd_dir, service_name)
if init_is_systemd(service_name=service_name):
service('unmask', service_name)
service('enable', service_name)
if service('is-enabled', service_name):
log('service {} already enabled'.format(service_name), level=DEBUG)
else:
service('unmask', service_name)
service('enable', service_name)
elif os.path.exists(upstart_file):
override_path = os.path.join(
init_dir, '{}.override'.format(service_name))
Expand Down
28 changes: 20 additions & 8 deletions charmhelpers/osplatform.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,13 @@ def get_platform():
will be returned (which is the name of the module).
This string is used to decide which platform module should be imported.
"""
# linux_distribution is deprecated and will be removed in Python 3.7
# Warnings *not* disabled, as we certainly need to fix this.
if hasattr(platform, 'linux_distribution'):
tuple_platform = platform.linux_distribution()
current_platform = tuple_platform[0]
else:
current_platform = _get_platform_from_fs()
current_platform = _get_current_platform()

if "Ubuntu" in current_platform:
return "ubuntu"
elif "CentOS" in current_platform:
return "centos"
elif "debian" in current_platform:
elif "debian" in current_platform or "Debian" in current_platform:
# Stock Python does not detect Ubuntu and instead returns debian.
# Or at least it does in some build environments like Travis CI
return "ubuntu"
Expand All @@ -36,6 +30,24 @@ def get_platform():
.format(current_platform))


def _get_current_platform():
"""Return the current platform information for the OS.
Attempts to lookup linux distribution information from the platform
module for releases of python < 3.7. For newer versions of python,
the platform is determined from the /etc/os-release file.
"""
# linux_distribution is deprecated and will be removed in Python 3.7
# Warnings *not* disabled, as we certainly need to fix this.
if hasattr(platform, 'linux_distribution'):
tuple_platform = platform.linux_distribution()
current_platform = tuple_platform[0]
else:
current_platform = _get_platform_from_fs()

return current_platform


def _get_platform_from_fs():
"""Get Platform from /etc/os-release."""
with open(os.path.join(os.sep, 'etc', 'os-release')) as fin:
Expand Down
5 changes: 5 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ options:
default: False
description: |
Setting this to True will allow supporting services to log to syslog.
audit-middleware:
type: boolean
default: False
description: |
Enable Keystone auditing middleware for logging API calls.
openstack-origin:
type: string
default: bobcat
Expand Down
13 changes: 10 additions & 3 deletions hooks/cinder_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ class CinderCharmError(Exception):
CINDER_CONF = '%s/cinder.conf' % CINDER_CONF_DIR
CINDER_API_CONF = '%s/api-paste.ini' % CINDER_CONF_DIR
CINDER_POLICY_JSON = '%s/policy.json' % CINDER_CONF_DIR
CINDER_AUDIT_MAP = '%s/api_audit_map.conf' % CINDER_CONF_DIR
CEPH_CONF = '/etc/ceph/ceph.conf'

HAPROXY_CONF = '/etc/haproxy/haproxy.cfg'
Expand Down Expand Up @@ -237,18 +238,24 @@ def required_interfaces():
cinder_contexts.VolumeUsageAuditContext(),
context.MemcacheContext(),
cinder_contexts.SectionalConfigContext(),
cinder_contexts.LVMContext()],
cinder_contexts.LVMContext(),
context.KeystoneAuditMiddleware(service='cinder')],
'services': ['cinder-api', 'cinder-volume', 'cinder-scheduler',
'haproxy']
}),
(CINDER_API_CONF, {
'contexts': [context.IdentityServiceContext()],
'contexts': [context.IdentityServiceContext(),
context.KeystoneAuditMiddleware(service='cinder')],
'services': ['cinder-api'],
}),
(CINDER_POLICY_JSON, {
'contexts': [],
'services': ['cinder-api']
}),
(CINDER_AUDIT_MAP, {
'contexts': [context.KeystoneAuditMiddleware(service='cinder')],
'services': ['cinder-api']
}),
(ceph_config_file(), {
'contexts': [context.CephContext()],
'services': ['cinder-volume']
Expand Down Expand Up @@ -773,7 +780,7 @@ def check_local_db_actions_complete():
# Echo notification
data = {CINDER_DB_INIT_ECHO_RKEY: init_id}
# BUG: #1928383 - clear CINDER_DB_INIT_RKEY if not the leader
if not(is_elected_leader(CLUSTER_RES)):
if not is_elected_leader(CLUSTER_RES):
data[CINDER_DB_INIT_RKEY] = None
relation_set(**data)

Expand Down
Loading

0 comments on commit e25b5d3

Please sign in to comment.