Browse Source

[freyes,r=james-page] Ensure VIP is tied to a good mysqld instance.

Re-license charm as GPL-2 for compatibility with bundled ocf script.
changes/65/287065/1
James Page 5 years ago
parent
commit
23dd36ab79
25 changed files with 2144 additions and 15 deletions
  1. +3
    -0
      .bzrignore
  2. +8
    -0
      Makefile
  3. +5
    -0
      charm-helpers-tests.yaml
  4. +24
    -12
      copyright
  5. +35
    -3
      hooks/percona_hooks.py
  6. +17
    -0
      hooks/percona_utils.py
  7. +636
    -0
      ocf/percona/mysql_monitor
  8. +6
    -0
      setup.cfg
  9. +1
    -0
      templates/my.cnf
  10. +29
    -0
      tests/00-setup.sh
  11. +29
    -0
      tests/10-deploy_test.py
  12. +38
    -0
      tests/20-broken-mysqld.py
  13. +38
    -0
      tests/30-kill-9-mysqld.py
  14. +151
    -0
      tests/basic_deployment.py
  15. +38
    -0
      tests/charmhelpers/__init__.py
  16. +15
    -0
      tests/charmhelpers/contrib/__init__.py
  17. +15
    -0
      tests/charmhelpers/contrib/amulet/__init__.py
  18. +93
    -0
      tests/charmhelpers/contrib/amulet/deployment.py
  19. +316
    -0
      tests/charmhelpers/contrib/amulet/utils.py
  20. +15
    -0
      tests/charmhelpers/contrib/openstack/__init__.py
  21. +15
    -0
      tests/charmhelpers/contrib/openstack/amulet/__init__.py
  22. +137
    -0
      tests/charmhelpers/contrib/openstack/amulet/deployment.py
  23. +294
    -0
      tests/charmhelpers/contrib/openstack/amulet/utils.py
  24. +65
    -0
      unit_tests/test_percona_hooks.py
  25. +121
    -0
      unit_tests/test_utils.py

+ 3
- 0
.bzrignore View File

@ -2,3 +2,6 @@ bin
.coverage
.pydevproject
.project
*.pyc
*.pyo
__pycache__

+ 8
- 0
Makefile View File

@ -9,6 +9,13 @@ lint:
unit_test:
@$(PYTHON) /usr/bin/nosetests --nologcapture unit_tests
test:
@echo Starting amulet tests...
#NOTE(beisner): can remove -v after bug 1320357 is fixed
# https://bugs.launchpad.net/amulet/+bug/1320357
# @juju test -v -p AMULET_HTTP_PROXY,AMULET_OS_VIP --timeout 2700
echo "Tests disables; http://pad.lv/1446169"
bin/charm_helpers_sync.py:
@mkdir -p bin
@bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \
@ -16,6 +23,7 @@ bin/charm_helpers_sync.py:
sync: bin/charm_helpers_sync.py
@$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers.yaml
@$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-tests.yaml
publish: lint
bzr push lp:charms/trusty/percona-cluster

+ 5
- 0
charm-helpers-tests.yaml View File

@ -0,0 +1,5 @@
branch: lp:charm-helpers
destination: tests/charmhelpers
include:
- contrib.amulet
- contrib.openstack.amulet

+ 24
- 12
copyright View File

@ -2,16 +2,28 @@ Format: http://dep.debian.net/deps/dep5/
Files: *
Copyright: Copyright 2011, Canonical Ltd., All Rights Reserved.
License: GPL-3
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
.
This program 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 General Public License for more details.
.
License: GPL-2
Files: ocf/percona/mysql_monitor
Copyright: Copyright (c) 2013, Percona inc., Yves Trudeau, Michael Coburn
License: GPL-2
License: GPL-2
This program is free software; you can redistribute it and/or modify
it under the terms of version 2 of the GNU General Public License as
published by the Free Software Foundation.
This program is distributed in the hope that it would be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Further, this software is distributed without any warranty that it is
free of the rightful claim of any third person regarding infringement
or the like. Any license provided herein, whether implied or
otherwise, applies only to this software file. Patent licenses, if
any, provided herein do not apply to combinations of this program with
other software, or any other product whatsoever.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
along with this program; if not, write the Free Software Foundation,
Inc., 59 Temple Place - Suite 330, Boston MA 02111-1307, USA.

+ 35
- 3
hooks/percona_hooks.py View File

