WRAPPER = "import importlib.util\nimport json\nimport sys\nimport types\n\n\n_unavailable =
None\ntry:\n from ansible.plugins.action import
ActionBase\n from ansible.errors import
AnsibleError\nexcept
Exception as exc:\n _unavailable = \"%s: %s\" % (type(exc).__name__, exc)\n\n\ndef _load_plugin(path):\n modname = \"krikri_action_%d\" % hash(path)\n spec = importlib.util.spec_from_file_location(modname, path)\n if spec is
None or spec.loader is
None:\n raise
ImportError(\"cannot load action plugin: %s\" % path)\n module = importlib.util.module_from_spec(spec)\n spec.loader.exec_module(module)\n return module\n\n\ndef _serialize(value):\n if isinstance(value, (set, frozenset)):\n return list(value)\n if isinstance(value, bytes):\n return value.decode(\"utf-8\", \"replace\")\n return str(value)\n\n\nclass _
PassthroughTemplar(object):\n #
Krikri hands the plugin task_vars as plain values (the\n # executor's own var context);
Ansible would template them\n # again here, which is a no-op for the literal data the\n # supported plugins read (amtega.check_platform's\n # check_platform_distributions dict and friends, issue #411).\n #
A plugin needing
REAL templating gets its value untouched\n # and fails like a plugin crashing in
Ansible would.\n\n def template(self, variable, **kwargs):\n return variable\n\n\ndef _emit(obj):\n #
One protocol line:
JSON with the marker that tells the engine\n # this is the wrapper speaking, not plugin noise on stdout.\n obj = dict(obj)\n obj[\"__krikri__\"] =
True\n sys.stdout.write(json.dumps(obj, default=_serialize) + \"\\n\")\n sys.stdout.flush()\n\n\ndef _ipc_execute_module(self, module_name=
None, module_args=
None, **kwargs):\n #
Ansible's real _execute_module transfers and runs a module\n # over a live connection - impossible on this stub runtime, so\n # the engine runs the named module instead (its own dispatch,\n # the task's host/connection/become) and the plugin gets back\n # the module's result dict. agaffney.synology_dsm's\n # synology_dsm_api_request builds 'uri' params and delegates\n # exactly this way, and the uri result
IS the task result\n # (issue #399 - live-verified vs 2.19.11).\n name = module_name or self._task.action\n args = module_args if module_args is not
None else dict(self._task.args)\n _emit({\"type\": \"execute_module\", \"module\": name, \"args\": args})\n while
True:\n line = sys.stdin.readline()\n if line == \"\":\n raise
AnsibleError(\"krikri action dispatch channel closed\")\n try:\n msg = json.loads(line)\n except
ValueError:\n continue\n if not isinstance(msg, dict) or not msg.get(\"__krikri__\"):\n continue\n if msg.get(\"error\") is not
None:\n raise
AnsibleError(msg[\"error\"])\n result = msg.get(\"result\")\n return result if result is not
None else {}\n\n\ndef _ipc_remove_tmp_path(self, tmp_path=
None, force=
False):\n #
The real method deletes the tmp dir over the connection shell;\n # krikri's inner dispatch manages the tmp dirs of the modules it\n # runs, so this is a no-op here rather than a crash on the stub\n # connection (a plugin that cleans up after _execute_module -\n # synology_dsm_api_request does - would otherwise die in\n # _low_level_execute_command).\n return
None\n\n\ndef main():\n payload = json.loads(sys.stdin.readline())\n if _unavailable is not
None:\n _emit({\"ok\":
False, \"kind\": \"unavailable\",\n \"error\": \"ansible is not importable by the controller python3: %s\"\n % _unavailable})\n return\n try:\n module = _load_plugin(payload.get(\"path\"))\n except
ImportError as exc:\n _emit({\"ok\":
False, \"kind\": \"load\", \"error\": str(exc)})\n return\n plugin_class = getattr(module, \"
ActionModule\",
None)\n if plugin_class is
None or not isinstance(plugin_class, type) or \\\n not issubclass(plugin_class,
ActionBase):\n _emit({\"ok\":
False, \"kind\": \"not_found\",\n \"error\": \"no
ActionModule subclass of
ActionBase in %s\"\n % payload.get(\"path\")})\n return\n task = types.
SimpleNamespace(\n args=payload.get(\"args\") or {},\n action=payload.get(\"name\"),\n async_val=0,\n check_mode=bool(payload.get(\"check_mode\")),\n become=bool(payload.get(\"become\")),\n become_user=payload.get(\"become_user\"),\n diff=
False,\n poll=0,\n no_log=
False,\n environment=[],\n delegate_to=
None,\n )\n #
The non-
None tmpdir placeholder keeps
ActionBase.run's\n # `_connection._shell.tmpdir is
None` early check from calling\n # _make_tmp_path - which would need a real connection shell.\n connection = types.
SimpleNamespace(\n transport=payload.get(\"connection\"),\n _shell=types.
SimpleNamespace(tmpdir=\"/tmp/krikri-action-tmpdir\"),\n )\n play_context = types.
SimpleNamespace(\n connection=payload.get(\"connection\"),\n remote_addr=payload.get(\"remote_addr\"),\n port=payload.get(\"port\"),\n remote_user=payload.get(\"remote_user\"),\n become=bool(payload.get(\"become\")),\n become_user=payload.get(\"become_user\"),\n become_method=
None,\n become_flags=
None,\n check_mode=bool(payload.get(\"check_mode\")),\n diff=
False,\n )\n # ansible-core 2.19 still passes shared_loader_obj on every action\n # plugin instantiation (task_executor._get_action_handler), even\n # though
ActionBase.__init__ made it optional (shared_loader_obj=
None)\n # in that release - a legacy plugin whose own __init__ declares it\n # as a
REQUIRED positional parameter (wcm_io_devops.conga_facts,\n # issue #658) therefore dies with \"missing 1 required positional\n # argument\" unless the engine passes it too, like
Ansible does.\n instance = plugin_class(task, connection, play_context,
None, _
PassthroughTemplar(),
None)\n if payload.get(\"ipc\"):\n instance._execute_module = types.
MethodType(_ipc_execute_module, instance)\n instance._remove_tmp_path = types.
MethodType(_ipc_remove_tmp_path, instance)\n try:\n result = instance.run(task_vars=payload.get(\"task_vars\") or {})\n except
AnsibleError as exc:\n #
An AnsibleError raised by the plugin itself
IS the task's\n # failure (
Ansible's task executor reports it as the plain\n # action-level \"
Task failed: <text>\" shape, live-verified vs\n # 2.19.11) - report the bare text under its own kind, not\n # the class-name-prefixed runner-crash shape.\n _emit({\"ok\":
False, \"kind\": \"task_failed\", \"error\": \"%s\" % exc})\n return\n _emit({\"ok\":
True, \"result\": result})\n\n\ntry:\n main()\n#
BaseException, not
Exception: a plugin's `sys.exit()` raises\n#
SystemExit, which would otherwise kill the wrapper before any\n#
JSON response - reported as a kind \"error\" plugin failure here\n# rather than silently degrading the whole mechanism.\nexcept
BaseException as exc:\n _emit({\"ok\":
False, \"kind\": \"error\",\n \"error\": \"%s: %s\" % (type(exc).__name__, exc)})"