mirror of
				https://github.com/kevinveenbirkenbach/computer-playbook.git
				synced 2025-10-31 18:29:21 +00:00 
			
		
		
		
	Added validation for deploy application ids
This commit is contained in:
		| @@ -6,7 +6,6 @@ import os | ||||
| import datetime | ||||
| import sys | ||||
|  | ||||
|  | ||||
| def run_ansible_playbook( | ||||
|     inventory, | ||||
|     modes, | ||||
| @@ -81,6 +80,24 @@ def run_ansible_playbook( | ||||
|     duration = end_time - start_time | ||||
|     print(f"⏱️ Total execution time: {duration}\n") | ||||
|  | ||||
| def validate_application_ids(inventory, app_ids): | ||||
|     """ | ||||
|     Abort the script if any application IDs are invalid, with detailed reasons. | ||||
|     """ | ||||
|     from utils.valid_deploy_id import ValidDeployId | ||||
|     validator = ValidDeployId() | ||||
|     invalid = validator.validate(inventory, app_ids) | ||||
|     if invalid: | ||||
|         print("\n❌ Detected invalid application_id(s):\n") | ||||
|         for app_id, status in invalid.items(): | ||||
|             reasons = [] | ||||
|             if not status['in_roles']: | ||||
|                 reasons.append("not defined in roles (cymais)") | ||||
|             if not status['in_inventory']: | ||||
|                 reasons.append("not found in inventory file") | ||||
|             print(f"  - {app_id}: " + ", ".join(reasons)) | ||||
|         sys.exit(1) | ||||
|  | ||||
|  | ||||
| def main(): | ||||
|     parser = argparse.ArgumentParser( | ||||
| @@ -150,6 +167,7 @@ def main(): | ||||
|     ) | ||||
|  | ||||
|     args = parser.parse_args() | ||||
|     validate_application_ids(args.inventory, args.id) | ||||
|  | ||||
|     modes = { | ||||
|         "mode_reset": args.reset, | ||||
|   | ||||
							
								
								
									
										116
									
								
								tests/unit/utils/test_valid_deploy_id.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								tests/unit/utils/test_valid_deploy_id.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| # File: tests/unit/utils/test_valid_deploy_id.py | ||||
| import os | ||||
| import tempfile | ||||
| import unittest | ||||
| import yaml | ||||
| from utils.valid_deploy_id import ValidDeployId | ||||
|  | ||||
| class TestValidDeployId(unittest.TestCase): | ||||
|     def setUp(self): | ||||
|         # Create a temporary directory for roles | ||||
|         self.temp_dir = tempfile.TemporaryDirectory() | ||||
|         self.roles_dir = os.path.join(self.temp_dir.name, 'roles') | ||||
|         os.makedirs(self.roles_dir) | ||||
|  | ||||
|         # Create a dummy role with application_id 'app1' | ||||
|         role_path = os.path.join(self.roles_dir, 'role1', 'vars') | ||||
|         os.makedirs(role_path) | ||||
|         with open(os.path.join(role_path, 'main.yml'), 'w', encoding='utf-8') as f: | ||||
|             yaml.safe_dump({'application_id': 'app1'}, f) | ||||
|  | ||||
|         # Initialize validator with our temp roles_dir | ||||
|         self.validator = ValidDeployId(roles_dir=self.roles_dir) | ||||
|  | ||||
|     def tearDown(self): | ||||
|         self.temp_dir.cleanup() | ||||
|  | ||||
|     def _write_ini_inventory(self, content): | ||||
|         fd, path = tempfile.mkstemp(suffix='.ini') | ||||
|         os.close(fd) | ||||
|         with open(path, 'w', encoding='utf-8') as f: | ||||
|             f.write(content) | ||||
|         return path | ||||
|  | ||||
|     def _write_yaml_inventory(self, data): | ||||
|         fd, path = tempfile.mkstemp(suffix='.yml') | ||||
|         os.close(fd) | ||||
|         with open(path, 'w', encoding='utf-8') as f: | ||||
|             yaml.safe_dump(data, f) | ||||
|         return path | ||||
|  | ||||
|     def test_valid_in_roles_and_ini_inventory(self): | ||||
|         # Inventory contains app1 as a host | ||||
|         ini_content = """ | ||||
|         [servers] | ||||
|         app1,otherhost | ||||
|         """ | ||||
|         inv = self._write_ini_inventory(ini_content) | ||||
|         result = self.validator.validate(inv, ['app1']) | ||||
|         self.assertEqual(result, {}, "app1 should be valid when in roles and ini inventory") | ||||
|  | ||||
|     def test_missing_in_roles(self): | ||||
|         # Inventory contains app2 but roles only have app1 | ||||
|         ini_content = """ | ||||
|         [servers] | ||||
|         app2 | ||||
|         """ | ||||
|         inv = self._write_ini_inventory(ini_content) | ||||
|         result = self.validator.validate(inv, ['app2']) | ||||
|         # app2 not in roles, but in inventory | ||||
|         expected = {'app2': {'in_roles': False, 'in_inventory': True}} | ||||
|         self.assertEqual(result, expected) | ||||
|  | ||||
|     def test_missing_in_inventory_ini(self): | ||||
|         # Roles have app1 but inventory does not mention it | ||||
|         ini_content = """ | ||||
|         [servers] | ||||
|         otherhost | ||||
|         """ | ||||
|         inv = self._write_ini_inventory(ini_content) | ||||
|         result = self.validator.validate(inv, ['app1']) | ||||
|         expected = {'app1': {'in_roles': True, 'in_inventory': False}} | ||||
|         self.assertEqual(result, expected) | ||||
|  | ||||
|     def test_missing_both_ini(self): | ||||
|         # Neither roles nor inventory have appX | ||||
|         ini_content = """ | ||||
|         [servers] | ||||
|         otherhost | ||||
|         """ | ||||
|         inv = self._write_ini_inventory(ini_content) | ||||
|         result = self.validator.validate(inv, ['appX']) | ||||
|         expected = {'appX': {'in_roles': False, 'in_inventory': False}} | ||||
|         self.assertEqual(result, expected) | ||||
|  | ||||
|     def test_valid_in_roles_and_yaml_inventory(self): | ||||
|         # YAML inventory with app1 as a dict key | ||||
|         data = {'app1': {'hosts': ['app1']}, 'group': {'app1': {}}} | ||||
|         inv = self._write_yaml_inventory(data) | ||||
|         result = self.validator.validate(inv, ['app1']) | ||||
|         self.assertEqual(result, {}, "app1 should be valid in roles and yaml inventory") | ||||
|  | ||||
|     def test_missing_in_roles_yaml(self): | ||||
|         # YAML inventory has app2 key but roles only have app1 | ||||
|         data = {'app2': {}} | ||||
|         inv = self._write_yaml_inventory(data) | ||||
|         result = self.validator.validate(inv, ['app2']) | ||||
|         expected = {'app2': {'in_roles': False, 'in_inventory': True}} | ||||
|         self.assertEqual(result, expected) | ||||
|  | ||||
|     def test_missing_in_inventory_yaml(self): | ||||
|         # Roles have app1 but YAML inventory has no app1 | ||||
|         data = {'group': {'other': {}}} | ||||
|         inv = self._write_yaml_inventory(data) | ||||
|         result = self.validator.validate(inv, ['app1']) | ||||
|         expected = {'app1': {'in_roles': True, 'in_inventory': False}} | ||||
|         self.assertEqual(result, expected) | ||||
|  | ||||
|     def test_missing_both_yaml(self): | ||||
|         data = {} | ||||
|         inv = self._write_yaml_inventory(data) | ||||
|         result = self.validator.validate(inv, ['unknown']) | ||||
|         expected = {'unknown': {'in_roles': False, 'in_inventory': False}} | ||||
|         self.assertEqual(result, expected) | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     unittest.main() | ||||
							
								
								
									
										89
									
								
								utils/valid_deploy_id.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								utils/valid_deploy_id.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | ||||
| # File: utils/valid_deploy_id.py | ||||
| """ | ||||
| Utility for validating deployment application IDs against defined roles and inventory. | ||||
| """ | ||||
| import os | ||||
| import yaml | ||||
| import glob | ||||
| import configparser | ||||
|  | ||||
| from filter_plugins.get_all_application_ids import get_all_application_ids | ||||
|  | ||||
| class ValidDeployId: | ||||
|     def __init__(self, roles_dir='roles'): | ||||
|         # Load all known application IDs from roles | ||||
|         self.valid_ids = set(get_all_application_ids(roles_dir)) | ||||
|  | ||||
|     def validate(self, inventory_path, ids): | ||||
|         """ | ||||
|         Validate a list of application IDs against both role definitions and inventory. | ||||
|         Returns a dict mapping invalid IDs to their presence status. | ||||
|         Example: | ||||
|           { | ||||
|             "app1": {"in_roles": False, "in_inventory": True}, | ||||
|             "app2": {"in_roles": True, "in_inventory": False} | ||||
|           } | ||||
|         """ | ||||
|         invalid = {} | ||||
|         for app_id in ids: | ||||
|             in_roles = app_id in self.valid_ids | ||||
|             in_inventory = self._exists_in_inventory(inventory_path, app_id) | ||||
|             if not (in_roles and in_inventory): | ||||
|                 invalid[app_id] = { | ||||
|                     'in_roles': in_roles, | ||||
|                     'in_inventory': in_inventory | ||||
|                 } | ||||
|         return invalid | ||||
|  | ||||
|     def _exists_in_inventory(self, inventory_path, app_id): | ||||
|         _, ext = os.path.splitext(inventory_path) | ||||
|         if ext in ('.yml', '.yaml'): | ||||
|             return self._search_yaml_keys(inventory_path, app_id) | ||||
|         else: | ||||
|             return self._search_ini_sections(inventory_path, app_id) | ||||
|  | ||||
|     def _search_ini_sections(self, inventory_path, app_id): | ||||
|         """ | ||||
|         Manually parse INI inventory for sections and host lists. | ||||
|         Returns True if app_id matches a section name or a host in a section. | ||||
|         """ | ||||
|         present = False | ||||
|         with open(inventory_path, 'r', encoding='utf-8') as f: | ||||
|             current_section = None | ||||
|             for raw in f: | ||||
|                 line = raw.strip() | ||||
|                 # Skip blanks and comments | ||||
|                 if not line or line.startswith(('#', ';')): | ||||
|                     continue | ||||
|                 # Section header | ||||
|                 if line.startswith('[') and line.endswith(']'): | ||||
|                     current_section = line[1:-1].strip() | ||||
|                     if current_section == app_id: | ||||
|                         return True | ||||
|                     continue | ||||
|                 # Host or variable line under a section | ||||
|                 if current_section: | ||||
|                     # Split on commas or whitespace | ||||
|                     for part in [p.strip() for p in line.replace(',', ' ').split()]: | ||||
|                         if part == app_id: | ||||
|                             return True | ||||
|         return False | ||||
|  | ||||
|     def _search_yaml_keys(self, inventory_path, app_id): | ||||
|         with open(inventory_path, 'r', encoding='utf-8') as f: | ||||
|             data = yaml.safe_load(f) | ||||
|         return self._find_key(data, app_id) | ||||
|  | ||||
|     def _find_key(self, node, key):  # recursive search | ||||
|         if isinstance(node, dict): | ||||
|             for k, v in node.items(): | ||||
|                 # If key matches and maps to a dict or list, consider it present | ||||
|                 if k == key and isinstance(v, (dict, list)): | ||||
|                     return True | ||||
|                 if self._find_key(v, key): | ||||
|                     return True | ||||
|         elif isinstance(node, list): | ||||
|             for item in node: | ||||
|                 if self._find_key(item, key): | ||||
|                     return True | ||||
|         return False | ||||
		Reference in New Issue
	
	Block a user