summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/source/developer/ansible.rst71
-rw-r--r--doc/source/howtos/nodepool_static.rst32
-rw-r--r--doc/source/installation.rst14
-rw-r--r--doc/source/job-content.rst5
-rw-r--r--doc/source/tutorials/keycloak.rst6
-rw-r--r--playbooks/tutorial/admin.yaml25
-rw-r--r--releasenotes/notes/deprecate-ansible-2-4c22db35d3c6c765.yaml5
-rw-r--r--tests/fixtures/config/ansible-callbacks/git/common-config/playbooks/callback.yaml6
-rw-r--r--tests/fixtures/config/ansible-callbacks/git/common-config/playbooks/callback_plugins/test_callback.py15
-rw-r--r--tests/fixtures/config/ansible-callbacks/main.yaml1
-rw-r--r--tests/fixtures/config/ansible-callbacks/main28.yaml7
-rw-r--r--tests/fixtures/config/ansible-callbacks/main29.yaml7
-rw-r--r--tests/fixtures/config/ansible-callbacks/main5.yaml7
-rw-r--r--tests/fixtures/config/ansible-versions/git/common-config/zuul.yaml2
-rw-r--r--tests/fixtures/config/ansible/git/org_ansible/playbooks/hello-ansible.yaml4
-rw-r--r--tests/fixtures/config/ansible/main.yaml1
-rw-r--r--tests/fixtures/config/ansible/main28.yaml11
-rw-r--r--tests/fixtures/config/ansible/main29.yaml11
-rw-r--r--tests/fixtures/config/ansible/main5.yaml11
-rw-r--r--tests/fixtures/config/executor-facts/git/org_project/playbooks/datetime-fact.yaml5
-rw-r--r--tests/fixtures/config/executor-facts/main.yaml1
-rw-r--r--tests/fixtures/config/executor-facts/main28.yaml9
-rw-r--r--tests/fixtures/config/executor-facts/main29.yaml9
-rw-r--r--tests/fixtures/config/executor-facts/main5.yaml9
-rw-r--r--tests/fixtures/config/remote-zuul-stream/git/org_project/playbooks/command.yaml14
-rw-r--r--tests/remote/test_remote_zuul_stream.py48
-rw-r--r--tests/unit/test_executor.py54
-rw-r--r--tests/unit/test_inventory.py6
-rw-r--r--tests/unit/test_scheduler.py11
-rw-r--r--tests/unit/test_v3.py19
-rw-r--r--tests/unit/test_web.py4
-rw-r--r--zuul/ansible/base/callback/zuul_stream.py34
-rw-r--r--zuul/executor/server.py1
-rw-r--r--zuul/lib/ansible-config.conf4
34 files changed, 404 insertions, 65 deletions
diff --git a/doc/source/developer/ansible.rst b/doc/source/developer/ansible.rst
index 415c47df7..c3135debe 100644
--- a/doc/source/developer/ansible.rst
+++ b/doc/source/developer/ansible.rst
@@ -4,19 +4,11 @@ Ansible Integration
Zuul contains Ansible modules and plugins to control the execution of Ansible
Job content.
-Build Log Support
------------------
+Zuul provides realtime build log streaming to end users so that users
+can watch long-running jobs in progress.
-Zuul provides realtime build log streaming to end users so that users can
-watch long-running jobs in progress. As jobs may be written that execute a
-shell script that could run for a long time, additional effort is expended
-to stream stdout and stderr of shell tasks as they happen rather than waiting
-for the command to finish.
-
-Zuul contains a modified version of the :ansible:module:`command`
-that starts a log streaming daemon on the build node.
-
-.. automodule:: zuul.ansible.base.library.command
+Streaming job output
+--------------------
All jobs run with the :py:mod:`zuul.ansible.base.callback.zuul_stream` callback
plugin enabled, which writes the build log to a file so that the
@@ -35,10 +27,55 @@ exposes that log stream over a websocket connection as part of
In addition to real-time streaming, Zuul also installs another callback module,
:py:mod:`zuul.ansible.base.callback.zuul_json.CallbackModule` that collects all
of the information about a given run into a json file which is written to the
-work dir so that it can be published along with build logs. Since the streaming
-log is by necessity a single text stream, choices have to be made for
-readability about what data is shown and what is not shown. The json log file
-is intended to allow for a richer more interactive set of data to be displayed
-to the user.
+work dir so that it can be published along with build logs.
.. autoclass:: zuul.ansible.base.callback.zuul_json.CallbackModule
+
+Since the streaming log is by necessity a single text stream, choices
+have to be made for readability about what data is shown and what is
+not shown. The json log file is intended to allow for a richer more
+interactive set of data to be displayed to the user.
+
+.. _zuul_console_streaming:
+
+Capturing live command output
+-----------------------------
+
+As jobs may execute long-running shell scripts or other commands,
+additional effort is expended to stream ``stdout`` and ``stderr`` of
+shell tasks as they happen rather than waiting for the command to
+finish.
+
+The global job configuration should run the ``zuul_console`` task as a
+very early prerequisite step.
+
+.. automodule:: zuul.ansible.base.library.zuul_console
+
+This will start a daemon that listens on TCP port 19885 on the testing
+node. This daemon can be queried to stream back the output of shell
+tasks as described below.
+
+Zuul contains a modified version of Ansible's
+:ansible:module:`command` module that overrides the default
+implementation.
+
+.. automodule:: zuul.ansible.base.library.command
+
+This library will capture the output of the running
+command and write it to a temporary file on the host the command is
+running on. These files are named in the format
+``/tmp/console-<uuid>-<task_id>-<host>.log``
+
+The ``zuul_stream`` callback mentioned above will send a request to
+the remote ``zuul_console`` daemon, providing the uuid and task id of
+the task it is currently processing. The ``zuul_console`` daemon will
+then read the logfile from disk and stream the data back as it
+appears, which ``zuul_stream`` will then present as described above.
+
+The ``zuul_stream`` callback will indicate to the ``zuul_console``
+daemon when it has finished reading the task, which prompts the remote
+side to remove the temporary streaming output files. In some cases,
+aborting the Ansible process may not give the ``zuul_stream`` callback
+the chance to send this notice, leaking the temporary files. If nodes
+are ephemeral this makes little difference, but these files may be
+visible on static nodes.
diff --git a/doc/source/howtos/nodepool_static.rst b/doc/source/howtos/nodepool_static.rst
index ff2d35d6a..c10672e7b 100644
--- a/doc/source/howtos/nodepool_static.rst
+++ b/doc/source/howtos/nodepool_static.rst
@@ -15,9 +15,9 @@ the following requirements:
* Must be reachable by Zuul executors and have SSH access enabled.
* Must have a user that Zuul can use for SSH.
-* Must have Python 2 installed for Ansible.
-* Must be reachable by Zuul executors over TCP port 19885 (console log
- streaming).
+* Must have an Ansible supported Python installed
+* Must be reachable by Zuul executors over TCP port 19885 for console
+ log streaming. See :ref:`nodepool_console_streaming`
When setting up your nodepool.yaml file, you will need the host keys
for each node for the ``host-key`` value. This can be obtained with
@@ -40,7 +40,7 @@ nodes. Place this file in ``/etc/nodepool/nodepool.yaml``:
- host: localhost
labels:
- - name: ubuntu-xenial
+ - name: ubuntu-jammy
providers:
- name: static-vms
@@ -49,14 +49,34 @@ nodes. Place this file in ``/etc/nodepool/nodepool.yaml``:
- name: main
nodes:
- name: 192.168.1.10
- labels: ubuntu-xenial
+ labels: ubuntu-jammy
host-key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGXqY02bdYqg1BcIf2x08zs60rS6XhlBSQ4qE47o5gb"
username: zuul
- name: 192.168.1.11
- labels: ubuntu-xenial
+ labels: ubuntu-jammy
host-key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGXqY02bdYqg1BcIf2x08zs60rS6XhlBSQ5sE47o5gc"
username: zuul
EOF"
Make sure that ``username``, ``host-key``, IP addresses and label names are
customized for your environment.
+
+.. _nodepool_console_streaming:
+
+Log streaming
+-------------
+
+The log streaming service enables Zuul to show the live status of
+long-running ``shell`` or ``command`` tasks. The server side is setup
+by the ``zuul_console:`` task built-in to Zuul's Ansible installation.
+The executor requires the ability to communicate with the job nodes on
+port 19885 for this to work.
+
+The log streaming service may leave files on the static node in the
+format ``/tmp/console-<uuid>-<task_id>-<host>.log`` if jobs are
+interrupted. These may be safely removed after a short period of
+inactivity with a command such as
+
+.. code-block:: shell
+
+ find /tmp -maxdepth 1 -name 'console-*-*-<host>.log' -mtime +2 -delete
diff --git a/doc/source/installation.rst b/doc/source/installation.rst
index a9a526f13..17665ca76 100644
--- a/doc/source/installation.rst
+++ b/doc/source/installation.rst
@@ -10,11 +10,15 @@ Nodepool
~~~~~~~~
In order to run all but the simplest jobs, Zuul uses a companion
-program, Nodepool, to supply the nodes (whether dynamic cloud
-instances or static hardware) used by jobs. Before starting Zuul,
-ensure you have Nodepool installed and any images you require built.
-Zuul only makes one requirement of these nodes: that it be able to log
-in given a username and ssh private key.
+program `Nodepool <https://opendev.org/zuul/nodepool>`__ to supply the
+nodes (whether dynamic cloud instances or static hardware) used by
+jobs. Before starting Zuul, ensure you have Nodepool installed and
+any images you require built.
+
+Zuul must be able to log into the nodes provisioned by Nodepool with a
+given username and SSH private key. Executors should also be able to
+talk to nodes on TCP port 19885 for log streaming; see
+:ref:`nodepool_console_streaming`.
ZooKeeper
~~~~~~~~~
diff --git a/doc/source/job-content.rst b/doc/source/job-content.rst
index 9b1059502..75044cf1c 100644
--- a/doc/source/job-content.rst
+++ b/doc/source/job-content.rst
@@ -332,6 +332,11 @@ of item.
connectivity issues then previous attempts may have been cancelled,
and this value will be greater than 1.
+ .. var:: ansible_version
+
+ The version of the Ansible community package release used for executing
+ the job.
+
.. var:: project
The item's project. This is a data structure with the following
diff --git a/doc/source/tutorials/keycloak.rst b/doc/source/tutorials/keycloak.rst
index 5242a4f05..896f35479 100644
--- a/doc/source/tutorials/keycloak.rst
+++ b/doc/source/tutorials/keycloak.rst
@@ -46,14 +46,14 @@ that we can update Zuul's configuration to add authentication.
.. code-block:: shell
cd zuul/doc/source/examples
- sudo -E docker-compose-compose -p zuul-tutorial down
+ sudo -E docker-compose -p zuul-tutorial stop
Restart the containers with a new Zuul configuration.
.. code-block:: shell
cd zuul/doc/source/examples
- ZUUL_TUTORIAL_CONFIG="./keycloak/etc_zuul/" sudo -E docker-compose-compose -p zuul-tutorial up -d
+ ZUUL_TUTORIAL_CONFIG="./keycloak/etc_zuul/" sudo -E docker-compose -p zuul-tutorial up -d
This tells docker-compose to use these Zuul `config files
<https://opendev.org/zuul/zuul/src/branch/master/doc/source/examples/keycloak>`_.
@@ -67,7 +67,7 @@ with this command:
.. code-block:: shell
cd zuul/doc/source/examples/keycloak
- sudo -E docker-compose-compose -p zuul-tutorial-keycloak up -d
+ sudo -E docker-compose -p zuul-tutorial-keycloak up -d
Once Keycloak is running, you can visit the web interface at
http://localhost:8082/
diff --git a/playbooks/tutorial/admin.yaml b/playbooks/tutorial/admin.yaml
index 9b36069e7..92d2b6d1f 100644
--- a/playbooks/tutorial/admin.yaml
+++ b/playbooks/tutorial/admin.yaml
@@ -2,13 +2,13 @@
- name: Run docker-compose down
when: not local
shell:
- cmd: docker-compose -p zuul-tutorial down
+ cmd: docker-compose -p zuul-tutorial stop
chdir: src/opendev.org/zuul/zuul/doc/source/examples
- name: Run docker-compose down
when: local
shell:
- cmd: docker-compose -p zuul-tutorial down
+ cmd: docker-compose -p zuul-tutorial stop
chdir: ../../doc/source/examples
# Restart with the new config
@@ -55,3 +55,24 @@
until: result.status == 200 and result.json["zuul_version"] is defined
changed_when: false
+- name: Verify Keycloak authentication is available
+ uri:
+ url: http://localhost:9000/api/tenant/example-tenant/info
+ method: GET
+ return_content: true
+ status_code: 200
+ body_format: json
+ register: result
+ failed_when: result.json["info"]["capabilities"]["auth"]["realms"]["zuul-demo"]["authority"] != "http://keycloak:8082/realms/zuul-demo"
+ changed_when: false
+
+- name: Verify that old builds are available
+ uri:
+ url: "http://localhost:9000/api/tenant/example-tenant/builds"
+ method: GET
+ return_content: true
+ status_code: 200
+ body_format: json
+ register: result
+ failed_when: "result.json | length < 4"
+ changed_when: false
diff --git a/releasenotes/notes/deprecate-ansible-2-4c22db35d3c6c765.yaml b/releasenotes/notes/deprecate-ansible-2-4c22db35d3c6c765.yaml
new file mode 100644
index 000000000..09a0a128c
--- /dev/null
+++ b/releasenotes/notes/deprecate-ansible-2-4c22db35d3c6c765.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+ - |
+ Ansible versions 2.8 and 2.9 are now deprecated in Zuul since they
+ are both unmaintaned. Ansible 5 is now the default version in Zuul.
diff --git a/tests/fixtures/config/ansible-callbacks/git/common-config/playbooks/callback.yaml b/tests/fixtures/config/ansible-callbacks/git/common-config/playbooks/callback.yaml
index 50bbbbfc5..13ddac988 100644
--- a/tests/fixtures/config/ansible-callbacks/git/common-config/playbooks/callback.yaml
+++ b/tests/fixtures/config/ansible-callbacks/git/common-config/playbooks/callback.yaml
@@ -1,4 +1,8 @@
- hosts: localhost
- gather_facts: smart
+ gather_facts: false
tasks:
- command: echo test
+
+ - name: Echo ansible version.
+ debug:
+ msg: Ansible version={{ ansible_version.major }}.{{ ansible_version.minor }}
diff --git a/tests/fixtures/config/ansible-callbacks/git/common-config/playbooks/callback_plugins/test_callback.py b/tests/fixtures/config/ansible-callbacks/git/common-config/playbooks/callback_plugins/test_callback.py
index 39ff7cd49..2597370bc 100644
--- a/tests/fixtures/config/ansible-callbacks/git/common-config/playbooks/callback_plugins/test_callback.py
+++ b/tests/fixtures/config/ansible-callbacks/git/common-config/playbooks/callback_plugins/test_callback.py
@@ -15,17 +15,20 @@ DOCUMENTATION = '''
class CallbackModule(CallbackBase):
- CALLBACK_VERSION = 1.0
+ """
+ test callback
+ """
+ CALLBACK_VERSION = 2.0
CALLBACK_NEEDS_WHITELIST = True
+ # aggregate means we can be loaded and not be the stdout plugin
+ CALLBACK_TYPE = 'aggregate'
+ CALLBACK_NAME = 'test_callback'
def __init__(self):
super(CallbackModule, self).__init__()
- 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)
-
+ def set_options(self, *args, **kw):
+ super(CallbackModule, self).set_options(*args, **kw)
self.file_name = self.get_option('file_name')
def v2_on_any(self, *args, **kwargs):
diff --git a/tests/fixtures/config/ansible-callbacks/main.yaml b/tests/fixtures/config/ansible-callbacks/main.yaml
index 9d01f542f..1e5247e4a 100644
--- a/tests/fixtures/config/ansible-callbacks/main.yaml
+++ b/tests/fixtures/config/ansible-callbacks/main.yaml
@@ -1,5 +1,6 @@
- tenant:
name: tenant-one
+ default-ansible-version: SETME
source:
gerrit:
config-projects:
diff --git a/tests/fixtures/config/ansible-callbacks/main28.yaml b/tests/fixtures/config/ansible-callbacks/main28.yaml
new file mode 100644
index 000000000..371710b4f
--- /dev/null
+++ b/tests/fixtures/config/ansible-callbacks/main28.yaml
@@ -0,0 +1,7 @@
+- tenant:
+ name: tenant-one
+ default-ansible-version: '2.8'
+ source:
+ gerrit:
+ config-projects:
+ - common-config
diff --git a/tests/fixtures/config/ansible-callbacks/main29.yaml b/tests/fixtures/config/ansible-callbacks/main29.yaml
new file mode 100644
index 000000000..b127139a9
--- /dev/null
+++ b/tests/fixtures/config/ansible-callbacks/main29.yaml
@@ -0,0 +1,7 @@
+- tenant:
+ name: tenant-one
+ default-ansible-version: '2.9'
+ source:
+ gerrit:
+ config-projects:
+ - common-config
diff --git a/tests/fixtures/config/ansible-callbacks/main5.yaml b/tests/fixtures/config/ansible-callbacks/main5.yaml
new file mode 100644
index 000000000..5efc12339
--- /dev/null
+++ b/tests/fixtures/config/ansible-callbacks/main5.yaml
@@ -0,0 +1,7 @@
+- tenant:
+ name: tenant-one
+ default-ansible-version: '5'
+ source:
+ gerrit:
+ config-projects:
+ - common-config
diff --git a/tests/fixtures/config/ansible-versions/git/common-config/zuul.yaml b/tests/fixtures/config/ansible-versions/git/common-config/zuul.yaml
index 91c8d6bca..3a34a1d86 100644
--- a/tests/fixtures/config/ansible-versions/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/ansible-versions/git/common-config/zuul.yaml
@@ -25,7 +25,7 @@
parent: ansible-version
vars:
test_ansible_version_major: 2
- test_ansible_version_minor: 9
+ test_ansible_version_minor: 12
# This job is used by a test case specifying a different ansible version in
# zuul.conf
diff --git a/tests/fixtures/config/ansible/git/org_ansible/playbooks/hello-ansible.yaml b/tests/fixtures/config/ansible/git/org_ansible/playbooks/hello-ansible.yaml
index 17ddc1661..d0458c710 100644
--- a/tests/fixtures/config/ansible/git/org_ansible/playbooks/hello-ansible.yaml
+++ b/tests/fixtures/config/ansible/git/org_ansible/playbooks/hello-ansible.yaml
@@ -3,3 +3,7 @@
- name: hello
debug:
msg: hello ansible
+
+ - name: Echo ansible version.
+ debug:
+ msg: Ansible version={{ ansible_version.major }}.{{ ansible_version.minor }}
diff --git a/tests/fixtures/config/ansible/main.yaml b/tests/fixtures/config/ansible/main.yaml
index 94e7aa78c..473bb5ef8 100644
--- a/tests/fixtures/config/ansible/main.yaml
+++ b/tests/fixtures/config/ansible/main.yaml
@@ -1,5 +1,6 @@
- tenant:
name: tenant-one
+ default-ansible-version: SETME
source:
gerrit:
config-projects:
diff --git a/tests/fixtures/config/ansible/main28.yaml b/tests/fixtures/config/ansible/main28.yaml
new file mode 100644
index 000000000..f2add49c7
--- /dev/null
+++ b/tests/fixtures/config/ansible/main28.yaml
@@ -0,0 +1,11 @@
+- tenant:
+ name: tenant-one
+ default-ansible-version: '2.8'
+ source:
+ gerrit:
+ config-projects:
+ - common-config
+ untrusted-projects:
+ - org/project
+ - bare-role
+ - org/ansible
diff --git a/tests/fixtures/config/ansible/main29.yaml b/tests/fixtures/config/ansible/main29.yaml
new file mode 100644
index 000000000..758292950
--- /dev/null
+++ b/tests/fixtures/config/ansible/main29.yaml
@@ -0,0 +1,11 @@
+- tenant:
+ name: tenant-one
+ default-ansible-version: '2.9'
+ source:
+ gerrit:
+ config-projects:
+ - common-config
+ untrusted-projects:
+ - org/project
+ - bare-role
+ - org/ansible
diff --git a/tests/fixtures/config/ansible/main5.yaml b/tests/fixtures/config/ansible/main5.yaml
new file mode 100644
index 000000000..b2364e80b
--- /dev/null
+++ b/tests/fixtures/config/ansible/main5.yaml
@@ -0,0 +1,11 @@
+- tenant:
+ name: tenant-one
+ default-ansible-version: '5'
+ source:
+ gerrit:
+ config-projects:
+ - common-config
+ untrusted-projects:
+ - org/project
+ - bare-role
+ - org/ansible
diff --git a/tests/fixtures/config/executor-facts/git/org_project/playbooks/datetime-fact.yaml b/tests/fixtures/config/executor-facts/git/org_project/playbooks/datetime-fact.yaml
index 300dfa5f0..53819aa00 100644
--- a/tests/fixtures/config/executor-facts/git/org_project/playbooks/datetime-fact.yaml
+++ b/tests/fixtures/config/executor-facts/git/org_project/playbooks/datetime-fact.yaml
@@ -1,5 +1,5 @@
- hosts: localhost
- gather_facts: smart
+ gather_facts: no
tasks:
- debug:
var: date_time
@@ -9,3 +9,6 @@
var: ansible_date_time
- assert:
that: ansible_date_time is not defined
+ - name: Echo ansible version
+ debug:
+ msg: Ansible version={{ ansible_version.major }}.{{ ansible_version.minor }}
diff --git a/tests/fixtures/config/executor-facts/main.yaml b/tests/fixtures/config/executor-facts/main.yaml
index 208e274b1..37c9dd4fc 100644
--- a/tests/fixtures/config/executor-facts/main.yaml
+++ b/tests/fixtures/config/executor-facts/main.yaml
@@ -1,5 +1,6 @@
- tenant:
name: tenant-one
+ default-ansible-version: SETME
source:
gerrit:
config-projects:
diff --git a/tests/fixtures/config/executor-facts/main28.yaml b/tests/fixtures/config/executor-facts/main28.yaml
new file mode 100644
index 000000000..686899bf8
--- /dev/null
+++ b/tests/fixtures/config/executor-facts/main28.yaml
@@ -0,0 +1,9 @@
+- tenant:
+ name: tenant-one
+ default-ansible-version: '2.8'
+ source:
+ gerrit:
+ config-projects:
+ - common-config
+ untrusted-projects:
+ - org/project
diff --git a/tests/fixtures/config/executor-facts/main29.yaml b/tests/fixtures/config/executor-facts/main29.yaml
new file mode 100644
index 000000000..df934ff22
--- /dev/null
+++ b/tests/fixtures/config/executor-facts/main29.yaml
@@ -0,0 +1,9 @@
+- tenant:
+ name: tenant-one
+ default-ansible-version: '2.9'
+ source:
+ gerrit:
+ config-projects:
+ - common-config
+ untrusted-projects:
+ - org/project
diff --git a/tests/fixtures/config/executor-facts/main5.yaml b/tests/fixtures/config/executor-facts/main5.yaml
new file mode 100644
index 000000000..55d9d10c0
--- /dev/null
+++ b/tests/fixtures/config/executor-facts/main5.yaml
@@ -0,0 +1,9 @@
+- tenant:
+ name: tenant-one
+ default-ansible-version: '5'
+ source:
+ gerrit:
+ config-projects:
+ - common-config
+ untrusted-projects:
+ - org/project
diff --git a/tests/fixtures/config/remote-zuul-stream/git/org_project/playbooks/command.yaml b/tests/fixtures/config/remote-zuul-stream/git/org_project/playbooks/command.yaml
index d737a1a9b..539db80b7 100644
--- a/tests/fixtures/config/remote-zuul-stream/git/org_project/playbooks/command.yaml
+++ b/tests/fixtures/config/remote-zuul-stream/git/org_project/playbooks/command.yaml
@@ -114,3 +114,17 @@
- name: Command Not Found
command: command-not-found
failed_when: false
+
+- hosts: compute1
+ tasks:
+
+ - name: Debug raw variable in msg
+ debug:
+ msg: '{{ ansible_version }}'
+
+ - name: Debug raw variable in a loop
+ debug:
+ msg: '{{ ansible_version }}'
+ loop:
+ - 1
+ - 2
diff --git a/tests/remote/test_remote_zuul_stream.py b/tests/remote/test_remote_zuul_stream.py
index 1c705127e..225c88e96 100644
--- a/tests/remote/test_remote_zuul_stream.py
+++ b/tests/remote/test_remote_zuul_stream.py
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+import io
+import logging
import os
import re
import textwrap
@@ -31,6 +33,12 @@ class FunctionalZuulStreamMixIn:
self.executor_server.log_console_port = self.log_console_port
self.wait_timeout = 180
self.fake_nodepool.remote_ansible = True
+ # This catches the Ansible output; rather than the callback
+ # output captured in the job log. For example if the callback
+ # fails, there will be an error output in this stream.
+ self.logger = logging.getLogger('zuul.AnsibleJob')
+ self.console_output = io.StringIO()
+ self.logger.addHandler(logging.StreamHandler(self.console_output))
ansible_remote = os.environ.get('ZUUL_REMOTE_IPV4')
self.assertIsNotNone(ansible_remote)
@@ -92,14 +100,20 @@ class FunctionalZuulStreamMixIn:
with open(path) as f:
return f.read()
- def assertLogLine(self, line, log):
- pattern = (r'^\d\d\d\d-\d\d-\d\d \d\d:\d\d\:\d\d\.\d\d\d\d\d\d \| %s$'
- % line)
+ def _assertLogLine(self, line, log, full_match=True):
+ pattern = (r'^\d\d\d\d-\d\d-\d\d \d\d:\d\d\:\d\d\.\d\d\d\d\d\d \| %s%s'
+ % (line, '$' if full_match else ''))
log_re = re.compile(pattern, re.MULTILINE)
m = log_re.search(log)
if m is None:
raise Exception("'%s' not found in log" % (line,))
+ def assertLogLineStartsWith(self, line, log):
+ self._assertLogLine(line, log, full_match=False)
+
+ def assertLogLine(self, line, log):
+ self._assertLogLine(line, log, full_match=True)
+
def _getLogTime(self, line, log):
pattern = (r'^(\d\d\d\d-\d\d-\d\d \d\d:\d\d\:\d\d\.\d\d\d\d\d\d)'
r' \| %s\n'
@@ -120,7 +134,21 @@ class FunctionalZuulStreamMixIn:
build = self.history[-1]
self.assertEqual(build.result, 'SUCCESS')
+ console_output = self.console_output.getvalue()
+ # This should be generic enough to match any callback
+ # plugin failures, which look something like
+ #
+ # [WARNING]: Failure using method (v2_runner_on_ok) in \
+ # callback plugin
+ # (<ansible.plugins.callback.zuul_stream.CallbackModule object at'
+ # 0x7f89f72a20b0>): 'dict' object has no attribute 'startswith'"
+ # Callback Exception:
+ # ...
+ #
+ self.assertNotIn('[WARNING]: Failure using method', console_output)
+
text = self._get_job_output(build)
+
self.assertLogLine(
r'RUN START: \[untrusted : review.example.com/org/project/'
r'playbooks/command.yaml@master\]', text)
@@ -186,6 +214,20 @@ class FunctionalZuulStreamMixIn:
self.assertLess((time2 - time1) / timedelta(milliseconds=1),
9000)
+ # This is from the debug: msg='{{ ansible_version }}'
+ # testing raw variable output. To make it version
+ # agnostic, match just the start of
+ # compute1 | ok: {'string': '2.9.27'...
+
+ # NOTE(ianw) 2022-08-24 : I don't know why the callback
+ # for debug: msg= doesn't put the hostname first like
+ # other output. Undetermined if bug or feature.
+ self.assertLogLineStartsWith(
+ r"""\{'string': '\d.""", text)
+ # ... handling loops is a different path, and that does
+ self.assertLogLineStartsWith(
+ r"""compute1 \| ok: \{'string': '\d.""", text)
+
def test_module_exception(self):
job = self._run_job('module_failure_exception')
with self.jobLog(job):
diff --git a/tests/unit/test_executor.py b/tests/unit/test_executor.py
index d18cf4448..6296ebe59 100644
--- a/tests/unit/test_executor.py
+++ b/tests/unit/test_executor.py
@@ -838,8 +838,10 @@ class TestLineMapping(AnsibleZuulTestCase):
)
-class TestExecutorFacts(AnsibleZuulTestCase):
+class ExecutorFactsMixin:
+ # These should be overridden in child classes.
tenant_config_file = 'config/executor-facts/main.yaml'
+ ansible_major_minor = 'X.Y'
def _get_file(self, build, path):
p = os.path.join(build.jobdir.root, path)
@@ -861,12 +863,34 @@ class TestExecutorFacts(AnsibleZuulTestCase):
date_time = \
j[0]['plays'][0]['tasks'][0]['hosts']['localhost']['date_time']
self.assertEqual(18, len(date_time))
+ build = self.getJobFromHistory('datetime-fact', result='SUCCESS')
+ with open(build.jobdir.job_output_file) as f:
+ output = f.read()
+ self.assertIn(f'Ansible version={self.ansible_major_minor}',
+ output)
-class TestAnsibleCallbackConfigs(AnsibleZuulTestCase):
+class TestExecutorFacts28(AnsibleZuulTestCase, ExecutorFactsMixin):
+ tenant_config_file = 'config/executor-facts/main28.yaml'
+ ansible_major_minor = '2.8'
+
+class TestExecutorFacts29(AnsibleZuulTestCase, ExecutorFactsMixin):
+ tenant_config_file = 'config/executor-facts/main29.yaml'
+ ansible_major_minor = '2.9'
+
+
+class TestExecutorFacts5(AnsibleZuulTestCase, ExecutorFactsMixin):
+ tenant_config_file = 'config/executor-facts/main5.yaml'
+ ansible_major_minor = '2.12'
+
+
+class AnsibleCallbackConfigsMixin:
config_file = 'zuul-executor-ansible-callback.conf'
+
+ # These should be overridden in child classes.
tenant_config_file = 'config/ansible-callbacks/main.yaml'
+ ansible_major_minor = 'X.Y'
def test_ansible_callback_config(self):
self.executor_server.keep_jobdir = True
@@ -905,6 +929,32 @@ class TestAnsibleCallbackConfigs(AnsibleZuulTestCase):
'common-config/playbooks/callback_plugins/',
c['callback_test_callback']['file_name'])
self.assertTrue(os.path.isfile(callback_result_file))
+ build = self.getJobFromHistory('callback-test', result='SUCCESS')
+ with open(build.jobdir.job_output_file) as f:
+ output = f.read()
+ self.assertIn(f'Ansible version={self.ansible_major_minor}',
+ output)
+
+
+class TestAnsibleCallbackConfigs28(AnsibleZuulTestCase,
+ AnsibleCallbackConfigsMixin):
+ config_file = 'zuul-executor-ansible-callback.conf'
+ tenant_config_file = 'config/ansible-callbacks/main28.yaml'
+ ansible_major_minor = '2.8'
+
+
+class TestAnsibleCallbackConfigs29(AnsibleZuulTestCase,
+ AnsibleCallbackConfigsMixin):
+ config_file = 'zuul-executor-ansible-callback.conf'
+ tenant_config_file = 'config/ansible-callbacks/main29.yaml'
+ ansible_major_minor = '2.9'
+
+
+class TestAnsibleCallbackConfigs5(AnsibleZuulTestCase,
+ AnsibleCallbackConfigsMixin):
+ config_file = 'zuul-executor-ansible-callback.conf'
+ tenant_config_file = 'config/ansible-callbacks/main5.yaml'
+ ansible_major_minor = '2.12'
class TestExecutorEnvironment(AnsibleZuulTestCase):
diff --git a/tests/unit/test_inventory.py b/tests/unit/test_inventory.py
index 8f5cca9ac..9dc1b3692 100644
--- a/tests/unit/test_inventory.py
+++ b/tests/unit/test_inventory.py
@@ -104,6 +104,7 @@ class TestInventoryGithub(TestInventoryBase):
z_vars = inventory['all']['vars']['zuul']
self.assertIn('executor', z_vars)
self.assertIn('src_root', z_vars['executor'])
+ self.assertIn('ansible_version', z_vars)
self.assertIn('job', z_vars)
self.assertIn('event_id', z_vars)
self.assertEqual(z_vars['job'], 'single-inventory')
@@ -137,6 +138,7 @@ class TestInventoryPythonPath(TestInventoryBase):
z_vars = inventory['all']['vars']['zuul']
self.assertIn('executor', z_vars)
self.assertIn('src_root', z_vars['executor'])
+ self.assertIn('ansible_version', z_vars)
self.assertIn('job', z_vars)
self.assertEqual(z_vars['job'], 'single-inventory')
self.assertEqual(z_vars['message'], 'QQ==')
@@ -167,6 +169,7 @@ class TestInventoryShellType(TestInventoryBase):
z_vars = inventory['all']['vars']['zuul']
self.assertIn('executor', z_vars)
self.assertIn('src_root', z_vars['executor'])
+ self.assertIn('ansible_version', z_vars)
self.assertIn('job', z_vars)
self.assertEqual(z_vars['job'], 'single-inventory')
self.assertEqual(z_vars['message'], 'QQ==')
@@ -195,6 +198,7 @@ class TestInventoryAutoPython(TestInventoryBase):
self.assertIn('executor', z_vars)
self.assertIn('src_root', z_vars['executor'])
self.assertIn('job', z_vars)
+ self.assertEqual(z_vars['ansible_version'], '2.8')
self.assertEqual(z_vars['job'], 'ansible-version28-inventory')
self.assertEqual(z_vars['message'], 'QQ==')
@@ -219,6 +223,7 @@ class TestInventoryAutoPython(TestInventoryBase):
self.assertIn('executor', z_vars)
self.assertIn('src_root', z_vars['executor'])
self.assertIn('job', z_vars)
+ self.assertEqual(z_vars['ansible_version'], '2.9')
self.assertEqual(z_vars['job'], 'ansible-version29-inventory')
self.assertEqual(z_vars['message'], 'QQ==')
@@ -243,6 +248,7 @@ class TestInventoryAutoPython(TestInventoryBase):
self.assertIn('executor', z_vars)
self.assertIn('src_root', z_vars['executor'])
self.assertIn('job', z_vars)
+ self.assertEqual(z_vars['ansible_version'], '5')
self.assertEqual(z_vars['job'], 'ansible-version5-inventory')
self.assertEqual(z_vars['message'], 'QQ==')
diff --git a/tests/unit/test_scheduler.py b/tests/unit/test_scheduler.py
index 978bc00a4..3445e9dc6 100644
--- a/tests/unit/test_scheduler.py
+++ b/tests/unit/test_scheduler.py
@@ -5372,6 +5372,17 @@ For CI problems and help debugging, contact ci@example.org"""
'database'].getBuildsets())
if buildsets:
break
+ # Stop queuing timer triggered jobs so that the assertions
+ # below don't race against more jobs being queued.
+ self.commitConfigUpdate('org/common-config', 'layouts/no-timer.yaml')
+ self.scheds.execute(lambda app: app.sched.reconfigure(app.config))
+ self.waitUntilSettled()
+ # If APScheduler is in mid-event when we remove the job, we
+ # can end up with one more event firing, so give it an extra
+ # second to settle.
+ time.sleep(3)
+ self.waitUntilSettled()
+
self.assertEqual(buildsets[0].result, 'CONFIG_ERROR')
self.assertIn('Job project-test2 depends on project-test1 '
'which was not run', buildsets[0].message)
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index a89bb3007..4c2befd61 100644
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -3733,9 +3733,9 @@ class TestInRepoJoin(ZuulTestCase):
class FunctionalAnsibleMixIn(object):
# A temporary class to hold new tests while others are disabled
+ # These should be overridden in child classes.
tenant_config_file = 'config/ansible/main.yaml'
- # This should be overriden in child classes.
- ansible_version = '2.9'
+ ansible_major_minor = 'X.Y'
def test_playbook(self):
# This test runs a bit long and needs extra time.
@@ -3826,6 +3826,7 @@ class FunctionalAnsibleMixIn(object):
self.assertEqual(build_bubblewrap.result, 'SUCCESS')
def test_repo_ansible(self):
+ self.executor_server.keep_jobdir = True
A = self.fake_gerrit.addFakeChange('org/ansible', 'master', 'A')
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
self.waitUntilSettled()
@@ -3835,18 +3836,26 @@ class FunctionalAnsibleMixIn(object):
self.assertHistory([
dict(name='hello-ansible', result='SUCCESS', changes='1,1'),
])
+ build = self.getJobFromHistory('hello-ansible', result='SUCCESS')
+ with open(build.jobdir.job_output_file) as f:
+ output = f.read()
+ self.assertIn(f'Ansible version={self.ansible_major_minor}',
+ output)
class TestAnsible28(AnsibleZuulTestCase, FunctionalAnsibleMixIn):
- ansible_version = '2.8'
+ tenant_config_file = 'config/ansible/main28.yaml'
+ ansible_major_minor = '2.8'
class TestAnsible29(AnsibleZuulTestCase, FunctionalAnsibleMixIn):
- ansible_version = '2.9'
+ tenant_config_file = 'config/ansible/main29.yaml'
+ ansible_major_minor = '2.9'
class TestAnsible5(AnsibleZuulTestCase, FunctionalAnsibleMixIn):
- ansible_version = '5'
+ tenant_config_file = 'config/ansible/main5.yaml'
+ ansible_major_minor = '2.12'
class TestPrePlaybooks(AnsibleZuulTestCase):
diff --git a/tests/unit/test_web.py b/tests/unit/test_web.py
index ba1931436..b15c01a69 100644
--- a/tests/unit/test_web.py
+++ b/tests/unit/test_web.py
@@ -1066,7 +1066,7 @@ class TestWeb(BaseTestWeb):
job_params = {
'job': 'project-test1',
- 'ansible_version': '2.9',
+ 'ansible_version': '5',
'timeout': None,
'post_timeout': None,
'items': [],
@@ -1164,7 +1164,7 @@ class TestWeb(BaseTestWeb):
"noop")
job_params = {
- 'ansible_version': '2.9',
+ 'ansible_version': '5',
'branch': 'master',
'extra_vars': {},
'group_vars': {},
diff --git a/zuul/ansible/base/callback/zuul_stream.py b/zuul/ansible/base/callback/zuul_stream.py
index 740f48114..fccfdc85d 100644
--- a/zuul/ansible/base/callback/zuul_stream.py
+++ b/zuul/ansible/base/callback/zuul_stream.py
@@ -43,6 +43,7 @@ import threading
import time
from ansible.plugins.callback import default
+from ansible.module_utils._text import to_text
from zuul.ansible import paths
from zuul.ansible import logconfig
@@ -503,8 +504,7 @@ class CallbackModule(default.CallbackModule):
if result._task.loop and 'results' in result_dict:
# items have their own events
pass
-
- elif result_dict.get('msg', '').startswith('MODULE FAILURE'):
+ elif to_text(result_dict.get('msg', '')).startswith('MODULE FAILURE'):
self._log_module_failure(result, result_dict)
elif result._task.action == 'debug':
# this is a debug statement, handle it special
@@ -523,7 +523,7 @@ class CallbackModule(default.CallbackModule):
# user provided. Note that msg may be a multi line block quote
# so we handle that here as well.
if keyname == 'msg':
- msg_lines = result_dict['msg'].rstrip().split('\n')
+ msg_lines = to_text(result_dict['msg']).rstrip().split('\n')
for msg_line in msg_lines:
self._log(msg=msg_line)
else:
@@ -546,10 +546,18 @@ class CallbackModule(default.CallbackModule):
elif result_dict.get('msg') == 'All items completed':
self._log_message(result, result_dict['msg'])
else:
- self._log_message(
- result,
- "Runtime: {delta}".format(
- **result_dict))
+ if 'delta' in result_dict:
+ self._log_message(
+ result,
+ "Runtime: {delta}".format(
+ **result_dict))
+ else:
+ # NOTE(ianw) 2022-08-24 : *Fairly* sure that you only
+ # fall into here when the call actually fails (and has
+ # not start/end time), but it is ignored by
+ # failed_when matching.
+ self._log_message(result, msg='ERROR (ignored)',
+ result_dict=result_dict)
def v2_runner_item_on_ok(self, result):
result_dict = dict(result._result)
@@ -565,7 +573,7 @@ class CallbackModule(default.CallbackModule):
# changes.
loop_var = result_dict.get('ansible_loop_var', 'item')
- if result_dict.get('msg', '').startswith('MODULE FAILURE'):
+ if to_text(result_dict.get('msg', '')).startswith('MODULE FAILURE'):
self._log_module_failure(result, result_dict)
elif result._task.action not in ('command', 'shell',
'win_command', 'win_shell'):
@@ -608,7 +616,7 @@ class CallbackModule(default.CallbackModule):
# changes.
loop_var = result_dict.get('ansible_loop_var', 'item')
- if result_dict.get('msg', '').startswith('MODULE FAILURE'):
+ if to_text(result_dict.get('msg', '')).startswith('MODULE FAILURE'):
self._log_module_failure(result, result_dict)
elif result._task.action not in ('command', 'shell',
'win_command', 'win_shell'):
@@ -741,7 +749,13 @@ class CallbackModule(default.CallbackModule):
msg = result_dict['msg']
result_dict = None
if msg:
- msg_lines = msg.rstrip().split('\n')
+ # ensure msg is a string; e.g.
+ #
+ # debug:
+ # msg: '{{ var }}'
+ #
+ # may not be!
+ msg_lines = to_text(msg).rstrip().split('\n')
if len(msg_lines) > 1:
self._log("{host} | {status}:".format(
host=hostname, status=status))
diff --git a/zuul/executor/server.py b/zuul/executor/server.py
index e00612e9e..1273cbaa4 100644
--- a/zuul/executor/server.py
+++ b/zuul/executor/server.py
@@ -2443,6 +2443,7 @@ class AnsibleJob(object):
work_root=self.jobdir.work_root,
result_data_file=self.jobdir.result_data_file,
inventory_file=self.jobdir.inventory)
+ zuul_vars['ansible_version'] = self.ansible_version
# Add playbook_context info
zuul_vars['playbook_context'] = dict(
diff --git a/zuul/lib/ansible-config.conf b/zuul/lib/ansible-config.conf
index 5bc3bd325..9fdc905b9 100644
--- a/zuul/lib/ansible-config.conf
+++ b/zuul/lib/ansible-config.conf
@@ -1,6 +1,6 @@
# This file describes the currently supported ansible versions
[common]
-default_version = 2.9
+default_version = 5
# OpenStackSDK 0.99.0 coincides with CORS header problems in some providers
requirements = openstacksdk<0.99 openshift jmespath google-cloud-storage pywinrm boto3 azure-storage-blob ibm-cos-sdk netaddr passlib
@@ -8,11 +8,13 @@ requirements = openstacksdk<0.99 openshift jmespath google-cloud-storage pywinrm
# Ansible 2.8.16 breaks the k8s connection plugin
# Jinja 3.1.1 is incompatible with 2.8
requirements = ansible>=2.8,<2.9,!=2.8.16 Jinja2<3.1.0
+deprecated = true
[2.9]
# Ansible 2.9.14 breaks the k8s connection plugin
# https://github.com/ansible/ansible/issues/72171
requirements = ansible>=2.9,<2.10,!=2.9.14
+deprecated = true
[5]
requirements = ansible>=5.0,<6.0