#!/usr/bin/python # Copyright: (c) 2020, Rhys Campbell # 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_balancer short_description: Manages the MongoDB Sharded Cluster Balancer. description: - Manages the MongoDB Sharded Cluster Balancer. - Start or stop the balancer. - Adjust the cluster chunksize. - Enable or disable autosplit. - Add or remove a balancer window. author: Rhys Campbell (@rhysmeister) version_added: "1.0.0" extends_documentation_fragment: - community.mongodb.login_options - community.mongodb.ssl_options options: autosplit: description: - Disable or enable the autosplit flag in the config.settings collection. required: false type: bool chunksize: description: - Control the size of chunks in the sharded cluster. - Value should be given in MB. required: false type: int state: description: - Manage the Balancer for the Cluster required: false type: str choices: - "started" - "stopped" default: "started" mongos_process: description: - Provide a custom name for the mongos process. - Most users can ignore this setting. required: false type: str default: "mongos" window: description: - Schedule the balancer window. - Provide the following dictionary keys start, stop, state - The state key should be "present" or "absent". - The start and stop keys are ignored when state is "absent". - start and stop should be strings in "HH:MM" format indicating the time bounds of the window. type: raw required: false 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: Start the balancer community.mongodb.mongodb_balancer: state: started - name: Stop the balancer and disable autosplit community.mongodb.mongodb_balancer: state: stopped autosplit: false - name: Enable autosplit community.mongodb.mongodb_balancer: autosplit: true - name: Change the default chunksize to 128MB community.mongodb.mongodb_balancer: chunksize: 128 - name: Add or update a balancing window community.mongodb.mongodb_balancer: window: start: "23:00" stop: "06:00" state: "present" - name: Remove a balancing window community.mongodb.mongodb_balancer: window: state: "absent" ''' RETURN = r''' changed: description: Whether the balancer state or autosplit changed. returned: success type: bool old_balancer_state: description: The previous state of the balancer returned: When balancer state is changed type: str new_balancer_state: description: The new state of the balancer. returned: When balancer state is changed type: str old_autosplit: description: The previous state of autosplit. returned: When autosplit is changed. type: str new_autosplit: description: The new state of autosplit. returned: When autosplit is changed. type: str old_chunksize: description: The previous value for chunksize. returned: When chunksize is changed. type: int new_chunksize: description: The new value for chunksize. returned: When chunksize is changed. type: int msg: description: A short description of what happened. returned: failure type: str failed: description: If something went wrong returned: failed type: bool ''' 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, ) has_ordereddict = False try: from collections import OrderedDict has_ordereddict = True except ImportError as excep: try: from ordereddict import OrderedDict has_ordereddict = True except ImportError as excep: pass def get_balancer_state(client): ''' Gets the state of the MongoDB balancer. The config.settings collection does not exist until the balancer has been started for the first time { "_id" : "balancer", "mode" : "full", "stopped" : false } { "_id" : "autosplit", "enabled" : true } ''' balancer_state = None result = client["config"].settings.find_one({"_id": "balancer"}) if not result: balancer_state = "stopped" else: if result['stopped'] is False: balancer_state = "started" else: balancer_state = "stopped" return balancer_state def stop_balancer(client): ''' Stops MongoDB balancer ''' cmd_doc = OrderedDict([ ('balancerStop', 1), ('maxTimeMS', 60000) ]) client['admin'].command(cmd_doc) time.sleep(1) def start_balancer(client): ''' Starts MongoDB balancer ''' cmd_doc = OrderedDict([ ('balancerStart', 1), ('maxTimeMS', 60000) ]) client['admin'].command(cmd_doc) time.sleep(1) def enable_autosplit(client): client["config"].settings.update_one({"_id": "autosplit"}, {"$set": {"enabled": True}}, upsert=True) def disable_autosplit(client): client["config"].settings.update_one({"_id": "autosplit"}, {"$set": {"enabled": False}}, upsert=True) def get_autosplit(client): autosplit = False result = client["config"].settings.find_one({"_id": "autosplit"}) if result is not None: autosplit = result['enabled'] return autosplit def get_chunksize(client): ''' Default chunksize is 64MB ''' chunksize = None result = client["config"].settings.find_one({"_id": "chunksize"}) if not result: chunksize = 64 else: chunksize = result['value'] return chunksize def set_chunksize(client, chunksize): client["config"].settings.update_one({"_id": "chunksize"}, {"$set": {"value": chunksize}}, upsert=True) def set_balancing_window(client, start, stop): s = False result = client["config"].settings.update_one({"_id": "balancer"}, {"$set": { "activeWindow": { "start": start, "stop": stop}}}, upsert=True) if result.modified_count == 1 or result.upserted_id is not None: s = True return s def remove_balancing_window(client): s = False result = client["config"].settings.update_one({"_id": "balancer"}, {"$unset": {"activeWindow": True}}) if result.modified_count == 1: s = True return s def balancing_window(client, start, stop): s = False if start is not None and stop is not None: result = client["config"].settings.find_one({"_id": "balancer", "activeWindow.start": start, "activeWindow.stop": stop}) else: result = client["config"].settings.find_one({"_id": "balancer", "activeWindow": {"$exists": True}}) if result: s = True return s def validate_window(window, module): if window is not None: if 'state' not in window.keys(): module.fail_json(msg="Balancing window state must be specified") elif window['state'] not in ['present', 'absent']: module.fail_json(msg="Balancing window state must be present or absent") elif window['state'] == "present" \ and ("start" not in window.keys() or "stop" not in window.keys()): module.fail_json(msg="Balancing window start and stop values must be specified") return True def main(): argument_spec = mongodb_common_argument_spec() argument_spec.update( autosplit=dict(type='bool', default=None), chunksize=dict(type='int', default=None), mongos_process=dict(type='str', required=False, default="mongos"), state=dict(type='str', default="started", choices=["started", "stopped"]), window=dict(type='raw', default=None) ) module = AnsibleModule( argument_spec=argument_spec, supports_check_mode=True, required_together=[['login_user', 'login_password']], ) if not has_ordereddict: module.fail_json(msg='Cannot import OrderedDict class. You can probably install with: pip install ordereddict') if not pymongo_found: module.fail_json(msg=missing_required_lib('pymongo'), exception=PYMONGO_IMP_ERR) login_host = module.params['login_host'] login_port = module.params['login_port'] balancer_state = module.params['state'] autosplit = module.params['autosplit'] chunksize = module.params['chunksize'] mongos_process = module.params['mongos_process'] window = module.params['window'] # Validate window validate_window(window, module) result = dict( changed=False, ) try: client = get_mongodb_client(module) client = mongo_auth(module, client) except Exception as excep: module.fail_json(msg='Unable to connect to MongoDB: %s' % to_native(excep)) changed = False cluster_balancer_state = None cluster_autosplit = None cluster_chunksize = None old_balancer_state = None new_balancer_state = None old_autosplit = None new_autosplit = None old_chunksize = None new_chunksize = None try: if client["admin"].command("serverStatus")["process"] != mongos_process: module.fail_json(msg="Process running on {0}:{1} is not a {2}".format(login_host, login_port, mongos_process)) cluster_balancer_state = get_balancer_state(client) if autosplit is not None: cluster_autosplit = get_autosplit(client) if chunksize is not None: cluster_chunksize = get_chunksize(client) if module.check_mode: if balancer_state != cluster_balancer_state: old_balancer_state = cluster_balancer_state new_balancer_state = balancer_state changed = True if (autosplit is not None and autosplit != cluster_autosplit): old_autosplit = cluster_autosplit new_autosplit = autosplit changed = True if (chunksize is not None and chunksize != cluster_chunksize): old_chunksize = cluster_chunksize new_chunksize = chunksize changed = True if window is not None: if balancing_window(client, window.get('start'), window.get('stop')): if window['state'] == "present": pass else: changed = True else: if window['state'] == "present": changed = True else: pass else: if balancer_state is not None \ and balancer_state != cluster_balancer_state: if balancer_state == "started": start_balancer(client) old_balancer_state = cluster_balancer_state new_balancer_state = get_balancer_state(client) changed = True else: stop_balancer(client) old_balancer_state = cluster_balancer_state new_balancer_state = get_balancer_state(client) changed = True if autosplit is not None \ and autosplit != cluster_autosplit: if autosplit: enable_autosplit(client) old_autosplit = cluster_autosplit new_autosplit = autosplit changed = True else: disable_autosplit(client) old_autosplit = cluster_autosplit new_autosplit = autosplit changed = True if (chunksize is not None and chunksize != cluster_chunksize): set_chunksize(client, chunksize) old_chunksize = cluster_chunksize new_chunksize = chunksize changed = True if window is not None: if balancing_window(client, window.get('start'), window.get('stop')): if window['state'] == "present": pass else: remove_balancing_window(client) changed = True else: if window['state'] == "present": set_balancing_window(client, window['start'], window['stop']) changed = True else: pass except Exception as excep: result["msg"] = "An error occurred: {0}".format(excep) result['changed'] = changed if old_balancer_state is not None: result['old_balancer_state'] = old_balancer_state result['new_balancer_state'] = new_balancer_state if old_autosplit is not None: result['old_autosplit'] = old_autosplit result['new_autosplit'] = new_autosplit if old_chunksize is not None: result['old_chunksize'] = old_chunksize result['new_chunksize'] = new_chunksize module.exit_json(**result) if __name__ == '__main__': main()