Merge lp:~freyes/charms/trusty/percona-cluster/lp1426508 into lp:~openstack-charmers-archive/charms/trusty/percona-cluster/next

Proposed by Felipe Reyes
Status: Superseded
Proposed branch: lp:~freyes/charms/trusty/percona-cluster/lp1426508
Merge into: lp:~openstack-charmers-archive/charms/trusty/percona-cluster/next
Diff against target: 1274 lines (+1132/-3)
14 files modified
.bzrignore (+3/-0)
Makefile (+4/-0)
copyright (+22/-0)
hooks/percona_hooks.py (+35/-3)
hooks/percona_utils.py (+17/-0)
ocf/percona/mysql_monitor (+636/-0)
setup.cfg (+6/-0)
templates/my.cnf (+1/-0)
tests/00-setup.sh (+29/-0)
tests/10-deploy_test.py (+29/-0)
tests/20-broken-mysqld.py (+38/-0)
tests/basic_deployment.py (+126/-0)
unit_tests/test_percona_hooks.py (+65/-0)
unit_tests/test_utils.py (+121/-0)
To merge this branch: bzr merge lp:~freyes/charms/trusty/percona-cluster/lp1426508
Reviewer Review Type Date Requested Status
Mario Splivalo Pending
OpenStack Charmers Pending
James Page Pending
Review via email: mp+253248@code.launchpad.net

This proposal supersedes a proposal from 2015-03-17.

This proposal has been superseded by a proposal from 2015-04-07.

Description of the change

Dear OpenStack Charmers,

This patch configures mysql_monitor[0] to keep updated two properties (readable and writable) on each node member of the cluster, these properties are used to define a location rule[1][2] that instructs pacemaker to run the vip only in nodes where the writable property is set to 1.

This fixes scenarios where mysql is out of sync, stopped (manually or because it crashed).

This MP also adds functional tests to check 2 scenarios: a standard 3 nodes deployment, another where mysql service is stopped in the node where the vip is running and it's checked that the vip was migrated to another node (and connectivity is OK after the migration). To run the functional tests a file called `local.yaml` has to be dropped in the charm's directory with the vip that has to be used, for instance if you're using lxc with the local provider you can run:

$ cat<<EOD > local.yaml
vip: "10.0.3.3"
EOD

Best,

Note0: This patch doesn't take care of starting mysql service if it's stopped, it just take care of monitor the service.
Note1: this patch requires hacluster MP available at [2] to support location rules definition
Note2: to know if the node is capable of receiving read/write requests the clustercheck[3] is used

[0] https://github.com/percona/percona-pacemaker-agents/blob/master/agents/mysql_monitor
[1] http://clusterlabs.org/doc/en-US/Pacemaker/1.1-crmsh/html/Clusters_from_Scratch/_specifying_a_preferred_location.html
[2] https://code.launchpad.net/~freyes/charms/trusty/hacluster/add-location/+merge/252127
[3] http://www.percona.com/doc/percona-xtradb-cluster/5.5/faq.html#q-how-can-i-check-the-galera-node-health

To post a comment you must log in.
Revision history for this message
James Page (james-page) wrote : Posted in a previous version of this proposal

Felipe

This looks like a really good start to resolving this challenge; generally you changes look fine (a few inline comments) but I really would like to see upgrades for existing deployments handled as well.

This would involve re-executing the ha_relation_joined function from the upgrade-charm/config-changed hook so that corosync can reconfigure its resources as required.

review: Needs Fixing
Revision history for this message
Felipe Reyes (freyes) wrote :

James, this new version of the patch addresses your feedback and adds a couple of unit tests for ha-relation-joined.

Thanks,

Revision history for this message
Felipe Reyes (freyes) wrote :

Mario was reviewing this patch and he found a problem when mysqld is killed and the pidfile is left the agent (mysql_monitor) doesn't properly detect that mysql is not running. I filed a pull request[0] to address this scenario.

[0] https://github.com/percona/percona-pacemaker-agents/pull/53

68. By Felipe Reyes

mysql_monitor: Apply patch available in upstream PR #52

https://github.com/percona/percona-pacemaker-agents/pull/53

69. By Felipe Reyes

Rename target to 'test' and use AMULET_OS_VIP to handoff the vip

70. By Felipe Reyes

Add tests/charmhelpers/

71. By Felipe Reyes

Add amulet test that runs 'killall -9 mysqld' in the master node

72. By Felipe Reyes

Resync charm helpers tests/

73. By Felipe Reyes

