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/assessment.py
CHANGED
|
@@ -9,7 +9,8 @@ import json
|
|
|
9
9
|
import re
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
-
from souschef.core import
|
|
12
|
+
from souschef.core import METADATA_FILENAME, _normalize_path, _safe_join
|
|
13
|
+
from souschef.core.errors import format_error_with_context
|
|
13
14
|
from souschef.core.validation import (
|
|
14
15
|
ValidationEngine,
|
|
15
16
|
ValidationLevel,
|
|
@@ -35,116 +36,110 @@ def assess_chef_migration_complexity(
|
|
|
35
36
|
|
|
36
37
|
"""
|
|
37
38
|
try:
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
for cookbook_path in paths:
|
|
52
|
-
if cookbook_path.exists():
|
|
53
|
-
# deepcode ignore PT: path normalized via _normalize_path
|
|
54
|
-
assessment = _assess_single_cookbook(cookbook_path)
|
|
55
|
-
cookbook_assessments.append(assessment)
|
|
56
|
-
|
|
57
|
-
# Aggregate metrics
|
|
58
|
-
overall_metrics["total_cookbooks"] += 1
|
|
59
|
-
overall_metrics["total_recipes"] += assessment["metrics"][
|
|
60
|
-
"recipe_count"
|
|
61
|
-
]
|
|
62
|
-
overall_metrics["total_resources"] += assessment["metrics"][
|
|
63
|
-
"resource_count"
|
|
64
|
-
]
|
|
65
|
-
overall_metrics["complexity_score"] += assessment["complexity_score"]
|
|
66
|
-
overall_metrics["estimated_effort_days"] += assessment[
|
|
67
|
-
"estimated_effort_days"
|
|
68
|
-
]
|
|
69
|
-
|
|
70
|
-
# Calculate averages
|
|
71
|
-
if cookbook_assessments:
|
|
72
|
-
overall_metrics["avg_complexity"] = int(
|
|
73
|
-
overall_metrics["complexity_score"] / len(cookbook_assessments)
|
|
74
|
-
)
|
|
39
|
+
# Validate inputs
|
|
40
|
+
error_msg = _validate_assessment_inputs(
|
|
41
|
+
cookbook_paths, migration_scope, target_platform
|
|
42
|
+
)
|
|
43
|
+
if error_msg:
|
|
44
|
+
return error_msg
|
|
45
|
+
|
|
46
|
+
# Parse cookbook paths (may be empty if none exist)
|
|
47
|
+
valid_paths = _parse_cookbook_paths(cookbook_paths)
|
|
48
|
+
|
|
49
|
+
# Analyze all cookbooks (handles empty list gracefully)
|
|
50
|
+
cookbook_assessments, overall_metrics = _analyze_cookbook_metrics(valid_paths)
|
|
75
51
|
|
|
76
|
-
# Generate
|
|
52
|
+
# Generate recommendations and reports
|
|
77
53
|
recommendations = _generate_migration_recommendations_from_assessment(
|
|
78
54
|
cookbook_assessments, overall_metrics, target_platform
|
|
79
55
|
)
|
|
80
|
-
|
|
81
|
-
# Create migration roadmap
|
|
82
56
|
roadmap = _create_migration_roadmap(cookbook_assessments)
|
|
83
57
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
58
|
+
# Format final assessment report
|
|
59
|
+
return _format_assessment_report(
|
|
60
|
+
migration_scope,
|
|
61
|
+
target_platform,
|
|
62
|
+
overall_metrics,
|
|
63
|
+
cookbook_assessments,
|
|
64
|
+
recommendations,
|
|
65
|
+
roadmap,
|
|
66
|
+
)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
return format_error_with_context(
|
|
69
|
+
e, "assessing Chef migration complexity", cookbook_paths
|
|
70
|
+
)
|
|
87
71
|
|
|
88
|
-
## Overall Migration Metrics:
|
|
89
|
-
{_format_overall_metrics(overall_metrics)}
|
|
90
72
|
|
|
91
|
-
|
|
92
|
-
|
|
73
|
+
def _validate_migration_plan_inputs(
|
|
74
|
+
cookbook_paths: str, migration_strategy: str, timeline_weeks: int
|
|
75
|
+
) -> str | None:
|
|
76
|
+
"""
|
|
77
|
+
Validate migration plan inputs.
|
|
93
78
|
|
|
94
|
-
|
|
95
|
-
|
|
79
|
+
Returns:
|
|
80
|
+
Error message if validation fails, None if valid.
|
|
96
81
|
|
|
97
|
-
|
|
98
|
-
|
|
82
|
+
"""
|
|
83
|
+
if not cookbook_paths or not cookbook_paths.strip():
|
|
84
|
+
return (
|
|
85
|
+
"Error: Cookbook paths cannot be empty\n\n"
|
|
86
|
+
"Suggestion: Provide comma-separated paths to Chef cookbooks"
|
|
87
|
+
)
|
|
99
88
|
|
|
100
|
-
|
|
101
|
-
|
|
89
|
+
valid_strategies = ["big_bang", "phased", "parallel"]
|
|
90
|
+
if migration_strategy not in valid_strategies:
|
|
91
|
+
return (
|
|
92
|
+
f"Error: Invalid migration strategy '{migration_strategy}'\n\n"
|
|
93
|
+
f"Suggestion: Use one of {', '.join(valid_strategies)}"
|
|
94
|
+
)
|
|
102
95
|
|
|
103
|
-
|
|
104
|
-
|
|
96
|
+
if not (1 <= timeline_weeks <= 104): # 1 week to 2 years
|
|
97
|
+
return (
|
|
98
|
+
f"Error: Timeline must be between 1 and 104 weeks, got {timeline_weeks}\n\n"
|
|
99
|
+
"Suggestion: Provide a realistic timeline (4-12 weeks typical)"
|
|
100
|
+
)
|
|
105
101
|
|
|
106
|
-
|
|
107
|
-
{_estimate_resource_requirements(overall_metrics, target_platform)}
|
|
108
|
-
"""
|
|
109
|
-
except Exception as e:
|
|
110
|
-
return f"Error assessing migration complexity: {e}"
|
|
102
|
+
return None
|
|
111
103
|
|
|
112
104
|
|
|
113
|
-
def
|
|
114
|
-
cookbook_paths: str, migration_strategy: str = "phased", timeline_weeks: int = 12
|
|
115
|
-
) -> str:
|
|
105
|
+
def _parse_and_assess_cookbooks(cookbook_paths: str) -> tuple[list, str | None]:
|
|
116
106
|
"""
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
Args:
|
|
120
|
-
cookbook_paths: Comma-separated paths to Chef cookbooks
|
|
121
|
-
migration_strategy: Migration approach (big_bang, phased, parallel)
|
|
122
|
-
timeline_weeks: Target timeline in weeks
|
|
107
|
+
Parse cookbook paths and assess each cookbook.
|
|
123
108
|
|
|
124
109
|
Returns:
|
|
125
|
-
|
|
110
|
+
Tuple of (cookbook_assessments, error_message).
|
|
126
111
|
|
|
127
112
|
"""
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
113
|
+
paths = [_normalize_path(path.strip()) for path in cookbook_paths.split(",")]
|
|
114
|
+
valid_paths = [p for p in paths if p.exists()]
|
|
115
|
+
|
|
116
|
+
if not valid_paths:
|
|
117
|
+
return (
|
|
118
|
+
[],
|
|
119
|
+
"Error: No valid cookbook paths found\n\n"
|
|
120
|
+
"Suggestion: Ensure paths exist and point to cookbook directories",
|
|
121
|
+
)
|
|
132
122
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
123
|
+
cookbook_assessments = []
|
|
124
|
+
for cookbook_path in valid_paths:
|
|
125
|
+
# deepcode ignore PT: path normalized via _normalize_path
|
|
126
|
+
assessment = _assess_single_cookbook(cookbook_path)
|
|
127
|
+
cookbook_assessments.append(assessment)
|
|
138
128
|
|
|
139
|
-
|
|
140
|
-
migration_plan = _generate_detailed_migration_plan(
|
|
141
|
-
cookbook_assessments, migration_strategy, timeline_weeks
|
|
142
|
-
)
|
|
129
|
+
return cookbook_assessments, None
|
|
143
130
|
|
|
144
|
-
|
|
131
|
+
|
|
132
|
+
def _format_migration_plan_output(
|
|
133
|
+
migration_plan: dict,
|
|
134
|
+
migration_strategy: str,
|
|
135
|
+
timeline_weeks: int,
|
|
136
|
+
num_cookbooks: int,
|
|
137
|
+
) -> str:
|
|
138
|
+
"""Format migration plan as markdown output."""
|
|
139
|
+
return f"""# Chef to Ansible Migration Plan
|
|
145
140
|
# Strategy: {migration_strategy}
|
|
146
141
|
# Timeline: {timeline_weeks} weeks
|
|
147
|
-
# Cookbooks: {
|
|
142
|
+
# Cookbooks: {num_cookbooks}
|
|
148
143
|
|
|
149
144
|
## Executive Summary:
|
|
150
145
|
{migration_plan["executive_summary"]}
|
|
@@ -173,8 +168,50 @@ def generate_migration_plan(
|
|
|
173
168
|
## Post-Migration Tasks:
|
|
174
169
|
{migration_plan["post_migration"]}
|
|
175
170
|
"""
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def generate_migration_plan(
|
|
174
|
+
cookbook_paths: str, migration_strategy: str = "phased", timeline_weeks: int = 12
|
|
175
|
+
) -> str:
|
|
176
|
+
"""
|
|
177
|
+
Generate a detailed migration plan from Chef to Ansible with timeline and milestones.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
cookbook_paths: Comma-separated paths to Chef cookbooks
|
|
181
|
+
migration_strategy: Migration approach (big_bang, phased, parallel)
|
|
182
|
+
timeline_weeks: Target timeline in weeks
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Detailed migration plan with phases, milestones, and deliverables
|
|
186
|
+
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
# Validate inputs
|
|
190
|
+
error = _validate_migration_plan_inputs(
|
|
191
|
+
cookbook_paths, migration_strategy, timeline_weeks
|
|
192
|
+
)
|
|
193
|
+
if error:
|
|
194
|
+
return error
|
|
195
|
+
|
|
196
|
+
# Parse and assess cookbooks
|
|
197
|
+
cookbook_assessments, error = _parse_and_assess_cookbooks(cookbook_paths)
|
|
198
|
+
if error:
|
|
199
|
+
return error
|
|
200
|
+
|
|
201
|
+
# Generate migration plan based on strategy
|
|
202
|
+
migration_plan = _generate_detailed_migration_plan(
|
|
203
|
+
cookbook_assessments, migration_strategy, timeline_weeks
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return _format_migration_plan_output(
|
|
207
|
+
migration_plan,
|
|
208
|
+
migration_strategy,
|
|
209
|
+
timeline_weeks,
|
|
210
|
+
len(cookbook_assessments),
|
|
211
|
+
)
|
|
212
|
+
|
|
176
213
|
except Exception as e:
|
|
177
|
-
return
|
|
214
|
+
return format_error_with_context(e, "generating migration plan", cookbook_paths)
|
|
178
215
|
|
|
179
216
|
|
|
180
217
|
def analyze_cookbook_dependencies(
|
|
@@ -192,9 +229,20 @@ def analyze_cookbook_dependencies(
|
|
|
192
229
|
|
|
193
230
|
"""
|
|
194
231
|
try:
|
|
232
|
+
# Validate inputs
|
|
233
|
+
valid_depths = ["direct", "transitive", "full"]
|
|
234
|
+
if dependency_depth not in valid_depths:
|
|
235
|
+
return (
|
|
236
|
+
f"Error: Invalid dependency depth '{dependency_depth}'\n\n"
|
|
237
|
+
f"Suggestion: Use one of {', '.join(valid_depths)}"
|
|
238
|
+
)
|
|
239
|
+
|
|
195
240
|
cookbook_path_obj = _normalize_path(cookbook_path)
|
|
196
241
|
if not cookbook_path_obj.exists():
|
|
197
|
-
return
|
|
242
|
+
return (
|
|
243
|
+
f"Error: Cookbook path not found: {cookbook_path}\n\n"
|
|
244
|
+
"Suggestion: Check that the path exists and points to a cookbook directory"
|
|
245
|
+
)
|
|
198
246
|
|
|
199
247
|
# Analyze dependencies
|
|
200
248
|
dependency_analysis = _analyze_cookbook_dependencies_detailed(cookbook_path_obj)
|
|
@@ -231,7 +279,9 @@ def analyze_cookbook_dependencies(
|
|
|
231
279
|
{_analyze_dependency_migration_impact(dependency_analysis)}
|
|
232
280
|
"""
|
|
233
281
|
except Exception as e:
|
|
234
|
-
return
|
|
282
|
+
return format_error_with_context(
|
|
283
|
+
e, "analyzing cookbook dependencies", cookbook_path
|
|
284
|
+
)
|
|
235
285
|
|
|
236
286
|
|
|
237
287
|
def generate_migration_report(
|
|
@@ -300,7 +350,7 @@ def generate_migration_report(
|
|
|
300
350
|
{report["appendices"]}
|
|
301
351
|
"""
|
|
302
352
|
except Exception as e:
|
|
303
|
-
return
|
|
353
|
+
return format_error_with_context(e, "generating migration report")
|
|
304
354
|
|
|
305
355
|
|
|
306
356
|
def validate_conversion(
|
|
@@ -347,31 +397,188 @@ def validate_conversion(
|
|
|
347
397
|
return _format_validation_results_text(conversion_type, results, summary)
|
|
348
398
|
|
|
349
399
|
except Exception as e:
|
|
350
|
-
return
|
|
400
|
+
return format_error_with_context(
|
|
401
|
+
e, f"validating Ansible {conversion_type} conversion"
|
|
402
|
+
)
|
|
351
403
|
|
|
352
404
|
|
|
353
405
|
# Private helper functions for assessment
|
|
354
406
|
|
|
355
407
|
|
|
356
|
-
def
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
408
|
+
def _validate_assessment_inputs(
|
|
409
|
+
cookbook_paths: str, migration_scope: str, target_platform: str
|
|
410
|
+
) -> str | None:
|
|
411
|
+
"""
|
|
412
|
+
Validate inputs for migration assessment.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
cookbook_paths: Paths to cookbooks
|
|
416
|
+
migration_scope: Scope of migration
|
|
417
|
+
target_platform: Target platform
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
Error message if validation fails, None otherwise
|
|
421
|
+
|
|
422
|
+
"""
|
|
423
|
+
if not cookbook_paths or not cookbook_paths.strip():
|
|
424
|
+
return (
|
|
425
|
+
"Error: Cookbook paths cannot be empty\n\n"
|
|
426
|
+
"Suggestion: Provide comma-separated paths to Chef cookbooks"
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
valid_scopes = ["full", "recipes_only", "infrastructure_only"]
|
|
430
|
+
if migration_scope not in valid_scopes:
|
|
431
|
+
return (
|
|
432
|
+
f"Error: Invalid migration scope '{migration_scope}'\n\n"
|
|
433
|
+
f"Suggestion: Use one of {', '.join(valid_scopes)}"
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
valid_platforms = ["ansible_awx", "ansible_core", "ansible_tower"]
|
|
437
|
+
if target_platform not in valid_platforms:
|
|
438
|
+
return (
|
|
439
|
+
f"Error: Invalid target platform '{target_platform}'\n\n"
|
|
440
|
+
f"Suggestion: Use one of {', '.join(valid_platforms)}"
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
return None
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _parse_cookbook_paths(cookbook_paths: str) -> list[Any]:
|
|
447
|
+
"""
|
|
448
|
+
Parse and validate cookbook paths.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
cookbook_paths: Comma-separated paths to cookbooks
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
List of valid Path objects (may be empty)
|
|
455
|
+
|
|
456
|
+
"""
|
|
457
|
+
paths = [_normalize_path(path.strip()) for path in cookbook_paths.split(",")]
|
|
458
|
+
valid_paths = [p for p in paths if p.exists()]
|
|
459
|
+
return valid_paths
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _analyze_cookbook_metrics(
|
|
463
|
+
valid_paths: list[Any],
|
|
464
|
+
) -> tuple[list[Any], dict[str, int]]:
|
|
465
|
+
"""
|
|
466
|
+
Analyze metrics for all cookbooks.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
valid_paths: List of valid cookbook paths
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
Tuple of (cookbook_assessments, overall_metrics)
|
|
473
|
+
|
|
474
|
+
"""
|
|
475
|
+
cookbook_assessments = []
|
|
476
|
+
overall_metrics = {
|
|
477
|
+
"total_cookbooks": 0,
|
|
478
|
+
"total_recipes": 0,
|
|
479
|
+
"total_resources": 0,
|
|
363
480
|
"complexity_score": 0,
|
|
364
481
|
"estimated_effort_days": 0,
|
|
365
|
-
"challenges": [],
|
|
366
|
-
"migration_priority": "medium",
|
|
367
|
-
"dependencies": [],
|
|
368
482
|
}
|
|
369
483
|
|
|
370
|
-
|
|
371
|
-
|
|
484
|
+
for cookbook_path in valid_paths:
|
|
485
|
+
# deepcode ignore PT: path normalized via _normalize_path
|
|
486
|
+
assessment = _assess_single_cookbook(cookbook_path)
|
|
487
|
+
cookbook_assessments.append(assessment)
|
|
488
|
+
|
|
489
|
+
# Aggregate metrics
|
|
490
|
+
overall_metrics["total_cookbooks"] += 1
|
|
491
|
+
overall_metrics["total_recipes"] += assessment["metrics"]["recipe_count"]
|
|
492
|
+
overall_metrics["total_resources"] += assessment["metrics"]["resource_count"]
|
|
493
|
+
overall_metrics["complexity_score"] += assessment["complexity_score"]
|
|
494
|
+
overall_metrics["estimated_effort_days"] += assessment["estimated_effort_days"]
|
|
495
|
+
|
|
496
|
+
# Calculate averages
|
|
497
|
+
if cookbook_assessments:
|
|
498
|
+
overall_metrics["avg_complexity"] = int(
|
|
499
|
+
overall_metrics["complexity_score"] / len(cookbook_assessments)
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
return cookbook_assessments, overall_metrics
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _format_assessment_report(
|
|
506
|
+
migration_scope: str,
|
|
507
|
+
target_platform: str,
|
|
508
|
+
overall_metrics: dict[str, int],
|
|
509
|
+
cookbook_assessments: list[Any],
|
|
510
|
+
recommendations: str,
|
|
511
|
+
roadmap: str,
|
|
512
|
+
) -> str:
|
|
513
|
+
"""
|
|
514
|
+
Format the final assessment report.
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
migration_scope: Scope of migration
|
|
518
|
+
target_platform: Target platform
|
|
519
|
+
overall_metrics: Overall metrics dictionary
|
|
520
|
+
cookbook_assessments: List of cookbook assessments
|
|
521
|
+
recommendations: Migration recommendations
|
|
522
|
+
roadmap: Migration roadmap
|
|
523
|
+
|
|
524
|
+
Returns:
|
|
525
|
+
Formatted report string
|
|
526
|
+
|
|
527
|
+
"""
|
|
528
|
+
return f"""# Chef to Ansible Migration Assessment
|
|
529
|
+
# Scope: {migration_scope}
|
|
530
|
+
# Target Platform: {target_platform}
|
|
531
|
+
|
|
532
|
+
## Overall Migration Metrics:
|
|
533
|
+
{_format_overall_metrics(overall_metrics)}
|
|
534
|
+
|
|
535
|
+
## Cookbook Assessments:
|
|
536
|
+
{_format_cookbook_assessments(cookbook_assessments)}
|
|
537
|
+
|
|
538
|
+
## Migration Complexity Analysis:
|
|
539
|
+
{_format_complexity_analysis(cookbook_assessments)}
|
|
540
|
+
|
|
541
|
+
## Migration Recommendations:
|
|
542
|
+
{recommendations}
|
|
543
|
+
|
|
544
|
+
## Migration Roadmap:
|
|
545
|
+
{roadmap}
|
|
546
|
+
|
|
547
|
+
## Risk Assessment:
|
|
548
|
+
{_assess_migration_risks(cookbook_assessments, target_platform)}
|
|
549
|
+
|
|
550
|
+
## Resource Requirements:
|
|
551
|
+
{_estimate_resource_requirements(overall_metrics, target_platform)}
|
|
552
|
+
"""
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _count_cookbook_artifacts(cookbook_path) -> dict[str, int]:
|
|
556
|
+
"""Count basic cookbook artifacts (recipes, templates, files)."""
|
|
557
|
+
recipes_dir = _safe_join(cookbook_path, "recipes")
|
|
372
558
|
recipe_count = len(list(recipes_dir.glob("*.rb"))) if recipes_dir.exists() else 0
|
|
373
559
|
|
|
374
|
-
|
|
560
|
+
templates_count = (
|
|
561
|
+
len(list(_safe_join(cookbook_path, "templates").glob("*")))
|
|
562
|
+
if _safe_join(cookbook_path, "templates").exists()
|
|
563
|
+
else 0
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
files_count = (
|
|
567
|
+
len(list(_safe_join(cookbook_path, "files").glob("*")))
|
|
568
|
+
if _safe_join(cookbook_path, "files").exists()
|
|
569
|
+
else 0
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
return {
|
|
573
|
+
"recipe_count": recipe_count,
|
|
574
|
+
"templates": templates_count,
|
|
575
|
+
"files": files_count,
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _analyze_recipe_complexity(cookbook_path) -> dict[str, int]:
|
|
580
|
+
"""Analyze recipe files for resource counts, Ruby blocks, and custom resources."""
|
|
581
|
+
recipes_dir = _safe_join(cookbook_path, "recipes")
|
|
375
582
|
resource_count = 0
|
|
376
583
|
custom_resources = 0
|
|
377
584
|
ruby_blocks = 0
|
|
@@ -381,7 +588,6 @@ def _assess_single_cookbook(cookbook_path) -> dict:
|
|
|
381
588
|
with recipe_file.open("r", encoding="utf-8", errors="ignore") as f:
|
|
382
589
|
content = f.read()
|
|
383
590
|
# Count Chef resources
|
|
384
|
-
|
|
385
591
|
resources = len(
|
|
386
592
|
re.findall(r'\w{1,100}\s+[\'"]([^\'"]{0,200})[\'"]\s+do', content)
|
|
387
593
|
)
|
|
@@ -395,57 +601,85 @@ def _assess_single_cookbook(cookbook_path) -> dict:
|
|
|
395
601
|
)
|
|
396
602
|
resource_count += resources
|
|
397
603
|
|
|
398
|
-
|
|
399
|
-
"recipe_count": recipe_count,
|
|
604
|
+
return {
|
|
400
605
|
"resource_count": resource_count,
|
|
401
606
|
"custom_resources": custom_resources,
|
|
402
607
|
"ruby_blocks": ruby_blocks,
|
|
403
|
-
"templates": len(list(_safe_join(cookbook, "templates").glob("*")))
|
|
404
|
-
if _safe_join(cookbook, "templates").exists()
|
|
405
|
-
else 0,
|
|
406
|
-
"files": len(list(_safe_join(cookbook, "files").glob("*")))
|
|
407
|
-
if _safe_join(cookbook, "files").exists()
|
|
408
|
-
else 0,
|
|
409
608
|
}
|
|
410
609
|
|
|
411
|
-
|
|
610
|
+
|
|
611
|
+
def _calculate_complexity_score(metrics: dict[str, int]) -> int:
|
|
612
|
+
"""Calculate complexity score (0-100) based on metrics."""
|
|
613
|
+
recipe_count = metrics["recipe_count"]
|
|
614
|
+
resource_count = metrics["resource_count"]
|
|
615
|
+
|
|
412
616
|
complexity_factors = {
|
|
413
617
|
"recipe_count": min(recipe_count * 2, 20),
|
|
414
618
|
"resource_density": min(resource_count / max(recipe_count, 1) * 5, 25),
|
|
415
|
-
"custom_resources": custom_resources * 10,
|
|
416
|
-
"ruby_blocks": ruby_blocks * 5,
|
|
417
|
-
"templates": min(
|
|
418
|
-
"files": min(
|
|
619
|
+
"custom_resources": metrics["custom_resources"] * 10,
|
|
620
|
+
"ruby_blocks": metrics["ruby_blocks"] * 5,
|
|
621
|
+
"templates": min(metrics["templates"] * 2, 15),
|
|
622
|
+
"files": min(metrics["files"] * 1, 10),
|
|
419
623
|
}
|
|
420
624
|
|
|
421
|
-
|
|
625
|
+
return int(sum(complexity_factors.values()))
|
|
422
626
|
|
|
423
|
-
# Estimate effort (person-days)
|
|
424
|
-
base_effort = recipe_count * 0.5 # 0.5 days per recipe
|
|
425
|
-
complexity_multiplier = 1 + (assessment["complexity_score"] / 100)
|
|
426
|
-
assessment["estimated_effort_days"] = round(base_effort * complexity_multiplier, 1)
|
|
427
627
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
628
|
+
def _identify_migration_challenges(
|
|
629
|
+
metrics: dict[str, int], complexity_score: int
|
|
630
|
+
) -> list[str]:
|
|
631
|
+
"""Identify migration challenges based on metrics."""
|
|
632
|
+
challenges = []
|
|
633
|
+
|
|
634
|
+
if metrics["custom_resources"] > 0:
|
|
635
|
+
challenges.append(
|
|
636
|
+
f"{metrics['custom_resources']} custom resources requiring manual conversion"
|
|
436
637
|
)
|
|
437
|
-
if
|
|
438
|
-
|
|
439
|
-
"
|
|
638
|
+
if metrics["ruby_blocks"] > 5:
|
|
639
|
+
challenges.append(
|
|
640
|
+
f"{metrics['ruby_blocks']} Ruby blocks needing shell script conversion"
|
|
440
641
|
)
|
|
642
|
+
if complexity_score > 70:
|
|
643
|
+
challenges.append("High complexity cookbook requiring expert review")
|
|
644
|
+
|
|
645
|
+
return challenges
|
|
441
646
|
|
|
442
|
-
# Set migration priority
|
|
443
|
-
if assessment["complexity_score"] < 30:
|
|
444
|
-
assessment["migration_priority"] = "low"
|
|
445
|
-
elif assessment["complexity_score"] > 70:
|
|
446
|
-
assessment["migration_priority"] = "high"
|
|
447
647
|
|
|
448
|
-
|
|
648
|
+
def _determine_migration_priority(complexity_score: int) -> str:
|
|
649
|
+
"""Determine migration priority based on complexity score."""
|
|
650
|
+
if complexity_score < 30:
|
|
651
|
+
return "low"
|
|
652
|
+
elif complexity_score > 70:
|
|
653
|
+
return "high"
|
|
654
|
+
return "medium"
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _assess_single_cookbook(cookbook_path) -> dict:
|
|
658
|
+
"""Assess complexity of a single cookbook."""
|
|
659
|
+
cookbook = _normalize_path(cookbook_path)
|
|
660
|
+
|
|
661
|
+
# Collect metrics
|
|
662
|
+
artifact_counts = _count_cookbook_artifacts(cookbook)
|
|
663
|
+
recipe_complexity = _analyze_recipe_complexity(cookbook)
|
|
664
|
+
metrics = {**artifact_counts, **recipe_complexity}
|
|
665
|
+
|
|
666
|
+
# Calculate complexity and effort
|
|
667
|
+
complexity_score = _calculate_complexity_score(metrics)
|
|
668
|
+
base_effort = metrics["recipe_count"] * 0.5 # 0.5 days per recipe
|
|
669
|
+
complexity_multiplier = 1 + (complexity_score / 100)
|
|
670
|
+
estimated_effort = round(base_effort * complexity_multiplier, 1)
|
|
671
|
+
|
|
672
|
+
# Build assessment
|
|
673
|
+
return {
|
|
674
|
+
"cookbook_name": cookbook.name,
|
|
675
|
+
"cookbook_path": str(cookbook),
|
|
676
|
+
"metrics": metrics,
|
|
677
|
+
"complexity_score": complexity_score,
|
|
678
|
+
"estimated_effort_days": estimated_effort,
|
|
679
|
+
"challenges": _identify_migration_challenges(metrics, complexity_score),
|
|
680
|
+
"migration_priority": _determine_migration_priority(complexity_score),
|
|
681
|
+
"dependencies": [],
|
|
682
|
+
}
|
|
449
683
|
|
|
450
684
|
|
|
451
685
|
def _format_overall_metrics(metrics: dict) -> str:
|
|
@@ -625,17 +859,20 @@ def _create_migration_roadmap(assessments: list) -> str:
|
|
|
625
859
|
return "\n".join(roadmap_formatted)
|
|
626
860
|
|
|
627
861
|
|
|
628
|
-
def
|
|
629
|
-
"""Assess
|
|
862
|
+
def _assess_technical_complexity_risks(assessments: list) -> list[str]:
|
|
863
|
+
"""Assess risks related to technical complexity."""
|
|
630
864
|
risks = []
|
|
631
|
-
|
|
632
|
-
# Technical risks
|
|
633
865
|
high_complexity_count = len([a for a in assessments if a["complexity_score"] > 70])
|
|
634
866
|
if high_complexity_count > 0:
|
|
635
867
|
risks.append(
|
|
636
868
|
f"🔴 HIGH: {high_complexity_count} high-complexity cookbooks may cause delays"
|
|
637
869
|
)
|
|
870
|
+
return risks
|
|
871
|
+
|
|
638
872
|
|
|
873
|
+
def _assess_custom_resource_risks(assessments: list) -> list[str]:
|
|
874
|
+
"""Assess risks related to custom resources and Ruby blocks."""
|
|
875
|
+
risks = []
|
|
639
876
|
custom_resource_count = sum(a["metrics"]["custom_resources"] for a in assessments)
|
|
640
877
|
if custom_resource_count > 0:
|
|
641
878
|
risks.append(
|
|
@@ -648,14 +885,33 @@ def _assess_migration_risks(assessments: list, target_platform: str) -> str:
|
|
|
648
885
|
f"🟡 MEDIUM: {ruby_block_count} Ruby blocks require shell script conversion"
|
|
649
886
|
)
|
|
650
887
|
|
|
651
|
-
|
|
888
|
+
return risks
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def _assess_timeline_risks(assessments: list) -> list[str]:
|
|
892
|
+
"""Assess risks related to migration timeline and scope."""
|
|
893
|
+
risks = []
|
|
652
894
|
total_effort = sum(a["estimated_effort_days"] for a in assessments)
|
|
653
895
|
if total_effort > 50:
|
|
654
896
|
risks.append("🟡 MEDIUM: Large migration scope may impact timeline")
|
|
897
|
+
return risks
|
|
655
898
|
|
|
656
|
-
|
|
899
|
+
|
|
900
|
+
def _assess_platform_risks(target_platform: str) -> list[str]:
|
|
901
|
+
"""Assess risks related to target platform."""
|
|
657
902
|
if target_platform == "ansible_awx":
|
|
658
|
-
|
|
903
|
+
return ["🟢 LOW: AWX integration well-supported with existing tools"]
|
|
904
|
+
return []
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def _assess_migration_risks(assessments: list, target_platform: str) -> str:
|
|
908
|
+
"""Assess migration risks."""
|
|
909
|
+
risks = []
|
|
910
|
+
|
|
911
|
+
risks.extend(_assess_technical_complexity_risks(assessments))
|
|
912
|
+
risks.extend(_assess_custom_resource_risks(assessments))
|
|
913
|
+
risks.extend(_assess_timeline_risks(assessments))
|
|
914
|
+
risks.extend(_assess_platform_risks(target_platform))
|
|
659
915
|
|
|
660
916
|
if not risks:
|
|
661
917
|
risks.append("🟢 LOW: No significant migration risks identified")
|
|
@@ -1146,6 +1402,46 @@ def _generate_migration_timeline(strategy: str, timeline_weeks: int) -> str:
|
|
|
1146
1402
|
return "\n".join([f"• {milestone}" for milestone in milestones])
|
|
1147
1403
|
|
|
1148
1404
|
|
|
1405
|
+
def _build_validation_header(
|
|
1406
|
+
conversion_type: str, summary: dict[str, int]
|
|
1407
|
+
) -> list[str]:
|
|
1408
|
+
"""Build the header section of validation results."""
|
|
1409
|
+
return [
|
|
1410
|
+
f"# Validation Results for {conversion_type} Conversion",
|
|
1411
|
+
"",
|
|
1412
|
+
"## Summary",
|
|
1413
|
+
f"• Errors: {summary['errors']}",
|
|
1414
|
+
f"• Warnings: {summary['warnings']}",
|
|
1415
|
+
f"• Info: {summary['info']}",
|
|
1416
|
+
"",
|
|
1417
|
+
]
|
|
1418
|
+
|
|
1419
|
+
|
|
1420
|
+
def _group_results_by_level(
|
|
1421
|
+
results: list[ValidationResult],
|
|
1422
|
+
) -> tuple[list[ValidationResult], list[ValidationResult], list[ValidationResult]]:
|
|
1423
|
+
"""Group validation results by severity level."""
|
|
1424
|
+
errors = [r for r in results if r.level == ValidationLevel.ERROR]
|
|
1425
|
+
warnings = [r for r in results if r.level == ValidationLevel.WARNING]
|
|
1426
|
+
infos = [r for r in results if r.level == ValidationLevel.INFO]
|
|
1427
|
+
return errors, warnings, infos
|
|
1428
|
+
|
|
1429
|
+
|
|
1430
|
+
def _format_result_section(
|
|
1431
|
+
title: str, icon: str, results: list[ValidationResult]
|
|
1432
|
+
) -> list[str]:
|
|
1433
|
+
"""Format a single validation results section."""
|
|
1434
|
+
if not results:
|
|
1435
|
+
return []
|
|
1436
|
+
|
|
1437
|
+
lines = [f"## {icon} {title}", ""]
|
|
1438
|
+
for result in results:
|
|
1439
|
+
lines.append(str(result))
|
|
1440
|
+
lines.append("")
|
|
1441
|
+
|
|
1442
|
+
return lines
|
|
1443
|
+
|
|
1444
|
+
|
|
1149
1445
|
def _format_validation_results_text(
|
|
1150
1446
|
conversion_type: str, results: list[ValidationResult], summary: dict[str, int]
|
|
1151
1447
|
) -> str:
|
|
@@ -1166,41 +1462,13 @@ def _format_validation_results_text(
|
|
|
1166
1462
|
|
|
1167
1463
|
✅ All validation checks passed! No issues found.
|
|
1168
1464
|
"""
|
|
1169
|
-
output_lines = [
|
|
1170
|
-
f"# Validation Results for {conversion_type} Conversion",
|
|
1171
|
-
"",
|
|
1172
|
-
"## Summary",
|
|
1173
|
-
f"• Errors: {summary['errors']}",
|
|
1174
|
-
f"• Warnings: {summary['warnings']}",
|
|
1175
|
-
f"• Info: {summary['info']}",
|
|
1176
|
-
"",
|
|
1177
|
-
]
|
|
1178
1465
|
|
|
1179
|
-
|
|
1180
|
-
errors
|
|
1181
|
-
warnings = [r for r in results if r.level == ValidationLevel.WARNING]
|
|
1182
|
-
infos = [r for r in results if r.level == ValidationLevel.INFO]
|
|
1466
|
+
output_lines = _build_validation_header(conversion_type, summary)
|
|
1467
|
+
errors, warnings, infos = _group_results_by_level(results)
|
|
1183
1468
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
for result in errors:
|
|
1188
|
-
output_lines.append(str(result))
|
|
1189
|
-
output_lines.append("")
|
|
1190
|
-
|
|
1191
|
-
if warnings:
|
|
1192
|
-
output_lines.append("## ⚠️ Warnings")
|
|
1193
|
-
output_lines.append("")
|
|
1194
|
-
for result in warnings:
|
|
1195
|
-
output_lines.append(str(result))
|
|
1196
|
-
output_lines.append("")
|
|
1197
|
-
|
|
1198
|
-
if infos:
|
|
1199
|
-
output_lines.append("## ℹ️ Information")
|
|
1200
|
-
output_lines.append("")
|
|
1201
|
-
for result in infos:
|
|
1202
|
-
output_lines.append(str(result))
|
|
1203
|
-
output_lines.append("")
|
|
1469
|
+
output_lines.extend(_format_result_section("❌ Errors", "", errors))
|
|
1470
|
+
output_lines.extend(_format_result_section("⚠️ Warnings", "", warnings))
|
|
1471
|
+
output_lines.extend(_format_result_section("ℹ️ Information", "", infos))
|
|
1204
1472
|
|
|
1205
1473
|
return "\n".join(output_lines)
|
|
1206
1474
|
|