mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-12-02 15:39:57 +00:00
Extend inventory create CLI with include/ignore filters and SSL/network defaults - https://chatgpt.com/share/692edecd-54c4-800f-b01b-35cf395a60f0
This commit is contained in:
@@ -9,27 +9,35 @@ This subcommand:
|
|||||||
|
|
||||||
1. Uses `build inventory full` to generate a dynamic inventory for the given
|
1. Uses `build inventory full` to generate a dynamic inventory for the given
|
||||||
host containing all invokable applications.
|
host containing all invokable applications.
|
||||||
2. Optionally filters the resulting groups by a user-provided list of
|
2. Optionally filters the resulting groups by:
|
||||||
application_ids (`--roles`).
|
- --include: only listed application_ids are kept
|
||||||
|
- --ignore: listed application_ids are removed
|
||||||
|
- --roles: legacy include filter (used only if --include/--ignore are not set)
|
||||||
3. Merges the generated inventory into an existing inventory file, without
|
3. Merges the generated inventory into an existing inventory file, without
|
||||||
deleting or overwriting unrelated entries.
|
deleting or overwriting unrelated entries.
|
||||||
4. Ensures `host_vars/<host>.yml` exists and stores base settings such as:
|
4. Ensures `host_vars/<host>.yml` exists and stores base settings such as:
|
||||||
- PRIMARY_DOMAIN
|
- PRIMARY_DOMAIN (optional)
|
||||||
- WEB_PROTOCOL
|
- SSL_ENABLED
|
||||||
|
- networks.internet.ip4
|
||||||
|
- networks.internet.ip6
|
||||||
Existing keys are preserved (only missing keys are added).
|
Existing keys are preserved (only missing keys are added).
|
||||||
5. For every application_id in the final inventory, uses:
|
5. For every application_id in the final inventory, uses:
|
||||||
- `meta/applications/role_name.py` to resolve the role path
|
- `meta/applications/role_name.py` to resolve the role path
|
||||||
- `create/credentials.py --snippet` to generate credentials YAML
|
- `create/credentials.py --snippet` to generate credentials YAML
|
||||||
snippets, and merges all snippets into host_vars in a single write.
|
snippets, and merges all snippets into host_vars in a single write.
|
||||||
|
6. If --vault-password-file is not provided, a file `.password` is created
|
||||||
|
in the inventory directory (if missing) and used as vault password file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Any, List, Set, Optional
|
from typing import Dict, Any, List, Set, Optional, NoReturn
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import os
|
import os
|
||||||
|
import secrets
|
||||||
|
import string
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import yaml
|
import yaml
|
||||||
@@ -67,6 +75,7 @@ def run_subprocess(
|
|||||||
raise SystemExit(msg)
|
raise SystemExit(msg)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def build_env_with_project_root(project_root: Path) -> Dict[str, str]:
|
def build_env_with_project_root(project_root: Path) -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Return an environment dict where PYTHONPATH includes the project root.
|
Return an environment dict where PYTHONPATH includes the project root.
|
||||||
@@ -83,6 +92,7 @@ def build_env_with_project_root(project_root: Path) -> Dict[str, str]:
|
|||||||
env["PYTHONPATH"] = root_str
|
env["PYTHONPATH"] = root_str
|
||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
def detect_project_root() -> Path:
|
def detect_project_root() -> Path:
|
||||||
"""
|
"""
|
||||||
Detect project root assuming this file is at: <root>/cli/create/inventory.py
|
Detect project root assuming this file is at: <root>/cli/create/inventory.py
|
||||||
@@ -109,9 +119,10 @@ def dump_yaml(path: Path, data: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
def parse_roles_list(raw_roles: Optional[List[str]]) -> Optional[Set[str]]:
|
def parse_roles_list(raw_roles: Optional[List[str]]) -> Optional[Set[str]]:
|
||||||
"""
|
"""
|
||||||
Parse a list of roles supplied on the CLI. Supports:
|
Parse a list of IDs supplied on the CLI. Supports:
|
||||||
--roles web-app-nextcloud web-app-mastodon
|
--include web-app-nextcloud web-app-mastodon
|
||||||
--roles web-app-nextcloud,web-app-mastodon
|
--include web-app-nextcloud,web-app-mastodon
|
||||||
|
Same logic is reused for --ignore and --roles.
|
||||||
"""
|
"""
|
||||||
if not raw_roles:
|
if not raw_roles:
|
||||||
return None
|
return None
|
||||||
@@ -128,6 +139,14 @@ def parse_roles_list(raw_roles: Optional[List[str]]) -> Optional[Set[str]]:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def generate_random_password(length: int = 64) -> str:
|
||||||
|
"""
|
||||||
|
Generate a random password using ASCII letters and digits.
|
||||||
|
"""
|
||||||
|
alphabet = string.ascii_letters + string.digits
|
||||||
|
return "".join(secrets.choice(alphabet) for _ in range(length))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Inventory generation (servers.yml via build/inventory/full.py)
|
# Inventory generation (servers.yml via build/inventory/full.py)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -160,17 +179,20 @@ def generate_dynamic_inventory(
|
|||||||
tmp_inventory.unlink(missing_ok=True)
|
tmp_inventory.unlink(missing_ok=True)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def filter_inventory_by_roles(inv_data: Dict[str, Any], roles_filter: Set[str]) -> Dict[str, Any]:
|
|
||||||
|
def _filter_inventory_children(
|
||||||
|
inv_data: Dict[str, Any],
|
||||||
|
predicate,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Return a new inventory dict that contains only the groups whose names
|
Generic helper: keep only children for which predicate(group_name, group_data) is True.
|
||||||
are in `roles_filter`. All other structure is preserved.
|
|
||||||
"""
|
"""
|
||||||
all_block = inv_data.get("all", {})
|
all_block = inv_data.get("all", {})
|
||||||
children = all_block.get("children", {}) or {}
|
children = all_block.get("children", {}) or {}
|
||||||
|
|
||||||
filtered_children: Dict[str, Any] = {}
|
filtered_children: Dict[str, Any] = {}
|
||||||
for group_name, group_data in children.items():
|
for group_name, group_data in children.items():
|
||||||
if group_name in roles_filter:
|
if predicate(group_name, group_data):
|
||||||
filtered_children[group_name] = group_data
|
filtered_children[group_name] = group_data
|
||||||
|
|
||||||
new_all = dict(all_block)
|
new_all = dict(all_block)
|
||||||
@@ -178,6 +200,36 @@ def filter_inventory_by_roles(inv_data: Dict[str, Any], roles_filter: Set[str])
|
|||||||
return {"all": new_all}
|
return {"all": new_all}
|
||||||
|
|
||||||
|
|
||||||
|
def filter_inventory_by_roles(inv_data: Dict[str, Any], roles_filter: Set[str]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Legacy: keep only groups whose names are in roles_filter.
|
||||||
|
"""
|
||||||
|
return _filter_inventory_children(
|
||||||
|
inv_data,
|
||||||
|
lambda group_name, _group_data: group_name in roles_filter,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_inventory_by_include(inv_data: Dict[str, Any], include_set: Set[str]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Keep only groups whose names are in include_set.
|
||||||
|
"""
|
||||||
|
return _filter_inventory_children(
|
||||||
|
inv_data,
|
||||||
|
lambda group_name, _group_data: group_name in include_set,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_inventory_by_ignore(inv_data: Dict[str, Any], ignore_set: Set[str]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Keep all groups except those whose names are in ignore_set.
|
||||||
|
"""
|
||||||
|
return _filter_inventory_children(
|
||||||
|
inv_data,
|
||||||
|
lambda group_name, _group_data: group_name not in ignore_set,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def merge_inventories(
|
def merge_inventories(
|
||||||
base: Dict[str, Any],
|
base: Dict[str, Any],
|
||||||
new: Dict[str, Any],
|
new: Dict[str, Any],
|
||||||
@@ -216,6 +268,7 @@ def merge_inventories(
|
|||||||
|
|
||||||
return base
|
return base
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# host_vars helpers
|
# host_vars helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -223,15 +276,19 @@ def merge_inventories(
|
|||||||
def ensure_host_vars_file(
|
def ensure_host_vars_file(
|
||||||
host_vars_file: Path,
|
host_vars_file: Path,
|
||||||
host: str,
|
host: str,
|
||||||
primary_domain: str,
|
primary_domain: Optional[str],
|
||||||
web_protocol: str,
|
ssl_disabled: bool,
|
||||||
|
ip4: str,
|
||||||
|
ip6: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Ensure host_vars/<host>.yml exists and contains base settings.
|
Ensure host_vars/<host>.yml exists and contains base settings.
|
||||||
|
|
||||||
Important: Existing keys are NOT overwritten. Only missing keys are added:
|
Important: Existing keys are NOT overwritten. Only missing keys are added:
|
||||||
- PRIMARY_DOMAIN
|
- PRIMARY_DOMAIN (only if primary_domain is provided)
|
||||||
- WEB_PROTOCOL
|
- SSL_ENABLED (true by default, false if --ssl-disabled is used)
|
||||||
|
- networks.internet.ip4
|
||||||
|
- networks.internet.ip6
|
||||||
|
|
||||||
Uses ruamel.yaml so that custom tags like !vault are preserved and do not
|
Uses ruamel.yaml so that custom tags like !vault are preserved and do not
|
||||||
break parsing (unlike PyYAML safe_load).
|
break parsing (unlike PyYAML safe_load).
|
||||||
@@ -254,15 +311,34 @@ def ensure_host_vars_file(
|
|||||||
data = tmp
|
data = tmp
|
||||||
|
|
||||||
# Only set defaults; do NOT override existing values
|
# Only set defaults; do NOT override existing values
|
||||||
if "PRIMARY_DOMAIN" not in data:
|
if primary_domain is not None and "PRIMARY_DOMAIN" not in data:
|
||||||
data["PRIMARY_DOMAIN"] = primary_domain
|
data["PRIMARY_DOMAIN"] = primary_domain
|
||||||
if "WEB_PROTOCOL" not in data:
|
|
||||||
data["WEB_PROTOCOL"] = web_protocol
|
if "SSL_ENABLED" not in data:
|
||||||
|
# By default SSL is enabled; --ssl-disabled flips this to false
|
||||||
|
data["SSL_ENABLED"] = not ssl_disabled
|
||||||
|
|
||||||
|
# networks.internet.ip4 / ip6
|
||||||
|
networks = data.get("networks")
|
||||||
|
if not isinstance(networks, CommentedMap):
|
||||||
|
networks = CommentedMap()
|
||||||
|
data["networks"] = networks
|
||||||
|
|
||||||
|
internet = networks.get("internet")
|
||||||
|
if not isinstance(internet, CommentedMap):
|
||||||
|
internet = CommentedMap()
|
||||||
|
networks["internet"] = internet
|
||||||
|
|
||||||
|
if "ip4" not in internet:
|
||||||
|
internet["ip4"] = ip4
|
||||||
|
if "ip6" not in internet:
|
||||||
|
internet["ip6"] = ip6
|
||||||
|
|
||||||
host_vars_file.parent.mkdir(parents=True, exist_ok=True)
|
host_vars_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with host_vars_file.open("w", encoding="utf-8") as f:
|
with host_vars_file.open("w", encoding="utf-8") as f:
|
||||||
yaml_rt.dump(data, f)
|
yaml_rt.dump(data, f)
|
||||||
|
|
||||||
|
|
||||||
def ensure_ruamel_map(node: CommentedMap, key: str) -> CommentedMap:
|
def ensure_ruamel_map(node: CommentedMap, key: str) -> CommentedMap:
|
||||||
"""
|
"""
|
||||||
Ensure node[key] exists and is a mapping (CommentedMap).
|
Ensure node[key] exists and is a mapping (CommentedMap).
|
||||||
@@ -339,11 +415,13 @@ def resolve_role_path(
|
|||||||
|
|
||||||
return role_path
|
return role_path
|
||||||
|
|
||||||
def fatal(msg: str) -> "NoReturn":
|
|
||||||
|
def fatal(msg: str) -> NoReturn:
|
||||||
"""Print a fatal error and exit with code 1."""
|
"""Print a fatal error and exit with code 1."""
|
||||||
sys.stderr.write(f"[FATAL] {msg}\n")
|
sys.stderr.write(f"[FATAL] {msg}\n")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Credentials generation via create/credentials.py --snippet
|
# Credentials generation via create/credentials.py --snippet
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -559,26 +637,36 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|||||||
"credentials for all selected applications."
|
"credentials for all selected applications."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"inventory_dir",
|
||||||
|
help="Inventory directory (e.g. inventories/galaxyserver).",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--host",
|
"--host",
|
||||||
required=True,
|
required=False,
|
||||||
help="Hostname to use in the inventory (e.g. galaxyserver, localhost).",
|
default="localhost",
|
||||||
|
help="Hostname to use in the inventory (default: localhost).",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--primary-domain",
|
"--primary-domain",
|
||||||
required=True,
|
required=False,
|
||||||
help="Primary domain for this host (e.g. infinito.nexus).",
|
default=None,
|
||||||
|
help="Primary domain for this host (e.g. infinito.nexus). Optional.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--web-protocol",
|
"--ssl-disabled",
|
||||||
default="https",
|
action="store_true",
|
||||||
choices=("http", "https"),
|
help="Disable SSL for this host (sets SSL_ENABLED: false in host_vars).",
|
||||||
help="Web protocol to use for this host (default: https).",
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--inventory-dir",
|
"--ip4",
|
||||||
required=True,
|
default="127.0.0.1",
|
||||||
help="Path to the inventory directory (e.g. inventories/galaxyserver).",
|
help="IPv4 address for networks.internet.ip4 (default: 127.0.0.1).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ip6",
|
||||||
|
default="::1",
|
||||||
|
help='IPv6 address for networks.internet.ip6 (default: "::1").',
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--inventory-file",
|
"--inventory-file",
|
||||||
@@ -588,15 +676,35 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|||||||
"--roles",
|
"--roles",
|
||||||
nargs="+",
|
nargs="+",
|
||||||
help=(
|
help=(
|
||||||
"Optional list of application_ids to include. "
|
"Optional legacy list of application_ids to include. "
|
||||||
"If omitted, all invokable applications are used. "
|
"Used only if neither --include nor --ignore is specified. "
|
||||||
"Supports comma-separated values as well."
|
"Supports comma-separated values as well."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--include",
|
||||||
|
nargs="+",
|
||||||
|
help=(
|
||||||
|
"Only include the listed application_ids in the inventory. "
|
||||||
|
"Mutually exclusive with --ignore."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ignore",
|
||||||
|
nargs="+",
|
||||||
|
help=(
|
||||||
|
"Exclude the listed application_ids from the inventory. "
|
||||||
|
"Mutually exclusive with --include."
|
||||||
|
),
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--vault-password-file",
|
"--vault-password-file",
|
||||||
required=True,
|
required=False,
|
||||||
help="Path to the Vault password file for credentials generation.",
|
default=None,
|
||||||
|
help=(
|
||||||
|
"Path to the Vault password file for credentials generation. "
|
||||||
|
"If omitted, <inventory-dir>/.password is created or reused."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--roles-dir",
|
"--roles-dir",
|
||||||
@@ -615,6 +723,15 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|||||||
|
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
# Parse include/ignore/roles lists
|
||||||
|
include_filter = parse_roles_list(args.include)
|
||||||
|
ignore_filter = parse_roles_list(args.ignore)
|
||||||
|
roles_filter = parse_roles_list(args.roles)
|
||||||
|
|
||||||
|
# Enforce mutual exclusivity: only one of --include / --ignore may be used
|
||||||
|
if include_filter and ignore_filter:
|
||||||
|
fatal("Options --include and --ignore are mutually exclusive. Please use only one of them.")
|
||||||
|
|
||||||
project_root = detect_project_root()
|
project_root = detect_project_root()
|
||||||
roles_dir = Path(args.roles_dir) if args.roles_dir else (project_root / "roles")
|
roles_dir = Path(args.roles_dir) if args.roles_dir else (project_root / "roles")
|
||||||
categories_file = Path(args.categories_file) if args.categories_file else (roles_dir / "categories.yml")
|
categories_file = Path(args.categories_file) if args.categories_file else (roles_dir / "categories.yml")
|
||||||
@@ -628,9 +745,24 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|||||||
host_vars_dir = inventory_dir / "host_vars"
|
host_vars_dir = inventory_dir / "host_vars"
|
||||||
host_vars_file = host_vars_dir / f"{args.host}.yml"
|
host_vars_file = host_vars_dir / f"{args.host}.yml"
|
||||||
|
|
||||||
vault_password_file = Path(args.vault_password_file).resolve()
|
# Vault password file: use provided one, otherwise create/reuse .password in inventory_dir
|
||||||
|
if args.vault_password_file:
|
||||||
|
vault_password_file = Path(args.vault_password_file).resolve()
|
||||||
|
else:
|
||||||
|
vault_password_file = inventory_dir / ".password"
|
||||||
|
if not vault_password_file.exists():
|
||||||
|
print(f"[INFO] No --vault-password-file provided. Creating {vault_password_file} ...")
|
||||||
|
password = generate_random_password()
|
||||||
|
with vault_password_file.open("w", encoding="utf-8") as f:
|
||||||
|
f.write(password + "\n")
|
||||||
|
try:
|
||||||
|
vault_password_file.chmod(0o600)
|
||||||
|
except PermissionError:
|
||||||
|
# Best-effort; ignore if chmod is not allowed
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
print(f"[INFO] Using existing vault password file: {vault_password_file}")
|
||||||
|
|
||||||
roles_filter = parse_roles_list(args.roles)
|
|
||||||
tmp_inventory = inventory_dir / "_inventory_full_tmp.yml"
|
tmp_inventory = inventory_dir / "_inventory_full_tmp.yml"
|
||||||
|
|
||||||
# 1) Generate dynamic inventory via build/inventory/full.py
|
# 1) Generate dynamic inventory via build/inventory/full.py
|
||||||
@@ -643,9 +775,15 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|||||||
project_root=project_root,
|
project_root=project_root,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2) Optional: filter by roles
|
# 2) Apply filters: include → ignore → legacy roles
|
||||||
if roles_filter:
|
if include_filter:
|
||||||
print(f"[INFO] Filtering inventory to roles: {', '.join(sorted(roles_filter))}")
|
print(f"[INFO] Including only application_ids: {', '.join(sorted(include_filter))}")
|
||||||
|
dyn_inv = filter_inventory_by_include(dyn_inv, include_filter)
|
||||||
|
elif ignore_filter:
|
||||||
|
print(f"[INFO] Ignoring application_ids: {', '.join(sorted(ignore_filter))}")
|
||||||
|
dyn_inv = filter_inventory_by_ignore(dyn_inv, ignore_filter)
|
||||||
|
elif roles_filter:
|
||||||
|
print(f"[INFO] Filtering inventory to roles (legacy): {', '.join(sorted(roles_filter))}")
|
||||||
dyn_inv = filter_inventory_by_roles(dyn_inv, roles_filter)
|
dyn_inv = filter_inventory_by_roles(dyn_inv, roles_filter)
|
||||||
|
|
||||||
# Collect final application_ids from dynamic inventory for credential generation
|
# Collect final application_ids from dynamic inventory for credential generation
|
||||||
@@ -673,7 +811,9 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|||||||
host_vars_file=host_vars_file,
|
host_vars_file=host_vars_file,
|
||||||
host=args.host,
|
host=args.host,
|
||||||
primary_domain=args.primary_domain,
|
primary_domain=args.primary_domain,
|
||||||
web_protocol=args.web_protocol,
|
ssl_disabled=args.ssl_disabled,
|
||||||
|
ip4=args.ip4,
|
||||||
|
ip6=args.ip6,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 5) Generate credentials for all application_ids (snippets + single merge)
|
# 5) Generate credentials for all application_ids (snippets + single merge)
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class TestCreateInventory(unittest.TestCase):
|
|||||||
- load existing YAML containing a !vault tag without crashing,
|
- load existing YAML containing a !vault tag without crashing,
|
||||||
- preserve the !vault node including its tag,
|
- preserve the !vault node including its tag,
|
||||||
- keep existing keys untouched,
|
- keep existing keys untouched,
|
||||||
- add PRIMARY_DOMAIN, and WEB_PROTOCOL only when missing,
|
- add PRIMARY_DOMAIN, SSL_ENABLED and networks.internet.ip4/ip6 only when missing,
|
||||||
- not overwrite them on subsequent calls.
|
- not overwrite them on subsequent calls.
|
||||||
"""
|
"""
|
||||||
yaml_rt = YAML(typ="rt")
|
yaml_rt = YAML(typ="rt")
|
||||||
@@ -123,12 +123,14 @@ existing_key: foo
|
|||||||
host_vars_dir.mkdir(parents=True, exist_ok=True)
|
host_vars_dir.mkdir(parents=True, exist_ok=True)
|
||||||
host_vars_file.write_text(initial_yaml, encoding="utf-8")
|
host_vars_file.write_text(initial_yaml, encoding="utf-8")
|
||||||
|
|
||||||
# Run ensure_host_vars_file
|
# Run ensure_host_vars_file (first time)
|
||||||
ensure_host_vars_file(
|
ensure_host_vars_file(
|
||||||
host_vars_file=host_vars_file,
|
host_vars_file=host_vars_file,
|
||||||
host=host,
|
host=host,
|
||||||
primary_domain="example.org",
|
primary_domain="example.org",
|
||||||
web_protocol="https",
|
ssl_disabled=False,
|
||||||
|
ip4="127.0.0.1",
|
||||||
|
ip6="::1",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Reload with ruamel.yaml to verify structure and tags
|
# Reload with ruamel.yaml to verify structure and tags
|
||||||
@@ -148,14 +150,26 @@ existing_key: foo
|
|||||||
|
|
||||||
# Default values must be added
|
# Default values must be added
|
||||||
self.assertEqual(data["PRIMARY_DOMAIN"], "example.org")
|
self.assertEqual(data["PRIMARY_DOMAIN"], "example.org")
|
||||||
self.assertEqual(data["WEB_PROTOCOL"], "https")
|
self.assertIn("SSL_ENABLED", data)
|
||||||
|
self.assertTrue(data["SSL_ENABLED"])
|
||||||
|
|
||||||
|
self.assertIn("networks", data)
|
||||||
|
self.assertIsInstance(data["networks"], CommentedMap)
|
||||||
|
self.assertIn("internet", data["networks"])
|
||||||
|
self.assertIsInstance(data["networks"]["internet"], CommentedMap)
|
||||||
|
|
||||||
|
internet = data["networks"]["internet"]
|
||||||
|
self.assertEqual(internet["ip4"], "127.0.0.1")
|
||||||
|
self.assertEqual(internet["ip6"], "::1")
|
||||||
|
|
||||||
# A second call must NOT overwrite existing defaults
|
# A second call must NOT overwrite existing defaults
|
||||||
ensure_host_vars_file(
|
ensure_host_vars_file(
|
||||||
host_vars_file=host_vars_file,
|
host_vars_file=host_vars_file,
|
||||||
host="other-host",
|
host="other-host",
|
||||||
primary_domain="other.example",
|
primary_domain="other.example",
|
||||||
web_protocol="http",
|
ssl_disabled=True, # would switch to false if it overwrote
|
||||||
|
ip4="10.0.0.1",
|
||||||
|
ip6="::2",
|
||||||
)
|
)
|
||||||
|
|
||||||
with host_vars_file.open("r", encoding="utf-8") as f:
|
with host_vars_file.open("r", encoding="utf-8") as f:
|
||||||
@@ -163,7 +177,11 @@ existing_key: foo
|
|||||||
|
|
||||||
# Values remain unchanged
|
# Values remain unchanged
|
||||||
self.assertEqual(data2["PRIMARY_DOMAIN"], "example.org")
|
self.assertEqual(data2["PRIMARY_DOMAIN"], "example.org")
|
||||||
self.assertEqual(data2["WEB_PROTOCOL"], "https")
|
self.assertTrue(data2["SSL_ENABLED"])
|
||||||
|
|
||||||
|
internet2 = data2["networks"]["internet"]
|
||||||
|
self.assertEqual(internet2["ip4"], "127.0.0.1")
|
||||||
|
self.assertEqual(internet2["ip6"], "::1")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user