diff --git a/lib/ansible/modules/database/mongodb/mongodb_info.py b/lib/ansible/modules/database/mongodb/mongodb_info.py new file mode 100644 index 00000000000..97bb4684014 --- /dev/null +++ b/lib/ansible/modules/database/mongodb/mongodb_info.py @@ -0,0 +1,444 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) +# 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 = r''' +--- +module: mongodb_info + +short_description: Gather information about MongoDB instance. + +description: +- Gather information about MongoDB instance. + +author: Andrew Klychkov (@Andersson007) + +version_added: '2.10' + +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 + 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 + ssl: + description: + - Whether to use an SSL connection when connecting to the database. + required: no + type: bool + default: no + 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'] + +notes: + - Requires the pymongo Python package on the remote host, version 2.4.2+. + +requirements: [ 'pymongo' ] +''' + +EXAMPLES = r''' +- name: Gather all supported information + 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 + mongodb_info: + login_user: admin + login_password: secret + filter: databases, total_size + +- name: Gather all information except parameters + 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: {"new_user": {"_id": "config.new_user", "db": "config", "mechanisms": ["SCRAM-SHA-1", "SCRAM-SHA-256"], "roles": []}} +roles: + description: Role information. + returned: always + type: dict + sample: {"restore": {"db": "admin", "inheritedRoles": [], "isBuiltin": true, "roles": []}} +parameters: + description: Server parameters information. + returned: always + type: dict + sample: {"maxOplogTruncationPointsAfterStartup": 100, "maxOplogTruncationPointsDuringStartup": 100, "maxSessions": 1000000} +''' + +import traceback + +from uuid import UUID + +import ssl as ssl_lib +from distutils.version import LooseVersion + +PYMONGO_IMP_ERR = None +try: + from pymongo import version as PyMongoVersion + from pymongo import MongoClient +except ImportError: + PYMONGO_IMP_ERR = traceback.format_exc() + pymongo_found = False +else: + pymongo_found = True + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils._text import to_native +from ansible.module_utils.six import iteritems + + +# ========================================= +# MongoDB module specific support methods. +# + +def check_compatibility(module, srv_version, driver_version): + """Check the compatibility between the driver and the database. + + See: https://docs.mongodb.com/ecosystem/drivers/driver-compatibility-reference/#python-driver-compatibility + + Args: + module: Ansible module. + srv_version (LooseVersion): MongoDB server version. + driver_version (LooseVersion): Pymongo version. + """ + msg = 'pymongo driver version and MongoDB version are incompatible: ' + + if srv_version >= LooseVersion('4.2') and driver_version < LooseVersion('3.9'): + msg += 'you must use pymongo 3.9+ with MongoDB >= 4.2' + module.fail_json(msg=msg) + + elif srv_version >= LooseVersion('4.0') and driver_version < LooseVersion('3.7'): + msg += 'you must use pymongo 3.7+ with MongoDB >= 4.0' + module.fail_json(msg=msg) + + elif srv_version >= LooseVersion('3.6') and driver_version < LooseVersion('3.6'): + msg += 'you must use pymongo 3.6+ with MongoDB >= 3.6' + module.fail_json(msg=msg) + + elif srv_version >= LooseVersion('3.4') and driver_version < LooseVersion('3.4'): + msg += 'you must use pymongo 3.4+ with MongoDB >= 3.4' + module.fail_json(msg=msg) + + elif srv_version >= LooseVersion('3.2') and driver_version < LooseVersion('3.2'): + msg += 'you must use pymongo 3.2+ with MongoDB >= 3.2' + module.fail_json(msg=msg) + + elif srv_version >= LooseVersion('3.0') and driver_version <= LooseVersion('2.8'): + msg += 'you must use pymongo 2.8+ with MongoDB 3.0' + module.fail_json(msg=msg) + + elif srv_version >= LooseVersion('2.6') and driver_version <= LooseVersion('2.7'): + msg += 'you must use pymongo 2.7+ with MongoDB 2.6' + module.fail_json(msg=msg) + + +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. + """ + 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 == 'role': + continue + + roles_dict[elem['role']][key] = val + + return 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. + """ + 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 == 'user': + continue + + if isinstance(val, UUID): + val = val.hex + + users_dict[elem['user']][key] = val + + return 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 = 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), + ssl=dict(type='bool', required=False, default=False), + ssl_cert_reqs=dict(type='str', required=False, default='CERT_REQUIRED', + choices=['CERT_NONE', 'CERT_OPTIONAL', 'CERT_REQUIRED']), + 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) + + login_user = module.params['login_user'] + login_password = module.params['login_password'] + login_database = module.params['login_database'] + login_host = module.params['login_host'] + login_port = module.params['login_port'] + ssl = module.params['ssl'] + filter_ = module.params['filter'] + + if filter_: + filter_ = [f.strip() for f in filter_] + + connection_params = { + 'host': login_host, + 'port': login_port, + } + + if ssl: + connection_params['ssl'] = ssl + connection_params['ssl_cert_reqs'] = getattr(ssl_lib, module.params['ssl_cert_reqs']) + + client = MongoClient(**connection_params) + + if login_user: + try: + client.admin.authenticate(login_user, login_password, source=login_database) + except Exception as e: + module.fail_json(msg='Unable to authenticate: %s' % to_native(e)) + + # Get server version: + try: + srv_version = LooseVersion(client.server_info()['version']) + except Exception as e: + module.fail_json(msg='Unable to get MongoDB server version: %s' % to_native(e)) + + # Get driver version:: + driver_version = LooseVersion(PyMongoVersion) + + # Check driver and server version compatibility: + check_compatibility(module, srv_version, driver_version) + + # Initialize an object and start main work: + mongodb = MongoDbInfo(module, client) + + module.exit_json(changed=False, **mongodb.get_info(filter_)) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/mongodb_info/aliases b/test/integration/targets/mongodb_info/aliases new file mode 100644 index 00000000000..fc236c318d8 --- /dev/null +++ b/test/integration/targets/mongodb_info/aliases @@ -0,0 +1,8 @@ +destructive +shippable/posix/group1 +skip/aix +skip/osx +skip/freebsd +skip/ubuntu +skip/redhat +skip/opensuse diff --git a/test/integration/targets/mongodb_info/defaults/main.yml b/test/integration/targets/mongodb_info/defaults/main.yml new file mode 100644 index 00000000000..5f41a8becf7 --- /dev/null +++ b/test/integration/targets/mongodb_info/defaults/main.yml @@ -0,0 +1,4 @@ +mongodb_default_port: 27017 +mongodb_admin_user: admin +mongodb_admin_password: admin +mongodb_default_db: admin diff --git a/test/integration/targets/mongodb_info/meta/main.yml b/test/integration/targets/mongodb_info/meta/main.yml new file mode 100644 index 00000000000..31081e2ede1 --- /dev/null +++ b/test/integration/targets/mongodb_info/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: +- setup_mongodb_v4 +- setup_remote_tmp_dir diff --git a/test/integration/targets/mongodb_info/tasks/main.yml b/test/integration/targets/mongodb_info/tasks/main.yml new file mode 100644 index 00000000000..859d4a66ae9 --- /dev/null +++ b/test/integration/targets/mongodb_info/tasks/main.yml @@ -0,0 +1,8 @@ +# Copyright 2020, Andrew Klychkov +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Main mongodb_info module tests +- import_tasks: mongodb_info.yml + when: + - ansible_distribution == 'CentOS' + - ansible_distribution_major_version is version('7', '>=') diff --git a/test/integration/targets/mongodb_info/tasks/mongodb_info.yml b/test/integration/targets/mongodb_info/tasks/mongodb_info.yml new file mode 100644 index 00000000000..3690622475a --- /dev/null +++ b/test/integration/targets/mongodb_info/tasks/mongodb_info.yml @@ -0,0 +1,60 @@ +# Copyright 2020, Andrew Klychkov +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +- vars: + task_parameters: &task_parameters + register: result + mongo_parameters: &mongo_parameters + login_port: '{{ mongodb_default_port }}' + login_user: '{{ mongodb_admin_user }}' + login_password: '{{ mongodb_admin_password }}' + login_database: '{{ mongodb_default_db }}' + + block: + + - name: Get info + <<: *task_parameters + mongodb_info: + <<: *mongo_parameters + + - assert: + that: + - result is not changed + - result.general.version == '4.2.3' + - result.databases.admin + - result.total_size + - result.users.admin + - result.roles.backup + - result.parameters.logLevel == 0 + + - name: Get info with filter + <<: *task_parameters + mongodb_info: + <<: *mongo_parameters + filter: general, total_size + + - assert: + that: + - result is not changed + - result.general.version == '4.2.3' + - result.total_size + - result.databases is not defined + - result.parameters is not defined + - result.users is not defined + - result.roles is not defined + + - name: Get info with filter + <<: *task_parameters + mongodb_info: + <<: *mongo_parameters + filter: '!parameters' + + - assert: + that: + - result is not changed + - result.general.version == '4.2.3' + - result.databases.admin + - result.total_size + - result.users.admin + - result.roles.backup + - result.parameters is not defined diff --git a/test/integration/targets/setup_mongodb_v4/defaults/main.yml b/test/integration/targets/setup_mongodb_v4/defaults/main.yml new file mode 100644 index 00000000000..89e0c4df560 --- /dev/null +++ b/test/integration/targets/setup_mongodb_v4/defaults/main.yml @@ -0,0 +1,13 @@ +mongodb_repo_name: mongodb +mongodb_repo_descr: MongoDB Repository +mongodb_repo_link: http://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/4.2/$basearch/ +mongodb_gpgkey: https://www.mongodb.org/static/pgp/server-4.2.asc +mongodb_pkgs: +- mongodb-org + +pymongo_repo: git://github.com/mongodb/mongo-python-driver.git + +mongodb_default_port: 27017 +mongodb_admin_user: admin +mongodb_admin_password: admin +mongodb_default_db: admin diff --git a/test/integration/targets/setup_mongodb_v4/handlers/main.yml b/test/integration/targets/setup_mongodb_v4/handlers/main.yml new file mode 100644 index 00000000000..f3379859df4 --- /dev/null +++ b/test/integration/targets/setup_mongodb_v4/handlers/main.yml @@ -0,0 +1,10 @@ +- name: Remove MongoDB packages + yum: + name: '{{ mongodb_pkgs }}' + state: absent + +- name: Stop MongoDB + systemd: + name: mongod.service + state: stopped + notify: Stop MongoDB diff --git a/test/integration/targets/setup_mongodb_v4/tasks/main.yml b/test/integration/targets/setup_mongodb_v4/tasks/main.yml new file mode 100644 index 00000000000..5e0f49cd3ff --- /dev/null +++ b/test/integration/targets/setup_mongodb_v4/tasks/main.yml @@ -0,0 +1,7 @@ +# Copyright 2020, Andrew Klychkov +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +- import_tasks: setup_mongodb.yml + when: + - ansible_distribution == 'CentOS' + - ansible_distribution_major_version is version('7', '>=') diff --git a/test/integration/targets/setup_mongodb_v4/tasks/setup_mongodb.yml b/test/integration/targets/setup_mongodb_v4/tasks/setup_mongodb.yml new file mode 100644 index 00000000000..e1b2c327958 --- /dev/null +++ b/test/integration/targets/setup_mongodb_v4/tasks/setup_mongodb.yml @@ -0,0 +1,34 @@ +# Copyright 2020, Andrew Klychkov +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +- name: Set up repo + yum_repository: + name: '{{ mongodb_repo_name }}' + description: '{{ mongodb_repo_descr }}' + baseurl: '{{ mongodb_repo_link }}' + gpgkey: '{{ mongodb_gpgkey }}' + gpgcheck: yes + +- name: Install MongoDB packages + yum: + name: '{{ mongodb_pkgs }}' + state: present + notify: Remove MongoDB packages + +- name: Install pymongo + shell: git clone '{{ pymongo_repo }}' pymongo && cd pymongo/ && python setup.py install + +- name: Start MongoDB + systemd: + name: mongod.service + state: started + notify: Stop MongoDB + +- name: Create admin user + mongodb_user: + login_port: '{{ mongodb_default_port }}' + database: '{{ mongodb_default_db }}' + name: '{{ mongodb_admin_user }}' + password: '{{ mongodb_admin_password }}' + roles: root + state: present