diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:03:42 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:03:42 +0000 |
commit | 66cec45960ce1d9c794e9399de15c138acb18aed (patch) | |
tree | 59cd19d69e9d56b7989b080da7c20ef1a3fe2a5a /ansible_collections/community/mongodb/plugins/module_utils | |
parent | Initial commit. (diff) | |
download | ansible-upstream.tar.xz ansible-upstream.zip |
Adding upstream version 7.3.0+dfsg.upstream/7.3.0+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/mongodb/plugins/module_utils')
3 files changed, 596 insertions, 0 deletions
diff --git a/ansible_collections/community/mongodb/plugins/module_utils/__init__.py b/ansible_collections/community/mongodb/plugins/module_utils/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/ansible_collections/community/mongodb/plugins/module_utils/__init__.py diff --git a/ansible_collections/community/mongodb/plugins/module_utils/mongodb_common.py b/ansible_collections/community/mongodb/plugins/module_utils/mongodb_common.py new file mode 100644 index 00000000..6726c8a0 --- /dev/null +++ b/ansible_collections/community/mongodb/plugins/module_utils/mongodb_common.py @@ -0,0 +1,461 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type +from ansible.module_utils.basic import missing_required_lib # pylint: disable=unused-import: +from ansible.module_utils.six.moves import configparser +from ansible.module_utils._text import to_native +import traceback +import os +import ssl as ssl_lib + + +try: + from bson.timestamp import Timestamp + from bson import ObjectId +except ImportError: + pass # TODO Should we do something here or are we covered by pymongo? + +MongoClient = None +PYMONGO_IMP_ERR = None +pymongo_found = None +PyMongoVersion = None +ConnectionFailure = None +OperationFailure = None +TYPES_NEED_TO_CONVERT = None + +try: + from pymongo.errors import ConnectionFailure # pylint: disable=unused-import: + from pymongo.errors import OperationFailure # pylint: disable=unused-import: + from pymongo import version as PyMongoVersion + from pymongo import MongoClient + pymongo_found = True +except ImportError: + PYMONGO_IMP_ERR = traceback.format_exc() + pymongo_found = False + +try: + TYPES_NEED_TO_CONVERT = (Timestamp, ObjectId) +except NameError: + pass # sanity tests + + +def check_compatibility(module, srv_version, driver_version): + if 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 + else: # for case when the member is not an arbiter + if current_member_config[member_index]["priority"] != \ + member_config[new_member_index].get(config_item, 1.0): + different = True + msg = "var different {0}".format(config_item) + break + return different # , msg + + +def lists_are_different(list1, list2): + diff = False + if sorted(list1) != sorted(list2): + diff = True + return diff + + +# Taken from https://github.com/ansible-collections/community.postgresql/blob/main/plugins/module_utils/postgres.py#L420 +def convert_to_supported(val): + """Convert unsupported type to appropriate. + Args: + val (any) -- Any value fetched from database. + Returns value of appropriate type. + """ + if isinstance(val, Timestamp): + return str(val) + elif isinstance(val, ObjectId): + return str(val) + + return val # By default returns the same value + + +def convert_bson_values_recur(mydict): + """ + Converts values that Ansible doesn't like + # https://github.com/ansible-collections/community.mongodb/issues/462 + """ + if isinstance(mydict, dict): + for key, value in mydict.items(): + if isinstance(value, dict): + mydict[key] = convert_bson_values_recur(value) + else: + if isinstance(value, TYPES_NEED_TO_CONVERT): + mydict[key] = convert_to_supported(value) + else: + mydict[key] = value + return mydict diff --git a/ansible_collections/community/mongodb/plugins/module_utils/mongodb_shell.py b/ansible_collections/community/mongodb/plugins/module_utils/mongodb_shell.py new file mode 100644 index 00000000..5108757c --- /dev/null +++ b/ansible_collections/community/mongodb/plugins/module_utils/mongodb_shell.py @@ -0,0 +1,135 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import shlex +import pipes +import re +import json +import os + + +def escape_param(param): + ''' + Escapes the given parameter + @param - The parameter to escape + ''' + escaped = None + if hasattr(shlex, 'quote'): + escaped = shlex.quote(param) + elif hasattr(pipes, 'quote'): + escaped = pipes.quote(param) + else: + escaped = "'" + param.replace("'", "'\\''") + "'" + return escaped + + +def add_arg_to_cmd(cmd_list, param_name, param_value, is_bool=False, omit=None): + """ + @cmd_list - List of cmd args. + @param_name - Param name / flag. + @param_value - Value of the parameter. + @is_bool - Flag is a boolean and has no value. + @omit - List of parameter to omit from the command line. + """ + if param_name.replace('-', '') not in omit: + if is_bool is False and param_value is not None: + cmd_list.append(param_name) + if param_name == "--eval": + cmd_list.append("{0}".format(escape_param(param_value))) + else: + cmd_list.append(param_value) + elif is_bool is True: + cmd_list.append(param_name) + return cmd_list + + +def extract_json_document(output): + """ + This is for specific type of mongo shell return data in the format SomeText() + https://github.com/ansible-collections/community.mongodb/issues/436 + i.e. + + """ + output = output.strip() + if re.match(r"^[a-zA-Z].*\(", output) and output.endswith(')'): + first_bracket = output.find('{') + last_bracket = output.rfind('}') + if first_bracket > 0 and last_bracket > 0: + tmp = output[first_bracket:last_bracket + 1] + tmp = tmp.replace('\n', '') + tmp = tmp.replace('\t', '') + if tmp is not None: + output = tmp + return output + + +def transform_output(output, transform_type, split_char): + output = extract_json_document(output) + if transform_type == "auto": # determine what transform_type to perform + if output.strip().startswith("{") or output.strip().startswith("["): + transform_type = "json" + elif isinstance(output.strip().split(None), list): # Splits on whitespace + transform_type = "split" + split_char = None + elif isinstance(output.strip().split(","), list): + transform_type = "split" + split_char = "," + elif isinstance(output.strip().split(" "), list): + transform_type = "split" + split_char = " " + elif isinstance(output.strip().split("|"), list): + transform_type = "split" + split_char = "|" + elif isinstance(output.strip().split("\t"), list): + transform_type = "split" + split_char = "\t" + else: + transform_type = "raw" + if transform_type == "json": + try: + output = json.loads(output) + except json.decoder.JSONDecodeError: + # Strip Extended JSON stuff like: + # "_id": ObjectId("58f56171ee9d4bd5e610d6b7"), + # "count": NumberLong(999), + output = re.sub(r'\:\s*\S+\s*\(\s*(\S+)\s*\)', r':\1', output) + try: + output = json.dumps(output, separators=(',', ':')) + doc = json.loads(output) + except json.decoder.JSONDecodeError as excep: + raise excep + elif transform_type == "split": + output = output.strip().split(split_char) + elif transform_type == "raw": + output = output.strip() + return output + + +def get_hash_value(module): + ''' + Returns the hash value of either the provided file or eval command + ''' + hash_value = None + try: + import hashlib + except ImportError as excep: + module.fail_json(msg="Unable to import hashlib: {0}".format(excep.message)) + if module.params['file'] is not None: + hash_value = hashlib.md5(module.params['file'].encode('utf-8')).hexdigest() + else: + hash_value = hashlib.md5(module.params['eval'].encode('utf-8')).hexdigest() + return hash_value + + +def touch(fname, times=None): + with open(fname, 'a'): + os.utime(fname, times) + + +def detect_if_cmd_exist(cmd="mongosh"): + path = os.getenv('PATH') + for folder in path.split(os.path.pathsep): + mongoCmd = os.path.join(folder, cmd) + if os.path.exists(mongoCmd) and os.access(mongoCmd, os.X_OK): + return True + return False |