From b79969da68ddaa73cfabdb866f80ac7f414b9f62 Mon Sep 17 00:00:00 2001
From: rainerleber <39616583+rainerleber@users.noreply.github.com>
Date: Thu, 27 May 2021 18:46:12 +0200
Subject: [PATCH] Add module hana_query to make SAP HANA administration easier.
 (#2623)

* new

* move link

* Apply suggestions from code review

Co-authored-by: Felix Fontein <felix@fontein.de>

* add more interesting return value in test

* remove unused objects

* removed unneeded function

* extend test output

* Update tests/unit/plugins/modules/database/saphana/test_hana_query.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Rainer Leber <rainer.leber@sva.de>
Co-authored-by: Felix Fontein <felix@fontein.de>
---
 .../modules/database/saphana/hana_query.py    | 187 ++++++++++++++++++
 plugins/modules/hana_query.py                 |   1 +
 .../modules/database/saphana/__init__.py      |   0
 .../database/saphana/test_hana_query.py       |  66 +++++++
 4 files changed, 254 insertions(+)
 create mode 100644 plugins/modules/database/saphana/hana_query.py
 create mode 120000 plugins/modules/hana_query.py
 create mode 100644 tests/unit/plugins/modules/database/saphana/__init__.py
 create mode 100644 tests/unit/plugins/modules/database/saphana/test_hana_query.py

diff --git a/plugins/modules/database/saphana/hana_query.py b/plugins/modules/database/saphana/hana_query.py
new file mode 100644
index 0000000000..ab147ef3fe
--- /dev/null
+++ b/plugins/modules/database/saphana/hana_query.py
@@ -0,0 +1,187 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.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 = r'''
+---
+module: hana_query
+short_description: Execute SQL on HANA
+version_added: 3.2.0
+description: This module executes SQL statements on HANA with hdbsql.
+options:
+    sid:
+        description: The system ID.
+        type: str
+        required: true
+    instance:
+        description: The instance number.
+        type: str
+        required: true
+    user:
+        description: A dedicated username. Defaults to C(SYSTEM).
+        type: str
+        default: SYSTEM
+    password:
+        description: The password to connect to the database.
+        type: str
+        required: true
+    autocommit:
+        description: Autocommit the statement.
+        type: bool
+        default: true
+    host:
+        description: The Host IP address. The port can be defined as well.
+        type: str
+    database:
+        description: Define the database on which to connect.
+        type: str
+    encrypted:
+        description: Use encrypted connection. Defaults to C(false).
+        type: bool
+        default: false
+    filepath:
+        description:
+        - One or more files each containing one SQL query to run.
+        - Must be a string or list containing strings.
+        type: list
+        elements: path
+    query:
+        description:
+        - SQL query to run.
+        - Must be a string or list containing strings. Please note that if you supply a string, it will be split by commas (C(,)) to a list.
+          It is better to supply a one-element list instead to avoid mangled input.
+        type: list
+        elements: str
+notes:
+    - Does not support C(check_mode).
+author:
+    - Rainer Leber (@rainerleber)
+'''
+
+EXAMPLES = r'''
+- name: Simple select query
+  community.general.hana_query:
+    sid: "hdb"
+    instance: "01"
+    password: "Test123"
+    query: "select user_name from users"
+
+- name: Run several queries
+  community.general.hana_query:
+    sid: "hdb"
+    instance: "01"
+    password: "Test123"
+    query:
+    - "select user_name from users;"
+    - select * from SYSTEM;
+    host: "localhost"
+    autocommit: False
+
+- name: Run several queries from file
+  community.general.hana_query:
+    sid: "hdb"
+    instance: "01"
+    password: "Test123"
+    filepath:
+    - /tmp/HANA_CPU_UtilizationPerCore_2.00.020+.txt
+    - /tmp/HANA.txt
+    host: "localhost"
+'''
+
+RETURN = r'''
+query_result:
+    description: List containing results of all queries executed (one sublist for every query).
+    returned: on success
+    type: list
+    elements: list
+    sample: [[{"Column": "Value1"}, {"Column": "Value2"}], [{"Column": "Value1"}, {"Column": "Value2"}]]
+'''
+
+import csv
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.six import StringIO
+from ansible.module_utils._text import to_native
+
+
+def csv_to_list(rawcsv):
+    reader_raw = csv.DictReader(StringIO(rawcsv))
+    reader = [dict((k, v.strip()) for k, v in row.items()) for row in reader_raw]
+    return list(reader)
+
+
+def main():
+    module = AnsibleModule(
+        argument_spec=dict(
+            sid=dict(type='str', required=True),
+            instance=dict(type='str', required=True),
+            encrypted=dict(type='bool', required=False, default=False),
+            host=dict(type='str', required=False),
+            user=dict(type='str', required=False, default="SYSTEM"),
+            password=dict(type='str', required=True, no_log=True),
+            database=dict(type='str', required=False),
+            query=dict(type='list', elements='str', required=False),
+            filepath=dict(type='list', elements='path', required=False),
+            autocommit=dict(type='bool', required=False, default=True),
+        ),
+        required_one_of=[('query', 'filepath')],
+        supports_check_mode=False,
+    )
+    rc, out, err, out_raw = [0, [], "", ""]
+
+    params = module.params
+
+    sid = (params['sid']).upper()
+    instance = params['instance']
+    user = params['user']
+    password = params['password']
+    autocommit = params['autocommit']
+    host = params['host']
+    database = params['database']
+    encrypted = params['encrypted']
+
+    filepath = params['filepath']
+    query = params['query']
+
+    bin_path = "/usr/sap/{sid}/HDB{instance}/exe/hdbsql".format(sid=sid, instance=instance)
+
+    try:
+        command = [module.get_bin_path(bin_path, required=True)]
+    except Exception as e:
+        module.fail_json(msg='Failed to find hdbsql at the expected path "{0}". Please check SID and instance number: "{1}"'.format(bin_path, to_native(e)))
+
+    if encrypted is True:
+        command.extend(['-attemptencrypt'])
+    if autocommit is False:
+        command.extend(['-z'])
+    if host is not None:
+        command.extend(['-n', host])
+    if database is not None:
+        command.extend(['-d', database])
+    # -x Suppresses additional output, such as the number of selected rows in a result set.
+    command.extend(['-x', '-i', instance, '-u', user, '-p', password])
+
+    if filepath is not None:
+        command.extend(['-I'])
+        for p in filepath:
+            # makes a command like hdbsql -i 01 -u SYSTEM -p secret123# -I /tmp/HANA_CPU_UtilizationPerCore_2.00.020+.txt,
+            # iterates through files and append the output to var out.
+            query_command = command + [p]
+            (rc, out_raw, err) = module.run_command(query_command)
+            out.append(csv_to_list(out_raw))
+    if query is not None:
+        for q in query:
+            # makes a command like hdbsql -i 01 -u SYSTEM -p secret123# "select user_name from users",
+            # iterates through multiple commands and append the output to var out.
+            query_command = command + [q]
+            (rc, out_raw, err) = module.run_command(query_command)
+            out.append(csv_to_list(out_raw))
+    changed = True
+
+    module.exit_json(changed=changed, rc=rc, query_result=out, stderr=err)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/plugins/modules/hana_query.py b/plugins/modules/hana_query.py
new file mode 120000
index 0000000000..ea869eb7a4
--- /dev/null
+++ b/plugins/modules/hana_query.py
@@ -0,0 +1 @@
+./database/saphana/hana_query.py
\ No newline at end of file
diff --git a/tests/unit/plugins/modules/database/saphana/__init__.py b/tests/unit/plugins/modules/database/saphana/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/unit/plugins/modules/database/saphana/test_hana_query.py b/tests/unit/plugins/modules/database/saphana/test_hana_query.py
new file mode 100644
index 0000000000..4d158c028e
--- /dev/null
+++ b/tests/unit/plugins/modules/database/saphana/test_hana_query.py
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2021, Rainer Leber (@rainerleber) <rainerleber@gmail.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
+
+from ansible_collections.community.general.plugins.modules import hana_query
+from ansible_collections.community.general.tests.unit.plugins.modules.utils import (
+    AnsibleExitJson,
+    AnsibleFailJson,
+    ModuleTestCase,
+    set_module_args,
+)
+from ansible_collections.community.general.tests.unit.compat.mock import patch
+from ansible.module_utils import basic
+
+
+def get_bin_path(*args, **kwargs):
+    """Function to return path of hdbsql"""
+    return "/usr/sap/HDB/HDB01/exe/hdbsql"
+
+
+class Testhana_query(ModuleTestCase):
+    """Main class for testing hana_query module."""
+
+    def setUp(self):
+        """Setup."""
+        super(Testhana_query, self).setUp()
+        self.module = hana_query
+        self.mock_get_bin_path = patch.object(basic.AnsibleModule, 'get_bin_path', get_bin_path)
+        self.mock_get_bin_path.start()
+        self.addCleanup(self.mock_get_bin_path.stop)  # ensure that the patching is 'undone'
+
+    def tearDown(self):
+        """Teardown."""
+        super(Testhana_query, self).tearDown()
+
+    def test_without_required_parameters(self):
+        """Failure must occurs when all parameters are missing."""
+        with self.assertRaises(AnsibleFailJson):
+            set_module_args({})
+            self.module.main()
+
+    def test_hana_query(self):
+        """Check that result is processed."""
+        set_module_args({
+            'sid': "HDB",
+            'instance': "01",
+            'encrypted': False,
+            'host': "localhost",
+            'user': "SYSTEM",
+            'password': "1234Qwer",
+            'database': "HDB",
+            'query': "SELECT * FROM users;"
+        })
+        with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+            run_command.return_value = 0, 'username,name\n  testuser,test user  \n myuser, my user   \n', ''
+            with self.assertRaises(AnsibleExitJson) as result:
+                hana_query.main()
+            self.assertEqual(result.exception.args[0]['query_result'], [[
+                {'username': 'testuser', 'name': 'test user'},
+                {'username': 'myuser', 'name': 'my user'},
+            ]])
+        self.assertEqual(run_command.call_count, 1)