Skip to content

Commit 0af7a7e

Browse files
committed
Add a new ara_label Ansible action plugin
This action plugin gives the ability to manipulate playbook labels in ara from within ansible playbooks.
1 parent 191788c commit 0af7a7e

File tree

3 files changed

+248
-0
lines changed

3 files changed

+248
-0
lines changed

ara/plugins/action/ara_label.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# Copyright (c) 2025 The ARA Records Ansible authors
2+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
3+
4+
from ansible.playbook.play import Play
5+
from ansible.plugins.action import ActionBase
6+
7+
from ara.clients import utils as client_utils
8+
9+
DOCUMENTATION = """
10+
---
11+
module: ara_label
12+
short_description: Manages labels on playbooks recorded by ara
13+
version_added: "1.7.3"
14+
author: "David Moreau-Simard <[email protected]>"
15+
description:
16+
- Adds or removes labels on playbooks recorded by ara
17+
options:
18+
playbook_id:
19+
description:
20+
- id of the playbook to manage labels on
21+
- if not set, the module will use the ongoing playbook's id
22+
required: false
23+
labels:
24+
description:
25+
- A list of labels to add to (or remove from) the playbook
26+
required: true
27+
state:
28+
description:
29+
- Whether the labels should be added (present) or removed (absent)
30+
default: present
31+
32+
requirements:
33+
- "python >= 3.8"
34+
- "ara >= 1.7.3"
35+
"""
36+
37+
EXAMPLES = """
38+
- name: Add a static label to this playbook (the one that is running)
39+
# Note: By default Ansible will run this task on every host.
40+
# Consider the use case and declare 'run_once: true' when there is no need to
41+
# run this task more than once.
42+
# This might sound obvious but it avoids updating the same labels 100 times
43+
# if there are 100 hosts, incurring a performance penalty needlessly.
44+
run_once: true
45+
ara_label:
46+
state: present
47+
labels:
48+
- state:running
49+
50+
- name: Add dynamically templated labels to this playbook
51+
ara_label:
52+
state: present
53+
labels:
54+
- "git:{{ lookup('git -C {{ playbook_dir }} rev-parse HEAD') }}"
55+
- "os:{{ ansible_distribution }}-{{ ansible_distribution_version }}"
56+
57+
- name: Add labels to a specific playbook
58+
ara_label:
59+
state: present
60+
playbook_id: 1
61+
labels:
62+
- state:deployed
63+
64+
- name: Remove labels from the running playbook (if they exist)
65+
ara_label:
66+
state: absent
67+
labels:
68+
- state:running
69+
"""
70+
71+
RETURN = """
72+
playbook:
73+
description: ID of the playbook the data was recorded in
74+
returned: on success
75+
type: int
76+
sample: 1
77+
labels:
78+
description: an updated list of labels
79+
returned: on success
80+
type: list
81+
sample: ["dev","os:Fedora-41"]
82+
"""
83+
84+
85+
class ActionModule(ActionBase):
86+
"""Manages labels on a playbook recorded by ara"""
87+
88+
TRANSFERS_FILES = False
89+
# Note: BYPASS_HOST_LOOP functions like a forced "run_once" on a task
90+
# We considered setting this as the default but decided against it for now.
91+
# Discussion here: https://github.com/ansible-community/ara/pull/274
92+
# BYPASS_HOST_LOOP = True
93+
VALID_ARGS = frozenset(("state", "playbook_id", "labels"))
94+
95+
def __init__(self, *args, **kwargs):
96+
super(ActionModule, self).__init__(*args, **kwargs)
97+
self.client = client_utils.active_client()
98+
99+
# TODO: We should move code like this in some form of common util library
100+
# this is largely taken how the callback does it
101+
def _set_playbook_labels(self, playbook, labels):
102+
# Labels may not exceed 255 characters
103+
# https://github.com/ansible-community/ara/issues/185
104+
# https://github.com/ansible-community/ara/issues/265
105+
expected_labels = []
106+
for label in labels:
107+
if len(label) >= 255:
108+
label = label[:254]
109+
expected_labels.append(label)
110+
111+
changed = False
112+
current_labels = [label["name"] for label in playbook["labels"]]
113+
if sorted(current_labels) != sorted(expected_labels):
114+
playbook = self.client.patch("/api/v1/playbooks/%s" % playbook["id"], labels=expected_labels)
115+
changed = True
116+
117+
return changed, playbook["labels"]
118+
119+
def run(self, tmp=None, task_vars=None):
120+
if task_vars is None:
121+
task_vars = dict()
122+
123+
for arg in self._task.args:
124+
if arg not in self.VALID_ARGS:
125+
result = {"failed": True, "msg": "{0} is not a valid option.".format(arg)}
126+
return result
127+
128+
result = super(ActionModule, self).run(tmp, task_vars)
129+
130+
state = self._task.args.get("state", "present")
131+
playbook_id = self._task.args.get("playbook_id", None)
132+
labels = self._task.args.get("labels", None)
133+
134+
required = ["labels"]
135+
for parameter in required:
136+
if not self._task.args.get(parameter):
137+
result["failed"] = True
138+
result["msg"] = "Parameter '{0}' is required".format(parameter)
139+
return result
140+
141+
if playbook_id is None:
142+
# Retrieve the playbook id by working our way up from the task to find
143+
# the play uuid. Once we have the play uuid, we can find the playbook.
144+
parent = self._task
145+
while not isinstance(parent._parent._play, Play):
146+
parent = parent._parent
147+
148+
play = self.client.get("/api/v1/plays?uuid=%s" % parent._parent._play._uuid)
149+
playbook_id = play["results"][0]["playbook"]
150+
151+
playbook = self.client.get("/api/v1/playbooks/%s" % playbook_id)
152+
current_labels = [label["name"] for label in playbook["labels"]]
153+
154+
if state == "present":
155+
expected_labels = current_labels + labels
156+
elif state == "absent":
157+
expected_labels = current_labels
158+
for label in labels:
159+
if label in current_labels:
160+
expected_labels.remove(label)
161+
162+
try:
163+
changed, labels = self._set_playbook_labels(playbook, expected_labels)
164+
result["changed"] = changed
165+
result["labels"] = [label["name"] for label in labels]
166+
result["playbook_id"] = playbook_id
167+
if result["changed"]:
168+
result["msg"] = "ara playbook labels updated."
169+
else:
170+
result["msg"] = "ara playbook labels unchanged."
171+
except Exception as e:
172+
result["failed"] = True
173+
result["msg"] = "An error occurred when updating playbook labels: %s" % str(e)
174+
return result

