diff --git a/.github/workflows/ansible-test-plugins.yml b/.github/workflows/ansible-test-plugins.yml index b6a9abe..5b98084 100644 --- a/.github/workflows/ansible-test-plugins.yml +++ b/.github/workflows/ansible-test-plugins.yml @@ -75,9 +75,12 @@ jobs: - name: Install ansible-base (${{ matrix.ansible }}) run: pip install https://github.com/ansible/ansible/archive/${{ matrix.ansible }}.tar.gz --disable-pip-version-check + - name: Instal clickhouse-driver + run: pip install clickhouse-driver + # Run the unit tests - name: Run unit test - run: ansible-test units -v --color --docker --coverage + run: ansible-test units -v --color --python 3.10 --docker --coverage working-directory: ./ansible_collections/community/clickhouse # ansible-test support producing code coverage date @@ -91,7 +94,7 @@ jobs: fail_ci_if_error: false integration: - name: "Integration (Python: ${{ matrix.python }}, Ansible: ${{ matrix.ansible }}" + name: "Integration (Python: ${{ matrix.python }}, Ansible: ${{ matrix.ansible }}, ClickHouse: ${{ matrix.clickhouse }}" runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/plugins/module_utils/clickhouse.py b/plugins/module_utils/clickhouse.py new file mode 100644 index 0000000..1a44f61 --- /dev/null +++ b/plugins/module_utils/clickhouse.py @@ -0,0 +1,110 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# Simplified BSD License (see simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils._text import to_native + +Client = None +try: + from clickhouse_driver import Client + from clickhouse_driver import __version__ as driver_version + HAS_DB_DRIVER = True +except ImportError: + HAS_DB_DRIVER = False + +PRIV_ERR_CODE = 497 + + +def client_common_argument_spec(): + """ + Return a dictionary with connection options. + + The options are commonly used by many modules. + """ + return dict( + login_host=dict(type='str', default='localhost'), + login_port=dict(type='int', default=None), + login_db=dict(type='str', default=None), + login_user=dict(type='str', default=None), + login_password=dict(type='str', default=None, no_log=True), + client_kwargs=dict(type='dict', default={}), + ) + + +def get_main_conn_kwargs(module): + """Retrieves main connection arguments values and translates + them into corresponding clickhouse_driver.Client() arguments. + + Returns a dictionary of arguments with values to pass to Client(). + """ + main_conn_kwargs = {} + main_conn_kwargs['host'] = module.params['login_host'] # Has a default value + if module.params['login_port']: + main_conn_kwargs['port'] = module.params['login_port'] + if module.params['login_db']: + main_conn_kwargs['database'] = module.params['login_db'] + if module.params['login_user']: + main_conn_kwargs['user'] = module.params['login_user'] + if module.params['login_password']: + main_conn_kwargs['password'] = module.params['login_password'] + return main_conn_kwargs + + +def check_clickhouse_driver(module): + """Checks if the driver is present. + + Informs user if no driver and fails. + """ + if not HAS_DB_DRIVER: + module.fail_json(msg=missing_required_lib('clickhouse_driver')) + + +def version_clickhouse_driver(): + """ + Returns the current version of clickhouse_driver. + """ + return driver_version + + +def connect_to_db_via_client(module, main_conn_kwargs, client_kwargs): + """Connects to DB using the Client() class. + + Returns Client() object. + """ + try: + # Merge the kwargs as Python 2 would through an error + # when unpaking them separately to Client() + client_kwargs.update(main_conn_kwargs) + client = Client(**client_kwargs) + except Exception as e: + module.fail_json(msg="Failed to connect to database: %s" % to_native(e)) + + return client + + +def execute_query(module, client, query, execute_kwargs=None): + """Execute query. + + Returns rows returned in response. + """ + # Some modules do not pass this argument + if execute_kwargs is None: + execute_kwargs = {} + + try: + result = client.execute(query, **execute_kwargs) + except Exception as e: + if "Not enough privileges" in to_native(e): + return PRIV_ERR_CODE + module.fail_json(msg="Failed to execute query: %s" % to_native(e)) + + return result diff --git a/plugins/module_utils/connect.py b/plugins/module_utils/connect.py deleted file mode 100644 index b6e4491..0000000 --- a/plugins/module_utils/connect.py +++ /dev/null @@ -1,38 +0,0 @@ -# This code is part of Ansible, but is an independent component. -# This particular file snippet, and this file snippet only, is BSD licensed. -# Modules you write using this snippet, which is embedded dynamically by Ansible -# still belong to the author of the module, and may assign their own license -# to the complete work. -# -# Simplified BSD License (see simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type - -from ansible.module_utils.basic import missing_required_lib - - -def client_common_argument_spec(): - """ - Return a dictionary with connection options. - - The options are commonly used by many modules. - """ - return dict( - login_host=dict(type='str', default='localhost'), - login_port=dict(type='int', default=None), - login_db=dict(type='str', default=None), - login_user=dict(type='str', default=None), - login_password=dict(type='str', default=None, no_log=True), - client_kwargs=dict(type='dict', default={}), - ) - - -def check_driver(module, has_db_driver): - """Checks if the driver is present. - - Informs user if no driver and fails. - """ - if not has_db_driver: - module.fail_json(msg=missing_required_lib('clickhouse_driver')) diff --git a/plugins/modules/clickhouse_client.py b/plugins/modules/clickhouse_client.py index 5dd73f0..13628be 100644 --- a/plugins/modules/clickhouse_client.py +++ b/plugins/modules/clickhouse_client.py @@ -121,18 +121,14 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils._text import to_native -from ansible_collections.community.clickhouse.plugins.module_utils.connect import ( - check_driver, +from ansible_collections.community.clickhouse.plugins.module_utils.clickhouse import ( + check_clickhouse_driver, client_common_argument_spec, + get_main_conn_kwargs, + connect_to_db_via_client, + execute_query, ) -Client = None -try: - from clickhouse_driver import Client - HAS_DB_DRIVER = True -except ImportError: - HAS_DB_DRIVER = False - def is_uuid(value): """Checks if the value is valid UUID. @@ -179,25 +175,6 @@ def replace_val_in_tuple(tup, idx, val): return tuple(tmp) -def get_main_conn_kwargs(module): - """Retrieves main connection arguments values and translates - them into corresponding clickhouse_driver.Client() arguments. - - Returns a dictionary of arguments with values to pass to Client(). - """ - main_conn_kwargs = {} - main_conn_kwargs['host'] = module.params['login_host'] # Has a default value - if module.params['login_port']: - main_conn_kwargs['port'] = module.params['login_port'] - if module.params['login_db']: - main_conn_kwargs['database'] = module.params['login_db'] - if module.params['login_user']: - main_conn_kwargs['user'] = module.params['login_user'] - if module.params['login_password']: - main_conn_kwargs['password'] = module.params['login_password'] - return main_conn_kwargs - - def get_query_statistics(module, client): """Retrieve query statistics from the Client() object. @@ -229,19 +206,6 @@ def get_query_statistics(module, client): return statistics -def execute_query(module, client, query, execute_kwargs): - """Execute query. - - Returns rows returned in response. - """ - try: - result = client.execute(query, **execute_kwargs) - except Exception as e: - module.fail_json(msg="Failed to execute query: %s" % to_native(e)) - - return result - - def get_substituted_query(module, client, query, execute_kwargs): """Substitute params in a query. If no params, just return the query as is. @@ -262,22 +226,6 @@ def get_substituted_query(module, client, query, execute_kwargs): return substituted_query -def connect_to_db_via_client(module, main_conn_kwargs, client_kwargs): - """Connects to DB using the Client() class. - - Returns Client() object. - """ - try: - # Merge the kwargs as Python 2 would through an error - # when unpaking them separately to Client() - client_kwargs.update(main_conn_kwargs) - client = Client(**client_kwargs) - except Exception as e: - module.fail_json(msg="Failed to connect to database: %s" % to_native(e)) - - return client - - def main(): # Set up arguments. # If there are common arguments shared across several modules, @@ -306,7 +254,7 @@ def main(): main_conn_kwargs = get_main_conn_kwargs(module) # Will fail if no driver informing the user - check_driver(module, HAS_DB_DRIVER) + check_clickhouse_driver(module) # Connect to DB client = connect_to_db_via_client(module, main_conn_kwargs, client_kwargs) diff --git a/plugins/modules/clickhouse_db.py b/plugins/modules/clickhouse_db.py index 6cb76ff..be5c5df 100644 --- a/plugins/modules/clickhouse_db.py +++ b/plugins/modules/clickhouse_db.py @@ -81,75 +81,19 @@ ''' from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.community.clickhouse.plugins.module_utils.connect import ( - check_driver, +from ansible_collections.community.clickhouse.plugins.module_utils.clickhouse import ( + check_clickhouse_driver, client_common_argument_spec, + get_main_conn_kwargs, + execute_query, + connect_to_db_via_client, ) -Client = None -try: - from clickhouse_driver import Client - HAS_DB_DRIVER = True -except ImportError: - HAS_DB_DRIVER = False executed_statements = [] -def get_main_conn_kwargs(module): - """Retrieves main connection arguments values and translates - them into corresponding clickhouse_driver.Client() arguments. - - Returns a dictionary of arguments with values to pass to Client(). - """ - main_conn_kwargs = {} - main_conn_kwargs['host'] = module.params['login_host'] # Has a default value - if module.params['login_port']: - main_conn_kwargs['port'] = module.params['login_port'] - if module.params['login_db']: - main_conn_kwargs['database'] = module.params['login_db'] - if module.params['login_user']: - main_conn_kwargs['user'] = module.params['login_user'] - if module.params['login_password']: - main_conn_kwargs['password'] = module.params['login_password'] - return main_conn_kwargs - - -def execute_query(module, client, query, execute_kwargs=None): - """Execute query. - - Returns rows returned in response. - """ - # Some modules do not pass this argument - if execute_kwargs is None: - execute_kwargs = {} - - try: - result = client.execute(query, **execute_kwargs) - except Exception as e: - module.fail_json(msg="Failed to execute query: %s" % to_native(e)) - - return result - - -def connect_to_db_via_client(module, main_conn_kwargs, client_kwargs): - """Connects to DB using the Client() class. - - Returns Client() object. - """ - try: - # Merge the kwargs as Python 2 would through an error - # when unpaking them separately to Client() - client_kwargs.update(main_conn_kwargs) - client = Client(**client_kwargs) - except Exception as e: - module.fail_json(msg="Failed to connect to database: %s" % to_native(e)) - - return client - - class ClickHouseDB(): def __init__(self, module, client, name): self.module = module @@ -238,7 +182,7 @@ def main(): engine = module.params['engine'] # Will fail if no driver informing the user - check_driver(module, HAS_DB_DRIVER) + check_clickhouse_driver(module) # Connect to DB client = connect_to_db_via_client(module, main_conn_kwargs, client_kwargs) diff --git a/plugins/modules/clickhouse_info.py b/plugins/modules/clickhouse_info.py index f624b74..f247aec 100644 --- a/plugins/modules/clickhouse_info.py +++ b/plugins/modules/clickhouse_info.py @@ -134,62 +134,20 @@ ''' from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.community.clickhouse.plugins.module_utils.connect import ( - check_driver, +from ansible_collections.community.clickhouse.plugins.module_utils.clickhouse import ( + check_clickhouse_driver, + version_clickhouse_driver, client_common_argument_spec, + get_main_conn_kwargs, + connect_to_db_via_client, + execute_query, ) -Client = None -try: - from clickhouse_driver import Client - from clickhouse_driver import __version__ as driver_version - HAS_DB_DRIVER = True -except ImportError: - HAS_DB_DRIVER = False PRIV_ERR_CODE = 497 -def get_main_conn_kwargs(module): - """Retrieves main connection arguments values and translates - them into corresponding clickhouse_driver.Client() arguments. - - Returns a dictionary of arguments with values to pass to Client(). - """ - main_conn_kwargs = {} - main_conn_kwargs['host'] = module.params['login_host'] # Has a default value - if module.params['login_port']: - main_conn_kwargs['port'] = module.params['login_port'] - if module.params['login_db']: - main_conn_kwargs['database'] = module.params['login_db'] - if module.params['login_user']: - main_conn_kwargs['user'] = module.params['login_user'] - if module.params['login_password']: - main_conn_kwargs['password'] = module.params['login_password'] - return main_conn_kwargs - - -def execute_query(module, client, query, execute_kwargs=None): - """Execute query. - - Returns rows returned in response. - """ - # Some modules do not pass this argument - if execute_kwargs is None: - execute_kwargs = {} - - try: - result = client.execute(query, **execute_kwargs) - except Exception as e: - if "Not enough privileges" in to_native(e): - return PRIV_ERR_CODE - module.fail_json(msg="Failed to execute query: %s" % to_native(e)) - - return result - - def get_databases(module, client): """Get databases. @@ -494,22 +452,6 @@ def get_server_version(module, client): return version -def connect_to_db_via_client(module, main_conn_kwargs, client_kwargs): - """Connects to DB using the Client() class. - - Returns Client() object. - """ - try: - # Merge the kwargs as Python 2 would through an error - # when unpaking them separately to Client() - client_kwargs.update(main_conn_kwargs) - client = Client(**client_kwargs) - except Exception as e: - module.fail_json(msg="Failed to connect to database: %s" % to_native(e)) - - return client - - def get_driver(module, client): """Gets driver information. @@ -518,7 +460,7 @@ def get_driver(module, client): Returns its version for now. """ - return {"version": driver_version} + return {"version": version_clickhouse_driver()} def handle_limit_values(module, supported_ret_vals, limit): @@ -589,7 +531,7 @@ def main(): limit = ret_val_func_mapping.keys() # Will fail if no driver informing the user - check_driver(module, HAS_DB_DRIVER) + check_clickhouse_driver(module) # Connect to DB client = connect_to_db_via_client(module, main_conn_kwargs, client_kwargs) diff --git a/tests/integration/targets/.gitkeep b/tests/integration/targets/.gitkeep deleted file mode 100644 index 5c4b109..0000000 --- a/tests/integration/targets/.gitkeep +++ /dev/null @@ -1,3 +0,0 @@ -This is a placeholder file that only exists to keep the current -directory in Git. It is safe to remove it once this directory contains -the actual test files. diff --git a/tests/unit/plugins/module_utils/test_clickhouse.py b/tests/unit/plugins/module_utils/test_clickhouse.py new file mode 100644 index 0000000..8a580bf --- /dev/null +++ b/tests/unit/plugins/module_utils/test_clickhouse.py @@ -0,0 +1,86 @@ +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +from importlib.util import find_spec + +from ansible_collections.community.clickhouse.plugins.module_utils.clickhouse import ( + check_clickhouse_driver, + version_clickhouse_driver, + client_common_argument_spec, + get_main_conn_kwargs, +) + +REASON = "The clickhouse_driver module is not installed" + + +class FakeAnsibleModule: + def __init__(self): + self.params = { + "login_host": "localhost", + "login_port": None, + "login_user": None, + "login_db": None, + "login_password": None, + "client_kwargs": {}, + } + + def fail_json(self, msg): + print(msg) + + +def test_client_common_argument_spec(): + EXPECTED = { + 'login_db': {'type': 'str', 'default': None}, + 'login_port': {'type': 'int', 'default': None}, + 'login_user': {'type': 'str', 'default': None}, + 'login_host': {'type': 'str', 'default': 'localhost'}, + 'login_password': {'type': 'str', 'default': None, 'no_log': True}, + 'client_kwargs': {'type': 'dict', 'default': {}} + } + + assert client_common_argument_spec() == EXPECTED + + +@pytest.mark.parametrize( + 'input_params,output_params', + [ + ('', {'host': 'localhost'},), + ({'login_host': 'test_host', 'login_port': 8000}, + {'host': 'test_host', 'port': 8000}, + ), + ({'login_host': '127.0.0.1', + 'login_db': 'test_database', + 'login_user': 'test_user', + 'login_port': 9000, + 'login_password': 'qwerty', + }, + {'host': '127.0.0.1', + 'database': 'test_database', + 'user': 'test_user', + 'port': 9000, + 'password': 'qwerty', + }, + ), + ] +) +def test_get_main_conn_kwargs(input_params, output_params): + fake_module = FakeAnsibleModule() + fake_module.params.update(input_params) + + assert get_main_conn_kwargs(fake_module) == output_params + + +@pytest.mark.skipif(find_spec('clickhouse_driver') is None, reason=REASON) +def test_version_clickhouse_driver(): + from clickhouse_driver import __version__ + + assert __version__ == version_clickhouse_driver() + + +def test_check_clickhouse_driver(): + fake_module = FakeAnsibleModule() + result = check_clickhouse_driver(fake_module) + + assert result is None or "clickhouse_driver" in result diff --git a/tests/units/.gitkeep b/tests/units/.gitkeep deleted file mode 100644 index 5c4b109..0000000 --- a/tests/units/.gitkeep +++ /dev/null @@ -1,3 +0,0 @@ -This is a placeholder file that only exists to keep the current -directory in Git. It is safe to remove it once this directory contains -the actual test files.