Skip to content

Commit

Permalink
Use client cert for keystone-identity
Browse files Browse the repository at this point in the history
When manila-ganesha is related to vault it needs a client cert to
configure the keystone-auth section of manila.conf to communicate with
keystone. This patch sets that up and removes the broken server cert
auto configuration which ended up masking the manila-share service.

func-test-pr: openstack-charmers/zaza-openstack-tests#1253
Change-Id: I55e9aa09b88684517d4052dc56eed0cab05a0262
Closes-Bug: #2064487
  • Loading branch information
xtrusia committed Oct 7, 2024
1 parent 84cf196 commit 6bdf638
Show file tree
Hide file tree
Showing 8 changed files with 812 additions and 3 deletions.
9 changes: 9 additions & 0 deletions osci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,17 @@
templates:
- charm-unit-jobs-py310
- charm-functional-jobs
check:
jobs:
- noble-caracal-vault_manila-ganesha
vars:
needs_charm_build: true
charm_build_name: manila-ganesha
build_type: charmcraft
charmcraft_channel: 3.x/stable

- job:
name: noble-caracal-vault_manila-ganesha
parent: func-target
vars:
tox_extra_args: '-- vault:noble-caracal-vault'
111 changes: 111 additions & 0 deletions src/lib/charm/openstack/manila_ganesha.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,30 @@

import collections
import errno
import os
import socket
import subprocess

import charms_openstack.charm
import charms_openstack.adapters
import charms_openstack.plugins
import charms_openstack.charm.utils
import charmhelpers.contrib.network.ip as ch_net_ip
import charms.reactive.relations as relations
from charmhelpers.core.host import (
cmp_pkgrevno,
service_pause,
mkdir,
path_hash,
write_file,
)
from charmhelpers.core.hookenv import (
ERROR,
config,
goal_state,
local_unit,
log,
network_get,
)
from charmhelpers.contrib.hahelpers.cluster import (
is_clustered,
Expand All @@ -49,6 +57,10 @@

MANILA_DIR = '/etc/manila/'
MANILA_CONF = MANILA_DIR + "manila.conf"
MANILA_SSL_DIR = MANILA_DIR + "ssl/"
MANILA_CLIENT_CERT_FILE = MANILA_SSL_DIR + "cert.crt"
MANILA_CLIENT_KEY_FILE = MANILA_SSL_DIR + "cert.key"
MANILA_CLIENT_CA_FILE = MANILA_SSL_DIR + "ca.crt"
MANILA_LOGGING_CONF = MANILA_DIR + "logging.conf"
MANILA_API_PASTE_CONF = MANILA_DIR + "api-paste.ini"
CEPH_CONF = '/etc/ceph/ceph.conf'
Expand Down Expand Up @@ -152,6 +164,28 @@ def service_username(self):
return self.credentials_username


class TlsCertificatesAdapter(
charms_openstack.adapters.OpenStackRelationAdapter):
"""Modifies the keystone-credentials interface to act like keystone."""

def _resolve_file_name(self, path):
if os.path.exists(path):
return path
return None

@property
def certfile(self):
return self._resolve_file_name(MANILA_CLIENT_CERT_FILE)

@property
def keyfile(self):
return self._resolve_file_name(MANILA_CLIENT_KEY_FILE)

@property
def cafile(self):
return self._resolve_file_name(MANILA_CLIENT_CA_FILE)


class GaneshaCharmRelationAdapters(
charms_openstack.adapters.OpenStackRelationAdapters):
relation_adapters = {
Expand All @@ -160,6 +194,7 @@ class GaneshaCharmRelationAdapters(
'manila-ganesha': charms_openstack.adapters.OpenStackRelationAdapter,
'identity-service': KeystoneCredentialAdapter,
'shared_db': charms_openstack.adapters.DatabaseRelationAdapter,
'certificates': TlsCertificatesAdapter,
}


Expand Down Expand Up @@ -222,6 +257,10 @@ class ManilaGaneshaCharm(charms_openstack.charm.HAOpenStackCharm,
('12', 'wallaby'),
('13', 'xena'),
('14', 'yoga'),
('15', 'zed'),
('16', 'antelope'),
('17', 'bobcat'),
('18', 'caracal'),
]),
}

Expand Down Expand Up @@ -411,6 +450,78 @@ def request_ceph_permissions(self, ceph):
'client': ch_core.hookenv.application_name()})
ceph.send_request_if_needed(rq)

def get_client_cert_cn_sans(self):
"""Get the tuple (cn, [sans]) for a client certificiate.
This is for the keystone endpoint/interface, so generate the client
cert data for that.
"""
try:
ingress = network_get('identity-service')['ingress-addresses']
except Exception as e:
# if it didn't work, log it as an error, and return (None, None)
log(f"Getting ingress for identity-service failed: {str(e)}",
level=ERROR)
return (None, None)
return (ingress[0], ingress[1:])

