mcp-souschef 2.1.2__py3-none-any.whl → 2.2.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.1.2.dist-info → mcp_souschef-2.2.0.dist-info}/METADATA +36 -8
- mcp_souschef-2.2.0.dist-info/RECORD +31 -0
- souschef/assessment.py +448 -180
- souschef/cli.py +90 -0
- souschef/converters/playbook.py +43 -5
- souschef/converters/resource.py +146 -49
- souschef/core/__init__.py +22 -0
- souschef/core/errors.py +275 -0
- souschef/deployment.py +412 -100
- souschef/parsers/habitat.py +35 -6
- souschef/parsers/inspec.py +72 -34
- souschef/parsers/metadata.py +59 -23
- souschef/profiling.py +568 -0
- souschef/server.py +589 -149
- mcp_souschef-2.1.2.dist-info/RECORD +0 -29
- {mcp_souschef-2.1.2.dist-info → mcp_souschef-2.2.0.dist-info}/WHEEL +0 -0
- {mcp_souschef-2.1.2.dist-info → mcp_souschef-2.2.0.dist-info}/entry_points.txt +0 -0
- {mcp_souschef-2.1.2.dist-info → mcp_souschef-2.2.0.dist-info}/licenses/LICENSE +0 -0
souschef/cli.py
CHANGED
|
@@ -11,6 +11,10 @@ from typing import NoReturn
|
|
|
11
11
|
|
|
12
12
|
import click
|
|
13
13
|
|
|
14
|
+
from souschef.profiling import (
|
|
15
|
+
generate_cookbook_performance_report,
|
|
16
|
+
profile_function,
|
|
17
|
+
)
|
|
14
18
|
from souschef.server import (
|
|
15
19
|
convert_inspec_to_test,
|
|
16
20
|
convert_resource_to_task,
|
|
@@ -425,6 +429,92 @@ def _output_result(result: str, output_format: str) -> None:
|
|
|
425
429
|
_output_text_format(result)
|
|
426
430
|
|
|
427
431
|
|
|
432
|
+
@cli.command()
|
|
433
|
+
@click.argument("cookbook_path", type=click.Path(exists=True))
|
|
434
|
+
@click.option(
|
|
435
|
+
"--output",
|
|
436
|
+
"-o",
|
|
437
|
+
type=click.Path(),
|
|
438
|
+
help="Save report to file instead of printing to stdout",
|
|
439
|
+
)
|
|
440
|
+
def profile(cookbook_path: str, output: str | None) -> None:
|
|
441
|
+
"""
|
|
442
|
+
Profile cookbook parsing performance and generate optimization report.
|
|
443
|
+
|
|
444
|
+
COOKBOOK_PATH: Path to the Chef cookbook to profile
|
|
445
|
+
|
|
446
|
+
This command analyzes the performance of parsing all cookbook components
|
|
447
|
+
(recipes, attributes, resources, templates) and provides recommendations
|
|
448
|
+
for optimization.
|
|
449
|
+
"""
|
|
450
|
+
try:
|
|
451
|
+
click.echo(f"Profiling cookbook: {cookbook_path}")
|
|
452
|
+
click.echo("This may take a moment for large cookbooks...")
|
|
453
|
+
|
|
454
|
+
report = generate_cookbook_performance_report(cookbook_path)
|
|
455
|
+
report_text = str(report)
|
|
456
|
+
|
|
457
|
+
if output:
|
|
458
|
+
Path(output).write_text(report_text)
|
|
459
|
+
click.echo(f"✓ Performance report saved to: {output}")
|
|
460
|
+
else:
|
|
461
|
+
click.echo(report_text)
|
|
462
|
+
|
|
463
|
+
except Exception as e:
|
|
464
|
+
click.echo(f"Error profiling cookbook: {e}", err=True)
|
|
465
|
+
sys.exit(1)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@cli.command()
|
|
469
|
+
@click.argument(
|
|
470
|
+
"operation",
|
|
471
|
+
type=click.Choice(["recipe", "attributes", "resource", "template"]),
|
|
472
|
+
)
|
|
473
|
+
@click.argument("path", type=click.Path(exists=True))
|
|
474
|
+
@click.option(
|
|
475
|
+
"--detailed",
|
|
476
|
+
is_flag=True,
|
|
477
|
+
help="Show detailed function call statistics",
|
|
478
|
+
)
|
|
479
|
+
def profile_operation(operation: str, path: str, detailed: bool) -> None:
|
|
480
|
+
"""
|
|
481
|
+
Profile a single parsing operation in detail.
|
|
482
|
+
|
|
483
|
+
OPERATION: Type of operation to profile
|
|
484
|
+
PATH: Path to the file to parse
|
|
485
|
+
|
|
486
|
+
This command profiles a single parsing operation and shows
|
|
487
|
+
execution time, memory usage, and optionally detailed function statistics.
|
|
488
|
+
"""
|
|
489
|
+
operation_map = {
|
|
490
|
+
"recipe": parse_recipe,
|
|
491
|
+
"attributes": parse_attributes,
|
|
492
|
+
"resource": parse_custom_resource,
|
|
493
|
+
"template": parse_template,
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
func = operation_map[operation]
|
|
497
|
+
|
|
498
|
+
try:
|
|
499
|
+
click.echo(f"Profiling {operation} parsing: {path}")
|
|
500
|
+
|
|
501
|
+
if detailed:
|
|
502
|
+
from souschef.profiling import detailed_profile_function
|
|
503
|
+
|
|
504
|
+
_, profile_result = detailed_profile_function(func, path)
|
|
505
|
+
click.echo(str(profile_result))
|
|
506
|
+
if profile_result.function_stats.get("top_functions"):
|
|
507
|
+
click.echo("\nDetailed Function Statistics:")
|
|
508
|
+
click.echo(profile_result.function_stats["top_functions"])
|
|
509
|
+
else:
|
|
510
|
+
_, profile_result = profile_function(func, path)
|
|
511
|
+
click.echo(str(profile_result))
|
|
512
|
+
|
|
513
|
+
except Exception as e:
|
|
514
|
+
click.echo(f"Error profiling operation: {e}", err=True)
|
|
515
|
+
sys.exit(1)
|
|
516
|
+
|
|
517
|
+
|
|
428
518
|
def main() -> NoReturn:
|
|
429
519
|
"""Run the CLI."""
|
|
430
520
|
cli()
|
souschef/converters/playbook.py
CHANGED
|
@@ -1441,8 +1441,40 @@ def _extract_chef_guards(resource: dict[str, str], raw_content: str) -> dict[str
|
|
|
1441
1441
|
return guards
|
|
1442
1442
|
|
|
1443
1443
|
|
|
1444
|
+
def _is_opening_delimiter(char: str, in_quotes: bool) -> bool:
|
|
1445
|
+
"""Check if character is an opening delimiter."""
|
|
1446
|
+
return char == "{" and not in_quotes
|
|
1447
|
+
|
|
1448
|
+
|
|
1449
|
+
def _is_closing_delimiter(char: str, in_quotes: bool) -> bool:
|
|
1450
|
+
"""Check if character is a closing delimiter."""
|
|
1451
|
+
return char == "}" and not in_quotes
|
|
1452
|
+
|
|
1453
|
+
|
|
1454
|
+
def _is_quote_character(char: str) -> bool:
|
|
1455
|
+
"""Check if character is a quote."""
|
|
1456
|
+
return char in ['"', "'"]
|
|
1457
|
+
|
|
1458
|
+
|
|
1459
|
+
def _should_split_here(char: str, in_quotes: bool, in_block: int) -> bool:
|
|
1460
|
+
"""Determine if we should split at this comma."""
|
|
1461
|
+
return char == "," and not in_quotes and in_block == 0
|
|
1462
|
+
|
|
1463
|
+
|
|
1444
1464
|
def _split_guard_array_parts(array_content: str) -> list[str]:
|
|
1445
|
-
"""
|
|
1465
|
+
"""
|
|
1466
|
+
Split array content by commas, respecting quotes and blocks.
|
|
1467
|
+
|
|
1468
|
+
Handles Chef guard arrays like: ['test -f /file', { block }, "string"]
|
|
1469
|
+
Tracks quote state and brace nesting to avoid splitting inside strings or blocks.
|
|
1470
|
+
|
|
1471
|
+
Args:
|
|
1472
|
+
array_content: Raw array content string
|
|
1473
|
+
|
|
1474
|
+
Returns:
|
|
1475
|
+
List of array parts split by commas
|
|
1476
|
+
|
|
1477
|
+
"""
|
|
1446
1478
|
parts = []
|
|
1447
1479
|
current_part = ""
|
|
1448
1480
|
in_quotes = False
|
|
@@ -1450,24 +1482,30 @@ def _split_guard_array_parts(array_content: str) -> list[str]:
|
|
|
1450
1482
|
quote_char = None
|
|
1451
1483
|
|
|
1452
1484
|
for char in array_content:
|
|
1453
|
-
|
|
1485
|
+
# Handle quote transitions
|
|
1486
|
+
if _is_quote_character(char) and not in_block:
|
|
1454
1487
|
if not in_quotes:
|
|
1455
1488
|
in_quotes = True
|
|
1456
1489
|
quote_char = char
|
|
1457
1490
|
elif char == quote_char:
|
|
1458
1491
|
in_quotes = False
|
|
1459
1492
|
quote_char = None
|
|
1460
|
-
|
|
1493
|
+
|
|
1494
|
+
# Handle block nesting
|
|
1495
|
+
elif _is_opening_delimiter(char, in_quotes):
|
|
1461
1496
|
in_block += 1
|
|
1462
|
-
elif char
|
|
1497
|
+
elif _is_closing_delimiter(char, in_quotes):
|
|
1463
1498
|
in_block -= 1
|
|
1464
|
-
|
|
1499
|
+
|
|
1500
|
+
# Handle splits at commas
|
|
1501
|
+
elif _should_split_here(char, in_quotes, in_block):
|
|
1465
1502
|
parts.append(current_part.strip())
|
|
1466
1503
|
current_part = ""
|
|
1467
1504
|
continue
|
|
1468
1505
|
|
|
1469
1506
|
current_part += char
|
|
1470
1507
|
|
|
1508
|
+
# Add final part if not empty
|
|
1471
1509
|
if current_part.strip():
|
|
1472
1510
|
parts.append(current_part.strip())
|
|
1473
1511
|
|
souschef/converters/resource.py
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
4
|
import json
|
|
5
|
+
from collections.abc import Callable
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
8
|
from souschef.core.constants import ACTION_TO_STATE, RESOURCE_MAPPINGS
|
|
8
9
|
|
|
10
|
+
# Type alias for parameter builder functions
|
|
11
|
+
ParamBuilder = Callable[[str, str, dict[str, Any]], dict[str, Any]]
|
|
12
|
+
|
|
9
13
|
|
|
10
14
|
def _parse_properties(properties_str: str) -> dict[str, Any]:
|
|
11
15
|
"""
|
|
@@ -87,6 +91,39 @@ def _get_service_params(resource_name: str, action: str) -> dict[str, Any]:
|
|
|
87
91
|
return params
|
|
88
92
|
|
|
89
93
|
|
|
94
|
+
def _get_template_file_params(resource_name: str, action: str) -> dict[str, Any]:
|
|
95
|
+
"""Get parameters for template resources."""
|
|
96
|
+
params = {
|
|
97
|
+
"src": resource_name,
|
|
98
|
+
"dest": resource_name.replace(".erb", ""),
|
|
99
|
+
}
|
|
100
|
+
if action == "create":
|
|
101
|
+
params["mode"] = "0644"
|
|
102
|
+
return params
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _get_regular_file_params(resource_name: str, action: str) -> dict[str, Any]:
|
|
106
|
+
"""Get parameters for regular file resources."""
|
|
107
|
+
params: dict[str, Any] = {"path": resource_name}
|
|
108
|
+
if action == "create":
|
|
109
|
+
params["state"] = "file"
|
|
110
|
+
params["mode"] = "0644"
|
|
111
|
+
else:
|
|
112
|
+
params["state"] = ACTION_TO_STATE.get(action, action)
|
|
113
|
+
return params
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _get_directory_params(resource_name: str, action: str) -> dict[str, Any]:
|
|
117
|
+
"""Get parameters for directory resources."""
|
|
118
|
+
params: dict[str, Any] = {
|
|
119
|
+
"path": resource_name,
|
|
120
|
+
"state": "directory",
|
|
121
|
+
}
|
|
122
|
+
if action == "create":
|
|
123
|
+
params["mode"] = "0755"
|
|
124
|
+
return params
|
|
125
|
+
|
|
126
|
+
|
|
90
127
|
def _get_file_params(
|
|
91
128
|
resource_name: str, action: str, resource_type: str
|
|
92
129
|
) -> dict[str, Any]:
|
|
@@ -102,34 +139,84 @@ def _get_file_params(
|
|
|
102
139
|
Dictionary of Ansible file parameters.
|
|
103
140
|
|
|
104
141
|
"""
|
|
105
|
-
params: dict[str, Any] = {}
|
|
106
|
-
|
|
107
142
|
if resource_type == "template":
|
|
108
|
-
|
|
109
|
-
params["dest"] = resource_name.replace(".erb", "")
|
|
110
|
-
if action == "create":
|
|
111
|
-
params["mode"] = "0644"
|
|
143
|
+
return _get_template_file_params(resource_name, action)
|
|
112
144
|
elif resource_type == "file":
|
|
113
|
-
|
|
114
|
-
if action == "create":
|
|
115
|
-
params["state"] = "file"
|
|
116
|
-
params["mode"] = "0644"
|
|
117
|
-
else:
|
|
118
|
-
params["state"] = ACTION_TO_STATE.get(action, action)
|
|
145
|
+
return _get_regular_file_params(resource_name, action)
|
|
119
146
|
elif resource_type == "directory":
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
147
|
+
return _get_directory_params(resource_name, action)
|
|
148
|
+
return {}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _get_package_params(
|
|
152
|
+
resource_name: str, action: str, props: dict[str, Any]
|
|
153
|
+
) -> dict[str, Any]:
|
|
154
|
+
"""Build parameters for package resources."""
|
|
155
|
+
return {"name": resource_name, "state": ACTION_TO_STATE.get(action, action)}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _get_execute_params(
|
|
159
|
+
resource_name: str, action: str, props: dict[str, Any]
|
|
160
|
+
) -> dict[str, Any]:
|
|
161
|
+
"""Build parameters for execute/bash resources."""
|
|
162
|
+
return {"cmd": resource_name}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _get_user_group_params(
|
|
166
|
+
resource_name: str, action: str, props: dict[str, Any]
|
|
167
|
+
) -> dict[str, Any]:
|
|
168
|
+
"""Build parameters for user/group resources."""
|
|
169
|
+
return {"name": resource_name, "state": ACTION_TO_STATE.get(action, "present")}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _get_remote_file_params(
|
|
173
|
+
resource_name: str, action: str, props: dict[str, Any]
|
|
174
|
+
) -> dict[str, Any]:
|
|
175
|
+
"""Build parameters for remote_file resources."""
|
|
176
|
+
params = {"dest": resource_name}
|
|
177
|
+
# Map Chef properties to Ansible parameters
|
|
178
|
+
prop_mappings = {
|
|
179
|
+
"source": "url",
|
|
180
|
+
"mode": "mode",
|
|
181
|
+
"owner": "owner",
|
|
182
|
+
"group": "group",
|
|
183
|
+
"checksum": "checksum",
|
|
184
|
+
}
|
|
185
|
+
for chef_prop, ansible_param in prop_mappings.items():
|
|
186
|
+
if chef_prop in props:
|
|
187
|
+
params[ansible_param] = props[chef_prop]
|
|
188
|
+
return params
|
|
124
189
|
|
|
190
|
+
|
|
191
|
+
def _get_default_params(resource_name: str, action: str) -> dict[str, Any]:
|
|
192
|
+
"""Build default parameters for unknown resource types."""
|
|
193
|
+
params = {"name": resource_name}
|
|
194
|
+
if action in ACTION_TO_STATE:
|
|
195
|
+
params["state"] = ACTION_TO_STATE[action]
|
|
125
196
|
return params
|
|
126
197
|
|
|
127
198
|
|
|
199
|
+
# Resource type to parameter builder mappings
|
|
200
|
+
RESOURCE_PARAM_BUILDERS: dict[str, ParamBuilder | str] = {
|
|
201
|
+
"package": _get_package_params,
|
|
202
|
+
"service": "service", # Uses _get_service_params
|
|
203
|
+
"systemd_unit": "service",
|
|
204
|
+
"template": "file", # Uses _get_file_params
|
|
205
|
+
"file": "file",
|
|
206
|
+
"directory": "file",
|
|
207
|
+
"execute": _get_execute_params,
|
|
208
|
+
"bash": _get_execute_params,
|
|
209
|
+
"user": _get_user_group_params,
|
|
210
|
+
"group": _get_user_group_params,
|
|
211
|
+
"remote_file": _get_remote_file_params,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
128
215
|
def _convert_chef_resource_to_ansible(
|
|
129
216
|
resource_type: str, resource_name: str, action: str, properties: str
|
|
130
217
|
) -> dict[str, Any]:
|
|
131
218
|
"""
|
|
132
|
-
Convert Chef resource to Ansible task dictionary.
|
|
219
|
+
Convert Chef resource to Ansible task dictionary using data-driven approach.
|
|
133
220
|
|
|
134
221
|
Args:
|
|
135
222
|
resource_type: The Chef resource type.
|
|
@@ -149,46 +236,56 @@ def _convert_chef_resource_to_ansible(
|
|
|
149
236
|
"name": f"{action.capitalize()} {resource_type} {resource_name}",
|
|
150
237
|
}
|
|
151
238
|
|
|
152
|
-
#
|
|
153
|
-
module_params: dict[str, Any] = {}
|
|
154
|
-
|
|
155
|
-
# Parse properties if provided
|
|
239
|
+
# Parse properties
|
|
156
240
|
props = _parse_properties(properties)
|
|
157
241
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
elif resource_type in ["template", "file", "directory"]:
|
|
164
|
-
module_params = _get_file_params(resource_name, action, resource_type)
|
|
165
|
-
elif resource_type in ["execute", "bash"]:
|
|
166
|
-
module_params["cmd"] = resource_name
|
|
242
|
+
# Build module parameters using appropriate builder
|
|
243
|
+
module_params = _build_module_params(resource_type, resource_name, action, props)
|
|
244
|
+
|
|
245
|
+
# Add special task-level flags for execute/bash resources
|
|
246
|
+
if resource_type in ["execute", "bash"]:
|
|
167
247
|
task["changed_when"] = "false"
|
|
168
|
-
elif resource_type in ["user", "group"]:
|
|
169
|
-
module_params["name"] = resource_name
|
|
170
|
-
module_params["state"] = ACTION_TO_STATE.get(action, "present")
|
|
171
|
-
elif resource_type == "remote_file":
|
|
172
|
-
module_params["dest"] = resource_name
|
|
173
|
-
if "source" in props:
|
|
174
|
-
module_params["url"] = props["source"]
|
|
175
|
-
if "mode" in props:
|
|
176
|
-
module_params["mode"] = props["mode"]
|
|
177
|
-
if "owner" in props:
|
|
178
|
-
module_params["owner"] = props["owner"]
|
|
179
|
-
if "group" in props:
|
|
180
|
-
module_params["group"] = props["group"]
|
|
181
|
-
if "checksum" in props:
|
|
182
|
-
module_params["checksum"] = props["checksum"]
|
|
183
|
-
else:
|
|
184
|
-
module_params["name"] = resource_name
|
|
185
|
-
if action in ACTION_TO_STATE:
|
|
186
|
-
module_params["state"] = ACTION_TO_STATE[action]
|
|
187
248
|
|
|
188
249
|
task[ansible_module] = module_params
|
|
189
250
|
return task
|
|
190
251
|
|
|
191
252
|
|
|
253
|
+
def _build_module_params(
|
|
254
|
+
resource_type: str, resource_name: str, action: str, props: dict[str, Any]
|
|
255
|
+
) -> dict[str, Any]:
|
|
256
|
+
"""
|
|
257
|
+
Build Ansible module parameters based on resource type.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
resource_type: The Chef resource type.
|
|
261
|
+
resource_name: The resource name.
|
|
262
|
+
action: The Chef action.
|
|
263
|
+
props: Parsed properties dictionary.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Dictionary of Ansible module parameters.
|
|
267
|
+
|
|
268
|
+
"""
|
|
269
|
+
# Look up the parameter builder for this resource type
|
|
270
|
+
builder = RESOURCE_PARAM_BUILDERS.get(resource_type)
|
|
271
|
+
|
|
272
|
+
if builder is None:
|
|
273
|
+
# Unknown resource type - use default builder
|
|
274
|
+
return _get_default_params(resource_name, action)
|
|
275
|
+
|
|
276
|
+
if isinstance(builder, str):
|
|
277
|
+
# Special handler reference (service/file)
|
|
278
|
+
if builder == "service":
|
|
279
|
+
return _get_service_params(resource_name, action)
|
|
280
|
+
elif builder == "file":
|
|
281
|
+
return _get_file_params(resource_name, action, resource_type)
|
|
282
|
+
# This shouldn't happen, but handle gracefully
|
|
283
|
+
return _get_default_params(resource_name, action)
|
|
284
|
+
|
|
285
|
+
# Call the parameter builder function
|
|
286
|
+
return builder(resource_name, action, props)
|
|
287
|
+
|
|
288
|
+
|
|
192
289
|
def _format_yaml_value(value: Any) -> str:
|
|
193
290
|
"""Format a value for YAML output."""
|
|
194
291
|
if isinstance(value, str):
|
souschef/core/__init__.py
CHANGED
|
@@ -38,6 +38,18 @@ from souschef.core.constants import (
|
|
|
38
38
|
REGEX_WORD_SYMBOLS,
|
|
39
39
|
RESOURCE_MAPPINGS,
|
|
40
40
|
)
|
|
41
|
+
from souschef.core.errors import (
|
|
42
|
+
ChefFileNotFoundError,
|
|
43
|
+
ConversionError,
|
|
44
|
+
InvalidCookbookError,
|
|
45
|
+
ParseError,
|
|
46
|
+
SousChefError,
|
|
47
|
+
ValidationError,
|
|
48
|
+
format_error_with_context,
|
|
49
|
+
validate_cookbook_structure,
|
|
50
|
+
validate_directory_exists,
|
|
51
|
+
validate_file_exists,
|
|
52
|
+
)
|
|
41
53
|
from souschef.core.path_utils import _normalize_path, _safe_join
|
|
42
54
|
from souschef.core.ruby_utils import _normalize_ruby_value
|
|
43
55
|
from souschef.core.validation import (
|
|
@@ -55,4 +67,14 @@ __all__ = [
|
|
|
55
67
|
"ValidationEngine",
|
|
56
68
|
"ValidationLevel",
|
|
57
69
|
"ValidationResult",
|
|
70
|
+
"SousChefError",
|
|
71
|
+
"ChefFileNotFoundError",
|
|
72
|
+
"InvalidCookbookError",
|
|
73
|
+
"ParseError",
|
|
74
|
+
"ConversionError",
|
|
75
|
+
"ValidationError",
|
|
76
|
+
"validate_file_exists",
|
|
77
|
+
"validate_directory_exists",
|
|
78
|
+
"validate_cookbook_structure",
|
|
79
|
+
"format_error_with_context",
|
|
58
80
|
]
|