From 249e509d8c3704d5cc79aa3c5bfce48d4579c290 Mon Sep 17 00:00:00 2001 From: Linuxfabrik Date: Mon, 3 Aug 2026 16:41:20 +0200 Subject: [PATCH 1/3] chore: add the shared ruff configuration lfops carried 48 Python files without any ruff configuration or hook, the only repository with Python code in that state. It now uses the same `[tool.ruff]` blocks as the others. Its bandit skips move out of the pre-commit hook arguments into `[tool.bandit]` at the same time, so a manual run matches what the hook does. Ansible module boilerplate is added to `ignore`: collections put DOCUMENTATION/EXAMPLES/RETURN above the imports, spell argument_spec with `dict()`, and carry the `__future__` / `__metaclass__` preamble that ansible-core documents. Vendored ansible-freeipa and python-gnupg code plus the vulture whitelist are excluded so patches stay sendable upstream. --- .pre-commit-config.yaml | 7 +++++++ pyproject.toml | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ccd4083d..f295d90c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,6 +30,13 @@ repos: - id: 'mixed-line-ending' - id: 'trailing-whitespace' + - repo: 'https://github.com/astral-sh/ruff-pre-commit' + rev: 'v0.16.1' + hooks: + - id: 'ruff-check' + args: ['--fix'] + - id: 'ruff-format' + - repo: 'https://github.com/PyCQA/bandit' rev: '1.9.4' hooks: diff --git a/pyproject.toml b/pyproject.toml index 25afd544..dd6ebdb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,43 @@ +[tool.ruff] +line-length = 88 +target-version = 'py39' +# Vendored upstream code that is kept as-is so patches stay sendable, plus the +# vulture whitelist, which is a list of bare names by construction and not +# runnable Python. +exclude = [ + '.vulture_whitelist.py', + 'plugins/module_utils/gnupg.py', + 'plugins/modules/ipa*.py', +] + +[tool.ruff.lint] +select = [ + 'B', # flake8-bugbear (potential bugs) + 'C4', # flake8-comprehensions + 'E', # pycodestyle errors + 'F', # pyflakes (logic errors) + 'I', # isort (import sorting) + 'RUF', # Ruff-specific rules + 'SIM', # flake8-simplify + 'UP', # pyupgrade (modernize syntax for py39+) + 'W', # pycodestyle warnings +] +ignore = [ + 'C408', # dict() instead of a literal (Ansible argument_spec convention) + 'E402', # import not at top of file (the DOCUMENTATION block comes first) + 'E501', # line too long (handled by code review, not auto-enforcement) + 'SIM102', # nested if statements (sometimes clearer than combined conditions) + 'SIM105', # contextlib.suppress (try/except/pass is more explicit) + 'UP001', # __metaclass__ = type is obsolete (Ansible module preamble) + 'UP009', # UTF-8 encoding declaration (we keep our standard file header) + 'UP010', # unnecessary __future__ import (Ansible module preamble) + 'UP015', # redundant open() mode (we keep explicit modes for clarity) +] + +[tool.ruff.format] +docstring-code-format = true +quote-style = 'single' + # Configuration for tools that the LFOps repo runs under pre-commit. # This file does *not* describe the Ansible collection itself — that lives # in galaxy.yml. It exists only to point the linters at the right paths. From c5db9c7096ecc7cb297cf35e8859366c0f7d9ca7 Mon Sep 17 00:00:00 2001 From: Linuxfabrik Date: Mon, 3 Aug 2026 16:42:29 +0200 Subject: [PATCH 2/3] fix: address the ruff findings in the plugins and tests Preserves the exception chain in the Bitwarden code (`raise ... from e`), marks deliberately unused unpacked bindings with a leading underscore, unpacks instead of concatenating the occ command lists, and annotates the shared recorder attributes of the test stubs as ClassVar. `sqlite_query.fetchone` is rewritten by hand rather than auto-fixed: ruff proposes `next(...)`, which raises StopIteration, while the surrounding code caught IndexError to return the empty result. The list-index form is kept and the empty case handled explicitly. --- plugins/filter/combine_lod.py | 2 +- plugins/filter/platform_select.py | 2 +- plugins/lookup/bitwarden_item.py | 7 ++++--- plugins/module_utils/bitwarden.py | 20 +++++++++---------- plugins/module_utils/uptimerobot.py | 1 - plugins/modules/bitwarden_item.py | 7 ++++--- plugins/modules/gpg_key.py | 3 +-- plugins/modules/lvm_pv.py | 8 ++++---- plugins/modules/nextcloud_occ_app_config.py | 12 ++++++++--- .../modules/nextcloud_occ_system_config.py | 13 +++++++++--- plugins/modules/sqlite_query.py | 6 ++---- plugins/modules/uptimerobot_monitor.py | 1 - tests/unit/plugins/filter/test_combine_lod.py | 1 - .../plugins/lookup/test_bitwarden_item.py | 7 +++++-- .../plugins/modules/test_bitwarden_item.py | 14 +++++++------ tests/unit/plugins/modules/test_gpg_key.py | 1 - .../modules/test_nextcloud_occ_app_config.py | 5 +++-- .../unit/plugins/modules/test_sqlite_query.py | 18 ++++++++--------- .../modules/test_uptimerobot_monitor.py | 4 +++- .../modules/test_uptimerobot_mwindow.py | 4 +++- 20 files changed, 77 insertions(+), 59 deletions(-) diff --git a/plugins/filter/combine_lod.py b/plugins/filter/combine_lod.py index b416bd24..de4b188c 100644 --- a/plugins/filter/combine_lod.py +++ b/plugins/filter/combine_lod.py @@ -224,7 +224,7 @@ def combine_lod(*args, **kwargs): -class FilterModule(object): +class FilterModule: """Register custom filter plugins in Ansible""" def filters(self): diff --git a/plugins/filter/platform_select.py b/plugins/filter/platform_select.py index e8ed9b68..4a6d2e00 100644 --- a/plugins/filter/platform_select.py +++ b/plugins/filter/platform_select.py @@ -126,7 +126,7 @@ def platform_select(values, ansible_facts, default=_SENTINEL): ) -class FilterModule(object): +class FilterModule: """Register custom filter plugins in Ansible""" def filters(self): diff --git a/plugins/lookup/bitwarden_item.py b/plugins/lookup/bitwarden_item.py index b23c539d..c001f615 100644 --- a/plugins/lookup/bitwarden_item.py +++ b/plugins/lookup/bitwarden_item.py @@ -283,8 +283,9 @@ from ansible.errors import AnsibleError from ansible.plugins.lookup import LookupBase from ansible.utils.display import Display -from ansible_collections.linuxfabrik.lfops.plugins.module_utils.bitwarden import \ - Bitwarden +from ansible_collections.linuxfabrik.lfops.plugins.module_utils.bitwarden import ( + Bitwarden, +) display = Display() # log prefix "lfbwlp" = Linuxfabrik Bitwarden Lookup Plugin @@ -319,7 +320,7 @@ def run(self, terms, variables=None, **kwargs): uris = term.get('uris', []) username = term.get('username', None) except Exception as e: - raise AnsibleError(f'Encountered exception while fetching {term}: {e}') + raise AnsibleError(f'Encountered exception while fetching {term}: {e}') from e if id_: result = bw.get_item_by_id(id_) diff --git a/plugins/module_utils/bitwarden.py b/plugins/module_utils/bitwarden.py index 16a28f39..09a3c3c5 100644 --- a/plugins/module_utils/bitwarden.py +++ b/plugins/module_utils/bitwarden.py @@ -98,7 +98,7 @@ def prepare_multipart_no_base64(fields): mime = mimetypes.guess_type(filename or '', strict=False)[0] or 'application/octet-stream' except Exception: mime = 'application/octet-stream' - main_type, sep, sub_type = mime.partition('/') + main_type, _sep, sub_type = mime.partition('/') else: raise TypeError( f'value must be a string, or mapping, cannot be type {value.__class__.__name__}' @@ -134,7 +134,7 @@ def prepare_multipart_no_base64(fields): b_data = m.as_bytes(policy=email.policy.HTTP) del m - headers, sep, b_content = b_data.partition(b'\r\n\r\n') + headers, _sep, b_content = b_data.partition(b'\r\n\r\n') del b_data parser = email.parser.BytesHeaderParser().parsebytes @@ -174,7 +174,7 @@ def _api_call(self, url_path, method='GET', body=None, body_format='json'): try: content_type, body = prepare_multipart_no_base64(body) except (TypeError, ValueError) as e: - raise BitwardenException(f'failed to parse body as form-multipart: {to_native(e)}') + raise BitwardenException(f'failed to parse body as form-multipart: {to_native(e)}') from e headers['Content-Type'] = content_type # mostly taken from ansible.builtin.url lookup plugin @@ -182,18 +182,18 @@ def _api_call(self, url_path, method='GET', body=None, body_format='json'): # increased the timeout since listing all items via `list/object/items` takes forever (13s for ~2500 items) response = open_url(url, method=method, data=body, headers=headers, timeout=60) except HTTPError as e: - raise BitwardenException(f'Received HTTP error for {url} : {to_native(e)}') + raise BitwardenException(f'Received HTTP error for {url} : {to_native(e)}') from e except URLError as e: - raise BitwardenException(f'Failed lookup url for {url} : {to_native(e)}') + raise BitwardenException(f'Failed lookup url for {url} : {to_native(e)}') from e except SSLValidationError as e: - raise BitwardenException(f"Error validating the server's certificate for {url}: {to_native(e)}") + raise BitwardenException(f"Error validating the server's certificate for {url}: {to_native(e)}") from e except ConnectionError as e: - raise BitwardenException(f'Error connecting to {url}: {to_native(e)}') + raise BitwardenException(f'Error connecting to {url}: {to_native(e)}') from e try: result = json.loads(to_text(response.read())) except json.decoder.JSONDecodeError as e: - raise BitwardenException(f'Unable to load JSON: {to_native(e)}') + raise BitwardenException(f'Unable to load JSON: {to_native(e)}') from e if not result.get('success'): raise BitwardenException(f"API call failed: {result.get('data')}") @@ -213,7 +213,7 @@ def _load_cache(self): item_count = len(self._cache['items']) if self._cache['items'] is not None else 0 display.vvv(f'lfbw - cache loaded from {CACHE_FILE} ({item_count} items)') return - except (IOError, OSError, ValueError, json.decoder.JSONDecodeError): + except (OSError, ValueError, json.decoder.JSONDecodeError): pass self._cache = { 'version': CACHE_VERSION, @@ -240,7 +240,7 @@ def _save_cache(self): except Exception: os.unlink(tmp_path) raise - except (IOError, OSError): + except OSError: display.vvv(f'lfbw - failed to save cache to {CACHE_FILE}') diff --git a/plugins/module_utils/uptimerobot.py b/plugins/module_utils/uptimerobot.py index c96eeda0..0713dc7a 100644 --- a/plugins/module_utils/uptimerobot.py +++ b/plugins/module_utils/uptimerobot.py @@ -29,7 +29,6 @@ from ansible.module_utils.urls import fetch_url - API_BASE = 'https://api.uptimerobot.com/v2/' ENV_API_KEY = 'UPTIMEROBOT_API_KEY' DEFAULT_API_KEY_FILE = '~/.uptimerobot' diff --git a/plugins/modules/bitwarden_item.py b/plugins/modules/bitwarden_item.py index e0f5b379..351d9169 100644 --- a/plugins/modules/bitwarden_item.py +++ b/plugins/modules/bitwarden_item.py @@ -258,8 +258,9 @@ import os from ansible.module_utils.basic import AnsibleModule -from ansible_collections.linuxfabrik.lfops.plugins.module_utils.bitwarden import \ - Bitwarden +from ansible_collections.linuxfabrik.lfops.plugins.module_utils.bitwarden import ( + Bitwarden, +) def diff_and_update(current, target): @@ -389,7 +390,7 @@ def run_module(): result = target_item if module.check_mode else bw.create_item(target_item) if attachments: - current_attachments = set(current_attachment['fileName'] for current_attachment in result.get('attachments', [])) + current_attachments = {current_attachment['fileName'] for current_attachment in result.get('attachments', [])} attachments_changed = False for attachment in attachments: if os.path.basename(attachment) not in current_attachments: diff --git a/plugins/modules/gpg_key.py b/plugins/modules/gpg_key.py index 12610d72..f23c2f46 100644 --- a/plugins/modules/gpg_key.py +++ b/plugins/modules/gpg_key.py @@ -248,7 +248,6 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.text.converters import to_native - from ansible_collections.linuxfabrik.lfops.plugins.module_utils.gnupg import GPG logger = logging.getLogger('gnupg') @@ -307,7 +306,7 @@ def match_key(key, params): # if there is at least one matching subkey, we assume a match. the unattended creation can only create a single subkey, # howerever, it is possible to add another one manually later. first_subkey_match = None - for subkey_id, subkey in key['subkey_info'].items(): + for _subkey_id, subkey in key['subkey_info'].items(): if algo_ids.get(int(subkey['algo']), 'Unknown') != params['subkey_type']: continue diff --git a/plugins/modules/lvm_pv.py b/plugins/modules/lvm_pv.py index 66c32215..eef2fb9e 100644 --- a/plugins/modules/lvm_pv.py +++ b/plugins/modules/lvm_pv.py @@ -83,7 +83,7 @@ def get_pv_status(module, device): def get_pv_size(module, device): """Get current PV size in bytes.""" cmd = ["pvs", "--noheadings", "--nosuffix", "--units", "b", "-o", "pv_size", device] - rc, out, err = module.run_command(cmd, check_rc=True) + _rc, out, _err = module.run_command(cmd, check_rc=True) return int(out.strip()) @@ -151,7 +151,7 @@ def main(): if force: cmd.append("-f") cmd.append(device) - rc, out, err = module.run_command(cmd, check_rc=True) + _rc, _out, _err = module.run_command(cmd, check_rc=True) changed = True actions.append("created") is_pv = True @@ -167,7 +167,7 @@ def main(): if rescan_device(module, device): actions.append("rescanned") original_size = get_pv_size(module, device) - rc, out, err = module.run_command(["pvresize", device], check_rc=True) + _rc, _out, _err = module.run_command(["pvresize", device], check_rc=True) new_size = get_pv_size(module, device) if new_size != original_size: changed = True @@ -184,7 +184,7 @@ def main(): cmd.append("-ff") changed = True cmd.append(device) - rc, out, err = module.run_command(cmd, check_rc=True) + _rc, _out, _err = module.run_command(cmd, check_rc=True) actions.append("removed") # Generate final message diff --git a/plugins/modules/nextcloud_occ_app_config.py b/plugins/modules/nextcloud_occ_app_config.py index 269f4cb9..8cd9ab63 100644 --- a/plugins/modules/nextcloud_occ_app_config.py +++ b/plugins/modules/nextcloud_occ_app_config.py @@ -223,7 +223,9 @@ def main(): '--output=json', 'config:app:get', app, - ] + name.split() # occ expects each part of the name as a separate argument + # occ expects each part of the name as a separate argument + *name.split(), + ] try: get_rc, get_stdout, _ = module.run_command(get_cmd) @@ -270,7 +272,9 @@ def main(): f'--value={value}', f'--type={value_type}', app, - ] + name.split() # occ expects each part of the name as a separate argument + # occ expects each part of the name as a separate argument + *name.split(), + ] try: set_rc, set_stdout, set_stderr = module.run_command(set_cmd, check_rc=True) @@ -309,7 +313,9 @@ def main(): '--no-interaction', 'config:app:delete', app, - ] + name.split() # occ expects each part of the name as a separate argument + # occ expects each part of the name as a separate argument + *name.split(), + ] try: delete_rc, delete_stdout, delete_stderr = module.run_command(delete_cmd, check_rc=True) diff --git a/plugins/modules/nextcloud_occ_system_config.py b/plugins/modules/nextcloud_occ_system_config.py index 2ae8e6da..afd88f00 100644 --- a/plugins/modules/nextcloud_occ_system_config.py +++ b/plugins/modules/nextcloud_occ_system_config.py @@ -109,6 +109,7 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.text.converters import to_native + def main(): # define available arguments/parameters a user can pass to this module module_args = dict( @@ -191,7 +192,9 @@ def main(): occ_path, '--no-interaction', 'config:system:get', - ] + name.split() # occ expects each part of the name as a separate argument + # occ expects each part of the name as a separate argument + *name.split(), + ] try: get_rc, get_stdout, _ = module.run_command(get_cmd) @@ -230,7 +233,9 @@ def main(): 'config:system:set', f'--value={value}', f'--type={value_type}', - ] + name.split() # occ expects each part of the name as a separate argument + # occ expects each part of the name as a separate argument + *name.split(), + ] try: set_rc, set_stdout, set_stderr = module.run_command(set_cmd, check_rc=True) @@ -268,7 +273,9 @@ def main(): occ_path, '--no-interaction', 'config:system:delete', - ] + name.split() # occ expects each part of the name as a separate argument + # occ expects each part of the name as a separate argument + *name.split(), + ] try: delete_rc, delete_stdout, delete_stderr = module.run_command(delete_cmd, check_rc=True) diff --git a/plugins/modules/sqlite_query.py b/plugins/modules/sqlite_query.py index 4131bd6d..05f56db4 100644 --- a/plugins/modules/sqlite_query.py +++ b/plugins/modules/sqlite_query.py @@ -166,10 +166,8 @@ def select(conn, sql, data=None, fetchone=False, as_dict=True): # https://stackoverflow.com/questions/3300464/how-can-i-get-dict-from-sqlite-query if as_dict: if fetchone: - try: - return (True, [dict(row) for row in c.fetchall()][0]) - except IndexError: - return (True, []) + rows = [dict(row) for row in c.fetchall()] + return (True, rows[0] if rows else []) return (True, [dict(row) for row in c.fetchall()]) if fetchone: return (True, c.fetchone()) diff --git a/plugins/modules/uptimerobot_monitor.py b/plugins/modules/uptimerobot_monitor.py index 4abfa139..60bb2e70 100644 --- a/plugins/modules/uptimerobot_monitor.py +++ b/plugins/modules/uptimerobot_monitor.py @@ -314,7 +314,6 @@ from ansible.module_utils.basic import AnsibleModule from ansible_collections.linuxfabrik.lfops.plugins.module_utils import uptimerobot as ur - # Fields we ship to new_monitor / edit_monitor and that we also diff against # the API's current state to decide whether an edit call is needed. _MONITOR_DIFFABLE_FIELDS = [ diff --git a/tests/unit/plugins/filter/test_combine_lod.py b/tests/unit/plugins/filter/test_combine_lod.py index c8c6c3d4..cfd52900 100644 --- a/tests/unit/plugins/filter/test_combine_lod.py +++ b/tests/unit/plugins/filter/test_combine_lod.py @@ -25,7 +25,6 @@ import unittest import yaml - from ansible.errors import AnsibleFilterError # The plugin lives outside any importable package, so load it by path diff --git a/tests/unit/plugins/lookup/test_bitwarden_item.py b/tests/unit/plugins/lookup/test_bitwarden_item.py index 3f913e6a..9ac5e496 100644 --- a/tests/unit/plugins/lookup/test_bitwarden_item.py +++ b/tests/unit/plugins/lookup/test_bitwarden_item.py @@ -18,15 +18,18 @@ __metaclass__ = type import unittest +from typing import ClassVar from ansible.errors import AnsibleError -from ansible_collections.linuxfabrik.lfops.plugins.lookup import bitwarden_item as lookup_mod +from ansible_collections.linuxfabrik.lfops.plugins.lookup import ( + bitwarden_item as lookup_mod, +) class _FakeBitwarden: """Minimal stand-in for the Bitwarden client used by the lookup.""" - items_by_search = [] + items_by_search: ClassVar[list] = [] item_by_id = None def __init__(self, *args, **kwargs): diff --git a/tests/unit/plugins/modules/test_bitwarden_item.py b/tests/unit/plugins/modules/test_bitwarden_item.py index 0089415d..e49a10a3 100644 --- a/tests/unit/plugins/modules/test_bitwarden_item.py +++ b/tests/unit/plugins/modules/test_bitwarden_item.py @@ -22,11 +22,13 @@ import copy import unittest import unittest.mock +from typing import ClassVar import ansible_harness - from ansible_collections.linuxfabrik.lfops.plugins.modules import bitwarden_item as mod -from ansible_collections.linuxfabrik.lfops.plugins.modules.bitwarden_item import diff_and_update +from ansible_collections.linuxfabrik.lfops.plugins.modules.bitwarden_item import ( + diff_and_update, +) class TestDiffAndUpdate(unittest.TestCase): @@ -34,7 +36,7 @@ class TestDiffAndUpdate(unittest.TestCase): def test_takes_over_id(self): current = {'id': 'abc', 'name': 'x'} target = {'name': 'x'} - changed, updated = diff_and_update(current, target) + _changed, updated = diff_and_update(current, target) self.assertEqual(updated['id'], 'abc') def test_no_change_when_equal(self): @@ -83,9 +85,9 @@ def test_nested_dict_no_change(self): class _FakeBitwarden: """Stand-in for the Bitwarden client; records writes instead of doing them.""" - items = [] - edited = [] - created = [] + items: ClassVar[list] = [] + edited: ClassVar[list] = [] + created: ClassVar[list] = [] def __init__(self, *args, **kwargs): pass diff --git a/tests/unit/plugins/modules/test_gpg_key.py b/tests/unit/plugins/modules/test_gpg_key.py index e7d7ac53..f9a2db9e 100644 --- a/tests/unit/plugins/modules/test_gpg_key.py +++ b/tests/unit/plugins/modules/test_gpg_key.py @@ -22,7 +22,6 @@ from ansible_collections.linuxfabrik.lfops.plugins.modules import gpg_key - _KEY = { 'algo': '1', # 1 -> RSA 'length': '2048', diff --git a/tests/unit/plugins/modules/test_nextcloud_occ_app_config.py b/tests/unit/plugins/modules/test_nextcloud_occ_app_config.py index 9871eb69..66a9933b 100644 --- a/tests/unit/plugins/modules/test_nextcloud_occ_app_config.py +++ b/tests/unit/plugins/modules/test_nextcloud_occ_app_config.py @@ -23,8 +23,9 @@ import unittest import ansible_harness - -from ansible_collections.linuxfabrik.lfops.plugins.modules import nextcloud_occ_app_config as mod +from ansible_collections.linuxfabrik.lfops.plugins.modules import ( + nextcloud_occ_app_config as mod, +) class TestValuesMatch(unittest.TestCase): diff --git a/tests/unit/plugins/modules/test_sqlite_query.py b/tests/unit/plugins/modules/test_sqlite_query.py index 684892ae..324790c1 100644 --- a/tests/unit/plugins/modules/test_sqlite_query.py +++ b/tests/unit/plugins/modules/test_sqlite_query.py @@ -18,12 +18,10 @@ __metaclass__ = type -import os import tempfile import unittest import ansible_harness - from ansible_collections.linuxfabrik.lfops.plugins.modules import sqlite_query as mod @@ -98,7 +96,7 @@ class TestMain(unittest.TestCase): def setUp(self): self.tmpdir = tempfile.mkdtemp(prefix='lfops_sqlite_main_test_') - ok, conn = mod.connect(path=self.tmpdir, filename='main.db') + _ok, conn = mod.connect(path=self.tmpdir, filename='main.db') conn.execute('CREATE TABLE t (id INTEGER)') conn.execute('INSERT INTO t VALUES (1)') conn.commit() @@ -108,9 +106,10 @@ def test_successful_query_exits_with_result(self): ansible_harness.set_module_args({ 'path': self.tmpdir, 'db': 'main.db', 'query': 'SELECT id FROM t', }) - with ansible_harness.patch_module(): - with self.assertRaises(ansible_harness.AnsibleExitJson) as cm: - mod.main() + with ansible_harness.patch_module(), self.assertRaises( + ansible_harness.AnsibleExitJson + ) as cm: + mod.main() self.assertEqual(cm.exception.args[0]['query_result'], [{'id': 1}]) self.assertFalse(cm.exception.args[0]['changed']) @@ -118,9 +117,10 @@ def test_failed_query_fails_the_task(self): ansible_harness.set_module_args({ 'path': self.tmpdir, 'db': 'main.db', 'query': 'SELECT * FROM does_not_exist', }) - with ansible_harness.patch_module(): - with self.assertRaises(ansible_harness.AnsibleFailJson) as cm: - mod.main() + with ansible_harness.patch_module(), self.assertRaises( + ansible_harness.AnsibleFailJson + ) as cm: + mod.main() self.assertIn('Query failed', cm.exception.args[0]['msg']) diff --git a/tests/unit/plugins/modules/test_uptimerobot_monitor.py b/tests/unit/plugins/modules/test_uptimerobot_monitor.py index 54491ee9..282594dc 100644 --- a/tests/unit/plugins/modules/test_uptimerobot_monitor.py +++ b/tests/unit/plugins/modules/test_uptimerobot_monitor.py @@ -20,7 +20,9 @@ import unittest -from ansible_collections.linuxfabrik.lfops.plugins.modules import uptimerobot_monitor as mod +from ansible_collections.linuxfabrik.lfops.plugins.modules import ( + uptimerobot_monitor as mod, +) class TestNormalizeAlertContacts(unittest.TestCase): diff --git a/tests/unit/plugins/modules/test_uptimerobot_mwindow.py b/tests/unit/plugins/modules/test_uptimerobot_mwindow.py index fd3eeeb7..685c9c41 100644 --- a/tests/unit/plugins/modules/test_uptimerobot_mwindow.py +++ b/tests/unit/plugins/modules/test_uptimerobot_mwindow.py @@ -17,7 +17,9 @@ import unittest -from ansible_collections.linuxfabrik.lfops.plugins.modules import uptimerobot_mwindow as mod +from ansible_collections.linuxfabrik.lfops.plugins.modules import ( + uptimerobot_mwindow as mod, +) class TestHhmmToMinutes(unittest.TestCase): From 78ab5ab7e125aeee0fe4f1d2358d16bb511b016c Mon Sep 17 00:00:00 2001 From: Linuxfabrik Date: Mon, 3 Aug 2026 16:43:18 +0200 Subject: [PATCH 3/3] style: apply ruff format First run of the formatter on this repository. No behaviour change. --- plugins/filter/combine_lod.py | 17 +- plugins/filter/platform_select.py | 34 +-- plugins/lookup/bitwarden_item.py | 37 ++- plugins/module_utils/bitwarden.py | 149 ++++++----- plugins/module_utils/ipa_diff.py | 39 +-- plugins/module_utils/uptimerobot.py | 174 +++++++++---- plugins/modules/bitwarden_item.py | 51 ++-- plugins/modules/gpg_key.py | 77 ++++-- plugins/modules/lvm_pv.py | 78 +++--- plugins/modules/nextcloud_occ_app.py | 54 +++- plugins/modules/nextcloud_occ_app_config.py | 45 ++-- .../modules/nextcloud_occ_system_config.py | 35 +-- plugins/modules/sqlite_query.py | 20 +- plugins/modules/uptimerobot_account_info.py | 34 ++- plugins/modules/uptimerobot_alert_contact.py | 94 ++++--- .../modules/uptimerobot_alert_contact_info.py | 28 +- plugins/modules/uptimerobot_monitor.py | 239 +++++++++++------ plugins/modules/uptimerobot_monitor_info.py | 28 +- plugins/modules/uptimerobot_mwindow.py | 148 +++++++---- plugins/modules/uptimerobot_mwindow_info.py | 28 +- plugins/modules/uptimerobot_psp.py | 116 ++++++--- plugins/modules/uptimerobot_psp_info.py | 28 +- tests/unit/plugins/filter/test_combine_lod.py | 240 ++++++++++++------ .../plugins/filter/test_platform_select.py | 14 +- .../plugins/lookup/test_bitwarden_item.py | 29 ++- .../plugins/module_utils/test_bitwarden.py | 69 +++-- .../plugins/module_utils/test_ipa_diff.py | 17 +- .../plugins/module_utils/test_uptimerobot.py | 18 +- .../plugins/modules/test_bitwarden_item.py | 68 +++-- tests/unit/plugins/modules/test_gpg_key.py | 3 +- .../modules/test_nextcloud_occ_app_config.py | 55 ++-- .../unit/plugins/modules/test_sqlite_query.py | 54 ++-- .../modules/test_uptimerobot_monitor.py | 12 +- .../modules/test_uptimerobot_mwindow.py | 10 +- tests/unit/test_plugin_docs.py | 19 +- 35 files changed, 1405 insertions(+), 756 deletions(-) diff --git a/plugins/filter/combine_lod.py b/plugins/filter/combine_lod.py index de4b188c..1af7dc13 100644 --- a/plugins/filter/combine_lod.py +++ b/plugins/filter/combine_lod.py @@ -14,7 +14,7 @@ from ansible.errors import AnsibleFilterError -DOCUMENTATION = r''' +DOCUMENTATION = r""" name: combine_lod version_added: "3.0.0" short_description: Merge lists of dictionaries by a unique key @@ -45,9 +45,9 @@ - Pass a list when no single key is unique on its own (e.g. C(["server_name", "server_port"]) for vHosts where the same hostname can appear on multiple ports). type: raw default: name -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" # create two lists of dictionaries - set_fact: # this list could be in the role defaults @@ -152,14 +152,14 @@ # value: 1 # - name: net.core.somaxconn # value: 2048 -''' +""" -RETURN = r''' +RETURN = r""" _value: description: Resulting merged list of dictionaries. type: list elements: dictionary -''' +""" def combine_lod(*args, **kwargs): @@ -184,7 +184,9 @@ def combine_lod(*args, **kwargs): for lod in list(args): for item in lod: if not isinstance(item, collections.abc.MutableMapping): - raise AnsibleFilterError('found a non-dictionary item in the list, this is not supported') + raise AnsibleFilterError( + 'found a non-dictionary item in the list, this is not supported' + ) # A unique_key is the item's identity, so every key must be set # explicitly and may not be left to a default applied elsewhere: @@ -223,7 +225,6 @@ def combine_lod(*args, **kwargs): return list(result.values()) - class FilterModule: """Register custom filter plugins in Ansible""" diff --git a/plugins/filter/platform_select.py b/plugins/filter/platform_select.py index 4a6d2e00..79cf042f 100644 --- a/plugins/filter/platform_select.py +++ b/plugins/filter/platform_select.py @@ -12,7 +12,7 @@ from ansible.errors import AnsibleFilterError -DOCUMENTATION = r''' +DOCUMENTATION = r""" name: platform_select version_added: "6.0.2" short_description: Pick the value matching the target host from a platform-keyed dictionary @@ -34,9 +34,9 @@ description: Value to return when no key in I(_input) matches the target host. If omitted, an unmatched call raises C(AnsibleFilterError). type: raw required: false -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" # in a role's vars/main.yml (auto-loaded at play parse, so visible to roles # that run earlier in the same play via the `__dependent_var` pattern): mariadb_server__python__modules__dependent_var: @@ -67,13 +67,13 @@ + (apache_httpd__python__modules__dependent_var | linuxfabrik.lfops.platform_select(ansible_facts, default=[])) }}' -''' +""" -RETURN = r''' +RETURN = r""" _value: description: The value associated with the most specific matching key in I(_input), or the supplied I(default) if no key matches. type: raw -''' +""" _SENTINEL = object() @@ -87,13 +87,13 @@ def platform_select(values, ansible_facts, default=_SENTINEL): """ if not isinstance(values, dict): raise AnsibleFilterError( - "platform_select: input must be a dict keyed by platform identifier, " - f"got {type(values).__name__}" + 'platform_select: input must be a dict keyed by platform identifier, ' + f'got {type(values).__name__}' ) if not isinstance(ansible_facts, dict): raise AnsibleFilterError( - "platform_select: ansible_facts must be a dict, " - f"got {type(ansible_facts).__name__}" + 'platform_select: ansible_facts must be a dict, ' + f'got {type(ansible_facts).__name__}' ) os_family = ansible_facts.get('os_family') @@ -106,10 +106,10 @@ def platform_select(values, ansible_facts, default=_SENTINEL): # least to most specific and the later (more specific) call wins. candidates = [ f'{distribution}{version}' if distribution and version else None, - f'{distribution}{major}' if distribution and major else None, + f'{distribution}{major}' if distribution and major else None, distribution, - f'{os_family}{version}' if os_family and version else None, - f'{os_family}{major}' if os_family and major else None, + f'{os_family}{version}' if os_family and version else None, + f'{os_family}{major}' if os_family and major else None, os_family, ] for key in candidates: @@ -119,10 +119,10 @@ def platform_select(values, ansible_facts, default=_SENTINEL): if default is not _SENTINEL: return default raise AnsibleFilterError( - f"platform_select: no key in the input dict matched the target host " - f"(os_family={os_family!r}, distribution={distribution!r}, " - f"distribution_major_version={major!r}, distribution_version={version!r}); " - f"input keys: {sorted(values)}" + f'platform_select: no key in the input dict matched the target host ' + f'(os_family={os_family!r}, distribution={distribution!r}, ' + f'distribution_major_version={major!r}, distribution_version={version!r}); ' + f'input keys: {sorted(values)}' ) diff --git a/plugins/lookup/bitwarden_item.py b/plugins/lookup/bitwarden_item.py index c001f615..b89a727b 100644 --- a/plugins/lookup/bitwarden_item.py +++ b/plugins/lookup/bitwarden_item.py @@ -10,7 +10,7 @@ __metaclass__ = type -DOCUMENTATION = r''' +DOCUMENTATION = r""" lookup: bitwarden_item short_description: Fetch (or create) a Bitwarden login item @@ -93,9 +93,9 @@ description: Username for the login item. Used both as a search filter and, if the item has to be created, as the C(login.username) value. required: False type: str -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" - name: 'The normal way using this lookup plugin. Search for the Bitwarden item using hostname, purpose and username. If not found, creates a new item called `appsrv01 - MariaDB`. Returns the password item, including a `username` and a `password` subkey.' ansible.builtin.debug: msg: "{{ lookup('linuxfabrik.lfops.bitwarden_item', @@ -170,9 +170,9 @@ 'collection_id': '16ea112a-dd5f-4f68-9dfb-95a9f302a8a5', }, ) }}" -''' +""" -RETURN = r''' +RETURN = r""" collectionIds: description: List of collection IDs in which the item is. type: list @@ -278,7 +278,7 @@ type: str returned: always sample: 'root' -''' +""" from ansible.errors import AnsibleError from ansible.plugins.lookup import LookupBase @@ -292,13 +292,15 @@ # https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#developing-lookup-plugins # inspired by the lookup plugins lastpass (same topic) and redis (more modern) -class LookupModule(LookupBase): +class LookupModule(LookupBase): def run(self, terms, variables=None, **kwargs): bw = Bitwarden() if not bw.is_unlocked: - raise AnsibleError('Not logged into Bitwarden, or Bitwarden Vault is locked. Please run `bw login` and `bw unlock` first.') + raise AnsibleError( + 'Not logged into Bitwarden, or Bitwarden Vault is locked. Please run `bw login` and `bw unlock` first.' + ) display.vvv('lfbwlp - run - bitwarden vault is unlocked') bw.sync() @@ -315,12 +317,17 @@ def run(self, terms, variables=None, **kwargs): notes = term.get('notes', 'Generated by Ansible.') organization_id = term.get('organization_id', None) password_length = term.get('password_length', 60) - password_choice = term.get('password_choice', '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') + password_choice = term.get( + 'password_choice', + '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', + ) purpose = term.get('purpose', None) uris = term.get('uris', []) username = term.get('username', None) except Exception as e: - raise AnsibleError(f'Encountered exception while fetching {term}: {e}') from e + raise AnsibleError( + f'Encountered exception while fetching {term}: {e}' + ) from e if id_: result = bw.get_item_by_id(id_) @@ -330,17 +337,21 @@ def run(self, terms, variables=None, **kwargs): result['username'] = result['login']['username'] result['password'] = result['login']['password'] ret.append(result) - continue # done here, go to next term + continue # done here, go to next term else: # item not found by ID. if there is an ID given we expect it to exist raise AnsibleError(f'Item with id {id_} not found.') name = Bitwarden.get_pretty_name(name, hostname, purpose) display.vvv(f'lfbwlp - run - get item: {name}') - result = bw.get_items(name, username, folder_id, collection_id, organization_id) + result = bw.get_items( + name, username, folder_id, collection_id, organization_id + ) if len(result) > 1: - raise AnsibleError('Found multiple Bitwarden items with the same name/title and username, cannot decide which one to use. Aborting.') + raise AnsibleError( + 'Found multiple Bitwarden items with the same name/title and username, cannot decide which one to use. Aborting.' + ) if len(result) == 1: display.vvv('lfbwlp - run - found existing item') diff --git a/plugins/module_utils/bitwarden.py b/plugins/module_utils/bitwarden.py index 09a3c3c5..7b82f5fd 100644 --- a/plugins/module_utils/bitwarden.py +++ b/plugins/module_utils/bitwarden.py @@ -32,6 +32,7 @@ try: from ansible.utils.display import Display + display = Display() except ImportError: # When used from a module (not a lookup plugin), this code runs inside an AnsiballZ @@ -39,6 +40,7 @@ class _NoopDisplay: def vvv(self, msg, **kwargs): pass + display = _NoopDisplay() @@ -95,7 +97,10 @@ def prepare_multipart_no_base64(fields): mime = value.get('mime_type') if not mime: try: - mime = mimetypes.guess_type(filename or '', strict=False)[0] or 'application/octet-stream' + mime = ( + mimetypes.guess_type(filename or '', strict=False)[0] + or 'application/octet-stream' + ) except Exception: mime = 'application/octet-stream' main_type, _sep, sub_type = mime.partition('/') @@ -106,7 +111,9 @@ def prepare_multipart_no_base64(fields): if not content and filename: with open(to_bytes(filename, errors='surrogate_or_strict'), 'rb') as f: - part = email.mime.application.MIMEApplication(f.read(), _encoder=email.encoders.encode_noop) + part = email.mime.application.MIMEApplication( + f.read(), _encoder=email.encoders.encode_noop + ) del part['Content-Type'] part.add_header('Content-Type', f'{main_type}/{sub_type}') else: @@ -115,16 +122,12 @@ def prepare_multipart_no_base64(fields): part.add_header('Content-Disposition', 'form-data') del part['MIME-Version'] - part.set_param( - 'name', - field, - header='Content-Disposition' - ) + part.set_param('name', field, header='Content-Disposition') if filename: part.set_param( 'filename', to_native(os.path.basename(filename)), - header='Content-Disposition' + header='Content-Disposition', ) m.attach(part) @@ -141,7 +144,7 @@ def prepare_multipart_no_base64(fields): return ( parser(headers)['content-type'], # Message converts to native strings - b_content + b_content, ) @@ -174,21 +177,33 @@ def _api_call(self, url_path, method='GET', body=None, body_format='json'): try: content_type, body = prepare_multipart_no_base64(body) except (TypeError, ValueError) as e: - raise BitwardenException(f'failed to parse body as form-multipart: {to_native(e)}') from e + raise BitwardenException( + f'failed to parse body as form-multipart: {to_native(e)}' + ) from e headers['Content-Type'] = content_type # mostly taken from ansible.builtin.url lookup plugin try: # increased the timeout since listing all items via `list/object/items` takes forever (13s for ~2500 items) - response = open_url(url, method=method, data=body, headers=headers, timeout=60) + response = open_url( + url, method=method, data=body, headers=headers, timeout=60 + ) except HTTPError as e: - raise BitwardenException(f'Received HTTP error for {url} : {to_native(e)}') from e + raise BitwardenException( + f'Received HTTP error for {url} : {to_native(e)}' + ) from e except URLError as e: - raise BitwardenException(f'Failed lookup url for {url} : {to_native(e)}') from e + raise BitwardenException( + f'Failed lookup url for {url} : {to_native(e)}' + ) from e except SSLValidationError as e: - raise BitwardenException(f"Error validating the server's certificate for {url}: {to_native(e)}") from e + raise BitwardenException( + f"Error validating the server's certificate for {url}: {to_native(e)}" + ) from e except ConnectionError as e: - raise BitwardenException(f'Error connecting to {url}: {to_native(e)}') from e + raise BitwardenException( + f'Error connecting to {url}: {to_native(e)}' + ) from e try: result = json.loads(to_text(response.read())) @@ -196,11 +211,10 @@ def _api_call(self, url_path, method='GET', body=None, body_format='json'): raise BitwardenException(f'Unable to load JSON: {to_native(e)}') from e if not result.get('success'): - raise BitwardenException(f"API call failed: {result.get('data')}") + raise BitwardenException(f'API call failed: {result.get("data")}') return result - def _load_cache(self): """Load the cache from disk. If missing, unreadable, or invalid, start with an empty cache. Freshness is handled by sync(). @@ -210,8 +224,12 @@ def _load_cache(self): data = json.load(f) if data.get('version') == CACHE_VERSION: self._cache = data - item_count = len(self._cache['items']) if self._cache['items'] is not None else 0 - display.vvv(f'lfbw - cache loaded from {CACHE_FILE} ({item_count} items)') + item_count = ( + len(self._cache['items']) if self._cache['items'] is not None else 0 + ) + display.vvv( + f'lfbw - cache loaded from {CACHE_FILE} ({item_count} items)' + ) return except (OSError, ValueError, json.decoder.JSONDecodeError): pass @@ -223,10 +241,8 @@ def _load_cache(self): } display.vvv('lfbw - no valid cache found, starting fresh') - def _save_cache(self): - """Write the cache to disk atomically. - """ + """Write the cache to disk atomically.""" try: fd, tmp_path = tempfile.mkstemp( dir=os.path.dirname(CACHE_FILE), @@ -243,7 +259,6 @@ def _save_cache(self): except OSError: display.vvv(f'lfbw - failed to save cache to {CACHE_FILE}') - def _get_template(self, template_name): """Return a template from cache, fetching from API on first use. Templates are static API schema definitions that never change. @@ -257,15 +272,12 @@ def _get_template(self, template_name): display.vvv(f'lfbw - using cached template "{template_name}"') return copy.deepcopy(self._cache['templates'][template_name]) - @property def is_unlocked(self): - """Check if the Bitwarden vault is unlocked. - """ + """Check if the Bitwarden vault is unlocked.""" result = self._api_call('status') return result['data']['template']['status'] == 'unlocked' - def sync(self, force=False, interval=60): """Pull the latest vault data from server and repopulate the items cache. Syncs only if the last sync was more than `interval` seconds ago, unless `force` is True. @@ -278,11 +290,17 @@ def sync(self, force=False, interval=60): result = self._api_call('list/object/items') self._cache['items'] = result['data']['data'] self._cache['sync_timestamp'] = time.time() - display.vvv(f"lfbw - sync complete, cached {len(self._cache['items'])} items") + display.vvv(f'lfbw - sync complete, cached {len(self._cache["items"])} items') self._save_cache() - - def get_items(self, name, username=None, folder_id=None, collection_id=None, organization_id=None): + def get_items( + self, + name, + username=None, + folder_id=None, + collection_id=None, + organization_id=None, + ): """Search for items in Bitwarden. Returns a list of the items that *exactly* matches all the parameters. A complete object: @@ -330,23 +348,23 @@ def get_items(self, name, username=None, folder_id=None, collection_id=None, org matching_items = [] for item in self._cache['items']: if item.get('type') != 1: - continue # skip non-login items (cards, secure notes, identities) - if item['name'] == name \ - and (item['login']['username'] == username) \ - and (item.get('folderId') == folder_id) \ - and ( - # cover case if collectionIds is an empty list - (collection_id is None and not item.get('collectionIds')) \ - or \ - (collection_id in item.get('collectionIds', [])) \ - ) \ - and (item.get('organizationId') == organization_id): + continue # skip non-login items (cards, secure notes, identities) + if ( + item['name'] == name + and (item['login']['username'] == username) + and (item.get('folderId') == folder_id) + and ( + # cover case if collectionIds is an empty list + (collection_id is None and not item.get('collectionIds')) + or (collection_id in item.get('collectionIds', [])) + ) + and (item.get('organizationId') == organization_id) + ): matching_items.append(item) display.vvv(f'lfbw - found {len(matching_items)} matching item(s)') return matching_items - def get_item_by_id(self, item_id): """Get an item by ID from Bitwarden. Looks in the cache first, then falls back to the API (the item may have been created externally). Returns the item; raises @@ -362,8 +380,11 @@ def get_item_by_id(self, item_id): result = self._api_call(f'object/item/{item_id}') return result['data'] - - def generate(self, password_length=60, password_choice='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'): # nosec B107 - this is the character set to draw from, not a password + def generate( + self, + password_length=60, + password_choice='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', + ): # nosec B107 - this is the character set to draw from, not a password """Generates a random password of a given length. If you want to generate a hex-based password, ensure that password_length is positive and even (as hex characters typically come in pairs representing bytes), and that password_choice is set to '0123456789abcdef'. @@ -375,10 +396,11 @@ def generate(self, password_length=60, password_choice='0123456789abcdefghijklmn if password_length <= 0: raise ValueError('Password length must be a positive integer.') if password_choice.lower() == '0123456789abcdef' and password_length % 2 != 0: - raise ValueError('Password length must be an even number to represent full hex bytes.') + raise ValueError( + 'Password length must be an even number to represent full hex bytes.' + ) return ''.join(secrets.choice(password_choice) for _ in range(password_length)) - def get_template_item_login_uri(self, uris): """Get an item.login.uri object from the vault. @@ -393,13 +415,14 @@ def get_template_item_login_uri(self, uris): if uris: template = self._get_template('item.login.uri') for uri in uris: - login_uri = template.copy() # make sure we are not editing the same object repeatedly + login_uri = ( + template.copy() + ) # make sure we are not editing the same object repeatedly login_uri['uri'] = uri login_uris.append(login_uri) return login_uris - def get_template_item_login(self, username=None, password=None, login_uris=None): """Get an item.login object from the vault. @@ -420,8 +443,15 @@ def get_template_item_login(self, username=None, password=None, login_uris=None) return login - - def get_template_item(self, name, login=None, notes=None, organization_id=None, collection_ids=None, folder_id=None): + def get_template_item( + self, + name, + login=None, + notes=None, + organization_id=None, + collection_ids=None, + folder_id=None, + ): """Get an item.login object from the vault. A complete item object: @@ -452,10 +482,8 @@ def get_template_item(self, name, login=None, notes=None, organization_id=None, return item - def create_item(self, item): - """Creates an item object in Bitwarden. - """ + """Creates an item object in Bitwarden.""" display.vvv(f'lfbw - creating item "{item.get("name", "")}"') result = self._api_call('object/item', method='POST', body=item) self._cache['items'].append(result['data']) @@ -463,10 +491,8 @@ def create_item(self, item): time.sleep(1) return result['data'] - def edit_item(self, item, item_id): - """Edits an item object in Bitwarden. - """ + """Edits an item object in Bitwarden.""" display.vvv(f'lfbw - editing item {item_id}') result = self._api_call(f'object/item/{item_id}', method='PUT', body=item) for i, cached_item in enumerate(self._cache['items']): @@ -477,10 +503,8 @@ def edit_item(self, item, item_id): time.sleep(1) return result['data'] - def add_attachment(self, item_id, attachment_path): - """Adds the file at `attachment_path` to the item specified by `item_id` - """ + """Adds the file at `attachment_path` to the item specified by `item_id`""" display.vvv(f'lfbw - adding attachment "{attachment_path}" to item {item_id}') body = { @@ -488,7 +512,12 @@ def add_attachment(self, item_id, attachment_path): 'filename': attachment_path, }, } - result = self._api_call(f'attachment?itemId={item_id}', method='POST', body=body, body_format='form-multipart') + result = self._api_call( + f'attachment?itemId={item_id}', + method='POST', + body=body, + body_format='form-multipart', + ) for i, cached_item in enumerate(self._cache['items']): if cached_item.get('id') == item_id: self._cache['items'][i] = result['data'] diff --git a/plugins/module_utils/ipa_diff.py b/plugins/module_utils/ipa_diff.py index 7f25fd54..2f63dd5b 100644 --- a/plugins/module_utils/ipa_diff.py +++ b/plugins/module_utils/ipa_diff.py @@ -24,9 +24,13 @@ def _compare_key(arg, ipa_arg): arg = [arg] if len(ipa_arg) != len(arg): return False - if ipa_arg and arg and not ( - isinstance(ipa_arg[0], type(arg[0])) - or isinstance(arg[0], type(ipa_arg[0])) + if ( + ipa_arg + and arg + and not ( + isinstance(ipa_arg[0], type(arg[0])) + or isinstance(arg[0], type(ipa_arg[0])) + ) ): arg = [to_text(_a) for _a in arg] try: @@ -52,12 +56,14 @@ def add_entry_diff(self, name, before, after): """Record a diff entry for one IPA object.""" if before == after: return - self._diffs.append({ - 'before_header': name, - 'after_header': name, - 'before': before, - 'after': after, - }) + self._diffs.append( + { + 'before_header': name, + 'after_header': name, + 'before': before, + 'after': after, + } + ) def gen_args_diff(args, res_find, ignore=None): @@ -77,13 +83,15 @@ def gen_args_diff(args, res_find, ignore=None): if key in ignore: continue arg = args[key] - ipa_arg = res_find.get(key, [""]) + ipa_arg = res_find.get(key, ['']) if not _compare_key(arg, ipa_arg): # Normalize for display - _ipa = ipa_arg[0] if isinstance(ipa_arg, (list, tuple)) \ - and len(ipa_arg) == 1 else ipa_arg - _arg = arg[0] if isinstance(arg, (list, tuple)) \ - and len(arg) == 1 else arg + _ipa = ( + ipa_arg[0] + if isinstance(ipa_arg, (list, tuple)) and len(ipa_arg) == 1 + else ipa_arg + ) + _arg = arg[0] if isinstance(arg, (list, tuple)) and len(arg) == 1 else arg before[key] = _ipa after[key] = _arg return before, after @@ -99,8 +107,7 @@ def gen_member_diff(member_key, add_list, del_list, current_list): return {}, {} current = sorted(current_list or []) desired = sorted( - [x for x in current if x not in (del_list or [])] - + (add_list or []) + [x for x in current if x not in (del_list or [])] + (add_list or []) ) return {member_key: current}, {member_key: desired} diff --git a/plugins/module_utils/uptimerobot.py b/plugins/module_utils/uptimerobot.py index 0713dc7a..767acad0 100644 --- a/plugins/module_utils/uptimerobot.py +++ b/plugins/module_utils/uptimerobot.py @@ -49,9 +49,14 @@ CACHE_TTL_SECONDS = 60 CACHE_DIR = os.path.join(os.path.expanduser('~/.cache'), 'ansible-uptimerobot') -_CACHEABLE_GETS = frozenset({ - 'getAlertContacts', 'getMWindows', 'getMonitors', 'getPSPs', -}) +_CACHEABLE_GETS = frozenset( + { + 'getAlertContacts', + 'getMWindows', + 'getMonitors', + 'getPSPs', + } +) _WRITE_INVALIDATES = { 'deleteAlertContact': 'getAlertContacts', 'deleteMWindow': 'getMWindows', @@ -73,15 +78,25 @@ MONITOR_STATUS_READ = {0: 'paused', 1: 'wait', 2: 'up', 8: 'seems_down', 9: 'down'} MONITOR_STATUS_WRITE = {'paused': 0, 'up': 1} # write side only allows pause/un-pause MONITOR_SUB_TYPE = { - 'http': 1, 'https': 443, 'ftp': 21, 'smtp': 25, - 'pop3': 110, 'imap': 143, 'custom': 99, + 'http': 1, + 'https': 443, + 'ftp': 21, + 'smtp': 25, + 'pop3': 110, + 'imap': 143, + 'custom': 99, } KEYWORD_TYPE = {'exist': 1, 'notex': 2} KEYWORD_CASE_TYPE = {'cs': 0, 'ci': 1} HTTP_AUTH_TYPE = {'basic': 1, 'digest': 2} HTTP_METHOD = { - 'head': 1, 'get': 2, 'post': 3, 'put': 4, - 'patch': 5, 'delete': 6, 'options': 7, + 'head': 1, + 'get': 2, + 'post': 3, + 'put': 4, + 'patch': 5, + 'delete': 6, + 'options': 7, } POST_TYPE = {'key-value': 1, 'raw data': 2} POST_CONTENT_TYPE = {'text/html': 0, 'content/json': 1} @@ -89,8 +104,13 @@ MWINDOW_TYPE = {'once': 1, 'daily': 2, 'weekly': 3, 'monthly': 4} MWINDOW_DAY = { - 'mon': 1, 'tue': 2, 'wed': 3, 'thu': 4, - 'fri': 5, 'sat': 6, 'sun': 7, + 'mon': 1, + 'tue': 2, + 'wed': 3, + 'thu': 4, + 'fri': 5, + 'sat': 6, + 'sun': 7, } MWINDOW_STATUS = {'paused': 0, 'active': 1} @@ -98,10 +118,23 @@ PSP_STATUS = {'paused': 0, 'active': 1} ALERT_CONTACT_TYPE_READ = { - 1: 'sms', 2: 'email', 3: 'twitter_dm', 5: 'web-hook', 6: 'pushbullet', - 7: 'zapier', 8: 'pushover', 9: 'hipchat', 10: 'slack', 11: 'voice-call', - 12: 'splunk', 13: 'pagerduty', 14: 'opsgenie', 15: 'ms_teams', - 16: 'google_chat', 17: 'discord', 18: 'mattermost', + 1: 'sms', + 2: 'email', + 3: 'twitter_dm', + 5: 'web-hook', + 6: 'pushbullet', + 7: 'zapier', + 8: 'pushover', + 9: 'hipchat', + 10: 'slack', + 11: 'voice-call', + 12: 'splunk', + 13: 'pagerduty', + 14: 'opsgenie', + 15: 'ms_teams', + 16: 'google_chat', + 17: 'discord', + 18: 'mattermost', } @@ -148,11 +181,13 @@ def resolve_api_key(module, api_key, api_key_file): module.log(f'uptimerobot: api_key resolved from env {ENV_API_KEY}') return env - module.fail_json(msg=( - 'No UptimeRobot API key found. Provide one via the `api_key` parameter, ' - f'an `api_key_file` (default: {DEFAULT_API_KEY_FILE}), or the {ENV_API_KEY} ' - 'environment variable.' - )) + module.fail_json( + msg=( + 'No UptimeRobot API key found. Provide one via the `api_key` parameter, ' + f'an `api_key_file` (default: {DEFAULT_API_KEY_FILE}), or the {ENV_API_KEY} ' + 'environment variable.' + ) + ) # --- Wire format helpers ----------------------------------------------------- @@ -168,7 +203,7 @@ def alert_contacts_wire(items): parts = [] for item in items: parts.append( - f"{item['id']}_{item.get('threshold', 0)}_{item.get('recurrence', 0)}" + f'{item["id"]}_{item.get("threshold", 0)}_{item.get("recurrence", 0)}' ) return '-'.join(parts) @@ -200,10 +235,7 @@ def _safe_keys(params): Used for module.log() so we can see which fields a call is sending without leaking secrets to syslog. """ - return sorted( - f'{k}=' if k in _SENSITIVE_KEYS else k - for k in params - ) + return sorted(f'{k}=' if k in _SENSITIVE_KEYS else k for k in params) def _cache_key(endpoint, api_key, params): @@ -285,7 +317,9 @@ def _request(module, api_key, endpoint, params, result_key): cached = _cache_read(endpoint, api_key, params) if cached is not None: n = len(cached) if isinstance(cached, list) else 1 - module.log(f'uptimerobot: cache HIT {endpoint} ({n} items, ttl={CACHE_TTL_SECONDS}s)') + module.log( + f'uptimerobot: cache HIT {endpoint} ({n} items, ttl={CACHE_TTL_SECONDS}s)' + ) return True, cached success, result = _request_uncached(module, api_key, endpoint, params, result_key) @@ -330,7 +364,9 @@ def _request_uncached(module, api_key, endpoint, params, result_key): pages += 1 if status == 429: - retry_after = int(info.get('retry-after') or DEFAULT_RATE_LIMIT_RETRY_SECONDS) + retry_after = int( + info.get('retry-after') or DEFAULT_RATE_LIMIT_RETRY_SECONDS + ) sleep_for = min(retry_after, 60) module.warn( f'uptimerobot: rate limited on {endpoint} (HTTP 429); sleeping {sleep_for}s and retrying once', @@ -358,13 +394,17 @@ def _request_uncached(module, api_key, endpoint, params, result_key): if payload.get('stat') != 'ok': err = payload.get('error') or {} - module.log(f"uptimerobot: POST {endpoint} stat=fail type={err.get('type', 'unknown')}") - return False, f"{err.get('type', 'unknown')}: {err.get('message', payload)}" + module.log( + f'uptimerobot: POST {endpoint} stat=fail type={err.get("type", "unknown")}' + ) + return False, f'{err.get("type", "unknown")}: {err.get("message", payload)}' if payload.get(result_key) is None: # Some endpoints return only `stat: 'ok'` (e.g. delete, edit when no # detail is included). Fall back to the message field if present. - module.log(f'uptimerobot: POST {endpoint} stat=ok (no {result_key} in payload)') + module.log( + f'uptimerobot: POST {endpoint} stat=ok (no {result_key} in payload)' + ) return True, payload.get('message', payload) item = payload[result_key] @@ -382,7 +422,9 @@ def _request_uncached(module, api_key, endpoint, params, result_key): break offset += PAGE_SIZE - module.log(f'uptimerobot: POST {endpoint} stat=ok pages={pages} items={len(aggregated)}') + module.log( + f'uptimerobot: POST {endpoint} stat=ok pages={pages} items={len(aggregated)}' + ) return True, aggregated @@ -413,14 +455,28 @@ def _translate_keys(params, mapping_per_key): # Common to new + edit. `id` and `status` are edit-only; `type` is create-only. _MONITOR_COMMON_KEYS = { - 'friendly_name', 'url', 'sub_type', 'port', - 'keyword_type', 'keyword_case_type', 'keyword_value', - 'interval', 'timeout', - 'http_username', 'http_password', 'http_auth_type', - 'post_type', 'post_value', 'http_method', 'post_content_type', - 'alert_contacts', 'mwindows', - 'custom_http_headers', 'custom_http_statuses', - 'ignore_ssl_errors', 'disable_domain_expire_notifications', + 'friendly_name', + 'url', + 'sub_type', + 'port', + 'keyword_type', + 'keyword_case_type', + 'keyword_value', + 'interval', + 'timeout', + 'http_username', + 'http_password', + 'http_auth_type', + 'post_type', + 'post_value', + 'http_method', + 'post_content_type', + 'alert_contacts', + 'mwindows', + 'custom_http_headers', + 'custom_http_statuses', + 'ignore_ssl_errors', + 'disable_domain_expire_notifications', } _MONITOR_TRANSLATIONS = { @@ -493,7 +549,9 @@ def _translate_monitor_response(item): item['status'] = MONITOR_STATUS_READ.get(item['status'], item['status']) for contact in item.get('alert_contacts') or []: if 'type' in contact and contact['type'] is not None: - contact['type'] = ALERT_CONTACT_TYPE_READ.get(contact['type'], contact['type']) + contact['type'] = ALERT_CONTACT_TYPE_READ.get( + contact['type'], contact['type'] + ) def new_monitor(module, api_key, params): @@ -506,7 +564,9 @@ def new_monitor(module, api_key, params): def edit_monitor(module, api_key, params): allowed = _MONITOR_COMMON_KEYS | {'id', 'status'} body = _filter_keys(params, allowed) - body = _translate_keys(body, dict(_MONITOR_TRANSLATIONS, status=MONITOR_STATUS_WRITE)) + body = _translate_keys( + body, dict(_MONITOR_TRANSLATIONS, status=MONITOR_STATUS_WRITE) + ) return _request(module, api_key, 'editMonitor', body, 'monitor') @@ -571,7 +631,15 @@ def new_mwindow(module, api_key, params): def edit_mwindow(module, api_key, params): - allowed = {'id', 'friendly_name', 'type', 'value', 'start_time', 'duration', 'status'} + allowed = { + 'id', + 'friendly_name', + 'type', + 'value', + 'start_time', + 'duration', + 'status', + } body = _filter_keys(params, allowed) body = _translate_keys(body, _MWINDOW_TRANSLATIONS) return _request(module, api_key, 'editMWindow', body, 'mwindow') @@ -618,8 +686,12 @@ def _translate_psp_response(item): def new_psp(module, api_key, params): # `status` is not allowed on create per upstream; only edit_psp accepts it. allowed = { - 'friendly_name', 'monitors', 'custom_domain', - 'password', 'sort', 'hide_url_links', + 'friendly_name', + 'monitors', + 'custom_domain', + 'password', + 'sort', + 'hide_url_links', } body = _filter_keys(params, allowed) body = _translate_keys(body, _PSP_TRANSLATIONS) @@ -628,8 +700,14 @@ def new_psp(module, api_key, params): def edit_psp(module, api_key, params): allowed = { - 'id', 'friendly_name', 'monitors', 'custom_domain', - 'password', 'sort', 'hide_url_links', 'status', + 'id', + 'friendly_name', + 'monitors', + 'custom_domain', + 'password', + 'sort', + 'hide_url_links', + 'status', } body = _filter_keys(params, allowed) body = _translate_keys(body, _PSP_TRANSLATIONS) @@ -653,7 +731,9 @@ def delete_psp(module, api_key, psp_id): def get_alert_contacts(module, api_key): - success, contacts = _request(module, api_key, 'getAlertContacts', {}, 'alert_contacts') + success, contacts = _request( + module, api_key, 'getAlertContacts', {}, 'alert_contacts' + ) if not success: return success, contacts if not isinstance(contacts, list): @@ -672,7 +752,9 @@ def _translate_alert_contact_response(item): def delete_alert_contact(module, api_key, contact_id): - return _request(module, api_key, 'deleteAlertContact', {'id': contact_id}, 'alert_contact') + return _request( + module, api_key, 'deleteAlertContact', {'id': contact_id}, 'alert_contact' + ) # --- Per-resource API: account ---------------------------------------------- diff --git a/plugins/modules/bitwarden_item.py b/plugins/modules/bitwarden_item.py index 351d9169..87370a6b 100644 --- a/plugins/modules/bitwarden_item.py +++ b/plugins/modules/bitwarden_item.py @@ -10,7 +10,7 @@ __metaclass__ = type -DOCUMENTATION = r''' +DOCUMENTATION = r""" module: bitwarden_item short_description: Create, update or fetch a Bitwarden login item @@ -95,9 +95,9 @@ description: Username to set on the login item. Used both as a search filter and, if the item has to be created or updated, as the C(login.username) value. required: False type: str -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" - name: 'Get or create a password item from Bitwarden (automated name creation).' linuxfabrik.lfops.bitwarden_item: hostname: 'appsrv11' @@ -145,9 +145,9 @@ - /tmp/file1 - /tmp/file2 register: creds -''' +""" -RETURN = r''' +RETURN = r""" collectionIds: description: List of collection IDs in which the item is. type: list @@ -253,7 +253,7 @@ type: str returned: always sample: 'root' -''' +""" import os @@ -275,8 +275,9 @@ def check_dict_for_changes(current, target): if check_dict_for_changes(current.get(key, {}), value): changed = True - elif (value != current.get(key)) \ - and not (not value and not current.get(key)): # compare None to empty lists and empty strings + elif (value != current.get(key)) and not ( + not value and not current.get(key) + ): # compare None to empty lists and empty strings changed = True return changed @@ -300,7 +301,9 @@ def run_module(): name=dict(type='str', required=False, default=None), notes=dict(type='str', required=False, default='Generated by Ansible.'), organization_id=dict(type='str', required=False, default=None), - password=dict(type='str', required=False, default=None, no_log=False), # if we set it to True, the passwords in the RETURN values are masked too. see https://github.com/ansible/ansible/issues/71789 + password=dict( + type='str', required=False, default=None, no_log=False + ), # if we set it to True, the passwords in the RETURN values are masked too. see https://github.com/ansible/ansible/issues/71789 purpose=dict(type='str', required=False, default=None), uris=dict(type='list', required=False, default=None), username=dict(type='str', required=False, default=None), @@ -310,20 +313,21 @@ def run_module(): # this includes instantiation, a couple of common attr would be the # args/params passed to the execution, as well as if this module # supports check mode - module = AnsibleModule( - argument_spec=module_args, - supports_check_mode=True - ) + module = AnsibleModule(argument_spec=module_args, supports_check_mode=True) attachments = module.params['attachments'] if attachments: basenames = [os.path.basename(attachment) for attachment in attachments] if len(set(basenames)) < len(basenames): - module.fail_json(msg='This module cannot handle multiple attachments with the same basename.') + module.fail_json( + msg='This module cannot handle multiple attachments with the same basename.' + ) for attachment in attachments: if not os.access(attachment, os.R_OK): - module.fail_json(msg=f'Could not read the attachments at "{attachment}".') + module.fail_json( + msg=f'Could not read the attachments at "{attachment}".' + ) # extract the variables to make the code more readable collection_id = module.params['collection_id'] @@ -341,7 +345,9 @@ def run_module(): bw = Bitwarden() if not bw.is_unlocked: - module.fail_json(msg='Not logged into Bitwarden, or Bitwarden Vault is locked. Please run `bw login` and `bw unlock` first.') + module.fail_json( + msg='Not logged into Bitwarden, or Bitwarden Vault is locked. Please run `bw login` and `bw unlock` first.' + ) # to be sure we are up to date bw.sync() @@ -351,10 +357,14 @@ def run_module(): current_item = bw.get_item_by_id(item_id) else: name = Bitwarden.get_pretty_name(name, hostname, purpose) - current_items = bw.get_items(name, username, folder_id, collection_id, organization_id) + current_items = bw.get_items( + name, username, folder_id, collection_id, organization_id + ) if len(current_items) > 1: - module.fail_json(msg='Found multiple Bitwarden items with the same name/title and username, cannot decide which one to use. Aborting.') + module.fail_json( + msg='Found multiple Bitwarden items with the same name/title and username, cannot decide which one to use. Aborting.' + ) current_item = current_items[0] if current_items else None @@ -390,7 +400,10 @@ def run_module(): result = target_item if module.check_mode else bw.create_item(target_item) if attachments: - current_attachments = {current_attachment['fileName'] for current_attachment in result.get('attachments', [])} + current_attachments = { + current_attachment['fileName'] + for current_attachment in result.get('attachments', []) + } attachments_changed = False for attachment in attachments: if os.path.basename(attachment) not in current_attachments: diff --git a/plugins/modules/gpg_key.py b/plugins/modules/gpg_key.py index f23c2f46..f8adb044 100644 --- a/plugins/modules/gpg_key.py +++ b/plugins/modules/gpg_key.py @@ -10,7 +10,7 @@ __metaclass__ = type -DOCUMENTATION = r''' +DOCUMENTATION = r""" module: gpg_key short_description: Find or create a GPG private key @@ -84,9 +84,9 @@ description: Algorithm of the (single) subkey to generate, and to match against existing subkeys. Only meaningful together with I(subkey_length). required: False type: str -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" - name: 'Generate a GPG key by using the defaults' linuxfabrik.lfops.gpg_key: register: gpg_key @@ -104,9 +104,9 @@ subkey_type: 'RSA' subkey_length: 4096 register: gpg_key -''' +""" -RETURN = r''' +RETURN = r""" ascii_armored_private_key: description: ASCII-armored export of the private key. returned: success @@ -239,7 +239,7 @@ returned: success type: str sample: '' -''' +""" import logging import os @@ -307,7 +307,6 @@ def match_key(key, params): # howerever, it is possible to add another one manually later. first_subkey_match = None for _subkey_id, subkey in key['subkey_info'].items(): - if algo_ids.get(int(subkey['algo']), 'Unknown') != params['subkey_type']: continue @@ -323,13 +322,22 @@ def match_key(key, params): def add_armored_exports_and_exit(gpg, module, result): - ascii_armored_private_key = gpg.export_keys(result['key']['fingerprint'], secret=True, passphrase=module.params['passphrase']) + ascii_armored_private_key = gpg.export_keys( + result['key']['fingerprint'], + secret=True, + passphrase=module.params['passphrase'], + ) if not ascii_armored_private_key: # sadly, we do not get more information from the library - module.fail_json(msg='Failed to export armored private key. Is the passphrase correct?', **result) + module.fail_json( + msg='Failed to export armored private key. Is the passphrase correct?', + **result, + ) result['ascii_armored_private_key'] = ascii_armored_private_key - ascii_armored_public_key = gpg.export_keys(result['key']['fingerprint'], passphrase=module.params['passphrase']) + ascii_armored_public_key = gpg.export_keys( + result['key']['fingerprint'], passphrase=module.params['passphrase'] + ) result['ascii_armored_public_key'] = ascii_armored_public_key module.exit_json(**result) @@ -339,16 +347,12 @@ def run_module(): module_args = dict( gpgbinary=dict(type='str', required=False, default='gpg'), gnupghome=dict(type='path', required=False), - name_real=dict(type='str', required=False, default='Autogenerated Key'), name_comment=dict(type='str', required=False, default='Generated by Ansible.'), name_email=dict(type='str', required=False, default='info@example.com'), - key_type=dict(type='str', required=False, default='RSA'), key_length=dict(type='int', required=False, default=1024), - passphrase=dict(type='str', required=False, default='', no_log=True), - subkey_type=dict(type='str', required=False), subkey_length=dict(type='int', required=False), ) @@ -358,14 +362,10 @@ def run_module(): key=None, ) - module = AnsibleModule( - argument_spec=module_args, - supports_check_mode=True - ) + module = AnsibleModule(argument_spec=module_args, supports_check_mode=True) gnupghome = module.params['gnupghome'] if gnupghome and not os.path.isdir(gnupghome): - if module.check_mode: # since there is no directory, there are no keys - meaning we have to generate one for sure. set to changed but do not do anything result['changed'] = True @@ -379,14 +379,24 @@ def run_module(): gnupghome=gnupghome, ) except (OSError, ValueError) as e: - module.fail_json(msg=f'There was an error executing gpg: {to_native(e)}', exception=traceback.format_exc(), **result) + module.fail_json( + msg=f'There was an error executing gpg: {to_native(e)}', + exception=traceback.format_exc(), + **result, + ) # use whatever logic you need to determine whether or not this module # made any modifications to your target keys = gpg.list_keys(secret=True) if keys.returncode != 0: - module.fail_json(msg='Failed to list current keys.', rc=keys.returncode, stdout=keys.data, stderr=keys.stderr, **result) + module.fail_json( + msg='Failed to list current keys.', + rc=keys.returncode, + stdout=keys.data, + stderr=keys.stderr, + **result, + ) # do match on all given arguments except for passphrase (as we do not see that one). match = None @@ -396,7 +406,10 @@ def run_module(): if match is None: match = key else: - module.fail_json(msg='Found multiple keys with the same attributes, cannot decide which one to use. Aborting.', **result) + module.fail_json( + msg='Found multiple keys with the same attributes, cannot decide which one to use. Aborting.', + **result, + ) if match: result['key'] = match @@ -414,8 +427,8 @@ def run_module(): # manipulate or modify the state as needed (this is going to be the # part where your module will do what it needs to do) params = {k: v for k, v in module.params.items() if v is not None} - params.pop('gnupghome', None) # provide default to prevent KeyError - params.pop('gpgbinary', None) # provide default to prevent KeyError + params.pop('gnupghome', None) # provide default to prevent KeyError + params.pop('gpgbinary', None) # provide default to prevent KeyError if not module.params['passphrase']: params['no_protection'] = True @@ -423,12 +436,24 @@ def run_module(): new_key = gpg.gen_key(input_data) if not new_key: # do not echo input_data here: it contains the cleartext passphrase - module.fail_json(msg='Failed to generate a new key.', rc=new_key.returncode, stdout=new_key.data, stderr=new_key.stderr, **result) + module.fail_json( + msg='Failed to generate a new key.', + rc=new_key.returncode, + stdout=new_key.data, + stderr=new_key.stderr, + **result, + ) # list the keys again, as we only got the fingerprint from gen_key() keys = gpg.list_keys(secret=True) if keys.returncode != 0: - module.fail_json(msg='Failed to list current keys.', rc=keys.returncode, stdout=keys.data, stderr=keys.stderr, **result) + module.fail_json( + msg='Failed to list current keys.', + rc=keys.returncode, + stdout=keys.data, + stderr=keys.stderr, + **result, + ) for key in keys: if key['fingerprint'] == new_key.fingerprint: diff --git a/plugins/modules/lvm_pv.py b/plugins/modules/lvm_pv.py index eef2fb9e..1bc45aa7 100644 --- a/plugins/modules/lvm_pv.py +++ b/plugins/modules/lvm_pv.py @@ -76,13 +76,13 @@ def get_pv_status(module, device): """Check if the device is already a PV.""" - cmd = ["pvs", "--noheadings", "--readonly", device] + cmd = ['pvs', '--noheadings', '--readonly', device] return module.run_command(cmd)[0] == 0 def get_pv_size(module, device): """Get current PV size in bytes.""" - cmd = ["pvs", "--noheadings", "--nosuffix", "--units", "b", "-o", "pv_size", device] + cmd = ['pvs', '--noheadings', '--nosuffix', '--units', 'b', '-o', 'pv_size', device] _rc, out, _err = module.run_command(cmd, check_rc=True) return int(out.strip()) @@ -90,70 +90,70 @@ def get_pv_size(module, device): def rescan_device(module, device): """Perform storage rescan for the device.""" base_device = os.path.basename(device) - is_partition = f"/sys/class/block/{base_device}/partition" + is_partition = f'/sys/class/block/{base_device}/partition' # Determine parent device if partition exists parent_device = base_device if os.path.exists(is_partition): parent_device = ( - base_device.rpartition("p")[0] if base_device.startswith("nvme") else base_device.rstrip("0123456789") + base_device.rpartition('p')[0] + if base_device.startswith('nvme') + else base_device.rstrip('0123456789') ) # Determine rescan path - rescan_path = ( - f"/sys/block/{parent_device}/device/{'rescan_controller' if base_device.startswith('nvme') else 'rescan'}" - ) + rescan_path = f'/sys/block/{parent_device}/device/{"rescan_controller" if base_device.startswith("nvme") else "rescan"}' if os.path.exists(rescan_path): try: - with open(rescan_path, "w") as f: - f.write("1") + with open(rescan_path, 'w') as f: + f.write('1') return True except OSError as e: - module.warn(f"Failed to rescan device {device}: {e!s}") + module.warn(f'Failed to rescan device {device}: {e!s}') else: - module.warn(f"Rescan path does not exist for device {device}") + module.warn(f'Rescan path does not exist for device {device}') return False def main(): module = AnsibleModule( argument_spec=dict( - device=dict(type="path", required=True), - state=dict(type="str", default="present", choices=["present", "absent"]), - force=dict(type="bool", default=False), - resize=dict(type="bool", default=False), + device=dict(type='path', required=True), + state=dict(type='str', default='present', choices=['present', 'absent']), + force=dict(type='bool', default=False), + resize=dict(type='bool', default=False), ), supports_check_mode=True, ) - device = module.params["device"] - state = module.params["state"] - force = module.params["force"] - resize = module.params["resize"] + device = module.params['device'] + state = module.params['state'] + force = module.params['force'] + resize = module.params['resize'] changed = False actions = [] # Validate device existence for present state - if state == "present" and not os.path.exists(device): - module.fail_json(msg=f"Device {device} not found") + if state == 'present' and not os.path.exists(device): + module.fail_json(msg=f'Device {device} not found') is_pv = get_pv_status(module, device) - if state == "present": + if state == 'present': # Create PV if needed if not is_pv: if module.check_mode: changed = True - actions.append("would be created") + actions.append('would be created') else: - cmd = ["pvcreate"] + cmd = ['pvcreate'] if force: - cmd.append("-f") + cmd.append('-f') cmd.append(device) _rc, _out, _err = module.run_command(cmd, check_rc=True) changed = True - actions.append("created") + actions.append('created') is_pv = True # Handle resizing @@ -161,39 +161,41 @@ def main(): if module.check_mode: # In check mode, assume resize would change changed = True - actions.append("would be resized") + actions.append('would be resized') else: # Perform device rescan if each time if rescan_device(module, device): - actions.append("rescanned") + actions.append('rescanned') original_size = get_pv_size(module, device) - _rc, _out, _err = module.run_command(["pvresize", device], check_rc=True) + _rc, _out, _err = module.run_command( + ['pvresize', device], check_rc=True + ) new_size = get_pv_size(module, device) if new_size != original_size: changed = True - actions.append("resized") + actions.append('resized') - elif state == "absent": + elif state == 'absent': if is_pv: if module.check_mode: changed = True - actions.append("would be removed") + actions.append('would be removed') else: - cmd = ["pvremove", "-y"] + cmd = ['pvremove', '-y'] if force: - cmd.append("-ff") + cmd.append('-ff') changed = True cmd.append(device) _rc, _out, _err = module.run_command(cmd, check_rc=True) - actions.append("removed") + actions.append('removed') # Generate final message if actions: - msg = f"PV {device}: {', '.join(actions)}" + msg = f'PV {device}: {", ".join(actions)}' else: - msg = f"No changes needed for PV {device}" + msg = f'No changes needed for PV {device}' module.exit_json(changed=changed, msg=msg) -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/plugins/modules/nextcloud_occ_app.py b/plugins/modules/nextcloud_occ_app.py index d379b145..af3a5682 100644 --- a/plugins/modules/nextcloud_occ_app.py +++ b/plugins/modules/nextcloud_occ_app.py @@ -10,7 +10,7 @@ __metaclass__ = type -DOCUMENTATION = r''' +DOCUMENTATION = r""" module: nextcloud_occ_app short_description: Install, enable, disable or remove a Nextcloud app via occ @@ -63,9 +63,9 @@ description: - Pre-fetched output of C(occ app:list --output=json), as either a JSON string or an already-parsed dict. When set, the module skips the C(app:list) call and reads the current state from this value, which avoids running C(occ) once per app when looping over a list. type: raw -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" - name: 'Enable a Nextcloud app' linuxfabrik.lfops.nextcloud_occ_app: name: 'notify_push' @@ -86,9 +86,9 @@ name: 'notify_push' state: 'present' force: true -''' +""" -RETURN = r''' +RETURN = r""" changed: description: Whether the app state had to be changed. returned: always @@ -109,7 +109,7 @@ description: Standard output of the last C(occ) command that was executed. returned: when changed and not in check mode type: str -''' +""" import json import traceback @@ -125,7 +125,9 @@ def get_current_state(module, php_path, occ_path, name, installed_apps_json): try: app_list = json.loads(installed_apps_json) except (json.JSONDecodeError, ValueError): - module.fail_json(msg=f'Failed to parse installed_apps_json: {installed_apps_json}') + module.fail_json( + msg=f'Failed to parse installed_apps_json: {installed_apps_json}' + ) else: app_list = installed_apps_json else: @@ -148,7 +150,9 @@ def get_current_state(module, php_path, occ_path, name, installed_apps_json): try: app_list = json.loads(stdout) except (json.JSONDecodeError, ValueError): - module.fail_json(msg=f'Failed to parse JSON from occ app:list output: {stdout}') + module.fail_json( + msg=f'Failed to parse JSON from occ app:list output: {stdout}' + ) enabled_apps = app_list.get('enabled', {}) disabled_apps = app_list.get('disabled', {}) @@ -163,7 +167,11 @@ def get_current_state(module, php_path, occ_path, name, installed_apps_json): def main(): module_args = dict( name=dict(type='str', required=True), - state=dict(type='str', choices=['absent', 'disabled', 'enabled', 'present'], default='enabled'), + state=dict( + type='str', + choices=['absent', 'disabled', 'enabled', 'present'], + default='enabled', + ), force=dict(type='bool', default=False), occ_path=dict(type='str', default='/var/www/html/nextcloud/occ'), php_path=dict(type='str', default='php'), @@ -183,7 +191,9 @@ def main(): installed_apps_json = module.params['installed_apps_json'] - current_state = get_current_state(module, php_path, occ_path, name, installed_apps_json) + current_state = get_current_state( + module, php_path, occ_path, name, installed_apps_json + ) result = { 'changed': False, @@ -203,7 +213,13 @@ def main(): cmd.append(name) commands.append(cmd) elif current_state == 'absent': - install_cmd = [php_path, occ_path, '--no-interaction', 'app:install', '--keep-disabled'] + install_cmd = [ + php_path, + occ_path, + '--no-interaction', + 'app:install', + '--keep-disabled', + ] if force: install_cmd.append('--force') install_cmd.append(name) @@ -216,13 +232,21 @@ def main(): elif state == 'disabled': if current_state == 'enabled': - commands.append([php_path, occ_path, '--no-interaction', 'app:disable', name]) + commands.append( + [php_path, occ_path, '--no-interaction', 'app:disable', name] + ) else: module.exit_json(**result) elif state == 'present': if current_state == 'absent': - install_cmd = [php_path, occ_path, '--no-interaction', 'app:install', '--keep-disabled'] + install_cmd = [ + php_path, + occ_path, + '--no-interaction', + 'app:install', + '--keep-disabled', + ] if force: install_cmd.append('--force') install_cmd.append(name) @@ -234,7 +258,9 @@ def main(): if current_state == 'absent': module.exit_json(**result) else: - commands.append([php_path, occ_path, '--no-interaction', 'app:remove', name]) + commands.append( + [php_path, occ_path, '--no-interaction', 'app:remove', name] + ) # if we get here, there are commands to run result['changed'] = True diff --git a/plugins/modules/nextcloud_occ_app_config.py b/plugins/modules/nextcloud_occ_app_config.py index 8cd9ab63..d47a21e5 100644 --- a/plugins/modules/nextcloud_occ_app_config.py +++ b/plugins/modules/nextcloud_occ_app_config.py @@ -10,7 +10,7 @@ __metaclass__ = type -DOCUMENTATION = r''' +DOCUMENTATION = r""" module: nextcloud_occ_app_config short_description: Manage a Nextcloud app configuration value via occ @@ -71,9 +71,9 @@ description: - Pre-fetched output of C(occ config:list --output=json --private), as either a JSON string or an already-parsed dict. When set, the module skips the C(config:app:get) call and reads the current value from this value, which avoids running C(occ) once per key when looping over many keys. type: raw -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" - name: 'Set an app configuration value' linuxfabrik.lfops.nextcloud_occ_app_config: app: 'core' @@ -82,9 +82,9 @@ type: 'integer' occ_path: '/data/nextcloud/occ' php_path: '/usr/bin/php' -''' +""" -RETURN = r''' +RETURN = r""" changed: description: Whether the value or type had to be changed. returned: always @@ -109,7 +109,7 @@ description: Standard output of the C(occ config:app:set) or C(config:app:delete) command. returned: when changed and not in check mode type: str -''' +""" import json import traceback @@ -137,14 +137,18 @@ def values_match(current_value, value, value_type): def main(): # define available arguments/parameters a user can pass to this module module_args = dict( - app=dict(type='str', required=True), - name=dict(type='str', required=True), - value=dict(type='str'), - type=dict(type='str', choices=['string', 'integer', 'float', 'boolean', 'array'], default='string'), - state=dict(type='str', choices=['absent', 'present'], default='present'), - occ_path=dict(type='str', default='/var/www/html/nextcloud/occ'), - php_path=dict(type='str', default='php'), - installed_config_json=dict(type='raw'), + app=dict(type='str', required=True), + name=dict(type='str', required=True), + value=dict(type='str'), + type=dict( + type='str', + choices=['string', 'integer', 'float', 'boolean', 'array'], + default='string', + ), + state=dict(type='str', choices=['absent', 'present'], default='present'), + occ_path=dict(type='str', default='/var/www/html/nextcloud/occ'), + php_path=dict(type='str', default='php'), + installed_config_json=dict(type='raw'), ) module = AnsibleModule( @@ -235,7 +239,9 @@ def main(): try: current = json.loads(get_stdout) if get_rc == 0 else {} except (json.JSONDecodeError, ValueError): - module.fail_json(msg=f'Failed to parse JSON from occ config:app:get output: {get_stdout}') + module.fail_json( + msg=f'Failed to parse JSON from occ config:app:get output: {get_stdout}' + ) key_exists = get_rc == 0 current_type = current.get('type', '') @@ -246,7 +252,9 @@ def main(): if state == 'present': # check if the current value and type match the desired settings - if current_type == value_type and values_match(current_value, value, value_type): + if current_type == value_type and values_match( + current_value, value, value_type + ): module.exit_json(**result) # else, the value will be changed @@ -286,7 +294,6 @@ def main(): result['stderr'] = set_stderr module.exit_json(**result) - elif state == 'absent': if not key_exists: # config does not exist, so there is no change @@ -318,7 +325,9 @@ def main(): ] try: - delete_rc, delete_stdout, delete_stderr = module.run_command(delete_cmd, check_rc=True) + delete_rc, delete_stdout, delete_stderr = module.run_command( + delete_cmd, check_rc=True + ) except Exception as e: module.fail_json(msg=to_native(e), exception=traceback.format_exc()) diff --git a/plugins/modules/nextcloud_occ_system_config.py b/plugins/modules/nextcloud_occ_system_config.py index afd88f00..f84e84a3 100644 --- a/plugins/modules/nextcloud_occ_system_config.py +++ b/plugins/modules/nextcloud_occ_system_config.py @@ -10,7 +10,7 @@ __metaclass__ = type -DOCUMENTATION = r''' +DOCUMENTATION = r""" module: nextcloud_occ_system_config short_description: Manage a Nextcloud system configuration value via occ @@ -65,9 +65,9 @@ description: - Pre-fetched output of C(occ config:list --output=json --private), as either a JSON string or an already-parsed dict. When set, the module skips the C(config:system:get) call and walks I(name) through the dict tree (descending into both dicts and lists by index), which avoids running C(occ) once per key when looping over many keys. type: raw -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" - name: 'Set a system configuration value' linuxfabrik.lfops.nextcloud_occ_system_config: name: 'check_for_working_wellknown_setup' @@ -78,9 +78,9 @@ linuxfabrik.lfops.nextcloud_occ_system_config: name: 'forbidden_filename_characters 0' value: '*' -''' +""" -RETURN = r''' +RETURN = r""" changed: description: Whether the value had to be changed. returned: always @@ -101,7 +101,7 @@ description: Standard output of the C(occ config:system:set) or C(config:system:delete) command. returned: when changed and not in check mode type: str -''' +""" import json import traceback @@ -113,13 +113,17 @@ def main(): # define available arguments/parameters a user can pass to this module module_args = dict( - name=dict(type='str', required=True), - value=dict(type='str'), - type=dict(type='str', choices=['string', 'integer', 'double', 'boolean'], default='string'), - state=dict(type='str', choices=['absent', 'present'], default='present'), - occ_path=dict(type='str', default='/var/www/html/nextcloud/occ'), - php_path=dict(type='str', default='php'), - installed_config_json=dict(type='raw'), + name=dict(type='str', required=True), + value=dict(type='str'), + type=dict( + type='str', + choices=['string', 'integer', 'double', 'boolean'], + default='string', + ), + state=dict(type='str', choices=['absent', 'present'], default='present'), + occ_path=dict(type='str', default='/var/www/html/nextcloud/occ'), + php_path=dict(type='str', default='php'), + installed_config_json=dict(type='raw'), ) module = AnsibleModule( @@ -247,7 +251,6 @@ def main(): result['stderr'] = set_stderr module.exit_json(**result) - elif state == 'absent': if not key_exists: # config does not exist, so there is no change @@ -278,7 +281,9 @@ def main(): ] try: - delete_rc, delete_stdout, delete_stderr = module.run_command(delete_cmd, check_rc=True) + delete_rc, delete_stdout, delete_stderr = module.run_command( + delete_cmd, check_rc=True + ) except Exception as e: module.fail_json(msg=to_native(e), exception=traceback.format_exc()) diff --git a/plugins/modules/sqlite_query.py b/plugins/modules/sqlite_query.py index 05f56db4..a5a0c568 100644 --- a/plugins/modules/sqlite_query.py +++ b/plugins/modules/sqlite_query.py @@ -10,7 +10,7 @@ __metaclass__ = type -DOCUMENTATION = r''' +DOCUMENTATION = r""" --- module: sqlite_query short_description: Run a read-only SQLite query @@ -58,9 +58,9 @@ type: str choices: ['select'] default: 'select' -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" - name: 'Simple select query' linuxfabrik.lfops.sqlite_query: db: 'acme.db' @@ -76,9 +76,9 @@ profile_name: 'CIS CentOS 7' enabled: 1 delegate_to: 'localhost' -''' +""" -RETURN = r''' +RETURN = r""" changed: description: Always C(false). The module never modifies the database. returned: always @@ -106,7 +106,7 @@ returned: always type: int sample: 42 -''' +""" import os @@ -143,7 +143,7 @@ def connect(path='', filename=''): conn.row_factory = sqlite3.Row # https://stackoverflow.com/questions/3425320/sqlite3-programmingerror-you-must-not-use-8-bit-bytestrings-unless-you-use-a-te conn.text_factory = str - conn.create_function("REGEXP", 2, regexp) + conn.create_function('REGEXP', 2, regexp) except Exception as e: return (False, f'Connecting to DB {db} failed, Error: {e}, CWD: {os.getcwd()}') return (True, conn) @@ -170,7 +170,7 @@ def select(conn, sql, data=None, fetchone=False, as_dict=True): return (True, rows[0] if rows else []) return (True, [dict(row) for row in c.fetchall()]) if fetchone: - return (True, c.fetchone()) + return (True, c.fetchone()) return (True, c.fetchall()) except Exception as e: return (False, f'Query failed: {sql}, Error: {e}, Data: {data}') @@ -224,7 +224,9 @@ def main(): query_result = [] if query_type == 'select': - success, query_result = select(conn, query, named_args, fetchone=fetch_one, as_dict=as_dict) + success, query_result = select( + conn, query, named_args, fetchone=fetch_one, as_dict=as_dict + ) changed = False if not success: close(conn) diff --git a/plugins/modules/uptimerobot_account_info.py b/plugins/modules/uptimerobot_account_info.py index 9a58aebb..84374893 100644 --- a/plugins/modules/uptimerobot_account_info.py +++ b/plugins/modules/uptimerobot_account_info.py @@ -10,7 +10,7 @@ __metaclass__ = type -DOCUMENTATION = r''' +DOCUMENTATION = r""" --- module: uptimerobot_account_info short_description: Read UptimeRobot account details @@ -30,10 +30,10 @@ description: Path to a file whose first line is the UptimeRobot API key. Tilde-expanded. type: str default: '~/.uptimerobot' -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" # 1) Read account quota and current usage. The API key comes from # ~/.uptimerobot when no parameter is given. - name: 'Capture UptimeRobot account info' @@ -58,10 +58,10 @@ - ansible.builtin.assert: that: 'ur_account.account.up_monitors / ur_account.account.monitor_limit < 0.9' fail_msg: 'UptimeRobot quota is nearly exhausted; bump the plan or delete stale monitors.' -''' +""" -RETURN = r''' +RETURN = r""" account: description: Account details as returned by C(getAccountDetails). type: dict @@ -80,7 +80,7 @@ sample: operation: 'read' fields: ['down_monitors', 'email', 'monitor_interval', 'monitor_limit', 'paused_monitors', 'up_monitors'] -''' +""" from ansible.module_utils.basic import AnsibleModule @@ -98,20 +98,26 @@ def main(): supports_check_mode=True, ) - api_key = ur.resolve_api_key(module, module.params.get('api_key'), module.params.get('api_key_file')) + api_key = ur.resolve_api_key( + module, module.params.get('api_key'), module.params.get('api_key_file') + ) module.log('uptimerobot_account_info: fetching account details') success, account = ur.get_account_details(module, api_key) if not success: module.fail_json(msg=f'Could not fetch UptimeRobot account details: {account}') module.log( - f"uptimerobot_account_info: monitor_limit={account.get('monitor_limit')} " - f"up={account.get('up_monitors')} down={account.get('down_monitors')} " - f"paused={account.get('paused_monitors')}" + f'uptimerobot_account_info: monitor_limit={account.get("monitor_limit")} ' + f'up={account.get("up_monitors")} down={account.get("down_monitors")} ' + f'paused={account.get("paused_monitors")}' + ) + module.exit_json( + changed=False, + account=account, + debug={ + 'operation': 'read', + 'fields': sorted(account.keys()) if isinstance(account, dict) else None, + }, ) - module.exit_json(changed=False, account=account, debug={ - 'operation': 'read', - 'fields': sorted(account.keys()) if isinstance(account, dict) else None, - }) if __name__ == '__main__': diff --git a/plugins/modules/uptimerobot_alert_contact.py b/plugins/modules/uptimerobot_alert_contact.py index c8b69b03..79a2f6b1 100644 --- a/plugins/modules/uptimerobot_alert_contact.py +++ b/plugins/modules/uptimerobot_alert_contact.py @@ -10,7 +10,7 @@ __metaclass__ = type -DOCUMENTATION = r''' +DOCUMENTATION = r""" --- module: uptimerobot_alert_contact short_description: Delete an UptimeRobot alert contact @@ -42,10 +42,10 @@ type: str choices: ['absent', 'present'] default: 'absent' -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" # UptimeRobot's v2 API does not allow CREATING or EDITING alert contacts via # the API — they must be added in the web UI (which sends an opt-in mail). # This module is therefore delete-only, useful for sweeping contacts that no @@ -73,10 +73,10 @@ loop: '{{ ur_contacts.alert_contacts | selectattr("status", "equalto", "not activated") | list }}' loop_control: label: '{{ item.friendly_name }}' -''' +""" -RETURN = r''' +RETURN = r""" alert_contact: description: The alert contact that was deleted (or that would have been deleted, in check mode), as returned by C(getAlertContacts). Empty dict when there was nothing to delete. type: dict @@ -89,7 +89,7 @@ operation: 'delete' contact_id: 7068316 friendly_name: 'monitoring@example.com' -''' +""" from ansible.module_utils.basic import AnsibleModule @@ -112,17 +112,23 @@ def main(): ) if module.params['state'] == 'present': - module.fail_json(msg=( - "uptimerobot_alert_contact only supports state='absent'. " - "UptimeRobot API v2 does not expose creating or editing alert " - "contacts; use the web UI for that." - )) - - api_key = ur.resolve_api_key(module, module.params.get('api_key'), module.params.get('api_key_file')) + module.fail_json( + msg=( + "uptimerobot_alert_contact only supports state='absent'. " + 'UptimeRobot API v2 does not expose creating or editing alert ' + 'contacts; use the web UI for that.' + ) + ) + + api_key = ur.resolve_api_key( + module, module.params.get('api_key'), module.params.get('api_key_file') + ) contact_id = module.params.get('id') friendly_name = module.params.get('friendly_name') - module.log(f'uptimerobot_alert_contact: looking up id={contact_id} friendly_name={friendly_name!r}') + module.log( + f'uptimerobot_alert_contact: looking up id={contact_id} friendly_name={friendly_name!r}' + ) target = None if contact_id is None: @@ -131,11 +137,15 @@ def main(): module.fail_json(msg=f'Could not list alert contacts: {contacts}') target = ur.find_by_friendly_name(contacts, friendly_name) if target is None: - module.exit_json(changed=False, alert_contact={}, debug={ - 'operation': 'noop', - 'reason': 'alert contact not present', - 'friendly_name': friendly_name, - }) + module.exit_json( + changed=False, + alert_contact={}, + debug={ + 'operation': 'noop', + 'reason': 'alert contact not present', + 'friendly_name': friendly_name, + }, + ) contact_id = int(target['id']) else: # We can still try to look up the friendly_name for the report, but it @@ -149,31 +159,43 @@ def main(): if target is None: # Either the listing failed or the contact does not exist; treat # both as "nothing to do". - module.exit_json(changed=False, alert_contact={}, debug={ - 'operation': 'noop', - 'reason': 'alert contact not present (or could not list)', - 'contact_id': contact_id, - }) + module.exit_json( + changed=False, + alert_contact={}, + debug={ + 'operation': 'noop', + 'reason': 'alert contact not present (or could not list)', + 'contact_id': contact_id, + }, + ) if module.check_mode: - module.exit_json(changed=True, alert_contact=target, debug={ - 'operation': 'delete (check_mode)', - 'contact_id': contact_id, - 'friendly_name': target.get('friendly_name'), - }) + module.exit_json( + changed=True, + alert_contact=target, + debug={ + 'operation': 'delete (check_mode)', + 'contact_id': contact_id, + 'friendly_name': target.get('friendly_name'), + }, + ) module.log( - f"uptimerobot_alert_contact: deleting id={contact_id} " - f"friendly_name={target.get('friendly_name')!r}" + f'uptimerobot_alert_contact: deleting id={contact_id} ' + f'friendly_name={target.get("friendly_name")!r}' ) success, result = ur.delete_alert_contact(module, api_key, contact_id) if not success: module.fail_json(msg=f'Could not delete alert contact {target!r}: {result}') - module.exit_json(changed=True, alert_contact=target, debug={ - 'operation': 'delete', - 'contact_id': contact_id, - 'friendly_name': target.get('friendly_name'), - }) + module.exit_json( + changed=True, + alert_contact=target, + debug={ + 'operation': 'delete', + 'contact_id': contact_id, + 'friendly_name': target.get('friendly_name'), + }, + ) if __name__ == '__main__': diff --git a/plugins/modules/uptimerobot_alert_contact_info.py b/plugins/modules/uptimerobot_alert_contact_info.py index 76f8dc50..d6a91ecb 100644 --- a/plugins/modules/uptimerobot_alert_contact_info.py +++ b/plugins/modules/uptimerobot_alert_contact_info.py @@ -10,7 +10,7 @@ __metaclass__ = type -DOCUMENTATION = r''' +DOCUMENTATION = r""" --- module: uptimerobot_alert_contact_info short_description: List UptimeRobot alert contacts @@ -34,10 +34,10 @@ description: - Filter the returned list to the contact whose C(friendly_name) is an exact match for this value. The result is still a list (length 0 or 1) for shape stability. type: str -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" # 1) List every alert contact on the account. - name: 'Capture all alert contacts' linuxfabrik.lfops.uptimerobot_alert_contact_info: @@ -67,10 +67,10 @@ | map(attribute="friendly_name") | list }} -''' +""" -RETURN = r''' +RETURN = r""" alert_contacts: description: List of alert contact dicts. Empty list when nothing matched. type: list @@ -83,7 +83,7 @@ sample: operation: 'list' count: 3 -''' +""" from ansible.module_utils.basic import AnsibleModule @@ -99,7 +99,9 @@ def main(): module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - api_key = ur.resolve_api_key(module, module.params.get('api_key'), module.params.get('api_key_file')) + api_key = ur.resolve_api_key( + module, module.params.get('api_key'), module.params.get('api_key_file') + ) friendly_name = module.params.get('friendly_name') module.log('uptimerobot_alert_contact_info: fetching alert contacts') @@ -111,10 +113,14 @@ def main(): match = ur.find_by_friendly_name(contacts, friendly_name) contacts = [match] if match else [] - module.exit_json(changed=False, alert_contacts=contacts, debug={ - 'operation': 'list', - 'count': len(contacts), - }) + module.exit_json( + changed=False, + alert_contacts=contacts, + debug={ + 'operation': 'list', + 'count': len(contacts), + }, + ) if __name__ == '__main__': diff --git a/plugins/modules/uptimerobot_monitor.py b/plugins/modules/uptimerobot_monitor.py index 60bb2e70..49c711ea 100644 --- a/plugins/modules/uptimerobot_monitor.py +++ b/plugins/modules/uptimerobot_monitor.py @@ -10,7 +10,7 @@ __metaclass__ = type -DOCUMENTATION = r''' +DOCUMENTATION = r""" --- module: uptimerobot_monitor short_description: Create, update or delete an UptimeRobot monitor @@ -202,10 +202,10 @@ id: description: Numeric ID of an existing maintenance window. Takes precedence over I(friendly_name) when both are set. type: int -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" # 1) Create-or-update a simple HTTPS monitor. Idempotent: re-running with the # same values reports `changed=false`. - name: 'Manage a simple HTTPS monitor' @@ -281,10 +281,10 @@ # * Tail per-call syslog: `journalctl --identifier ansible-uptimerobot_monitor --follow` # * Inspect the structured operation summary: `register: r` + `debug var=r.debug`. # * Use `--check --diff` to preview create/update/delete without API writes. -''' +""" -RETURN = r''' +RETURN = r""" monitor: description: - On create or update, the monitor object as returned by UptimeRobot's C(newMonitor) / C(editMonitor). On delete, the last known state of the monitor as returned by C(getMonitors). @@ -308,7 +308,7 @@ friendly_name: '001 www.example.com/index.php/login' monitor_id: 794294 diff_fields: ['interval'] -''' +""" from ansible.module_utils.basic import AnsibleModule @@ -318,15 +318,27 @@ # the API's current state to decide whether an edit call is needed. _MONITOR_DIFFABLE_FIELDS = [ 'url', - 'sub_type', 'port', - 'keyword_type', 'keyword_case_type', 'keyword_value', - 'interval', 'timeout', - 'http_username', 'http_password', 'http_auth_type', - 'post_type', 'post_value', 'http_method', 'post_content_type', - 'custom_http_headers', 'custom_http_statuses', - 'ignore_ssl_errors', 'disable_domain_expire_notifications', + 'sub_type', + 'port', + 'keyword_type', + 'keyword_case_type', + 'keyword_value', + 'interval', + 'timeout', + 'http_username', + 'http_password', + 'http_auth_type', + 'post_type', + 'post_value', + 'http_method', + 'post_content_type', + 'custom_http_headers', + 'custom_http_statuses', + 'ignore_ssl_errors', + 'disable_domain_expire_notifications', 'status', - 'alert_contacts', 'mwindows', + 'alert_contacts', + 'mwindows', ] @@ -352,11 +364,13 @@ def _build_alert_contacts(module, api_key, items): if name not in by_name: module.fail_json(msg=f'Alert contact {name!r} not found on UptimeRobot') contact_id = int(by_name[name]['id']) - resolved.append({ - 'id': contact_id, - 'threshold': int(item.get('threshold', 0)), - 'recurrence': int(item.get('recurrence', 0)), - }) + resolved.append( + { + 'id': contact_id, + 'threshold': int(item.get('threshold', 0)), + 'recurrence': int(item.get('recurrence', 0)), + } + ) return ur.alert_contacts_wire(resolved) @@ -377,7 +391,9 @@ def _build_mwindows(module, api_key, items): if wid is None: name = item['friendly_name'] if name not in by_name: - module.fail_json(msg=f'Maintenance window {name!r} not found on UptimeRobot') + module.fail_json( + msg=f'Maintenance window {name!r} not found on UptimeRobot' + ) wid = int(by_name[name]['id']) ids.append(int(wid)) return ur.mwindows_wire(ids) @@ -392,11 +408,13 @@ def _normalize_current_alert_contacts(current_field): return '' items = [] for ac in current_field: - items.append({ - 'id': int(ac['id']), - 'threshold': int(ac.get('threshold', 0)), - 'recurrence': int(ac.get('recurrence', 0)), - }) + items.append( + { + 'id': int(ac['id']), + 'threshold': int(ac.get('threshold', 0)), + 'recurrence': int(ac.get('recurrence', 0)), + } + ) items.sort(key=lambda x: x['id']) return ur.alert_contacts_wire(items) @@ -415,7 +433,9 @@ def _normalize_desired_alert_contacts(wire): parts = [] for token in wire.split('-'): bits = token.split('_') - parts.append({'id': int(bits[0]), 'threshold': int(bits[1]), 'recurrence': int(bits[2])}) + parts.append( + {'id': int(bits[0]), 'threshold': int(bits[1]), 'recurrence': int(bits[2])} + ) parts.sort(key=lambda x: x['id']) return ur.alert_contacts_wire(parts) @@ -433,10 +453,12 @@ def main(): api_key_file=dict(type='str', required=False, default='~/.uptimerobot'), friendly_name=dict(type='str', required=True), state=dict(type='str', choices=['absent', 'present'], default='present'), - url=dict(type='str'), type=dict(type='str', choices=['beat', 'http', 'keyw', 'ping', 'port']), - sub_type=dict(type='str', choices=['custom', 'ftp', 'http', 'https', 'imap', 'pop3', 'smtp']), + sub_type=dict( + type='str', + choices=['custom', 'ftp', 'http', 'https', 'imap', 'pop3', 'smtp'], + ), port=dict(type='int'), keyword_type=dict(type='str', choices=['exist', 'notex']), keyword_case_type=dict(type='str', choices=['ci', 'cs']), @@ -447,14 +469,19 @@ def main(): http_username=dict(type='str'), http_password=dict(type='str', no_log=True), http_auth_type=dict(type='str', choices=['basic', 'digest']), - http_method=dict(type='str', choices=['delete', 'get', 'head', 'options', 'patch', 'post', 'put']), + http_method=dict( + type='str', + choices=['delete', 'get', 'head', 'options', 'patch', 'post', 'put'], + ), post_type=dict(type='str', choices=['key-value', 'raw data']), post_value=dict(type='str'), post_content_type=dict(type='str', choices=['content/json', 'text/html']), custom_http_headers=dict(type='raw'), custom_http_statuses=dict(type='str'), ignore_ssl_errors=dict(type='bool'), - disable_domain_expire_notifications=dict(type='str', choices=['disable', 'enable']), + disable_domain_expire_notifications=dict( + type='str', choices=['disable', 'enable'] + ), alert_contacts=dict(type='list', elements='dict'), mwindows=dict(type='list', elements='dict'), ) @@ -464,7 +491,9 @@ def main(): supports_check_mode=True, ) - api_key = ur.resolve_api_key(module, module.params.get('api_key'), module.params.get('api_key_file')) + api_key = ur.resolve_api_key( + module, module.params.get('api_key'), module.params.get('api_key_file') + ) friendly_name = module.params['friendly_name'] state = module.params['state'] @@ -476,60 +505,92 @@ def main(): if not success: module.fail_json(msg=f'Could not list monitors: {monitors}') current = ur.find_by_friendly_name(monitors, friendly_name) - module.log(f'uptimerobot_monitor: existing={bool(current)} (out of {len(monitors)} monitors on the account)') + module.log( + f'uptimerobot_monitor: existing={bool(current)} (out of {len(monitors)} monitors on the account)' + ) # Step 2: build the desired payload (only fields the user actually set). desired = {} for field in [ - 'url', 'sub_type', 'port', - 'keyword_type', 'keyword_case_type', 'keyword_value', - 'interval', 'timeout', - 'http_username', 'http_password', 'http_auth_type', - 'post_type', 'post_value', 'http_method', 'post_content_type', - 'custom_http_headers', 'custom_http_statuses', - 'ignore_ssl_errors', 'disable_domain_expire_notifications', + 'url', + 'sub_type', + 'port', + 'keyword_type', + 'keyword_case_type', + 'keyword_value', + 'interval', + 'timeout', + 'http_username', + 'http_password', + 'http_auth_type', + 'post_type', + 'post_value', + 'http_method', + 'post_content_type', + 'custom_http_headers', + 'custom_http_statuses', + 'ignore_ssl_errors', + 'disable_domain_expire_notifications', 'status', ]: value = module.params.get(field) if value is not None and value != '': desired[field] = value if module.params.get('alert_contacts'): - desired['alert_contacts'] = _build_alert_contacts(module, api_key, module.params['alert_contacts']) + desired['alert_contacts'] = _build_alert_contacts( + module, api_key, module.params['alert_contacts'] + ) if module.params.get('mwindows'): - desired['mwindows'] = _build_mwindows(module, api_key, module.params['mwindows']) + desired['mwindows'] = _build_mwindows( + module, api_key, module.params['mwindows'] + ) # --- absent ------------------------------------------------------------- if state == 'absent': if current is None: - module.exit_json(changed=False, monitor={}, debug={ - 'operation': 'noop', - 'reason': 'monitor not present', - 'friendly_name': friendly_name, - }) + module.exit_json( + changed=False, + monitor={}, + debug={ + 'operation': 'noop', + 'reason': 'monitor not present', + 'friendly_name': friendly_name, + }, + ) delete_before = { 'friendly_name': current.get('friendly_name'), 'id': current.get('id'), 'url': current.get('url'), } if module.check_mode: - module.exit_json(changed=True, monitor=current, + module.exit_json( + changed=True, + monitor=current, diff={'before': delete_before, 'after': {}}, debug={ 'operation': 'delete (check_mode)', 'friendly_name': friendly_name, 'monitor_id': current['id'], - }) - module.log(f"uptimerobot_monitor: deleting id={current['id']} friendly_name={friendly_name!r}") + }, + ) + module.log( + f'uptimerobot_monitor: deleting id={current["id"]} friendly_name={friendly_name!r}' + ) success, result = ur.delete_monitor(module, api_key, current['id']) if not success: - module.fail_json(msg=f'Could not delete monitor {friendly_name!r}: {result}') - module.exit_json(changed=True, monitor=current, + module.fail_json( + msg=f'Could not delete monitor {friendly_name!r}: {result}' + ) + module.exit_json( + changed=True, + monitor=current, diff={'before': delete_before, 'after': {}}, debug={ 'operation': 'delete', 'friendly_name': friendly_name, 'monitor_id': current['id'], - }) + }, + ) # --- present, create ---------------------------------------------------- if current is None: @@ -545,24 +606,34 @@ def main(): body.pop('status', None) create_diff = {'before': {}, 'after': dict(body)} if module.check_mode: - module.exit_json(changed=True, monitor=body, + module.exit_json( + changed=True, + monitor=body, diff=create_diff, debug={ 'operation': 'create (check_mode)', 'friendly_name': friendly_name, 'sent_keys': sorted(body.keys()), - }) - module.log(f'uptimerobot_monitor: creating friendly_name={friendly_name!r} sent_keys={sorted(body.keys())}') + }, + ) + module.log( + f'uptimerobot_monitor: creating friendly_name={friendly_name!r} sent_keys={sorted(body.keys())}' + ) success, result = ur.new_monitor(module, api_key, body) if not success: - module.fail_json(msg=f'Could not create monitor {friendly_name!r}: {result}') - module.exit_json(changed=True, monitor=result, + module.fail_json( + msg=f'Could not create monitor {friendly_name!r}: {result}' + ) + module.exit_json( + changed=True, + monitor=result, diff=create_diff, debug={ 'operation': 'create', 'friendly_name': friendly_name, 'sent_keys': sorted(body.keys()), - }) + }, + ) # --- present, update ---------------------------------------------------- # Build the comparable representation of the current state. `get_monitors` @@ -587,17 +658,25 @@ def main(): 'custom_http_headers': current.get('custom_http_headers'), 'custom_http_statuses': current.get('custom_http_statuses'), 'ignore_ssl_errors': current.get('ignore_ssl_errors'), - 'disable_domain_expire_notifications': current.get('disable_domain_expire_notifications'), + 'disable_domain_expire_notifications': current.get( + 'disable_domain_expire_notifications' + ), 'status': current.get('status'), - 'alert_contacts': _normalize_current_alert_contacts(current.get('alert_contacts')), + 'alert_contacts': _normalize_current_alert_contacts( + current.get('alert_contacts') + ), 'mwindows': _normalize_current_mwindows(current.get('mwindows')), } desired_compare = dict(desired) if 'alert_contacts' in desired_compare: - desired_compare['alert_contacts'] = _normalize_desired_alert_contacts(desired_compare['alert_contacts']) + desired_compare['alert_contacts'] = _normalize_desired_alert_contacts( + desired_compare['alert_contacts'] + ) if 'mwindows' in desired_compare: - desired_compare['mwindows'] = _normalize_desired_mwindows(desired_compare['mwindows']) + desired_compare['mwindows'] = _normalize_desired_mwindows( + desired_compare['mwindows'] + ) # `http_password` and `http_auth_type` can't be diffed reliably because # the API hides them in `getMonitors` responses (the `auth_type` field @@ -609,15 +688,21 @@ def main(): field_diff = ur.diff_for_update(current_compare, desired_compare, diff_fields) if not field_diff: - module.log(f"uptimerobot_monitor: id={current['id']} no diff -> changed=false") - module.exit_json(changed=False, monitor=current, debug={ - 'operation': 'noop', - 'reason': 'no diff', - 'friendly_name': friendly_name, - 'monitor_id': current['id'], - }) + module.log(f'uptimerobot_monitor: id={current["id"]} no diff -> changed=false') + module.exit_json( + changed=False, + monitor=current, + debug={ + 'operation': 'noop', + 'reason': 'no diff', + 'friendly_name': friendly_name, + 'monitor_id': current['id'], + }, + ) - module.log(f"uptimerobot_monitor: id={current['id']} diff_fields={sorted(field_diff.keys())}") + module.log( + f'uptimerobot_monitor: id={current["id"]} diff_fields={sorted(field_diff.keys())}' + ) update_diff = { 'before': {k: current_compare.get(k) for k in field_diff}, @@ -627,28 +712,34 @@ def main(): if module.check_mode: preview = dict(current) preview.update(field_diff) - module.exit_json(changed=True, monitor=preview, + module.exit_json( + changed=True, + monitor=preview, diff=update_diff, debug={ 'operation': 'update (check_mode)', 'friendly_name': friendly_name, 'monitor_id': current['id'], 'diff_fields': sorted(field_diff.keys()), - }) + }, + ) body = dict(desired) body['id'] = current['id'] success, result = ur.edit_monitor(module, api_key, body) if not success: module.fail_json(msg=f'Could not edit monitor {friendly_name!r}: {result}') - module.exit_json(changed=True, monitor=result, + module.exit_json( + changed=True, + monitor=result, diff=update_diff, debug={ 'operation': 'update', 'friendly_name': friendly_name, 'monitor_id': current['id'], 'diff_fields': sorted(field_diff.keys()), - }) + }, + ) if __name__ == '__main__': diff --git a/plugins/modules/uptimerobot_monitor_info.py b/plugins/modules/uptimerobot_monitor_info.py index d5bd49f0..93b343ea 100644 --- a/plugins/modules/uptimerobot_monitor_info.py +++ b/plugins/modules/uptimerobot_monitor_info.py @@ -10,7 +10,7 @@ __metaclass__ = type -DOCUMENTATION = r''' +DOCUMENTATION = r""" --- module: uptimerobot_monitor_info short_description: List UptimeRobot monitors @@ -38,10 +38,10 @@ description: - Server-side, case-insensitive substring filter forwarded to UptimeRobot's C(search) parameter. Useful to keep the response small when the account has thousands of monitors. Combine with I(friendly_name) to narrow further. type: str -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" # 1) Quick ad-hoc list of every monitor on the account. The API key is read # from ~/.uptimerobot when not passed. - name: 'Capture all monitors' @@ -94,10 +94,10 @@ | map(attribute="friendly_name") | list }} -''' +""" -RETURN = r''' +RETURN = r""" monitors: description: List of monitor dicts. Empty list when nothing matched. type: list @@ -110,7 +110,7 @@ sample: operation: 'list' count: 17 -''' +""" from ansible.module_utils.basic import AnsibleModule @@ -127,7 +127,9 @@ def main(): module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - api_key = ur.resolve_api_key(module, module.params.get('api_key'), module.params.get('api_key_file')) + api_key = ur.resolve_api_key( + module, module.params.get('api_key'), module.params.get('api_key_file') + ) search = module.params.get('search') or None friendly_name = module.params.get('friendly_name') @@ -140,10 +142,14 @@ def main(): match = ur.find_by_friendly_name(monitors, friendly_name) monitors = [match] if match else [] - module.exit_json(changed=False, monitors=monitors, debug={ - 'operation': 'list', - 'count': len(monitors), - }) + module.exit_json( + changed=False, + monitors=monitors, + debug={ + 'operation': 'list', + 'count': len(monitors), + }, + ) if __name__ == '__main__': diff --git a/plugins/modules/uptimerobot_mwindow.py b/plugins/modules/uptimerobot_mwindow.py index a7d91b22..e12a571d 100644 --- a/plugins/modules/uptimerobot_mwindow.py +++ b/plugins/modules/uptimerobot_mwindow.py @@ -10,7 +10,7 @@ __metaclass__ = type -DOCUMENTATION = r''' +DOCUMENTATION = r""" --- module: uptimerobot_mwindow short_description: Create, update or delete an UptimeRobot maintenance window @@ -66,10 +66,10 @@ description: C(active) un-pauses the window, C(paused) pauses it. Only honoured on edit; UptimeRobot rejects this field on create. type: str choices: ['active', 'paused'] -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" # 1) Create-or-update a weekly window. friendly_name is auto-synthesised as # "weekly mon 03:30-05:30" so re-runs are idempotent without naming it. - name: 'Weekly Monday-night maintenance window' @@ -106,10 +106,10 @@ - linuxfabrik.lfops.uptimerobot_mwindow: friendly_name: 'old-window' state: 'absent' -''' +""" -RETURN = r''' +RETURN = r""" mwindow: description: - On create or update, the maintenance window as returned by UptimeRobot's C(newMWindow) / C(editMWindow). On delete, the last known state of the window. @@ -126,7 +126,7 @@ friendly_name: 'weekly mon 03:30-05:30' mwindow_id: 12345 diff_fields: ['duration'] -''' +""" from ansible.module_utils.basic import AnsibleModule @@ -154,7 +154,7 @@ def _synthesise_name(params): parts = [params['type']] if params.get('value'): parts.append(str(params['value'])) - parts.append(f"{params['start_time']}-{params['end_time']}") + parts.append(f'{params["start_time"]}-{params["end_time"]}') return ' '.join(parts) @@ -178,61 +178,89 @@ def main(): mutually_exclusive=[['end_time', 'duration']], ) - api_key = ur.resolve_api_key(module, module.params.get('api_key'), module.params.get('api_key_file')) + api_key = ur.resolve_api_key( + module, module.params.get('api_key'), module.params.get('api_key_file') + ) state = module.params['state'] # Synthesise friendly_name on create when the user didn't pass one. friendly_name = module.params.get('friendly_name') if not friendly_name and state == 'present': - if not (module.params.get('type') and module.params.get('start_time') and module.params.get('end_time')): - module.fail_json(msg='Either pass `friendly_name` or pass `type`, `start_time`, `end_time` so it can be synthesised.') + if not ( + module.params.get('type') + and module.params.get('start_time') + and module.params.get('end_time') + ): + module.fail_json( + msg='Either pass `friendly_name` or pass `type`, `start_time`, `end_time` so it can be synthesised.' + ) friendly_name = _synthesise_name(module.params) module.log(f'uptimerobot_mwindow: looking up friendly_name={friendly_name!r}') success, mwindows = ur.get_mwindows(module, api_key) if not success: module.fail_json(msg=f'Could not list maintenance windows: {mwindows}') - current = ur.find_by_friendly_name(mwindows, friendly_name) if friendly_name else None - module.log(f'uptimerobot_mwindow: existing={bool(current)} (out of {len(mwindows)} mwindows on the account)') + current = ( + ur.find_by_friendly_name(mwindows, friendly_name) if friendly_name else None + ) + module.log( + f'uptimerobot_mwindow: existing={bool(current)} (out of {len(mwindows)} mwindows on the account)' + ) if state == 'absent': if current is None: - module.exit_json(changed=False, mwindow={}, debug={ - 'operation': 'noop', - 'reason': 'mwindow not present', - 'friendly_name': friendly_name, - }) + module.exit_json( + changed=False, + mwindow={}, + debug={ + 'operation': 'noop', + 'reason': 'mwindow not present', + 'friendly_name': friendly_name, + }, + ) delete_before = { 'friendly_name': current.get('friendly_name'), 'id': current.get('id'), 'type': current.get('type'), } if module.check_mode: - module.exit_json(changed=True, mwindow=current, + module.exit_json( + changed=True, + mwindow=current, diff={'before': delete_before, 'after': {}}, debug={ 'operation': 'delete (check_mode)', 'friendly_name': friendly_name, 'mwindow_id': current['id'], - }) - module.log(f"uptimerobot_mwindow: deleting id={current['id']}") + }, + ) + module.log(f'uptimerobot_mwindow: deleting id={current["id"]}') success, result = ur.delete_mwindow(module, api_key, current['id']) if not success: - module.fail_json(msg=f'Could not delete maintenance window {friendly_name!r}: {result}') - module.exit_json(changed=True, mwindow=current, + module.fail_json( + msg=f'Could not delete maintenance window {friendly_name!r}: {result}' + ) + module.exit_json( + changed=True, + mwindow=current, diff={'before': delete_before, 'after': {}}, debug={ 'operation': 'delete', 'friendly_name': friendly_name, 'mwindow_id': current['id'], - }) + }, + ) # Build desired payload. duration = module.params.get('duration') if duration is None and module.params.get('end_time'): if not module.params.get('start_time'): - module.fail_json(msg='`start_time` is required when `end_time` is given (so duration can be computed).') - duration = _compute_duration(module.params['start_time'], module.params['end_time']) + module.fail_json( + msg='`start_time` is required when `end_time` is given (so duration can be computed).' + ) + duration = _compute_duration( + module.params['start_time'], module.params['end_time'] + ) desired = { 'friendly_name': friendly_name, @@ -248,32 +276,46 @@ def main(): # Create. type/start_time/duration are required for new mwindows. for required in ('type', 'start_time'): if not desired.get(required): - module.fail_json(msg=f'`{required}` is required when creating a new maintenance window.') + module.fail_json( + msg=f'`{required}` is required when creating a new maintenance window.' + ) if not desired.get('duration'): - module.fail_json(msg='Either `end_time` or `duration` is required when creating a new maintenance window.') + module.fail_json( + msg='Either `end_time` or `duration` is required when creating a new maintenance window.' + ) # `status` not honoured on create. body = dict(desired) body.pop('status', None) create_diff = {'before': {}, 'after': dict(body)} if module.check_mode: - module.exit_json(changed=True, mwindow=body, + module.exit_json( + changed=True, + mwindow=body, diff=create_diff, debug={ 'operation': 'create (check_mode)', 'friendly_name': friendly_name, 'sent_keys': sorted(body.keys()), - }) - module.log(f'uptimerobot_mwindow: creating friendly_name={friendly_name!r} sent_keys={sorted(body.keys())}') + }, + ) + module.log( + f'uptimerobot_mwindow: creating friendly_name={friendly_name!r} sent_keys={sorted(body.keys())}' + ) success, result = ur.new_mwindow(module, api_key, body) if not success: - module.fail_json(msg=f'Could not create maintenance window {friendly_name!r}: {result}') - module.exit_json(changed=True, mwindow=result, + module.fail_json( + msg=f'Could not create maintenance window {friendly_name!r}: {result}' + ) + module.exit_json( + changed=True, + mwindow=result, diff=create_diff, debug={ 'operation': 'create', 'friendly_name': friendly_name, 'sent_keys': sorted(body.keys()), - }) + }, + ) # Update. `get_mwindows` already translated type/value/status to labels. # `friendly_name` already encodes type/value/start_time/end_time (it is @@ -286,15 +328,21 @@ def main(): current_compare = {field: current.get(field) for field in diff_fields} field_diff = ur.diff_for_update(current_compare, desired, diff_fields) if not field_diff: - module.log(f"uptimerobot_mwindow: id={current['id']} no diff -> changed=false") - module.exit_json(changed=False, mwindow=current, debug={ - 'operation': 'noop', - 'reason': 'no diff', - 'friendly_name': friendly_name, - 'mwindow_id': current['id'], - }) + module.log(f'uptimerobot_mwindow: id={current["id"]} no diff -> changed=false') + module.exit_json( + changed=False, + mwindow=current, + debug={ + 'operation': 'noop', + 'reason': 'no diff', + 'friendly_name': friendly_name, + 'mwindow_id': current['id'], + }, + ) - module.log(f"uptimerobot_mwindow: id={current['id']} diff_fields={sorted(field_diff.keys())}") + module.log( + f'uptimerobot_mwindow: id={current["id"]} diff_fields={sorted(field_diff.keys())}' + ) update_diff = { 'before': {k: current_compare.get(k) for k in field_diff}, @@ -304,28 +352,36 @@ def main(): if module.check_mode: preview = dict(current) preview.update(field_diff) - module.exit_json(changed=True, mwindow=preview, + module.exit_json( + changed=True, + mwindow=preview, diff=update_diff, debug={ 'operation': 'update (check_mode)', 'friendly_name': friendly_name, 'mwindow_id': current['id'], 'diff_fields': sorted(field_diff.keys()), - }) + }, + ) body = dict(desired) body['id'] = current['id'] success, result = ur.edit_mwindow(module, api_key, body) if not success: - module.fail_json(msg=f'Could not edit maintenance window {friendly_name!r}: {result}') - module.exit_json(changed=True, mwindow=result, + module.fail_json( + msg=f'Could not edit maintenance window {friendly_name!r}: {result}' + ) + module.exit_json( + changed=True, + mwindow=result, diff=update_diff, debug={ 'operation': 'update', 'friendly_name': friendly_name, 'mwindow_id': current['id'], 'diff_fields': sorted(field_diff.keys()), - }) + }, + ) if __name__ == '__main__': diff --git a/plugins/modules/uptimerobot_mwindow_info.py b/plugins/modules/uptimerobot_mwindow_info.py index aa2ce29d..15870b89 100644 --- a/plugins/modules/uptimerobot_mwindow_info.py +++ b/plugins/modules/uptimerobot_mwindow_info.py @@ -10,7 +10,7 @@ __metaclass__ = type -DOCUMENTATION = r''' +DOCUMENTATION = r""" --- module: uptimerobot_mwindow_info short_description: List UptimeRobot maintenance windows @@ -34,10 +34,10 @@ description: - Filter the returned list to the maintenance window whose C(friendly_name) is an exact match for this value. The result is still a list (length 0 or 1) for shape stability. type: str -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" # 1) List every maintenance window on the account. - name: 'Capture all maintenance windows' linuxfabrik.lfops.uptimerobot_mwindow_info: @@ -68,10 +68,10 @@ | map(attribute="friendly_name") | list }} -''' +""" -RETURN = r''' +RETURN = r""" mwindows: description: List of maintenance window dicts. Empty list when nothing matched. type: list @@ -84,7 +84,7 @@ sample: operation: 'list' count: 4 -''' +""" from ansible.module_utils.basic import AnsibleModule @@ -100,7 +100,9 @@ def main(): module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - api_key = ur.resolve_api_key(module, module.params.get('api_key'), module.params.get('api_key_file')) + api_key = ur.resolve_api_key( + module, module.params.get('api_key'), module.params.get('api_key_file') + ) friendly_name = module.params.get('friendly_name') module.log('uptimerobot_mwindow_info: fetching maintenance windows') @@ -112,10 +114,14 @@ def main(): match = ur.find_by_friendly_name(mwindows, friendly_name) mwindows = [match] if match else [] - module.exit_json(changed=False, mwindows=mwindows, debug={ - 'operation': 'list', - 'count': len(mwindows), - }) + module.exit_json( + changed=False, + mwindows=mwindows, + debug={ + 'operation': 'list', + 'count': len(mwindows), + }, + ) if __name__ == '__main__': diff --git a/plugins/modules/uptimerobot_psp.py b/plugins/modules/uptimerobot_psp.py index 7522ba6e..39ec3e27 100644 --- a/plugins/modules/uptimerobot_psp.py +++ b/plugins/modules/uptimerobot_psp.py @@ -10,7 +10,7 @@ __metaclass__ = type -DOCUMENTATION = r''' +DOCUMENTATION = r""" --- module: uptimerobot_psp short_description: Create, update or delete an UptimeRobot Public Status Page @@ -76,10 +76,10 @@ description: C(active) un-pauses the page, C(paused) pauses it. Only honoured on edit; UptimeRobot rejects this field on create. type: str choices: ['active', 'paused'] -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" # 1) Create-or-update a public status page. Monitors are referenced by their # friendly_name; the module resolves them to numeric IDs at runtime. - name: 'Public status page for example.com' @@ -112,10 +112,10 @@ - linuxfabrik.lfops.uptimerobot_psp: friendly_name: 'old-status-page' state: 'absent' -''' +""" -RETURN = r''' +RETURN = r""" psp: description: - On create or update, the PSP as returned by UptimeRobot's C(newPSP) / C(editPSP). On delete, the last known state of the PSP. @@ -132,7 +132,7 @@ friendly_name: 'Status - example.com' psp_id: 4321 diff_fields: ['monitors'] -''' +""" from ansible.module_utils.basic import AnsibleModule @@ -171,7 +171,9 @@ def main(): custom_domain=dict(type='str'), custom_url=dict(type='str'), password=dict(type='str', no_log=True), - sort=dict(type='str', choices=['a-z', 'down-up-paused', 'up-down-paused', 'z-a']), + sort=dict( + type='str', choices=['a-z', 'down-up-paused', 'up-down-paused', 'z-a'] + ), hide_url_links=dict(type='bool'), status=dict(type='str', choices=['active', 'paused']), ) @@ -182,7 +184,9 @@ def main(): mutually_exclusive=[['custom_domain', 'custom_url']], ) - api_key = ur.resolve_api_key(module, module.params.get('api_key'), module.params.get('api_key_file')) + api_key = ur.resolve_api_key( + module, module.params.get('api_key'), module.params.get('api_key_file') + ) friendly_name = module.params['friendly_name'] state = module.params['state'] @@ -191,42 +195,56 @@ def main(): if not success: module.fail_json(msg=f'Could not list PSPs: {psps}') current = ur.find_by_friendly_name(psps, friendly_name) - module.log(f'uptimerobot_psp: existing={bool(current)} (out of {len(psps)} PSPs on the account)') + module.log( + f'uptimerobot_psp: existing={bool(current)} (out of {len(psps)} PSPs on the account)' + ) if state == 'absent': if current is None: - module.exit_json(changed=False, psp={}, debug={ - 'operation': 'noop', - 'reason': 'PSP not present', - 'friendly_name': friendly_name, - }) + module.exit_json( + changed=False, + psp={}, + debug={ + 'operation': 'noop', + 'reason': 'PSP not present', + 'friendly_name': friendly_name, + }, + ) delete_before = { 'friendly_name': current.get('friendly_name'), 'id': current.get('id'), 'custom_domain': current.get('custom_domain'), } if module.check_mode: - module.exit_json(changed=True, psp=current, + module.exit_json( + changed=True, + psp=current, diff={'before': delete_before, 'after': {}}, debug={ 'operation': 'delete (check_mode)', 'friendly_name': friendly_name, 'psp_id': current['id'], - }) - module.log(f"uptimerobot_psp: deleting id={current['id']}") + }, + ) + module.log(f'uptimerobot_psp: deleting id={current["id"]}') success, result = ur.delete_psp(module, api_key, current['id']) if not success: module.fail_json(msg=f'Could not delete PSP {friendly_name!r}: {result}') - module.exit_json(changed=True, psp=current, + module.exit_json( + changed=True, + psp=current, diff={'before': delete_before, 'after': {}}, debug={ 'operation': 'delete', 'friendly_name': friendly_name, 'psp_id': current['id'], - }) + }, + ) # Desired payload. - custom_domain = module.params.get('custom_domain') or module.params.get('custom_url') + custom_domain = module.params.get('custom_domain') or module.params.get( + 'custom_url' + ) monitors_wire = _resolve_monitor_ids(module, api_key, module.params.get('monitors')) desired = { @@ -245,30 +263,40 @@ def main(): body.pop('status', None) # not allowed on create create_diff = {'before': {}, 'after': dict(body)} if module.check_mode: - module.exit_json(changed=True, psp=body, + module.exit_json( + changed=True, + psp=body, diff=create_diff, debug={ 'operation': 'create (check_mode)', 'friendly_name': friendly_name, 'sent_keys': sorted(body.keys()), - }) - module.log(f'uptimerobot_psp: creating friendly_name={friendly_name!r} sent_keys={sorted(body.keys())}') + }, + ) + module.log( + f'uptimerobot_psp: creating friendly_name={friendly_name!r} sent_keys={sorted(body.keys())}' + ) success, result = ur.new_psp(module, api_key, body) if not success: module.fail_json(msg=f'Could not create PSP {friendly_name!r}: {result}') - module.exit_json(changed=True, psp=result, + module.exit_json( + changed=True, + psp=result, diff=create_diff, debug={ 'operation': 'create', 'friendly_name': friendly_name, 'sent_keys': sorted(body.keys()), - }) + }, + ) # Update. `get_psps` already translated sort/status to labels. The API # returns monitors as a list of IDs and password is never returned, so # those need their own normalisation. current_compare = { - 'monitors': ur.monitors_wire(sorted(int(m) for m in (current.get('monitors') or []))), + 'monitors': ur.monitors_wire( + sorted(int(m) for m in (current.get('monitors') or [])) + ), 'custom_domain': current.get('custom_domain'), 'sort': current.get('sort'), 'hide_url_links': current.get('hide_url_links'), @@ -282,17 +310,21 @@ def main(): diff_fields = ['monitors', 'custom_domain', 'sort', 'hide_url_links', 'status'] field_diff = ur.diff_for_update(current_compare, desired_compare, diff_fields) if not field_diff and 'password' not in desired: - module.log(f"uptimerobot_psp: id={current['id']} no diff -> changed=false") - module.exit_json(changed=False, psp=current, debug={ - 'operation': 'noop', - 'reason': 'no diff', - 'friendly_name': friendly_name, - 'psp_id': current['id'], - }) + module.log(f'uptimerobot_psp: id={current["id"]} no diff -> changed=false') + module.exit_json( + changed=False, + psp=current, + debug={ + 'operation': 'noop', + 'reason': 'no diff', + 'friendly_name': friendly_name, + 'psp_id': current['id'], + }, + ) module.log( - f"uptimerobot_psp: id={current['id']} diff_fields={sorted(field_diff.keys())}" - f"{' (+password)' if 'password' in desired else ''}" + f'uptimerobot_psp: id={current["id"]} diff_fields={sorted(field_diff.keys())}' + f'{" (+password)" if "password" in desired else ""}' ) update_diff = { @@ -307,28 +339,34 @@ def main(): if module.check_mode: preview = dict(current) preview.update(field_diff) - module.exit_json(changed=True, psp=preview, + module.exit_json( + changed=True, + psp=preview, diff=update_diff, debug={ 'operation': 'update (check_mode)', 'friendly_name': friendly_name, 'psp_id': current['id'], 'diff_fields': sorted(field_diff.keys()), - }) + }, + ) body = dict(desired) body['id'] = current['id'] success, result = ur.edit_psp(module, api_key, body) if not success: module.fail_json(msg=f'Could not edit PSP {friendly_name!r}: {result}') - module.exit_json(changed=True, psp=result, + module.exit_json( + changed=True, + psp=result, diff=update_diff, debug={ 'operation': 'update', 'friendly_name': friendly_name, 'psp_id': current['id'], 'diff_fields': sorted(field_diff.keys()), - }) + }, + ) if __name__ == '__main__': diff --git a/plugins/modules/uptimerobot_psp_info.py b/plugins/modules/uptimerobot_psp_info.py index f87f5596..2878cb49 100644 --- a/plugins/modules/uptimerobot_psp_info.py +++ b/plugins/modules/uptimerobot_psp_info.py @@ -10,7 +10,7 @@ __metaclass__ = type -DOCUMENTATION = r''' +DOCUMENTATION = r""" --- module: uptimerobot_psp_info short_description: List UptimeRobot Public Status Pages @@ -34,10 +34,10 @@ description: - Filter the returned list to the PSP whose C(friendly_name) is an exact match for this value. The result is still a list (length 0 or 1) for shape stability. type: str -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" # 1) List every public status page on the account. - name: 'Capture all public status pages' linuxfabrik.lfops.uptimerobot_psp_info: @@ -66,10 +66,10 @@ | map(attribute="friendly_name") | list }} -''' +""" -RETURN = r''' +RETURN = r""" psps: description: List of PSP dicts. Empty list when nothing matched. type: list @@ -82,7 +82,7 @@ sample: operation: 'list' count: 2 -''' +""" from ansible.module_utils.basic import AnsibleModule @@ -98,7 +98,9 @@ def main(): module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - api_key = ur.resolve_api_key(module, module.params.get('api_key'), module.params.get('api_key_file')) + api_key = ur.resolve_api_key( + module, module.params.get('api_key'), module.params.get('api_key_file') + ) friendly_name = module.params.get('friendly_name') module.log('uptimerobot_psp_info: fetching public status pages') @@ -110,10 +112,14 @@ def main(): match = ur.find_by_friendly_name(psps, friendly_name) psps = [match] if match else [] - module.exit_json(changed=False, psps=psps, debug={ - 'operation': 'list', - 'count': len(psps), - }) + module.exit_json( + changed=False, + psps=psps, + debug={ + 'operation': 'list', + 'count': len(psps), + }, + ) if __name__ == '__main__': diff --git a/tests/unit/plugins/filter/test_combine_lod.py b/tests/unit/plugins/filter/test_combine_lod.py index cfd52900..ac7655e3 100644 --- a/tests/unit/plugins/filter/test_combine_lod.py +++ b/tests/unit/plugins/filter/test_combine_lod.py @@ -31,8 +31,13 @@ # (repo_root/plugins/filter/combine_lod.py) relative to this test file. _PLUGIN_PATH = os.path.join( os.path.dirname(__file__), - '..', '..', '..', '..', - 'plugins', 'filter', 'combine_lod.py', + '..', + '..', + '..', + '..', + 'plugins', + 'filter', + 'combine_lod.py', ) _spec = importlib.util.spec_from_file_location('combine_lod', _PLUGIN_PATH) _module = importlib.util.module_from_spec(_spec) @@ -41,15 +46,16 @@ class Test(unittest.TestCase): - def test_combine_lod_non_dict_item(self): """non-dictionary list elements are not supported""" - input1 = yaml.safe_load(textwrap.dedent(''' + input1 = yaml.safe_load( + textwrap.dedent(""" - name: 'myvar' value: 'test1' - 'im a string' - ''')) + """) + ) with self.assertRaises(AnsibleFilterError): combine_lod(input1) @@ -57,20 +63,26 @@ def test_combine_lod_non_dict_item(self): def test_combine_lod_last(self): """the last element should always win and overwrite the earlier ones""" - input1 = yaml.safe_load(textwrap.dedent(''' + input1 = yaml.safe_load( + textwrap.dedent(""" - name: 'myvar' value: 'test1' - ''')) + """) + ) - input2 = yaml.safe_load(textwrap.dedent(''' + input2 = yaml.safe_load( + textwrap.dedent(""" - name: 'myvar' value: 'test2' - ''')) + """) + ) - expected = yaml.safe_load(textwrap.dedent(''' + expected = yaml.safe_load( + textwrap.dedent(""" - name: 'myvar' value: 'test2' - ''')) + """) + ) result = combine_lod(input1, input2) self.assertEqual(result, expected) @@ -78,24 +90,29 @@ def test_combine_lod_last(self): def test_combine_lod_multiple(self): """test the basic functionality if there are multiple list elements""" - input1 = yaml.safe_load(textwrap.dedent(''' + input1 = yaml.safe_load( + textwrap.dedent(""" - name: 'first variable' value: 'test1' - name: 'second variable' value: 'test2' - name: 'other_var' value: 'linuxfabrik' - ''')) + """) + ) - input2 = yaml.safe_load(textwrap.dedent(''' + input2 = yaml.safe_load( + textwrap.dedent(""" - name: 'first variable' value: 'test1 - edited' - name: 'second variable' value: 'test2 - edited' new_value: 'new here' - ''')) + """) + ) - expected = yaml.safe_load(textwrap.dedent(''' + expected = yaml.safe_load( + textwrap.dedent(""" - name: 'first variable' value: 'test1 - edited' - name: 'second variable' @@ -103,7 +120,8 @@ def test_combine_lod_multiple(self): new_value: 'new here' - name: 'other_var' value: 'linuxfabrik' - ''')) + """) + ) result = combine_lod(input1, input2) self.assertEqual(result, expected) @@ -111,17 +129,21 @@ def test_combine_lod_multiple(self): def test_combine_lod_single_input(self): """test if everything works if the lists are already combined beforehand""" - input1 = yaml.safe_load(textwrap.dedent(''' + input1 = yaml.safe_load( + textwrap.dedent(""" - name: 'myvar' value: 'test1' - name: 'myvar' value: 'test2' - ''')) + """) + ) - expected = yaml.safe_load(textwrap.dedent(''' + expected = yaml.safe_load( + textwrap.dedent(""" - name: 'myvar' value: 'test2' - ''')) + """) + ) result = combine_lod(input1) self.assertEqual(result, expected) @@ -129,26 +151,32 @@ def test_combine_lod_single_input(self): def test_combine_lod_replace_given_keys(self): """only the given keys are overwritten, not the whole list element""" - input1 = yaml.safe_load(textwrap.dedent(''' + input1 = yaml.safe_load( + textwrap.dedent(""" - name: 'myvar' value: 'value 1' my_list: - 'input1' - 'lots of default entries' - ''')) + """) + ) - input2 = yaml.safe_load(textwrap.dedent(''' + input2 = yaml.safe_load( + textwrap.dedent(""" - name: 'myvar' value: 'value 2' - ''')) + """) + ) - expected = yaml.safe_load(textwrap.dedent(''' + expected = yaml.safe_load( + textwrap.dedent(""" - name: 'myvar' value: 'value 2' my_list: - 'input1' - 'lots of default entries' - ''')) + """) + ) result = combine_lod(input1, input2) self.assertEqual(result, expected) @@ -156,70 +184,86 @@ def test_combine_lod_replace_given_keys(self): def test_combine_lod_different_single_unique_key(self): """test if using a different single unique_key works""" - input1 = yaml.safe_load(textwrap.dedent(''' + input1 = yaml.safe_load( + textwrap.dedent(""" - filename: 'myvar 1' value: 'value 1' - filename: 'myvar 2' value: 'value 1' - ''')) + """) + ) - input2 = yaml.safe_load(textwrap.dedent(''' + input2 = yaml.safe_load( + textwrap.dedent(""" - filename: 'myvar 1' value: 'value 1 - edited' - ''')) + """) + ) - expected = yaml.safe_load(textwrap.dedent(''' + expected = yaml.safe_load( + textwrap.dedent(""" - filename: 'myvar 1' value: 'value 1 - edited' - filename: 'myvar 2' value: 'value 1' - ''')) + """) + ) - result = combine_lod(input1, input2, unique_key="filename") + result = combine_lod(input1, input2, unique_key='filename') self.assertEqual(result, expected) def test_combine_lod_different_list_unique_key(self): """test if using a list of unique_keys works""" - input1 = yaml.safe_load(textwrap.dedent(''' + input1 = yaml.safe_load( + textwrap.dedent(""" - server_name: 'myvar' server_port: 80 value: 'value 80' - server_name: 'myvar' server_port: 443 value: 'value 443' - ''')) + """) + ) - input2 = yaml.safe_load(textwrap.dedent(''' + input2 = yaml.safe_load( + textwrap.dedent(""" - server_name: 'myvar' server_port: 80 value: 'value 81' - ''')) + """) + ) - expected = yaml.safe_load(textwrap.dedent(''' + expected = yaml.safe_load( + textwrap.dedent(""" - server_name: 'myvar' server_port: 80 value: 'value 81' - server_name: 'myvar' server_port: 443 value: 'value 443' - ''')) + """) + ) - result = combine_lod(input1, input2, unique_key=["server_name", "server_port"]) + result = combine_lod(input1, input2, unique_key=['server_name', 'server_port']) self.assertEqual(result, expected) def test_combine_lod_missing_unique_key(self): """the plugin should throw an error if it cannot find the unique_key for all list elements""" - input1 = yaml.safe_load(textwrap.dedent(''' + input1 = yaml.safe_load( + textwrap.dedent(""" - name: 'myvar' value: 'value 1' - ''')) + """) + ) - input2 = yaml.safe_load(textwrap.dedent(''' + input2 = yaml.safe_load( + textwrap.dedent(""" - wrong_name: 'myvar' value: 'value 1' - ''')) + """) + ) with self.assertRaisesRegex(AnsibleFilterError, "unique key 'name'"): combine_lod(input1, input2) @@ -231,19 +275,23 @@ def test_combine_lod_single_key_falsy_value_allowed(self): real identity and must be kept and folded, not rejected. """ - input1 = yaml.safe_load(textwrap.dedent(''' + input1 = yaml.safe_load( + textwrap.dedent(""" - id: 0 value: 'first' - id: 0 value: 'second' - ''')) + """) + ) - expected = yaml.safe_load(textwrap.dedent(''' + expected = yaml.safe_load( + textwrap.dedent(""" - id: 0 value: 'second' - ''')) + """) + ) - result = combine_lod(input1, unique_key="id") + result = combine_lod(input1, unique_key='id') self.assertEqual(result, expected) def test_combine_lod_composite_key_missing_component(self): @@ -254,32 +302,38 @@ def test_combine_lod_composite_key_missing_component(self): merge with an item that states the same component's default explicitly. """ - input1 = yaml.safe_load(textwrap.dedent(''' + input1 = yaml.safe_load( + textwrap.dedent(""" - server_name: 'myvar' server_port: 80 value: 'value 80' - server_name: 'myvar' value: 'no port here' - ''')) + """) + ) # the error must name the missing key and show the present identifier - with self.assertRaisesRegex(AnsibleFilterError, "server_port"): - combine_lod(input1, unique_key=["server_name", "server_port"]) + with self.assertRaisesRegex(AnsibleFilterError, 'server_port'): + combine_lod(input1, unique_key=['server_name', 'server_port']) try: - combine_lod(input1, unique_key=["server_name", "server_port"]) + combine_lod(input1, unique_key=['server_name', 'server_port']) except AnsibleFilterError as exc: self.assertIn('server_port', str(exc)) - self.assertIn('myvar', str(exc)) # the present server_name, to locate the item + self.assertIn( + 'myvar', str(exc) + ) # the present server_name, to locate the item def test_combine_lod_composite_key_all_components_missing(self): """a composite key where every component is missing must raise as well""" - input1 = yaml.safe_load(textwrap.dedent(''' + input1 = yaml.safe_load( + textwrap.dedent(""" - value: 'no keys at all' - ''')) + """) + ) with self.assertRaises(AnsibleFilterError): - combine_lod(input1, unique_key=["server_name", "server_port"]) + combine_lod(input1, unique_key=['server_name', 'server_port']) def test_combine_lod_composite_key_falsy_component_allowed(self): """ @@ -288,7 +342,8 @@ def test_combine_lod_composite_key_falsy_component_allowed(self): socket) is a real identity and must be kept and folded, not rejected. """ - input1 = yaml.safe_load(textwrap.dedent(''' + input1 = yaml.safe_load( + textwrap.dedent(""" - address: '/var/lib/mysql/mysql.sock' hostgroup: 1 port: 0 @@ -296,41 +351,50 @@ def test_combine_lod_composite_key_falsy_component_allowed(self): hostgroup: 1 port: 0 max_connections: 100 - ''')) + """) + ) - expected = yaml.safe_load(textwrap.dedent(''' + expected = yaml.safe_load( + textwrap.dedent(""" - address: '/var/lib/mysql/mysql.sock' hostgroup: 1 port: 0 max_connections: 100 - ''')) + """) + ) - result = combine_lod(input1, unique_key=["hostgroup", "address", "port"]) + result = combine_lod(input1, unique_key=['hostgroup', 'address', 'port']) self.assertEqual(result, expected) def test_combine_lod_list_merge(self): """a key holding a list should be replaced wholesale, no append / prepend""" - input1 = yaml.safe_load(textwrap.dedent(''' + input1 = yaml.safe_load( + textwrap.dedent(""" - name: 'myvar' my_list: - 'input1' - 'input_repeated' - ''')) + """) + ) - input2 = yaml.safe_load(textwrap.dedent(''' + input2 = yaml.safe_load( + textwrap.dedent(""" - name: 'myvar' my_list: - 'input2' - 'input_repeated' - ''')) + """) + ) - expected = yaml.safe_load(textwrap.dedent(''' + expected = yaml.safe_load( + textwrap.dedent(""" - name: 'myvar' my_list: - 'input2' - 'input_repeated' - ''')) + """) + ) result = combine_lod(input1, input2) self.assertEqual(result, expected) @@ -338,7 +402,8 @@ def test_combine_lod_list_merge(self): def test_combine_lod_no_recursion(self): """the plugin should not recurse into dicts or lists""" - input1 = yaml.safe_load(textwrap.dedent(''' + input1 = yaml.safe_load( + textwrap.dedent(""" - name: 'myvar' value: 'value 1' my_list: @@ -346,17 +411,21 @@ def test_combine_lod_no_recursion(self): - name: 'my sub var' value: 'sub value 1' - 'input_repeated' - ''')) + """) + ) - input2 = yaml.safe_load(textwrap.dedent(''' + input2 = yaml.safe_load( + textwrap.dedent(""" - name: 'myvar' value: 'value 2' my_dict: name: 'my sub var' value: 'sub value 1' - ''')) + """) + ) - expected = yaml.safe_load(textwrap.dedent(''' + expected = yaml.safe_load( + textwrap.dedent(""" - name: 'myvar' value: 'value 2' my_list: @@ -367,7 +436,8 @@ def test_combine_lod_no_recursion(self): my_dict: name: 'my sub var' value: 'sub value 1' - ''')) + """) + ) result = combine_lod(input1, input2) self.assertEqual(result, expected) @@ -375,20 +445,26 @@ def test_combine_lod_no_recursion(self): def test_combine_lod_no_modification(self): """in this case the plugin should not modify anything""" - input1 = yaml.safe_load(textwrap.dedent(''' + input1 = yaml.safe_load( + textwrap.dedent(""" - name: 'myvar' value: 'value 1' - ''')) + """) + ) - input2 = yaml.safe_load(textwrap.dedent(''' + input2 = yaml.safe_load( + textwrap.dedent(""" - name: 'myvar' value: 'value 1' - ''')) + """) + ) - expected = yaml.safe_load(textwrap.dedent(''' + expected = yaml.safe_load( + textwrap.dedent(""" - name: 'myvar' value: 'value 1' - ''')) + """) + ) result = combine_lod(input1, input2) self.assertEqual(result, expected) diff --git a/tests/unit/plugins/filter/test_platform_select.py b/tests/unit/plugins/filter/test_platform_select.py index 03519927..1f25b802 100644 --- a/tests/unit/plugins/filter/test_platform_select.py +++ b/tests/unit/plugins/filter/test_platform_select.py @@ -29,8 +29,13 @@ # (repo_root/plugins/filter/platform_select.py) relative to this test file. _PLUGIN_PATH = os.path.join( os.path.dirname(__file__), - '..', '..', '..', '..', - 'plugins', 'filter', 'platform_select.py', + '..', + '..', + '..', + '..', + 'plugins', + 'filter', + 'platform_select.py', ) _spec = importlib.util.spec_from_file_location('platform_select', _PLUGIN_PATH) _module = importlib.util.module_from_spec(_spec) @@ -60,7 +65,6 @@ class Test(unittest.TestCase): - # --- basic matching -------------------------------------------------- def test_os_family_only(self): @@ -141,7 +145,9 @@ def test_default_none_is_distinct_from_no_default(self): platform_select(values, _FACTS_SUSE) def test_empty_input_dict_with_default(self): - self.assertEqual(platform_select({}, _FACTS_ROCKY8, default='fallback'), 'fallback') + self.assertEqual( + platform_select({}, _FACTS_ROCKY8, default='fallback'), 'fallback' + ) def test_empty_input_dict_without_default_raises(self): with self.assertRaises(AnsibleFilterError): diff --git a/tests/unit/plugins/lookup/test_bitwarden_item.py b/tests/unit/plugins/lookup/test_bitwarden_item.py index 9ac5e496..c17d847e 100644 --- a/tests/unit/plugins/lookup/test_bitwarden_item.py +++ b/tests/unit/plugins/lookup/test_bitwarden_item.py @@ -42,7 +42,14 @@ def is_unlocked(self): def sync(self, *args, **kwargs): pass - def get_items(self, name, username=None, folder_id=None, collection_id=None, organization_id=None): + def get_items( + self, + name, + username=None, + folder_id=None, + collection_id=None, + organization_id=None, + ): return list(type(self).items_by_search) def get_item_by_id(self, item_id): @@ -54,7 +61,6 @@ def get_pretty_name(name, hostname=None, purpose=None): class _BitwardenLookupTestCase(unittest.TestCase): - def setUp(self): self._orig = lookup_mod.Bitwarden lookup_mod.Bitwarden = _FakeBitwarden @@ -67,10 +73,12 @@ def tearDown(self): class TestRun(_BitwardenLookupTestCase): - def test_existing_single_item_lifts_credentials(self): _FakeBitwarden.items_by_search = [ - {'name': 'host - db', 'login': {'username': 'dba', 'password': 'linuxfabrik'}}, + { + 'name': 'host - db', + 'login': {'username': 'dba', 'password': 'linuxfabrik'}, + }, ] result = self.lookup.run([{'name': 'host - db', 'username': 'dba'}]) self.assertEqual(len(result), 1) @@ -79,15 +87,22 @@ def test_existing_single_item_lifts_credentials(self): def test_multiple_matches_raise(self): _FakeBitwarden.items_by_search = [ - {'name': 'host - db', 'login': {'username': 'dba', 'password': 'linuxfabrik'}}, - {'name': 'host - db', 'login': {'username': 'dba', 'password': 'linuxfabrik'}}, + { + 'name': 'host - db', + 'login': {'username': 'dba', 'password': 'linuxfabrik'}, + }, + { + 'name': 'host - db', + 'login': {'username': 'dba', 'password': 'linuxfabrik'}, + }, ] with self.assertRaises(AnsibleError): self.lookup.run([{'name': 'host - db', 'username': 'dba'}]) def test_lookup_by_id_lifts_credentials(self): _FakeBitwarden.item_by_id = { - 'id': 'abc', 'login': {'username': 'dba', 'password': 'linuxfabrik'}, + 'id': 'abc', + 'login': {'username': 'dba', 'password': 'linuxfabrik'}, } result = self.lookup.run([{'id': 'abc'}]) self.assertEqual(len(result), 1) diff --git a/tests/unit/plugins/module_utils/test_bitwarden.py b/tests/unit/plugins/module_utils/test_bitwarden.py index 97367368..004b4338 100644 --- a/tests/unit/plugins/module_utils/test_bitwarden.py +++ b/tests/unit/plugins/module_utils/test_bitwarden.py @@ -28,8 +28,13 @@ _MODULE_PATH = os.path.join( os.path.dirname(__file__), - '..', '..', '..', '..', - 'plugins', 'module_utils', 'bitwarden.py', + '..', + '..', + '..', + '..', + 'plugins', + 'module_utils', + 'bitwarden.py', ) _spec = importlib.util.spec_from_file_location('bitwarden', _MODULE_PATH) bitwarden = importlib.util.module_from_spec(_spec) @@ -55,7 +60,6 @@ def _make_bitwarden(tmp_path='/nonexistent/lfops_bw_test_cache.json'): class TestGenerate(unittest.TestCase): - def test_length_and_charset(self): bw = _make_bitwarden() result = bw.generate(password_length=32, password_choice='abc') @@ -72,26 +76,32 @@ def test_hex_requires_even_length(self): with self.assertRaises(ValueError): bw.generate(password_length=3, password_choice='0123456789abcdef') # even length is fine - self.assertEqual(len(bw.generate(password_length=4, password_choice='0123456789abcdef')), 4) + self.assertEqual( + len(bw.generate(password_length=4, password_choice='0123456789abcdef')), 4 + ) class TestGetPrettyName(unittest.TestCase): - def test_explicit_name_wins(self): - self.assertEqual(bitwarden.Bitwarden.get_pretty_name('myname', 'host', 'purpose'), 'myname') + self.assertEqual( + bitwarden.Bitwarden.get_pretty_name('myname', 'host', 'purpose'), 'myname' + ) def test_hostname_only(self): - self.assertEqual(bitwarden.Bitwarden.get_pretty_name('', hostname='app4711'), 'app4711') + self.assertEqual( + bitwarden.Bitwarden.get_pretty_name('', hostname='app4711'), 'app4711' + ) def test_hostname_and_purpose(self): self.assertEqual( - bitwarden.Bitwarden.get_pretty_name('', hostname='app4711', purpose='MariaDB'), + bitwarden.Bitwarden.get_pretty_name( + '', hostname='app4711', purpose='MariaDB' + ), 'app4711 - MariaDB', ) class TestApiCall(unittest.TestCase): - def setUp(self): self.bw = _make_bitwarden() self._orig_open_url = bitwarden.open_url @@ -100,18 +110,25 @@ def tearDown(self): bitwarden.open_url = self._orig_open_url def test_success_returns_result(self): - bitwarden.open_url = lambda *a, **k: _FakeResponse({'success': True, 'data': {'x': 1}}) + bitwarden.open_url = lambda *a, **k: _FakeResponse( + {'success': True, 'data': {'x': 1}} + ) result = self.bw._api_call('status') self.assertEqual(result, {'success': True, 'data': {'x': 1}}) def test_unsuccessful_payload_raises(self): - bitwarden.open_url = lambda *a, **k: _FakeResponse({'success': False, 'data': 'nope'}) + bitwarden.open_url = lambda *a, **k: _FakeResponse( + {'success': False, 'data': 'nope'} + ) with self.assertRaises(bitwarden.BitwardenException): self.bw._api_call('status') def test_http_error_raises_bitwarden_exception(self): def _raise(*a, **k): - raise HTTPError('http://127.0.0.1:8087/status', 500, 'err', {}, io.BytesIO(b'')) + raise HTTPError( + 'http://127.0.0.1:8087/status', 500, 'err', {}, io.BytesIO(b'') + ) + bitwarden.open_url = _raise with self.assertRaises(bitwarden.BitwardenException): self.bw._api_call('status') @@ -120,23 +137,39 @@ def test_invalid_json_raises_bitwarden_exception(self): class _BadResponse: def read(self): return b'not json' + bitwarden.open_url = lambda *a, **k: _BadResponse() with self.assertRaises(bitwarden.BitwardenException): self.bw._api_call('status') class TestGetItems(unittest.TestCase): - def setUp(self): self.bw = _make_bitwarden() # seed the in-memory cache directly; get_items only reads it self.bw._cache = { 'items': [ - {'type': 1, 'name': 'host - db', 'login': {'username': 'dba'}, - 'folderId': None, 'collectionIds': [], 'organizationId': None}, - {'type': 2, 'name': 'host - db', 'login': {'username': 'dba'}}, # non-login, skipped - {'type': 1, 'name': 'other', 'login': {'username': 'dba'}, - 'folderId': None, 'collectionIds': [], 'organizationId': None}, + { + 'type': 1, + 'name': 'host - db', + 'login': {'username': 'dba'}, + 'folderId': None, + 'collectionIds': [], + 'organizationId': None, + }, + { + 'type': 2, + 'name': 'host - db', + 'login': {'username': 'dba'}, + }, # non-login, skipped + { + 'type': 1, + 'name': 'other', + 'login': {'username': 'dba'}, + 'folderId': None, + 'collectionIds': [], + 'organizationId': None, + }, ], } diff --git a/tests/unit/plugins/module_utils/test_ipa_diff.py b/tests/unit/plugins/module_utils/test_ipa_diff.py index 054d57ce..8f2f8da1 100644 --- a/tests/unit/plugins/module_utils/test_ipa_diff.py +++ b/tests/unit/plugins/module_utils/test_ipa_diff.py @@ -21,7 +21,6 @@ class TestCompareKey(unittest.TestCase): - def test_scalar_equal(self): self.assertTrue(ipa_diff._compare_key('a', 'a')) self.assertFalse(ipa_diff._compare_key('a', 'b')) @@ -38,9 +37,10 @@ def test_scalar_promoted_to_list(self): class TestGenArgsDiff(unittest.TestCase): - def test_only_changed_keys(self): - before, after = ipa_diff.gen_args_diff({'a': 'x', 'b': 'y'}, {'a': ['x'], 'b': ['z']}) + before, after = ipa_diff.gen_args_diff( + {'a': 'x', 'b': 'y'}, {'a': ['x'], 'b': ['z']} + ) self.assertEqual(before, {'b': 'z'}) self.assertEqual(after, {'b': 'y'}) @@ -53,18 +53,20 @@ def test_empty_args(self): class TestGenMemberDiff(unittest.TestCase): - def test_no_change(self): - self.assertEqual(ipa_diff.gen_member_diff('member_user', [], [], ['a']), ({}, {})) + self.assertEqual( + ipa_diff.gen_member_diff('member_user', [], [], ['a']), ({}, {}) + ) def test_add_and_delete(self): - before, after = ipa_diff.gen_member_diff('member_user', ['c'], ['a'], ['a', 'b']) + before, after = ipa_diff.gen_member_diff( + 'member_user', ['c'], ['a'], ['a', 'b'] + ) self.assertEqual(before, {'member_user': ['a', 'b']}) self.assertEqual(after, {'member_user': ['b', 'c']}) class TestMergeDiffs(unittest.TestCase): - def test_merge(self): before, after = ipa_diff.merge_diffs(({'a': 1}, {'a': 2}), ({'b': 3}, {'b': 4})) self.assertEqual(before, {'a': 1, 'b': 3}) @@ -72,7 +74,6 @@ def test_merge(self): class TestIPADiffTracker(unittest.TestCase): - def test_empty_build(self): self.assertEqual(ipa_diff.IPADiffTracker().build_diff(), {}) diff --git a/tests/unit/plugins/module_utils/test_uptimerobot.py b/tests/unit/plugins/module_utils/test_uptimerobot.py index 8dbf2c6c..5c47723e 100644 --- a/tests/unit/plugins/module_utils/test_uptimerobot.py +++ b/tests/unit/plugins/module_utils/test_uptimerobot.py @@ -26,9 +26,10 @@ class TestWireBuilders(unittest.TestCase): - def test_alert_contacts_wire_with_defaults(self): - wire = ur.alert_contacts_wire([{'id': 1, 'threshold': 5, 'recurrence': 0}, {'id': 2}]) + wire = ur.alert_contacts_wire( + [{'id': 1, 'threshold': 5, 'recurrence': 0}, {'id': 2}] + ) self.assertEqual(wire, '1_5_0-2_0_0') def test_mwindows_wire(self): @@ -39,7 +40,6 @@ def test_monitors_wire(self): class TestTranslateHelpers(unittest.TestCase): - def test_translate_known_and_unknown(self): self.assertEqual(ur._translate('http', ur.MONITOR_TYPE), 1) self.assertEqual(ur._translate('nope', ur.MONITOR_TYPE), 'nope') @@ -58,7 +58,6 @@ def test_translate_keys_only_strings(self): class TestSafeKeysAndCache(unittest.TestCase): - def test_safe_keys_redacts_secrets(self): self.assertEqual( ur._safe_keys({'api_key': 'x', 'password': 'y', 'friendly_name': 'n'}), @@ -74,7 +73,6 @@ def test_cache_key_stable_and_param_sensitive(self): class TestFriendlyNames(unittest.TestCase): - def test_find_by_friendly_name(self): items = [{'friendly_name': 'a'}, {'friendly_name': 'b'}] self.assertEqual(ur.find_by_friendly_name(items, 'b'), {'friendly_name': 'b'}) @@ -82,15 +80,18 @@ def test_find_by_friendly_name(self): def test_resolve_friendly_names_ok(self): items = [{'friendly_name': 'a', 'id': 1}, {'friendly_name': 'b', 'id': 2}] - self.assertEqual(ur.resolve_friendly_names(items, ['b', 'a'], 'monitor'), [2, 1]) + self.assertEqual( + ur.resolve_friendly_names(items, ['b', 'a'], 'monitor'), [2, 1] + ) def test_resolve_friendly_names_unknown_raises(self): with self.assertRaises(ValueError): - ur.resolve_friendly_names([{'friendly_name': 'a', 'id': 1}], ['zzz'], 'monitor') + ur.resolve_friendly_names( + [{'friendly_name': 'a', 'id': 1}], ['zzz'], 'monitor' + ) class TestDiffForUpdate(unittest.TestCase): - def test_diff_compares_stringified(self): # int 1 vs string '1' must be considered equal (no spurious change) out = ur.diff_for_update({'x': 1, 'y': 2}, {'x': '1', 'y': '3'}, ['x', 'y']) @@ -102,7 +103,6 @@ def test_diff_skips_fields_not_in_desired(self): class TestResponseTranslators(unittest.TestCase): - def test_monitor_response_maps_ids_to_labels(self): item = {'type': 1, 'status': 2, 'http_method': 2, 'auth_type': 1} ur._translate_monitor_response(item) diff --git a/tests/unit/plugins/modules/test_bitwarden_item.py b/tests/unit/plugins/modules/test_bitwarden_item.py index e49a10a3..67fbe10e 100644 --- a/tests/unit/plugins/modules/test_bitwarden_item.py +++ b/tests/unit/plugins/modules/test_bitwarden_item.py @@ -32,7 +32,6 @@ class TestDiffAndUpdate(unittest.TestCase): - def test_takes_over_id(self): current = {'id': 'abc', 'name': 'x'} target = {'name': 'x'} @@ -74,7 +73,12 @@ def test_nested_dict_no_change(self): _EXISTING_ITEM = { 'id': 'abc', 'name': 'host - db', - 'login': {'username': 'dba', 'password': 'linuxfabrik-existing', 'totp': '', 'uris': []}, + 'login': { + 'username': 'dba', + 'password': 'linuxfabrik-existing', + 'totp': '', + 'uris': [], + }, 'notes': 'Generated by Ansible.', 'organizationId': None, 'collectionIds': None, @@ -109,13 +113,28 @@ def get_template_item_login_uri(self, uris): return list(uris or []) def get_template_item_login(self, username=None, password=None, login_uris=None): - return {'username': username, 'password': password, 'totp': '', 'uris': login_uris or []} + return { + 'username': username, + 'password': password, + 'totp': '', + 'uris': login_uris or [], + } - def get_template_item(self, name, login=None, notes=None, organization_id=None, - collection_ids=None, folder_id=None): + def get_template_item( + self, + name, + login=None, + notes=None, + organization_id=None, + collection_ids=None, + folder_id=None, + ): return { - 'name': name, 'login': login, 'notes': notes, - 'organizationId': organization_id, 'collectionIds': collection_ids, + 'name': name, + 'login': login, + 'notes': notes, + 'organizationId': organization_id, + 'collectionIds': collection_ids, 'folderId': folder_id, } @@ -137,7 +156,6 @@ def create_item(self, item): class TestMain(unittest.TestCase): - def setUp(self): _FakeBitwarden.items = [] _FakeBitwarden.edited = [] @@ -163,19 +181,27 @@ def _run(self, args): def test_check_mode_create_does_not_write(self): _FakeBitwarden.items = [] # nothing exists -> would create - result = self._run({ - 'name': 'host - db', 'username': 'dba', 'password': 'linuxfabrik-new', - '_ansible_check_mode': True, - }) + result = self._run( + { + 'name': 'host - db', + 'username': 'dba', + 'password': 'linuxfabrik-new', + '_ansible_check_mode': True, + } + ) self.assertTrue(result['changed']) self.assertEqual(_FakeBitwarden.created, []) # no write in check mode def test_check_mode_edit_does_not_write(self): _FakeBitwarden.items = [_EXISTING_ITEM] - result = self._run({ - 'name': 'host - db', 'username': 'dba', 'password': 'a-different-password', - '_ansible_check_mode': True, - }) + result = self._run( + { + 'name': 'host - db', + 'username': 'dba', + 'password': 'a-different-password', + '_ansible_check_mode': True, + } + ) self.assertTrue(result['changed']) self.assertEqual(_FakeBitwarden.edited, []) # no write in check mode @@ -189,9 +215,13 @@ def test_none_password_does_not_overwrite(self): def test_changed_password_writes_when_not_check_mode(self): _FakeBitwarden.items = [_EXISTING_ITEM] - result = self._run({ - 'name': 'host - db', 'username': 'dba', 'password': 'a-different-password', - }) + result = self._run( + { + 'name': 'host - db', + 'username': 'dba', + 'password': 'a-different-password', + } + ) self.assertTrue(result['changed']) self.assertEqual(len(_FakeBitwarden.edited), 1) diff --git a/tests/unit/plugins/modules/test_gpg_key.py b/tests/unit/plugins/modules/test_gpg_key.py index f9a2db9e..c18678e0 100644 --- a/tests/unit/plugins/modules/test_gpg_key.py +++ b/tests/unit/plugins/modules/test_gpg_key.py @@ -23,7 +23,7 @@ from ansible_collections.linuxfabrik.lfops.plugins.modules import gpg_key _KEY = { - 'algo': '1', # 1 -> RSA + 'algo': '1', # 1 -> RSA 'length': '2048', 'uids': ['Test Name (a comment) '], } @@ -40,7 +40,6 @@ class TestMatchKey(unittest.TestCase): - def test_full_match(self): self.assertTrue(gpg_key.match_key(copy.deepcopy(_KEY), dict(_PARAMS))) diff --git a/tests/unit/plugins/modules/test_nextcloud_occ_app_config.py b/tests/unit/plugins/modules/test_nextcloud_occ_app_config.py index 66a9933b..4406d791 100644 --- a/tests/unit/plugins/modules/test_nextcloud_occ_app_config.py +++ b/tests/unit/plugins/modules/test_nextcloud_occ_app_config.py @@ -29,20 +29,27 @@ class TestValuesMatch(unittest.TestCase): - def test_array_equal_ignoring_whitespace(self): # occ returns '["alpha","beta"]'; user passes a spaced literal - self.assertTrue(mod.values_match('["alpha","beta"]', '["alpha", "beta"]', 'array')) + self.assertTrue( + mod.values_match('["alpha","beta"]', '["alpha", "beta"]', 'array') + ) def test_array_canonical_vs_user(self): # cached path stores json.dumps(list) -> '["alpha", "beta"]' - self.assertTrue(mod.values_match('["alpha", "beta"]', '["alpha","beta"]', 'array')) + self.assertTrue( + mod.values_match('["alpha", "beta"]', '["alpha","beta"]', 'array') + ) def test_array_different(self): - self.assertFalse(mod.values_match('["alpha", "beta"]', '["alpha","gamma"]', 'array')) + self.assertFalse( + mod.values_match('["alpha", "beta"]', '["alpha","gamma"]', 'array') + ) def test_array_invalid_json_is_not_a_match(self): - self.assertFalse(mod.values_match("['alpha', 'beta']", '["alpha","beta"]', 'array')) + self.assertFalse( + mod.values_match("['alpha', 'beta']", '["alpha","beta"]', 'array') + ) def test_non_array_string_compare(self): self.assertTrue(mod.values_match('90', '90', 'integer')) @@ -68,24 +75,32 @@ def _run(self, args): raise AssertionError('module did not call exit_json') def test_array_already_set_is_idempotent(self): - result = self._run({ - 'app': 'core', - 'name': 'test_array', - 'value': '["alpha","beta"]', - 'type': 'array', - 'installed_config_json': {'apps': {'core': {'test_array': ['alpha', 'beta']}}}, - }) + result = self._run( + { + 'app': 'core', + 'name': 'test_array', + 'value': '["alpha","beta"]', + 'type': 'array', + 'installed_config_json': { + 'apps': {'core': {'test_array': ['alpha', 'beta']}} + }, + } + ) self.assertFalse(result['changed']) def test_array_differs_reports_change(self): - result = self._run({ - 'app': 'core', - 'name': 'test_array', - 'value': '["alpha","beta"]', - 'type': 'array', - 'installed_config_json': {'apps': {'core': {'test_array': ['alpha', 'gamma']}}}, - '_ansible_check_mode': True, # avoid the real occ config:app:set call - }) + result = self._run( + { + 'app': 'core', + 'name': 'test_array', + 'value': '["alpha","beta"]', + 'type': 'array', + 'installed_config_json': { + 'apps': {'core': {'test_array': ['alpha', 'gamma']}} + }, + '_ansible_check_mode': True, # avoid the real occ config:app:set call + } + ) self.assertTrue(result['changed']) diff --git a/tests/unit/plugins/modules/test_sqlite_query.py b/tests/unit/plugins/modules/test_sqlite_query.py index 324790c1..dc234b2b 100644 --- a/tests/unit/plugins/modules/test_sqlite_query.py +++ b/tests/unit/plugins/modules/test_sqlite_query.py @@ -26,7 +26,6 @@ class SqliteHelpersTestCase(unittest.TestCase): - def setUp(self): self.tmpdir = tempfile.mkdtemp(prefix='lfops_sqlite_test_') ok, self.conn = mod.connect(path=self.tmpdir, filename='test.db') @@ -41,14 +40,15 @@ def tearDown(self): class TestSelect(SqliteHelpersTestCase): - def test_as_dict(self): ok, rows = mod.select(self.conn, 'SELECT id, name FROM t WHERE id = 1') self.assertTrue(ok) self.assertEqual(rows, [{'id': 1, 'name': 'alpha'}]) def test_as_tuple(self): - ok, rows = mod.select(self.conn, 'SELECT id, name FROM t WHERE id = 1', as_dict=False) + ok, rows = mod.select( + self.conn, 'SELECT id, name FROM t WHERE id = 1', as_dict=False + ) self.assertTrue(ok) self.assertEqual(tuple(rows[0]), (1, 'alpha')) @@ -58,13 +58,17 @@ def test_fetch_one(self): self.assertEqual(row, {'id': 1}) def test_fetch_one_empty(self): - ok, row = mod.select(self.conn, 'SELECT id FROM t WHERE id = 999', fetchone=True) + ok, row = mod.select( + self.conn, 'SELECT id FROM t WHERE id = 999', fetchone=True + ) self.assertTrue(ok) self.assertEqual(row, []) def test_named_args(self): ok, rows = mod.select( - self.conn, 'SELECT name FROM t WHERE id = :wanted', data={'wanted': 2}, + self.conn, + 'SELECT name FROM t WHERE id = :wanted', + data={'wanted': 2}, ) self.assertTrue(ok) self.assertEqual(rows, [{'name': 'beta'}]) @@ -76,7 +80,6 @@ def test_bad_query_reports_failure(self): class TestRegexp(SqliteHelpersTestCase): - def test_regexp_in_where(self): ok, rows = mod.select(self.conn, "SELECT name FROM t WHERE name REGEXP '^al'") self.assertTrue(ok) @@ -84,9 +87,10 @@ def test_regexp_in_where(self): class TestConnectFailure(unittest.TestCase): - def test_connect_to_unwritable_path(self): - ok, result = mod.connect(path='/nonexistent/dir/that/should/not/exist', filename='x.db') + ok, result = mod.connect( + path='/nonexistent/dir/that/should/not/exist', filename='x.db' + ) self.assertFalse(ok) self.assertIn('failed', result.lower()) @@ -103,23 +107,33 @@ def setUp(self): mod.close(conn) def test_successful_query_exits_with_result(self): - ansible_harness.set_module_args({ - 'path': self.tmpdir, 'db': 'main.db', 'query': 'SELECT id FROM t', - }) - with ansible_harness.patch_module(), self.assertRaises( - ansible_harness.AnsibleExitJson - ) as cm: + ansible_harness.set_module_args( + { + 'path': self.tmpdir, + 'db': 'main.db', + 'query': 'SELECT id FROM t', + } + ) + with ( + ansible_harness.patch_module(), + self.assertRaises(ansible_harness.AnsibleExitJson) as cm, + ): mod.main() self.assertEqual(cm.exception.args[0]['query_result'], [{'id': 1}]) self.assertFalse(cm.exception.args[0]['changed']) def test_failed_query_fails_the_task(self): - ansible_harness.set_module_args({ - 'path': self.tmpdir, 'db': 'main.db', 'query': 'SELECT * FROM does_not_exist', - }) - with ansible_harness.patch_module(), self.assertRaises( - ansible_harness.AnsibleFailJson - ) as cm: + ansible_harness.set_module_args( + { + 'path': self.tmpdir, + 'db': 'main.db', + 'query': 'SELECT * FROM does_not_exist', + } + ) + with ( + ansible_harness.patch_module(), + self.assertRaises(ansible_harness.AnsibleFailJson) as cm, + ): mod.main() self.assertIn('Query failed', cm.exception.args[0]['msg']) diff --git a/tests/unit/plugins/modules/test_uptimerobot_monitor.py b/tests/unit/plugins/modules/test_uptimerobot_monitor.py index 282594dc..f10908ff 100644 --- a/tests/unit/plugins/modules/test_uptimerobot_monitor.py +++ b/tests/unit/plugins/modules/test_uptimerobot_monitor.py @@ -26,7 +26,6 @@ class TestNormalizeAlertContacts(unittest.TestCase): - def test_current_is_sorted_by_id(self): current = [ {'id': 2, 'threshold': 5, 'recurrence': 0, 'friendly_name': 'b'}, @@ -38,11 +37,15 @@ def test_empty_current(self): self.assertEqual(mod._normalize_current_alert_contacts([]), '') def test_desired_wire_is_sorted(self): - self.assertEqual(mod._normalize_desired_alert_contacts('2_5_0-1_0_0'), '1_0_0-2_5_0') + self.assertEqual( + mod._normalize_desired_alert_contacts('2_5_0-1_0_0'), '1_0_0-2_5_0' + ) def test_current_and_desired_match_when_equivalent(self): - current = [{'id': 1, 'threshold': 0, 'recurrence': 0}, - {'id': 2, 'threshold': 5, 'recurrence': 0}] + current = [ + {'id': 1, 'threshold': 0, 'recurrence': 0}, + {'id': 2, 'threshold': 5, 'recurrence': 0}, + ] self.assertEqual( mod._normalize_current_alert_contacts(current), mod._normalize_desired_alert_contacts('2_5_0-1_0_0'), @@ -50,7 +53,6 @@ def test_current_and_desired_match_when_equivalent(self): class TestNormalizeMwindows(unittest.TestCase): - def test_current_sorted(self): self.assertEqual(mod._normalize_current_mwindows([{'id': 3}, {'id': 1}]), '1-3') diff --git a/tests/unit/plugins/modules/test_uptimerobot_mwindow.py b/tests/unit/plugins/modules/test_uptimerobot_mwindow.py index 685c9c41..06d2ebae 100644 --- a/tests/unit/plugins/modules/test_uptimerobot_mwindow.py +++ b/tests/unit/plugins/modules/test_uptimerobot_mwindow.py @@ -23,7 +23,6 @@ class TestHhmmToMinutes(unittest.TestCase): - def test_basic(self): self.assertEqual(mod._hhmm_to_minutes('00:00'), 0) self.assertEqual(mod._hhmm_to_minutes('01:30'), 90) @@ -31,7 +30,6 @@ def test_basic(self): class TestComputeDuration(unittest.TestCase): - def test_same_day(self): self.assertEqual(mod._compute_duration('09:00', '17:00'), 480) @@ -44,9 +42,13 @@ def test_equal_start_end_is_full_day(self): class TestSynthesiseName(unittest.TestCase): - def test_with_value(self): - params = {'type': 'weekly', 'value': 'mon-wed', 'start_time': '03:30', 'end_time': '05:30'} + params = { + 'type': 'weekly', + 'value': 'mon-wed', + 'start_time': '03:30', + 'end_time': '05:30', + } self.assertEqual(mod._synthesise_name(params), 'weekly mon-wed 03:30-05:30') def test_without_value(self): diff --git a/tests/unit/test_plugin_docs.py b/tests/unit/test_plugin_docs.py index 18cd3d04..591fb81e 100644 --- a/tests/unit/test_plugin_docs.py +++ b/tests/unit/test_plugin_docs.py @@ -33,7 +33,9 @@ import yaml -_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +_REPO_ROOT = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +) _PLUGIN_GLOBS = [ 'plugins/filter/*.py', 'plugins/lookup/*.py', @@ -64,8 +66,11 @@ def _extract_doc_constants(source): continue names = [t.id for t in node.targets if isinstance(t, ast.Name)] for wanted in ('DOCUMENTATION', 'RETURN'): - if wanted in names and isinstance(node.value, ast.Constant) \ - and isinstance(node.value.value, str): + if ( + wanted in names + and isinstance(node.value, ast.Constant) + and isinstance(node.value.value, str) + ): docs[wanted] = yaml.safe_load(node.value.value) return docs @@ -90,7 +95,6 @@ def _iter_description_problems(obj, path=''): class TestPluginDocs(unittest.TestCase): - def test_in_house_plugins_have_renderable_descriptions(self): files = _in_house_plugin_files() self.assertTrue(files, 'no in-house plugin files found') @@ -101,9 +105,12 @@ def test_in_house_plugins_have_renderable_descriptions(self): docs = _extract_doc_constants(source) problems = [] for const_name, doc in docs.items(): - problems += [f'{const_name}{p}' for p in _iter_description_problems(doc)] + problems += [ + f'{const_name}{p}' for p in _iter_description_problems(doc) + ] self.assertEqual( - problems, [], + problems, + [], 'description fields must be str or list[str] ' '(a colon + space in a bullet makes ansible-doc fail):\n ' + '\n '.join(problems),