[bradm] Really fix text conflicts
This commit is contained in:
commit
20c8ccda19
|
@ -8,3 +8,4 @@ include:
|
||||||
- payload.execd
|
- payload.execd
|
||||||
- contrib.network.ip
|
- contrib.network.ip
|
||||||
- contrib.database
|
- contrib.database
|
||||||
|
- contrib.charmsupport
|
||||||
|
|
17
config.yaml
17
config.yaml
|
@ -77,4 +77,19 @@ options:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: False
|
default: False
|
||||||
description: Adds two config options (wsrep_drupal_282555_workaround and wsrep_retry_autocommit) as a workaround for Percona Primary Key bug (see lplp1366997).
|
description: Adds two config options (wsrep_drupal_282555_workaround and wsrep_retry_autocommit) as a workaround for Percona Primary Key bug (see lplp1366997).
|
||||||
|
nagios_context:
|
||||||
|
default: "juju"
|
||||||
|
type: string
|
||||||
|
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'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
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Copyright 2014-2015 Canonical Limited.
|
||||||
|
#
|
||||||
|
# This file is part of charm-helpers.
|
||||||
|
#
|
||||||
|
# charm-helpers is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||||
|
# published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# charm-helpers is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
|
@ -0,0 +1,358 @@
|
||||||
|
# Copyright 2014-2015 Canonical Limited.
|
||||||
|
#
|
||||||
|
# This file is part of charm-helpers.
|
||||||
|
#
|
||||||
|
# charm-helpers is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||||
|
# published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# charm-helpers is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""Compatibility with the nrpe-external-master charm"""
|
||||||
|
# Copyright 2012 Canonical Ltd.
|
||||||
|
#
|
||||||
|
# Authors:
|
||||||
|
# Matthew Wedgwood <matthew.wedgwood@canonical.com>
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import pwd
|
||||||
|
import grp
|
||||||
|
import os
|
||||||
|
import glob
|
||||||
|
import shutil
|
||||||
|
import re
|
||||||
|
import shlex
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from charmhelpers.core.hookenv import (
|
||||||
|
config,
|
||||||
|
local_unit,
|
||||||
|
log,
|
||||||
|
relation_ids,
|
||||||
|
relation_set,
|
||||||
|
relations_of_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
from charmhelpers.core.host import service
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
#
|
||||||
|
# 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 _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 write(self, nagios_context, hostname, nagios_servicegroups):
|
||||||
|
nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(
|
||||||
|
self.command)
|
||||||
|
with open(nrpe_check_file, 'w') as nrpe_check_config:
|
||||||
|
nrpe_check_config.write("# check {}\n".format(self.shortname))
|
||||||
|
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):
|
||||||
|
for f in os.listdir(NRPE.nagios_exportdir):
|
||||||
|
if re.search('.*{}.cfg'.format(self.command), f):
|
||||||
|
os.remove(os.path.join(NRPE.nagios_exportdir, f))
|
||||||
|
|
||||||
|
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 = '{}/service__{}_{}.cfg'.format(
|
||||||
|
NRPE.nagios_exportdir, hostname, self.command)
|
||||||
|
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'
|
||||||
|
|
||||||
|
def __init__(self, hostname=None):
|
||||||
|
super(NRPE, self).__init__()
|
||||||
|
self.config = config()
|
||||||
|
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:
|
||||||
|
self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
|
||||||
|
self.checks = []
|
||||||
|
|
||||||
|
def add_check(self, *args, **kwargs):
|
||||||
|
self.checks.append(Check(*args, **kwargs))
|
||||||
|
|
||||||
|
def write(self):
|
||||||
|
try:
|
||||||
|
nagios_uid = pwd.getpwnam('nagios').pw_uid
|
||||||
|
nagios_gid = grp.getgrnam('nagios').gr_gid
|
||||||
|
except:
|
||||||
|
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}}}
|
||||||
|
for nrpecheck in self.checks:
|
||||||
|
nrpecheck.write(self.nagios_context, self.hostname,
|
||||||
|
self.nagios_servicegroups)
|
||||||
|
nrpe_monitors[nrpecheck.shortname] = {
|
||||||
|
"command": nrpecheck.command,
|
||||||
|
}
|
||||||
|
|
||||||
|
service('restart', 'nagios-nrpe-server')
|
||||||
|
|
||||||
|
for rid in relation_ids("local-monitors"):
|
||||||
|
relation_set(relation_id=rid, monitors=yaml.dump(monitors))
|
||||||
|
|
||||||
|
|
||||||
|
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_hostname' 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):
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
for svc in services:
|
||||||
|
upstart_init = '/etc/init/%s.conf' % svc
|
||||||
|
sysv_init = '/etc/init.d/%s' % svc
|
||||||
|
if 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
|
||||||
|
cron_file = ('*/5 * * * * root '
|
||||||
|
'/usr/local/lib/nagios/plugins/check_exit_status.pl '
|
||||||
|
'-s /etc/init.d/%s status > '
|
||||||
|
'/var/lib/nagios/service-check-%s.txt\n' % (svc,
|
||||||
|
svc)
|
||||||
|
)
|
||||||
|
f = open(cronpath, 'w')
|
||||||
|
f.write(cron_file)
|
||||||
|
f.close()
|
||||||
|
nrpe.add_check(
|
||||||
|
shortname=svc,
|
||||||
|
description='process check {%s}' % unit_name,
|
||||||
|
check_cmd='check_status_file.py -f '
|
||||||
|
'/var/lib/nagios/service-check-%s.txt' % svc,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_nrpe_checks():
|
||||||
|
"""
|
||||||
|
Copy the nrpe checks into place
|
||||||
|
|
||||||
|
"""
|
||||||
|
NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
|
||||||
|
nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks',
|
||||||
|
'charmhelpers', 'contrib', 'openstack',
|
||||||
|
'files')
|
||||||
|
|
||||||
|
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')
|
|
@ -0,0 +1,175 @@
|
||||||
|
# Copyright 2014-2015 Canonical Limited.
|
||||||
|
#
|
||||||
|
# This file is part of charm-helpers.
|
||||||
|
#
|
||||||
|
# charm-helpers is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||||
|
# published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# charm-helpers is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
'''
|
||||||
|
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']
|
|
@ -48,6 +48,9 @@ from charmhelpers.core.hookenv import (
|
||||||
from charmhelpers.core.decorators import (
|
from charmhelpers.core.decorators import (
|
||||||
retry_on_exception,
|
retry_on_exception,
|
||||||
)
|
)
|
||||||
|
from charmhelpers.core.strutils import (
|
||||||
|
bool_from_string,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class HAIncompleteConfig(Exception):
|
class HAIncompleteConfig(Exception):
|
||||||
|
@ -164,7 +167,8 @@ def https():
|
||||||
.
|
.
|
||||||
returns: boolean
|
returns: boolean
|
||||||
'''
|
'''
|
||||||
if config_get('use-https') == "yes":
|
use_https = config_get('use-https')
|
||||||
|
if use_https and bool_from_string(use_https):
|
||||||
return True
|
return True
|
||||||
if config_get('ssl_cert') and config_get('ssl_key'):
|
if config_get('ssl_cert') and config_get('ssl_key'):
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -17,11 +17,11 @@
|
||||||
# You should have received a copy of the GNU Lesser General Public License
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
|
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
|
||||||
|
|
||||||
|
|
||||||
class Fstab(io.FileIO):
|
class Fstab(io.FileIO):
|
||||||
"""This class extends file in order to implement a file reader/writer
|
"""This class extends file in order to implement a file reader/writer
|
||||||
|
@ -77,7 +77,7 @@ class Fstab(io.FileIO):
|
||||||
for line in self.readlines():
|
for line in self.readlines():
|
||||||
line = line.decode('us-ascii')
|
line = line.decode('us-ascii')
|
||||||
try:
|
try:
|
||||||
if line.strip() and not line.startswith("#"):
|
if line.strip() and not line.strip().startswith("#"):
|
||||||
yield self._hydrate_entry(line)
|
yield self._hydrate_entry(line)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
@ -104,7 +104,7 @@ class Fstab(io.FileIO):
|
||||||
|
|
||||||
found = False
|
found = False
|
||||||
for index, line in enumerate(lines):
|
for index, line in enumerate(lines):
|
||||||
if not line.startswith("#"):
|
if line.strip() and not line.strip().startswith("#"):
|
||||||
if self._hydrate_entry(line) == entry:
|
if self._hydrate_entry(line) == entry:
|
||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
|
|
|
@ -305,11 +305,11 @@ def restart_on_change(restart_map, stopstart=False):
|
||||||
ceph_client_changed function.
|
ceph_client_changed function.
|
||||||
"""
|
"""
|
||||||
def wrap(f):
|
def wrap(f):
|
||||||
def wrapped_f(*args):
|
def wrapped_f(*args, **kwargs):
|
||||||
checksums = {}
|
checksums = {}
|
||||||
for path in restart_map:
|
for path in restart_map:
|
||||||
checksums[path] = file_hash(path)
|
checksums[path] = file_hash(path)
|
||||||
f(*args)
|
f(*args, **kwargs)
|
||||||
restarts = []
|
restarts = []
|
||||||
for path in restart_map:
|
for path in restart_map:
|
||||||
if checksums[path] != file_hash(path):
|
if checksums[path] != file_hash(path):
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2014-2015 Canonical Limited.
|
||||||
|
#
|
||||||
|
# This file is part of charm-helpers.
|
||||||
|
#
|
||||||
|
# charm-helpers is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||||
|
# published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# charm-helpers is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
|
||||||
|
def bool_from_string(value):
|
||||||
|
"""Interpret string value as boolean.
|
||||||
|
|
||||||
|
Returns True if value translates to True otherwise False.
|
||||||
|
"""
|
||||||
|
if isinstance(value, six.string_types):
|
||||||
|
value = six.text_type(value)
|
||||||
|
else:
|
||||||
|
msg = "Unable to interpret non-string value '%s' as boolean" % (value)
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
value = value.strip().lower()
|
||||||
|
|
||||||
|
if value in ['y', 'yes', 'true', 't']:
|
||||||
|
return True
|
||||||
|
elif value in ['n', 'no', 'false', 'f']:
|
||||||
|
return False
|
||||||
|
|
||||||
|
msg = "Unable to interpret string value '%s' as boolean" % (value)
|
||||||
|
raise ValueError(msg)
|
|
@ -17,8 +17,6 @@
|
||||||
# You should have received a copy of the GNU Lesser General Public License
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
|
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from subprocess import check_call
|
from subprocess import check_call
|
||||||
|
@ -29,6 +27,8 @@ from charmhelpers.core.hookenv import (
|
||||||
ERROR,
|
ERROR,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
|
||||||
|
|
||||||
|
|
||||||
def create(sysctl_dict, sysctl_file):
|
def create(sysctl_dict, sysctl_file):
|
||||||
"""Creates a sysctl.conf file from a YAML associative array
|
"""Creates a sysctl.conf file from a YAML associative array
|
||||||
|
|
|
@ -0,0 +1,477 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright 2014-2015 Canonical Limited.
|
||||||
|
#
|
||||||
|
# This file is part of charm-helpers.
|
||||||
|
#
|
||||||
|
# charm-helpers is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3 as
|
||||||
|
# published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# charm-helpers is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Authors:
|
||||||
|
# Kapil Thangavelu <kapil.foss@gmail.com>
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
Intro
|
||||||
|
-----
|
||||||
|
|
||||||
|
A simple way to store state in units. This provides a key value
|
||||||
|
storage with support for versioned, transactional operation,
|
||||||
|
and can calculate deltas from previous values to simplify unit logic
|
||||||
|
when processing changes.
|
||||||
|
|
||||||
|
|
||||||
|
Hook Integration
|
||||||
|
----------------
|
||||||
|
|
||||||
|
There are several extant frameworks for hook execution, including
|
||||||
|
|
||||||
|
- charmhelpers.core.hookenv.Hooks
|
||||||
|
- charmhelpers.core.services.ServiceManager
|
||||||
|
|
||||||
|
The storage classes are framework agnostic, one simple integration is
|
||||||
|
via the HookData contextmanager. It will record the current hook
|
||||||
|
execution environment (including relation data, config data, etc.),
|
||||||
|
setup a transaction and allow easy access to the changes from
|
||||||
|
previously seen values. One consequence of the integration is the
|
||||||
|
reservation of particular keys ('rels', 'unit', 'env', 'config',
|
||||||
|
'charm_revisions') for their respective values.
|
||||||
|
|
||||||
|
Here's a fully worked integration example using hookenv.Hooks::
|
||||||
|
|
||||||
|
from charmhelper.core import hookenv, unitdata
|
||||||
|
|
||||||
|
hook_data = unitdata.HookData()
|
||||||
|
db = unitdata.kv()
|
||||||
|
hooks = hookenv.Hooks()
|
||||||
|
|
||||||
|
@hooks.hook
|
||||||
|
def config_changed():
|
||||||
|
# Print all changes to configuration from previously seen
|
||||||
|
# values.
|
||||||
|
for changed, (prev, cur) in hook_data.conf.items():
|
||||||
|
print('config changed', changed,
|
||||||
|
'previous value', prev,
|
||||||
|
'current value', cur)
|
||||||
|
|
||||||
|
# Get some unit specific bookeeping
|
||||||
|
if not db.get('pkg_key'):
|
||||||
|
key = urllib.urlopen('https://example.com/pkg_key').read()
|
||||||
|
db.set('pkg_key', key)
|
||||||
|
|
||||||
|
# Directly access all charm config as a mapping.
|
||||||
|
conf = db.getrange('config', True)
|
||||||
|
|
||||||
|
# Directly access all relation data as a mapping
|
||||||
|
rels = db.getrange('rels', True)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
with hook_data():
|
||||||
|
hook.execute()
|
||||||
|
|
||||||
|
|
||||||
|
A more basic integration is via the hook_scope context manager which simply
|
||||||
|
manages transaction scope (and records hook name, and timestamp)::
|
||||||
|
|
||||||
|
>>> from unitdata import kv
|
||||||
|
>>> db = kv()
|
||||||
|
>>> with db.hook_scope('install'):
|
||||||
|
... # do work, in transactional scope.
|
||||||
|
... db.set('x', 1)
|
||||||
|
>>> db.get('x')
|
||||||
|
1
|
||||||
|
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
Values are automatically json de/serialized to preserve basic typing
|
||||||
|
and complex data struct capabilities (dicts, lists, ints, booleans, etc).
|
||||||
|
|
||||||
|
Individual values can be manipulated via get/set::
|
||||||
|
|
||||||
|
>>> kv.set('y', True)
|
||||||
|
>>> kv.get('y')
|
||||||
|
True
|
||||||
|
|
||||||
|
# We can set complex values (dicts, lists) as a single key.
|
||||||
|
>>> kv.set('config', {'a': 1, 'b': True'})
|
||||||
|
|
||||||
|
# Also supports returning dictionaries as a record which
|
||||||
|
# provides attribute access.
|
||||||
|
>>> config = kv.get('config', record=True)
|
||||||
|
>>> config.b
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
Groups of keys can be manipulated with update/getrange::
|
||||||
|
|
||||||
|
>>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
|
||||||
|
>>> kv.getrange('gui.', strip=True)
|
||||||
|
{'z': 1, 'y': 2}
|
||||||
|
|
||||||
|
When updating values, its very helpful to understand which values
|
||||||
|
have actually changed and how have they changed. The storage
|
||||||
|
provides a delta method to provide for this::
|
||||||
|
|
||||||
|
>>> data = {'debug': True, 'option': 2}
|
||||||
|
>>> delta = kv.delta(data, 'config.')
|
||||||
|
>>> delta.debug.previous
|
||||||
|
None
|
||||||
|
>>> delta.debug.current
|
||||||
|
True
|
||||||
|
>>> delta
|
||||||
|
{'debug': (None, True), 'option': (None, 2)}
|
||||||
|
|
||||||
|
Note the delta method does not persist the actual change, it needs to
|
||||||
|
be explicitly saved via 'update' method::
|
||||||
|
|
||||||
|
>>> kv.update(data, 'config.')
|
||||||
|
|
||||||
|
Values modified in the context of a hook scope retain historical values
|
||||||
|
associated to the hookname.
|
||||||
|
|
||||||
|
>>> with db.hook_scope('config-changed'):
|
||||||
|
... db.set('x', 42)
|
||||||
|
>>> db.gethistory('x')
|
||||||
|
[(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
|
||||||
|
(2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import contextlib
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pprint
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
|
||||||
|
__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
|
||||||
|
|
||||||
|
|
||||||
|
class Storage(object):
|
||||||
|
"""Simple key value database for local unit state within charms.
|
||||||
|
|
||||||
|
Modifications are automatically committed at hook exit. That's
|
||||||
|
currently regardless of exit code.
|
||||||
|
|
||||||
|
To support dicts, lists, integer, floats, and booleans values
|
||||||
|
are automatically json encoded/decoded.
|
||||||
|
"""
|
||||||
|
def __init__(self, path=None):
|
||||||
|
self.db_path = path
|
||||||
|
if path is None:
|
||||||
|
self.db_path = os.path.join(
|
||||||
|
os.environ.get('CHARM_DIR', ''), '.unit-state.db')
|
||||||
|
self.conn = sqlite3.connect('%s' % self.db_path)
|
||||||
|
self.cursor = self.conn.cursor()
|
||||||
|
self.revision = None
|
||||||
|
self._closed = False
|
||||||
|
self._init()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self._closed:
|
||||||
|
return
|
||||||
|
self.flush(False)
|
||||||
|
self.cursor.close()
|
||||||
|
self.conn.close()
|
||||||
|
self._closed = True
|
||||||
|
|
||||||
|
def _scoped_query(self, stmt, params=None):
|
||||||
|
if params is None:
|
||||||
|
params = []
|
||||||
|
return stmt, params
|
||||||
|
|
||||||
|
def get(self, key, default=None, record=False):
|
||||||
|
self.cursor.execute(
|
||||||
|
*self._scoped_query(
|
||||||
|
'select data from kv where key=?', [key]))
|
||||||
|
result = self.cursor.fetchone()
|
||||||
|
if not result:
|
||||||
|
return default
|
||||||
|
if record:
|
||||||
|
return Record(json.loads(result[0]))
|
||||||
|
return json.loads(result[0])
|
||||||
|
|
||||||
|
def getrange(self, key_prefix, strip=False):
|
||||||
|
stmt = "select key, data from kv where key like '%s%%'" % key_prefix
|
||||||
|
self.cursor.execute(*self._scoped_query(stmt))
|
||||||
|
result = self.cursor.fetchall()
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
return None
|
||||||
|
if not strip:
|
||||||
|
key_prefix = ''
|
||||||
|
return dict([
|
||||||
|
(k[len(key_prefix):], json.loads(v)) for k, v in result])
|
||||||
|
|
||||||
|
def update(self, mapping, prefix=""):
|
||||||
|
for k, v in mapping.items():
|
||||||
|
self.set("%s%s" % (prefix, k), v)
|
||||||
|
|
||||||
|
def unset(self, key):
|
||||||
|
self.cursor.execute('delete from kv where key=?', [key])
|
||||||
|
if self.revision and self.cursor.rowcount:
|
||||||
|
self.cursor.execute(
|
||||||
|
'insert into kv_revisions values (?, ?, ?)',
|
||||||
|
[key, self.revision, json.dumps('DELETED')])
|
||||||
|
|
||||||
|
def set(self, key, value):
|
||||||
|
serialized = json.dumps(value)
|
||||||
|
|
||||||
|
self.cursor.execute(
|
||||||
|
'select data from kv where key=?', [key])
|
||||||
|
exists = self.cursor.fetchone()
|
||||||
|
|
||||||
|
# Skip mutations to the same value
|
||||||
|
if exists:
|
||||||
|
if exists[0] == serialized:
|
||||||
|
return value
|
||||||
|
|
||||||
|
if not exists:
|
||||||
|
self.cursor.execute(
|
||||||
|
'insert into kv (key, data) values (?, ?)',
|
||||||
|
(key, serialized))
|
||||||
|
else:
|
||||||
|
self.cursor.execute('''
|
||||||
|
update kv
|
||||||
|
set data = ?
|
||||||
|
where key = ?''', [serialized, key])
|
||||||
|
|
||||||
|
# Save
|
||||||
|
if not self.revision:
|
||||||
|
return value
|
||||||
|
|
||||||
|
self.cursor.execute(
|
||||||
|
'select 1 from kv_revisions where key=? and revision=?',
|
||||||
|
[key, self.revision])
|
||||||
|
exists = self.cursor.fetchone()
|
||||||
|
|
||||||
|
if not exists:
|
||||||
|
self.cursor.execute(
|
||||||
|
'''insert into kv_revisions (
|
||||||
|
revision, key, data) values (?, ?, ?)''',
|
||||||
|
(self.revision, key, serialized))
|
||||||
|
else:
|
||||||
|
self.cursor.execute(
|
||||||
|
'''
|
||||||
|
update kv_revisions
|
||||||
|
set data = ?
|
||||||
|
where key = ?
|
||||||
|
and revision = ?''',
|
||||||
|
[serialized, key, self.revision])
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def delta(self, mapping, prefix):
|
||||||
|
"""
|
||||||
|
return a delta containing values that have changed.
|
||||||
|
"""
|
||||||
|
previous = self.getrange(prefix, strip=True)
|
||||||
|
if not previous:
|
||||||
|
pk = set()
|
||||||
|
else:
|
||||||
|
pk = set(previous.keys())
|
||||||
|
ck = set(mapping.keys())
|
||||||
|
delta = DeltaSet()
|
||||||
|
|
||||||
|
# added
|
||||||
|
for k in ck.difference(pk):
|
||||||
|
delta[k] = Delta(None, mapping[k])
|
||||||
|
|
||||||
|
# removed
|
||||||
|
for k in pk.difference(ck):
|
||||||
|
delta[k] = Delta(previous[k], None)
|
||||||
|
|
||||||
|
# changed
|
||||||
|
for k in pk.intersection(ck):
|
||||||
|
c = mapping[k]
|
||||||
|
p = previous[k]
|
||||||
|
if c != p:
|
||||||
|
delta[k] = Delta(p, c)
|
||||||
|
|
||||||
|
return delta
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def hook_scope(self, name=""):
|
||||||
|
"""Scope all future interactions to the current hook execution
|
||||||
|
revision."""
|
||||||
|
assert not self.revision
|
||||||
|
self.cursor.execute(
|
||||||
|
'insert into hooks (hook, date) values (?, ?)',
|
||||||
|
(name or sys.argv[0],
|
||||||
|
datetime.datetime.utcnow().isoformat()))
|
||||||
|
self.revision = self.cursor.lastrowid
|
||||||
|
try:
|
||||||
|
yield self.revision
|
||||||
|
self.revision = None
|
||||||
|
except:
|
||||||
|
self.flush(False)
|
||||||
|
self.revision = None
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
self.flush()
|
||||||
|
|
||||||
|
def flush(self, save=True):
|
||||||
|
if save:
|
||||||
|
self.conn.commit()
|
||||||
|
elif self._closed:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self.conn.rollback()
|
||||||
|
|
||||||
|
def _init(self):
|
||||||
|
self.cursor.execute('''
|
||||||
|
create table if not exists kv (
|
||||||
|
key text,
|
||||||
|
data text,
|
||||||
|
primary key (key)
|
||||||
|
)''')
|
||||||
|
self.cursor.execute('''
|
||||||
|
create table if not exists kv_revisions (
|
||||||
|
key text,
|
||||||
|
revision integer,
|
||||||
|
data text,
|
||||||
|
primary key (key, revision)
|
||||||
|
)''')
|
||||||
|
self.cursor.execute('''
|
||||||
|
create table if not exists hooks (
|
||||||
|
version integer primary key autoincrement,
|
||||||
|
hook text,
|
||||||
|
date text
|
||||||
|
)''')
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def gethistory(self, key, deserialize=False):
|
||||||
|
self.cursor.execute(
|
||||||
|
'''
|
||||||
|
select kv.revision, kv.key, kv.data, h.hook, h.date
|
||||||
|
from kv_revisions kv,
|
||||||
|
hooks h
|
||||||
|
where kv.key=?
|
||||||
|
and kv.revision = h.version
|
||||||
|
''', [key])
|
||||||
|
if deserialize is False:
|
||||||
|
return self.cursor.fetchall()
|
||||||
|
return map(_parse_history, self.cursor.fetchall())
|
||||||
|
|
||||||
|
def debug(self, fh=sys.stderr):
|
||||||
|
self.cursor.execute('select * from kv')
|
||||||
|
pprint.pprint(self.cursor.fetchall(), stream=fh)
|
||||||
|
self.cursor.execute('select * from kv_revisions')
|
||||||
|
pprint.pprint(self.cursor.fetchall(), stream=fh)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_history(d):
|
||||||
|
return (d[0], d[1], json.loads(d[2]), d[3],
|
||||||
|
datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
|
||||||
|
|
||||||
|
|
||||||
|
class HookData(object):
|
||||||
|
"""Simple integration for existing hook exec frameworks.
|
||||||
|
|
||||||
|
Records all unit information, and stores deltas for processing
|
||||||
|
by the hook.
|
||||||
|
|
||||||
|
Sample::
|
||||||
|
|
||||||
|
from charmhelper.core import hookenv, unitdata
|
||||||
|
|
||||||
|
changes = unitdata.HookData()
|
||||||
|
db = unitdata.kv()
|
||||||
|
hooks = hookenv.Hooks()
|
||||||
|
|
||||||
|
@hooks.hook
|
||||||
|
def config_changed():
|
||||||
|
# View all changes to configuration
|
||||||
|
for changed, (prev, cur) in changes.conf.items():
|
||||||
|
print('config changed', changed,
|
||||||
|
'previous value', prev,
|
||||||
|
'current value', cur)
|
||||||
|
|
||||||
|
# Get some unit specific bookeeping
|
||||||
|
if not db.get('pkg_key'):
|
||||||
|
key = urllib.urlopen('https://example.com/pkg_key').read()
|
||||||
|
db.set('pkg_key', key)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
with changes():
|
||||||
|
hook.execute()
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.kv = kv()
|
||||||
|
self.conf = None
|
||||||
|
self.rels = None
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def __call__(self):
|
||||||
|
from charmhelpers.core import hookenv
|
||||||
|
hook_name = hookenv.hook_name()
|
||||||
|
|
||||||
|
with self.kv.hook_scope(hook_name):
|
||||||
|
self._record_charm_version(hookenv.charm_dir())
|
||||||
|
delta_config, delta_relation = self._record_hook(hookenv)
|
||||||
|
yield self.kv, delta_config, delta_relation
|
||||||
|
|
||||||
|
def _record_charm_version(self, charm_dir):
|
||||||
|
# Record revisions.. charm revisions are meaningless
|
||||||
|
# to charm authors as they don't control the revision.
|
||||||
|
# so logic dependnent on revision is not particularly
|
||||||
|
# useful, however it is useful for debugging analysis.
|
||||||
|
charm_rev = open(
|
||||||
|
os.path.join(charm_dir, 'revision')).read().strip()
|
||||||
|
charm_rev = charm_rev or '0'
|
||||||
|
revs = self.kv.get('charm_revisions', [])
|
||||||
|
if charm_rev not in revs:
|
||||||
|
revs.append(charm_rev.strip() or '0')
|
||||||
|
self.kv.set('charm_revisions', revs)
|
||||||
|
|
||||||
|
def _record_hook(self, hookenv):
|
||||||
|
data = hookenv.execution_environment()
|
||||||
|
self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
|
||||||
|
self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
|
||||||
|
self.kv.set('env', data['env'])
|
||||||
|
self.kv.set('unit', data['unit'])
|
||||||
|
self.kv.set('relid', data.get('relid'))
|
||||||
|
return conf_delta, rels_delta
|
||||||
|
|
||||||
|
|
||||||
|
class Record(dict):
|
||||||
|
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
def __getattr__(self, k):
|
||||||
|
if k in self:
|
||||||
|
return self[k]
|
||||||
|
raise AttributeError(k)
|
||||||
|
|
||||||
|
|
||||||
|
class DeltaSet(Record):
|
||||||
|
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
|
||||||
|
Delta = collections.namedtuple('Delta', ['previous', 'current'])
|
||||||
|
|
||||||
|
|
||||||
|
_KV = None
|
||||||
|
|
||||||
|
|
||||||
|
def kv():
|
||||||
|
global _KV
|
||||||
|
if _KV is None:
|
||||||
|
_KV = Storage()
|
||||||
|
return _KV
|
|
@ -18,6 +18,16 @@ import os
|
||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from charmhelpers.fetch import (
|
||||||
|
BaseFetchHandler,
|
||||||
|
UnhandledSource
|
||||||
|
)
|
||||||
|
from charmhelpers.payload.archive import (
|
||||||
|
get_archive_handler,
|
||||||
|
extract,
|
||||||
|
)
|
||||||
|
from charmhelpers.core.host import mkdir, check_hash
|
||||||
|
|
||||||
import six
|
import six
|
||||||
if six.PY3:
|
if six.PY3:
|
||||||
from urllib.request import (
|
from urllib.request import (
|
||||||
|
@ -35,16 +45,6 @@ else:
|
||||||
)
|
)
|
||||||
from urlparse import urlparse, urlunparse, parse_qs
|
from urlparse import urlparse, urlunparse, parse_qs
|
||||||
|
|
||||||
from charmhelpers.fetch import (
|
|
||||||
BaseFetchHandler,
|
|
||||||
UnhandledSource
|
|
||||||
)
|
|
||||||
from charmhelpers.payload.archive import (
|
|
||||||
get_archive_handler,
|
|
||||||
extract,
|
|
||||||
)
|
|
||||||
from charmhelpers.core.host import mkdir, check_hash
|
|
||||||
|
|
||||||
|
|
||||||
def splituser(host):
|
def splituser(host):
|
||||||
'''urllib.splituser(), but six's support of this seems broken'''
|
'''urllib.splituser(), but six's support of this seems broken'''
|
||||||
|
|
|
@ -32,7 +32,7 @@ except ImportError:
|
||||||
apt_install("python-git")
|
apt_install("python-git")
|
||||||
from git import Repo
|
from git import Repo
|
||||||
|
|
||||||
from git.exc import GitCommandError
|
from git.exc import GitCommandError # noqa E402
|
||||||
|
|
||||||
|
|
||||||
class GitUrlFetchHandler(BaseFetchHandler):
|
class GitUrlFetchHandler(BaseFetchHandler):
|
||||||
|
|
|
@ -69,6 +69,8 @@ from charmhelpers.contrib.network.ip import (
|
||||||
is_address_in_network,
|
is_address_in_network,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from charmhelpers.contrib.charmsupport import nrpe
|
||||||
|
|
||||||
hooks = Hooks()
|
hooks = Hooks()
|
||||||
|
|
||||||
LEADER_RES = 'grp_percona_cluster'
|
LEADER_RES = 'grp_percona_cluster'
|
||||||
|
@ -426,6 +428,23 @@ def ha_relation_changed():
|
||||||
relation_clear(r_id)
|
relation_clear(r_id)
|
||||||
|
|
||||||
|
|
||||||
|
@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.add_init_service_checks(nrpe_setup, 'mysql', current_unit)
|
||||||
|
nrpe_setup.add_check(
|
||||||
|
shortname='mysql_proc',
|
||||||
|
description='Check MySQL process {%s}' % current_unit,
|
||||||
|
check_cmd='check_procs -c 1:1 -C mysqld'
|
||||||
|
)
|
||||||
|
nrpe_setup.write()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
try:
|
try:
|
||||||
hooks.execute(sys.argv)
|
hooks.execute(sys.argv)
|
||||||
|
|
Loading…
Reference in New Issue