1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
|
#!/usr/bin/python
# Copyright: (c) 2018, Rhys Campbell <rhys.james.campbell@googlemail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mongodb_status
short_description: Validates the status of the replicaset.
description:
- Validates the status of the replicaset.
- The module expects all replicaset nodes to be PRIMARY, SECONDARY or ARBITER.
- Will wait until a timeout for the replicaset state to converge if required.
- Can also be used to lookup the current PRIMARY member (see examples).
author: Rhys Campbell (@rhysmeister)
version_added: "1.0.0"
extends_documentation_fragment:
- community.mongodb.login_options
- community.mongodb.ssl_options
options:
replica_set:
description:
- Replicaset name.
type: str
default: rs0
poll:
description:
- The maximum number of times to query for the replicaset status before the set converges or we fail.
type: int
default: 1
interval:
description:
- The number of seconds to wait between polling executions.
type: int
default: 30
validate:
description:
- The type of validate to perform on the replicaset.
- default, Suitable for most purposes. Validate that there are an odd
number of servers and one is PRIMARY and the remainder are in a SECONDARY
or ARBITER state.
- votes, Check the number of votes is odd and one is a PRIMARY and the
remainder are in a SECONDARY or ARBITER state. Authentication is
required here to get the replicaset configuration.
- minimal, Just checks that one server is in a PRIMARY state with the
remainder being SECONDARY or ARBITER.
type: str
choices:
- default
- votes
- minimal
default: default
notes:
- Requires the pymongo Python package on the remote host, version 2.4.2+. This
can be installed using pip or the OS package manager. @see U(http://api.mongodb.org/python/current/installation.html)
requirements:
- pymongo
'''
EXAMPLES = r'''
- name: Check replicaset is healthy, fail if not after first attempt
community.mongodb.mongodb_status:
replica_set: rs0
when: ansible_hostname == "mongodb1"
- name: Wait for the replicaset rs0 to converge, check 5 times, 10 second interval between checks
community.mongodb.mongodb_status:
replica_set: rs0
poll: 5
interval: 10
when: ansible_hostname == "mongodb1"
# Get the replicaset status and then lookup the primary's hostname and save to a variable
- name: Ensure replicaset is stable before beginning
community.mongodb.mongodb_status:
login_user: "{{ admin_user }}"
login_password: "{{ admin_user_password }}"
poll: 3
interval: 10
register: rs
- name: Lookup PRIMARY replicaset member
set_fact:
primary: "{{ item.key.split('.')[0] }}"
loop: "{{ lookup('dict', rs.replicaset) }}"
when: "'PRIMARY' in item.value"
'''
RETURN = r'''
failed:
description: If the module has failed or not.
returned: always
type: bool
iterations:
description: Number of times the module has queried the replicaset status.
returned: always
type: int
msg:
description: Status message.
returned: always
type: str
replicaset:
description: The last queried status of all the members of the replicaset if obtainable.
returned: always
type: dict
'''
import time
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
missing_required_lib,
mongodb_common_argument_spec,
mongo_auth,
PYMONGO_IMP_ERR,
pymongo_found,
get_mongodb_client,
)
def replicaset_config(client):
"""
Return the replicaset config document
https://docs.mongodb.com/manual/reference/command/replSetGetConfig/
"""
rs = client.admin.command('replSetGetConfig')
return rs
def replicaset_votes(config_document):
"""
Return the number of votes in the replicaset
"""
votes = 0
for member in config_document["config"]['members']:
votes += member['votes']
return votes
def replicaset_status(client, module):
"""
Return the replicaset status document from MongoDB
# https://docs.mongodb.com/manual/reference/command/replSetGetStatus/
"""
rs = client.admin.command('replSetGetStatus')
return rs
def replicaset_members(replicaset_document):
"""
Returns the members section of the MongoDB replicaset document
"""
return replicaset_document["members"]
def replicaset_friendly_document(members_document):
"""
Returns a version of the members document with
only the info this module requires: name & stateStr
"""
friendly_document = {}
for member in members_document:
friendly_document[member["name"]] = member["stateStr"]
return friendly_document
def replicaset_statuses(members_document, module):
"""
Return a list of the statuses
"""
statuses = []
for member in members_document:
statuses.append(members_document[member])
return statuses
def replicaset_good(statuses, module, votes):
"""
Returns true if the replicaset is in a "good" condition.
Good is defined as an odd number of servers >= 3, with
max one primary, and any even amount of
secondary and arbiter servers
"""
msg = "Unset"
status = None
valid_statuses = ["PRIMARY", "SECONDARY", "ARBITER"]
validate = module.params['validate']
if validate == "default":
if len(statuses) % 2 == 1:
if (statuses.count("PRIMARY") == 1
and ((statuses.count("SECONDARY")
+ statuses.count("ARBITER")) % 2 == 0)
and len(set(statuses) - set(valid_statuses)) == 0):
status = True
msg = "replicaset is in a converged state"
else:
status = False
msg = "replicaset is not currently in a converged state"
else:
msg = "Even number of servers in replicaset."
status = False
elif validate == "votes":
# Need to validate the number of votes in the replicaset
if votes % 2 == 1: # We have a good number of votes
if (statuses.count("PRIMARY") == 1
and len(set(statuses) - set(valid_statuses)) == 0):
status = True
msg = "replicaset is in a converged state"
else:
status = False
msg = "replicaset is not currently in a converged state"
else:
msg = "Even number of votes in replicaset."
status = False
elif validate == "minimal":
if (statuses.count("PRIMARY") == 1
and len(set(statuses) - set(valid_statuses)) == 0):
status = True
msg = "replicaset is in a converged state"
else:
status = False
msg = "replicaset is not currently in a converged state"
else:
module.fail_json(msg="Invalid value for validate has been provided: {0}".format(validate))
return status, msg
def replicaset_status_poll(client, module):
"""
client - MongoDB Client
poll - Number of times to poll
interval - interval between polling attempts
"""
iterations = 0 # How many times we have queried the cluster
failures = 0 # Number of failures when querying the replicaset
poll = module.params['poll']
interval = module.params['interval']
status = None
return_doc = {}
votes = None
config = None
while iterations < poll:
try:
iterations += 1
replicaset_document = replicaset_status(client, module)
members = replicaset_members(replicaset_document)
friendly_document = replicaset_friendly_document(members)
statuses = replicaset_statuses(friendly_document, module)
if module.params['validate'] == "votes": # Requires auth
config = replicaset_config(client)
votes = replicaset_votes(config)
status, msg = replicaset_good(statuses, module, votes)
if status: # replicaset looks good
return_doc = {"failures": failures,
"poll": poll,
"iterations": iterations,
"msg": msg,
"replicaset": friendly_document}
break
else:
failures += 1
return_doc = {"failures": failures,
"poll": poll,
"iterations": iterations,
"msg": msg,
"replicaset": friendly_document,
"failed": True}
if iterations == poll:
break
else:
time.sleep(interval)
except Exception as e:
failures += 1
return_doc['failed'] = True
return_doc['msg'] = str(e)
status = False
if iterations == poll:
break
else:
time.sleep(interval)
return_doc['failures'] = failures
return status, return_doc['msg'], return_doc
# =========================================
# Module execution.
#
def main():
argument_spec = mongodb_common_argument_spec()
argument_spec.update(
interval=dict(type='int', default=30),
poll=dict(type='int', default=1),
replica_set=dict(type='str', default="rs0"),
validate=dict(type='str', choices=['default', 'votes', 'minimal'], default='default'),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=False,
required_together=[['login_user', 'login_password']],
)
if not pymongo_found:
module.fail_json(msg=missing_required_lib('pymongo'),
exception=PYMONGO_IMP_ERR)
replica_set = module.params['replica_set']
msg = None
result = dict(
failed=False,
replica_set=replica_set,
)
try:
client = get_mongodb_client(module, directConnection=True)
client = mongo_auth(module, client, directConnection=True)
except Exception as e:
module.fail_json(msg='Unable to connect to database: %s' % to_native(e))
if len(replica_set) == 0:
module.fail_json(msg="Parameter 'replica_set' must not be an empty string")
try:
status, msg, return_doc = replicaset_status_poll(client, module) # Sort out the return doc
replicaset = return_doc['replicaset']
iterations = return_doc['iterations']
except Exception as e:
module.fail_json(msg='Unable to query replica_set info: {0}: {1}'.format(str(e), msg))
if status is False:
module.fail_json(msg=msg, replicaset=replicaset, iterations=iterations)
else:
module.exit_json(msg=msg, replicaset=replicaset, iterations=iterations)
if __name__ == '__main__':
main()
|