Add support for gnocchi

Add new metric-service interface to support use of
Gnocchi as a storage backend for resource and metric
data.

Configure ceilometer to use the gnocchi dispatcher
in the event that ceilometer is related to gnocchi.
This has the side effect of disabling the ceilometer
API - Aodh and Gnocchi API's should be used directly
in this deployment topology.

Note that Gnocchi is only supported in OpenStack
Mitaka or later; 'metrics-service' is added to the
required interfaces configuration as an alternative
to 'mongodb' for >= Mitaka.

Change-Id: Ia31dfefd5efa3fb5ec2ba5d132ee865c567bd8df
This commit is contained in:
James Page 2017-07-06 17:32:19 +01:00
parent 5dd3d9d851
commit 72522a341f
13 changed files with 186 additions and 110 deletions

View File

@ -74,7 +74,7 @@ options:
api-workers:
type: int
default: 1
description: |
description: |
Number of workers for Ceilometer API server. (>= Kilo).
# Monitoring config
nagios_context:
@ -222,3 +222,9 @@ options:
description: |
Connect timeout configuration in ms for haproxy, used in HA
configurations. If not provided, default value of 5000ms is used.
gnocchi-archive-policy:
type: string
default: low
description: |
Archive retention policy to use when Ceilometer is deployed with
Gnocchi for resource, metric and measures storage.

View File

@ -70,6 +70,7 @@ from ceilometer_utils import (
set_shared_secret,
assess_status,
reload_systemd,
ceilometer_upgrade,
)
from ceilometer_contexts import CEILOMETER_PORT
from charmhelpers.contrib.openstack.ip import (
@ -134,32 +135,36 @@ def db_joined():
relation_set(ceilometer_database=CEILOMETER_DB)
@hooks.hook("metric-service-relation-joined")
def metric_service_joined():
# NOTE(jamespage): gnocchiclient is required to support
# the gnocchi event dispatcher
apt_install(filter_installed_packages(['python-gnocchiclient']),
fatal=True)
@hooks.hook("amqp-relation-changed",
"amqp-relation-departed",
"shared-db-relation-changed",
"shared-db-relation-departed")
"shared-db-relation-departed",
"identity-service-relation-changed",
"identity-service-relation-departed",
"metric-service-relation-changed",
"metric-service-relation-departed")
@restart_on_change(restart_map())
def any_changed():
CONFIGS.write_all()
configure_https()
for rid in relation_ids('identity-service'):
keystone_joined(relid=rid)
ceilometer_joined()
@hooks.hook("identity-service-relation-changed")
@restart_on_change(restart_map())
def identity_service_relation_changed():
CONFIGS.write_all()
configure_https()
keystone_joined()
ceilometer_joined()
@hooks.hook("amqp-relation-departed")
@restart_on_change(restart_map())
def amqp_departed():
if 'amqp' not in CONFIGS.complete_contexts():
log('amqp relation incomplete. Peer not ready?')
return
CONFIGS.write_all()
# NOTE(jamespage): ceilometer@ocata requires both gnocchi
# and mongodb to be configured to successfully
# upgrade the underlying data stores.
if ('metric-service' in CONFIGS.complete_contexts() and
'identity-service' in CONFIGS.complete_contexts() and
'mongodb' in CONFIGS.complete_contexts()):
ceilometer_upgrade()
def configure_https():

View File

@ -0,0 +1 @@
ceilometer_hooks.py

View File

@ -0,0 +1 @@
ceilometer_hooks.py

View File

@ -0,0 +1 @@
ceilometer_hooks.py

View File

@ -0,0 +1 @@
ceilometer_hooks.py

View File

@ -145,3 +145,17 @@ class ApacheSSLContext(SSLContext):
external_ports = [CEILOMETER_PORT]
service_namespace = "ceilometer"
class MetricServiceContext(OSContextGenerator):
interfaces = ['metric-service']
def __call__(self):
for relid in relation_ids('metric-service'):
for unit in related_units(relid):
gnocchi_url = relation_get('gnocchi_url', unit=unit, rid=relid)
if gnocchi_url:
return {'gnocchi_url': gnocchi_url,
'archive_policy': config('gnocchi-archive-policy')}
return {}

View File

