Allow tasks to notify a fqcn handler name (#68213)

* Allow tasks to notify a fqcn handler name

* Add tests. Fixes #68181

* Add changelog fragment

* Add test to ensure handlers are deduped properly with fqcn, role, and just handler names

* Add some docs about new special vars
This commit is contained in:
Matt Martz 2020-04-08 11:36:38 -05:00 committed by GitHub
parent 4f8e98d322
commit 087be1da50
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 67 additions and 18 deletions

View file

@ -0,0 +1,6 @@
bugfixes:
- Allow tasks to notify a fqcn handler name (https://github.com/ansible/ansible/issues/68181)
minor_changes:
- Add new magic variable ``ansible_collection`` that contains the collection name
- Add new magic variable ``ansible_role_name`` that contains the FQCN of the role
- Updates ``ansible_role_names``, ``ansible_play_role_names``, and ``ansible_dependent_role_names`` to include the FQCN

View file

@ -67,6 +67,12 @@ ansible_role_names
The names of the roles currently imported into the current play, or roles referenced as dependencies of the roles
imported into the current play.
ansible_role_name
The fully qualified collection role name, in the format of ``namespace.collection.role_name``
ansible_collection_name
The name of the collection the task that is executing is a part of. In the format of ``namespace.collection``
ansible_run_tags
Contents of the ``--tags`` CLI option, which specifies which tags will be included for the current run.

View file

@ -128,7 +128,9 @@ class Role(Base, Conditional, Taggable, CollectionSearch):
def __repr__(self):
return self.get_name()
def get_name(self):
def get_name(self, include_role_fqcn=True):
if include_role_fqcn:
return '.'.join(x for x in (self._role_collection, self._role_name) if x)
return self._role_name
@staticmethod
@ -155,8 +157,8 @@ class Role(Base, Conditional, Taggable, CollectionSearch):
params['from_include'] = from_include
hashed_params = hash_params(params)
if role_include.role in play.ROLE_CACHE:
for (entry, role_obj) in iteritems(play.ROLE_CACHE[role_include.role]):
if role_include.get_name() in play.ROLE_CACHE:
for (entry, role_obj) in iteritems(play.ROLE_CACHE[role_include.get_name()]):
if hashed_params == entry:
if parent_role:
role_obj.add_parent(parent_role)
@ -169,11 +171,11 @@ class Role(Base, Conditional, Taggable, CollectionSearch):
r = Role(play=play, from_files=from_files, from_include=from_include)
r._load_role_data(role_include, parent_role=parent_role)
if role_include.role not in play.ROLE_CACHE:
play.ROLE_CACHE[role_include.role] = dict()
if role_include.get_name() not in play.ROLE_CACHE:
play.ROLE_CACHE[role_include.get_name()] = dict()
# FIXME: how to handle cache keys for collection-based roles, since they're technically adjustable per task?
play.ROLE_CACHE[role_include.role][hashed_params] = r
play.ROLE_CACHE[role_include.get_name()][hashed_params] = r
return r
except RuntimeError:

View file

@ -231,3 +231,8 @@ class RoleDefinition(Base, Conditional, Taggable, CollectionSearch):
def get_role_path(self):
return self._role_path
def get_name(self, include_role_fqcn=True):
if include_role_fqcn:
return '.'.join(x for x in (self._role_collection, self.role) if x)
return self.role

View file

@ -111,16 +111,19 @@ class Task(Base, Conditional, Taggable, CollectionSearch):
path = "%s:%s" % (self._parent._play._ds._data_source, self._parent._play._ds._line_number)
return path
def get_name(self):
def get_name(self, include_role_fqcn=True):
''' return the name of the task '''
if self._role and self.name and ("%s : " % self._role._role_name) not in self.name:
return "%s : %s" % (self._role.get_name(), self.name)
if self._role:
role_name = self._role.get_name(include_role_fqcn=include_role_fqcn)
if self._role and self.name and role_name not in self.name:
return "%s : %s" % (role_name, self.name)
elif self.name:
return self.name
else:
if self._role:
return "%s : %s" % (self._role.get_name(), self.action)
return "%s : %s" % (role_name, self.action)
else:
return "%s" % (self.action,)

View file

@ -460,10 +460,13 @@ class StrategyBase:
# include the role name (if the handler is from a role). If that
# is not found, we resort to the simple name field, which doesn't
# have anything extra added to it.
if handler_task.name == handler_name:
return handler_task
else:
if handler_task.get_name() == handler_name:
candidates = (
handler_task.name,
handler_task.get_name(include_role_fqcn=False),
handler_task.get_name(include_role_fqcn=True),
)
if handler_name in candidates:
return handler_task
except (UndefinedError, AnsibleUndefinedVariable):
# We skip this handler due to the fact that it may be using
@ -705,7 +708,7 @@ class StrategyBase:
if original_task._role is not None and role_ran: # TODO: and original_task.action != 'include_role':?
# lookup the role in the ROLE_CACHE to make sure we're dealing
# with the correct object and mark it as executed
for (entry, role_obj) in iteritems(iterator._play.ROLE_CACHE[original_task._role._role_name]):
for (entry, role_obj) in iteritems(iterator._play.ROLE_CACHE[original_task._role.get_name()]):
if role_obj._uuid == original_task._role._uuid:
role_obj._had_task_run[original_host.name] = True

View file

@ -457,9 +457,9 @@ class VariableManager:
if play:
# This is a list of all role names of all dependencies for all roles for this play
dependency_role_names = list(set([d._role_name for r in play.roles for d in r.get_all_dependencies()]))
dependency_role_names = list(set([d.get_name() for r in play.roles for d in r.get_all_dependencies()]))
# This is a list of all role names of all roles for this play
play_role_names = [r._role_name for r in play.roles]
play_role_names = [r.get_name() for r in play.roles]
# ansible_role_names includes all role names, dependent or directly referenced by the play
variables['ansible_role_names'] = list(set(dependency_role_names + play_role_names))
@ -477,9 +477,11 @@ class VariableManager:
if task:
if task._role:
variables['role_name'] = task._role.get_name()
variables['role_name'] = task._role.get_name(include_role_fqcn=False)
variables['role_path'] = task._role._role_path
variables['role_uuid'] = text_type(task._role._uuid)
variables['ansible_collection_name'] = task._role._role_collection
variables['ansible_role_name'] = task._role.get_name()
if self._inventory is not None:
variables['groups'] = self._inventory.get_groups_dict()

View file

@ -0,0 +1,6 @@
# This handler should only be called 1 time, if it's called more than once
# this task should fail on subsequent executions
- name: test_fqcn_handler
set_fact:
handler_counter: '{{ handler_counter|int + 1 }}'
failed_when: handler_counter|int > 1

View file

@ -0,0 +1,2 @@
dependencies:
- testns.testcoll.common_handlers

View file

@ -0,0 +1,7 @@
- debug:
msg: Fire fqcn handler
changed_when: true
notify:
- 'testns.testcoll.common_handlers : test_fqcn_handler'
- 'common_handlers : test_fqcn_handler'
- 'test_fqcn_handler'

View file

@ -394,3 +394,10 @@
that:
- hostvars['dynamic_host_a'] is defined
- hostvars['dynamic_host_a'].connection_out.stdout == "localconn ran echo 'hello world'"
- name: Test FQCN handlers
hosts: testhost
vars:
handler_counter: 0
roles:
- testns.testcoll.test_fqcn_handlers