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: 1270 lines (+1128/-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 (+632/-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
James Page Pending
Review via email: mp+253245@code.launchpad.net

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

This proposal has been superseded by a proposal from 2015-03-17.

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
66. By Felipe Reyes

Moved mysql_monitor installation to config-changed hook

67. By Felipe Reyes

Add mysql_monitor agent to copyright definition

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-03-17 17:47: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-03-17 17:47: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-03-17 17:47: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-03-17 17:47: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-03-17 17:47: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-03-17 17:47:23 +0000
176@@ -0,0 +1,632 @@
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+ fi
725+ else
726+ ocf_log $1 "MySQL is not running"
727+ set_reader_attr 0
728+ set_writer_attr 0
729+ fi
730+}
731+
732+mysql_monitor_monitor() {
733+ # Monitor _MUST!_ differentiate correctly between running
734+ # (SUCCESS), failed (ERROR) or _cleanly_ stopped (NOT RUNNING).
735+ # That is THREE states, not just yes/no.
736+
737+ if [ -f ${OCF_RESKEY_state} ]; then
738+ return $OCF_SUCCESS
739+ fi
740+ if false ; then
741+ return $OCF_ERR_GENERIC
742+ fi
743+ return $OCF_NOT_RUNNING
744+}
745+
746+mysql_monitor_validate() {
747+
748+ # Is the state directory writable?
749+ state_dir=`dirname "$OCF_RESKEY_state"`
750+ touch "$state_dir/$$"
751+ if [ $? != 0 ]; then
752+ return $OCF_ERR_ARGS
753+ fi
754+ rm "$state_dir/$$"
755+
756+ return $OCF_SUCCESS
757+}
758+
759+##########################################################################
760+# If DEBUG_LOG is set, make this resource agent easy to debug: set up the
761+# debug log and direct all output to it. Otherwise, redirect to /dev/null.
762+# The log directory must be a directory owned by root, with permissions 0700,
763+# and the log must be writable and not a symlink.
764+##########################################################################
765+DEBUG_LOG="/tmp/mysql_monitor.ocf.ra.debug/log"
766+if [ "${DEBUG_LOG}" -a -w "${DEBUG_LOG}" -a ! -L "${DEBUG_LOG}" ]; then
767+ DEBUG_LOG_DIR="${DEBUG_LOG%/*}"
768+ if [ -d "${DEBUG_LOG_DIR}" ]; then
769+ exec 9>>"$DEBUG_LOG"
770+ exec 2>&9
771+ date >&9
772+ echo "$*" >&9
773+ env | grep OCF_ | sort >&9
774+ set -x
775+ else
776+ exec 9>/dev/null
777+ fi
778+fi
779+
780+
781+case $__OCF_ACTION in
782+meta-data) meta_data
783+ exit $OCF_SUCCESS
784+ ;;
785+start) mysql_monitor_start;;
786+stop) mysql_monitor_stop;;
787+monitor) mysql_monitor
788+ mysql_monitor_monitor;;
789+migrate_to) ocf_log info "Migrating ${OCF_RESOURCE_INSTANCE} to ${OCF_RESKEY_CRM_meta_migrate_target}."
790+ mysql_monitor_stop
791+ ;;
792+migrate_from) ocf_log info "Migrating ${OCF_RESOURCE_INSTANCE} from ${OCF_RESKEY_CRM_meta_migrate_source}."
793+ mysql_monitor_start
794+ ;;
795+reload) ocf_log info "Reloading ${OCF_RESOURCE_INSTANCE} ..."
796+ ;;
797+validate-all) mysql_monitor_validate;;
798+usage|help) mysql_monitor_usage
799+ exit $OCF_SUCCESS
800+ ;;
801+*) mysql_monitor_usage
802+ exit $OCF_ERR_UNIMPLEMENTED
803+ ;;
804+esac
805+rc=$?
806+ocf_log debug "${OCF_RESOURCE_INSTANCE} $__OCF_ACTION : $rc"
807+exit $rc
808+
809
810=== added file 'setup.cfg'
811--- setup.cfg 1970-01-01 00:00:00 +0000
812+++ setup.cfg 2015-03-17 17:47:23 +0000
813@@ -0,0 +1,6 @@
814+[nosetests]
815+verbosity=2
816+with-coverage=1
817+cover-erase=1
818+cover-package=hooks
819+
820
821=== modified file 'templates/my.cnf'
822--- templates/my.cnf 2015-03-04 15:30:55 +0000
823+++ templates/my.cnf 2015-03-17 17:47:23 +0000
824@@ -11,6 +11,7 @@
825
826 datadir=/var/lib/mysql
827 user=mysql
828+pid_file = /var/run/mysqld/mysqld.pid
829
830 # Path to Galera library
831 wsrep_provider=/usr/lib/libgalera_smm.so
832
833=== added directory 'tests'
834=== added file 'tests/00-setup.sh'
835--- tests/00-setup.sh 1970-01-01 00:00:00 +0000
836+++ tests/00-setup.sh 2015-03-17 17:47:23 +0000
837@@ -0,0 +1,29 @@
838+#!/bin/bash -x
839+# The script installs amulet and other tools needed for the amulet tests.
840+
841+# Get the status of the amulet package, this returns 0 of package is installed.
842+dpkg -s amulet
843+if [ $? -ne 0 ]; then
844+ # Install the Amulet testing harness.
845+ sudo add-apt-repository -y ppa:juju/stable
846+ sudo apt-get update
847+ sudo apt-get install -y -q amulet juju-core charm-tools
848+fi
849+
850+
851+PACKAGES="python3 python3-yaml"
852+for pkg in $PACKAGES; do
853+ dpkg -s python3
854+ if [ $? -ne 0 ]; then
855+ sudo apt-get install -y -q $pkg
856+ fi
857+done
858+
859+
860+if [ ! -f "$(dirname $0)/../local.yaml" ]; then
861+ echo "To run these amulet tests a vip is needed, create a file called \
862+local.yaml in the charm dir, this file must contain a 'vip', if you're \
863+using the local provider with lxc you could use a free IP from the range \
864+10.0.3.0/24"
865+ exit 1
866+fi
867
868=== added file 'tests/10-deploy_test.py'
869--- tests/10-deploy_test.py 1970-01-01 00:00:00 +0000
870+++ tests/10-deploy_test.py 2015-03-17 17:47:23 +0000
871@@ -0,0 +1,29 @@
872+#!/usr/bin/python3
873+# test percona-cluster (3 nodes)
874+
875+import basic_deployment
876+import time
877+
878+
879+class ThreeNode(basic_deployment.BasicDeployment):
880+ def __init__(self):
881+ super(ThreeNode, self).__init__(units=3)
882+
883+ def run(self):
884+ super(ThreeNode, self).run()
885+ # we are going to kill the master
886+ old_master = self.master_unit
887+ self.master_unit.run('sudo poweroff')
888+
889+ time.sleep(10) # give some time to pacemaker to react
890+ new_master = self.find_master()
891+ assert new_master is not None, "master unit not found"
892+ assert (new_master.info['public-address'] !=
893+ old_master.info['public-address'])
894+
895+ assert self.is_port_open(address=self.vip), 'cannot connect to vip'
896+
897+
898+if __name__ == "__main__":
899+ t = ThreeNode()
900+ t.run()
901
902=== added file 'tests/20-broken-mysqld.py'
903--- tests/20-broken-mysqld.py 1970-01-01 00:00:00 +0000
904+++ tests/20-broken-mysqld.py 2015-03-17 17:47:23 +0000
905@@ -0,0 +1,38 @@
906+#!/usr/bin/python3
907+# test percona-cluster (3 nodes)
908+
909+import basic_deployment
910+import time
911+
912+
913+class ThreeNode(basic_deployment.BasicDeployment):
914+ def __init__(self):
915+ super(ThreeNode, self).__init__(units=3)
916+
917+ def run(self):
918+ super(ThreeNode, self).run()
919+ # we are going to kill the master
920+ old_master = self.master_unit
921+ print('stopping mysql in %s' % str(self.master_unit.info))
922+ self.master_unit.run('sudo service mysql stop')
923+
924+ print('looking for the new master')
925+ i = 0
926+ changed = False
927+ while i < 10 and not changed:
928+ i += 1
929+ time.sleep(5) # give some time to pacemaker to react
930+ new_master = self.find_master()
931+
932+ if (new_master and new_master.info['unit_name'] !=
933+ old_master.info['unit_name']):
934+ changed = True
935+
936+ assert changed, "The master didn't change"
937+
938+ assert self.is_port_open(address=self.vip), 'cannot connect to vip'
939+
940+
941+if __name__ == "__main__":
942+ t = ThreeNode()
943+ t.run()
944
945=== added file 'tests/basic_deployment.py'
946--- tests/basic_deployment.py 1970-01-01 00:00:00 +0000
947+++ tests/basic_deployment.py 2015-03-17 17:47:23 +0000
948@@ -0,0 +1,126 @@
949+import amulet
950+import os
951+import telnetlib
952+import unittest
953+import yaml
954+
955+
956+class BasicDeployment(unittest.TestCase):
957+ def __init__(self, vip=None, units=1):
958+ self.units = units
959+ self.master_unit = None
960+ self.vip = None
961+ if vip:
962+ self.vip = vip
963+ elif 'VIP' in os.environ:
964+ self.vip = os.environ.get('VIP')
965+ elif os.path.isfile('local.yaml'):
966+ with open('local.yaml', 'rb') as f:
967+ self.cfg = yaml.safe_load(f.read())
968+
969+ self.vip = self.cfg.get('vip')
970+ else:
971+ amulet.raise_status(amulet.SKIP,
972+ ("please set the vip in local.yaml "
973+ "to run this test suite"))
974+
975+ def run(self):
976+ # The number of seconds to wait for the environment to setup.
977+ seconds = 1200
978+
979+ self.d = amulet.Deployment(series="trusty")
980+ self.d.add('percona-cluster', units=self.units)
981+ self.d.add('hacluster')
982+ self.d.relate('percona-cluster:ha', 'hacluster:ha')
983+
984+ cfg_percona = {'sst-password': 'ubuntu',
985+ 'root-password': 't00r',
986+ 'dataset-size': '128M',
987+ 'vip': self.vip}
988+
989+ cfg_ha = {'debug': True,
990+ 'corosync_mcastaddr': '226.94.1.4',
991+ 'corosync_key': ('xZP7GDWV0e8Qs0GxWThXirNNYlScgi3sRTdZk/IXKD'
992+ 'qkNFcwdCWfRQnqrHU/6mb6sz6OIoZzX2MtfMQIDcXu'
993+ 'PqQyvKuv7YbRyGHmQwAWDUA4ed759VWAO39kHkfWp9'
994+ 'y5RRk/wcHakTcWYMwm70upDGJEP00YT3xem3NQy27A'
995+ 'C1w=')}
996+
997+ self.d.configure('percona-cluster', cfg_percona)
998+ self.d.configure('hacluster', cfg_ha)
999+
1000+ try:
1001+ self.d.setup(timeout=seconds)
1002+ self.d.sentry.wait(seconds)
1003+ except amulet.helpers.TimeoutError:
1004+ message = 'The environment did not setup in %d seconds.' % seconds
1005+ amulet.raise_status(amulet.SKIP, msg=message)
1006+ except:
1007+ raise
1008+
1009+ self.master_unit = self.find_master()
1010+ assert self.master_unit is not None, 'percona-cluster vip not found'
1011+
1012+ output, code = self.master_unit.run('sudo crm_verify --live-check')
1013+ assert code == 0, "'crm_verify --live-check' failed"
1014+
1015+ resources = ['res_mysql_vip']
1016+ resources += ['res_mysql_monitor:%d' % i for i in range(self.units)]
1017+
1018+ assert sorted(self.get_pcmkr_resources()) == sorted(resources)
1019+
1020+ for i in range(self.units):
1021+ uid = 'percona-cluster/%d' % i
1022+ unit = self.d.sentry.unit[uid]
1023+ assert self.is_mysqld_running(unit), 'mysql not running: %s' % uid
1024+
1025+ def find_master(self):
1026+ for unit_id, unit in self.d.sentry.unit.items():
1027+ if not unit_id.startswith('percona-cluster/'):
1028+ continue
1029+
1030+ # is the vip running here?
1031+ output, code = unit.run('sudo ip a | grep %s' % self.vip)
1032+ print(unit_id)
1033+ print(output)
1034+ if code == 0:
1035+ print('vip(%s) running in %s' % (self.vip, unit_id))
1036+ return unit
1037+
1038+ def get_pcmkr_resources(self, unit=None):
1039+ if unit:
1040+ u = unit
1041+ else:
1042+ u = self.master_unit
1043+
1044+ output, code = u.run('sudo crm_resource -l')
1045+
1046+ assert code == 0, 'could not get "crm resource list"'
1047+
1048+ return output.split('\n')
1049+
1050+ def is_mysqld_running(self, unit=None):
1051+ if unit:
1052+ u = unit
1053+ else:
1054+ u = self.master_unit
1055+
1056+ output, code = u.run('pidof mysqld')
1057+
1058+ if code != 0:
1059+ return False
1060+
1061+ return self.is_port_open(u, '3306')
1062+
1063+ def is_port_open(self, unit=None, port='3306', address=None):
1064+ if unit:
1065+ addr = unit.info['public-address']
1066+ elif address:
1067+ addr = address
1068+ else:
1069+ raise Exception('Please provide a unit or address')
1070+ try:
1071+ telnetlib.Telnet(addr, port)
1072+ return True
1073+ except TimeoutError: # noqa this exception only available in py3
1074+ return False
1075
1076=== added file 'unit_tests/test_percona_hooks.py'
1077--- unit_tests/test_percona_hooks.py 1970-01-01 00:00:00 +0000
1078+++ unit_tests/test_percona_hooks.py 2015-03-17 17:47:23 +0000
1079@@ -0,0 +1,65 @@
1080+import mock
1081+import sys
1082+from test_utils import CharmTestCase
1083+
1084+sys.modules['MySQLdb'] = mock.Mock()
1085+import percona_hooks as hooks
1086+
1087+TO_PATCH = ['log', 'config',
1088+ 'get_db_helper',
1089+ 'relation_ids',
1090+ 'relation_set']
1091+
1092+
1093+class TestHaRelation(CharmTestCase):
1094+ def setUp(self):
1095+ CharmTestCase.setUp(self, hooks, TO_PATCH)
1096+
1097+ @mock.patch('sys.exit')
1098+ def test_relation_not_configured(self, exit_):
1099+ self.config.return_value = None
1100+
1101+ class MyError(Exception):
1102+ pass
1103+
1104+ def f(x):
1105+ raise MyError(x)
1106+ exit_.side_effect = f
1107+ self.assertRaises(MyError, hooks.ha_relation_joined)
1108+
1109+ def test_resources(self):
1110+ self.relation_ids.return_value = ['ha:1']
1111+ password = 'ubuntu'
1112+ helper = mock.Mock()
1113+ attrs = {'get_mysql_password.return_value': password}
1114+ helper.configure_mock(**attrs)
1115+ self.get_db_helper.return_value = helper
1116+ self.test_config.set('vip', '10.0.3.3')
1117+ self.test_config.set('sst-password', password)
1118+ def f(k):
1119+ return self.test_config.get(k)
1120+
1121+ self.config.side_effect = f
1122+ hooks.ha_relation_joined()
1123+
1124+ resources = {'res_mysql_vip': 'ocf:heartbeat:IPaddr2',
1125+ 'res_mysql_monitor': 'ocf:percona:mysql_monitor'}
1126+ resource_params = {'res_mysql_vip': ('params ip="10.0.3.3" '
1127+ 'cidr_netmask="24" '
1128+ 'nic="eth0"'),
1129+ 'res_mysql_monitor':
1130+ hooks.RES_MONITOR_PARAMS % {'sstpass': 'ubuntu'}}
1131+ groups = {'grp_percona_cluster': 'res_mysql_vip'}
1132+
1133+ clones = {'cl_mysql_monitor': 'res_mysql_monitor meta interleave=true'}
1134+
1135+ colocations = {'vip_mysqld': 'inf: grp_percona_cluster cl_mysql_monitor'}
1136+
1137+ locations = {'loc_percona_cluster':
1138+ 'grp_percona_cluster rule inf: writable eq 1'}
1139+
1140+ self.relation_set.assert_called_with(
1141+ relation_id='ha:1', corosync_bindiface=f('ha-bindiface'),
1142+ corosync_mcastport=f('ha-mcastport'), resources=resources,
1143+ resource_params=resource_params, groups=groups,
1144+ clones=clones, colocations=colocations, locations=locations)
1145
1146=== added file 'unit_tests/test_utils.py'
1147--- unit_tests/test_utils.py 1970-01-01 00:00:00 +0000
1148+++ unit_tests/test_utils.py 2015-03-17 17:47:23 +0000
1149@@ -0,0 +1,121 @@
1150+import logging
1151+import unittest
1152+import os
1153+import yaml
1154+
1155+from contextlib import contextmanager
1156+from mock import patch, MagicMock
1157+
1158+
1159+def load_config():
1160+ '''
1161+ Walk backwords from __file__ looking for config.yaml, load and return the
1162+ 'options' section'
1163+ '''
1164+ config = None
1165+ f = __file__
1166+ while config is None:
1167+ d = os.path.dirname(f)
1168+ if os.path.isfile(os.path.join(d, 'config.yaml')):
1169+ config = os.path.join(d, 'config.yaml')
1170+ break
1171+ f = d
1172+
1173+ if not config:
1174+ logging.error('Could not find config.yaml in any parent directory '
1175+ 'of %s. ' % file)
1176+ raise Exception
1177+
1178+ return yaml.safe_load(open(config).read())['options']
1179+
1180+
1181+def get_default_config():
1182+ '''
1183+ Load default charm config from config.yaml return as a dict.
1184+ If no default is set in config.yaml, its value is None.
1185+ '''
1186+ default_config = {}
1187+ config = load_config()
1188+ for k, v in config.iteritems():
1189+ if 'default' in v:
1190+ default_config[k] = v['default']
1191+ else:
1192+ default_config[k] = None
1193+ return default_config
1194+
1195+
1196+class CharmTestCase(unittest.TestCase):
1197+
1198+ def setUp(self, obj, patches):
1199+ super(CharmTestCase, self).setUp()
1200+ self.patches = patches
1201+ self.obj = obj
1202+ self.test_config = TestConfig()
1203+ self.test_relation = TestRelation()
1204+ self.patch_all()
1205+
1206+ def patch(self, method):
1207+ _m = patch.object(self.obj, method)
1208+ mock = _m.start()
1209+ self.addCleanup(_m.stop)
1210+ return mock
1211+
1212+ def patch_all(self):
1213+ for method in self.patches:
1214+ setattr(self, method, self.patch(method))
1215+
1216+
1217+class TestConfig(object):
1218+
1219+ def __init__(self):
1220+ self.config = get_default_config()
1221+
1222+ def get(self, attr=None):
1223+ if not attr:
1224+ return self.get_all()
1225+ try:
1226+ return self.config[attr]
1227+ except KeyError:
1228+ return None
1229+
1230+ def get_all(self):
1231+ return self.config
1232+
1233+ def set(self, attr, value):
1234+ if attr not in self.config:
1235+ raise KeyError
1236+ self.config[attr] = value
1237+
1238+
1239+class TestRelation(object):
1240+
1241+ def __init__(self, relation_data={}):
1242+ self.relation_data = relation_data
1243+
1244+ def set(self, relation_data):
1245+ self.relation_data = relation_data
1246+
1247+ def get(self, attr=None, unit=None, rid=None):
1248+ if attr is None:
1249+ return self.relation_data
1250+ elif attr in self.relation_data:
1251+ return self.relation_data[attr]
1252+ return None
1253+
1254+
1255+@contextmanager
1256+def patch_open():
1257+ '''Patch open() to allow mocking both open() itself and the file that is
1258+ yielded.
1259+
1260+ Yields the mock for "open" and "file", respectively.'''
1261+ mock_open = MagicMock(spec=open)
1262+ mock_file = MagicMock(spec=file)
1263+
1264+ @contextmanager
1265+ def stub_open(*args, **kwargs):
1266+ mock_open(*args, **kwargs)
1267+ yield mock_file
1268+
1269+ with patch('__builtin__.open', stub_open):
1270+ yield mock_open, mock_file

Subscribers

People subscribed via source and target branches