mirror of
https://github.com/containers/ansible-podman-collections.git
synced 2026-02-04 07:11:49 +00:00
WIP: add inventory plugins
Signed-off-by: Sagi Shnaidman <sshnaidm@redhat.com>
This commit is contained in:
parent
6e8c88068f
commit
6a713de37a
4 changed files with 395 additions and 1 deletions
110
.github/workflows/test-inventory-examples.yml
vendored
Normal file
110
.github/workflows/test-inventory-examples.yml
vendored
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
name: Test inventory and example playbooks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ main, connections ]
|
||||
|
||||
jobs:
|
||||
examples:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install Ansible 2.18
|
||||
run: python3 -m pip install --user --force-reinstall --upgrade ansible-core==2.18
|
||||
|
||||
- name: Build and install the collection tarball
|
||||
run: |
|
||||
rm -rf /tmp/just_new_collection
|
||||
~/.local/bin/ansible-galaxy collection build --output-path /tmp/just_new_collection --force
|
||||
~/.local/bin/ansible-galaxy collection install -vvv --force /tmp/just_new_collection/*.tar.gz
|
||||
|
||||
- name: Install system deps (podman, buildah)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y podman buildah jq
|
||||
podman --version
|
||||
buildah --version
|
||||
|
||||
- name: Configure rootless storage
|
||||
run: |
|
||||
mkdir -p ~/.config/containers
|
||||
printf '[storage]\ndriver = "overlay"\n' > ~/.config/containers/storage.conf
|
||||
printf '[engine]\ncgroup_manager = "cgroupfs"\nevents_logger = "file"\n' > ~/.config/containers/engine.conf
|
||||
|
||||
- name: Install Ansible
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install ansible-core
|
||||
ansible --version
|
||||
|
||||
- name: Build basic containers for discovery
|
||||
run: |
|
||||
podman pull alpine:latest
|
||||
podman run -d --name podman-inventory-test alpine:latest sleep 3600
|
||||
buildah from --name hello-buildah alpine:latest
|
||||
echo 'Podman ps output:'
|
||||
podman ps -a --format json | jq '.'
|
||||
echo 'Buildah containers output:'
|
||||
buildah containers -a --json | jq '.'
|
||||
|
||||
- name: Write podman inventory source
|
||||
run: |
|
||||
mkdir -p ci/tmpinv
|
||||
cat > ci/tmpinv/podman.yml <<'EOF'
|
||||
plugin: containers.podman.podman_containers
|
||||
include_stopped: false
|
||||
connection_plugin: containers.podman.podman
|
||||
EOF
|
||||
|
||||
- name: Write buildah inventory source
|
||||
run: |
|
||||
cat > ci/tmpinv/buildah.yml <<'EOF'
|
||||
plugin: containers.podman.buildah_containers
|
||||
connection_plugin: containers.podman.buildah
|
||||
EOF
|
||||
|
||||
- name: Sanity check podman inventory
|
||||
run: |
|
||||
out=$(ANSIBLE_INVENTORY_ENABLED=containers.podman.podman_containers,yaml,ini ansible-inventory -i ci/tmpinv/podman.yml --list)
|
||||
echo "$out" | jq '.'
|
||||
echo "$out" | jq -e '. | to_entries | any(.value.hosts != null and (.value.hosts | length) > 0)'
|
||||
|
||||
- name: Sanity check buildah inventory
|
||||
run: |
|
||||
out=$(ANSIBLE_INVENTORY_ENABLED=containers.podman.buildah_containers,yaml,ini ansible-inventory -i ci/tmpinv/buildah.yml --list)
|
||||
echo "$out" | jq '.'
|
||||
echo "$out" | jq -e '. | to_entries | any(.value.hosts != null and (.value.hosts | length) > 0)'
|
||||
|
||||
- name: Run example Node build
|
||||
working-directory: playbook/examples
|
||||
run: |
|
||||
ansible-playbook -i localhost, -c local build_node_ai_api.yml -e image_name=ci-ai-node:latest
|
||||
buildah images --format '{{.Name}}:{{.Tag}}' | grep -q '^ci-ai-node:latest$'
|
||||
|
||||
- name: Run example Go multistage build
|
||||
working-directory: playbook/examples
|
||||
run: |
|
||||
ansible-playbook -i localhost, -c local build_go_ai_multistage.yml -e image_name=ci-ai-go:latest
|
||||
buildah images --format '{{.Name}}:{{.Tag}}' | grep -q '^ci-ai-go:latest$'
|
||||
|
||||
- name: Run AI env build
|
||||
working-directory: playbook/examples
|
||||
run: |
|
||||
ansible-playbook -i localhost, -c local build_ai_env_with_ansible.yml -e image_name=ci-ai-env:latest
|
||||
buildah images --format '{{.Name}}:{{.Tag}}' | grep -q '^ci-ai-env:latest$'
|
||||
|
||||
- name: Show resulting images
|
||||
run: |
|
||||
buildah images
|
||||
|
||||
|
||||
|
|
@ -6,6 +6,6 @@ ansible-galaxy collection build --output-path /tmp/ansible-lint-collection --for
|
|||
pushd /tmp/ansible-lint-collection/
|
||||
ansible-galaxy collection install -vvv --force $(ls /tmp/ansible-lint-collection/) -p /tmp/ansible-lint-installs
|
||||
pushd /tmp/ansible-lint-installs/ansible_collections/containers/podman
|
||||
ansible-test units --python 3.12 -vvv
|
||||
ansible-test units --python $(python -V | sed "s/Python //g" | awk -F"." {'print $1"."$2'}) -vvv
|
||||
popd
|
||||
popd
|
||||
|
|
|
|||
114
plugins/inventory/buildah_containers.py
Normal file
114
plugins/inventory/buildah_containers.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
# Copyright (c) 2025
|
||||
# GNU General Public License v3.0+
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
name: buildah_containers
|
||||
short_description: Inventory plugin that discovers Buildah working containers as hosts
|
||||
version_added: '1.18.0'
|
||||
description:
|
||||
- Discover Buildah working containers on the local host and add them as inventory hosts.
|
||||
- Each discovered host is assigned the Buildah connection plugin so tasks execute inside the working container.
|
||||
options:
|
||||
plugin:
|
||||
description: Token that ensures this is a source file for the 'containers.podman.buildah_containers' inventory plugin.
|
||||
required: true
|
||||
type: str
|
||||
choices: ['containers.podman.buildah_containers']
|
||||
executable:
|
||||
description: Path to the C(buildah) executable.
|
||||
type: str
|
||||
default: buildah
|
||||
env:
|
||||
- name: ANSIBLE_BUILDAH_EXECUTABLE
|
||||
name_patterns:
|
||||
description: Glob patterns to match working container names or IDs; empty means include all.
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
connection_plugin:
|
||||
description: Fully-qualified connection plugin to use for discovered hosts.
|
||||
type: str
|
||||
default: containers.podman.buildah
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
plugin: containers.podman.buildah_containers
|
||||
connection_plugin: containers.podman.buildah
|
||||
name_patterns:
|
||||
- my-build-*
|
||||
"""
|
||||
|
||||
import json
|
||||
import fnmatch
|
||||
import shutil
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
from ansible.errors import AnsibleParserError
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable
|
||||
|
||||
|
||||
class InventoryModule(BaseInventoryPlugin, Cacheable):
|
||||
NAME = "containers.podman.buildah_containers"
|
||||
|
||||
def verify_file(self, path: str) -> bool:
|
||||
if not super(InventoryModule, self).verify_file(path):
|
||||
return False
|
||||
unused, ext = os.path.splitext(path)
|
||||
if ext not in (".yml", ".yaml"):
|
||||
return False
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
header = f.read(2048)
|
||||
return (
|
||||
(f"plugin: {self.NAME}\n" in header)
|
||||
or (f"plugin: '{self.NAME}'" in header)
|
||||
or (f'plugin: "{self.NAME}"' in header)
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def parse(self, inventory, loader, path, cache=True):
|
||||
super(InventoryModule, self).parse(inventory, loader, path)
|
||||
config = self._read_config_data(path)
|
||||
|
||||
executable = config.get("executable", "buildah")
|
||||
name_patterns = list(config.get("name_patterns", []) or [])
|
||||
connection_plugin = config.get("connection_plugin", "containers.podman.buildah")
|
||||
|
||||
buildah_path = shutil.which(executable) or executable
|
||||
|
||||
# 'buildah containers -a --format json' lists working containers
|
||||
args = [buildah_path, "containers", "-a", "--json"]
|
||||
try:
|
||||
output = subprocess.check_output(args, stderr=subprocess.STDOUT)
|
||||
containers = json.loads(output.decode("utf-8"))
|
||||
except Exception as exc:
|
||||
raise AnsibleParserError(f"Failed to list buildah containers: {exc}")
|
||||
|
||||
for c in containers or []:
|
||||
name = c.get("name") or c.get("containername") or c.get("id")
|
||||
cid = c.get("id") or c.get("containerid")
|
||||
if not name and cid:
|
||||
name = cid[:12]
|
||||
|
||||
# name filtering
|
||||
if name_patterns:
|
||||
if not any(fnmatch.fnmatch(name, pat) or (cid and fnmatch.fnmatch(cid, pat)) for pat in name_patterns):
|
||||
continue
|
||||
|
||||
host = name or cid
|
||||
if not host:
|
||||
continue
|
||||
|
||||
self.inventory.add_host(host)
|
||||
self.inventory.set_variable(host, "ansible_connection", connection_plugin)
|
||||
self.inventory.set_variable(host, "ansible_host", name or cid)
|
||||
if cid:
|
||||
self.inventory.set_variable(host, "buildah_container_id", cid)
|
||||
if name:
|
||||
self.inventory.set_variable(host, "buildah_container_name", name)
|
||||
170
plugins/inventory/podman_containers.py
Normal file
170
plugins/inventory/podman_containers.py
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
# Copyright (c) 2025
|
||||
# GNU General Public License v3.0+
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
name: podman_containers
|
||||
short_description: Inventory plugin that discovers Podman containers as hosts
|
||||
version_added: '1.18.0'
|
||||
description:
|
||||
- Discover running (and optionally stopped) Podman containers on the local host and add them as inventory hosts.
|
||||
- Each discovered host is assigned an Ansible connection plugin so tasks execute inside the container without SSH.
|
||||
options:
|
||||
plugin:
|
||||
description: Token that ensures this is a source file for the 'containers.podman.podman_containers' inventory plugin.
|
||||
required: true
|
||||
type: str
|
||||
choices: ['containers.podman.podman_containers']
|
||||
executable:
|
||||
description: Path to the C(podman) executable.
|
||||
type: str
|
||||
default: podman
|
||||
env:
|
||||
- name: ANSIBLE_PODMAN_EXECUTABLE
|
||||
include_stopped:
|
||||
description: Whether to include stopped/exited containers.
|
||||
type: bool
|
||||
default: false
|
||||
name_patterns:
|
||||
description: Glob patterns to match container names or IDs; empty means include all.
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
label_selectors:
|
||||
description: Key/value labels that must match (all) for a container to be included.
|
||||
type: dict
|
||||
default: {}
|
||||
connection_plugin:
|
||||
description: Fully-qualified connection plugin to use for discovered hosts.
|
||||
type: str
|
||||
default: containers.podman.podman
|
||||
group_by_image:
|
||||
description: Add containers to a group derived from image name (e.g., C(image_node_14)).
|
||||
type: bool
|
||||
default: true
|
||||
group_by_label:
|
||||
description: Label keys to group containers by (C(label_<key>_<value>)).
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
plugin: containers.podman.podman_containers
|
||||
include_stopped: false
|
||||
label_selectors:
|
||||
role: api
|
||||
connection_plugin: containers.podman.podman
|
||||
"""
|
||||
|
||||
import json
|
||||
import fnmatch
|
||||
import shutil
|
||||
import subprocess
|
||||
import os
|
||||
from typing import Dict, List
|
||||
|
||||
from ansible.errors import AnsibleParserError
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable
|
||||
|
||||
|
||||
class InventoryModule(BaseInventoryPlugin, Cacheable):
|
||||
NAME = "containers.podman.podman_containers"
|
||||
|
||||
def verify_file(self, path: str) -> bool:
|
||||
if not super(InventoryModule, self).verify_file(path):
|
||||
return False
|
||||
unused, ext = os.path.splitext(path)
|
||||
if ext not in (".yml", ".yaml"):
|
||||
return False
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
header = f.read(2048)
|
||||
return (
|
||||
(f"plugin: {self.NAME}\n" in header)
|
||||
or (f"plugin: '{self.NAME}'" in header)
|
||||
or (f'plugin: "{self.NAME}"' in header)
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def parse(self, inventory, loader, path, cache=True):
|
||||
super(InventoryModule, self).parse(inventory, loader, path)
|
||||
config = self._read_config_data(path)
|
||||
|
||||
executable = config.get("executable", "podman")
|
||||
include_stopped = bool(config.get("include_stopped", False))
|
||||
name_patterns = list(config.get("name_patterns", []) or [])
|
||||
label_selectors: Dict[str, str] = dict(config.get("label_selectors", {}) or {})
|
||||
connection_plugin = config.get("connection_plugin", "containers.podman.podman")
|
||||
group_by_image = bool(config.get("group_by_image", True))
|
||||
group_by_label: List[str] = list(config.get("group_by_label", []) or [])
|
||||
|
||||
podman_path = shutil.which(executable) or executable
|
||||
|
||||
args = [podman_path, "ps", "--format", "json"]
|
||||
if include_stopped:
|
||||
args.insert(2, "-a")
|
||||
|
||||
try:
|
||||
output = subprocess.check_output(args, stderr=subprocess.STDOUT)
|
||||
containers = json.loads(output.decode("utf-8"))
|
||||
except Exception as exc:
|
||||
raise AnsibleParserError(f"Failed to list podman containers: {exc}")
|
||||
|
||||
for c in containers or []:
|
||||
name = (
|
||||
(c.get("Names") or [c.get("Names", "")])[0]
|
||||
if isinstance(c.get("Names"), list)
|
||||
else c.get("Names") or c.get("Names", "")
|
||||
)
|
||||
cid = c.get("Id") or c.get("ID") or c.get("Id")
|
||||
if not name and cid:
|
||||
name = cid[:12]
|
||||
|
||||
# name filtering
|
||||
if name_patterns:
|
||||
if not any(fnmatch.fnmatch(name, pat) or (cid and fnmatch.fnmatch(cid, pat)) for pat in name_patterns):
|
||||
continue
|
||||
|
||||
# label filtering
|
||||
labels = c.get("Labels") or {}
|
||||
if any(labels.get(k) != v for k, v in label_selectors.items()):
|
||||
continue
|
||||
|
||||
host = name or cid
|
||||
if not host:
|
||||
continue
|
||||
|
||||
self.inventory.add_host(host)
|
||||
# Set connection plugin and remote_addr (container id or name works)
|
||||
self.inventory.set_variable(host, "ansible_connection", connection_plugin)
|
||||
self.inventory.set_variable(host, "ansible_host", name or cid)
|
||||
|
||||
# Common vars
|
||||
image = c.get("Image") or c.get("ImageName")
|
||||
status = c.get("Status") or c.get("State")
|
||||
self.inventory.set_variable(host, "podman_container_id", cid)
|
||||
self.inventory.set_variable(host, "podman_container_name", name)
|
||||
if image:
|
||||
self.inventory.set_variable(host, "podman_image", image)
|
||||
if status:
|
||||
self.inventory.set_variable(host, "podman_status", status)
|
||||
if labels:
|
||||
self.inventory.set_variable(host, "podman_labels", labels)
|
||||
|
||||
# Grouping
|
||||
if group_by_image and image:
|
||||
safe_image = image.replace(":", "_").replace("/", "_").replace("-", "_")
|
||||
self.inventory.add_group(f"image_{safe_image}")
|
||||
self.inventory.add_host(host, group=f"image_{safe_image}")
|
||||
|
||||
for key in group_by_label:
|
||||
if key in labels:
|
||||
val = str(labels.get(key)).replace("/", "_").replace(":", "_").replace("-", "_")
|
||||
group = f"label_{key}_{val}"
|
||||
self.inventory.add_group(group)
|
||||
self.inventory.add_host(host, group=group)
|
||||
Loading…
Add table
Add a link
Reference in a new issue