From 024e40d5f4a475e072f3c37146b2e7192fdb4b02 Mon Sep 17 00:00:00 2001 From: Marcos Diez <marcos@unitron.com.br> Date: Tue, 3 Jan 2017 13:40:02 -0300 Subject: [PATCH] new lookup module: mongodb (#15057) * new lookup module: mongodb lookup * fix versionadded for MongoDB Lookup * tests should run again * removed use of basestring * we don't use iteritems anymore * run tests again * run tests again2 * run tests again3 * run tests again4 --- docsite/rst/playbooks_lookups.rst | 92 ++++++++++++++ lib/ansible/plugins/lookup/mongodb.py | 167 ++++++++++++++++++++++++++ 2 files changed, 259 insertions(+) create mode 100644 lib/ansible/plugins/lookup/mongodb.py diff --git a/docsite/rst/playbooks_lookups.rst b/docsite/rst/playbooks_lookups.rst index 007f2255938..09c4c966028 100644 --- a/docsite/rst/playbooks_lookups.rst +++ b/docsite/rst/playbooks_lookups.rst @@ -368,6 +368,98 @@ TLSA usage, selector, mtype, cert TXT strings ========== ============================================================================= +.. _mongodb_lookup: + +MongoDB Lookup +`````````````` +.. versionadded:: 2.3 + +.. warning:: This lookup depends on the `pymongo 2.4+ <http://www.mongodb.org/>`_ + library. + + +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. + +Currently, the following parameters are supported. + +=========================== ========= ======= ==================== ======================================================================================================================================================================= +Parameter Mandatory Type Default Value Comment +--------------------------- --------- ------- -------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +connection_string no string mongodb://localhost/ Can be any valid MongoDB connection string, supporting authentication, replicasets, etc. More info at https://docs.mongodb.org/manual/reference/connection-string/ +extra_connection_parameters no dict {} Dictionary with extra parameters like ssl, ssl_keyfile, maxPoolSize etc... Check the full list here: https://api.mongodb.org/python/current/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient +database yes string Name of the database which the query will be made +collection yes string Name of the collection which the query will be made +filter no dict [pymongo default] Criteria of the output Example: { "hostname": "batman" } +projection no dict [pymongo default] Fields you want returned. Example: { "pid": True , "_id" : False , "hostname" : True } +skip no integer [pymongo default] How many results should be skept +limit no integer [pymongo default] How many results should be shown +sort no list [pymongo default] Sorting rules. Please notice the constats are replaced by strings. [ [ "startTime" , "ASCENDING" ] , [ "age", "DESCENDING" ] ] +[any find() parameter] no [any] [pymongo default] Every parameter with exception to *connection_string*, *database* and *collection* are passed to pymongo directly. +=========================== ========= ======= ==================== ======================================================================================================================================================================= + +Please check https://api.mongodb.org/python/current/api/pymongo/collection.html?highlight=find#pymongo.collection.Collection.find for more detais. + +Since there are too many parameters for this lookup method, below is a sample playbook which shows its usage and a nice way to feed the parameters:: + + --- + - hosts: all + gather_facts: false + + vars: + mongodb_parameters: + #optional parameter, default = "mongodb://localhost/" + # connection_string: "mongodb://localhost/" + # extra_connection_parameters: { "ssl" : True , "ssl_certfile": /etc/self_signed_certificate.pem" } + + #mandatory parameters + database: 'local' + collection: "startup_log" + + #optional query parameters + #we accept any parameter from the normal mongodb query. + # the offical documentation is here + # https://api.mongodb.org/python/current/api/pymongo/collection.html?highlight=find#pymongo.collection.Collection.find + # filter: { "hostname": "batman" } + projection: { "pid": True , "_id" : False , "hostname" : True } + # skip: 0 + limit: 1 + # sort: [ [ "startTime" , "ASCENDING" ] , [ "age", "DESCENDING" ] ] + + tasks: + - debug: msg="Mongo has already started with the following PID [{{ item.pid }}]" + with_items: + - "{{ lookup('mongodb', mongodb_parameters) }}" + + + +Sample output:: + + --- + mdiez@batman:~/ansible$ ansible-playbook m.yml -i localhost.ini + + PLAY [all] ********************************************************************* + + TASK [debug] ******************************************************************* + Sunday 20 March 2016 22:40:39 +0200 (0:00:00.023) 0:00:00.023 ********** + ok: [localhost] => (item={u'hostname': u'batman', u'pid': 60639L}) => { + "item": { + "hostname": "batman", + "pid": 60639 + }, + "msg": "Mongo has already started with the following PID [60639]" + } + + PLAY RECAP ********************************************************************* + localhost : ok=1 changed=0 unreachable=0 failed=0 + + Sunday 20 March 2016 22:40:39 +0200 (0:00:00.067) 0:00:00.091 ********** + =============================================================================== + debug ------------------------------------------------------------------- 0.07s + mdiez@batman:~/ansible$ + + .. _more_lookups: More Lookups diff --git a/lib/ansible/plugins/lookup/mongodb.py b/lib/ansible/plugins/lookup/mongodb.py new file mode 100644 index 00000000000..49262769b80 --- /dev/null +++ b/lib/ansible/plugins/lookup/mongodb.py @@ -0,0 +1,167 @@ +# (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) +from __future__ import unicode_literals +from ansible.module_utils.six import string_types +import datetime + +__metaclass__ = type + +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 + + +from ansible.errors import AnsibleError +from ansible.plugins.lookup import LookupBase + +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("Error. Sort parameters must be a list, not [ {} ]".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 == "ASCENDING": + item[1] = ASCENDING + elif sort_order == "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, (int, long, 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 = reslut[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 "{}".format(result) + + + def run(self, terms, variables, **kwargs): + + ret = [] + for term in terms: + ''' + Makes a MongoDB query and returns the output as a valid list of json. + Timestamps are converted to epoch integers/longs. + + Here is a sample playbook that uses it: + +------------------------------------------------------------------------------- +- hosts: all + gather_facts: false + + vars: + mongodb_parameters: + #optional parameter, default = "mongodb://localhost/" + # connection_string: "mongodb://localhost/" + + #mandatory parameters + database: 'local' + collection: "startup_log" + + #optional query parameters + #we accept any parameter from the normal mongodb query. + # the offical documentation is here + # https://api.mongodb.org/python/current/api/pymongo/collection.html?highlight=find#pymongo.collection.Collection.find + # filter: { "hostname": "batman" } + # projection: { "pid": True , "_id" : False , "hostname" : True } + # skip: 0 + # limit: 1 + # sort: [ [ "startTime" , "ASCENDING" ] , [ "age", "DESCENDING" ] ] + # extra_connection_parameters = { } + + # dictionary with extra parameters like ssl, ssl_keyfile, maxPoolSize etc... + # the full list is available here. It varies from PyMongo version + # https://api.mongodb.org/python/current/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient + + tasks: + - debug: msg="Mongo has already started with the following PID [{{ item.pid }}] - full_data {{ item }} " + with_items: + - "{{ lookup('mongodb', mongodb_parameters) }}" +------------------------------------------------------------------------------- + ''' + + connection_string = term.get('connection_string', "mongodb://localhost") + database = term["database"] + collection = term['collection'] + extra_connection_parameters = term.get('extra_connection_parameters', {}) + + if "extra_connection_parameters" in term: + del term["extra_connection_parameters"] + if "connection_string" in term: + del term["connection_string"] + del term["database"] + del term["collection"] + + if "sort" in term: + term["sort"] = self._fix_sort_parameter(term["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('unable to connect to database: %s' % str(e)) + + + + return ret