mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-02-04 07:51:50 +00:00
Add Incus inventory plugin (#10972)
* BOTMETA: Add Incus inventory plugin Signed-off-by: Stéphane Graber <stgraber@stgraber.org> * plugins/inventory: Implement basic Incus support This is a simple inventory plugin leveraging the local `incus` command line tool. It supports accessing multiple remotes and projects, builds a simple group hierarchy based on the remotes and projects and exposes most properties as variable. It also supports basic filtering using the server-side filtering syntax supported by the Incus CLI. Signed-off-by: Stéphane Graber <stgraber@stgraber.org> * plugins/inventory/incus: Add support for constructable groups This allows the use of constructable groups and also allows disabling the default group structure. Signed-off-by: Stéphane Graber <stgraber@stgraber.org> * plugins/inventory/incus: Add unit tests Signed-off-by: Stéphane Graber <stgraber@stgraber.org> --------- Signed-off-by: Stéphane Graber <stgraber@stgraber.org>
This commit is contained in:
parent
af8c4fb95e
commit
a1bf2fc44a
3 changed files with 378 additions and 0 deletions
232
plugins/inventory/incus.py
Normal file
232
plugins/inventory/incus.py
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
# Copyright (c) 2025 Stéphane Graber <stgraber@stgraber.org>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
name: incus
|
||||
short_description: Incus inventory source
|
||||
version_added: 12.0.0
|
||||
author:
|
||||
- Stéphane Graber (@stgraber)
|
||||
requirements:
|
||||
- Incus CLI (C(incus))
|
||||
description:
|
||||
- Get inventory hosts from the Incus container and virtual-machine manager.
|
||||
options:
|
||||
plugin:
|
||||
description:
|
||||
- The name of this plugin, it should always be set to community.general.incus for this plugin to work.
|
||||
required: true
|
||||
choices: ['community.general.incus']
|
||||
type: str
|
||||
default_groups:
|
||||
description:
|
||||
- Whether to generate default groups based on remote and project.
|
||||
type: bool
|
||||
default: true
|
||||
filters:
|
||||
description:
|
||||
- Filter expression as supported by C(incus list).
|
||||
type: list
|
||||
elements: string
|
||||
default: []
|
||||
host_domain:
|
||||
description:
|
||||
- Domain to append to the host FQDN.
|
||||
type: string
|
||||
host_fqdn:
|
||||
description:
|
||||
- Whether to generate a FQDN for the host name.
|
||||
- This will use the INSTANCE.PROJECT.REMOTE syntax.
|
||||
type: bool
|
||||
default: true
|
||||
remotes:
|
||||
description:
|
||||
- The names of the Incus remotes to use (per C(incus remote list)).
|
||||
- Remotes are used to access multiple servers from a single client.
|
||||
- By default the inventory will go over all projects for each remote.
|
||||
- It is possible to specify a specific project using V(remote:project).
|
||||
type: list
|
||||
elements: string
|
||||
default: ["local"]
|
||||
extends_documentation_fragment:
|
||||
- ansible.builtin.constructed
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
---
|
||||
# Pull instances from all projects on the local remote.
|
||||
plugin: community.general.incus
|
||||
|
||||
---
|
||||
# Pull running VMs from all projects on the local remote.
|
||||
plugin: community.general.incus
|
||||
filters:
|
||||
- type=virtual-machine
|
||||
- status=running
|
||||
|
||||
---
|
||||
# Pull instances from two different remotes
|
||||
plugin: community.general.incus
|
||||
remotes:
|
||||
- remote-1
|
||||
- remote-2
|
||||
|
||||
---
|
||||
# Pull instances from two different remotes
|
||||
# Limiting the second to the default project
|
||||
plugin: community.general.incus
|
||||
remotes:
|
||||
- remote-1
|
||||
- remote-2:default
|
||||
"""
|
||||
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
|
||||
from ansible.utils.display import Display
|
||||
from json import loads
|
||||
from subprocess import check_output
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
"""Host inventory parser for Incus."""
|
||||
|
||||
NAME = "community.general.incus"
|
||||
|
||||
def __init__(self):
|
||||
super(InventoryModule, self).__init__()
|
||||
|
||||
def verify_file(self, path):
|
||||
valid = False
|
||||
|
||||
if super(InventoryModule, self).verify_file(path):
|
||||
if path.endswith(("incus.yaml", "incus.yml")):
|
||||
valid = True
|
||||
else:
|
||||
self.display.vvv(
|
||||
'Skipping due to inventory source not ending in "incus.yaml" nor "incus.yml"'
|
||||
)
|
||||
|
||||
return valid
|
||||
|
||||
def parse(self, inventory, loader, path, cache=True):
|
||||
super(InventoryModule, self).parse(inventory, loader, path)
|
||||
|
||||
self._read_config_data(path)
|
||||
|
||||
self.populate()
|
||||
|
||||
def populate(self):
|
||||
# Create top-level "incus" group if missing.
|
||||
default_groups = self.get_option("default_groups")
|
||||
if default_groups:
|
||||
self.inventory.add_group("incus")
|
||||
|
||||
for remote in self.get_option("remotes"):
|
||||
# Split the remote name from the project name (if specified).
|
||||
remote_name = ""
|
||||
project_name = ""
|
||||
|
||||
fields = remote.split(":", 1)
|
||||
if len(fields) == 2:
|
||||
remote_name = fields[0]
|
||||
project_name = fields[1]
|
||||
else:
|
||||
remote_name = fields[0]
|
||||
|
||||
# Create the remote-specific group if missing.
|
||||
group_remote = f"incus_{remote_name}"
|
||||
if default_groups:
|
||||
self.inventory.add_group(group_remote)
|
||||
self.inventory.add_child("incus", group_remote)
|
||||
|
||||
# Get a list of projects.
|
||||
projects = []
|
||||
if project_name:
|
||||
projects = [project_name]
|
||||
else:
|
||||
projects = [
|
||||
entry["name"]
|
||||
for entry in self._run_incus("project", "list", f"{remote_name}:")
|
||||
]
|
||||
|
||||
# Get a list of instances.
|
||||
for project in projects:
|
||||
# Create the project-specific group if missing.
|
||||
group_project = f"{group_remote}_{project}"
|
||||
if default_groups:
|
||||
self.inventory.add_group(group_project)
|
||||
self.inventory.add_child(group_remote, group_project)
|
||||
|
||||
# List the instances.
|
||||
list_cmd = [
|
||||
"list",
|
||||
f"{remote_name}:",
|
||||
"--project",
|
||||
project,
|
||||
] + self.get_option("filters")
|
||||
for instance in self._run_incus(*list_cmd):
|
||||
# Compute the host name.
|
||||
host_name = instance["name"]
|
||||
if self.get_option("host_fqdn"):
|
||||
host_name = f"{host_name}.{project}.{remote_name}"
|
||||
|
||||
domain = self.get_option("host_domain")
|
||||
if domain:
|
||||
host_name = f"{host_name}.{domain}"
|
||||
|
||||
# Add some extra variables.
|
||||
host_vars = {}
|
||||
host_vars["ansible_incus_remote"] = remote_name
|
||||
host_vars["ansible_incus_project"] = project
|
||||
|
||||
for prop in (
|
||||
"architecture",
|
||||
"config",
|
||||
"description",
|
||||
"devices",
|
||||
"ephemeral",
|
||||
"expanded_config",
|
||||
"expanded_devices",
|
||||
"location",
|
||||
"profiles",
|
||||
"status",
|
||||
"type",
|
||||
):
|
||||
host_vars[f"ansible_incus_{prop}"] = instance[prop]
|
||||
|
||||
# Add the host to the inventory and constructed groups.
|
||||
self._add_host(host_name, host_vars)
|
||||
|
||||
# Add the host to the built-in groups.
|
||||
if default_groups:
|
||||
self.inventory.add_host(host_name, group_project)
|
||||
|
||||
def _add_host(self, hostname, host_vars):
|
||||
self.inventory.add_host(hostname, group="all")
|
||||
|
||||
for var_name, var_value in host_vars.items():
|
||||
self.inventory.set_variable(hostname, var_name, var_value)
|
||||
|
||||
strict = self.get_option("strict")
|
||||
|
||||
# Add variables created by the user's Jinja2 expressions to the host
|
||||
self._set_composite_vars(
|
||||
self.get_option("compose"), host_vars, hostname, strict=True
|
||||
)
|
||||
|
||||
# Create user-defined groups using variables and Jinja2 conditionals
|
||||
self._add_host_to_composed_groups(
|
||||
self.get_option("groups"), host_vars, hostname, strict=strict
|
||||
)
|
||||
self._add_host_to_keyed_groups(
|
||||
self.get_option("keyed_groups"), host_vars, hostname, strict=strict
|
||||
)
|
||||
|
||||
def _run_incus(self, *args):
|
||||
local_cmd = ["incus"] + list(args) + ["--format=json"]
|
||||
stdout = check_output(local_cmd)
|
||||
return loads(stdout)
|
||||
Loading…
Add table
Add a link
Reference in a new issue