Skip to content

Commit 6fbb12e

Browse files
authored
Accommodate specified inventory files (#4393)
1 parent 2726e13 commit 6fbb12e

File tree

10 files changed

+176
-6
lines changed

10 files changed

+176
-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/bad_inventory

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[group]
2+
= = I
3+
= = am
4+
= = not
5+
= = a
6+
= = valid
7+
= = 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
@@ -457,6 +457,14 @@ def get_cli_parser() -> argparse.ArgumentParser:
457457
default=None,
458458
help=f"Specify ignore file to use. By default it will look for '{IGNORE_FILE.default}' or '{IGNORE_FILE.alternative}'",
459459
)
460+
parser.add_argument(
461+
"-I",
462+
"--inventory",
463+
dest="inventory",
464+
action="append",
465+
type=str,
466+
help="Specify inventory host path or comma separated host list",
467+
)
460468
parser.add_argument(
461469
"--offline",
462470
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
@@ -340,13 +344,16 @@ def _get_ansible_syntax_check_matches(
340344
playbook_path = fh.name
341345
else:
342346
playbook_path = str(lintable.path.expanduser())
343-
# To avoid noisy warnings we pass localhost as current inventory:
344-
# [WARNING]: No inventory was parsed, only implicit localhost is available
345-
# [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
346347
cmd = [
347348
"ansible-playbook",
348-
"-i",
349-
"localhost,",
349+
*[
350+
inventory_opt
351+
for inventory_opts in [
352+
("-i", inventory_file)
353+
for inventory_file in self._get_inventory_files(app)
354+
]
355+
for inventory_opt in inventory_opts
356+
],
350357
"--syntax-check",
351358
playbook_path,
352359
]
@@ -451,6 +458,62 @@ def _get_ansible_syntax_check_matches(
451458
fh.close()
452459
return results
453460

461+
def _get_inventory_files(self, app: App) -> list[str]:
462+
config_mgr = ConfigManager()
463+
ansible_cfg_inventory = config_mgr.get_config_value(
464+
"DEFAULT_HOST_LIST",
465+
)
466+
if app.options.inventory or ansible_cfg_inventory != [
467+
config_mgr.get_configuration_definitions()["DEFAULT_HOST_LIST"].get(
468+
"default",
469+
),
470+
]:
471+
inventory_files = [
472+
inventory_file
473+
for inventory_list in [
474+
# creates nested inventory list
475+
(inventory.split(",") if "," in inventory else [inventory])
476+
for inventory in (
477+
app.options.inventory
478+
if app.options.inventory
479+
else ansible_cfg_inventory
480+
)
481+
]
482+
for inventory_file in inventory_list
483+
]
484+
485+
# silence noise when using parse_source
486+
with mock.patch.object(
487+
ansible.inventory.manager,
488+
"display",
489+
mock.Mock(),
490+
):
491+
for inventory_file in inventory_files:
492+
if not Path(inventory_file).exists():
493+
_logger.warning(
494+
"Unable to use %s as an inventory source: no such file or directory",
495+
inventory_file,
496+
)
497+
elif os.access(
498+
inventory_file,
499+
os.R_OK,
500+
) and not ansible.inventory.manager.InventoryManager(
501+
DataLoader(),
502+
).parse_source(
503+
inventory_file,
504+
):
505+
_logger.warning(
506+
"Unable to parse %s as an inventory source",
507+
inventory_file,
508+
)
509+
else:
510+
# To avoid noisy warnings we pass localhost as current inventory:
511+
# [WARNING]: No inventory was parsed, only implicit localhost is available
512+
# [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
513+
inventory_files = ["localhost"]
514+
515+
return inventory_files
516+
454517
def _filter_excluded_matches(self, matches: list[MatchError]) -> list[MatchError]:
455518
return [
456519
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/i_dont_exist"],
72+
"Unable to use inventories/i_dont_exist as an inventory source: no such file or directory",
73+
id="1",
74+
),
75+
pytest.param(
76+
["-I", "inventories/bad_inventory"],
77+
"Unable to parse inventories/bad_inventory 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 can emit useful warnings about inventory files."""
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
@@ -75,7 +75,7 @@ setenv =
7575
PRE_COMMIT_COLOR = always
7676
# Number of expected test passes, safety measure for accidental skip of
7777
# tests. Update value if you add/remove tests. (tox-extra)
78-
PYTEST_REQPASS = 895
78+
PYTEST_REQPASS = 901
7979
FORCE_COLOR = 1
8080
pre: PIP_PRE = 1
8181
allowlist_externals =

0 commit comments

Comments
 (0)