Init: mediaserver

This commit is contained in:
2023-02-08 12:13:28 +01:00
parent 848bc9739c
commit f7c23d4ba9
31914 changed files with 6175775 additions and 0 deletions

View File

@@ -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__()

View File

@@ -0,0 +1,44 @@
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
'''

View File

@@ -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
'''

View File

@@ -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

View File

@@ -0,0 +1,410 @@
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
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
MongoClient = None
PYMONGO_IMP_ERR = None
pymongo_found = None
PyMongoVersion = None
ConnectionFailure = None
OperationFailure = None
try:
from pymongo.errors import ConnectionFailure
from pymongo.errors import OperationFailure
from pymongo import version as PyMongoVersion
from pymongo import MongoClient
pymongo_found = True
except ImportError:
PYMONGO_IMP_ERR = traceback.format_exc()
pymongo_found = False
def check_compatibility(module, srv_version, driver_version):
if driver_version.startswith('3.12') or driver_version.startswith('4'):
if int(srv_version[0]) < 4:
if module.params['strict_compatibility']:
module.fail_json("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("You must use pymongo 3.12+ or 4+.")
else:
module.warn("You should use pymongo 3.12+ or 4+ but {0} was found.".format(driver_version))
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),
)
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):
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']
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 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:
if int(PyMongoVersion[0]) < 4: # pymongo < 4
client.admin.authenticate(login_user, login_password, source=login_database)
else: # 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, 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:
if int(PyMongoVersion[0]) < 4: # pymongo < 4
client.admin.authenticate(login_user, login_password, source=login_database)
else: # pymongo >= 4
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 (PyMongoVersion.startswith('3.12') or int(PyMongoVersion[0]) > 4) \
or 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
return different # , msg
def lists_are_different(list1, list2):
diff = False
if sorted(list1) != sorted(list2):
diff = True
return diff

View File

@@ -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()

View File

@@ -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()

View File

@@ -0,0 +1,313 @@
#!/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 (
missing_required_lib,
mongodb_common_argument_spec,
mongo_auth,
PYMONGO_IMP_ERR,
pymongo_found,
get_mongodb_client,
)
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))
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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -0,0 +1,191 @@
#!/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,
check_srv_version,
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()

View File

@@ -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()

View File

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

View File

@@ -0,0 +1,345 @@
#!/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
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()

View File

@@ -0,0 +1,305 @@
#!/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.
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,
ssl_connection_options,
mongo_auth,
PYMONGO_IMP_ERR,
pymongo_found,
get_mongodb_client,
)
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))
shard_created = False
dbs_to_shard = []
if sharded_databases is not None:
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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -0,0 +1,448 @@
#!/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:
- mongo or 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 currently uses the mongo shell by default. This will change to mongosh in an upcoming version and support for mongo will be dropped
extends_documentation_fragment:
- community.mongodb.login_options
options:
mongo_cmd:
description:
- The MongoDB shell command.
type: str
default: "mongo"
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 re
import json
import os
import shlex
import pipes
__metaclass__ = type
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
mongodb_common_argument_spec
)
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.
WriteResult({
"nInserted" : 0,
"writeError" : {
"code" : 11000,
"errmsg" : "E11000 duplicate key error collection: state.hosts index: _id_ dup key: { _id: \"r1\" }"
}
})
"""
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("\"", '\\\"')
tmp = tmp.replace('\n', '')
tmp = tmp.replace('\t', '')
if tmp is not None:
output = tmp
# elif re.match(r"^[a-zA-Z].*", output):
# first_bracket = output.find('{')
# last_bracket = output.rfind('}')
# tmp = output[first_bracket:last_bracket + 1]
# 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.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 main():
argument_spec = mongodb_common_argument_spec(ssl_options=False)
argument_spec.update(
mongo_cmd=dict(type='str', default="mongo"),
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'] == "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()

View File

@@ -0,0 +1,138 @@
#!/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
mongodb_maintenance:
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,
ssl_connection_options,
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()

View File

@@ -0,0 +1,354 @@
#!/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,
ssl_connection_options,
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()

View File

@@ -0,0 +1,249 @@
#!/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,
ssl_connection_options,
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()

View File

@@ -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()