Files
charm-ceilometer/lib/ceilometer_utils.py
T
Liam Young e3982a2d98 Fix restart when endpoint notification is received
Restarts were configured only when the ceilometer agent endpoint
had changed and only alarm services were triggered to be restarted.
This change adds a check for the gnocchi service having changed
too and restarts all services to be on the safe side.

Closes-Bug: #1867924
Change-Id: I48e2f079e2db640d485bc74bfc2cedfd7e82ac84
2020-08-20 09:17:58 +00:00

711 lines
23 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 uuid
import subprocess
import traceback
from collections import OrderedDict
from charmhelpers.contrib.openstack import (
templating,
context,
)
from ceilometer_contexts import (
ApacheSSLContext,
LoggingConfigContext,
MongoDBContext,
CeilometerContext,
CeilometerPipelineContext,
HAProxyContext,
MetricServiceContext,
CEILOMETER_PORT,
RemoteSinksContext,
AMQPListenersContext,
)
from charmhelpers.contrib.openstack.utils import (
get_os_codename_package,
get_os_codename_install_source,
configure_installation_source,
os_release,
pause_unit,
resume_unit,
make_assess_status_func,
os_application_version_set,
token_cache_pkgs,
enable_memcache,
CompareOpenStackReleases,
reset_os_release,
)
from charmhelpers.core.hookenv import (
config,
is_leader,
leader_get,
leader_set,
log,
DEBUG,
relation_ids,
)
from charmhelpers.fetch import (
apt_update,
apt_install,
apt_upgrade,
apt_purge,
apt_autoremove,
filter_missing_packages,
)
from charmhelpers.core.host import init_is_systemd
from copy import deepcopy
HAPROXY_CONF = '/etc/haproxy/haproxy.cfg'
CEILOMETER_CONF_DIR = "/etc/ceilometer"
CEILOMETER_CONF = "%s/ceilometer.conf" % CEILOMETER_CONF_DIR
CEILOMETER_PIPELINE_YAML = "%s/pipeline.yaml" % CEILOMETER_CONF_DIR
POLLING_CONF = "%s/polling.yaml" % CEILOMETER_CONF_DIR
CEILOMETER_API_SYSTEMD_CONF = (
'/etc/systemd/system/ceilometer-api.service.d/override.conf'
)
HTTPS_APACHE_CONF = "/etc/apache2/sites-available/openstack_https_frontend"
HTTPS_APACHE_24_CONF = "/etc/apache2/sites-available/" \
"openstack_https_frontend.conf"
CLUSTER_RES = 'grp_ceilometer_vips'
MEMCACHED_CONF = '/etc/memcached.conf'
PIPELINE_CONF = '/etc/ceilometer/event_pipeline.yaml'
CEILOMETER_BASE_SERVICES = [
'ceilometer-agent-central',
'ceilometer-collector',
'ceilometer-api',
]
QUEENS_SERVICES = [
'ceilometer-agent-central',
'ceilometer-agent-notification'
]
ICEHOUSE_SERVICES = [
'ceilometer-alarm-notifier',
'ceilometer-alarm-evaluator',
'ceilometer-agent-notification'
]
MITAKA_SERVICES = [
'ceilometer-agent-notification'
]
CEILOMETER_DB = "ceilometer"
CEILOMETER_SERVICE = "ceilometer"
GNOCCHI_SERVICE = "gnocchi"
CEILOMETER_BASE_PACKAGES = [
'haproxy',
'apache2',
'ceilometer-agent-central',
'ceilometer-collector',
'ceilometer-api',
'python-pymongo',
]
PY3_PACKAGES = [
'python3-ceilometer',
]
ICEHOUSE_PACKAGES = [
'ceilometer-alarm-notifier',
'ceilometer-alarm-evaluator',
'ceilometer-agent-notification'
]
MITAKA_PACKAGES = [
'ceilometer-agent-notification'
]
QUEENS_PACKAGES = [
'ceilometer-agent-central',
'ceilometer-agent-notification'
]
REQUIRED_INTERFACES = {
'database': ['mongodb'],
'messaging': ['amqp'],
'identity': ['identity-service'],
}
CEILOMETER_ROLE = "ResellerAdmin"
SVC = 'ceilometer'
WSGI_CEILOMETER_API_CONF = '/etc/apache2/sites-enabled/wsgi-openstack-api.conf'
PACKAGE_CEILOMETER_API_CONF = '/etc/apache2/sites-enabled/ceilometer-api.conf'
QUEENS_CONFIG_FILES = OrderedDict([
(CEILOMETER_CONF, {
'hook_contexts': [
context.IdentityCredentialsContext(service=SVC,
service_user=SVC),
context.AMQPContext(ssl_dir=CEILOMETER_CONF_DIR),
LoggingConfigContext(),
MongoDBContext(),
CeilometerContext(),
context.SyslogContext(),
context.MemcacheContext(),
MetricServiceContext(),
context.WorkerConfigContext(),
AMQPListenersContext(ssl_dir=CEILOMETER_CONF_DIR)],
'services': QUEENS_SERVICES
}),
(POLLING_CONF, {
'hook_contexts': [
CeilometerContext()],
'services': QUEENS_SERVICES
}),
(PIPELINE_CONF, {
'hook_contexts': [RemoteSinksContext()],
'services': QUEENS_SERVICES,
}),
])
CONFIG_FILES = OrderedDict([
(CEILOMETER_CONF, {
'hook_contexts': [context.IdentityServiceContext(service=SVC,
service_user=SVC),
context.AMQPContext(ssl_dir=CEILOMETER_CONF_DIR),
context.InternalEndpointContext(),
LoggingConfigContext(),
MongoDBContext(),
CeilometerContext(),
context.SyslogContext(),
HAProxyContext(),
context.MemcacheContext(),
MetricServiceContext(),
context.WorkerConfigContext()],
'services': CEILOMETER_BASE_SERVICES
}),
(CEILOMETER_API_SYSTEMD_CONF, {
'hook_contexts': [HAProxyContext()],
'services': ['ceilometer-api'],
}),
(CEILOMETER_PIPELINE_YAML, {
'hook_contexts': [CeilometerPipelineContext()],
'services': ['ceilometer-collector'],
}),
(HAPROXY_CONF, {
'hook_contexts': [context.HAProxyContext(singlenode_mode=True),
HAProxyContext()],
'services': ['haproxy'],
}),
(HTTPS_APACHE_CONF, {
'hook_contexts': [ApacheSSLContext()],
# Include ceilometer-api to fix Bug #1632287 This is a temporary
# tactival fix as the charm will be rewritten to use mod_wsgi next
# cycle
'services': ['ceilometer-api', 'apache2'],
}),
(HTTPS_APACHE_24_CONF, {
'hook_contexts': [ApacheSSLContext()],
'services': ['ceilometer-api', 'apache2'],
}),
(PIPELINE_CONF, {
'hook_contexts': [RemoteSinksContext()],
'services': ['ceilometer-collector'],
}),
])
TEMPLATES = 'templates'
SHARED_SECRET = "/etc/ceilometer/secret.txt"
VERSION_PACKAGE = 'ceilometer-common'
def register_configs():
"""
Register config files with their respective contexts.
Regstration of some configs may not be required depending on
existing of certain relations.
"""
# if called without anything installed (eg during install hook)
# just default to earliest supported release. configs dont get touched
# till post-install, anyway.
release = get_os_codename_package('ceilometer-common', fatal=False)
configs = templating.OSConfigRenderer(templates_dir=TEMPLATES,
openstack_release=release)
if not release:
log("Not installed yet, no way to determine the OS release. "
"Skipping register configs", DEBUG)
return configs
if CompareOpenStackReleases(release) >= 'queens':
for conf in QUEENS_CONFIG_FILES:
configs.register(conf, QUEENS_CONFIG_FILES[conf]['hook_contexts'])
else:
for conf in (CEILOMETER_CONF, HAPROXY_CONF):
configs.register(conf, CONFIG_FILES[conf]['hook_contexts'])
if init_is_systemd():
configs.register(
CEILOMETER_API_SYSTEMD_CONF,
CONFIG_FILES[CEILOMETER_API_SYSTEMD_CONF]['hook_contexts']
)
if os.path.exists('/etc/apache2/conf-available'):
configs.register(
HTTPS_APACHE_24_CONF,
CONFIG_FILES[HTTPS_APACHE_24_CONF]['hook_contexts']
)
else:
configs.register(
HTTPS_APACHE_CONF,
CONFIG_FILES[HTTPS_APACHE_CONF]['hook_contexts']
)
if enable_memcache(release=release):
configs.register(MEMCACHED_CONF, [context.MemcacheContext()])
if run_in_apache():
wsgi_script = "/usr/share/ceilometer/app.wsgi"
configs.register(
WSGI_CEILOMETER_API_CONF,
[context.WSGIWorkerConfigContext(name="ceilometer",
script=wsgi_script),
CeilometerContext(),
HAProxyContext()]
)
if CompareOpenStackReleases(release) >= 'mitaka':
conf = PIPELINE_CONF
configs.register(conf, CONFIG_FILES[conf]['hook_contexts'])
return configs
def restart_map():
"""
Determine the correct resource map to be passed to
charmhelpers.core.restart_on_change() based on the services configured.
:returns: dict: A dictionary mapping config file to lists of services
that should be restarted when file changes.
"""
cmp_codename = CompareOpenStackReleases(
get_os_codename_install_source(config('openstack-origin')))
if cmp_codename >= 'queens':
_config_files = QUEENS_CONFIG_FILES
else:
_config_files = CONFIG_FILES
_map = {}
for f, ctxt in _config_files.items():
svcs = []
for svc in ctxt['services']:
svcs.append(svc)
if f == CEILOMETER_CONF:
for svc in ceilometer_release_services():
svcs.append(svc)
if svcs:
_map[f] = svcs
if (cmp_codename < 'queens' and
enable_memcache(source=config('openstack-origin'))):
_map[MEMCACHED_CONF] = ['memcached']
if cmp_codename < 'queens' and run_in_apache():
for cfile in _map:
svcs = _map[cfile]
if 'ceilometer-api' in svcs:
svcs.remove('ceilometer-api')
if 'apache2' not in svcs:
svcs.append('apache2')
_map['WSGI_CEILOMETER_API_CONF'] = ['apache2']
return _map
def services():
""" Returns a list of services associate with this charm """
_services = []
for v in restart_map().values():
_services = _services + v
_services = set(_services)
# Bug#1664898 Remove check for ceilometer-agent-central
# service.
cmp_codename = CompareOpenStackReleases(
get_os_codename_install_source(config('openstack-origin')))
if cmp_codename >= 'liberty' and 'ceilometer-agent-central' in _services:
_services.remove('ceilometer-agent-central')
return list(_services)
def determine_ports():
"""Assemble a list of API ports for services the charm is managing
@returns [ports] - list of ports that the charm manages.
"""
# TODO(ajkavanagh) - determine what other ports the service listens on
# apart from the main CEILOMETER port
cmp_codename = CompareOpenStackReleases(
get_os_codename_install_source(config('openstack-origin')))
if cmp_codename >= 'queens':
# NOTE(jamespage): No API service for queens or later
return []
return [CEILOMETER_PORT]
def get_ceilometer_context():
""" Retrieve a map of all current relation data for agent configuration """
cmp_codename = CompareOpenStackReleases(
get_os_codename_install_source(config('openstack-origin')))
if cmp_codename >= 'queens':
_config_files = QUEENS_CONFIG_FILES
else:
_config_files = CONFIG_FILES
ctxt = {}
for hcontext in _config_files[CEILOMETER_CONF]['hook_contexts']:
ctxt.update(hcontext())
return ctxt
def do_openstack_upgrade(configs):
"""
Perform an upgrade. Takes care of upgrading packages, rewriting
configs, database migrations and potentially any other post-upgrade
actions.
:param configs: The charms main OSConfigRenderer object.
"""
new_src = config('openstack-origin')
new_os_rel = get_os_codename_install_source(new_src)
log('Performing OpenStack upgrade to %s.' % (new_os_rel))
configure_installation_source(new_src)
dpkg_opts = [
'--option', 'Dpkg::Options::=--force-confnew',
'--option', 'Dpkg::Options::=--force-confdef',
]
apt_update(fatal=True)
apt_upgrade(options=dpkg_opts, fatal=True, dist=True)
reset_os_release()
apt_install(packages=get_packages(),
options=dpkg_opts,
fatal=True)
remove_old_packages()
# set CONFIGS to load templates from new release
configs.set_release(openstack_release=new_os_rel)
if run_in_apache():
disable_package_apache_site()
def ceilometer_release_services():
cmp_codename = CompareOpenStackReleases(
get_os_codename_install_source(config('openstack-origin')))
if cmp_codename >= 'mitaka' and cmp_codename < 'queens':
return MITAKA_SERVICES
elif cmp_codename >= 'icehouse' and cmp_codename < 'mitaka':
return ICEHOUSE_SERVICES
else:
return []
def ceilometer_release_packages():
cmp_codename = CompareOpenStackReleases(
get_os_codename_install_source(config('openstack-origin')))
if cmp_codename >= 'mitaka' and cmp_codename < 'queens':
return MITAKA_PACKAGES
elif cmp_codename >= 'icehouse':
return ICEHOUSE_PACKAGES
else:
return []
def get_packages():
cmp_codename = CompareOpenStackReleases(
get_os_codename_install_source(config('openstack-origin')))
# NOTE(jamespage): @queens ceilometer has no API service, so
# no requirement for token caching.
if cmp_codename >= 'queens':
packages = deepcopy(QUEENS_PACKAGES)
if cmp_codename >= 'rocky':
packages.extend(PY3_PACKAGES)
else:
packages = (deepcopy(CEILOMETER_BASE_PACKAGES) +
ceilometer_release_packages())
packages.extend(token_cache_pkgs(source=config('openstack-origin')))
return packages
def determine_purge_packages():
'''
Determine list of packages that where previously installed which are no
longer needed.
:returns: list of package names
'''
cmp_codename = CompareOpenStackReleases(
get_os_codename_install_source(config('openstack-origin')))
if cmp_codename >= 'rocky':
pkgs = [p for p in CEILOMETER_BASE_PACKAGES if p.startswith('python-')]
pkgs.append('python-ceilometer')
pkgs.append('python-memcache')
return pkgs
return []
def remove_old_packages():
'''Purge any packages that need ot be removed.
:returns: bool Whether packages were removed.
'''
installed_packages = filter_missing_packages(determine_purge_packages())
if installed_packages:
apt_purge(installed_packages, fatal=True)
apt_autoremove(purge=True, fatal=True)
return bool(installed_packages)
def get_shared_secret():
"""
Returns the current shared secret for the ceilometer node. If the shared
secret does not exist, this method will generate one.
"""
secret = None
if not os.path.exists(SHARED_SECRET):
secret = str(uuid.uuid4())
set_shared_secret(secret)
else:
with open(SHARED_SECRET, 'rt') as secret_file:
secret = secret_file.read().strip()
return secret
def set_shared_secret(secret):
"""
Sets the shared secret which is used to sign ceilometer messages.
:param secret: the secret to set
"""
with open(SHARED_SECRET, 'wt') as secret_file:
secret_file.write(secret)
def get_optional_relations():
"""Return a dictionary of optional relations.
@returns {relation: relation_name}
"""
optional_interfaces = {}
if relation_ids('event-service'):
optional_interfaces['event-service'] = ['event-service']
return optional_interfaces
def assess_status(configs):
"""Assess status of current unit
Decides what the state of the unit should be based on the current
configuration.
SIDE EFFECT: calls set_os_workload_status(...) which sets the workload
status of the unit.
Also calls status_set(...) directly if paused state isn't complete.
@param configs: a templating.OSConfigRenderer() object
@returns None - this function is executed for its side-effect
"""
assess_status_func(configs)()
os_application_version_set(VERSION_PACKAGE)
def resolve_required_interfaces():
"""Helper function to build a map of required interfaces based on the
OpenStack release being deployed.
@returns dict - a dictionary keyed by high-level type of interfaces names
"""
required_ints = deepcopy(REQUIRED_INTERFACES)
required_ints.update(get_optional_relations())
if CompareOpenStackReleases(os_release('ceilometer-common')) >= 'mitaka':
required_ints['database'].append('metric-service')
if CompareOpenStackReleases(os_release('ceilometer-common')) >= 'queens':
required_ints['database'].remove('mongodb')
required_ints['identity'] = ['identity-credentials']
return required_ints
def assess_status_func(configs):
"""Helper function to create the function that will assess_status() for
the unit.
Uses charmhelpers.contrib.openstack.utils.make_assess_status_func() to
create the appropriate status function and then returns it.
Used directly by assess_status() and also for pausing and resuming
the unit.
@param configs: a templating.OSConfigRenderer() object
@return f() -> None : a function that assesses the unit's workload status
"""
return make_assess_status_func(
configs, resolve_required_interfaces(),
charm_func=check_ceilometer_upgraded,
services=services(), ports=determine_ports())
def pause_unit_helper(configs):
"""Helper function to pause a unit, and then call assess_status(...) in
effect, so that the status is correctly updated.
Uses charmhelpers.contrib.openstack.utils.pause_unit() to do the work.
@param configs: a templating.OSConfigRenderer() object
@returns None - this function is executed for its side-effect
"""
_pause_resume_helper(pause_unit, configs)
def resume_unit_helper(configs):
"""Helper function to resume a unit, and then call assess_status(...) in
effect, so that the status is correctly updated.
Uses charmhelpers.contrib.openstack.utils.resume_unit() to do the work.
@param configs: a templating.OSConfigRenderer() object
@returns None - this function is executed for its side-effect
"""
_pause_resume_helper(resume_unit, configs)
def _pause_resume_helper(f, configs):
"""Helper function that uses the make_assess_status_func(...) from
charmhelpers.contrib.openstack.utils to create an assess_status(...)
function that can be used with the pause/resume of the unit
@param f: the function to be used with the assess_status(...) function
@returns None - this function is executed for its side-effect
"""
# TODO(ajkavanagh) - ports= has been left off because of the race hazard
# that exists due to service_start()
f(assess_status_func(configs),
services=services(),
ports=determine_ports())
# NOTE(jamespage): Drop once charm switches to apache+mod_wsgi.
def reload_systemd():
"""Reload systemd configuration on systemd based installs
"""
if init_is_systemd():
subprocess.check_call(['systemctl', 'daemon-reload'])
def run_in_apache():
"""Return true if ceilometer API is run under apache2 with mod_wsgi in
this release.
"""
os_cmp = CompareOpenStackReleases(os_release('ceilometer-common'))
return (os_cmp >= 'ocata' and os_cmp < 'queens')
def disable_package_apache_site():
"""Ensure that the package-provided apache configuration is disabled to
prevent it from conflicting with the charm-provided version.
"""
if os.path.exists(PACKAGE_CEILOMETER_API_CONF):
subprocess.check_call(['a2dissite', 'ceilometer-api'])
class FailedAction(Exception):
"""
A custom error to inform the caller that the action has failed.
Provides message, output and traceback.
"""
def __init__(self, message, outcome=None, trace=None):
self.outcome = outcome
self.trace = trace
super(FailedAction, self).__init__(message)
def ceilometer_upgrade_helper(CONFIGS):
"""Helper function to run ceilometer-upgrade, and then call
assess_status(...) in effect, so that the status is correctly updated.
Uses ceilometer_upgrade to do the work.
@param configs: a templating.OSConfigRenderer() object
@returns None - this function is executed for its side-effect
"""
cmp_codename = CompareOpenStackReleases(
get_os_codename_install_source(config('openstack-origin')))
if cmp_codename < 'queens':
identity_relation = 'identity-service'
else:
identity_relation = 'identity-credentials'
# NOTE(jamespage): ceilometer@ocata requires both gnocchi
# and mongodb to be configured to successfully
# upgrade the underlying data stores.
if ('metric-service' not in CONFIGS.complete_contexts() or
identity_relation not in CONFIGS.complete_contexts()):
raise FailedAction('The {} and or metric-service relations are not '
'complete. ceilometer-upgrade cannot be run until '
'they are ready.'.format(identity_relation))
# NOTE(jamespage): however at queens, this limitation has gone!
if (cmp_codename < 'pike' and
'mongodb' not in CONFIGS.complete_contexts()):
raise FailedAction('This version of ceilometer requires both gnocchi '
'and mongodb. Mongodb relation incomplete.')
try:
ceilometer_upgrade(action=True)
except subprocess.CalledProcessError as e:
msg = '{}'.format(e)
raise FailedAction('ceilometer-upgrade resulted in an '
'unexpected error: {}'.format(msg),
outcome='ceilometer-upgrade failed, see traceback.',
trace=traceback.format_exc())
def ceilometer_upgrade(action=False):
"""Execute ceilometer-upgrade command, with retry on failure if gnocchi
API is not ready for requests"""
if is_leader() or action:
if (CompareOpenStackReleases(os_release('ceilometer-common')) >=
'queens'):
cmd = ['ceilometer-upgrade', '--debug', '--retry', '10']
elif (CompareOpenStackReleases(os_release('ceilometer-common')) >=
'newton'):
cmd = ['ceilometer-upgrade', '--debug']
else:
cmd = ['ceilometer-dbsync']
log("Running ceilometer-upgrade: {}".format(" ".join(cmd)), DEBUG)
subprocess.check_call(cmd)
log("ceilometer-upgrade succeeded", DEBUG)
leader_set(ceilometer_upgrade_run=True)
def check_ceilometer_upgraded(configs):
"""Assess status check if ceilometer-upgrade action has run
When related to gnocchi check that the ceilometer-upgrade action has been
run. Set blocked when action run is still required. Set None None when no
action is required.
:param configs: The charms main OSConfigRenderer object.
:return: str, str tuple or None, None
"""
if (relation_ids("metric-service") and not
leader_get("ceilometer_upgrade_run")):
log("Action ceilometer-upgrade not yet run, setting status "
"blocked")
return "blocked", ("Run the ceilometer-upgrade action on the "
"leader to initialize ceilometer and gnocchi")
# Avoid changing status check
return None, None