diff --git a/metadata.yaml b/metadata.yaml index e75a35d..4edc3bd 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -23,6 +23,9 @@ requires: interface: ceph-client certificates: interface: tls-certificates +provides: + admin-access: + interface: ceph-iscsi-admin-access peers: cluster: interface: ceph-iscsi-peer diff --git a/osci.yaml b/osci.yaml new file mode 100644 index 0000000..d55a58f --- /dev/null +++ b/osci.yaml @@ -0,0 +1,29 @@ +- project: + templates: + - charm-unit-jobs + check: + jobs: + - ceph-iscsi-focal-octopus + - ceph-iscsi-focal-octopus-ec + vars: + needs_charm_build: true + charm_build_name: ceph-iscsi + build_type: charmcraft +- job: + name: ceph-iscsi-focal-octopus + parent: func-target + dependencies: + - osci-lint + - tox-py35 + - tox-py36 + - tox-py37 + - tox-py38 + vars: + tox_extra_args: focal +- job: + name: ceph-iscsi-focal-octopus-ec + parent: func-target + dependencies: &smoke-jobs + - ceph-iscsi-focal-octopus + vars: + tox_extra_args: focal-ec diff --git a/requirements.txt b/requirements.txt index 8cfee5a..160abfd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ git+https://github.com/canonical/operator.git#egg=ops git+https://opendev.org/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client 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/openstack-charmers/ops-interface-ceph-iscsi-admin-access#egg=interface_ceph_iscsi_admin_access diff --git a/src/charm.py b/src/charm.py index 84a4fa9..c5eb3fd 100755 --- a/src/charm.py +++ b/src/charm.py @@ -17,12 +17,12 @@ """Charm for deploying and maintaining the Ceph iSCSI service.""" import copy -import socket import logging import os import subprocess import sys import string +import socket import secrets from pathlib import Path @@ -36,6 +36,7 @@ import ops.model import charmhelpers.core.host as ch_host import charmhelpers.core.templating as ch_templating import interface_ceph_client.ceph_client as ceph_client +import interface_ceph_iscsi_admin_access.admin_access as admin_access import interface_ceph_iscsi_peer import interface_tls_certificates.ca_client as ca_client @@ -102,7 +103,20 @@ class GatewayClientPeerAdapter( """ ips = copy.deepcopy(self.allowed_ips) ips.extend(self.relation.peer_addresses) - return ' '.join(sorted(ips)) + return ','.join(sorted(ips)) + + +class AdminAccessAdapter( + ops_openstack.adapters.OpenStackOperRelationAdapter): + + @property + def trusted_ips(self): + """List of IP addresses permitted to use API. + + :returns: Ceph iSCSI clients + :rtype: str + """ + return ','.join(sorted(self.relation.client_addresses)) class TLSCertificatesAdapter( @@ -130,6 +144,7 @@ class CephISCSIGatewayAdapters( 'ceph-client': CephClientAdapter, 'cluster': GatewayClientPeerAdapter, 'certificates': TLSCertificatesAdapter, + 'admin-access': AdminAccessAdapter, } @@ -184,12 +199,19 @@ class CephISCSIGatewayCharmBase( self.peers = interface_ceph_iscsi_peer.CephISCSIGatewayPeers( self, 'cluster') + self.admin_access = \ + admin_access.CephISCSIAdminAccessProvides( + self, + 'admin-access') self.ca_client = ca_client.CAClient( self, 'certificates') self.adapters = CephISCSIGatewayAdapters( - (self.ceph_client, self.peers, self.ca_client), + (self.ceph_client, self.peers, self.ca_client, self.admin_access), self) + self.framework.observe( + self.admin_access.on.admin_access_request, + self.publish_admin_access_info) self.framework.observe( self.ceph_client.on.broker_available, self.request_ceph_pool) @@ -240,6 +262,7 @@ class CephISCSIGatewayCharmBase( alphabet = string.ascii_letters + string.digits password = ''.join(secrets.choice(alphabet) for i in range(8)) self.peers.set_admin_password(password) + self.publish_admin_access_info(event) def config_get(self, key): """Retrieve config option. @@ -274,7 +297,6 @@ class CephISCSIGatewayCharmBase( def request_ceph_pool(self, event): """Request pools from Ceph cluster.""" - print("request_ceph_pool") if not self.ceph_client.broker_available: logging.info("Cannot request ceph setup at this time") return @@ -440,8 +462,26 @@ class CephISCSIGatewayCharmBase( encoding=serialization.Encoding.PEM)) subprocess.check_call(['update-ca-certificates']) self._stored.enable_tls = True + # Endpoint has switch to TLS, need to inform users. + self.publish_admin_access_info(event) self.render_config(event) + def publish_admin_access_info(self, event): + """Publish creds and endpoint to related charms""" + if not self.peers.admin_password: + logging.info("Defering setup") + event.defer() + return + if self._stored.enable_tls: + scheme = 'https' + else: + scheme = 'http' + self.admin_access.publish_gateway( + socket.getfqdn(), + 'admin', + self.peers.admin_password, + scheme) + def custom_status_check(self): """Custom update status checks.""" if ch_host.is_container(): diff --git a/templates/iscsi-gateway.cfg b/templates/iscsi-gateway.cfg index 8900529..eb7d0e0 100644 --- a/templates/iscsi-gateway.cfg +++ b/templates/iscsi-gateway.cfg @@ -11,4 +11,8 @@ api_secure = {{ certificates.enable_tls }} api_user = admin api_password = {{ cluster.admin_password }} api_port = 5000 +{% if admin_access.trusted_ips -%} +trusted_ip_list = {{ cluster.trusted_ips }},{{ admin_access.trusted_ips }} +{% else -%} trusted_ip_list = {{ cluster.trusted_ips }} +{% endif -%} diff --git a/unit_tests/test_ceph_iscsi_charm.py b/unit_tests/test_ceph_iscsi_charm.py index 9d2a641..9a37bac 100644 --- a/unit_tests/test_ceph_iscsi_charm.py +++ b/unit_tests/test_ceph_iscsi_charm.py @@ -139,8 +139,10 @@ class TestCephISCSIGatewayCharmBase(CharmTestCase): PATCHES = [ 'ch_templating', 'gwcli_client', - 'subprocess', 'os', + 'secrets', + 'socket', + 'subprocess', ] def setUp(self): @@ -148,6 +150,12 @@ class TestCephISCSIGatewayCharmBase(CharmTestCase): self.harness = Harness( _CephISCSIGatewayCharmBase, ) + self.test_hostname = 'server1' + self.socket.gethostname.return_value = self.test_hostname + self.test_fqdn = self.test_hostname + '.foo' + self.socket.getfqdn.return_value = self.test_fqdn + self.secrets.choice.return_value = 'r' + self.test_admin_password = 'rrrrrrrr' self.gwc = MagicMock() self.gwcli_client.GatewayClient.return_value = self.gwc @@ -182,7 +190,7 @@ class TestCephISCSIGatewayCharmBase(CharmTestCase): self.assertFalse(self.harness.charm._stored.target_created) self.assertFalse(self.harness.charm._stored.enable_tls) - def add_cluster_relation(self): + def add_base_cluster_relation(self): rel_id = self.harness.add_relation('cluster', 'ceph-iscsi') self.harness.add_relation_unit( rel_id, @@ -197,10 +205,33 @@ class TestCephISCSIGatewayCharmBase(CharmTestCase): }) return rel_id + def complete_cluster_relation(self, rel_id): + self.harness.update_relation_data( + rel_id, + 'ceph-iscsi/1', + { + 'ingress-address': '10.0.0.2', + 'gateway_ready': 'True', + 'gateway_fqdn': 'ceph-iscsi-1.example' + }) + + def add_admin_access_relation(self): + rel_id = self.harness.add_relation('admin-access', '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.2', + }) + return rel_id + @patch('socket.getfqdn') def test_on_create_target_action(self, _getfqdn): _getfqdn.return_value = 'ceph-iscsi-0.example' - self.add_cluster_relation() + self.add_base_cluster_relation() self.harness.begin() action_event = MagicMock() action_event.params = { @@ -245,7 +276,7 @@ class TestCephISCSIGatewayCharmBase(CharmTestCase): @patch('socket.getfqdn') def test_on_create_target_action_ec(self, _getfqdn): _getfqdn.return_value = 'ceph-iscsi-0.example' - self.add_cluster_relation() + self.add_base_cluster_relation() self.harness.begin() action_event = MagicMock() action_event.params = { @@ -296,10 +327,8 @@ class TestCephISCSIGatewayCharmBase(CharmTestCase): 'iscsi-metapool', 'disk1') - @patch.object(charm.secrets, 'choice') - def test_on_has_peers(self, _choice): + def test_on_has_peers(self): rel_id = self.harness.add_relation('cluster', 'ceph-iscsi') - _choice.return_value = 'r' self.harness.begin() self.harness.add_relation_unit( rel_id, @@ -316,10 +345,10 @@ class TestCephISCSIGatewayCharmBase(CharmTestCase): 'gateway_fqdn': 'ceph-iscsi-1.example' }) self.assertEqual( - self.harness.charm.peers.admin_password, 'rrrrrrrr') + self.harness.charm.peers.admin_password, self.test_admin_password) def test_on_has_peers_not_leader(self): - self.add_cluster_relation() + self.add_base_cluster_relation() self.harness.begin() self.assertIsNone( self.harness.charm.peers.admin_password) @@ -329,7 +358,7 @@ class TestCephISCSIGatewayCharmBase(CharmTestCase): self.harness.charm.peers.admin_password) def test_on_has_peers_existing_password(self): - rel_id = self.add_cluster_relation() + rel_id = self.add_base_cluster_relation() self.harness.update_relation_data( rel_id, 'ceph-iscsi', @@ -370,7 +399,7 @@ class TestCephISCSIGatewayCharmBase(CharmTestCase): def test_on_pools_available(self): self.os.path.exists.return_value = False self.os.path.basename = os.path.basename - rel_id = self.add_cluster_relation() + rel_id = self.add_base_cluster_relation() self.harness.update_relation_data( rel_id, 'ceph-iscsi', @@ -392,9 +421,7 @@ class TestCephISCSIGatewayCharmBase(CharmTestCase): rel_data = self.harness.get_relation_data(rel_id, 'ceph-iscsi/0') self.assertEqual(rel_data['gateway_ready'], 'True') - @patch('socket.gethostname') - def test_on_certificates_relation_joined(self, _gethostname): - _gethostname.return_value = 'server1' + def test_on_certificates_relation_joined(self): rel_id = self.harness.add_relation('certificates', 'vault') self.harness.begin() self.harness.add_relation_unit( @@ -407,19 +434,17 @@ class TestCephISCSIGatewayCharmBase(CharmTestCase): rel_data = self.harness.get_relation_data(rel_id, 'ceph-iscsi/0') self.assertEqual( rel_data['application_cert_requests'], - '{"server1": {"sans": ["10.0.0.10", "server1"]}}') + '{"server1.foo": {"sans": ["10.0.0.10", "server1"]}}') - @patch('socket.gethostname') - def test_on_certificates_relation_changed(self, _gethostname): + def test_on_certificates_relation_changed(self): mock_TLS_CERT_PATH = MagicMock() mock_TLS_CA_CERT_PATH = MagicMock() mock_TLS_KEY_PATH = MagicMock() mock_KEY_AND_CERT_PATH = MagicMock() mock_TLS_PUB_KEY_PATH = MagicMock() - _gethostname.return_value = 'server1' self.subprocess.check_output.return_value = b'pubkey' rel_id = self.harness.add_relation('certificates', 'vault') - self.add_cluster_relation() + self.add_base_cluster_relation() self.harness.begin() self.harness.charm.TLS_CERT_PATH = mock_TLS_CERT_PATH self.harness.charm.TLS_CA_CERT_PATH = mock_TLS_CA_CERT_PATH @@ -460,3 +485,26 @@ class TestCephISCSIGatewayCharmBase(CharmTestCase): self.assertIsInstance( self.harness.charm.unit.status, BlockedStatus) + + def test_publish_admin_access_info(self): + cluster_rel_id = self.add_base_cluster_relation() + admin_access_rel_id = self.add_admin_access_relation() + self.harness.begin() + self.harness.set_leader() + self.complete_cluster_relation(cluster_rel_id) + self.assertEqual( + self.harness.get_relation_data( + admin_access_rel_id, + 'ceph-iscsi/0'), + { + 'host': '10.0.0.10', + 'name': self.test_fqdn, + 'port': '5000', + 'scheme': 'http'}) + self.assertEqual( + self.harness.get_relation_data( + admin_access_rel_id, + 'ceph-iscsi'), + { + 'password': self.test_admin_password, + 'username': 'admin'})