@ -28,6 +28,7 @@ from ceilometer_contexts import (
MongoDBContext,
CeilometerContext,
HAProxyContext,
MetricServiceContext,
CEILOMETER_PORT,
)
from charmhelpers.contrib.openstack.utils import (
@ -43,9 +44,10 @@ from charmhelpers.contrib.openstack.utils import (
enable_memcache,
CompareOpenStackReleases,
)
from charmhelpers.core.hookenv import config, log
from charmhelpers.core.hookenv import config, log, is_leader
from charmhelpers.fetch import apt_update, apt_install, apt_upgrade
from charmhelpers.core.host import init_is_systemd
from charmhelpers.core.decorators import retry_on_exception
from copy import deepcopy
HAPROXY_CONF = '/etc/haproxy/haproxy.cfg'
@ -119,7 +121,8 @@ CONFIG_FILES = OrderedDict([
CeilometerContext(),
context.SyslogContext(),
HAProxyContext(),
context.MemcacheContext()],
context.MemcacheContext(),
MetricServiceContext()],
'services': CEILOMETER_BASE_SERVICES
}),
(CEILOMETER_API_SYSTEMD_CONF, {
@ -363,6 +366,18 @@ def assess_status(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)
if CompareOpenStackReleases(os_release('ceilometer-common')) >= 'mitaka':
required_ints['database'].append('metric-service')
return required_ints
def assess_status_func(configs):
"""Helper function to create the function that will assess_status() for
the unit.
@ -375,7 +390,7 @@ def assess_status_func(configs):
@return f() -> None : a function that assesses the unit's workload status
"""
return make_assess_status_func(
configs, REQUIRED_INTERFACES,
configs, resolve_required_interfaces(),
services=services(), ports=determine_ports())
@ -437,3 +452,16 @@ def disable_package_apache_site():
"""
if os.path.exists(PACKAGE_CEILOMETER_API_CONF):
subprocess.check_call(['a2dissite', 'ceilometer-api'])
@retry_on_exception(5, exc_type=subprocess.CalledProcessError)
def ceilometer_upgrade():
"""Execute ceilometer-upgrade command, with retry on failure if gnocchi
API is not ready for requests"""
if is_leader():
if (CompareOpenStackReleases(os_release('ceilometer-common')) >=
'newton'):
cmd = ['ceilometer-upgrade']
else:
cmd = ['ceilometer-dbsync']
subprocess.check_call(cmd)

View File

@ -39,6 +39,8 @@ requires:
ha:
interface: hacluster
scope: container
metric-service:
interface: gnocchi
peers:
cluster:
interface: ceilometer-ha

View File

@ -10,6 +10,11 @@ verbose = {{ verbose }}
use_syslog = {{ use_syslog }}
event_pipeline_cfg_file = /etc/ceilometer/event_pipeline_alarm.yaml
{% if gnocchi_url -%}
meter_dispatchers = gnocchi
event_dispatchers = gnocchi
{%- endif %}
[api]
port = {{ port }}
workers = {{ api_workers }}
@ -30,6 +35,7 @@ user_domain_name = default
auth_type = password
{% endif -%}
{% if db_host or db_mongo_servers -%}
[database]
{% if db_replset: -%}
connection = mongodb://{{ db_mongo_servers }}/{{ db_name }}?readPreference=primaryPreferred&replicaSet={{ db_replset }}
@ -39,10 +45,18 @@ connection = mongodb://{{ db_host }}:{{ db_port }}/{{ db_name }}
{% endif %}
metering_time_to_live = {{ metering_time_to_live }}
event_time_to_live = {{ event_time_to_live }}
{%- endif %}
[publisher]
telemetry_secret = {{ metering_secret }}
{% if gnocchi_url -%}
[dispatcher_gnocchi]
filter_service_activity = False
archive_policy = {{ archive_policy }}
url = {{ gnocchi_url }}
{%- endif %}
{% include "section-keystone-authtoken-mitaka" %}
{% include "section-rabbitmq-oslo" %}

View File

@ -438,44 +438,6 @@ class CeilometerBasicDeployment(OpenStackAmuletDeployment):
u.log.debug('OK')
def test_209_nova_compute_ceilometer_agent_relation(self):
"""Verify the nova-compute to ceilometer relation data"""
u.log.debug('Checking nova-compute:ceilometer relation data...')
unit = self.nova_sentry
relation = ['nova-ceilometer', 'ceilometer-agent:nova-ceilometer']
expected = {'private-address': u.valid_ip}
ret = u.validate_relation_data(unit, relation, expected)
if ret:
message = u.relation_error('ceilometer-service', ret)
amulet.raise_status(amulet.FAIL, msg=message)
u.log.debug('OK')
def test_210_ceilometer_agent_nova_compute_relation(self):
"""Verify the ceilometer to nova-compute relation data"""
u.log.debug('Checking ceilometer:nova-compute relation data...')
unit = self.ceil_agent_sentry
relation = ['nova-ceilometer', 'nova-compute:nova-ceilometer']
sub = ('{"nova": {"/etc/nova/nova.conf": {"sections": {"DEFAULT": '
'[["instance_usage_audit", "True"], '
'["instance_usage_audit_period", "hour"], '
'["notify_on_state_change", "vm_and_task_state"], '
'["notification_driver", "ceilometer.compute.nova_notifier"], '
'["notification_driver", '
'"nova.openstack.common.notifier.rpc_notifier"]]}}}}')
expected = {
'subordinate_configuration': sub,
'private-address': u.valid_ip
}
ret = u.validate_relation_data(unit, relation, expected)
if ret:
message = u.relation_error('ceilometer-service', ret)
amulet.raise_status(amulet.FAIL, msg=message)
u.log.debug('OK')
def test_300_ceilometer_config(self):
"""Verify the data in the ceilometer config file."""
u.log.debug('Checking ceilometer config file data...')
@ -549,49 +511,6 @@ class CeilometerBasicDeployment(OpenStackAmuletDeployment):
u.log.debug('OK')
def test_301_nova_config(self):
"""Verify data in the nova compute nova config file"""
u.log.debug('Checking nova compute config file...')
unit = self.nova_sentry
conf = '/etc/nova/nova.conf'
expected = {
'DEFAULT': {
'verbose': 'False',
'debug': 'False',
'use_syslog': 'False',
'my_ip': u.valid_ip,
}
}
# NOTE(beisner): notification_driver is not checked like the
# others, as configparser does not support duplicate config
# options, and dicts cant have duplicate keys.
# Ex. from conf file:
# notification_driver = ceilometer.compute.nova_notifier
# notification_driver = nova.openstack.common.notifier.rpc_notifier
for section, pairs in expected.iteritems():
ret = u.validate_config_data(unit, conf, section, pairs)
if ret:
message = "ceilometer config error: {}".format(ret)
amulet.raise_status(amulet.FAIL, msg=message)
# Check notification_driver existence via simple grep cmd
lines = [('notification_driver = '
'ceilometer.compute.nova_notifier'),
('notification_driver = '
'nova.openstack.common.notifier.rpc_notifier')]
sentry_units = [unit]
cmds = []
for line in lines:
cmds.append('grep "{}" {}'.format(line, conf))
ret = u.check_commands_on_units(cmds, sentry_units)
if ret:
amulet.raise_status(amulet.FAIL, msg=ret)
u.log.debug('OK')
def test_400_api_connection(self):
"""Simple api calls to check service is up and responding"""
u.log.debug('Checking api functionality...')

View File

@ -135,12 +135,38 @@ class CeilometerHooksTest(CharmTestCase):
self.relation_set.assert_called_with(
ceilometer_database='ceilometer')
@patch.object(hooks, 'ceilometer_upgrade')
@patch.object(hooks, 'keystone_joined')
@patch('charmhelpers.core.hookenv.config')
@patch.object(hooks, 'ceilometer_joined')
def test_any_changed(self, joined, mock_config):
def test_any_changed_with_metrics(self, ceilometer_joined, mock_config,
keystone_joined, ceilometer_upgrade):
self.CONFIGS.complete_contexts.return_value = [
'metric-service',
'identity-service',
'mongodb'
]
self.relation_ids.return_value = ['identity-service:1']
hooks.hooks.execute(['hooks/shared-db-relation-changed'])
self.CONFIGS.write_all.assert_called_once()
ceilometer_joined.assert_called_once()
keystone_joined.assert_called_with(relid='identity-service:1')
ceilometer_upgrade.assert_called_once()
self.configure_https.assert_called_once()
@patch.object(hooks, 'ceilometer_upgrade')
@patch.object(hooks, 'keystone_joined')
@patch('charmhelpers.core.hookenv.config')
@patch.object(hooks, 'ceilometer_joined')
def test_any_changed(self, ceilometer_joined, mock_config,
keystone_joined, ceilometer_upgrade):
self.relation_ids.return_value = ['identity-service:1']
hooks.hooks.execute(['hooks/shared-db-relation-changed'])
self.assertTrue(self.CONFIGS.write_all.called)
self.assertTrue(joined.called)
self.assertTrue(ceilometer_joined.called)
keystone_joined.assert_called_with(relid='identity-service:1')
ceilometer_upgrade.assert_not_called()
self.configure_https.assert_called_once()
@patch('charmhelpers.core.hookenv.config')
@patch.object(hooks, 'install')
@ -150,14 +176,17 @@ class CeilometerHooksTest(CharmTestCase):
self.assertTrue(changed.called)
self.assertTrue(install.called)
@patch.object(hooks, 'any_changed')
@patch('charmhelpers.core.hookenv.config')
@patch.object(hooks, 'cluster_joined')
def test_upgrade_charm_with_cluster(self, cluster_joined, mock_config):
def test_upgrade_charm_with_cluster(self, cluster_joined, mock_config,
any_changed):
self.relation_ids.return_value = ['ceilometer/0',
'ceilometer/1',
'ceilometer/2']
hooks.hooks.execute(['hooks/upgrade-charm'])
self.assertEquals(cluster_joined.call_count, 3)
any_changed.assert_called_once()
@patch.object(hooks, 'install_event_pipeline_setting')
@patch('charmhelpers.core.hookenv.config')
@ -494,3 +523,12 @@ class CeilometerHooksTest(CharmTestCase):
self.relation_ids.return_value = ['identity-service/0']
hooks.hooks.execute(['hooks/ha-relation-changed'])
self.assertEquals(mock_keystone_joined.call_count, 1)
def test_metric_service_joined(self):
self.filter_installed_packages.return_value = ['python-gnocchiclient']
hooks.hooks.execute(['hooks/metric-service-relation-joined'])
self.filter_installed_packages.assert_called_with(
['python-gnocchiclient']
)
self.apt_install.assert_called_with(['python-gnocchiclient'],
fatal=True)

View File

@ -37,6 +37,7 @@ TO_PATCH = [
'enable_memcache',
'token_cache_pkgs',
'os_release',
'is_leader',
]
@ -209,7 +210,7 @@ class CeilometerUtilsTest(CharmTestCase):
utils.VERSION_PACKAGE
)
@patch.object(utils, 'REQUIRED_INTERFACES')
@patch.object(utils, 'resolve_required_interfaces')
@patch.object(utils, 'services')
@patch.object(utils, 'determine_ports')
@patch.object(utils, 'make_assess_status_func')
@ -217,12 +218,13 @@ class CeilometerUtilsTest(CharmTestCase):
make_assess_status_func,
determine_ports,
services,
REQUIRED_INTERFACES):
resolve_required_interfaces):
services.return_value = 's1'
determine_ports.return_value = 'p1'
resolve_required_interfaces.return_value = {'a': ['b']}
utils.assess_status_func('test-config')
make_assess_status_func.assert_called_once_with(
'test-config', REQUIRED_INTERFACES, services='s1', ports='p1')
'test-config', {'a': ['b']}, services='s1', ports='p1')
def test_pause_unit_helper(self):
with patch.object(utils, '_pause_resume_helper') as prh:
@ -243,3 +245,47 @@ class CeilometerUtilsTest(CharmTestCase):
utils._pause_resume_helper(f, 'some-config')
asf.assert_called_once_with('some-config')
f.assert_called_once_with('assessor', services='s1', ports='p1')
def test_resolve_required_interfaces(self):
self.os_release.side_effect = None
self.os_release.return_value = 'icehouse'
self.assertEqual(
utils.resolve_required_interfaces(),
{
'database': ['mongodb'],
'messaging': ['amqp'],
'identity': ['identity-service'],
}
)
def test_resolve_required_interfaces_mitaka(self):
self.os_release.side_effect = None
self.os_release.return_value = 'mitaka'
self.assertEqual(
utils.resolve_required_interfaces(),
{
'database': ['mongodb', 'metric-service'],
'messaging': ['amqp'],
'identity': ['identity-service'],
}
)
@patch.object(utils, 'subprocess')
def test_ceilometer_upgrade(self, mock_subprocess):
self.is_leader.return_value = True
self.os_release.return_value = 'ocata'
utils.ceilometer_upgrade()
mock_subprocess.check_call.assert_called_with(['ceilometer-upgrade'])
@patch.object(utils, 'subprocess')
def test_ceilometer_upgrade_mitaka(self, mock_subprocess):
self.is_leader.return_value = True
self.os_release.return_value = 'mitaka'
utils.ceilometer_upgrade()
mock_subprocess.check_call.assert_called_with(['ceilometer-dbsync'])
@patch.object(utils, 'subprocess')
def test_ceilometer_upgrade_follower(self, mock_subprocess):
self.is_leader.return_value = False
utils.ceilometer_upgrade()
mock_subprocess.check_call.assert_not_called()