@ -50,6 +50,7 @@ from percona_utils import (
assert_charm_supports_ipv6,
unit_sorted,
get_db_helper,
install_mysql_ocf,
)
from charmhelpers.contrib.database.mysql import (
PerconaClusterHelper,
@ -72,6 +73,13 @@ from charmhelpers.contrib.network.ip import (
hooks = Hooks()
LEADER_RES = 'grp_percona_cluster'
RES_MONITOR_PARAMS = ('params user="sstuser" password="%(sstpass)s" '
'pid="/var/run/mysqld/mysqld.pid" '
'socket="/var/run/mysqld/mysqld.sock" '
'max_slave_lag="5" '
'cluster_type="pxc" '
'op monitor interval="1s" timeout="30s" '
'OCF_CHECK_LEVEL="1"')
@hooks.hook('install')
@ -155,6 +163,13 @@ def config_changed():
for unit in related_units(r_id):
shared_db_changed(r_id, unit)
# (re)install pcmkr agent
install_mysql_ocf()
if relation_ids('ha'):
# make sure all the HA resources are (re)created
ha_relation_joined()
@hooks.hook('cluster-relation-joined')
def cluster_joined(relation_id=None):
@ -387,17 +402,34 @@ def ha_relation_joined():
vip_params = 'params ip="%s" cidr_netmask="%s" nic="%s"' % \
(vip, vip_cidr, vip_iface)
resources = {'res_mysql_vip': res_mysql_vip}
resource_params = {'res_mysql_vip': vip_params}
resources = {'res_mysql_vip': res_mysql_vip,
'res_mysql_monitor': 'ocf:percona:mysql_monitor'}
db_helper = get_db_helper()
cfg_passwd = config('sst-password')
sstpsswd = db_helper.get_mysql_password(username='sstuser',
password=cfg_passwd)
resource_params = {'res_mysql_vip': vip_params,
'res_mysql_monitor':
RES_MONITOR_PARAMS % {'sstpass': sstpsswd}}
groups = {'grp_percona_cluster': 'res_mysql_vip'}
clones = {'cl_mysql_monitor': 'res_mysql_monitor meta interleave=true'}
colocations = {'vip_mysqld': 'inf: grp_percona_cluster cl_mysql_monitor'}
locations = {'loc_percona_cluster':
'grp_percona_cluster rule inf: writable eq 1'}
for rel_id in relation_ids('ha'):
relation_set(relation_id=rel_id,
corosync_bindiface=corosync_bindiface,
corosync_mcastport=corosync_mcastport,
resources=resources,
resource_params=resource_params,
groups=groups)
groups=groups,
clones=clones,
colocations=colocations,
locations=locations)
@hooks.hook('ha-relation-changed')


+ 17
- 0
hooks/percona_utils.py View File

@ -4,10 +4,12 @@ from subprocess import Popen, PIPE
import socket
import tempfile
import os
import shutil
from charmhelpers.core.host import (
lsb_release
)
from charmhelpers.core.hookenv import (
charm_dir,
unit_get,
relation_ids,
related_units,
@ -229,3 +231,18 @@ def unit_sorted(units):
"""Return a sorted list of unit names."""
return sorted(
units, lambda a, b: cmp(int(a.split('/')[-1]), int(b.split('/')[-1])))
def install_mysql_ocf():
dest_dir = '/usr/lib/ocf/resource.d/percona/'
for fname in ['ocf/percona/mysql_monitor']:
src_file = os.path.join(charm_dir(), fname)
if not os.path.isdir(dest_dir):
os.makedirs(dest_dir)
dest_file = os.path.join(dest_dir, os.path.basename(src_file))
if not os.path.exists(dest_file):
log('Installing %s' % dest_file, level='INFO')
shutil.copy(src_file, dest_file)
else:
log("'%s' already exists, skipping" % dest_file, level='INFO')

+ 636
- 0
ocf/percona/mysql_monitor View File

@ -0,0 +1,636 @@
#!/bin/bash
#
#
# MySQL_Monitor agent, set writeable and readable attributes based on the
# state of the local MySQL, running and read_only or not. The agent basis is
# the original "Dummy" agent written by Lars Marowsky-Brée and part of the
# Pacemaker distribution. Many functions are from mysql_prm.
#
#
# Copyright (c) 2013, Percona inc., Yves Trudeau, Michael Coburn
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it would be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# Further, this software is distributed without any warranty that it is
# free of the rightful claim of any third person regarding infringement
# or the like. Any license provided herein, whether implied or
# otherwise, applies only to this software file. Patent licenses, if
# any, provided herein do not apply to combinations of this program with
# other software, or any other product whatsoever.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write the Free Software Foundation,
# Inc., 59 Temple Place - Suite 330, Boston MA 02111-1307, USA.
#
# Version: 20131119163921
#
# See usage() function below for more details...
#
# OCF instance parameters:
#
# OCF_RESKEY_state
# OCF_RESKEY_user
# OCF_RESKEY_password
# OCF_RESKEY_client_binary
# OCF_RESKEY_pid
# OCF_RESKEY_socket
# OCF_RESKEY_reader_attribute
# OCF_RESKEY_reader_failcount
# OCF_RESKEY_writer_attribute
# OCF_RESKEY_max_slave_lag
# OCF_RESKEY_cluster_type
#
#######################################################################
# Initialization:
: ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/lib/heartbeat}
. ${OCF_FUNCTIONS_DIR}/ocf-shellfuncs
#######################################################################
HOSTOS=`uname`
if [ "X${HOSTOS}" = "XOpenBSD" ];then
OCF_RESKEY_client_binary_default="/usr/local/bin/mysql"
OCF_RESKEY_pid_default="/var/mysql/mysqld.pid"
OCF_RESKEY_socket_default="/var/run/mysql/mysql.sock"
else
OCF_RESKEY_client_binary_default="/usr/bin/mysql"
OCF_RESKEY_pid_default="/var/run/mysql/mysqld.pid"
OCF_RESKEY_socket_default="/var/lib/mysql/mysql.sock"
fi
OCF_RESKEY_reader_attribute_default="readable"
OCF_RESKEY_writer_attribute_default="writable"
OCF_RESKEY_reader_failcount_default="1"
OCF_RESKEY_user_default="root"
OCF_RESKEY_password_default=""
OCF_RESKEY_max_slave_lag_default="3600"
OCF_RESKEY_cluster_type_default="replication"
: ${OCF_RESKEY_state=${HA_RSCTMP}/mysql-monitor-${OCF_RESOURCE_INSTANCE}.state}
: ${OCF_RESKEY_client_binary=${OCF_RESKEY_client_binary_default}}
: ${OCF_RESKEY_pid=${OCF_RESKEY_pid_default}}
: ${OCF_RESKEY_socket=${OCF_RESKEY_socket_default}}
: ${OCF_RESKEY_reader_attribute=${OCF_RESKEY_reader_attribute_default}}
: ${OCF_RESKEY_reader_failcount=${OCF_RESKEY_reader_failcount_default}}
: ${OCF_RESKEY_writer_attribute=${OCF_RESKEY_writer_attribute_default}}
: ${OCF_RESKEY_user=${OCF_RESKEY_user_default}}
: ${OCF_RESKEY_password=${OCF_RESKEY_password_default}}
: ${OCF_RESKEY_max_slave_lag=${OCF_RESKEY_max_slave_lag_default}}
: ${OCF_RESKEY_cluster_type=${OCF_RESKEY_cluster_type_default}}
MYSQL="$OCF_RESKEY_client_binary -A -S $OCF_RESKEY_socket --connect_timeout=10 --user=$OCF_RESKEY_user --password=$OCF_RESKEY_password "
HOSTNAME=`uname -n`
CRM_ATTR="${HA_SBIN_DIR}/crm_attribute -N $HOSTNAME "
meta_data() {
cat <<END
<?xml version="1.0"?>
<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
<resource-agent name="mysql_monitor" version="0.9">
<version>1.0</version>
<longdesc lang="en">
This agent monitors the local MySQL instance and set the writable and readable
attributes according to what it finds. It checks if MySQL is running and if
it is read-only or not.
</longdesc>
<shortdesc lang="en">Agent monitoring mysql</shortdesc>
<parameters>
<parameter name="state" unique="1">
<longdesc lang="en">
Location to store the resource state in.
</longdesc>
<shortdesc lang="en">State file</shortdesc>
<content type="string" default="${HA_RSCTMP}/Mysql-monitor-${OCF_RESOURCE_INSTANCE}.state" />
</parameter>
<parameter name="user" unique="0">
<longdesc lang="en">
MySQL user to connect to the local MySQL instance to check the slave status and
if the read_only variable is set. It requires the replication client priviledge.
</longdesc>
<shortdesc lang="en">MySQL user</shortdesc>
<content type="string" default="${OCF_RESKEY_user_default}" />
</parameter>
<parameter name="password" unique="0">
<longdesc lang="en">
Password of the mysql user to connect to the local MySQL instance
</longdesc>
<shortdesc lang="en">MySQL password</shortdesc>
<content type="string" default="${OCF_RESKEY_password_default}" />
</parameter>
<parameter name="client_binary" unique="0">
<longdesc lang="en">
MySQL Client Binary path.
</longdesc>
<shortdesc lang="en">MySQL client binary path</shortdesc>
<content type="string" default="${OCF_RESKEY_client_binary_default}" />
</parameter>
<parameter name="socket" unique="0">
<longdesc lang="en">
Unix socket to use in order to connect to MySQL on the host
</longdesc>
<shortdesc lang="en">MySQL socket</shortdesc>
<content type="string" default="${OCF_RESKEY_socket_default}" />
</parameter>
<parameter name="pid" unique="0">
<longdesc lang="en">
MySQL pid file, used to verify MySQL is running.
</longdesc>
<shortdesc lang="en">MySQL pid file</shortdesc>
<content type="string" default="${OCF_RESKEY_pid_default}" />
</parameter>
<parameter name="reader_attribute" unique="0">
<longdesc lang="en">
The reader attribute in the cib that can be used by location rules to allow or not
reader VIPs on a host.
</longdesc>
<shortdesc lang="en">Reader attribute</shortdesc>
<content type="string" default="${OCF_RESKEY_reader_attribute_default}" />
</parameter>
<parameter name="writer_attribute" unique="0">
<longdesc lang="en">
The reader attribute in the cib that can be used by location rules to allow or not
reader VIPs on a host.
</longdesc>
<shortdesc lang="en">Writer attribute</shortdesc>
<content type="string" default="${OCF_RESKEY_writer_attribute_default}" />
</parameter>
<parameter name="max_slave_lag" unique="0" required="0">
<longdesc lang="en">
The maximum number of seconds a replication slave is allowed to lag
behind its master in order to have a reader VIP on it.
</longdesc>
<shortdesc lang="en">Maximum time (seconds) a MySQL slave is allowed
to lag behind a master</shortdesc>
<content type="integer" default="${OCF_RESKEY_max_slave_lag_default}"/>
</parameter>
<parameter name="cluster_type" unique="0" required="0">
<longdesc lang="en">
Type of cluster, three possible values: pxc, replication, read-only. "pxc" is
for Percona XtraDB cluster, it uses the clustercheck script and set the
reader_attribute and writer_attribute according to the return code.
"replication" checks the read-only state and the slave status, only writable
node(s) will get the writer_attribute (and the reader_attribute) and on the
read-only nodes, replication status will be checked and the reader_attribute set
according to the state. "read-only" will just check if the read-only variable,
if read/write, it will get both the writer_attribute and reader_attribute set, if
read-only it will get only the reader_attribute.
</longdesc>
<shortdesc lang="en">Type of cluster</shortdesc>
<content type="string" default="${OCF_RESKEY_cluster_type_default}"/>
</parameter>
</parameters>
<actions>
<action name="start" timeout="20" />
<action name="stop" timeout="20" />
<action name="monitor" timeout="20" interval="10" depth="0" />
<action name="reload" timeout="20" />
<action name="migrate_to" timeout="20" />
<action name="migrate_from" timeout="20" />
<action name="meta-data" timeout="5" />
<action name="validate-all" timeout="20" />
</actions>
</resource-agent>
END
}
#######################################################################
# Non API functions
# Extract fields from slave status
parse_slave_info() {
# Extracts field $1 from result of "SHOW SLAVE STATUS\G" from file $2
sed -ne "s/^.* $1: \(.*\)$/\1/p" < $2
}
# Read the slave status and
get_slave_info() {
local mysql_options tmpfile
if [ "$master_log_file" -a "$master_host" ]; then
# variables are already defined, get_slave_info has been run before
return $OCF_SUCCESS
else
tmpfile=`mktemp ${HA_RSCTMP}/check_slave.${OCF_RESOURCE_INSTANCE}.XXXXXX`
mysql_run -Q -sw -O $MYSQL $MYSQL_OPTIONS_REPL \
-e 'SHOW SLAVE STATUS\G' > $tmpfile
if [ -s $tmpfile ]; then
master_host=`parse_slave_info Master_Host $tmpfile`
slave_sql=`parse_slave_info Slave_SQL_Running $tmpfile`
slave_io=`parse_slave_info Slave_IO_Running $tmpfile`
slave_io_state=`parse_slave_info Slave_IO_State $tmpfile`
last_errno=`parse_slave_info Last_Errno $tmpfile`
secs_behind=`parse_slave_info Seconds_Behind_Master $tmpfile`
ocf_log debug "MySQL instance has a non empty slave status"
else
# Instance produced an empty "SHOW SLAVE STATUS" output --
# instance is not a slave
ocf_log err "check_slave invoked on an instance that is not a replication slave."
rm -f $tmpfile
return $OCF_ERR_GENERIC
fi
rm -f $tmpfile
return $OCF_SUCCESS
fi
}
get_read_only() {
# Check if read-only is set
local read_only_state
read_only_state=`mysql_run -Q -sw -O $MYSQL -N $MYSQL_OPTIONS_REPL \
-e "SHOW VARIABLES like 'read_only'" | awk '{print $2}'`
if [ "$read_only_state" = "ON" ]; then
return 0
else
return 1
fi
}
# get the attribute controlling the readers VIP
get_reader_attr() {
local attr_value
local rc
attr_value=`$CRM_ATTR -l reboot --name ${OCF_RESKEY_reader_attribute} --query -q`
rc=$?
if [ "$rc" -eq "0" ]; then
echo $attr_value
else
echo -1
fi
}
# Set the attribute controlling the readers VIP
set_reader_attr() {
local curr_attr_value
curr_attr_value=$(get_reader_attr)
if [ "$1" -eq "0" ]; then
if [ "$curr_attr_value" -gt "0" ]; then
curr_attr_value=$((${curr_attr_value}-1))
$CRM_ATTR -l reboot --name ${OCF_RESKEY_reader_attribute} -v $curr_attr_value
else
$CRM_ATTR -l reboot --name ${OCF_RESKEY_reader_attribute} -v 0
fi
else
if [ "$curr_attr_value" -ne "$OCF_RESKEY_reader_failcount" ]; then
$CRM_ATTR -l reboot --name ${OCF_RESKEY_reader_attribute} -v $OCF_RESKEY_reader_failcount
fi
fi
}
# get the attribute controlling the writer VIP
get_writer_attr() {
local attr_value
local rc
attr_value=`$CRM_ATTR -l reboot --name ${OCF_RESKEY_writer_attribute} --query -q`
rc=$?
if [ "$rc" -eq "0" ]; then
echo $attr_value
else
echo -1
fi
}
# Set the attribute controlling the writer VIP
set_writer_attr() {
local curr_attr_value
curr_attr_value=$(get_writer_attr)
if [ "$1" -ne "$curr_attr_value" ]; then
if [ "$1" -eq "0" ]; then
$CRM_ATTR -l reboot --name ${OCF_RESKEY_writer_attribute} -v 0
else
$CRM_ATTR -l reboot --name ${OCF_RESKEY_writer_attribute} -v 1
fi
fi
}
#
# mysql_run: Run a mysql command, log its output and return the proper error code.
# Usage: mysql_run [-Q] [-info|-warn|-err] [-O] [-sw] <command>
# -Q: don't log the output of the command if it succeeds
# -info|-warn|-err: log the output of the command at given
# severity if it fails (defaults to err)
# -O: echo the output of the command
# -sw: Suppress 5.6 client warning when password is used on the command line
# Adapted from ocf_run.
#
mysql_run() {
local rc
local output outputfile
local verbose=1
local returnoutput
local loglevel=err
local suppress_56_password_warning
local var
for var in 1 2 3 4
do
case "$1" in
"-Q")
verbose=""
shift 1;;
"-info"|"-warn"|"-err")
loglevel=`echo $1 | sed -e s/-//g`
shift 1;;
"-O")
returnoutput=1
shift 1;;
"-sw")
suppress_56_password_warning=1
shift 1;;
*)
;;
esac
done
outputfile=`mktemp ${HA_RSCTMP}/mysql_run.${OCF_RESOURCE_INSTANCE}.XXXXXX`
error=`"$@" 2>&1 1>$outputfile`
rc=$?
if [ "$suppress_56_password_warning" -eq 1 ]; then
error=`echo "$error" | egrep -v '^Warning: Using a password on the command line'`
fi
output=`cat $outputfile`
rm -f $outputfile
if [ $rc -eq 0 ]; then
if [ "$verbose" -a ! -z "$output" ]; then
ocf_log info "$output"
fi
if [ "$returnoutput" -a ! -z "$output" ]; then
echo "$output"
fi
MYSQL_LAST_ERR=$OCF_SUCCESS
return $OCF_SUCCESS
else
if [ ! -z "$error" ]; then
ocf_log $loglevel "$error"
regex='^ERROR ([[:digit:]]{4}).*'
if [[ $error =~ $regex ]]; then
mysql_code=${BASH_REMATCH[1]}
if [ -n "$mysql_code" ]; then
MYSQL_LAST_ERR=$mysql_code
return $rc
fi
fi
else
ocf_log $loglevel "command failed: $*"
fi
# No output to parse so return the standard exit code.
MYSQL_LAST_ERR=$rc
return $rc
fi
}
#######################################################################
# API functions
mysql_monitor_usage() {
cat <<END
usage: $0 {start|stop|monitor|migrate_to|migrate_from|validate-all|meta-data}
Expects to have a fully populated OCF RA-compliant environment set.
END
}
mysql_monitor_start() {
# Initialise the attribute in the cib if they are not already there.
if [ $(get_reader_attr) -eq -1 ]; then
set_reader_attr 0
fi
if [ $(get_writer_attr) -eq -1 ]; then
set_writer_attr 0
fi
mysql_monitor
mysql_monitor_monitor
if [ $? = $OCF_SUCCESS ]; then
return $OCF_SUCCESS
fi
touch ${OCF_RESKEY_state}
}
mysql_monitor_stop() {
set_reader_attr 0
set_writer_attr 0
mysql_monitor_monitor
if [ $? = $OCF_SUCCESS ]; then
rm ${OCF_RESKEY_state}
fi
return $OCF_SUCCESS
}
# Monitor MySQL, not the agent itself
mysql_monitor() {
if [ -e $OCF_RESKEY_pid ]; then
pid=`cat $OCF_RESKEY_pid`;
if [ -d /proc -a -d /proc/1 ]; then
[ "u$pid" != "u" -a -d /proc/$pid ]
else
kill -s 0 $pid >/dev/null 2>&1
fi
if [ $? -eq 0 ]; then
case ${OCF_RESKEY_cluster_type} in
'replication'|'REPLICATION')
if get_read_only; then
# a slave?
set_writer_attr 0
get_slave_info
rc=$?
if [ $rc -eq 0 ]; then
# show slave status is not empty
# Is there a master_log_file defined? (master_log_file is deleted
# by reset slave
if [ "$master_log_file" ]; then
# is read_only but no slave config...
set_reader_attr 0
else
# has a slave config
if [ "$slave_sql" = 'Yes' -a "$slave_io" = 'Yes' ]; then
# $secs_behind can be NULL so must be tested only
# if replication is OK
if [ $secs_behind -gt $OCF_RESKEY_max_slave_lag ]; then
set_reader_attr 0
else
set_reader_attr 1
fi
else
set_reader_attr 0
fi
fi
else
# "SHOW SLAVE STATUS" returns an empty set if instance is not a
# replication slave
set_reader_attr 0
fi
else
# host is RW
set_reader_attr 1
set_writer_attr 1
fi
;;
'pxc'|'PXC')
pxcstat=`/usr/bin/clustercheck $OCF_RESKEY_user $OCF_RESKEY_password `
if [ $? -eq 0 ]; then
set_reader_attr 1
set_writer_attr 1
else
set_reader_attr 0
set_writer_attr 0
fi
;;
'read-only'|'READ-ONLY')
if get_read_only; then
set_reader_attr 1
set_writer_attr 0
else
set_reader_attr 1
set_writer_attr 1
fi
;;
esac
else
ocf_log $1 "MySQL is not running, but there is a pidfile"
set_reader_attr 0
set_writer_attr 0
fi
else
ocf_log $1 "MySQL is not running"
set_reader_attr 0
set_writer_attr 0
fi
}
mysql_monitor_monitor() {
# Monitor _MUST!_ differentiate correctly between running
# (SUCCESS), failed (ERROR) or _cleanly_ stopped (NOT RUNNING).
# That is THREE states, not just yes/no.
if [ -f ${OCF_RESKEY_state} ]; then
return $OCF_SUCCESS
fi
if false ; then
return $OCF_ERR_GENERIC
fi
return $OCF_NOT_RUNNING
}
mysql_monitor_validate() {
# Is the state directory writable?
state_dir=`dirname "$OCF_RESKEY_state"`
touch "$state_dir/$$"
if [ $? != 0 ]; then
return $OCF_ERR_ARGS
fi
rm "$state_dir/$$"
return $OCF_SUCCESS
}
##########################################################################
# If DEBUG_LOG is set, make this resource agent easy to debug: set up the
# debug log and direct all output to it. Otherwise, redirect to /dev/null.
# The log directory must be a directory owned by root, with permissions 0700,
# and the log must be writable and not a symlink.
##########################################################################
DEBUG_LOG="/tmp/mysql_monitor.ocf.ra.debug/log"
if [ "${DEBUG_LOG}" -a -w "${DEBUG_LOG}" -a ! -L "${DEBUG_LOG}" ]; then
DEBUG_LOG_DIR="${DEBUG_LOG%/*}"
if [ -d "${DEBUG_LOG_DIR}" ]; then
exec 9>>"$DEBUG_LOG"
exec 2>&9
date >&9
echo "$*" >&9
env | grep OCF_ | sort >&9
set -x
else
exec 9>/dev/null
fi
fi
case $__OCF_ACTION in
meta-data) meta_data
exit $OCF_SUCCESS
;;
start) mysql_monitor_start;;
stop) mysql_monitor_stop;;
monitor) mysql_monitor
mysql_monitor_monitor;;
migrate_to) ocf_log info "Migrating ${OCF_RESOURCE_INSTANCE} to ${OCF_RESKEY_CRM_meta_migrate_target}."
mysql_monitor_stop
;;
migrate_from) ocf_log info "Migrating ${OCF_RESOURCE_INSTANCE} from ${OCF_RESKEY_CRM_meta_migrate_source}."
mysql_monitor_start
;;
reload) ocf_log info "Reloading ${OCF_RESOURCE_INSTANCE} ..."
;;
validate-all) mysql_monitor_validate;;
usage|help) mysql_monitor_usage
exit $OCF_SUCCESS
;;
*) mysql_monitor_usage
exit $OCF_ERR_UNIMPLEMENTED
;;
esac
rc=$?
ocf_log debug "${OCF_RESOURCE_INSTANCE} $__OCF_ACTION : $rc"
exit $rc

