Merge ~freyes/charm-mongodb:status-fix into ~mongodb-charmers/charm-mongodb:master

Proposed by Felipe Reyes
Status: Merged
Merged at revision: 8faa36725b36c34ca625ea88391420083b3a2934
Proposed branch: ~freyes/charm-mongodb:status-fix
Merge into: ~mongodb-charmers/charm-mongodb:master
Diff against target: 494 lines (+175/-36)
5 files modified
hooks/hooks.py (+121/-25)
hooks/update-status (+1/-0)
tests/base_deploy.py (+0/-2)
tests/deploy_replicaset.py (+32/-0)
unit_tests/test_hooks.py (+21/-9)
Reviewer Review Type Date Requested Status
Stuart Bishop (community) Approve
MongoDB Charm Maintainers Pending
Review via email: mp+345962@code.launchpad.net

Description of the change

Hello,

I'm adding the missing pieces on top of Mario's branch[0] as he delegated on me this functionality, because he's not able to keep working on it.

The most relevant changes I made are:

f52c8db Add functional test to check workload status
2ef8834 Log replSetGetStatus only when status not found for self
eca7ebd update-status: check replica set status when relation is made
669bf19 Set workload status before returning in update_status()

Thanks,

[0] https://code.launchpad.net/~mariosplivalo/mongodb-charm/+git/mongodb-charm/+merge/341289

To post a comment you must log in.
Revision history for this message
Stuart Bishop (stub) wrote :

This looks good. Some comments inline, mostly trivial things to fix before landing. Adding a timeout when adding a secondary probably requires more thought, and likely best deferred until later.

Pre-approved from me, or I can rereview if you feel it is necessary.

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

Hi Stuart,

I addressed your comments, except some of the changes to the messages printed to the users, mostly because those were chosen by Mario and some of them align better with what we see in other charms (e.g. "Unit is ready...")

The functional tests log can be reviewed at http://paste.ubuntu.com/p/bn6hbtDw4g/

Best,

PS: I don't have merge privileges, so if you are OK with this MP would be great if you could merge it and publish the charm into the charmstore.

Revision history for this message
Stuart Bishop (stub) wrote :

Landed and released as cs:~mongodb-charmers/mongodb-6 / cs:mongodb-48

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

On Mon, May 28, 2018 at 07:32:00AM -0000, Stuart Bishop wrote:
> Landed and released as cs:~mongodb-charmers/mongodb-6 / cs:mongodb-48