def handle_changed_client_cert_files(self, ca, cert, key):
"""Handle changes to client cert, key or ca.
If the client certs have changed on disk, rerender and restart manila.
The cert and key need to be written to:
- /etc/manila/ssl/cert.crt - MANILA_CLIENT_CERT_FILE
- /etc/manila/ssl/cert.key - MANILA_CLIENT_KEY_FILE
- /etc/manila/ssl/ca.cert - MANILA_CLIENT_CA_FILE
"""
# lives ensrure that the cert dir exists
mkdir(MANILA_SSL_DIR)
paths = {
MANILA_CLIENT_CA_FILE: ca,
MANILA_CLIENT_CERT_FILE: cert,
MANILA_CLIENT_KEY_FILE: key,
}
checksums = {path: path_hash(path) for path in paths.keys()}
# write or remove the files.
for path, contents in paths.items():
if contents is None:
# delete the file
realpath = os.path.abspath(path)
path_exists = os.path.exists(realpath)
if path_exists:
try:
os.remove(path)
except OSError as e:
log("Path {} couldn't be deleted: {}"
.format(path, str(e)), level=ERROR)
else:
write_file(path,
contents.encode(),
owner=self.user,
group=self.group,
perms=0o640)
new_checksums = {path: path_hash(path) for path in paths.keys()}
if new_checksums != checksums:
interfaces = (
'ceph.available',
'amqp.available',
'manila-plugin.available',
'shared-db.available',
'identity-service.available',
'certificates.available',
)
# check all the interfaces are available
endpoints = []
for interface in interfaces:
endpoint = relations.endpoint_from_flag(interface)
if not endpoint:
# if not available don't attempt to render
return
endpoints.append(endpoint)
self.render_with_interfaces(endpoints)

def install_nrpe_checks(self, enable_cron=True):
return install_nrpe_checks(enable_cron=enable_cron)

Expand Down
76 changes: 74 additions & 2 deletions src/reactive/manila_ganesha.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@
'config.changed',
'update-status',
'upgrade-charm',
'certificates.available',
# TODO: remove follwoing commented out code.
# remove certificates.available as we want to wire in the call ourselves
# directly.
# 'certificates.available',
)


Expand Down Expand Up @@ -78,7 +81,14 @@ def render_things(*args):
level=ch_core.hookenv.INFO)
charm_instance.configure_ceph_keyring(ceph_relation.key)

charm_instance.render_with_interfaces(args)
# add in optional certificates.available relation for https to keystone
certificates = relations.endpoint_from_flag('certificates.available')
if certificates:
interfaces = list(args) + [certificates]
else:
interfaces = list(args)

charm_instance.render_with_interfaces(interfaces)

reactive.set_flag('config.rendered')
charm_instance.assess_status()
Expand Down Expand Up @@ -156,6 +166,68 @@ def disable_services():
reactive.set_flag('services-disabled')


@reactive.when('certificates.ca.available')
def install_root_ca_cert():
print("running install_root_ca_cert")
cert_provider = relations.endpoint_from_flag('certificates.ca.available')
if cert_provider:
print("cert_provider lives")
update_client_certs_and_ca(cert_provider)


@reactive.when('certificates.available')
def set_client_cert_request():
"""Set up the client certificate request.
If the charm is related to vault then it will send a client cert request
(set it on the relation) so that the keystone auth can be configured with a
client cert, key and CA to authenticate with keystone (HTTP).
"""
print("running set_client_cert_request")
cert_provider = relations.endpoint_from_flag('certificates.available')
if cert_provider:
print("cert_provider lives")
with charm.provide_charm_instance() as the_charm:
client_cn, client_sans = the_charm.get_client_cert_cn_sans()
print(f"client_cn: {client_cn}, client_sans: {client_sans}")
if client_cn:
cert_provider.request_client_cert(client_cn, client_sans)


@reactive.when('certificates.certs.available')
def update_client_cert():
print("running update_client_cert")
cert_provider = relations.endpoint_from_flag('certificates.available')
if cert_provider:
print("cert_provider lives")
update_client_certs_and_ca(cert_provider)


def update_client_certs_and_ca(cert_provider):
"""Get the CA, and client cert, key and then update the config."""
ca = cert_provider.root_ca_cert
chain = cert_provider.root_ca_chain
if ca and chain:
if ca not in chain:
ca = chain + ca
else:
ca = chain
cert = key = None
try:
client_cert = cert_provider.client_certs[0] # only requested one cert
cert = client_cert.cert
key = client_cert.key
except IndexError:
pass
with charm.provide_charm_instance() as the_charm:
print(f"updating: {ca}\n{cert}\n{key}")
if ca:
the_charm.configure_ca(ca)
if chain:
the_charm.configure_ca(chain, postfix="chain")
the_charm.handle_changed_client_cert_files(ca, cert, key)


@reactive.when('nrpe-external-master.available')
def configure_nrpe():
"""Config and install NRPE plugins."""
Expand Down
14 changes: 14 additions & 0 deletions src/templates/rocky/manila.conf
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ lock_path = /var/lib/manila/tmp
# parts/section-keystone-authtoken includes the [keystone_authtoken] section
# identifier
{% include "parts/section-keystone-authtoken" %}
{% if certificates -%}
# Certificates for https connections to keystone when tls-certificates is
# available
{# NOTE(ajkavanagh) 'certificates' is an optional relation and so we have to -#}
{# check for its existence before access the .parts. -#}
{% if certificates.certfile -%}
certfile = {{ certificates.certfile }}
keyfile = {{ certificates.keyfile }}
{% endif -%}
{% if certificates.cafile -%}
cafile = {{ certificates.cafile }}
insecure = false
{% endif -%}
{% endif %}

[oslo_messaging_amqp]

Expand Down
Loading

0 comments on commit 6bdf638

Please sign in to comment.