diff options
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 |