1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

puppet: refactored to use CmdRunner (#5612)

* puppet: refactored to use CmdRunner

* add changelog fragment

* add more tests
This commit is contained in:
Alexei Znamensky 2022-12-15 09:30:03 +13:00 committed by GitHub
parent c3bc172bf6
commit f95e0d775d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 282 additions and 105 deletions

3
.github/BOTMETA.yml vendored
View file

@ -309,6 +309,9 @@ files:
$module_utils/pipx.py: $module_utils/pipx.py:
labels: pipx labels: pipx
maintainers: russoz maintainers: russoz
$module_utils/puppet.py:
labels: puppet
maintainers: russoz
$module_utils/pure.py: $module_utils/pure.py:
labels: pure pure_storage labels: pure pure_storage
maintainers: $team_purestorage maintainers: $team_purestorage

View file

@ -0,0 +1,2 @@
minor_changes:
- puppet - refactored module to use ``CmdRunner`` for executing ``puppet`` (https://github.com/ansible-collections/community.general/pull/5612).

View file

@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022, Alexei Znamensky <russoz@gmail.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
from ansible_collections.community.general.plugins.module_utils.cmd_runner import CmdRunner, cmd_runner_fmt
_PUPPET_PATH_PREFIX = ["/opt/puppetlabs/bin"]
def get_facter_dir():
if os.getuid() == 0:
return '/etc/facter/facts.d'
else:
return os.path.expanduser('~/.facter/facts.d')
def _puppet_cmd(module):
return module.get_bin_path("puppet", False, _PUPPET_PATH_PREFIX)
# If the `timeout` CLI command feature is removed,
# Then we could add this as a fixed param to `puppet_runner`
def ensure_agent_enabled(module):
runner = CmdRunner(
module,
command="puppet",
path_prefix=_PUPPET_PATH_PREFIX,
arg_formats=dict(
_agent_disabled=cmd_runner_fmt.as_fixed(['config', 'print', 'agent_disabled_lockfile']),
),
check_rc=False,
)
rc, stdout, stderr = runner("_agent_disabled").run()
if os.path.exists(stdout.strip()):
module.fail_json(
msg="Puppet agent is administratively disabled.",
disabled=True)
elif rc != 0:
module.fail_json(
msg="Puppet agent state could not be determined.")
def puppet_runner(module):
# Keeping backward compatibility, allow for running with the `timeout` CLI command.
# If this can be replaced with ansible `timeout` parameter in playbook,
# then this function could be removed.
def _prepare_base_cmd():
_tout_cmd = module.get_bin_path("timeout", False)
if _tout_cmd:
cmd = ["timeout", "-s", "9", module.params["timeout"], _puppet_cmd(module)]
else:
cmd = ["puppet"]
return cmd
def noop_func(v):
_noop = cmd_runner_fmt.as_map({
True: "--noop",
False: "--no-noop",
})
return _noop(module.check_mode or v)
_logdest_map = {
"syslog": ["--logdest", "syslog"],
"all": ["--logdest", "syslog", "--logdest", "console"],
}
@cmd_runner_fmt.unpack_args
def execute_func(execute, manifest):
if execute:
return ["--execute", execute]
else:
return [manifest]
runner = CmdRunner(
module,
command=_prepare_base_cmd(),
path_prefix=_PUPPET_PATH_PREFIX,
arg_formats=dict(
_agent_fixed=cmd_runner_fmt.as_fixed([
"agent", "--onetime", "--no-daemonize", "--no-usecacheonfailure",
"--no-splay", "--detailed-exitcodes", "--verbose", "--color", "0",
]),
_apply_fixed=cmd_runner_fmt.as_fixed(["apply", "--detailed-exitcodes"]),
puppetmaster=cmd_runner_fmt.as_opt_val("--server"),
show_diff=cmd_runner_fmt.as_bool("--show-diff"),
confdir=cmd_runner_fmt.as_opt_val("--confdir"),
environment=cmd_runner_fmt.as_opt_val("--environment"),
tags=cmd_runner_fmt.as_func(lambda v: ["--tags", ",".join(v)]),
certname=cmd_runner_fmt.as_opt_eq_val("--certname"),
noop=cmd_runner_fmt.as_func(noop_func),
use_srv_records=cmd_runner_fmt.as_map({
True: "--usr_srv_records",
False: "--no-usr_srv_records",
}),
logdest=cmd_runner_fmt.as_map(_logdest_map, default=[]),
modulepath=cmd_runner_fmt.as_opt_eq_val("--modulepath"),
_execute=cmd_runner_fmt.as_func(execute_func),
summarize=cmd_runner_fmt.as_bool("--summarize"),
debug=cmd_runner_fmt.as_bool("--debug"),
verbose=cmd_runner_fmt.as_bool("--verbose"),
),
check_rc=False,
)
return runner

View file

@ -152,15 +152,9 @@ import json
import os import os
import stat import stat
import ansible_collections.community.general.plugins.module_utils.puppet as puppet_utils
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six.moves import shlex_quote
def _get_facter_dir():
if os.getuid() == 0:
return '/etc/facter/facts.d'
else:
return os.path.expanduser('~/.facter/facts.d')
def _write_structured_data(basedir, basename, data): def _write_structured_data(basedir, basename, data):
@ -212,16 +206,6 @@ def main():
) )
p = module.params p = module.params
global PUPPET_CMD
PUPPET_CMD = module.get_bin_path("puppet", False, ['/opt/puppetlabs/bin'])
if not PUPPET_CMD:
module.fail_json(
msg="Could not find puppet. Please ensure it is installed.")
global TIMEOUT_CMD
TIMEOUT_CMD = module.get_bin_path("timeout", False)
if p['manifest']: if p['manifest']:
if not os.path.exists(p['manifest']): if not os.path.exists(p['manifest']):
module.fail_json( module.fail_json(
@ -230,90 +214,24 @@ def main():
# Check if puppet is disabled here # Check if puppet is disabled here
if not p['manifest']: if not p['manifest']:
rc, stdout, stderr = module.run_command( puppet_utils.ensure_agent_enabled(module)
PUPPET_CMD + " config print agent_disabled_lockfile")
if os.path.exists(stdout.strip()):
module.fail_json(
msg="Puppet agent is administratively disabled.",
disabled=True)
elif rc != 0:
module.fail_json(
msg="Puppet agent state could not be determined.")
if module.params['facts'] and not module.check_mode: if module.params['facts'] and not module.check_mode:
_write_structured_data( _write_structured_data(
_get_facter_dir(), puppet_utils.get_facter_dir(),
module.params['facter_basename'], module.params['facter_basename'],
module.params['facts']) module.params['facts'])
if TIMEOUT_CMD: runner = puppet_utils.puppet_runner(module)
base_cmd = "%(timeout_cmd)s -s 9 %(timeout)s %(puppet_cmd)s" % dict(
timeout_cmd=TIMEOUT_CMD,
timeout=shlex_quote(p['timeout']),
puppet_cmd=PUPPET_CMD)
else:
base_cmd = PUPPET_CMD
if not p['manifest'] and not p['execute']: if not p['manifest'] and not p['execute']:
cmd = ("%(base_cmd)s agent --onetime" args_order = "_agent_fixed puppetmaster show_diff confdir environment tags certname noop use_srv_records"
" --no-daemonize --no-usecacheonfailure --no-splay" with runner(args_order) as ctx:
" --detailed-exitcodes --verbose --color 0") % dict(base_cmd=base_cmd) rc, stdout, stderr = ctx.run()
if p['puppetmaster']:
cmd += " --server %s" % shlex_quote(p['puppetmaster'])
if p['show_diff']:
cmd += " --show_diff"
if p['confdir']:
cmd += " --confdir %s" % shlex_quote(p['confdir'])
if p['environment']:
cmd += " --environment '%s'" % p['environment']
if p['tags']:
cmd += " --tags '%s'" % ','.join(p['tags'])
if p['certname']:
cmd += " --certname='%s'" % p['certname']
if module.check_mode:
cmd += " --noop"
elif 'noop' in p:
if p['noop']:
cmd += " --noop"
else:
cmd += " --no-noop"
if p['use_srv_records'] is not None:
if not p['use_srv_records']:
cmd += " --no-use_srv_records"
else:
cmd += " --use_srv_records"
else: else:
cmd = "%s apply --detailed-exitcodes " % base_cmd args_order = "_apply_fixed logdest modulepath environment certname tags noop _execute summarize debug verbose"
if p['logdest'] == 'syslog': with runner(args_order) as ctx:
cmd += "--logdest syslog " rc, stdout, stderr = ctx.run(_execute=[p['execute'], p['manifest']])
if p['logdest'] == 'all':
cmd += " --logdest syslog --logdest console"
if p['modulepath']:
cmd += "--modulepath='%s'" % p['modulepath']
if p['environment']:
cmd += "--environment '%s' " % p['environment']
if p['certname']:
cmd += " --certname='%s'" % p['certname']
if p['tags']:
cmd += " --tags '%s'" % ','.join(p['tags'])
if module.check_mode:
cmd += "--noop "
elif 'noop' in p:
if p['noop']:
cmd += " --noop"
else:
cmd += " --no-noop"
if p['execute']:
cmd += " --execute '%s'" % p['execute']
else:
cmd += " %s" % shlex_quote(p['manifest'])
if p['summarize']:
cmd += " --summarize"
if p['debug']:
cmd += " --debug"
if p['verbose']:
cmd += " --verbose"
rc, stdout, stderr = module.run_command(cmd)
if rc == 0: if rc == 0:
# success # success
@ -335,11 +253,11 @@ def main():
elif rc == 124: elif rc == 124:
# timeout # timeout
module.exit_json( module.exit_json(
rc=rc, msg="%s timed out" % cmd, stdout=stdout, stderr=stderr) rc=rc, msg="%s timed out" % ctx.cmd, stdout=stdout, stderr=stderr)
else: else:
# failure # failure
module.fail_json( module.fail_json(
rc=rc, msg="%s failed with return code: %d" % (cmd, rc), rc=rc, msg="%s failed with return code: %d" % (ctx.cmd, rc),
stdout=stdout, stderr=stderr) stdout=stdout, stderr=stderr)

View file

@ -18,8 +18,7 @@ plugins/modules/manageiq_provider.py validate-modules:undocumented-parameter
plugins/modules/manageiq_tags.py validate-modules:parameter-state-invalid-choice plugins/modules/manageiq_tags.py validate-modules:parameter-state-invalid-choice
plugins/modules/osx_defaults.py validate-modules:parameter-state-invalid-choice plugins/modules/osx_defaults.py validate-modules:parameter-state-invalid-choice
plugins/modules/parted.py validate-modules:parameter-state-invalid-choice plugins/modules/parted.py validate-modules:parameter-state-invalid-choice
plugins/modules/puppet.py use-argspec-type-path plugins/modules/puppet.py validate-modules:parameter-invalid # invalid alias - removed in 7.0.0
plugins/modules/puppet.py validate-modules:parameter-invalid # invalid alias - removed in 7.0.0
plugins/modules/rax_files_objects.py use-argspec-type-path plugins/modules/rax_files_objects.py use-argspec-type-path
plugins/modules/rax_files.py validate-modules:parameter-state-invalid-choice plugins/modules/rax_files.py validate-modules:parameter-state-invalid-choice
plugins/modules/rax.py use-argspec-type-path # fix needed plugins/modules/rax.py use-argspec-type-path # fix needed

View file

@ -13,8 +13,7 @@ plugins/modules/manageiq_provider.py validate-modules:undocumented-parameter
plugins/modules/manageiq_tags.py validate-modules:parameter-state-invalid-choice plugins/modules/manageiq_tags.py validate-modules:parameter-state-invalid-choice
plugins/modules/osx_defaults.py validate-modules:parameter-state-invalid-choice plugins/modules/osx_defaults.py validate-modules:parameter-state-invalid-choice
plugins/modules/parted.py validate-modules:parameter-state-invalid-choice plugins/modules/parted.py validate-modules:parameter-state-invalid-choice
plugins/modules/puppet.py use-argspec-type-path plugins/modules/puppet.py validate-modules:parameter-invalid # invalid alias - removed in 7.0.0
plugins/modules/puppet.py validate-modules:parameter-invalid # invalid alias - removed in 7.0.0
plugins/modules/rax_files_objects.py use-argspec-type-path plugins/modules/rax_files_objects.py use-argspec-type-path
plugins/modules/rax_files.py validate-modules:parameter-state-invalid-choice plugins/modules/rax_files.py validate-modules:parameter-state-invalid-choice
plugins/modules/rax.py use-argspec-type-path # fix needed plugins/modules/rax.py use-argspec-type-path # fix needed

View file

@ -13,8 +13,7 @@ plugins/modules/manageiq_provider.py validate-modules:undocumented-parameter
plugins/modules/manageiq_tags.py validate-modules:parameter-state-invalid-choice plugins/modules/manageiq_tags.py validate-modules:parameter-state-invalid-choice
plugins/modules/osx_defaults.py validate-modules:parameter-state-invalid-choice plugins/modules/osx_defaults.py validate-modules:parameter-state-invalid-choice
plugins/modules/parted.py validate-modules:parameter-state-invalid-choice plugins/modules/parted.py validate-modules:parameter-state-invalid-choice
plugins/modules/puppet.py use-argspec-type-path plugins/modules/puppet.py validate-modules:parameter-invalid # invalid alias - removed in 7.0.0
plugins/modules/puppet.py validate-modules:parameter-invalid # invalid alias - removed in 7.0.0
plugins/modules/rax_files_objects.py use-argspec-type-path plugins/modules/rax_files_objects.py use-argspec-type-path
plugins/modules/rax_files.py validate-modules:parameter-state-invalid-choice plugins/modules/rax_files.py validate-modules:parameter-state-invalid-choice
plugins/modules/rax.py use-argspec-type-path # fix needed plugins/modules/rax.py use-argspec-type-path # fix needed

View file

@ -14,8 +14,7 @@ plugins/modules/manageiq_provider.py validate-modules:undocumented-parameter
plugins/modules/manageiq_tags.py validate-modules:parameter-state-invalid-choice plugins/modules/manageiq_tags.py validate-modules:parameter-state-invalid-choice
plugins/modules/osx_defaults.py validate-modules:parameter-state-invalid-choice plugins/modules/osx_defaults.py validate-modules:parameter-state-invalid-choice
plugins/modules/parted.py validate-modules:parameter-state-invalid-choice plugins/modules/parted.py validate-modules:parameter-state-invalid-choice
plugins/modules/puppet.py use-argspec-type-path plugins/modules/puppet.py validate-modules:parameter-invalid # invalid alias - removed in 7.0.0
plugins/modules/puppet.py validate-modules:parameter-invalid # invalid alias - removed in 7.0.0
plugins/modules/rax_files_objects.py use-argspec-type-path plugins/modules/rax_files_objects.py use-argspec-type-path
plugins/modules/rax_files.py validate-modules:parameter-state-invalid-choice plugins/modules/rax_files.py validate-modules:parameter-state-invalid-choice
plugins/modules/rax.py use-argspec-type-path # fix needed plugins/modules/rax.py use-argspec-type-path # fix needed

View file

@ -14,8 +14,7 @@ plugins/modules/manageiq_provider.py validate-modules:undocumented-parameter
plugins/modules/manageiq_tags.py validate-modules:parameter-state-invalid-choice plugins/modules/manageiq_tags.py validate-modules:parameter-state-invalid-choice
plugins/modules/osx_defaults.py validate-modules:parameter-state-invalid-choice plugins/modules/osx_defaults.py validate-modules:parameter-state-invalid-choice
plugins/modules/parted.py validate-modules:parameter-state-invalid-choice plugins/modules/parted.py validate-modules:parameter-state-invalid-choice
plugins/modules/puppet.py use-argspec-type-path plugins/modules/puppet.py validate-modules:parameter-invalid # invalid alias - removed in 7.0.0
plugins/modules/puppet.py validate-modules:parameter-invalid # invalid alias - removed in 7.0.0
plugins/modules/rax_files_objects.py use-argspec-type-path plugins/modules/rax_files_objects.py use-argspec-type-path
plugins/modules/rax_files.py validate-modules:parameter-state-invalid-choice plugins/modules/rax_files.py validate-modules:parameter-state-invalid-choice
plugins/modules/rax.py use-argspec-type-path # fix needed plugins/modules/rax.py use-argspec-type-path # fix needed

View file

@ -0,0 +1,145 @@
# -*- coding: utf-8 -*-
# Author: Alexei Znamensky (russoz@gmail.com)
# Largely adapted from test_redhat_subscription by
# Jiri Hnidek (jhnidek@redhat.com)
#
# Copyright (c) Alexei Znamensky (russoz@gmail.com)
# Copyright (c) Jiri Hnidek (jhnidek@redhat.com)
#
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import json
from ansible_collections.community.general.plugins.modules import puppet
import pytest
TESTED_MODULE = puppet.__name__
@pytest.fixture
def patch_get_bin_path(mocker):
"""
Function used for mocking AnsibleModule.get_bin_path
"""
def mockie(self, path, *args, **kwargs):
return "/testbin/{0}".format(path)
mocker.patch("ansible.module_utils.basic.AnsibleModule.get_bin_path", mockie)
TEST_CASES = [
[
{},
{
"id": "puppet_agent_plain",
"run_command.calls": [
(
["/testbin/puppet", "config", "print", "agent_disabled_lockfile"],
{"environ_update": {"LANGUAGE": "C", "LC_ALL": "C"}, "check_rc": False},
(0, "blah, anything", "",), # output rc, out, err
),
(
[
"/testbin/timeout", "-s", "9", "30m", "/testbin/puppet", "agent", "--onetime", "--no-daemonize",
"--no-usecacheonfailure", "--no-splay", "--detailed-exitcodes", "--verbose", "--color", "0"
],
{"environ_update": {"LANGUAGE": "C", "LC_ALL": "C"}, "check_rc": False},
(0, "", "",), # output rc, out, err
),
],
"changed": False,
}
],
[
{
"certname": "potatobox"
},
{
"id": "puppet_agent_certname",
"run_command.calls": [
(
["/testbin/puppet", "config", "print", "agent_disabled_lockfile"],
{"environ_update": {"LANGUAGE": "C", "LC_ALL": "C"}, "check_rc": False},
(0, "blah, anything", "",), # output rc, out, err
),
(
[
"/testbin/timeout", "-s", "9", "30m", "/testbin/puppet", "agent", "--onetime", "--no-daemonize",
"--no-usecacheonfailure", "--no-splay", "--detailed-exitcodes", "--verbose", "--color", "0", "--certname=potatobox"
],
{"environ_update": {"LANGUAGE": "C", "LC_ALL": "C"}, "check_rc": False},
(0, "", "",), # output rc, out, err
),
],
"changed": False,
}
],
[
{
"tags": ["a", "b", "c"]
},
{
"id": "puppet_agent_tags_abc",
"run_command.calls": [
(
["/testbin/puppet", "config", "print", "agent_disabled_lockfile"],
{"environ_update": {"LANGUAGE": "C", "LC_ALL": "C"}, "check_rc": False},
(0, "blah, anything", "",), # output rc, out, err
),
(
[
"/testbin/timeout", "-s", "9", "30m", "/testbin/puppet", "agent", "--onetime", "--no-daemonize",
"--no-usecacheonfailure", "--no-splay", "--detailed-exitcodes", "--verbose", "--color", "0", "--tags", "a,b,c"
],
{"environ_update": {"LANGUAGE": "C", "LC_ALL": "C"}, "check_rc": False},
(0, "", "",), # output rc, out, err
),
],
"changed": False,
}
],
]
TEST_CASES_IDS = [item[1]["id"] for item in TEST_CASES]
@pytest.mark.parametrize("patch_ansible_module, testcase",
TEST_CASES,
ids=TEST_CASES_IDS,
indirect=["patch_ansible_module"])
@pytest.mark.usefixtures("patch_ansible_module")
def test_puppet(mocker, capfd, patch_get_bin_path, testcase):
"""
Run unit tests for test cases listen in TEST_CASES
"""
# Mock function used for running commands first
call_results = [item[2] for item in testcase["run_command.calls"]]
mock_run_command = mocker.patch(
"ansible.module_utils.basic.AnsibleModule.run_command",
side_effect=call_results)
# Try to run test case
with pytest.raises(SystemExit):
puppet.main()
out, err = capfd.readouterr()
results = json.loads(out)
print("results =\n%s" % results)
assert mock_run_command.call_count == len(testcase["run_command.calls"])
if mock_run_command.call_count:
call_args_list = [(item[0][0], item[1]) for item in mock_run_command.call_args_list]
expected_call_args_list = [(item[0], item[1]) for item in testcase["run_command.calls"]]
print("call args list =\n%s" % call_args_list)
print("expected args list =\n%s" % expected_call_args_list)
assert call_args_list == expected_call_args_list
assert results.get("changed", False) == testcase["changed"]
if "failed" in testcase:
assert results.get("failed", False) == testcase["failed"]
if "msg" in testcase:
assert results.get("msg", "") == testcase["msg"]