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/converters/resource.py
CHANGED
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
4
|
import json
|
|
5
|
+
import re
|
|
5
6
|
from collections.abc import Callable
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
9
|
+
from souschef.converters.cookbook_specific import (
|
|
10
|
+
build_cookbook_resource_params,
|
|
11
|
+
get_cookbook_package_config,
|
|
12
|
+
)
|
|
8
13
|
from souschef.core.constants import ACTION_TO_STATE, RESOURCE_MAPPINGS
|
|
9
14
|
|
|
10
15
|
# Type alias for parameter builder functions
|
|
@@ -25,20 +30,59 @@ def _parse_properties(properties_str: str) -> dict[str, Any]:
|
|
|
25
30
|
if not properties_str:
|
|
26
31
|
return {}
|
|
27
32
|
try:
|
|
28
|
-
#
|
|
33
|
+
# Use ast.literal_eval for safe parsing of Python literals
|
|
29
34
|
result = ast.literal_eval(properties_str)
|
|
30
35
|
if isinstance(result, dict):
|
|
31
36
|
return result
|
|
32
37
|
return {}
|
|
33
38
|
except (ValueError, SyntaxError):
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
# If parsing fails, return empty dict rather than using unsafe eval
|
|
40
|
+
return {}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _normalize_template_value(value: Any) -> Any:
|
|
44
|
+
"""Normalize Ruby-style attribute references into Jinja templates."""
|
|
45
|
+
if isinstance(value, str):
|
|
46
|
+
# Convert Chef node attributes to Ansible variables dynamically
|
|
47
|
+
def _replace_node_attr(match):
|
|
48
|
+
cookbook = match.group(1)
|
|
49
|
+
attr = match.group(2)
|
|
50
|
+
|
|
51
|
+
# Convert cookbook name to readable format
|
|
52
|
+
def char_to_word(c: str) -> str:
|
|
53
|
+
number_words = {
|
|
54
|
+
"1": "one",
|
|
55
|
+
"2": "two",
|
|
56
|
+
"3": "three",
|
|
57
|
+
"4": "four",
|
|
58
|
+
"5": "five",
|
|
59
|
+
"6": "six",
|
|
60
|
+
"7": "seven",
|
|
61
|
+
"8": "eight",
|
|
62
|
+
"9": "nine",
|
|
63
|
+
"0": "zero",
|
|
64
|
+
}
|
|
65
|
+
return number_words.get(c, c)
|
|
66
|
+
|
|
67
|
+
# Replace non-alphanumeric characters with underscores
|
|
68
|
+
readable_cookbook = "".join(
|
|
69
|
+
char_to_word(c) if c.isdigit() else (c if c.isalnum() else "_")
|
|
70
|
+
for c in cookbook
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Ensure we don't have multiple consecutive underscores
|
|
74
|
+
readable_cookbook = re.sub(r"_+", "_", readable_cookbook)
|
|
75
|
+
# Remove leading/trailing underscores
|
|
76
|
+
readable_cookbook = readable_cookbook.strip("_")
|
|
77
|
+
|
|
78
|
+
return f"{{{{ {readable_cookbook}_{attr} }}}}"
|
|
79
|
+
|
|
80
|
+
value = re.sub(r"node\['(\w+)'\]\['(\w+)'\]", _replace_node_attr, value)
|
|
81
|
+
|
|
82
|
+
# Wrap in Jinja if it's a node reference
|
|
83
|
+
if "node[" in value:
|
|
84
|
+
return f"{{{{ {value} }}}}"
|
|
85
|
+
return value
|
|
42
86
|
|
|
43
87
|
|
|
44
88
|
def convert_resource_to_task(
|
|
@@ -188,6 +232,26 @@ def _get_remote_file_params(
|
|
|
188
232
|
return params
|
|
189
233
|
|
|
190
234
|
|
|
235
|
+
def _get_include_recipe_params(
|
|
236
|
+
resource_name: str, action: str, props: dict[str, Any]
|
|
237
|
+
) -> dict[str, Any]:
|
|
238
|
+
"""
|
|
239
|
+
Build parameters for include_recipe resources.
|
|
240
|
+
|
|
241
|
+
Uses cookbook-specific configurations when available.
|
|
242
|
+
Falls back to include_role for unknown cookbooks.
|
|
243
|
+
"""
|
|
244
|
+
cookbook_config = get_cookbook_package_config(resource_name)
|
|
245
|
+
if cookbook_config:
|
|
246
|
+
# Return a copy to prevent callers from mutating the shared mapping.
|
|
247
|
+
return dict(cookbook_config["params"])
|
|
248
|
+
|
|
249
|
+
# For unknown cookbooks, use include_role with the cookbook name
|
|
250
|
+
# Extract cookbook name from "cookbook::recipe" format
|
|
251
|
+
cookbook_name = resource_name.split("::")[0]
|
|
252
|
+
return {"name": cookbook_name}
|
|
253
|
+
|
|
254
|
+
|
|
191
255
|
def _get_default_params(resource_name: str, action: str) -> dict[str, Any]:
|
|
192
256
|
"""Build default parameters for unknown resource types."""
|
|
193
257
|
params = {"name": resource_name}
|
|
@@ -209,6 +273,7 @@ RESOURCE_PARAM_BUILDERS: dict[str, ParamBuilder | str] = {
|
|
|
209
273
|
"user": _get_user_group_params,
|
|
210
274
|
"group": _get_user_group_params,
|
|
211
275
|
"remote_file": _get_remote_file_params,
|
|
276
|
+
"include_recipe": _get_include_recipe_params,
|
|
212
277
|
}
|
|
213
278
|
|
|
214
279
|
|
|
@@ -229,7 +294,26 @@ def _convert_chef_resource_to_ansible(
|
|
|
229
294
|
|
|
230
295
|
"""
|
|
231
296
|
# Get Ansible module name
|
|
232
|
-
ansible_module = RESOURCE_MAPPINGS.get(resource_type
|
|
297
|
+
ansible_module = RESOURCE_MAPPINGS.get(resource_type)
|
|
298
|
+
|
|
299
|
+
# Check for cookbook-specific include_recipe configurations
|
|
300
|
+
if resource_type == "include_recipe":
|
|
301
|
+
cookbook_config = get_cookbook_package_config(resource_name)
|
|
302
|
+
if cookbook_config:
|
|
303
|
+
ansible_module = cookbook_config["module"]
|
|
304
|
+
else:
|
|
305
|
+
# For include_recipe without specific mapping, use include_role
|
|
306
|
+
ansible_module = "ansible.builtin.include_role"
|
|
307
|
+
|
|
308
|
+
# Handle unknown resource types
|
|
309
|
+
if ansible_module is None:
|
|
310
|
+
# Return a task with just a comment for unknown resources
|
|
311
|
+
return {
|
|
312
|
+
"name": f"Create {resource_type} {resource_name}",
|
|
313
|
+
"# Unknown": f"{resource_type}:",
|
|
314
|
+
"resource_name": resource_name,
|
|
315
|
+
"state": "present",
|
|
316
|
+
}
|
|
233
317
|
|
|
234
318
|
# Start building the task
|
|
235
319
|
task: dict[str, Any] = {
|
|
@@ -242,6 +326,12 @@ def _convert_chef_resource_to_ansible(
|
|
|
242
326
|
# Build module parameters using appropriate builder
|
|
243
327
|
module_params = _build_module_params(resource_type, resource_name, action, props)
|
|
244
328
|
|
|
329
|
+
# Override with cookbook-specific params for include_recipe
|
|
330
|
+
if resource_type == "include_recipe":
|
|
331
|
+
cookbook_config = get_cookbook_package_config(resource_name)
|
|
332
|
+
if cookbook_config:
|
|
333
|
+
module_params = cookbook_config["params"].copy()
|
|
334
|
+
|
|
245
335
|
# Add special task-level flags for execute/bash resources
|
|
246
336
|
if resource_type in ["execute", "bash"]:
|
|
247
337
|
task["changed_when"] = "false"
|
|
@@ -266,6 +356,13 @@ def _build_module_params(
|
|
|
266
356
|
Dictionary of Ansible module parameters.
|
|
267
357
|
|
|
268
358
|
"""
|
|
359
|
+
# First check for cookbook-specific resource types
|
|
360
|
+
cookbook_params = build_cookbook_resource_params(
|
|
361
|
+
resource_type, resource_name, action, props
|
|
362
|
+
)
|
|
363
|
+
if cookbook_params is not None:
|
|
364
|
+
return cookbook_params
|
|
365
|
+
|
|
269
366
|
# Look up the parameter builder for this resource type
|
|
270
367
|
builder = RESOURCE_PARAM_BUILDERS.get(resource_type)
|
|
271
368
|
|
|
@@ -297,6 +394,8 @@ def _format_dict_value(key: str, value: dict[str, Any]) -> list[str]:
|
|
|
297
394
|
"""Format a dictionary value for YAML output."""
|
|
298
395
|
lines = [f" {key}:"]
|
|
299
396
|
for param_key, param_value in value.items():
|
|
397
|
+
# Indent nested params by four spaces so downstream formatting nests
|
|
398
|
+
# module parameters under the module key.
|
|
300
399
|
lines.append(f" {param_key}: {_format_yaml_value(param_value)}")
|
|
301
400
|
return lines
|
|
302
401
|
|
|
@@ -317,6 +416,10 @@ def _format_ansible_task(task: dict[str, Any]) -> str:
|
|
|
317
416
|
for key, value in task.items():
|
|
318
417
|
if key == "name":
|
|
319
418
|
continue
|
|
419
|
+
if key == "# Unknown":
|
|
420
|
+
# Handle unknown resources with a comment
|
|
421
|
+
result.append(f" # {value}")
|
|
422
|
+
continue
|
|
320
423
|
if isinstance(value, dict):
|
|
321
424
|
result.extend(_format_dict_value(key, value))
|
|
322
425
|
else:
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Chef ERB template to Jinja2 converter."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from souschef.parsers.template import (
|
|
6
|
+
_convert_erb_to_jinja2,
|
|
7
|
+
_extract_template_variables,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def convert_template_file(erb_path: str) -> dict:
|
|
12
|
+
"""
|
|
13
|
+
Convert an ERB template file to Jinja2 format.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
erb_path: Path to the ERB template file.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Dictionary containing:
|
|
20
|
+
- success: bool, whether conversion succeeded
|
|
21
|
+
- original_file: str, path to original ERB file
|
|
22
|
+
- jinja2_file: str, suggested path for .j2 file
|
|
23
|
+
- jinja2_content: str, converted Jinja2 template content
|
|
24
|
+
- variables: list, variables found in template
|
|
25
|
+
- error: str (optional), error message if conversion failed
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
file_path = Path(erb_path).resolve()
|
|
30
|
+
|
|
31
|
+
if not file_path.exists():
|
|
32
|
+
return {
|
|
33
|
+
"success": False,
|
|
34
|
+
"error": f"File not found: {erb_path}",
|
|
35
|
+
"original_file": erb_path,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if not file_path.is_file():
|
|
39
|
+
return {
|
|
40
|
+
"success": False,
|
|
41
|
+
"error": f"Path is not a file: {erb_path}",
|
|
42
|
+
"original_file": erb_path,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Read ERB template
|
|
46
|
+
try:
|
|
47
|
+
content = file_path.read_text(encoding="utf-8")
|
|
48
|
+
except UnicodeDecodeError:
|
|
49
|
+
return {
|
|
50
|
+
"success": False,
|
|
51
|
+
"error": f"Unable to decode {erb_path} as UTF-8 text",
|
|
52
|
+
"original_file": str(file_path),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Extract variables
|
|
56
|
+
variables = _extract_template_variables(content)
|
|
57
|
+
|
|
58
|
+
# Convert ERB to Jinja2
|
|
59
|
+
jinja2_content = _convert_erb_to_jinja2(content)
|
|
60
|
+
|
|
61
|
+
# Determine output file name
|
|
62
|
+
jinja2_file = str(file_path).replace(".erb", ".j2")
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
"success": True,
|
|
66
|
+
"original_file": str(file_path),
|
|
67
|
+
"jinja2_file": jinja2_file,
|
|
68
|
+
"jinja2_content": jinja2_content,
|
|
69
|
+
"variables": sorted(variables),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
except Exception as e:
|
|
73
|
+
return {
|
|
74
|
+
"success": False,
|
|
75
|
+
"error": f"Conversion failed: {e}",
|
|
76
|
+
"original_file": erb_path,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def convert_cookbook_templates(cookbook_path: str) -> dict:
|
|
81
|
+
"""
|
|
82
|
+
Convert all ERB templates in a cookbook to Jinja2.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
cookbook_path: Path to the cookbook directory.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Dictionary containing:
|
|
89
|
+
- success: bool, whether all conversions succeeded
|
|
90
|
+
- templates_converted: int, number of templates successfully converted
|
|
91
|
+
- templates_failed: int, number of templates that failed conversion
|
|
92
|
+
- results: list of dict, individual template conversion results
|
|
93
|
+
- error: str (optional), error message if cookbook not found
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
cookbook_dir = Path(cookbook_path).resolve()
|
|
98
|
+
|
|
99
|
+
if not cookbook_dir.exists():
|
|
100
|
+
return {
|
|
101
|
+
"success": False,
|
|
102
|
+
"error": f"Cookbook directory not found: {cookbook_path}",
|
|
103
|
+
"templates_converted": 0,
|
|
104
|
+
"templates_failed": 0,
|
|
105
|
+
"results": [],
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Find all .erb files in the cookbook
|
|
109
|
+
erb_files = list(cookbook_dir.glob("**/*.erb"))
|
|
110
|
+
|
|
111
|
+
if not erb_files:
|
|
112
|
+
return {
|
|
113
|
+
"success": True,
|
|
114
|
+
"templates_converted": 0,
|
|
115
|
+
"templates_failed": 0,
|
|
116
|
+
"results": [],
|
|
117
|
+
"message": "No ERB templates found in cookbook",
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
results = []
|
|
121
|
+
templates_converted = 0
|
|
122
|
+
templates_failed = 0
|
|
123
|
+
|
|
124
|
+
for erb_file in erb_files:
|
|
125
|
+
result = convert_template_file(str(erb_file))
|
|
126
|
+
results.append(result)
|
|
127
|
+
|
|
128
|
+
if result["success"]:
|
|
129
|
+
templates_converted += 1
|
|
130
|
+
else:
|
|
131
|
+
templates_failed += 1
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
"success": templates_failed == 0,
|
|
135
|
+
"templates_converted": templates_converted,
|
|
136
|
+
"templates_failed": templates_failed,
|
|
137
|
+
"results": results,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
return {
|
|
142
|
+
"success": False,
|
|
143
|
+
"error": f"Failed to convert cookbook templates: {e}",
|
|
144
|
+
"templates_converted": 0,
|
|
145
|
+
"templates_failed": 0,
|
|
146
|
+
"results": [],
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def convert_template_with_ai(erb_path: str, ai_service=None) -> dict:
|
|
151
|
+
"""
|
|
152
|
+
Convert an ERB template to Jinja2 using AI assistance for complex conversions.
|
|
153
|
+
|
|
154
|
+
This function first attempts rule-based conversion, then optionally uses AI
|
|
155
|
+
for validation or complex Ruby logic that can't be automatically converted.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
erb_path: Path to the ERB template file.
|
|
159
|
+
ai_service: Optional AI service instance for complex conversions.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Dictionary with conversion results (same format as convert_template_file).
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
# Start with rule-based conversion
|
|
166
|
+
result = convert_template_file(erb_path)
|
|
167
|
+
|
|
168
|
+
# Add conversion method metadata
|
|
169
|
+
result["conversion_method"] = "rule-based"
|
|
170
|
+
|
|
171
|
+
# Future enhancement: Use AI service to validate/improve complex conversions
|
|
172
|
+
if ai_service is not None:
|
|
173
|
+
# AI validation/improvement logic deferred to future enhancement
|
|
174
|
+
# when AI integration becomes more critical to the template conversion process
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
return result
|
souschef/core/constants.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"""Constants used throughout SousChef."""
|
|
2
2
|
|
|
3
3
|
__all__ = [
|
|
4
|
+
"VERSION",
|
|
4
5
|
"ANSIBLE_SERVICE_MODULE",
|
|
5
6
|
"METADATA_FILENAME",
|
|
6
7
|
"ERROR_PREFIX",
|
|
7
8
|
"NODE_PREFIX",
|
|
9
|
+
"ATTRIBUTE_PREFIX",
|
|
10
|
+
"VALUE_PREFIX",
|
|
8
11
|
"CHEF_RECIPE_PREFIX",
|
|
9
12
|
"CHEF_ROLE_PREFIX",
|
|
10
13
|
"REGEX_WHITESPACE_QUOTE",
|
|
@@ -39,6 +42,9 @@ __all__ = [
|
|
|
39
42
|
"ACTION_TO_STATE",
|
|
40
43
|
]
|
|
41
44
|
|
|
45
|
+
# Version
|
|
46
|
+
VERSION = "2.6.0"
|
|
47
|
+
|
|
42
48
|
# Ansible module names
|
|
43
49
|
ANSIBLE_SERVICE_MODULE = "ansible.builtin.service"
|
|
44
50
|
|
|
@@ -48,6 +54,8 @@ METADATA_FILENAME = "metadata.rb"
|
|
|
48
54
|
# Common prefixes
|
|
49
55
|
ERROR_PREFIX = "Error:"
|
|
50
56
|
NODE_PREFIX = "node["
|
|
57
|
+
ATTRIBUTE_PREFIX = "Attribute: "
|
|
58
|
+
VALUE_PREFIX = "Value: "
|
|
51
59
|
CHEF_RECIPE_PREFIX = "recipe["
|
|
52
60
|
CHEF_ROLE_PREFIX = "role["
|
|
53
61
|
|
|
@@ -126,6 +134,11 @@ RESOURCE_MAPPINGS = {
|
|
|
126
134
|
"mount": "ansible.builtin.mount",
|
|
127
135
|
"git": "ansible.builtin.git",
|
|
128
136
|
"remote_file": "ansible.builtin.get_url",
|
|
137
|
+
"nodejs_npm": "community.general.npm",
|
|
138
|
+
# Map include_recipe to a generic package install so well-known recipes
|
|
139
|
+
# (like nodejs) can be materialised into concrete tasks instead of
|
|
140
|
+
# producing stubbed import_role calls.
|
|
141
|
+
"include_recipe": "ansible.builtin.package",
|
|
129
142
|
}
|
|
130
143
|
|
|
131
144
|
# Chef action to Ansible state mappings
|