360 lines
12 KiB
Python
360 lines
12 KiB
Python
import os
|
|
import re
|
|
import uuid
|
|
|
|
from charmhelpers.core.hookenv import (
|
|
config,
|
|
log,
|
|
relation_ids,
|
|
related_units,
|
|
relation_get,
|
|
unit_get,
|
|
service_name,
|
|
leader_get,
|
|
DEBUG,
|
|
status_set,
|
|
)
|
|
from charmhelpers.contrib.openstack.context import (
|
|
OSContextGenerator,
|
|
ApacheSSLContext as SSLContext,
|
|
IdentityServiceContext,
|
|
)
|
|
from charmhelpers.contrib.hahelpers.cluster import (
|
|
determine_api_port,
|
|
determine_apache_port,
|
|
https,
|
|
)
|
|
from charmhelpers.contrib.network.ip import (
|
|
format_ipv6_addr,
|
|
get_ipv6_addr,
|
|
)
|
|
from charmhelpers.contrib.openstack.utils import (
|
|
os_release,
|
|
CompareOpenStackReleases,
|
|
get_host_ip,
|
|
)
|
|
|
|
|
|
SWIFT_HASH_FILE = '/var/lib/juju/swift-hash-path.conf'
|
|
WWW_DIR = '/var/www/swift-rings'
|
|
|
|
|
|
class SwiftProxyCharmException(Exception):
|
|
pass
|
|
|
|
|
|
class HAProxyContext(OSContextGenerator):
|
|
interfaces = ['cluster']
|
|
|
|
def __call__(self):
|
|
"""Extends the main charmhelpers HAProxyContext with a port mapping
|
|
specific to this charm.
|
|
Also used to extend cinder.conf context with correct api_listening_port
|
|
"""
|
|
haproxy_port = config('bind-port')
|
|
api_port = determine_apache_port(config('bind-port'),
|
|
singlenode_mode=True)
|
|
|
|
healthcheck = [{
|
|
'option': 'httpchk GET /healthcheck',
|
|
'http-check': 'expect status 200',
|
|
}]
|
|
|
|
backend_options = {'swift_api': healthcheck}
|
|
|
|
ctxt = {
|
|
'service_ports': {'swift_api': [haproxy_port, api_port]},
|
|
'backend_options': backend_options,
|
|
'https': https()
|
|
}
|
|
return ctxt
|
|
|
|
|
|
class ApacheSSLContext(SSLContext):
|
|
interfaces = ['https']
|
|
service_namespace = 'swift'
|
|
|
|
# We make this a property so that we avoid import-time
|
|
# dependencies on config()
|
|
|
|
@property
|
|
def external_ports(self):
|
|
return [config('bind-port')]
|
|
|
|
|
|
class SwiftRingContext(OSContextGenerator):
|
|
|
|
def __call__(self):
|
|
allowed_hosts = []
|
|
for relid in relation_ids('swift-storage'):
|
|
for unit in related_units(relid):
|
|
host = relation_get('private-address', unit, relid)
|
|
if config('prefer-ipv6'):
|
|
host_ip = get_ipv6_addr(exc_list=[config('vip')])[0]
|
|
else:
|
|
host_ip = get_host_ip(host)
|
|
|
|
allowed_hosts.append(host_ip)
|
|
|
|
ctxt = {
|
|
'www_dir': WWW_DIR,
|
|
'allowed_hosts': allowed_hosts
|
|
}
|
|
return ctxt
|
|
|
|
|
|
class SwiftS3Context(OSContextGenerator):
|
|
|
|
def __call__(self):
|
|
ctxt = {}
|
|
if CompareOpenStackReleases(os_release('swift-common')) >= 'queens':
|
|
if config('region'):
|
|
ctxt['location'] = config('region')
|
|
else:
|
|
log("Region config not provided so location cannot be set - "
|
|
"required from S3 Sig V4 support to work correctly", DEBUG)
|
|
|
|
return ctxt
|
|
|
|
|
|
class SwiftIdentityContext(OSContextGenerator):
|
|
interfaces = ['identity-service']
|
|
|
|
def __call__(self):
|
|
bind_port = config('bind-port')
|
|
workers = config('workers')
|
|
if workers == 0:
|
|
import multiprocessing
|
|
workers = multiprocessing.cpu_count()
|
|
if config('prefer-ipv6'):
|
|
proxy_ip = ('[{}]'
|
|
.format(get_ipv6_addr(exc_list=[config('vip')])[0]))
|
|
memcached_ip = 'ip6-localhost'
|
|
else:
|
|
proxy_ip = get_host_ip(unit_get('private-address'))
|
|
memcached_ip = get_host_ip(unit_get('private-address'))
|
|
|
|
ctxt = {
|
|
'proxy_ip': proxy_ip,
|
|
'memcached_ip': memcached_ip,
|
|
'bind_port': determine_api_port(bind_port, singlenode_mode=True),
|
|
'workers': workers,
|
|
'operator_roles': config('operator-roles'),
|
|
'delay_auth_decision': config('delay-auth-decision'),
|
|
'node_timeout': config('node-timeout'),
|
|
'recoverable_node_timeout': config('recoverable-node-timeout'),
|
|
'log_headers': config('log-headers'),
|
|
'statsd_host': config('statsd-host'),
|
|
'statsd_port': config('statsd-port'),
|
|
'statsd_sample_rate': config('statsd-sample-rate'),
|
|
'static_large_object_segments': config(
|
|
'static-large-object-segments'),
|
|
'enable_multi_region': config('enable-multi-region'),
|
|
'read_affinity': get_read_affinity(),
|
|
'write_affinity': get_write_affinity(),
|
|
'write_affinity_node_count': get_write_affinity_node_count()
|
|
}
|
|
|
|
cmp_openstack = CompareOpenStackReleases(os_release('swift'))
|
|
if cmp_openstack < 'train':
|
|
# swauth is no longer supported for OpenStack Train and later
|
|
admin_key = leader_get('swauth-admin-key')
|
|
if admin_key is not None:
|
|
ctxt['swauth_admin_key'] = admin_key
|
|
|
|
if config('debug'):
|
|
ctxt['log_level'] = 'DEBUG'
|
|
else:
|
|
ctxt['log_level'] = 'INFO'
|
|
|
|
# Instead of duplicating code lets use charm-helpers to set signing_dir
|
|
# TODO(hopem): refactor this context handler to use charm-helpers
|
|
# code.
|
|
_ctxt = IdentityServiceContext(service='swift', service_user='swift')()
|
|
signing_dir = _ctxt.get('signing_dir')
|
|
if signing_dir:
|
|
ctxt['signing_dir'] = signing_dir
|
|
|
|
ctxt['ssl'] = False
|
|
|
|
auth_type = config('auth-type')
|
|
ctxt['auth_type'] = auth_type
|
|
|
|
auth_host = config('keystone-auth-host')
|
|
admin_user = config('keystone-admin-user')
|
|
admin_password = config('keystone-admin-user')
|
|
if (auth_type == 'keystone' and auth_host and
|
|
admin_user and admin_password):
|
|
log('Using user-specified Keystone configuration.')
|
|
ks_auth = {
|
|
'auth_type': 'keystone',
|
|
'auth_protocol': config('keystone-auth-protocol'),
|
|
'keystone_host': auth_host,
|
|
'auth_port': config('keystone-auth-port'),
|
|
'service_user': admin_user,
|
|
'service_password': admin_password,
|
|
'service_tenant': config('keystone-admin-tenant-name'),
|
|
}
|
|
ctxt.update(ks_auth)
|
|
|
|
# Sometime during the 20.08 development cycle, keystone changed from
|
|
# every unit setting relation data to just the leader. This means that
|
|
# the charm needs to data from the first relation that actually has
|
|
# data, or almalgamate the data from all the relations. For this charm,
|
|
# it merges from the relation ids available like the charms.reactive
|
|
# system does.
|
|
_keys = (('auth_protocol', 'auth_protocol', 'http'),
|
|
('service_protocol', 'service_protocol', 'http'),
|
|
('keystone_host', 'auth_host', None),
|
|
('service_host', 'service_host', None),
|
|
('auth_port', 'auth_port', None),
|
|
('service_user', 'service_username', None),
|
|
('service_password', 'service_password', None),
|
|
('service_tenant', 'service_tenant', None),
|
|
('service_port', 'service_port', None),
|
|
('api_version', 'api_version', '2'))
|
|
_keysv3 = (('admin_domain_id', 'admin_domain_id'),
|
|
('service_tenant_id', 'service_tenant_id'),
|
|
('admin_domain_name', 'service_domain'),
|
|
('admin_tenant_name', 'service_tenant'))
|
|
|
|
kvs = {}
|
|
relids = relation_ids('identity-service')
|
|
# if we have relids at all, then set the auth_type to keystone
|
|
if relids:
|
|
kvs['auth_type'] = 'keystone'
|
|
# merge the data from the related units
|
|
for (key, source, default) in _keys:
|
|
for relid in relids:
|
|
for unit in related_units(relid):
|
|
value = relation_get(source, unit, relid)
|
|
if value is not None:
|
|
kvs[key] = value
|
|
else:
|
|
kvs[key] = kvs.get(key, default)
|
|
# if the api is version 3, also merge the additional keys
|
|
if kvs.get('api_version', None) == '3':
|
|
for (key, source) in _keysv3:
|
|
for relid in relids:
|
|
for unit in related_units(relid):
|
|
value = relation_get(source, unit, relid)
|
|
if value is not None:
|
|
kvs[key] = value
|
|
|
|
# merge in the creds from the relation; which override the config
|
|
ctxt.update(kvs)
|
|
|
|
if config('prefer-ipv6'):
|
|
for key in ['keystone_host', 'service_host']:
|
|
host = ctxt.get(key)
|
|
if host:
|
|
ctxt[key] = format_ipv6_addr(host)
|
|
|
|
return ctxt
|
|
|
|
|
|
class MemcachedContext(OSContextGenerator):
|
|
|
|
def __call__(self):
|
|
ctxt = {}
|
|
if config('prefer-ipv6'):
|
|
ctxt['memcached_ip'] = 'ip6-localhost'
|
|
else:
|
|
ctxt['memcached_ip'] = get_host_ip(unit_get('private-address'))
|
|
|
|
return ctxt
|
|
|
|
|
|
def get_swift_hash():
|
|
if os.path.isfile(SWIFT_HASH_FILE):
|
|
with open(SWIFT_HASH_FILE, 'r') as hashfile:
|
|
swift_hash = hashfile.read().strip()
|
|
elif config('swift-hash'):
|
|
swift_hash = config('swift-hash')
|
|
with open(SWIFT_HASH_FILE, 'w') as hashfile:
|
|
hashfile.write(swift_hash)
|
|
else:
|
|
model_uuid = os.environ.get("JUJU_ENV_UUID",
|
|
os.environ.get("JUJU_MODEL_UUID"))
|
|
swift_hash = str(uuid.uuid3(uuid.UUID(model_uuid),
|
|
service_name()))
|
|
with open(SWIFT_HASH_FILE, 'w') as hashfile:
|
|
hashfile.write(swift_hash)
|
|
|
|
return swift_hash
|
|
|
|
|
|
def get_read_affinity():
|
|
""" Gets read-affinity config option (lp1815879)
|
|
|
|
Checks whether read-affinity config option is set correctly and if so
|
|
returns its value.
|
|
|
|
:returns: read-affinity config option
|
|
:rtype: str
|
|
:raises: SwiftProxyCharmException
|
|
"""
|
|
if config('read-affinity'):
|
|
read_affinity = config('read-affinity')
|
|
pattern = re.compile(r"^r\d+z?(\d+)?=\d+(,\s?r\d+z?(\d+)?=\d+)*$")
|
|
if not pattern.match(read_affinity):
|
|
msg = "'read-affinity' config option is malformed"
|
|
status_set('blocked', msg)
|
|
raise SwiftProxyCharmException(msg)
|
|
return read_affinity
|
|
else:
|
|
return None
|
|
|
|
|
|
def get_write_affinity():
|
|
""" Gets write-affinity config option (lp1815879)
|
|
|
|
Checks whether write-affinity config option is set correctly and if so
|
|
returns its value.
|
|
|
|
:returns: write-affinity config option
|
|
:rtype: str
|
|
:raises: SwiftProxyCharmException
|
|
"""
|
|
if config('write-affinity'):
|
|
write_affinity = config('write-affinity')
|
|
pattern = re.compile(r"^r\d+(,\s?r\d+)*$")
|
|
if not pattern.match(write_affinity):
|
|
msg = "'write-affinity' config option is malformed"
|
|
status_set('blocked', msg)
|
|
raise SwiftProxyCharmException(msg)
|
|
return write_affinity
|
|
else:
|
|
return None
|
|
|
|
|
|
def get_write_affinity_node_count():
|
|
""" Gets write-affinity-node-count config option (lp1815879)
|
|
|
|
Checks whether write-affinity-node-count config option is set correctly
|
|
and if so returns its value.
|
|
|
|
:returns: write-affinity-node-count config option
|
|
:rtype: str
|
|
:raises: SwiftProxyCharmException
|
|
"""
|
|
if config('write-affinity-node-count'):
|
|
write_affinity_node_count = config('write-affinity-node-count')
|
|
pattern = re.compile(r"^\d+(\s\*\sreplicas)?$")
|
|
if not pattern.match(write_affinity_node_count):
|
|
msg = "'write-affinity-node-count' config option is malformed"
|
|
status_set('blocked', msg)
|
|
raise SwiftProxyCharmException(msg)
|
|
return write_affinity_node_count
|
|
else:
|
|
return None
|
|
|
|
|
|
class SwiftHashContext(OSContextGenerator):
|
|
|
|
def __call__(self):
|
|
ctxt = {
|
|
'swift_hash': get_swift_hash()
|
|
}
|
|
return ctxt
|