summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/mongodb/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'ansible_collections/community/mongodb/plugins')
-rw-r--r--ansible_collections/community/mongodb/plugins/cache/__init__.py0
-rw-r--r--ansible_collections/community/mongodb/plugins/cache/mongodb.py204
-rw-r--r--ansible_collections/community/mongodb/plugins/doc_fragments/login_options.py49
-rw-r--r--ansible_collections/community/mongodb/plugins/doc_fragments/ssl_options.py79
-rw-r--r--ansible_collections/community/mongodb/plugins/lookup/__init__.py0
-rw-r--r--ansible_collections/community/mongodb/plugins/lookup/mongodb.py269
-rw-r--r--ansible_collections/community/mongodb/plugins/module_utils/__init__.py0
-rw-r--r--ansible_collections/community/mongodb/plugins/module_utils/mongodb_common.py482
-rw-r--r--ansible_collections/community/mongodb/plugins/module_utils/mongodb_shell.py135
-rw-r--r--ansible_collections/community/mongodb/plugins/modules/__init__.py0
-rw-r--r--ansible_collections/community/mongodb/plugins/modules/mongodb_balancer.py452
-rw-r--r--ansible_collections/community/mongodb/plugins/modules/mongodb_index.py405
-rw-r--r--ansible_collections/community/mongodb/plugins/modules/mongodb_info.py315
-rw-r--r--ansible_collections/community/mongodb/plugins/modules/mongodb_maintenance.py146
-rw-r--r--ansible_collections/community/mongodb/plugins/modules/mongodb_monitoring.py197
-rw-r--r--ansible_collections/community/mongodb/plugins/modules/mongodb_oplog.py190
-rw-r--r--ansible_collections/community/mongodb/plugins/modules/mongodb_parameter.py144
-rw-r--r--ansible_collections/community/mongodb/plugins/modules/mongodb_replicaset.py611
-rw-r--r--ansible_collections/community/mongodb/plugins/modules/mongodb_role.py387
-rw-r--r--ansible_collections/community/mongodb/plugins/modules/mongodb_schema.py346
-rw-r--r--ansible_collections/community/mongodb/plugins/modules/mongodb_shard.py306
-rw-r--r--ansible_collections/community/mongodb/plugins/modules/mongodb_shard_tag.py216
-rw-r--r--ansible_collections/community/mongodb/plugins/modules/mongodb_shard_zone.py323
-rw-r--r--ansible_collections/community/mongodb/plugins/modules/mongodb_shell.py328
-rw-r--r--ansible_collections/community/mongodb/plugins/modules/mongodb_shutdown.py137
-rw-r--r--ansible_collections/community/mongodb/plugins/modules/mongodb_status.py353
-rw-r--r--ansible_collections/community/mongodb/plugins/modules/mongodb_stepdown.py248
-rw-r--r--ansible_collections/community/mongodb/plugins/modules/mongodb_user.py428
28 files changed, 6750 insertions, 0 deletions
diff --git a/ansible_collections/community/mongodb/plugins/cache/__init__.py b/ansible_collections/community/mongodb/plugins/cache/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/cache/__init__.py
diff --git a/ansible_collections/community/mongodb/plugins/cache/mongodb.py b/ansible_collections/community/mongodb/plugins/cache/mongodb.py
new file mode 100644
index 000000000..b51b7b293
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/cache/mongodb.py
@@ -0,0 +1,204 @@
+# (c) 2018, Matt Martz <matt@sivel.net>
+# 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
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = '''
+name: mongodb
+author:
+ - Matt Martz (@sivel)
+version_added: "1.0.0"
+short_description: Use MongoDB for caching
+description:
+ - This cache uses per host records saved in MongoDB.
+requirements:
+ - pymongo>=3
+options:
+ _uri:
+ description:
+ - MongoDB Connection String URI
+ required: False
+ env:
+ - name: ANSIBLE_CACHE_PLUGIN_CONNECTION
+ ini:
+ - key: fact_caching_connection
+ section: defaults
+ _prefix:
+ description: User defined prefix to use when creating the DB entries
+ default: ansible_facts
+ env:
+ - name: ANSIBLE_CACHE_PLUGIN_PREFIX
+ ini:
+ - key: fact_caching_prefix
+ section: defaults
+ _timeout:
+ default: 86400
+ description: Expiration timeout in seconds for the cache plugin data. Set to 0 to never expire
+ env:
+ - name: ANSIBLE_CACHE_PLUGIN_TIMEOUT
+ ini:
+ - key: fact_caching_timeout
+ section: defaults
+ type: integer
+'''
+
+import datetime
+
+from contextlib import contextmanager
+
+from ansible import constants as C
+from ansible.errors import AnsibleError
+from ansible.plugins.cache import BaseCacheModule
+from ansible.utils.display import Display
+from ansible.module_utils._text import to_native
+
+pymongo_missing = False
+
+try:
+ import pymongo
+except ImportError:
+ pymongo_missing = True
+
+display = Display()
+
+
+class CacheModule(BaseCacheModule):
+ """
+ A caching module backed by mongodb.
+ """
+ def __init__(self, *args, **kwargs):
+ try:
+ if pymongo_missing:
+ raise AnsibleError("The 'pymongo' python module is required for the mongodb fact cache, 'pip install pymongo>=3.0'")
+ super(CacheModule, self).__init__(*args, **kwargs)
+ self._connection = self.get_option('_uri')
+ self._timeout = int(self.get_option('_timeout'))
+ self._prefix = self.get_option('_prefix')
+ except KeyError:
+ self._connection = C.CACHE_PLUGIN_CONNECTION
+ self._timeout = int(C.CACHE_PLUGIN_TIMEOUT)
+ self._prefix = C.CACHE_PLUGIN_PREFIX
+
+ self._cache = {}
+ self._managed_indexes = False
+
+ def _ttl_index_exists(self, collection):
+ '''
+ Returns true if an index named ttl exists
+ on the given collection.
+ '''
+ exists = False
+ try:
+ indexes = collection.list_indexes()
+ for index in indexes:
+ if index["name"] == "ttl":
+ exists = True
+ break
+ except pymongo.errors.OperationFailure as excep:
+ raise AnsibleError('Error checking MongoDB index: %s' % to_native(excep))
+ return exists
+
+ def _manage_indexes(self, collection):
+ '''
+ This function manages indexes on the mongo collection.
+ We only do this once, at run time based on _managed_indexes,
+ rather than per connection instantiation as that would be overkill
+ '''
+ _timeout = self._timeout
+ if _timeout and _timeout > 0:
+ try:
+ collection.create_index(
+ 'date',
+ name='ttl',
+ expireAfterSeconds=_timeout
+ )
+ except pymongo.errors.OperationFailure:
+ # We make it here when the fact_caching_timeout was set to a different value between runs
+ if self._ttl_index_exists(collection):
+ collection.drop_index('ttl')
+ return self._manage_indexes(collection)
+ else:
+ if self._ttl_index_exists(collection):
+ collection.drop_index('ttl')
+
+ @contextmanager
+ def _collection(self):
+ '''
+ This is a context manager for opening and closing mongo connections as needed. This exists as to not create a global
+ connection, due to pymongo not being fork safe (http://api.mongodb.com/python/current/faq.html#is-pymongo-fork-safe)
+ '''
+ mongo = pymongo.MongoClient(self._connection)
+ try:
+ db = mongo.get_default_database()
+ except pymongo.errors.ConfigurationError:
+ # We'll fall back to using ``ansible`` as the database if one was not provided
+ # in the MongoDB Connection String URI
+ db = mongo['ansible']
+
+ # The collection is hard coded as ``cache``, there are no configuration options for this
+ collection = db['cache']
+ if not self._managed_indexes:
+ # Only manage the indexes once per run, not per connection
+ self._manage_indexes(collection)
+ self._managed_indexes = True
+
+ yield collection
+
+ mongo.close()
+
+ def _make_key(self, key):
+ return '%s%s' % (self._prefix, key)
+
+ def get(self, key):
+ if key not in self._cache:
+ with self._collection() as collection:
+ value = collection.find_one({'_id': self._make_key(key)})
+ self._cache[key] = value['data']
+
+ return self._cache.get(key)
+
+ def set(self, key, value):
+ self._cache[key] = value
+ with self._collection() as collection:
+ collection.update_one(
+ {'_id': self._make_key(key)},
+ {
+ '$set': {
+ '_id': self._make_key(key),
+ 'data': value,
+ 'date': datetime.datetime.utcnow()
+ }
+ },
+ upsert=True
+ )
+
+ def keys(self):
+ with self._collection() as collection:
+ return [doc['_id'] for doc in collection.find({}, {'_id': True})]
+
+ def contains(self, key):
+ with self._collection() as collection:
+ return bool(collection.count({'_id': self._make_key(key)}))
+
+ def delete(self, key):
+ del self._cache[key]
+ with self._collection() as collection:
+ collection.delete_one({'_id': self._make_key(key)})
+
+ def flush(self):
+ with self._collection() as collection:
+ collection.delete_many({})
+
+ def copy(self):
+ with self._collection() as collection:
+ return dict((d['_id'], d['data']) for d in collection.find({}))
+
+ def __getstate__(self):
+ return dict()
+
+ def __setstate__(self, data):
+ self.__init__()
diff --git a/ansible_collections/community/mongodb/plugins/doc_fragments/login_options.py b/ansible_collections/community/mongodb/plugins/doc_fragments/login_options.py
new file mode 100644
index 000000000..5307bca6e
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/doc_fragments/login_options.py
@@ -0,0 +1,49 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class ModuleDocFragment(object):
+ # Standard documentation
+ DOCUMENTATION = r'''
+options:
+ login_user:
+ description:
+ - The MongoDB user to login with.
+ - Required when I(login_password) is specified.
+ required: no
+ type: str
+ login_password:
+ description:
+ - The password used to authenticate with.
+ - Required when I(login_user) is specified.
+ required: no
+ type: str
+ login_database:
+ description:
+ - The database where login credentials are stored.
+ required: no
+ type: str
+ default: 'admin'
+ login_host:
+ description:
+ - The host running MongoDB instance to login to.
+ required: no
+ type: str
+ default: 'localhost'
+ login_port:
+ description:
+ - The MongoDB server port to login to.
+ required: no
+ type: int
+ default: 27017
+ strict_compatibility:
+ description:
+ - Enforce strict requirements for pymongo and MongoDB software versions
+ type: bool
+ default: True
+ atlas_auth:
+ description:
+ - Authentication path intended for MongoDB Atlas Instances
+ type: bool
+ default: False
+'''
diff --git a/ansible_collections/community/mongodb/plugins/doc_fragments/ssl_options.py b/ansible_collections/community/mongodb/plugins/doc_fragments/ssl_options.py
new file mode 100644
index 000000000..efd330b7d
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/doc_fragments/ssl_options.py
@@ -0,0 +1,79 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class ModuleDocFragment(object):
+ # Standard documentation
+ DOCUMENTATION = r'''
+options:
+ ssl:
+ description:
+ - Whether to use an SSL connection when connecting to the database.
+ required: no
+ type: bool
+ default: no
+ aliases:
+ - tls
+ ssl_cert_reqs:
+ description:
+ - Specifies whether a certificate is required from the other side of the connection,
+ and whether it will be validated if provided.
+ required: no
+ type: str
+ default: 'CERT_REQUIRED'
+ choices:
+ - 'CERT_NONE'
+ - 'CERT_OPTIONAL'
+ - 'CERT_REQUIRED'
+ aliases:
+ - tlsAllowInvalidCertificates
+ ssl_ca_certs:
+ description:
+ - The ssl_ca_certs option takes a path to a CA file.
+ required: no
+ type: str
+ aliases:
+ - tlsCAFile
+ ssl_crlfile:
+ description:
+ - The ssl_crlfile option takes a path to a CRL file.
+ required: no
+ type: str
+ ssl_certfile:
+ description:
+ - Present a client certificate using the ssl_certfile option.
+ required: no
+ type: str
+ aliases:
+ - tlsCertificateKeyFile
+ ssl_keyfile:
+ description:
+ - Private key for the client certificate.
+ required: no
+ type: str
+ ssl_pem_passphrase:
+ description:
+ - Passphrase to decrypt encrypted private keys.
+ required: no
+ type: str
+ aliases:
+ - tlsCertificateKeyFilePassword
+ auth_mechanism:
+ description:
+ - Authentication type.
+ required: no
+ type: str
+ choices:
+ - 'SCRAM-SHA-256'
+ - 'SCRAM-SHA-1'
+ - 'MONGODB-X509'
+ - 'GSSAPI'
+ - 'PLAIN'
+ connection_options:
+ description:
+ - Additional connection options.
+ - Supply as a list of dicts or strings containing key value pairs seperated with '='.
+ required: no
+ type: list
+ elements: raw
+'''
diff --git a/ansible_collections/community/mongodb/plugins/lookup/__init__.py b/ansible_collections/community/mongodb/plugins/lookup/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/lookup/__init__.py
diff --git a/ansible_collections/community/mongodb/plugins/lookup/mongodb.py b/ansible_collections/community/mongodb/plugins/lookup/mongodb.py
new file mode 100644
index 000000000..b49c3f334
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/lookup/mongodb.py
@@ -0,0 +1,269 @@
+# (c) 2016, Marcos Diez <marcos@unitron.com.br>
+# https://github.com/marcosdiez/
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+
+__metaclass__ = type
+
+DOCUMENTATION = '''
+name: mongodb
+author:
+ - Marcos Diez (@marcosdiez)
+version_added: "1.0.0"
+short_description: lookup info from MongoDB
+description:
+ - 'The ``MongoDB`` lookup runs the *find()* command on a given *collection* on a given *MongoDB* server.'
+ - 'The result is a list of jsons, so slightly different from what PyMongo returns. In particular, *timestamps* are converted to epoch integers.'
+options:
+ connect_string:
+ description:
+ - Can be any valid MongoDB connection string, supporting authentication, replica sets, etc.
+ - "More info at U(https://docs.mongodb.org/manual/reference/connection-string/)"
+ default: "mongodb://localhost/"
+ database:
+ description:
+ - Name of the database which the query will be made
+ required: True
+ collection:
+ description:
+ - Name of the collection which the query will be made
+ required: True
+ filter:
+ description:
+ - Criteria of the output
+ type: 'dict'
+ default: {}
+ projection:
+ description:
+ - Fields you want returned
+ type: dict
+ default: {}
+ skip:
+ description:
+ - How many results should be skipped
+ type: integer
+ limit:
+ description:
+ - How many results should be shown
+ type: integer
+ sort:
+ description:
+ - Sorting rules.
+ - Please use the strings C(ASCENDING) and C(DESCENDING) to set the order.
+ - Check the example for more information.
+ type: list
+ elements: list
+ default: []
+ extra_connection_parameters:
+ description:
+ - Extra connection parameters that to be sent to pymongo.MongoClient
+ - Check the example to see how to connect to mongo using an SSL certificate.
+ - "All possible parameters are here: U(https://api.mongodb.com/python/current/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient)"
+ type: dict
+ default: {}
+notes:
+ - "Please check https://api.mongodb.org/python/current/api/pymongo/collection.html?highlight=find#pymongo.collection.Collection.find for more details."
+requirements:
+ - pymongo >= 2.4 (python library)
+'''
+
+EXAMPLES = '''
+- hosts: localhost
+ gather_facts: false
+ vars:
+ mongodb_parameters:
+ #mandatory parameters
+ database: 'local'
+ collection: "startup_log"
+ #optional
+ connection_string: "mongodb://localhost/"
+ # connection_string: "mongodb://username:password@my.server.com:27017/"
+ # extra_connection_parameters: { "ssl" : True , "ssl_certfile": /etc/self_signed_certificate.pem" }
+ #optional query parameters, we accept any parameter from the normal mongodb query.
+ # filter: { "hostname": "u18" }
+ projection: { "pid": True , "_id" : False , "hostname" : True }
+ skip: 0
+ limit: 1
+ sort: [ [ "startTime" , "ASCENDING" ] , [ "age", "DESCENDING" ] ]
+ tasks:
+ - debug: msg="The PID from MongoDB is {{ lookup('mongodb', mongodb_parameters ).pid }}"
+
+ - debug: msg="The HostName from the MongoDB server is {{ lookup('mongodb', mongodb_parameters ).hostname }}"
+
+ - debug: msg="Mongo DB is stored at {{ lookup('mongodb', mongodb_parameters_inline )}}"
+ vars:
+ mongodb_parameters_inline:
+ database: 'local'
+ collection: "startup_log"
+ connection_string: "mongodb://localhost/"
+ limit: 1
+ projection: { "cmdline.storage": True }
+
+ # lookup syntax, does the same as below
+ - debug: msg="The hostname is {{ item.hostname }} and the pid is {{ item.pid }}"
+ loop: "{{ lookup('mongodb', mongodb_parameters, wantlist=True) }}"
+
+ # query syntax, does the same as above
+ - debug: msg="The hostname is {{ item.hostname }} and the pid is {{ item.pid }}"
+ loop: "{{ query('mongodb', mongodb_parameters) }}"
+
+ - name: "Raw output from the mongodb lookup (a json with pid and hostname )"
+ debug: msg="{{ lookup('mongodb', mongodb_parameters) }}"
+
+ - name: "Yet another mongodb query, now with the parameters on the task itself"
+ debug: msg="pid={{item.pid}} hostname={{item.hostname}} version={{ item.buildinfo.version }}"
+ with_mongodb:
+ - database: 'local'
+ collection: "startup_log"
+ connection_string: "mongodb://localhost/"
+ limit: 1
+ projection: { "pid": True , "hostname": True , "buildinfo.version": True }
+
+ # Please notice this specific query may result more than one result. This is expected
+ - name: "Shows the whole output from mongodb"
+ debug: msg="{{ item }}"
+ with_mongodb:
+ - database: 'local'
+ collection: "startup_log"
+ connection_string: "mongodb://localhost/"
+
+
+'''
+
+RETURN = """
+ _list_of_jsons:
+ description:
+ - a list of JSONs with the results of the MongoDB query.
+ type: list
+"""
+
+import datetime
+
+from ansible.module_utils.six import string_types, integer_types
+from ansible.module_utils._text import to_native
+from ansible.errors import AnsibleError
+from ansible.plugins.lookup import LookupBase
+
+try:
+ from pymongo import ASCENDING, DESCENDING
+ from pymongo.errors import ConnectionFailure
+ from pymongo import MongoClient
+except ImportError:
+ try: # for older PyMongo 2.2
+ from pymongo import Connection as MongoClient
+ except ImportError:
+ pymongo_found = False
+ else:
+ pymongo_found = True
+else:
+ pymongo_found = True
+
+
+class LookupModule(LookupBase):
+
+ def _fix_sort_parameter(self, sort_parameter):
+ if sort_parameter is None:
+ return sort_parameter
+
+ if not isinstance(sort_parameter, list):
+ raise AnsibleError(u"Error. Sort parameters must be a list, not [ {0} ]".format(sort_parameter))
+
+ for item in sort_parameter:
+ self._convert_sort_string_to_constant(item)
+
+ return sort_parameter
+
+ def _convert_sort_string_to_constant(self, item):
+ original_sort_order = item[1]
+ sort_order = original_sort_order.upper()
+ if sort_order == u"ASCENDING":
+ item[1] = ASCENDING
+ elif sort_order == u"DESCENDING":
+ item[1] = DESCENDING
+ # else the user knows what s/he is doing and we won't predict. PyMongo will return an error if necessary
+
+ def convert_mongo_result_to_valid_json(self, result):
+ if result is None:
+ return result
+ if isinstance(result, integer_types + (float, bool)):
+ return result
+ if isinstance(result, string_types):
+ return result
+ elif isinstance(result, list):
+ new_list = []
+ for elem in result:
+ new_list.append(self.convert_mongo_result_to_valid_json(elem))
+ return new_list
+ elif isinstance(result, dict):
+ new_dict = {}
+ for key in result.keys():
+ value = result[key] # python2 and 3 compatible....
+ new_dict[key] = self.convert_mongo_result_to_valid_json(value)
+ return new_dict
+ elif isinstance(result, datetime.datetime):
+ # epoch
+ return (result - datetime.datetime(1970, 1, 1)). total_seconds()
+ else:
+ # failsafe
+ return u"{0}".format(result)
+
+ def run(self, terms, variables, **kwargs):
+ try:
+ return self._run_helper(terms)
+ except Exception as e:
+ print(u"There was an exception on the mongodb_lookup: {0}".format(to_native(e)))
+ raise e
+
+ def _run_helper(self, terms):
+ if not pymongo_found:
+ raise AnsibleError(u"pymongo is required in the control node (this machine) for mongodb lookup.")
+ ret = []
+ for term in terms:
+ for required_parameter in [u"database", u"collection"]:
+ if required_parameter not in term:
+ raise AnsibleError(u"missing mandatory parameter [{0}]".format(required_parameter))
+
+ connection_string = term.get(u'connection_string', u"mongodb://localhost")
+ database = term[u"database"]
+ collection = term[u'collection']
+ extra_connection_parameters = term.get(u'extra_connection_parameters', {})
+
+ if u"extra_connection_parameters" in term:
+ del term[u"extra_connection_parameters"]
+ if u"connection_string" in term:
+ del term[u"connection_string"]
+ del term[u"database"]
+ del term[u"collection"]
+
+ if u"sort" in term:
+ term[u"sort"] = self._fix_sort_parameter(term[u"sort"])
+
+ # all other parameters are sent to mongo, so we are future and past proof
+
+ try:
+ client = MongoClient(connection_string, **extra_connection_parameters)
+ results = client[database][collection].find(**term)
+
+ for result in results:
+ result = self.convert_mongo_result_to_valid_json(result)
+ ret.append(result)
+
+ except ConnectionFailure as e:
+ raise AnsibleError(u'unable to connect to database: %s' % str(e))
+
+ return ret
diff --git a/ansible_collections/community/mongodb/plugins/module_utils/__init__.py b/ansible_collections/community/mongodb/plugins/module_utils/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/module_utils/__init__.py
diff --git a/ansible_collections/community/mongodb/plugins/module_utils/mongodb_common.py b/ansible_collections/community/mongodb/plugins/module_utils/mongodb_common.py
new file mode 100644
index 000000000..e1ab27293
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/module_utils/mongodb_common.py
@@ -0,0 +1,482 @@
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+from ansible.module_utils.basic import missing_required_lib # pylint: disable=unused-import:
+from ansible.module_utils.six.moves import configparser
+from ansible.module_utils._text import to_native
+import traceback
+import os
+import ssl as ssl_lib
+
+
+try:
+ from bson.timestamp import Timestamp
+ from bson import ObjectId
+except ImportError:
+ pass # TODO Should we do something here or are we covered by pymongo?
+
+MongoClient = None
+PYMONGO_IMP_ERR = None
+pymongo_found = None
+PyMongoVersion = None
+ConnectionFailure = None
+OperationFailure = None
+TYPES_NEED_TO_CONVERT = None
+
+try:
+ from pymongo.errors import ConnectionFailure # pylint: disable=unused-import:
+ from pymongo.errors import OperationFailure # pylint: disable=unused-import:
+ from pymongo import version as PyMongoVersion
+ from pymongo import MongoClient
+ pymongo_found = True
+except ImportError:
+ PYMONGO_IMP_ERR = traceback.format_exc()
+ pymongo_found = False
+
+try:
+ TYPES_NEED_TO_CONVERT = (Timestamp, ObjectId)
+except NameError:
+ pass # sanity tests
+
+
+def check_compatibility(module, srv_version, driver_version):
+ if int(driver_version[0]) >= 4:
+ if int(srv_version[0]) < 4:
+ if module.params['strict_compatibility']:
+ module.fail_json(msg="This version of MongoDB is pretty old and these modules are no longer tested against this version.")
+ else:
+ module.warn("This version of MongoDB is pretty old and these modules are no longer tested against this version.")
+ else:
+ if module.params['strict_compatibility']:
+ module.fail_json(msg="You must use pymongo 4+.")
+ else:
+ module.warn("You must use pymongo 4+.")
+
+
+def load_mongocnf():
+ config = configparser.RawConfigParser()
+ mongocnf = os.path.expanduser('~/.mongodb.cnf')
+
+ try:
+ config.readfp(open(mongocnf))
+ except (configparser.NoOptionError, IOError):
+ return False
+
+ creds = dict(
+ user=config.get('client', 'user'),
+ password=config.get('client', 'pass')
+ )
+
+ return creds
+
+
+def index_exists(client, database, collection, index_name):
+ """
+ Returns true if an index on the collection exists with the given name
+ @client: MongoDB connection.
+ @database: MongoDB Database.
+ @collection: MongoDB collection.
+ @index_name: The index name.
+ """
+ exists = False
+ indexes = client[database][collection].list_indexes()
+ for index in indexes:
+ if index["name"] == index_name:
+ exists = True
+ return exists
+
+
+def create_index(client, database, collection, keys, options):
+ """
+ Creates an index on the given collection
+ @client: MongoDB connection.
+ @database: MongoDB Database - str.
+ @collection: MongoDB collection - str.
+ @keys: Specification of index - dict.
+ """
+ client[database][collection].create_index(list(keys.items()),
+ **options)
+
+
+def drop_index(client, database, collection, index_name):
+ client[database][collection].drop_index(index_name)
+
+
+def member_state(client):
+ """Check if a replicaset exists.
+
+ Args:
+ client (cursor): Mongodb cursor on admin database.
+
+ Returns:
+ str: member state i.e. PRIMARY, SECONDARY
+ """
+ state = None
+ doc = client['admin'].command('replSetGetStatus')
+ for member in doc["members"]:
+ if "self" in member.keys():
+ state = str(member['stateStr'])
+ return state
+
+
+def mongodb_common_argument_spec(ssl_options=True):
+ """
+ Returns a dict containing common options shared across the MongoDB modules.
+ """
+ options = dict(
+ login_user=dict(type='str', required=False),
+ login_password=dict(type='str', required=False, no_log=True),
+ login_database=dict(type='str', required=False, default='admin'),
+ login_host=dict(type='str', required=False, default='localhost'),
+ login_port=dict(type='int', required=False, default=27017),
+ strict_compatibility=dict(type='bool', default=True),
+ atlas_auth=dict(type='bool', default=False),
+ )
+ ssl_options_dict = dict(
+ ssl=dict(type='bool', required=False, default=False, aliases=['tls']),
+ ssl_cert_reqs=dict(type='str',
+ required=False,
+ default='CERT_REQUIRED',
+ choices=['CERT_NONE',
+ 'CERT_OPTIONAL',
+ 'CERT_REQUIRED'],
+ aliases=['tlsAllowInvalidCertificates']),
+ ssl_ca_certs=dict(type='str', default=None, aliases=['tlsCAFile']),
+ ssl_crlfile=dict(type='str', default=None),
+ ssl_certfile=dict(type='str', default=None, aliases=['tlsCertificateKeyFile']),
+ ssl_keyfile=dict(type='str', default=None, no_log=True),
+ ssl_pem_passphrase=dict(type='str', default=None, no_log=True, aliases=['tlsCertificateKeyFilePassword']),
+ auth_mechanism=dict(type='str',
+ required=False,
+ default=None,
+ choices=['SCRAM-SHA-256',
+ 'SCRAM-SHA-1',
+ 'MONGODB-X509',
+ 'GSSAPI',
+ 'PLAIN']),
+ connection_options=dict(type='list',
+ elements='raw',
+ default=None)
+ )
+ if ssl_options:
+ options.update(ssl_options_dict)
+ return options
+
+
+def rename_ssl_option_for_pymongo4(connection_options):
+ """
+ This function renames the old ssl parameter, and sorts the data out,
+ when the driver use is >= PyMongo 4
+ """
+ if int(PyMongoVersion[0]) >= 4:
+ if connection_options.get('ssl_cert_reqs', None) == 'CERT_NONE':
+ connection_options['tlsAllowInvalidCertificates'] = False
+ elif connection_options.get('ssl_cert_reqs', None) == 'CERT_REQUIRED':
+ connection_options['tlsAllowInvalidCertificates'] = False
+ connection_options.pop('ssl_cert_reqs', None)
+ if connection_options.get('ssl_ca_certs', None) is not None:
+ connection_options['tlsCAFile'] = connection_options['ssl_ca_certs']
+ connection_options.pop('ssl_ca_certs', None)
+ connection_options.pop('ssl_crlfile', None)
+ if connection_options.get('ssl_certfile', None) is not None:
+ connection_options['tlsCertificateKeyFile'] = connection_options['ssl_certfile']
+ elif connection_options.get('ssl_keyfile', None) is not None:
+ connection_options['tlsCertificateKeyFile'] = connection_options['ssl_keyfile']
+ connection_options.pop('ssl_certfile', None)
+ connection_options.pop('ssl_keyfile', None)
+ if connection_options.get('ssl_pem_passphrase', None) is not None:
+ connection_options['tlsCertificateKeyFilePassword'] = connection_options['ssl_pem_passphrase']
+ connection_options.pop('ssl_pem_passphrase', None)
+ return connection_options
+
+
+def add_option_if_not_none(param_name, module, connection_params):
+ '''
+ @param_name - The parameter name to check
+ @module - The ansible module object
+ @connection_params - Dict containing the connection params
+ '''
+ if module.params[param_name] is not None:
+ connection_params[param_name] = module.params[param_name]
+ return connection_params
+
+
+def ssl_connection_options(connection_params, module):
+ connection_params['ssl'] = True
+ if module.params['ssl_cert_reqs'] is not None:
+ connection_params['ssl_cert_reqs'] = getattr(ssl_lib, module.params['ssl_cert_reqs'])
+ connection_params = add_option_if_not_none('ssl_ca_certs', module, connection_params)
+ connection_params = add_option_if_not_none('ssl_crlfile', module, connection_params)
+ connection_params = add_option_if_not_none('ssl_certfile', module, connection_params)
+ connection_params = add_option_if_not_none('ssl_keyfile', module, connection_params)
+ connection_params = add_option_if_not_none('ssl_pem_passphrase', module, connection_params)
+ if module.params['auth_mechanism'] is not None:
+ connection_params['authMechanism'] = module.params['auth_mechanism']
+ if module.params['connection_options'] is not None:
+ for item in module.params['connection_options']:
+ if isinstance(item, dict):
+ for key, value in item.items():
+ connection_params[key] = value
+ elif isinstance(item, str) and "=" in item:
+ connection_params[item.split('=')[0]] = item.split('=')[1]
+ else:
+ raise ValueError("Invalid value supplied in connection_options: {0} .".format(str(item)))
+ return connection_params
+
+
+def check_srv_version(module, client):
+ srv_version = None
+ try:
+ srv_version = client.server_info()['version']
+ except Exception as excep:
+ module.fail_json(msg='Unable to get MongoDB server version: %s' % to_native(excep))
+ return srv_version
+
+
+def check_driver_compatibility(module, client, srv_version):
+ try:
+ # Get driver version::
+ driver_version = PyMongoVersion
+ # Check driver and server version compatibility:
+ check_compatibility(module, srv_version, driver_version)
+ except Exception as excep:
+ module.fail_json(msg='Unable to check driver compatibility: %s' % to_native(excep))
+
+
+def get_mongodb_client(module, login_user=None, login_password=None, login_database=None, directConnection=False):
+ """
+ Build the connection params dict and returns a MongoDB Client object
+ """
+ connection_params = {
+ 'host': module.params['login_host'],
+ 'port': module.params['login_port'],
+ }
+
+ if directConnection:
+ connection_params['directConnection'] = True
+ if module.params['ssl']:
+ connection_params = ssl_connection_options(connection_params, module)
+ connection_params = rename_ssl_option_for_pymongo4(connection_params)
+ # param exists only in some modules
+ if 'replica_set' in module.params and 'reconfigure' not in module.params:
+ connection_params["replicaset"] = module.params['replica_set']
+ elif 'replica_set' in module.params and 'reconfigure' in module.params \
+ and module.params['reconfigure']:
+ connection_params["replicaset"] = module.params['replica_set']
+ if login_user:
+ connection_params['username'] = login_user
+ connection_params['password'] = login_password
+ connection_params['authSource'] = login_database
+ client = MongoClient(**connection_params)
+ return client
+
+
+def is_auth_enabled(module):
+ """
+ Returns True if auth is enabled on the mongo instance
+ For PyMongo 4+ we have to connect directly to the instance
+ rather than the replicaset
+ """
+ auth_is_enabled = None
+ connection_params = {}
+ connection_params['host'] = module.params['login_host']
+ connection_params['port'] = module.params['login_port']
+ connection_params['directConnection'] = True # Need to do this for 3.12.* as well
+ if int(PyMongoVersion[0]) >= 4: # we need to connect directly to the instance
+ connection_params['directConnection'] = True
+ else:
+ if 'replica_set' in module.params and module.params['replica_set'] is not None:
+ connection_params['replicaset'] = module.params['replica_set']
+ if module.params['ssl']:
+ connection_params = ssl_connection_options(connection_params, module)
+ connection_params = rename_ssl_option_for_pymongo4(connection_params)
+ try:
+ myclient = MongoClient(**connection_params)
+ myclient['admin'].command('listDatabases', 1.0)
+ auth_is_enabled = False
+ except Exception as excep:
+ if hasattr(excep, 'code') and excep.code in [13]:
+ auth_is_enabled = True
+ if auth_is_enabled is None: # if this is still none we have a problem
+ module.fail_json(msg='Unable to determine if auth is enabled: {0}'.format(traceback.format_exc()))
+ finally:
+ myclient.close()
+ return auth_is_enabled
+
+
+def mongo_auth(module, client, directConnection=False):
+ """
+ TODO: This function was extracted from code from the mongodb_replicaset module.
+ We should refactor other modules to use this where appropriate. - DONE?
+ @module - The calling Ansible module
+ @client - The MongoDB connection object
+ """
+ login_user = module.params['login_user']
+ login_password = module.params['login_password']
+ login_database = module.params['login_database']
+
+ atlas_auth = module.params['atlas_auth']
+
+ fail_msg = None # Our test code had issues with multiple exit points with fail_json
+
+ crypt_flag = 'ssl'
+ if 'tls' in module.params:
+ crypt_flag = 'tls'
+
+ if not atlas_auth:
+
+ if login_user is None and login_password is None:
+ mongocnf_creds = load_mongocnf()
+ if mongocnf_creds is not False:
+ login_user = mongocnf_creds['user']
+ login_password = mongocnf_creds['password']
+ elif not all([login_user, login_password]) and module.params[crypt_flag] is False:
+ fail_msg = "When supplying login arguments, both 'login_user' and 'login_password' must be provided"
+
+ if 'create_for_localhost_exception' not in module.params and fail_msg is None:
+ try:
+ if is_auth_enabled(module):
+ if login_user is not None and login_password is not None:
+ client = get_mongodb_client(module, login_user, login_password, login_database, directConnection=directConnection)
+ else:
+ fail_msg = 'No credentials to authenticate'
+ except Exception as excep:
+ fail_msg = 'unable to connect to database: %s' % to_native(excep)
+ # Get server version:
+ if fail_msg is None:
+ srv_version = check_srv_version(module, client)
+ check_driver_compatibility(module, client, srv_version)
+ elif fail_msg is None: # this is the mongodb_user module
+ if login_user is not None and login_password is not None:
+ client = get_mongodb_client(module, login_user, login_password, login_database, directConnection=directConnection)
+ # Get server version:
+ srv_version = check_srv_version(module, client)
+ check_driver_compatibility(module, client, srv_version)
+ elif module.params['strict_compatibility'] is False:
+ if module.params['database'] not in ["admin", "$external"]:
+ fail_msg = 'The localhost login exception only allows the first admin account to be created'
+ # else: this has to be the first admin user added
+ if fail_msg:
+ module.fail_json(msg=fail_msg)
+ else: # Atlas auth path
+ if 'create_for_localhost_exception' not in module.params and fail_msg is None:
+ try:
+ if login_user is not None and login_password is not None:
+ # pymongo >= 4. There's no authenticate method in pymongo 4.0. Recreate the connection object
+ client = get_mongodb_client(module, login_user, login_password, login_database)
+ else:
+ fail_msg = 'No credentials to authenticate'
+ except Exception as excep:
+ fail_msg = 'unable to connect to database: %s' % to_native(excep)
+ elif fail_msg is None: # this is the mongodb_user module
+ if login_user is not None and login_password is not None:
+ client = get_mongodb_client(module, login_user, login_password, login_database, directConnection=False)
+ # Get server version:
+ srv_version = check_srv_version(module, client)
+ check_driver_compatibility(module, client, srv_version)
+ elif module.params['strict_compatibility'] is False:
+ if module.params['database'] not in ["admin", "$external"]:
+ fail_msg = 'The localhost login exception only allows the first admin account to be created'
+ # else: this has to be the first admin user added
+ if fail_msg:
+ module.fail_json(msg=fail_msg)
+ return client
+
+
+def member_dicts_different(conf, member_config):
+ '''
+ Returns if there is a difference in the replicaset configuration that we care about
+ @con - The current MongoDB Replicaset configure document
+ @member_config - The member dict config provided by the module. List of dicts
+ '''
+ current_member_config = conf['members']
+ member_config_defaults = {
+ "arbiterOnly": False,
+ "buildIndexes": True,
+ "hidden": False,
+ "priority": {"nonarbiter": 1.0, "arbiter": 0},
+ "tags": {},
+ "secondardDelaySecs": 0,
+ "votes": 1
+ }
+ different = False
+ msg = "None"
+ current_member_hosts = []
+ for member in current_member_config:
+ current_member_hosts.append(member['host'])
+ member_config_hosts = []
+ for member in member_config:
+ if ':' not in member['host']: # no port supplied
+ member_config_hosts.append(member['host'] + ":27017")
+ else:
+ member_config_hosts.append(member['host'])
+ if sorted(current_member_hosts) != sorted(member_config_hosts): # compare if members are the same
+ different = True
+ msg = "hosts different"
+ else: # Compare dict key to see if votes, tags etc have changed. We also default value if key is not specified
+ for host in current_member_hosts:
+ member_index = next((index for (index, d) in enumerate(current_member_config) if d["host"] == host), None)
+ new_member_index = next((index for (index, d) in enumerate(member_config) if d["host"] == host), None)
+ for config_item in member_config_defaults:
+ if config_item != "priority":
+ if current_member_config[member_index].get(config_item, member_config_defaults[config_item]) != \
+ member_config[new_member_index].get(config_item, member_config_defaults[config_item]):
+ different = True
+ msg = "var different {0} {1} {2}".format(config_item,
+ current_member_config[member_index].get(config_item, member_config_defaults[config_item]),
+ member_config[new_member_index].get(config_item, member_config_defaults[config_item]))
+ break
+ else: # priority a special case
+ role = "nonarbiter"
+ if current_member_config[member_index]["arbiterOnly"]:
+ role = "arbiter"
+ if current_member_config[member_index][config_item] != \
+ member_config[new_member_index].get(config_item, member_config_defaults[config_item][role]):
+ different = True
+ msg = "var different {0}".format(config_item)
+ break
+ else: # for case when the member is not an arbiter
+ if current_member_config[member_index]["priority"] != \
+ member_config[new_member_index].get(config_item, 1.0):
+ different = True
+ msg = "var different {0}".format(config_item)
+ break
+ return different # , msg
+
+
+def lists_are_different(list1, list2):
+ diff = False
+ if sorted(list1) != sorted(list2):
+ diff = True
+ return diff
+
+
+# Taken from https://github.com/ansible-collections/community.postgresql/blob/main/plugins/module_utils/postgres.py#L420
+def convert_to_supported(val):
+ """Convert unsupported type to appropriate.
+ Args:
+ val (any) -- Any value fetched from database.
+ Returns value of appropriate type.
+ """
+ if isinstance(val, Timestamp):
+ return str(val)
+ elif isinstance(val, ObjectId):
+ return str(val)
+
+ return val # By default returns the same value
+
+
+def convert_bson_values_recur(mydict):
+ """
+ Converts values that Ansible doesn't like
+ # https://github.com/ansible-collections/community.mongodb/issues/462
+ """
+ if isinstance(mydict, dict):
+ for key, value in mydict.items():
+ if isinstance(value, dict):
+ mydict[key] = convert_bson_values_recur(value)
+ else:
+ if isinstance(value, TYPES_NEED_TO_CONVERT):
+ mydict[key] = convert_to_supported(value)
+ else:
+ mydict[key] = value
+ return mydict
diff --git a/ansible_collections/community/mongodb/plugins/module_utils/mongodb_shell.py b/ansible_collections/community/mongodb/plugins/module_utils/mongodb_shell.py
new file mode 100644
index 000000000..5108757c8
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/module_utils/mongodb_shell.py
@@ -0,0 +1,135 @@
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import shlex
+import pipes
+import re
+import json
+import os
+
+
+def escape_param(param):
+ '''
+ Escapes the given parameter
+ @param - The parameter to escape
+ '''
+ escaped = None
+ if hasattr(shlex, 'quote'):
+ escaped = shlex.quote(param)
+ elif hasattr(pipes, 'quote'):
+ escaped = pipes.quote(param)
+ else:
+ escaped = "'" + param.replace("'", "'\\''") + "'"
+ return escaped
+
+
+def add_arg_to_cmd(cmd_list, param_name, param_value, is_bool=False, omit=None):
+ """
+ @cmd_list - List of cmd args.
+ @param_name - Param name / flag.
+ @param_value - Value of the parameter.
+ @is_bool - Flag is a boolean and has no value.
+ @omit - List of parameter to omit from the command line.
+ """
+ if param_name.replace('-', '') not in omit:
+ if is_bool is False and param_value is not None:
+ cmd_list.append(param_name)
+ if param_name == "--eval":
+ cmd_list.append("{0}".format(escape_param(param_value)))
+ else:
+ cmd_list.append(param_value)
+ elif is_bool is True:
+ cmd_list.append(param_name)
+ return cmd_list
+
+
+def extract_json_document(output):
+ """
+ This is for specific type of mongo shell return data in the format SomeText()
+ https://github.com/ansible-collections/community.mongodb/issues/436
+ i.e.
+
+ """
+ output = output.strip()
+ if re.match(r"^[a-zA-Z].*\(", output) and output.endswith(')'):
+ first_bracket = output.find('{')
+ last_bracket = output.rfind('}')
+ if first_bracket > 0 and last_bracket > 0:
+ tmp = output[first_bracket:last_bracket + 1]
+ tmp = tmp.replace('\n', '')
+ tmp = tmp.replace('\t', '')
+ if tmp is not None:
+ output = tmp
+ return output
+
+
+def transform_output(output, transform_type, split_char):
+ output = extract_json_document(output)
+ if transform_type == "auto": # determine what transform_type to perform
+ if output.strip().startswith("{") or output.strip().startswith("["):
+ transform_type = "json"
+ elif isinstance(output.strip().split(None), list): # Splits on whitespace
+ transform_type = "split"
+ split_char = None
+ elif isinstance(output.strip().split(","), list):
+ transform_type = "split"
+ split_char = ","
+ elif isinstance(output.strip().split(" "), list):
+ transform_type = "split"
+ split_char = " "
+ elif isinstance(output.strip().split("|"), list):
+ transform_type = "split"
+ split_char = "|"
+ elif isinstance(output.strip().split("\t"), list):
+ transform_type = "split"
+ split_char = "\t"
+ else:
+ transform_type = "raw"
+ if transform_type == "json":
+ try:
+ output = json.loads(output)
+ except json.decoder.JSONDecodeError:
+ # Strip Extended JSON stuff like:
+ # "_id": ObjectId("58f56171ee9d4bd5e610d6b7"),
+ # "count": NumberLong(999),
+ output = re.sub(r'\:\s*\S+\s*\(\s*(\S+)\s*\)', r':\1', output)
+ try:
+ output = json.dumps(output, separators=(',', ':'))
+ doc = json.loads(output)
+ except json.decoder.JSONDecodeError as excep:
+ raise excep
+ elif transform_type == "split":
+ output = output.strip().split(split_char)
+ elif transform_type == "raw":
+ output = output.strip()
+ return output
+
+
+def get_hash_value(module):
+ '''
+ Returns the hash value of either the provided file or eval command
+ '''
+ hash_value = None
+ try:
+ import hashlib
+ except ImportError as excep:
+ module.fail_json(msg="Unable to import hashlib: {0}".format(excep.message))
+ if module.params['file'] is not None:
+ hash_value = hashlib.md5(module.params['file'].encode('utf-8')).hexdigest()
+ else:
+ hash_value = hashlib.md5(module.params['eval'].encode('utf-8')).hexdigest()
+ return hash_value
+
+
+def touch(fname, times=None):
+ with open(fname, 'a'):
+ os.utime(fname, times)
+
+
+def detect_if_cmd_exist(cmd="mongosh"):
+ path = os.getenv('PATH')
+ for folder in path.split(os.path.pathsep):
+ mongoCmd = os.path.join(folder, cmd)
+ if os.path.exists(mongoCmd) and os.access(mongoCmd, os.X_OK):
+ return True
+ return False
diff --git a/ansible_collections/community/mongodb/plugins/modules/__init__.py b/ansible_collections/community/mongodb/plugins/modules/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/modules/__init__.py
diff --git a/ansible_collections/community/mongodb/plugins/modules/mongodb_balancer.py b/ansible_collections/community/mongodb/plugins/modules/mongodb_balancer.py
new file mode 100644
index 000000000..0e9b33a34
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/modules/mongodb_balancer.py
@@ -0,0 +1,452 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2020, 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_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()
diff --git a/ansible_collections/community/mongodb/plugins/modules/mongodb_index.py b/ansible_collections/community/mongodb/plugins/modules/mongodb_index.py
new file mode 100644
index 000000000..ee69c093b
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/modules/mongodb_index.py
@@ -0,0 +1,405 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Rhys Campbell (@rhysmeister) <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_index
+
+short_description: Creates or drops indexes on MongoDB collections.
+
+description:
+ - Creates or drops indexes on MongoDB collections.
+ - Supports multiple index options, i.e. unique, sparse and partial.
+ - Validates existence of indexes by name only.
+
+author: Rhys Campbell (@rhysmeister)
+version_added: "1.0.0"
+
+extends_documentation_fragment:
+ - community.mongodb.login_options
+ - community.mongodb.ssl_options
+
+options:
+ indexes:
+ description:
+ - List of indexes to create or drop
+ type: list
+ elements: raw
+ required: yes
+ replica_set:
+ description:
+ - Replica set to connect to (automatically connects to primary for writes).
+ type: str
+notes:
+ - Requires the pymongo Python package on the remote host, version 2.4.2+.
+
+requirements:
+ - pymongo
+'''
+
+EXAMPLES = r'''
+- name: Create a single index on a collection
+ community.mongodb.mongodb_index:
+ login_user: admin
+ login_password: secret
+ indexes:
+ - database: mydb
+ collection: test
+ keys:
+ - username: 1
+ last_login: -1
+ options:
+ name: myindex
+ state: present
+
+- name: Drop an index on a collection
+ community.mongodb.mongodb_index:
+ login_user: admin
+ login_password: secret
+ indexes:
+ - database: mydb
+ collection: test
+ options:
+ name: myindex
+ state: absent
+
+- name: Create multiple indexes
+ community.mongodb.mongodb_index:
+ login_user: admin
+ login_password: secret
+ indexes:
+ - database: mydb
+ collection: test
+ keys:
+ - username: 1
+ last_login: -1
+ options:
+ name: myindex
+ state: present
+ - database: mydb
+ collection: test
+ keys:
+ - email: 1
+ last_login: -1
+ options:
+ name: myindex2
+ state: present
+
+- name: Add a unique index
+ community.mongodb.mongodb_index:
+ login_port: 27017
+ login_user: admin
+ login_password: secret
+ login_database: "admin"
+ indexes:
+ - database: "test"
+ collection: "rhys"
+ keys:
+ username: 1
+ options:
+ name: myuniqueindex
+ unique: true
+ state: present
+
+- name: Add a ttl index
+ community.mongodb.mongodb_index:
+ login_port: 27017
+ login_user: admin
+ login_password: secret
+ login_database: "admin"
+ indexes:
+ - database: "test"
+ collection: "rhys"
+ keys:
+ created: 1
+ options:
+ name: myttlindex
+ expireAfterSeconds: 3600
+ state: present
+
+- name: Add a sparse index
+ community.mongodb.mongodb_index:
+ login_port: 27017
+ login_user: admin
+ login_password: secret
+ login_database: "admin"
+ indexes:
+ - database: "test"
+ collection: "rhys"
+ keys:
+ last_login: -1
+ options:
+ name: mysparseindex
+ sparse: true
+ state: present
+
+- name: Add a partial index
+ community.mongodb.mongodb_index:
+ login_port: 27017
+ login_user: admin
+ login_password: secret
+ login_database: "admin"
+ indexes:
+ - database: "test"
+ collection: "rhys"
+ keys:
+ last_login: -1
+ options:
+ name: mypartialindex
+ partialFilterExpression:
+ rating:
+ $gt: 5
+ state: present
+
+- name: Add a index in the background (background option is deprecated from 4.2+)
+ community.mongodb.mongodb_index:
+ login_port: 27017
+ login_user: admin
+ login_password: secret
+ login_database: "admin"
+ indexes:
+ - database: "test"
+ collection: "rhys"
+ options:
+ name: idxbackground
+ keys:
+ username: -1
+ backgroud: true
+ state: present
+
+- name: Check creating 5 index all with multiple options specified
+ community.mongodb.mongodb_index:
+ login_port: 27017
+ login_user: admin
+ login_password: secret
+ login_database: "admin"
+ indexes:
+ - database: "test"
+ collection: "indextest"
+ options:
+ name: "idx_unq_username"
+ unique: true
+ keys:
+ username: -1
+ state: present
+ - database: "test"
+ collection: "indextest"
+ options:
+ name: "idx_last_login"
+ sparse: true
+ keys:
+ last_login: -1
+ state: present
+ - database: "test"
+ collection: "indextest"
+ options:
+ name: "myindex"
+ keys:
+ first_name: 1
+ last_name: -1
+ city: 1
+ state: present
+ - database: "test"
+ collection: partialtest
+ options:
+ name: "idx_partialtest"
+ partialFilterExpression:
+ rating:
+ $gt: 5
+ keys:
+ rating: -1
+ title: 1
+ state: present
+ - database: "test"
+ collection: "wideindex"
+ options:
+ name: "mywideindex"
+ keys:
+ email: -1
+ username: 1
+ first_name: 1
+ last_name: 1
+ dob: -1
+ city: 1
+ last_login: -1
+ review_count: 1
+ rating_count: 1
+ last_post: -1
+ state: present
+'''
+
+RETURN = r'''
+indexes_created:
+ description: List of indexes created.
+ returned: always
+ type: list
+ sample: ["myindex", "myindex2"]
+indexes_dropped:
+ description: List of indexes dropped.
+ returned: always
+ type: list
+ sample: ["myindex", "myindex2"]
+changed:
+ description: Indicates the module has changed something.
+ returned: When the module has changed something.
+ type: bool
+failed:
+ description: Indicates the module has failed.
+ returned: When the module has encountered an error.
+ type: bool
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
+ missing_required_lib,
+ mongodb_common_argument_spec,
+ PYMONGO_IMP_ERR,
+ pymongo_found,
+ index_exists,
+ create_index,
+ drop_index,
+ mongo_auth,
+ get_mongodb_client,
+)
+
+
+def validate_module(module):
+ '''
+ Runs validation rules specific the mongodb_index module
+ '''
+ required_index_keys = [
+ "database",
+ "collection",
+ "options",
+ "state",
+ ]
+ indexes = module.params['indexes']
+
+ if len(indexes) == 0:
+ module.fail_json(msg="One or more indexes must be specified")
+ if not all(isinstance(i, dict) for i in indexes):
+ module.fail_json(msg="Indexes must be supplied as dictionaries")
+
+ # Ensure keys are present in index spec
+ for k in required_index_keys:
+ for i in indexes:
+ if k not in i.keys():
+ module.fail_json(msg="Missing required index key {0}".format(k))
+
+ # Check index subkeys look correct
+ for i in indexes:
+ if not isinstance(i["database"], str):
+ module.fail_json(msg="database key should be str")
+ elif not isinstance(i["collection"], str):
+ module.fail_json(msg="collection key should be str")
+ elif i["state"] == "present" and "keys" not in i.keys():
+ module.fail_json(msg="keys must be supplied when state is present")
+ elif i["state"] == "present" and not isinstance(i["keys"], dict):
+ module.fail_json(msg="keys key should be dict")
+ elif not isinstance(i["options"], dict):
+ module.fail_json(msg="options key should be dict")
+ elif "name" not in i["options"]:
+ module.fail_json(msg="The options dict must contain a name field")
+ elif i["state"] not in ["present", "absent"]:
+ module.fail_json(msg="state must be one of present or absent")
+
+
+# ================
+# Module execution
+#
+def main():
+ argument_spec = mongodb_common_argument_spec()
+ argument_spec.update(
+ indexes=dict(type='list', elements='raw', required=True),
+ replica_set=dict(type='str'),
+ )
+ 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)
+
+ validate_module(module)
+
+ indexes = module.params['indexes']
+
+ client = get_mongodb_client(module)
+ client = mongo_auth(module, client)
+
+ # Pre flight checks done
+ indexes_created = []
+ indexes_dropped = []
+ changed = None
+ for i in indexes:
+ try:
+ idx = index_exists(client, i["database"], i["collection"], i["options"]["name"])
+ except Exception as excep:
+ module.fail_json(msg="Could not determine index status: {0}".format(str(excep)))
+ if module.check_mode:
+ if idx:
+ if i["state"] == "present":
+ changed = False
+ elif i["state"] == "absent":
+ indexes_dropped.append("{0}.{1}.{2}".format(i["database"],
+ i["collection"],
+ i["options"]["name"]))
+ changed = True
+ else:
+ if i["state"] == "present":
+ indexes_created.append("{0}.{1}.{2}".format(i["database"],
+ i["collection"],
+ i["options"]["name"]))
+ changed = True
+ elif i["state"] == "absent":
+ changed = False
+ else:
+ if idx:
+ if i["state"] == "present":
+ changed = False
+ elif i["state"] == "absent":
+ try:
+ drop_index(client, i["database"], i["collection"],
+ i["options"]["name"])
+ indexes_dropped.append("{0}.{1}.{2}".format(i["database"],
+ i["collection"],
+ i["options"]["name"]))
+ changed = True
+ except Exception as excep:
+ module.fail_json(msg="Error dropping index: {0}".format(str(excep)))
+
+ else:
+ if i["state"] == "present":
+ try:
+ create_index(client=client,
+ database=i["database"],
+ collection=i["collection"],
+ keys=i["keys"],
+ options=i["options"])
+ indexes_created.append("{0}.{1}.{2}".format(i["database"],
+ i["collection"],
+ i["options"]["name"]))
+ changed = True
+ except Exception as excep:
+ module.fail_json(msg="Error creating index: {0}".format(str(excep)))
+ elif i["state"] == "absent":
+ changed = False
+
+ module.exit_json(changed=changed,
+ indexes_created=indexes_created,
+ indexes_dropped=indexes_dropped)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/mongodb/plugins/modules/mongodb_info.py b/ansible_collections/community/mongodb/plugins/modules/mongodb_info.py
new file mode 100644
index 000000000..3341c870b
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/modules/mongodb_info.py
@@ -0,0 +1,315 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru>
+# 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_info
+
+short_description: Gather information about MongoDB instance.
+
+description:
+- Gather information about MongoDB instance.
+
+author: Andrew Klychkov (@Andersson007)
+version_added: "1.0.0"
+
+extends_documentation_fragment:
+ - community.mongodb.login_options
+ - community.mongodb.ssl_options
+
+options:
+ filter:
+ description:
+ - Limit the collected information by comma separated string or YAML list.
+ - Allowable values are C(general), C(databases), C(total_size), C(parameters), C(users), C(roles).
+ - By default, collects all subsets.
+ - You can use '!' before value (for example, C(!users)) to exclude it from the information.
+ - If you pass including and excluding values to the filter, for example, I(filter=!general,users),
+ the excluding values, C(!general) in this case, will be ignored.
+ required: no
+ type: list
+ elements: str
+
+notes:
+ - Requires the pymongo Python package on the remote host, version 2.4.2+.
+
+requirements:
+ - pymongo
+'''
+
+EXAMPLES = r'''
+- name: Gather all supported information
+ community.mongodb.mongodb_info:
+ login_user: admin
+ login_password: secret
+ register: result
+
+- name: Show gathered info
+ debug:
+ msg: '{{ result }}'
+
+- name: Gather only information about databases and their total size
+ community.mongodb.mongodb_info:
+ login_user: admin
+ login_password: secret
+ filter: databases, total_size
+
+- name: Gather all information except parameters
+ community.mongodb.mongodb_info:
+ login_user: admin
+ login_password: secret
+ filter: '!parameters'
+'''
+
+RETURN = r'''
+general:
+ description: General instance information.
+ returned: always
+ type: dict
+ sample: {"allocator": "tcmalloc", "bits": 64, "storageEngines": ["biggie"], "version": "4.2.3", "maxBsonObjectSize": 16777216}
+databases:
+ description: Database information.
+ returned: always
+ type: dict
+ sample: {"admin": {"empty": false, "sizeOnDisk": 245760}, "config": {"empty": false, "sizeOnDisk": 110592}}
+total_size:
+ description: Total size of all databases in bytes.
+ returned: always
+ type: int
+ sample: 397312
+users:
+ description: User information.
+ returned: always
+ type: dict
+ sample: { "db": {"new_user": {"_id": "config.new_user", "mechanisms": ["SCRAM-SHA-1", "SCRAM-SHA-256"], "roles": []}}}
+roles:
+ description: Role information.
+ returned: always
+ type: dict
+ sample: { "db": {"restore": {"inheritedRoles": [], "isBuiltin": true, "roles": []}}}
+parameters:
+ description: Server parameters information.
+ returned: always
+ type: dict
+ sample: {"maxOplogTruncationPointsAfterStartup": 100, "maxOplogTruncationPointsDuringStartup": 100, "maxSessions": 1000000}
+'''
+
+from uuid import UUID
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils._text import to_native
+from ansible.module_utils.six import iteritems
+from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
+ convert_bson_values_recur,
+ get_mongodb_client,
+ missing_required_lib,
+ mongodb_common_argument_spec,
+ mongo_auth,
+ PYMONGO_IMP_ERR,
+ pymongo_found,
+)
+
+
+class MongoDbInfo():
+ """Class for gathering MongoDB instance information.
+
+ Args:
+ module (AnsibleModule): Object of AnsibleModule class.
+ client (pymongo): pymongo client object to interact with the database.
+ """
+ def __init__(self, module, client):
+ self.module = module
+ self.client = client
+ self.admin_db = self.client.admin
+ self.info = {
+ 'general': {},
+ 'databases': {},
+ 'total_size': {},
+ 'parameters': {},
+ 'users': {},
+ 'roles': {},
+ }
+
+ def get_info(self, filter_):
+ """Get MongoDB instance information and return it based on filter_.
+
+ Args:
+ filter_ (list): List of collected subsets (e.g., general, users, etc.),
+ when it is empty, return all available information.
+ """
+ self.__collect()
+
+ inc_list = []
+ exc_list = []
+
+ if filter_:
+ partial_info = {}
+
+ for fi in filter_:
+ if fi.lstrip('!') not in self.info:
+ self.module.warn("filter element '%s' is not allowable, ignored" % fi)
+ continue
+
+ if fi[0] == '!':
+ exc_list.append(fi.lstrip('!'))
+
+ else:
+ inc_list.append(fi)
+
+ if inc_list:
+ for i in self.info:
+ if i in inc_list:
+ partial_info[i] = self.info[i]
+
+ else:
+ for i in self.info:
+ if i not in exc_list:
+ partial_info[i] = self.info[i]
+
+ return partial_info
+
+ else:
+ return self.info
+
+ def __collect(self):
+ """Collect information."""
+ # Get general info:
+ self.info['general'] = self.client.server_info()
+
+ # Get parameters:
+ self.info['parameters'] = self.get_parameters_info()
+
+ # Gather info about databases and their total size:
+ self.info['databases'], self.info['total_size'] = self.get_db_info()
+
+ for dbname, val in iteritems(self.info['databases']):
+ # Gather info about users for each database:
+ self.info['users'].update(self.get_users_info(dbname))
+
+ # Gather info about roles for each database:
+ self.info['roles'].update(self.get_roles_info(dbname))
+
+ self.info = convert_bson_values_recur(self.info)
+
+ def get_roles_info(self, dbname):
+ """Gather information about roles.
+
+ Args:
+ dbname (str): Database name to get role info from.
+
+ Returns a dictionary with role information for the given db.
+ """
+ db = self.client[dbname]
+ result = db.command({'rolesInfo': 1, 'showBuiltinRoles': True})['roles']
+
+ roles_dict = {}
+ for elem in result:
+ roles_dict[elem['role']] = {}
+ for key, val in iteritems(elem):
+ if key in ['role', 'db']:
+ continue
+
+ roles_dict[elem['role']][key] = val
+
+ return {dbname: roles_dict}
+
+ def get_users_info(self, dbname):
+ """Gather information about users.
+
+ Args:
+ dbname (str): Database name to get user info from.
+
+ Returns a dictionary with user information for the given db.
+ """
+ db = self.client[dbname]
+ result = db.command({'usersInfo': 1})['users']
+
+ users_dict = {}
+ for elem in result:
+ users_dict[elem['user']] = {}
+ for key, val in iteritems(elem):
+ if key in ['user', 'db']:
+ continue
+
+ if isinstance(val, UUID):
+ val = val.hex
+
+ users_dict[elem['user']][key] = val
+
+ return {dbname: users_dict}
+
+ def get_db_info(self):
+ """Gather information about databases.
+
+ Returns a dictionary with database information.
+ """
+ result = self.admin_db.command({'listDatabases': 1})
+ total_size = int(result['totalSize'])
+ result = result['databases']
+
+ db_dict = {}
+ for elem in result:
+ db_dict[elem['name']] = {}
+ for key, val in iteritems(elem):
+ if key == 'name':
+ continue
+
+ if key == 'sizeOnDisk':
+ val = int(val)
+
+ db_dict[elem['name']][key] = val
+
+ return db_dict, total_size
+
+ def get_parameters_info(self):
+ """Gather parameters information.
+
+ Returns a dictionary with parameters.
+ """
+ return self.admin_db.command({'getParameter': '*'})
+
+
+# ================
+# Module execution
+#
+def main():
+ argument_spec = mongodb_common_argument_spec()
+ argument_spec.update(
+ filter=dict(type='list', elements='str', required=False)
+ )
+ 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)
+
+ filter_ = module.params['filter']
+
+ if filter_:
+ filter_ = [f.strip() for f in filter_]
+
+ 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))
+
+ # Initialize an object and start main work:
+ mongodb = MongoDbInfo(module, client)
+
+ module.exit_json(changed=False, **mongodb.get_info(filter_))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/mongodb/plugins/modules/mongodb_maintenance.py b/ansible_collections/community/mongodb/plugins/modules/mongodb_maintenance.py
new file mode 100644
index 000000000..d470040ce
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/modules/mongodb_maintenance.py
@@ -0,0 +1,146 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2020, 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_maintenance
+short_description: Enables or disables maintenance mode for a secondary member.
+description:
+ - Enables or disables maintenance mode for a secondary member.
+ - Wrapper around the replSetMaintenance command.
+ - Performs no actions against a PRIMARY member.
+ - When enabled SECONDARY members will not service reads.
+author: Rhys Campbell (@rhysmeister)
+version_added: "1.0.0"
+
+extends_documentation_fragment:
+ - community.mongodb.login_options
+ - community.mongodb.ssl_options
+
+options:
+ maintenance:
+ description: Enable or disable maintenance mode.
+ type: bool
+ default: 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: Enable maintenance mode
+ community.mongodb.mongodb_maintenance:
+ maintenance: true
+
+- name: Disable maintenance mode
+ community.mongodb.mongodb_maintenance:
+ maintenance: false
+'''
+
+RETURN = r'''
+changed:
+ description: Whether the member was placed into maintenance mode or not.
+ returned: success
+ type: bool
+msg:
+ description: A short description of what happened.
+ returned: success
+ type: str
+failed:
+ description: If something went wrong
+ returned: failed
+ type: bool
+'''
+
+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,
+ member_state,
+ mongo_auth,
+ PYMONGO_IMP_ERR,
+ pymongo_found,
+ get_mongodb_client,
+)
+
+
+def put_in_maint_mode(client):
+ client['admin'].command('replSetMaintenance', True)
+
+
+def remove_maint_mode(client):
+ client['admin'].command('replSetMaintenance', False)
+
+
+def main():
+ argument_spec = mongodb_common_argument_spec()
+ argument_spec.update(
+ maintenance=dict(type='bool', default=False)
+ )
+ 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)
+
+ maintenance = module.params['maintenance']
+
+ result = dict(
+ changed=False,
+ )
+
+ try:
+ client = get_mongodb_client(module, directConnection=True)
+ client = mongo_auth(module, client, directConnection=True)
+ except Exception as excep:
+ module.fail_json(msg='Unable to connect to MongoDB: %s' % to_native(excep))
+
+ try:
+ state = member_state(client)
+ if state == "PRIMARY":
+ result["msg"] = "no action taken as member state was PRIMARY"
+ elif state == "SECONDARY":
+ if maintenance:
+ if module.check_mode:
+ result["changed"] = True
+ result["msg"] = "member was placed into maintenance mode"
+ else:
+ put_in_maint_mode(client)
+ result["changed"] = True
+ result["msg"] = "member was placed into maintenance mode"
+ else:
+ result["msg"] = "No action taken as maintenance parameter is false and member state is SECONDARY"
+ elif state == "RECOVERING":
+ if maintenance:
+ result["msg"] = "no action taken as member is already in a RECOVERING state"
+ else:
+ if module.check_mode:
+ result["changed"] = True
+ result["msg"] = "the member was removed from maintenance mode"
+ else:
+ remove_maint_mode(client)
+ result["changed"] = True
+ result["msg"] = "the member was removed from maintenance mode"
+ else:
+ result["msg"] = "no action taken as member state was {0}".format(state)
+ except Exception as excep:
+ module.fail_json(msg='module encountered an error: %s' % to_native(excep))
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/mongodb/plugins/modules/mongodb_monitoring.py b/ansible_collections/community/mongodb/plugins/modules/mongodb_monitoring.py
new file mode 100644
index 000000000..d399a9907
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/modules/mongodb_monitoring.py
@@ -0,0 +1,197 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2021, Rhys Campbell rhyscampbell@blueiwn.ch
+# 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_monitoring
+short_description: Manages the free monitoring feature.
+description:
+ - Manages the free monitoring feature.
+ - Optionally return the monitoring url.
+author: Rhys Campbell (@rhysmeister)
+version_added: "1.3.0"
+
+extends_documentation_fragment:
+ - community.mongodb.login_options
+ - community.mongodb.ssl_options
+
+options:
+ state:
+ description: Manage the free monitoring feature.
+ type: str
+ choices:
+ - "started"
+ - "stopped"
+ default: "started"
+ return_url:
+ description: When true return the monitoring url if available.
+ type: bool
+ default: 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: Enable monitoring
+ community.mongodb.mongodb_monitoring:
+ state: "started"
+
+- name: Disable monitoring
+ community.mongodb.mongodb_monitoring:
+ state: "stopped"
+
+- name: Enable monitoring and return the monitoring url
+ community.mongodb_monitoring:
+ state: "started"
+ return_url: "yes"
+'''
+
+RETURN = r'''
+changed:
+ description: Whether the monitoring status changed.
+ returned: success
+ type: bool
+msg:
+ description: A short description of what happened.
+ returned: success
+ type: str
+failed:
+ description: If something went wrong
+ returned: failed
+ type: bool
+url:
+ description: The MongoDB instance Monitoring url.
+ returned: When requested and available.
+ type: str
+'''
+
+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,
+ PYMONGO_IMP_ERR,
+ pymongo_found,
+ mongo_auth,
+ 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 stop_monitoring(client):
+ '''
+ Stops MongoDB Free Monitoring
+ '''
+ cmd_doc = OrderedDict([('setFreeMonitoring', 1),
+ ('action', 'disable')])
+ client['admin'].command(cmd_doc)
+
+
+def start_monitoring(client):
+ '''
+ Stops MongoDB Free Monitoring
+ '''
+ cmd_doc = OrderedDict([('setFreeMonitoring', 1),
+ ('action', 'enable')])
+ client['admin'].command(cmd_doc)
+
+
+def get_monitoring_status(client):
+ '''
+ Gets the state of MongoDB Monitoring.
+ N.B. If Monitoring has never been enabled the
+ free_monitoring record in admin.system.version
+ will not yet exist.
+ '''
+ monitoring_state = None
+ url = None
+ result = client["admin"]['system.version'].find_one({"_id": "free_monitoring"})
+ if not result:
+ monitoring_state = "stopped"
+ else:
+ url = result["informationalURL"]
+ if result["state"] == "enabled":
+ monitoring_state = "started"
+ else:
+ monitoring_state = "stopped"
+ return monitoring_state, url
+
+
+def main():
+ argument_spec = mongodb_common_argument_spec()
+ argument_spec.update(
+ state=dict(type='str', default='started', choices=['started', 'stopped']),
+ return_url=dict(type='bool', default=False)
+ )
+
+ 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)
+
+ state = module.params['state']
+ return_url = module.params['return_url']
+
+ 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))
+
+ current_monitoring_state, url = get_monitoring_status(client)
+ result = {}
+ if state == "started":
+ if current_monitoring_state == "started":
+ result['changed'] = False
+ result['msg'] = "Free monitoring is already started"
+ else:
+ if module.check_mode is False:
+ start_monitoring(client)
+ result['changed'] = True
+ result['msg'] = "Free monitoring has been started"
+ else:
+ if current_monitoring_state == "started":
+ if module.check_mode is False:
+ stop_monitoring(client)
+ result['changed'] = True
+ result['msg'] = "Free monitoring has been stopped"
+ else:
+ result['changed'] = False
+ result['msg'] = "Free monitoring is already stopped"
+
+ if return_url and url:
+ result['url'] = url
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/mongodb/plugins/modules/mongodb_oplog.py b/ansible_collections/community/mongodb/plugins/modules/mongodb_oplog.py
new file mode 100644
index 000000000..5735f0ddf
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/modules/mongodb_oplog.py
@@ -0,0 +1,190 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2020, 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_oplog
+short_description: Resizes the MongoDB oplog.
+description:
+ - Resizes the MongoDB oplog.
+ - This module should only be used with MongoDB 3.6 and above.
+ - Old MongoDB versions should use an alternative method.
+ - Consult U(https://docs.mongodb.com/manual/tutorial/change-oplog-size) for further info.
+author: Rhys Campbell (@rhysmeister)
+version_added: "1.0.0"
+
+extends_documentation_fragment:
+ - community.mongodb.login_options
+ - community.mongodb.ssl_options
+
+options:
+ oplog_size_mb:
+ description:
+ - New size of the oplog in MB.
+ type: int
+ required: true
+ compact:
+ description:
+ - Runs compact against the oplog.rs collection in the local database to reclaim disk space.
+ - Performs no actions against PRIMARY members.
+ - The MongoDB user must have the compact role on the local database for this feature to work.
+ type: bool
+ default: false
+ 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: Resize oplog to 16 gigabytes, or 16000 megabytes
+ community.mongodb.mongodb_oplog:
+ oplog_size_mb: 16000
+
+- name: Resize oplog to 8 gigabytes and compact secondaries to reclaim space
+ community.mongodb.mongodb_oplog:
+ oplog_size_mb: 8000
+ compact: true
+'''
+
+RETURN = r'''
+changed:
+ description: Whether the member oplog was modified.
+ returned: success
+ type: bool
+compacted:
+ description: Whether the member oplog was compacted.
+ returned: success
+ type: bool
+msg:
+ description: A short description of what happened.
+ returned: success
+ type: str
+failed:
+ description: If something went wrong
+ returned: failed
+ type: bool
+'''
+
+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,
+ member_state,
+ 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_olplog_size(client):
+ return int(client["local"].command("collStats", "oplog.rs")["maxSize"]) / 1024 / 1024
+
+
+def set_oplog_size(client, oplog_size_mb):
+ cmd_doc = OrderedDict([
+ ('replSetResizeOplog', 1),
+ ('size', oplog_size_mb)
+ ])
+ client["admin"].command(cmd_doc)
+
+
+def compact_oplog(client):
+ client["local"].command("compact", "oplog.rs")
+
+
+def main():
+ argument_spec = mongodb_common_argument_spec()
+ argument_spec.update(
+ compact=dict(type='bool', default=False),
+ oplog_size_mb=dict(type='int', required=True),
+ )
+ 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)
+
+ oplog_size_mb = float(module.params['oplog_size_mb']) # MongoDB 4.4 inists on a real
+ compact = module.params['compact']
+
+ result = dict(
+ changed=False,
+ )
+
+ try:
+ client = get_mongodb_client(module, directConnection=True)
+ client = mongo_auth(module, client, directConnection=True)
+ except Exception as excep:
+ module.fail_json(msg='Unable to connect to MongoDB: %s' % to_native(excep))
+
+ try:
+ current_oplog_size = get_olplog_size(client)
+ except Exception as excep:
+ module.fail_json(msg='Unable to get current oplog size: %s' % to_native(excep))
+ if oplog_size_mb == current_oplog_size:
+ result["msg"] = "oplog_size_mb is already {0} mb".format(int(oplog_size_mb))
+ result["compacted"] = False
+ else:
+ try:
+ state = member_state(client)
+ except Exception as excep:
+ module.fail_json(msg='Unable to get member state: %s' % to_native(excep))
+ if module.check_mode:
+ result["changed"] = True
+ result["msg"] = "oplog has been resized from {0} mb to {1} mb".format(int(current_oplog_size),
+ int(oplog_size_mb))
+ if state == "SECONDARY" and compact and current_oplog_size > oplog_size_mb:
+ result["compacted"] = True
+ else:
+ result["compacted"] = False
+ else:
+ try:
+ set_oplog_size(client, oplog_size_mb)
+ result["changed"] = True
+ result["msg"] = "oplog has been resized from {0} mb to {1} mb".format(int(current_oplog_size),
+ int(oplog_size_mb))
+ except Exception as excep:
+ module.fail_json(msg='Unable to set oplog size: %s' % to_native(excep))
+ if state == "SECONDARY" and compact and current_oplog_size > oplog_size_mb:
+ try:
+ compact_oplog(client)
+ result["compacted"] = True
+ except Exception as excep:
+ module.fail_json(msg='Error compacting member oplog: %s' % to_native(excep))
+ else:
+ result["compacted"] = False
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/mongodb/plugins/modules/mongodb_parameter.py b/ansible_collections/community/mongodb/plugins/modules/mongodb_parameter.py
new file mode 100644
index 000000000..d167cf646
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/modules/mongodb_parameter.py
@@ -0,0 +1,144 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2016, Loic Blot <loic.blot@unix-experience.fr>
+# Sponsored by Infopro Digital. http://www.infopro-digital.com/
+# Sponsored by E.T.A.I. http://www.etai.fr/
+#
+# 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_parameter
+short_description: Change an administrative parameter on a MongoDB server
+description:
+ - Change an administrative parameter on a MongoDB server.
+version_added: "1.0.0"
+
+extends_documentation_fragment:
+ - community.mongodb.login_options
+ - community.mongodb.ssl_options
+
+options:
+ replica_set:
+ description:
+ - Replica set to connect to (automatically connects to primary for writes).
+ type: str
+ param:
+ description:
+ - MongoDB administrative parameter to modify.
+ type: str
+ required: true
+ value:
+ description:
+ - MongoDB administrative parameter value to set.
+ type: str
+ required: true
+ param_type:
+ description:
+ - Define the type of parameter value.
+ default: str
+ type: str
+ choices: [int, str]
+
+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 also U(http://api.mongodb.org/python/current/installation.html)
+requirements: [ "pymongo" ]
+author: "Loic Blot (@nerzhul)"
+'''
+
+EXAMPLES = r'''
+- name: Set MongoDB syncdelay to 60 (this is an int)
+ community.mongodb.mongodb_parameter:
+ param: syncdelay
+ value: 60
+ param_type: int
+'''
+
+RETURN = r'''
+before:
+ description: value before modification
+ returned: success
+ type: str
+after:
+ description: value after modification
+ returned: success
+ type: str
+'''
+
+import traceback
+
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+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,
+ OperationFailure,
+ get_mongodb_client,
+)
+
+# =========================================
+# Module execution.
+#
+
+
+def main():
+ argument_spec = mongodb_common_argument_spec()
+ argument_spec.update(
+ replica_set=dict(default=None),
+ param=dict(required=True),
+ value=dict(required=True),
+ param_type=dict(default="str", choices=['str', 'int'])
+ )
+ 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)
+
+ param = module.params['param']
+ param_type = module.params['param_type']
+ value = module.params['value']
+
+ # Verify parameter is coherent with specified type
+ try:
+ if param_type == 'int':
+ value = int(value)
+ except ValueError:
+ module.fail_json(msg="value '%s' is not %s" % (value, param_type))
+
+ try:
+ client = get_mongodb_client(module, directConnection=True)
+ client = mongo_auth(module, client, directConnection=True)
+ except Exception as excep:
+ module.fail_json(msg='Unable to connect to MongoDB: %s' % to_native(excep))
+
+ db = client.admin
+
+ try:
+ after_value = db.command("setParameter", **{param: value})
+ except OperationFailure as e:
+ module.fail_json(msg="unable to change parameter: %s" % to_native(e), exception=traceback.format_exc())
+
+ if "was" not in after_value:
+ module.exit_json(changed=True, msg="Unable to determine old value, assume it changed.")
+ else:
+ module.exit_json(changed=(value != after_value["was"]), before=after_value["was"],
+ after=value)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/mongodb/plugins/modules/mongodb_replicaset.py b/ansible_collections/community/mongodb/plugins/modules/mongodb_replicaset.py
new file mode 100644
index 000000000..d0baf661e
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/modules/mongodb_replicaset.py
@@ -0,0 +1,611 @@
+#!/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 (re)configuration possible (see examples).
+ - Initialize the replicaset before adding users as per \
+ [best practice](https://www.mongodb.com/docs/manual/tutorial/deploy-replica-set-with-keyfile-access-control/).
+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 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()
diff --git a/ansible_collections/community/mongodb/plugins/modules/mongodb_role.py b/ansible_collections/community/mongodb/plugins/modules/mongodb_role.py
new file mode 100644
index 000000000..012f553a0
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/modules/mongodb_role.py
@@ -0,0 +1,387 @@
+#!/usr/bin/python
+
+# (c) 2022, Rhys Campbell <rhyscampbell@bluewin.ch>
+
+# 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 = '''
+---
+module: mongodb_role
+short_description: Adds or removes a role from a MongoDB database
+description:
+ - Adds or removes a role from a MongoDB database.
+ - For further information on the required format for \
+ the privileges, authenticationRestriction or roles \
+ parameters, see the MongoDB Documentation https://www.mongodb.com/docs/manual/reference/command/createRole/
+version_added: "1.5.0"
+
+extends_documentation_fragment:
+ - community.mongodb.login_options
+ - community.mongodb.ssl_options
+
+options:
+ replica_set:
+ description:
+ - Replica set to connect to (automatically connects to primary for writes).
+ type: str
+ database:
+ description:
+ - The name of the database to add/remove the role from.
+ required: true
+ type: str
+ aliases: [db]
+ name:
+ description:
+ - The name of the role to add or remove.
+ required: true
+ aliases: [user]
+ type: str
+ privileges:
+ type: list
+ elements: raw
+ description:
+ - >
+ The privileges to grant the role. A privilege consists of a resource
+ and permitted actions.
+ default: []
+ authenticationRestrictions:
+ type: list
+ elements: raw
+ description:
+ - >
+ The authentication restrictions the server enforces on the role.
+ Specifies a list of IP addresses and CIDR ranges users granted
+ this role are allowed to connect to and/or which they can connect from.
+ Provide a list of dictionaries with the following
+ fields: clientSource (list), serverAddress (list).
+ Provide an empty list if you don't want to use the field.
+ default: []
+ roles:
+ type: list
+ elements: raw
+ description:
+ - >
+ The database user roles should be provided as a dictionary with the db and role keys.
+ default: []
+ state:
+ description:
+ - The database user state.
+ default: present
+ choices: [absent, present]
+ type: str
+ debug:
+ description:
+ - Enable extra debugging output.
+ default: false
+ type: bool
+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. Newer mongo server versions require newer
+ pymongo versions. @see http://api.mongodb.org/python/current/installation.html
+requirements:
+ - "pymongo"
+author:
+ - "Rhys Campbell (@rhysmeister)"
+'''
+
+EXAMPLES = '''
+- name: Create sales role
+ community.mongodb.mongodb_role:
+ name: sales
+ database: salesdb
+ privileges:
+ - resource:
+ db: salesdb
+ collection: ""
+ actions:
+ - find
+ state: present
+
+- name: Create ClusterAdmin Role
+ community.mongodb.mongodb_role:
+ name: myClusterwideAdmin
+ database: admin
+ privileges:
+ - resource:
+ cluster: true
+ actions:
+ - addShard
+ - resource:
+ db: config
+ collection: ""
+ actions:
+ - find
+ - update
+ - insert
+ - remove
+ - resource:
+ db: "users"
+ collection: "usersCollection"
+ actions:
+ - update
+ - insert
+ - remove
+ - resource:
+ db: ""
+ collection: ""
+ actions:
+ - find
+ roles:
+ - role: "read"
+ db: "admin"
+ state: present
+
+- name: Create ClusterAdmin Role with a login only from 127.0.0.1 restriction
+ community.mongodb.mongodb_role:
+ name: myClusterwideAdmin
+ database: admin
+ privileges:
+ - resource:
+ cluster: true
+ actions:
+ - addShard
+ - resource:
+ db: config
+ collection: ""
+ actions:
+ - find
+ - update
+ - insert
+ - resource:
+ db: "users"
+ collection: "usersCollection"
+ actions:
+ - update
+ - insert
+ - remove
+ - resource:
+ db: ""
+ collection: ""
+ actions:
+ - find
+ roles:
+ - role: "read"
+ db: "admin"
+ - role: "read"
+ db: "mynewdb"
+ authenticationRestrictions:
+ - clientSource:
+ - "127.0.0.1"
+ serverAddress: []
+ state: present
+
+- name: Delete sales role
+ community.mongodb.mongodb_role:
+ name: sales
+ database: "salesdb"
+ state: absent
+
+- name: Delete myClusterwideAdmin role
+ community.mongodb.mongodb_role:
+ name: myClusterwideAdmin
+ database: admin
+ state: absent
+'''
+
+RETURN = '''
+user:
+ description: The name of the role to add or remove.
+ returned: success
+ type: str
+'''
+
+import traceback
+
+
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+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 role_find(client, role, db_name):
+ """Check if the role exists.
+
+ Args:
+ client (cursor): Mongodb cursor on admin database.
+ user (str): Role to check.
+ db_name (str): Role's database.
+
+ Returns:
+ dict: when role exists, False otherwise.
+ """
+ try:
+ mongo_role = None
+ rolesDoc = {
+ 'rolesInfo': 1,
+ 'showAuthenticationRestrictions': True,
+ 'showPrivileges': True
+ }
+ for mongo_role in client[db_name].command(rolesDoc)['roles']:
+ if mongo_role['role'] == role:
+ # NOTE: there is no 'db' field in mongo 2.4.
+ if 'db' not in mongo_role:
+ return mongo_role
+ # Workaround to make the condition works with AWS DocumentDB,
+ # since all users are in the admin database.
+ if mongo_role["db"] in [db_name, "admin"]:
+ return mongo_role
+ except Exception as excep:
+ if hasattr(excep, 'code') and excep.code == 31: # 31=RoleNotFound
+ pass # Allow return False
+ else:
+ raise
+ return False
+
+
+def role_add(client, db_name, role, privileges, roles, authenticationRestrictions):
+ db = client[db_name]
+
+ try:
+ exists = role_find(client, role, db_name)
+ except Exception as excep:
+ # probably not needed for role create... to clarify
+ # We get this exception: "not authorized on admin to execute command"
+ # when auth is enabled on a new instance. The localhost exception should
+ # allow us to create the first user. If the localhost exception does not apply,
+ # then user creation will also fail with unauthorized. So, ignore Unauthorized here.
+ if hasattr(excep, 'code') and excep.code == 13: # 13=Unauthorized
+ exists = False
+ else:
+ raise
+
+ if exists:
+ role_add_db_command = 'updateRole'
+ else:
+ role_add_db_command = 'createRole'
+
+ role_dict = {}
+
+ role_dict["privileges"] = privileges
+ role_dict["roles"] = roles
+ role_dict["authenticationRestrictions"] = authenticationRestrictions
+ db.command(role_add_db_command, role, **role_dict)
+
+
+def role_remove(module, client, db_name, role):
+ exists = role_find(client, role, db_name)
+ if exists:
+ if module.check_mode:
+ module.exit_json(changed=True, role=role)
+ db = client[db_name]
+ db.command("dropRole", role)
+ else:
+ module.exit_json(changed=False, role=role)
+
+
+def check_if_role_changed(client, role, db_name, privileges, authenticationRestrictions, roles):
+ role_dict = role_find(client, role, db_name)
+ changed = False
+ if role_dict:
+ reformat_authenticationRestrictions = []
+ if 'authenticationRestrictions' in role_dict:
+ for item in role_dict['authenticationRestrictions']:
+ reformat_authenticationRestrictions.append(item[0]) # seems to be a list of lists of dict, we want a list of dicts
+ if ('privileges' in role_dict and
+ [{'resource': d['resource'], 'actions': sorted(d['actions'])} for d in role_dict['privileges']] !=
+ [{'resource': d['resource'], 'actions': sorted(d['actions'])} for d in privileges] or
+ 'privileges' not in role_dict and privileges != []):
+ changed = True
+ elif ('roles' in role_dict and
+ sorted(role_dict['roles'], key=lambda x: (x["db"], x["role"])) !=
+ sorted(roles, key=lambda x: (x["db"], x["role"])) or
+ 'roles' not in role_dict and roles != []):
+ changed = True
+ elif ('authenticationRestrictions' in role_dict and
+ sorted(reformat_authenticationRestrictions, key=lambda x: (x['clientSource'], x['serverAddress'])) !=
+ sorted(authenticationRestrictions, key=lambda x: (x['clientSource'], x['serverAddress'])) or
+ 'authenticationRestrictions' not in role_dict and authenticationRestrictions != []):
+ changed = True
+ else:
+ raise Exception("Role not found") # TODO replace with proper exception
+ return changed
+
+
+# =========================================
+# Module execution.
+#
+
+def main():
+ argument_spec = mongodb_common_argument_spec()
+ argument_spec.update(
+ replica_set=dict(default=None),
+ database=dict(required=True, aliases=['db']),
+ name=dict(required=True, aliases=['user']),
+ privileges=dict(default=[], type='list', elements='raw'),
+ authenticationRestrictions=dict(default=[], type='list', elements='raw'),
+ roles=dict(default=[], type='list', elements='raw'),
+ state=dict(default='present', choices=['absent', 'present']),
+ debug=dict(type='bool', default=False),
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ if not pymongo_found:
+ module.fail_json(msg=missing_required_lib('pymongo'),
+ exception=PYMONGO_IMP_ERR)
+
+ try:
+ directConnection = False
+ if module.params['replica_set'] is None:
+ directConnection = True
+ client = get_mongodb_client(module, directConnection=directConnection)
+ client = mongo_auth(module, client, directConnection=directConnection)
+ except Exception as e:
+ module.fail_json(msg='Unable to connect to database: %s' % to_native(e))
+
+ changed = None
+ role = state = module.params['name']
+ state = module.params['state']
+ db_name = module.params['database']
+ privileges = module.params['privileges']
+ roles = module.params['roles']
+ authenticationRestrictions = module.params['authenticationRestrictions']
+ debug = module.params['debug']
+ # TODO _ Functions use a different param order... make consistent
+ try:
+ if state == 'present':
+ if role_find(client, role, db_name) is False:
+ if module.check_mode is False:
+ role_add(client, db_name, role, privileges, roles, authenticationRestrictions)
+ changed = True
+ else:
+ if check_if_role_changed(client, role, db_name, privileges, authenticationRestrictions, roles):
+ if module.check_mode is False:
+ role_add(client, db_name, role, privileges, roles, authenticationRestrictions)
+ changed = True
+ else:
+ changed = False
+ elif state == 'absent':
+ if role_find(client, role, db_name):
+ if module.check_mode is False:
+ role_remove(module, client, db_name, role)
+ changed = True
+ else:
+ changed = False
+ module.exit_json(changed=changed, role=role)
+ except Exception as e:
+ if debug:
+ module.fail_json(msg=str(e), traceback=traceback.format_exc())
+ else:
+ module.fail_json(msg=str(e))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/mongodb/plugins/modules/mongodb_schema.py b/ansible_collections/community/mongodb/plugins/modules/mongodb_schema.py
new file mode 100644
index 000000000..4443c0c82
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/modules/mongodb_schema.py
@@ -0,0 +1,346 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2021, Rhys Campbell (@rhysmeister) <rhyscampbell@bluewin.ch>
+# 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_schema
+
+short_description: Manages MongoDB Document Schema Validators.
+
+description:
+- Manages MongoDB Document Schema Validators.
+- Create, update and remove Validators on a collection.
+- Supports the entire range of jsonSchema keywords.
+- See [jsonSchema Available Keywords](https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/#available-keywords) for details.
+
+author: Rhys Campbell (@rhysmeister)
+version_added: "1.3.0"
+
+extends_documentation_fragment:
+ - community.mongodb.login_options
+ - community.mongodb.ssl_options
+
+options:
+ db:
+ description:
+ - The database to work with.
+ required: yes
+ type: str
+ collection:
+ description:
+ - The collection to work with.
+ required: yes
+ type: str
+ required:
+ description:
+ - List of fields that are required.
+ type: list
+ elements: str
+ properties:
+ description:
+ - Individual property specification.
+ type: dict
+ default: {}
+ action:
+ description:
+ - The validation action for MongoDB to perform when handling invalid documents.
+ type: str
+ choices:
+ - "error"
+ - "warn"
+ default: "error"
+ level:
+ description:
+ - The validation level MongoDB should apply when updating existing documents.
+ type: str
+ choices:
+ - "strict"
+ - "moderate"
+ default: "strict"
+ replica_set:
+ description:
+ - Replicaset name.
+ type: str
+ default: null
+ state:
+ description:
+ - The state of the validator.
+ type: str
+ choices:
+ - "present"
+ - "absent"
+ default: "present"
+ debug:
+ description:
+ - Enable additional debugging output.
+ type: bool
+ default: false
+
+notes:
+ - Requires the pymongo Python package on the remote host, version 2.4.2+.
+
+requirements:
+ - pymongo
+'''
+
+EXAMPLES = r'''
+---
+- name: Require that an email address field is in every document
+ community.mongodb.mongodb_schema:
+ collection: contacts
+ db: rhys
+ required:
+ - email
+
+- name: Remove a schema rule
+ community.mongodb.mongodb_schema:
+ collection: contacts
+ db: rhys
+ state: absent
+
+
+- name: More advanced example using properties
+ community.mongodb.mongodb_schema:
+ collection: contacts
+ db: rhys
+ properties:
+ email:
+ maxLength: 150
+ minLength: 5
+ options:
+ bsonType: array
+ maxItems: 10
+ minItems: 5
+ uniqueItems: true
+ status:
+ bsonType: string
+ description: "can only be ACTIVE or DISABLED"
+ enum:
+ - ACTIVE
+ - DISABLED
+ year:
+ bsonType: int
+ description: "must be an integer from 2021 to 3020"
+ exclusiveMaximum: false
+ maximum: 3020
+ minimum: 2021
+ required:
+ - email
+ - first_name
+ - last_name
+'''
+
+RETURN = r'''
+changed:
+ description: If the module caused a change.
+ returned: on success
+ type: bool
+msg:
+ description: Status message.
+ returned: always
+ type: str
+validator:
+ description: The validator document as read from the instance.
+ returned: when debug is true
+ type: dict
+module_config:
+ description: The validator document as indicated by the module invocation.
+ returned: when debug is true
+ type: dict
+'''
+
+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,
+)
+import json
+
+
+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_validator(client, db, collection):
+ validator = None
+ cmd_doc = OrderedDict([
+ ('listCollections', 1),
+ ('filter', {"name": collection})
+ ])
+ doc = None
+ results = client[db].command(cmd_doc)["cursor"]["firstBatch"]
+ if len(results) > 0:
+ doc = results[0]
+ if doc is not None and 'options' in doc and 'validator' in doc['options']:
+ validator = doc['options']['validator']["$jsonSchema"]
+ if 'validationAction' in doc['options']:
+ validator['validationAction'] = doc['options']['validationAction']
+ if 'validationLevel' in doc['options']:
+ validator['validationLevel'] = doc['options']['validationLevel']
+ return validator
+
+
+def validator_is_different(client, db, collection, required, properties, action, level):
+ is_different = False
+ validator = get_validator(client, db, collection)
+ if validator is not None:
+ if sorted(required) != sorted(validator.get('required', [])):
+ is_different = True
+ if action != validator.get('validationAction', 'error'):
+ is_different = True
+ if level != validator.get('validationLevel', 'strict'):
+ is_different = True
+ dict1 = json.dumps(properties, sort_keys=True)
+ dict2 = json.dumps(validator.get('properties', {}), sort_keys=True)
+ if dict1 != dict2:
+ is_different = True
+ else:
+ is_different = True
+ return is_different
+
+
+def add_validator(client, db, collection, required, properties, action, level):
+ cmd_doc = OrderedDict([
+ ('collMod', collection),
+ ('validator', {'$jsonSchema': {"bsonType": "object",
+ "required": required,
+ "properties": properties}}),
+ ('validationAction', action),
+ ('validationLevel', level)
+ ])
+ if collection not in client[db].list_collection_names():
+ client[db].create_collection(collection)
+ client[db].command(cmd_doc)
+
+
+def remove_validator(client, db, collection):
+ cmd_doc = OrderedDict([
+ ('collMod', collection),
+ ('validator', {}),
+ ('validationLevel', "off")
+ ])
+ client[db].command(cmd_doc)
+
+
+# ================
+# Module execution
+#
+
+def main():
+ argument_spec = mongodb_common_argument_spec()
+ argument_spec.update(
+ db=dict(type='str', required=True),
+ collection=dict(type='str', required=True),
+ required=dict(type='list', elements='str'),
+ properties=dict(type='dict', default={}),
+ action=dict(type='str', choices=['error', 'warn'], default="error"),
+ level=dict(type='str', choices=['strict', 'moderate'], default="strict"),
+ state=dict(type='str', choices=['present', 'absent'], default='present'),
+ debug=dict(type='bool', default=False),
+ replica_set=dict(type='str', default=None),
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ required_together=[['login_user', 'login_password']],
+ required_if=[("state", "present", ("db", "collection"))]
+ )
+
+ 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)
+
+ db = module.params['db']
+ collection = module.params['collection']
+ required = module.params['required']
+ properties = module.params['properties']
+ action = module.params['action']
+ level = module.params['level']
+ state = module.params['state']
+ debug = module.params['debug']
+
+ try:
+ client = get_mongodb_client(module)
+ client = mongo_auth(module, client)
+ except Exception as e:
+ module.fail_json(msg='Unable to connect to database: %s' % to_native(e))
+
+ result = dict(
+ changed=False,
+ )
+
+ validator = get_validator(client, db, collection)
+ if state == "present":
+ if validator is not None:
+ diff = validator_is_different(client, db, collection, required,
+ properties, action, level)
+ if diff:
+ if not module.check_mode:
+ add_validator(client,
+ db,
+ collection,
+ required,
+ properties,
+ action,
+ level)
+ result['changed'] = True
+ result['msg'] = "The validator was updated on the given collection"
+ else:
+ result['changed'] = False
+ result['msg'] = "The validator exists as configured on the given collection"
+ else:
+ if not module.check_mode:
+ add_validator(client,
+ db,
+ collection,
+ required,
+ properties,
+ action,
+ level)
+ result['changed'] = True
+ result['msg'] = "The validator has been added to the given collection"
+ elif state == "absent":
+ if validator is None:
+ result['changed'] = False
+ result['msg'] = "A validator does not exist on the given collection."
+ else:
+ if not module.check_mode:
+ remove_validator(client, db, collection)
+ result['changed'] = True
+ result['msg'] = "The validator has been removed from the given collection"
+
+ if debug:
+ result['validator'] = validator
+ result['module_config'] = {"required": required,
+ "properties": properties,
+ "validationAction": action,
+ "validationLevel": level}
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/mongodb/plugins/modules/mongodb_shard.py b/ansible_collections/community/mongodb/plugins/modules/mongodb_shard.py
new file mode 100644
index 000000000..bc37f59db
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/modules/mongodb_shard.py
@@ -0,0 +1,306 @@
+#!/usr/bin/python
+
+# (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 = '''
+---
+module: mongodb_shard
+short_description: Add or remove shards from a MongoDB Cluster
+description:
+ - Add or remove shards from a MongoDB Cluster.
+author: Rhys Campbell (@rhysmeister)
+version_added: "1.0.0"
+
+extends_documentation_fragment:
+ - community.mongodb.login_options
+ - community.mongodb.ssl_options
+
+options:
+ shard:
+ description:
+ - The shard connection string.
+ - Should be supplied in the form <replicaset>/host:port as detailed in U(https://docs.mongodb.com/manual/tutorial/add-shards-to-shard-cluster/).
+ - For example rs0/example1.mongodb.com:27017.
+ required: true
+ type: str
+ sharded_databases:
+ description:
+ - Enable sharding on the listed database.
+ - Can be supplied as a string or a list of strings.
+ - Sharding cannot be disabled on a database.
+ - Starting in MongoDB 6.0, the enableSharding command is no longer required to shard a collection and this parameter is ignored.
+ required: false
+ type: raw
+ mongos_process:
+ description:
+ - Provide a custom name for the mongos process you are connecting to.
+ - Most users can ignore this setting.
+ required: false
+ type: str
+ default: "mongos"
+ state:
+ description:
+ - Whether the shard should be present or absent from the Cluster.
+ required: false
+ type: str
+ default: present
+ choices:
+ - "absent"
+ - "present"
+
+notes:
+ - Requires the pymongo Python package on the remote host, version 2.4.2+.
+requirements: [ pymongo ]
+'''
+
+EXAMPLES = '''
+- name: Add a replicaset shard named rs1 with a member running on port 27018 on mongodb0.example.net
+ community.mongodb.mongodb_shard:
+ login_user: admin
+ login_password: admin
+ shard: "rs1/mongodb0.example.net:27018"
+ state: present
+
+- name: Add a standalone mongod shard running on port 27018 of mongodb0.example.net
+ community.mongodb.mongodb_shard:
+ login_user: admin
+ login_password: admin
+ shard: "mongodb0.example.net:27018"
+ state: present
+
+- name: To remove a shard called 'rs1'
+ community.mongodb.mongodb_shard:
+ login_user: admin
+ login_password: admin
+ shard: rs1
+ state: absent
+
+# Single node shard running on localhost
+- name: Ensure shard rs0 exists
+ community.mongodb.mongodb_shard:
+ login_user: admin
+ login_password: secret
+ shard: "rs0/localhost:3001"
+ state: present
+
+# Single node shard running on localhost
+- name: Ensure shard rs1 exists
+ community.mongodb.mongodb_shard:
+ login_user: admin
+ login_password: secret
+ shard: "rs1/localhost:3002"
+ state: present
+
+# Enable sharding on a few databases when creating the shard
+- name: To remove a shard called 'rs1'
+ community.mongodb.mongodb_shard:
+ login_user: admin
+ login_password: admin
+ shard: rs1
+ sharded_databases:
+ - db1
+ - db2
+ state: present
+'''
+
+RETURN = '''
+mongodb_shard:
+ description: The name of the shard to create.
+ returned: success
+ type: str
+sharded_enabled:
+ description: Databases that have had sharding enabled during module execution.
+ returned: success when sharding is enabled
+ type: list
+'''
+
+import traceback
+
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+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,
+ check_srv_version
+)
+
+
+def shard_find(client, shard):
+ """Check if a shard exists.
+
+ Args:
+ client (cursor): Mongodb cursor on admin database.
+ shard (str): shard to check.
+
+ Returns:
+ dict: when user exists, False otherwise.
+ """
+ if '/' in shard:
+ s = shard.split('/')[0]
+ else:
+ s = shard
+ for shard in client["config"].shards.find({"_id": s}):
+ return shard
+ return False
+
+
+def shard_add(client, shard):
+ try:
+ sh = client["admin"].command('addShard', shard)
+ except Exception as excep:
+ raise excep
+ return sh
+
+
+def shard_remove(client, shard):
+ try:
+ sh = client["admin"].command('removeShard', shard)
+ except Exception as excep:
+ raise excep
+ return sh
+
+
+def sharded_dbs(client):
+ '''
+ Returns the sharded databases
+ Args:
+ client (cursor): Mongodb cursor on admin database.
+ Returns:
+ a list of database names that are sharded
+ '''
+ sharded_databases = []
+ for entry in client["config"].databases.find({"partitioned": True}, {"_id": 1}):
+ sharded_databases.append(entry["_id"])
+ return sharded_databases
+
+
+def enable_database_sharding(client, database):
+ '''
+ Enables sharding on a database
+ Args:
+ client (cursor): Mongodb cursor on admin database.
+ Returns:
+ true on success, false on failure
+ '''
+ s = False
+ db = client["admin"].command('enableSharding', database)
+ if db:
+ s = True
+ return s
+
+
+def any_dbs_to_shard(client, sharded_databases):
+ '''
+ Return a list of databases that need to have sharding enabled
+ sharded_databases - Provided by module
+ cluster_sharded_databases - List of sharded dbs from the mongos
+ '''
+ dbs_to_shard = []
+ cluster_sharded_databases = sharded_dbs(client)
+ for db in sharded_databases:
+ if db not in cluster_sharded_databases:
+ dbs_to_shard.append(db)
+ return dbs_to_shard
+
+
+# =========================================
+# Module execution.
+#
+
+
+def main():
+ argument_spec = mongodb_common_argument_spec()
+ argument_spec.update(
+ mongos_process=dict(type='str', required=False, default="mongos"),
+ shard=dict(type='str', required=True),
+ sharded_databases=dict(type="raw", required=False),
+ state=dict(type='str', required=False, default='present', choices=['absent', 'present'])
+ )
+ 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)
+
+ login_host = module.params['login_host']
+ login_port = module.params['login_port']
+ shard = module.params['shard']
+ state = module.params['state']
+ sharded_databases = module.params['sharded_databases']
+ mongos_process = module.params['mongos_process']
+
+ 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))
+
+ 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))
+
+ dbs_to_shard = []
+
+ if sharded_databases is not None and int(check_srv_version(module, client)[0]) < 6:
+ if isinstance(sharded_databases, str):
+ sharded_databases = list(sharded_databases)
+ dbs_to_shard = any_dbs_to_shard(client, sharded_databases)
+
+ if module.check_mode:
+ if state == "present":
+ changed = False
+ if not shard_find(client, shard) or len(dbs_to_shard) > 0:
+ changed = True
+ elif state == "absent":
+ if not shard_find(client, shard):
+ changed = False
+ else:
+ changed = True
+ else:
+ if state == "present":
+ if not shard_find(client, shard):
+ shard_add(client, shard)
+ changed = True
+ else:
+ changed = False
+ if len(dbs_to_shard) > 0:
+ for db in dbs_to_shard:
+ enable_database_sharding(client, db)
+ changed = True
+ elif state == "absent":
+ if shard_find(client, shard):
+ shard_remove(client, shard)
+ changed = True
+ else:
+ changed = False
+ except Exception as e:
+ action = "add"
+ if state == "absent":
+ action = "remove"
+ module.fail_json(msg='Unable to {0} shard: %s'.format(action) % to_native(e), exception=traceback.format_exc())
+
+ result = {
+ "changed": changed,
+ "shard": shard,
+ }
+ if len(dbs_to_shard) > 0:
+ result['sharded_enabled'] = dbs_to_shard
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/mongodb/plugins/modules/mongodb_shard_tag.py b/ansible_collections/community/mongodb/plugins/modules/mongodb_shard_tag.py
new file mode 100644
index 000000000..c6b1a1339
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/modules/mongodb_shard_tag.py
@@ -0,0 +1,216 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2021, Rhys Campbell <rhyscampbell@blueiwn.ch>
+# 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_shard_tag
+short_description: Manage Shard Tags.
+description:
+ - Manage Shard Tags..
+ - Add and remove shard tags.
+author: Rhys Campbell (@rhysmeister)
+version_added: "1.3.0"
+
+extends_documentation_fragment:
+ - community.mongodb.login_options
+ - community.mongodb.ssl_options
+
+options:
+ name:
+ description:
+ - The name of the tag.
+ required: true
+ type: str
+ shard:
+ description:
+ - The name of the shard to assign or remove the tag from.
+ required: true
+ type: str
+ state:
+ description:
+ - The state of the zone.
+ required: false
+ type: str
+ choices:
+ - "present"
+ - "absent"
+ default: "present"
+ mongos_process:
+ description:
+ - Provide a custom name for the mongos process.
+ - Most users can ignore this setting.
+ required: false
+ type: str
+ default: "mongos"
+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: Add the NYC tag to a shard called rs0
+ community.mongodb.mongodb_shard_tag:
+ name: "NYC"
+ shard: "rs0"
+ state: "present"
+
+- name: Remove the NYC tag from rs0
+ community.mongodb.mongodb_shard_tag:
+ name: "NYC"
+ shard: "rs0"
+ state: "absent"
+'''
+
+RETURN = r'''
+changed:
+ description: True when a change has happened
+ returned: success
+ type: bool
+msg:
+ description: A short description of what happened.
+ returned: failure
+ type: str
+failed:
+ description: If something went wrong
+ returned: failed
+ type: bool
+'''
+
+
+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 tag_exists(client, shard, tag):
+ '''
+ Returns True if the giventag is assign to the shard
+ @client - MongoDB connection
+ @tag - The zone to check for
+ @shard - The shard name
+ '''
+ status = None
+ result = client["config"].shards.find_one({"_id": shard, "tags": tag})
+ if result:
+ status = True
+ else:
+ status = False
+ return status
+
+
+def add_zone_tag(client, shard, tag):
+ '''
+ Adds a tag to a shard
+ @client - MongoDB connection
+ @shard - The shard name
+ @tag - The tag or Zone name
+ '''
+ cmd_doc = OrderedDict([
+ ('addShardToZone', shard),
+ ('zone', tag),
+ ])
+ client['admin'].command(cmd_doc)
+
+
+def remove_zone_tag(client, shard, tag):
+ '''
+ Remove a zone tag from a shard.
+ @client - MongoDB connection
+ @shard - The shard name
+ @tag - The tag or Zone name
+ '''
+ cmd_doc = OrderedDict([
+ ('removeShardFromZone', shard),
+ ('zone', tag),
+ ])
+ client['admin'].command(cmd_doc)
+
+
+def main():
+ argument_spec = mongodb_common_argument_spec()
+ argument_spec.update(
+ name=dict(type='str', required=True),
+ shard=dict(type='str', required=True),
+ mongos_process=dict(type='str', required=False, default="mongos"),
+ state=dict(type='str', default="present", choices=["present", "absent"]),
+ )
+ 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)
+
+ state = module.params['state']
+ tag = module.params['name']
+ shard = module.params['shard']
+
+ 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))
+
+ try:
+ if tag_exists(client, shard, tag):
+ if state == "present":
+ result['changed'] = False
+ result['msg'] = "The tag {0} is already assigned to the shard {1}".format(tag, shard)
+ elif state == "absent":
+ if not module.check_mode:
+ remove_zone_tag(client, shard, tag)
+ result['changed'] = True
+ result['msg'] = "The tag {0} was removed from the shard {1}".format(tag, shard)
+ else:
+ if state == "present":
+ if not module.check_mode:
+ add_zone_tag(client, shard, tag)
+ result['changed'] = True
+ result['msg'] = "The tag {0} was assigned to the shard {1}".format(tag, shard)
+ elif state == "absent":
+ result['changed'] = False
+ result['msg'] = "The tag {0} is not assigned to the shard {1}".format(tag, shard)
+ except Exception as excep:
+ module.fail_json(msg="An error occurred: {0}".format(excep))
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/mongodb/plugins/modules/mongodb_shard_zone.py b/ansible_collections/community/mongodb/plugins/modules/mongodb_shard_zone.py
new file mode 100644
index 000000000..1d1c68fde
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/modules/mongodb_shard_zone.py
@@ -0,0 +1,323 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2021, Rhys Campbell <rhyscampbell@blueiwn.ch>
+# 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_shard_zone
+short_description: Manage Shard Zones.
+description:
+ - Manage Shard Zones.
+ - Add and remove shard zones.
+author: Rhys Campbell (@rhysmeister)
+version_added: "1.3.0"
+
+extends_documentation_fragment:
+ - community.mongodb.login_options
+ - community.mongodb.ssl_options
+
+options:
+ name:
+ description:
+ - The name of the zone.
+ required: true
+ type: str
+ namespace:
+ description:
+ - The namespace the zone is assigned to
+ - Should be given in the form database.collection.
+ type: str
+ ranges:
+ description:
+ - The ranges assigned to the Zone.
+ type: list
+ elements: list
+ state:
+ description:
+ - The state of the zone.
+ required: false
+ type: str
+ choices:
+ - "present"
+ - "absent"
+ default: "present"
+ mongos_process:
+ description:
+ - Provide a custom name for the mongos process.
+ - Most users can ignore this setting.
+ required: false
+ type: str
+ default: "mongos"
+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: Add a shard zone for NYC
+ community.mongodb.mongodb_shard_zone:
+ name: "NYC"
+ namespace: "records.users"
+ ranges:
+ - [{ zipcode: "10001" }, { zipcode: "10281" }]
+ - [{ zipcode: "11201" }, { zipcode: "11240" }]
+ state: "present"
+
+- name: Remove all zone ranges
+ community.mongodb.mongodb_shard_zone:
+ name: "NYC"
+ namespace: "records.users"
+ state: "absent"
+
+- name: Remove a specific zone range
+ community.mongodb.mongodb_shard_zone:
+ name: "NYC"
+ namespace: "records.users"
+ ranges:
+ - [{ zipcode: "11201" }, { zipcode: "11240" }]
+ state: "absent"
+'''
+
+RETURN = r'''
+changed:
+ description: True when a change has happened
+ returned: success
+ type: bool
+msg:
+ description: A short description of what happened.
+ returned: failure
+ type: str
+failed:
+ description: If something went wrong
+ returned: failed
+ type: bool
+'''
+
+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 zone_range_exists(client, namespace, min, max, tag):
+ '''
+ Returns true if a particular zone range exists
+ Record format seems to be different than the docs state in 4.4.6
+ { "_id" : ObjectId("60e2e7cff7c9d447440bb114"),
+ "ns" : "records.users",
+ "min" : { "zipcode" : "10001" },
+ "max" : { "zipcode" : "10281" },
+ "tag" : "NYC" }
+
+ @client - MongoDB connection
+ @namespace - In the form database.collection
+ @min - The min range value
+ @max - The max range value
+ @tag - The tag or Zone name
+ '''
+ query = {
+ # "_id.ns": namespace, 4.4.X Bug??? ObjectId given as id
+ # "_id.min": min,
+ 'ns': namespace,
+ 'min': min,
+ 'max': max,
+ 'tag': tag
+ }
+
+ status = None
+ result = client["config"].tags.find_one(query)
+ if result:
+ status = True
+ else:
+ status = False
+ return status
+
+
+def zone_exists(client, tag):
+ '''
+ Returns True if the given zone exists
+ @client - MongoDB connection
+ @tag - The zone to check for
+ '''
+ status = None
+ result = client["config"].shards.find_one({"tags": tag})
+ if result:
+ status = True
+ else:
+ status = False
+ return status
+
+
+def add_zone_range(client, namespace, min, max, tag):
+ '''
+ Adds a zone range
+ @client - MongoDB connection
+ @namespace - In the form database.collection
+ @min - The min range value
+ @max - The max range value
+ @tag - The tag or Zone name
+ '''
+ cmd_doc = OrderedDict([
+ ('updateZoneKeyRange', namespace),
+ ('min', min),
+ ('max', max),
+ ('zone', tag),
+ ])
+ client['admin'].command(cmd_doc)
+
+
+def remove_zone_range(client, namespace, min, max):
+ '''
+ Remove a zone range.
+ We do this by setting the zone to None
+ @client - MongoDB connection
+ @namespace - In the form database.collection
+ @min - The min range value
+ @max - The max range value
+ '''
+ cmd_doc = OrderedDict([
+ ('updateZoneKeyRange', namespace),
+ ('min', min),
+ ('max', max),
+ ('zone', None),
+ ])
+ client['admin'].command(cmd_doc)
+
+
+def remove_all_zone_range_by_tag(client, tag):
+ result = client["config"].tags.find({"tag": tag})
+ for r in result:
+ remove_zone_range(client, r['ns'], r['min'], r['max'])
+
+
+def zone_range_count(client, tag):
+ '''
+ Returns the count of records that exists for the given tag in config.tags
+ '''
+ return client['config'].tags.count_documents({"tag": tag})
+
+
+def main():
+ argument_spec = mongodb_common_argument_spec()
+ argument_spec.update(
+ name=dict(type='str', required=True),
+ namespace=dict(type='str'),
+ ranges=dict(type='list', elements='list'),
+ mongos_process=dict(type='str', required=False, default="mongos"),
+ state=dict(type='str', default="present", choices=["present", "absent"]),
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ required_together=[['login_user', 'login_password']],
+ required_if=[("state", "present", ("namespace", "ranges"))]
+ )
+
+ 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)
+
+ state = module.params['state']
+ zone_name = module.params['name']
+ namespace = module.params['namespace']
+ ranges = module.params['ranges']
+
+ if ranges is not None:
+ if not isinstance(ranges, list) or not isinstance(ranges[0], list) or not isinstance(ranges[0][0], dict):
+ module.fail_json(msg="Provided ranges are invalid {0} {1} {2}".format(str(type(ranges)),
+ str(type(ranges[0])),
+ str(type(ranges[0][0]))))
+
+ 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))
+
+ try:
+ if not zone_exists(client, zone_name):
+ msg = ("The tag {0} does not exist. You need to associate a tag with"
+ " a shard before using this module. You can do that with the"
+ " mongodb_shard_tag module.".format(zone_name))
+ module.fail_json(msg=msg)
+ else:
+ # first check if the ranges exist
+ range_count = 0
+ if state == "present":
+ for range in ranges:
+ if zone_range_exists(client, namespace, range[0], range[1], zone_name):
+ range_count += 1
+ result['range_count'] = range_count
+ result['ranges'] = len(ranges)
+ if range_count == len(ranges): # All ranges are the same
+ result['changed'] = False
+ result['msg'] = "All Zone Ranges present for {0}".format(zone_name)
+ else:
+ for range in ranges:
+ if not module.check_mode:
+ add_zone_range(client, namespace, range[0], range[1], zone_name)
+ result['changed'] = True
+ result['msg'] = "Added zone ranges for {0}".format(zone_name)
+ elif state == "absent":
+ range_count = zone_range_count(client, zone_name)
+ deleted_count = 0
+ if range_count > 0 and ranges is None:
+ if not module.check_mode:
+ remove_all_zone_range_by_tag(client, zone_name)
+ deleted_count = range_count
+ result['changed'] = True
+ result['msg'] = "{0} zone ranges for {1} deleted.".format(deleted_count, zone_name)
+ elif ranges is not None:
+ for range in ranges:
+ if zone_range_exists(client, namespace, range[0], range[1], zone_name):
+ if not module.check_mode:
+ remove_zone_range(client, namespace, range[0], range[1])
+ deleted_count += 1
+ if deleted_count > 0:
+ result['changed'] = True
+ result['msg'] = "{0} zone ranges for {1} deleted.".format(deleted_count, zone_name)
+ else:
+ result['changed'] = False
+ result['msg'] = "The provided zone ranges are not present for {0}".format(zone_name)
+ else:
+ result['changed'] = False
+ result['msg'] = "No zone ranges present for {0}".format(zone_name)
+ except Exception as excep:
+ module.fail_json(msg="An error occurred: {0}".format(excep))
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/mongodb/plugins/modules/mongodb_shell.py b/ansible_collections/community/mongodb/plugins/modules/mongodb_shell.py
new file mode 100644
index 000000000..618cafae9
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/modules/mongodb_shell.py
@@ -0,0 +1,328 @@
+#!/usr/bin/python
+
+# 2020 Rhys Campbell <rhys.james.campbell@googlemail.com>
+# https://github.com/rhysmeister
+# 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
+
+
+DOCUMENTATION = '''
+---
+module: mongodb_shell
+author: Rhys Campbell (@rhysmeister)
+version_added: "1.1.0"
+short_description: Run commands via the MongoDB shell.
+requirements:
+ - mongosh
+description:
+ - Run commands via the MongoDB shell.
+ - Commands provided with the eval parameter or included in a Javascript file.
+ - Attempts to parse returned data into a format that Ansible can use.
+ - Module uses the mongosh shell by default.
+ - Support for mongo is depreciated.
+
+extends_documentation_fragment:
+ - community.mongodb.login_options
+
+options:
+ mongo_cmd:
+ description:
+ - The MongoDB shell command.
+ - auto - Automatically detect which MongoDB shell command used. Use "mongosh" if available, else use "mongo" command.
+ - mongo - This should still work for most cases but you might have problems with json parsinf. Use transform_type of 'raw' is you encounter problems.
+ type: str
+ default: "mongosh"
+ db:
+ description:
+ - The database to run commands against
+ type: str
+ required: false
+ default: "test"
+ file:
+ description:
+ - Path to a file containing MongoDB commands.
+ type: str
+ eval:
+ description:
+ - A MongoDB command to run.
+ type: str
+ nodb:
+ description:
+ - Specify a non-default encoding for output.
+ type: bool
+ default: false
+ norc:
+ description:
+ - Prevents the shell from sourcing and evaluating ~/.mongorc.js on start up.
+ type: bool
+ default: false
+ quiet:
+ description:
+ - Silences output from the shell during the connection process..
+ type: bool
+ default: true
+ debug:
+ description:
+ - show additional debug info.
+ type: bool
+ default: false
+ transform:
+ description:
+ - Transform the output returned to the user.
+ - auto - Attempt to automatically decide the best tranformation.
+ - split - Split output on a character.
+ - json - parse as json.
+ - raw - Return the raw output.
+ type: str
+ choices:
+ - "auto"
+ - "split"
+ - "json"
+ - "raw"
+ default: "auto"
+ split_char:
+ description:
+ - Used by the split action in the transform stage.
+ type: str
+ default: " "
+ stringify:
+ description:
+ - Wraps the command in eval in JSON.stringify(<js cmd>) (mongo) or EJSON.stringify(<js cmd>) (mongosh).
+ - Useful for escaping documents that are returned in Extended JSON format.
+ - Automatically set to false when using mongo.
+ - Automatically set to true when using mongosh.
+ - Set explicitly to override automatic selection.
+ type: bool
+ default: null
+ additional_args:
+ description:
+ - Additional arguments to supply to the mongo command.
+ - Supply as key-value pairs.
+ - If the parameter is a valueless flag supply an empty string as the value.
+ type: raw
+ idempotent:
+ description:
+ - Provides a form of pseudo-idempotency to the module.
+ - We perform a hash calculation on the contents of the eval key or the file name provided in the file key.
+ - When the command is first execute a filed called <hash>.success will be created.
+ - The module will not rerun the command if this file exists and idempotent is set to true.
+ type: bool
+ default: false
+ omit:
+ description:
+ - Parameter to omit from the command line.
+ - This should match the parameter name that the MongoDB shell accepts not the module name.
+ type: list
+ elements: str
+ default: []
+'''
+
+EXAMPLES = '''
+- name: Run the listDatabases command
+ community.mongodb.mongodb_shell:
+ login_user: user
+ login_password: secret
+ eval: "db.adminCommand('listDatabases')"
+
+- name: List collections and stringify the output
+ community.mongodb.mongodb_shell:
+ login_user: user
+ login_password: secret
+ eval: "db.adminCommand('listCollections')"
+ stringify: yes
+
+- name: Run the showBuiltinRoles command
+ community.mongodb.mongodb_shell:
+ login_user: user
+ login_password: secret
+ eval: "db.getRoles({showBuiltinRoles: true})"
+
+- name: Run a js file containing MongoDB commands with pseudo-idempotency
+ community.mongodb.mongodb_shell:
+ login_user: user
+ login_password: secret
+ file: "/path/to/mongo/file.js"
+ idempotent: yes
+
+- name: Provide a couple of additional cmd args
+ community.mongodb.mongodb_shell:
+ login_user: user
+ login_password: secret
+ eval: "db.adminCommand('listDatabases')"
+ additional_args:
+ verbose: True
+ networkMessageCompressors: "snappy"
+'''
+
+RETURN = '''
+file:
+ description: JS file that was executed successfully.
+ returned: When a js file is used.
+ type: str
+msg:
+ description: A message indicating what has happened.
+ returned: always
+ type: str
+transformed_output:
+ description: Output from the mongo command. We attempt to parse this into a list or json where possible.
+ returned: on success
+ type: list
+changed:
+ description: Change status.
+ returned: always
+ type: bool
+failed:
+ description: Something went wrong.
+ returned: on failure
+ type: bool
+out:
+ description: Raw stdout from mongo.
+ returned: when debug is set to true
+ type: str
+err:
+ description: Raw stderr from mongo.
+ returned: when debug is set to true
+ type: str
+rc:
+ description: Return code from mongo.
+ returned: when debug is set to true
+ type: int
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+import os
+__metaclass__ = type
+
+from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
+ mongodb_common_argument_spec
+)
+
+from ansible_collections.community.mongodb.plugins.module_utils.mongodb_shell import (
+ add_arg_to_cmd,
+ transform_output,
+ get_hash_value,
+ touch,
+ detect_if_cmd_exist
+)
+
+
+def main():
+ argument_spec = mongodb_common_argument_spec(ssl_options=False)
+ argument_spec.update(
+ mongo_cmd=dict(type='str', default="mongosh"),
+ file=dict(type='str', required=False),
+ eval=dict(type='str', required=False),
+ db=dict(type='str', required=False, default="test"),
+ nodb=dict(type='bool', required=False, default=False),
+ norc=dict(type='bool', required=False, default=False),
+ quiet=dict(type='bool', required=False, default=True),
+ debug=dict(type='bool', required=False, default=False),
+ transform=dict(type='str', choices=["auto", "split", "json", "raw"], default="auto"),
+ split_char=dict(type='str', default=" "),
+ stringify=dict(type='bool', default=None),
+ additional_args=dict(type='raw'),
+ idempotent=dict(type='bool', default=False),
+ omit=dict(type='list', elements='str', default=[]),
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=False,
+ required_together=[['login_user', 'login_password']],
+ mutually_exclusive=[["eval", "file"]]
+ )
+
+ if module.params['mongo_cmd'] == "auto":
+ module.params['mongo_cmd'] = "mongosh" if detect_if_cmd_exist() else "mongo"
+
+ if module.params['mongo_cmd'] == "mongo" and module.params['stringify'] is None:
+ module.params['stringify'] = False
+ elif module.params['mongo_cmd'] == "mongosh" and module.params['stringify'] is None:
+ module.params['stringify'] = True
+
+ args = [
+ module.params['mongo_cmd'],
+ module.params['db']
+ ]
+
+ hash_value = get_hash_value(module)
+
+ if module.params['idempotent']:
+ if os.path.isfile("{0}.success".format(hash_value)):
+ module.exit_json(changed=False,
+ msg="The file {0}.success was found meaning this "
+ "command has already successfully executed "
+ "on this MongoDB host.".format(hash_value))
+
+ if not module.params['file']:
+ if module.params['eval'].startswith("show "):
+ msg = "You cannot use any shell helper (e.g. use <dbname>, show dbs, etc.)"\
+ " inside the eval parameter because they are not valid JavaScript."
+ module.fail_json(msg=msg)
+ if module.params['stringify']:
+ if module.params['mongo_cmd'] != "mongosh":
+ module.params['eval'] = "JSON.stringify({0})".format(module.params['eval'])
+ else:
+ module.params['eval'] = "EJSON.stringify({0})".format(module.params['eval'])
+
+ omit = module.params['omit']
+
+ args = add_arg_to_cmd(args, "--host", module.params['login_host'], omit=omit)
+ args = add_arg_to_cmd(args, "--port", module.params['login_port'], omit=omit)
+ args = add_arg_to_cmd(args, "--username", module.params['login_user'], omit=omit)
+ args = add_arg_to_cmd(args, "--password", module.params['login_password'], omit=omit)
+ args = add_arg_to_cmd(args, "--authenticationDatabase", module.params['login_database'], omit=omit)
+ args = add_arg_to_cmd(args, "--eval", module.params['eval'], omit=omit)
+ args = add_arg_to_cmd(args, "--nodb", None, module.params['nodb'], omit=omit)
+ args = add_arg_to_cmd(args, "--norc", None, module.params['norc'], omit=omit)
+ args = add_arg_to_cmd(args, "--quiet", None, module.params['quiet'], omit=omit)
+
+ additional_args = module.params['additional_args']
+ if additional_args is not None:
+ for key, value in additional_args.items():
+ if isinstance(value, bool):
+ args.append(" --{0}".format(key))
+ elif isinstance(value, str) or isinstance(value, int):
+ args.append(" --{0} {1}".format(key, value))
+ if module.params['file']:
+ args.append(module.params['file'])
+
+ rc = None
+ out = ''
+ err = ''
+ result = {}
+ cmd = " ".join(str(item) for item in args)
+
+ (rc, out, err) = module.run_command(cmd, check_rc=False)
+
+ if module.params['debug']:
+ result['out'] = out
+ result['err'] = err
+ result['rc'] = rc
+ result['cmd'] = cmd
+
+ if rc != 0:
+ if err is None or err == "":
+ err = out
+ module.fail_json(msg=err.strip(), **result)
+ else:
+ result['changed'] = True
+ if module.params['idempotent']:
+ touch("{0}.success".format(hash_value))
+ try:
+ output = transform_output(out,
+ module.params['transform'],
+ module.params['split_char'])
+ result['transformed_output'] = output
+ result['msg'] = "transform type was {0}".format(module.params['transform'])
+ if module.params['file'] is not None:
+ result['file'] = module.params['file']
+ except Exception as excep:
+ result['msg'] = "Error tranforming output: {0}".format(str(excep))
+ result['transformed_output'] = None
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/mongodb/plugins/modules/mongodb_shutdown.py b/ansible_collections/community/mongodb/plugins/modules/mongodb_shutdown.py
new file mode 100644
index 000000000..60e3f027b
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/modules/mongodb_shutdown.py
@@ -0,0 +1,137 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2020, 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_shutdown
+short_description: Cleans up all database resources and then terminates the mongod/mongos process.
+description:
+ - Cleans up all database resources and then terminates the process.
+author: Rhys Campbell (@rhysmeister)
+version_added: "1.0.0"
+
+extends_documentation_fragment:
+ - community.mongodb.login_options
+ - community.mongodb.ssl_options
+
+options:
+ force:
+ description:
+ - Specify true to force the mongod to shut down.
+ - Force shutdown interrupts any ongoing operations on the mongod and may result in unexpected behavior.
+ type: bool
+ default: false
+ timeout:
+ description:
+ - The number of seconds the primary should wait for a secondary to catch up.
+ type: int
+ default: 10
+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: Attempt to perform a clean shutdown
+ community.mongodb.mongodb_shutdown:
+
+- name: Force shutdown with a timeout of 60 seconds
+ community.mongodb.mongodb_shutdown:
+ force: true
+ timeout: 60
+'''
+
+RETURN = r'''
+changed:
+ description: Whether the member was shutdown.
+ returned: success
+ type: bool
+msg:
+ description: A short description of what happened.
+ returned: success
+ type: str
+failed:
+ description: If something went wrong
+ returned: failed
+ type: bool
+'''
+
+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 main():
+ argument_spec = mongodb_common_argument_spec()
+ argument_spec.update(
+ force=dict(type='bool', default=False),
+ timeout=dict(type='int', default=10)
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ required_together=[['login_user', 'login_password']],
+ )
+
+ 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))
+
+ if not pymongo_found:
+ module.fail_json(msg=missing_required_lib('pymongo'),
+ exception=PYMONGO_IMP_ERR)
+
+ force = module.params['force']
+ timeout = module.params['timeout']
+
+ result = dict(
+ changed=False,
+ )
+
+ try:
+ client = get_mongodb_client(module, directConnection=True)
+ client = mongo_auth(module, client, directConnection=True)
+ except Exception as excep:
+ module.fail_json(msg='Unable to connect to MongoDB: %s' % to_native(excep))
+
+ try:
+ cmd_doc = OrderedDict([
+ ('shutdown', 1),
+ ('force', force),
+ ('timeoutSecs', timeout)
+ ])
+ client['admin'].command(cmd_doc)
+ result["changed"] = True
+ result["msg"] = "mongod process was terminated sucessfully"
+ except Exception as excep:
+ if "connection closed" in str(excep):
+ result["changed"] = True
+ result["msg"] = "mongod process was terminated sucessfully"
+ else:
+ result["msg"] = "An error occurred: {0}".format(excep)
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/mongodb/plugins/modules/mongodb_status.py b/ansible_collections/community/mongodb/plugins/modules/mongodb_status.py
new file mode 100644
index 000000000..4b7a208db
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/modules/mongodb_status.py
@@ -0,0 +1,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()
diff --git a/ansible_collections/community/mongodb/plugins/modules/mongodb_stepdown.py b/ansible_collections/community/mongodb/plugins/modules/mongodb_stepdown.py
new file mode 100644
index 000000000..cd0580e7a
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/modules/mongodb_stepdown.py
@@ -0,0 +1,248 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2020, 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_stepdown
+short_description: Step down the MongoDB node from a PRIMARY state.
+description: >
+ Step down the MongoDB node from the PRIMARY state if it has that status.
+ Returns OK immediately if the member is already in the SECONDARY or ARBITER states.
+ Will wait until a timeout for the member state to reach SECONDARY or PRIMARY,
+ if the member state is currently STARTUP, RECOVERING, STARTUP2 or ROLLBACK,
+ before taking any needed action.
+author: Rhys Campbell (@rhysmeister)
+version_added: "1.0.0"
+
+extends_documentation_fragment:
+ - community.mongodb.login_options
+ - community.mongodb.ssl_options
+
+options:
+ poll:
+ description:
+ - The maximum number of times query for the member status.
+ type: int
+ default: 1
+ interval:
+ description:
+ - The number of seconds to wait between poll executions.
+ type: int
+ default: 30
+ stepdown_seconds:
+ description:
+ - The number of seconds to step down the primary, during which time the stepdown member is ineligible for becoming primary.
+ type: int
+ default: 60
+ secondary_catch_up:
+ description:
+ - The secondaryCatchUpPeriodSecs parameter for the stepDown command.
+ - The number of seconds that mongod will wait for an electable secondary to catch up to the primary.
+ type: int
+ default: 10
+ force:
+ description:
+ - Optional. A boolean that determines whether the primary steps down if no electable and up-to-date secondary exists within the wait period.
+ type: bool
+ default: 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: Step down the current MongoDB member
+ community.mongodb.mongodb_stepdown:
+ login_user: admin
+ login_password: secret
+
+- name: Step down the current MongoDB member, poll a maximum of 5 times if member state is recovering
+ community.mongodb.mongodb_stepdown:
+ login_user: admin
+ login_password: secret
+ poll: 5
+ interval: 10
+'''
+
+RETURN = r'''
+failed:
+ description: If the module had failed or not.
+ returned: always
+ type: bool
+iteration:
+ description: Number of times the module has queried the replicaset status.
+ returned: always
+ type: int
+msg:
+ description: Status message.
+ returned: always
+ type: str
+'''
+
+
+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 member_status(client):
+ """
+ Return the member status string
+ # https://docs.mongodb.com/manual/reference/command/replSetGetStatus/
+ """
+ myStateStr = None
+ rs = client.admin.command('replSetGetStatus')
+ for member in rs["members"]:
+ if "self" in member.keys():
+ myStateStr = member["stateStr"]
+ return myStateStr
+
+
+def member_stepdown(client, module):
+ """
+ client - MongoDB Client
+ module - Ansible module object
+ """
+
+ 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))
+
+ iterations = 0 # How many times we have queried the member
+ failures = 0 # Number of failures when querying the replicaset
+ poll = module.params['poll']
+ interval = module.params['interval']
+ stepdown_seconds = module.params['stepdown_seconds']
+ secondary_catch_up = module.params['secondary_catch_up']
+ force = module.params['force']
+ return_doc = {}
+ status = None
+
+ while iterations < poll:
+ try:
+ iterations += 1
+ return_doc['iterations'] = iterations
+ myStateStr = member_status(client)
+ if myStateStr == "PRIMARY":
+ # Run step down command
+ if module.check_mode:
+ return_doc["msg"] = "member was stepped down"
+ return_doc['changed'] = True
+ status = True
+ break
+ else:
+ cmd_doc = OrderedDict([
+ ('replSetStepDown', stepdown_seconds),
+ ('secondaryCatchUpPeriodSecs', secondary_catch_up),
+ ('force', force)
+ ])
+ try:
+ client.admin.command(cmd_doc) # For now we assume the stepDown was successful
+ except Exception as excep:
+ # 4.0 and below close the connection as part of the stepdown.
+ # This code should be removed once we support 4.2+ onwards
+ # https://tinyurl.com/yc79g9ay
+ if str(excep) == "connection closed":
+ pass
+ else:
+ raise excep
+ return_doc['changed'] = True
+ status = True
+ return_doc["msg"] = "member was stepped down"
+ break
+ elif myStateStr in ["SECONDARY", "ARBITER"]:
+ return_doc["msg"] = "member was already at {0} state".format(myStateStr)
+ return_doc['changed'] = False
+ status = True
+ break
+ elif myStateStr in ["STARTUP", "RECOVERING", "STARTUP2", "ROLLBACK"]:
+ time.sleep(interval) # Wait for interval
+ else:
+ return_doc["msg"] = "Unexpected member state {0}".format(myStateStr)
+ return_doc['changed'] = False
+ status = False
+ break
+ except Exception as e:
+ failures += 1
+ return_doc['failed'] = True
+ return_doc['changed'] = False
+ return_doc['msg'] = str(e)
+ status = False
+ if iterations == poll:
+ break
+ else:
+ time.sleep(interval)
+
+ return status, return_doc['msg'], return_doc
+
+
+# =========================================
+# Module execution.
+#
+
+
+def main():
+ argument_spec = mongodb_common_argument_spec()
+ argument_spec.update(
+ force=dict(type='bool', default=False),
+ interval=dict(type='int', default=30),
+ poll=dict(type='int', default=1),
+ secondary_catch_up=dict(type='int', default=10),
+ stepdown_seconds=dict(type='int', default=60)
+ )
+ 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)
+
+ result = dict(
+ failed=False,
+ )
+
+ 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))
+
+ try:
+ status, msg, return_doc = member_stepdown(client, module)
+ iterations = return_doc['iterations']
+ changed = return_doc['changed']
+ except Exception as e:
+ module.fail_json(msg='Unable to query replica_set info: %s' % str(e))
+
+ if status is False:
+ module.fail_json(msg=msg, iterations=iterations, changed=changed)
+ else:
+ module.exit_json(msg=msg, iterations=iterations, changed=changed)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/mongodb/plugins/modules/mongodb_user.py b/ansible_collections/community/mongodb/plugins/modules/mongodb_user.py
new file mode 100644
index 000000000..eab0d186c
--- /dev/null
+++ b/ansible_collections/community/mongodb/plugins/modules/mongodb_user.py
@@ -0,0 +1,428 @@
+#!/usr/bin/python
+
+# (c) 2012, Elliott Foster <elliott@fourkitchens.com>
+# Sponsored by Four Kitchens http://fourkitchens.com.
+# (c) 2014, Epic Games, Inc.
+#
+# 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 = '''
+---
+module: mongodb_user
+short_description: Adds or removes a user from a MongoDB database
+description:
+ - Adds or removes a user from a MongoDB database.
+version_added: "1.0.0"
+
+extends_documentation_fragment:
+ - community.mongodb.login_options
+ - community.mongodb.ssl_options
+
+options:
+ replica_set:
+ description:
+ - Replica set to connect to (automatically connects to primary for writes).
+ type: str
+ database:
+ description:
+ - The name of the database to add/remove the user from.
+ required: true
+ type: str
+ aliases: [db]
+ name:
+ description:
+ - The name of the user to add or remove.
+ required: true
+ aliases: [user]
+ type: str
+ password:
+ description:
+ - The password to use for the user.
+ type: str
+ aliases: [pass]
+ roles:
+ type: list
+ elements: raw
+ description:
+ - >
+ The database user roles valid values could either be one or more of the following strings:
+ 'read', 'readWrite', 'dbAdmin', 'userAdmin', 'clusterAdmin', 'readAnyDatabase', 'readWriteAnyDatabase', 'userAdminAnyDatabase',
+ 'dbAdminAnyDatabase'
+ - "Or the following dictionary '{ db: DATABASE_NAME, role: ROLE_NAME }'."
+ - "This param requires pymongo 2.5+. If it is a string, mongodb 2.4+ is also required. If it is a dictionary, mongo 2.6+ is required."
+ state:
+ description:
+ - The database user state.
+ default: present
+ choices: [absent, present]
+ type: str
+ update_password:
+ default: always
+ choices: [always, on_create]
+ description:
+ - C(always) will always update passwords and cause the module to return changed.
+ - C(on_create) will only set the password for newly created users.
+ - This must be C(always) to use the localhost exception when adding the first admin user.
+ - This option is effectively ignored when using x.509 certs. It is defaulted to 'on_create' to maintain a \
+ a specific module behaviour when the login_database is '$external'.
+ type: str
+ create_for_localhost_exception:
+ type: path
+ description:
+ - This is parmeter is only useful for handling special treatment around the localhost exception.
+ - If C(login_user) is defined, then the localhost exception is not active and this parameter has no effect.
+ - If this file is NOT present (and C(login_user) is not defined), then touch this file after successfully adding the user.
+ - If this file is present (and C(login_user) is not defined), then skip this task.
+
+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. Newer mongo server versions require newer
+ pymongo versions. @see http://api.mongodb.org/python/current/installation.html
+requirements:
+ - "pymongo"
+author:
+ - "Elliott Foster (@elliotttf)"
+ - "Julien Thebault (@Lujeni)"
+'''
+
+EXAMPLES = '''
+- name: Create 'burgers' database user with name 'bob' and password '12345'.
+ community.mongodb.mongodb_user:
+ database: burgers
+ name: bob
+ password: 12345
+ state: present
+
+- name: Create a database user via SSL (MongoDB must be compiled with the SSL option and configured properly)
+ community.mongodb.mongodb_user:
+ database: burgers
+ name: bob
+ password: 12345
+ state: present
+ ssl: True
+
+- name: Delete 'burgers' database user with name 'bob'.
+ community.mongodb.mongodb_user:
+ database: burgers
+ name: bob
+ state: absent
+
+- name: Define more users with various specific roles (if not defined, no roles is assigned, and the user will be added via pre mongo 2.2 style)
+ community.mongodb.mongodb_user:
+ database: burgers
+ name: ben
+ password: 12345
+ roles: read
+ state: present
+
+- name: Define roles
+ community.mongodb.mongodb_user:
+ database: burgers
+ name: jim
+ password: 12345
+ roles: readWrite,dbAdmin,userAdmin
+ state: present
+
+- name: Define roles
+ community.mongodb.mongodb_user:
+ database: burgers
+ name: joe
+ password: 12345
+ roles: readWriteAnyDatabase
+ state: present
+
+- name: Add a user to database in a replica set, the primary server is automatically discovered and written to
+ community.mongodb.mongodb_user:
+ database: burgers
+ name: bob
+ replica_set: belcher
+ password: 12345
+ roles: readWriteAnyDatabase
+ state: present
+
+# add a user 'oplog_reader' with read only access to the 'local' database on the replica_set 'belcher'. This is useful for oplog access (MONGO_OPLOG_URL).
+# please notice the credentials must be added to the 'admin' database because the 'local' database is not synchronized and can't receive user credentials
+# To login with such user, the connection string should be MONGO_OPLOG_URL="mongodb://oplog_reader:oplog_reader_password@server1,server2/local?authSource=admin"
+# This syntax requires mongodb 2.6+ and pymongo 2.5+
+- name: Roles as a dictionary
+ community.mongodb.mongodb_user:
+ login_user: root
+ login_password: root_password
+ database: admin
+ user: oplog_reader
+ password: oplog_reader_password
+ state: present
+ replica_set: belcher
+ roles:
+ - db: local
+ role: read
+
+- name: Adding a user with X.509 Member Authentication
+ community.mongodb.mongodb_user:
+ login_host: "mongodb-host.test"
+ login_port: 27001
+ login_database: "$external"
+ database: "admin"
+ name: "admin"
+ password: "test"
+ roles:
+ - dbAdminAnyDatabase
+ ssl: true
+ ssl_ca_certs: "/tmp/ca.crt"
+ ssl_certfile: "/tmp/tls.key" #cert and key in one file
+ state: present
+ auth_mechanism: "MONGODB-X509"
+ connection_options:
+ - "tlsAllowInvalidHostnames=true"
+'''
+
+RETURN = '''
+user:
+ description: The name of the user to add or remove.
+ returned: success
+ type: str
+'''
+
+import os
+import traceback
+from operator import itemgetter
+
+
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+from ansible.module_utils.six import binary_type, text_type
+from ansible.module_utils._text import to_native, to_bytes
+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 user_find(client, user, db_name):
+ """Check if the user exists.
+
+ Args:
+ client (cursor): Mongodb cursor on admin database.
+ user (str): User to check.
+ db_name (str): User's database.
+
+ Returns:
+ dict: when user exists, False otherwise.
+ """
+ try:
+ for mongo_user in client[db_name].command('usersInfo')['users']:
+ if mongo_user['user'] == user:
+ # NOTE: there is no 'db' field in mongo 2.4.
+ if 'db' not in mongo_user:
+ return mongo_user
+ # Workaround to make the condition works with AWS DocumentDB,
+ # since all users are in the admin database.
+ if mongo_user["db"] in [db_name, "admin"]:
+ return mongo_user
+ except Exception as excep:
+ if hasattr(excep, 'code') and excep.code == 11: # 11=UserNotFound
+ pass # Allow return False
+ else:
+ raise
+ return False
+
+
+def user_add(module, client, db_name, user, password, roles):
+ # pymongo's user_add is a _create_or_update_user so we won't know if it was changed or updated
+ # without reproducing a lot of the logic in database.py of pymongo
+ db = client[db_name]
+
+ try:
+ exists = user_find(client, user, db_name)
+ except Exception as excep:
+ # We get this exception: "not authorized on admin to execute command"
+ # when auth is enabled on a new instance. The loalhost exception should
+ # allow us to create the first user. If the localhost exception does not apply,
+ # then user creation will also fail with unauthorized. So, ignore Unauthorized here.
+ if hasattr(excep, 'code') and excep.code == 13: # 13=Unauthorized
+ exists = False
+ else:
+ raise
+
+ if exists:
+ user_add_db_command = 'updateUser'
+ else:
+ user_add_db_command = 'createUser'
+
+ user_dict = {}
+
+ if password is not None:
+ user_dict["pwd"] = password
+ if roles is not None:
+ user_dict["roles"] = roles
+
+ db.command(user_add_db_command, user, **user_dict)
+
+
+def user_remove(module, client, db_name, user):
+ exists = user_find(client, user, db_name)
+ if exists:
+ if module.check_mode:
+ module.exit_json(changed=True, user=user)
+ db = client[db_name]
+ db.command("dropUser", user)
+ else:
+ module.exit_json(changed=False, user=user)
+
+
+def check_if_roles_changed(uinfo, roles, db_name):
+ # We must be aware of users which can read the oplog on a replicaset
+ # Such users must have access to the local DB, but since this DB does not store users credentials
+ # and is not synchronized among replica sets, the user must be stored on the admin db
+ # Therefore their structure is the following :
+ # {
+ # "_id" : "admin.oplog_reader",
+ # "user" : "oplog_reader",
+ # "db" : "admin", # <-- admin DB
+ # "roles" : [
+ # {
+ # "role" : "read",
+ # "db" : "local" # <-- local DB
+ # }
+ # ]
+ # }
+
+ def make_sure_roles_are_a_list_of_dict(roles, db_name):
+ output = list()
+ for role in roles:
+ if isinstance(role, (binary_type, text_type)):
+ new_role = {"role": role, "db": db_name}
+ output.append(new_role)
+ else:
+ output.append(role)
+ return output
+
+ roles_as_list_of_dict = make_sure_roles_are_a_list_of_dict(roles, db_name)
+ uinfo_roles = uinfo.get('roles', [])
+
+ if sorted(roles_as_list_of_dict, key=itemgetter('db')) == sorted(uinfo_roles, key=itemgetter('db')):
+ return False
+ return True
+
+
+# =========================================
+# Module execution.
+#
+
+def main():
+ argument_spec = mongodb_common_argument_spec()
+ argument_spec.update(
+ database=dict(required=True, aliases=['db']),
+ name=dict(required=True, aliases=['user']),
+ password=dict(aliases=['pass'], no_log=True),
+ replica_set=dict(default=None),
+ roles=dict(default=None, type='list', elements='raw'),
+ state=dict(default='present', choices=['absent', 'present']),
+ update_password=dict(default="always", choices=["always", "on_create"], no_log=False),
+ create_for_localhost_exception=dict(default=None, type='path'),
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ login_user = module.params['login_user']
+
+ # Certs don't have a password but we want this module behaviour
+ if module.params['login_database'] == '$external':
+ module.params['update_password'] = 'on_create'
+
+ if not pymongo_found:
+ module.fail_json(msg=missing_required_lib('pymongo'),
+ exception=PYMONGO_IMP_ERR)
+
+ create_for_localhost_exception = module.params['create_for_localhost_exception']
+ b_create_for_localhost_exception = (
+ to_bytes(create_for_localhost_exception, errors='surrogate_or_strict')
+ if create_for_localhost_exception is not None else None
+ )
+
+ db_name = module.params['database']
+ user = module.params['name']
+ password = module.params['password']
+ roles = module.params['roles'] or []
+ state = module.params['state']
+ update_password = module.params['update_password']
+
+ try:
+ directConnection = False
+ if module.params['replica_set'] is None:
+ directConnection = True
+ client = get_mongodb_client(module, directConnection=directConnection)
+ client = mongo_auth(module, client, directConnection=directConnection)
+ except Exception as e:
+ module.fail_json(msg='Unable to connect to database: %s' % to_native(e))
+
+ if state == 'present':
+ if password is None and update_password == 'always':
+ module.fail_json(msg='password parameter required when adding a user unless update_password is set to on_create')
+
+ if login_user is None and create_for_localhost_exception is not None:
+ if os.path.exists(b_create_for_localhost_exception):
+ try:
+ client.close()
+ except Exception:
+ pass
+ module.exit_json(changed=False, user=user, skipped=True, msg="The path in create_for_localhost_exception exists.")
+
+ try:
+ if update_password != 'always':
+ uinfo = user_find(client, user, db_name)
+ if uinfo:
+ password = None
+ if not check_if_roles_changed(uinfo, roles, db_name):
+ module.exit_json(changed=False, user=user)
+
+ if module.check_mode:
+ module.exit_json(changed=True, user=user)
+ user_add(module, client, db_name, user, password, roles)
+ except Exception as e:
+ module.fail_json(msg='Unable to add or update user: %s' % to_native(e), exception=traceback.format_exc())
+ finally:
+ try:
+ client.close()
+ except Exception:
+ pass
+ # Here we can check password change if mongo provide a query for that : https://jira.mongodb.org/browse/SERVER-22848
+ # newuinfo = user_find(client, user, db_name)
+ # if uinfo['role'] == newuinfo['role'] and CheckPasswordHere:
+ # module.exit_json(changed=False, user=user)
+
+ if login_user is None and create_for_localhost_exception is not None:
+ # localhost exception applied.
+ try:
+ # touch the file
+ open(b_create_for_localhost_exception, 'wb').close()
+ except Exception as e:
+ module.fail_json(
+ changed=True,
+ msg='Added user but unable to touch create_for_localhost_exception file %s: %s' % (create_for_localhost_exception, to_native(e)),
+ exception=traceback.format_exc()
+ )
+
+ elif state == 'absent':
+ try:
+ user_remove(module, client, db_name, user)
+ except Exception as e:
+ module.fail_json(msg='Unable to remove user: %s' % to_native(e), exception=traceback.format_exc())
+ finally:
+ try:
+ client.close()
+ except Exception:
+ pass
+ module.exit_json(changed=True, user=user)
+
+
+if __name__ == '__main__':
+ main()