diff --git a/osci.yaml b/osci.yaml index a781be1..70d7d0a 100644 --- a/osci.yaml +++ b/osci.yaml @@ -2,8 +2,17 @@ templates: - charm-unit-jobs-py310 - charm-functional-jobs + check: + jobs: + - jammy-antelope-vault_manila-ganesha vars: needs_charm_build: true charm_build_name: manila-ganesha build_type: charmcraft charmcraft_channel: 2.1/stable + +- job: + name: jammy-antelope-vault_manila-ganesha + parent: func-target + vars: + tox_extra_args: '-- vault:jammy-antelope-vault' diff --git a/src/lib/charm/openstack/manila_ganesha.py b/src/lib/charm/openstack/manila_ganesha.py index 82eb653..92b16c7 100644 --- a/src/lib/charm/openstack/manila_ganesha.py +++ b/src/lib/charm/openstack/manila_ganesha.py @@ -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, @@ -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' @@ -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 = { @@ -160,6 +194,7 @@ class GaneshaCharmRelationAdapters( 'manila-ganesha': charms_openstack.adapters.OpenStackRelationAdapter, 'identity-service': KeystoneCredentialAdapter, 'shared_db': charms_openstack.adapters.DatabaseRelationAdapter, + 'certificates': TlsCertificatesAdapter, } @@ -222,6 +257,10 @@ class ManilaGaneshaCharm(charms_openstack.charm.HAOpenStackCharm, ('12', 'wallaby'), ('13', 'xena'), ('14', 'yoga'), + ('15', 'zed'), + ('16', 'antelope'), + ('17', 'bobcat'), + ('18', 'caracal'), ]), } @@ -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) diff --git a/src/reactive/manila_ganesha.py b/src/reactive/manila_ganesha.py index b022aa2..b65b017 100644 --- a/src/reactive/manila_ganesha.py +++ b/src/reactive/manila_ganesha.py @@ -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', ) @@ -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() @@ -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.""" diff --git a/src/templates/rocky/manila.conf b/src/templates/rocky/manila.conf index f23505e..6ed0335 100644 --- a/src/templates/rocky/manila.conf +++ b/src/templates/rocky/manila.conf @@ -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] diff --git a/src/tests/bundles/jammy-antelope-vault.yaml b/src/tests/bundles/jammy-antelope-vault.yaml new file mode 100644 index 0000000..6b9033b --- /dev/null +++ b/src/tests/bundles/jammy-antelope-vault.yaml @@ -0,0 +1,368 @@ +variables: + openstack-origin: &openstack-origin cloud:jammy-antelope + +local_overlay_enabled: True + +series: jammy + +comment: +- 'machines section to decide order of deployment. database sooner = faster' +machines: + '0': + constraints: mem=3072M + '1': + constraints: mem=3072M + '2': + constraints: mem=3072M + '3': + '4': + '5': + '6': + '7': + '8': + '9': + '10': + '11': + '12': + '13': + '14': + '15': + '16': + '17': + constraints: mem=8G + '18': + constraints: mem=8G + '19': + '20': + '21': + '22': + '23': + '24': + +services: + + manila-mysql-router: + charm: ch:mysql-router + channel: 8.0/edge + manila-ganesha-mysql-router: + charm: ch:mysql-router + channel: 8.0/edge + keystone-mysql-router: + charm: ch:mysql-router + channel: 8.0/edge + neutron-api-mysql-router: + charm: ch:mysql-router + channel: 8.0/edge + nova-cloud-controller-mysql-router: + charm: ch:mysql-router + channel: 8.0/edge + glance-mysql-router: + charm: ch:mysql-router + channel: 8.0/edge + placement-mysql-router: + charm: ch:mysql-router + channel: 8.0/edge + + mysql-innodb-cluster: + charm: ch:mysql-innodb-cluster + num_units: 3 + options: + source: *openstack-origin + to: + - '0' + - '1' + - '2' + channel: 8.0/edge + + manila-ganesha-az1: + num_units: 3 + charm: ../../../manila-ganesha_ubuntu-22.04-amd64.charm + options: + openstack-origin: *openstack-origin + to: + - '3' + - '4' + - '5' + + ceph-mon: + charm: ch:ceph-mon + num_units: 3 + options: + source: *openstack-origin + to: + - '6' + - '7' + - '8' + channel: quincy/edge + + ceph-osd: + charm: ch:ceph-osd + num_units: 3 + options: + source: *openstack-origin + storage: + osd-devices: 'cinder,10G' + to: + - '9' + - '10' + - '11' + channel: quincy/edge + + ceph-fs: + charm: ch:ceph-fs + num_units: 2 + options: + source: *openstack-origin + to: + - '12' + - '13' + channel: quincy/edge + + manila: + charm: ch:manila + num_units: 1 + options: + default-share-backend: cephfsnfs1 + share-protocols: NFS + openstack-origin: *openstack-origin + to: + - '14' + channel: 2023.1/edge + + nova-cloud-controller: + charm: ch:nova-cloud-controller + num_units: 1 + options: + network-manager: Neutron + openstack-origin: *openstack-origin + to: + - '15' + channel: 2023.1/edge + + placement: + charm: ch:placement + num_units: 1 + options: + openstack-origin: *openstack-origin + to: + - '16' + channel: 2023.1/edge + + nova-compute: + charm: ch:nova-compute + num_units: 2 + options: + config-flags: default_ephemeral_format=ext4 + enable-live-migration: true + enable-resize: true + migration-auth-type: ssh + openstack-origin: *openstack-origin + to: + - '17' + - '18' + channel: 2023.1/edge + + glance: + charm: ch:glance + num_units: 1 + options: + openstack-origin: *openstack-origin + to: + - '19' + channel: 2023.1/edge + + neutron-api: + charm: ch:neutron-api + num_units: 1 + options: + manage-neutron-plugin-legacy-mode: true + neutron-plugin: ovs + flat-network-providers: physnet1 + neutron-security-groups: true + openstack-origin: *openstack-origin + to: + - '20' + channel: 2023.1/edge + + neutron-openvswitch: + charm: ch:neutron-openvswitch + channel: 2023.1/edge + + neutron-gateway: + charm: ch:neutron-gateway + num_units: 1 + options: + bridge-mappings: physnet1:br-ex + openstack-origin: *openstack-origin + to: + - '21' + channel: 2023.1/edge + + rabbitmq-server: + charm: ch:rabbitmq-server + num_units: 1 + to: + - '22' + channel: 3.9/edge + + keystone: + charm: ch:keystone + num_units: 1 + options: + openstack-origin: *openstack-origin + to: + - '23' + channel: 2023.1/edge + + nrpe: + charm: ch:nrpe + channel: latest/edge + + vault: + charm: vault + channel: 1.8/edge + num_units: 1 + to: + - '24' + +relations: + + - - 'ceph-mon' + - 'ceph-osd' + + - - 'ceph-mon' + - 'ceph-fs' + + - - 'ceph-mon' + - 'manila-ganesha-az1' + + - - 'manila:shared-db' + - 'manila-mysql-router:shared-db' + - - 'manila-mysql-router:db-router' + - 'mysql-innodb-cluster:db-router' + + - - 'manila-ganesha-az1' + - 'rabbitmq-server' + + - - 'manila-ganesha-az1' + - 'keystone' + + - - 'manila' + - 'manila-ganesha-az1' + + - - 'manila-ganesha-az1:shared-db' + - 'manila-ganesha-mysql-router:shared-db' + - - 'manila-ganesha-mysql-router:db-router' + - 'mysql-innodb-cluster:db-router' + + - - 'manila' + - 'rabbitmq-server' + + - - 'manila' + - 'keystone' + + - - 'keystone:shared-db' + - 'keystone-mysql-router:shared-db' + - - 'keystone-mysql-router:db-router' + - 'mysql-innodb-cluster:db-router' + + - - 'neutron-api:shared-db' + - 'neutron-api-mysql-router:shared-db' + - - 'neutron-api-mysql-router:db-router' + - 'mysql-innodb-cluster:db-router' + + - - 'neutron-api:amqp' + - 'rabbitmq-server:amqp' + + - - 'neutron-api:neutron-api' + - 'nova-cloud-controller:neutron-api' + + - - 'placement:placement' + - 'nova-cloud-controller:placement' + + - - 'placement:amqp' + - 'rabbitmq-server:amqp' + + - - 'placement:shared-db' + - 'placement-mysql-router:shared-db' + - - 'placement-mysql-router:db-router' + - 'mysql-innodb-cluster:db-router' + + - - 'placement:identity-service' + - 'keystone:identity-service' + + - - 'neutron-api:neutron-plugin-api' + - 'neutron-gateway:neutron-plugin-api' + + - - 'neutron-api:identity-service' + - 'keystone:identity-service' + + - - 'nova-compute:neutron-plugin' + - 'neutron-openvswitch:neutron-plugin' + + - - 'nova-cloud-controller:shared-db' + - 'nova-cloud-controller-mysql-router:shared-db' + - - 'nova-cloud-controller-mysql-router:db-router' + - 'mysql-innodb-cluster:db-router' + + - - 'neutron-gateway:amqp' + - 'rabbitmq-server:amqp' + + - - 'nova-cloud-controller:amqp' + - 'rabbitmq-server:amqp' + + - - 'nova-compute:amqp' + - 'rabbitmq-server:amqp' + + - - 'neutron-openvswitch:amqp' + - 'rabbitmq-server:amqp' + + - - 'nova-cloud-controller:identity-service' + - 'keystone:identity-service' + + - - 'nova-cloud-controller:cloud-compute' + - 'nova-compute:cloud-compute' + + - - 'glance:identity-service' + - 'keystone:identity-service' + + - - 'glance:shared-db' + - 'glance-mysql-router:shared-db' + - - 'glance-mysql-router:db-router' + - 'mysql-innodb-cluster:db-router' + + - - 'glance:amqp' + - 'rabbitmq-server:amqp' + + - - 'nova-compute:image-service' + - 'glance:image-service' + + - - 'nova-cloud-controller:image-service' + - 'glance:image-service' + + - - 'nova-cloud-controller:quantum-network-service' + - 'neutron-gateway:quantum-network-service' + + - - 'manila-ganesha-az1:nrpe-external-master' + - 'nrpe:nrpe-external-master' + + - - 'keystone' + - 'vault' + + - - 'glance' + - 'vault' + + - - 'manila' + - 'vault' + + - - 'manila-ganesha-az1' + - 'vault' + + - - 'neutron-api' + - 'vault' + + - - 'nova-cloud-controller' + - 'vault' + + - - 'placement' + - 'vault' diff --git a/src/tests/tests.yaml b/src/tests/tests.yaml index a0c6891..4616e4e 100644 --- a/src/tests/tests.yaml +++ b/src/tests/tests.yaml @@ -10,10 +10,15 @@ dev_bundles: smoke_bundles: - jammy-yoga -target_deploy_status: {} +target_deploy_status: + vault: + workload-status: blocked + workload-status-message-prefix: Vault needs to be initialized tests: - zaza.openstack.charm_tests.manila_ganesha.tests.ManilaGaneshaTests + - vault: + - zaza.openstack.charm_tests.manila_ganesha.tests.ManilaGaneshaTests configure: - zaza.openstack.charm_tests.glance.setup.add_lts_image @@ -22,6 +27,14 @@ configure: - zaza.openstack.charm_tests.nova.setup.manage_ssh_key - zaza.openstack.charm_tests.keystone.setup.add_demo_user - zaza.openstack.charm_tests.manila_ganesha.setup.setup_ganesha_share_type + - vault: + - zaza.openstack.charm_tests.vault.setup.auto_initialize + - zaza.openstack.charm_tests.glance.setup.add_lts_image + - zaza.openstack.charm_tests.neutron.setup.basic_overcloud_network + - zaza.openstack.charm_tests.nova.setup.create_flavors + - zaza.openstack.charm_tests.nova.setup.manage_ssh_key + - zaza.openstack.charm_tests.keystone.setup.add_demo_user + - zaza.openstack.charm_tests.manila_ganesha.setup.setup_ganesha_share_type tests_options: force_deploy: diff --git a/unit_tests/test_lib_charm_openstack_manila_ganesha.py b/unit_tests/test_lib_charm_openstack_manila_ganesha.py index ee31caa..0fc7b27 100644 --- a/unit_tests/test_lib_charm_openstack_manila_ganesha.py +++ b/unit_tests/test_lib_charm_openstack_manila_ganesha.py @@ -26,6 +26,51 @@ def setUp(self): self.patch_release(manila_ganesha.ManilaGaneshaCharm.release) +class TestTlsCertificatesAdapter(Helper): + + def test__resolve_file_name(self): + self.patch('os.path.exists', name='os_path_exists') + self.os_path_exists.return_value = False + relation = mock.MagicMock() + a = manila_ganesha.TlsCertificatesAdapter(relation) + self.assertEqual(a._resolve_file_name('some-path'), None) + self.os_path_exists.return_value = True + self.assertEqual(a._resolve_file_name('some-path'), 'some-path') + + def test_certfile_property(self): + relation = mock.MagicMock() + a = manila_ganesha.TlsCertificatesAdapter(relation) + self.patch_object(a, + '_resolve_file_name', + name='mock_resolve_file_name') + self.mock_resolve_file_name.return_value = None + self.assertEqual(a.certfile, None) + self.mock_resolve_file_name.return_value = 'the-certfile' + self.assertEqual(a.certfile, 'the-certfile') + + def test_keyfile_property(self): + relation = mock.MagicMock() + a = manila_ganesha.TlsCertificatesAdapter(relation) + self.patch_object(a, + '_resolve_file_name', + name='mock_resolve_file_name') + self.mock_resolve_file_name.return_value = None + self.assertEqual(a.keyfile, None) + self.mock_resolve_file_name.return_value = 'the-keyfile' + self.assertEqual(a.certfile, 'the-keyfile') + + def test_cafile_property(self): + relation = mock.MagicMock() + a = manila_ganesha.TlsCertificatesAdapter(relation) + self.patch_object(a, + '_resolve_file_name', + name='mock_resolve_file_name') + self.mock_resolve_file_name.return_value = None + self.assertEqual(a.cafile, None) + self.mock_resolve_file_name.return_value = 'the-cafile' + self.assertEqual(a.certfile, 'the-cafile') + + class TestManilaGaneshaCharm(Helper): def test_request_ceph_permissions(self): @@ -63,3 +108,176 @@ def test_access_ip_with_vip(self): self.get_relation_ip.assert_called_once_with('tenant-storage') self.is_address_in_network.assert_called_once_with( '10.0.0.0/24', '10.0.0.10') + + def test_get_client_cert_cn_sans(self): + c = manila_ganesha.ManilaGaneshaCharm() + self.patch_object(manila_ganesha, 'network_get') + self.network_get.return_value = { + 'ingress-addresses': ['ip1', 'ip2', 'ip3'], + } + self.assertEqual(c.get_client_cert_cn_sans(), ('ip1', ['ip2', 'ip3'])) + self.network_get.assert_called_once_with('identity-service') + + def raises(*args, **kwargs): + raise Exception('bang!') + + self.network_get.side_effect = raises + self.patch_object(manila_ganesha, 'log') + self.assertEqual(c.get_client_cert_cn_sans(), (None, None)) + self.log.assert_called_once() + self.assertRegex(self.log.call_args.args[0], + r"^Getting ingress.*failed") + + def test_handle_changed_client_cert_files__none(self): + # test that calling with None on all values ensures not files + self.patch_object(manila_ganesha, 'mkdir', name='mock_mkdir') + self.patch_object(manila_ganesha, 'path_hash', name='mock_path_hash') + self.patch('os.path.exists', name='mock_os_path_exists') + self.patch_object(manila_ganesha, 'log', name='mock_log') + self.patch('os.remove', name='mock_os_remove') + self.patch_object(manila_ganesha, 'write_file', name='mock_write_file') + self.patch_object(manila_ganesha.relations, 'endpoint_from_flag', + name='mock_endpoint_from_flag') + c = manila_ganesha.ManilaGaneshaCharm() + self.patch_object(c, + 'render_with_interfaces', + name='mock_render_with_interfaces') + + # Set up test conditions. + self.mock_path_hash.return_value = None # no file changes at all + self.mock_os_path_exists.side_effect = [False, True, False] + + # call with all None. + c.handle_changed_client_cert_files(None, None, None) + + # validate that things got called + self.mock_os_remove.assert_called_once_with( + manila_ganesha.MANILA_CLIENT_CERT_FILE) + self.mock_write_file.assert_not_called() + self.mock_endpoint_from_flag.assert_not_called() + self.mock_render_with_interfaces.assert_not_called() + + def test_handle_changed_client_cert_files__none_os_remove_error(self): + # test that calling with None on all values ensures not files + self.patch_object(manila_ganesha, 'mkdir', name='mock_mkdir') + self.patch_object(manila_ganesha, 'path_hash', name='mock_path_hash') + self.patch('os.path.exists', name='mock_os_path_exists') + self.patch_object(manila_ganesha, 'log', name='mock_log') + self.patch('os.remove', name='mock_os_remove') + self.patch_object(manila_ganesha, 'write_file', name='mock_write_file') + self.patch_object(manila_ganesha.relations, 'endpoint_from_flag', + name='mock_endpoint_from_flag') + c = manila_ganesha.ManilaGaneshaCharm() + self.patch_object(c, + 'render_with_interfaces', + name='mock_render_with_interfaces') + + # Set up test conditions. + def raises(_path): + if _path == manila_ganesha.MANILA_CLIENT_CERT_FILE: + raise OSError('bang!') + + self.mock_path_hash.return_value = None # no file changes at all + self.mock_os_path_exists.side_effect = [True, True, False] + self.mock_os_remove.side_effect = raises + + # call with all None. + c.handle_changed_client_cert_files(None, None, None) + + # validate that things got called + self.mock_os_remove.assert_has_calls([ + mock.call(manila_ganesha.MANILA_CLIENT_CA_FILE), + mock.call(manila_ganesha.MANILA_CLIENT_CERT_FILE), + ]) + self.assertRegex(self.mock_log.call_args.args[0], + r"^Path " + manila_ganesha.MANILA_CLIENT_CERT_FILE + + r".*deleted") + self.mock_write_file.assert_not_called() + self.mock_endpoint_from_flag.assert_not_called() + self.mock_render_with_interfaces.assert_not_called() + + def test_handle_changed_client_cert_files__all(self): + # test that calling with None on all values ensures not files + self.patch_object(manila_ganesha, 'mkdir', name='mock_mkdir') + self.patch_object(manila_ganesha, 'path_hash', name='mock_path_hash') + self.patch('os.path.exists', name='mock_os_path_exists') + self.patch_object(manila_ganesha, 'log', name='mock_log') + self.patch('os.remove', name='mock_os_remove') + self.patch_object(manila_ganesha, 'write_file', name='mock_write_file') + self.patch_object(manila_ganesha.relations, 'endpoint_from_flag', + name='mock_endpoint_from_flag') + c = manila_ganesha.ManilaGaneshaCharm() + self.patch_object(c, + 'render_with_interfaces', + name='mock_render_with_interfaces') + + # Set up test conditions. + self.mock_path_hash.side_effect = [None, None, None, 'h1', 'h2', 'h3'] + self.mock_endpoint_from_flag.side_effect = [ + 'e1', 'e2', 'e3', 'e4', 'e5', 'e6'] + + # call with all None. + c.handle_changed_client_cert_files('ca', 'cert', 'key') + + # validate that things got called + self.mock_os_remove.assert_not_called() + self.mock_write_file.assert_has_calls([ + mock.call(manila_ganesha.MANILA_CLIENT_CA_FILE, b"ca", + owner=c.user, group=c.group, perms=0o640), + mock.call(manila_ganesha.MANILA_CLIENT_CERT_FILE, b"cert", + owner=c.user, group=c.group, perms=0o640), + mock.call(manila_ganesha.MANILA_CLIENT_KEY_FILE, b"key", + owner=c.user, group=c.group, perms=0o640), + ]) + self.mock_endpoint_from_flag.assert_has_calls([ + mock.call('ceph.available'), + mock.call('amqp.available'), + mock.call('manila-plugin.available'), + mock.call('shared-db.available'), + mock.call('identity-service.available'), + mock.call('certificates.available'), + ]) + self.mock_render_with_interfaces.assert_called_once_with( + ['e1', 'e2', 'e3', 'e4', 'e5', 'e6']) + + def test_handle_changed_client_cert_files__all_not_all_endpoints(self): + # test that calling with None on all values ensures not files + self.patch_object(manila_ganesha, 'mkdir', name='mock_mkdir') + self.patch_object(manila_ganesha, 'path_hash', name='mock_path_hash') + self.patch('os.path.exists', name='mock_os_path_exists') + self.patch_object(manila_ganesha, 'log', name='mock_log') + self.patch('os.remove', name='mock_os_remove') + self.patch_object(manila_ganesha, 'write_file', name='mock_write_file') + self.patch_object(manila_ganesha.relations, 'endpoint_from_flag', + name='mock_endpoint_from_flag') + c = manila_ganesha.ManilaGaneshaCharm() + self.patch_object(c, + 'render_with_interfaces', + name='mock_render_with_interfaces') + + # Set up test conditions. + self.mock_path_hash.side_effect = [None, None, None, 'h1', 'h2', 'h3'] + self.mock_endpoint_from_flag.side_effect = [ + 'e1', 'e2', 'e3', 'e4', None, 'e6'] + + # call with all None. + c.handle_changed_client_cert_files('ca', 'cert', 'key') + + # validate that things got called + self.mock_os_remove.assert_not_called() + self.mock_write_file.assert_has_calls([ + mock.call(manila_ganesha.MANILA_CLIENT_CA_FILE, b"ca", + owner=c.user, group=c.group, perms=0o640), + mock.call(manila_ganesha.MANILA_CLIENT_CERT_FILE, b"cert", + owner=c.user, group=c.group, perms=0o640), + mock.call(manila_ganesha.MANILA_CLIENT_KEY_FILE, b"key", + owner=c.user, group=c.group, perms=0o640), + ]) + self.mock_endpoint_from_flag.assert_has_calls([ + mock.call('ceph.available'), + mock.call('amqp.available'), + mock.call('manila-plugin.available'), + mock.call('shared-db.available'), + mock.call('identity-service.available'), + ]) + self.mock_render_with_interfaces.assert_not_called() diff --git a/unit_tests/test_manila_ganesha_handlers.py b/unit_tests/test_manila_ganesha_handlers.py index 72bc6e3..9fb5d5f 100644 --- a/unit_tests/test_manila_ganesha_handlers.py +++ b/unit_tests/test_manila_ganesha_handlers.py @@ -52,6 +52,9 @@ def test_hooks(self): 'configure_nrpe': ('nrpe-external-master.available',), 'update_ident_username': ('config.changed.service-user', 'identity-service.connected',), + 'install_root_ca_cert': ('certificates.ca.available',), + 'set_client_cert_request': ('certificates.available',), + 'update_client_cert': ('certificates.certs.available',), }, 'when_not': { 'ceph_connected': ('ganesha-pool-configured',),