Skip to content

Loader

Loader Module

loader

Inventory loading functionality.

This module provides functionality to load AVD inventories from YAML files and convert them into structured data models.

InventoryLoader

InventoryLoader()

Loader for AVD inventory structures.

This class handles loading and parsing AVD inventory files from disk, validating the directory structure, and converting YAML data into structured data models.

Examples:

>>> loader = InventoryLoader()
>>> inventory = loader.load(Path("./inventory"))
>>> print(f"Loaded {len(inventory.get_all_devices())} devices")
Source code in avd_cli/logics/loader.py
def __init__(self) -> None:
    """Initialize the inventory loader."""
    self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")

load

load(inventory_path: Path) -> InventoryData

Load AVD inventory from directory.

Parameters:

Name Type Description Default
inventory_path Path

Path to inventory directory

required

Returns:

Type Description
InventoryData

Loaded inventory data structure

Raises:

Type Description
InvalidInventoryError

If inventory structure is invalid

FileSystemError

If inventory directory cannot be read

Source code in avd_cli/logics/loader.py
def load(self, inventory_path: Path) -> InventoryData:
    """Load AVD inventory from directory.

    Parameters
    ----------
    inventory_path : Path
        Path to inventory directory

    Returns
    -------
    InventoryData
        Loaded inventory data structure

    Raises
    ------
    InvalidInventoryError
        If inventory structure is invalid
    FileSystemError
        If inventory directory cannot be read
    """
    self.logger.info("Loading inventory from: %s", inventory_path)

    # Validate directory exists and is readable
    self._validate_inventory_path(inventory_path)

    # Load global variables
    global_vars = self._load_global_vars(inventory_path)

    # Load group variables from group_vars/
    group_vars = self._load_group_vars(inventory_path)

    # Load group variables from inventory.yml and merge
    inventory_group_vars = self._load_inventory_group_vars(inventory_path)
    for group_name, group_data in inventory_group_vars.items():
        if group_name in group_vars:
            # Merge: group_vars/ files take precedence over inventory.yml
            merged = {**group_data, **group_vars[group_name]}
            group_vars[group_name] = merged
        else:
            group_vars[group_name] = group_data

    # Load host variables from host_vars/
    host_vars = self._load_host_vars(inventory_path)

    # Load hosts from inventory.yml and merge with host_vars
    inventory_hosts = self._load_inventory_hosts(inventory_path)
    for hostname, host_data in inventory_hosts.items():
        if hostname in host_vars:
            # Merge: host_vars takes precedence
            merged = {**host_data, **host_vars[hostname]}
            host_vars[hostname] = merged
        else:
            host_vars[hostname] = host_data

    self.logger.info(
        "Loaded: %d global vars, %d groups, %d hosts",
        len(global_vars),
        len(group_vars),
        len(host_vars),
    )

    # Build template context BEFORE resolving templates
    # This allows templates to reference hostvars that include group vars
    self.logger.debug("Building template context with enriched hostvars")
    context = build_template_context(global_vars, group_vars, host_vars)

    # Resolve Jinja2 templates in multiple passes
    # This handles nested templates (templates that reference other templates)
    self.logger.debug("Resolving Jinja2 templates in inventory variables")
    max_passes = 5  # Prevent infinite loops
    for pass_num in range(max_passes):
        # Rebuild context with partially resolved variables
        context = build_template_context(global_vars, group_vars, host_vars)
        resolver = TemplateResolver(context)

        # Resolve templates in all variable dictionaries
        new_global_vars = resolver.resolve_recursive(global_vars)
        new_group_vars = {name: resolver.resolve_recursive(data) for name, data in group_vars.items()}
        new_host_vars = {name: resolver.resolve_recursive(data) for name, data in host_vars.items()}

        # Check if anything changed (templates resolved)
        if (
            new_global_vars == global_vars
            and new_group_vars == group_vars
            and new_host_vars == host_vars
        ):
            self.logger.debug("Template resolution complete after %d passes", pass_num + 1)
            break

        global_vars = new_global_vars
        group_vars = new_group_vars
        host_vars = new_host_vars
    else:
        self.logger.warning(
            "Template resolution reached max passes (%d), some templates may remain unresolved",
            max_passes,
        )

    # Build group hierarchy map from inventory structure
    group_hierarchy = self._build_group_hierarchy(inventory_path)

    # Build hostname-to-group mapping from inventory structure
    host_to_group = self._build_host_to_group_map(inventory_path)

    # Parse loaded data into DeviceDefinition and FabricDefinition objects
    fabrics = self._parse_fabrics(global_vars, group_vars, host_vars, group_hierarchy, host_to_group)

    # Create inventory data structure with resolved variables
    inventory = InventoryData(
        root_path=inventory_path,
        fabrics=fabrics,
        global_vars=global_vars,
        group_vars=group_vars,
        host_vars=host_vars,
    )

    return inventory