mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
Adding ODBC module (#606)
* Adding ODBC module * Adding symink and fixing docs and argspec * Another sanity issue * Hopefully last fix for elements * Making changes suggested by felixfontein * Making changes suggested by Andersson007 * Removing defaults and added info in description * Fixing line too long * More cleanup suggested by felixfontein * Changing module call
This commit is contained in:
parent
9e76fdc668
commit
a86195623b
9 changed files with 374 additions and 0 deletions
157
plugins/modules/database/misc/odbc.py
Normal file
157
plugins/modules/database/misc/odbc.py
Normal file
|
@ -0,0 +1,157 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2019, John Westcott <john.westcott.iv@redhat.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: odbc
|
||||
author: "John Westcott IV (@john-westcott-iv)"
|
||||
version_added: "1.0.0"
|
||||
short_description: Execute SQL via ODBC
|
||||
description:
|
||||
- Read/Write info via ODBC drivers.
|
||||
options:
|
||||
dsn:
|
||||
description:
|
||||
- The connection string passed into ODBC.
|
||||
required: yes
|
||||
type: str
|
||||
query:
|
||||
description:
|
||||
- The SQL query to perform.
|
||||
required: yes
|
||||
type: str
|
||||
params:
|
||||
description:
|
||||
- Parameters to pass to the SQL query.
|
||||
type: list
|
||||
elements: str
|
||||
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "pyodbc"
|
||||
|
||||
notes:
|
||||
- "Like the command module, this module always returns changed = yes whether or not the query would change the database."
|
||||
- "To alter this behavior you can use C(changed_when): [yes or no]."
|
||||
- "For details about return values (description and row_count) see U(https://github.com/mkleehammer/pyodbc/wiki/Cursor)."
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Set some values in the test db
|
||||
community.general.odbc:
|
||||
dsn: "DRIVER={ODBC Driver 13 for SQL Server};Server=db.ansible.com;Database=my_db;UID=admin;PWD=password;"
|
||||
query: "Select * from table_a where column1 = ?"
|
||||
params:
|
||||
- "value1"
|
||||
changed_when: no
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
results:
|
||||
description: List of lists of strings containing selected rows, likely empty for DDL statements.
|
||||
returned: success
|
||||
type: list
|
||||
elements: list
|
||||
description:
|
||||
description: "List of dicts about the columns selected from the cursors, likely empty for DDL statements. See notes."
|
||||
returned: success
|
||||
type: list
|
||||
elements: dict
|
||||
row_count:
|
||||
description: "The number of rows selected or modified according to the cursor defaults to -1. See notes."
|
||||
returned: success
|
||||
type: str
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
HAS_PYODBC = None
|
||||
try:
|
||||
import pyodbc
|
||||
HAS_PYODBC = True
|
||||
except ImportError as e:
|
||||
HAS_PYODBC = False
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
dsn=dict(type='str', required=True, no_log=True),
|
||||
query=dict(type='str', required=True),
|
||||
params=dict(type='list', elements='str'),
|
||||
),
|
||||
)
|
||||
|
||||
dsn = module.params.get('dsn')
|
||||
query = module.params.get('query')
|
||||
params = module.params.get('params')
|
||||
|
||||
if not HAS_PYODBC:
|
||||
module.fail_json(msg=missing_required_lib('pyodbc'))
|
||||
|
||||
# Try to make a connection with the DSN
|
||||
connection = None
|
||||
try:
|
||||
connection = pyodbc.connect(dsn)
|
||||
except Exception as e:
|
||||
module.fail_json(msg='Failed to connect to DSN: {0}'.format(to_native(e)))
|
||||
|
||||
result = dict(
|
||||
changed=True,
|
||||
description=[],
|
||||
row_count=-1,
|
||||
results=[],
|
||||
)
|
||||
|
||||
try:
|
||||
cursor = connection.cursor()
|
||||
|
||||
if params:
|
||||
cursor.execute(query, params)
|
||||
else:
|
||||
cursor.execute(query)
|
||||
cursor.commit()
|
||||
try:
|
||||
# Get the rows out into an 2d array
|
||||
for row in cursor.fetchall():
|
||||
new_row = []
|
||||
for column in row:
|
||||
new_row.append("{0}".format(column))
|
||||
result['results'].append(new_row)
|
||||
|
||||
# Return additional information from the cursor
|
||||
for row_description in cursor.description:
|
||||
description = {}
|
||||
description['name'] = row_description[0]
|
||||
description['type'] = row_description[1].__name__
|
||||
description['display_size'] = row_description[2]
|
||||
description['internal_size'] = row_description[3]
|
||||
description['precision'] = row_description[4]
|
||||
description['scale'] = row_description[5]
|
||||
description['nullable'] = row_description[6]
|
||||
result['description'].append(description)
|
||||
|
||||
result['row_count'] = cursor.rowcount
|
||||
except pyodbc.ProgrammingError as pe:
|
||||
pass
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Exception while reading rows: {0}".format(to_native(e)))
|
||||
|
||||
cursor.close()
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Failed to execute query: {0}".format(to_native(e)))
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
1
plugins/modules/odbc.py
Symbolic link
1
plugins/modules/odbc.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
./database/misc/odbc.py
|
5
tests/integration/targets/odbc/aliases
Normal file
5
tests/integration/targets/odbc/aliases
Normal file
|
@ -0,0 +1,5 @@
|
|||
destructive
|
||||
shippable/posix/group1
|
||||
skip/osx
|
||||
skip/rhel8.0
|
||||
skip/freebsd
|
27
tests/integration/targets/odbc/defaults/main.yml
Normal file
27
tests/integration/targets/odbc/defaults/main.yml
Normal file
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
# defaults file for test_postgresql_db
|
||||
my_user: 'ansible_user'
|
||||
my_pass: 'md5d5e044ccd9b4b8adc89e8fed2eb0db8a'
|
||||
my_pass_decrypted: '6EjMk<hcX3<5(Yp?Xi5aQ8eS`a#Ni'
|
||||
dsn: "DRIVER={PostgreSQL};Server=localhost;Port=5432;Database=postgres;Uid={{ my_user }};Pwd={{ my_pass_decrypted }};UseUnicode=True"
|
||||
packages:
|
||||
RedHat:
|
||||
- postgresql-odbc
|
||||
- unixODBC
|
||||
- unixODBC-devel
|
||||
- gcc
|
||||
- gcc-c++
|
||||
Debian:
|
||||
- odbc-postgresql
|
||||
- unixodbc
|
||||
- unixodbc-dev
|
||||
- gcc
|
||||
- g++
|
||||
Suse:
|
||||
- psqlODBC
|
||||
- unixODBC
|
||||
- unixODBC-devel
|
||||
- gcc
|
||||
- gcc-c++
|
||||
FreeBSD:
|
||||
- unixODBC
|
3
tests/integration/targets/odbc/meta/main.yml
Normal file
3
tests/integration/targets/odbc/meta/main.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
dependencies:
|
||||
- setup_postgresql_db
|
7
tests/integration/targets/odbc/tasks/install_pyodbc.yml
Normal file
7
tests/integration/targets/odbc/tasks/install_pyodbc.yml
Normal file
|
@ -0,0 +1,7 @@
|
|||
- name: "Install {{ ansible_os_family }} Libraries"
|
||||
package:
|
||||
name: "{{ packages[ansible_os_family] }}"
|
||||
|
||||
- name: "Install pyodbc"
|
||||
pip:
|
||||
name: pyodbc
|
144
tests/integration/targets/odbc/tasks/main.yml
Normal file
144
tests/integration/targets/odbc/tasks/main.yml
Normal file
|
@ -0,0 +1,144 @@
|
|||
#
|
||||
# Test for proper failures without pyodbc
|
||||
#
|
||||
# Some of the docker images already have pyodbc installed on it
|
||||
- include_tasks: no_pyodbc.yml
|
||||
when: ansible_os_family != 'FreeBSD' and ansible_os_family != 'Suse' and ansible_os_family != 'Debian'
|
||||
|
||||
#
|
||||
# Get pyodbc installed
|
||||
#
|
||||
- include_tasks: install_pyodbc.yml
|
||||
|
||||
#
|
||||
# Test missing parameters & invalid DSN
|
||||
#
|
||||
- include_tasks: negative_tests.yml
|
||||
|
||||
#
|
||||
# Setup DSN per env
|
||||
#
|
||||
- name: Changing DSN for Suse
|
||||
set_fact:
|
||||
dsn: "DRIVER={PSQL};Server=localhost;Port=5432;Database=postgres;Uid={{ my_user }};Pwd={{ my_pass_decrypted }};UseUnicode=True"
|
||||
when: ansible_os_family == 'Suse'
|
||||
|
||||
- name: Changing DSN for Debian
|
||||
set_fact:
|
||||
dsn: "DRIVER={PostgreSQL Unicode};Server=localhost;Port=5432;Database=postgres;Uid={{ my_user }};Pwd={{ my_pass_decrypted }};UseUnicode=True"
|
||||
when: ansible_os_family == 'Debian'
|
||||
|
||||
#
|
||||
# Name setup database
|
||||
#
|
||||
- name: Create a user to run the tests with
|
||||
postgresql_user:
|
||||
name: "{{ my_user }}"
|
||||
password: "{{ my_pass }}"
|
||||
encrypted: 'yes'
|
||||
role_attr_flags: "SUPERUSER"
|
||||
db: postgres
|
||||
become_user: "{{ pg_user }}"
|
||||
become: True
|
||||
|
||||
- name: Create a table
|
||||
odbc:
|
||||
dsn: "{{ dsn }}"
|
||||
query: |
|
||||
CREATE TABLE films (
|
||||
code char(5) CONSTRAINT firstkey PRIMARY KEY,
|
||||
title varchar(40) NOT NULL,
|
||||
did integer NOT NULL,
|
||||
date_prod date,
|
||||
kind varchar(10),
|
||||
len interval hour to minute
|
||||
);
|
||||
become_user: "{{ pg_user }}"
|
||||
become: True
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is changed
|
||||
|
||||
#
|
||||
# Insert records
|
||||
#
|
||||
- name: Insert a record without params
|
||||
odbc:
|
||||
dsn: "{{ dsn }}"
|
||||
query: "INSERT INTO films (code, title, did, date_prod, kind, len) VALUES ('asdfg', 'My First Movie', 1, '2019-01-12', 'SyFi', '02:00')"
|
||||
become_user: "{{ pg_user }}"
|
||||
become: True
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is changed
|
||||
|
||||
- name: Insert a record with params
|
||||
odbc:
|
||||
dsn: "{{ dsn }}"
|
||||
query: "INSERT INTO films (code, title, did, date_prod, kind, len) VALUES (?, ?, ?, ?, ?, ?)"
|
||||
params:
|
||||
- 'qwert'
|
||||
- 'My Second Movie'
|
||||
- 2
|
||||
- '2019-01-12'
|
||||
- 'Comedy'
|
||||
- '01:30'
|
||||
become_user: "{{ pg_user }}"
|
||||
become: True
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is changed
|
||||
- results['row_count'] == -1
|
||||
- results['results'] == []
|
||||
- results['description'] == []
|
||||
|
||||
#
|
||||
# Select data
|
||||
#
|
||||
- name: Perform select single row without params (do not coherse changed)
|
||||
odbc:
|
||||
dsn: "{{ dsn }}"
|
||||
query: "SELECT * FROM films WHERE code='asdfg'"
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is changed
|
||||
- results is successful
|
||||
- results.row_count == 1
|
||||
|
||||
- name: Perform select multiple rows with params (coherse changed)
|
||||
odbc:
|
||||
dsn: "{{ dsn }}"
|
||||
query: 'SELECT * FROM films WHERE code=? or code=?'
|
||||
params:
|
||||
- 'asdfg'
|
||||
- 'qwert'
|
||||
register: results
|
||||
changed_when: False
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is not changed
|
||||
- results is successful
|
||||
- results.row_count == 2
|
||||
|
||||
- name: Drop the table
|
||||
odbc:
|
||||
dsn: "{{ dsn }}"
|
||||
query: "DROP TABLE films"
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is successful
|
||||
- results is changed
|
||||
- results['row_count'] == -1
|
||||
- results['results'] == []
|
||||
- results['description'] == []
|
19
tests/integration/targets/odbc/tasks/negative_tests.yml
Normal file
19
tests/integration/targets/odbc/tasks/negative_tests.yml
Normal file
|
@ -0,0 +1,19 @@
|
|||
#
|
||||
# Missing params for the module
|
||||
# There is nothing you need to do here because the params are required
|
||||
#
|
||||
|
||||
#
|
||||
# Invalid DSN in the module
|
||||
#
|
||||
- name: "Test with an invalid DSN"
|
||||
odbc:
|
||||
dsn: "t1"
|
||||
query: "SELECT * FROM nothing"
|
||||
register: results
|
||||
ignore_errors: True
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is failed
|
||||
- "'Failed to connect to DSN' in results.msg"
|
11
tests/integration/targets/odbc/tasks/no_pyodbc.yml
Normal file
11
tests/integration/targets/odbc/tasks/no_pyodbc.yml
Normal file
|
@ -0,0 +1,11 @@
|
|||
- name: Testing the module without pyodbc
|
||||
odbc:
|
||||
dsn: "Test"
|
||||
query: "SELECT * FROM nothing"
|
||||
ignore_errors: True
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is failed
|
||||
- "'Failed to import the required Python library (pyodbc) on' in results.msg"
|
Loading…
Reference in a new issue