Merge "Add support for Ansible 5"
This commit is contained in:
commit
5b281e76a9
|
@ -45,6 +45,12 @@
|
|||
vars:
|
||||
zuul_ansible_version: 2.9
|
||||
|
||||
- job:
|
||||
name: zuul-stream-functional-5
|
||||
parent: zuul-stream-functional
|
||||
vars:
|
||||
zuul_ansible_version: 5
|
||||
|
||||
- job:
|
||||
name: zuul-tox
|
||||
description: |
|
||||
|
@ -300,6 +306,7 @@
|
|||
- web/.*
|
||||
- zuul-stream-functional-2.8
|
||||
- zuul-stream-functional-2.9
|
||||
- zuul-stream-functional-5
|
||||
- zuul-tox-remote
|
||||
- zuul-quick-start:
|
||||
requires: nodepool-container-image
|
||||
|
@ -326,6 +333,7 @@
|
|||
- web/.*
|
||||
- zuul-stream-functional-2.8
|
||||
- zuul-stream-functional-2.9
|
||||
- zuul-stream-functional-5
|
||||
- zuul-tox-remote
|
||||
- zuul-quick-start:
|
||||
requires: nodepool-container-image
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
# Copyright: (c) 2022, Acme Gating LLC
|
||||
#
|
||||
# This module 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.
|
||||
#
|
||||
# This software 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 this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
|
||||
def run_module():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(key=dict(type='str', required=True)),
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
raise Exception("Test module failure exception " + module.params['key'])
|
||||
|
||||
|
||||
def main():
|
||||
run_module()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -4,18 +4,16 @@
|
|||
|
||||
- block:
|
||||
|
||||
- name: Run a shell task with an ansible python exception
|
||||
command: echo foo
|
||||
args:
|
||||
chdir: /failure-shelltask/somewhere/that/does/not/exist
|
||||
- name: Run a task with an ansible python exception
|
||||
zuul_fail:
|
||||
key: fail-task
|
||||
|
||||
always:
|
||||
|
||||
- name: Loop with items on an ansible python exception
|
||||
command: "echo {{ item }}"
|
||||
zuul_fail:
|
||||
key: fail-loop
|
||||
with_items:
|
||||
- item1
|
||||
- item2
|
||||
- item3
|
||||
args:
|
||||
chdir: /failure-itemloop/somewhere/that/does/not/exist
|
||||
|
|
|
@ -35,20 +35,18 @@
|
|||
- name: complex2
|
||||
- name: complex3
|
||||
|
||||
- name: Run a shell task with an ansible python exception
|
||||
command: echo foo
|
||||
args:
|
||||
chdir: /shelltask/somewhere/that/does/not/exist
|
||||
- name: Run a task with an ansible python exception
|
||||
zuul_fail:
|
||||
key: task
|
||||
failed_when: false
|
||||
|
||||
- name: Loop with items on an ansible python exception
|
||||
command: "echo {{ item }}"
|
||||
zuul_fail:
|
||||
key: loop
|
||||
with_items:
|
||||
- item1
|
||||
- item2
|
||||
- item3
|
||||
args:
|
||||
chdir: /itemloop/somewhere/that/does/not/exist
|
||||
failed_when: false
|
||||
|
||||
- name: Print binary data
|
||||
|
|
|
@ -51,25 +51,25 @@
|
|||
egrep "^.+\| node1 \| ok: Item: Runtime" job-output.txt
|
||||
egrep "^.+\| node2 \| ok: Item: Runtime" job-output.txt
|
||||
|
||||
- name: Validate output - shell task with exception
|
||||
- name: Validate output - failed shell task
|
||||
shell: |
|
||||
egrep "^.+\| node1 \| (OSError|FileNotFoundError).+\/shelltask\/" job-output.txt
|
||||
egrep "^.+\| node2 \| (OSError|FileNotFoundError).+\/shelltask\/" job-output.txt
|
||||
egrep "^.+\| node1 \| Exception: Test module failure exception task" job-output.txt
|
||||
egrep "^.+\| node2 \| Exception: Test module failure exception task" job-output.txt
|
||||
|
||||
- name: Validate output - item loop with exception
|
||||
shell: |
|
||||
egrep "^.+\| node1 \| (OSError|FileNotFoundError).+\/itemloop\/" job-output.txt
|
||||
egrep "^.+\| node2 \| (OSError|FileNotFoundError).+\/itemloop\/" job-output.txt
|
||||
egrep "^.+\| node1 \| Exception: Test module failure exception loop" job-output.txt
|
||||
egrep "^.+\| node2 \| Exception: Test module failure exception loop" job-output.txt
|
||||
|
||||
- name: Validate output - failure shell task with exception
|
||||
shell: |
|
||||
egrep "^.+\| node1 \| (OSError|FileNotFoundError).+\/failure-shelltask\/" job-output.txt
|
||||
egrep "^.+\| node2 \| (OSError|FileNotFoundError).+\/failure-shelltask\/" job-output.txt
|
||||
egrep "^.+\| node1 \| Exception: Test module failure exception fail-task" job-output.txt
|
||||
egrep "^.+\| node2 \| Exception: Test module failure exception fail-task" job-output.txt
|
||||
|
||||
- name: Validate output - failure item loop with exception
|
||||
shell: |
|
||||
egrep "^.+\| node1 \| (OSError|FileNotFoundError).+\/failure-itemloop\/" job-output.txt
|
||||
egrep "^.+\| node2 \| (OSError|FileNotFoundError).+\/failure-itemloop\/" job-output.txt
|
||||
egrep "^.+\| node1 \| Exception: Test module failure exception fail-loop" job-output.txt
|
||||
egrep "^.+\| node2 \| Exception: Test module failure exception fail-loop" job-output.txt
|
||||
|
||||
- name: Validate output - binary data
|
||||
shell: |
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
features:
|
||||
- |
|
||||
Ansible version 5 is now available. The default Ansible version
|
||||
is still 2.9, but version 5 may be selected by using
|
||||
:attr:`job.ansible-version`.
|
||||
|
||||
upgrade:
|
||||
- |
|
||||
This is the first version of Ansible added to Zuul since the
|
||||
Ansible project began releasing the `Ansible community` package.
|
||||
Zuul includes the Ansible community package since it includes a
|
||||
wide selection of useful modules, many of which were included by
|
||||
default in previous versions of Ansible.
|
||||
|
||||
Only the major version of Ansible community is specified
|
||||
(e.g. ``ansible-version: 5``). This corresponds to a single minor
|
||||
release of Ansible core (e.g., Ansible community 5 corresponds to
|
||||
Ansible core 2.12). Ansible releases minor versions of the
|
||||
community package which may contain updates to the included
|
||||
Ansible collections as well as micro version updates of Ansible
|
||||
core (e.g. Ansible community 5.6 includes ansible-core 2.12.4).
|
||||
|
||||
Zuul does not specify the minor version of Ansible community,
|
||||
therefore the latest available micro-version will be installed at
|
||||
build-time. If you need more control over the version of Ansible
|
||||
used, see the help text for ``zuul-manage-ansible``.
|
|
@ -52,6 +52,13 @@
|
|||
test_ansible_version_major: 2
|
||||
test_ansible_version_minor: 9
|
||||
|
||||
- job:
|
||||
name: ansible-5
|
||||
parent: ansible-version
|
||||
ansible-version: 5
|
||||
vars:
|
||||
test_ansible_version_major: 2
|
||||
test_ansible_version_minor: 12
|
||||
|
||||
- project:
|
||||
name: common-config
|
||||
|
@ -60,6 +67,7 @@
|
|||
- ansible-default
|
||||
- ansible-28
|
||||
- ansible-29
|
||||
- ansible-5
|
||||
|
||||
- project:
|
||||
name: org/project
|
||||
|
@ -68,3 +76,4 @@
|
|||
- ansible-default-zuul-conf
|
||||
- ansible-28
|
||||
- ansible-29
|
||||
- ansible-5
|
||||
|
|
|
@ -107,3 +107,21 @@
|
|||
label: ubuntu-xenial
|
||||
ansible-version: '2.8'
|
||||
run: playbooks/ansible-version.yaml
|
||||
|
||||
- job:
|
||||
name: ansible-version29-inventory
|
||||
nodeset:
|
||||
nodes:
|
||||
- name: ubuntu-xenial
|
||||
label: ubuntu-xenial
|
||||
ansible-version: '2.9'
|
||||
run: playbooks/ansible-version.yaml
|
||||
|
||||
- job:
|
||||
name: ansible-version5-inventory
|
||||
nodeset:
|
||||
nodes:
|
||||
- name: ubuntu-xenial
|
||||
label: ubuntu-xenial
|
||||
ansible-version: '5'
|
||||
run: playbooks/ansible-version.yaml
|
||||
|
|
|
@ -8,3 +8,5 @@
|
|||
- group-inventory
|
||||
- hostvars-inventory
|
||||
- ansible-version28-inventory
|
||||
- ansible-version29-inventory
|
||||
- ansible-version5-inventory
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
- hosts: localhost
|
||||
tasks:
|
||||
- name: Local shell task with python exception
|
||||
command: echo foo
|
||||
args:
|
||||
chdir: /local-shelltask/somewhere/that/does/not/exist
|
||||
failed_when: false
|
|
@ -15,7 +15,3 @@
|
|||
- job:
|
||||
name: base
|
||||
parent: null
|
||||
|
||||
- job:
|
||||
name: command-localhost
|
||||
run: playbooks/command-localhost.yaml
|
||||
|
|
|
@ -89,11 +89,3 @@
|
|||
- failed_in_loop1
|
||||
- failed_in_loop2
|
||||
ignore_errors: True
|
||||
|
||||
- hosts: all
|
||||
tasks:
|
||||
- name: Remote shell task with python exception
|
||||
command: echo foo
|
||||
args:
|
||||
chdir: /remote-shelltask/somewhere/that/does/not/exist
|
||||
failed_when: false
|
||||
|
|
|
@ -101,3 +101,11 @@ class TestActionModules29(AnsibleZuulTestCase, FunctionalActionModulesMixIn):
|
|||
def setUp(self):
|
||||
super().setUp()
|
||||
self._setUp()
|
||||
|
||||
|
||||
class TestActionModules5(AnsibleZuulTestCase, FunctionalActionModulesMixIn):
|
||||
ansible_version = '5'
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self._setUp()
|
||||
|
|
|
@ -158,3 +158,11 @@ class TestZuulJSON29(AnsibleZuulTestCase, FunctionalZuulJSONMixIn):
|
|||
def setUp(self):
|
||||
super().setUp()
|
||||
self._setUp()
|
||||
|
||||
|
||||
class TestZuulJSON5(AnsibleZuulTestCase, FunctionalZuulJSONMixIn):
|
||||
ansible_version = '5'
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self._setUp()
|
||||
|
|
|
@ -26,7 +26,8 @@ class FunctionalZuulStreamMixIn:
|
|||
wait_timeout = 120
|
||||
|
||||
def _setUp(self):
|
||||
self.log_console_port = 19000 + int(self.ansible_version.split('.')[1])
|
||||
self.log_console_port = 19000 + int(
|
||||
self.ansible_core_version.split('.')[1])
|
||||
self.fake_nodepool.remote_ansible = True
|
||||
|
||||
ansible_remote = os.environ.get('ZUUL_REMOTE_IPV4')
|
||||
|
@ -109,7 +110,7 @@ class FunctionalZuulStreamMixIn:
|
|||
r'playbooks/command.yaml@master\]', text)
|
||||
self.assertLogLine(r'PLAY \[all\]', text)
|
||||
self.assertLogLine(
|
||||
r'Ansible version={}'.format(self.ansible_version), text)
|
||||
r'Ansible version={}'.format(self.ansible_core_version), text)
|
||||
self.assertLogLine(r'TASK \[Show contents of first file\]', text)
|
||||
self.assertLogLine(r'controller \| command test one', text)
|
||||
self.assertLogLine(
|
||||
|
@ -149,12 +150,6 @@ class FunctionalZuulStreamMixIn:
|
|||
self.assertLogLine(r'compute1 \| failed_in_loop2', text)
|
||||
self.assertLogLine(r'compute1 \| ok: Item: failed_in_loop2 '
|
||||
r'Result: 1', text)
|
||||
self.assertLogLine(r'compute1 \| .*No such file or directory: .*'
|
||||
r'\'/remote-shelltask/somewhere/'
|
||||
r'that/does/not/exist\'', text)
|
||||
self.assertLogLine(r'controller \| .*No such file or directory: .*'
|
||||
r'\'/remote-shelltask/somewhere/'
|
||||
r'that/does/not/exist\'', text)
|
||||
self.assertLogLine(
|
||||
r'controller \| ok: Runtime: \d:\d\d:\d\d\.\d\d\d\d\d\d', text)
|
||||
self.assertLogLine('PLAY RECAP', text)
|
||||
|
@ -167,18 +162,6 @@ class FunctionalZuulStreamMixIn:
|
|||
r'RUN END RESULT_NORMAL: \[untrusted : review.example.com/'
|
||||
r'org/project/playbooks/command.yaml@master]', text)
|
||||
|
||||
# Run a pre-defined job that is defined in a trusted repo to test
|
||||
# localhost tasks.
|
||||
job = self._run_job('command-localhost', create=False)
|
||||
with self.jobLog(job):
|
||||
build = self.history[-1]
|
||||
self.assertEqual(build.result, 'SUCCESS')
|
||||
|
||||
text = self._get_job_output(build)
|
||||
self.assertLogLine(r'localhost \| .*No such file or directory: .*'
|
||||
r'\'/local-shelltask/somewhere/'
|
||||
r'that/does/not/exist\'', text)
|
||||
|
||||
def test_module_exception(self):
|
||||
job = self._run_job('module_failure_exception')
|
||||
with self.jobLog(job):
|
||||
|
@ -201,19 +184,14 @@ class FunctionalZuulStreamMixIn:
|
|||
text = self._get_job_output(build)
|
||||
self.assertLogLine(r'TASK \[Module failure\]', text)
|
||||
|
||||
if self.ansible_version in ('2.5', '2.6'):
|
||||
regex = r'controller \| MODULE FAILURE: This module is broken'
|
||||
else:
|
||||
# Ansible starting with 2.7 emits a different error message
|
||||
# if a module exits without an exception or the ansible
|
||||
# supplied methods.
|
||||
regex = r'controller \| "msg": "New-style module did not ' \
|
||||
r'handle its own exit"'
|
||||
regex = r'controller \| "msg": "New-style module did not ' \
|
||||
r'handle its own exit"'
|
||||
self.assertLogLine(regex, text)
|
||||
|
||||
|
||||
class TestZuulStream28(AnsibleZuulTestCase, FunctionalZuulStreamMixIn):
|
||||
ansible_version = '2.8'
|
||||
ansible_core_version = '2.8'
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
@ -222,6 +200,16 @@ class TestZuulStream28(AnsibleZuulTestCase, FunctionalZuulStreamMixIn):
|
|||
|
||||
class TestZuulStream29(AnsibleZuulTestCase, FunctionalZuulStreamMixIn):
|
||||
ansible_version = '2.9'
|
||||
ansible_core_version = '2.9'
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self._setUp()
|
||||
|
||||
|
||||
class TestZuulStream5(AnsibleZuulTestCase, FunctionalZuulStreamMixIn):
|
||||
ansible_version = '5'
|
||||
ansible_core_version = '2.12'
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
|
|
@ -201,6 +201,54 @@ class TestInventoryAutoPython(TestInventoryBase):
|
|||
self.executor_server.release()
|
||||
self.waitUntilSettled()
|
||||
|
||||
def test_auto_python_ansible29_inventory(self):
|
||||
inventory = self._get_build_inventory('ansible-version29-inventory')
|
||||
|
||||
all_nodes = ('ubuntu-xenial',)
|
||||
self.assertIn('all', inventory)
|
||||
self.assertIn('hosts', inventory['all'])
|
||||
self.assertIn('vars', inventory['all'])
|
||||
for node_name in all_nodes:
|
||||
self.assertIn(node_name, inventory['all']['hosts'])
|
||||
node_vars = inventory['all']['hosts'][node_name]
|
||||
self.assertEqual(
|
||||
'auto', node_vars['ansible_python_interpreter'])
|
||||
|
||||
self.assertIn('zuul', inventory['all']['vars'])
|
||||
z_vars = inventory['all']['vars']['zuul']
|
||||
self.assertIn('executor', z_vars)
|
||||
self.assertIn('src_root', z_vars['executor'])
|
||||
self.assertIn('job', z_vars)
|
||||
self.assertEqual(z_vars['job'], 'ansible-version29-inventory')
|
||||
self.assertEqual(z_vars['message'], 'QQ==')
|
||||
|
||||
self.executor_server.release()
|
||||
self.waitUntilSettled()
|
||||
|
||||
def test_auto_python_ansible5_inventory(self):
|
||||
inventory = self._get_build_inventory('ansible-version5-inventory')
|
||||
|
||||
all_nodes = ('ubuntu-xenial',)
|
||||
self.assertIn('all', inventory)
|
||||
self.assertIn('hosts', inventory['all'])
|
||||
self.assertIn('vars', inventory['all'])
|
||||
for node_name in all_nodes:
|
||||
self.assertIn(node_name, inventory['all']['hosts'])
|
||||
node_vars = inventory['all']['hosts'][node_name]
|
||||
self.assertEqual(
|
||||
'auto', node_vars['ansible_python_interpreter'])
|
||||
|
||||
self.assertIn('zuul', inventory['all']['vars'])
|
||||
z_vars = inventory['all']['vars']['zuul']
|
||||
self.assertIn('executor', z_vars)
|
||||
self.assertIn('src_root', z_vars['executor'])
|
||||
self.assertIn('job', z_vars)
|
||||
self.assertEqual(z_vars['job'], 'ansible-version5-inventory')
|
||||
self.assertEqual(z_vars['message'], 'QQ==')
|
||||
|
||||
self.executor_server.release()
|
||||
self.waitUntilSettled()
|
||||
|
||||
|
||||
class TestInventory(TestInventoryBase):
|
||||
|
||||
|
|
|
@ -3810,6 +3810,10 @@ class TestAnsible29(AnsibleZuulTestCase, FunctionalAnsibleMixIn):
|
|||
ansible_version = '2.9'
|
||||
|
||||
|
||||
class TestAnsible5(AnsibleZuulTestCase, FunctionalAnsibleMixIn):
|
||||
ansible_version = '5'
|
||||
|
||||
|
||||
class TestPrePlaybooks(AnsibleZuulTestCase):
|
||||
# A temporary class to hold new tests while others are disabled
|
||||
|
||||
|
@ -7843,6 +7847,7 @@ class TestAnsibleVersion(AnsibleZuulTestCase):
|
|||
dict(name='ansible-default', result='SUCCESS', changes='1,1'),
|
||||
dict(name='ansible-28', result='SUCCESS', changes='1,1'),
|
||||
dict(name='ansible-29', result='SUCCESS', changes='1,1'),
|
||||
dict(name='ansible-5', result='SUCCESS', changes='1,1'),
|
||||
], ordered=False)
|
||||
|
||||
|
||||
|
@ -7863,6 +7868,7 @@ class TestDefaultAnsibleVersion(AnsibleZuulTestCase):
|
|||
changes='1,1'),
|
||||
dict(name='ansible-28', result='SUCCESS', changes='1,1'),
|
||||
dict(name='ansible-29', result='SUCCESS', changes='1,1'),
|
||||
dict(name='ansible-5', result='SUCCESS', changes='1,1'),
|
||||
], ordered=False)
|
||||
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
../../base/library/command.py
|
|
@ -0,0 +1,675 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>, and others
|
||||
# Copyright: (c) 2016, Toshio Kuratomi <tkuratomi@ansible.com>
|
||||
#
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['stableinterface'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
|
||||
# flake8: noqa
|
||||
# This file shares a significant chunk of code with an upstream ansible
|
||||
# function, run_command. The goal is to not have to fork quite so much
|
||||
# of that function, and discussing that design with upstream means we
|
||||
# should keep the changes to substantive ones only. For that reason, this
|
||||
# file is purposely not enforcing pep8, as making the function pep8 clean
|
||||
# would remove our ability to easily have a discussion with our friends
|
||||
# upstream
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: command
|
||||
short_description: Executes a command on a remote node
|
||||
version_added: historical
|
||||
description:
|
||||
- The C(command) module takes the command name followed by a list of space-delimited arguments.
|
||||
- The given command will be executed on all selected nodes. It will not be
|
||||
processed through the shell, so variables like C($HOME) and operations
|
||||
like C("<"), C(">"), C("|"), C(";") and C("&") will not work (use the M(shell)
|
||||
module if you need these features).
|
||||
- For Windows targets, use the M(win_command) module instead.
|
||||
options:
|
||||
free_form:
|
||||
description:
|
||||
- The command module takes a free form command to run. There is no parameter actually named 'free form'.
|
||||
See the examples!
|
||||
required: yes
|
||||
argv:
|
||||
description:
|
||||
- Allows the user to provide the command as a list vs. a string. Only the string or the list form can be
|
||||
provided, not both. One or the other must be provided.
|
||||
version_added: "2.6"
|
||||
creates:
|
||||
description:
|
||||
- A filename or (since 2.0) glob pattern, when it already exists, this step will B(not) be run.
|
||||
removes:
|
||||
description:
|
||||
- A filename or (since 2.0) glob pattern, when it does not exist, this step will B(not) be run.
|
||||
version_added: "0.8"
|
||||
chdir:
|
||||
description:
|
||||
- Change into this directory before running the command.
|
||||
version_added: "0.6"
|
||||
warn:
|
||||
description:
|
||||
- If command_warnings are on in ansible.cfg, do not warn about this particular line if set to C(no).
|
||||
type: bool
|
||||
default: 'yes'
|
||||
version_added: "1.8"
|
||||
stdin:
|
||||
version_added: "2.4"
|
||||
description:
|
||||
- Set the stdin of the command directly to the specified value.
|
||||
notes:
|
||||
- If you want to run a command through the shell (say you are using C(<), C(>), C(|), etc), you actually want the M(shell) module instead.
|
||||
Parsing shell metacharacters can lead to unexpected commands being executed if quoting is not done correctly so it is more secure to
|
||||
use the C(command) module when possible.
|
||||
- " C(creates), C(removes), and C(chdir) can be specified after the command.
|
||||
For instance, if you only want to run a command if a certain file does not exist, use this."
|
||||
- The C(executable) parameter is removed since version 2.4. If you have a need for this parameter, use the M(shell) module instead.
|
||||
- For Windows targets, use the M(win_command) module instead.
|
||||
author:
|
||||
- Ansible Core Team
|
||||
- Michael DeHaan
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: return motd to registered var
|
||||
command: cat /etc/motd
|
||||
register: mymotd
|
||||
|
||||
- name: Run the command if the specified file does not exist.
|
||||
command: /usr/bin/make_database.sh arg1 arg2
|
||||
args:
|
||||
creates: /path/to/database
|
||||
|
||||
# You can also use the 'args' form to provide the options.
|
||||
- name: This command will change the working directory to somedir/ and will only run when /path/to/database doesn't exist.
|
||||
command: /usr/bin/make_database.sh arg1 arg2
|
||||
args:
|
||||
chdir: somedir/
|
||||
creates: /path/to/database
|
||||
|
||||
- name: use argv to send the command as a list. Be sure to leave command empty
|
||||
command:
|
||||
args:
|
||||
argv:
|
||||
- echo
|
||||
- testing
|
||||
|
||||
- name: safely use templated variable to run command. Always use the quote filter to avoid injection issues.
|
||||
command: cat {{ myfile|quote }}
|
||||
register: myoutput
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
cmd:
|
||||
description: the cmd that was run on the remote machine
|
||||
returned: always
|
||||
type: list
|
||||
sample:
|
||||
- echo
|
||||
- hello
|
||||
delta:
|
||||
description: cmd end time - cmd start time
|
||||
returned: always
|
||||
type: string
|
||||
sample: 0:00:00.001529
|
||||
end:
|
||||
description: cmd end time
|
||||
returned: always
|
||||
type: string
|
||||
sample: '2017-09-29 22:03:48.084657'
|
||||
start:
|
||||
description: cmd start time
|
||||
returned: always
|
||||
type: string
|
||||
sample: '2017-09-29 22:03:48.083128'
|
||||
'''
|
||||
|
||||
import datetime
|
||||
import glob
|
||||
import os
|
||||
import shlex
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
# Imports needed for Zuul things
|
||||
import re
|
||||
import subprocess
|
||||
import traceback
|
||||
import threading
|
||||
from ansible.module_utils.basic import heuristic_log_sanitize
|
||||
from ansible.module_utils.six import (
|
||||
PY2,
|
||||
PY3,
|
||||
b,
|
||||
binary_type,
|
||||
string_types,
|
||||
text_type,
|
||||
)
|
||||
from ansible.module_utils.six.moves import shlex_quote
|
||||
from ansible.module_utils._text import to_native, to_bytes, to_text
|
||||
|
||||
|
||||
LOG_STREAM_FILE = '/tmp/console-{log_uuid}.log'
|
||||
PASSWD_ARG_RE = re.compile(r'^[-]{0,2}pass[-]?(word|wd)?')
|
||||
# List to save stdout log lines in as we collect them
|
||||
_log_lines = []
|
||||
|
||||
|
||||
class Console(object):
|
||||
def __init__(self, log_uuid):
|
||||
self.logfile_name = LOG_STREAM_FILE.format(log_uuid=log_uuid)
|
||||
|
||||
def __enter__(self):
|
||||
self.logfile = open(self.logfile_name, 'ab', buffering=0)
|
||||
return self
|
||||
|
||||
def __exit__(self, etype, value, tb):
|
||||
self.logfile.close()
|
||||
|
||||
def addLine(self, ln):
|
||||
# Note this format with deliminator is "inspired" by the old
|
||||
# Jenkins format but with microsecond resolution instead of
|
||||
# millisecond. It is kept so log parsing/formatting remains
|
||||
# consistent.
|
||||
ts = str(datetime.datetime.now()).encode('utf-8')
|
||||
if not isinstance(ln, bytes):
|
||||
try:
|
||||
ln = ln.encode('utf-8')
|
||||
except Exception:
|
||||
ln = repr(ln).encode('utf-8') + b'\n'
|
||||
outln = b'%s | %s' % (ts, ln)
|
||||
self.logfile.write(outln)
|
||||
|
||||
|
||||
def follow(fd, log_uuid):
|
||||
newline_warning = False
|
||||
with Console(log_uuid) as console:
|
||||
while True:
|
||||
line = fd.readline()
|
||||
if not line:
|
||||
break
|
||||
_log_lines.append(line)
|
||||
if not line.endswith(b'\n'):
|
||||
line += b'\n'
|
||||
newline_warning = True
|
||||
console.addLine(line)
|
||||
if newline_warning:
|
||||
console.addLine('[Zuul] No trailing newline\n')
|
||||
|
||||
|
||||
# Taken from ansible/module_utils/basic.py ... forking the method for now
|
||||
# so that we can dive in and figure out how to make appropriate hook points
|
||||
def zuul_run_command(self, args, zuul_log_id, check_rc=False, close_fds=True, executable=None, data=None, binary_data=False, path_prefix=None, cwd=None,
|
||||
use_unsafe_shell=False, prompt_regex=None, environ_update=None, umask=None, encoding='utf-8', errors='surrogate_or_strict'):
|
||||
'''
|
||||
Execute a command, returns rc, stdout, and stderr.
|
||||
|
||||
:arg args: is the command to run
|
||||
* If args is a list, the command will be run with shell=False.
|
||||
* If args is a string and use_unsafe_shell=False it will split args to a list and run with shell=False
|
||||
* If args is a string and use_unsafe_shell=True it runs with shell=True.
|
||||
:kw check_rc: Whether to call fail_json in case of non zero RC.
|
||||
Default False
|
||||
:kw close_fds: See documentation for subprocess.Popen(). Default True
|
||||
:kw executable: See documentation for subprocess.Popen(). Default None
|
||||
:kw data: If given, information to write to the stdin of the command
|
||||
:kw binary_data: If False, append a newline to the data. Default False
|
||||
:kw path_prefix: If given, additional path to find the command in.
|
||||
This adds to the PATH environment vairable so helper commands in
|
||||
the same directory can also be found
|
||||
:kw cwd: If given, working directory to run the command inside
|
||||
:kw use_unsafe_shell: See `args` parameter. Default False
|
||||
:kw prompt_regex: Regex string (not a compiled regex) which can be
|
||||
used to detect prompts in the stdout which would otherwise cause
|
||||
the execution to hang (especially if no input data is specified)
|
||||
:kw environ_update: dictionary to *update* os.environ with
|
||||
:kw umask: Umask to be used when running the command. Default None
|
||||
:kw encoding: Since we return native strings, on python3 we need to
|
||||
know the encoding to use to transform from bytes to text. If you
|
||||
want to always get bytes back, use encoding=None. The default is
|
||||
"utf-8". This does not affect transformation of strings given as
|
||||
args.
|
||||
:kw errors: Since we return native strings, on python3 we need to
|
||||
transform stdout and stderr from bytes to text. If the bytes are
|
||||
undecodable in the ``encoding`` specified, then use this error
|
||||
handler to deal with them. The default is ``surrogate_or_strict``
|
||||
which means that the bytes will be decoded using the
|
||||
surrogateescape error handler if available (available on all
|
||||
python3 versions we support) otherwise a UnicodeError traceback
|
||||
will be raised. This does not affect transformations of strings
|
||||
given as args.
|
||||
:returns: A 3-tuple of return code (integer), stdout (native string),
|
||||
and stderr (native string). On python2, stdout and stderr are both
|
||||
byte strings. On python3, stdout and stderr are text strings converted
|
||||
according to the encoding and errors parameters. If you want byte
|
||||
strings on python3, use encoding=None to turn decoding to text off.
|
||||
'''
|
||||
|
||||
if not isinstance(args, (list, binary_type, text_type)):
|
||||
msg = "Argument 'args' to run_command must be list or string"
|
||||
self.fail_json(rc=257, cmd=args, msg=msg)
|
||||
|
||||
shell = False
|
||||
if use_unsafe_shell:
|
||||
|
||||
# stringify args for unsafe/direct shell usage
|
||||
if isinstance(args, list):
|
||||
args = " ".join([shlex_quote(x) for x in args])
|
||||
|
||||
# not set explicitly, check if set by controller
|
||||
if executable:
|
||||
args = [executable, '-c', args]
|
||||
elif self._shell not in (None, '/bin/sh'):
|
||||
args = [self._shell, '-c', args]
|
||||
else:
|
||||
shell = True
|
||||
else:
|
||||
# ensure args are a list
|
||||
if isinstance(args, (binary_type, text_type)):
|
||||
# On python2.6 and below, shlex has problems with text type
|
||||
# On python3, shlex needs a text type.
|
||||
if PY2:
|
||||
args = to_bytes(args, errors='surrogate_or_strict')
|
||||
elif PY3:
|
||||
args = to_text(args, errors='surrogateescape')
|
||||
args = shlex.split(args)
|
||||
|
||||
# expand shellisms
|
||||
args = [os.path.expanduser(os.path.expandvars(x)) for x in args if x is not None]
|
||||
|
||||
prompt_re = None
|
||||
if prompt_regex:
|
||||
if isinstance(prompt_regex, text_type):
|
||||
if PY3:
|
||||
prompt_regex = to_bytes(prompt_regex, errors='surrogateescape')
|
||||
elif PY2:
|
||||
prompt_regex = to_bytes(prompt_regex, errors='surrogate_or_strict')
|
||||
try:
|
||||
prompt_re = re.compile(prompt_regex, re.MULTILINE)
|
||||
except re.error:
|
||||
self.fail_json(msg="invalid prompt regular expression given to run_command")
|
||||
|
||||
rc = 0
|
||||
msg = None
|
||||
st_in = None
|
||||
|
||||
# Manipulate the environ we'll send to the new process
|
||||
old_env_vals = {}
|
||||
# We can set this from both an attribute and per call
|
||||
for key, val in self.run_command_environ_update.items():
|
||||
old_env_vals[key] = os.environ.get(key, None)
|
||||
os.environ[key] = val
|
||||
if environ_update:
|
||||
for key, val in environ_update.items():
|
||||
old_env_vals[key] = os.environ.get(key, None)
|
||||
os.environ[key] = val
|
||||
if path_prefix:
|
||||
old_env_vals['PATH'] = os.environ['PATH']
|
||||
os.environ['PATH'] = "%s:%s" % (path_prefix, os.environ['PATH'])
|
||||
|
||||
# If using test-module and explode, the remote lib path will resemble ...
|
||||
# /tmp/test_module_scratch/debug_dir/ansible/module_utils/basic.py
|
||||
# If using ansible or ansible-playbook with a remote system ...
|
||||
# /tmp/ansible_vmweLQ/ansible_modlib.zip/ansible/module_utils/basic.py
|
||||
|
||||
# Clean out python paths set by ansiballz
|
||||
if 'PYTHONPATH' in os.environ:
|
||||
pypaths = os.environ['PYTHONPATH'].split(':')
|
||||
pypaths = [x for x in pypaths
|
||||
if not x.endswith('/ansible_modlib.zip') and
|
||||
not x.endswith('/debug_dir')]
|
||||
os.environ['PYTHONPATH'] = ':'.join(pypaths)
|
||||
if not os.environ['PYTHONPATH']:
|
||||
del os.environ['PYTHONPATH']
|
||||
|
||||
# create a printable version of the command for use
|
||||
# in reporting later, which strips out things like
|
||||
# passwords from the args list
|
||||
to_clean_args = args
|
||||
if PY2:
|
||||
if isinstance(args, text_type):
|
||||
to_clean_args = to_bytes(args)
|
||||
else:
|
||||
if isinstance(args, binary_type):
|
||||
to_clean_args = to_text(args)
|
||||
if isinstance(args, (text_type, binary_type)):
|
||||
to_clean_args = shlex.split(to_clean_args)
|
||||
|
||||
clean_args = []
|
||||
is_passwd = False
|
||||
for arg in (to_native(a) for a in to_clean_args):
|
||||
if is_passwd:
|
||||
is_passwd = False
|
||||
clean_args.append('********')
|
||||
continue
|
||||
if PASSWD_ARG_RE.match(arg):
|
||||
sep_idx = arg.find('=')
|
||||
if sep_idx > -1:
|
||||
clean_args.append('%s=********' % arg[:sep_idx])
|
||||
continue
|
||||
else:
|
||||
is_passwd = True
|
||||
arg = heuristic_log_sanitize(arg, self.no_log_values)
|
||||
clean_args.append(arg)
|
||||
clean_args = ' '.join(shlex_quote(arg) for arg in clean_args)
|
||||
|
||||
if data:
|
||||
st_in = subprocess.PIPE
|
||||
|
||||
# ZUUL: changed stderr to follow stdout
|
||||
kwargs = dict(
|
||||
executable=executable,
|
||||
shell=shell,
|
||||
close_fds=close_fds,
|
||||
stdin=st_in,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
|
||||
# store the pwd
|
||||
prev_dir = os.getcwd()
|
||||
|
||||
# make sure we're in the right working directory
|
||||
if cwd and os.path.isdir(cwd):
|
||||
cwd = os.path.abspath(os.path.expanduser(cwd))
|
||||
kwargs['cwd'] = cwd
|
||||
try:
|
||||
os.chdir(cwd)
|
||||
except (OSError, IOError) as e:
|
||||
self.fail_json(rc=e.errno, msg="Could not open %s, %s" % (cwd, to_native(e)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
old_umask = None
|
||||
if umask:
|
||||
old_umask = os.umask(umask)
|
||||
|
||||
t = None
|
||||
fail_json_kwargs = None
|
||||
|
||||
try:
|
||||
if self._debug:
|
||||
self.log('Executing: ' + clean_args)
|
||||
|
||||
# ZUUL: Replaced the execution loop with the zuul_runner run function
|
||||
|
||||
cmd = subprocess.Popen(args, **kwargs)
|
||||
if self.no_log:
|
||||
t = None
|
||||
else:
|
||||
t = threading.Thread(target=follow, args=(cmd.stdout, zuul_log_id))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
# ZUUL: Our log thread will catch the output so don't do that here.
|
||||
|
||||
# # the communication logic here is essentially taken from that
|
||||
# # of the _communicate() function in ssh.py
|
||||
#
|
||||
# stdout = b('')
|
||||
# stderr = b('')
|
||||
#
|
||||
# # ZUUL: stderr follows stdout
|
||||
# rpipes = [cmd.stdout]
|
||||
|
||||
if data:
|
||||
if not binary_data:
|
||||
data += '\n'
|
||||
if isinstance(data, text_type):
|
||||
data = to_bytes(data)
|
||||
cmd.stdin.write(data)
|
||||
cmd.stdin.close()
|
||||
|
||||
# while True:
|
||||
# rfds, wfds, efds = select.select(rpipes, [], rpipes, 1)
|
||||
# stdout += self._read_from_pipes(rpipes, rfds, cmd.stdout)
|
||||
#
|
||||
# # ZUUL: stderr follows stdout
|
||||
# # stderr += self._read_from_pipes(rpipes, rfds, cmd.stderr)
|
||||
#
|
||||
# # if we're checking for prompts, do it now
|
||||
# if prompt_re:
|
||||
# if prompt_re.search(stdout) and not data:
|
||||
# if encoding:
|
||||
# stdout = to_native(stdout, encoding=encoding, errors=errors)
|
||||
# else:
|
||||
# stdout = stdout
|
||||
# return (257, stdout, "A prompt was encountered while running a command, but no input data was specified")
|
||||
# # only break out if no pipes are left to read or
|
||||
# # the pipes are completely read and
|
||||
# # the process is terminated
|
||||
# if (not rpipes or not rfds) and cmd.poll() is not None:
|
||||
# break
|
||||
# # No pipes are left to read but process is not yet terminated
|
||||
# # Only then it is safe to wait for the process to be finished
|
||||
# # NOTE: Actually cmd.poll() is always None here if rpipes is empty
|
||||
# elif not rpipes and cmd.poll() is None:
|
||||
# cmd.wait()
|
||||
# # The process is terminated. Since no pipes to read from are
|
||||
# # left, there is no need to call select() again.
|
||||
# break
|
||||
|
||||
# ZUUL: If the console log follow thread *is* stuck in readline,
|
||||
# we can't close stdout (attempting to do so raises an
|
||||
# exception) , so this is disabled.
|
||||
# cmd.stdout.close()
|
||||
# cmd.stderr.close()
|
||||
|
||||
rc = cmd.wait()
|
||||
|
||||
# Give the thread that is writing the console log up to 10 seconds
|
||||
# to catch up and exit. If it hasn't done so by then, it is very
|
||||
# likely stuck in readline() because it spawed a child that is
|
||||
# holding stdout or stderr open.
|
||||
if t:
|
||||
t.join(10)
|
||||
with Console(zuul_log_id) as console:
|
||||
if t.is_alive():
|
||||
console.addLine("[Zuul] standard output/error still open "
|
||||
"after child exited")
|
||||
# ZUUL: stdout and stderr are in the console log file
|
||||
# ZUUL: return the saved log lines so we can ship them back
|
||||
stdout = b('').join(_log_lines)
|
||||
else:
|
||||
stdout = b('')
|
||||
stderr = b('')
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
self.log("Error Executing CMD:%s Exception:%s" % (clean_args, to_native(e)))
|
||||
# ZUUL: store fail_json_kwargs and fail later in finally
|
||||
fail_json_kwargs = dict(rc=e.errno, msg=to_native(e), cmd=clean_args)
|
||||
except Exception as e:
|
||||
self.log("Error Executing CMD:%s Exception:%s" % (clean_args, to_native(traceback.format_exc())))
|
||||
# ZUUL: store fail_json_kwargs and fail later in finally
|
||||
fail_json_kwargs = dict(rc=257, msg=to_native(e), exception=traceback.format_exc(), cmd=clean_args)
|
||||
finally:
|
||||
if t:
|
||||
with Console(zuul_log_id) as console:
|
||||
if t.is_alive():
|
||||
console.addLine("[Zuul] standard output/error still open "
|
||||
"after child exited")
|
||||
if fail_json_kwargs:
|
||||
# we hit an exception and need to use the rc from
|
||||
# fail_json_kwargs
|
||||
rc = fail_json_kwargs['rc']
|
||||
|
||||
console.addLine("[Zuul] Task exit code: %s\n" % rc)
|
||||
|
||||
if fail_json_kwargs:
|
||||
self.fail_json(**fail_json_kwargs)
|
||||
|
||||
# Restore env settings
|
||||
for key, val in old_env_vals.items():
|
||||
if val is None:
|
||||
del os.environ[key]
|
||||
else:
|
||||
os.environ[key] = val
|
||||
|
||||
if old_umask:
|
||||
os.umask(old_umask)
|
||||
|
||||
if rc != 0 and check_rc:
|
||||
msg = heuristic_log_sanitize(stderr.rstrip(), self.no_log_values)
|
||||
self.fail_json(cmd=clean_args, rc=rc, stdout=stdout, stderr=stderr, msg=msg)
|
||||
|
||||
# reset the pwd
|
||||
os.chdir(prev_dir)
|
||||
|
||||
if encoding is not None:
|
||||
return (rc, to_native(stdout, encoding=encoding, errors=errors),
|
||||
to_native(stderr, encoding=encoding, errors=errors))
|
||||
return (rc, stdout, stderr)
|
||||
|
||||
|
||||
def check_command(module, commandline):
|
||||
arguments = {'chown': 'owner', 'chmod': 'mode', 'chgrp': 'group',
|
||||
'ln': 'state=link', 'mkdir': 'state=directory',
|
||||
'rmdir': 'state=absent', 'rm': 'state=absent', 'touch': 'state=touch'}
|
||||
commands = {'curl': 'get_url or uri', 'wget': 'get_url or uri',
|
||||
'svn': 'subversion', 'service': 'service',
|
||||
'mount': 'mount', 'rpm': 'yum, dnf or zypper', 'yum': 'yum', 'apt-get': 'apt',
|
||||
'tar': 'unarchive', 'unzip': 'unarchive', 'sed': 'replace, lineinfile or template',
|
||||
'dnf': 'dnf', 'zypper': 'zypper'}
|
||||
become = ['sudo', 'su', 'pbrun', 'pfexec', 'runas', 'pmrun', 'machinectl']
|
||||
if isinstance(commandline, list):
|
||||
command = commandline[0]
|
||||
else:
|
||||
command = commandline.split()[0]
|
||||
command = os.path.basename(command)
|
||||
|
||||
disable_suffix = "If you need to use command because {mod} is insufficient you can add" \
|
||||
" warn=False to this command task or set command_warnings=False in" \
|
||||
" ansible.cfg to get rid of this message."
|
||||
substitutions = {'mod': None, 'cmd': command}
|
||||
|
||||
if command in arguments:
|
||||
msg = "Consider using the {mod} module with {subcmd} rather than running {cmd}. " + disable_suffix
|
||||
substitutions['mod'] = 'file'
|
||||
substitutions['subcmd'] = arguments[command]
|
||||
module.warn(msg.format(**substitutions))
|
||||
|
||||
if command in commands:
|
||||
msg = "Consider using the {mod} module rather than running {cmd}. " + disable_suffix
|
||||
substitutions['mod'] = commands[command]
|
||||
module.warn(msg.format(**substitutions))
|
||||
|
||||
if command in become:
|
||||
module.warn("Consider using 'become', 'become_method', and 'become_user' rather than running %s" % (command,))
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
# the command module is the one ansible module that does not take key=value args
|
||||
# hence don't copy this one if you are looking to build others!
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
_raw_params=dict(),
|
||||
_uses_shell=dict(type='bool', default=False),
|
||||
argv=dict(type='list'),
|
||||
chdir=dict(type='path'),
|
||||
executable=dict(),
|
||||
creates=dict(type='path'),
|
||||
removes=dict(type='path'),
|
||||
# The default for this really comes from the action plugin
|
||||
warn=dict(type='bool', default=True),
|
||||
stdin=dict(required=False),
|
||||
zuul_log_id=dict(type='str'),
|
||||
)
|
||||
)
|
||||
shell = module.params['_uses_shell']
|
||||
chdir = module.params['chdir']
|
||||
executable = module.params['executable']
|
||||
args = module.params['_raw_params']
|
||||
argv = module.params['argv']
|
||||
creates = module.params['creates']
|
||||
removes = module.params['removes']
|
||||
warn = module.params['warn']
|
||||
stdin = module.params['stdin']
|
||||
zuul_log_id = module.params['zuul_log_id']
|
||||
|
||||
if not shell and executable:
|
||||
module.warn("As of Ansible 2.4, the parameter 'executable' is no longer supported with the 'command' module. Not using '%s'." % executable)
|
||||
executable = None
|
||||
|
||||
if not zuul_log_id:
|
||||
module.fail_json(rc=256, msg="zuul_log_id missing: %s" % module.params)
|
||||
|
||||
if (not args or args.strip() == '') and not argv:
|
||||
module.fail_json(rc=256, msg="no command given")
|
||||
|
||||
if args and argv:
|
||||
module.fail_json(rc=256, msg="only command or argv can be given, not both")
|
||||
|
||||
if not shell and args:
|
||||
args = shlex.split(args)
|
||||
|
||||
args = args or argv
|
||||
|
||||
if chdir:
|
||||
chdir = os.path.abspath(chdir)
|
||||
os.chdir(chdir)
|
||||
|
||||
if creates:
|
||||
# do not run the command if the line contains creates=filename
|
||||
# and the filename already exists. This allows idempotence
|
||||
# of command executions.
|
||||
if glob.glob(creates):
|
||||
module.exit_json(
|
||||
cmd=args,
|
||||
stdout="skipped, since %s exists" % creates,
|
||||
changed=False,
|
||||
rc=0
|
||||
)
|
||||
|
||||
if removes:
|
||||
# do not run the command if the line contains removes=filename
|
||||
# and the filename does not exist. This allows idempotence
|
||||
# of command executions.
|
||||
if not glob.glob(removes):
|
||||
module.exit_json(
|
||||
cmd=args,
|
||||
stdout="skipped, since %s does not exist" % removes,
|
||||
changed=False,
|
||||
rc=0
|
||||
)
|
||||
|
||||
if warn:
|
||||
check_command(module, args)
|
||||
|
||||
startd = datetime.datetime.now()
|
||||
|
||||
rc, out, err = zuul_run_command(module, args, zuul_log_id, executable=executable, use_unsafe_shell=shell, encoding=None, data=stdin)
|
||||
|
||||
endd = datetime.datetime.now()
|
||||
delta = endd - startd
|
||||
|
||||
result = dict(
|
||||
cmd=args,
|
||||
stdout=out.rstrip(b"\r\n"),
|
||||
stderr=err.rstrip(b"\r\n"),
|
||||
rc=rc,
|
||||
start=str(startd),
|
||||
end=str(endd),
|
||||
delta=str(delta),
|
||||
changed=True,
|
||||
zuul_log_id=zuul_log_id
|
||||
)
|
||||
|
||||
if rc != 0:
|
||||
module.fail_json(msg='non-zero return code', **result)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1 +0,0 @@
|
|||
../../base/library/command.py
|
|
@ -0,0 +1,675 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>, and others
|
||||
# Copyright: (c) 2016, Toshio Kuratomi <tkuratomi@ansible.com>
|
||||
#
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['stableinterface'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
|
||||
# flake8: noqa
|
||||
# This file shares a significant chunk of code with an upstream ansible
|
||||
# function, run_command. The goal is to not have to fork quite so much
|
||||
# of that function, and discussing that design with upstream means we
|
||||
# should keep the changes to substantive ones only. For that reason, this
|
||||
# file is purposely not enforcing pep8, as making the function pep8 clean
|
||||
# would remove our ability to easily have a discussion with our friends
|
||||
# upstream
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: command
|
||||
short_description: Executes a command on a remote node
|
||||
version_added: historical
|
||||
description:
|
||||
- The C(command) module takes the command name followed by a list of space-delimited arguments.
|
||||
- The given command will be executed on all selected nodes. It will not be
|
||||
processed through the shell, so variables like C($HOME) and operations
|
||||
like C("<"), C(">"), C("|"), C(";") and C("&") will not work (use the M(shell)
|
||||
module if you need these features).
|
||||
- For Windows targets, use the M(win_command) module instead.
|
||||
options:
|
||||
free_form:
|
||||
description:
|
||||
- The command module takes a free form command to run. There is no parameter actually named 'free form'.
|
||||
See the examples!
|
||||
required: yes
|
||||
argv:
|
||||
description:
|
||||
- Allows the user to provide the command as a list vs. a string. Only the string or the list form can be
|
||||
provided, not both. One or the other must be provided.
|
||||
version_added: "2.6"
|
||||
creates:
|
||||
description:
|
||||
- A filename or (since 2.0) glob pattern, when it already exists, this step will B(not) be run.
|
||||
removes:
|
||||
description:
|
||||
- A filename or (since 2.0) glob pattern, when it does not exist, this step will B(not) be run.
|
||||
version_added: "0.8"
|
||||
chdir:
|
||||
description:
|
||||
- Change into this directory before running the command.
|
||||
version_added: "0.6"
|
||||
warn:
|
||||
description:
|
||||
- If command_warnings are on in ansible.cfg, do not warn about this particular line if set to C(no).
|
||||
type: bool
|
||||
default: 'yes'
|
||||
version_added: "1.8"
|
||||
stdin:
|
||||
version_added: "2.4"
|
||||
description:
|
||||
- Set the stdin of the command directly to the specified value.
|
||||
notes:
|
||||
- If you want to run a command through the shell (say you are using C(<), C(>), C(|), etc), you actually want the M(shell) module instead.
|
||||
Parsing shell metacharacters can lead to unexpected commands being executed if quoting is not done correctly so it is more secure to
|
||||
use the C(command) module when possible.
|
||||
- " C(creates), C(removes), and C(chdir) can be specified after the command.
|
||||
For instance, if you only want to run a command if a certain file does not exist, use this."
|
||||
- The C(executable) parameter is removed since version 2.4. If you have a need for this parameter, use the M(shell) module instead.
|
||||
- For Windows targets, use the M(win_command) module instead.
|
||||
author:
|
||||
- Ansible Core Team
|
||||
- Michael DeHaan
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: return motd to registered var
|
||||
command: cat /etc/motd
|
||||
register: mymotd
|
||||
|
||||
- name: Run the command if the specified file does not exist.
|
||||
command: /usr/bin/make_database.sh arg1 arg2
|
||||
args:
|
||||
creates: /path/to/database
|
||||
|
||||
# You can also use the 'args' form to provide the options.
|
||||
- name: This command will change the working directory to somedir/ and will only run when /path/to/database doesn't exist.
|
||||
command: /usr/bin/make_database.sh arg1 arg2
|
||||
args:
|
||||
chdir: somedir/
|
||||
creates: /path/to/database
|
||||
|
||||
- name: use argv to send the command as a list. Be sure to leave command empty
|
||||
command:
|
||||
args:
|
||||
argv:
|
||||
- echo
|
||||
- testing
|
||||
|
||||
- name: safely use templated variable to run command. Always use the quote filter to avoid injection issues.
|
||||
command: cat {{ myfile|quote }}
|
||||
register: myoutput
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
cmd:
|
||||
description: the cmd that was run on the remote machine
|
||||
returned: always
|
||||
type: list
|
||||
sample:
|
||||
- echo
|
||||
- hello
|
||||
delta:
|
||||
description: cmd end time - cmd start time
|
||||
returned: always
|
||||
type: string
|
||||
sample: 0:00:00.001529
|
||||
end:
|
||||
description: cmd end time
|
||||
returned: always
|
||||
type: string
|
||||
sample: '2017-09-29 22:03:48.084657'
|
||||
start:
|
||||
description: cmd start time
|
||||
returned: always
|
||||
type: string
|
||||
sample: '2017-09-29 22:03:48.083128'
|
||||
'''
|
||||
|
||||
import datetime
|
||||
import glob
|
||||
import os
|
||||
import shlex
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
# Imports needed for Zuul things
|
||||
import re
|
||||
import subprocess
|
||||
import traceback
|
||||
import threading
|
||||
from ansible.module_utils.basic import heuristic_log_sanitize
|
||||
from ansible.module_utils.six import (
|
||||
PY2,
|
||||
PY3,
|
||||
b,
|
||||
binary_type,
|
||||
string_types,
|
||||
text_type,
|
||||
)
|
||||
from ansible.module_utils.six.moves import shlex_quote
|
||||
from ansible.module_utils._text import to_native, to_bytes, to_text
|
||||
|
||||
|
||||
LOG_STREAM_FILE = '/tmp/console-{log_uuid}.log'
|
||||
PASSWD_ARG_RE = re.compile(r'^[-]{0,2}pass[-]?(word|wd)?')
|
||||
# List to save stdout log lines in as we collect them
|
||||
_log_lines = []
|
||||
|
||||
|
||||
class Console(object):
|
||||
def __init__(self, log_uuid):
|
||||
self.logfile_name = LOG_STREAM_FILE.format(log_uuid=log_uuid)
|
||||
|
||||
def __enter__(self):
|
||||
self.logfile = open(self.logfile_name, 'ab', buffering=0)
|
||||
return self
|
||||
|
||||
def __exit__(self, etype, value, tb):
|
||||
self.logfile.close()
|
||||
|
||||
def addLine(self, ln):
|
||||
# Note this format with deliminator is "inspired" by the old
|
||||
# Jenkins format but with microsecond resolution instead of
|
||||
# millisecond. It is kept so log parsing/formatting remains
|
||||
# consistent.
|
||||
ts = str(datetime.datetime.now()).encode('utf-8')
|
||||
if not isinstance(ln, bytes):
|
||||
try:
|
||||
ln = ln.encode('utf-8')
|
||||
except Exception:
|
||||
ln = repr(ln).encode('utf-8') + b'\n'
|
||||
outln = b'%s | %s' % (ts, ln)
|
||||
self.logfile.write(outln)
|
||||
|
||||
|
||||
def follow(fd, log_uuid):
|
||||
newline_warning = False
|
||||
with Console(log_uuid) as console:
|
||||
while True:
|
||||
line = fd.readline()
|
||||
if not line:
|
||||
break
|
||||
_log_lines.append(line)
|
||||
if not line.endswith(b'\n'):
|
||||
line += b'\n'
|
||||
newline_warning = True
|
||||
console.addLine(line)
|
||||
if newline_warning:
|
||||
console.addLine('[Zuul] No trailing newline\n')
|
||||
|
||||
|
||||
# Taken from ansible/module_utils/basic.py ... forking the method for now
|
||||
# so that we can dive in and figure out how to make appropriate hook points
|
||||
def zuul_run_command(self, args, zuul_log_id, check_rc=False, close_fds=True, executable=None, data=None, binary_data=False, path_prefix=None, cwd=None,
|
||||
use_unsafe_shell=False, prompt_regex=None, environ_update=None, umask=None, encoding='utf-8', errors='surrogate_or_strict'):
|
||||
'''
|
||||
Execute a command, returns rc, stdout, and stderr.
|
||||
|
||||
:arg args: is the command to run
|
||||
* If args is a list, the command will be run with shell=False.
|
||||
* If args is a string and use_unsafe_shell=False it will split args to a list and run with shell=False
|
||||
* If args is a string and use_unsafe_shell=True it runs with shell=True.
|
||||
:kw check_rc: Whether to call fail_json in case of non zero RC.
|
||||
Default False
|
||||
:kw close_fds: See documentation for subprocess.Popen(). Default True
|
||||
:kw executable: See documentation for subprocess.Popen(). Default None
|
||||
:kw data: If given, information to write to the stdin of the command
|
||||
:kw binary_data: If False, append a newline to the data. Default False
|
||||
:kw path_prefix: If given, additional path to find the command in.
|
||||
This adds to the PATH environment vairable so helper commands in
|
||||
the same directory can also be found
|
||||
:kw cwd: If given, working directory to run the command inside
|
||||
:kw use_unsafe_shell: See `args` parameter. Default False
|
||||
:kw prompt_regex: Regex string (not a compiled regex) which can be
|
||||
used to detect prompts in the stdout which would otherwise cause
|
||||
the execution to hang (especially if no input data is specified)
|
||||
:kw environ_update: dictionary to *update* os.environ with
|
||||
:kw umask: Umask to be used when running the command. Default None
|
||||
:kw encoding: Since we return native strings, on python3 we need to
|
||||
know the encoding to use to transform from bytes to text. If you
|
||||
want to always get bytes back, use encoding=None. The default is
|
||||
"utf-8". This does not affect transformation of strings given as
|
||||
args.
|
||||
:kw errors: Since we return native strings, on python3 we need to
|
||||
transform stdout and stderr from bytes to text. If the bytes are
|
||||
undecodable in the ``encoding`` specified, then use this error
|
||||
handler to deal with them. The default is ``surrogate_or_strict``
|
||||
which means that the bytes will be decoded using the
|
||||
surrogateescape error handler if available (available on all
|
||||
python3 versions we support) otherwise a UnicodeError traceback
|
||||
will be raised. This does not affect transformations of strings
|
||||
given as args.
|
||||
:returns: A 3-tuple of return code (integer), stdout (native string),
|
||||
and stderr (native string). On python2, stdout and stderr are both
|
||||
byte strings. On python3, stdout and stderr are text strings converted
|
||||
according to the encoding and errors parameters. If you want byte
|
||||
strings on python3, use encoding=None to turn decoding to text off.
|
||||
'''
|
||||
|
||||
if not isinstance(args, (list, binary_type, text_type)):
|
||||
msg = "Argument 'args' to run_command must be list or string"
|
||||
self.fail_json(rc=257, cmd=args, msg=msg)
|
||||
|
||||
shell = False
|
||||
if use_unsafe_shell:
|
||||
|
||||
# stringify args for unsafe/direct shell usage
|
||||
if isinstance(args, list):
|
||||
args = " ".join([shlex_quote(x) for x in args])
|
||||
|
||||
# not set explicitly, check if set by controller
|
||||
if executable:
|
||||
args = [executable, '-c', args]
|
||||
elif self._shell not in (None, '/bin/sh'):
|
||||
args = [self._shell, '-c', args]
|
||||
else:
|
||||
shell = True
|
||||
else:
|
||||
# ensure args are a list
|
||||
if isinstance(args, (binary_type, text_type)):
|
||||
# On python2.6 and below, shlex has problems with text type
|
||||
# On python3, shlex needs a text type.
|
||||
if PY2:
|
||||
args = to_bytes(args, errors='surrogate_or_strict')
|
||||
elif PY3:
|
||||
args = to_text(args, errors='surrogateescape')
|
||||
args = shlex.split(args)
|
||||
|
||||
# expand shellisms
|
||||
args = [os.path.expanduser(os.path.expandvars(x)) for x in args if x is not None]
|
||||
|
||||
prompt_re = None
|
||||
if prompt_regex:
|
||||
if isinstance(prompt_regex, text_type):
|
||||
if PY3:
|
||||
prompt_regex = to_bytes(prompt_regex, errors='surrogateescape')
|
||||
elif PY2:
|
||||
prompt_regex = to_bytes(prompt_regex, errors='surrogate_or_strict')
|
||||
try:
|
||||
prompt_re = re.compile(prompt_regex, re.MULTILINE)
|
||||
except re.error:
|
||||
self.fail_json(msg="invalid prompt regular expression given to run_command")
|
||||
|
||||
rc = 0
|
||||
msg = None
|
||||
st_in = None
|
||||
|
||||
# Manipulate the environ we'll send to the new process
|
||||
old_env_vals = {}
|
||||
# We can set this from both an attribute and per call
|
||||
for key, val in self.run_command_environ_update.items():
|
||||
old_env_vals[key] = os.environ.get(key, None)
|
||||
os.environ[key] = val
|
||||
if environ_update:
|
||||
for key, val in environ_update.items():
|
||||
old_env_vals[key] = os.environ.get(key, None)
|
||||
os.environ[key] = val
|
||||
if path_prefix:
|
||||
old_env_vals['PATH'] = os.environ['PATH']
|
||||
os.environ['PATH'] = "%s:%s" % (path_prefix, os.environ['PATH'])
|
||||
|
||||
# If using test-module and explode, the remote lib path will resemble ...
|
||||
# /tmp/test_module_scratch/debug_dir/ansible/module_utils/basic.py
|
||||
# If using ansible or ansible-playbook with a remote system ...
|
||||
# /tmp/ansible_vmweLQ/ansible_modlib.zip/ansible/module_utils/basic.py
|
||||
|
||||
# Clean out python paths set by ansiballz
|
||||
if 'PYTHONPATH' in os.environ:
|
||||
pypaths = os.environ['PYTHONPATH'].split(':')
|
||||
pypaths = [x for x in pypaths
|
||||
if not x.endswith('/ansible_modlib.zip') and
|
||||
not x.endswith('/debug_dir')]
|
||||
os.environ['PYTHONPATH'] = ':'.join(pypaths)
|
||||
if not os.environ['PYTHONPATH']:
|
||||
del os.environ['PYTHONPATH']
|
||||
|
||||
# create a printable version of the command for use
|
||||
# in reporting later, which strips out things like
|
||||
# passwords from the args list
|
||||
to_clean_args = args
|
||||
if PY2:
|
||||
if isinstance(args, text_type):
|
||||
to_clean_args = to_bytes(args)
|
||||
else:
|
||||
if isinstance(args, binary_type):
|
||||
to_clean_args = to_text(args)
|
||||
if isinstance(args, (text_type, binary_type)):
|
||||
to_clean_args = shlex.split(to_clean_args)
|
||||
|
||||
clean_args = []
|
||||
is_passwd = False
|
||||
for arg in (to_native(a) for a in to_clean_args):
|
||||
if is_passwd:
|
||||
is_passwd = False
|
||||
clean_args.append('********')
|
||||
continue
|
||||
if PASSWD_ARG_RE.match(arg):
|
||||
sep_idx = arg.find('=')
|
||||
if sep_idx > -1:
|
||||
clean_args.append('%s=********' % arg[:sep_idx])
|
||||
continue
|
||||
else:
|
||||
is_passwd = True
|
||||
arg = heuristic_log_sanitize(arg, self.no_log_values)
|
||||
clean_args.append(arg)
|
||||
clean_args = ' '.join(shlex_quote(arg) for arg in clean_args)
|
||||
|
||||
if data:
|
||||
st_in = subprocess.PIPE
|
||||
|
||||
# ZUUL: changed stderr to follow stdout
|
||||
kwargs = dict(
|
||||
executable=executable,
|
||||
shell=shell,
|
||||
close_fds=close_fds,
|
||||
stdin=st_in,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
|
||||
# store the pwd
|
||||
prev_dir = os.getcwd()
|
||||
|
||||
# make sure we're in the right working directory
|
||||
if cwd and os.path.isdir(cwd):
|
||||
cwd = os.path.abspath(os.path.expanduser(cwd))
|
||||
kwargs['cwd'] = cwd
|
||||
try:
|
||||
os.chdir(cwd)
|
||||
except (OSError, IOError) as e:
|
||||
self.fail_json(rc=e.errno, msg="Could not open %s, %s" % (cwd, to_native(e)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
old_umask = None
|
||||
if umask:
|
||||
old_umask = os.umask(umask)
|
||||
|
||||
t = None
|
||||
fail_json_kwargs = None
|
||||
|
||||
try:
|
||||
if self._debug:
|
||||
self.log('Executing: ' + clean_args)
|
||||
|
||||
# ZUUL: Replaced the execution loop with the zuul_runner run function
|
||||
|
||||
cmd = subprocess.Popen(args, **kwargs)
|
||||
if self.no_log:
|
||||
t = None
|
||||
else:
|
||||
t = threading.Thread(target=follow, args=(cmd.stdout, zuul_log_id))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
# ZUUL: Our log thread will catch the output so don't do that here.
|
||||
|
||||
# # the communication logic here is essentially taken from that
|
||||
# # of the _communicate() function in ssh.py
|
||||
#
|
||||
# stdout = b('')
|
||||
# stderr = b('')
|
||||
#
|
||||
# # ZUUL: stderr follows stdout
|
||||
# rpipes = [cmd.stdout]
|
||||
|
||||
if data:
|
||||
if not binary_data:
|
||||
data += '\n'
|
||||
if isinstance(data, text_type):
|
||||
data = to_bytes(data)
|
||||
cmd.stdin.write(data)
|
||||
cmd.stdin.close()
|
||||
|
||||
# while True:
|
||||
# rfds, wfds, efds = select.select(rpipes, [], rpipes, 1)
|
||||
# stdout += self._read_from_pipes(rpipes, rfds, cmd.stdout)
|
||||
#
|
||||
# # ZUUL: stderr follows stdout
|
||||
# # stderr += self._read_from_pipes(rpipes, rfds, cmd.stderr)
|
||||
#
|
||||
# # if we're checking for prompts, do it now
|
||||
# if prompt_re:
|
||||
# if prompt_re.search(stdout) and not data:
|
||||
# if encoding:
|
||||
# stdout = to_native(stdout, encoding=encoding, errors=errors)
|
||||
# else:
|
||||
# stdout = stdout
|
||||
# return (257, stdout, "A prompt was encountered while running a command, but no input data was specified")
|
||||
# # only break out if no pipes are left to read or
|
||||
# # the pipes are completely read and
|
||||
# # the process is terminated
|
||||
# if (not rpipes or not rfds) and cmd.poll() is not None:
|
||||
# break
|
||||
# # No pipes are left to read but process is not yet terminated
|
||||
# # Only then it is safe to wait for the process to be finished
|
||||
# # NOTE: Actually cmd.poll() is always None here if rpipes is empty
|
||||
# elif not rpipes and cmd.poll() is None:
|
||||
# cmd.wait()
|
||||
# # The process is terminated. Since no pipes to read from are
|
||||
# # left, there is no need to call select() again.
|
||||
# break
|
||||
|
||||
# ZUUL: If the console log follow thread *is* stuck in readline,
|
||||
# we can't close stdout (attempting to do so raises an
|
||||
# exception) , so this is disabled.
|
||||
# cmd.stdout.close()
|
||||
# cmd.stderr.close()
|
||||
|
||||
rc = cmd.wait()
|
||||
|
||||
# Give the thread that is writing the console log up to 10 seconds
|
||||
# to catch up and exit. If it hasn't done so by then, it is very
|
||||
# likely stuck in readline() because it spawed a child that is
|
||||
# holding stdout or stderr open.
|
||||
if t:
|
||||
t.join(10)
|
||||
with Console(zuul_log_id) as console:
|
||||
if t.is_alive():
|
||||
console.addLine("[Zuul] standard output/error still open "
|
||||
"after child exited")
|
||||
# ZUUL: stdout and stderr are in the console log file
|
||||
# ZUUL: return the saved log lines so we can ship them back
|
||||
stdout = b('').join(_log_lines)
|
||||
else:
|
||||
stdout = b('')
|
||||
stderr = b('')
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
self.log("Error Executing CMD:%s Exception:%s" % (clean_args, to_native(e)))
|
||||
# ZUUL: store fail_json_kwargs and fail later in finally
|
||||
fail_json_kwargs = dict(rc=e.errno, msg=to_native(e), cmd=clean_args)
|
||||
except Exception as e:
|
||||
self.log("Error Executing CMD:%s Exception:%s" % (clean_args, to_native(traceback.format_exc())))
|
||||
# ZUUL: store fail_json_kwargs and fail later in finally
|
||||
fail_json_kwargs = dict(rc=257, msg=to_native(e), exception=traceback.format_exc(), cmd=clean_args)
|
||||
finally:
|
||||
if t:
|
||||
with Console(zuul_log_id) as console:
|
||||
if t.is_alive():
|
||||
console.addLine("[Zuul] standard output/error still open "
|
||||
"after child exited")
|
||||
if fail_json_kwargs:
|
||||
# we hit an exception and need to use the rc from
|
||||
# fail_json_kwargs
|
||||
rc = fail_json_kwargs['rc']
|
||||
|
||||
console.addLine("[Zuul] Task exit code: %s\n" % rc)
|
||||
|
||||
if fail_json_kwargs:
|
||||
self.fail_json(**fail_json_kwargs)
|
||||
|
||||
# Restore env settings
|
||||
for key, val in old_env_vals.items():
|
||||
if val is None:
|
||||
del os.environ[key]
|
||||
else:
|
||||
os.environ[key] = val
|
||||
|
||||
if old_umask:
|
||||
os.umask(old_umask)
|
||||
|
||||
if rc != 0 and check_rc:
|
||||
msg = heuristic_log_sanitize(stderr.rstrip(), self.no_log_values)
|
||||
self.fail_json(cmd=clean_args, rc=rc, stdout=stdout, stderr=stderr, msg=msg)
|
||||
|
||||
# reset the pwd
|
||||
os.chdir(prev_dir)
|
||||
|
||||
if encoding is not None:
|
||||
return (rc, to_native(stdout, encoding=encoding, errors=errors),
|
||||
to_native(stderr, encoding=encoding, errors=errors))
|
||||
return (rc, stdout, stderr)
|
||||
|
||||
|
||||
def check_command(module, commandline):
|
||||
arguments = {'chown': 'owner', 'chmod': 'mode', 'chgrp': 'group',
|
||||
'ln': 'state=link', 'mkdir': 'state=directory',
|
||||
'rmdir': 'state=absent', 'rm': 'state=absent', 'touch': 'state=touch'}
|
||||
commands = {'curl': 'get_url or uri', 'wget': 'get_url or uri',
|
||||
'svn': 'subversion', 'service': 'service',
|
||||
'mount': 'mount', 'rpm': 'yum, dnf or zypper', 'yum': 'yum', 'apt-get': 'apt',
|
||||
'tar': 'unarchive', 'unzip': 'unarchive', 'sed': 'replace, lineinfile or template',
|
||||
'dnf': 'dnf', 'zypper': 'zypper'}
|
||||
become = ['sudo', 'su', 'pbrun', 'pfexec', 'runas', 'pmrun', 'machinectl']
|
||||
if isinstance(commandline, list):
|
||||
command = commandline[0]
|
||||
else:
|
||||
command = commandline.split()[0]
|
||||
command = os.path.basename(command)
|
||||
|
||||
disable_suffix = "If you need to use command because {mod} is insufficient you can add" \
|
||||
" warn=False to this command task or set command_warnings=False in" \
|
||||
" ansible.cfg to get rid of this message."
|
||||
substitutions = {'mod': None, 'cmd': command}
|
||||
|
||||
if command in arguments:
|
||||
msg = "Consider using the {mod} module with {subcmd} rather than running {cmd}. " + disable_suffix
|
||||
substitutions['mod'] = 'file'
|
||||
substitutions['subcmd'] = arguments[command]
|
||||
module.warn(msg.format(**substitutions))
|
||||
|
||||
if command in commands:
|
||||
msg = "Consider using the {mod} module rather than running {cmd}. " + disable_suffix
|
||||
substitutions['mod'] = commands[command]
|
||||
module.warn(msg.format(**substitutions))
|
||||
|
||||
if command in become:
|
||||
module.warn("Consider using 'become', 'become_method', and 'become_user' rather than running %s" % (command,))
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
# the command module is the one ansible module that does not take key=value args
|
||||
# hence don't copy this one if you are looking to build others!
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
_raw_params=dict(),
|
||||
_uses_shell=dict(type='bool', default=False),
|
||||
argv=dict(type='list'),
|
||||
chdir=dict(type='path'),
|
||||
executable=dict(),
|
||||
creates=dict(type='path'),
|
||||
removes=dict(type='path'),
|
||||
# The default for this really comes from the action plugin
|
||||
warn=dict(type='bool', default=True),
|
||||
stdin=dict(required=False),
|
||||
zuul_log_id=dict(type='str'),
|
||||
)
|
||||
)
|
||||
shell = module.params['_uses_shell']
|
||||
chdir = module.params['chdir']
|
||||
executable = module.params['executable']
|
||||
args = module.params['_raw_params']
|
||||
argv = module.params['argv']
|
||||
creates = module.params['creates']
|
||||
removes = module.params['removes']
|
||||
warn = module.params['warn']
|
||||
stdin = module.params['stdin']
|
||||
zuul_log_id = module.params['zuul_log_id']
|
||||
|
||||
if not shell and executable:
|
||||
module.warn("As of Ansible 2.4, the parameter 'executable' is no longer supported with the 'command' module. Not using '%s'." % executable)
|
||||
executable = None
|
||||
|
||||
if not zuul_log_id:
|
||||
module.fail_json(rc=256, msg="zuul_log_id missing: %s" % module.params)
|
||||
|
||||
if (not args or args.strip() == '') and not argv:
|
||||
module.fail_json(rc=256, msg="no command given")
|
||||
|
||||
if args and argv:
|
||||
module.fail_json(rc=256, msg="only command or argv can be given, not both")
|
||||
|
||||
if not shell and args:
|
||||
args = shlex.split(args)
|
||||
|
||||
args = args or argv
|
||||
|
||||
if chdir:
|
||||
chdir = os.path.abspath(chdir)
|
||||
os.chdir(chdir)
|
||||
|
||||
if creates:
|
||||
# do not run the command if the line contains creates=filename
|
||||
# and the filename already exists. This allows idempotence
|
||||
# of command executions.
|
||||
if glob.glob(creates):
|
||||
module.exit_json(
|
||||
cmd=args,
|
||||
stdout="skipped, since %s exists" % creates,
|
||||
changed=False,
|
||||
rc=0
|
||||
)
|
||||
|
||||
if removes:
|
||||
# do not run the command if the line contains removes=filename
|
||||
# and the filename does not exist. This allows idempotence
|
||||
# of command executions.
|
||||
if not glob.glob(removes):
|
||||
module.exit_json(
|
||||
cmd=args,
|
||||
stdout="skipped, since %s does not exist" % removes,
|
||||
changed=False,
|
||||
rc=0
|
||||
)
|
||||
|
||||
if warn:
|
||||
check_command(module, args)
|
||||
|
||||
startd = datetime.datetime.now()
|
||||
|
||||
rc, out, err = zuul_run_command(module, args, zuul_log_id, executable=executable, use_unsafe_shell=shell, encoding=None, data=stdin)
|
||||
|
||||
endd = datetime.datetime.now()
|
||||
delta = endd - startd
|
||||
|
||||
result = dict(
|
||||
cmd=args,
|
||||
stdout=out.rstrip(b"\r\n"),
|
||||
stderr=err.rstrip(b"\r\n"),
|
||||
rc=rc,
|
||||
start=str(startd),
|
||||
end=str(endd),
|
||||
delta=str(delta),
|
||||
changed=True,
|
||||
zuul_log_id=zuul_log_id
|
||||
)
|
||||
|
||||
if rc != 0:
|
||||
module.fail_json(msg='non-zero return code', **result)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1 @@
|
|||
../../base/action/__init__.py
|
|
@ -0,0 +1 @@
|
|||
../../base/action/command.py
|
|
@ -0,0 +1 @@
|
|||
../../base/action/command.pyi
|
|
@ -0,0 +1 @@
|
|||
../../base/action/zuul_return.py
|
|
@ -0,0 +1 @@
|
|||
../../base/callback/__init__.py
|
|
@ -0,0 +1 @@
|
|||
../../base/callback/zuul_json.py
|
|
@ -0,0 +1 @@
|
|||
../../base/callback/zuul_stream.py
|
|
@ -0,0 +1 @@
|
|||
../../base/callback/zuul_unreachable.py
|
|
@ -0,0 +1 @@
|
|||
../../base/filter/__init__.py
|
|
@ -0,0 +1 @@
|
|||
../../base/filter/zuul_filters.py
|
|
@ -0,0 +1 @@
|
|||
../../base/library/__init__.py
|
|
@ -0,0 +1 @@
|
|||
../../base/library/command.py
|
|
@ -0,0 +1 @@
|
|||
../../base/library/zuul_console.py
|
|
@ -0,0 +1 @@
|
|||
../logconfig.py
|
|
@ -0,0 +1 @@
|
|||
../paths.py
|
|
@ -3,18 +3,12 @@
|
|||
|
||||
# Copyright: (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>, and others
|
||||
# Copyright: (c) 2016, Toshio Kuratomi <tkuratomi@ansible.com>
|
||||
#
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['stableinterface'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
|
||||
# flake8: noqa
|
||||
# This file shares a significant chunk of code with an upstream ansible
|
||||
# function, run_command. The goal is to not have to fork quite so much
|
||||
|
@ -24,115 +18,215 @@ ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|||
# would remove our ability to easily have a discussion with our friends
|
||||
# upstream
|
||||
|
||||
DOCUMENTATION = '''
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: command
|
||||
short_description: Executes a command on a remote node
|
||||
short_description: Execute commands on targets
|
||||
version_added: historical
|
||||
description:
|
||||
- The C(command) module takes the command name followed by a list of space-delimited arguments.
|
||||
- The given command will be executed on all selected nodes. It will not be
|
||||
processed through the shell, so variables like C($HOME) and operations
|
||||
like C("<"), C(">"), C("|"), C(";") and C("&") will not work (use the M(shell)
|
||||
module if you need these features).
|
||||
- For Windows targets, use the M(win_command) module instead.
|
||||
- The given command will be executed on all selected nodes.
|
||||
- The command(s) will not be
|
||||
processed through the shell, so variables like C($HOSTNAME) and operations
|
||||
like C("*"), C("<"), C(">"), C("|"), C(";") and C("&") will not work.
|
||||
Use the M(ansible.builtin.shell) module if you need these features.
|
||||
- To create C(command) tasks that are easier to read than the ones using space-delimited
|
||||
arguments, pass parameters using the C(args) L(task keyword,../reference_appendices/playbooks_keywords.html#task)
|
||||
or use C(cmd) parameter.
|
||||
- Either a free form command or C(cmd) parameter is required, see the examples.
|
||||
- For Windows targets, use the M(ansible.windows.win_command) module instead.
|
||||
extends_documentation_fragment:
|
||||
- action_common_attributes
|
||||
- action_common_attributes.raw
|
||||
attributes:
|
||||
check_mode:
|
||||
details: while the command itself is arbitrary and cannot be subject to the check mode semantics it adds C(creates)/C(removes) options as a workaround
|
||||
support: partial
|
||||
diff_mode:
|
||||
support: none
|
||||
platform:
|
||||
support: full
|
||||
platforms: posix
|
||||
raw:
|
||||
support: full
|
||||
options:
|
||||
free_form:
|
||||
description:
|
||||
- The command module takes a free form command to run. There is no parameter actually named 'free form'.
|
||||
See the examples!
|
||||
required: yes
|
||||
argv:
|
||||
- The command module takes a free form string as a command to run.
|
||||
- There is no actual parameter named 'free form'.
|
||||
cmd:
|
||||
type: str
|
||||
description:
|
||||
- Allows the user to provide the command as a list vs. a string. Only the string or the list form can be
|
||||
provided, not both. One or the other must be provided.
|
||||
- The command to run.
|
||||
argv:
|
||||
type: list
|
||||
elements: str
|
||||
description:
|
||||
- Passes the command as a list rather than a string.
|
||||
- Use C(argv) to avoid quoting values that would otherwise be interpreted incorrectly (for example "user name").
|
||||
- Only the string (free form) or the list (argv) form can be provided, not both. One or the other must be provided.
|
||||
version_added: "2.6"
|
||||
creates:
|
||||
type: path
|
||||
description:
|
||||
- A filename or (since 2.0) glob pattern, when it already exists, this step will B(not) be run.
|
||||
- A filename or (since 2.0) glob pattern. If a matching file already exists, this step B(will not) be run.
|
||||
- This is checked before I(removes) is checked.
|
||||
removes:
|
||||
type: path
|
||||
description:
|
||||
- A filename or (since 2.0) glob pattern, when it does not exist, this step will B(not) be run.
|
||||
- A filename or (since 2.0) glob pattern. If a matching file exists, this step B(will) be run.
|
||||
- This is checked after I(creates) is checked.
|
||||
version_added: "0.8"
|
||||
chdir:
|
||||
type: path
|
||||
description:
|
||||
- Change into this directory before running the command.
|
||||
version_added: "0.6"
|
||||
warn:
|
||||
description:
|
||||
- If command_warnings are on in ansible.cfg, do not warn about this particular line if set to C(no).
|
||||
- (deprecated) Enable or disable task warnings.
|
||||
- This feature is deprecated and will be removed in 2.14.
|
||||
- As of version 2.11, this option is now disabled by default.
|
||||
type: bool
|
||||
default: 'yes'
|
||||
default: no
|
||||
version_added: "1.8"
|
||||
stdin:
|
||||
version_added: "2.4"
|
||||
description:
|
||||
- Set the stdin of the command directly to the specified value.
|
||||
type: str
|
||||
version_added: "2.4"
|
||||
stdin_add_newline:
|
||||
type: bool
|
||||
default: yes
|
||||
description:
|
||||
- If set to C(yes), append a newline to stdin data.
|
||||
version_added: "2.8"
|
||||
strip_empty_ends:
|
||||
description:
|
||||
- Strip empty lines from the end of stdout/stderr in result.
|
||||
version_added: "2.8"
|
||||
type: bool
|
||||
default: yes
|
||||
notes:
|
||||
- If you want to run a command through the shell (say you are using C(<), C(>), C(|), etc), you actually want the M(shell) module instead.
|
||||
- If you want to run a command through the shell (say you are using C(<), C(>), C(|), and so on),
|
||||
you actually want the M(ansible.builtin.shell) module instead.
|
||||
Parsing shell metacharacters can lead to unexpected commands being executed if quoting is not done correctly so it is more secure to
|
||||
use the C(command) module when possible.
|
||||
- " C(creates), C(removes), and C(chdir) can be specified after the command.
|
||||
For instance, if you only want to run a command if a certain file does not exist, use this."
|
||||
- The C(executable) parameter is removed since version 2.4. If you have a need for this parameter, use the M(shell) module instead.
|
||||
- For Windows targets, use the M(win_command) module instead.
|
||||
- C(creates), C(removes), and C(chdir) can be specified after the command.
|
||||
For instance, if you only want to run a command if a certain file does not exist, use this.
|
||||
- Check mode is supported when passing C(creates) or C(removes). If running in check mode and either of these are specified, the module will
|
||||
check for the existence of the file and report the correct changed status. If these are not supplied, the task will be skipped.
|
||||
- The C(executable) parameter is removed since version 2.4. If you have a need for this parameter, use the M(ansible.builtin.shell) module instead.
|
||||
- For Windows targets, use the M(ansible.windows.win_command) module instead.
|
||||
- For rebooting systems, use the M(ansible.builtin.reboot) or M(ansible.windows.win_reboot) module.
|
||||
seealso:
|
||||
- module: ansible.builtin.raw
|
||||
- module: ansible.builtin.script
|
||||
- module: ansible.builtin.shell
|
||||
- module: ansible.windows.win_command
|
||||
author:
|
||||
- Ansible Core Team
|
||||
- Michael DeHaan
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: return motd to registered var
|
||||
command: cat /etc/motd
|
||||
EXAMPLES = r'''
|
||||
- name: Return motd to registered var
|
||||
ansible.builtin.command: cat /etc/motd
|
||||
register: mymotd
|
||||
|
||||
- name: Run the command if the specified file does not exist.
|
||||
command: /usr/bin/make_database.sh arg1 arg2
|
||||
# free-form (string) arguments, all arguments on one line
|
||||
- name: Run command if /path/to/database does not exist (without 'args')
|
||||
ansible.builtin.command: /usr/bin/make_database.sh db_user db_name creates=/path/to/database
|
||||
|
||||
# free-form (string) arguments, some arguments on separate lines with the 'args' keyword
|
||||
# 'args' is a task keyword, passed at the same level as the module
|
||||
- name: Run command if /path/to/database does not exist (with 'args' keyword)
|
||||
ansible.builtin.command: /usr/bin/make_database.sh db_user db_name
|
||||
args:
|
||||
creates: /path/to/database
|
||||
|
||||
# You can also use the 'args' form to provide the options.
|
||||
- name: This command will change the working directory to somedir/ and will only run when /path/to/database doesn't exist.
|
||||
command: /usr/bin/make_database.sh arg1 arg2
|
||||
# 'cmd' is module parameter
|
||||
- name: Run command if /path/to/database does not exist (with 'cmd' parameter)
|
||||
ansible.builtin.command:
|
||||
cmd: /usr/bin/make_database.sh db_user db_name
|
||||
creates: /path/to/database
|
||||
|
||||
- name: Change the working directory to somedir/ and run the command as db_owner if /path/to/database does not exist
|
||||
ansible.builtin.command: /usr/bin/make_database.sh db_user db_name
|
||||
become: yes
|
||||
become_user: db_owner
|
||||
args:
|
||||
chdir: somedir/
|
||||
creates: /path/to/database
|
||||
|
||||
- name: use argv to send the command as a list. Be sure to leave command empty
|
||||
command:
|
||||
args:
|
||||
# argv (list) arguments, each argument on a separate line, 'args' keyword not necessary
|
||||
# 'argv' is a parameter, indented one level from the module
|
||||
- name: Use 'argv' to send a command as a list - leave 'command' empty
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- echo
|
||||
- testing
|
||||
- /usr/bin/make_database.sh
|
||||
- Username with whitespace
|
||||
- dbname with whitespace
|
||||
creates: /path/to/database
|
||||
|
||||
- name: safely use templated variable to run command. Always use the quote filter to avoid injection issues.
|
||||
command: cat {{ myfile|quote }}
|
||||
- name: Safely use templated variable to run command. Always use the quote filter to avoid injection issues
|
||||
ansible.builtin.command: cat {{ myfile|quote }}
|
||||
register: myoutput
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
RETURN = r'''
|
||||
msg:
|
||||
description: changed
|
||||
returned: always
|
||||
type: bool
|
||||
sample: True
|
||||
start:
|
||||
description: The command execution start time.
|
||||
returned: always
|
||||
type: str
|
||||
sample: '2017-09-29 22:03:48.083128'
|
||||
end:
|
||||
description: The command execution end time.
|
||||
returned: always
|
||||
type: str
|
||||
sample: '2017-09-29 22:03:48.084657'
|
||||
delta:
|
||||
description: The command execution delta time.
|
||||
returned: always
|
||||
type: str
|
||||
sample: '0:00:00.001529'
|
||||
stdout:
|
||||
description: The command standard output.
|
||||
returned: always
|
||||
type: str
|
||||
sample: 'Clustering node rabbit@slave1 with rabbit@master …'
|
||||
stderr:
|
||||
description: The command standard error.
|
||||
returned: always
|
||||
type: str
|
||||
sample: 'ls cannot access foo: No such file or directory'
|
||||
cmd:
|
||||
description: the cmd that was run on the remote machine
|
||||
description: The command executed by the task.
|
||||
returned: always
|
||||
type: list
|
||||
sample:
|
||||
- echo
|
||||
- hello
|
||||
delta:
|
||||
description: cmd end time - cmd start time
|
||||
rc:
|
||||
description: The command return code (0 means success).
|
||||
returned: always
|
||||
type: string
|
||||
sample: 0:00:00.001529
|
||||
end:
|
||||
description: cmd end time
|
||||
type: int
|
||||
sample: 0
|
||||
stdout_lines:
|
||||
description: The command standard output split in lines.
|
||||
returned: always
|
||||
type: string
|
||||
sample: '2017-09-29 22:03:48.084657'
|
||||
start:
|
||||
description: cmd start time
|
||||
type: list
|
||||
sample: [u'Clustering node rabbit@slave1 with rabbit@master …']
|
||||
stderr_lines:
|
||||
description: The command standard error split in lines.
|
||||
returned: always
|
||||
type: string
|
||||
sample: '2017-09-29 22:03:48.083128'
|
||||
type: list
|
||||
sample: [u'ls cannot access foo: No such file or directory', u'ls …']
|
||||
'''
|
||||
|
||||
import datetime
|
||||
|
@ -141,6 +235,8 @@ import os
|
|||
import shlex
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native, to_bytes, to_text
|
||||
from ansible.module_utils.common.collections import is_iterable
|
||||
|
||||
# Imports needed for Zuul things
|
||||
import re
|
||||
|
@ -157,7 +253,6 @@ from ansible.module_utils.six import (
|
|||
text_type,
|
||||
)
|
||||
from ansible.module_utils.six.moves import shlex_quote
|
||||
from ansible.module_utils._text import to_native, to_bytes, to_text
|
||||
|
||||
|
||||
LOG_STREAM_FILE = '/tmp/console-{log_uuid}.log'
|
||||
|
@ -211,7 +306,8 @@ def follow(fd, log_uuid):
|
|||
# Taken from ansible/module_utils/basic.py ... forking the method for now
|
||||
# so that we can dive in and figure out how to make appropriate hook points
|
||||
def zuul_run_command(self, args, zuul_log_id, check_rc=False, close_fds=True, executable=None, data=None, binary_data=False, path_prefix=None, cwd=None,
|
||||
use_unsafe_shell=False, prompt_regex=None, environ_update=None, umask=None, encoding='utf-8', errors='surrogate_or_strict'):
|
||||
use_unsafe_shell=False, prompt_regex=None, environ_update=None, umask=None, encoding='utf-8', errors='surrogate_or_strict',
|
||||
expand_user_and_vars=True, pass_fds=None, before_communicate_callback=None, ignore_invalid_cwd=True):
|
||||
'''
|
||||
Execute a command, returns rc, stdout, and stderr.
|
||||
|
||||
|
@ -226,14 +322,14 @@ def zuul_run_command(self, args, zuul_log_id, check_rc=False, close_fds=True, ex
|
|||
:kw data: If given, information to write to the stdin of the command
|
||||
:kw binary_data: If False, append a newline to the data. Default False
|
||||
:kw path_prefix: If given, additional path to find the command in.
|
||||
This adds to the PATH environment vairable so helper commands in
|
||||
This adds to the PATH environment variable so helper commands in
|
||||
the same directory can also be found
|
||||
:kw cwd: If given, working directory to run the command inside
|
||||
:kw use_unsafe_shell: See `args` parameter. Default False
|
||||
:kw prompt_regex: Regex string (not a compiled regex) which can be
|
||||
used to detect prompts in the stdout which would otherwise cause
|
||||
the execution to hang (especially if no input data is specified)
|
||||
:kw environ_update: dictionary to *update* os.environ with
|
||||
:kw environ_update: dictionary to *update* environ variables with
|
||||
:kw umask: Umask to be used when running the command. Default None
|
||||
:kw encoding: Since we return native strings, on python3 we need to
|
||||
know the encoding to use to transform from bytes to text. If you
|
||||
|
@ -249,12 +345,30 @@ def zuul_run_command(self, args, zuul_log_id, check_rc=False, close_fds=True, ex
|
|||
python3 versions we support) otherwise a UnicodeError traceback
|
||||
will be raised. This does not affect transformations of strings
|
||||
given as args.
|
||||
:kw expand_user_and_vars: When ``use_unsafe_shell=False`` this argument
|
||||
dictates whether ``~`` is expanded in paths and environment variables
|
||||
are expanded before running the command. When ``True`` a string such as
|
||||
``$SHELL`` will be expanded regardless of escaping. When ``False`` and
|
||||
``use_unsafe_shell=False`` no path or variable expansion will be done.
|
||||
:kw pass_fds: When running on Python 3 this argument
|
||||
dictates which file descriptors should be passed
|
||||
to an underlying ``Popen`` constructor. On Python 2, this will
|
||||
set ``close_fds`` to False.
|
||||
:kw before_communicate_callback: This function will be called
|
||||
after ``Popen`` object will be created
|
||||
but before communicating to the process.
|
||||
(``Popen`` object will be passed to callback as a first argument)
|
||||
:kw ignore_invalid_cwd: This flag indicates whether an invalid ``cwd``
|
||||
(non-existent or not a directory) should be ignored or should raise
|
||||
an exception.
|
||||
:returns: A 3-tuple of return code (integer), stdout (native string),
|
||||
and stderr (native string). On python2, stdout and stderr are both
|
||||
byte strings. On python3, stdout and stderr are text strings converted
|
||||
according to the encoding and errors parameters. If you want byte
|
||||
strings on python3, use encoding=None to turn decoding to text off.
|
||||
'''
|
||||
# used by clean args later on
|
||||
self._clean = None
|
||||
|
||||
if not isinstance(args, (list, binary_type, text_type)):
|
||||
msg = "Argument 'args' to run_command must be list or string"
|
||||
|
@ -265,13 +379,16 @@ def zuul_run_command(self, args, zuul_log_id, check_rc=False, close_fds=True, ex
|
|||
|
||||
# stringify args for unsafe/direct shell usage
|
||||
if isinstance(args, list):
|
||||
args = " ".join([shlex_quote(x) for x in args])
|
||||
args = b" ".join([to_bytes(shlex_quote(x), errors='surrogate_or_strict') for x in args])
|
||||
else:
|
||||
args = to_bytes(args, errors='surrogate_or_strict')
|
||||
|
||||
# not set explicitly, check if set by controller
|
||||
if executable:
|
||||
args = [executable, '-c', args]
|
||||
executable = to_bytes(executable, errors='surrogate_or_strict')
|
||||
args = [executable, b'-c', args]
|
||||
elif self._shell not in (None, '/bin/sh'):
|
||||
args = [self._shell, '-c', args]
|
||||
args = [to_bytes(self._shell, errors='surrogate_or_strict'), b'-c', args]
|
||||
else:
|
||||
shell = True
|
||||
else:
|
||||
|
@ -285,8 +402,11 @@ def zuul_run_command(self, args, zuul_log_id, check_rc=False, close_fds=True, ex
|
|||
args = to_text(args, errors='surrogateescape')
|
||||
args = shlex.split(args)
|
||||
|
||||
# expand shellisms
|
||||
args = [os.path.expanduser(os.path.expandvars(x)) for x in args if x is not None]
|
||||
# expand ``~`` in paths, and all environment vars
|
||||
if expand_user_and_vars:
|
||||
args = [to_bytes(os.path.expanduser(os.path.expandvars(x)), errors='surrogate_or_strict') for x in args if x is not None]
|
||||
else:
|
||||
args = [to_bytes(x, errors='surrogate_or_strict') for x in args if x is not None]
|
||||
|
||||
prompt_re = None
|
||||
if prompt_regex:
|
||||
|
@ -304,69 +424,39 @@ def zuul_run_command(self, args, zuul_log_id, check_rc=False, close_fds=True, ex
|
|||
msg = None
|
||||
st_in = None
|
||||
|
||||
# Manipulate the environ we'll send to the new process
|
||||
old_env_vals = {}
|
||||
env = os.environ.copy()
|
||||
# We can set this from both an attribute and per call
|
||||
for key, val in self.run_command_environ_update.items():
|
||||
old_env_vals[key] = os.environ.get(key, None)
|
||||
os.environ[key] = val
|
||||
if environ_update:
|
||||
for key, val in environ_update.items():
|
||||
old_env_vals[key] = os.environ.get(key, None)
|
||||
os.environ[key] = val
|
||||
env.update(self.run_command_environ_update or {})
|
||||
env.update(environ_update or {})
|
||||
if path_prefix:
|
||||
old_env_vals['PATH'] = os.environ['PATH']
|
||||
os.environ['PATH'] = "%s:%s" % (path_prefix, os.environ['PATH'])
|
||||
path = env.get('PATH', '')
|
||||
if path:
|
||||
env['PATH'] = "%s:%s" % (path_prefix, path)
|
||||
else:
|
||||
env['PATH'] = path_prefix
|
||||
|
||||
# If using test-module and explode, the remote lib path will resemble ...
|
||||
# If using test-module.py and explode, the remote lib path will resemble:
|
||||
# /tmp/test_module_scratch/debug_dir/ansible/module_utils/basic.py
|
||||
# If using ansible or ansible-playbook with a remote system ...
|
||||
# If using ansible or ansible-playbook with a remote system:
|
||||
# /tmp/ansible_vmweLQ/ansible_modlib.zip/ansible/module_utils/basic.py
|
||||
|
||||
# Clean out python paths set by ansiballz
|
||||
if 'PYTHONPATH' in os.environ:
|
||||
pypaths = os.environ['PYTHONPATH'].split(':')
|
||||
pypaths = [x for x in pypaths
|
||||
if not x.endswith('/ansible_modlib.zip') and
|
||||
if 'PYTHONPATH' in env:
|
||||
pypaths = [x for x in env['PYTHONPATH'].split(':')
|
||||
if x and
|
||||
not x.endswith('/ansible_modlib.zip') and
|
||||
not x.endswith('/debug_dir')]
|
||||
os.environ['PYTHONPATH'] = ':'.join(pypaths)
|
||||
if not os.environ['PYTHONPATH']:
|
||||
del os.environ['PYTHONPATH']
|
||||
|
||||
# create a printable version of the command for use
|
||||
# in reporting later, which strips out things like
|
||||
# passwords from the args list
|
||||
to_clean_args = args
|
||||
if PY2:
|
||||
if isinstance(args, text_type):
|
||||
to_clean_args = to_bytes(args)
|
||||
else:
|
||||
if isinstance(args, binary_type):
|
||||
to_clean_args = to_text(args)
|
||||
if isinstance(args, (text_type, binary_type)):
|
||||
to_clean_args = shlex.split(to_clean_args)
|
||||
|
||||
clean_args = []
|
||||
is_passwd = False
|
||||
for arg in (to_native(a) for a in to_clean_args):
|
||||
if is_passwd:
|
||||
is_passwd = False
|
||||
clean_args.append('********')
|
||||
continue
|
||||
if PASSWD_ARG_RE.match(arg):
|
||||
sep_idx = arg.find('=')
|
||||
if sep_idx > -1:
|
||||
clean_args.append('%s=********' % arg[:sep_idx])
|
||||
continue
|
||||
else:
|
||||
is_passwd = True
|
||||
arg = heuristic_log_sanitize(arg, self.no_log_values)
|
||||
clean_args.append(arg)
|
||||
clean_args = ' '.join(shlex_quote(arg) for arg in clean_args)
|
||||
if pypaths and any(pypaths):
|
||||
env['PYTHONPATH'] = ':'.join(pypaths)
|
||||
|
||||
if data:
|
||||
st_in = subprocess.PIPE
|
||||
|
||||
def preexec():
|
||||
self._restore_signal_handlers()
|
||||
if umask:
|
||||
os.umask(umask)
|
||||
|
||||
# ZUUL: changed stderr to follow stdout
|
||||
kwargs = dict(
|
||||
executable=executable,
|
||||
|
@ -375,35 +465,35 @@ def zuul_run_command(self, args, zuul_log_id, check_rc=False, close_fds=True, ex
|
|||
stdin=st_in,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
preexec_fn=preexec,
|
||||
env=env,
|
||||
)
|
||||
|
||||
# store the pwd
|
||||
prev_dir = os.getcwd()
|
||||
if PY3 and pass_fds:
|
||||
kwargs["pass_fds"] = pass_fds
|
||||
elif PY2 and pass_fds:
|
||||
kwargs['close_fds'] = False
|
||||
|
||||
# make sure we're in the right working directory
|
||||
if cwd and os.path.isdir(cwd):
|
||||
cwd = os.path.abspath(os.path.expanduser(cwd))
|
||||
kwargs['cwd'] = cwd
|
||||
try:
|
||||
os.chdir(cwd)
|
||||
except (OSError, IOError) as e:
|
||||
self.fail_json(rc=e.errno, msg="Could not open %s, %s" % (cwd, to_native(e)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
old_umask = None
|
||||
if umask:
|
||||
old_umask = os.umask(umask)
|
||||
if cwd:
|
||||
cwd = to_bytes(os.path.abspath(os.path.expanduser(cwd)), errors='surrogate_or_strict')
|
||||
if os.path.isdir(cwd):
|
||||
kwargs['cwd'] = cwd
|
||||
elif not ignore_invalid_cwd:
|
||||
self.fail_json(msg="Provided cwd is not a valid directory: %s" % cwd)
|
||||
|
||||
t = None
|
||||
fail_json_kwargs = None
|
||||
|
||||
try:
|
||||
if self._debug:
|
||||
self.log('Executing: ' + clean_args)
|
||||
self.log('Executing: ' + self._clean_args(args))
|
||||
|
||||
# ZUUL: Replaced the execution loop with the zuul_runner run function
|
||||
|
||||
cmd = subprocess.Popen(args, **kwargs)
|
||||
if before_communicate_callback:
|
||||
before_communicate_callback(cmd)
|
||||
|
||||
if self.no_log:
|
||||
t = None
|
||||
else:
|
||||
|
@ -413,15 +503,6 @@ def zuul_run_command(self, args, zuul_log_id, check_rc=False, close_fds=True, ex
|
|||
|
||||
# ZUUL: Our log thread will catch the output so don't do that here.
|
||||
|
||||
# # the communication logic here is essentially taken from that
|
||||
# # of the _communicate() function in ssh.py
|
||||
#
|
||||
# stdout = b('')
|
||||
# stderr = b('')
|
||||
#
|
||||
# # ZUUL: stderr follows stdout
|
||||
# rpipes = [cmd.stdout]
|
||||
|
||||
if data:
|
||||
if not binary_data:
|
||||
data += '\n'
|
||||
|
@ -430,35 +511,6 @@ def zuul_run_command(self, args, zuul_log_id, check_rc=False, close_fds=True, ex
|
|||
cmd.stdin.write(data)
|
||||
cmd.stdin.close()
|
||||
|
||||
# while True:
|
||||
# rfds, wfds, efds = select.select(rpipes, [], rpipes, 1)
|
||||
# stdout += self._read_from_pipes(rpipes, rfds, cmd.stdout)
|
||||
#
|
||||
# # ZUUL: stderr follows stdout
|
||||
# # stderr += self._read_from_pipes(rpipes, rfds, cmd.stderr)
|
||||
#
|
||||
# # if we're checking for prompts, do it now
|
||||
# if prompt_re:
|
||||
# if prompt_re.search(stdout) and not data:
|
||||
# if encoding:
|
||||
# stdout = to_native(stdout, encoding=encoding, errors=errors)
|
||||
# else:
|
||||
# stdout = stdout
|
||||
# return (257, stdout, "A prompt was encountered while running a command, but no input data was specified")
|
||||
# # only break out if no pipes are left to read or
|
||||
# # the pipes are completely read and
|
||||
# # the process is terminated
|
||||
# if (not rpipes or not rfds) and cmd.poll() is not None:
|
||||
# break
|
||||
# # No pipes are left to read but process is not yet terminated
|
||||
# # Only then it is safe to wait for the process to be finished
|
||||
# # NOTE: Actually cmd.poll() is always None here if rpipes is empty
|
||||
# elif not rpipes and cmd.poll() is None:
|
||||
# cmd.wait()
|
||||
# # The process is terminated. Since no pipes to read from are
|
||||
# # left, there is no need to call select() again.
|
||||
# break
|
||||
|
||||
# ZUUL: If the console log follow thread *is* stuck in readline,
|
||||
# we can't close stdout (attempting to do so raises an
|
||||
# exception) , so this is disabled.
|
||||
|
@ -467,10 +519,10 @@ def zuul_run_command(self, args, zuul_log_id, check_rc=False, close_fds=True, ex
|
|||
|
||||
rc = cmd.wait()
|
||||
|
||||
# Give the thread that is writing the console log up to 10 seconds
|
||||
# to catch up and exit. If it hasn't done so by then, it is very
|
||||
# likely stuck in readline() because it spawed a child that is
|
||||
# holding stdout or stderr open.
|
||||
# ZUUL: Give the thread that is writing the console log up to
|
||||
# 10 seconds to catch up and exit. If it hasn't done so by
|
||||
# then, it is very likely stuck in readline() because it
|
||||
# spawed a child that is holding stdout or stderr open.
|
||||
if t:
|
||||
t.join(10)
|
||||
with Console(zuul_log_id) as console:
|
||||
|
@ -485,13 +537,13 @@ def zuul_run_command(self, args, zuul_log_id, check_rc=False, close_fds=True, ex
|
|||
stderr = b('')
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
self.log("Error Executing CMD:%s Exception:%s" % (clean_args, to_native(e)))
|
||||
self.log("Error Executing CMD:%s Exception:%s" % (self._clean_args(args), to_native(e)))
|
||||
# ZUUL: store fail_json_kwargs and fail later in finally
|
||||
fail_json_kwargs = dict(rc=e.errno, msg=to_native(e), cmd=clean_args)
|
||||
fail_json_kwargs = dict(rc=e.errno, stdout=b'', stderr=b'', msg=to_native(e), cmd=self._clean_args(args))
|
||||
except Exception as e:
|
||||
self.log("Error Executing CMD:%s Exception:%s" % (clean_args, to_native(traceback.format_exc())))
|
||||
self.log("Error Executing CMD:%s Exception:%s" % (self._clean_args(args), to_native(traceback.format_exc())))
|
||||
# ZUUL: store fail_json_kwargs and fail later in finally
|
||||
fail_json_kwargs = dict(rc=257, msg=to_native(e), exception=traceback.format_exc(), cmd=clean_args)
|
||||
fail_json_kwargs = dict(rc=257, stdout=b'', stderr=b'', msg=to_native(e), exception=traceback.format_exc(), cmd=self._clean_args(args))
|
||||
finally:
|
||||
if t:
|
||||
with Console(zuul_log_id) as console:
|
||||
|
@ -508,26 +560,14 @@ def zuul_run_command(self, args, zuul_log_id, check_rc=False, close_fds=True, ex
|
|||
if fail_json_kwargs:
|
||||
self.fail_json(**fail_json_kwargs)
|
||||
|
||||
# Restore env settings
|
||||
for key, val in old_env_vals.items():
|
||||
if val is None:
|
||||
del os.environ[key]
|
||||
else:
|
||||
os.environ[key] = val
|
||||
|
||||
if old_umask:
|
||||
os.umask(old_umask)
|
||||
|
||||
if rc != 0 and check_rc:
|
||||
msg = heuristic_log_sanitize(stderr.rstrip(), self.no_log_values)
|
||||
self.fail_json(cmd=clean_args, rc=rc, stdout=stdout, stderr=stderr, msg=msg)
|
||||
|
||||
# reset the pwd
|
||||
os.chdir(prev_dir)
|
||||
self.fail_json(cmd=self._clean_args(args), rc=rc, stdout=stdout, stderr=stderr, msg=msg)
|
||||
|
||||
if encoding is not None:
|
||||
return (rc, to_native(stdout, encoding=encoding, errors=errors),
|
||||
to_native(stderr, encoding=encoding, errors=errors))
|
||||
|
||||
return (rc, stdout, stderr)
|
||||
|
||||
|
||||
|
@ -547,19 +587,19 @@ def check_command(module, commandline):
|
|||
command = commandline.split()[0]
|
||||
command = os.path.basename(command)
|
||||
|
||||
disable_suffix = "If you need to use command because {mod} is insufficient you can add" \
|
||||
" warn=False to this command task or set command_warnings=False in" \
|
||||
" ansible.cfg to get rid of this message."
|
||||
disable_suffix = "If you need to use '{cmd}' because the {mod} module is insufficient you can add" \
|
||||
" 'warn: false' to this command task or set 'command_warnings=False' in" \
|
||||
" the defaults section of ansible.cfg to get rid of this message."
|
||||
substitutions = {'mod': None, 'cmd': command}
|
||||
|
||||
if command in arguments:
|
||||
msg = "Consider using the {mod} module with {subcmd} rather than running {cmd}. " + disable_suffix
|
||||
msg = "Consider using the {mod} module with {subcmd} rather than running '{cmd}'. " + disable_suffix
|
||||
substitutions['mod'] = 'file'
|
||||
substitutions['subcmd'] = arguments[command]
|
||||
module.warn(msg.format(**substitutions))
|
||||
|
||||
if command in commands:
|
||||
msg = "Consider using the {mod} module rather than running {cmd}. " + disable_suffix
|
||||
msg = "Consider using the {mod} module rather than running '{cmd}'. " + disable_suffix
|
||||
substitutions['mod'] = commands[command]
|
||||
module.warn(msg.format(**substitutions))
|
||||
|
||||
|
@ -571,20 +611,24 @@ def main():
|
|||
|
||||
# the command module is the one ansible module that does not take key=value args
|
||||
# hence don't copy this one if you are looking to build others!
|
||||
# NOTE: ensure splitter.py is kept in sync for exceptions
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
_raw_params=dict(),
|
||||
_uses_shell=dict(type='bool', default=False),
|
||||
argv=dict(type='list'),
|
||||
argv=dict(type='list', elements='str'),
|
||||
chdir=dict(type='path'),
|
||||
executable=dict(),
|
||||
creates=dict(type='path'),
|
||||
removes=dict(type='path'),
|
||||
# The default for this really comes from the action plugin
|
||||
warn=dict(type='bool', default=True),
|
||||
warn=dict(type='bool', default=False, removed_in_version='2.14', removed_from_collection='ansible.builtin'),
|
||||
stdin=dict(required=False),
|
||||
stdin_add_newline=dict(type='bool', default=True),
|
||||
strip_empty_ends=dict(type='bool', default=True),
|
||||
zuul_log_id=dict(type='str'),
|
||||
)
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
shell = module.params['_uses_shell']
|
||||
chdir = module.params['chdir']
|
||||
|
@ -595,8 +639,13 @@ def main():
|
|||
removes = module.params['removes']
|
||||
warn = module.params['warn']
|
||||
stdin = module.params['stdin']
|
||||
stdin_add_newline = module.params['stdin_add_newline']
|
||||
strip = module.params['strip_empty_ends']
|
||||
zuul_log_id = module.params['zuul_log_id']
|
||||
|
||||
# we promissed these in 'always' ( _lines get autoaded on action plugin)
|
||||
r = {'changed': False, 'stdout': '', 'stderr': '', 'rc': None, 'cmd': None, 'start': None, 'end': None, 'delta': None, 'msg': ''}
|
||||
|
||||
if not shell and executable:
|
||||
module.warn("As of Ansible 2.4, the parameter 'executable' is no longer supported with the 'command' module. Not using '%s'." % executable)
|
||||
executable = None
|
||||
|
@ -605,70 +654,92 @@ def main():
|
|||
module.fail_json(rc=256, msg="zuul_log_id missing: %s" % module.params)
|
||||
|
||||
if (not args or args.strip() == '') and not argv:
|
||||
module.fail_json(rc=256, msg="no command given")
|
||||
r['rc'] = 256
|
||||
r['msg'] = "no command given"
|
||||
module.fail_json(**r)
|
||||
|
||||
if args and argv:
|
||||
module.fail_json(rc=256, msg="only command or argv can be given, not both")
|
||||
r['rc'] = 256
|
||||
r['msg'] = "only command or argv can be given, not both"
|
||||
module.fail_json(**r)
|
||||
|
||||
if not shell and args:
|
||||
args = shlex.split(args)
|
||||
|
||||
args = args or argv
|
||||
# All args must be strings
|
||||
if is_iterable(args, include_strings=False):
|
||||
args = [to_native(arg, errors='surrogate_or_strict', nonstring='simplerepr') for arg in args]
|
||||
|
||||
if chdir:
|
||||
chdir = os.path.abspath(chdir)
|
||||
os.chdir(chdir)
|
||||
|
||||
if creates:
|
||||
# do not run the command if the line contains creates=filename
|
||||
# and the filename already exists. This allows idempotence
|
||||
# of command executions.
|
||||
if glob.glob(creates):
|
||||
module.exit_json(
|
||||
cmd=args,
|
||||
stdout="skipped, since %s exists" % creates,
|
||||
changed=False,
|
||||
rc=0
|
||||
)
|
||||
|
||||
if removes:
|
||||
# do not run the command if the line contains removes=filename
|
||||
# and the filename does not exist. This allows idempotence
|
||||
# of command executions.
|
||||
if not glob.glob(removes):
|
||||
module.exit_json(
|
||||
cmd=args,
|
||||
stdout="skipped, since %s does not exist" % removes,
|
||||
changed=False,
|
||||
rc=0
|
||||
)
|
||||
|
||||
r['cmd'] = args
|
||||
if warn:
|
||||
# nany telling you to use module instead!
|
||||
check_command(module, args)
|
||||
|
||||
startd = datetime.datetime.now()
|
||||
if chdir:
|
||||
chdir = to_bytes(chdir, errors='surrogate_or_strict')
|
||||
|
||||
rc, out, err = zuul_run_command(module, args, zuul_log_id, executable=executable, use_unsafe_shell=shell, encoding=None, data=stdin)
|
||||
try:
|
||||
os.chdir(chdir)
|
||||
except (IOError, OSError) as e:
|
||||
r['msg'] = 'Unable to change directory before execution: %s' % to_text(e)
|
||||
module.fail_json(**r)
|
||||
|
||||
endd = datetime.datetime.now()
|
||||
delta = endd - startd
|
||||
# check_mode partial support, since it only really works in checking creates/removes
|
||||
if module.check_mode:
|
||||
shoulda = "Would"
|
||||
else:
|
||||
shoulda = "Did"
|
||||
|
||||
result = dict(
|
||||
cmd=args,
|
||||
stdout=out.rstrip(b"\r\n"),
|
||||
stderr=err.rstrip(b"\r\n"),
|
||||
rc=rc,
|
||||
start=str(startd),
|
||||
end=str(endd),
|
||||
delta=str(delta),
|
||||
changed=True,
|
||||
zuul_log_id=zuul_log_id
|
||||
)
|
||||
# special skips for idempotence if file exists (assumes command creates)
|
||||
if creates:
|
||||
if glob.glob(creates):
|
||||
r['msg'] = "%s not run command since '%s' exists" % (shoulda, creates)
|
||||
r['stdout'] = "skipped, since %s exists" % creates # TODO: deprecate
|
||||
|
||||
if rc != 0:
|
||||
module.fail_json(msg='non-zero return code', **result)
|
||||
r['rc'] = 0
|
||||
|
||||
module.exit_json(**result)
|
||||
# special skips for idempotence if file does not exist (assumes command removes)
|
||||
if not r['msg'] and removes:
|
||||
if not glob.glob(removes):
|
||||
r['msg'] = "%s not run command since '%s' does not exist" % (shoulda, removes)
|
||||
r['stdout'] = "skipped, since %s does not exist" % removes # TODO: deprecate
|
||||
r['rc'] = 0
|
||||
|
||||
if r['msg']:
|
||||
module.exit_json(**r)
|
||||
|
||||
# actually executes command (or not ...)
|
||||
if not module.check_mode:
|
||||
r['start'] = datetime.datetime.now()
|
||||
r['rc'], r['stdout'], r['stderr'] = zuul_run_command(module, args, zuul_log_id, executable=executable, use_unsafe_shell=shell, encoding=None,
|
||||
data=stdin, binary_data=(not stdin_add_newline))
|
||||
r['end'] = datetime.datetime.now()
|
||||
else:
|
||||
# this is partial check_mode support, since we end up skipping if we get here
|
||||
r['rc'] = 0
|
||||
r['msg'] = "Command would have run if not in check mode"
|
||||
r['skipped'] = True
|
||||
|
||||
r['changed'] = True
|
||||
r['zuul_log_id'] = zuul_log_id
|
||||
|
||||
# convert to text for jsonization and usability
|
||||
if r['start'] is not None and r['end'] is not None:
|
||||
# these are datetime objects, but need them as strings to pass back
|
||||
r['delta'] = to_text(r['end'] - r['start'])
|
||||
r['end'] = to_text(r['end'])
|
||||
r['start'] = to_text(r['start'])
|
||||
|
||||
if strip:
|
||||
r['stdout'] = to_text(r['stdout']).rstrip("\r\n")
|
||||
r['stderr'] = to_text(r['stderr']).rstrip("\r\n")
|
||||
|
||||
if r['rc'] != 0:
|
||||
r['msg'] = 'non-zero return code'
|
||||
module.fail_json(**r)
|
||||
|
||||
module.exit_json(**r)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -615,7 +615,7 @@ class JobParser(object):
|
|||
'post-run': to_list(str),
|
||||
'run': to_list(str),
|
||||
'cleanup-run': to_list(str),
|
||||
'ansible-version': vs.Any(str, float),
|
||||
'ansible-version': vs.Any(str, float, int),
|
||||
'_source_context': model.SourceContext,
|
||||
'_start_mark': model.ZuulMark,
|
||||
'roles': to_list(role),
|
||||
|
@ -753,8 +753,8 @@ class JobParser(object):
|
|||
|
||||
# Configure and validate ansible version
|
||||
if 'ansible-version' in conf:
|
||||
# The ansible-version can be treated by yaml as a float so convert
|
||||
# it to a string.
|
||||
# The ansible-version can be treated by yaml as a float or
|
||||
# int so convert it to a string.
|
||||
ansible_version = str(conf['ansible-version'])
|
||||
self.pcontext.ansible_manager.requestVersion(ansible_version)
|
||||
job.ansible_version = ansible_version
|
||||
|
|
|
@ -12,3 +12,6 @@ requirements = ansible>=2.8,<2.9,!=2.8.16 Jinja2<3.1.0
|
|||
# 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
|
||||
|
||||
[5]
|
||||
requirements = ansible>=5.0,<6.0
|
||||
|
|
Loading…
Reference in New Issue