Merge lp:~freyes/charms/trusty/percona-cluster/lp1426508 into lp:~openstack-charmers-archive/charms/trusty/percona-cluster/next
- Trusty Tahr (14.04)
- lp1426508
- Merge into next
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 |
Related bugs: |
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.
Commit message
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:/
[1] http://
[2] https:/
[3] http://
James Page (james-page) wrote : Posted in a previous version of this proposal | # |
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,
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:/
- 68. By Felipe Reyes
-
mysql_monitor: Apply patch available in upstream PR #52
- 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
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 |
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.