+ 6
- 0
setup.cfg View File

@ -0,0 +1,6 @@
[nosetests]
verbosity=2
with-coverage=1
cover-erase=1
cover-package=hooks

+ 1
- 0
templates/my.cnf View File

@ -11,6 +11,7 @@ wsrep_provider_options = {{ wsrep_provider_options }}
datadir=/var/lib/mysql
user=mysql
pid_file = /var/run/mysqld/mysqld.pid
# Path to Galera library
wsrep_provider=/usr/lib/libgalera_smm.so


+ 29
- 0
tests/00-setup.sh View File

@ -0,0 +1,29 @@
#!/bin/bash -x
# The script installs amulet and other tools needed for the amulet tests.
# Get the status of the amulet package, this returns 0 of package is installed.
dpkg -s amulet
if [ $? -ne 0 ]; then
# Install the Amulet testing harness.
sudo add-apt-repository -y ppa:juju/stable
sudo apt-get update
sudo apt-get install -y -q amulet juju-core charm-tools
fi
PACKAGES="python3 python3-yaml"
for pkg in $PACKAGES; do
dpkg -s python3
if [ $? -ne 0 ]; then
sudo apt-get install -y -q $pkg
fi
done
#if [ ! -f "$(dirname $0)/../local.yaml" ]; then
# echo "To run these amulet tests a vip is needed, create a file called \
#local.yaml in the charm dir, this file must contain a 'vip', if you're \
#using the local provider with lxc you could use a free IP from the range \
#10.0.3.0/24"
# exit 1
#fi

