Skip to content

Commit 7595e5f

Browse files
committed
Accommodate specified inventory files
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.
1 parent e32a77f commit 7595e5f

File tree

10 files changed

+177
-6
lines changed

10 files changed

+177
-6
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
- name: Test
3+
hosts:
4+
- group_name
5+
serial: "{{ batch | default(groups['group_name'] | length) }}"
6+
gather_facts: false
7+
tasks:
8+
- name: Debug
9+
delegate_to: localhost
10+
ansible.builtin.debug:
11+
msg: "{{ batch | default(groups['group_name'] | length) }}"

inventories/badinventory

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[group]
2+
::*@YU&#Y$&YQ$*@^*@Y^*#YS*^GAS^GAD::""[][[]]]
3+
= = I
4+
= = am
5+
= = not
6+
= = a
7+
= = valid
8+
= = inventory

inventories/bar

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
not_the_group_name:
2+
hosts:
3+
host1:
4+
host2:

inventories/baz

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
group_name:
2+
hosts:
3+
host1:
4+
host2:

inventories/foo

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
group_name:
2+
hosts:
3+
host1:
4+
host2:

src/ansiblelint/cli.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,14 @@ def get_cli_parser() -> argparse.ArgumentParser:
456456
default=None,
457457
help=f"Specify ignore file to use. By default it will look for '{IGNORE_FILE.default}' or '{IGNORE_FILE.alternative}'",
458458
)
459+
parser.add_argument(
460+
"-I",
461+
"--inventory",
462+
dest="inventory",
463+
action="append",
464+
type=str,
465+
help="Specify inventory host path or comma separated host list",
466+
)
459467
parser.add_argument(
460468
"--offline",
461469
dest="offline",

src/ansiblelint/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ class Options: # pylint: disable=too-many-instance-attributes
173173
version: bool = False # display version command
174174
list_profiles: bool = False # display profiles command
175175
ignore_file: Path | None = None
176+
inventory: list[str] | None = None
176177
max_tasks: int = 100
177178
max_block_depth: int = 20
178179
# Refer to https://docs.ansible.com/ansible/latest/reference_appendices/release_and_maintenance.html#ansible-core-support-matrix

src/ansiblelint/runner.py

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@
1818
from pathlib import Path
1919
from tempfile import NamedTemporaryFile
2020
from typing import TYPE_CHECKING, Any
21+
from unittest import mock
2122

23+
import ansible.inventory.manager
24+
from ansible.config.manager import ConfigManager
2225
from ansible.errors import AnsibleError
26+
from ansible.parsing.dataloader import DataLoader
2327
from ansible.parsing.splitter import split_args
2428
from ansible.parsing.yaml.constructor import AnsibleMapping
2529
from ansible.plugins.loader import add_all_plugin_dirs
@@ -336,13 +340,16 @@ def _get_ansible_syntax_check_matches(
336340
playbook_path = fh.name
337341
else:
338342
playbook_path = str(lintable.path.expanduser())
339-
# To avoid noisy warnings we pass localhost as current inventory:
340-
# [WARNING]: No inventory was parsed, only implicit localhost is available
341-
# [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
342343
cmd = [
343344
"ansible-playbook",
344-
"-i",
345-
"localhost,",
345+
*[
346+
inventory_opt
347+
for inventory_opts in [
348+
("-i", inventory_file)
349+
for inventory_file in self._get_inventory_files(app)
350+
]
351+
for inventory_opt in inventory_opts
352+
],
346353
"--syntax-check",
347354
playbook_path,
348355
]
@@ -447,6 +454,62 @@ def _get_ansible_syntax_check_matches(
447454
fh.close()
448455
return results
449456

457+
def _get_inventory_files(self, app: App) -> list[str]:
458+
config_mgr = ConfigManager()
459+
ansible_cfg_inventory = config_mgr.get_config_value(
460+
"DEFAULT_HOST_LIST",
461+
)
462+
if app.options.inventory or ansible_cfg_inventory != [
463+
config_mgr.get_configuration_definitions()["DEFAULT_HOST_LIST"].get(
464+
"default",
465+
),
466+
]:
467+
inventory_files = [
468+
inventory_file
469+
for inventory_list in [
470+
# creates nested inventory list
471+
(inventory.split(",") if "," in inventory else [inventory])
472+
for inventory in (
473+
app.options.inventory
474+
if app.options.inventory
475+
else ansible_cfg_inventory
476+
)
477+
]
478+
for inventory_file in inventory_list
479+
]
480+
481+
# silence noise when using parse_source
482+
with mock.patch.object(
483+
ansible.inventory.manager,
484+
"display",
485+
mock.Mock(),
486+
):
487+
for inventory_file in inventory_files:
488+
if not Path(inventory_file).exists():
489+
_logger.warning(
490+
"Unable to use %s as an inventory source: no such file or directory",
491+
inventory_file,
492+
)
493+
elif os.access(
494+
inventory_file,
495+
os.R_OK,
496+
) and not ansible.inventory.manager.InventoryManager(
497+
DataLoader(),
498+
).parse_source(
499+
inventory_file,
500+
):
501+
_logger.warning(
502+
"Unable to parse %s as an inventory source",
503+
inventory_file,
504+
)
505+
else:
506+
# To avoid noisy warnings we pass localhost as current inventory:
507+
# [WARNING]: No inventory was parsed, only implicit localhost is available
508+
# [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
509+
inventory_files = ["localhost"]
510+
511+
return inventory_files
512+
450513
def _filter_excluded_matches(self, matches: list[MatchError]) -> list[MatchError]:
451514
return [
452515
match

test/test_app.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from pathlib import Path
44

5+
import pytest
6+
57
from ansiblelint.constants import RC
68
from ansiblelint.file_utils import Lintable
79
from ansiblelint.testing import run_ansible_lint
@@ -29,3 +31,69 @@ def test_app_no_matches(tmp_path: Path) -> None:
2931
"""Validate that linter returns special exit code if no files are analyzed."""
3032
result = run_ansible_lint(cwd=tmp_path)
3133
assert result.returncode == RC.NO_FILES_MATCHED
34+
35+
36+
@pytest.mark.parametrize(
37+
"inventory_opts",
38+
(
39+
pytest.param(["-I", "inventories/foo"], id="1"),
40+
pytest.param(
41+
[
42+
"-I",
43+
"inventories/bar",
44+
"-I",
45+
"inventories/baz",
46+
],
47+
id="2",
48+
),
49+
pytest.param(
50+
[
51+
"-I",
52+
"inventories/foo,inventories/bar",
53+
"-I",
54+
"inventories/baz",
55+
],
56+
id="3",
57+
),
58+
),
59+
)
60+
def test_with_inventory(inventory_opts: list[str]) -> None:
61+
"""Validate using --inventory remedies syntax-check[specific] violation."""
62+
lintable = Lintable("examples/playbooks/test_using_inventory.yml")
63+
result = run_ansible_lint(lintable.filename, *inventory_opts)
64+
assert result.returncode == RC.SUCCESS
65+
66+
67+
@pytest.mark.parametrize(
68+
("inventory_opts", "error_msg"),
69+
(
70+
pytest.param(
71+
["-I", "inventories/idontexist"],
72+
"Unable to use inventories/idontexist as an inventory source: no such file or directory",
73+
id="1",
74+
),
75+
pytest.param(
76+
["-I", "inventories/badinventory"],
77+
"Unable to parse inventories/badinventory as an inventory source",
78+
id="2",
79+
),
80+
),
81+
)
82+
def test_with_inventory_emit_warning(inventory_opts: list[str], error_msg: str) -> None:
83+
"""Validate using --inventory remedies syntax-check[specific] violation."""
84+
lintable = Lintable("examples/playbooks/test_using_inventory.yml")
85+
result = run_ansible_lint(lintable.filename, *inventory_opts)
86+
assert error_msg in result.stderr
87+
88+
89+
def test_with_inventory_via_ansible_cfg(tmp_path: Path) -> None:
90+
"""Validate using inventory file from ansible.cfg remedies syntax-check[specific] violation."""
91+
(tmp_path / "ansible.cfg").write_text("[defaults]\ninventory = foo\n")
92+
(tmp_path / "foo").write_text("[group_name]\nhost1\nhost2\n")
93+
lintable = Lintable(tmp_path / "playbook.yml")
94+
lintable.content = "---\n- name: Test\n hosts:\n - group_name\n serial: \"{{ batch | default(groups['group_name'] | length) }}\"\n"
95+
lintable.kind = "playbook"
96+
lintable.write(force=True)
97+
98+
result = run_ansible_lint(lintable.filename, cwd=tmp_path)
99+
assert result.returncode == RC.SUCCESS

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ setenv =
7676
PRE_COMMIT_COLOR = always
7777
# Number of expected test passes, safety measure for accidental skip of
7878
# tests. Update value if you add/remove tests. (tox-extra)
79-
PYTEST_REQPASS = 895
79+
PYTEST_REQPASS = 901
8080
FORCE_COLOR = 1
8181
pre: PIP_PRE = 1
8282
allowlist_externals =

0 commit comments

Comments
 (0)