Pull hacluster from next using openstack charm-helpers base class

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2015-02-06 07:28:54 +0000
3+++ .bzrignore 2015-04-07 17:10:23 +0000
4@@ -2,3 +2,6 @@
5 .coverage
6 .pydevproject
7 .project
8+*.pyc
9+*.pyo
10+__pycache__
11
12=== modified file 'Makefile'
13--- Makefile 2014-10-02 16:12:44 +0000
14+++ Makefile 2015-04-07 17:10:23 +0000
15@@ -9,6 +9,10 @@
16 unit_test:
17 @$(PYTHON) /usr/bin/nosetests --nologcapture unit_tests
18
19+functional_test:
20+ @echo Starting amulet tests...
21+ @juju test -v -p AMULET_HTTP_PROXY --timeout 900
22+
23 bin/charm_helpers_sync.py:
24 @mkdir -p bin
25 @bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \
26
27=== modified file 'copyright'
28--- copyright 2013-09-19 15:40:50 +0000
29+++ copyright 2015-04-07 17:10:23 +0000
30@@ -15,3 +15,25 @@
31 .
32 You should have received a copy of the GNU General Public License
33 along with this program. If not, see <http://www.gnu.org/licenses/>.
34+
35+Files: ocf/percona/mysql_monitor
36+Copyright: Copyright (c) 2013, Percona inc., Yves Trudeau, Michael Coburn
37+License: GPL-2
38+ This program is free software; you can redistribute it and/or modify
39+ it under the terms of version 2 of the GNU General Public License as
40+ published by the Free Software Foundation.
41+
42+ This program is distributed in the hope that it would be useful, but
43+ WITHOUT ANY WARRANTY; without even the implied warranty of
44+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
45+
46+ Further, this software is distributed without any warranty that it is
47+ free of the rightful claim of any third person regarding infringement
48+ or the like. Any license provided herein, whether implied or
49+ otherwise, applies only to this software file. Patent licenses, if
50+ any, provided herein do not apply to combinations of this program with
51+ other software, or any other product whatsoever.
52+
53+ You should have received a copy of the GNU General Public License
54+ along with this program; if not, write the Free Software Foundation,
55+ Inc., 59 Temple Place - Suite 330, Boston MA 02111-1307, USA.
56
57=== modified file 'hooks/percona_hooks.py'
58--- hooks/percona_hooks.py 2015-02-16 14:12:42 +0000
59+++ hooks/percona_hooks.py 2015-04-07 17:10:23 +0000
60@@ -50,6 +50,7 @@
61 assert_charm_supports_ipv6,
62 unit_sorted,
63 get_db_helper,
64+ install_mysql_ocf,
65 )
66 from charmhelpers.contrib.database.mysql import (
67 PerconaClusterHelper,
68@@ -72,6 +73,13 @@
69 hooks = Hooks()
70
71 LEADER_RES = 'grp_percona_cluster'
72+RES_MONITOR_PARAMS = ('params user="sstuser" password="%(sstpass)s" '
73+ 'pid="/var/run/mysqld/mysqld.pid" '
74+ 'socket="/var/run/mysqld/mysqld.sock" '
75+ 'max_slave_lag="5" '
76+ 'cluster_type="pxc" '
77+ 'op monitor interval="1s" timeout="30s" '
78+ 'OCF_CHECK_LEVEL="1"')
79
80
81 @hooks.hook('install')
82@@ -155,6 +163,13 @@
83 for unit in related_units(r_id):
84 shared_db_changed(r_id, unit)
85
86+ # (re)install pcmkr agent
87+ install_mysql_ocf()
88+
89+ if relation_ids('ha'):
90+ # make sure all the HA resources are (re)created
91+ ha_relation_joined()
92+
93
94 @hooks.hook('cluster-relation-joined')
95 def cluster_joined(relation_id=None):
96@@ -387,17 +402,34 @@
97 vip_params = 'params ip="%s" cidr_netmask="%s" nic="%s"' % \
98 (vip, vip_cidr, vip_iface)
99
100- resources = {'res_mysql_vip': res_mysql_vip}
101- resource_params = {'res_mysql_vip': vip_params}
102+ resources = {'res_mysql_vip': res_mysql_vip,
103+ 'res_mysql_monitor': 'ocf:percona:mysql_monitor'}
104+ db_helper = get_db_helper()
105+ cfg_passwd = config('sst-password')
106+ sstpsswd = db_helper.get_mysql_password(username='sstuser',
107+ password=cfg_passwd)
108+ resource_params = {'res_mysql_vip': vip_params,
109+ 'res_mysql_monitor':
110+ RES_MONITOR_PARAMS % {'sstpass': sstpsswd}}
111 groups = {'grp_percona_cluster': 'res_mysql_vip'}
112
113+ clones = {'cl_mysql_monitor': 'res_mysql_monitor meta interleave=true'}
114+
115+ colocations = {'vip_mysqld': 'inf: grp_percona_cluster cl_mysql_monitor'}
116+
117+ locations = {'loc_percona_cluster':
118+ 'grp_percona_cluster rule inf: writable eq 1'}
119+
120 for rel_id in relation_ids('ha'):
121 relation_set(relation_id=rel_id,
122 corosync_bindiface=corosync_bindiface,
123 corosync_mcastport=corosync_mcastport,
124 resources=resources,
125 resource_params=resource_params,
126- groups=groups)
127+ groups=groups,
128+ clones=clones,
129+ colocations=colocations,
130+ locations=locations)
131
132
133 @hooks.hook('ha-relation-changed')
134
135=== modified file 'hooks/percona_utils.py'
136--- hooks/percona_utils.py 2015-02-05 09:59:36 +0000
137+++ hooks/percona_utils.py 2015-04-07 17:10:23 +0000
138@@ -4,10 +4,12 @@
139 import socket
140 import tempfile
141 import os
142+import shutil
143 from charmhelpers.core.host import (
144 lsb_release
145 )
146 from charmhelpers.core.hookenv import (
147+ charm_dir,
148 unit_get,
149 relation_ids,
150 related_units,
151@@ -229,3 +231,18 @@
152 """Return a sorted list of unit names."""
153 return sorted(
154 units, lambda a, b: cmp(int(a.split('/')[-1]), int(b.split('/')[-1])))
155+
156+
157+def install_mysql_ocf():
158+ dest_dir = '/usr/lib/ocf/resource.d/percona/'
159+ for fname in ['ocf/percona/mysql_monitor']:
160+ src_file = os.path.join(charm_dir(), fname)
161+ if not os.path.isdir(dest_dir):
162+ os.makedirs(dest_dir)
163+
164+ dest_file = os.path.join(dest_dir, os.path.basename(src_file))
165+ if not os.path.exists(dest_file):
166+ log('Installing %s' % dest_file, level='INFO')
167+ shutil.copy(src_file, dest_file)
168+ else:
169+ log("'%s' already exists, skipping" % dest_file, level='INFO')
170
171=== added directory 'ocf'
172=== added directory 'ocf/percona'
173=== added file 'ocf/percona/mysql_monitor'
174--- ocf/percona/mysql_monitor 1970-01-01 00:00:00 +0000
175+++ ocf/percona/mysql_monitor 2015-04-07 17:10:23 +0000
176@@ -0,0 +1,636 @@
177+#!/bin/bash
178+#
179+#
180+# MySQL_Monitor agent, set writeable and readable attributes based on the
181+# state of the local MySQL, running and read_only or not. The agent basis is
182+# the original "Dummy" agent written by Lars Marowsky-Brée and part of the
183+# Pacemaker distribution. Many functions are from mysql_prm.
184+#
185+#
186+# Copyright (c) 2013, Percona inc., Yves Trudeau, Michael Coburn
187+#
188+# This program is free software; you can redistribute it and/or modify
189+# it under the terms of version 2 of the GNU General Public License as
190+# published by the Free Software Foundation.
191+#
192+# This program is distributed in the hope that it would be useful, but
193+# WITHOUT ANY WARRANTY; without even the implied warranty of
194+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
195+#
196+# Further, this software is distributed without any warranty that it is
197+# free of the rightful claim of any third person regarding infringement
198+# or the like. Any license provided herein, whether implied or
199+# otherwise, applies only to this software file. Patent licenses, if
200+# any, provided herein do not apply to combinations of this program with
201+# other software, or any other product whatsoever.
202+#
203+# You should have received a copy of the GNU General Public License
204+# along with this program; if not, write the Free Software Foundation,
205+# Inc., 59 Temple Place - Suite 330, Boston MA 02111-1307, USA.
206+#
207+# Version: 20131119163921
208+#
209+# See usage() function below for more details...
210+#
211+# OCF instance parameters:
212+#
213+# OCF_RESKEY_state
214+# OCF_RESKEY_user
215+# OCF_RESKEY_password
216+# OCF_RESKEY_client_binary
217+# OCF_RESKEY_pid
218+# OCF_RESKEY_socket
219+# OCF_RESKEY_reader_attribute
220+# OCF_RESKEY_reader_failcount
221+# OCF_RESKEY_writer_attribute
222+# OCF_RESKEY_max_slave_lag
223+# OCF_RESKEY_cluster_type
224+#
225+#######################################################################
226+# Initialization:
227+
228+: ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/lib/heartbeat}
229+. ${OCF_FUNCTIONS_DIR}/ocf-shellfuncs
230+
231+#######################################################################
232+
233+HOSTOS=`uname`
234+if [ "X${HOSTOS}" = "XOpenBSD" ];then
235+OCF_RESKEY_client_binary_default="/usr/local/bin/mysql"
236+OCF_RESKEY_pid_default="/var/mysql/mysqld.pid"
237+OCF_RESKEY_socket_default="/var/run/mysql/mysql.sock"
238+else
239+OCF_RESKEY_client_binary_default="/usr/bin/mysql"
240+OCF_RESKEY_pid_default="/var/run/mysql/mysqld.pid"
241+OCF_RESKEY_socket_default="/var/lib/mysql/mysql.sock"
242+fi
243+OCF_RESKEY_reader_attribute_default="readable"
244+OCF_RESKEY_writer_attribute_default="writable"
245+OCF_RESKEY_reader_failcount_default="1"
246+OCF_RESKEY_user_default="root"
247+OCF_RESKEY_password_default=""
248+OCF_RESKEY_max_slave_lag_default="3600"
249+OCF_RESKEY_cluster_type_default="replication"
250+
251+: ${OCF_RESKEY_state=${HA_RSCTMP}/mysql-monitor-${OCF_RESOURCE_INSTANCE}.state}
252+: ${OCF_RESKEY_client_binary=${OCF_RESKEY_client_binary_default}}
253+: ${OCF_RESKEY_pid=${OCF_RESKEY_pid_default}}
254+: ${OCF_RESKEY_socket=${OCF_RESKEY_socket_default}}
255+: ${OCF_RESKEY_reader_attribute=${OCF_RESKEY_reader_attribute_default}}
256+: ${OCF_RESKEY_reader_failcount=${OCF_RESKEY_reader_failcount_default}}
257+: ${OCF_RESKEY_writer_attribute=${OCF_RESKEY_writer_attribute_default}}
258+: ${OCF_RESKEY_user=${OCF_RESKEY_user_default}}
259+: ${OCF_RESKEY_password=${OCF_RESKEY_password_default}}
260+: ${OCF_RESKEY_max_slave_lag=${OCF_RESKEY_max_slave_lag_default}}
261+: ${OCF_RESKEY_cluster_type=${OCF_RESKEY_cluster_type_default}}
262+
263+MYSQL="$OCF_RESKEY_client_binary -A -S $OCF_RESKEY_socket --connect_timeout=10 --user=$OCF_RESKEY_user --password=$OCF_RESKEY_password "
264+HOSTNAME=`uname -n`
265+CRM_ATTR="${HA_SBIN_DIR}/crm_attribute -N $HOSTNAME "
266+
267+meta_data() {
268+ cat <<END
269+<?xml version="1.0"?>
270+<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
271+<resource-agent name="mysql_monitor" version="0.9">
272+<version>1.0</version>
273+
274+<longdesc lang="en">
275+This agent monitors the local MySQL instance and set the writable and readable
276+attributes according to what it finds. It checks if MySQL is running and if
277+it is read-only or not.
278+</longdesc>
279+<shortdesc lang="en">Agent monitoring mysql</shortdesc>
280+
281+<parameters>
282+<parameter name="state" unique="1">
283+<longdesc lang="en">
284+Location to store the resource state in.
285+</longdesc>
286+<shortdesc lang="en">State file</shortdesc>
287+<content type="string" default="${HA_RSCTMP}/Mysql-monitor-${OCF_RESOURCE_INSTANCE}.state" />
288+</parameter>
289+
290+<parameter name="user" unique="0">
291+<longdesc lang="en">
292+MySQL user to connect to the local MySQL instance to check the slave status and
293+if the read_only variable is set. It requires the replication client priviledge.
294+</longdesc>
295+<shortdesc lang="en">MySQL user</shortdesc>
296+<content type="string" default="${OCF_RESKEY_user_default}" />
297+</parameter>
298+
299+<parameter name="password" unique="0">
300+<longdesc lang="en">
301+Password of the mysql user to connect to the local MySQL instance
302+</longdesc>
303+<shortdesc lang="en">MySQL password</shortdesc>
304+<content type="string" default="${OCF_RESKEY_password_default}" />
305+</parameter>
306+
307+<parameter name="client_binary" unique="0">
308+<longdesc lang="en">
309+MySQL Client Binary path.
310+</longdesc>
311+<shortdesc lang="en">MySQL client binary path</shortdesc>
312+<content type="string" default="${OCF_RESKEY_client_binary_default}" />
313+</parameter>
314+
315+<parameter name="socket" unique="0">
316+<longdesc lang="en">
317+Unix socket to use in order to connect to MySQL on the host
318+</longdesc>
319+<shortdesc lang="en">MySQL socket</shortdesc>
320+<content type="string" default="${OCF_RESKEY_socket_default}" />
321+</parameter>
322+
323+<parameter name="pid" unique="0">
324+<longdesc lang="en">
325+MySQL pid file, used to verify MySQL is running.
326+</longdesc>
327+<shortdesc lang="en">MySQL pid file</shortdesc>
328+<content type="string" default="${OCF_RESKEY_pid_default}" />
329+</parameter>
330+
331+<parameter name="reader_attribute" unique="0">
332+<longdesc lang="en">
333+The reader attribute in the cib that can be used by location rules to allow or not
334+reader VIPs on a host.
335+</longdesc>
336+<shortdesc lang="en">Reader attribute</shortdesc>
337+<content type="string" default="${OCF_RESKEY_reader_attribute_default}" />
338+</parameter>
339+
340+<parameter name="writer_attribute" unique="0">
341+<longdesc lang="en">
342+The reader attribute in the cib that can be used by location rules to allow or not
343+reader VIPs on a host.
344+</longdesc>
345+<shortdesc lang="en">Writer attribute</shortdesc>
346+<content type="string" default="${OCF_RESKEY_writer_attribute_default}" />
347+</parameter>
348+
349+<parameter name="max_slave_lag" unique="0" required="0">
350+<longdesc lang="en">
351+The maximum number of seconds a replication slave is allowed to lag
352+behind its master in order to have a reader VIP on it.
353+</longdesc>
354+<shortdesc lang="en">Maximum time (seconds) a MySQL slave is allowed
355+to lag behind a master</shortdesc>
356+<content type="integer" default="${OCF_RESKEY_max_slave_lag_default}"/>
357+</parameter>
358+
359+<parameter name="cluster_type" unique="0" required="0">
360+<longdesc lang="en">
361+Type of cluster, three possible values: pxc, replication, read-only. "pxc" is
362+for Percona XtraDB cluster, it uses the clustercheck script and set the
363+reader_attribute and writer_attribute according to the return code.
364+"replication" checks the read-only state and the slave status, only writable
365+node(s) will get the writer_attribute (and the reader_attribute) and on the
366+read-only nodes, replication status will be checked and the reader_attribute set
367+according to the state. "read-only" will just check if the read-only variable,
368+if read/write, it will get both the writer_attribute and reader_attribute set, if
369+read-only it will get only the reader_attribute.
370+</longdesc>
371+<shortdesc lang="en">Type of cluster</shortdesc>
372+<content type="string" default="${OCF_RESKEY_cluster_type_default}"/>
373+</parameter>
374+
375+</parameters>
376+
377+<actions>
378+<action name="start" timeout="20" />
379+<action name="stop" timeout="20" />
380+<action name="monitor" timeout="20" interval="10" depth="0" />
381+<action name="reload" timeout="20" />
382+<action name="migrate_to" timeout="20" />
383+<action name="migrate_from" timeout="20" />
384+<action name="meta-data" timeout="5" />
385+<action name="validate-all" timeout="20" />
386+</actions>
387+</resource-agent>
388+END
389+}
390+
391+#######################################################################
392+# Non API functions
393+
394+# Extract fields from slave status
395+parse_slave_info() {
396+ # Extracts field $1 from result of "SHOW SLAVE STATUS\G" from file $2
397+ sed -ne "s/^.* $1: \(.*\)$/\1/p" < $2
398+}
399+
400+# Read the slave status and
401+get_slave_info() {
402+
403+ local mysql_options tmpfile
404+
405+ if [ "$master_log_file" -a "$master_host" ]; then
406+ # variables are already defined, get_slave_info has been run before
407+ return $OCF_SUCCESS
408+ else
409+ tmpfile=`mktemp ${HA_RSCTMP}/check_slave.${OCF_RESOURCE_INSTANCE}.XXXXXX`
410+
411+ mysql_run -Q -sw -O $MYSQL $MYSQL_OPTIONS_REPL \
412+ -e 'SHOW SLAVE STATUS\G' > $tmpfile
413+
414+ if [ -s $tmpfile ]; then
415+ master_host=`parse_slave_info Master_Host $tmpfile`
416+ slave_sql=`parse_slave_info Slave_SQL_Running $tmpfile`
417+ slave_io=`parse_slave_info Slave_IO_Running $tmpfile`
418+ slave_io_state=`parse_slave_info Slave_IO_State $tmpfile`
419+ last_errno=`parse_slave_info Last_Errno $tmpfile`
420+ secs_behind=`parse_slave_info Seconds_Behind_Master $tmpfile`
421+ ocf_log debug "MySQL instance has a non empty slave status"
422+ else
423+ # Instance produced an empty "SHOW SLAVE STATUS" output --
424+ # instance is not a slave
425+
426+ ocf_log err "check_slave invoked on an instance that is not a replication slave."
427+ rm -f $tmpfile
428+ return $OCF_ERR_GENERIC
429+ fi
430+ rm -f $tmpfile
431+ return $OCF_SUCCESS
432+ fi
433+}
434+
435+get_read_only() {
436+ # Check if read-only is set
437+ local read_only_state
438+
439+ read_only_state=`mysql_run -Q -sw -O $MYSQL -N $MYSQL_OPTIONS_REPL \
440+ -e "SHOW VARIABLES like 'read_only'" | awk '{print $2}'`
441+
442+ if [ "$read_only_state" = "ON" ]; then
443+ return 0
444+ else
445+ return 1
446+ fi
447+}
448+
449+# get the attribute controlling the readers VIP
450+get_reader_attr() {
451+ local attr_value
452+ local rc
453+
454+ attr_value=`$CRM_ATTR -l reboot --name ${OCF_RESKEY_reader_attribute} --query -q`
455+ rc=$?
456+ if [ "$rc" -eq "0" ]; then
457+ echo $attr_value
458+ else
459+ echo -1
460+ fi
461+
462+}
463+
464+# Set the attribute controlling the readers VIP
465+set_reader_attr() {
466+ local curr_attr_value
467+
468+ curr_attr_value=$(get_reader_attr)
469+
470+ if [ "$1" -eq "0" ]; then
471+ if [ "$curr_attr_value" -gt "0" ]; then
472+ curr_attr_value=$((${curr_attr_value}-1))
473+ $CRM_ATTR -l reboot --name ${OCF_RESKEY_reader_attribute} -v $curr_attr_value
474+ else
475+ $CRM_ATTR -l reboot --name ${OCF_RESKEY_reader_attribute} -v 0
476+ fi
477+ else
478+ if [ "$curr_attr_value" -ne "$OCF_RESKEY_reader_failcount" ]; then
479+ $CRM_ATTR -l reboot --name ${OCF_RESKEY_reader_attribute} -v $OCF_RESKEY_reader_failcount
480+ fi
481+ fi
482+
483+}
484+
485+# get the attribute controlling the writer VIP
486+get_writer_attr() {
487+ local attr_value
488+ local rc
489+
490+ attr_value=`$CRM_ATTR -l reboot --name ${OCF_RESKEY_writer_attribute} --query -q`
491+ rc=$?
492+ if [ "$rc" -eq "0" ]; then
493+ echo $attr_value
494+ else
495+ echo -1
496+ fi
497+
498+}
499+
500+# Set the attribute controlling the writer VIP
501+set_writer_attr() {
502+ local curr_attr_value
503+
504+ curr_attr_value=$(get_writer_attr)
505+
506+ if [ "$1" -ne "$curr_attr_value" ]; then
507+ if [ "$1" -eq "0" ]; then
508+ $CRM_ATTR -l reboot --name ${OCF_RESKEY_writer_attribute} -v 0
509+ else
510+ $CRM_ATTR -l reboot --name ${OCF_RESKEY_writer_attribute} -v 1
511+ fi
512+ fi
513+}
514+
515+#
516+# mysql_run: Run a mysql command, log its output and return the proper error code.
517+# Usage: mysql_run [-Q] [-info|-warn|-err] [-O] [-sw] <command>
518+# -Q: don't log the output of the command if it succeeds
519+# -info|-warn|-err: log the output of the command at given
520+# severity if it fails (defaults to err)
521+# -O: echo the output of the command
522+# -sw: Suppress 5.6 client warning when password is used on the command line
523+# Adapted from ocf_run.
524+#
525+mysql_run() {
526+ local rc
527+ local output outputfile
528+ local verbose=1
529+ local returnoutput
530+ local loglevel=err
531+ local suppress_56_password_warning
532+ local var
533+
534+ for var in 1 2 3 4
535+ do
536+ case "$1" in
537+ "-Q")
538+ verbose=""
539+ shift 1;;
540+ "-info"|"-warn"|"-err")
541+ loglevel=`echo $1 | sed -e s/-//g`
542+ shift 1;;
543+ "-O")
544+ returnoutput=1
545+ shift 1;;
546+ "-sw")
547+ suppress_56_password_warning=1
548+ shift 1;;
549+
550+ *)
551+ ;;
552+ esac
553+ done
554+
555+ outputfile=`mktemp ${HA_RSCTMP}/mysql_run.${OCF_RESOURCE_INSTANCE}.XXXXXX`
556+ error=`"$@" 2>&1 1>$outputfile`
557+ rc=$?
558+ if [ "$suppress_56_password_warning" -eq 1 ]; then
559+ error=`echo "$error" | egrep -v '^Warning: Using a password on the command line'`
560+ fi
561+ output=`cat $outputfile`
562+ rm -f $outputfile
563+
564+ if [ $rc -eq 0 ]; then
565+ if [ "$verbose" -a ! -z "$output" ]; then
566+ ocf_log info "$output"
567+ fi
568+
569+ if [ "$returnoutput" -a ! -z "$output" ]; then
570+ echo "$output"
571+ fi
572+
573+ MYSQL_LAST_ERR=$OCF_SUCCESS
574+ return $OCF_SUCCESS
575+ else
576+ if [ ! -z "$error" ]; then
577+ ocf_log $loglevel "$error"
578+ regex='^ERROR ([[:digit:]]{4}).*'
579+ if [[ $error =~ $regex ]]; then
580+ mysql_code=${BASH_REMATCH[1]}
581+ if [ -n "$mysql_code" ]; then
582+ MYSQL_LAST_ERR=$mysql_code
583+ return $rc
584+ fi
585+ fi
586+ else
587+ ocf_log $loglevel "command failed: $*"
588+ fi
589+ # No output to parse so return the standard exit code.
590+ MYSQL_LAST_ERR=$rc
591+ return $rc
592+ fi
593+}
594+
595+
596+
597+
598+#######################################################################
599+# API functions
600+
601+mysql_monitor_usage() {
602+ cat <<END
603+usage: $0 {start|stop|monitor|migrate_to|migrate_from|validate-all|meta-data}
604+
605+Expects to have a fully populated OCF RA-compliant environment set.
606+END
607+}
608+
609+mysql_monitor_start() {
610+
611+ # Initialise the attribute in the cib if they are not already there.
612+ if [ $(get_reader_attr) -eq -1 ]; then
613+ set_reader_attr 0
614+ fi
615+
616+ if [ $(get_writer_attr) -eq -1 ]; then
617+ set_writer_attr 0
618+ fi
619+
620+ mysql_monitor
621+ mysql_monitor_monitor
622+ if [ $? = $OCF_SUCCESS ]; then
623+ return $OCF_SUCCESS
624+ fi
625+ touch ${OCF_RESKEY_state}
626+}
627+
628+mysql_monitor_stop() {
629+
630+ set_reader_attr 0
631+ set_writer_attr 0
632+
633+ mysql_monitor_monitor
634+ if [ $? = $OCF_SUCCESS ]; then
635+ rm ${OCF_RESKEY_state}
636+ fi
637+ return $OCF_SUCCESS
638+
639+}
640+
641+# Monitor MySQL, not the agent itself
642+mysql_monitor() {
643+ if [ -e $OCF_RESKEY_pid ]; then
644+ pid=`cat $OCF_RESKEY_pid`;
645+ if [ -d /proc -a -d /proc/1 ]; then
646+ [ "u$pid" != "u" -a -d /proc/$pid ]
647+ else
648+ kill -s 0 $pid >/dev/null 2>&1
649+ fi
650+
651+ if [ $? -eq 0 ]; then
652+
653+ case ${OCF_RESKEY_cluster_type} in
654+ 'replication'|'REPLICATION')
655+ if get_read_only; then
656+ # a slave?
657+
658+ set_writer_attr 0
659+
660+ get_slave_info
661+ rc=$?
662+
663+ if [ $rc -eq 0 ]; then
664+ # show slave status is not empty
665+ # Is there a master_log_file defined? (master_log_file is deleted
666+ # by reset slave
667+ if [ "$master_log_file" ]; then
668+ # is read_only but no slave config...
669+
670+ set_reader_attr 0
671+
672+ else
673+ # has a slave config
674+
675+ if [ "$slave_sql" = 'Yes' -a "$slave_io" = 'Yes' ]; then
676+ # $secs_behind can be NULL so must be tested only
677+ # if replication is OK
678+ if [ $secs_behind -gt $OCF_RESKEY_max_slave_lag ]; then
679+ set_reader_attr 0
680+ else
681+ set_reader_attr 1
682+ fi
683+ else
684+ set_reader_attr 0
685+ fi
686+ fi
687+ else
688+ # "SHOW SLAVE STATUS" returns an empty set if instance is not a
689+ # replication slave
690+
691+ set_reader_attr 0
692+
693+ fi
694+ else
695+ # host is RW
696+ set_reader_attr 1
697+ set_writer_attr 1
698+ fi
699+ ;;
700+
701+ 'pxc'|'PXC')
702+ pxcstat=`/usr/bin/clustercheck $OCF_RESKEY_user $OCF_RESKEY_password `
703+ if [ $? -eq 0 ]; then
704+ set_reader_attr 1
705+ set_writer_attr 1
706+ else
707+ set_reader_attr 0
708+ set_writer_attr 0
709+ fi
710+
711+ ;;
712+
713+ 'read-only'|'READ-ONLY')
714+ if get_read_only; then
715+ set_reader_attr 1
716+ set_writer_attr 0
717+ else
718+ set_reader_attr 1
719+ set_writer_attr 1
720+ fi
721+ ;;
722+
723+ esac
724+ else
725+ ocf_log $1 "MySQL is not running, but there is a pidfile"
726+ set_reader_attr 0
727+ set_writer_attr 0
728+ fi
729+ else
730+ ocf_log $1 "MySQL is not running"
731+ set_reader_attr 0
732+ set_writer_attr 0
733+ fi
734+}
735+
736+mysql_monitor_monitor() {
737+ # Monitor _MUST!_ differentiate correctly between running
738+ # (SUCCESS), failed (ERROR) or _cleanly_ stopped (NOT RUNNING).
739+ # That is THREE states, not just yes/no.
740+
741+ if [ -f ${OCF_RESKEY_state} ]; then
742+ return $OCF_SUCCESS
743+ fi
744+ if false ; then
745+ return $OCF_ERR_GENERIC
746+ fi
747+ return $OCF_NOT_RUNNING
748+}
749+
750+mysql_monitor_validate() {
751+
752+ # Is the state directory writable?
753+ state_dir=`dirname "$OCF_RESKEY_state"`
754+ touch "$state_dir/$$"
755+ if [ $? != 0 ]; then
756+ return $OCF_ERR_ARGS
757+ fi
758+ rm "$state_dir/$$"
759+
760+ return $OCF_SUCCESS
761+}
762+
763+##########################################################################
764+# If DEBUG_LOG is set, make this resource agent easy to debug: set up the
765+# debug log and direct all output to it. Otherwise, redirect to /dev/null.
766+# The log directory must be a directory owned by root, with permissions 0700,
767+# and the log must be writable and not a symlink.
768+##########################################################################
769+DEBUG_LOG="/tmp/mysql_monitor.ocf.ra.debug/log"
770+if [ "${DEBUG_LOG}" -a -w "${DEBUG_LOG}" -a ! -L "${DEBUG_LOG}" ]; then
771+ DEBUG_LOG_DIR="${DEBUG_LOG%/*}"
772+ if [ -d "${DEBUG_LOG_DIR}" ]; then
773+ exec 9>>"$DEBUG_LOG"
774+ exec 2>&9
775+ date >&9
776+ echo "$*" >&9
777+ env | grep OCF_ | sort >&9
778+ set -x
779+ else
780+ exec 9>/dev/null
781+ fi
782+fi
783+
784+
785+case $__OCF_ACTION in
786+meta-data) meta_data
787+ exit $OCF_SUCCESS
788+ ;;
789+start) mysql_monitor_start;;
790+stop) mysql_monitor_stop;;
791+monitor) mysql_monitor
792+ mysql_monitor_monitor;;
793+migrate_to) ocf_log info "Migrating ${OCF_RESOURCE_INSTANCE} to ${OCF_RESKEY_CRM_meta_migrate_target}."
794+ mysql_monitor_stop
795+ ;;
796+migrate_from) ocf_log info "Migrating ${OCF_RESOURCE_INSTANCE} from ${OCF_RESKEY_CRM_meta_migrate_source}."
797+ mysql_monitor_start
798+ ;;
799+reload) ocf_log info "Reloading ${OCF_RESOURCE_INSTANCE} ..."
800+ ;;
801+validate-all) mysql_monitor_validate;;
802+usage|help) mysql_monitor_usage
803+ exit $OCF_SUCCESS
804+ ;;
805+*) mysql_monitor_usage
806+ exit $OCF_ERR_UNIMPLEMENTED
807+ ;;
808+esac
809+rc=$?
810+ocf_log debug "${OCF_RESOURCE_INSTANCE} $__OCF_ACTION : $rc"
811+exit $rc
812+
813
814=== added file 'setup.cfg'
815--- setup.cfg 1970-01-01 00:00:00 +0000
816+++ setup.cfg 2015-04-07 17:10:23 +0000
817@@ -0,0 +1,6 @@
818+[nosetests]
819+verbosity=2
820+with-coverage=1
821+cover-erase=1
822+cover-package=hooks
823+
824
825=== modified file 'templates/my.cnf'
826--- templates/my.cnf 2015-03-04 15:30:55 +0000
827+++ templates/my.cnf 2015-04-07 17:10:23 +0000
828@@ -11,6 +11,7 @@
829
830 datadir=/var/lib/mysql
831 user=mysql
832+pid_file = /var/run/mysqld/mysqld.pid
833
834 # Path to Galera library
835 wsrep_provider=/usr/lib/libgalera_smm.so
836
837=== added directory 'tests'
838=== added file 'tests/00-setup.sh'
839--- tests/00-setup.sh 1970-01-01 00:00:00 +0000
840+++ tests/00-setup.sh 2015-04-07 17:10:23 +0000
841@@ -0,0 +1,29 @@
842+#!/bin/bash -x
843+# The script installs amulet and other tools needed for the amulet tests.
844+
845+# Get the status of the amulet package, this returns 0 of package is installed.
846+dpkg -s amulet
847+if [ $? -ne 0 ]; then
848+ # Install the Amulet testing harness.
849+ sudo add-apt-repository -y ppa:juju/stable
850+ sudo apt-get update
851+ sudo apt-get install -y -q amulet juju-core charm-tools
852+fi
853+
854+
855+PACKAGES="python3 python3-yaml"
856+for pkg in $PACKAGES; do
857+ dpkg -s python3
858+ if [ $? -ne 0 ]; then
859+ sudo apt-get install -y -q $pkg
860+ fi
861+done
862+
863+
864+if [ ! -f "$(dirname $0)/../local.yaml" ]; then
865+ echo "To run these amulet tests a vip is needed, create a file called \
866+local.yaml in the charm dir, this file must contain a 'vip', if you're \
867+using the local provider with lxc you could use a free IP from the range \
868+10.0.3.0/24"
869+ exit 1
870+fi
871
872=== added file 'tests/10-deploy_test.py'
873--- tests/10-deploy_test.py 1970-01-01 00:00:00 +0000
874+++ tests/10-deploy_test.py 2015-04-07 17:10:23 +0000
875@@ -0,0 +1,29 @@
876+#!/usr/bin/python3
877+# test percona-cluster (3 nodes)
878+
879+import basic_deployment
880+import time
881+
882+
883+class ThreeNode(basic_deployment.BasicDeployment):
884+ def __init__(self):
885+ super(ThreeNode, self).__init__(units=3)
886+
887+ def run(self):
888+ super(ThreeNode, self).run()
889+ # we are going to kill the master
890+ old_master = self.master_unit
891+ self.master_unit.run('sudo poweroff')
892+
893+ time.sleep(10) # give some time to pacemaker to react
894+ new_master = self.find_master()
895+ assert new_master is not None, "master unit not found"
896+ assert (new_master.info['public-address'] !=
897+ old_master.info['public-address'])
898+
899+ assert self.is_port_open(address=self.vip), 'cannot connect to vip'
900+
901+
902+if __name__ == "__main__":
903+ t = ThreeNode()
904+ t.run()
905
906=== added file 'tests/20-broken-mysqld.py'
907--- tests/20-broken-mysqld.py 1970-01-01 00:00:00 +0000
908+++ tests/20-broken-mysqld.py 2015-04-07 17:10:23 +0000
909@@ -0,0 +1,38 @@
910+#!/usr/bin/python3
911+# test percona-cluster (3 nodes)
912+
913+import basic_deployment
914+import time
915+
916+
917+class ThreeNode(basic_deployment.BasicDeployment):
918+ def __init__(self):
919+ super(ThreeNode, self).__init__(units=3)
920+
921+ def run(self):
922+ super(ThreeNode, self).run()
923+ # we are going to kill the master
924+ old_master = self.master_unit
925+ print('stopping mysql in %s' % str(self.master_unit.info))
926+ self.master_unit.run('sudo service mysql stop')
927+
928+ print('looking for the new master')
929+ i = 0
930+ changed = False
931+ while i < 10 and not changed:
932+ i += 1
933+ time.sleep(5) # give some time to pacemaker to react
934+ new_master = self.find_master()
935+
936+ if (new_master and new_master.info['unit_name'] !=
937+ old_master.info['unit_name']):
938+ changed = True
939+
940+ assert changed, "The master didn't change"
941+
942+ assert self.is_port_open(address=self.vip), 'cannot connect to vip'
943+
944+
945+if __name__ == "__main__":
946+ t = ThreeNode()
947+ t.run()
948
949=== added file 'tests/basic_deployment.py'
950--- tests/basic_deployment.py 1970-01-01 00:00:00 +0000
951+++ tests/basic_deployment.py 2015-04-07 17:10:23 +0000
952@@ -0,0 +1,126 @@
953+import amulet
954+import os
955+import telnetlib
956+import unittest
957+import yaml
958+
959+
960+class BasicDeployment(unittest.TestCase):
961+ def __init__(self, vip=None, units=1):
962+ self.units = units
963+ self.master_unit = None
964+ self.vip = None
965+ if vip:
966+ self.vip = vip
967+ elif 'VIP' in os.environ:
968+ self.vip = os.environ.get('VIP')
969+ elif os.path.isfile('local.yaml'):
970+ with open('local.yaml', 'rb') as f:
971+ self.cfg = yaml.safe_load(f.read())
972+
973+ self.vip = self.cfg.get('vip')
974+ else:
975+ amulet.raise_status(amulet.SKIP,
976+ ("please set the vip in local.yaml "
977+ "to run this test suite"))
978+
979+ def run(self):
980+ # The number of seconds to wait for the environment to setup.
981+ seconds = 1200
982+
983+ self.d = amulet.Deployment(series="trusty")
984+ self.d.add('percona-cluster', units=self.units)
985+ self.d.add('hacluster')
986+ self.d.relate('percona-cluster:ha', 'hacluster:ha')
987+
988+ cfg_percona = {'sst-password': 'ubuntu',
989+ 'root-password': 't00r',
990+ 'dataset-size': '128M',
991+ 'vip': self.vip}
992+
993+ cfg_ha = {'debug': True,
994+ 'corosync_mcastaddr': '226.94.1.4',
995+ 'corosync_key': ('xZP7GDWV0e8Qs0GxWThXirNNYlScgi3sRTdZk/IXKD'
996+ 'qkNFcwdCWfRQnqrHU/6mb6sz6OIoZzX2MtfMQIDcXu'
997+ 'PqQyvKuv7YbRyGHmQwAWDUA4ed759VWAO39kHkfWp9'
998+ 'y5RRk/wcHakTcWYMwm70upDGJEP00YT3xem3NQy27A'
999+ 'C1w=')}
1000+
1001+ self.d.configure('percona-cluster', cfg_percona)
1002+ self.d.configure('hacluster', cfg_ha)
1003+
1004+ try:
1005+ self.d.setup(timeout=seconds)
1006+ self.d.sentry.wait(seconds)
1007+ except amulet.helpers.TimeoutError:
1008+ message = 'The environment did not setup in %d seconds.' % seconds
1009+ amulet.raise_status(amulet.SKIP, msg=message)
1010+ except:
1011+ raise
1012+
1013+ self.master_unit = self.find_master()
1014+ assert self.master_unit is not None, 'percona-cluster vip not found'
1015+
1016+ output, code = self.master_unit.run('sudo crm_verify --live-check')
1017+ assert code == 0, "'crm_verify --live-check' failed"
1018+
1019+ resources = ['res_mysql_vip']
1020+ resources += ['res_mysql_monitor:%d' % i for i in range(self.units)]
1021+
1022+ assert sorted(self.get_pcmkr_resources()) == sorted(resources)
1023+
1024+ for i in range(self.units):
1025+ uid = 'percona-cluster/%d' % i
1026+ unit = self.d.sentry.unit[uid]
1027+ assert self.is_mysqld_running(unit), 'mysql not running: %s' % uid
1028+
1029+ def find_master(self):
1030+ for unit_id, unit in self.d.sentry.unit.items():
1031+ if not unit_id.startswith('percona-cluster/'):
1032+ continue
1033+
1034+ # is the vip running here?
1035+ output, code = unit.run('sudo ip a | grep %s' % self.vip)
1036+ print(unit_id)
1037+ print(output)
1038+ if code == 0:
1039+ print('vip(%s) running in %s' % (self.vip, unit_id))
1040+ return unit
1041+
1042+ def get_pcmkr_resources(self, unit=None):
1043+ if unit:
1044+ u = unit
1045+ else:
1046+ u = self.master_unit
1047+
1048+ output, code = u.run('sudo crm_resource -l')
1049+
1050+ assert code == 0, 'could not get "crm resource list"'
1051+
1052+ return output.split('\n')
1053+
1054+ def is_mysqld_running(self, unit=None):
1055+ if unit:
1056+ u = unit
1057+ else:
1058+ u = self.master_unit
1059+
1060+ output, code = u.run('pidof mysqld')
1061+
1062+ if code != 0:
1063+ return False
1064+
1065+ return self.is_port_open(u, '3306')
1066+
1067+ def is_port_open(self, unit=None, port='3306', address=None):
1068+ if unit:
1069+ addr = unit.info['public-address']
1070+ elif address:
1071+ addr = address
1072+ else:
1073+ raise Exception('Please provide a unit or address')
1074+ try:
1075+ telnetlib.Telnet(addr, port)
1076+ return True
1077+ except TimeoutError: # noqa this exception only available in py3
1078+ return False
1079
1080=== added file 'unit_tests/test_percona_hooks.py'
1081--- unit_tests/test_percona_hooks.py 1970-01-01 00:00:00 +0000
1082+++ unit_tests/test_percona_hooks.py 2015-04-07 17:10:23 +0000
1083@@ -0,0 +1,65 @@
1084+import mock
1085+import sys
1086+from test_utils import CharmTestCase
1087+
1088+sys.modules['MySQLdb'] = mock.Mock()
1089+import percona_hooks as hooks
1090+
1091+TO_PATCH = ['log', 'config',
1092+ 'get_db_helper',
1093+ 'relation_ids',
1094+ 'relation_set']
1095+
1096+
1097+class TestHaRelation(CharmTestCase):
1098+ def setUp(self):
1099+ CharmTestCase.setUp(self, hooks, TO_PATCH)
1100+
1101+ @mock.patch('sys.exit')
1102+ def test_relation_not_configured(self, exit_):
1103+ self.config.return_value = None
1104+
1105+ class MyError(Exception):
1106+ pass
1107+
1108+ def f(x):
1109+ raise MyError(x)
1110+ exit_.side_effect = f
1111+ self.assertRaises(MyError, hooks.ha_relation_joined)
1112+
1113+ def test_resources(self):
1114+ self.relation_ids.return_value = ['ha:1']
1115+ password = 'ubuntu'
1116+ helper = mock.Mock()
1117+ attrs = {'get_mysql_password.return_value': password}
1118+ helper.configure_mock(**attrs)
1119+ self.get_db_helper.return_value = helper
1120+ self.test_config.set('vip', '10.0.3.3')
1121+ self.test_config.set('sst-password', password)
1122+ def f(k):
1123+ return self.test_config.get(k)
1124+
1125+ self.config.side_effect = f
1126+ hooks.ha_relation_joined()
1127+
1128+ resources = {'res_mysql_vip': 'ocf:heartbeat:IPaddr2',
1129+ 'res_mysql_monitor': 'ocf:percona:mysql_monitor'}
1130+ resource_params = {'res_mysql_vip': ('params ip="10.0.3.3" '
1131+ 'cidr_netmask="24" '
1132+ 'nic="eth0"'),
1133+ 'res_mysql_monitor':
1134+ hooks.RES_MONITOR_PARAMS % {'sstpass': 'ubuntu'}}
1135+ groups = {'grp_percona_cluster': 'res_mysql_vip'}
1136+
1137+ clones = {'cl_mysql_monitor': 'res_mysql_monitor meta interleave=true'}
1138+
1139+ colocations = {'vip_mysqld': 'inf: grp_percona_cluster cl_mysql_monitor'}
1140+
1141+ locations = {'loc_percona_cluster':
1142+ 'grp_percona_cluster rule inf: writable eq 1'}
1143+
1144+ self.relation_set.assert_called_with(
1145+ relation_id='ha:1', corosync_bindiface=f('ha-bindiface'),
1146+ corosync_mcastport=f('ha-mcastport'), resources=resources,
1147+ resource_params=resource_params, groups=groups,
1148+ clones=clones, colocations=colocations, locations=locations)
1149
1150=== added file 'unit_tests/test_utils.py'
1151--- unit_tests/test_utils.py 1970-01-01 00:00:00 +0000
1152+++ unit_tests/test_utils.py 2015-04-07 17:10:23 +0000
1153@@ -0,0 +1,121 @@
1154+import logging
1155+import unittest
1156+import os
1157+import yaml
1158+
1159+from contextlib import contextmanager
1160+from mock import patch, MagicMock
1161+
1162+
1163+def load_config():
1164+ '''
1165+ Walk backwords from __file__ looking for config.yaml, load and return the
1166+ 'options' section'
1167+ '''
1168+ config = None
1169+ f = __file__
1170+ while config is None:
1171+ d = os.path.dirname(f)
1172+ if os.path.isfile(os.path.join(d, 'config.yaml')):
1173+ config = os.path.join(d, 'config.yaml')
1174+ break
1175+ f = d
1176+
1177+ if not config:
1178+ logging.error('Could not find config.yaml in any parent directory '
1179+ 'of %s. ' % file)
1180+ raise Exception
1181+
1182+ return yaml.safe_load(open(config).read())['options']
1183+
1184+
1185+def get_default_config():
1186+ '''
1187+ Load default charm config from config.yaml return as a dict.
1188+ If no default is set in config.yaml, its value is None.
1189+ '''
1190+ default_config = {}
1191+ config = load_config()
1192+ for k, v in config.iteritems():
1193+ if 'default' in v:
1194+ default_config[k] = v['default']
1195+ else:
1196+ default_config[k] = None
1197+ return default_config
1198+
1199+
1200+class CharmTestCase(unittest.TestCase):
1201+
1202+ def setUp(self, obj, patches):
1203+ super(CharmTestCase, self).setUp()
1204+ self.patches = patches
1205+ self.obj = obj
1206+ self.test_config = TestConfig()
1207+ self.test_relation = TestRelation()
1208+ self.patch_all()
1209+
1210+ def patch(self, method):
1211+ _m = patch.object(self.obj, method)
1212+ mock = _m.start()
1213+ self.addCleanup(_m.stop)
1214+ return mock
1215+
1216+ def patch_all(self):
1217+ for method in self.patches:
1218+ setattr(self, method, self.patch(method))
1219+
1220+
1221+class TestConfig(object):
1222+
1223+ def __init__(self):
1224+ self.config = get_default_config()
1225+
1226+ def get(self, attr=None):
1227+ if not attr:
1228+ return self.get_all()
1229+ try:
1230+ return self.config[attr]
1231+ except KeyError:
1232+ return None
1233+
1234+ def get_all(self):
1235+ return self.config
1236+
1237+ def set(self, attr, value):
1238+ if attr not in self.config:
1239+ raise KeyError
1240+ self.config[attr] = value
1241+
1242+
1243+class TestRelation(object):
1244+
1245+ def __init__(self, relation_data={}):
1246+ self.relation_data = relation_data
1247+
1248+ def set(self, relation_data):
1249+ self.relation_data = relation_data
1250+
1251+ def get(self, attr=None, unit=None, rid=None):
1252+ if attr is None:
1253+ return self.relation_data
1254+ elif attr in self.relation_data:
1255+ return self.relation_data[attr]
1256+ return None
1257+
1258+
1259+@contextmanager
1260+def patch_open():
1261+ '''Patch open() to allow mocking both open() itself and the file that is
1262+ yielded.
1263+
1264+ Yields the mock for "open" and "file", respectively.'''
1265+ mock_open = MagicMock(spec=open)
1266+ mock_file = MagicMock(spec=file)
1267+
1268+ @contextmanager
1269+ def stub_open(*args, **kwargs):
1270+ mock_open(*args, **kwargs)
1271+ yield mock_file
1272+
1273+ with patch('__builtin__.open', stub_open):
1274+ yield mock_open, mock_file

Subscribers

People subscribed via source and target branches