+ 29
- 0
tests/10-deploy_test.py View File

@ -0,0 +1,29 @@
#!/usr/bin/python3
# test percona-cluster (3 nodes)
import basic_deployment
import time
class ThreeNode(basic_deployment.BasicDeployment):
def __init__(self):
super(ThreeNode, self).__init__(units=3)
def run(self):
super(ThreeNode, self).run()
# we are going to kill the master
old_master = self.master_unit
self.master_unit.run('sudo poweroff')
time.sleep(10) # give some time to pacemaker to react
new_master = self.find_master()
assert new_master is not None, "master unit not found"
assert (new_master.info['public-address'] !=
old_master.info['public-address'])
assert self.is_port_open(address=self.vip), 'cannot connect to vip'
if __name__ == "__main__":
t = ThreeNode()
t.run()

+ 38
- 0
tests/20-broken-mysqld.py View File

@ -0,0 +1,38 @@
#!/usr/bin/python3
# test percona-cluster (3 nodes)
import basic_deployment
import time
class ThreeNode(basic_deployment.BasicDeployment):
def __init__(self):
super(ThreeNode, self).__init__(units=3)
def run(self):
super(ThreeNode, self).run()
# we are going to kill the master
old_master = self.master_unit
print('stopping mysql in %s' % str(self.master_unit.info))
self.master_unit.run('sudo service mysql stop')
print('looking for the new master')
i = 0
changed = False
while i < 10 and not changed:
i += 1
time.sleep(5) # give some time to pacemaker to react
new_master = self.find_master()
if (new_master and new_master.info['unit_name'] !=
old_master.info['unit_name']):
changed = True
assert changed, "The master didn't change"
assert self.is_port_open(address=self.vip), 'cannot connect to vip'
if __name__ == "__main__":
t = ThreeNode()
t.run()

