mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-02-04 07:51:50 +00:00
Merge fe89b60b80 into 95b24ac3fe
This commit is contained in:
commit
8321ab48e3
13 changed files with 2671 additions and 0 deletions
2
.github/BOTMETA.yml
vendored
2
.github/BOTMETA.yml
vendored
|
|
@ -742,6 +742,8 @@ files:
|
|||
maintainers: resmo
|
||||
$modules/ini_file.py:
|
||||
maintainers: jpmens noseka1
|
||||
$modules/toml_file.py:
|
||||
maintainers: jdrowne
|
||||
$modules/installp.py:
|
||||
keywords: aix efix lpar wpar
|
||||
labels: aix installp
|
||||
|
|
|
|||
973
plugins/modules/toml_file.py
Normal file
973
plugins/modules/toml_file.py
Normal file
|
|
@ -0,0 +1,973 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
# Copyright (c) 2026, Jose Drowne
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: toml_file
|
||||
short_description: Manage individual settings in TOML files
|
||||
version_added: 12.4.0
|
||||
extends_documentation_fragment:
|
||||
- ansible.builtin.files
|
||||
- community.general.attributes
|
||||
description:
|
||||
- Manage (add, remove, change) individual settings in a TOML file without having to manage the file as a whole
|
||||
with, say, M(ansible.builtin.template) or M(ansible.builtin.assemble).
|
||||
- Adds missing tables if they do not exist.
|
||||
- Comments and formatting are preserved using the C(tomlkit) library.
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
options:
|
||||
path:
|
||||
description:
|
||||
- Path to the TOML file; this file is created if required and O(create=true).
|
||||
type: path
|
||||
required: true
|
||||
aliases: [dest]
|
||||
table:
|
||||
description:
|
||||
- Table name in the TOML file.
|
||||
- Use dotted notation for nested tables (for example, V(server.database)).
|
||||
- For array of tables (C([[table]])), use bracket notation to specify the index
|
||||
(for example, V(products[0]) for the first entry, V(products[append]) to append a new entry).
|
||||
- Nested arrays of tables are supported (for example, V(servers[1].databases[0])).
|
||||
- If omitted or empty string, the key is set at the document root level.
|
||||
type: str
|
||||
key:
|
||||
description:
|
||||
- The key to manage within the table (or document root if O(table) is not specified).
|
||||
- Use dotted notation to address nested keys (for example, V(connection.timeout)).
|
||||
- May be omitted when removing an entire table with O(state=absent).
|
||||
type: str
|
||||
value:
|
||||
description:
|
||||
- The value to set for the key.
|
||||
- JSON types are automatically converted to appropriate TOML types.
|
||||
- Use O(value_type) to force a specific type.
|
||||
- Required when O(state=present) and O(key) is specified.
|
||||
type: raw
|
||||
value_type:
|
||||
description:
|
||||
- Force the value to be a specific TOML type.
|
||||
type: str
|
||||
choices:
|
||||
auto: Automatically detects the type from the input value.
|
||||
string: Forces the value to be a TOML string.
|
||||
literal_string: Creates a TOML literal string (single quotes, no escape processing).
|
||||
multiline_string: Creates a TOML multi-line basic string (triple double quotes).
|
||||
multiline_literal_string: Creates a TOML multi-line literal string (triple single quotes).
|
||||
integer: Forces the value to be a TOML integer.
|
||||
hex_integer: Forces the value to be a TOML hexadecimal integer (for example, V(0xDEADBEEF)).
|
||||
octal_integer: Forces the value to be a TOML octal integer (for example, V(0o755)).
|
||||
binary_integer: Forces the value to be a TOML binary integer (for example, V(0b11010110)).
|
||||
float: Forces the value to be a TOML float. Supports V(inf), V(-inf), and V(nan).
|
||||
boolean: Forces the value to be a TOML boolean.
|
||||
datetime: Parses the value as an ISO 8601 offset or local datetime.
|
||||
date: Parses the value as a local date (for example, V(1979-05-27)).
|
||||
time: Parses the value as a local time (for example, V(07:32:00)).
|
||||
array: Ensures the value is a TOML array.
|
||||
inline_table: Ensures the value is a TOML inline table.
|
||||
default: auto
|
||||
state:
|
||||
description:
|
||||
- If set to V(absent), the specified key (or entire table if no key is specified) is removed.
|
||||
- If set to V(present), the specified key is added or updated.
|
||||
type: str
|
||||
choices: [absent, present]
|
||||
default: present
|
||||
backup:
|
||||
description:
|
||||
- Create a backup file including the timestamp information so you can get the original file back
|
||||
if you somehow clobbered it incorrectly.
|
||||
type: bool
|
||||
default: false
|
||||
create:
|
||||
description:
|
||||
- If set to V(false), the module fails if the file does not already exist.
|
||||
- By default it creates the file if it is missing.
|
||||
type: bool
|
||||
default: true
|
||||
follow:
|
||||
description:
|
||||
- This flag indicates that filesystem links, if they exist, should be followed.
|
||||
- O(follow=true) can modify O(path) when combined with parameters such as O(mode).
|
||||
type: bool
|
||||
default: false
|
||||
requirements:
|
||||
- tomlkit
|
||||
notes:
|
||||
- The C(tomlkit) library preserves comments and formatting in TOML files.
|
||||
- When O(state=present) and O(key) is specified, O(value) must also be provided.
|
||||
seealso:
|
||||
- module: community.general.ini_file
|
||||
author:
|
||||
- Jose Drowne (@jdrowne)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Set server host in TOML file
|
||||
community.general.toml_file:
|
||||
path: /etc/myapp/config.toml
|
||||
table: server
|
||||
key: host
|
||||
value: localhost
|
||||
|
||||
- name: Set nested configuration value
|
||||
community.general.toml_file:
|
||||
path: /etc/myapp/config.toml
|
||||
table: server.database
|
||||
key: port
|
||||
value: 5432
|
||||
value_type: integer
|
||||
|
||||
- name: Set a boolean value
|
||||
community.general.toml_file:
|
||||
path: /etc/myapp/config.toml
|
||||
table: features
|
||||
key: debug
|
||||
value: true
|
||||
value_type: boolean
|
||||
|
||||
- name: Set an array value
|
||||
community.general.toml_file:
|
||||
path: /etc/myapp/config.toml
|
||||
table: server
|
||||
key: allowed_hosts
|
||||
value:
|
||||
- localhost
|
||||
- 127.0.0.1
|
||||
- "::1"
|
||||
|
||||
- name: Set a value at document root level
|
||||
community.general.toml_file:
|
||||
path: /etc/myapp/config.toml
|
||||
key: title
|
||||
value: My Application
|
||||
|
||||
- name: Remove a key from a table
|
||||
community.general.toml_file:
|
||||
path: /etc/myapp/config.toml
|
||||
table: server
|
||||
key: deprecated_option
|
||||
state: absent
|
||||
|
||||
- name: Remove an entire table
|
||||
community.general.toml_file:
|
||||
path: /etc/myapp/config.toml
|
||||
table: obsolete_section
|
||||
state: absent
|
||||
|
||||
- name: Add entry to array of tables
|
||||
community.general.toml_file:
|
||||
path: /etc/myapp/config.toml
|
||||
table: products[append]
|
||||
key: name
|
||||
value: Hammer
|
||||
|
||||
- name: Modify first entry in array of tables
|
||||
community.general.toml_file:
|
||||
path: /etc/myapp/config.toml
|
||||
table: products[0]
|
||||
key: price
|
||||
value: 9.99
|
||||
|
||||
- name: Modify nested array of tables
|
||||
community.general.toml_file:
|
||||
path: /etc/myapp/config.toml
|
||||
table: servers[0].databases[1]
|
||||
key: name
|
||||
value: secondary_db
|
||||
|
||||
- name: Set an inline table
|
||||
community.general.toml_file:
|
||||
path: /etc/myapp/config.toml
|
||||
table: database
|
||||
key: connection
|
||||
value:
|
||||
host: localhost
|
||||
port: 5432
|
||||
value_type: inline_table
|
||||
|
||||
- name: Ensure config exists with backup
|
||||
community.general.toml_file:
|
||||
path: /etc/myapp/config.toml
|
||||
table: server
|
||||
key: port
|
||||
value: 8080
|
||||
backup: true
|
||||
|
||||
- name: Set infinity value
|
||||
community.general.toml_file:
|
||||
path: /etc/myapp/config.toml
|
||||
table: limits
|
||||
key: max_value
|
||||
value: inf
|
||||
value_type: float
|
||||
|
||||
- name: Set a date value
|
||||
community.general.toml_file:
|
||||
path: /etc/myapp/config.toml
|
||||
table: project
|
||||
key: start_date
|
||||
value: "2026-01-17"
|
||||
value_type: date
|
||||
|
||||
- name: Set a time value
|
||||
community.general.toml_file:
|
||||
path: /etc/myapp/config.toml
|
||||
table: schedule
|
||||
key: daily_backup
|
||||
value: "03:00:00"
|
||||
value_type: time
|
||||
|
||||
- name: Set a literal string (no escape processing)
|
||||
community.general.toml_file:
|
||||
path: /etc/myapp/config.toml
|
||||
table: paths
|
||||
key: regex
|
||||
value: '\\d+\\.\\d+'
|
||||
value_type: literal_string
|
||||
|
||||
- name: Set a multiline string
|
||||
community.general.toml_file:
|
||||
path: /etc/myapp/config.toml
|
||||
table: messages
|
||||
key: welcome
|
||||
value: |
|
||||
Welcome to our application!
|
||||
We hope you enjoy using it.
|
||||
value_type: multiline_string
|
||||
|
||||
- name: Set a hexadecimal integer
|
||||
community.general.toml_file:
|
||||
path: /etc/myapp/config.toml
|
||||
table: display
|
||||
key: color
|
||||
value: 16777215
|
||||
value_type: hex_integer
|
||||
|
||||
- name: Set an octal integer (file permissions)
|
||||
community.general.toml_file:
|
||||
path: /etc/myapp/config.toml
|
||||
table: files
|
||||
key: mode
|
||||
value: 493
|
||||
value_type: octal_integer
|
||||
|
||||
- name: Set a binary integer
|
||||
community.general.toml_file:
|
||||
path: /etc/myapp/config.toml
|
||||
table: flags
|
||||
key: bits
|
||||
value: 170
|
||||
value_type: binary_integer
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
path:
|
||||
description: The path to the TOML file.
|
||||
returned: success
|
||||
type: str
|
||||
sample: /etc/myapp/config.toml
|
||||
backup_file:
|
||||
description: The name of the backup file that was created.
|
||||
returned: when O(backup=true) and file was modified
|
||||
type: str
|
||||
sample: /etc/myapp/config.toml.2026-01-17@12:34:56~
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import traceback
|
||||
import typing as t
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils import deps
|
||||
|
||||
with deps.declare("tomlkit"):
|
||||
import tomlkit
|
||||
from tomlkit.items import AoT, Array, InlineTable, Integer, Table, Trivia
|
||||
|
||||
|
||||
class TomlFileError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
# Type alias for array-of-tables index (int for position, "append" for new entry, None for default)
|
||||
IndexType = t.Union[int, t.Literal["append"], None] # noqa: UP007
|
||||
|
||||
|
||||
@dataclass
|
||||
class TomlParams:
|
||||
"""Parameters for TOML file operations."""
|
||||
|
||||
path: str
|
||||
table: str | None
|
||||
key: str | None
|
||||
value: t.Any
|
||||
value_type: t.Literal[
|
||||
"auto",
|
||||
"string",
|
||||
"literal_string",
|
||||
"multiline_string",
|
||||
"multiline_literal_string",
|
||||
"integer",
|
||||
"hex_integer",
|
||||
"octal_integer",
|
||||
"binary_integer",
|
||||
"float",
|
||||
"boolean",
|
||||
"datetime",
|
||||
"date",
|
||||
"time",
|
||||
"array",
|
||||
"inline_table",
|
||||
]
|
||||
state: t.Literal["absent", "present"]
|
||||
backup: bool
|
||||
create: bool
|
||||
follow: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class NavContext:
|
||||
"""Context for table navigation operations."""
|
||||
|
||||
parsed: list[tuple[str, IndexType]]
|
||||
create: bool
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Path Parsing Functions
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def parse_table_path(table_path: str | None) -> list[tuple[str, IndexType]]:
|
||||
"""Parse a table path with optional array indices."""
|
||||
if table_path is None or table_path == "":
|
||||
return [] # Empty list means document root
|
||||
|
||||
result = []
|
||||
segment_pattern = re.compile(r"^([^\[\]]+)(?:\[(-?\d+|append)\])?$")
|
||||
|
||||
for segment in table_path.split("."):
|
||||
match = segment_pattern.match(segment)
|
||||
if not match:
|
||||
raise TomlFileError(f"Invalid table path segment: '{segment}'")
|
||||
name = match.group(1)
|
||||
index_str = match.group(2)
|
||||
index: IndexType
|
||||
if index_str is None:
|
||||
index = None
|
||||
elif index_str == "append":
|
||||
index = "append"
|
||||
else:
|
||||
index = int(index_str)
|
||||
result.append((name, index))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _format_path(parsed: list[tuple[str, IndexType]], up_to: int | None = None) -> str:
|
||||
"""Format a parsed path back to string for error messages."""
|
||||
if up_to is None:
|
||||
up_to = len(parsed)
|
||||
return ".".join(name + (f"[{idx}]" if idx is not None else "") for name, idx in parsed[:up_to])
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Value Conversion Functions (dispatch pattern for O(1) lookup)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def _convert_auto(value: t.Any) -> t.Any:
|
||||
"""Auto-detect and convert value type."""
|
||||
if isinstance(value, (bool, int, float)):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
return _try_parse_datetime_string(value)
|
||||
if isinstance(value, list):
|
||||
return _convert_list_to_array(value)
|
||||
if isinstance(value, dict):
|
||||
return _convert_dict_to_inline_table(value)
|
||||
return str(value)
|
||||
|
||||
|
||||
def _try_parse_datetime_string(value: str) -> str | datetime:
|
||||
"""Try parsing string as datetime, return as-is if not."""
|
||||
if "T" in value or " " in value:
|
||||
try:
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
return value
|
||||
|
||||
|
||||
def _convert_list_to_array(value: list[t.Any]) -> Array:
|
||||
"""Convert Python list to TOML array."""
|
||||
arr = tomlkit.array()
|
||||
for item in value:
|
||||
arr.append(convert_value(item, "auto"))
|
||||
return arr
|
||||
|
||||
|
||||
def _convert_dict_to_inline_table(value: dict[str, t.Any]) -> InlineTable:
|
||||
"""Convert Python dict to TOML inline table."""
|
||||
tbl = tomlkit.inline_table()
|
||||
for k, v in value.items():
|
||||
tbl[k] = convert_value(v, "auto")
|
||||
return tbl
|
||||
|
||||
|
||||
def _convert_string(value: t.Any) -> str:
|
||||
return str(value)
|
||||
|
||||
|
||||
def _convert_literal_string(value: t.Any) -> str:
|
||||
return tomlkit.string(str(value), literal=True)
|
||||
|
||||
|
||||
def _convert_multiline_string(value: t.Any) -> str:
|
||||
return tomlkit.string(str(value), multiline=True)
|
||||
|
||||
|
||||
def _convert_multiline_literal_string(value: t.Any) -> str:
|
||||
return tomlkit.string(str(value), literal=True, multiline=True)
|
||||
|
||||
|
||||
def _convert_integer(value: t.Any) -> int:
|
||||
return int(value)
|
||||
|
||||
|
||||
def _convert_hex_integer(value: t.Any) -> Integer:
|
||||
int_val = int(value) if isinstance(value, int) else int(str(value), 16)
|
||||
return Integer(int_val, Trivia(), hex(int_val))
|
||||
|
||||
|
||||
def _convert_octal_integer(value: t.Any) -> Integer:
|
||||
int_val = int(value) if isinstance(value, int) else int(str(value), 8)
|
||||
return Integer(int_val, Trivia(), oct(int_val))
|
||||
|
||||
|
||||
def _convert_binary_integer(value: t.Any) -> Integer:
|
||||
int_val = int(value) if isinstance(value, int) else int(str(value), 2)
|
||||
return Integer(int_val, Trivia(), bin(int_val))
|
||||
|
||||
|
||||
def _convert_float(value: t.Any) -> float:
|
||||
if isinstance(value, str):
|
||||
return float(value.lower())
|
||||
return float(value)
|
||||
|
||||
|
||||
def _convert_boolean(value: t.Any) -> bool:
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
return value.lower() in ("true", "yes", "1", "on")
|
||||
return bool(value)
|
||||
|
||||
|
||||
def _convert_datetime(value: t.Any) -> datetime:
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
return datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
||||
|
||||
|
||||
def _convert_date(value: t.Any) -> t.Any:
|
||||
from datetime import date as date_type
|
||||
|
||||
if isinstance(value, date_type) and not isinstance(value, datetime):
|
||||
return tomlkit.date(value.isoformat())
|
||||
return tomlkit.date(str(value))
|
||||
|
||||
|
||||
def _convert_time(value: t.Any) -> t.Any:
|
||||
from datetime import time as time_type
|
||||
|
||||
if isinstance(value, time_type):
|
||||
return tomlkit.time(value.isoformat())
|
||||
return tomlkit.time(str(value))
|
||||
|
||||
|
||||
def _convert_array(value: t.Any) -> Array:
|
||||
if isinstance(value, list):
|
||||
return _convert_list_to_array(value)
|
||||
arr = tomlkit.array()
|
||||
arr.append(convert_value(value, "auto"))
|
||||
return arr
|
||||
|
||||
|
||||
def _convert_inline_table(value: t.Any) -> InlineTable:
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"Cannot convert {type(value).__name__} to inline_table")
|
||||
return _convert_dict_to_inline_table(value)
|
||||
|
||||
|
||||
# Dispatch dict for O(1) type lookup
|
||||
_CONVERTERS = {
|
||||
"auto": _convert_auto,
|
||||
"string": _convert_string,
|
||||
"literal_string": _convert_literal_string,
|
||||
"multiline_string": _convert_multiline_string,
|
||||
"multiline_literal_string": _convert_multiline_literal_string,
|
||||
"integer": _convert_integer,
|
||||
"hex_integer": _convert_hex_integer,
|
||||
"octal_integer": _convert_octal_integer,
|
||||
"binary_integer": _convert_binary_integer,
|
||||
"float": _convert_float,
|
||||
"boolean": _convert_boolean,
|
||||
"datetime": _convert_datetime,
|
||||
"date": _convert_date,
|
||||
"time": _convert_time,
|
||||
"array": _convert_array,
|
||||
"inline_table": _convert_inline_table,
|
||||
}
|
||||
|
||||
|
||||
def convert_value(value: t.Any, value_type: str = "auto") -> t.Any:
|
||||
"""Convert a value to the appropriate TOML type."""
|
||||
converter = _CONVERTERS.get(value_type)
|
||||
if converter:
|
||||
return converter(value)
|
||||
return value
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Table Navigation Functions (extracted for reduced nesting)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def load_toml_document(path: str, create: bool = True) -> tomlkit.TOMLDocument:
|
||||
"""Load a TOML document from file."""
|
||||
if not os.path.exists(path):
|
||||
if not create:
|
||||
raise TomlFileError(f"Destination {path} does not exist!")
|
||||
return tomlkit.document()
|
||||
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return tomlkit.parse(f.read())
|
||||
except Exception as e:
|
||||
raise TomlFileError(f"Failed to parse TOML file: {e}") from e
|
||||
|
||||
|
||||
def _create_new_aot_entry(current: Table | tomlkit.TOMLDocument, name: str) -> Table:
|
||||
"""Create a new array of tables with first entry."""
|
||||
new_table = tomlkit.table()
|
||||
aot = tomlkit.aot()
|
||||
aot.append(new_table)
|
||||
current[name] = aot
|
||||
return new_table
|
||||
|
||||
|
||||
def _create_regular_table(current: Table | tomlkit.TOMLDocument, name: str) -> Table:
|
||||
"""Create a regular table."""
|
||||
new_table = tomlkit.table()
|
||||
current[name] = new_table
|
||||
return new_table
|
||||
|
||||
|
||||
def _create_table_segment(
|
||||
current: Table | tomlkit.TOMLDocument, name: str, index: IndexType, ctx: NavContext, i: int
|
||||
) -> Table:
|
||||
"""Create a new table segment when name not in current."""
|
||||
if not ctx.create:
|
||||
raise TomlFileError(f"Table '{_format_path(ctx.parsed, i + 1)}' does not exist")
|
||||
if index == "append":
|
||||
return _create_new_aot_entry(current, name)
|
||||
if index is not None:
|
||||
raise TomlFileError(f"Cannot create table at index {index} - array of tables '{name}' does not exist")
|
||||
return _create_regular_table(current, name)
|
||||
|
||||
|
||||
def _get_aot_default(aot: AoT, ctx: NavContext, i: int) -> Table:
|
||||
"""Get first entry from AoT when no index specified."""
|
||||
if len(aot) == 0:
|
||||
raise TomlFileError(f"Array of tables '{_format_path(ctx.parsed, i + 1)}' is empty")
|
||||
return aot[0]
|
||||
|
||||
|
||||
def _get_aot_at_index(aot: AoT, index: int, ctx: NavContext, i: int) -> Table:
|
||||
"""Get AoT entry at specific index."""
|
||||
if 0 <= index < len(aot):
|
||||
return aot[index]
|
||||
raise TomlFileError(f"Array of tables index {index} is out of range for '{_format_path(ctx.parsed, i + 1)}'")
|
||||
|
||||
|
||||
def _handle_aot_append(aot: AoT, is_last: bool, ctx: NavContext, i: int) -> Table:
|
||||
"""Handle AoT[append] - append a new entry to the array of tables."""
|
||||
if not is_last:
|
||||
raise TomlFileError(f"Cannot use [append] in middle of path '{_format_path(ctx.parsed)}'")
|
||||
if not ctx.create:
|
||||
raise TomlFileError("Cannot use [append] with state=absent; use [-1] to access the last entry")
|
||||
new_table = tomlkit.table()
|
||||
new_table.trivia.indent = "\n"
|
||||
aot.append(new_table)
|
||||
return new_table
|
||||
|
||||
|
||||
def _navigate_aot(aot: AoT, index: IndexType, is_last: bool, ctx: NavContext, i: int) -> Table:
|
||||
"""Navigate within an array of tables."""
|
||||
if index is None:
|
||||
return _get_aot_default(aot, ctx, i)
|
||||
if index == "append":
|
||||
return _handle_aot_append(aot, is_last, ctx, i)
|
||||
return _get_aot_at_index(aot, index, ctx, i)
|
||||
|
||||
|
||||
def _navigate_existing_segment(
|
||||
current: Table | tomlkit.TOMLDocument, name: str, index: IndexType, ctx: NavContext, i: int
|
||||
) -> Table | tomlkit.TOMLDocument:
|
||||
"""Navigate into an existing table segment."""
|
||||
item = current[name]
|
||||
is_last = i == len(ctx.parsed) - 1
|
||||
if isinstance(item, AoT):
|
||||
return _navigate_aot(item, index, is_last, ctx, i)
|
||||
if isinstance(item, (Table, dict)):
|
||||
if index is not None:
|
||||
raise TomlFileError(f"'{_format_path(ctx.parsed, i + 1)}' is not an array of tables, cannot use index")
|
||||
return item
|
||||
raise TomlFileError(f"'{_format_path(ctx.parsed, i + 1)}' is not a table")
|
||||
|
||||
|
||||
def navigate_to_table(
|
||||
doc: tomlkit.TOMLDocument, table_path: str | None, create: bool = False
|
||||
) -> Table | tomlkit.TOMLDocument:
|
||||
"""Navigate to a table in the document."""
|
||||
parsed = parse_table_path(table_path)
|
||||
if not parsed:
|
||||
return doc
|
||||
|
||||
ctx = NavContext(parsed=parsed, create=create)
|
||||
current: Table | tomlkit.TOMLDocument = doc
|
||||
for i, (name, index) in enumerate(parsed):
|
||||
if name not in current:
|
||||
current = _create_table_segment(current, name, index, ctx, i)
|
||||
else:
|
||||
current = _navigate_existing_segment(current, name, index, ctx, i)
|
||||
return current
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Key Operations
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def navigate_to_key_parent(
|
||||
target: Table | tomlkit.TOMLDocument, key: str
|
||||
) -> tuple[Table | InlineTable | tomlkit.TOMLDocument, str]:
|
||||
"""Navigate to the parent container of a key."""
|
||||
parts = key.split(".")
|
||||
if len(parts) == 1:
|
||||
return target, key
|
||||
|
||||
parent: Table | InlineTable | tomlkit.TOMLDocument = target
|
||||
for i, part in enumerate(parts[:-1]):
|
||||
if part not in parent:
|
||||
new_table = tomlkit.inline_table()
|
||||
parent[part] = new_table
|
||||
parent = new_table
|
||||
else:
|
||||
item = parent[part]
|
||||
if isinstance(item, (Table, InlineTable, dict)):
|
||||
parent = item
|
||||
else:
|
||||
raise TomlFileError(f"'{'.'.join(parts[: i + 1])}' is not a table, cannot navigate further")
|
||||
|
||||
return parent, parts[-1]
|
||||
|
||||
|
||||
def _values_equal(a: t.Any, b: t.Any) -> bool:
|
||||
"""Check if two values are equal."""
|
||||
if isinstance(a, Array) and isinstance(b, (Array, list)):
|
||||
if len(a) != len(b):
|
||||
return False
|
||||
return all(_values_equal(x, y) for x, y in zip(a, b))
|
||||
if isinstance(a, (InlineTable, Table)) and isinstance(b, (InlineTable, Table, dict)):
|
||||
if len(a) != len(b):
|
||||
return False
|
||||
return all(k in b and _values_equal(a[k], b[k]) for k in a)
|
||||
return a == b
|
||||
|
||||
|
||||
def set_key_value(target: Table | tomlkit.TOMLDocument, key: str, value: t.Any, value_type: str = "auto") -> bool:
|
||||
"""Set a key to a value in the target table."""
|
||||
parent, final_key = navigate_to_key_parent(target, key)
|
||||
converted_value = convert_value(value, value_type)
|
||||
|
||||
if final_key in parent:
|
||||
existing = parent[final_key]
|
||||
if _values_equal(existing, converted_value):
|
||||
return False
|
||||
|
||||
parent[final_key] = converted_value
|
||||
return True
|
||||
|
||||
|
||||
def remove_key(target: Table | tomlkit.TOMLDocument, key: str) -> bool:
|
||||
"""Remove a key from the target table."""
|
||||
parts = key.split(".")
|
||||
parent: Table | InlineTable | tomlkit.TOMLDocument = target
|
||||
for part in parts[:-1]:
|
||||
if part not in parent:
|
||||
return False
|
||||
item = parent[part]
|
||||
if isinstance(item, (Table, InlineTable, dict)):
|
||||
parent = item
|
||||
else:
|
||||
return False
|
||||
|
||||
final_key = parts[-1]
|
||||
if final_key in parent:
|
||||
del parent[final_key]
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Table Removal
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def remove_table(doc: tomlkit.TOMLDocument, table_path: str) -> bool:
|
||||
"""Remove a table from the document."""
|
||||
parsed = parse_table_path(table_path)
|
||||
if not parsed:
|
||||
raise TomlFileError("Cannot remove document root")
|
||||
|
||||
if len(parsed) == 1:
|
||||
parent: Table | tomlkit.TOMLDocument = doc
|
||||
else:
|
||||
try:
|
||||
parent_path = _format_path(parsed, len(parsed) - 1)
|
||||
parent = navigate_to_table(doc, parent_path, create=False)
|
||||
except TomlFileError:
|
||||
return False
|
||||
|
||||
name, index = parsed[-1]
|
||||
if name not in parent:
|
||||
return False
|
||||
|
||||
item = parent[name]
|
||||
if isinstance(item, AoT) and index is not None:
|
||||
return _remove_aot_entry(item, index)
|
||||
|
||||
del parent[name]
|
||||
return True
|
||||
|
||||
|
||||
def _remove_aot_entry(aot: AoT, index: int | t.Literal["append"]) -> bool:
|
||||
"""Remove an entry from an array of tables."""
|
||||
if index == "append":
|
||||
raise TomlFileError("Cannot use [append] with state=absent; use [-1] to remove the last entry")
|
||||
if index == -1:
|
||||
if len(aot) > 0:
|
||||
del aot[-1]
|
||||
return True
|
||||
return False
|
||||
if 0 <= index < len(aot):
|
||||
del aot[index]
|
||||
return True
|
||||
raise TomlFileError(f"Array of tables index {index} out of range")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Main TOML Operations (extracted for reduced nesting and statements)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def _init_diff(path: str, diff_enabled: bool, doc: tomlkit.TOMLDocument) -> dict[str, str]:
|
||||
"""Initialize the diff dictionary."""
|
||||
diff = {
|
||||
"before": "",
|
||||
"after": "",
|
||||
"before_header": f"{path} (content)",
|
||||
"after_header": f"{path} (content)",
|
||||
}
|
||||
if diff_enabled:
|
||||
diff["before"] = tomlkit.dumps(doc)
|
||||
return diff
|
||||
|
||||
|
||||
def _handle_present(doc: tomlkit.TOMLDocument, params: TomlParams, original_content: str) -> tuple[bool, str]:
|
||||
"""Handle state=present operations."""
|
||||
if params.key:
|
||||
if params.value is None:
|
||||
raise TomlFileError("Parameter 'value' is required when state=present and key is specified")
|
||||
target = navigate_to_table(doc, params.table, create=True)
|
||||
changed = set_key_value(target, params.key, params.value, params.value_type)
|
||||
msg = "key added" if changed else "OK"
|
||||
return changed, msg
|
||||
|
||||
if params.table:
|
||||
navigate_to_table(doc, params.table, create=True)
|
||||
new_content = tomlkit.dumps(doc)
|
||||
if new_content != original_content:
|
||||
return True, "table added"
|
||||
return False, "nothing to do"
|
||||
|
||||
|
||||
def _handle_absent(doc: tomlkit.TOMLDocument, params: TomlParams) -> tuple[bool, str]:
|
||||
"""Handle state=absent operations."""
|
||||
if params.key:
|
||||
try:
|
||||
target = navigate_to_table(doc, params.table, create=False)
|
||||
changed = remove_key(target, params.key)
|
||||
return changed, "key removed" if changed else "key not found"
|
||||
except TomlFileError:
|
||||
return False, "key not found"
|
||||
|
||||
if params.table:
|
||||
changed = remove_table(doc, params.table)
|
||||
return changed, "table removed" if changed else "table not found"
|
||||
return False, "nothing to do"
|
||||
|
||||
|
||||
def _write_if_changed(
|
||||
module: AnsibleModule, target_path: str, doc: tomlkit.TOMLDocument, changed: bool, backup: bool
|
||||
) -> str | None:
|
||||
"""Write the document to file if changed."""
|
||||
if not changed or module.check_mode:
|
||||
return None
|
||||
|
||||
backup_file = None
|
||||
if backup and os.path.exists(target_path):
|
||||
backup_file = module.backup_local(target_path)
|
||||
|
||||
destpath = os.path.dirname(target_path)
|
||||
if destpath and not os.path.exists(destpath):
|
||||
os.makedirs(destpath)
|
||||
|
||||
content = tomlkit.dumps(doc)
|
||||
encoded_content = to_bytes(content)
|
||||
|
||||
try:
|
||||
tmpfd, tmpfile = tempfile.mkstemp(dir=module.tmpdir)
|
||||
with os.fdopen(tmpfd, "wb") as f:
|
||||
f.write(encoded_content)
|
||||
except OSError:
|
||||
module.fail_json(msg="Unable to create temporary file", exception=traceback.format_exc())
|
||||
|
||||
try:
|
||||
module.atomic_move(tmpfile, os.path.abspath(target_path))
|
||||
except OSError:
|
||||
module.fail_json(
|
||||
msg=f"Unable to move temporary file {tmpfile} to {target_path}", exception=traceback.format_exc()
|
||||
)
|
||||
|
||||
return backup_file
|
||||
|
||||
|
||||
def do_toml(module: AnsibleModule, params: TomlParams) -> tuple[bool, str | None, dict[str, str], str]:
|
||||
"""Execute the main TOML file operation."""
|
||||
target_path = os.path.realpath(params.path) if params.follow and os.path.islink(params.path) else params.path
|
||||
doc = load_toml_document(target_path, params.create)
|
||||
|
||||
diff = _init_diff(params.path, module._diff, doc)
|
||||
original_content = tomlkit.dumps(doc)
|
||||
|
||||
if params.state == "present":
|
||||
changed, msg = _handle_present(doc, params, original_content)
|
||||
else:
|
||||
changed, msg = _handle_absent(doc, params)
|
||||
|
||||
if module._diff:
|
||||
diff["after"] = tomlkit.dumps(doc)
|
||||
|
||||
backup_file = _write_if_changed(module, target_path, doc, changed, params.backup)
|
||||
return changed, backup_file, diff, msg
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Module Entry Point
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def main() -> None:
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
path=dict(type="path", required=True, aliases=["dest"]),
|
||||
table=dict(type="str"),
|
||||
key=dict(type="str", no_log=False),
|
||||
value=dict(type="raw"),
|
||||
value_type=dict(
|
||||
type="str",
|
||||
default="auto",
|
||||
choices=[
|
||||
"auto",
|
||||
"string",
|
||||
"literal_string",
|
||||
"multiline_string",
|
||||
"multiline_literal_string",
|
||||
"integer",
|
||||
"hex_integer",
|
||||
"octal_integer",
|
||||
"binary_integer",
|
||||
"float",
|
||||
"boolean",
|
||||
"datetime",
|
||||
"date",
|
||||
"time",
|
||||
"array",
|
||||
"inline_table",
|
||||
],
|
||||
),
|
||||
state=dict(type="str", default="present", choices=["absent", "present"]),
|
||||
backup=dict(type="bool", default=False),
|
||||
create=dict(type="bool", default=True),
|
||||
follow=dict(type="bool", default=False),
|
||||
),
|
||||
add_file_common_args=True,
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
deps.validate(module)
|
||||
|
||||
params = TomlParams(
|
||||
path=module.params["path"],
|
||||
table=module.params["table"],
|
||||
key=module.params["key"],
|
||||
value=module.params["value"],
|
||||
value_type=module.params["value_type"],
|
||||
state=module.params["state"],
|
||||
backup=module.params["backup"],
|
||||
create=module.params["create"],
|
||||
follow=module.params["follow"],
|
||||
)
|
||||
|
||||
try:
|
||||
changed, backup_file, diff, msg = do_toml(module, params)
|
||||
except TomlFileError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
if not module.check_mode and os.path.exists(params.path):
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
changed = module.set_fs_attributes_if_different(file_args, changed)
|
||||
|
||||
results = dict(
|
||||
changed=changed,
|
||||
diff=diff,
|
||||
msg=msg,
|
||||
path=params.path,
|
||||
)
|
||||
if backup_file is not None:
|
||||
results["backup_file"] = backup_file
|
||||
|
||||
module.exit_json(**results)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
5
tests/integration/targets/toml_file/aliases
Normal file
5
tests/integration/targets/toml_file/aliases
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Copyright (c) Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
azp/posix/2
|
||||
7
tests/integration/targets/toml_file/meta/main.yml
Normal file
7
tests/integration/targets/toml_file/meta/main.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
# Copyright (c) Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
dependencies:
|
||||
- setup_remote_tmp_dir
|
||||
188
tests/integration/targets/toml_file/tasks/00-basic.yml
Normal file
188
tests/integration/targets/toml_file/tasks/00-basic.yml
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
---
|
||||
# Copyright (c) Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
## Basic tests - create, update, remove key
|
||||
|
||||
- name: test-basic 1 - set "create=false" on non-existing file and fail
|
||||
toml_file:
|
||||
path: "{{ non_existing_file }}"
|
||||
table: server
|
||||
key: host
|
||||
value: localhost
|
||||
create: false
|
||||
register: result_basic_1
|
||||
ignore_errors: true
|
||||
|
||||
- name: test-basic 1 - verify error message
|
||||
assert:
|
||||
that:
|
||||
- result_basic_1 is not changed
|
||||
- result_basic_1 is failed
|
||||
- "'does not exist' in result_basic_1.msg"
|
||||
|
||||
- name: test-basic 2 - create a new TOML file with a key
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server
|
||||
key: host
|
||||
value: localhost
|
||||
register: result_basic_2
|
||||
|
||||
- name: test-basic 2 - verify file created
|
||||
assert:
|
||||
that:
|
||||
- result_basic_2 is changed
|
||||
- result_basic_2.msg == "key added"
|
||||
|
||||
- name: test-basic 2 - read and verify content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_basic_2
|
||||
|
||||
- name: test-basic 2 - verify TOML content
|
||||
vars:
|
||||
expected: |
|
||||
[server]
|
||||
host = "localhost"
|
||||
assert:
|
||||
that:
|
||||
- (content_basic_2.content | b64decode) == expected
|
||||
|
||||
- name: test-basic 3 - set the same key again (idempotent)
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server
|
||||
key: host
|
||||
value: localhost
|
||||
register: result_basic_3
|
||||
|
||||
- name: test-basic 3 - verify no change
|
||||
assert:
|
||||
that:
|
||||
- result_basic_3 is not changed
|
||||
|
||||
- name: test-basic 4 - update the key value
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server
|
||||
key: host
|
||||
value: "192.168.1.1"
|
||||
register: result_basic_4
|
||||
|
||||
- name: test-basic 4 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_basic_4 is changed
|
||||
|
||||
- name: test-basic 4 - read and verify content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_basic_4
|
||||
|
||||
- name: test-basic 4 - verify updated TOML content
|
||||
vars:
|
||||
expected: |
|
||||
[server]
|
||||
host = "192.168.1.1"
|
||||
assert:
|
||||
that:
|
||||
- (content_basic_4.content | b64decode) == expected
|
||||
|
||||
- name: test-basic 5 - remove the key
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server
|
||||
key: host
|
||||
state: absent
|
||||
register: result_basic_5
|
||||
|
||||
- name: test-basic 5 - verify removal
|
||||
assert:
|
||||
that:
|
||||
- result_basic_5 is changed
|
||||
- result_basic_5.msg == "key removed"
|
||||
|
||||
- name: test-basic 5 - read and verify content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_basic_5
|
||||
|
||||
- name: test-basic 5 - verify key removed from TOML
|
||||
vars:
|
||||
expected: |
|
||||
[server]
|
||||
assert:
|
||||
that:
|
||||
- (content_basic_5.content | b64decode) == expected
|
||||
|
||||
- name: test-basic 6 - remove non-existing key (idempotent)
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server
|
||||
key: nonexistent
|
||||
state: absent
|
||||
register: result_basic_6
|
||||
|
||||
- name: test-basic 6 - verify no change
|
||||
assert:
|
||||
that:
|
||||
- result_basic_6 is not changed
|
||||
- result_basic_6.msg == "key not found"
|
||||
|
||||
- name: test-basic 7 - set key at document root
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
key: title
|
||||
value: "My Application"
|
||||
register: result_basic_7
|
||||
|
||||
- name: test-basic 7 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_basic_7 is changed
|
||||
|
||||
- name: test-basic 7 - read and verify content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_basic_7
|
||||
|
||||
- name: test-basic 7 - verify root-level key
|
||||
vars:
|
||||
expected: |
|
||||
title = "My Application"
|
||||
|
||||
[server]
|
||||
assert:
|
||||
that:
|
||||
- (content_basic_7.content | b64decode) == expected
|
||||
|
||||
- name: test-basic 8 - check mode test (no actual change)
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server
|
||||
key: port
|
||||
value: 8080
|
||||
check_mode: true
|
||||
register: result_basic_8
|
||||
|
||||
- name: test-basic 8 - verify check mode reports change
|
||||
assert:
|
||||
that:
|
||||
- result_basic_8 is changed
|
||||
|
||||
- name: test-basic 8 - verify file was not actually modified
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_basic_8
|
||||
|
||||
- name: test-basic 8 - verify port not in file
|
||||
vars:
|
||||
expected: |
|
||||
title = "My Application"
|
||||
|
||||
[server]
|
||||
assert:
|
||||
that:
|
||||
- (content_basic_8.content | b64decode) == expected
|
||||
135
tests/integration/targets/toml_file/tasks/01-tables.yml
Normal file
135
tests/integration/targets/toml_file/tasks/01-tables.yml
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
---
|
||||
# Copyright (c) Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
## Table tests - create and remove tables
|
||||
|
||||
- name: test-table 1 - create a table with a key
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: database
|
||||
key: host
|
||||
value: localhost
|
||||
register: result_table_1
|
||||
|
||||
- name: test-table 1 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_table_1 is changed
|
||||
|
||||
- name: test-table 1 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_table_1
|
||||
|
||||
- name: test-table 1 - verify table created
|
||||
vars:
|
||||
expected: |
|
||||
[database]
|
||||
host = "localhost"
|
||||
assert:
|
||||
that:
|
||||
- (content_table_1.content | b64decode) == expected
|
||||
|
||||
- name: test-table 2 - add another key to the same table
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: database
|
||||
key: port
|
||||
value: 5432
|
||||
register: result_table_2
|
||||
|
||||
- name: test-table 2 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_table_2 is changed
|
||||
|
||||
- name: test-table 2 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_table_2
|
||||
|
||||
- name: test-table 2 - verify key added
|
||||
vars:
|
||||
expected: |
|
||||
[database]
|
||||
host = "localhost"
|
||||
port = 5432
|
||||
assert:
|
||||
that:
|
||||
- (content_table_2.content | b64decode) == expected
|
||||
|
||||
- name: test-table 3 - create a second table
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server
|
||||
key: name
|
||||
value: myserver
|
||||
register: result_table_3
|
||||
|
||||
- name: test-table 3 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_table_3 is changed
|
||||
|
||||
- name: test-table 3 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_table_3
|
||||
|
||||
- name: test-table 3 - verify both tables exist
|
||||
vars:
|
||||
expected: |
|
||||
[database]
|
||||
host = "localhost"
|
||||
port = 5432
|
||||
|
||||
[server]
|
||||
name = "myserver"
|
||||
assert:
|
||||
that:
|
||||
- (content_table_3.content | b64decode) == expected
|
||||
|
||||
- name: test-table 4 - remove entire table
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server
|
||||
state: absent
|
||||
register: result_table_4
|
||||
|
||||
- name: test-table 4 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_table_4 is changed
|
||||
- result_table_4.msg == "table removed"
|
||||
|
||||
- name: test-table 4 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_table_4
|
||||
|
||||
- name: test-table 4 - verify table removed
|
||||
vars:
|
||||
# Note: tomlkit preserves the blank line that preceded the removed table
|
||||
expected: |+
|
||||
[database]
|
||||
host = "localhost"
|
||||
port = 5432
|
||||
|
||||
assert:
|
||||
that:
|
||||
- (content_table_4.content | b64decode) == expected
|
||||
|
||||
- name: test-table 5 - remove non-existing table (idempotent)
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: nonexistent
|
||||
state: absent
|
||||
register: result_table_5
|
||||
|
||||
- name: test-table 5 - verify no change
|
||||
assert:
|
||||
that:
|
||||
- result_table_5 is not changed
|
||||
- result_table_5.msg == "table not found"
|
||||
148
tests/integration/targets/toml_file/tasks/02-nested.yml
Normal file
148
tests/integration/targets/toml_file/tasks/02-nested.yml
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
---
|
||||
# Copyright (c) Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
## Nested tests - dotted paths for tables and keys
|
||||
|
||||
- name: test-nested 1 - create nested table with dotted notation
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server.database
|
||||
key: port
|
||||
value: 5432
|
||||
register: result_nested_1
|
||||
|
||||
- name: test-nested 1 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_nested_1 is changed
|
||||
|
||||
- name: test-nested 1 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_nested_1
|
||||
|
||||
- name: test-nested 1 - verify nested table created
|
||||
vars:
|
||||
expected: |
|
||||
[server.database]
|
||||
port = 5432
|
||||
assert:
|
||||
that:
|
||||
- (content_nested_1.content | b64decode) == expected
|
||||
|
||||
- name: test-nested 2 - add another key to nested table
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server.database
|
||||
key: name
|
||||
value: mydb
|
||||
register: result_nested_2
|
||||
|
||||
- name: test-nested 2 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_nested_2 is changed
|
||||
|
||||
- name: test-nested 2 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_nested_2
|
||||
|
||||
- name: test-nested 2 - verify key added
|
||||
vars:
|
||||
expected: |
|
||||
[server.database]
|
||||
port = 5432
|
||||
name = "mydb"
|
||||
assert:
|
||||
that:
|
||||
- (content_nested_2.content | b64decode) == expected
|
||||
|
||||
- name: test-nested 3 - create deeply nested table
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: app.config.logging
|
||||
key: level
|
||||
value: debug
|
||||
register: result_nested_3
|
||||
|
||||
- name: test-nested 3 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_nested_3 is changed
|
||||
|
||||
- name: test-nested 3 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_nested_3
|
||||
|
||||
- name: test-nested 3 - verify deeply nested table
|
||||
vars:
|
||||
expected: |
|
||||
[server.database]
|
||||
port = 5432
|
||||
name = "mydb"
|
||||
|
||||
[app.config.logging]
|
||||
level = "debug"
|
||||
assert:
|
||||
that:
|
||||
- (content_nested_3.content | b64decode) == expected
|
||||
|
||||
- name: test-nested 4 - remove key from nested table
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server.database
|
||||
key: name
|
||||
state: absent
|
||||
register: result_nested_4
|
||||
|
||||
- name: test-nested 4 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_nested_4 is changed
|
||||
|
||||
- name: test-nested 4 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_nested_4
|
||||
|
||||
- name: test-nested 4 - verify key removed
|
||||
vars:
|
||||
expected: |
|
||||
[server.database]
|
||||
port = 5432
|
||||
|
||||
[app.config.logging]
|
||||
level = "debug"
|
||||
assert:
|
||||
that:
|
||||
- (content_nested_4.content | b64decode) == expected
|
||||
|
||||
- name: test-nested 5 - remove nested table
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server.database
|
||||
state: absent
|
||||
register: result_nested_5
|
||||
|
||||
- name: test-nested 5 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_nested_5 is changed
|
||||
|
||||
- name: test-nested 5 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_nested_5
|
||||
|
||||
- name: test-nested 5 - verify table removed
|
||||
vars:
|
||||
expected: |
|
||||
[app.config.logging]
|
||||
level = "debug"
|
||||
assert:
|
||||
that:
|
||||
- (content_nested_5.content | b64decode) == expected
|
||||
141
tests/integration/targets/toml_file/tasks/03-arrays.yml
Normal file
141
tests/integration/targets/toml_file/tasks/03-arrays.yml
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
---
|
||||
# Copyright (c) Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
## Array tests - array values
|
||||
|
||||
- name: test-array 1 - set an array value
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server
|
||||
key: ports
|
||||
value:
|
||||
- 80
|
||||
- 443
|
||||
- 8080
|
||||
register: result_array_1
|
||||
|
||||
- name: test-array 1 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_array_1 is changed
|
||||
|
||||
- name: test-array 1 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_array_1
|
||||
|
||||
- name: test-array 1 - verify array in TOML
|
||||
vars:
|
||||
expected: |
|
||||
[server]
|
||||
ports = [80, 443, 8080]
|
||||
assert:
|
||||
that:
|
||||
- (content_array_1.content | b64decode) == expected
|
||||
|
||||
- name: test-array 2 - set the same array again (idempotent)
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server
|
||||
key: ports
|
||||
value:
|
||||
- 80
|
||||
- 443
|
||||
- 8080
|
||||
register: result_array_2
|
||||
|
||||
- name: test-array 2 - verify no change
|
||||
assert:
|
||||
that:
|
||||
- result_array_2 is not changed
|
||||
|
||||
- name: test-array 3 - update array value
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server
|
||||
key: ports
|
||||
value:
|
||||
- 80
|
||||
- 443
|
||||
register: result_array_3
|
||||
|
||||
- name: test-array 3 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_array_3 is changed
|
||||
|
||||
- name: test-array 3 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_array_3
|
||||
|
||||
- name: test-array 3 - verify updated array
|
||||
vars:
|
||||
expected: |
|
||||
[server]
|
||||
ports = [80, 443]
|
||||
assert:
|
||||
that:
|
||||
- (content_array_3.content | b64decode) == expected
|
||||
|
||||
- name: test-array 4 - set array of strings
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server
|
||||
key: allowed_hosts
|
||||
value:
|
||||
- localhost
|
||||
- "127.0.0.1"
|
||||
- "::1"
|
||||
register: result_array_4
|
||||
|
||||
- name: test-array 4 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_array_4 is changed
|
||||
|
||||
- name: test-array 4 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_array_4
|
||||
|
||||
- name: test-array 4 - verify string array
|
||||
vars:
|
||||
expected: |
|
||||
[server]
|
||||
ports = [80, 443]
|
||||
allowed_hosts = ["localhost", "127.0.0.1", "::1"]
|
||||
assert:
|
||||
that:
|
||||
- (content_array_4.content | b64decode) == expected
|
||||
|
||||
- name: test-array 5 - set empty array
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server
|
||||
key: empty_list
|
||||
value: []
|
||||
register: result_array_5
|
||||
|
||||
- name: test-array 5 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_array_5 is changed
|
||||
|
||||
- name: test-array 5 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_array_5
|
||||
|
||||
- name: test-array 5 - verify empty array
|
||||
vars:
|
||||
expected: |
|
||||
[server]
|
||||
ports = [80, 443]
|
||||
allowed_hosts = ["localhost", "127.0.0.1", "::1"]
|
||||
empty_list = []
|
||||
assert:
|
||||
that:
|
||||
- (content_array_5.content | b64decode) == expected
|
||||
183
tests/integration/targets/toml_file/tasks/04-array-tables.yml
Normal file
183
tests/integration/targets/toml_file/tasks/04-array-tables.yml
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
---
|
||||
# Copyright (c) Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
## Array of tables tests - [[table]] handling
|
||||
|
||||
- name: test-aot 1 - create first array of tables entry
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: products[append]
|
||||
key: name
|
||||
value: Hammer
|
||||
register: result_aot_1
|
||||
|
||||
- name: test-aot 1 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_aot_1 is changed
|
||||
|
||||
- name: test-aot 1 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_aot_1
|
||||
|
||||
- name: test-aot 1 - verify array of tables created
|
||||
vars:
|
||||
expected: |
|
||||
[[products]]
|
||||
name = "Hammer"
|
||||
assert:
|
||||
that:
|
||||
- (content_aot_1.content | b64decode) == expected
|
||||
|
||||
- name: test-aot 2 - add key to existing entry
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: products[0]
|
||||
key: price
|
||||
value: 9.99
|
||||
register: result_aot_2
|
||||
|
||||
- name: test-aot 2 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_aot_2 is changed
|
||||
|
||||
- name: test-aot 2 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_aot_2
|
||||
|
||||
- name: test-aot 2 - verify price added
|
||||
vars:
|
||||
expected: |
|
||||
[[products]]
|
||||
name = "Hammer"
|
||||
price = 9.99
|
||||
assert:
|
||||
that:
|
||||
- (content_aot_2.content | b64decode) == expected
|
||||
|
||||
- name: test-aot 3 - append new entry to array of tables
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: products[append]
|
||||
key: name
|
||||
value: Nail
|
||||
register: result_aot_3
|
||||
|
||||
- name: test-aot 3 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_aot_3 is changed
|
||||
|
||||
- name: test-aot 3 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_aot_3
|
||||
|
||||
- name: test-aot 3 - verify second entry added
|
||||
vars:
|
||||
expected: |
|
||||
[[products]]
|
||||
name = "Hammer"
|
||||
price = 9.99
|
||||
|
||||
[[products]]
|
||||
name = "Nail"
|
||||
assert:
|
||||
that:
|
||||
- (content_aot_3.content | b64decode) == expected
|
||||
|
||||
- name: test-aot 4 - modify second entry
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: products[1]
|
||||
key: price
|
||||
value: 0.05
|
||||
register: result_aot_4
|
||||
|
||||
- name: test-aot 4 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_aot_4 is changed
|
||||
|
||||
- name: test-aot 4 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_aot_4
|
||||
|
||||
- name: test-aot 4 - verify second entry modified
|
||||
vars:
|
||||
expected: |
|
||||
[[products]]
|
||||
name = "Hammer"
|
||||
price = 9.99
|
||||
|
||||
[[products]]
|
||||
name = "Nail"
|
||||
price = 0.05
|
||||
assert:
|
||||
that:
|
||||
- (content_aot_4.content | b64decode) == expected
|
||||
|
||||
- name: test-aot 5 - remove key from first entry
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: products[0]
|
||||
key: price
|
||||
state: absent
|
||||
register: result_aot_5
|
||||
|
||||
- name: test-aot 5 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_aot_5 is changed
|
||||
|
||||
- name: test-aot 5 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_aot_5
|
||||
|
||||
- name: test-aot 5 - verify key removed from first entry
|
||||
vars:
|
||||
expected: |
|
||||
[[products]]
|
||||
name = "Hammer"
|
||||
|
||||
[[products]]
|
||||
name = "Nail"
|
||||
price = 0.05
|
||||
assert:
|
||||
that:
|
||||
- (content_aot_5.content | b64decode) == expected
|
||||
|
||||
- name: test-aot 6 - remove last entry from array of tables
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: products[-1]
|
||||
state: absent
|
||||
register: result_aot_6
|
||||
|
||||
- name: test-aot 6 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_aot_6 is changed
|
||||
|
||||
- name: test-aot 6 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_aot_6
|
||||
|
||||
- name: test-aot 6 - verify last entry removed
|
||||
vars:
|
||||
# Note: tomlkit preserves the blank line that preceded the removed entry
|
||||
expected: |+
|
||||
[[products]]
|
||||
name = "Hammer"
|
||||
|
||||
assert:
|
||||
that:
|
||||
- (content_aot_6.content | b64decode) == expected
|
||||
573
tests/integration/targets/toml_file/tasks/05-types.yml
Normal file
573
tests/integration/targets/toml_file/tasks/05-types.yml
Normal file
|
|
@ -0,0 +1,573 @@
|
|||
---
|
||||
# Copyright (c) Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
## Type tests - explicit type coercion
|
||||
|
||||
## Group 1: Basic types (integer, float, boolean, string)
|
||||
|
||||
- name: test-type 1 - set integer value
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: types
|
||||
key: count
|
||||
value: 42
|
||||
value_type: integer
|
||||
register: result_type_1
|
||||
|
||||
- name: test-type 1 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_type_1 is changed
|
||||
|
||||
- name: test-type 1 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_type_1
|
||||
|
||||
- name: test-type 1 - verify integer
|
||||
vars:
|
||||
expected: |
|
||||
[types]
|
||||
count = 42
|
||||
assert:
|
||||
that:
|
||||
- (content_type_1.content | b64decode) == expected
|
||||
|
||||
- name: test-type 2 - set float value
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: types
|
||||
key: ratio
|
||||
value: 3.14159
|
||||
value_type: float
|
||||
register: result_type_2
|
||||
|
||||
- name: test-type 2 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_type_2 is changed
|
||||
|
||||
- name: test-type 2 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_type_2
|
||||
|
||||
- name: test-type 2 - verify float
|
||||
vars:
|
||||
expected: |
|
||||
[types]
|
||||
count = 42
|
||||
ratio = 3.14159
|
||||
assert:
|
||||
that:
|
||||
- (content_type_2.content | b64decode) == expected
|
||||
|
||||
- name: test-type 3 - set boolean true
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: types
|
||||
key: enabled
|
||||
value: true
|
||||
value_type: boolean
|
||||
register: result_type_3
|
||||
|
||||
- name: test-type 3 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_type_3 is changed
|
||||
|
||||
- name: test-type 3 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_type_3
|
||||
|
||||
- name: test-type 3 - verify boolean
|
||||
vars:
|
||||
expected: |
|
||||
[types]
|
||||
count = 42
|
||||
ratio = 3.14159
|
||||
enabled = true
|
||||
assert:
|
||||
that:
|
||||
- (content_type_3.content | b64decode) == expected
|
||||
|
||||
- name: test-type 4 - set boolean false
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: types
|
||||
key: disabled
|
||||
value: false
|
||||
value_type: boolean
|
||||
register: result_type_4
|
||||
|
||||
- name: test-type 4 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_type_4 is changed
|
||||
|
||||
- name: test-type 4 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_type_4
|
||||
|
||||
- name: test-type 4 - verify boolean false
|
||||
vars:
|
||||
expected: |
|
||||
[types]
|
||||
count = 42
|
||||
ratio = 3.14159
|
||||
enabled = true
|
||||
disabled = false
|
||||
assert:
|
||||
that:
|
||||
- (content_type_4.content | b64decode) == expected
|
||||
|
||||
- name: test-type 5 - set string value (force string)
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: types
|
||||
key: number_as_string
|
||||
value: "12345"
|
||||
value_type: string
|
||||
register: result_type_5
|
||||
|
||||
- name: test-type 5 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_type_5 is changed
|
||||
|
||||
- name: test-type 5 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_type_5
|
||||
|
||||
- name: test-type 5 - verify string (should be quoted)
|
||||
vars:
|
||||
expected: |
|
||||
[types]
|
||||
count = 42
|
||||
ratio = 3.14159
|
||||
enabled = true
|
||||
disabled = false
|
||||
number_as_string = "12345"
|
||||
assert:
|
||||
that:
|
||||
- (content_type_5.content | b64decode) == expected
|
||||
|
||||
## Group 2: Reset and test datetime, inline_table, array
|
||||
|
||||
- name: Reset output file for group 2
|
||||
file:
|
||||
path: "{{ output_file }}"
|
||||
state: absent
|
||||
|
||||
- name: test-type 6 - set datetime value
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: types
|
||||
key: timestamp
|
||||
value: "2026-01-17T12:00:00"
|
||||
value_type: datetime
|
||||
register: result_type_6
|
||||
|
||||
- name: test-type 6 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_type_6 is changed
|
||||
|
||||
- name: test-type 6 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_type_6
|
||||
|
||||
- name: test-type 6 - verify datetime
|
||||
assert:
|
||||
that:
|
||||
- "'timestamp = 2026-01-17' in (content_type_6.content | b64decode)"
|
||||
|
||||
- name: test-type 7 - set inline table
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: types
|
||||
key: point
|
||||
value:
|
||||
x: 10
|
||||
y: 20
|
||||
value_type: inline_table
|
||||
register: result_type_7
|
||||
|
||||
- name: test-type 7 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_type_7 is changed
|
||||
|
||||
- name: test-type 7 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_type_7
|
||||
|
||||
- name: test-type 7 - verify inline table
|
||||
assert:
|
||||
that:
|
||||
- "'point = {' in (content_type_7.content | b64decode)"
|
||||
- "'x = 10' in (content_type_7.content | b64decode)"
|
||||
- "'y = 20' in (content_type_7.content | b64decode)"
|
||||
|
||||
- name: test-type 8 - force value to array
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: types
|
||||
key: single_as_array
|
||||
value: single_value
|
||||
value_type: array
|
||||
register: result_type_8
|
||||
|
||||
- name: test-type 8 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_type_8 is changed
|
||||
|
||||
- name: test-type 8 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_type_8
|
||||
|
||||
- name: test-type 8 - verify array
|
||||
assert:
|
||||
that:
|
||||
- "'single_as_array = [\"single_value\"]' in (content_type_8.content | b64decode)"
|
||||
|
||||
## Group 3: Reset and test special floats (inf, -inf, nan)
|
||||
|
||||
- name: Reset output file for group 3
|
||||
file:
|
||||
path: "{{ output_file }}"
|
||||
state: absent
|
||||
|
||||
- name: test-type 9 - set positive infinity
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: types
|
||||
key: max_limit
|
||||
value: inf
|
||||
value_type: float
|
||||
register: result_type_9
|
||||
|
||||
- name: test-type 9 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_type_9 is changed
|
||||
|
||||
- name: test-type 9 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_type_9
|
||||
|
||||
- name: test-type 9 - verify infinity in file
|
||||
vars:
|
||||
expected: |
|
||||
[types]
|
||||
max_limit = inf
|
||||
assert:
|
||||
that:
|
||||
- (content_type_9.content | b64decode) == expected
|
||||
|
||||
- name: test-type 10 - set negative infinity
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: types
|
||||
key: min_limit
|
||||
value: "-inf"
|
||||
value_type: float
|
||||
register: result_type_10
|
||||
|
||||
- name: test-type 10 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_type_10 is changed
|
||||
|
||||
- name: test-type 10 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_type_10
|
||||
|
||||
- name: test-type 10 - verify negative infinity in file
|
||||
vars:
|
||||
expected: |
|
||||
[types]
|
||||
max_limit = inf
|
||||
min_limit = -inf
|
||||
assert:
|
||||
that:
|
||||
- (content_type_10.content | b64decode) == expected
|
||||
|
||||
- name: test-type 11 - set nan
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: types
|
||||
key: undefined
|
||||
value: nan
|
||||
value_type: float
|
||||
register: result_type_11
|
||||
|
||||
- name: test-type 11 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_type_11 is changed
|
||||
|
||||
- name: test-type 11 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_type_11
|
||||
|
||||
- name: test-type 11 - verify nan in file
|
||||
vars:
|
||||
expected: |
|
||||
[types]
|
||||
max_limit = inf
|
||||
min_limit = -inf
|
||||
undefined = nan
|
||||
assert:
|
||||
that:
|
||||
- (content_type_11.content | b64decode) == expected
|
||||
|
||||
## Group 4: Reset and test date and time
|
||||
|
||||
- name: Reset output file for group 4
|
||||
file:
|
||||
path: "{{ output_file }}"
|
||||
state: absent
|
||||
|
||||
- name: test-type 12 - set local date
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: types
|
||||
key: birthday
|
||||
value: "1990-05-15"
|
||||
value_type: date
|
||||
register: result_type_12
|
||||
|
||||
- name: test-type 12 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_type_12 is changed
|
||||
|
||||
- name: test-type 12 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_type_12
|
||||
|
||||
- name: test-type 12 - verify date in file
|
||||
vars:
|
||||
expected: |
|
||||
[types]
|
||||
birthday = 1990-05-15
|
||||
assert:
|
||||
that:
|
||||
- (content_type_12.content | b64decode) == expected
|
||||
|
||||
- name: test-type 13 - set local time
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: types
|
||||
key: alarm
|
||||
value: "07:30:00"
|
||||
value_type: time
|
||||
register: result_type_13
|
||||
|
||||
- name: test-type 13 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_type_13 is changed
|
||||
|
||||
- name: test-type 13 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_type_13
|
||||
|
||||
- name: test-type 13 - verify time in file
|
||||
vars:
|
||||
expected: |
|
||||
[types]
|
||||
birthday = 1990-05-15
|
||||
alarm = 07:30:00
|
||||
assert:
|
||||
that:
|
||||
- (content_type_13.content | b64decode) == expected
|
||||
|
||||
## Group 5: Reset and test string variants
|
||||
|
||||
- name: Reset output file for group 5
|
||||
file:
|
||||
path: "{{ output_file }}"
|
||||
state: absent
|
||||
|
||||
- name: test-type 14 - set literal string
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: types
|
||||
key: regex_pattern
|
||||
value: '\d+\.\d+'
|
||||
value_type: literal_string
|
||||
register: result_type_14
|
||||
|
||||
- name: test-type 14 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_type_14 is changed
|
||||
|
||||
- name: test-type 14 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_type_14
|
||||
|
||||
- name: test-type 14 - verify literal string uses single quotes
|
||||
assert:
|
||||
that:
|
||||
- >-
|
||||
"regex_pattern = '" in (content_type_14.content | b64decode)
|
||||
|
||||
- name: test-type 15 - set multiline string
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: types
|
||||
key: description
|
||||
value: "Line one\nLine two"
|
||||
value_type: multiline_string
|
||||
register: result_type_15
|
||||
|
||||
- name: test-type 15 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_type_15 is changed
|
||||
|
||||
- name: test-type 15 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_type_15
|
||||
|
||||
- name: test-type 15 - verify multiline string uses triple quotes
|
||||
assert:
|
||||
that:
|
||||
- "'description = \"\"\"' in (content_type_15.content | b64decode)"
|
||||
|
||||
- name: test-type 16 - set multiline literal string
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: types
|
||||
key: raw_block
|
||||
value: "Raw\ntext\nblock"
|
||||
value_type: multiline_literal_string
|
||||
register: result_type_16
|
||||
|
||||
- name: test-type 16 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_type_16 is changed
|
||||
|
||||
- name: test-type 16 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_type_16
|
||||
|
||||
- name: test-type 16 - verify multiline literal string uses triple single quotes
|
||||
assert:
|
||||
that:
|
||||
- "\"raw_block = '''\" in (content_type_16.content | b64decode)"
|
||||
|
||||
## Group 6: Reset and test integer variants
|
||||
|
||||
- name: Reset output file for group 6
|
||||
file:
|
||||
path: "{{ output_file }}"
|
||||
state: absent
|
||||
|
||||
- name: test-type 17 - set hexadecimal integer
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: types
|
||||
key: hex_color
|
||||
value: 16777215
|
||||
value_type: hex_integer
|
||||
register: result_type_17
|
||||
|
||||
- name: test-type 17 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_type_17 is changed
|
||||
|
||||
- name: test-type 17 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_type_17
|
||||
|
||||
- name: test-type 17 - verify hex integer in file
|
||||
vars:
|
||||
expected: |
|
||||
[types]
|
||||
hex_color = 0xffffff
|
||||
assert:
|
||||
that:
|
||||
- (content_type_17.content | b64decode) == expected
|
||||
|
||||
- name: test-type 18 - set octal integer
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: types
|
||||
key: file_mode
|
||||
value: 493
|
||||
value_type: octal_integer
|
||||
register: result_type_18
|
||||
|
||||
- name: test-type 18 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_type_18 is changed
|
||||
|
||||
- name: test-type 18 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_type_18
|
||||
|
||||
- name: test-type 18 - verify octal integer in file
|
||||
vars:
|
||||
expected: |
|
||||
[types]
|
||||
hex_color = 0xffffff
|
||||
file_mode = 0o755
|
||||
assert:
|
||||
that:
|
||||
- (content_type_18.content | b64decode) == expected
|
||||
|
||||
- name: test-type 19 - set binary integer
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: types
|
||||
key: flags
|
||||
value: 170
|
||||
value_type: binary_integer
|
||||
register: result_type_19
|
||||
|
||||
- name: test-type 19 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_type_19 is changed
|
||||
|
||||
- name: test-type 19 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_type_19
|
||||
|
||||
- name: test-type 19 - verify binary integer in file
|
||||
vars:
|
||||
expected: |
|
||||
[types]
|
||||
hex_color = 0xffffff
|
||||
file_mode = 0o755
|
||||
flags = 0b10101010
|
||||
assert:
|
||||
that:
|
||||
- (content_type_19.content | b64decode) == expected
|
||||
127
tests/integration/targets/toml_file/tasks/06-comments.yml
Normal file
127
tests/integration/targets/toml_file/tasks/06-comments.yml
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
---
|
||||
# Copyright (c) Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
## Comment preservation tests
|
||||
|
||||
- name: test-comment 1 - create a TOML file with comments
|
||||
copy:
|
||||
dest: "{{ output_file }}"
|
||||
content: |
|
||||
# This is a file-level comment
|
||||
title = "My App"
|
||||
|
||||
# Server configuration
|
||||
[server]
|
||||
# The host to bind to
|
||||
host = "localhost"
|
||||
# The port to listen on
|
||||
port = 8080
|
||||
|
||||
- name: test-comment 2 - modify a value (should preserve comments)
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server
|
||||
key: port
|
||||
value: 9090
|
||||
register: result_comment_2
|
||||
|
||||
- name: test-comment 2 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_comment_2 is changed
|
||||
|
||||
- name: test-comment 2 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_comment_2
|
||||
|
||||
- name: test-comment 2 - verify comments preserved
|
||||
vars:
|
||||
expected: |
|
||||
# This is a file-level comment
|
||||
title = "My App"
|
||||
|
||||
# Server configuration
|
||||
[server]
|
||||
# The host to bind to
|
||||
host = "localhost"
|
||||
# The port to listen on
|
||||
port = 9090
|
||||
assert:
|
||||
that:
|
||||
- (content_comment_2.content | b64decode) == expected
|
||||
|
||||
- name: test-comment 3 - add a new key (should preserve existing comments)
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server
|
||||
key: timeout
|
||||
value: 30
|
||||
register: result_comment_3
|
||||
|
||||
- name: test-comment 3 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_comment_3 is changed
|
||||
|
||||
- name: test-comment 3 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_comment_3
|
||||
|
||||
- name: test-comment 3 - verify comments still preserved
|
||||
vars:
|
||||
expected: |
|
||||
# This is a file-level comment
|
||||
title = "My App"
|
||||
|
||||
# Server configuration
|
||||
[server]
|
||||
# The host to bind to
|
||||
host = "localhost"
|
||||
# The port to listen on
|
||||
port = 9090
|
||||
timeout = 30
|
||||
assert:
|
||||
that:
|
||||
- (content_comment_3.content | b64decode) == expected
|
||||
|
||||
- name: test-comment 4 - add a new table (should preserve existing comments)
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: database
|
||||
key: host
|
||||
value: db.example.com
|
||||
register: result_comment_4
|
||||
|
||||
- name: test-comment 4 - verify change
|
||||
assert:
|
||||
that:
|
||||
- result_comment_4 is changed
|
||||
|
||||
- name: test-comment 4 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_comment_4
|
||||
|
||||
- name: test-comment 4 - verify original structure preserved
|
||||
vars:
|
||||
expected: |
|
||||
# This is a file-level comment
|
||||
title = "My App"
|
||||
|
||||
# Server configuration
|
||||
[server]
|
||||
# The host to bind to
|
||||
host = "localhost"
|
||||
# The port to listen on
|
||||
port = 9090
|
||||
timeout = 30
|
||||
|
||||
[database]
|
||||
host = "db.example.com"
|
||||
assert:
|
||||
that:
|
||||
- (content_comment_4.content | b64decode) == expected
|
||||
108
tests/integration/targets/toml_file/tasks/07-backup.yml
Normal file
108
tests/integration/targets/toml_file/tasks/07-backup.yml
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
---
|
||||
# Copyright (c) Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
## Backup tests
|
||||
|
||||
- name: test-backup 1 - create initial file
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server
|
||||
key: host
|
||||
value: localhost
|
||||
register: result_backup_1
|
||||
|
||||
- name: test-backup 1 - verify no backup file returned
|
||||
assert:
|
||||
that:
|
||||
- result_backup_1 is changed
|
||||
- "'backup_file' not in result_backup_1"
|
||||
|
||||
- name: test-backup 1 - read content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_backup_1
|
||||
|
||||
- name: test-backup 1 - verify initial content
|
||||
vars:
|
||||
expected: |
|
||||
[server]
|
||||
host = "localhost"
|
||||
assert:
|
||||
that:
|
||||
- (content_backup_1.content | b64decode) == expected
|
||||
|
||||
- name: test-backup 2 - modify with backup enabled
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server
|
||||
key: host
|
||||
value: "192.168.1.1"
|
||||
backup: true
|
||||
register: result_backup_2
|
||||
|
||||
- name: test-backup 2 - verify backup file created
|
||||
assert:
|
||||
that:
|
||||
- result_backup_2 is changed
|
||||
- "'backup_file' in result_backup_2"
|
||||
- result_backup_2.backup_file is match(".*test.toml.*~$")
|
||||
|
||||
- name: test-backup 2 - verify backup file exists
|
||||
stat:
|
||||
path: "{{ result_backup_2.backup_file }}"
|
||||
register: stat_backup_2
|
||||
|
||||
- name: test-backup 2 - assert backup file exists
|
||||
assert:
|
||||
that:
|
||||
- stat_backup_2.stat.exists
|
||||
|
||||
- name: test-backup 2 - read backup file content
|
||||
slurp:
|
||||
src: "{{ result_backup_2.backup_file }}"
|
||||
register: content_backup_2
|
||||
|
||||
- name: test-backup 2 - verify backup has original content
|
||||
vars:
|
||||
expected: |
|
||||
[server]
|
||||
host = "localhost"
|
||||
assert:
|
||||
that:
|
||||
- (content_backup_2.content | b64decode) == expected
|
||||
|
||||
- name: test-backup 2 - read current file content
|
||||
slurp:
|
||||
src: "{{ output_file }}"
|
||||
register: content_backup_2_current
|
||||
|
||||
- name: test-backup 2 - verify current file has new content
|
||||
vars:
|
||||
expected: |
|
||||
[server]
|
||||
host = "192.168.1.1"
|
||||
assert:
|
||||
that:
|
||||
- (content_backup_2_current.content | b64decode) == expected
|
||||
|
||||
- name: test-backup 3 - no change should not create backup
|
||||
toml_file:
|
||||
path: "{{ output_file }}"
|
||||
table: server
|
||||
key: host
|
||||
value: "192.168.1.1"
|
||||
backup: true
|
||||
register: result_backup_3
|
||||
|
||||
- name: test-backup 3 - verify no backup when no change
|
||||
assert:
|
||||
that:
|
||||
- result_backup_3 is not changed
|
||||
- "'backup_file' not in result_backup_3 or result_backup_3.backup_file is none"
|
||||
|
||||
- name: test-backup 4 - cleanup backup file
|
||||
file:
|
||||
path: "{{ result_backup_2.backup_file }}"
|
||||
state: absent
|
||||
81
tests/integration/targets/toml_file/tasks/main.yml
Normal file
81
tests/integration/targets/toml_file/tasks/main.yml
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
####################################################################
|
||||
# WARNING: These are designed specifically for Ansible tests #
|
||||
# and should not be used as examples of how to write Ansible roles #
|
||||
####################################################################
|
||||
|
||||
# test code for toml_file module
|
||||
# Copyright (c) 2026, Jose Drowne
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
- name: Install tomlkit
|
||||
pip:
|
||||
name: tomlkit
|
||||
state: present
|
||||
|
||||
- name: Record the output directory
|
||||
set_fact:
|
||||
output_file: "{{ remote_tmp_dir }}/test.toml"
|
||||
non_existing_file: "{{ remote_tmp_dir }}/nonexistent.toml"
|
||||
|
||||
- name: Include tasks
|
||||
block:
|
||||
- name: Include tasks to perform basic tests
|
||||
include_tasks: 00-basic.yml
|
||||
|
||||
- name: Reset output file
|
||||
file:
|
||||
path: "{{ output_file }}"
|
||||
state: absent
|
||||
|
||||
- name: Include tasks to perform table tests
|
||||
include_tasks: 01-tables.yml
|
||||
|
||||
- name: Reset output file
|
||||
file:
|
||||
path: "{{ output_file }}"
|
||||
state: absent
|
||||
|
||||
- name: Include tasks to perform nested tests
|
||||
include_tasks: 02-nested.yml
|
||||
|
||||
- name: Reset output file
|
||||
file:
|
||||
path: "{{ output_file }}"
|
||||
state: absent
|
||||
|
||||
- name: Include tasks to perform array tests
|
||||
include_tasks: 03-arrays.yml
|
||||
|
||||
- name: Reset output file
|
||||
file:
|
||||
path: "{{ output_file }}"
|
||||
state: absent
|
||||
|
||||
- name: Include tasks to perform array of tables tests
|
||||
include_tasks: 04-array-tables.yml
|
||||
|
||||
- name: Reset output file
|
||||
file:
|
||||
path: "{{ output_file }}"
|
||||
state: absent
|
||||
|
||||
- name: Include tasks to perform type tests
|
||||
include_tasks: 05-types.yml
|
||||
|
||||
- name: Reset output file
|
||||
file:
|
||||
path: "{{ output_file }}"
|
||||
state: absent
|
||||
|
||||
- name: Include tasks to perform comment preservation tests
|
||||
include_tasks: 06-comments.yml
|
||||
|
||||
- name: Reset output file
|
||||
file:
|
||||
path: "{{ output_file }}"
|
||||
state: absent
|
||||
|
||||
- name: Include tasks to perform backup tests
|
||||
include_tasks: 07-backup.yml
|
||||
Loading…
Add table
Add a link
Reference in a new issue