charm-ceph-radosgw/hooks/ceph_radosgw_context.py
Shunde Zhang 6f2a7540e8 Add a config option for virtual hosted bucket
func-test-pr: https://github.com/openstack-charmers/zaza-openstack-tests/pull/1187

Closes-Bug: #1871745
Change-Id: I295baab496d1eb95daaa8073d4119d01b90d0b38
2024-04-05 16:17:08 +11:00

356 lines
12 KiB
Python

# Copyright 2016 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 os
import re
import socket
import tempfile
import shutil
import multisite
from charmhelpers.contrib.openstack import context
from charmhelpers.contrib.hahelpers.cluster import (
determine_api_port,
determine_apache_port,
https,
)
from charmhelpers.core.host import (
cmp_pkgrevno,
arch,
)
from charmhelpers.core.hookenv import (
DEBUG,
WARNING,
ERROR,
config,
log,
related_units,
relation_get,
relation_ids,
unit_public_ip,
leader_get,
)
from charmhelpers.contrib.network.ip import (
format_ipv6_addr,
get_ipv6_addr,
)
from charmhelpers.contrib.storage.linux.ceph import CephConfContext
import utils
BEAST_FRONTEND = 'beast'
CIVETWEB_FRONTEND = 'civetweb'
SUPPORTED_FRONTENDS = (BEAST_FRONTEND, CIVETWEB_FRONTEND)
UNSUPPORTED_BEAST_ARCHS = ('s390x', 'riscv64')
class ApacheSSLContext(context.ApacheSSLContext):
interfaces = ['https']
service_namespace = 'ceph-radosgw'
def __call__(self):
self.external_ports = [utils.listen_port()]
ctx = super(ApacheSSLContext, self).__call__()
ctx['virtual_hosted_bucket_enabled'] = \
config('virtual-hosted-bucket-enabled')
return ctx
class HAProxyContext(context.HAProxyContext):
def __call__(self):
ctxt = super(HAProxyContext, self).__call__()
port = utils.listen_port()
service = 'cephradosgw-server'
# Apache ports
a_cephradosgw_api = determine_apache_port(port, singlenode_mode=True)
port_mapping = {
service: [port, a_cephradosgw_api]
}
ctxt['cephradosgw_bind_port'] = determine_api_port(
port,
singlenode_mode=True,
)
# for haproxy.conf
backend_options = {
service: [{
'option': 'httpchk GET /swift/healthcheck',
}]
}
ctxt['service_ports'] = port_mapping
ctxt['backend_options'] = backend_options
ctxt['https'] = https()
return ctxt
class IdentityServiceContext(context.IdentityServiceContext):
interfaces = ['identity-service']
def __call__(self):
ctxt = super(IdentityServiceContext, self).__call__()
if not ctxt:
return
if cmp_pkgrevno('radosgw', "10.2.0") >= 0:
ctxt['auth_keystone_v3_supported'] = True
if (not ctxt.get('admin_domain_id') and
float(ctxt.get('api_version', '2.0')) < 3):
ctxt.pop('admin_domain_id')
ctxt['auth_type'] = 'keystone'
if cmp_pkgrevno('radosgw', '15.0.0') < 0:
ctxt['keystone_revocation_parameter_supported'] = True
if cmp_pkgrevno('radosgw', "11.0.0") >= 0:
ctxt['user_roles'] = config('operator-roles')
ctxt['admin_roles'] = config('admin-roles')
else:
ctxt['user_roles'] = config('operator-roles')
if config('admin-roles'):
ctxt['user_roles'] += (',' + config('admin-roles'))
ctxt['cache_size'] = config('cache-size')
ctxt['namespace_tenants'] = leader_get('namespace_tenants') == 'True'
if self.context_complete(ctxt):
return ctxt
return {}
def ensure_host_resolvable_v6(hostname):
"""Ensure that we can resolve our hostname to an IPv6 address by adding it
to /etc/hosts if it is not already resolvable.
"""
try:
socket.getaddrinfo(hostname, None, socket.AF_INET6)
except socket.gaierror:
log("Host '%s' is not ipv6 resolvable - adding to /etc/hosts" %
hostname, level=DEBUG)
else:
log("Host '%s' appears to be ipv6 resolvable" % (hostname),
level=DEBUG)
return
# This must be the backend address used by haproxy
host_addr = get_ipv6_addr(exc_list=[config('vip')])[0]
dtmp = tempfile.mkdtemp()
try:
tmp_hosts = os.path.join(dtmp, 'hosts')
shutil.copy('/etc/hosts', tmp_hosts)
with open(tmp_hosts, 'a+') as fd:
lines = fd.readlines()
for line in lines:
key = r"^%s\s+" % (host_addr)
if re.search(key, line):
break
else:
fd.write("%s\t%s\n" % (host_addr, hostname))
os.rename(tmp_hosts, '/etc/hosts')
finally:
shutil.rmtree(dtmp)
def resolve_http_frontend():
"""Automatically determine the HTTP frontend configuration
Determines the best HTTP frontend configuration based
on the Ceph release in use and the architecture of the
machine being used.
:returns http frontend configuration to use.
:rtype: str
"""
octopus_or_later = cmp_pkgrevno('radosgw', '15.2.0') >= 0
pacific_or_later = cmp_pkgrevno('radosgw', '16.2.0') >= 0
if octopus_or_later:
# Pacific or later supports beast on all architectures
# but octopus does not support s390x or riscv64
if not pacific_or_later and arch() in UNSUPPORTED_BEAST_ARCHS:
return CIVETWEB_FRONTEND
else:
return BEAST_FRONTEND
return CIVETWEB_FRONTEND
def validate_http_frontend(frontend_config):
"""Validate HTTP frontend configuration
:param frontend_config: user provided config value
:type: str
:raises: ValueError if the provided config is not valid
"""
mimic_or_later = cmp_pkgrevno('radosgw', '13.2.0') >= 0
pacific_or_later = cmp_pkgrevno('radosgw', '16.2.0') >= 0
quincy_or_later = cmp_pkgrevno('radosgw', '17.0.0') >= 0
if frontend_config not in SUPPORTED_FRONTENDS:
e = ('Please provide either civetweb or beast for '
'http-frontend configuration')
log(e, level=ERROR)
raise ValueError(e)
if frontend_config == BEAST_FRONTEND:
if not mimic_or_later:
e = ('Use of the beast HTTP frontend requires Ceph '
'mimic or later.')
log(e, level=ERROR)
raise ValueError(e)
if not pacific_or_later and arch() in UNSUPPORTED_BEAST_ARCHS:
e = ('Use of the beast HTTP frontend on {} requires Ceph '
'pacific or later.'.format(arch()))
log(e, level=ERROR)
raise ValueError(e)
if frontend_config == CIVETWEB_FRONTEND and quincy_or_later:
e = 'Civetweb frontend is not supported after Ceph Pacific.'
log(e, level=ERROR)
raise ValueError(e)
class MonContext(context.CephContext):
interfaces = ['mon']
def __call__(self):
if not relation_ids(self.interfaces[0]):
return {}
host = socket.gethostname()
systemd_rgw = False
mon_hosts = []
auths = []
fsid = None
for rid in relation_ids(self.interfaces[0]):
for unit in related_units(rid):
if fsid is None:
fsid = relation_get('fsid', rid=rid, unit=unit)
_auth = relation_get('auth', rid=rid, unit=unit)
if _auth:
auths.append(_auth)
ceph_pub_addr = relation_get('ceph-public-address', rid=rid,
unit=unit)
unit_priv_addr = relation_get('private-address', rid=rid,
unit=unit)
ceph_addr = ceph_pub_addr or unit_priv_addr
ceph_addr = format_ipv6_addr(ceph_addr) or ceph_addr
if ceph_addr:
mon_hosts.append(ceph_addr)
if relation_get('rgw.{}_key'.format(host), rid=rid, unit=unit):
systemd_rgw = True
if len(set(auths)) != 1:
e = ("Inconsistent or absent auth returned by mon units. Setting "
"auth_supported to 'none'")
log(e, level=WARNING)
auth = 'none'
else:
auth = auths[0]
# /etc/init.d/radosgw mandates that a dns name is used for this
# parameter so ensure that address is resolvable
if config('prefer-ipv6'):
ensure_host_resolvable_v6(host)
port = determine_api_port(utils.listen_port(), singlenode_mode=True)
if config('prefer-ipv6'):
port = "[::]:%s" % (port)
http_frontend = config('http-frontend')
if not http_frontend:
http_frontend = resolve_http_frontend()
else:
validate_http_frontend(http_frontend)
mon_hosts.sort()
ctxt = {
'auth_supported': auth,
'mon_hosts': ' '.join(mon_hosts),
'hostname': host,
'old_auth': cmp_pkgrevno('radosgw', "0.51") < 0,
'systemd_rgw': systemd_rgw,
'use_syslog': str(config('use-syslog')).lower(),
'loglevel': config('loglevel'),
'port': port,
'ipv6': config('prefer-ipv6'),
'virtual_hosted_bucket_enabled':
config('virtual-hosted-bucket-enabled'),
# The public unit IP is only used in case the authentication is
# *Not* keystone - in which case it is used to make sure the
# storage endpoint returned by the built-in auth is the HAproxy
# (since it defaults to the port the service runs on, and that is
# not available externally). ~tribaal
'unit_public_ip': unit_public_ip(),
'fsid': fsid,
'rgw_swift_versioning': config('rgw-swift-versioning-enabled'),
'relaxed_s3_bucket_names': config('relaxed-s3-bucket-names'),
'frontend': http_frontend,
'behind_https_proxy': https(),
}
if config('virtual-hosted-bucket-enabled'):
if config('os-public-hostname'):
ctxt['public_hostname'] = config('os-public-hostname')
else:
log("When virtual_hosted_bucket_enabled is true, "
"os_public_hostname must have a value.", level=WARNING)
# NOTE(dosaboy): these sections must correspond to what is supported in
# the config template.
sections = ['global', 'client.radosgw.gateway']
user_provided = CephConfContext(permitted_sections=sections)()
user_provided = {k.replace('.', '_'): user_provided[k]
for k in user_provided}
ctxt.update(user_provided)
if self.context_complete(ctxt):
# Multi-site zone configuration is optional,
# so add after assessment
zone = config('zone')
zonegroup = config('zonegroup')
realm = config('realm')
log("config: zone {} zonegroup {} realm {}"
.format(zone, zonegroup, realm), level=DEBUG)
if zone in multisite.plain_list('zone'):
ctxt['rgw_zone'] = zone
if zonegroup in multisite.plain_list('zonegroup'):
ctxt['rgw_zonegroup'] = zonegroup
if realm in multisite.plain_list('realm'):
ctxt['rgw_realm'] = realm
return ctxt
return {}
def context_complete(self, ctxt):
"""Overridden here to ensure the context is actually complete.
We set `key` and `auth` to None here, by default, to ensure
that the context will always evaluate to incomplete until the
Ceph relation has actually sent these details; otherwise,
there is a potential race condition between the relation
appearing and the first unit actually setting this data on the
relation.
:param ctxt: The current context members
:type ctxt: Dict[str, ANY]
:returns: True if the context is complete
:rtype: bool
"""
if 'fsid' not in ctxt:
return False
return context.OSContextGenerator.context_complete(self, ctxt)