+ 38
- 0
tests/30-kill-9-mysqld.py View File

@ -0,0 +1,38 @@
#!/usr/bin/python3
# test percona-cluster (3 nodes)
import basic_deployment
import time
class ThreeNode(basic_deployment.BasicDeployment):
def __init__(self):
super(ThreeNode, self).__init__(units=3)
def run(self):
super(ThreeNode, self).run()
# we are going to kill the master
old_master = self.master_unit
print('kill-9 mysqld in %s' % str(self.master_unit.info))
self.master_unit.run('sudo killall -9 mysqld')
print('looking for the new master')
i = 0
changed = False
while i < 10 and not changed:
i += 1
time.sleep(5) # give some time to pacemaker to react
new_master = self.find_master()
if (new_master and new_master.info['unit_name'] !=
old_master.info['unit_name']):
changed = True
assert changed, "The master didn't change"
assert self.is_port_open(address=self.vip), 'cannot connect to vip'
if __name__ == "__main__":
t = ThreeNode()
t.run()

+ 151
- 0
tests/basic_deployment.py View File

@ -0,0 +1,151 @@
import amulet
import os
import time
import telnetlib
import unittest
import yaml
from charmhelpers.contrib.openstack.amulet.deployment import (
OpenStackAmuletDeployment
)
class BasicDeployment(OpenStackAmuletDeployment):
def __init__(self, vip=None, units=1, series="trusty", openstack=None,
source=None, stable=False):
super(BasicDeployment, self).__init__(series, openstack, source,
stable)
self.units = units
self.master_unit = None
self.vip = None
if vip:
self.vip = vip
elif 'AMULET_OS_VIP' in os.environ:
self.vip = os.environ.get('AMULET_OS_VIP')
elif os.path.isfile('local.yaml'):
with open('local.yaml', 'rb') as f:
self.cfg = yaml.safe_load(f.read())
self.vip = self.cfg.get('vip')
else:
amulet.raise_status(amulet.SKIP,
("please set the vip in local.yaml or env var "
"AMULET_OS_VIP to run this test suite"))
def _add_services(self):
"""Add services
Add the services that we're testing, where percona-cluster is local,
and the rest of the service are from lp branches that are
compatible with the local charm (e.g. stable or next).
"""
this_service = {'name': 'percona-cluster',
'units': self.units}
other_services = [{'name': 'hacluster'}]
super(BasicDeployment, self)._add_services(this_service,
other_services)
def _add_relations(self):
"""Add all of the relations for the services."""
relations = {'percona-cluster:ha': 'hacluster:ha'}
super(BasicDeployment, self)._add_relations(relations)
def _configure_services(self):
"""Configure all of the services."""
cfg_percona = {'sst-password': 'ubuntu',
'root-password': 't00r',
'dataset-size': '512M',
'vip': self.vip}
cfg_ha = {'debug': True,
'corosync_mcastaddr': '226.94.1.4',
'corosync_key': ('xZP7GDWV0e8Qs0GxWThXirNNYlScgi3sRTdZk/IXKD'
'qkNFcwdCWfRQnqrHU/6mb6sz6OIoZzX2MtfMQIDcXu'
'PqQyvKuv7YbRyGHmQwAWDUA4ed759VWAO39kHkfWp9'
'y5RRk/wcHakTcWYMwm70upDGJEP00YT3xem3NQy27A'
'C1w=')}
configs = {'percona-cluster': cfg_percona,
'hacluster': cfg_ha}
super(BasicDeployment, self)._configure_services(configs)
def run(self):
# The number of seconds to wait for the environment to setup.
seconds = 1200
self._add_services()
self._add_relations()
self._configure_services()
self._deploy()
i = 0
while i < 30 and not self.master_unit:
self.master_unit = self.find_master()
i += 1
time.sleep(10)
assert self.master_unit is not None, 'percona-cluster vip not found'
output, code = self.master_unit.run('sudo crm_verify --live-check')
assert code == 0, "'crm_verify --live-check' failed"
resources = ['res_mysql_vip']
resources += ['res_mysql_monitor:%d' % i for i in range(self.units)]
assert sorted(self.get_pcmkr_resources()) == sorted(resources)
for i in range(self.units):
uid = 'percona-cluster/%d' % i
unit = self.d.sentry.unit[uid]
assert self.is_mysqld_running(unit), 'mysql not running: %s' % uid
def find_master(self):
for unit_id, unit in self.d.sentry.unit.items():
if not unit_id.startswith('percona-cluster/'):
continue
# is the vip running here?
output, code = unit.run('sudo ip a | grep "inet %s/"' % self.vip)
print('---')
print(unit_id)
print(output)
if code == 0:
print('vip(%s) running in %s' % (self.vip, unit_id))
return unit
def get_pcmkr_resources(self, unit=None):
if unit:
u = unit
else:
u = self.master_unit
output, code = u.run('sudo crm_resource -l')
assert code == 0, 'could not get "crm resource list"'
return output.split('\n')
def is_mysqld_running(self, unit=None):
if unit:
u = unit
else:
u = self.master_unit
output, code = u.run('pidof mysqld')
if code != 0:
return False
return self.is_port_open(u, '3306')
def is_port_open(self, unit=None, port='3306', address=None):
if unit:
addr = unit.info['public-address']
elif address:
addr = address
else:
raise Exception('Please provide a unit or address')
try:
telnetlib.Telnet(addr, port)
return True
except TimeoutError: # noqa this exception only available in py3
return False

