mcp-souschef 2.5.3__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mcp_souschef-2.5.3.dist-info → mcp_souschef-3.0.0.dist-info}/METADATA +135 -28
- mcp_souschef-3.0.0.dist-info/RECORD +46 -0
- {mcp_souschef-2.5.3.dist-info → mcp_souschef-3.0.0.dist-info}/WHEEL +1 -1
- souschef/__init__.py +43 -3
- souschef/assessment.py +1260 -69
- souschef/ci/common.py +126 -0
- souschef/ci/github_actions.py +4 -93
- souschef/ci/gitlab_ci.py +3 -53
- souschef/ci/jenkins_pipeline.py +3 -60
- souschef/cli.py +129 -20
- souschef/converters/__init__.py +2 -2
- souschef/converters/cookbook_specific.py +125 -0
- souschef/converters/cookbook_specific.py.backup +109 -0
- souschef/converters/playbook.py +1022 -15
- souschef/converters/resource.py +113 -10
- souschef/converters/template.py +177 -0
- souschef/core/constants.py +13 -0
- souschef/core/metrics.py +313 -0
- souschef/core/path_utils.py +12 -9
- souschef/core/validation.py +53 -0
- souschef/deployment.py +85 -33
- souschef/parsers/attributes.py +397 -32
- souschef/parsers/recipe.py +48 -10
- souschef/server.py +715 -37
- souschef/ui/app.py +1658 -379
- souschef/ui/health_check.py +36 -0
- souschef/ui/pages/ai_settings.py +563 -0
- souschef/ui/pages/cookbook_analysis.py +3270 -166
- souschef/ui/pages/validation_reports.py +274 -0
- mcp_souschef-2.5.3.dist-info/RECORD +0 -38
- {mcp_souschef-2.5.3.dist-info → mcp_souschef-3.0.0.dist-info}/entry_points.txt +0 -0
- {mcp_souschef-2.5.3.dist-info → mcp_souschef-3.0.0.dist-info}/licenses/LICENSE +0 -0
souschef/server.py
CHANGED
|
@@ -10,7 +10,7 @@ from mcp.server.fastmcp import FastMCP
|
|
|
10
10
|
|
|
11
11
|
# Import assessment functions with aliases to avoid name conflicts
|
|
12
12
|
from souschef.assessment import (
|
|
13
|
-
|
|
13
|
+
analyse_cookbook_dependencies as _analyse_cookbook_dependencies,
|
|
14
14
|
)
|
|
15
15
|
from souschef.assessment import (
|
|
16
16
|
assess_chef_migration_complexity as _assess_chef_migration_complexity,
|
|
@@ -79,7 +79,7 @@ from souschef.converters.playbook import ( # noqa: F401
|
|
|
79
79
|
|
|
80
80
|
# Import playbook converter functions
|
|
81
81
|
from souschef.converters.playbook import (
|
|
82
|
-
|
|
82
|
+
analyse_chef_search_patterns as _analyse_chef_search_patterns,
|
|
83
83
|
)
|
|
84
84
|
from souschef.converters.playbook import (
|
|
85
85
|
convert_chef_search_to_inventory as _convert_chef_search_to_inventory,
|
|
@@ -139,8 +139,8 @@ from souschef.core.validation import ( # noqa: F401
|
|
|
139
139
|
# Note: MCP tool wrappers exist for some of these, but tests import directly
|
|
140
140
|
# codeql[py/unused-import]: Backward compatibility exports for test suite
|
|
141
141
|
from souschef.deployment import ( # noqa: F401
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
_analyse_cookbook_for_awx,
|
|
143
|
+
_analyse_cookbooks_directory,
|
|
144
144
|
_detect_deployment_patterns_in_recipe,
|
|
145
145
|
_extract_cookbook_attributes,
|
|
146
146
|
_extract_cookbook_dependencies,
|
|
@@ -151,7 +151,7 @@ from souschef.deployment import ( # noqa: F401
|
|
|
151
151
|
_generate_survey_fields_from_attributes,
|
|
152
152
|
_parse_chef_runlist,
|
|
153
153
|
_recommend_ansible_strategies,
|
|
154
|
-
|
|
154
|
+
analyse_chef_application_patterns,
|
|
155
155
|
convert_chef_deployment_to_ansible_strategy,
|
|
156
156
|
generate_awx_inventory_source_from_chef,
|
|
157
157
|
generate_awx_job_template_from_cookbook,
|
|
@@ -163,9 +163,6 @@ from souschef.deployment import ( # noqa: F401
|
|
|
163
163
|
|
|
164
164
|
# Re-exports for backward compatibility (used by tests)
|
|
165
165
|
# These are imported and re-exported intentionally
|
|
166
|
-
from souschef.deployment import (
|
|
167
|
-
analyze_chef_application_patterns as _analyze_chef_application_patterns,
|
|
168
|
-
)
|
|
169
166
|
from souschef.deployment import (
|
|
170
167
|
convert_chef_deployment_to_ansible_strategy as _convert_chef_deployment_to_ansible_strategy,
|
|
171
168
|
)
|
|
@@ -285,6 +282,9 @@ ERROR_FILE_NOT_FOUND = "Error: File not found at {path}"
|
|
|
285
282
|
ERROR_IS_DIRECTORY = "Error: {path} is a directory, not a file"
|
|
286
283
|
ERROR_PERMISSION_DENIED = "Error: Permission denied for {path}"
|
|
287
284
|
|
|
285
|
+
# File constants
|
|
286
|
+
METADATA_RB = "metadata.rb"
|
|
287
|
+
|
|
288
288
|
# Validation Framework Classes
|
|
289
289
|
|
|
290
290
|
|
|
@@ -903,9 +903,9 @@ def generate_ansible_vault_from_databags(
|
|
|
903
903
|
|
|
904
904
|
|
|
905
905
|
@mcp.tool()
|
|
906
|
-
def
|
|
906
|
+
def analyse_chef_databag_usage(cookbook_path: str, databags_path: str = "") -> str:
|
|
907
907
|
"""
|
|
908
|
-
|
|
908
|
+
Analyse Chef cookbook for data bag usage and provide migration recommendations.
|
|
909
909
|
|
|
910
910
|
Args:
|
|
911
911
|
cookbook_path: Path to Chef cookbook
|
|
@@ -928,7 +928,7 @@ def analyze_chef_databag_usage(cookbook_path: str, databags_path: str = "") -> s
|
|
|
928
928
|
if databags_path:
|
|
929
929
|
databags = _normalize_path(databags_path)
|
|
930
930
|
if databags.exists():
|
|
931
|
-
databag_structure =
|
|
931
|
+
databag_structure = _analyse_databag_structure(databags)
|
|
932
932
|
|
|
933
933
|
# Generate recommendations
|
|
934
934
|
recommendations = _generate_databag_migration_recommendations(
|
|
@@ -1064,11 +1064,11 @@ def generate_inventory_from_chef_environments(
|
|
|
1064
1064
|
|
|
1065
1065
|
|
|
1066
1066
|
@mcp.tool()
|
|
1067
|
-
def
|
|
1067
|
+
def analyse_chef_environment_usage(
|
|
1068
1068
|
cookbook_path: str, environments_path: str = ""
|
|
1069
1069
|
) -> str:
|
|
1070
1070
|
"""
|
|
1071
|
-
|
|
1071
|
+
Analyse Chef cookbook for environment usage.
|
|
1072
1072
|
|
|
1073
1073
|
Provides migration recommendations.
|
|
1074
1074
|
|
|
@@ -1093,7 +1093,7 @@ def analyze_chef_environment_usage(
|
|
|
1093
1093
|
if environments_path:
|
|
1094
1094
|
environments = _normalize_path(environments_path)
|
|
1095
1095
|
if environments.exists():
|
|
1096
|
-
environment_structure =
|
|
1096
|
+
environment_structure = _analyse_environments_structure(environments)
|
|
1097
1097
|
|
|
1098
1098
|
# Generate recommendations
|
|
1099
1099
|
recommendations = _generate_environment_migration_recommendations(
|
|
@@ -1646,8 +1646,8 @@ def _find_environment_patterns_in_content(content: str, file_path: str) -> list:
|
|
|
1646
1646
|
return patterns
|
|
1647
1647
|
|
|
1648
1648
|
|
|
1649
|
-
def
|
|
1650
|
-
"""
|
|
1649
|
+
def _analyse_environments_structure(environments_path) -> dict:
|
|
1650
|
+
"""Analyse the structure of Chef environments directory."""
|
|
1651
1651
|
structure: dict[str, Any] = {"total_environments": 0, "environments": {}}
|
|
1652
1652
|
|
|
1653
1653
|
for env_file in environments_path.glob("*.rb"):
|
|
@@ -1679,8 +1679,8 @@ def _analyze_environments_structure(environments_path) -> dict:
|
|
|
1679
1679
|
return structure
|
|
1680
1680
|
|
|
1681
1681
|
|
|
1682
|
-
def
|
|
1683
|
-
"""
|
|
1682
|
+
def _analyse_usage_pattern_recommendations(usage_patterns: list) -> list[str]:
|
|
1683
|
+
"""Analyse usage patterns and generate recommendations."""
|
|
1684
1684
|
if not usage_patterns:
|
|
1685
1685
|
return []
|
|
1686
1686
|
|
|
@@ -1711,8 +1711,8 @@ def _analyze_usage_pattern_recommendations(usage_patterns: list) -> list[str]:
|
|
|
1711
1711
|
return recommendations
|
|
1712
1712
|
|
|
1713
1713
|
|
|
1714
|
-
def
|
|
1715
|
-
"""
|
|
1714
|
+
def _analyse_structure_recommendations(env_structure: dict) -> list[str]:
|
|
1715
|
+
"""Analyse environment structure and generate recommendations."""
|
|
1716
1716
|
if not env_structure:
|
|
1717
1717
|
return []
|
|
1718
1718
|
|
|
@@ -1760,8 +1760,8 @@ def _generate_environment_migration_recommendations(
|
|
|
1760
1760
|
) -> str:
|
|
1761
1761
|
"""Generate migration recommendations based on environment usage analysis."""
|
|
1762
1762
|
recommendations = []
|
|
1763
|
-
recommendations.extend(
|
|
1764
|
-
recommendations.extend(
|
|
1763
|
+
recommendations.extend(_analyse_usage_pattern_recommendations(usage_patterns))
|
|
1764
|
+
recommendations.extend(_analyse_structure_recommendations(env_structure))
|
|
1765
1765
|
recommendations.extend(_get_general_migration_recommendations())
|
|
1766
1766
|
|
|
1767
1767
|
return "\n".join(recommendations)
|
|
@@ -2090,8 +2090,8 @@ def _find_databag_patterns_in_content(content: str, file_path: str) -> list:
|
|
|
2090
2090
|
return patterns
|
|
2091
2091
|
|
|
2092
2092
|
|
|
2093
|
-
def
|
|
2094
|
-
"""
|
|
2093
|
+
def _analyse_databag_structure(databags_path) -> dict:
|
|
2094
|
+
"""Analyse the structure of Chef data bags directory."""
|
|
2095
2095
|
structure: dict[str, Any] = {
|
|
2096
2096
|
"total_databags": 0,
|
|
2097
2097
|
"total_items": 0,
|
|
@@ -2135,9 +2135,9 @@ def _analyze_databag_structure(databags_path) -> dict:
|
|
|
2135
2135
|
return structure
|
|
2136
2136
|
|
|
2137
2137
|
|
|
2138
|
-
def
|
|
2138
|
+
def _analyse_usage_patterns(usage_patterns: list) -> list[str]:
|
|
2139
2139
|
"""
|
|
2140
|
-
|
|
2140
|
+
Analyse databag usage patterns and generate recommendations.
|
|
2141
2141
|
|
|
2142
2142
|
Args:
|
|
2143
2143
|
usage_patterns: List of usage pattern dicts
|
|
@@ -2178,9 +2178,9 @@ def _analyze_usage_patterns(usage_patterns: list) -> list[str]:
|
|
|
2178
2178
|
return recommendations
|
|
2179
2179
|
|
|
2180
2180
|
|
|
2181
|
-
def
|
|
2181
|
+
def _analyse_databag_structure_recommendations(databag_structure: dict) -> list[str]:
|
|
2182
2182
|
"""
|
|
2183
|
-
|
|
2183
|
+
Analyse databag structure and generate recommendations.
|
|
2184
2184
|
|
|
2185
2185
|
Args:
|
|
2186
2186
|
databag_structure: Dict with structure analysis
|
|
@@ -2245,11 +2245,11 @@ def _generate_databag_migration_recommendations(
|
|
|
2245
2245
|
recommendations = []
|
|
2246
2246
|
|
|
2247
2247
|
# Analyze usage patterns
|
|
2248
|
-
recommendations.extend(
|
|
2248
|
+
recommendations.extend(_analyse_usage_patterns(usage_patterns))
|
|
2249
2249
|
|
|
2250
2250
|
# Analyze structure
|
|
2251
2251
|
recommendations.extend(
|
|
2252
|
-
|
|
2252
|
+
_analyse_databag_structure_recommendations(databag_structure)
|
|
2253
2253
|
)
|
|
2254
2254
|
|
|
2255
2255
|
# Add variable scope best practices
|
|
@@ -2329,7 +2329,7 @@ mcp.tool()(_generate_awx_inventory_source_from_chef)
|
|
|
2329
2329
|
mcp.tool()(_convert_chef_deployment_to_ansible_strategy)
|
|
2330
2330
|
mcp.tool()(_generate_blue_green_deployment_playbook)
|
|
2331
2331
|
mcp.tool()(_generate_canary_deployment_strategy)
|
|
2332
|
-
mcp.tool()(
|
|
2332
|
+
mcp.tool()(analyse_chef_application_patterns)
|
|
2333
2333
|
|
|
2334
2334
|
|
|
2335
2335
|
# ============================================================================
|
|
@@ -2389,9 +2389,9 @@ def generate_migration_plan(
|
|
|
2389
2389
|
|
|
2390
2390
|
|
|
2391
2391
|
@mcp.tool()
|
|
2392
|
-
def
|
|
2392
|
+
def analyse_cookbook_dependencies(cookbook_paths: str) -> str:
|
|
2393
2393
|
"""
|
|
2394
|
-
|
|
2394
|
+
Analyse dependencies between Chef cookbooks.
|
|
2395
2395
|
|
|
2396
2396
|
Maps cookbook dependencies, identifies circular dependencies, and
|
|
2397
2397
|
recommends migration order.
|
|
@@ -2403,7 +2403,7 @@ def analyze_cookbook_dependencies(cookbook_paths: str) -> str:
|
|
|
2403
2403
|
Dependency analysis report in markdown format.
|
|
2404
2404
|
|
|
2405
2405
|
"""
|
|
2406
|
-
return
|
|
2406
|
+
return _analyse_cookbook_dependencies(cookbook_paths)
|
|
2407
2407
|
|
|
2408
2408
|
|
|
2409
2409
|
@mcp.tool()
|
|
@@ -2559,9 +2559,10 @@ def generate_dynamic_inventory_script(search_queries: str) -> str:
|
|
|
2559
2559
|
return _generate_dynamic_inventory_script(search_queries)
|
|
2560
2560
|
|
|
2561
2561
|
|
|
2562
|
-
|
|
2562
|
+
@mcp.tool()
|
|
2563
|
+
def analyse_chef_search_patterns(recipe_or_cookbook_path: str) -> str:
|
|
2563
2564
|
"""
|
|
2564
|
-
|
|
2565
|
+
Analyse Chef search patterns in recipe or cookbook.
|
|
2565
2566
|
|
|
2566
2567
|
Args:
|
|
2567
2568
|
recipe_or_cookbook_path: Path to recipe or cookbook.
|
|
@@ -2570,7 +2571,7 @@ def analyze_chef_search_patterns(recipe_or_cookbook_path: str) -> str:
|
|
|
2570
2571
|
Analysis of search patterns found.
|
|
2571
2572
|
|
|
2572
2573
|
"""
|
|
2573
|
-
return
|
|
2574
|
+
return _analyse_chef_search_patterns(recipe_or_cookbook_path)
|
|
2574
2575
|
|
|
2575
2576
|
|
|
2576
2577
|
@mcp.tool()
|
|
@@ -2811,6 +2812,683 @@ def parse_chef_migration_assessment(
|
|
|
2811
2812
|
)
|
|
2812
2813
|
|
|
2813
2814
|
|
|
2815
|
+
@mcp.tool()
|
|
2816
|
+
def convert_cookbook_comprehensive(
|
|
2817
|
+
cookbook_path: str,
|
|
2818
|
+
output_path: str,
|
|
2819
|
+
assessment_data: str = "",
|
|
2820
|
+
include_templates: bool = True,
|
|
2821
|
+
include_attributes: bool = True,
|
|
2822
|
+
include_recipes: bool = True,
|
|
2823
|
+
role_name: str = "",
|
|
2824
|
+
) -> str:
|
|
2825
|
+
"""
|
|
2826
|
+
Convert an entire Chef cookbook to a complete Ansible role.
|
|
2827
|
+
|
|
2828
|
+
This function performs comprehensive conversion of a Chef cookbook to an Ansible role,
|
|
2829
|
+
including recipes, templates, attributes, and proper role structure. It can use
|
|
2830
|
+
assessment data to optimize the conversion process.
|
|
2831
|
+
|
|
2832
|
+
Args:
|
|
2833
|
+
cookbook_path: Path to the Chef cookbook directory
|
|
2834
|
+
output_path: Directory where the Ansible role will be created
|
|
2835
|
+
assessment_data: Optional JSON string with assessment results for optimization
|
|
2836
|
+
include_templates: Whether to convert ERB templates to Jinja2 (default: True)
|
|
2837
|
+
include_attributes: Whether to convert attributes to Ansible variables (default: True)
|
|
2838
|
+
include_recipes: Whether to convert recipes to Ansible tasks (default: True)
|
|
2839
|
+
role_name: Name for the Ansible role (defaults to cookbook name)
|
|
2840
|
+
|
|
2841
|
+
Returns:
|
|
2842
|
+
Summary of the conversion process and created files
|
|
2843
|
+
|
|
2844
|
+
"""
|
|
2845
|
+
try:
|
|
2846
|
+
from souschef.core.path_utils import _normalize_path
|
|
2847
|
+
|
|
2848
|
+
cookbook_dir = _normalize_path(cookbook_path)
|
|
2849
|
+
output_dir = _normalize_path(output_path)
|
|
2850
|
+
|
|
2851
|
+
if not cookbook_dir.exists():
|
|
2852
|
+
return f"Error: Cookbook path does not exist: {cookbook_path}"
|
|
2853
|
+
|
|
2854
|
+
# Parse assessment data if provided
|
|
2855
|
+
assessment = {}
|
|
2856
|
+
if assessment_data:
|
|
2857
|
+
try:
|
|
2858
|
+
assessment = json.loads(assessment_data)
|
|
2859
|
+
except json.JSONDecodeError:
|
|
2860
|
+
return f"Error: Invalid assessment data JSON: {assessment_data}"
|
|
2861
|
+
|
|
2862
|
+
# Get cookbook metadata and setup
|
|
2863
|
+
cookbook_name, role_name = _setup_conversion_metadata(cookbook_dir, role_name)
|
|
2864
|
+
|
|
2865
|
+
# Create role directory structure
|
|
2866
|
+
role_dir = _create_role_structure(output_dir, role_name)
|
|
2867
|
+
|
|
2868
|
+
# Initialize conversion summary
|
|
2869
|
+
conversion_summary = {
|
|
2870
|
+
"cookbook_name": cookbook_name,
|
|
2871
|
+
"role_name": role_name,
|
|
2872
|
+
"converted_files": [],
|
|
2873
|
+
"errors": [],
|
|
2874
|
+
"warnings": [],
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
# Convert components
|
|
2878
|
+
if include_recipes:
|
|
2879
|
+
_convert_recipes(cookbook_dir, role_dir, conversion_summary)
|
|
2880
|
+
|
|
2881
|
+
if include_templates:
|
|
2882
|
+
_convert_templates(cookbook_dir, role_dir, conversion_summary)
|
|
2883
|
+
|
|
2884
|
+
if include_attributes:
|
|
2885
|
+
_convert_attributes(cookbook_dir, role_dir, conversion_summary)
|
|
2886
|
+
|
|
2887
|
+
# Create main task file and metadata
|
|
2888
|
+
_create_main_task_file(
|
|
2889
|
+
cookbook_dir, role_dir, conversion_summary, include_recipes
|
|
2890
|
+
)
|
|
2891
|
+
_create_role_metadata(
|
|
2892
|
+
role_dir, role_name, cookbook_name, assessment, conversion_summary
|
|
2893
|
+
)
|
|
2894
|
+
|
|
2895
|
+
# Generate and return summary report
|
|
2896
|
+
return _generate_conversion_report(conversion_summary, role_dir)
|
|
2897
|
+
|
|
2898
|
+
except Exception as e:
|
|
2899
|
+
return format_error_with_context(
|
|
2900
|
+
e, "converting cookbook comprehensively", cookbook_path
|
|
2901
|
+
)
|
|
2902
|
+
|
|
2903
|
+
|
|
2904
|
+
def _setup_conversion_metadata(cookbook_dir: Path, role_name: str) -> tuple[str, str]:
|
|
2905
|
+
"""Get cookbook metadata and determine role name."""
|
|
2906
|
+
metadata_file = cookbook_dir / METADATA_RB
|
|
2907
|
+
cookbook_name = cookbook_dir.name
|
|
2908
|
+
if metadata_file.exists():
|
|
2909
|
+
metadata = _parse_cookbook_metadata(str(metadata_file))
|
|
2910
|
+
name_from_metadata = metadata.get("name")
|
|
2911
|
+
if name_from_metadata is not None:
|
|
2912
|
+
cookbook_name = str(name_from_metadata)
|
|
2913
|
+
|
|
2914
|
+
if not role_name:
|
|
2915
|
+
role_name = cookbook_name
|
|
2916
|
+
|
|
2917
|
+
return cookbook_name, role_name
|
|
2918
|
+
|
|
2919
|
+
|
|
2920
|
+
def _create_role_structure(output_dir: Path, role_name: str) -> Path:
|
|
2921
|
+
"""Create the standard Ansible role directory structure."""
|
|
2922
|
+
role_dir = output_dir / role_name
|
|
2923
|
+
role_tasks_dir = role_dir / "tasks"
|
|
2924
|
+
role_templates_dir = role_dir / "templates"
|
|
2925
|
+
role_vars_dir = role_dir / "vars"
|
|
2926
|
+
role_defaults_dir = role_dir / "defaults"
|
|
2927
|
+
|
|
2928
|
+
for directory in [
|
|
2929
|
+
role_tasks_dir,
|
|
2930
|
+
role_templates_dir,
|
|
2931
|
+
role_vars_dir,
|
|
2932
|
+
role_defaults_dir,
|
|
2933
|
+
]:
|
|
2934
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
2935
|
+
|
|
2936
|
+
return role_dir
|
|
2937
|
+
|
|
2938
|
+
|
|
2939
|
+
def _convert_recipes(
|
|
2940
|
+
cookbook_dir: Path, role_dir: Path, conversion_summary: dict
|
|
2941
|
+
) -> None:
|
|
2942
|
+
"""Convert Chef recipes to Ansible tasks."""
|
|
2943
|
+
recipes_dir = cookbook_dir / "recipes"
|
|
2944
|
+
role_tasks_dir = role_dir / "tasks"
|
|
2945
|
+
|
|
2946
|
+
if not recipes_dir.exists():
|
|
2947
|
+
conversion_summary["warnings"].append(
|
|
2948
|
+
f"No recipes directory found in {cookbook_dir.name}. "
|
|
2949
|
+
"Cookbook cannot be converted to Ansible tasks."
|
|
2950
|
+
)
|
|
2951
|
+
return
|
|
2952
|
+
|
|
2953
|
+
from souschef.converters.playbook import generate_playbook_from_recipe
|
|
2954
|
+
|
|
2955
|
+
recipe_files = list(recipes_dir.glob("*.rb"))
|
|
2956
|
+
if not recipe_files:
|
|
2957
|
+
conversion_summary["warnings"].append(
|
|
2958
|
+
f"No recipe files (*.rb) found in {cookbook_dir.name}/recipes/. "
|
|
2959
|
+
"Cookbook has no recipes to convert."
|
|
2960
|
+
)
|
|
2961
|
+
return
|
|
2962
|
+
|
|
2963
|
+
for recipe_file in recipe_files:
|
|
2964
|
+
try:
|
|
2965
|
+
recipe_name = recipe_file.stem
|
|
2966
|
+
|
|
2967
|
+
# Parse recipe to validate it can be processed
|
|
2968
|
+
parse_result = _parse_recipe(str(recipe_file))
|
|
2969
|
+
if parse_result.startswith("Error:"):
|
|
2970
|
+
conversion_summary["errors"].append(
|
|
2971
|
+
f"Failed to parse recipe {recipe_name}: {parse_result}"
|
|
2972
|
+
)
|
|
2973
|
+
continue
|
|
2974
|
+
|
|
2975
|
+
# Convert to Ansible tasks
|
|
2976
|
+
playbook_yaml = generate_playbook_from_recipe(str(recipe_file))
|
|
2977
|
+
|
|
2978
|
+
# Write as task file (paths normalized at function entry)
|
|
2979
|
+
task_file = (
|
|
2980
|
+
role_tasks_dir / f"{recipe_name}.yml"
|
|
2981
|
+
) # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected
|
|
2982
|
+
task_file.write_text(playbook_yaml)
|
|
2983
|
+
|
|
2984
|
+
conversion_summary["converted_files"].append(
|
|
2985
|
+
{
|
|
2986
|
+
"type": "task",
|
|
2987
|
+
"source": f"recipes/{recipe_name}.rb",
|
|
2988
|
+
"target": f"{role_dir.name}/tasks/{recipe_name}.yml",
|
|
2989
|
+
}
|
|
2990
|
+
)
|
|
2991
|
+
|
|
2992
|
+
except Exception as e:
|
|
2993
|
+
conversion_summary["errors"].append(
|
|
2994
|
+
f"Error converting recipe {recipe_file.name}: {str(e)}"
|
|
2995
|
+
)
|
|
2996
|
+
|
|
2997
|
+
|
|
2998
|
+
def _convert_templates(
|
|
2999
|
+
cookbook_dir: Path, role_dir: Path, conversion_summary: dict
|
|
3000
|
+
) -> None:
|
|
3001
|
+
"""Convert ERB templates to Jinja2 templates."""
|
|
3002
|
+
templates_dir = cookbook_dir / "templates"
|
|
3003
|
+
role_templates_dir = role_dir / "templates"
|
|
3004
|
+
|
|
3005
|
+
if not templates_dir.exists():
|
|
3006
|
+
return
|
|
3007
|
+
|
|
3008
|
+
for template_file in templates_dir.rglob("*.erb"):
|
|
3009
|
+
try:
|
|
3010
|
+
# Convert ERB to Jinja2
|
|
3011
|
+
conversion_result = _parse_template(str(template_file))
|
|
3012
|
+
if conversion_result.startswith("Error:"):
|
|
3013
|
+
conversion_summary["errors"].append(
|
|
3014
|
+
f"Failed to convert template {template_file.name}: {conversion_result}"
|
|
3015
|
+
)
|
|
3016
|
+
continue
|
|
3017
|
+
|
|
3018
|
+
# Parse the JSON result
|
|
3019
|
+
try:
|
|
3020
|
+
template_data = json.loads(conversion_result)
|
|
3021
|
+
jinja2_content = template_data.get("jinja2_template", "")
|
|
3022
|
+
|
|
3023
|
+
# Determine relative path for role templates (paths normalized at function entry)
|
|
3024
|
+
rel_path = template_file.relative_to(
|
|
3025
|
+
templates_dir
|
|
3026
|
+
) # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected
|
|
3027
|
+
target_file = (
|
|
3028
|
+
role_templates_dir / rel_path.with_suffix("")
|
|
3029
|
+
) # Remove .erb extension # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected
|
|
3030
|
+
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
3031
|
+
target_file.write_text(jinja2_content)
|
|
3032
|
+
|
|
3033
|
+
conversion_summary["converted_files"].append(
|
|
3034
|
+
{
|
|
3035
|
+
"type": "template",
|
|
3036
|
+
"source": f"templates/{rel_path}",
|
|
3037
|
+
"target": f"{role_dir.name}/templates/{rel_path.with_suffix('')}",
|
|
3038
|
+
}
|
|
3039
|
+
)
|
|
3040
|
+
|
|
3041
|
+
except json.JSONDecodeError:
|
|
3042
|
+
conversion_summary["errors"].append(
|
|
3043
|
+
f"Invalid JSON result for template {template_file.name}"
|
|
3044
|
+
)
|
|
3045
|
+
|
|
3046
|
+
except Exception as e:
|
|
3047
|
+
conversion_summary["errors"].append(
|
|
3048
|
+
f"Error converting template {template_file.name}: {str(e)}"
|
|
3049
|
+
)
|
|
3050
|
+
|
|
3051
|
+
|
|
3052
|
+
def _convert_attributes(
|
|
3053
|
+
cookbook_dir: Path, role_dir: Path, conversion_summary: dict
|
|
3054
|
+
) -> None:
|
|
3055
|
+
"""Convert Chef attributes to Ansible variables."""
|
|
3056
|
+
import yaml
|
|
3057
|
+
|
|
3058
|
+
attributes_dir = cookbook_dir / "attributes"
|
|
3059
|
+
role_defaults_dir = role_dir / "defaults"
|
|
3060
|
+
|
|
3061
|
+
if not attributes_dir.exists():
|
|
3062
|
+
return
|
|
3063
|
+
|
|
3064
|
+
for attr_file in attributes_dir.glob("*.rb"):
|
|
3065
|
+
try:
|
|
3066
|
+
# Read the file content
|
|
3067
|
+
content = attr_file.read_text()
|
|
3068
|
+
|
|
3069
|
+
# Extract attributes using internal function
|
|
3070
|
+
from souschef.parsers.attributes import (
|
|
3071
|
+
_extract_attributes,
|
|
3072
|
+
_resolve_attribute_precedence,
|
|
3073
|
+
)
|
|
3074
|
+
|
|
3075
|
+
raw_attributes = _extract_attributes(content)
|
|
3076
|
+
|
|
3077
|
+
if not raw_attributes:
|
|
3078
|
+
conversion_summary["warnings"].append(
|
|
3079
|
+
f"No attributes found in {attr_file.name}"
|
|
3080
|
+
)
|
|
3081
|
+
continue
|
|
3082
|
+
|
|
3083
|
+
# Resolve precedence to get final values
|
|
3084
|
+
resolved_attributes = _resolve_attribute_precedence(raw_attributes)
|
|
3085
|
+
|
|
3086
|
+
# Convert to Ansible variable format (flatten nested keys)
|
|
3087
|
+
ansible_vars = {}
|
|
3088
|
+
for attr_path, attr_info in resolved_attributes.items():
|
|
3089
|
+
# Convert Chef attribute paths to Ansible variable names
|
|
3090
|
+
# e.g., "nginx.port" becomes "nginx_port"
|
|
3091
|
+
ansible_key = attr_path.replace(".", "_")
|
|
3092
|
+
ansible_vars[ansible_key] = attr_info["value"]
|
|
3093
|
+
|
|
3094
|
+
# Write as defaults (paths normalized at function entry)
|
|
3095
|
+
defaults_file = (
|
|
3096
|
+
role_defaults_dir / f"{attr_file.stem}.yml"
|
|
3097
|
+
) # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected
|
|
3098
|
+
defaults_yaml = yaml.dump(ansible_vars, default_flow_style=False, indent=2)
|
|
3099
|
+
defaults_file.write_text(defaults_yaml)
|
|
3100
|
+
|
|
3101
|
+
conversion_summary["converted_files"].append(
|
|
3102
|
+
{
|
|
3103
|
+
"type": "defaults",
|
|
3104
|
+
"source": f"attributes/{attr_file.name}",
|
|
3105
|
+
"target": f"{role_dir.name}/defaults/{attr_file.stem}.yml",
|
|
3106
|
+
}
|
|
3107
|
+
)
|
|
3108
|
+
|
|
3109
|
+
except Exception as e:
|
|
3110
|
+
conversion_summary["errors"].append(
|
|
3111
|
+
f"Error converting attributes {attr_file.name}: {str(e)}"
|
|
3112
|
+
)
|
|
3113
|
+
|
|
3114
|
+
|
|
3115
|
+
def _create_main_task_file(
|
|
3116
|
+
cookbook_dir: Path, role_dir: Path, conversion_summary: dict, include_recipes: bool
|
|
3117
|
+
) -> None:
|
|
3118
|
+
"""Create main.yml task file from default recipe if it exists."""
|
|
3119
|
+
if not include_recipes:
|
|
3120
|
+
return
|
|
3121
|
+
|
|
3122
|
+
default_task_file = role_dir / "tasks" / "main.yml"
|
|
3123
|
+
if default_task_file.exists():
|
|
3124
|
+
return # Already exists
|
|
3125
|
+
|
|
3126
|
+
default_recipe = cookbook_dir / "recipes" / "default.rb"
|
|
3127
|
+
if not default_recipe.exists():
|
|
3128
|
+
return
|
|
3129
|
+
|
|
3130
|
+
try:
|
|
3131
|
+
from souschef.converters.playbook import generate_playbook_from_recipe
|
|
3132
|
+
|
|
3133
|
+
playbook_yaml = generate_playbook_from_recipe(str(default_recipe))
|
|
3134
|
+
default_task_file.write_text(playbook_yaml)
|
|
3135
|
+
conversion_summary["converted_files"].append(
|
|
3136
|
+
{
|
|
3137
|
+
"type": "task",
|
|
3138
|
+
"source": "recipes/default.rb",
|
|
3139
|
+
"target": f"{role_dir.name}/tasks/main.yml",
|
|
3140
|
+
}
|
|
3141
|
+
)
|
|
3142
|
+
except Exception as e:
|
|
3143
|
+
conversion_summary["warnings"].append(
|
|
3144
|
+
f"Could not create main.yml from default recipe: {str(e)}"
|
|
3145
|
+
)
|
|
3146
|
+
|
|
3147
|
+
|
|
3148
|
+
def _create_role_metadata(
|
|
3149
|
+
role_dir: Path,
|
|
3150
|
+
role_name: str,
|
|
3151
|
+
cookbook_name: str,
|
|
3152
|
+
assessment: dict,
|
|
3153
|
+
conversion_summary: dict,
|
|
3154
|
+
) -> None:
|
|
3155
|
+
"""Create Ansible role metadata file."""
|
|
3156
|
+
import yaml
|
|
3157
|
+
|
|
3158
|
+
# role_dir created from normalized paths
|
|
3159
|
+
meta_dir = role_dir / "meta"
|
|
3160
|
+
meta_dir.mkdir(exist_ok=True)
|
|
3161
|
+
meta_file = (
|
|
3162
|
+
meta_dir / "main.yml"
|
|
3163
|
+
) # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected
|
|
3164
|
+
|
|
3165
|
+
meta_content: dict[str, Any] = {
|
|
3166
|
+
"galaxy_info": {
|
|
3167
|
+
"role_name": role_name,
|
|
3168
|
+
"author": "SousChef Migration",
|
|
3169
|
+
"description": f"Converted from Chef cookbook {cookbook_name}",
|
|
3170
|
+
"license": "MIT",
|
|
3171
|
+
"min_ansible_version": "2.9",
|
|
3172
|
+
"platforms": ["ubuntu", "centos", "redhat"],
|
|
3173
|
+
"galaxy_tags": ["chef", "migration", "converted"],
|
|
3174
|
+
},
|
|
3175
|
+
"dependencies": [],
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
# Add dependencies from assessment data if available
|
|
3179
|
+
if assessment and "dependencies" in assessment:
|
|
3180
|
+
deps = assessment["dependencies"]
|
|
3181
|
+
if isinstance(deps, list):
|
|
3182
|
+
meta_content["dependencies"] = [{"role": dep} for dep in deps]
|
|
3183
|
+
|
|
3184
|
+
meta_yaml = yaml.dump(meta_content, default_flow_style=False, indent=2)
|
|
3185
|
+
meta_file.write_text(meta_yaml)
|
|
3186
|
+
|
|
3187
|
+
conversion_summary["converted_files"].append(
|
|
3188
|
+
{
|
|
3189
|
+
"type": "meta",
|
|
3190
|
+
"source": METADATA_RB,
|
|
3191
|
+
"target": f"{role_name}/meta/main.yml",
|
|
3192
|
+
}
|
|
3193
|
+
)
|
|
3194
|
+
|
|
3195
|
+
|
|
3196
|
+
def _generate_conversion_report(conversion_summary: dict, role_dir: Path) -> str:
|
|
3197
|
+
"""Generate a comprehensive conversion report."""
|
|
3198
|
+
summary_lines = [
|
|
3199
|
+
f"# Cookbook Conversion Summary: {conversion_summary['cookbook_name']} → {conversion_summary['role_name']}",
|
|
3200
|
+
"",
|
|
3201
|
+
"## Files Converted:",
|
|
3202
|
+
]
|
|
3203
|
+
|
|
3204
|
+
for file_info in conversion_summary["converted_files"]:
|
|
3205
|
+
summary_lines.append(
|
|
3206
|
+
f"- {file_info['type'].title()}: {file_info['source']} → {file_info['target']}"
|
|
3207
|
+
)
|
|
3208
|
+
|
|
3209
|
+
if conversion_summary["errors"]:
|
|
3210
|
+
summary_lines.append("")
|
|
3211
|
+
summary_lines.append("## Errors:")
|
|
3212
|
+
for error in conversion_summary["errors"]:
|
|
3213
|
+
summary_lines.append(f"- ❌ {error}")
|
|
3214
|
+
|
|
3215
|
+
if conversion_summary["warnings"]:
|
|
3216
|
+
summary_lines.append("")
|
|
3217
|
+
summary_lines.append("## Warnings:")
|
|
3218
|
+
for warning in conversion_summary["warnings"]:
|
|
3219
|
+
summary_lines.append(f"- ⚠️ {warning}")
|
|
3220
|
+
|
|
3221
|
+
summary_lines.append("")
|
|
3222
|
+
summary_lines.append(f"## Role Location: {role_dir}")
|
|
3223
|
+
summary_lines.append("")
|
|
3224
|
+
summary_lines.append("## Next Steps:")
|
|
3225
|
+
summary_lines.append("1. Review converted files for accuracy")
|
|
3226
|
+
summary_lines.append("2. Test the role with ansible-playbook --check")
|
|
3227
|
+
summary_lines.append("3. Update variable references and dependencies")
|
|
3228
|
+
summary_lines.append("4. Run integration tests")
|
|
3229
|
+
|
|
3230
|
+
return "\n".join(summary_lines)
|
|
3231
|
+
|
|
3232
|
+
|
|
3233
|
+
@mcp.tool()
|
|
3234
|
+
def convert_all_cookbooks_comprehensive(
|
|
3235
|
+
cookbooks_path: str,
|
|
3236
|
+
output_path: str,
|
|
3237
|
+
assessment_data: str = "",
|
|
3238
|
+
include_templates: bool = True,
|
|
3239
|
+
include_attributes: bool = True,
|
|
3240
|
+
include_recipes: bool = True,
|
|
3241
|
+
) -> str:
|
|
3242
|
+
"""
|
|
3243
|
+
Convert all Chef cookbooks in a directory to complete Ansible roles.
|
|
3244
|
+
|
|
3245
|
+
This function performs comprehensive conversion of all Chef cookbooks found in the
|
|
3246
|
+
specified directory to Ansible roles, including recipes, templates, attributes,
|
|
3247
|
+
and proper role structure. It can use assessment data to optimize the conversion process.
|
|
3248
|
+
|
|
3249
|
+
Args:
|
|
3250
|
+
cookbooks_path: Path to the directory containing Chef cookbooks
|
|
3251
|
+
output_path: Directory where the Ansible roles will be created
|
|
3252
|
+
assessment_data: Optional JSON string with assessment results for optimization
|
|
3253
|
+
include_templates: Whether to convert ERB templates to Jinja2 (default: True)
|
|
3254
|
+
include_attributes: Whether to convert attributes to Ansible variables (default: True)
|
|
3255
|
+
include_recipes: Whether to convert recipes to Ansible tasks (default: True)
|
|
3256
|
+
|
|
3257
|
+
Returns:
|
|
3258
|
+
Summary of the conversion process and created files for all cookbooks
|
|
3259
|
+
|
|
3260
|
+
"""
|
|
3261
|
+
try:
|
|
3262
|
+
cookbooks_dir, output_dir = _validate_conversion_paths(
|
|
3263
|
+
cookbooks_path, output_path
|
|
3264
|
+
)
|
|
3265
|
+
assessment = _parse_assessment_data(assessment_data)
|
|
3266
|
+
cookbook_dirs = _find_cookbook_directories(cookbooks_dir)
|
|
3267
|
+
|
|
3268
|
+
if not cookbook_dirs:
|
|
3269
|
+
return f"Error: No Chef cookbooks found in {cookbooks_path}. Cookbooks must contain a {METADATA_RB} file."
|
|
3270
|
+
|
|
3271
|
+
overall_summary = _initialize_conversion_summary(len(cookbook_dirs))
|
|
3272
|
+
_convert_all_cookbooks(
|
|
3273
|
+
cookbook_dirs,
|
|
3274
|
+
output_dir,
|
|
3275
|
+
assessment,
|
|
3276
|
+
include_templates,
|
|
3277
|
+
include_attributes,
|
|
3278
|
+
include_recipes,
|
|
3279
|
+
overall_summary,
|
|
3280
|
+
)
|
|
3281
|
+
|
|
3282
|
+
return _generate_batch_conversion_report(overall_summary, output_dir)
|
|
3283
|
+
|
|
3284
|
+
except Exception as e:
|
|
3285
|
+
return format_error_with_context(
|
|
3286
|
+
e, "converting all cookbooks comprehensively", cookbooks_path
|
|
3287
|
+
)
|
|
3288
|
+
|
|
3289
|
+
|
|
3290
|
+
def _validate_conversion_paths(
|
|
3291
|
+
cookbooks_path: str, output_path: str
|
|
3292
|
+
) -> tuple[Path, Path]:
|
|
3293
|
+
"""Validate and return Path objects for conversion paths."""
|
|
3294
|
+
from souschef.core.path_utils import _normalize_path
|
|
3295
|
+
|
|
3296
|
+
cookbooks_dir = _normalize_path(cookbooks_path)
|
|
3297
|
+
output_dir = _normalize_path(output_path)
|
|
3298
|
+
|
|
3299
|
+
if not cookbooks_dir.exists():
|
|
3300
|
+
raise ValueError(f"Cookbooks path does not exist: {cookbooks_path}")
|
|
3301
|
+
|
|
3302
|
+
return cookbooks_dir, output_dir
|
|
3303
|
+
|
|
3304
|
+
|
|
3305
|
+
def _parse_assessment_data(assessment_data: str) -> dict[Any, Any]:
|
|
3306
|
+
"""Parse assessment data JSON if provided."""
|
|
3307
|
+
if not assessment_data:
|
|
3308
|
+
return {}
|
|
3309
|
+
|
|
3310
|
+
try:
|
|
3311
|
+
parsed = json.loads(assessment_data)
|
|
3312
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
3313
|
+
except json.JSONDecodeError as e:
|
|
3314
|
+
raise ValueError(f"Invalid assessment data JSON: {assessment_data}") from e
|
|
3315
|
+
|
|
3316
|
+
|
|
3317
|
+
def _find_cookbook_directories(cookbooks_dir: Path) -> list[Path]:
|
|
3318
|
+
"""Find all cookbook directories containing metadata.rb files."""
|
|
3319
|
+
return [
|
|
3320
|
+
item
|
|
3321
|
+
for item in cookbooks_dir.iterdir()
|
|
3322
|
+
if item.is_dir() and (item / METADATA_RB).exists()
|
|
3323
|
+
]
|
|
3324
|
+
|
|
3325
|
+
|
|
3326
|
+
def _initialize_conversion_summary(total_cookbooks: int) -> dict:
|
|
3327
|
+
"""Initialize the overall conversion summary structure."""
|
|
3328
|
+
return {
|
|
3329
|
+
"total_cookbooks": total_cookbooks,
|
|
3330
|
+
"converted_cookbooks": [],
|
|
3331
|
+
"failed_cookbooks": [],
|
|
3332
|
+
"total_converted_files": 0,
|
|
3333
|
+
"total_errors": 0,
|
|
3334
|
+
"total_warnings": 0,
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
|
|
3338
|
+
def _convert_all_cookbooks(
|
|
3339
|
+
cookbook_dirs: list[Path],
|
|
3340
|
+
output_dir: Path,
|
|
3341
|
+
assessment: dict,
|
|
3342
|
+
include_templates: bool,
|
|
3343
|
+
include_attributes: bool,
|
|
3344
|
+
include_recipes: bool,
|
|
3345
|
+
overall_summary: dict,
|
|
3346
|
+
) -> None:
|
|
3347
|
+
"""Convert all cookbooks and update the overall summary."""
|
|
3348
|
+
for cookbook_dir in cookbook_dirs:
|
|
3349
|
+
try:
|
|
3350
|
+
_convert_single_cookbook_comprehensive(
|
|
3351
|
+
cookbook_dir,
|
|
3352
|
+
output_dir,
|
|
3353
|
+
assessment,
|
|
3354
|
+
include_templates,
|
|
3355
|
+
include_attributes,
|
|
3356
|
+
include_recipes,
|
|
3357
|
+
overall_summary,
|
|
3358
|
+
)
|
|
3359
|
+
except Exception as e:
|
|
3360
|
+
overall_summary["failed_cookbooks"].append(
|
|
3361
|
+
{
|
|
3362
|
+
"cookbook_name": cookbook_dir.name,
|
|
3363
|
+
"error": str(e),
|
|
3364
|
+
}
|
|
3365
|
+
)
|
|
3366
|
+
|
|
3367
|
+
|
|
3368
|
+
def _convert_single_cookbook_comprehensive(
|
|
3369
|
+
cookbook_dir: Path,
|
|
3370
|
+
output_dir: Path,
|
|
3371
|
+
assessment: dict,
|
|
3372
|
+
include_templates: bool,
|
|
3373
|
+
include_attributes: bool,
|
|
3374
|
+
include_recipes: bool,
|
|
3375
|
+
overall_summary: dict,
|
|
3376
|
+
) -> None:
|
|
3377
|
+
"""Convert a single cookbook comprehensively."""
|
|
3378
|
+
cookbook_name = cookbook_dir.name
|
|
3379
|
+
|
|
3380
|
+
# Get role name from metadata or directory name
|
|
3381
|
+
role_name = _get_role_name(cookbook_dir, cookbook_name)
|
|
3382
|
+
|
|
3383
|
+
# Create role directory structure
|
|
3384
|
+
role_dir = _create_role_structure(output_dir, role_name)
|
|
3385
|
+
|
|
3386
|
+
# Initialize conversion summary for this cookbook
|
|
3387
|
+
conversion_summary = {
|
|
3388
|
+
"cookbook_name": cookbook_name,
|
|
3389
|
+
"role_name": role_name,
|
|
3390
|
+
"converted_files": [],
|
|
3391
|
+
"errors": [],
|
|
3392
|
+
"warnings": [],
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3395
|
+
# Convert components
|
|
3396
|
+
if include_recipes:
|
|
3397
|
+
_convert_recipes(cookbook_dir, role_dir, conversion_summary)
|
|
3398
|
+
|
|
3399
|
+
if include_templates:
|
|
3400
|
+
_convert_templates(cookbook_dir, role_dir, conversion_summary)
|
|
3401
|
+
|
|
3402
|
+
if include_attributes:
|
|
3403
|
+
_convert_attributes(cookbook_dir, role_dir, conversion_summary)
|
|
3404
|
+
|
|
3405
|
+
# Create main task file and metadata
|
|
3406
|
+
_create_main_task_file(cookbook_dir, role_dir, conversion_summary, include_recipes)
|
|
3407
|
+
_create_role_metadata(
|
|
3408
|
+
role_dir, role_name, cookbook_name, assessment, conversion_summary
|
|
3409
|
+
)
|
|
3410
|
+
|
|
3411
|
+
# Add to overall summary
|
|
3412
|
+
overall_summary["converted_cookbooks"].append(
|
|
3413
|
+
{
|
|
3414
|
+
"cookbook_name": cookbook_name,
|
|
3415
|
+
"role_name": role_name,
|
|
3416
|
+
"role_path": str(role_dir),
|
|
3417
|
+
"converted_files": len(conversion_summary["converted_files"]),
|
|
3418
|
+
"errors": len(conversion_summary["errors"]),
|
|
3419
|
+
"warnings": len(conversion_summary["warnings"]),
|
|
3420
|
+
}
|
|
3421
|
+
)
|
|
3422
|
+
|
|
3423
|
+
overall_summary["total_converted_files"] += len(
|
|
3424
|
+
conversion_summary["converted_files"]
|
|
3425
|
+
)
|
|
3426
|
+
overall_summary["total_errors"] += len(conversion_summary["errors"])
|
|
3427
|
+
overall_summary["total_warnings"] += len(conversion_summary["warnings"])
|
|
3428
|
+
|
|
3429
|
+
|
|
3430
|
+
def _get_role_name(cookbook_dir: Path, default_name: str) -> str:
|
|
3431
|
+
"""Get the role name from metadata or return default."""
|
|
3432
|
+
metadata_file = cookbook_dir / METADATA_RB
|
|
3433
|
+
if metadata_file.exists():
|
|
3434
|
+
metadata = _parse_cookbook_metadata(str(metadata_file))
|
|
3435
|
+
name = metadata.get("name")
|
|
3436
|
+
# Ensure we return a string, handling potential list values
|
|
3437
|
+
if name is None:
|
|
3438
|
+
return default_name
|
|
3439
|
+
if isinstance(name, list):
|
|
3440
|
+
return name[0] if name else default_name
|
|
3441
|
+
return str(name)
|
|
3442
|
+
return default_name
|
|
3443
|
+
|
|
3444
|
+
|
|
3445
|
+
def _generate_batch_conversion_report(overall_summary: dict, output_dir: Path) -> str:
|
|
3446
|
+
"""Generate a comprehensive batch conversion report."""
|
|
3447
|
+
summary_lines = [
|
|
3448
|
+
"# Batch Cookbook Conversion Summary",
|
|
3449
|
+
"",
|
|
3450
|
+
"## Overview:",
|
|
3451
|
+
f"- Total cookbooks found: {overall_summary['total_cookbooks']}",
|
|
3452
|
+
f"- Successfully converted: {len(overall_summary['converted_cookbooks'])}",
|
|
3453
|
+
f"- Failed conversions: {len(overall_summary['failed_cookbooks'])}",
|
|
3454
|
+
f"- Total files converted: {overall_summary['total_converted_files']}",
|
|
3455
|
+
f"- Total errors: {overall_summary['total_errors']}",
|
|
3456
|
+
f"- Total warnings: {overall_summary['total_warnings']}",
|
|
3457
|
+
"",
|
|
3458
|
+
]
|
|
3459
|
+
|
|
3460
|
+
if overall_summary["converted_cookbooks"]:
|
|
3461
|
+
summary_lines.append("## Successfully Converted Cookbooks:")
|
|
3462
|
+
for cookbook in overall_summary["converted_cookbooks"]:
|
|
3463
|
+
summary_lines.append(
|
|
3464
|
+
f"- **{cookbook['cookbook_name']}** → `{cookbook['role_name']}`"
|
|
3465
|
+
)
|
|
3466
|
+
summary_lines.append(f" - Files converted: {cookbook['converted_files']}")
|
|
3467
|
+
summary_lines.append(f" - Errors: {cookbook['errors']}")
|
|
3468
|
+
summary_lines.append(f" - Warnings: {cookbook['warnings']}")
|
|
3469
|
+
summary_lines.append(f" - Location: {cookbook['role_path']}")
|
|
3470
|
+
summary_lines.append("")
|
|
3471
|
+
|
|
3472
|
+
if overall_summary["failed_cookbooks"]:
|
|
3473
|
+
summary_lines.append("## Failed Conversions:")
|
|
3474
|
+
for failed in overall_summary["failed_cookbooks"]:
|
|
3475
|
+
summary_lines.append(
|
|
3476
|
+
f"- ❌ **{failed['cookbook_name']}**: {failed['error']}"
|
|
3477
|
+
)
|
|
3478
|
+
summary_lines.append("")
|
|
3479
|
+
|
|
3480
|
+
summary_lines.append(f"## Output Directory: {output_dir}")
|
|
3481
|
+
summary_lines.append("")
|
|
3482
|
+
summary_lines.append("## Next Steps:")
|
|
3483
|
+
summary_lines.append("1. Review converted roles for accuracy")
|
|
3484
|
+
summary_lines.append("2. Test roles with ansible-playbook --check")
|
|
3485
|
+
summary_lines.append("3. Update variable references and cross-role dependencies")
|
|
3486
|
+
summary_lines.append("4. Run integration tests across all converted roles")
|
|
3487
|
+
summary_lines.append("5. Consider using Ansible Galaxy for role distribution")
|
|
3488
|
+
|
|
3489
|
+
return "\n".join(summary_lines)
|
|
3490
|
+
|
|
3491
|
+
|
|
2814
3492
|
# AWX/AAP deployment wrappers for backward compatibility
|
|
2815
3493
|
def main() -> None:
|
|
2816
3494
|
"""
|