Restructured CLI logic

This commit is contained in:
2025-07-10 21:26:44 +02:00
parent 8457325b5c
commit c160c58a5c
44 changed files with 97 additions and 155 deletions

View File

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env python3
import os
import sys
import unittest
import tempfile
import shutil
import yaml
# Adjust path to include cli/ folder
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../..", "cli")))
from cli.generate.conditional_role_include import build_dependency_graph, topological_sort, gen_condi_role_incl
class TestGeneratePlaybook(unittest.TestCase):
def setUp(self):
# Create a temporary directory to simulate roles
self.temp_dir = tempfile.mkdtemp()
# Define mock roles and dependencies
self.roles = {
'role-a': {'run_after': [], 'application_id': 'a'},
'role-b': {'run_after': ['role-a'], 'application_id': 'b'},
'role-c': {'run_after': ['role-b'], 'application_id': 'c'},
'role-d': {'run_after': [], 'application_id': 'd'},
}
for role_name, meta in self.roles.items():
role_path = os.path.join(self.temp_dir, role_name)
os.makedirs(os.path.join(role_path, 'meta'), exist_ok=True)
os.makedirs(os.path.join(role_path, 'vars'), exist_ok=True)
meta_file = {
'galaxy_info': {
'run_after': meta['run_after']
}
}
vars_file = {
'application_id': meta['application_id']
}
with open(os.path.join(role_path, 'meta', 'main.yml'), 'w') as f:
yaml.dump(meta_file, f)
with open(os.path.join(role_path, 'vars', 'main.yml'), 'w') as f:
yaml.dump(vars_file, f)
def tearDown(self):
# Clean up the temporary directory
shutil.rmtree(self.temp_dir)
def test_dependency_graph_and_sort(self):
graph, in_degree, roles = build_dependency_graph(self.temp_dir)
self.assertIn('role-a', graph)
self.assertIn('role-b', graph)
self.assertEqual(graph['role-a'], ['role-b'])
self.assertEqual(graph['role-b'], ['role-c'])
self.assertEqual(graph['role-c'], [])
self.assertEqual(in_degree['role-c'], 1)
self.assertEqual(in_degree['role-b'], 1)
self.assertEqual(in_degree['role-a'], 0)
self.assertEqual(in_degree['role-d'], 0)
sorted_roles = topological_sort(graph, in_degree)
# The expected order must be a → b → c, d can be anywhere before or after
self.assertTrue(sorted_roles.index('role-a') < sorted_roles.index('role-b') < sorted_roles.index('role-c'))
def test_gen_condi_role_incl(self):
entries = gen_condi_role_incl(self.temp_dir)
text = ''.join(entries)
self.assertIn("setup a", text)
self.assertIn("setup b", text)
self.assertIn("setup c", text)
self.assertIn("setup d", text)
# Order must preserve run_after
a_index = text.index("setup a")
b_index = text.index("setup b")
c_index = text.index("setup c")
self.assertTrue(a_index < b_index < c_index)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,56 @@
import os
import unittest
import tempfile
import shutil
import yaml
from pathlib import Path
import subprocess
class TestGenerateDefaultApplications(unittest.TestCase):
def setUp(self):
# Create temp role structure
self.temp_dir = Path(tempfile.mkdtemp())
self.roles_dir = self.temp_dir / "roles"
self.roles_dir.mkdir()
# Sample role
self.sample_role = self.roles_dir / "web-app-testapp"
(self.sample_role / "vars").mkdir(parents=True)
(self.sample_role / "config").mkdir(parents=True)
# Write application_id and configuration
(self.sample_role / "vars" / "main.yml").write_text("application_id: testapp\n")
(self.sample_role / "config" / "main.yml").write_text("foo: bar\nbaz: 123\n")
# Output file path
self.output_file = self.temp_dir / "group_vars" / "all" / "04_applications.yml"
def tearDown(self):
shutil.rmtree(self.temp_dir)
def test_script_generates_expected_yaml(self):
script_path = Path(__file__).resolve().parent.parent.parent.parent.parent.parent / "cli/generate/defaults/applications.py"
result = subprocess.run(
[
"python3", str(script_path),
"--roles-dir", str(self.roles_dir),
"--output-file", str(self.output_file)
],
capture_output=True,
text=True,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
self.assertTrue(self.output_file.exists(), "Output file was not created.")
data = yaml.safe_load(self.output_file.read_text())
self.assertIn("defaults_applications", data)
self.assertIn("testapp", data["defaults_applications"])
self.assertEqual(data["defaults_applications"]["testapp"]["foo"], "bar")
self.assertEqual(data["defaults_applications"]["testapp"]["baz"], 123)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,67 @@
import os
import unittest
import tempfile
import shutil
import yaml
from pathlib import Path
import subprocess
class TestGenerateDefaultApplicationsUsers(unittest.TestCase):
def setUp(self):
# Setup temporary roles directory
self.temp_dir = Path(tempfile.mkdtemp())
self.roles_dir = self.temp_dir / "roles"
self.roles_dir.mkdir()
# Sample role with users meta
self.role = self.roles_dir / "web-app-app-with-users"
(self.role / "vars").mkdir(parents=True)
(self.role / "config").mkdir(parents=True)
(self.role / "meta").mkdir(parents=True)
(self.role / "users").mkdir(parents=True)
# Write application_id and configuration
(self.role / "vars" / "main.yml").write_text("application_id: app_with_users\n")
(self.role / "config" / "main.yml").write_text("setting: value\n")
# Write users meta
users_meta = {
'users': {
'alice': {'uid': 2001, 'gid': 2001},
'bob': {'uid': 2002, 'gid': 2002}
}
}
with (self.role / "users" / "main.yml").open('w', encoding='utf-8') as f:
yaml.dump(users_meta, f)
# Output file path
self.output_file = self.temp_dir / "output.yml"
def tearDown(self):
shutil.rmtree(self.temp_dir)
def test_users_injection(self):
"""
When a users.yml exists with defined users, the script should inject a 'users'
mapping in the generated YAML, mapping each username to a Jinja2 reference.
"""
script_path = Path(__file__).resolve().parents[5] / "cli" / "generate/defaults/applications.py"
result = subprocess.run([
"python3", str(script_path),
"--roles-dir", str(self.roles_dir),
"--output-file", str(self.output_file)
], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, msg=result.stderr)
data = yaml.safe_load(self.output_file.read_text())
apps = data.get('defaults_applications', {})
# Only the app with users should be present
self.assertIn('app_with_users', apps)
# 'users' section should be present and correct
users_map = apps['app_with_users']['users']
expected = {'alice': '{{ users["alice"] }}', 'bob': '{{ users["bob"] }}'}
self.assertEqual(users_map, expected)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,136 @@
import os
import sys
import unittest
import tempfile
import shutil
import yaml
from collections import OrderedDict
# Add cli/ to import path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../..", "cli/generate/defaults/")))
import users
class TestGenerateUsers(unittest.TestCase):
def test_build_users_auto_increment_and_overrides(self):
defs = {
'alice': {},
'bob': {'uid': 2000, 'email': 'bob@custom.com', 'description': 'Custom user'},
'carol': {}
}
build = users.build_users(
defs=defs,
primary_domain='example.com',
start_id=1001,
become_pwd='pw'
)
# alice should get uid/gid 1001
self.assertEqual(build['alice']['uid'], 1001)
self.assertEqual(build['alice']['gid'], 1001)
self.assertEqual(build['alice']['email'], 'alice@example.com')
# bob overrides
self.assertEqual(build['bob']['uid'], 2000)
self.assertEqual(build['bob']['gid'], 2000)
self.assertEqual(build['bob']['email'], 'bob@custom.com')
self.assertIn('description', build['bob'])
# carol should get next free id = 1002
self.assertEqual(build['carol']['uid'], 1002)
self.assertEqual(build['carol']['gid'], 1002)
def test_build_users_default_lookup_password(self):
"""
When no 'password' override is provided,
the become_pwd lookup template string must be used as the password.
"""
defs = {'frank': {}}
lookup_template = '{{ lookup("password", "/dev/null length=42 chars=ascii_letters,digits") }}'
build = users.build_users(
defs=defs,
primary_domain='example.com',
start_id=1001,
become_pwd=lookup_template
)
self.assertEqual(
build['frank']['password'],
lookup_template,
"The lookup template string was not correctly applied as the default password"
)
def test_build_users_override_password(self):
"""
When a 'password' override is provided,
that custom password must be used instead of become_pwd.
"""
defs = {'eva': {'password': 'custompw'}}
lookup_template = '{{ lookup("password", "/dev/null length=42 chars=ascii_letters,digits") }}'
build = users.build_users(
defs=defs,
primary_domain='example.com',
start_id=1001,
become_pwd=lookup_template
)
self.assertEqual(
build['eva']['password'],
'custompw',
"The override password was not correctly applied"
)
def test_build_users_duplicate_override_uid(self):
defs = {
'u1': {'uid': 1001},
'u2': {'uid': 1001}
}
with self.assertRaises(ValueError):
users.build_users(defs, 'ex.com', 1001, 'pw')
def test_build_users_shared_gid_allowed(self):
# Allow two users to share the same GID when one overrides gid and the other uses that as uid
defs = {
'a': {'uid': 1500},
'b': {'gid': 1500}
}
build = users.build_users(defs, 'ex.com', 1500, 'pw')
# Both should have gid 1500
self.assertEqual(build['a']['gid'], 1500)
self.assertEqual(build['b']['gid'], 1500)
def test_build_users_duplicate_username_email(self):
defs = {
'u1': {'username': 'same', 'email': 'same@ex.com'},
'u2': {'username': 'same'}
}
# second user with same username should raise
with self.assertRaises(ValueError):
users.build_users(defs, 'ex.com', 1001, 'pw')
def test_dictify_converts_ordereddict(self):
od = users.OrderedDict([('a', 1), ('b', {'c': 2})])
result = users.dictify(OrderedDict(od))
self.assertIsInstance(result, dict)
self.assertEqual(result, {'a': 1, 'b': {'c': 2}})
def test_load_user_defs_and_conflict(self):
# create temp roles structure
tmp = tempfile.mkdtemp()
try:
os.makedirs(os.path.join(tmp, 'role1/users'))
os.makedirs(os.path.join(tmp, 'role2/users'))
# role1 defines user x
with open(os.path.join(tmp, 'role1/users/main.yml'), 'w') as f:
yaml.safe_dump({'users': {'x': {'email': 'x@a'}}}, f)
# role2 defines same user x with same value
with open(os.path.join(tmp, 'role2/users/main.yml'), 'w') as f:
yaml.safe_dump({'users': {'x': {'email': 'x@a'}}}, f)
defs = users.load_user_defs(tmp)
self.assertIn('x', defs)
# now conflict definition
with open(os.path.join(tmp, 'role2/users/main.yml'), 'w') as f:
yaml.safe_dump({'users': {'x': {'email': 'x@b'}}}, f)
with self.assertRaises(ValueError):
users.load_user_defs(tmp)
finally:
shutil.rmtree(tmp)
if __name__ == '__main__':
unittest.main()