+ 38
- 0
tests/charmhelpers/__init__.py View File

@ -0,0 +1,38 @@
# 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/>.
# Bootstrap charm-helpers, installing its dependencies if necessary using
# only standard libraries.
import subprocess
import sys
try:
import six # flake8: noqa
except ImportError:
if sys.version_info.major == 2:
subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
else:
subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
import six # flake8: noqa
try:
import yaml # flake8: noqa
except ImportError:
if sys.version_info.major == 2:
subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
else:
subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
import yaml # flake8: noqa

+ 15
- 0
tests/charmhelpers/contrib/__init__.py View File

@ -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/>.

+ 15
- 0
tests/charmhelpers/contrib/amulet/__init__.py View File

@ -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/>.

+ 93
- 0
tests/charmhelpers/contrib/amulet/deployment.py View File

@ -0,0 +1,93 @@
# 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 amulet
import os
import six
class AmuletDeployment(object):
"""Amulet deployment.
This class provides generic Amulet deployment and test runner
methods.
"""
def __init__(self, series=None):
"""Initialize the deployment environment."""
self.series = None
if series:
self.series = series
self.d = amulet.Deployment(series=self.series)
else:
self.d = amulet.Deployment()
def _add_services(self, this_service, other_services):
"""Add services.
Add services to the deployment where this_service is the local charm
that we're testing and other_services are the other services that
are being used in the local amulet tests.
"""
if this_service['name'] != os.path.basename(os.getcwd()):
s = this_service['name']
msg = "The charm's root directory name needs to be {}".format(s)
amulet.raise_status(amulet.FAIL, msg=msg)
if 'units' not in this_service:
this_service['units'] = 1
self.d.add(this_service['name'], units=this_service['units'])
for svc in other_services:
if 'location' in svc:
branch_location = svc['location']
elif self.series:
branch_location = 'cs:{}/{}'.format(self.series, svc['name']),
else:
branch_location = None
if 'units' not in svc:
svc['units'] = 1
self.d.add(svc['name'], charm=branch_location, units=svc['units'])
def _add_relations(self, relations):
"""Add all of the relations for the services."""
for k, v in six.iteritems(relations):
self.d.relate(k, v)
def _configure_services(self, configs):
"""Configure all of the services."""
for service, config in six.iteritems(configs):
self.d.configure(service, config)
def _deploy(self):
"""Deploy environment and wait for all hooks to finish executing."""
try:
self.d.setup(timeout=900)
self.d.sentry.wait(timeout=900)
except amulet.helpers.TimeoutError:
amulet.raise_status(amulet.FAIL, msg="Deployment timed out")
except Exception:
raise
def run_tests(self):
"""Run all of the methods that are prefixed with 'test_'."""
for test in dir(self):
if test.startswith('test_'):
getattr(self, test)()

