diff --git a/metadata.yaml b/metadata.yaml index fcbb1dd..0011610 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -24,7 +24,7 @@ requires: certificates: interface: tls-certificates loadbalancer: - interface: api-endpoints + interface: openstack-loadbalancer alertmanager-service: interface: http prometheus: diff --git a/requirements.txt b/requirements.txt index 03eb941..3b6834c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ git+https://opendev.org/openstack/charm-ops-openstack#egg=ops_openstack #git+https://opendev.org/openstack/charm-ops-interface-tls-certificates#egg=interface_tls_certificates git+https://github.com/gnuoy/ops-interface-tls-certificates@no-exception-for-inflight-request#egg=interface_tls_certificates git+https://github.com/openstack-charmers/ops-interface-ceph-iscsi-admin-access#egg=interface_ceph_iscsi_admin_access +git+https://github.com/openstack-charmers/ops-interface-openstack-loadbalancer#egg=interface_openstack_loadbalancer diff --git a/src/charm.py b/src/charm.py index 409c0ce..87bf3fc 100755 --- a/src/charm.py +++ b/src/charm.py @@ -18,6 +18,7 @@ from typing import List, Union, Tuple import base64 import interface_tls_certificates.ca_client as ca_client +import interface_openstack_loadbalancer.loadbalancer as ops_lb_interface import re import secrets import socket @@ -27,7 +28,6 @@ import tenacity import ops_openstack.plugins.classes import interface_ceph_iscsi_admin_access.admin_access as admin_access import interface_dashboard -import interface_api_endpoints import interface_grafana_dashboard import interface_http import interface_radosgw_user @@ -57,6 +57,7 @@ class CephDashboardCharm(ops_openstack.core.OSBaseCharm): TLS_CHARM_CA_CERT_PATH = TLS_CA_CERT_DIR / 'charm_config_juju_ca_cert.crt' TLS_PORT = 8443 DASH_DIR = Path('src/dashboards') + LB_SERVICE_NAME = "ceph-dashboard" class CharmCephOption(): """Manage a charm option to ceph command to manage that option""" @@ -175,7 +176,7 @@ class CephDashboardCharm(ops_openstack.core.OSBaseCharm): self._configure_dashboard) self.framework.observe( self.ca_client.on.ca_available, - self._on_ca_available) + self._configure_dashboard) self.framework.observe( self.ca_client.on.tls_server_config_ready, self._configure_dashboard) @@ -189,16 +190,9 @@ class CephDashboardCharm(ops_openstack.core.OSBaseCharm): self.framework.observe( self.on.delete_user_action, self._delete_user_action) - self.ingress = interface_api_endpoints.APIEndpointsRequires( + self.ingress = ops_lb_interface.OSLoadbalancerRequires( self, - 'loadbalancer', - { - 'endpoints': [{ - 'service-type': 'ceph-dashboard', - 'frontend-port': self.TLS_PORT, - 'backend-port': self.TLS_PORT, - 'backend-ip': self._get_bind_ip(), - 'check-type': 'httpd'}]}) + 'loadbalancer') self.grafana_dashboard = \ interface_grafana_dashboard.GrafanaDashboardProvides( self, @@ -218,8 +212,23 @@ class CephDashboardCharm(ops_openstack.core.OSBaseCharm): self.framework.observe( self.prometheus.on.http_ready, self._configure_dashboard) + self.framework.observe( + self.ingress.on.lb_relation_ready, + self._request_loadbalancer) + self.framework.observe( + self.ingress.on.lb_configured, + self._configure_dashboard) self._stored.set_default(is_started=False) + def _request_loadbalancer(self, _) -> None: + """Send request to create loadbalancer""" + self.ingress.request_loadbalancer( + self.LB_SERVICE_NAME, + self.TLS_PORT, + self.TLS_PORT, + self._get_bind_ip(), + 'httpd') + def _register_dashboards(self) -> None: """Register all dashboards with grafana""" for dash_file in self.DASH_DIR.glob("*.json"): @@ -273,9 +282,24 @@ class CephDashboardCharm(ops_openstack.core.OSBaseCharm): creds[0]['access_key'], creds[0]['secret_key']) - def _on_ca_available(self, _) -> None: + def request_certificates(self) -> None: """Request TLS certificates.""" + if not self.ca_client.is_joined: + logging.debug( + "Cannot request certificates, relation not present.") + return addresses = set() + if self.ingress.relations: + lb_response = self.ingress.get_frontend_data() + if lb_response: + lb_config = lb_response[self.LB_SERVICE_NAME] + addresses.update( + [i for d in lb_config.values() for i in d['ip']]) + else: + logging.debug( + ("Defering certificate request until loadbalancer has " + "responded.")) + return for binding_name in ['public']: binding = self.model.get_binding(binding_name) addresses.add(binding.network.ingress_address) @@ -390,6 +414,7 @@ class CephDashboardCharm(ops_openstack.core.OSBaseCharm): def _configure_dashboard(self, _) -> None: """Configure dashboard""" + self.request_certificates() if not self.mon.mons_ready: logging.info("Not configuring dashboard, mons not ready") return diff --git a/src/interface_api_endpoints.py b/src/interface_api_endpoints.py deleted file mode 100644 index 8908e44..0000000 --- a/src/interface_api_endpoints.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 - -import json - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object) - - -class EndpointDataEvent(EventBase): - pass - - -class APIEndpointsEvents(ObjectEvents): - ep_ready = EventSource(EndpointDataEvent) - - -class APIEndpointsRequires(Object): - - on = APIEndpointsEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name, config_dict): - super().__init__(charm, relation_name) - self.config_dict = config_dict - self.relation_name = relation_name - self.framework.observe( - charm.on[self.relation_name].relation_changed, - self._on_relation_changed) - - def _on_relation_changed(self, event): - """Handle the relation-changed event.""" - event.relation.data[self.model.unit]['endpoints'] = json.dumps( - self.config_dict['endpoints']) - - def update_config(self, config_dict): - """Allow for updates to relation.""" - self.config_dict = config_dict - relation = self.model.get_relation(self.relation_name) - if relation: - relation.data[self.model.unit]['endpoints'] = json.dumps( - self.config_dict['endpoints']) - - -class APIEndpointsProvides(Object): - - on = APIEndpointsEvents() - _stored = StoredState() - - def __init__(self, charm): - super().__init__(charm, "loadbalancer") - # Observe the relation-changed hook event and bind - # self.on_relation_changed() to handle the event. - self.framework.observe( - charm.on["loadbalancer"].relation_changed, - self._on_relation_changed) - self.charm = charm - - def _on_relation_changed(self, event): - """Handle a change to the loadbalancer relation.""" - self.on.ep_ready.emit() diff --git a/unit_tests/test_ceph_dashboard_charm.py b/unit_tests/test_ceph_dashboard_charm.py index 1332ea1..b58825f 100644 --- a/unit_tests/test_ceph_dashboard_charm.py +++ b/unit_tests/test_ceph_dashboard_charm.py @@ -15,6 +15,7 @@ # limitations under the License. import base64 +import json import unittest import sys @@ -385,6 +386,9 @@ class TestCephDashboardCharmBase(CharmTestCase): _gethostname.return_value = 'server1' cert_rel_id = self.harness.add_relation('certificates', 'vault') dash_rel_id = self.harness.add_relation('dashboard', 'ceph-mon') + lb_rel_id = self.harness.add_relation( + 'loadbalancer', + 'openstack-loadbalancer') self.harness.begin() self.harness.set_leader() self.harness.charm.TLS_CERT_PATH = mock_TLS_CERT_PATH @@ -401,6 +405,40 @@ class TestCephDashboardCharmBase(CharmTestCase): self.harness.add_relation_unit( cert_rel_id, 'vault/0') + self.harness.add_relation_unit( + lb_rel_id, + 'openstack-loadbalancer/0') + # If lb relation is present but has not responded then certs should + # not have been requested yet. + self.assertEqual( + self.harness.get_relation_data( + cert_rel_id, + 'ceph-dashboard/0'), + {}) + self.harness.update_relation_data( + lb_rel_id, + 'openstack-loadbalancer', + { + 'frontends': json.dumps( + { + 'ceph-dashboard': { + 'admin': { + 'ip': ['10.20.0.101'], + 'port': 8443, + 'protocol': 'http'}, + 'internal': { + 'ip': ['10.30.0.101'], + 'port': 8443, + 'protocol': 'http'}, + 'public': { + 'ip': ['10.10.0.101'], + 'port': 8443, + 'protocol': 'http'}}})}) + self.assertNotEqual( + self.harness.get_relation_data( + cert_rel_id, + 'ceph-dashboard/0'), + {}) self.harness.update_relation_data( cert_rel_id, 'vault/0', diff --git a/unit_tests/test_interface_api_endpoints.py b/unit_tests/test_interface_api_endpoints.py deleted file mode 100644 index 63e876b..0000000 --- a/unit_tests/test_interface_api_endpoints.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2021 Canonical Ltd. -# -# 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 copy -import json -import unittest -import sys -sys.path.append('lib') # noqa -sys.path.append('src') # noqa -from ops.testing import Harness -from ops.charm import CharmBase -import interface_api_endpoints - - -class TestAPIEndpointsRequires(unittest.TestCase): - - class MyCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.seen_events = [] - self.ingress = interface_api_endpoints.APIEndpointsRequires( - self, - 'loadbalancer', - { - 'endpoints': [{ - 'service-type': 'ceph-dashboard', - 'frontend-port': 8443, - 'backend-port': 8443, - 'backend-ip': '10.0.0.10', - 'check-type': 'httpd'}]}) - - def setUp(self): - super().setUp() - self.harness = Harness( - self.MyCharm, - meta=''' -name: my-charm -requires: - loadbalancer: - interface: api-endpoints -''' - ) - self.eps = [{ - 'service-type': 'ceph-dashboard', - 'frontend-port': 8443, - 'backend-port': 8443, - 'backend-ip': '10.0.0.10', - 'check-type': 'httpd'}] - - def add_loadbalancer_relation(self): - rel_id = self.harness.add_relation( - 'loadbalancer', - 'service-loadbalancer') - self.harness.add_relation_unit( - rel_id, - 'service-loadbalancer/0') - self.harness.update_relation_data( - rel_id, - 'service-loadbalancer/0', - {'ingress-address': '10.0.0.3'}) - return rel_id - - def test_init(self): - self.harness.begin() - self.assertEqual( - self.harness.charm.ingress.config_dict, - {'endpoints': self.eps}) - self.assertEqual( - self.harness.charm.ingress.relation_name, - 'loadbalancer') - - def test__on_relation_changed(self): - self.harness.begin() - rel_id = self.add_loadbalancer_relation() - rel_data = self.harness.get_relation_data( - rel_id, - 'my-charm/0') - self.assertEqual( - rel_data['endpoints'], - json.dumps(self.eps)) - - def test_update_config(self): - self.harness.begin() - rel_id = self.add_loadbalancer_relation() - new_eps = copy.deepcopy(self.eps) - new_eps.append({ - 'service-type': 'ceph-dashboard', - 'frontend-port': 9443, - 'backend-port': 9443, - 'backend-ip': '10.0.0.10', - 'check-type': 'https'}) - self.harness.charm.ingress.update_config( - {'endpoints': new_eps}) - rel_data = self.harness.get_relation_data( - rel_id, - 'my-charm/0') - self.assertEqual( - rel_data['endpoints'], - json.dumps(new_eps)) - - -class TestAPIEndpointsProvides(unittest.TestCase): - - class MyCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - self.seen_events = [] - self.api_eps = interface_api_endpoints.APIEndpointsProvides(self) - self.framework.observe( - self.api_eps.on.ep_ready, - self._log_event) - - def _log_event(self, event): - self.seen_events.append(type(event).__name__) - - def setUp(self): - super().setUp() - self.harness = Harness( - self.MyCharm, - meta=''' -name: my-charm -provides: - loadbalancer: - interface: api-endpoints -''' - ) - - def test_on_changed(self): - self.harness.begin() - # No MonReadyEvent as relation is absent - self.assertEqual( - self.harness.charm.seen_events, - []) - rel_id = self.harness.add_relation('loadbalancer', 'ceph-dashboard') - self.harness.add_relation_unit( - rel_id, - 'ceph-dashboard/0') - self.harness.update_relation_data( - rel_id, - 'ceph-dashboard/0', - {'ingress-address': '10.0.0.3'}) - self.assertEqual( - self.harness.charm.seen_events, - ['EndpointDataEvent'])