Repository URL to install this package:
Version:
6.0.0 ▾
|
#!/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_replicaset
short_description: Initialises a MongoDB replicaset.
description:
- Initialises a MongoDB replicaset in a new deployment.
- Validates the replicaset name for existing deployments.
- Advanced replicaset member configuration possible (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
members:
description:
- Yaml list consisting of the replicaset members.
- Csv string will also be accepted i.e. mongodb1:27017,mongodb2:27017,mongodb3:27017.
- A dictionary can also be used to specify advanced replicaset member options.
- If a port number is not provided then 27017 is assumed.
type: list
elements: raw
validate:
description:
- Performs some basic validation on the provided replicaset config.
type: bool
default: yes
arbiter_at_index:
description:
- Identifies the position of the member in the array that is an arbiter.
type: int
chaining_allowed:
description:
- When I(settings.chaining_allowed=true), the replicaset allows secondary members to replicate from other
secondary members.
- When I(settings.chaining_allowed=false), secondaries can replicate only from the primary.
type: bool
default: yes
heartbeat_timeout_secs:
description:
- Number of seconds that the replicaset members wait for a successful heartbeat from each other.
- If a member does not respond in time, other members mark the delinquent member as inaccessible.
- The setting only applies when using I(protocol_version=0). When using I(protocol_version=1) the relevant
setting is I(settings.election_timeout_millis).
type: int
default: 10
election_timeout_millis:
description:
- The time limit in milliseconds for detecting when a replicaset's primary is unreachable.
type: int
default: 10000
protocol_version:
description: Version of the replicaset election protocol.
type: int
choices: [ 0, 1 ]
default: 1
reconfigure:
description:
- This feature is currently experimental. Please test your scenario thoroughly.
- Consult the integration test file for supported scenarios - \
[Integration tests](https://github.com/ansible-collections/community.mongodb/tree/master/tests/integration/targets/mongodb_replicaset/tasks). \
See files prefixed with 330.
- Whether to perform replicaset reconfiguration actions.
- Only relevant when the replicaset already exists.
- Only one member should be removed or added per invocation.
- Members should be specific as either all strings or all dicts when reconfiguring.
- Currently no support for replicaset settings document changes.
type: bool
default: false
force:
description:
- Only relevant when reconfigure = true.
- Specify true to force the available replica set members to accept the new configuration.
- Force reconfiguration can result in unexpected or undesired behavior, including rollback of "majority" committed writes.
type: bool
default: false
max_time_ms:
description:
- Specifies a cumulative time limit in milliseconds for processing the replicaset reconfiguration.
type: int
default: null
debug:
description:
- Add additonal info for debug.
type: bool
default: false
cluster_cmd:
description:
- Command the module should use to obtain information about the MongoDB node we are connecting to.
type: str
choices:
- isMaster
- hello
default: hello
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'''
# Create a replicaset called 'rs0' with the 3 provided members
- name: Ensure replicaset rs0 exists
community.mongodb.mongodb_replicaset:
login_host: localhost
login_user: admin
login_password: admin
replica_set: rs0
members:
- mongodb1:27017
- mongodb2:27017
- mongodb3:27017
when: groups.mongod.index(inventory_hostname) == 0
# Create two single-node replicasets on the localhost for testing
- name: Ensure replicaset rs0 exists
community.mongodb.mongodb_replicaset:
login_host: localhost
login_port: 3001
login_user: admin
login_password: secret
login_database: admin
replica_set: rs0
members: localhost:3001
validate: no
- name: Ensure replicaset rs1 exists
community.mongodb.mongodb_replicaset:
login_host: localhost
login_port: 3002
login_user: admin
login_password: secret
login_database: admin
replica_set: rs1
members: localhost:3002
validate: no
- name: Create a replicaset and use a custom priority for each member
community.mongodb.mongodb_replicaset:
login_host: localhost
login_user: admin
login_password: admin
replica_set: rs0
members:
- host: "localhost:3001"
priority: 1
- host: "localhost:3002"
priority: 0.5
- host: "localhost:3003"
priority: 0.5
when: groups.mongod.index(inventory_hostname) == 0
- name: Create replicaset rs1 with options and member tags
community.mongodb.mongodb_replicaset:
login_host: localhost
login_port: 3001
login_database: admin
replica_set: rs1
members:
- host: "localhost:3001"
priority: 1
tags:
dc: "east"
usage: "production"
- host: "localhost:3002"
priority: 1
tags:
dc: "east"
usage: "production"
- host: "localhost:3003"
priority: 0
hidden: true
slaveDelay: 3600
tags:
dc: "west"
usage: "reporting"
- name: Replicaset with one arbiter node (mongodb3 - index is zero-based)
community.mongodb.mongodb_replicaset:
login_user: admin
login_password: admin
replica_set: rs0
members:
- mongodb1:27017
- mongodb2:27017
- mongodb3:27017
arbiter_at_index: 2
when: groups.mongod.index(inventory_hostname) == 0
- name: Add a new member to a replicaset - Safe for pre-5.0 consult documentation - https://docs.mongodb.com/manual/tutorial/expand-replica-set/
block:
- name: Create replicaset with module - with dicts
community.mongodb.mongodb_replicaset:
replica_set: "rs0"
members:
- host: localhost:3001
- host: localhost:3002
- host: localhost:3003
- name: Wait for the replicaset to stabilise
community.mongodb.mongodb_status:
replica_set: "rs0"
poll: 5
interval: 10
- name: Remove a member from the replicaset
community.mongodb.mongodb_replicaset:
replica_set: "rs0"
reconfigure: yes
members:
- host: localhost:3001
- host: localhost:3002
- name: Wait for the replicaset to stabilise after member removal
community.mongodb.mongodb_status:
replica_set: "rs0"
validate: minimal
poll: 5
interval: 10
- name: Add a member to the replicaset
community.mongodb.mongodb_replicaset:
replica_set: "rs0"
reconfigure: yes
members:
- host: localhost:3001
- host: localhost:3002
- host: localhost:3004
hidden: true
votes: 0
priority: 0
- name: Wait for the replicaset to stabilise after member addition
community.mongodb.mongodb_status:
replica_set: "rs0"
validate: minimal
poll: 5
interval: 30
- name: Reconfigure the replicaset - Make member 3004 a normal voting member
community.mongodb.mongodb_replicaset:
replica_set: "rs0"
reconfigure: yes
members:
- host: localhost:3001
- host: localhost:3002
- host: localhost:3004
hidden: false
votes: 1
priority: 1
- name: Wait for the replicaset to stabilise
community.mongodb.mongodb_status:
replica_set: "rs0"
poll: 5
interval: 30
'''
RETURN = r'''
mongodb_replicaset:
description: The name of the replicaset that has been created.
returned: success
type: str
reconfigure:
description: If a replicaset reconfiguration occured.
returned: On rpelicaset reconfiguration
type: bool
'''
from copy import deepcopy
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,
member_dicts_different,
lists_are_different,
PYMONGO_IMP_ERR,
pymongo_found,
get_mongodb_client,
)
def get_replicaset_config(client):
conf = client.admin.command({'replSetGetConfig': 1})
return conf['config']
def get_member_names(client):
conf = get_replicaset_config(client)
members = []
for member in conf['members']:
members.append(member['host'])
return members
def modify_members(module, config, members):
"""
Modifies the members section of the config document as appropriate.
@module - Ansible module object
@config - Replicaset config document from MongoDB
@members - Members config from module
"""
try: # refactor repeated code
from collections import OrderedDict
except ImportError as excep:
try:
from ordereddict import OrderedDict
except ImportError as excep:
module.fail_json(msg='Cannot import OrderedDict class. You can probably install with: pip install ordereddict: %s'
% to_native(excep))
new_member_config = [] # the list of dicts containing the members for the replicaset configuration document
existing_members = [] # members that are staying in the config
max_id = 0
if all(isinstance(member, str) for member in members):
for current_member in config['members']:
if current_member["host"] in members:
new_member_config.append(current_member)
existing_members.append(current_member["host"])
if current_member["_id"] > max_id:
max_id = current_member["_id"]
member_additions = list(set(members) - set(existing_members))
if len(member_additions) > 0:
for member in member_additions:
if ':' not in member: # No port supplied. Assume 27017
member += ":27017"
new_member_config.append(OrderedDict([("_id", max_id + 1), ("host", member)]))
max_id += 1
config["members"] = new_member_config
elif all(isinstance(member, dict) for member in members):
# We need to put the _id values in into the matching document and generate them for new hosts
# TODO: https://docs.mongodb.com/manual/reference/replica-configuration/#mongodb-rsconf-rsconf.members-n-._id
# Maybe we can add a new member id parameter value, stick with the incrementing for now
# Perhaps even save this in the mongodb instance?
# first get all the existing members of the replicaset
new_member_config = []
existing_members = {}
matched_members = [] # members that have been supplied by the moduel and matched with existing members
max_id = 0
for member in config["members"]:
existing_members[member["host"]] = member["_id"]
if member["_id"] > max_id:
max_id = member["_id"]
# append existing members with the appropriate _id
for member in members:
if member["host"] in existing_members:
member["_id"] = existing_members[member["host"]]
matched_members.append(member["host"])
new_member_config.append(member)
for member in members:
if member["host"] not in matched_members: # new member , append and increment id
max_id = max_id + 1
member["_id"] = max_id
new_member_config.append(member)
config["members"] = new_member_config
else:
module.fail_json(msg="All items in members must be either of type dict of str")
return config
def replicaset_reconfigure(module, client, config, force, max_time_ms):
config['version'] += 1
try:
from collections import OrderedDict
except ImportError as excep:
try:
from ordereddict import OrderedDict
except ImportError as excep:
module.fail_json(msg='Cannot import OrderedDict class. You can probably install with: pip install ordereddict: %s'
% to_native(excep))
cmd_doc = OrderedDict([("replSetReconfig", config),
("force", force)])
if max_time_ms is not None:
cmd_doc.update({"maxTimeMS": max_time_ms})
client.admin.command(cmd_doc)
# return result
def replicaset_find(client, cluster_cmd):
"""Check if a replicaset exists.
Args:
client (cursor): Mongodb cursor on admin database.
cluster_cmd (str): Either isMaster or hello
Returns:
str: when the node is a member of a replicaset , False otherwise.
"""
doc = client['admin'].command(cluster_cmd)
if 'setName' in doc:
return str(doc['setName'])
return False
def replicaset_add(module, client, replica_set, members, arbiter_at_index, protocol_version,
chaining_allowed, heartbeat_timeout_secs, election_timeout_millis):
try:
from collections import OrderedDict
except ImportError as excep:
try:
from ordereddict import OrderedDict
except ImportError as excep:
module.fail_json(msg='Cannot import OrderedDict class. You can probably install with: pip install ordereddict: %s'
% to_native(excep))
members_dict_list = []
index = 0
settings = {
"chainingAllowed": bool(chaining_allowed),
}
if protocol_version == 0:
settings['heartbeatTimeoutSecs'] = heartbeat_timeout_secs
else:
settings['electionTimeoutMillis'] = election_timeout_millis
for member in members:
if isinstance(member, str):
if ':' not in member: # No port supplied. Assume 27017
member += ":27017"
members_dict_list.append(OrderedDict([("_id", int(index)), ("host", member)]))
if index == arbiter_at_index:
members_dict_list[index]['arbiterOnly'] = True
index += 1
elif isinstance(member, dict):
hostname = member["host"]
if ':' not in hostname:
hostname += ":27017"
members_dict_list.append(OrderedDict([("_id", int(index)), ("host", hostname)]))
for key in list(member.keys()):
if key != "host":
members_dict_list[index][key] = member[key]
if index == arbiter_at_index:
members_dict_list[index]['arbiterOnly'] = True
index += 1
else:
raise ValueError("member should be a str or dict. Instead found: {0}".format(str(type(members))))
conf = OrderedDict([("_id", replica_set),
("protocolVersion", protocol_version),
("members", members_dict_list),
("settings", settings)])
try:
client["admin"].command('replSetInitiate', conf)
except Exception as excep:
raise Exception("Some problem {0} | {1}".format(str(excep), str(conf)))
def replicaset_remove(module, client, replica_set):
raise NotImplementedError
def modify_members_flow(module, client, members, result):
debug = module.params['debug']
force = module.params['force']
max_time_ms = module.params['max_time_ms']
diff = False
modified_config = None
config = None
try:
config = get_replicaset_config(client)
except Exception as excep:
module.fail_json(msg="Unable to get replicaset configuration {0}".format(excep))
if isinstance(members[0], str):
diff = lists_are_different(members, get_member_names(client))
elif isinstance(members[0], dict):
diff = member_dicts_different(config, members)
else:
module.fail_json(msg="members must be either str or dict")
if diff:
if not module.check_mode:
try:
modified_config = modify_members(module, config, members)
if debug:
result['config'] = str(config)
result['modified_config'] = str(modified_config)
replicaset_reconfigure(module, client, modified_config, force, max_time_ms)
except Exception as excep:
module.fail_json(msg="Failed reconfiguring replicaset {0}, config doc {1}".format(excep, modified_config))
result['changed'] = True
result['msg'] = "replicaset reconfigured"
else:
result['changed'] = False
return result
# =========================================
# Module execution.
#
def main():
argument_spec = mongodb_common_argument_spec()
argument_spec.update(
arbiter_at_index=dict(type='int'),
chaining_allowed=dict(type='bool', default=True),
election_timeout_millis=dict(type='int', default=10000),
heartbeat_timeout_secs=dict(type='int', default=10),
members=dict(type='list', elements='raw'),
protocol_version=dict(type='int', default=1, choices=[0, 1]),
replica_set=dict(type='str', default="rs0"),
validate=dict(type='bool', default=True),
reconfigure=dict(type='bool', default=False),
force=dict(type='bool', default=False),
max_time_ms=dict(type='int', default=None),
debug=dict(type='bool', default=False),
cluster_cmd=dict(type='str', choices=['isMaster', 'hello'], default='hello')
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
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']
members = module.params['members']
arbiter_at_index = module.params['arbiter_at_index']
validate = module.params['validate']
protocol_version = module.params['protocol_version']
chaining_allowed = module.params['chaining_allowed']
heartbeat_timeout_secs = module.params['heartbeat_timeout_secs']
election_timeout_millis = module.params['election_timeout_millis']
reconfigure = module.params['reconfigure']
force = module.params['force'] # TODO tidy this stuff up
max_time_ms = module.params['max_time_ms']
debug = module.params['debug']
cluster_cmd = module.params['cluster_cmd']
if validate and reconfigure is False:
if len(members) <= 2 or len(members) % 2 == 0:
module.fail_json(msg="MongoDB Replicaset validation failed. Invalid number of replicaset members.")
if arbiter_at_index is not None and len(members) - 1 < arbiter_at_index:
module.fail_json(msg="MongoDB Replicaset validation failed. Invalid arbiter index.")
result = dict(
changed=False,
replica_set=replica_set,
)
try:
client = get_mongodb_client(module, directConnection=True)
except Exception as e:
module.fail_json(msg='Unable to connect to database: %s' % to_native(e))
try:
rs = replicaset_find(client, cluster_cmd) # does not require auth
except Exception as e:
module.fail_json(msg='Unable to connect to query replicaset: %s' % to_native(e))
if isinstance(rs, str):
if replica_set == rs:
if reconfigure:
client = mongo_auth(module, client)
result = modify_members_flow(module, client, members, result)
else:
result['changed'] = False
result['replica_set'] = rs
module.exit_json(**result)
else:
module.fail_json(msg="The replica_set name of {0} does not match the expected: {1}".format(rs, replica_set))
else: # replicaset does not exist
# Some validation stuff
if len(replica_set) == 0:
module.fail_json(msg="Parameter replica_set must not be an empty string")
if module.check_mode is False:
try:
replicaset_add(module, client, replica_set, members,
arbiter_at_index, protocol_version,
chaining_allowed, heartbeat_timeout_secs,
election_timeout_millis)
result['changed'] = True
except Exception as e:
module.fail_json(msg='Unable to create replica_set: %s' % to_native(e))
else:
result['changed'] = True
module.exit_json(**result)
if __name__ == '__main__':
main()