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
=== modified file '.bzrignore'
--- .bzrignore 2015-02-06 07:28:54 +0000
+++ .bzrignore 2015-04-07 17:10:23 +0000
@@ -2,3 +2,6 @@
2.coverage2.coverage
3.pydevproject3.pydevproject
4.project4.project
5*.pyc
6*.pyo
7__pycache__
58
=== modified file 'Makefile'
--- Makefile 2014-10-02 16:12:44 +0000
+++ Makefile 2015-04-07 17:10:23 +0000
@@ -9,6 +9,10 @@
9unit_test:9unit_test:
10 @$(PYTHON) /usr/bin/nosetests --nologcapture unit_tests10 @$(PYTHON) /usr/bin/nosetests --nologcapture unit_tests
1111
12functional_test:
13 @echo Starting amulet tests...
14 @juju test -v -p AMULET_HTTP_PROXY --timeout 900
15
12bin/charm_helpers_sync.py:16bin/charm_helpers_sync.py:
13 @mkdir -p bin17 @mkdir -p bin
14 @bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \18 @bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \
1519
=== modified file 'copyright'
--- copyright 2013-09-19 15:40:50 +0000
+++ copyright 2015-04-07 17:10:23 +0000
@@ -15,3 +15,25 @@
15 .15 .
16 You should have received a copy of the GNU General Public License16 You should have received a copy of the GNU General Public License
17 along with this program. If not, see <http://www.gnu.org/licenses/>.17 along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19Files: ocf/percona/mysql_monitor
20Copyright: Copyright (c) 2013, Percona inc., Yves Trudeau, Michael Coburn
21License: GPL-2
22 This program is free software; you can redistribute it and/or modify
23 it under the terms of version 2 of the GNU General Public License as
24 published by the Free Software Foundation.
25
26 This program is distributed in the hope that it would be useful, but
27 WITHOUT ANY WARRANTY; without even the implied warranty of
28 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
29
30 Further, this software is distributed without any warranty that it is
31 free of the rightful claim of any third person regarding infringement
32 or the like. Any license provided herein, whether implied or
33 otherwise, applies only to this software file. Patent licenses, if
34 any, provided herein do not apply to combinations of this program with
35 other software, or any other product whatsoever.
36
37 You should have received a copy of the GNU General Public License
38 along with this program; if not, write the Free Software Foundation,
39 Inc., 59 Temple Place - Suite 330, Boston MA 02111-1307, USA.
1840
=== modified file 'hooks/percona_hooks.py'
--- hooks/percona_hooks.py 2015-02-16 14:12:42 +0000
+++ hooks/percona_hooks.py 2015-04-07 17:10:23 +0000
@@ -50,6 +50,7 @@
50 assert_charm_supports_ipv6,50 assert_charm_supports_ipv6,
51 unit_sorted,51 unit_sorted,
52 get_db_helper,52 get_db_helper,
53 install_mysql_ocf,
53)54)
54from charmhelpers.contrib.database.mysql import (55from charmhelpers.contrib.database.mysql import (
55 PerconaClusterHelper,56 PerconaClusterHelper,
@@ -72,6 +73,13 @@
72hooks = Hooks()73hooks = Hooks()
7374
74LEADER_RES = 'grp_percona_cluster'75LEADER_RES = 'grp_percona_cluster'
76RES_MONITOR_PARAMS = ('params user="sstuser" password="%(sstpass)s" '
77 'pid="/var/run/mysqld/mysqld.pid" '
78 'socket="/var/run/mysqld/mysqld.sock" '
79 'max_slave_lag="5" '
80 'cluster_type="pxc" '
81 'op monitor interval="1s" timeout="30s" '
82 'OCF_CHECK_LEVEL="1"')
7583
7684
77@hooks.hook('install')85@hooks.hook('install')
@@ -155,6 +163,13 @@
155 for unit in related_units(r_id):163 for unit in related_units(r_id):
156 shared_db_changed(r_id, unit)164 shared_db_changed(r_id, unit)
157165
166 # (re)install pcmkr agent
167 install_mysql_ocf()
168
169 if relation_ids('ha'):
170 # make sure all the HA resources are (re)created
171 ha_relation_joined()
172
158173
159@hooks.hook('cluster-relation-joined')174@hooks.hook('cluster-relation-joined')
160def cluster_joined(relation_id=None):175def cluster_joined(relation_id=None):
@@ -387,17 +402,34 @@
387 vip_params = 'params ip="%s" cidr_netmask="%s" nic="%s"' % \402 vip_params = 'params ip="%s" cidr_netmask="%s" nic="%s"' % \
388 (vip, vip_cidr, vip_iface)403 (vip, vip_cidr, vip_iface)
389404
390 resources = {'res_mysql_vip': res_mysql_vip}405 resources = {'res_mysql_vip': res_mysql_vip,
391 resource_params = {'res_mysql_vip': vip_params}406 'res_mysql_monitor': 'ocf:percona:mysql_monitor'}
407 db_helper = get_db_helper()
408 cfg_passwd = config('sst-password')
409 sstpsswd = db_helper.get_mysql_password(username='sstuser',
410 password=cfg_passwd)
411 resource_params = {'res_mysql_vip': vip_params,
412 'res_mysql_monitor':
413 RES_MONITOR_PARAMS % {'sstpass': sstpsswd}}
392 groups = {'grp_percona_cluster': 'res_mysql_vip'}414 groups = {'grp_percona_cluster': 'res_mysql_vip'}
393415
416 clones = {'cl_mysql_monitor': 'res_mysql_monitor meta interleave=true'}
417
418 colocations = {'vip_mysqld': 'inf: grp_percona_cluster cl_mysql_monitor'}
419
420 locations = {'loc_percona_cluster':
421 'grp_percona_cluster rule inf: writable eq 1'}
422
394 for rel_id in relation_ids('ha'):423 for rel_id in relation_ids('ha'):
395 relation_set(relation_id=rel_id,424 relation_set(relation_id=rel_id,
396 corosync_bindiface=corosync_bindiface,425 corosync_bindiface=corosync_bindiface,
397 corosync_mcastport=corosync_mcastport,426 corosync_mcastport=corosync_mcastport,
398 resources=resources,427 resources=resources,
399 resource_params=resource_params,428 resource_params=resource_params,
400 groups=groups)429 groups=groups,
430 clones=clones,
431 colocations=colocations,
432 locations=locations)
401433
402434
403@hooks.hook('ha-relation-changed')435@hooks.hook('ha-relation-changed')
404436
=== modified file 'hooks/percona_utils.py'
--- hooks/percona_utils.py 2015-02-05 09:59:36 +0000
+++ hooks/percona_utils.py 2015-04-07 17:10:23 +0000
@@ -4,10 +4,12 @@
4import socket4import socket
5import tempfile5import tempfile
6import os6import os
7import shutil
7from charmhelpers.core.host import (8from charmhelpers.core.host import (
8 lsb_release9 lsb_release
9)10)
10from charmhelpers.core.hookenv import (11from charmhelpers.core.hookenv import (
12 charm_dir,
11 unit_get,13 unit_get,
12 relation_ids,14 relation_ids,
13 related_units,15 related_units,
@@ -229,3 +231,18 @@
229 """Return a sorted list of unit names."""231 """Return a sorted list of unit names."""
230 return sorted(232 return sorted(
231 units, lambda a, b: cmp(int(a.split('/')[-1]), int(b.split('/')[-1])))233 units, lambda a, b: cmp(int(a.split('/')[-1]), int(b.split('/')[-1])))
234
235
236def install_mysql_ocf():
237 dest_dir = '/usr/lib/ocf/resource.d/percona/'
238 for fname in ['ocf/percona/mysql_monitor']:
239 src_file = os.path.join(charm_dir(), fname)
240 if not os.path.isdir(dest_dir):
241 os.makedirs(dest_dir)
242
243 dest_file = os.path.join(dest_dir, os.path.basename(src_file))
244 if not os.path.exists(dest_file):
245 log('Installing %s' % dest_file, level='INFO')
246 shutil.copy(src_file, dest_file)
247 else:
248 log("'%s' already exists, skipping" % dest_file, level='INFO')
232249
=== added directory 'ocf'
=== added directory 'ocf/percona'
=== added file 'ocf/percona/mysql_monitor'
--- ocf/percona/mysql_monitor 1970-01-01 00:00:00 +0000
+++ ocf/percona/mysql_monitor 2015-04-07 17:10:23 +0000
@@ -0,0 +1,636 @@
1#!/bin/bash
2#
3#
4# MySQL_Monitor agent, set writeable and readable attributes based on the
5# state of the local MySQL, running and read_only or not. The agent basis is
6# the original "Dummy" agent written by Lars Marowsky-Brée and part of the
7# Pacemaker distribution. Many functions are from mysql_prm.
8#
9#
10# Copyright (c) 2013, Percona inc., Yves Trudeau, Michael Coburn
11#
12# This program is free software; you can redistribute it and/or modify
13# it under the terms of version 2 of the GNU General Public License as
14# published by the Free Software Foundation.
15#
16# This program is distributed in the hope that it would be useful, but
17# WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
19#
20# Further, this software is distributed without any warranty that it is
21# free of the rightful claim of any third person regarding infringement
22# or the like. Any license provided herein, whether implied or
23# otherwise, applies only to this software file. Patent licenses, if
24# any, provided herein do not apply to combinations of this program with
25# other software, or any other product whatsoever.
26#
27# You should have received a copy of the GNU General Public License
28# along with this program; if not, write the Free Software Foundation,
29# Inc., 59 Temple Place - Suite 330, Boston MA 02111-1307, USA.
30#
31# Version: 20131119163921
32#
33# See usage() function below for more details...
34#
35# OCF instance parameters:
36#
37# OCF_RESKEY_state
38# OCF_RESKEY_user
39# OCF_RESKEY_password
40# OCF_RESKEY_client_binary
41# OCF_RESKEY_pid
42# OCF_RESKEY_socket
43# OCF_RESKEY_reader_attribute
44# OCF_RESKEY_reader_failcount
45# OCF_RESKEY_writer_attribute
46# OCF_RESKEY_max_slave_lag
47# OCF_RESKEY_cluster_type
48#
49#######################################################################
50# Initialization:
51
52: ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/lib/heartbeat}
53. ${OCF_FUNCTIONS_DIR}/ocf-shellfuncs
54
55#######################################################################
56
57HOSTOS=`uname`
58if [ "X${HOSTOS}" = "XOpenBSD" ];then
59OCF_RESKEY_client_binary_default="/usr/local/bin/mysql"
60OCF_RESKEY_pid_default="/var/mysql/mysqld.pid"
61OCF_RESKEY_socket_default="/var/run/mysql/mysql.sock"
62else
63OCF_RESKEY_client_binary_default="/usr/bin/mysql"
64OCF_RESKEY_pid_default="/var/run/mysql/mysqld.pid"
65OCF_RESKEY_socket_default="/var/lib/mysql/mysql.sock"
66fi
67OCF_RESKEY_reader_attribute_default="readable"
68OCF_RESKEY_writer_attribute_default="writable"
69OCF_RESKEY_reader_failcount_default="1"
70OCF_RESKEY_user_default="root"
71OCF_RESKEY_password_default=""
72OCF_RESKEY_max_slave_lag_default="3600"
73OCF_RESKEY_cluster_type_default="replication"
74
75: ${OCF_RESKEY_state=${HA_RSCTMP}/mysql-monitor-${OCF_RESOURCE_INSTANCE}.state}
76: ${OCF_RESKEY_client_binary=${OCF_RESKEY_client_binary_default}}
77: ${OCF_RESKEY_pid=${OCF_RESKEY_pid_default}}
78: ${OCF_RESKEY_socket=${OCF_RESKEY_socket_default}}
79: ${OCF_RESKEY_reader_attribute=${OCF_RESKEY_reader_attribute_default}}
80: ${OCF_RESKEY_reader_failcount=${OCF_RESKEY_reader_failcount_default}}
81: ${OCF_RESKEY_writer_attribute=${OCF_RESKEY_writer_attribute_default}}
82: ${OCF_RESKEY_user=${OCF_RESKEY_user_default}}
83: ${OCF_RESKEY_password=${OCF_RESKEY_password_default}}
84: ${OCF_RESKEY_max_slave_lag=${OCF_RESKEY_max_slave_lag_default}}
85: ${OCF_RESKEY_cluster_type=${OCF_RESKEY_cluster_type_default}}
86
87MYSQL="$OCF_RESKEY_client_binary -A -S $OCF_RESKEY_socket --connect_timeout=10 --user=$OCF_RESKEY_user --password=$OCF_RESKEY_password "
88HOSTNAME=`uname -n`
89CRM_ATTR="${HA_SBIN_DIR}/crm_attribute -N $HOSTNAME "
90
91meta_data() {
92 cat <<END
93<?xml version="1.0"?>
94<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
95<resource-agent name="mysql_monitor" version="0.9">
96<version>1.0</version>
97
98<longdesc lang="en">
99This agent monitors the local MySQL instance and set the writable and readable
100attributes according to what it finds. It checks if MySQL is running and if
101it is read-only or not.
102</longdesc>
103<shortdesc lang="en">Agent monitoring mysql</shortdesc>
104
105<parameters>
106<parameter name="state" unique="1">
107<longdesc lang="en">
108Location to store the resource state in.
109</longdesc>
110<shortdesc lang="en">State file</shortdesc>
111<content type="string" default="${HA_RSCTMP}/Mysql-monitor-${OCF_RESOURCE_INSTANCE}.state" />
112</parameter>
113
114<parameter name="user" unique="0">
115<longdesc lang="en">
116MySQL user to connect to the local MySQL instance to check the slave status and
117if the read_only variable is set. It requires the replication client priviledge.
118</longdesc>
119<shortdesc lang="en">MySQL user</shortdesc>
120<content type="string" default="${OCF_RESKEY_user_default}" />
121</parameter>
122
123<parameter name="password" unique="0">
124<longdesc lang="en">
125Password of the mysql user to connect to the local MySQL instance
126</longdesc>
127<shortdesc lang="en">MySQL password</shortdesc>
128<content type="string" default="${OCF_RESKEY_password_default}" />
129</parameter>
130
131<parameter name="client_binary" unique="0">
132<longdesc lang="en">
133MySQL Client Binary path.
134</longdesc>
135<shortdesc lang="en">MySQL client binary path</shortdesc>
136<content type="string" default="${OCF_RESKEY_client_binary_default}" />
137</parameter>
138
139<parameter name="socket" unique="0">
140<longdesc lang="en">
141Unix socket to use in order to connect to MySQL on the host
142</longdesc>
143<shortdesc lang="en">MySQL socket</shortdesc>
144<content type="string" default="${OCF_RESKEY_socket_default}" />
145</parameter>
146
147<parameter name="pid" unique="0">
148<longdesc lang="en">
149MySQL pid file, used to verify MySQL is running.
150</longdesc>
151<shortdesc lang="en">MySQL pid file</shortdesc>
152<content type="string" default="${OCF_RESKEY_pid_default}" />
153</parameter>
154
155<parameter name="reader_attribute" unique="0">
156<longdesc lang="en">
157The reader attribute in the cib that can be used by location rules to allow or not
158reader VIPs on a host.
159</longdesc>
160<shortdesc lang="en">Reader attribute</shortdesc>
161<content type="string" default="${OCF_RESKEY_reader_attribute_default}" />
162</parameter>
163
164<parameter name="writer_attribute" unique="0">
165<longdesc lang="en">
166The reader attribute in the cib that can be used by location rules to allow or not
167reader VIPs on a host.
168</longdesc>
169<shortdesc lang="en">Writer attribute</shortdesc>
170<content type="string" default="${OCF_RESKEY_writer_attribute_default}" />
171</parameter>
172
173<parameter name="max_slave_lag" unique="0" required="0">
174<longdesc lang="en">
175The maximum number of seconds a replication slave is allowed to lag
176behind its master in order to have a reader VIP on it.
177</longdesc>
178<shortdesc lang="en">Maximum time (seconds) a MySQL slave is allowed
179to lag behind a master</shortdesc>
180<content type="integer" default="${OCF_RESKEY_max_slave_lag_default}"/>
181</parameter>
182
183<parameter name="cluster_type" unique="0" required="0">
184<longdesc lang="en">
185Type of cluster, three possible values: pxc, replication, read-only. "pxc" is
186for Percona XtraDB cluster, it uses the clustercheck script and set the
187reader_attribute and writer_attribute according to the return code.
188"replication" checks the read-only state and the slave status, only writable
189node(s) will get the writer_attribute (and the reader_attribute) and on the
190read-only nodes, replication status will be checked and the reader_attribute set
191according to the state. "read-only" will just check if the read-only variable,
192if read/write, it will get both the writer_attribute and reader_attribute set, if
193read-only it will get only the reader_attribute.
194</longdesc>
195<shortdesc lang="en">Type of cluster</shortdesc>
196<content type="string" default="${OCF_RESKEY_cluster_type_default}"/>
197</parameter>
198
199</parameters>
200
201<actions>
202<action name="start" timeout="20" />
203<action name="stop" timeout="20" />
204<action name="monitor" timeout="20" interval="10" depth="0" />
205<action name="reload" timeout="20" />
206<action name="migrate_to" timeout="20" />
207<action name="migrate_from" timeout="20" />
208<action name="meta-data" timeout="5" />
209<action name="validate-all" timeout="20" />
210</actions>
211</resource-agent>
212END
213}
214
215#######################################################################
216# Non API functions
217
218# Extract fields from slave status
219parse_slave_info() {
220 # Extracts field $1 from result of "SHOW SLAVE STATUS\G" from file $2
221 sed -ne "s/^.* $1: \(.*\)$/\1/p" < $2
222}
223
224# Read the slave status and
225get_slave_info() {
226
227 local mysql_options tmpfile
228
229 if [ "$master_log_file" -a "$master_host" ]; then
230 # variables are already defined, get_slave_info has been run before
231 return $OCF_SUCCESS
232 else
233 tmpfile=`mktemp ${HA_RSCTMP}/check_slave.${OCF_RESOURCE_INSTANCE}.XXXXXX`
234
235 mysql_run -Q -sw -O $MYSQL $MYSQL_OPTIONS_REPL \
236 -e 'SHOW SLAVE STATUS\G' > $tmpfile
237
238 if [ -s $tmpfile ]; then
239 master_host=`parse_slave_info Master_Host $tmpfile`
240 slave_sql=`parse_slave_info Slave_SQL_Running $tmpfile`
241 slave_io=`parse_slave_info Slave_IO_Running $tmpfile`
242 slave_io_state=`parse_slave_info Slave_IO_State $tmpfile`
243 last_errno=`parse_slave_info Last_Errno $tmpfile`
244 secs_behind=`parse_slave_info Seconds_Behind_Master $tmpfile`
245 ocf_log debug "MySQL instance has a non empty slave status"
246 else
247 # Instance produced an empty "SHOW SLAVE STATUS" output --
248 # instance is not a slave
249
250 ocf_log err "check_slave invoked on an instance that is not a replication slave."
251 rm -f $tmpfile
252 return $OCF_ERR_GENERIC
253 fi
254 rm -f $tmpfile
255 return $OCF_SUCCESS
256 fi
257}
258
259get_read_only() {
260 # Check if read-only is set
261 local read_only_state
262
263 read_only_state=`mysql_run -Q -sw -O $MYSQL -N $MYSQL_OPTIONS_REPL \
264 -e "SHOW VARIABLES like 'read_only'" | awk '{print $2}'`
265
266 if [ "$read_only_state" = "ON" ]; then
267 return 0
268 else
269 return 1
270 fi
271}
272
273# get the attribute controlling the readers VIP
274get_reader_attr() {
275 local attr_value
276 local rc
277
278 attr_value=`$CRM_ATTR -l reboot --name ${OCF_RESKEY_reader_attribute} --query -q`
279 rc=$?
280 if [ "$rc" -eq "0" ]; then
281 echo $attr_value
282 else
283 echo -1
284 fi
285
286}
287
288# Set the attribute controlling the readers VIP
289set_reader_attr() {
290 local curr_attr_value
291
292 curr_attr_value=$(get_reader_attr)
293
294 if [ "$1" -eq "0" ]; then
295 if [ "$curr_attr_value" -gt "0" ]; then
296 curr_attr_value=$((${curr_attr_value}-1))
297 $CRM_ATTR -l reboot --name ${OCF_RESKEY_reader_attribute} -v $curr_attr_value
298 else
299 $CRM_ATTR -l reboot --name ${OCF_RESKEY_reader_attribute} -v 0
300 fi
301 else
302 if [ "$curr_attr_value" -ne "$OCF_RESKEY_reader_failcount" ]; then
303 $CRM_ATTR -l reboot --name ${OCF_RESKEY_reader_attribute} -v $OCF_RESKEY_reader_failcount
304 fi
305 fi
306
307}
308
309# get the attribute controlling the writer VIP
310get_writer_attr() {
311 local attr_value
312 local rc
313
314 attr_value=`$CRM_ATTR -l reboot --name ${OCF_RESKEY_writer_attribute} --query -q`
315 rc=$?
316 if [ "$rc" -eq "0" ]; then
317 echo $attr_value
318 else
319 echo -1
320 fi
321
322}
323
324# Set the attribute controlling the writer VIP
325set_writer_attr() {
326 local curr_attr_value
327
328 curr_attr_value=$(get_writer_attr)
329
330 if [ "$1" -ne "$curr_attr_value" ]; then
331 if [ "$1" -eq "0" ]; then
332 $CRM_ATTR -l reboot --name ${OCF_RESKEY_writer_attribute} -v 0
333 else
334 $CRM_ATTR -l reboot --name ${OCF_RESKEY_writer_attribute} -v 1
335 fi
336 fi
337}
338
339#
340# mysql_run: Run a mysql command, log its output and return the proper error code.
341# Usage: mysql_run [-Q] [-info|-warn|-err] [-O] [-sw] <command>
342# -Q: don't log the output of the command if it succeeds
343# -info|-warn|-err: log the output of the command at given
344# severity if it fails (defaults to err)
345# -O: echo the output of the command
346# -sw: Suppress 5.6 client warning when password is used on the command line
347# Adapted from ocf_run.
348#
349mysql_run() {
350 local rc
351 local output outputfile
352 local verbose=1
353 local returnoutput
354 local loglevel=err
355 local suppress_56_password_warning
356 local var
357
358 for var in 1 2 3 4
359 do
360 case "$1" in
361 "-Q")
362 verbose=""
363 shift 1;;
364 "-info"|"-warn"|"-err")
365 loglevel=`echo $1 | sed -e s/-//g`
366 shift 1;;
367 "-O")
368 returnoutput=1
369 shift 1;;
370 "-sw")
371 suppress_56_password_warning=1
372 shift 1;;
373
374 *)
375 ;;
376 esac
377 done
378
379 outputfile=`mktemp ${HA_RSCTMP}/mysql_run.${OCF_RESOURCE_INSTANCE}.XXXXXX`
380 error=`"$@" 2>&1 1>$outputfile`
381 rc=$?
382 if [ "$suppress_56_password_warning" -eq 1 ]; then
383 error=`echo "$error" | egrep -v '^Warning: Using a password on the command line'`
384 fi
385 output=`cat $outputfile`
386 rm -f $outputfile
387
388 if [ $rc -eq 0 ]; then
389 if [ "$verbose" -a ! -z "$output" ]; then
390 ocf_log info "$output"
391 fi
392
393 if [ "$returnoutput" -a ! -z "$output" ]; then
394 echo "$output"
395 fi
396
397 MYSQL_LAST_ERR=$OCF_SUCCESS
398 return $OCF_SUCCESS
399 else
400 if [ ! -z "$error" ]; then
401 ocf_log $loglevel "$error"
402 regex='^ERROR ([[:digit:]]{4}).*'
403 if [[ $error =~ $regex ]]; then
404 mysql_code=${BASH_REMATCH[1]}
405 if [ -n "$mysql_code" ]; then
406 MYSQL_LAST_ERR=$mysql_code
407 return $rc
408 fi
409 fi
410 else
411 ocf_log $loglevel "command failed: $*"
412 fi
413 # No output to parse so return the standard exit code.
414 MYSQL_LAST_ERR=$rc
415 return $rc
416 fi
417}
418
419
420
421
422#######################################################################
423# API functions
424
425mysql_monitor_usage() {
426 cat <<END
427usage: $0 {start|stop|monitor|migrate_to|migrate_from|validate-all|meta-data}
428
429Expects to have a fully populated OCF RA-compliant environment set.
430END
431}
432
433mysql_monitor_start() {
434
435 # Initialise the attribute in the cib if they are not already there.
436 if [ $(get_reader_attr) -eq -1 ]; then
437 set_reader_attr 0
438 fi
439
440 if [ $(get_writer_attr) -eq -1 ]; then
441 set_writer_attr 0
442 fi
443
444 mysql_monitor
445 mysql_monitor_monitor
446 if [ $? = $OCF_SUCCESS ]; then
447 return $OCF_SUCCESS
448 fi
449 touch ${OCF_RESKEY_state}
450}
451
452mysql_monitor_stop() {
453
454 set_reader_attr 0
455 set_writer_attr 0
456
457 mysql_monitor_monitor
458 if [ $? = $OCF_SUCCESS ]; then
459 rm ${OCF_RESKEY_state}
460 fi
461 return $OCF_SUCCESS
462
463}
464
465# Monitor MySQL, not the agent itself
466mysql_monitor() {
467 if [ -e $OCF_RESKEY_pid ]; then
468 pid=`cat $OCF_RESKEY_pid`;
469 if [ -d /proc -a -d /proc/1 ]; then
470 [ "u$pid" != "u" -a -d /proc/$pid ]
471 else
472 kill -s 0 $pid >/dev/null 2>&1
473 fi
474
475 if [ $? -eq 0 ]; then
476
477 case ${OCF_RESKEY_cluster_type} in
478 'replication'|'REPLICATION')
479 if get_read_only; then
480 # a slave?
481
482 set_writer_attr 0
483
484 get_slave_info
485 rc=$?
486
487 if [ $rc -eq 0 ]; then
488 # show slave status is not empty
489 # Is there a master_log_file defined? (master_log_file is deleted
490 # by reset slave
491 if [ "$master_log_file" ]; then
492 # is read_only but no slave config...
493
494 set_reader_attr 0
495
496 else
497 # has a slave config
498
499 if [ "$slave_sql" = 'Yes' -a "$slave_io" = 'Yes' ]; then
500 # $secs_behind can be NULL so must be tested only
501 # if replication is OK
502 if [ $secs_behind -gt $OCF_RESKEY_max_slave_lag ]; then
503 set_reader_attr 0
504 else
505 set_reader_attr 1
506 fi
507 else
508 set_reader_attr 0
509 fi
510 fi
511 else
512 # "SHOW SLAVE STATUS" returns an empty set if instance is not a
513 # replication slave
514
515 set_reader_attr 0
516
517 fi
518 else
519 # host is RW
520 set_reader_attr 1
521 set_writer_attr 1
522 fi
523 ;;
524
525 'pxc'|'PXC')
526 pxcstat=`/usr/bin/clustercheck $OCF_RESKEY_user $OCF_RESKEY_password `
527 if [ $? -eq 0 ]; then
528 set_reader_attr 1
529 set_writer_attr 1
530 else
531 set_reader_attr 0
532 set_writer_attr 0
533 fi
534
535 ;;
536
537 'read-only'|'READ-ONLY')
538 if get_read_only; then
539 set_reader_attr 1
540 set_writer_attr 0
541 else
542 set_reader_attr 1
543 set_writer_attr 1
544 fi
545 ;;
546
547 esac
548 else
549 ocf_log $1 "MySQL is not running, but there is a pidfile"
550 set_reader_attr 0
551 set_writer_attr 0
552 fi
553 else
554 ocf_log $1 "MySQL is not running"
555 set_reader_attr 0
556 set_writer_attr 0
557 fi
558}
559
560mysql_monitor_monitor() {
561 # Monitor _MUST!_ differentiate correctly between running
562 # (SUCCESS), failed (ERROR) or _cleanly_ stopped (NOT RUNNING).
563 # That is THREE states, not just yes/no.
564
565 if [ -f ${OCF_RESKEY_state} ]; then
566 return $OCF_SUCCESS
567 fi
568 if false ; then
569 return $OCF_ERR_GENERIC
570 fi
571 return $OCF_NOT_RUNNING
572}
573
574mysql_monitor_validate() {
575
576 # Is the state directory writable?
577 state_dir=`dirname "$OCF_RESKEY_state"`
578 touch "$state_dir/$$"
579 if [ $? != 0 ]; then
580 return $OCF_ERR_ARGS
581 fi
582 rm "$state_dir/$$"
583
584 return $OCF_SUCCESS
585}
586
587##########################################################################
588# If DEBUG_LOG is set, make this resource agent easy to debug: set up the
589# debug log and direct all output to it. Otherwise, redirect to /dev/null.
590# The log directory must be a directory owned by root, with permissions 0700,
591# and the log must be writable and not a symlink.
592##########################################################################
593DEBUG_LOG="/tmp/mysql_monitor.ocf.ra.debug/log"
594if [ "${DEBUG_LOG}" -a -w "${DEBUG_LOG}" -a ! -L "${DEBUG_LOG}" ]; then
595 DEBUG_LOG_DIR="${DEBUG_LOG%/*}"
596 if [ -d "${DEBUG_LOG_DIR}" ]; then
597 exec 9>>"$DEBUG_LOG"
598 exec 2>&9
599 date >&9
600 echo "$*" >&9
601 env | grep OCF_ | sort >&9
602 set -x
603 else
604 exec 9>/dev/null
605 fi
606fi
607
608
609case $__OCF_ACTION in
610meta-data) meta_data
611 exit $OCF_SUCCESS
612 ;;
613start) mysql_monitor_start;;
614stop) mysql_monitor_stop;;
615monitor) mysql_monitor
616 mysql_monitor_monitor;;
617migrate_to) ocf_log info "Migrating ${OCF_RESOURCE_INSTANCE} to ${OCF_RESKEY_CRM_meta_migrate_target}."
618 mysql_monitor_stop
619 ;;
620migrate_from) ocf_log info "Migrating ${OCF_RESOURCE_INSTANCE} from ${OCF_RESKEY_CRM_meta_migrate_source}."
621 mysql_monitor_start
622 ;;
623reload) ocf_log info "Reloading ${OCF_RESOURCE_INSTANCE} ..."
624 ;;
625validate-all) mysql_monitor_validate;;
626usage|help) mysql_monitor_usage
627 exit $OCF_SUCCESS
628 ;;
629*) mysql_monitor_usage
630 exit $OCF_ERR_UNIMPLEMENTED
631 ;;
632esac
633rc=$?
634ocf_log debug "${OCF_RESOURCE_INSTANCE} $__OCF_ACTION : $rc"
635exit $rc
636
0637
=== added file 'setup.cfg'
--- setup.cfg 1970-01-01 00:00:00 +0000
+++ setup.cfg 2015-04-07 17:10:23 +0000
@@ -0,0 +1,6 @@
1[nosetests]
2verbosity=2
3with-coverage=1
4cover-erase=1
5cover-package=hooks
6
07
=== modified file 'templates/my.cnf'
--- templates/my.cnf 2015-03-04 15:30:55 +0000
+++ templates/my.cnf 2015-04-07 17:10:23 +0000
@@ -11,6 +11,7 @@
1111
12datadir=/var/lib/mysql12datadir=/var/lib/mysql
13user=mysql13user=mysql
14pid_file = /var/run/mysqld/mysqld.pid
1415
15# Path to Galera library16# Path to Galera library
16wsrep_provider=/usr/lib/libgalera_smm.so17wsrep_provider=/usr/lib/libgalera_smm.so
1718
=== added directory 'tests'
=== added file 'tests/00-setup.sh'
--- tests/00-setup.sh 1970-01-01 00:00:00 +0000
+++ tests/00-setup.sh 2015-04-07 17:10:23 +0000
@@ -0,0 +1,29 @@
1#!/bin/bash -x
2# The script installs amulet and other tools needed for the amulet tests.
3
4# Get the status of the amulet package, this returns 0 of package is installed.
5dpkg -s amulet
6if [ $? -ne 0 ]; then
7 # Install the Amulet testing harness.
8 sudo add-apt-repository -y ppa:juju/stable
9 sudo apt-get update
10 sudo apt-get install -y -q amulet juju-core charm-tools
11fi
12
13
14PACKAGES="python3 python3-yaml"
15for pkg in $PACKAGES; do
16 dpkg -s python3
17 if [ $? -ne 0 ]; then
18 sudo apt-get install -y -q $pkg
19 fi
20done
21
22
23if [ ! -f "$(dirname $0)/../local.yaml" ]; then
24 echo "To run these amulet tests a vip is needed, create a file called \
25local.yaml in the charm dir, this file must contain a 'vip', if you're \
26using the local provider with lxc you could use a free IP from the range \
2710.0.3.0/24"
28 exit 1
29fi
030
=== added file 'tests/10-deploy_test.py'
--- tests/10-deploy_test.py 1970-01-01 00:00:00 +0000
+++ tests/10-deploy_test.py 2015-04-07 17:10:23 +0000
@@ -0,0 +1,29 @@
1#!/usr/bin/python3
2# test percona-cluster (3 nodes)
3
4import basic_deployment
5import time
6
7
8class ThreeNode(basic_deployment.BasicDeployment):
9 def __init__(self):
10 super(ThreeNode, self).__init__(units=3)
11
12 def run(self):
13 super(ThreeNode, self).run()
14 # we are going to kill the master
15 old_master = self.master_unit
16 self.master_unit.run('sudo poweroff')
17
18 time.sleep(10) # give some time to pacemaker to react
19 new_master = self.find_master()
20 assert new_master is not None, "master unit not found"
21 assert (new_master.info['public-address'] !=
22 old_master.info['public-address'])
23
24 assert self.is_port_open(address=self.vip), 'cannot connect to vip'
25
26
27if __name__ == "__main__":
28 t = ThreeNode()
29 t.run()
030
=== added file 'tests/20-broken-mysqld.py'
--- tests/20-broken-mysqld.py 1970-01-01 00:00:00 +0000
+++ tests/20-broken-mysqld.py 2015-04-07 17:10:23 +0000
@@ -0,0 +1,38 @@
1#!/usr/bin/python3
2# test percona-cluster (3 nodes)
3
4import basic_deployment
5import time
6
7
8class ThreeNode(basic_deployment.BasicDeployment):
9 def __init__(self):
10 super(ThreeNode, self).__init__(units=3)
11
12 def run(self):
13 super(ThreeNode, self).run()
14 # we are going to kill the master
15 old_master = self.master_unit
16 print('stopping mysql in %s' % str(self.master_unit.info))
17 self.master_unit.run('sudo service mysql stop')
18
19 print('looking for the new master')
20 i = 0
21 changed = False
22 while i < 10 and not changed:
23 i += 1
24 time.sleep(5) # give some time to pacemaker to react
25 new_master = self.find_master()
26
27 if (new_master and new_master.info['unit_name'] !=
28 old_master.info['unit_name']):
29 changed = True
30
31 assert changed, "The master didn't change"
32
33 assert self.is_port_open(address=self.vip), 'cannot connect to vip'
34
35
36if __name__ == "__main__":
37 t = ThreeNode()
38 t.run()
039
=== added file 'tests/basic_deployment.py'
--- tests/basic_deployment.py 1970-01-01 00:00:00 +0000
+++ tests/basic_deployment.py 2015-04-07 17:10:23 +0000
@@ -0,0 +1,126 @@
1import amulet
2import os
3import telnetlib
4import unittest
5import yaml
6
7
8class BasicDeployment(unittest.TestCase):
9 def __init__(self, vip=None, units=1):
10 self.units = units
11 self.master_unit = None
12 self.vip = None
13 if vip:
14 self.vip = vip
15 elif 'VIP' in os.environ:
16 self.vip = os.environ.get('VIP')
17 elif os.path.isfile('local.yaml'):
18 with open('local.yaml', 'rb') as f:
19 self.cfg = yaml.safe_load(f.read())
20
21 self.vip = self.cfg.get('vip')
22 else:
23 amulet.raise_status(amulet.SKIP,
24 ("please set the vip in local.yaml "
25 "to run this test suite"))
26
27 def run(self):
28 # The number of seconds to wait for the environment to setup.
29 seconds = 1200
30
31 self.d = amulet.Deployment(series="trusty")
32 self.d.add('percona-cluster', units=self.units)
33 self.d.add('hacluster')
34 self.d.relate('percona-cluster:ha', 'hacluster:ha')
35
36 cfg_percona = {'sst-password': 'ubuntu',
37 'root-password': 't00r',
38 'dataset-size': '128M',
39 'vip': self.vip}
40
41 cfg_ha = {'debug': True,
42 'corosync_mcastaddr': '226.94.1.4',
43 'corosync_key': ('xZP7GDWV0e8Qs0GxWThXirNNYlScgi3sRTdZk/IXKD'
44 'qkNFcwdCWfRQnqrHU/6mb6sz6OIoZzX2MtfMQIDcXu'
45 'PqQyvKuv7YbRyGHmQwAWDUA4ed759VWAO39kHkfWp9'
46 'y5RRk/wcHakTcWYMwm70upDGJEP00YT3xem3NQy27A'
47 'C1w=')}
48
49 self.d.configure('percona-cluster', cfg_percona)
50 self.d.configure('hacluster', cfg_ha)
51
52 try:
53 self.d.setup(timeout=seconds)
54 self.d.sentry.wait(seconds)
55 except amulet.helpers.TimeoutError:
56 message = 'The environment did not setup in %d seconds.' % seconds
57 amulet.raise_status(amulet.SKIP, msg=message)
58 except:
59 raise
60
61 self.master_unit = self.find_master()
62 assert self.master_unit is not None, 'percona-cluster vip not found'
63
64 output, code = self.master_unit.run('sudo crm_verify --live-check')
65 assert code == 0, "'crm_verify --live-check' failed"
66
67 resources = ['res_mysql_vip']
68 resources += ['res_mysql_monitor:%d' % i for i in range(self.units)]
69
70 assert sorted(self.get_pcmkr_resources()) == sorted(resources)
71
72 for i in range(self.units):
73 uid = 'percona-cluster/%d' % i
74 unit = self.d.sentry.unit[uid]
75 assert self.is_mysqld_running(unit), 'mysql not running: %s' % uid
76
77 def find_master(self):
78 for unit_id, unit in self.d.sentry.unit.items():
79 if not unit_id.startswith('percona-cluster/'):
80 continue
81
82 # is the vip running here?
83 output, code = unit.run('sudo ip a | grep %s' % self.vip)
84 print(unit_id)
85 print(output)
86 if code == 0:
87 print('vip(%s) running in %s' % (self.vip, unit_id))
88 return unit
89
90 def get_pcmkr_resources(self, unit=None):
91 if unit:
92 u = unit
93 else:
94 u = self.master_unit
95
96 output, code = u.run('sudo crm_resource -l')
97
98 assert code == 0, 'could not get "crm resource list"'
99
100 return output.split('\n')
101
102 def is_mysqld_running(self, unit=None):
103 if unit:
104 u = unit
105 else:
106 u = self.master_unit
107
108 output, code = u.run('pidof mysqld')
109
110 if code != 0:
111 return False
112
113 return self.is_port_open(u, '3306')
114
115 def is_port_open(self, unit=None, port='3306', address=None):
116 if unit:
117 addr = unit.info['public-address']
118 elif address:
119 addr = address
120 else:
121 raise Exception('Please provide a unit or address')
122 try:
123 telnetlib.Telnet(addr, port)
124 return True
125 except TimeoutError: # noqa this exception only available in py3
126 return False
0127
=== added file 'unit_tests/test_percona_hooks.py'
--- unit_tests/test_percona_hooks.py 1970-01-01 00:00:00 +0000
+++ unit_tests/test_percona_hooks.py 2015-04-07 17:10:23 +0000
@@ -0,0 +1,65 @@
1import mock
2import sys
3from test_utils import CharmTestCase
4
5sys.modules['MySQLdb'] = mock.Mock()
6import percona_hooks as hooks
7
8TO_PATCH = ['log', 'config',
9 'get_db_helper',
10 'relation_ids',
11 'relation_set']
12
13
14class TestHaRelation(CharmTestCase):
15 def setUp(self):
16 CharmTestCase.setUp(self, hooks, TO_PATCH)
17
18 @mock.patch('sys.exit')
19 def test_relation_not_configured(self, exit_):
20 self.config.return_value = None
21
22 class MyError(Exception):
23 pass
24
25 def f(x):
26 raise MyError(x)
27 exit_.side_effect = f
28 self.assertRaises(MyError, hooks.ha_relation_joined)
29
30 def test_resources(self):
31 self.relation_ids.return_value = ['ha:1']
32 password = 'ubuntu'
33 helper = mock.Mock()
34 attrs = {'get_mysql_password.return_value': password}
35 helper.configure_mock(**attrs)
36 self.get_db_helper.return_value = helper
37 self.test_config.set('vip', '10.0.3.3')
38 self.test_config.set('sst-password', password)
39 def f(k):
40 return self.test_config.get(k)
41
42 self.config.side_effect = f
43 hooks.ha_relation_joined()
44
45 resources = {'res_mysql_vip': 'ocf:heartbeat:IPaddr2',
46 'res_mysql_monitor': 'ocf:percona:mysql_monitor'}
47 resource_params = {'res_mysql_vip': ('params ip="10.0.3.3" '
48 'cidr_netmask="24" '
49 'nic="eth0"'),
50 'res_mysql_monitor':
51 hooks.RES_MONITOR_PARAMS % {'sstpass': 'ubuntu'}}
52 groups = {'grp_percona_cluster': 'res_mysql_vip'}
53
54 clones = {'cl_mysql_monitor': 'res_mysql_monitor meta interleave=true'}
55
56 colocations = {'vip_mysqld': 'inf: grp_percona_cluster cl_mysql_monitor'}
57
58 locations = {'loc_percona_cluster':
59 'grp_percona_cluster rule inf: writable eq 1'}
60
61 self.relation_set.assert_called_with(
62 relation_id='ha:1', corosync_bindiface=f('ha-bindiface'),
63 corosync_mcastport=f('ha-mcastport'), resources=resources,
64 resource_params=resource_params, groups=groups,
65 clones=clones, colocations=colocations, locations=locations)
066
=== added file 'unit_tests/test_utils.py'
--- unit_tests/test_utils.py 1970-01-01 00:00:00 +0000
+++ unit_tests/test_utils.py 2015-04-07 17:10:23 +0000
@@ -0,0 +1,121 @@
1import logging
2import unittest
3import os
4import yaml
5
6from contextlib import contextmanager
7from mock import patch, MagicMock
8
9
10def load_config():
11 '''
12 Walk backwords from __file__ looking for config.yaml, load and return the
13 'options' section'
14 '''
15 config = None
16 f = __file__
17 while config is None:
18 d = os.path.dirname(f)
19 if os.path.isfile(os.path.join(d, 'config.yaml')):
20 config = os.path.join(d, 'config.yaml')
21 break
22 f = d
23
24 if not config:
25 logging.error('Could not find config.yaml in any parent directory '
26 'of %s. ' % file)
27 raise Exception
28
29 return yaml.safe_load(open(config).read())['options']
30
31
32def get_default_config():
33 '''
34 Load default charm config from config.yaml return as a dict.
35 If no default is set in config.yaml, its value is None.
36 '''
37 default_config = {}
38 config = load_config()
39 for k, v in config.iteritems():
40 if 'default' in v:
41 default_config[k] = v['default']
42 else:
43 default_config[k] = None
44 return default_config
45
46
47class CharmTestCase(unittest.TestCase):
48
49 def setUp(self, obj, patches):
50 super(CharmTestCase, self).setUp()
51 self.patches = patches
52 self.obj = obj
53 self.test_config = TestConfig()
54 self.test_relation = TestRelation()
55 self.patch_all()
56
57 def patch(self, method):
58 _m = patch.object(self.obj, method)
59 mock = _m.start()
60 self.addCleanup(_m.stop)
61 return mock
62
63 def patch_all(self):
64 for method in self.patches:
65 setattr(self, method, self.patch(method))
66
67
68class TestConfig(object):
69
70 def __init__(self):
71 self.config = get_default_config()
72
73 def get(self, attr=None):
74 if not attr:
75 return self.get_all()
76 try:
77 return self.config[attr]
78 except KeyError:
79 return None
80
81 def get_all(self):
82 return self.config
83
84 def set(self, attr, value):
85 if attr not in self.config:
86 raise KeyError
87 self.config[attr] = value
88
89
90class TestRelation(object):
91
92 def __init__(self, relation_data={}):
93 self.relation_data = relation_data
94
95 def set(self, relation_data):
96 self.relation_data = relation_data
97
98 def get(self, attr=None, unit=None, rid=None):
99 if attr is None:
100 return self.relation_data
101 elif attr in self.relation_data:
102 return self.relation_data[attr]
103 return None
104
105
106@contextmanager
107def patch_open():
108 '''Patch open() to allow mocking both open() itself and the file that is
109 yielded.
110
111 Yields the mock for "open" and "file", respectively.'''
112 mock_open = MagicMock(spec=open)
113 mock_file = MagicMock(spec=file)
114
115 @contextmanager
116 def stub_open(*args, **kwargs):
117 mock_open(*args, **kwargs)
118 yield mock_file
119
120 with patch('__builtin__.open', stub_open):
121 yield mock_open, mock_file

Subscribers

People subscribed via source and target branches