Skip to content

Commit

Permalink
Accommodate specified inventory files
Browse files Browse the repository at this point in the history
The ansible_cfg_inventory conditional in the _get_inventory_files method
is used because otherwise, when not passing in an inventory file via
CLI, inventory_files would set to the /etc/ansible/hosts (the default
value for the DEFAULT_HOST_LIST config). In most cases, I'd presume that
wouldn't be desired.
  • Loading branch information
cavcrosby committed Nov 24, 2024
1 parent e83eb91 commit 12c1678
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 6 deletions.
11 changes: 11 additions & 0 deletions examples/playbooks/test_using_inventory.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
- name: Test
hosts:
- group_name
serial: "{{ batch | default(groups['group_name'] | length) }}"
gather_facts: false
tasks:
- name: Debug
delegate_to: localhost
ansible.builtin.debug:
msg: "{{ batch | default(groups['group_name'] | length) }}"
7 changes: 7 additions & 0 deletions inventories/bad_inventory
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[group]
= = I
= = am
= = not
= = a
= = valid
= = inventory
4 changes: 4 additions & 0 deletions inventories/bar
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
not_the_group_name:
hosts:
host1:
host2:
4 changes: 4 additions & 0 deletions inventories/baz
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
group_name:
hosts:
host1:
host2:
4 changes: 4 additions & 0 deletions inventories/foo
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
group_name:
hosts:
host1:
host2:
8 changes: 8 additions & 0 deletions src/ansiblelint/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,14 @@ def get_cli_parser() -> argparse.ArgumentParser:
default=None,
help=f"Specify ignore file to use. By default it will look for '{IGNORE_FILE.default}' or '{IGNORE_FILE.alternative}'",
)
parser.add_argument(
"-I",
"--inventory",
dest="inventory",
action="append",
type=str,
help="Specify inventory host path or comma separated host list",
)
parser.add_argument(
"--offline",
dest="offline",
Expand Down
1 change: 1 addition & 0 deletions src/ansiblelint/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ class Options: # pylint: disable=too-many-instance-attributes
version: bool = False # display version command
list_profiles: bool = False # display profiles command
ignore_file: Path | None = None
inventory: list[str] | None = None
max_tasks: int = 100
max_block_depth: int = 20
# Refer to https://docs.ansible.com/ansible/latest/reference_appendices/release_and_maintenance.html#ansible-core-support-matrix
Expand Down
73 changes: 68 additions & 5 deletions src/ansiblelint/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING, Any
from unittest import mock

import ansible.inventory.manager
from ansible.config.manager import ConfigManager
from ansible.errors import AnsibleError
from ansible.parsing.dataloader import DataLoader
from ansible.parsing.splitter import split_args
from ansible.parsing.yaml.constructor import AnsibleMapping
from ansible.plugins.loader import add_all_plugin_dirs
Expand Down Expand Up @@ -340,13 +344,16 @@ def _get_ansible_syntax_check_matches(
playbook_path = fh.name
else:
playbook_path = str(lintable.path.expanduser())
# To avoid noisy warnings we pass localhost as current inventory:
# [WARNING]: No inventory was parsed, only implicit localhost is available
# [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
cmd = [
"ansible-playbook",
"-i",
"localhost,",
*[
inventory_opt
for inventory_opts in [
("-i", inventory_file)
for inventory_file in self._get_inventory_files(app)
]
for inventory_opt in inventory_opts
],
"--syntax-check",
playbook_path,
]
Expand Down Expand Up @@ -451,6 +458,62 @@ def _get_ansible_syntax_check_matches(
fh.close()
return results

def _get_inventory_files(self, app: App) -> list[str]:
config_mgr = ConfigManager()
ansible_cfg_inventory = config_mgr.get_config_value(
"DEFAULT_HOST_LIST",
)
if app.options.inventory or ansible_cfg_inventory != [
config_mgr.get_configuration_definitions()["DEFAULT_HOST_LIST"].get(
"default",
),
]:
inventory_files = [
inventory_file
for inventory_list in [
# creates nested inventory list
(inventory.split(",") if "," in inventory else [inventory])
for inventory in (
app.options.inventory
if app.options.inventory
else ansible_cfg_inventory
)
]
for inventory_file in inventory_list
]

# silence noise when using parse_source
with mock.patch.object(
ansible.inventory.manager,
"display",
mock.Mock(),
):
for inventory_file in inventory_files:
if not Path(inventory_file).exists():
_logger.warning(
"Unable to use %s as an inventory source: no such file or directory",
inventory_file,
)
elif os.access(
inventory_file,
os.R_OK,
) and not ansible.inventory.manager.InventoryManager(
DataLoader(),
).parse_source(
inventory_file,
):
_logger.warning(
"Unable to parse %s as an inventory source",
inventory_file,
)
else:
# To avoid noisy warnings we pass localhost as current inventory:
# [WARNING]: No inventory was parsed, only implicit localhost is available
# [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
inventory_files = ["localhost"]

return inventory_files

def _filter_excluded_matches(self, matches: list[MatchError]) -> list[MatchError]:
return [
match
Expand Down
68 changes: 68 additions & 0 deletions test/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from pathlib import Path

import pytest

from ansiblelint.constants import RC
from ansiblelint.file_utils import Lintable
from ansiblelint.testing import run_ansible_lint
Expand Down Expand Up @@ -29,3 +31,69 @@ def test_app_no_matches(tmp_path: Path) -> None:
"""Validate that linter returns special exit code if no files are analyzed."""
result = run_ansible_lint(cwd=tmp_path)
assert result.returncode == RC.NO_FILES_MATCHED


@pytest.mark.parametrize(
"inventory_opts",
(
pytest.param(["-I", "inventories/foo"], id="1"),
pytest.param(
[
"-I",
"inventories/bar",
"-I",
"inventories/baz",
],
id="2",
),
pytest.param(
[
"-I",
"inventories/foo,inventories/bar",
"-I",
"inventories/baz",
],
id="3",
),
),
)
def test_with_inventory(inventory_opts: list[str]) -> None:
"""Validate using --inventory remedies syntax-check[specific] violation."""
lintable = Lintable("examples/playbooks/test_using_inventory.yml")
result = run_ansible_lint(lintable.filename, *inventory_opts)
assert result.returncode == RC.SUCCESS


@pytest.mark.parametrize(
("inventory_opts", "error_msg"),
(
pytest.param(
["-I", "inventories/i_dont_exist"],
"Unable to use inventories/i_dont_exist as an inventory source: no such file or directory",
id="1",
),
pytest.param(
["-I", "inventories/bad_inventory"],
"Unable to parse inventories/bad_inventory as an inventory source",
id="2",
),
),
)
def test_with_inventory_emit_warning(inventory_opts: list[str], error_msg: str) -> None:
"""Validate using --inventory can emit useful warnings about inventory files."""
lintable = Lintable("examples/playbooks/test_using_inventory.yml")
result = run_ansible_lint(lintable.filename, *inventory_opts)
assert error_msg in result.stderr


def test_with_inventory_via_ansible_cfg(tmp_path: Path) -> None:
"""Validate using inventory file from ansible.cfg remedies syntax-check[specific] violation."""
(tmp_path / "ansible.cfg").write_text("[defaults]\ninventory = foo\n")
(tmp_path / "foo").write_text("[group_name]\nhost1\nhost2\n")
lintable = Lintable(tmp_path / "playbook.yml")
lintable.content = "---\n- name: Test\n hosts:\n - group_name\n serial: \"{{ batch | default(groups['group_name'] | length) }}\"\n"
lintable.kind = "playbook"
lintable.write(force=True)

result = run_ansible_lint(lintable.filename, cwd=tmp_path)
assert result.returncode == RC.SUCCESS
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ setenv =
PRE_COMMIT_COLOR = always
# Number of expected test passes, safety measure for accidental skip of
# tests. Update value if you add/remove tests. (tox-extra)
PYTEST_REQPASS = 895
PYTEST_REQPASS = 901
FORCE_COLOR = 1
pre: PIP_PRE = 1
allowlist_externals =
Expand Down

0 comments on commit 12c1678

Please sign in to comment.