diff --git a/ansible.cfg b/ansible.cfg index 99fe3df6..96643c7d 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -10,9 +10,9 @@ vault_password_file = vault-pass.yml gathering = smart #display_ok_hosts = no # zeigt nur noch changed und error tasks/hosts an #display_skipped_hosts = yes # dito -callback_plugins = ./plugins/callback +# callback_plugins = ./plugins/callback # python3 -m ara.setup.callback_plugins -callbacks_enabled = mhansen.ansible_trace.trace # https://github.com/mhansen/ansible-trace +# callbacks_enabled = mhansen.ansible_trace.trace # https://github.com/mhansen/ansible-trace [inventory] [privilege_escalation] diff --git a/group_vars/fileserver.yml b/group_vars/fileserver.yml index 2f753dfc..957941e8 100644 --- a/group_vars/fileserver.yml +++ b/group_vars/fileserver.yml @@ -1,7 +1,7 @@ --- ### PROXMOX # fileserver3 ist ein LXC-Container; der Container ist "privileged" damit der Bind-Mount die richtigen Rechte bekommt - + ### oefenweb.ufw ufw_rules: - rule: allow @@ -71,8 +71,6 @@ password: "{{ lookup('keepass', 'fileserver_smb_user_pve', 'password') }}" - name: 'brother_ads2700w' password: "{{ lookup('keepass', 'fileserver_smb_user_brother_ads2700w', 'password') }}" - - name: 'photoprism' - password: "{{ lookup('keepass', 'fileserver_smb_user_photoprism', 'password') }}" ### mgrote.apt_manage_packages apt_packages_internet: diff --git a/inventory b/inventory index 8dd70ce3..426f3e94 100644 --- a/inventory +++ b/inventory @@ -29,7 +29,6 @@ all: vmtest: hosts: vm-test-2004.grote.lan: - vm-test-2204.grote.lan: proxmox: hosts: pve5.grote.lan: @@ -57,6 +56,5 @@ all: k3s-nfs2.grote.lan: test: hosts: - vm-test-2004.grote.lan: vm-test-2204.grote.lan: pve5-test.grote.lan: diff --git a/plugins/callback/ara_default.py b/plugins/callback/ara_default.py deleted file mode 100644 index a16202fb..00000000 --- a/plugins/callback/ara_default.py +++ /dev/null @@ -1,618 +0,0 @@ -# Copyright (c) 2018 Red Hat, Inc. -# -# This file is part of ARA: Ansible Run Analysis. -# -# ARA is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# ARA is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with ARA. If not, see . - -from __future__ import absolute_import, division, print_function - -import datetime -import json -import logging -import os -import socket -from concurrent.futures import ThreadPoolExecutor - -from ansible import __version__ as ansible_version -from ansible.parsing.ajson import AnsibleJSONEncoder -from ansible.plugins.callback import CallbackBase -from ansible.vars.clean import module_response_deepcopy, strip_internal_keys - -from ara.clients import utils as client_utils - -# Ansible CLI options are now in ansible.context in >= 2.8 -# https://github.com/ansible/ansible/commit/afdbb0d9d5bebb91f632f0d4a1364de5393ba17a -try: - from ansible import context - - cli_options = {key: value for key, value in context.CLIARGS.items()} -except ImportError: - # < 2.8 doesn't have ansible.context - try: - from __main__ import cli - - cli_options = cli.options.__dict__ - except ImportError: - # using API without CLI - cli_options = {} - - -DOCUMENTATION = """ -callback: ara -callback_type: notification -requirements: - - ara -short_description: Sends playbook execution data to the ARA API internally or over HTTP -description: - - Sends playbook execution data to the ARA API internally or over HTTP -options: - api_client: - description: The client to use for communicating with the API - default: offline - env: - - name: ARA_API_CLIENT - ini: - - section: ara - key: api_client - choices: ['offline', 'http'] - api_server: - description: When using the HTTP client, the base URL to the ARA API server - default: http://127.0.0.1:8000 - env: - - name: ARA_API_SERVER - ini: - - section: ara - key: api_server - api_username: - description: If authentication is required, the username to authenticate with - default: null - env: - - name: ARA_API_USERNAME - ini: - - section: ara - key: api_username - api_password: - description: If authentication is required, the password to authenticate with - default: null - env: - - name: ARA_API_PASSWORD - ini: - - section: ara - key: api_password - api_insecure: - description: Can be enabled to ignore SSL certification of the API server - type: bool - default: false - env: - - name: ARA_API_INSECURE - ini: - - section: ara - key: api_insecure - api_timeout: - description: Timeout, in seconds, before giving up on HTTP requests - type: integer - default: 30 - env: - - name: ARA_API_TIMEOUT - ini: - - section: ara - key: api_timeout - argument_labels: - description: | - A list of CLI arguments that, if set, will be automatically applied to playbooks as labels. - Note that CLI arguments are not always named the same as how they are represented by Ansible. - For example, --limit is "subset", --user is "remote_user" but --check is "check". - type: list - default: - - remote_user - - check - - tags - - skip_tags - - subset - env: - - name: ARA_ARGUMENT_LABELS - ini: - - section: ara - key: argument_labels - default_labels: - description: A list of default labels that will be applied to playbooks - type: list - default: [] - env: - - name: ARA_DEFAULT_LABELS - ini: - - section: ara - key: default_labels - ignored_facts: - description: List of host facts that will not be saved by ARA - type: list - default: ["ansible_env"] - env: - - name: ARA_IGNORED_FACTS - ini: - - section: ara - key: ignored_facts - ignored_arguments: - description: List of Ansible arguments that will not be saved by ARA - type: list - default: ["extra_vars"] - env: - - name: ARA_IGNORED_ARGUMENTS - ini: - - section: ara - key: ignored_arguments - ignored_files: - description: List of patterns that will not be saved by ARA - type: list - default: [] - env: - - name: ARA_IGNORED_FILES - ini: - - section: ara - key: ignored_files - callback_threads: - description: - - The number of threads to use in API client thread pools - - When set to 0, no threading will be used (default) which is appropriate for usage with sqlite - - Using threads is recommended when the server is using MySQL or PostgreSQL - type: integer - default: 0 - env: - - name: ARA_CALLBACK_THREADS - ini: - - section: ara - key: callback_threads -""" - - -class CallbackModule(CallbackBase): - """ - Saves data from an Ansible run into a database - """ - - CALLBACK_VERSION = 2.0 - CALLBACK_TYPE = "awesome" - CALLBACK_NAME = "ara_default" - - def __init__(self): - super(CallbackModule, self).__init__() - self.log = logging.getLogger("ara.plugins.callback.default") - # These are configured in self.set_options - self.client = None - self.callback_threads = None - - self.ignored_facts = [] - self.ignored_arguments = [] - self.ignored_files = [] - - self.result = None - self.result_started = {} - self.result_ended = {} - self.task = None - self.play = None - self.playbook = None - self.stats = None - self.file_cache = {} - self.host_cache = {} - self.task_cache = {} - self.delegation_cache = {} - - def set_options(self, task_keys=None, var_options=None, direct=None): - super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct) - - self.argument_labels = self.get_option("argument_labels") - self.default_labels = self.get_option("default_labels") - self.ignored_facts = self.get_option("ignored_facts") - self.ignored_arguments = self.get_option("ignored_arguments") - self.ignored_files = self.get_option("ignored_files") - - client = self.get_option("api_client") - endpoint = self.get_option("api_server") - timeout = self.get_option("api_timeout") - username = self.get_option("api_username") - password = self.get_option("api_password") - insecure = self.get_option("api_insecure") - self.client = client_utils.get_client( - client=client, - endpoint=endpoint, - timeout=timeout, - username=username, - password=password, - verify=False if insecure else True, - ) - - # TODO: Consider un-hardcoding this and plumbing pool_maxsize to requests.adapters.HTTPAdapter. - # In the meantime default to 4 so we don't go above requests.adapters.DEFAULT_POOLSIZE. - # Otherwise we can hit "urllib3.connectionpool: Connection pool is full" - self.callback_threads = self.get_option("callback_threads") - if self.callback_threads > 4: - self.callback_threads = 4 - - def _submit_thread(self, threadpool, func, *args, **kwargs): - # Manages whether or not the function should be threaded to keep things DRY - if self.callback_threads: - # Pick from one of two thread pools (global or task) - threads = getattr(self, threadpool + "_threads") - threads.submit(func, *args, **kwargs) - else: - func(*args, **kwargs) - - def v2_playbook_on_start(self, playbook): - self.log.debug("v2_playbook_on_start") - - if self.callback_threads: - self.global_threads = ThreadPoolExecutor(max_workers=self.callback_threads) - self.log.debug("Global thread pool initialized with %s thread(s)" % self.callback_threads) - - content = None - - if playbook._file_name == "__adhoc_playbook__": - content = cli_options["module_name"] - if cli_options["module_args"]: - content = "{0}: {1}".format(content, cli_options["module_args"]) - path = "Ad-Hoc: {0}".format(content) - else: - path = os.path.abspath(playbook._file_name) - - # Potentially sanitize some user-specified keys - for argument in self.ignored_arguments: - if argument in cli_options: - self.log.debug("Ignoring argument: %s" % argument) - cli_options[argument] = "Not saved by ARA as configured by 'ignored_arguments'" - - # Retrieve and format CLI options for argument labels - argument_labels = [] - for label in self.argument_labels: - if label in cli_options: - # Some arguments are lists or tuples - if isinstance(cli_options[label], tuple) or isinstance(cli_options[label], list): - # Only label these if they're not empty - if cli_options[label]: - argument_labels.append("%s:%s" % (label, ",".join(cli_options[label]))) - # Some arguments are booleans - elif isinstance(cli_options[label], bool): - argument_labels.append("%s:%s" % (label, cli_options[label])) - # The rest can be printed as-is if there is something set - elif cli_options[label]: - argument_labels.append("%s:%s" % (label, cli_options[label])) - self.argument_labels = argument_labels - - # Create the playbook - self.playbook = self.client.post( - "/api/v1/playbooks", - ansible_version=ansible_version, - arguments=cli_options, - status="running", - path=path, - controller=socket.getfqdn(), - started=datetime.datetime.now(datetime.timezone.utc).isoformat(), - ) - - # Record the playbook file - self._submit_thread("global", self._get_or_create_file, path, content) - - return self.playbook - - def v2_playbook_on_play_start(self, play): - self.log.debug("v2_playbook_on_play_start") - self._end_task() - self._end_play() - - # Load variables to verify if there is anything relevant for ara - play_vars = play._variable_manager.get_vars(play=play)["vars"] - if "ara_playbook_name" in play_vars: - self._submit_thread("global", self._set_playbook_name, play_vars["ara_playbook_name"]) - - labels = self.default_labels + self.argument_labels - if "ara_playbook_labels" in play_vars: - # ara_playbook_labels can be supplied as a list inside a playbook - # but it might also be specified as a comma separated string when - # using extra-vars - if isinstance(play_vars["ara_playbook_labels"], list): - labels.extend(play_vars["ara_playbook_labels"]) - elif isinstance(play_vars["ara_playbook_labels"], str): - labels.extend(play_vars["ara_playbook_labels"].split(",")) - else: - raise TypeError("ara_playbook_labels must be a list or a comma-separated string") - if labels: - self._submit_thread("global", self._set_playbook_labels, labels) - - # Record all the files involved in the play - for path in play._loader._FILE_CACHE.keys(): - self._submit_thread("global", self._get_or_create_file, path) - - # Note: ansible-runner suffixes play UUIDs when running in serial so 34cff6f4-9f8e-6137-3461-000000000005 can - # end up being 34cff6f4-9f8e-6137-3461-000000000005_2. Remove anything beyond standard 36 character UUIDs. - # https://github.com/ansible-community/ara/issues/211 - # Create the play - self.play = self.client.post( - "/api/v1/plays", - name=play.name, - status="running", - uuid=play._uuid[:36], - playbook=self.playbook["id"], - started=datetime.datetime.now(datetime.timezone.utc).isoformat(), - ) - - return self.play - - def v2_playbook_on_handler_task_start(self, task): - self.log.debug("v2_playbook_on_handler_task_start") - # TODO: Why doesn't `v2_playbook_on_handler_task_start` have is_conditional ? - self.v2_playbook_on_task_start(task, False, handler=True) - - def v2_playbook_on_task_start(self, task, is_conditional, handler=False): - self.log.debug("v2_playbook_on_task_start") - self._end_task() - - if self.callback_threads: - self.task_threads = ThreadPoolExecutor(max_workers=self.callback_threads) - self.log.debug("Task thread pool initialized with %s thread(s)" % self.callback_threads) - - pathspec = task.get_path() - if pathspec: - path, lineno = pathspec.split(":", 1) - lineno = int(lineno) - else: - # Task doesn't have a path, default to "something" - path = self.playbook["path"] - lineno = 1 - - # Get task file - task_file = self._get_or_create_file(path) - - # Get task - self.task = self._get_or_create_task(task, task_file["id"], lineno, handler) - - return self.task - - def v2_runner_on_start(self, host, task): - # v2_runner_on_start was added in 2.8 so this doesn't get run for Ansible 2.7 and below. - self.result_started[host.get_name()] = datetime.datetime.now(datetime.timezone.utc).isoformat() - - def v2_runner_on_ok(self, result, **kwargs): - self._submit_thread("task", self._load_result, result, "ok", **kwargs) - - def v2_runner_on_unreachable(self, result, **kwargs): - self._submit_thread("task", self._load_result, result, "unreachable", **kwargs) - - def v2_runner_on_failed(self, result, **kwargs): - self._submit_thread("task", self._load_result, result, "failed", **kwargs) - - def v2_runner_on_skipped(self, result, **kwargs): - self._submit_thread("task", self._load_result, result, "skipped", **kwargs) - - def v2_runner_item_on_ok(self, result): - self._update_delegation_cache(result) - - def v2_runner_item_on_failed(self, result): - self._update_delegation_cache(result) - - def v2_runner_item_on_skipped(self, result): - pass - # result._task.delegate_to can end up being a variable from this hook, don't save it. - # https://github.com/ansible/ansible/issues/75339 - # self._update_delegation_cache(result) - - def v2_playbook_on_stats(self, stats): - self.log.debug("v2_playbook_on_stats") - self._end_task() - self._end_play() - self._load_stats(stats) - self._end_playbook(stats) - - def _end_task(self): - if self.task is not None: - self._submit_thread( - "task", - self.client.patch, - "/api/v1/tasks/%s" % self.task["id"], - status="completed", - ended=datetime.datetime.now(datetime.timezone.utc).isoformat(), - ) - if self.callback_threads: - # Flush threads before moving on to next task to make sure all results are saved - self.log.debug("waiting for task threads...") - self.task_threads.shutdown(wait=True) - self.task_threads = None - self.task = None - - def _end_play(self): - if self.play is not None: - self._submit_thread( - "global", - self.client.patch, - "/api/v1/plays/%s" % self.play["id"], - status="completed", - ended=datetime.datetime.now(datetime.timezone.utc).isoformat(), - ) - self.play = None - - def _end_playbook(self, stats): - status = "unknown" - if len(stats.failures) >= 1 or len(stats.dark) >= 1: - status = "failed" - else: - status = "completed" - - self._submit_thread( - "global", - self.client.patch, - "/api/v1/playbooks/%s" % self.playbook["id"], - status=status, - ended=datetime.datetime.now(datetime.timezone.utc).isoformat(), - ) - - if self.callback_threads: - self.log.debug("waiting for global threads...") - self.global_threads.shutdown(wait=True) - - def _set_playbook_name(self, name): - if self.playbook["name"] != name: - self.playbook = self.client.patch("/api/v1/playbooks/%s" % self.playbook["id"], name=name) - - def _set_playbook_labels(self, labels): - # Only update labels if our cache doesn't match - current_labels = [label["name"] for label in self.playbook["labels"]] - if sorted(current_labels) != sorted(labels): - self.log.debug("Updating playbook labels to match: %s" % ",".join(labels)) - self.playbook = self.client.patch("/api/v1/playbooks/%s" % self.playbook["id"], labels=labels) - - def _get_or_create_file(self, path, content=None): - if path not in self.file_cache: - self.log.debug("File not in cache, getting or creating: %s" % path) - for ignored_file_pattern in self.ignored_files: - if ignored_file_pattern in path: - self.log.debug("Ignoring file {1}, matched pattern: {0}".format(ignored_file_pattern, path)) - content = "Not saved by ARA as configured by 'ignored_files'" - if content is None: - try: - with open(path, "r") as fd: - content = fd.read() - except IOError as e: - self.log.error("Unable to open {0} for reading: {1}".format(path, str(e))) - content = """ARA was not able to read this file successfully. - Refer to the logs for more information""" - - self.file_cache[path] = self.client.post( - "/api/v1/files", playbook=self.playbook["id"], path=path, content=content - ) - - return self.file_cache[path] - - def _get_or_create_host(self, host): - # Note: The get_or_create is handled through the serializer of the API server. - if host not in self.host_cache: - self.log.debug("Host not in cache, getting or creating: %s" % host) - self.host_cache[host] = self.client.post("/api/v1/hosts", name=host, playbook=self.playbook["id"]) - return self.host_cache[host] - - def _get_or_create_task(self, task, file_id=None, lineno=None, handler=None): - # Note: The get_or_create is handled through the serializer of the API server. - task_uuid = str(task._uuid)[:36] - if task_uuid not in self.task_cache: - if None in (file_id, lineno, handler): - raise ValueError("file_id, lineno, and handler are required to create a task") - - self.log.debug("Task not in cache, getting or creating: %s" % task) - self.task_cache[task_uuid] = self.client.post( - "/api/v1/tasks", - name=task.get_name(), - status="running", - action=task.action, - play=self.play["id"], - playbook=self.playbook["id"], - file=file_id, - tags=task.tags, - lineno=lineno, - handler=handler, - started=datetime.datetime.now(datetime.timezone.utc).isoformat(), - ) - - return self.task_cache[task_uuid] - - def _update_delegation_cache(self, result): - # If the task is a loop and delegate_to is a variable, result._task.delegate_to can return the variable - # instead of it's value when using the v2_runner_on_* hooks. - # We're caching the actual host names here from v2_runner_item_on_* hooks. - # https://github.com/ansible/ansible/issues/75339 - if result._task.delegate_to: - task_uuid = str(result._task._uuid[:36]) - if task_uuid not in self.delegation_cache: - self.delegation_cache[task_uuid] = [] - self.delegation_cache[task_uuid].append(result._task.delegate_to) - - def _load_result(self, result, status, **kwargs): - """ - This method is called when an individual task instance on a single - host completes. It is responsible for logging a single result to the - database. - """ - hostname = result._host.get_name() - self.result_ended[hostname] = datetime.datetime.now(datetime.timezone.utc).isoformat() - - # Retrieve the host so we can associate the result to the host id - host = self._get_or_create_host(hostname) - - # If the task was delegated to another host, retrieve that too. - # Since a single task can be delegated to multiple hosts (ex: looping on a host group and using delegate_to) - # this must be a list of hosts. - delegated_to = [] - # The value of result._task.delegate_to doesn't get templated if the task was skipped - # https://github.com/ansible/ansible/issues/75339#issuecomment-888724838 - if result._task.delegate_to and status != "skipped": - task_uuid = str(result._task._uuid[:36]) - if task_uuid in self.delegation_cache: - for delegated in self.delegation_cache[task_uuid]: - delegated_to.append(self._get_or_create_host(delegated)) - else: - delegated_to.append(self._get_or_create_host(result._task.delegate_to)) - - # Retrieve the task so we can associate the result to the task id - task = self._get_or_create_task(result._task) - - results = strip_internal_keys(module_response_deepcopy(result._result)) - - # Round-trip through JSON to sort keys and convert Ansible types - # to standard types - try: - jsonified = json.dumps(results, cls=AnsibleJSONEncoder, ensure_ascii=False, sort_keys=True) - except TypeError: - # Python 3 can't sort non-homogenous keys. - # https://bugs.python.org/issue25457 - jsonified = json.dumps(results, cls=AnsibleJSONEncoder, ensure_ascii=False, sort_keys=False) - results = json.loads(jsonified) - - # Sanitize facts - if "ansible_facts" in results: - for fact in self.ignored_facts: - if fact in results["ansible_facts"]: - self.log.debug("Ignoring fact: %s" % fact) - results["ansible_facts"][fact] = "Not saved by ARA as configured by 'ignored_facts'" - - self.result = self.client.post( - "/api/v1/results", - playbook=self.playbook["id"], - task=task["id"], - host=host["id"], - delegated_to=[h["id"] for h in delegated_to], - play=task["play"], - content=results, - status=status, - started=self.result_started[hostname] if hostname in self.result_started else task["started"], - ended=self.result_ended[hostname], - changed=result._result.get("changed", False), - # Note: ignore_errors might be None instead of a boolean - ignore_errors=kwargs.get("ignore_errors", False) or False, - ) - - if task["action"] in ["setup", "gather_facts"] and "ansible_facts" in results: - self.client.patch("/api/v1/hosts/%s" % host["id"], facts=results["ansible_facts"]) - - def _load_stats(self, stats): - hosts = sorted(stats.processed.keys()) - for hostname in hosts: - host = self._get_or_create_host(hostname) - host_stats = stats.summarize(hostname) - - self._submit_thread( - "global", - self.client.patch, - "/api/v1/hosts/%s" % host["id"], - changed=host_stats["changed"], - unreachable=host_stats["unreachable"], - failed=host_stats["failures"], - ok=host_stats["ok"], - skipped=host_stats["skipped"], - ) diff --git a/plugins/callback/trace.py b/plugins/callback/trace.py deleted file mode 100644 index 13c7b51c..00000000 --- a/plugins/callback/trace.py +++ /dev/null @@ -1,168 +0,0 @@ -# Copyright 2021 Google LLC -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# version 3 as published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - - -DOCUMENTATION = ''' - name: trace - type: aggregate - short_description: write playbook output to Chrome's Trace Event Format file - description: - - This callback writes playbook output to Chrome Trace Event Format file. - author: Mark Hansen (@mhansen) - options: - trace_output_dir: - name: output dir - default: ./trace - description: Directory to write files to. - env: - - name: TRACE_OUTPUT_DIR - hide_task_arguments: - name: Hide the arguments for a task - default: False - description: Hide the arguments for a task - env: - - name: HIDE_TASK_ARGUMENTS - requirements: - - enable in configuration -''' - -import atexit -import json -import os -import time -from dataclasses import dataclass -from datetime import datetime -from typing import Dict, TextIO - -from ansible.plugins.callback import CallbackBase - - -class CallbackModule(CallbackBase): - """ - This callback traces execution time of tasks to Trace Event Format. - - This plugin makes use of the following environment variables: - TRACE_OUTPUT_DIR (optional): Directory to write JSON files to. - Default: ./trace - TRACE_HIDE_TASK_ARGUMENTS (optional): Hide the arguments for a task - Default: False - """ - - CALLBACK_VERSION = 2.0 - CALLBACK_TYPE = 'aggregate' - CALLBACK_NAME = 'trace' - CALLBACK_NEEDS_ENABLED = True - - def __init__(self): - super(CallbackModule, self).__init__() - - self._output_dir: str = os.getenv('TRACE_OUTPUT_DIR', os.path.join(os.path.expanduser('.'), 'trace')) - self._hide_task_arguments: str = os.getenv('TRACE_HIDE_TASK_ARGUMENTS', 'False').lower() - self._hosts: Dict[Host] = {} - self._next_pid: int = 1 - self._first: bool = True - self._start_date: str = datetime.now().isoformat() - self._output_file: str = 'trace-%s.json' % self._start_date - - if not os.path.exists(self._output_dir): - os.makedirs(self._output_dir) - output_file = os.path.join(self._output_dir, self._output_file) - self._f: TextIO = open(output_file, 'w') - self._f.write("[\n") - - atexit.register(self._end) - - def _write_event(self, e: Dict): - if not self._first: - self._f.write(",\n") - self._first = False - json.dump(e, self._f, sort_keys=True, indent=2) # sort for reproducibility - self._f.flush() - - def v2_runner_on_start(self, host, task): - uuid = task._uuid - name = task.get_name().strip() - - args = None - if not task.no_log and self._hide_task_arguments == 'false': - args = task.args - - host_uuid = host._uuid - if host_uuid not in self._hosts: - pid = self._next_pid - self._hosts[host_uuid] = Host(pid=pid, name=host.name) - self._next_pid += 1 - # https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#bookmark=id.iycbnb4z7i9g - self._write_event({ - "name": "process_name", - "pid": pid, - "cat": "process", - "ph": "M", - "args": { - "name": host.name, - }, - }) - - # See "Duration Events" in: - # https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#heading=h.nso4gcezn7n1 - self._write_event({ - "name": name, - "cat": "runner", - "ph": "B", # Begin - "ts": time.time_ns() / 1000, - "pid": self._hosts[host_uuid].pid, - "id": abs(hash(uuid)), - "args": { - "args": args, - "task": name, - "path": task.get_path(), - "host": host.name, - }, - }) - - - def _end_span(self, result, status: str): - task = result._task - uuid = task._uuid - # See "Duration Events" in: - # https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#heading=h.nso4gcezn7n1 - self._write_event({ - "name": task.get_name().strip(), - "cat": "runner", - "id": abs(hash(uuid)), - "ph": "E", # End - "ts": time.time_ns() / 1000, - "pid": self._hosts[result._host._uuid].pid, - "args": { - "status": status, - }, - }) - - def v2_runner_on_ok(self, result): - self._end_span(result, status="ok") - - def v2_runner_on_unreachable(self, result): - self._end_span(result, 'unreachable') - - def v2_runner_on_failed(self, result, ignore_errors=False): - self._end_span(result, status='failed') - - def v2_runner_on_skipped(self, result): - self._end_span(result, status='skipped') - - def _end(self): - self._f.write("\n]") - self._f.close() - -@dataclass -class Host: - name: str - pid: int