doc/source/ansible-plugins-and-use-cases.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,48 @@ or through the environment variable ``ARA_ARGUMENT_LABELS``:
149149
150150
export ARA_ARGUMENT_LABELS=check,subset,tags
151151
152+
ara_label: managing labels on playbooks
153+
---------------------------------------
154+
155+
The ``ara_label`` Ansible action plugin can be enabled by :ref:`configuring Ansible <ansible-configuration>` with the
156+
``ANSIBLE_ACTION_PLUGINS`` environment variable or the ``action_plugins`` setting in an ``ansible.cfg`` file.
157+
158+
It provides another way to manage labels on playbooks recorded by ara from within your playbooks:
159+
160+
.. code-block:: yaml
161+
162+
- name: Add a static label to this playbook (the one that is running)
163+
# Note: By default Ansible will run this task on every host.
164+
# Consider the use case and declare 'run_once: true' when there is no need to
165+
# run this task more than once.
166+
# This might sound obvious but it avoids updating the same labels 100 times
167+
# if there are 100 hosts, incurring a performance penalty needlessly.
168+
run_once: true
169+
ara_label:
170+
state: present
171+
labels:
172+
- state:running
173+
174+
- name: Add dynamically templated labels to this playbook
175+
ara_label:
176+
state: present
177+
labels:
178+
- "git:{{ lookup('git -C {{ playbook_dir }} rev-parse HEAD') }}"
179+
- "os:{{ ansible_distribution }}-{{ ansible_distribution_version }}"
180+
181+
- name: Add labels to a specific playbook
182+
ara_label:
183+
state: present
184+
playbook_id: 1
185+
labels:
186+
- state:deployed
187+
188+
- name: Remove labels from the running playbook (if they exist)
189+
ara_label:
190+
state: absent
191+
labels:
192+
- state:running
193+
152194
ara_api: free-form API queries
153195
------------------------------
154196

tests/integration/roles/smoke-tests/tasks/ara-ops.yaml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,35 @@
174174
assert:
175175
that:
176176
- invalid.failed
177+
178+
- name: Add a static label to this playbook (the one that is running)
179+
# Note: By default Ansible will run this task on every host.
180+
# Consider the use case and declare 'run_once: true' when there is no need to
181+
# run this task more than once.
182+
# This might sound obvious but it avoids updating the same labels 100 times
183+
# if there are 100 hosts, incurring a performance penalty needlessly.
184+
run_once: true
185+
ara_label:
186+
state: present
187+
labels:
188+
- "state:running"
189+
190+
- name: Add dynamically templated labels to this playbook
191+
ara_label:
192+
state: present
193+
labels:
194+
- "git:{{ lookup('pipe', 'git -C {{ playbook_dir }} rev-parse HEAD') }}"
195+
- "os:{{ ansible_distribution }}-{{ ansible_distribution_version }}"
196+
197+
- name: Add labels to a specific playbook
198+
ara_label:
199+
state: present
200+
playbook_id: 1
201+
labels:
202+
- "state:deployed"
203+
204+
- name: Remove labels from the running playbook (if they exist)
205+
ara_label:
206+
state: absent
207+
labels:
208+
- "state:running"

0 commit comments

Comments
 (0)