
* Specify minimum Juju version * Stop cluster count block being overwritted on setup * Fix cluster count in peer relation when relation is not ready * Move location of ops-openstack to openstack-charmers
332 lines
12 KiB
Python
Executable File
332 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import socket
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import string
|
|
import secrets
|
|
|
|
sys.path.append('lib')
|
|
|
|
from ops.framework import (
|
|
StoredState,
|
|
)
|
|
from ops.main import main
|
|
import ops.model
|
|
import charmhelpers.core.host as ch_host
|
|
import charmhelpers.core.templating as ch_templating
|
|
import interface_ceph_client
|
|
import interface_ceph_iscsi_peer
|
|
import interface_tls_certificates
|
|
|
|
import adapters
|
|
import ops_openstack
|
|
import gwcli_client
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class CephClientAdapter(adapters.OpenStackOperRelationAdapter):
|
|
|
|
def __init__(self, relation):
|
|
super(CephClientAdapter, self).__init__(relation)
|
|
|
|
@property
|
|
def mon_hosts(self):
|
|
hosts = self.relation.get_relation_data()['mon_hosts']
|
|
return ' '.join(sorted(hosts))
|
|
|
|
@property
|
|
def auth_supported(self):
|
|
return self.relation.get_relation_data()['auth']
|
|
|
|
@property
|
|
def key(self):
|
|
return self.relation.get_relation_data()['key']
|
|
|
|
|
|
class PeerAdapter(adapters.OpenStackOperRelationAdapter):
|
|
|
|
def __init__(self, relation):
|
|
super(PeerAdapter, self).__init__(relation)
|
|
|
|
|
|
class GatewayClientPeerAdapter(PeerAdapter):
|
|
|
|
def __init__(self, relation):
|
|
super(GatewayClientPeerAdapter, self).__init__(relation)
|
|
|
|
@property
|
|
def gw_hosts(self):
|
|
hosts = self.relation.peer_addresses
|
|
return ' '.join(sorted(hosts))
|
|
|
|
|
|
class TLSCertificatesAdapter(adapters.OpenStackOperRelationAdapter):
|
|
|
|
def __init__(self, relation):
|
|
super(TLSCertificatesAdapter, self).__init__(relation)
|
|
|
|
@property
|
|
def enable_tls(self):
|
|
return bool(self.relation.application_certs)
|
|
|
|
|
|
class CephISCSIGatewayAdapters(adapters.OpenStackRelationAdapters):
|
|
|
|
relation_adapters = {
|
|
'ceph-client': CephClientAdapter,
|
|
'cluster': GatewayClientPeerAdapter,
|
|
'certificates': TLSCertificatesAdapter,
|
|
}
|
|
|
|
|
|
class CephISCSIGatewayCharmBase(ops_openstack.OSBaseCharm):
|
|
|
|
state = StoredState()
|
|
PACKAGES = ['ceph-iscsi', 'tcmu-runner', 'ceph-common']
|
|
CEPH_CAPABILITIES = [
|
|
"osd", "allow *",
|
|
"mon", "allow *",
|
|
"mgr", "allow r"]
|
|
|
|
RESTART_MAP = {
|
|
'/etc/ceph/ceph.conf': ['rbd-target-api', 'rbd-target-gw'],
|
|
'/etc/ceph/iscsi-gateway.cfg': ['rbd-target-api'],
|
|
'/etc/ceph/ceph.client.ceph-iscsi.keyring': ['rbd-target-api']}
|
|
|
|
DEFAULT_TARGET = "iqn.2003-01.com.ubuntu.iscsi-gw:iscsi-igw"
|
|
REQUIRED_RELATIONS = ['ceph-client', 'cluster']
|
|
# Two has been tested before is probably fine too but needs
|
|
# validating
|
|
ALLOWED_UNIT_COUNTS = [2]
|
|
release = 'default'
|
|
|
|
def __init__(self, framework, key):
|
|
super().__init__(framework, key)
|
|
logging.info("Using {} class".format(self.release))
|
|
self.state.set_default(target_created=False)
|
|
self.state.set_default(enable_tls=False)
|
|
self.state.set_default(additional_trusted_ips=[])
|
|
self.ceph_client = interface_ceph_client.CephClientRequires(
|
|
self,
|
|
'ceph-client')
|
|
self.peers = interface_ceph_iscsi_peer.CephISCSIGatewayPeers(
|
|
self,
|
|
'cluster')
|
|
self.tls = interface_tls_certificates.TlsRequires(self, "certificates")
|
|
self.adapters = CephISCSIGatewayAdapters(
|
|
(self.ceph_client, self.peers, self.tls),
|
|
self)
|
|
self.framework.observe(self.on.ceph_client_relation_joined, self)
|
|
self.framework.observe(self.ceph_client.on.pools_available, self)
|
|
self.framework.observe(self.peers.on.has_peers, self)
|
|
self.framework.observe(self.peers.on.ready_peers, self)
|
|
self.framework.observe(self.on.create_target_action, self)
|
|
self.framework.observe(self.on.add_trusted_ip_action, self)
|
|
self.framework.observe(self.on.certificates_relation_joined, self)
|
|
self.framework.observe(self.on.certificates_relation_changed, self)
|
|
self.framework.observe(self.on.config_changed, self)
|
|
self.framework.observe(self.on.upgrade_charm, self)
|
|
|
|
def on_install(self, event):
|
|
if ch_host.is_container():
|
|
logging.info("Installing into a container is not supported")
|
|
self.update_status()
|
|
else:
|
|
self.install_pkgs()
|
|
|
|
def on_add_trusted_ip_action(self, event):
|
|
self.state.additional_trusted_ips.append(
|
|
event.params['ips'].split(' '))
|
|
logging.info(self.state.additional_trusted_ips)
|
|
|
|
def on_create_target_action(self, event):
|
|
gw_client = gwcli_client.GatewayClient()
|
|
target = event.params.get('iqn', self.DEFAULT_TARGET)
|
|
gateway_units = event.params.get(
|
|
'gateway-units',
|
|
[u for u in self.peers.ready_peer_details.keys()])
|
|
gw_client.create_target(target)
|
|
for gw_unit, gw_config in self.peers.ready_peer_details.items():
|
|
added_gateways = []
|
|
if gw_unit in gateway_units:
|
|
gw_client.add_gateway_to_target(
|
|
target,
|
|
gw_config['ip'],
|
|
gw_config['fqdn'])
|
|
added_gateways.append(gw_unit)
|
|
gw_client.create_pool(
|
|
event.params['pool-name'],
|
|
event.params['image-name'],
|
|
event.params['image-size'])
|
|
gw_client.add_client_to_target(
|
|
target,
|
|
event.params['client-initiatorname'])
|
|
gw_client.add_client_auth(
|
|
target,
|
|
event.params['client-initiatorname'],
|
|
event.params['client-username'],
|
|
event.params['client-password'])
|
|
gw_client.add_disk_to_client(
|
|
target,
|
|
event.params['client-initiatorname'],
|
|
event.params['pool-name'],
|
|
event.params['image-name'])
|
|
event.set_results({'iqn': target})
|
|
|
|
def setup_default_target(self):
|
|
gw_client = gwcli_client.GatewayClient()
|
|
gw_client.create_target(self.DEFAULT_TARGET)
|
|
for gw_unit, gw_config in self.peers.ready_peer_details.items():
|
|
gw_client.add_gateway_to_target(
|
|
self.DEFAULT_TARGET,
|
|
gw_config['ip'],
|
|
gw_config['fqdn'])
|
|
self.state.target_created = True
|
|
|
|
def on_ready_peers(self, event):
|
|
if not self.unit.is_leader():
|
|
logging.info("Leader should do setup")
|
|
return
|
|
if not self.state.is_started:
|
|
logging.info("Cannot perform setup yet, not started")
|
|
event.defer()
|
|
return
|
|
if self.state.target_created:
|
|
logging.info("Initial target setup already complete")
|
|
return
|
|
else:
|
|
# This appears to race and sometime runs before the
|
|
# peer is 100% ready. There is probably little value
|
|
# in this anyway so may just remove it.
|
|
# self.setup_default_target()
|
|
return
|
|
|
|
def on_has_peers(self, event):
|
|
logging.info("Unit has peers")
|
|
if self.unit.is_leader() and not self.peers.admin_password:
|
|
logging.info("Setting admin password")
|
|
alphabet = string.ascii_letters + string.digits
|
|
password = ''.join(secrets.choice(alphabet) for i in range(8))
|
|
self.peers.set_admin_password(password)
|
|
|
|
def on_ceph_client_relation_joined(self, event):
|
|
logging.info("Requesting replicated pool")
|
|
self.ceph_client.create_replicated_pool(
|
|
self.model.config['rbd-metadata-pool'])
|
|
logging.info("Requesting permissions")
|
|
self.ceph_client.request_ceph_permissions(
|
|
'ceph-iscsi',
|
|
self.CEPH_CAPABILITIES)
|
|
self.ceph_client.request_osd_settings({
|
|
'osd heartbeat grace': 20,
|
|
'osd heartbeat interval': 5})
|
|
|
|
def on_config_changed(self, event):
|
|
if self.state.is_started:
|
|
self.on_pools_available(event)
|
|
self.on_ceph_client_relation_joined(event)
|
|
|
|
def on_upgrade_charm(self, event):
|
|
if self.state.is_started:
|
|
self.on_pools_available(event)
|
|
self.on_ceph_client_relation_joined(event)
|
|
|
|
def on_pools_available(self, event):
|
|
logging.info("on_pools_available")
|
|
if not self.peers.admin_password:
|
|
logging.info("Defering setup")
|
|
event.defer()
|
|
return
|
|
|
|
def daemon_reload_and_restart(service_name):
|
|
subprocess.check_call(['systemctl', 'daemon-reload'])
|
|
subprocess.check_call(['systemctl', 'restart', service_name])
|
|
|
|
rfuncs = {
|
|
'rbd-target-api': daemon_reload_and_restart}
|
|
|
|
@ch_host.restart_on_change(self.RESTART_MAP, restart_functions=rfuncs)
|
|
def render_configs():
|
|
for config_file in self.RESTART_MAP.keys():
|
|
ch_templating.render(
|
|
os.path.basename(config_file),
|
|
config_file,
|
|
self.adapters)
|
|
logging.info("Rendering config")
|
|
render_configs()
|
|
logging.info("Setting started state")
|
|
self.peers.announce_ready()
|
|
self.state.is_started = True
|
|
self.update_status()
|
|
logging.info("on_pools_available: status updated")
|
|
|
|
def on_certificates_relation_joined(self, event):
|
|
addresses = set()
|
|
for binding_name in ['public', 'cluster']:
|
|
binding = self.model.get_binding(binding_name)
|
|
addresses.add(binding.network.ingress_address)
|
|
addresses.add(binding.network.bind_address)
|
|
sans = [str(s) for s in addresses]
|
|
sans.append(socket.gethostname())
|
|
self.tls.request_application_cert(socket.getfqdn(), sans)
|
|
|
|
def on_certificates_relation_changed(self, event):
|
|
app_certs = self.tls.application_certs
|
|
if not all([self.tls.root_ca_cert, app_certs]):
|
|
return
|
|
if self.tls.chain:
|
|
# Append chain file so that clients that trust the root CA will
|
|
# trust certs signed by an intermediate in the chain
|
|
ca_cert_data = self.tls.root_ca_cert + os.linesep + self.tls.chain
|
|
else:
|
|
ca_cert_data = self.tls.root_ca_cert
|
|
pem_data = app_certs['cert'] + os.linesep + app_certs['key']
|
|
tls_files = {
|
|
'/etc/ceph/iscsi-gateway.crt': app_certs['cert'],
|
|
'/etc/ceph/iscsi-gateway.key': app_certs['key'],
|
|
'/etc/ceph/iscsi-gateway.pem': pem_data,
|
|
'/usr/local/share/ca-certificates/vault_ca_cert.crt': ca_cert_data}
|
|
for tls_file, tls_data in sorted(tls_files.items()):
|
|
with open(tls_file, 'w') as f:
|
|
f.write(tls_data)
|
|
subprocess.check_call(['update-ca-certificates'])
|
|
cert_out = subprocess.check_output(
|
|
('openssl x509 -inform pem -in /etc/ceph/iscsi-gateway.pem '
|
|
'-pubkey -noout').split())
|
|
with open('/etc/ceph/iscsi-gateway-pub.key', 'w') as f:
|
|
f.write(cert_out.decode('UTF-8'))
|
|
self.state.enable_tls = True
|
|
self.on_pools_available(event)
|
|
|
|
def custom_status_check(self):
|
|
if ch_host.is_container():
|
|
self.unit.status = ops.model.BlockedStatus(
|
|
'Charm cannot be deployed into a container')
|
|
return False
|
|
if self.peers.unit_count not in self.ALLOWED_UNIT_COUNTS:
|
|
self.unit.status = ops.model.BlockedStatus(
|
|
'{} is an invalid unit count'.format(self.peers.unit_count))
|
|
return False
|
|
return True
|
|
|
|
|
|
@ops_openstack.charm_class
|
|
class CephISCSIGatewayCharmJewel(CephISCSIGatewayCharmBase):
|
|
|
|
state = StoredState()
|
|
release = 'jewel'
|
|
|
|
|
|
@ops_openstack.charm_class
|
|
class CephISCSIGatewayCharmOcto(CephISCSIGatewayCharmBase):
|
|
|
|
state = StoredState()
|
|
release = 'octopus'
|
|
|
|
if __name__ == '__main__':
|
|
main(ops_openstack.get_charm_class_for_release())
|