+ 316
- 0
tests/charmhelpers/contrib/amulet/utils.py View File

@ -0,0 +1,316 @@
# 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 ConfigParser
import io
import logging
import re
import sys
import time
import six
class AmuletUtils(object):
"""Amulet utilities.
This class provides common utility functions that are used by Amulet
tests.
"""
def __init__(self, log_level=logging.ERROR):
self.log = self.get_logger(level=log_level)
def get_logger(self, name="amulet-logger", level=logging.DEBUG):
"""Get a logger object that will log to stdout."""
log = logging
logger = log.getLogger(name)
fmt = log.Formatter("%(asctime)s %(funcName)s "
"%(levelname)s: %(message)s")
handler = log.StreamHandler(stream=sys.stdout)
handler.setLevel(level)
handler.setFormatter(fmt)
logger.addHandler(handler)
logger.setLevel(level)
return logger
def valid_ip(self, ip):
if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
return True
else:
return False
def valid_url(self, url):
p = re.compile(
r'^(?:http|ftp)s?://'
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # noqa
r'localhost|'
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
r'(?::\d+)?'
r'(?:/?|[/?]\S+)$',
re.IGNORECASE)
if p.match(url):
return True
else:
return False
def validate_services(self, commands):
"""Validate services.
Verify the specified services are running on the corresponding
service units.
"""
for k, v in six.iteritems(commands):
for cmd in v:
output, code = k.run(cmd)
if code != 0:
return "command `{}` returned {}".format(cmd, str(code))
return None
def _get_config(self, unit, filename):
"""Get a ConfigParser object for parsing a unit's config file."""
file_contents = unit.file_contents(filename)
config = ConfigParser.ConfigParser()
config.readfp(io.StringIO(file_contents))
return config
def validate_config_data(self, sentry_unit, config_file, section,
expected):
"""Validate config file data.
Verify that the specified section of the config file contains
the expected option key:value pairs.
"""
config = self._get_config(sentry_unit, config_file)
if section != 'DEFAULT' and not config.has_section(section):
return "section [{}] does not exist".format(section)
for k in expected.keys():
if not config.has_option(section, k):
return "section [{}] is missing option {}".format(section, k)
if config.get(section, k) != expected[k]:
return "section [{}] {}:{} != expected {}:{}".format(
section, k, config.get(section, k), k, expected[k])
return None
def _validate_dict_data(self, expected, actual):
"""Validate dictionary data.
Compare expected dictionary data vs actual dictionary data.
The values in the 'expected' dictionary can be strings, bools, ints,
longs, or can be a function that evaluate a variable and returns a
bool.
"""
self.log.debug('actual: {}'.format(repr(actual)))
self.log.debug('expected: {}'.format(repr(expected)))
for k, v in six.iteritems(expected):
if k in actual:
if (isinstance(v, six.string_types) or
isinstance(v, bool) or
isinstance(v, six.integer_types)):
if v != actual[k]:
return "{}:{}".format(k, actual[k])
elif not v(actual[k]):
return "{}:{}".format(k, actual[k])
else:
return "key '{}' does not exist".format(k)
return None
def validate_relation_data(self, sentry_unit, relation, expected):
"""Validate actual relation data based on expected relation data."""
actual = sentry_unit.relation(relation[0], relation[1])
return self._validate_dict_data(expected, actual)
def _validate_list_data(self, expected, actual):
"""Compare expected list vs actual list data."""
for e in expected:
if e not in actual:
return "expected item {} not found in actual list".format(e)
return None
def not_null(self, string):
if string is not None:
return True
else:
return False
def _get_file_mtime(self, sentry_unit, filename):
"""Get last modification time of file."""
return sentry_unit.file_stat(filename)['mtime']
def _get_dir_mtime(self, sentry_unit, directory):
"""Get last modification time of directory."""
return sentry_unit.directory_stat(directory)['mtime']
def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False):
"""Get process' start time.
Determine start time of the process based on the last modification
time of the /proc/pid directory. If pgrep_full is True, the process
name is matched against the full command line.
"""
if pgrep_full:
cmd = 'pgrep -o -f {}'.format(service)
else:
cmd = 'pgrep -o {}'.format(service)
cmd = cmd + ' | grep -v pgrep || exit 0'
cmd_out = sentry_unit.run(cmd)
self.log.debug('CMDout: ' + str(cmd_out))
if cmd_out[0]:
self.log.debug('Pid for %s %s' % (service, str(cmd_out[0])))
proc_dir = '/proc/{}'.format(cmd_out[0].strip())
return self._get_dir_mtime(sentry_unit, proc_dir)
def service_restarted(self, sentry_unit, service, filename,
pgrep_full=False, sleep_time=20):
"""Check if service was restarted.
Compare a service's start time vs a file's last modification time
(such as a config file for that service) to determine if the service
has been restarted.
"""
time.sleep(sleep_time)
if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
self._get_file_mtime(sentry_unit, filename)):
return True
else:
return False
def service_restarted_since(self, sentry_unit, mtime, service,
pgrep_full=False, sleep_time=20,
retry_count=2):
"""Check if service was been started after a given time.
Args:
sentry_unit (sentry): The sentry unit to check for the service on
mtime (float): The epoch time to check against
service (string): service name to look for in process table
pgrep_full (boolean): Use full command line search mode with pgrep
sleep_time (int): Seconds to sleep before looking for process
retry_count (int): If service is not found, how many times to retry
Returns:
bool: True if service found and its start time it newer than mtime,
False if service is older than mtime or if service was
not found.
"""
self.log.debug('Checking %s restarted since %s' % (service, mtime))
time.sleep(sleep_time)
proc_start_time = self._get_proc_start_time(sentry_unit, service,
pgrep_full)
while retry_count > 0 and not proc_start_time:
self.log.debug('No pid file found for service %s, will retry %i '
'more times' % (service, retry_count))
time.sleep(30)
proc_start_time = self._get_proc_start_time(sentry_unit, service,
pgrep_full)
retry_count = retry_count - 1
if not proc_start_time:
self.log.warn('No proc start time found, assuming service did '
'not start')
return False
if proc_start_time >= mtime:
self.log.debug('proc start time is newer than provided mtime'
'(%s >= %s)' % (proc_start_time, mtime))
return True
else:
self.log.warn('proc start time (%s) is older than provided mtime '
'(%s), service did not restart' % (proc_start_time,
mtime))
return False
def config_updated_since(self, sentry_unit, filename, mtime,
sleep_time=20):
"""Check if file was modified after a given time.
Args:
sentry_unit (sentry): The sentry unit to check the file mtime on
filename (string): The file to check mtime of
mtime (float): The epoch time to check against
sleep_time (int): Seconds to sleep before looking for process
Returns:
bool: True if file was modified more recently than mtime, False if
file was modified before mtime,
"""
self.log.debug('Checking %s updated since %s' % (filename, mtime))
time.sleep(sleep_time)
file_mtime = self._get_file_mtime(sentry_unit, filename)
if file_mtime >= mtime:
self.log.debug('File mtime is newer than provided mtime '
'(%s >= %s)' % (file_mtime, mtime))
return True
else:
self.log.warn('File mtime %s is older than provided mtime %s'
% (file_mtime, mtime))
return False
def validate_service_config_changed(self, sentry_unit, mtime, service,
filename, pgrep_full=False,
sleep_time=20, retry_count=2):
"""Check service and file were updated after mtime
Args:
sentry_unit (sentry): The sentry unit to check for the service on
mtime (float): The epoch time to check against
service (string): service name to look for in process table
filename (string): The file to check mtime of
pgrep_full (boolean): Use full command line search mode with pgrep
sleep_time (int): Seconds to sleep before looking for process
retry_count (int): If service is not found, how many times to retry
Typical Usage:
u = OpenStackAmuletUtils(ERROR)
...
mtime = u.get_sentry_time(self.cinder_sentry)
self.d.configure('cinder', {'verbose': 'True', 'debug': 'True'})
if not u.validate_service_config_changed(self.cinder_sentry,
mtime,
'cinder-api',
'/etc/cinder/cinder.conf')
amulet.raise_status(amulet.FAIL, msg='update failed')
Returns:
bool: True if both service and file where updated/restarted after
mtime, False if service is older than mtime or if service was
not found or if filename was modified before mtime.
"""
self.log.debug('Checking %s restarted since %s' % (service, mtime))
time.sleep(sleep_time)
service_restart = self.service_restarted_since(sentry_unit, mtime,
service,
pgrep_full=pgrep_full,
sleep_time=0,
retry_count=retry_count)
config_update = self.config_updated_since(sentry_unit, filename, mtime,
sleep_time=0)
return service_restart and config_update
def get_sentry_time(self, sentry_unit):
"""Return current epoch time on a sentry"""
cmd = "date +'%s'"
return float(sentry_unit.run(cmd)[0])
def relation_error(self, name, data):
return 'unexpected relation data in {} - {}'.format(name, data)
def endpoint_error(self, name, data):
return 'unexpected endpoint data in {} - {}'.format(name, data)