thank you so much :-)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/hooks/hooks.py b/hooks/hooks.py
2index d3774a7..9de334d 100755
3--- a/hooks/hooks.py
4+++ b/hooks/hooks.py
5@@ -6,7 +6,9 @@ Created on Aug 1, 2012
6 '''
7
8 import commands
9+import json
10 import os
11+import pprint
12 import re
13 import signal
14 import socket
15@@ -30,14 +32,15 @@ from string import Template
16 from textwrap import dedent
17 from yaml.constructor import ConstructorError
18
19+from charmhelpers.core.decorators import retry_on_exception
20+from charmhelpers.payload.execd import execd_preinstall
21+
22 from charmhelpers.fetch import (
23 add_source,
24 apt_update,
25 apt_install
26 )
27
28-import json
29-
30 from charmhelpers.core.host import (
31 service,
32 lsb_release,
33@@ -47,6 +50,7 @@ from charmhelpers.core.hookenv import (
34 close_port,
35 config,
36 is_relation_made,
37+ log as juju_log,
38 open_port,
39 unit_get,
40 relation_get,
41@@ -58,15 +62,12 @@ from charmhelpers.core.hookenv import (
42 Hooks,
43 DEBUG,
44 WARNING,
45- is_leader
46+ is_leader,
47+ status_set,
48+ application_version_set,
49 )
50
51-from charmhelpers.core.hookenv import log as juju_log
52-
53-from charmhelpers.payload.execd import execd_preinstall
54-
55 from charmhelpers.contrib.hahelpers.cluster import (
56- oldest_peer,
57 peer_units
58 )
59
60@@ -586,8 +587,8 @@ def remove_replset_from_upstart():
61 """
62 try:
63 mongodb_init_config = open(default_mongodb_init_config).read()
64-
65- if re.search(' --replSet', mongodb_init_config,
66+
67+ if re.search(' --replSet', mongodb_init_config,
68 re.MULTILINE) is not None:
69 mongodb_init_config = re.sub(' --replSet .\w+', '',
70 mongodb_init_config)
71@@ -921,6 +922,7 @@ def arm64_trusty_quirk():
72 def install_hook():
73 juju_log('Begin install hook.')
74 execd_preinstall()
75+ status_set('maintenance', 'Installing packages')
76 juju_log("Installing mongodb")
77 add_source(config('source'), config('key'))
78
79@@ -935,9 +937,9 @@ def install_hook():
80 @hooks.hook('config-changed')
81 def config_changed():
82 juju_log("Entering config_changed")
83- print "Entering config_changed"
84+ status_set('maintenance', 'Configuring unit')
85 config_data = config()
86- print "config_data: ", config_data
87+ juju_log("config_data: {}".format(config_data), level=DEBUG)
88 mongodb_config = open(default_mongodb_config).read()
89
90 # Trigger volume initialization logic for permanent storage
91@@ -976,11 +978,12 @@ def config_changed():
92 current_web_admin_ui_port = int(current_mongodb_port) + 1000
93 new_web_admin_ui_port = int(config_data['port']) + 1000
94
95- print "current_mongodb_port: ", current_mongodb_port
96+ juju_log("Configured mongodb port: {}".format(current_mongodb_port),
97+ level=DEBUG)
98 public_address = unit_get('public-address')
99- print "public_address: ", public_address
100+ juju_log("unit's public_address: {}".format(public_address), level=DEBUG)
101 private_address = unit_get('private-address')
102- print "private_address: ", private_address
103+ juju_log("unit's private_address: {}".format(private_address), level=DEBUG)
104
105 # Update mongodb configuration file
106 mongodb_config = mongodb_conf(config_data)
107@@ -1009,6 +1012,7 @@ def config_changed():
108 write_logrotate_config(config_data)
109
110 # restart mongodb
111+ status_set('maintenance', 'Restarting mongod')
112 restart_mongod()
113
114 # attach to replSet ( if needed )
115@@ -1069,8 +1073,10 @@ def config_changed():
116 open_port(config_data['mongos_port'])
117
118 update_nrpe_config()
119+ application_version_set(get_mongod_version())
120+ update_status()
121
122- print "About to leave config_changed"
123+ juju_log("About to leave config_changed", level=DEBUG)
124 return(True)
125
126
127@@ -1148,6 +1154,7 @@ def replica_set_relation_joined():
128 if enable_replset(my_replset):
129 juju_log('Restarting mongodb after config change (enable replset)',
130 level=DEBUG)
131+ status_set('maintenance', 'Restarting mongod to enable replicaset')
132 restart_mongod()
133
134 relation_set(relation_id(), {
135@@ -1157,6 +1164,8 @@ def replica_set_relation_joined():
136 'install-order': my_install_order,
137 'type': 'replset',
138 })
139+
140+ update_status()
141 juju_log("replica_set_relation_joined-finish")
142
143
144@@ -1165,7 +1174,8 @@ def am_i_primary():
145 for i in xrange(10):
146 try:
147 r = run_admin_command(c, 'replSetGetStatus')
148- juju_log('am_i_primary: replSetGetStatus returned: %s' % str(r),
149+ pretty_r = pprint.pformat(r)
150+ juju_log('am_i_primary: replSetGetStatus returned: %s' % pretty_r,
151 level=DEBUG)
152 return r['myState'] == MONGO_PRIMARY
153 except OperationFailure as e:
154@@ -1180,7 +1190,7 @@ def am_i_primary():
155 # replication not initialized yet (Mongo3.4+)
156 return False
157 elif 'not running with --replSet' in str(e):
158- # replicaset not configured
159+ # replicaset not configured
160 return False
161 else:
162 raise
163@@ -1191,6 +1201,56 @@ def am_i_primary():
164 raise TimeoutException('Unable to determine if local unit is primary')
165
166
167+def get_replicaset_status():
168+ """Connect to mongod and get the status of replicaset
169+ This function is used mainly within update_status() to display
170+ replicaset status in 'juju status' output
171+
172+ :returns string: can be any of replicaset states
173+ (https://docs.mongodb.com/manual/reference/replica-states/)
174+ or can be the string of an exception while getting the status
175+ """
176+
177+ c = MongoClient('localhost')
178+ try:
179+ r = run_admin_command(c, 'replSetGetStatus')
180+ for member in r['members']:
181+ if 'self' in member:
182+ return member['stateStr']
183+
184+ # if 'self' was not found in the output, then log a warning and print
185+ # the output given by replSetGetStatus
186+ r_pretty = pprint.pformat(r)
187+ juju_log('get_replicaset_status() failed to get replicaset state:' +
188+ r_pretty, level=WARNING)
189+ return 'Unknown replica set state'
190+
191+ except OperationFailure as e:
192+ juju_log('get_replicaset_status() exception: %s' % str(e), DEBUG)
193+ if 'not running with --replSet' in str(e):
194+ return 'not in replicaset'
195+ else:
196+ return str(e)
197+
198+def get_mongod_version():
199+ """ Connects to mongod and get the db.version() output
200+ Mainly used for application_set_version in config-changed hook
201+ """
202+
203+ c = MongoClient('localhost')
204+ return c.server_info()['version']
205+
206+
207+# Retry until the replica set is in active state, retry 45 times before failing,
208+# wait 1, 2, 3, 4, ... seconds between each retry, this will add 33 minutes of
209+# accumulated sleep()
210+@retry_on_exception(num_retries=45, base_delay=1)
211+def wait_until_replset_is_active():
212+ status = update_status()
213+ if status != 'active':
214+ raise Exception('ReplicaSet not active: {}'.format(status))
215+
216+
217 @hooks.hook('replica-set-relation-changed')
218 def replica_set_relation_changed():
219 private_address = unit_get('private-address')
220@@ -1208,6 +1268,7 @@ def replica_set_relation_changed():
221 # Initialize the replicaset - we do this only on the leader!
222 if is_leader():
223 juju_log('Initializing replicaset')
224+ status_set('maintenance', 'Initializing replicaset')
225 init_replset()
226
227 unit = "%s:%s" % (private_address, config('port'))
228@@ -1218,9 +1279,11 @@ def replica_set_relation_changed():
229 juju_log('Adding new secondary... %s' % unit_remote, level=DEBUG)
230 join_replset(unit, unit_remote)
231
232+ wait_until_replset_is_active()
233 juju_log('replica_set_relation_changed-finish')
234
235
236+
237 @hooks.hook('replica-set-relation-departed')
238 def replica_set_relation_departed():
239 juju_log('replica_set_relation_departed-start')
240@@ -1263,12 +1326,12 @@ def replica_set_relation_broken():
241
242 c = MongoClient('localhost')
243 r = c.admin.command('isMaster')
244-
245+
246 try:
247 master_node = r['primary']
248 except KeyError:
249 pass
250-
251+
252 if 'master_node' in locals(): # unit is part of replicaset, remove it!
253 unit = "%s:%s" % (unit_get('private-address'), config('port'))
254 juju_log('Removing myself via %s' % (master_node), 'DEBUG')
255@@ -1350,11 +1413,12 @@ def mongos_relation_changed():
256 port = relation_get('port')
257 rel_type = relation_get('type')
258 if hostname is None or port is None or rel_type is None:
259- print("mongos_relation_changed: relation data not ready.")
260+ juju_log("mongos_relation_changed: relation data not ready.",
261+ level=DEBUG)
262 return
263 if rel_type == 'configsvr':
264 config_servers = load_config_servers(default_mongos_list)
265- print "Adding config server: %s:%s" % (hostname, port)
266+ juju_log("Adding config server: %s:%s" % (hostname, port), level=DEBUG)
267 if hostname is not None and \
268 port is not None and \
269 hostname != '' and \
270@@ -1379,11 +1443,11 @@ def mongos_relation_changed():
271 mongo_client(mongos_host, shard_command2)
272
273 else:
274- print("mongos_relation_change: undefined rel_type: %s" %
275- rel_type)
276+ juju_log("mongos_relation_change: undefined rel_type: %s" % rel_type,
277+ level=DEBUG)
278 return
279
280- print("mongos_relation_changed returns: %s" % retVal)
281+ juju_log("mongos_relation_changed returns: %s" % retVal, level=DEBUG)
282
283
284 @hooks.hook('mongos-relation-broken')
285@@ -1439,6 +1503,38 @@ def uprade_charm():
286 remove_rest_from_upstart()
287
288
289+@hooks.hook('update-status')
290+def update_status():
291+ """
292+ Returns: workload_state (so that some hooks know they need to re-run
293+ update_status if needed)
294+ """
295+
296+ workload = 'active'
297+ status = 'Unit is ready'
298+
299+ if is_relation_made('replica-set'):
300+ # only check for replica-set state if the relation was made which means
301+ # more than 1 units were deployed and peer related.
302+ mongo_status = get_replicaset_status()
303+ if mongo_status in ('PRIMARY', 'SECONDARY'):
304+ workload = 'active'
305+ status = 'Unit is ready as ' + mongo_status
306+ elif mongo_status in ('not in replicaset',):
307+ workload = 'active'
308+ status = 'Unit is ready, ' + mongo_status
309+ else:
310+ workload = 'maintenance'
311+ status = mongo_status
312+ juju_log('mongo_status is unknown: {}'.format(status), level=DEBUG)
313+
314+ juju_log('Setting workload: {} - {}'.format(workload, status), level=DEBUG)
315+ status_set(workload, status)
316+
317+ return workload
318+
319+
320+
321 def run(command, exit_on_error=True):
322 '''Run a command and return the output.'''
323 try:
324diff --git a/hooks/update-status b/hooks/update-status
325new file mode 120000
326index 0000000..9416ca6
327--- /dev/null
328+++ b/hooks/update-status
329@@ -0,0 +1 @@
330+hooks.py
331\ No newline at end of file
332diff --git a/tests/base_deploy.py b/tests/base_deploy.py
333index 8930966..b136cca 100644
334--- a/tests/base_deploy.py
335+++ b/tests/base_deploy.py
336@@ -24,8 +24,6 @@ class BasicMongo(object):
337 message = 'The environment did not setup in %d seconds.',
338 self.deploy_timeout
339 amulet.raise_status(amulet.SKIP, msg=message)
340- except:
341- raise
342
343 self.sentry_dict = {svc: self.d.sentry[svc]
344 for svc in list(self.d.sentry.unit)}
345diff --git a/tests/deploy_replicaset.py b/tests/deploy_replicaset.py
346index 9506407..6fa0290 100644
347--- a/tests/deploy_replicaset.py
348+++ b/tests/deploy_replicaset.py
349@@ -1,6 +1,8 @@
350 #!/usr/bin/env python3
351
352 import amulet
353+import logging
354+import re
355 import sys
356 import time
357 import traceback
358@@ -12,6 +14,7 @@ from base_deploy import BasicMongo
359
360 # max amount of time to wait before testing for replicaset status
361 wait_for_replicaset = 600
362+logger = logging.getLogger(__name__)
363
364
365 class Replicaset(BasicMongo):
366@@ -109,6 +112,34 @@ class Replicaset(BasicMongo):
367 def validate_running_services(self):
368 super(Replicaset, self).validate_running_services()
369
370+ def validate_workload_status(self):
371+ primaries = 0
372+ secondaries = 0
373+ regex = re.compile('^Unit is ready as (PRIMARY|SECONDARY)$')
374+ self.d.sentry.wait_for_messages({'mongodb': regex})
375+
376+ # count how many primaries and secondaries were reported in the
377+ # workload status
378+ for unit_name, unit in self.d.sentry.get_status()['mongodb'].items():
379+ workload_msg = unit['workload-status']['message']
380+ matched = re.match(regex, workload_msg)
381+
382+ if not matched:
383+ msg = "'{}' does not match '{}'".format(workload_msg, regex)
384+ amulet.raise_status(amulet.FAIL, msg=msg)
385+ elif matched.group(1) == 'PRIMARY':
386+ primaries += 1
387+ elif matched.group(1) == 'SECONDARY':
388+ secondaries += 1
389+ else:
390+ amulet.raise_status(amulet.FAIL,
391+ msg='Unknown state: %s' % matched.group(1))
392+
393+ logger.debug('Secondary units found: %d' % secondaries)
394+ if primaries > 1:
395+ msg = "Found %d primaries, expected 1" % primaries
396+ amulet.raise_status(amulet.FAIL, msg=msg)
397+
398 def run(self):
399 self.deploy()
400 self.validate_status_interface()
401@@ -116,3 +147,4 @@ class Replicaset(BasicMongo):
402 self.validate_replicaset_setup()
403 self.validate_replicaset_relation_joined()
404 self.validate_world_connectivity()
405+ self.validate_workload_status()
406diff --git a/unit_tests/test_hooks.py b/unit_tests/test_hooks.py
407index 603b149..354d104 100644
408--- a/unit_tests/test_hooks.py
409+++ b/unit_tests/test_hooks.py
410@@ -36,19 +36,24 @@ class MongoHooksTest(CharmTestCase):
411 self.config.side_effect = self.test_config.get
412 self.relation_get.side_effect = self.test_relation.get
413
414+ @patch.object(hooks, 'is_relation_made')
415+ @patch.object(hooks, 'get_replicaset_status')
416 @patch.object(hooks, 'restart_mongod')
417 @patch.object(hooks, 'enable_replset')
418 # Note: patching the os.environ dictionary in-line here so there's no
419 # additional parameter sent into the function
420 @patch.dict('os.environ', JUJU_UNIT_NAME='fake-unit/0')
421 def test_replica_set_relation_joined(self, mock_enable_replset,
422- mock_restart):
423+ mock_restart, mock_get_replset_status,
424+ mock_is_rel_made):
425 self.unit_get.return_value = 'private.address'
426 self.test_config.set('port', '1234')
427 self.test_config.set('replicaset', 'fake-replicaset')
428 self.relation_id.return_value = 'fake-relation-id'
429
430 mock_enable_replset.return_value = False
431+ mock_get_replset_status.return_value = 'PRIMARY'
432+ mock_is_rel_made.return_value = True
433
434 hooks.replica_set_relation_joined()
435
436@@ -276,17 +281,24 @@ class MongoHooksTest(CharmTestCase):
437 mock_subprocess.assert_called_once_with(expected_cmd, shell=True)
438 self.assertFalse(rv)
439
440+ @patch.object(hooks, 'is_relation_made')
441+ @patch.object(hooks, 'run_admin_command')
442+ @patch.object(hooks, 'is_leader')
443+ @patch.object(hooks, 'get_replicaset_status')
444 @patch.object(hooks, 'am_i_primary')
445 @patch.object(hooks, 'init_replset')
446 @patch.object(hooks, 'relation_get')
447 @patch.object(hooks, 'peer_units')
448- @patch.object(hooks, 'oldest_peer')
449 @patch.object(hooks, 'join_replset')
450 @patch.object(hooks, 'unit_get')
451 def test_replica_set_relation_changed(self, mock_unit_get,
452- mock_join_replset, mock_oldest_peer,
453- mock_peer_units, mock_relation_get,
454- mock_init_replset, mock_is_primary):
455+ mock_join_replset, mock_peer_units,
456+ mock_relation_get, mock_init_replset,
457+ mock_is_primary,
458+ mock_get_replset_status,
459+ mock_is_leader,
460+ mock_run_admin_cmd,
461+ mock_is_rel_made):
462 # set the unit_get('private-address')
463 mock_unit_get.return_value = 'juju-local-unit-0.local'
464 mock_relation_get.return_value = None
465@@ -298,6 +310,10 @@ class MongoHooksTest(CharmTestCase):
466 # Test remote hostname is valid, but master is somehow not defined
467 mock_join_replset.reset_mock()
468 mock_relation_get.return_value = 'juju-local-unit-0'
469+ mock_is_leader.return_value = False
470+ mock_run_admin_cmd.return_value = {'myState': hooks.MONGO_PRIMARY}
471+ mock_get_replset_status.return_value = 'PRIMARY'
472+ mock_is_rel_made.return_value = True
473
474 hooks.replica_set_relation_changed()
475
476@@ -306,9 +322,7 @@ class MongoHooksTest(CharmTestCase):
477 # Test when not oldest peer, don't init replica set
478 mock_join_replset.reset_mock()
479 mock_init_replset.reset_mock()
480- mock_oldest_peer.reset_mock()
481 mock_peer_units.return_value = ['mongodb/1', 'mongodb/2']
482- mock_oldest_peer.return_value = False
483
484 hooks.replica_set_relation_changed()
485
486@@ -317,8 +331,6 @@ class MongoHooksTest(CharmTestCase):
487 # Test when its also the PRIMARY
488 mock_relation_get.reset_mock()
489 mock_relation_get.side_effect = ['juju-remote-unit-0', '12345']
490- mock_oldest_peer.reset_mock()
491- mock_oldest_peer.return_value = False
492 mock_is_primary.reset_mock()
493 mock_is_primary.return_value = True
494 mock_join_replset.reset_mock()

Subscribers

People subscribed via source and target branches

to status/vote changes: