Merge ~freyes/charm-mongodb:status-fix into ~mongodb-charmers/charm-mongodb:master
- Git
- lp:~freyes/charm-mongodb
- status-fix
- Merge into master
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) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stuart Bishop (community) | Approve | ||
MongoDB Charm Maintainers | Pending | ||
Review via email: mp+345962@code.launchpad.net |
Commit message
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:/
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://
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.
Stuart Bishop (stub) wrote : | # |
Landed and released as cs:~mongodb-
Felipe Reyes (freyes) wrote : | # |
On Mon, May 28, 2018 at 07:32:00AM -0000, Stuart Bishop wrote:
> Landed and released as cs:~mongodb-
thank you so much :-)
Preview Diff
1 | diff --git a/hooks/hooks.py b/hooks/hooks.py |
2 | index 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: |
324 | diff --git a/hooks/update-status b/hooks/update-status |
325 | new file mode 120000 |
326 | index 0000000..9416ca6 |
327 | --- /dev/null |
328 | +++ b/hooks/update-status |
329 | @@ -0,0 +1 @@ |
330 | +hooks.py |
331 | \ No newline at end of file |
332 | diff --git a/tests/base_deploy.py b/tests/base_deploy.py |
333 | index 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)} |
345 | diff --git a/tests/deploy_replicaset.py b/tests/deploy_replicaset.py |
346 | index 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() |
406 | diff --git a/unit_tests/test_hooks.py b/unit_tests/test_hooks.py |
407 | index 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() |
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.