+ 15
- 0
tests/charmhelpers/contrib/openstack/__init__.py View File

@ -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/>.

+ 15
- 0
tests/charmhelpers/contrib/openstack/amulet/__init__.py View File

@ -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/>.

+ 137
- 0
tests/charmhelpers/contrib/openstack/amulet/deployment.py View File

@ -0,0 +1,137 @@
# 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
from collections import OrderedDict
from charmhelpers.contrib.amulet.deployment import (
AmuletDeployment
)
class OpenStackAmuletDeployment(AmuletDeployment):
"""OpenStack amulet deployment.
This class inherits from AmuletDeployment and has additional support
that is specifically for use by OpenStack charms.
"""
def __init__(self, series=None, openstack=None, source=None, stable=True):
"""Initialize the deployment environment."""
super(OpenStackAmuletDeployment, self).__init__(series)
self.openstack = openstack
self.source = source
self.stable = stable
# Note(coreycb): this needs to be changed when new next branches come
# out.
self.current_next = "trusty"
def _determine_branch_locations(self, other_services):
"""Determine the branch locations for the other services.
Determine if the local branch being tested is derived from its
stable or next (dev) branch, and based on this, use the corresonding
stable or next branches for the other_services."""
base_charms = ['mysql', 'mongodb']
if self.stable:
for svc in other_services:
temp = 'lp:charms/{}'
svc['location'] = temp.format(svc['name'])
else:
for svc in other_services:
if svc['name'] in base_charms:
temp = 'lp:charms/{}'
svc['location'] = temp.format(svc['name'])
else:
temp = 'lp:~openstack-charmers/charms/{}/{}/next'
svc['location'] = temp.format(self.current_next,
svc['name'])
return other_services
def _add_services(self, this_service, other_services):
"""Add services to the deployment and set openstack-origin/source."""
other_services = self._determine_branch_locations(other_services)
super(OpenStackAmuletDeployment, self)._add_services(this_service,
other_services)
services = other_services
services.append(this_service)
use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
'ceph-osd', 'ceph-radosgw']