#!/usr/bin/python # Copyright (c) 2021, Kris Budde = 2.7 - pymssql author: - Kris Budde (@kbudde) ''' EXAMPLES = r''' - name: Check DB connection community.general.mssql_script: login_user: "{{ mssql_login_user }}" login_password: "{{ mssql_login_password }}" login_host: "{{ mssql_host }}" login_port: "{{ mssql_port }}" db: master script: "SELECT 1" - name: Query with parameter community.general.mssql_script: login_user: "{{ mssql_login_user }}" login_password: "{{ mssql_login_password }}" login_host: "{{ mssql_host }}" login_port: "{{ mssql_port }}" script: | SELECT name, state_desc FROM sys.databases WHERE name = %(dbname)s params: dbname: msdb register: result_params - assert: that: - result_params.query_results[0][0][0][0] == 'msdb' - result_params.query_results[0][0][0][1] == 'ONLINE' - name: two batches with default output community.general.mssql_script: login_user: "{{ mssql_login_user }}" login_password: "{{ mssql_login_password }}" login_host: "{{ mssql_host }}" login_port: "{{ mssql_port }}" script: | SELECT 'Batch 0 - Select 0' SELECT 'Batch 0 - Select 1' GO SELECT 'Batch 1 - Select 0' register: result_batches - assert: that: - result_batches.query_results | length == 2 # two batch results - result_batches.query_results[0] | length == 2 # two selects in first batch - result_batches.query_results[0][0] | length == 1 # one row in first select - result_batches.query_results[0][0][0] | length == 1 # one column in first row - result_batches.query_results[0][0][0][0] == 'Batch 0 - Select 0' # each row contains a list of values. - name: two batches with dict output community.general.mssql_script: login_user: "{{ mssql_login_user }}" login_password: "{{ mssql_login_password }}" login_host: "{{ mssql_host }}" login_port: "{{ mssql_port }}" output: dict script: | SELECT 'Batch 0 - Select 0' as b0s0 SELECT 'Batch 0 - Select 1' as b0s1 GO SELECT 'Batch 1 - Select 0' as b1s0 register: result_batches_dict - assert: that: - result_batches_dict.query_results_dict | length == 2 # two batch results - result_batches_dict.query_results_dict[0] | length == 2 # two selects in first batch - result_batches_dict.query_results_dict[0][0] | length == 1 # one row in first select - result_batches_dict.query_results_dict[0][0][0]['b0s0'] == 'Batch 0 - Select 0' # column 'b0s0' of first row ''' RETURN = r''' query_results: description: List of batches (queries separated by V(GO) keyword). type: list elements: list returned: success and O(output=default) sample: [[[["Batch 0 - Select 0"]], [["Batch 0 - Select 1"]]], [[["Batch 1 - Select 0"]]]] contains: queries: description: - List of result sets of each query. - If a query returns no results, the results of this and all the following queries will not be included in the output. - Use the V(GO) keyword in O(script) to separate queries. type: list elements: list contains: rows: description: List of rows returned by query. type: list elements: list contains: column_value: description: - List of column values. - Any non-standard JSON type is converted to string. type: list example: ["Batch 0 - Select 0"] returned: success, if output is default query_results_dict: description: List of batches (queries separated by V(GO) keyword). type: list elements: list returned: success and O(output=dict) sample: [[[["Batch 0 - Select 0"]], [["Batch 0 - Select 1"]]], [[["Batch 1 - Select 0"]]]] contains: queries: description: - List of result sets of each query. - If a query returns no results, the results of this and all the following queries will not be included in the output. Use 'GO' keyword to separate queries. type: list elements: list contains: rows: description: List of rows returned by query. type: list elements: list contains: column_dict: description: - Dictionary of column names and values. - Any non-standard JSON type is converted to string. type: dict example: {"col_name": "Batch 0 - Select 0"} returned: success, if output is dict ''' from ansible.module_utils.basic import AnsibleModule, missing_required_lib import traceback import json PYMSSQL_IMP_ERR = None try: import pymssql except ImportError: PYMSSQL_IMP_ERR = traceback.format_exc() MSSQL_FOUND = False else: MSSQL_FOUND = True def clean_output(o): return str(o) def run_module(): module_args = dict( name=dict(required=False, aliases=['db'], default=''), login_user=dict(), login_password=dict(no_log=True), login_host=dict(required=True), login_port=dict(type='int', default=1433), script=dict(required=True), output=dict(default='default', choices=['dict', 'default']), params=dict(type='dict'), ) result = dict( changed=False, ) module = AnsibleModule( argument_spec=module_args, supports_check_mode=True ) if not MSSQL_FOUND: module.fail_json(msg=missing_required_lib( 'pymssql'), exception=PYMSSQL_IMP_ERR) db = module.params['name'] login_user = module.params['login_user'] login_password = module.params['login_password'] login_host = module.params['login_host'] login_port = module.params['login_port'] script = module.params['script'] output = module.params['output'] sql_params = module.params['params'] login_querystring = login_host if login_port != 1433: login_querystring = "%s:%s" % (login_host, login_port) if login_user is not None and login_password is None: module.fail_json( msg="when supplying login_user argument, login_password must also be provided") try: conn = pymssql.connect( user=login_user, password=login_password, host=login_querystring, database=db) cursor = conn.cursor() except Exception as e: if "Unknown database" in str(e): errno, errstr = e.args module.fail_json(msg="ERROR: %s %s" % (errno, errstr)) else: module.fail_json(msg="unable to connect, check login_user and login_password are correct, or alternatively check your " "@sysconfdir@/freetds.conf / ${HOME}/.freetds.conf") conn.autocommit(True) query_results_key = 'query_results' if output == 'dict': cursor = conn.cursor(as_dict=True) query_results_key = 'query_results_dict' # Process the script into batches queries = [] current_batch = [] for statement in script.splitlines(True): # Ignore the Byte Order Mark, if found if statement.strip() == '\uFEFF': continue # Assume each 'GO' is on its own line but may have leading/trailing whitespace # and be of mixed-case if statement.strip().upper() != 'GO': current_batch.append(statement) else: queries.append(''.join(current_batch)) current_batch = [] if len(current_batch) > 0: queries.append(''.join(current_batch)) result['changed'] = True if module.check_mode: module.exit_json(**result) query_results = [] for query in queries: # Catch and exit on any bad query errors try: cursor.execute(query, sql_params) qry_result = [] rows = cursor.fetchall() while rows: qry_result.append(rows) rows = cursor.fetchall() query_results.append(qry_result) except Exception as e: # We know we executed the statement so this error just means we have no resultset # which is ok (eg UPDATE/INSERT) if ( type(e).__name__ == 'OperationalError' and str(e) == 'Statement not executed or executed statement has no resultset' ): query_results.append([]) else: error_msg = '%s: %s' % (type(e).__name__, str(e)) module.fail_json(msg="query failed", query=query, error=error_msg, **result) # ensure that the result is json serializable qry_results = json.loads(json.dumps(query_results, default=clean_output)) result[query_results_key] = qry_results module.exit_json(**result) def main(): run_module() if __name__ == '__main__': main()