Add nrpe support
Parses services() available, and uses charmhelpers' haproxy checks to set default checks for Heat. By enabling nrpe-external-master interface, we also allow hacluster subordinate application to be monitored for check_crm and pacemakerd. Change-Id: I4ad2b1c514099f9b6fe352cdab65587780915e5c Closes-Bug: 1664745 Signed-off-by: Alvaro Uria <alvaro.uria@canonical.com>
This commit is contained in:
parent
bdc84f7980
commit
213a6a8c95
|
@ -5,6 +5,7 @@ include:
|
|||
- osplatform
|
||||
- cli
|
||||
- fetch
|
||||
- contrib.charmsupport
|
||||
- contrib.openstack|inc=*
|
||||
- contrib.python
|
||||
- contrib.storage
|
||||
|
|
15
config.yaml
15
config.yaml
|
@ -93,6 +93,21 @@ options:
|
|||
set to twice the number of CPU cores a service unit has. When deployed
|
||||
in a LXD container, this default value will be capped to 4 workers
|
||||
unless this configuration option is set.
|
||||
nagios_context:
|
||||
type: string
|
||||
default: "juju"
|
||||
description: |
|
||||
Used by the nrpe-external-master subordinate charm. A string that will
|
||||
be prepended to instance name to set the host name in nagios. So for
|
||||
instance the hostname would be something like 'juju-myservice-0'. If
|
||||
you are running multiple environments with the same services in them
|
||||
this allows you to differentiate between them.
|
||||
nagios_servicegroups:
|
||||
type: string
|
||||
default: ""
|
||||
description: |
|
||||
A comma-separated list of nagios servicegroups. If left empty, the
|
||||
nagios_context will be used as the servicegroup
|
||||
# Network config (by default all access is over 'private-address')
|
||||
os-admin-network:
|
||||
type: string
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# Copyright 2014-2015 Canonical Limited.
|
||||
#
|
||||
# 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.
|
|
@ -0,0 +1,511 @@
|
|||
# Copyright 2014-2015 Canonical Limited.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Compatibility with the nrpe-external-master charm"""
|
||||
# Copyright 2012 Canonical Ltd.
|
||||
#
|
||||
# Authors:
|
||||
# Matthew Wedgwood <matthew.wedgwood@canonical.com>
|
||||
|
||||
import glob
|
||||
import grp
|
||||
import os
|
||||
import pwd
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import yaml
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
config,
|
||||
hook_name,
|
||||
local_unit,
|
||||
log,
|
||||
relation_get,
|
||||
relation_ids,
|
||||
relation_set,
|
||||
relations_of_type,
|
||||
)
|
||||
|
||||
from charmhelpers.core.host import service
|
||||
from charmhelpers.core import host
|
||||
|
||||
# This module adds compatibility with the nrpe-external-master and plain nrpe
|
||||
# subordinate charms. To use it in your charm:
|
||||
#
|
||||
# 1. Update metadata.yaml
|
||||
#
|
||||
# provides:
|
||||
# (...)
|
||||
# nrpe-external-master:
|
||||
# interface: nrpe-external-master
|
||||
# scope: container
|
||||
#
|
||||
# and/or
|
||||
#
|
||||
# provides:
|
||||
# (...)
|
||||
# local-monitors:
|
||||
# interface: local-monitors
|
||||
# scope: container
|
||||
|
||||
#
|
||||
# 2. Add the following to config.yaml
|
||||
#
|
||||
# nagios_context:
|
||||
# default: "juju"
|
||||
# type: string
|
||||
# description: |
|
||||
# Used by the nrpe subordinate charms.
|
||||
# A string that will be prepended to instance name to set the host name
|
||||
# in nagios. So for instance the hostname would be something like:
|
||||
# juju-myservice-0
|
||||
# If you're running multiple environments with the same services in them
|
||||
# this allows you to differentiate between them.
|
||||
# nagios_servicegroups:
|
||||
# default: ""
|
||||
# type: string
|
||||
# description: |
|
||||
# A comma-separated list of nagios servicegroups.
|
||||
# If left empty, the nagios_context will be used as the servicegroup
|
||||
#
|
||||
# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master
|
||||
#
|
||||
# 4. Update your hooks.py with something like this:
|
||||
#
|
||||
# from charmsupport.nrpe import NRPE
|
||||
# (...)
|
||||
# def update_nrpe_config():
|
||||
# nrpe_compat = NRPE()
|
||||
# nrpe_compat.add_check(
|
||||
# shortname = "myservice",
|
||||
# description = "Check MyService",
|
||||
# check_cmd = "check_http -w 2 -c 10 http://localhost"
|
||||
# )
|
||||
# nrpe_compat.add_check(
|
||||
# "myservice_other",
|
||||
# "Check for widget failures",
|
||||
# check_cmd = "/srv/myapp/scripts/widget_check"
|
||||
# )
|
||||
# nrpe_compat.write()
|
||||
#
|
||||
# def config_changed():
|
||||
# (...)
|
||||
# update_nrpe_config()
|
||||
#
|
||||
# def nrpe_external_master_relation_changed():
|
||||
# update_nrpe_config()
|
||||
#
|
||||
# def local_monitors_relation_changed():
|
||||
# update_nrpe_config()
|
||||
#
|
||||
# 4.a If your charm is a subordinate charm set primary=False
|
||||
#
|
||||
# from charmsupport.nrpe import NRPE
|
||||
# (...)
|
||||
# def update_nrpe_config():
|
||||
# nrpe_compat = NRPE(primary=False)
|
||||
#
|
||||
# 5. ln -s hooks.py nrpe-external-master-relation-changed
|
||||
# ln -s hooks.py local-monitors-relation-changed
|
||||
|
||||
|
||||
class CheckException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Check(object):
|
||||
shortname_re = '[A-Za-z0-9-_.@]+$'
|
||||
service_template = ("""
|
||||
#---------------------------------------------------
|
||||
# This file is Juju managed
|
||||
#---------------------------------------------------
|
||||
define service {{
|
||||
use active-service
|
||||
host_name {nagios_hostname}
|
||||
service_description {nagios_hostname}[{shortname}] """
|
||||
"""{description}
|
||||
check_command check_nrpe!{command}
|
||||
servicegroups {nagios_servicegroup}
|
||||
}}
|
||||
""")
|
||||
|
||||
def __init__(self, shortname, description, check_cmd):
|
||||
super(Check, self).__init__()
|
||||
# XXX: could be better to calculate this from the service name
|
||||
if not re.match(self.shortname_re, shortname):
|
||||
raise CheckException("shortname must match {}".format(
|
||||
Check.shortname_re))
|
||||
self.shortname = shortname
|
||||
self.command = "check_{}".format(shortname)
|
||||
# Note: a set of invalid characters is defined by the
|
||||
# Nagios server config
|
||||
# The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()=
|
||||
self.description = description
|
||||
self.check_cmd = self._locate_cmd(check_cmd)
|
||||
|
||||
def _get_check_filename(self):
|
||||
return os.path.join(NRPE.nrpe_confdir, '{}.cfg'.format(self.command))
|
||||
|
||||
def _get_service_filename(self, hostname):
|
||||
return os.path.join(NRPE.nagios_exportdir,
|
||||
'service__{}_{}.cfg'.format(hostname, self.command))
|
||||
|
||||
def _locate_cmd(self, check_cmd):
|
||||
search_path = (
|
||||
'/usr/lib/nagios/plugins',
|
||||
'/usr/local/lib/nagios/plugins',
|
||||
)
|
||||
parts = shlex.split(check_cmd)
|
||||
for path in search_path:
|
||||
if os.path.exists(os.path.join(path, parts[0])):
|
||||
command = os.path.join(path, parts[0])
|
||||
if len(parts) > 1:
|
||||
command += " " + " ".join(parts[1:])
|
||||
return command
|
||||
log('Check command not found: {}'.format(parts[0]))
|
||||
return ''
|
||||
|
||||
def _remove_service_files(self):
|
||||
if not os.path.exists(NRPE.nagios_exportdir):
|
||||
return
|
||||
for f in os.listdir(NRPE.nagios_exportdir):
|
||||
if f.endswith('_{}.cfg'.format(self.command)):
|
||||
os.remove(os.path.join(NRPE.nagios_exportdir, f))
|
||||
|
||||
def remove(self, hostname):
|
||||
nrpe_check_file = self._get_check_filename()
|
||||
if os.path.exists(nrpe_check_file):
|
||||
os.remove(nrpe_check_file)
|
||||
self._remove_service_files()
|
||||
|
||||
def write(self, nagios_context, hostname, nagios_servicegroups):
|
||||
nrpe_check_file = self._get_check_filename()
|
||||
with open(nrpe_check_file, 'w') as nrpe_check_config:
|
||||
nrpe_check_config.write("# check {}\n".format(self.shortname))
|
||||
if nagios_servicegroups:
|
||||
nrpe_check_config.write(
|
||||
"# The following header was added automatically by juju\n")
|
||||
nrpe_check_config.write(
|
||||
"# Modifying it will affect nagios monitoring and alerting\n")
|
||||
nrpe_check_config.write(
|
||||
"# servicegroups: {}\n".format(nagios_servicegroups))
|
||||
nrpe_check_config.write("command[{}]={}\n".format(
|
||||
self.command, self.check_cmd))
|
||||
|
||||
if not os.path.exists(NRPE.nagios_exportdir):
|
||||
log('Not writing service config as {} is not accessible'.format(
|
||||
NRPE.nagios_exportdir))
|
||||
else:
|
||||
self.write_service_config(nagios_context, hostname,
|
||||
nagios_servicegroups)
|
||||
|
||||
def write_service_config(self, nagios_context, hostname,
|
||||
nagios_servicegroups):
|
||||
self._remove_service_files()
|
||||
|
||||
templ_vars = {
|
||||
'nagios_hostname': hostname,
|
||||
'nagios_servicegroup': nagios_servicegroups,
|
||||
'description': self.description,
|
||||
'shortname': self.shortname,
|
||||
'command': self.command,
|
||||
}
|
||||
nrpe_service_text = Check.service_template.format(**templ_vars)
|
||||
nrpe_service_file = self._get_service_filename(hostname)
|
||||
with open(nrpe_service_file, 'w') as nrpe_service_config:
|
||||
nrpe_service_config.write(str(nrpe_service_text))
|
||||
|
||||
def run(self):
|
||||
subprocess.call(self.check_cmd)
|
||||
|
||||
|
||||
class NRPE(object):
|
||||
nagios_logdir = '/var/log/nagios'
|
||||
nagios_exportdir = '/var/lib/nagios/export'
|
||||
nrpe_confdir = '/etc/nagios/nrpe.d'
|
||||
homedir = '/var/lib/nagios' # home dir provided by nagios-nrpe-server
|
||||
|
||||
def __init__(self, hostname=None, primary=True):
|
||||
super(NRPE, self).__init__()
|
||||
self.config = config()
|
||||
self.primary = primary
|
||||
self.nagios_context = self.config['nagios_context']
|
||||
if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']:
|
||||
self.nagios_servicegroups = self.config['nagios_servicegroups']
|
||||
else:
|
||||
self.nagios_servicegroups = self.nagios_context
|
||||
self.unit_name = local_unit().replace('/', '-')
|
||||
if hostname:
|
||||
self.hostname = hostname
|
||||
else:
|
||||
nagios_hostname = get_nagios_hostname()
|
||||
if nagios_hostname:
|
||||
self.hostname = nagios_hostname
|
||||
else:
|
||||
self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
|
||||
self.checks = []
|
||||
# Iff in an nrpe-external-master relation hook, set primary status
|
||||
relation = relation_ids('nrpe-external-master')
|
||||
if relation:
|
||||
log("Setting charm primary status {}".format(primary))
|
||||
for rid in relation:
|
||||
relation_set(relation_id=rid, relation_settings={'primary': self.primary})
|
||||
self.remove_check_queue = set()
|
||||
|
||||
@classmethod
|
||||
def does_nrpe_conf_dir_exist(cls):
|
||||
"""Return True if th nrpe_confdif directory exists."""
|
||||
return os.path.isdir(cls.nrpe_confdir)
|
||||
|
||||
def add_check(self, *args, **kwargs):
|
||||
shortname = None
|
||||
if kwargs.get('shortname') is None:
|
||||
if len(args) > 0:
|
||||
shortname = args[0]
|
||||
else:
|
||||
shortname = kwargs['shortname']
|
||||
|
||||
self.checks.append(Check(*args, **kwargs))
|
||||
try:
|
||||
self.remove_check_queue.remove(shortname)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def remove_check(self, *args, **kwargs):
|
||||
if kwargs.get('shortname') is None:
|
||||
raise ValueError('shortname of check must be specified')
|
||||
|
||||
# Use sensible defaults if they're not specified - these are not
|
||||
# actually used during removal, but they're required for constructing
|
||||
# the Check object; check_disk is chosen because it's part of the
|
||||
# nagios-plugins-basic package.
|
||||
if kwargs.get('check_cmd') is None:
|
||||
kwargs['check_cmd'] = 'check_disk'
|
||||
if kwargs.get('description') is None:
|
||||
kwargs['description'] = ''
|
||||
|
||||
check = Check(*args, **kwargs)
|
||||
check.remove(self.hostname)
|
||||
self.remove_check_queue.add(kwargs['shortname'])
|
||||
|
||||
def write(self):
|
||||
try:
|
||||
nagios_uid = pwd.getpwnam('nagios').pw_uid
|
||||
nagios_gid = grp.getgrnam('nagios').gr_gid
|
||||
except Exception:
|
||||
log("Nagios user not set up, nrpe checks not updated")
|
||||
return
|
||||
|
||||
if not os.path.exists(NRPE.nagios_logdir):
|
||||
os.mkdir(NRPE.nagios_logdir)
|
||||
os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid)
|
||||
|
||||
nrpe_monitors = {}
|
||||
monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}}
|
||||
|
||||
# check that the charm can write to the conf dir. If not, then nagios
|
||||
# probably isn't installed, and we can defer.
|
||||
if not self.does_nrpe_conf_dir_exist():
|
||||
return
|
||||
|
||||
for nrpecheck in self.checks:
|
||||
nrpecheck.write(self.nagios_context, self.hostname,
|
||||
self.nagios_servicegroups)
|
||||
nrpe_monitors[nrpecheck.shortname] = {
|
||||
"command": nrpecheck.command,
|
||||
}
|
||||
|
||||
# update-status hooks are configured to firing every 5 minutes by
|
||||
# default. When nagios-nrpe-server is restarted, the nagios server
|
||||
# reports checks failing causing unnecessary alerts. Let's not restart
|
||||
# on update-status hooks.
|
||||
if not hook_name() == 'update-status':
|
||||
service('restart', 'nagios-nrpe-server')
|
||||
|
||||
monitor_ids = relation_ids("local-monitors") + \
|
||||
relation_ids("nrpe-external-master")
|
||||
for rid in monitor_ids:
|
||||
reldata = relation_get(unit=local_unit(), rid=rid)
|
||||
if 'monitors' in reldata:
|
||||
# update the existing set of monitors with the new data
|
||||
old_monitors = yaml.safe_load(reldata['monitors'])
|
||||
old_nrpe_monitors = old_monitors['monitors']['remote']['nrpe']
|
||||
# remove keys that are in the remove_check_queue
|
||||
old_nrpe_monitors = {k: v for k, v in old_nrpe_monitors.items()
|
||||
if k not in self.remove_check_queue}
|
||||
# update/add nrpe_monitors
|
||||
old_nrpe_monitors.update(nrpe_monitors)
|
||||
old_monitors['monitors']['remote']['nrpe'] = old_nrpe_monitors
|
||||
# write back to the relation
|
||||
relation_set(relation_id=rid, monitors=yaml.dump(old_monitors))
|
||||
else:
|
||||
# write a brand new set of monitors, as no existing ones.
|
||||
relation_set(relation_id=rid, monitors=yaml.dump(monitors))
|
||||
|
||||
self.remove_check_queue.clear()
|
||||
|
||||
|
||||
def get_nagios_hostcontext(relation_name='nrpe-external-master'):
|
||||
"""
|
||||
Query relation with nrpe subordinate, return the nagios_host_context
|
||||
|
||||
:param str relation_name: Name of relation nrpe sub joined to
|
||||
"""
|
||||
for rel in relations_of_type(relation_name):
|
||||
if 'nagios_host_context' in rel:
|
||||
return rel['nagios_host_context']
|
||||
|
||||
|
||||
def get_nagios_hostname(relation_name='nrpe-external-master'):
|
||||
"""
|
||||
Query relation with nrpe subordinate, return the nagios_hostname
|
||||
|
||||
:param str relation_name: Name of relation nrpe sub joined to
|
||||
"""
|
||||
for rel in relations_of_type(relation_name):
|
||||
if 'nagios_hostname' in rel:
|
||||
return rel['nagios_hostname']
|
||||
|
||||
|
||||
def get_nagios_unit_name(relation_name='nrpe-external-master'):
|
||||
"""
|
||||
Return the nagios unit name prepended with host_context if needed
|
||||
|
||||
:param str relation_name: Name of relation nrpe sub joined to
|
||||
"""
|
||||
host_context = get_nagios_hostcontext(relation_name)
|
||||
if host_context:
|
||||
unit = "%s:%s" % (host_context, local_unit())
|
||||
else:
|
||||
unit = local_unit()
|
||||
return unit
|
||||
|
||||
|
||||
def add_init_service_checks(nrpe, services, unit_name, immediate_check=True):
|
||||
"""
|
||||
Add checks for each service in list
|
||||
|
||||
:param NRPE nrpe: NRPE object to add check to
|
||||
:param list services: List of services to check
|
||||
:param str unit_name: Unit name to use in check description
|
||||
:param bool immediate_check: For sysv init, run the service check immediately
|
||||
"""
|
||||
for svc in services:
|
||||
# Don't add a check for these services from neutron-gateway
|
||||
if svc in ['ext-port', 'os-charm-phy-nic-mtu']:
|
||||
next
|
||||
|
||||
upstart_init = '/etc/init/%s.conf' % svc
|
||||
sysv_init = '/etc/init.d/%s' % svc
|
||||
|
||||
if host.init_is_systemd(service_name=svc):
|
||||
nrpe.add_check(
|
||||
shortname=svc,
|
||||
description='process check {%s}' % unit_name,
|
||||
check_cmd='check_systemd.py %s' % svc
|
||||
)
|
||||
elif os.path.exists(upstart_init):
|
||||
nrpe.add_check(
|
||||
shortname=svc,
|
||||
description='process check {%s}' % unit_name,
|
||||
check_cmd='check_upstart_job %s' % svc
|
||||
)
|
||||
elif os.path.exists(sysv_init):
|
||||
cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
|
||||
checkpath = '%s/service-check-%s.txt' % (nrpe.homedir, svc)
|
||||
croncmd = (
|
||||
'/usr/local/lib/nagios/plugins/check_exit_status.pl '
|
||||
'-e -s /etc/init.d/%s status' % svc
|
||||
)
|
||||
cron_file = '*/5 * * * * root %s > %s\n' % (croncmd, checkpath)
|
||||
f = open(cronpath, 'w')
|
||||
f.write(cron_file)
|
||||
f.close()
|
||||
nrpe.add_check(
|
||||
shortname=svc,
|
||||
description='service check {%s}' % unit_name,
|
||||
check_cmd='check_status_file.py -f %s' % checkpath,
|
||||
)
|
||||
# if /var/lib/nagios doesn't exist open(checkpath, 'w') will fail
|
||||
# (LP: #1670223).
|
||||
if immediate_check and os.path.isdir(nrpe.homedir):
|
||||
f = open(checkpath, 'w')
|
||||
subprocess.call(
|
||||
croncmd.split(),
|
||||
stdout=f,
|
||||
stderr=subprocess.STDOUT
|
||||
)
|
||||
f.close()
|
||||
os.chmod(checkpath, 0o644)
|
||||
|
||||
|
||||
def copy_nrpe_checks(nrpe_files_dir=None):
|
||||
"""
|
||||
Copy the nrpe checks into place
|
||||
|
||||
"""
|
||||
NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
|
||||
if nrpe_files_dir is None:
|
||||
# determine if "charmhelpers" is in CHARMDIR or CHARMDIR/hooks
|
||||
for segment in ['.', 'hooks']:
|
||||
nrpe_files_dir = os.path.abspath(os.path.join(
|
||||
os.getenv('CHARM_DIR'),
|
||||
segment,
|
||||
'charmhelpers',
|
||||
'contrib',
|
||||
'openstack',
|
||||
'files'))
|
||||
if os.path.isdir(nrpe_files_dir):
|
||||
break
|
||||
else:
|
||||
raise RuntimeError("Couldn't find charmhelpers directory")
|
||||
if not os.path.exists(NAGIOS_PLUGINS):
|
||||
os.makedirs(NAGIOS_PLUGINS)
|
||||
for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")):
|
||||
if os.path.isfile(fname):
|
||||
shutil.copy2(fname,
|
||||
os.path.join(NAGIOS_PLUGINS, os.path.basename(fname)))
|
||||
|
||||
|
||||
def add_haproxy_checks(nrpe, unit_name):
|
||||
"""
|
||||
Add checks for each service in list
|
||||
|
||||
:param NRPE nrpe: NRPE object to add check to
|
||||
:param str unit_name: Unit name to use in check description
|
||||
"""
|
||||
nrpe.add_check(
|
||||
shortname='haproxy_servers',
|
||||
description='Check HAProxy {%s}' % unit_name,
|
||||
check_cmd='check_haproxy.sh')
|
||||
nrpe.add_check(
|
||||
shortname='haproxy_queue',
|
||||
description='Check HAProxy queue depth {%s}' % unit_name,
|
||||
check_cmd='check_haproxy_queue_depth.sh')
|
||||
|
||||
|
||||
def remove_deprecated_check(nrpe, deprecated_services):
|
||||
"""
|
||||
Remove checks fro deprecated services in list
|
||||
|
||||
:param nrpe: NRPE object to remove check from
|
||||
:type nrpe: NRPE
|
||||
:param deprecated_services: List of deprecated services that are removed
|
||||
:type deprecated_services: list
|
||||
"""
|
||||
for dep_svc in deprecated_services:
|
||||
log('Deprecated service: {}'.format(dep_svc))
|
||||
nrpe.remove_check(shortname=dep_svc)
|
|
@ -0,0 +1,173 @@
|
|||
# Copyright 2014-2015 Canonical Limited.
|
||||
#
|
||||
# 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.
|
||||
|
||||
'''
|
||||
Functions for managing volumes in juju units. One volume is supported per unit.
|
||||
Subordinates may have their own storage, provided it is on its own partition.
|
||||
|
||||
Configuration stanzas::
|
||||
|
||||
volume-ephemeral:
|
||||
type: boolean
|
||||
default: true
|
||||
description: >
|
||||
If false, a volume is mounted as sepecified in "volume-map"
|
||||
If true, ephemeral storage will be used, meaning that log data
|
||||
will only exist as long as the machine. YOU HAVE BEEN WARNED.
|
||||
volume-map:
|
||||
type: string
|
||||
default: {}
|
||||
description: >
|
||||
YAML map of units to device names, e.g:
|
||||
"{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }"
|
||||
Service units will raise a configure-error if volume-ephemeral
|
||||
is 'true' and no volume-map value is set. Use 'juju set' to set a
|
||||
value and 'juju resolved' to complete configuration.
|
||||
|
||||
Usage::
|
||||
|
||||
from charmsupport.volumes import configure_volume, VolumeConfigurationError
|
||||
from charmsupport.hookenv import log, ERROR
|
||||
def post_mount_hook():
|
||||
stop_service('myservice')
|
||||
def post_mount_hook():
|
||||
start_service('myservice')
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
configure_volume(before_change=pre_mount_hook,
|
||||
after_change=post_mount_hook)
|
||||
except VolumeConfigurationError:
|
||||
log('Storage could not be configured', ERROR)
|
||||
|
||||
'''
|
||||
|
||||
# XXX: Known limitations
|
||||
# - fstab is neither consulted nor updated
|
||||
|
||||
import os
|
||||
from charmhelpers.core import hookenv
|
||||
from charmhelpers.core import host
|
||||
import yaml
|
||||
|
||||
|
||||
MOUNT_BASE = '/srv/juju/volumes'
|
||||
|
||||
|
||||
class VolumeConfigurationError(Exception):
|
||||
'''Volume configuration data is missing or invalid'''
|
||||
pass
|
||||
|
||||
|
||||
def get_config():
|
||||
'''Gather and sanity-check volume configuration data'''
|
||||
volume_config = {}
|
||||
config = hookenv.config()
|
||||
|
||||
errors = False
|
||||
|
||||
if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'):
|
||||
volume_config['ephemeral'] = True
|
||||
else:
|
||||
volume_config['ephemeral'] = False
|
||||
|
||||
try:
|
||||
volume_map = yaml.safe_load(config.get('volume-map', '{}'))
|
||||
except yaml.YAMLError as e:
|
||||
hookenv.log("Error parsing YAML volume-map: {}".format(e),
|
||||
hookenv.ERROR)
|
||||
errors = True
|
||||
if volume_map is None:
|
||||
# probably an empty string
|
||||
volume_map = {}
|
||||
elif not isinstance(volume_map, dict):
|
||||
hookenv.log("Volume-map should be a dictionary, not {}".format(
|
||||
type(volume_map)))
|
||||
errors = True
|
||||
|
||||
volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME'])
|
||||
if volume_config['device'] and volume_config['ephemeral']:
|
||||
# asked for ephemeral storage but also defined a volume ID
|
||||
hookenv.log('A volume is defined for this unit, but ephemeral '
|
||||
'storage was requested', hookenv.ERROR)
|
||||
errors = True
|
||||
elif not volume_config['device'] and not volume_config['ephemeral']:
|
||||
# asked for permanent storage but did not define volume ID
|
||||
hookenv.log('Ephemeral storage was requested, but there is no volume '
|
||||
'defined for this unit.', hookenv.ERROR)
|
||||
errors = True
|
||||
|
||||
unit_mount_name = hookenv.local_unit().replace('/', '-')
|
||||
volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name)
|
||||
|
||||
if errors:
|
||||
return None
|
||||
return volume_config
|
||||
|
||||
|
||||
def mount_volume(config):
|
||||
if os.path.exists(config['mountpoint']):
|
||||
if not os.path.isdir(config['mountpoint']):
|
||||
hookenv.log('Not a directory: {}'.format(config['mountpoint']))
|
||||
raise VolumeConfigurationError()
|
||||
else:
|
||||
host.mkdir(config['mountpoint'])
|
||||
if os.path.ismount(config['mountpoint']):
|
||||
unmount_volume(config)
|
||||
if not host.mount(config['device'], config['mountpoint'], persist=True):
|
||||
raise VolumeConfigurationError()
|
||||
|
||||
|
||||
def unmount_volume(config):
|
||||
if os.path.ismount(config['mountpoint']):
|
||||
if not host.umount(config['mountpoint'], persist=True):
|
||||
raise VolumeConfigurationError()
|
||||
|
||||
|
||||
def managed_mounts():
|
||||
'''List of all mounted managed volumes'''
|
||||
return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts())
|
||||
|
||||
|
||||
def configure_volume(before_change=lambda: None, after_change=lambda: None):
|
||||
'''Set up storage (or don't) according to the charm's volume configuration.
|
||||
Returns the mount point or "ephemeral". before_change and after_change
|
||||
are optional functions to be called if the volume configuration changes.
|
||||
'''
|
||||
|
||||
config = get_config()
|
||||
if not config:
|
||||
hookenv.log('Failed to read volume configuration', hookenv.CRITICAL)
|
||||
raise VolumeConfigurationError()
|
||||
|
||||
if config['ephemeral']:
|
||||
if os.path.ismount(config['mountpoint']):
|
||||
before_change()
|
||||
unmount_volume(config)
|
||||
after_change()
|
||||
return 'ephemeral'
|
||||
else:
|
||||
# persistent storage
|
||||
if os.path.ismount(config['mountpoint']):
|
||||
mounts = dict(managed_mounts())
|
||||
if mounts.get(config['mountpoint']) != config['device']:
|
||||
before_change()
|
||||
unmount_volume(config)
|
||||
mount_volume(config)
|
||||
after_change()
|
||||
else:
|
||||
before_change()
|
||||
mount_volume(config)
|
||||
after_change()
|
||||
return config['mountpoint']
|
|
@ -100,9 +100,10 @@ from heat_context import (
|
|||
HEAT_PATH,
|
||||
)
|
||||
|
||||
from charmhelpers.contrib.charmsupport import nrpe
|
||||
from charmhelpers.contrib.hardening.harden import harden
|
||||
from charmhelpers.contrib.openstack.context import ADDRESS_TYPES
|
||||
from charmhelpers.payload.execd import execd_preinstall
|
||||
from charmhelpers.contrib.hardening.harden import harden
|
||||
|
||||
from charmhelpers.contrib.openstack.cert_utils import (
|
||||
get_certificate_request,
|
||||
|
@ -164,6 +165,7 @@ def config_changed():
|
|||
|
||||
CONFIGS.write_all()
|
||||
configure_https()
|
||||
update_nrpe_config()
|
||||
|
||||
for rid in relation_ids('cluster'):
|
||||
cluster_joined(relation_id=rid)
|
||||
|
@ -206,6 +208,8 @@ def upgrade_charm():
|
|||
# now we just delete the file
|
||||
os.remove(encryption_path)
|
||||
leader_elected()
|
||||
update_nrpe_config()
|
||||
|
||||
# call the policy overrides handler which will install any policy overrides
|
||||
policyd.maybe_do_policyd_overrides(
|
||||
os_release('heat-common'),
|
||||
|
@ -440,6 +444,20 @@ def post_series_upgrade():
|
|||
resume_unit_helper, CONFIGS)
|
||||
|
||||
|
||||
@hooks.hook('nrpe-external-master-relation-joined',
|
||||
'nrpe-external-master-relation-changed')
|
||||
def update_nrpe_config():
|
||||
# python-dbus is used by check_upstart_job
|
||||
apt_install('python-dbus')
|
||||
hostname = nrpe.get_nagios_hostname()
|
||||
current_unit = nrpe.get_nagios_unit_name()
|
||||
nrpe_setup = nrpe.NRPE(hostname=hostname)
|
||||
nrpe.copy_nrpe_checks()
|
||||
nrpe.add_init_service_checks(nrpe_setup, services(), current_unit)
|
||||
nrpe.add_haproxy_checks(nrpe_setup, current_unit)
|
||||
nrpe_setup.write()
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
hooks.execute(sys.argv)
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
heat_relations.py
|
|
@ -0,0 +1 @@
|
|||
heat_relations.py
|
|
@ -18,6 +18,10 @@ extra-bindings:
|
|||
public:
|
||||
admin:
|
||||
internal:
|
||||
provides:
|
||||
nrpe-external-master:
|
||||
interface: nrpe-external-master
|
||||
scope: container
|
||||
requires:
|
||||
shared-db:
|
||||
interface: mysql-shared
|
||||
|
|
|
@ -111,10 +111,12 @@ class HeatRelationTests(CharmTestCase):
|
|||
@patch.object(relations, 'configure_https')
|
||||
@patch.object(relations.policyd,
|
||||
'maybe_do_policyd_overrides_on_config_changed')
|
||||
@patch.object(relations, 'update_nrpe_config')
|
||||
def test_config_changed_no_upgrade(
|
||||
self,
|
||||
mock_update_nrpe_config,
|
||||
maybe_do_policyd_overrides_on_config_changed,
|
||||
mock_configure_https
|
||||
mock_configure_https,
|
||||
):
|
||||
self.openstack_upgrade_available.return_value = False
|
||||
relations.config_changed()
|
||||
|
@ -122,8 +124,10 @@ class HeatRelationTests(CharmTestCase):
|
|||
@patch.object(relations, 'configure_https')
|
||||
@patch.object(relations.policyd,
|
||||
'maybe_do_policyd_overrides_on_config_changed')
|
||||
@patch.object(relations, 'update_nrpe_config')
|
||||
def test_config_changed_with_upgrade(
|
||||
self,
|
||||
mock_update_nrpe_config,
|
||||
maybe_do_policyd_overrides_on_config_changed,
|
||||
mock_configure_https
|
||||
):
|
||||
|
@ -134,8 +138,10 @@ class HeatRelationTests(CharmTestCase):
|
|||
@patch.object(relations, 'configure_https')
|
||||
@patch.object(relations.policyd,
|
||||
'maybe_do_policyd_overrides_on_config_changed')
|
||||
@patch.object(relations, 'update_nrpe_config')
|
||||
def test_config_changed_with_openstack_upgrade_action(
|
||||
self,
|
||||
mock_update_nrpe_config,
|
||||
maybe_do_policyd_overrides_on_config_changed,
|
||||
mock_configure_https
|
||||
):
|
||||
|
@ -150,7 +156,9 @@ class HeatRelationTests(CharmTestCase):
|
|||
@patch('os.remove')
|
||||
@patch.object(relations, 'leader_elected')
|
||||
@patch.object(relations.policyd, 'maybe_do_policyd_overrides')
|
||||
@patch.object(relations, 'update_nrpe_config')
|
||||
def test_upgrade_charm(self,
|
||||
mock_update_nrpe_config,
|
||||
mock_maybe_do_policyd_overrides,
|
||||
leader_elected, os_remove, os_path_isfile):
|
||||
os_path_isfile.return_value = False
|
||||
|
@ -353,3 +361,30 @@ class HeatRelationTests(CharmTestCase):
|
|||
relations.ha_joined(relation_id='rid:23')
|
||||
self.relation_set.assert_called_once_with(
|
||||
relation_id='rid:23', rel_data='data')
|
||||
|
||||
@patch.object(relations, "apt_install")
|
||||
@patch.object(relations.nrpe, "get_nagios_hostname")
|
||||
@patch.object(relations.nrpe, "get_nagios_unit_name")
|
||||
@patch.object(relations.nrpe, "NRPE")
|
||||
@patch.object(relations.nrpe, "copy_nrpe_checks")
|
||||
@patch.object(relations.nrpe, "add_init_service_checks")
|
||||
@patch.object(relations.nrpe, "add_haproxy_checks")
|
||||
def test_update_nrpe_config(self, mock_add_haproxy, mock_add_init_svc,
|
||||
mock_copy_nrpe_checks, mock_nrpe,
|
||||
mock_nagios_unitname, mock_nagios_hostname,
|
||||
mock_apt_install):
|
||||
relations.update_nrpe_config()
|
||||
mock_apt_install.assert_called_once_with("python-dbus")
|
||||
mock_nagios_hostname.assert_called_once_with()
|
||||
mock_nagios_unitname.assert_called_once_with()
|
||||
mock_nrpe.assert_called_once_with(
|
||||
hostname=mock_nagios_hostname.return_value)
|
||||
mock_copy_nrpe_checks.assert_called_once_with()
|
||||
mock_add_init_svc.assert_called_once_with(
|
||||
mock_nrpe.return_value,
|
||||
relations.services(),
|
||||
mock_nagios_unitname.return_value,
|
||||
)
|
||||
mock_add_haproxy.assert_called_once_with(
|
||||
mock_nrpe.return_value, mock_nagios_unitname.return_value)
|
||||
mock_nrpe.return_value.write.assert_called_once_with()
|
||||
|
|
Loading…
Reference in New Issue