mcp-souschef 2.2.0__py3-none-any.whl → 2.8.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.2.0.dist-info → mcp_souschef-2.8.0.dist-info}/METADATA +226 -38
- mcp_souschef-2.8.0.dist-info/RECORD +42 -0
- mcp_souschef-2.8.0.dist-info/entry_points.txt +4 -0
- souschef/__init__.py +10 -2
- souschef/assessment.py +113 -30
- souschef/ci/__init__.py +11 -0
- souschef/ci/github_actions.py +379 -0
- souschef/ci/gitlab_ci.py +299 -0
- souschef/ci/jenkins_pipeline.py +343 -0
- souschef/cli.py +605 -5
- 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 +853 -15
- souschef/converters/resource.py +103 -1
- souschef/core/constants.py +13 -0
- souschef/core/path_utils.py +12 -9
- souschef/core/validation.py +35 -2
- souschef/deployment.py +29 -27
- souschef/filesystem/operations.py +0 -7
- souschef/parsers/__init__.py +6 -1
- souschef/parsers/attributes.py +397 -32
- souschef/parsers/inspec.py +343 -18
- souschef/parsers/metadata.py +30 -0
- souschef/parsers/recipe.py +48 -10
- souschef/server.py +429 -178
- souschef/ui/__init__.py +8 -0
- souschef/ui/app.py +2998 -0
- souschef/ui/health_check.py +36 -0
- souschef/ui/pages/ai_settings.py +497 -0
- souschef/ui/pages/cookbook_analysis.py +1360 -0
- mcp_souschef-2.2.0.dist-info/RECORD +0 -31
- mcp_souschef-2.2.0.dist-info/entry_points.txt +0 -4
- {mcp_souschef-2.2.0.dist-info → mcp_souschef-2.8.0.dist-info}/WHEEL +0 -0
- {mcp_souschef-2.2.0.dist-info → mcp_souschef-2.8.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
|
|
@@ -41,6 +46,51 @@ def _parse_properties(properties_str: str) -> dict[str, Any]:
|
|
|
41
46
|
return {}
|
|
42
47
|
|
|
43
48
|
|
|
49
|
+
def _normalize_template_value(value: Any) -> Any:
|
|
50
|
+
"""Normalize Ruby-style attribute references into Jinja templates."""
|
|
51
|
+
if isinstance(value, str):
|
|
52
|
+
# Convert Chef node attributes to Ansible variables dynamically
|
|
53
|
+
def _replace_node_attr(match):
|
|
54
|
+
cookbook = match.group(1)
|
|
55
|
+
attr = match.group(2)
|
|
56
|
+
|
|
57
|
+
# Convert cookbook name to readable format
|
|
58
|
+
def char_to_word(c: str) -> str:
|
|
59
|
+
number_words = {
|
|
60
|
+
"1": "one",
|
|
61
|
+
"2": "two",
|
|
62
|
+
"3": "three",
|
|
63
|
+
"4": "four",
|
|
64
|
+
"5": "five",
|
|
65
|
+
"6": "six",
|
|
66
|
+
"7": "seven",
|
|
67
|
+
"8": "eight",
|
|
68
|
+
"9": "nine",
|
|
69
|
+
"0": "zero",
|
|
70
|
+
}
|
|
71
|
+
return number_words.get(c, c)
|
|
72
|
+
|
|
73
|
+
# Replace non-alphanumeric characters with underscores
|
|
74
|
+
readable_cookbook = "".join(
|
|
75
|
+
char_to_word(c) if c.isdigit() else (c if c.isalnum() else "_")
|
|
76
|
+
for c in cookbook
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Ensure we don't have multiple consecutive underscores
|
|
80
|
+
readable_cookbook = re.sub(r"_+", "_", readable_cookbook)
|
|
81
|
+
# Remove leading/trailing underscores
|
|
82
|
+
readable_cookbook = readable_cookbook.strip("_")
|
|
83
|
+
|
|
84
|
+
return f"{{{{ {readable_cookbook}_{attr} }}}}"
|
|
85
|
+
|
|
86
|
+
value = re.sub(r"node\['(\w+)'\]\['(\w+)'\]", _replace_node_attr, value)
|
|
87
|
+
|
|
88
|
+
# Wrap in Jinja if it's a node reference
|
|
89
|
+
if "node[" in value:
|
|
90
|
+
return f"{{{{ {value} }}}}"
|
|
91
|
+
return value
|
|
92
|
+
|
|
93
|
+
|
|
44
94
|
def convert_resource_to_task(
|
|
45
95
|
resource_type: str, resource_name: str, action: str = "create", properties: str = ""
|
|
46
96
|
) -> str:
|
|
@@ -188,6 +238,22 @@ def _get_remote_file_params(
|
|
|
188
238
|
return params
|
|
189
239
|
|
|
190
240
|
|
|
241
|
+
def _get_include_recipe_params(
|
|
242
|
+
resource_name: str, action: str, props: dict[str, Any]
|
|
243
|
+
) -> dict[str, Any]:
|
|
244
|
+
"""
|
|
245
|
+
Build parameters for include_recipe resources.
|
|
246
|
+
|
|
247
|
+
Uses cookbook-specific configurations when available.
|
|
248
|
+
"""
|
|
249
|
+
cookbook_config = get_cookbook_package_config(resource_name)
|
|
250
|
+
if cookbook_config:
|
|
251
|
+
# Return a copy to prevent callers from mutating the shared mapping.
|
|
252
|
+
return dict(cookbook_config["params"])
|
|
253
|
+
# Default behavior for recipes without a specific mapping.
|
|
254
|
+
return {"name": resource_name, "state": "present"}
|
|
255
|
+
|
|
256
|
+
|
|
191
257
|
def _get_default_params(resource_name: str, action: str) -> dict[str, Any]:
|
|
192
258
|
"""Build default parameters for unknown resource types."""
|
|
193
259
|
params = {"name": resource_name}
|
|
@@ -209,6 +275,7 @@ RESOURCE_PARAM_BUILDERS: dict[str, ParamBuilder | str] = {
|
|
|
209
275
|
"user": _get_user_group_params,
|
|
210
276
|
"group": _get_user_group_params,
|
|
211
277
|
"remote_file": _get_remote_file_params,
|
|
278
|
+
"include_recipe": _get_include_recipe_params,
|
|
212
279
|
}
|
|
213
280
|
|
|
214
281
|
|
|
@@ -229,7 +296,23 @@ def _convert_chef_resource_to_ansible(
|
|
|
229
296
|
|
|
230
297
|
"""
|
|
231
298
|
# Get Ansible module name
|
|
232
|
-
ansible_module = RESOURCE_MAPPINGS.get(resource_type
|
|
299
|
+
ansible_module = RESOURCE_MAPPINGS.get(resource_type)
|
|
300
|
+
|
|
301
|
+
# Check for cookbook-specific include_recipe configurations
|
|
302
|
+
if resource_type == "include_recipe":
|
|
303
|
+
cookbook_config = get_cookbook_package_config(resource_name)
|
|
304
|
+
if cookbook_config:
|
|
305
|
+
ansible_module = cookbook_config["module"]
|
|
306
|
+
|
|
307
|
+
# Handle unknown resource types
|
|
308
|
+
if ansible_module is None:
|
|
309
|
+
# Return a task with just a comment for unknown resources
|
|
310
|
+
return {
|
|
311
|
+
"name": f"Create {resource_type} {resource_name}",
|
|
312
|
+
"# Unknown": f"{resource_type}:",
|
|
313
|
+
"resource_name": resource_name,
|
|
314
|
+
"state": "present",
|
|
315
|
+
}
|
|
233
316
|
|
|
234
317
|
# Start building the task
|
|
235
318
|
task: dict[str, Any] = {
|
|
@@ -242,6 +325,12 @@ def _convert_chef_resource_to_ansible(
|
|
|
242
325
|
# Build module parameters using appropriate builder
|
|
243
326
|
module_params = _build_module_params(resource_type, resource_name, action, props)
|
|
244
327
|
|
|
328
|
+
# Override with cookbook-specific params for include_recipe
|
|
329
|
+
if resource_type == "include_recipe":
|
|
330
|
+
cookbook_config = get_cookbook_package_config(resource_name)
|
|
331
|
+
if cookbook_config:
|
|
332
|
+
module_params = cookbook_config["params"].copy()
|
|
333
|
+
|
|
245
334
|
# Add special task-level flags for execute/bash resources
|
|
246
335
|
if resource_type in ["execute", "bash"]:
|
|
247
336
|
task["changed_when"] = "false"
|
|
@@ -266,6 +355,13 @@ def _build_module_params(
|
|
|
266
355
|
Dictionary of Ansible module parameters.
|
|
267
356
|
|
|
268
357
|
"""
|
|
358
|
+
# First check for cookbook-specific resource types
|
|
359
|
+
cookbook_params = build_cookbook_resource_params(
|
|
360
|
+
resource_type, resource_name, action, props
|
|
361
|
+
)
|
|
362
|
+
if cookbook_params is not None:
|
|
363
|
+
return cookbook_params
|
|
364
|
+
|
|
269
365
|
# Look up the parameter builder for this resource type
|
|
270
366
|
builder = RESOURCE_PARAM_BUILDERS.get(resource_type)
|
|
271
367
|
|
|
@@ -297,6 +393,8 @@ def _format_dict_value(key: str, value: dict[str, Any]) -> list[str]:
|
|
|
297
393
|
"""Format a dictionary value for YAML output."""
|
|
298
394
|
lines = [f" {key}:"]
|
|
299
395
|
for param_key, param_value in value.items():
|
|
396
|
+
# Indent nested params by four spaces so downstream formatting nests
|
|
397
|
+
# module parameters under the module key.
|
|
300
398
|
lines.append(f" {param_key}: {_format_yaml_value(param_value)}")
|
|
301
399
|
return lines
|
|
302
400
|
|
|
@@ -317,6 +415,10 @@ def _format_ansible_task(task: dict[str, Any]) -> str:
|
|
|
317
415
|
for key, value in task.items():
|
|
318
416
|
if key == "name":
|
|
319
417
|
continue
|
|
418
|
+
if key == "# Unknown":
|
|
419
|
+
# Handle unknown resources with a comment
|
|
420
|
+
result.append(f" # {value}")
|
|
421
|
+
continue
|
|
320
422
|
if isinstance(value, dict):
|
|
321
423
|
result.extend(_format_dict_value(key, value))
|
|
322
424
|
else:
|
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
|
souschef/core/path_utils.py
CHANGED
|
@@ -7,10 +7,8 @@ def _normalize_path(path_str: str) -> Path:
|
|
|
7
7
|
"""
|
|
8
8
|
Normalize a file path for safe filesystem operations.
|
|
9
9
|
|
|
10
|
-
This function resolves relative paths and symlinks
|
|
11
|
-
preventing path traversal attacks (CWE-23).
|
|
12
|
-
intentionally allows full filesystem access as it runs in the user's
|
|
13
|
-
local environment with their permissions.
|
|
10
|
+
This function validates input and resolves relative paths and symlinks
|
|
11
|
+
to absolute paths, preventing path traversal attacks (CWE-23).
|
|
14
12
|
|
|
15
13
|
Args:
|
|
16
14
|
path_str: Path string to normalize.
|
|
@@ -19,17 +17,22 @@ def _normalize_path(path_str: str) -> Path:
|
|
|
19
17
|
Resolved absolute Path object.
|
|
20
18
|
|
|
21
19
|
Raises:
|
|
22
|
-
ValueError: If the path contains null bytes or is invalid.
|
|
20
|
+
ValueError: If the path contains null bytes, traversal attempts, or is invalid.
|
|
23
21
|
|
|
24
22
|
"""
|
|
23
|
+
if not isinstance(path_str, str):
|
|
24
|
+
raise ValueError(f"Path must be a string, got {type(path_str)}")
|
|
25
|
+
|
|
26
|
+
# Reject paths with null bytes
|
|
25
27
|
if "\x00" in path_str:
|
|
26
28
|
raise ValueError(f"Path contains null bytes: {path_str!r}")
|
|
27
29
|
|
|
30
|
+
# Reject paths with obvious directory traversal attempts
|
|
31
|
+
if ".." in path_str:
|
|
32
|
+
raise ValueError(f"Path contains directory traversal: {path_str!r}")
|
|
33
|
+
|
|
28
34
|
try:
|
|
29
|
-
# Resolve to absolute path, removing
|
|
30
|
-
# This is the path normalization function itself that validates input
|
|
31
|
-
# lgtm[py/path-injection]
|
|
32
|
-
# codeql[py/path-injection]
|
|
35
|
+
# Resolve to absolute path, removing ., and resolving symlinks
|
|
33
36
|
return Path(path_str).resolve()
|
|
34
37
|
except (OSError, RuntimeError) as e:
|
|
35
38
|
raise ValueError(f"Invalid path {path_str}: {e}") from e
|
souschef/core/validation.py
CHANGED
|
@@ -234,10 +234,43 @@ class ValidationEngine:
|
|
|
234
234
|
if "import pytest" in result:
|
|
235
235
|
# Testinfra format
|
|
236
236
|
self._validate_python_syntax(result)
|
|
237
|
-
elif "
|
|
238
|
-
#
|
|
237
|
+
elif "require 'serverspec'" in result:
|
|
238
|
+
# ServerSpec format (Ruby)
|
|
239
|
+
self._validate_ruby_syntax(result)
|
|
240
|
+
elif "---" in result or ("package:" in result and "service:" in result):
|
|
241
|
+
# Ansible assert or Goss YAML format
|
|
239
242
|
self._validate_yaml_syntax(result)
|
|
240
243
|
|
|
244
|
+
def _validate_ruby_syntax(self, ruby_content: str) -> None:
|
|
245
|
+
"""
|
|
246
|
+
Validate Ruby syntax.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
ruby_content: Ruby content to validate.
|
|
250
|
+
|
|
251
|
+
"""
|
|
252
|
+
# Basic Ruby syntax checks
|
|
253
|
+
if not ruby_content.strip():
|
|
254
|
+
self._add_result(
|
|
255
|
+
ValidationLevel.ERROR,
|
|
256
|
+
ValidationCategory.SYNTAX,
|
|
257
|
+
"Empty Ruby content",
|
|
258
|
+
suggestion="Ensure the conversion produced valid Ruby code",
|
|
259
|
+
)
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
# Check for balanced blocks (describe/do/end)
|
|
263
|
+
do_count = len(re.findall(r"\bdo\b", ruby_content))
|
|
264
|
+
end_count = len(re.findall(r"\bend\b", ruby_content))
|
|
265
|
+
|
|
266
|
+
if do_count != end_count:
|
|
267
|
+
self._add_result(
|
|
268
|
+
ValidationLevel.ERROR,
|
|
269
|
+
ValidationCategory.SYNTAX,
|
|
270
|
+
f"Unbalanced Ruby blocks: {do_count} 'do' but {end_count} 'end'",
|
|
271
|
+
suggestion="Check that all 'do' blocks have matching 'end' keywords",
|
|
272
|
+
)
|
|
273
|
+
|
|
241
274
|
def _validate_yaml_syntax(self, yaml_content: str) -> None:
|
|
242
275
|
"""
|
|
243
276
|
Validate YAML syntax.
|
souschef/deployment.py
CHANGED
|
@@ -51,7 +51,7 @@ def generate_awx_job_template_from_cookbook(
|
|
|
51
51
|
)
|
|
52
52
|
|
|
53
53
|
cookbook = validate_cookbook_structure(cookbook_path)
|
|
54
|
-
cookbook_analysis =
|
|
54
|
+
cookbook_analysis = _analyse_cookbook_for_awx(cookbook, cookbook_name)
|
|
55
55
|
job_template = _generate_awx_job_template(
|
|
56
56
|
cookbook_analysis, cookbook_name, target_environment, include_survey
|
|
57
57
|
)
|
|
@@ -184,7 +184,7 @@ def generate_awx_project_from_cookbooks(
|
|
|
184
184
|
)
|
|
185
185
|
|
|
186
186
|
# Analyze all cookbooks
|
|
187
|
-
cookbooks_analysis =
|
|
187
|
+
cookbooks_analysis = _analyse_cookbooks_directory(cookbooks_path)
|
|
188
188
|
|
|
189
189
|
# Generate project structure
|
|
190
190
|
project_config = _generate_awx_project_config(project_name, scm_type, scm_url)
|
|
@@ -329,7 +329,7 @@ def convert_chef_deployment_to_ansible_strategy(
|
|
|
329
329
|
)
|
|
330
330
|
|
|
331
331
|
# Analyze Chef deployment pattern
|
|
332
|
-
pattern_analysis =
|
|
332
|
+
pattern_analysis = _analyse_chef_deployment_pattern(cookbook)
|
|
333
333
|
|
|
334
334
|
# Determine best strategy if auto-detect
|
|
335
335
|
if deployment_pattern == "auto":
|
|
@@ -474,14 +474,16 @@ def _validate_canary_inputs(
|
|
|
474
474
|
raise ValueError("Steps must be between 1 and 100")
|
|
475
475
|
if steps != sorted(steps):
|
|
476
476
|
return None, (
|
|
477
|
-
|
|
477
|
+
"Error: Rollout steps must be in ascending order: "
|
|
478
|
+
f"{rollout_steps}\n\n"
|
|
478
479
|
"Suggestion: Use format like '10,25,50,100'"
|
|
479
480
|
)
|
|
480
481
|
return steps, None
|
|
481
482
|
except ValueError as e:
|
|
482
|
-
return
|
|
483
|
+
return (
|
|
484
|
+
None,
|
|
483
485
|
f"Error: Invalid rollout steps '{rollout_steps}': {e}\n\n"
|
|
484
|
-
"Suggestion: Use comma-separated percentages like '10,25,50,100'"
|
|
486
|
+
"Suggestion: Use comma-separated percentages like '10,25,50,100'",
|
|
485
487
|
)
|
|
486
488
|
|
|
487
489
|
|
|
@@ -624,11 +626,11 @@ def generate_canary_deployment_strategy(
|
|
|
624
626
|
)
|
|
625
627
|
|
|
626
628
|
|
|
627
|
-
def
|
|
629
|
+
def analyse_chef_application_patterns(
|
|
628
630
|
cookbook_path: str, application_type: str = "web_application"
|
|
629
631
|
) -> str:
|
|
630
632
|
"""
|
|
631
|
-
|
|
633
|
+
Analyse cookbook deployment patterns and recommend Ansible strategies.
|
|
632
634
|
|
|
633
635
|
Detects blue/green, canary, rolling, or custom deployment approaches.
|
|
634
636
|
Application type helps tune recommendations for web/database/service workloads.
|
|
@@ -645,7 +647,7 @@ def analyze_chef_application_patterns(
|
|
|
645
647
|
)
|
|
646
648
|
|
|
647
649
|
# Analyze cookbook for application patterns
|
|
648
|
-
analysis =
|
|
650
|
+
analysis = _analyse_application_cookbook(cookbook, application_type)
|
|
649
651
|
|
|
650
652
|
return f"""# Chef Application Patterns Analysis
|
|
651
653
|
# Cookbook: {cookbook.name}
|
|
@@ -683,9 +685,9 @@ def analyze_chef_application_patterns(
|
|
|
683
685
|
# AWX Helper Functions
|
|
684
686
|
|
|
685
687
|
|
|
686
|
-
def
|
|
688
|
+
def _analyse_recipes(cookbook_path: Path) -> list[dict[str, Any]]:
|
|
687
689
|
"""
|
|
688
|
-
|
|
690
|
+
Analyse recipes directory for AWX job steps.
|
|
689
691
|
|
|
690
692
|
Args:
|
|
691
693
|
cookbook_path: Path to cookbook root
|
|
@@ -708,11 +710,11 @@ def _analyze_recipes(cookbook_path: Path) -> list[dict[str, Any]]:
|
|
|
708
710
|
return recipes
|
|
709
711
|
|
|
710
712
|
|
|
711
|
-
def
|
|
713
|
+
def _analyse_attributes_for_survey(
|
|
712
714
|
cookbook_path: Path,
|
|
713
715
|
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
714
716
|
"""
|
|
715
|
-
|
|
717
|
+
Analyse attributes directory for survey field generation.
|
|
716
718
|
|
|
717
719
|
Args:
|
|
718
720
|
cookbook_path: Path to cookbook root
|
|
@@ -746,7 +748,7 @@ def _analyze_attributes_for_survey(
|
|
|
746
748
|
return attributes, survey_fields
|
|
747
749
|
|
|
748
750
|
|
|
749
|
-
def
|
|
751
|
+
def _analyse_metadata_dependencies(cookbook_path: Path) -> list[str]:
|
|
750
752
|
"""
|
|
751
753
|
Extract cookbook dependencies from metadata.
|
|
752
754
|
|
|
@@ -793,9 +795,9 @@ def _collect_static_files(cookbook_path: Path) -> tuple[list[str], list[str]]:
|
|
|
793
795
|
return templates, files
|
|
794
796
|
|
|
795
797
|
|
|
796
|
-
def
|
|
798
|
+
def _analyse_cookbook_for_awx(cookbook_path: Path, cookbook_name: str) -> dict:
|
|
797
799
|
"""
|
|
798
|
-
|
|
800
|
+
Analyse Chef cookbook structure for AWX job template generation.
|
|
799
801
|
|
|
800
802
|
Orchestrates multiple analysis helpers to build comprehensive cookbook metadata.
|
|
801
803
|
|
|
@@ -808,9 +810,9 @@ def _analyze_cookbook_for_awx(cookbook_path: Path, cookbook_name: str) -> dict:
|
|
|
808
810
|
|
|
809
811
|
"""
|
|
810
812
|
# Analyze each dimension independently
|
|
811
|
-
recipes =
|
|
812
|
-
attributes, survey_fields =
|
|
813
|
-
dependencies =
|
|
813
|
+
recipes = _analyse_recipes(cookbook_path)
|
|
814
|
+
attributes, survey_fields = _analyse_attributes_for_survey(cookbook_path)
|
|
815
|
+
dependencies = _analyse_metadata_dependencies(cookbook_path)
|
|
814
816
|
templates, files = _collect_static_files(cookbook_path)
|
|
815
817
|
|
|
816
818
|
# Assemble complete analysis
|
|
@@ -1153,8 +1155,8 @@ def _generate_survey_fields_from_attributes(attributes: dict) -> list:
|
|
|
1153
1155
|
return survey_fields
|
|
1154
1156
|
|
|
1155
1157
|
|
|
1156
|
-
def
|
|
1157
|
-
"""
|
|
1158
|
+
def _analyse_cookbooks_directory(cookbooks_path: Path) -> dict:
|
|
1159
|
+
"""Analyse entire cookbooks directory structure."""
|
|
1158
1160
|
analysis: dict[str, Any] = {
|
|
1159
1161
|
"total_cookbooks": 0,
|
|
1160
1162
|
"cookbooks": {},
|
|
@@ -1170,7 +1172,7 @@ def _analyze_cookbooks_directory(cookbooks_path: Path) -> dict:
|
|
|
1170
1172
|
cookbook_name = cookbook_dir.name
|
|
1171
1173
|
analysis["total_cookbooks"] += 1
|
|
1172
1174
|
|
|
1173
|
-
cookbook_analysis =
|
|
1175
|
+
cookbook_analysis = _analyse_cookbook_for_awx(cookbook_dir, cookbook_name)
|
|
1174
1176
|
analysis["cookbooks"][cookbook_name] = cookbook_analysis
|
|
1175
1177
|
|
|
1176
1178
|
# Aggregate stats
|
|
@@ -1184,8 +1186,8 @@ def _analyze_cookbooks_directory(cookbooks_path: Path) -> dict:
|
|
|
1184
1186
|
# Deployment Strategy Helper Functions
|
|
1185
1187
|
|
|
1186
1188
|
|
|
1187
|
-
def
|
|
1188
|
-
"""
|
|
1189
|
+
def _analyse_chef_deployment_pattern(cookbook_path: Path) -> dict:
|
|
1190
|
+
"""Analyse Chef cookbook for deployment patterns."""
|
|
1189
1191
|
analysis: dict[str, Any] = {
|
|
1190
1192
|
"deployment_steps": [],
|
|
1191
1193
|
"health_checks": [],
|
|
@@ -1503,8 +1505,8 @@ def _assess_complexity_from_resource_count(resource_count: int) -> tuple[str, st
|
|
|
1503
1505
|
return "medium", "2-3 weeks", "medium"
|
|
1504
1506
|
|
|
1505
1507
|
|
|
1506
|
-
def
|
|
1507
|
-
"""
|
|
1508
|
+
def _analyse_application_cookbook(cookbook_path: Path, app_type: str) -> dict:
|
|
1509
|
+
"""Analyse Chef cookbook for application deployment patterns."""
|
|
1508
1510
|
analysis: dict[str, Any] = {
|
|
1509
1511
|
"application_type": app_type,
|
|
1510
1512
|
"deployment_patterns": [],
|
|
@@ -1648,7 +1650,7 @@ def _format_deployment_patterns(analysis: dict) -> str:
|
|
|
1648
1650
|
|
|
1649
1651
|
def _format_chef_resources_analysis(analysis: dict) -> str:
|
|
1650
1652
|
"""Format Chef resources analysis."""
|
|
1651
|
-
# Check for new format first (from
|
|
1653
|
+
# Check for new format first (from _analyse_application_cookbook)
|
|
1652
1654
|
resources = analysis.get("resources", [])
|
|
1653
1655
|
if resources:
|
|
1654
1656
|
# Count resource types
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"""Filesystem operations for Chef cookbook exploration."""
|
|
2
2
|
|
|
3
|
-
from mcp.server.fastmcp import FastMCP
|
|
4
|
-
|
|
5
3
|
from souschef.core.constants import (
|
|
6
4
|
ERROR_FILE_NOT_FOUND,
|
|
7
5
|
ERROR_IS_DIRECTORY,
|
|
@@ -9,11 +7,7 @@ from souschef.core.constants import (
|
|
|
9
7
|
)
|
|
10
8
|
from souschef.core.path_utils import _normalize_path
|
|
11
9
|
|
|
12
|
-
# Get MCP instance to register tools
|
|
13
|
-
mcp = FastMCP("souschef")
|
|
14
|
-
|
|
15
10
|
|
|
16
|
-
@mcp.tool()
|
|
17
11
|
def list_directory(path: str) -> list[str] | str:
|
|
18
12
|
"""
|
|
19
13
|
List the contents of a directory.
|
|
@@ -40,7 +34,6 @@ def list_directory(path: str) -> list[str] | str:
|
|
|
40
34
|
return f"An error occurred: {e}"
|
|
41
35
|
|
|
42
36
|
|
|
43
|
-
@mcp.tool()
|
|
44
37
|
def read_file(path: str) -> str:
|
|
45
38
|
"""
|
|
46
39
|
Read the contents of a file.
|
souschef/parsers/__init__.py
CHANGED
|
@@ -13,7 +13,11 @@ from souschef.parsers.inspec import (
|
|
|
13
13
|
generate_inspec_from_chef,
|
|
14
14
|
parse_inspec_profile,
|
|
15
15
|
)
|
|
16
|
-
from souschef.parsers.metadata import
|
|
16
|
+
from souschef.parsers.metadata import (
|
|
17
|
+
list_cookbook_structure,
|
|
18
|
+
parse_cookbook_metadata,
|
|
19
|
+
read_cookbook_metadata,
|
|
20
|
+
)
|
|
17
21
|
from souschef.parsers.recipe import parse_recipe
|
|
18
22
|
from souschef.parsers.resource import parse_custom_resource
|
|
19
23
|
from souschef.parsers.template import parse_template
|
|
@@ -24,6 +28,7 @@ __all__ = [
|
|
|
24
28
|
"parse_attributes",
|
|
25
29
|
"parse_custom_resource",
|
|
26
30
|
"read_cookbook_metadata",
|
|
31
|
+
"parse_cookbook_metadata",
|
|
27
32
|
"list_cookbook_structure",
|
|
28
33
|
"parse_inspec_profile",
|
|
29
34
|
"convert_inspec_to_test",
|