|
| 1 | +import os |
| 2 | +import yaml |
| 3 | +from ansible.errors import AnsibleFilterError |
| 4 | +from ansible.utils.display import Display |
| 5 | + |
| 6 | +display = Display() |
| 7 | + |
| 8 | +class FilterModule(object): |
| 9 | + def filters(self): |
| 10 | + return { |
| 11 | + 'resolve_progress_dependencies': self.resolve_progress_dependencies |
| 12 | + } |
| 13 | + |
| 14 | + def _get_role_fqcn_parts(self, role_name_str): |
| 15 | + """Splits a role name (potentially FQCN) into (namespace, collection, simple_name)""" |
| 16 | + parts = role_name_str.split('.') |
| 17 | + if len(parts) >= 3: |
| 18 | + return parts[0], parts[1], '.'.join(parts[2:]) # Handles simple_name containing dots |
| 19 | + elif len(parts) == 1: # Simple name |
| 20 | + return None, None, parts[0] |
| 21 | + else: # Ambiguous, treat as simple for now or could be an error |
| 22 | + display.vvv(f"Ambiguous role name for FQCN parsing: {role_name_str}") |
| 23 | + return None, None, role_name_str |
| 24 | + |
| 25 | + |
| 26 | + def _get_role_meta(self, role_path): |
| 27 | + meta_file = os.path.join(role_path, 'meta', 'main.yml') |
| 28 | + if not os.path.exists(meta_file): |
| 29 | + display.vvv(f"Meta file not found: {meta_file}") |
| 30 | + return None |
| 31 | + try: |
| 32 | + with open(meta_file, 'r') as f: |
| 33 | + content = yaml.safe_load(f) |
| 34 | + display.vvv(f"Meta content for {role_path}: {content}") |
| 35 | + return content |
| 36 | + except Exception as e: |
| 37 | + display.warning(f"Error parsing meta file {meta_file}: {e}") |
| 38 | + return None |
| 39 | + |
| 40 | + def _get_role_defaults(self, role_path): |
| 41 | + defaults_file = os.path.join(role_path, 'defaults', 'main.yml') |
| 42 | + if not os.path.exists(defaults_file): |
| 43 | + display.vvv(f"Defaults file not found: {defaults_file}") |
| 44 | + return {} |
| 45 | + try: |
| 46 | + with open(defaults_file, 'r') as f: |
| 47 | + content = yaml.safe_load(f) or {} |
| 48 | + display.vvv(f"Defaults content for {role_path}: {content}") |
| 49 | + return content |
| 50 | + except Exception as e: |
| 51 | + display.warning(f"Error parsing defaults file {defaults_file}: {e}") |
| 52 | + return {} |
| 53 | + |
| 54 | + def resolve_progress_dependencies(self, initial_roles_config, playbook_dir, ansible_collections_base_relative_path): |
| 55 | + """ |
| 56 | + :param initial_roles_config: List of role dicts from provision_roles |
| 57 | + :param playbook_dir: The directory of the playbook being run. |
| 58 | + :param ansible_collections_base_relative_path: Relative path from playbook_dir to the root of ansible_collections (e.g., '../ansible_collections') |
| 59 | + """ |
| 60 | + display.v(f"Starting progress dependency resolution. Playbook dir: {playbook_dir}") |
| 61 | + display.v(f"Received initial_roles_config: {initial_roles_config}") |
| 62 | + display.v(f"Ansible collections base relative path: {ansible_collections_base_relative_path}") |
| 63 | + |
| 64 | + all_roles_data = {} # Keyed by FQCN |
| 65 | + abs_ansible_collections_base = os.path.normpath(os.path.join(playbook_dir, ansible_collections_base_relative_path)) |
| 66 | + display.v(f"Absolute ansible_collections base path: {abs_ansible_collections_base}") |
| 67 | + |
| 68 | + if not os.path.isdir(abs_ansible_collections_base): |
| 69 | + display.error(f"Ansible collections base directory not found: {abs_ansible_collections_base}") |
| 70 | + return [] # Return empty if base collections dir is not found |
| 71 | + |
| 72 | + # Phase 1: Discover all roles by scanning the ansible_collections_base_path |
| 73 | + for namespace_name in os.listdir(abs_ansible_collections_base): |
| 74 | + namespace_path = os.path.join(abs_ansible_collections_base, namespace_name) |
| 75 | + if not os.path.isdir(namespace_path): |
| 76 | + continue |
| 77 | + |
| 78 | + for collection_name in os.listdir(namespace_path): |
| 79 | + collection_path = os.path.join(namespace_path, collection_name) |
| 80 | + if not os.path.isdir(collection_path): |
| 81 | + continue |
| 82 | + |
| 83 | + abs_collection_roles_dir = os.path.join(collection_path, 'roles') |
| 84 | + display.v(f"Scanning for roles in: {abs_collection_roles_dir} (collection: {namespace_name}.{collection_name})") |
| 85 | + |
| 86 | + if not os.path.isdir(abs_collection_roles_dir): |
| 87 | + display.vvv(f"No 'roles' directory in collection {namespace_name}.{collection_name} at {collection_path}") |
| 88 | + continue |
| 89 | + |
| 90 | + for simple_role_name in os.listdir(abs_collection_roles_dir): |
| 91 | + role_path = os.path.join(abs_collection_roles_dir, simple_role_name) |
| 92 | + if os.path.isdir(role_path): |
| 93 | + fqcn = f"{namespace_name}.{collection_name}.{simple_role_name}" |
| 94 | + # This block should be nested to ensure fqcn is defined |
| 95 | + if fqcn not in all_roles_data: |
| 96 | + display.vv(f"Found role '{simple_role_name}' (FQCN: {fqcn}) at {role_path}") |
| 97 | + meta_content = self._get_role_meta(role_path) |
| 98 | + defaults_content = self._get_role_defaults(role_path) |
| 99 | + all_roles_data[fqcn] = { |
| 100 | + 'path': role_path, |
| 101 | + 'meta': meta_content, |
| 102 | + 'defaults': defaults_content, |
| 103 | + 'name': simple_role_name, # Simple name |
| 104 | + 'fqcn': fqcn, |
| 105 | + 'namespace': namespace_name, |
| 106 | + 'collection': collection_name |
| 107 | + } |
| 108 | + display.vvv(f"All discovered roles data (keyed by FQCN): {list(all_roles_data.keys())}") |
| 109 | + |
| 110 | + # Phase 2: Recursive resolution |
| 111 | + final_progress_definitions = [] |
| 112 | + roles_to_process_queue = [] |
| 113 | + for r_conf in initial_roles_config: |
| 114 | + if r_conf.get('enabled', True): # Default to enabled |
| 115 | + # r_conf['name'] is expected to be an FQCN from Hosts.yml |
| 116 | + roles_to_process_queue.append({'fqcn': r_conf['name'], 'source_vars': r_conf.get('vars', {})}) |
| 117 | + |
| 118 | + counted_role_fqcns = set() |
| 119 | + explored_for_dependencies_fqcns = set() |
| 120 | + |
| 121 | + display.v(f"Initial processing queue: {roles_to_process_queue}") |
| 122 | + |
| 123 | + idx = 0 |
| 124 | + while idx < len(roles_to_process_queue): |
| 125 | + current_task = roles_to_process_queue[idx] |
| 126 | + idx += 1 |
| 127 | + |
| 128 | + current_fqcn = current_task['fqcn'] |
| 129 | + source_vars = current_task['source_vars'] |
| 130 | + |
| 131 | + display.vv(f"Processing '{current_fqcn}' from queue. Source vars: {source_vars}") |
| 132 | + |
| 133 | + if current_fqcn in counted_role_fqcns and current_fqcn in explored_for_dependencies_fqcns: |
| 134 | + display.vvv(f"'{current_fqcn}' already counted and explored. Skipping.") |
| 135 | + continue |
| 136 | + |
| 137 | + role_data = all_roles_data.get(current_fqcn) |
| 138 | + if not role_data: |
| 139 | + display.warning(f"Role '{current_fqcn}' (from provision_roles/dependency) not found in scanned collections. Skipping.") |
| 140 | + continue |
| 141 | + |
| 142 | + role_defaults = role_data.get('defaults', {}) |
| 143 | + count_this_role_progress = source_vars.get('count_progress', role_defaults.get('count_progress', False)) |
| 144 | + if isinstance(count_this_role_progress, str): |
| 145 | + count_this_role_progress = count_this_role_progress.lower() == 'true' |
| 146 | + |
| 147 | + display.vvv(f"Role '{current_fqcn}': count_progress={count_this_role_progress} (source: {source_vars.get('count_progress')}, default: {role_defaults.get('count_progress')})") |
| 148 | + |
| 149 | + if count_this_role_progress and current_fqcn not in counted_role_fqcns: |
| 150 | + progress_units = source_vars.get('progress_units', role_defaults.get('progress_units', 1)) |
| 151 | + try: |
| 152 | + progress_units = int(progress_units) |
| 153 | + except ValueError: |
| 154 | + display.warning(f"Invalid progress_units '{progress_units}' for role '{current_fqcn}'. Defaulting to 1.") |
| 155 | + progress_units = 1 |
| 156 | + |
| 157 | + final_progress_definitions.append({'name': current_fqcn, 'progress_units': progress_units}) |
| 158 | + counted_role_fqcns.add(current_fqcn) |
| 159 | + display.vv(f"Added '{current_fqcn}' to progress count with {progress_units} units.") |
| 160 | + |
| 161 | + if current_fqcn not in explored_for_dependencies_fqcns: |
| 162 | + explored_for_dependencies_fqcns.add(current_fqcn) |
| 163 | + meta = role_data.get('meta') |
| 164 | + if meta and 'dependencies' in meta and isinstance(meta['dependencies'], list): |
| 165 | + for dep in meta['dependencies']: |
| 166 | + dep_name_str = None |
| 167 | + dep_vars = {} |
| 168 | + if isinstance(dep, dict): |
| 169 | + dep_name_str = dep.get('role') |
| 170 | + dep_vars = {k: v for k, v in dep.items() if k != 'role'} |
| 171 | + elif isinstance(dep, str): |
| 172 | + dep_name_str = dep |
| 173 | + |
| 174 | + if not dep_name_str: |
| 175 | + continue |
| 176 | + |
| 177 | + dep_fqcn_to_queue = None |
| 178 | + # Check if dep_name_str is already an FQCN |
| 179 | + if '.' in dep_name_str and len(dep_name_str.split('.')) >= 3 : # Heuristic for FQCN |
| 180 | + if dep_name_str in all_roles_data: |
| 181 | + dep_fqcn_to_queue = dep_name_str |
| 182 | + else: |
| 183 | + display.vvv(f"Dependency '{dep_name_str}' looks like FQCN but not found in all_roles_data.") |
| 184 | + else: # Simple name, try to resolve within current role's collection |
| 185 | + parent_namespace = role_data.get('namespace') |
| 186 | + parent_collection = role_data.get('collection') |
| 187 | + if parent_namespace and parent_collection: |
| 188 | + potential_fqcn = f"{parent_namespace}.{parent_collection}.{dep_name_str}" |
| 189 | + if potential_fqcn in all_roles_data: |
| 190 | + dep_fqcn_to_queue = potential_fqcn |
| 191 | + else: |
| 192 | + display.vvv(f"Simple dependency '{dep_name_str}' not found as '{potential_fqcn}' in same collection as '{current_fqcn}'.") |
| 193 | + else: |
| 194 | + display.vvv(f"Cannot resolve simple dependency '{dep_name_str}' for '{current_fqcn}' due to missing parent N/C info.") |
| 195 | + |
| 196 | + if dep_fqcn_to_queue: |
| 197 | + is_in_queue = any(item['fqcn'] == dep_fqcn_to_queue for item in roles_to_process_queue[idx:]) |
| 198 | + if dep_fqcn_to_queue not in explored_for_dependencies_fqcns and not is_in_queue: |
| 199 | + display.vvv(f"Queueing dependency '{dep_fqcn_to_queue}' of '{current_fqcn}' with vars {dep_vars}") |
| 200 | + roles_to_process_queue.append({'fqcn': dep_fqcn_to_queue, 'source_vars': dep_vars}) |
| 201 | + else: |
| 202 | + display.vvv(f"Dependency '{dep_fqcn_to_queue}' of '{current_fqcn}' already explored or in queue. Skipping queue add.") |
| 203 | + else: |
| 204 | + display.warning(f"Could not resolve dependency '{dep_name_str}' for role '{current_fqcn}'. Skipping.") |
| 205 | + else: |
| 206 | + display.vvv(f"No dependencies found or meta missing for '{current_fqcn}'.") |
| 207 | + else: |
| 208 | + display.vvv(f"'{current_fqcn}' already explored for dependencies.") |
| 209 | + |
| 210 | + display.v(f"Final progress definitions: {final_progress_definitions}") |
| 211 | + return final_progress_definitions |
0 commit comments