mcp-souschef 2.8.0__py3-none-any.whl → 3.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.8.0.dist-info → mcp_souschef-3.2.0.dist-info}/METADATA +159 -384
- mcp_souschef-3.2.0.dist-info/RECORD +47 -0
- {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.2.0.dist-info}/WHEEL +1 -1
- souschef/__init__.py +31 -7
- souschef/assessment.py +1451 -105
- souschef/ci/common.py +126 -0
- souschef/ci/github_actions.py +3 -92
- souschef/ci/gitlab_ci.py +2 -52
- souschef/ci/jenkins_pipeline.py +2 -59
- souschef/cli.py +149 -16
- souschef/converters/playbook.py +378 -138
- souschef/converters/resource.py +12 -11
- souschef/converters/template.py +177 -0
- souschef/core/__init__.py +6 -1
- souschef/core/metrics.py +313 -0
- souschef/core/path_utils.py +233 -19
- souschef/core/validation.py +53 -0
- souschef/deployment.py +71 -12
- souschef/generators/__init__.py +13 -0
- souschef/generators/repo.py +695 -0
- souschef/parsers/attributes.py +1 -1
- souschef/parsers/habitat.py +1 -1
- souschef/parsers/inspec.py +25 -2
- souschef/parsers/metadata.py +5 -3
- souschef/parsers/recipe.py +1 -1
- souschef/parsers/resource.py +1 -1
- souschef/parsers/template.py +1 -1
- souschef/server.py +1039 -121
- souschef/ui/app.py +486 -374
- souschef/ui/pages/ai_settings.py +74 -8
- souschef/ui/pages/cookbook_analysis.py +3216 -373
- souschef/ui/pages/validation_reports.py +274 -0
- mcp_souschef-2.8.0.dist-info/RECORD +0 -42
- souschef/converters/cookbook_specific.py.backup +0 -109
- {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.2.0.dist-info}/entry_points.txt +0 -0
- {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.2.0.dist-info}/licenses/LICENSE +0 -0
souschef/core/path_utils.py
CHANGED
|
@@ -1,47 +1,107 @@
|
|
|
1
1
|
"""Path utility functions for safe filesystem operations."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
|
|
6
|
-
def
|
|
7
|
+
def _trusted_workspace_root() -> Path:
|
|
8
|
+
"""Return the trusted workspace root used for containment checks."""
|
|
9
|
+
return Path.cwd().resolve()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _ensure_within_base_path(path_obj: Path, base_path: Path) -> Path:
|
|
13
|
+
"""
|
|
14
|
+
Ensure a path stays within a trusted base directory.
|
|
15
|
+
|
|
16
|
+
This is a path containment validator that prevents directory traversal
|
|
17
|
+
attacks (CWE-22) by ensuring paths stay within trusted boundaries.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
path_obj: Path to validate.
|
|
21
|
+
base_path: Trusted base directory.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Resolved Path guaranteed to be contained within ``base_path``.
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
ValueError: If the path escapes the base directory.
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
# Use pathlib.Path.resolve() for normalization (prevents traversal)
|
|
31
|
+
base_resolved: Path = Path(base_path).resolve()
|
|
32
|
+
candidate_resolved: Path = Path(path_obj).resolve()
|
|
33
|
+
|
|
34
|
+
# Check containment using relative_to (raises ValueError if not contained)
|
|
35
|
+
try:
|
|
36
|
+
candidate_resolved.relative_to(base_resolved)
|
|
37
|
+
except ValueError as e:
|
|
38
|
+
msg = f"Path traversal attempt: escapes {base_resolved}"
|
|
39
|
+
raise ValueError(msg) from e
|
|
40
|
+
|
|
41
|
+
return candidate_resolved # nosonar
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _normalize_path(path_str: str | Path) -> Path:
|
|
7
45
|
"""
|
|
8
46
|
Normalize a file path for safe filesystem operations.
|
|
9
47
|
|
|
10
48
|
This function validates input and resolves relative paths and symlinks
|
|
11
49
|
to absolute paths, preventing path traversal attacks (CWE-23).
|
|
12
50
|
|
|
51
|
+
This is a sanitizer for path inputs - it validates and normalizes
|
|
52
|
+
paths before any filesystem operations.
|
|
53
|
+
|
|
13
54
|
Args:
|
|
14
|
-
path_str: Path string to normalize.
|
|
55
|
+
path_str: Path string or Path object to normalize.
|
|
15
56
|
|
|
16
57
|
Returns:
|
|
17
58
|
Resolved absolute Path object.
|
|
18
59
|
|
|
19
60
|
Raises:
|
|
20
|
-
ValueError: If the path contains null bytes
|
|
61
|
+
ValueError: If the path contains null bytes or is invalid.
|
|
21
62
|
|
|
22
63
|
"""
|
|
23
|
-
if
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
raise ValueError(f"Path
|
|
64
|
+
# Convert Path to string if needed for validation
|
|
65
|
+
if isinstance(path_str, Path):
|
|
66
|
+
path_obj = path_str
|
|
67
|
+
elif isinstance(path_str, str):
|
|
68
|
+
# Reject paths with null bytes (CWE-158 prevention)
|
|
69
|
+
if "\x00" in path_str:
|
|
70
|
+
raise ValueError(f"Path contains null bytes: {path_str!r}")
|
|
71
|
+
path_obj = Path(path_str)
|
|
72
|
+
else:
|
|
73
|
+
raise ValueError(f"Path must be a string or Path object, got {type(path_str)}")
|
|
33
74
|
|
|
34
75
|
try:
|
|
35
|
-
#
|
|
36
|
-
|
|
76
|
+
# Path.resolve() normalizes the path, resolving symlinks and ".." sequences
|
|
77
|
+
# This prevents path traversal attacks by canonicalizing the path
|
|
78
|
+
# Input validated for null bytes; Path.resolve() returns safe absolute path
|
|
79
|
+
resolved_path = path_obj.expanduser().resolve() # nosonar
|
|
80
|
+
# Explicit assignment to mark as sanitized output
|
|
81
|
+
normalized: Path = resolved_path # nosonar
|
|
82
|
+
return normalized
|
|
37
83
|
except (OSError, RuntimeError) as e:
|
|
38
84
|
raise ValueError(f"Invalid path {path_str}: {e}") from e
|
|
39
85
|
|
|
40
86
|
|
|
87
|
+
def _normalize_trusted_base(base_path: Path | str) -> Path:
|
|
88
|
+
"""
|
|
89
|
+
Normalise a base path.
|
|
90
|
+
|
|
91
|
+
This normalizes the path without enforcing workspace containment.
|
|
92
|
+
Workspace containment is enforced at the application entry points,
|
|
93
|
+
not at the path utility level.
|
|
94
|
+
"""
|
|
95
|
+
return _normalize_path(base_path)
|
|
96
|
+
|
|
97
|
+
|
|
41
98
|
def _safe_join(base_path: Path, *parts: str) -> Path:
|
|
42
99
|
"""
|
|
43
100
|
Safely join path components ensuring result stays within base directory.
|
|
44
101
|
|
|
102
|
+
This prevents path traversal by validating the joined result stays
|
|
103
|
+
contained within the base directory (CWE-22 mitigation).
|
|
104
|
+
|
|
45
105
|
Args:
|
|
46
106
|
base_path: Normalized base path.
|
|
47
107
|
*parts: Path components to join.
|
|
@@ -53,9 +113,163 @@ def _safe_join(base_path: Path, *parts: str) -> Path:
|
|
|
53
113
|
ValueError: If result would escape base_path.
|
|
54
114
|
|
|
55
115
|
"""
|
|
56
|
-
|
|
116
|
+
# Resolve base path to canonical form
|
|
117
|
+
base_resolved: Path = Path(base_path).resolve()
|
|
118
|
+
|
|
119
|
+
# Join and resolve the full path
|
|
120
|
+
joined_path: Path = base_resolved.joinpath(*parts)
|
|
121
|
+
result_resolved: Path = joined_path.resolve()
|
|
122
|
+
|
|
123
|
+
# Validate containment using relative_to
|
|
124
|
+
try:
|
|
125
|
+
result_resolved.relative_to(base_resolved)
|
|
126
|
+
except ValueError as e:
|
|
127
|
+
msg = f"Path traversal attempt: {parts} escapes {base_path}"
|
|
128
|
+
raise ValueError(msg) from e
|
|
129
|
+
|
|
130
|
+
return result_resolved # nosonar
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _validated_candidate(path_obj: Path, safe_base: Path) -> Path:
|
|
134
|
+
"""
|
|
135
|
+
Validate a candidate path stays contained under ``safe_base``.
|
|
136
|
+
|
|
137
|
+
This is a path sanitizer that ensures directory traversal attacks
|
|
138
|
+
are prevented by validating containment (CWE-22 mitigation).
|
|
139
|
+
"""
|
|
140
|
+
# Resolve both paths to canonical forms
|
|
141
|
+
base_resolved: Path = Path(safe_base).resolve()
|
|
142
|
+
candidate_resolved: Path = Path(path_obj).resolve()
|
|
143
|
+
|
|
144
|
+
# Check containment using relative_to
|
|
57
145
|
try:
|
|
58
|
-
|
|
59
|
-
return result
|
|
146
|
+
candidate_resolved.relative_to(base_resolved)
|
|
60
147
|
except ValueError as e:
|
|
61
|
-
|
|
148
|
+
msg = f"Path traversal attempt: escapes {base_resolved}"
|
|
149
|
+
raise ValueError(msg) from e
|
|
150
|
+
|
|
151
|
+
return candidate_resolved # nosonar
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def safe_exists(path_obj: Path, base_path: Path) -> bool:
|
|
155
|
+
"""Check existence after enforcing base containment."""
|
|
156
|
+
safe_base = _normalize_trusted_base(base_path)
|
|
157
|
+
candidate: Path = _validated_candidate(path_obj, safe_base)
|
|
158
|
+
return candidate.exists()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def safe_is_dir(path_obj: Path, base_path: Path) -> bool:
|
|
162
|
+
"""Check directory-ness after enforcing base containment."""
|
|
163
|
+
safe_base = _normalize_trusted_base(base_path)
|
|
164
|
+
candidate: Path = _validated_candidate(path_obj, safe_base)
|
|
165
|
+
return candidate.is_dir()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def safe_is_file(path_obj: Path, base_path: Path) -> bool:
|
|
169
|
+
"""Check file-ness after enforcing base containment."""
|
|
170
|
+
safe_base = _normalize_trusted_base(base_path)
|
|
171
|
+
candidate: Path = _validated_candidate(path_obj, safe_base)
|
|
172
|
+
return candidate.is_file()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def safe_glob(dir_path: Path, pattern: str, base_path: Path) -> list[Path]:
|
|
176
|
+
"""
|
|
177
|
+
Glob inside a directory after enforcing containment.
|
|
178
|
+
|
|
179
|
+
Only literal patterns provided by code should be used for ``pattern``.
|
|
180
|
+
"""
|
|
181
|
+
if ".." in pattern:
|
|
182
|
+
msg = f"Unsafe glob pattern detected: {pattern!r}"
|
|
183
|
+
raise ValueError(msg)
|
|
184
|
+
if pattern.startswith((os.sep, "\\")):
|
|
185
|
+
msg = f"Absolute glob patterns are not allowed: {pattern!r}"
|
|
186
|
+
raise ValueError(msg)
|
|
187
|
+
|
|
188
|
+
safe_base = _normalize_trusted_base(base_path)
|
|
189
|
+
safe_dir: Path = _validated_candidate(_normalize_path(dir_path), safe_base)
|
|
190
|
+
|
|
191
|
+
results: list[Path] = []
|
|
192
|
+
for result in safe_dir.glob(pattern): # nosonar
|
|
193
|
+
# Validate each glob result stays within base
|
|
194
|
+
validated_result: Path = _validated_candidate(Path(result), safe_base)
|
|
195
|
+
results.append(validated_result)
|
|
196
|
+
|
|
197
|
+
return results
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def safe_mkdir(
|
|
201
|
+
path_obj: Path, base_path: Path, parents: bool = False, exist_ok: bool = False
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Create directory after enforcing base containment."""
|
|
204
|
+
safe_base = _normalize_trusted_base(base_path)
|
|
205
|
+
safe_path = _validated_candidate(_normalize_path(path_obj), safe_base)
|
|
206
|
+
|
|
207
|
+
safe_path.mkdir(parents=parents, exist_ok=exist_ok) # nosonar
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def safe_read_text(path_obj: Path, base_path: Path, encoding: str = "utf-8") -> str:
|
|
211
|
+
"""
|
|
212
|
+
Read text from file after enforcing base containment.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
path_obj: Path to the file to read.
|
|
216
|
+
base_path: Trusted base directory for containment check.
|
|
217
|
+
encoding: Text encoding (default: 'utf-8').
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
File contents as string.
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
ValueError: If the path escapes the base directory.
|
|
224
|
+
|
|
225
|
+
"""
|
|
226
|
+
safe_base = _normalize_trusted_base(base_path)
|
|
227
|
+
safe_path = _validated_candidate(_normalize_path(path_obj), safe_base)
|
|
228
|
+
|
|
229
|
+
return safe_path.read_text(encoding=encoding) # nosonar
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def safe_write_text(
|
|
233
|
+
path_obj: Path, base_path: Path, text: str, encoding: str = "utf-8"
|
|
234
|
+
) -> None:
|
|
235
|
+
"""
|
|
236
|
+
Write text to file after enforcing base containment.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
path_obj: Path to the file to write.
|
|
240
|
+
base_path: Trusted base directory for containment check.
|
|
241
|
+
text: Text content to write.
|
|
242
|
+
encoding: Text encoding (default: 'utf-8').
|
|
243
|
+
|
|
244
|
+
"""
|
|
245
|
+
safe_base = _normalize_trusted_base(base_path)
|
|
246
|
+
safe_path = _validated_candidate(_normalize_path(path_obj), safe_base)
|
|
247
|
+
|
|
248
|
+
safe_path.write_text(text, encoding=encoding) # nosonar
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def safe_iterdir(path_obj: Path, base_path: Path) -> list[Path]:
|
|
252
|
+
"""
|
|
253
|
+
Iterate directory contents after enforcing base containment.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
path_obj: Directory path to iterate.
|
|
257
|
+
base_path: Trusted base directory for containment check.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
List of validated paths within the directory.
|
|
261
|
+
|
|
262
|
+
Raises:
|
|
263
|
+
ValueError: If path escapes the base directory.
|
|
264
|
+
|
|
265
|
+
"""
|
|
266
|
+
safe_base = _normalize_trusted_base(base_path)
|
|
267
|
+
safe_path = _validated_candidate(_normalize_path(path_obj), safe_base)
|
|
268
|
+
|
|
269
|
+
results: list[Path] = []
|
|
270
|
+
for item in safe_path.iterdir(): # nosonar
|
|
271
|
+
# Validate each item stays within base
|
|
272
|
+
validated_item: Path = _validated_candidate(item, safe_base)
|
|
273
|
+
results.append(validated_item)
|
|
274
|
+
|
|
275
|
+
return results
|
souschef/core/validation.py
CHANGED
|
@@ -586,3 +586,56 @@ class ValidationEngine:
|
|
|
586
586
|
elif result.level == ValidationLevel.INFO:
|
|
587
587
|
summary["info"] += 1
|
|
588
588
|
return summary
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _format_validation_results_summary(
|
|
592
|
+
conversion_type: str, summary: dict[str, int]
|
|
593
|
+
) -> str:
|
|
594
|
+
"""
|
|
595
|
+
Format validation results as a summary.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
conversion_type: Type of conversion.
|
|
599
|
+
summary: Summary of validation results.
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
Formatted summary output.
|
|
603
|
+
|
|
604
|
+
"""
|
|
605
|
+
total_issues = summary["errors"] + summary["warnings"] + summary["info"]
|
|
606
|
+
|
|
607
|
+
if total_issues == 0:
|
|
608
|
+
return f"""# Validation Summary for {conversion_type} Conversion
|
|
609
|
+
|
|
610
|
+
✅ **All validation checks passed!** No issues found.
|
|
611
|
+
|
|
612
|
+
Errors: 0
|
|
613
|
+
Warnings: 0
|
|
614
|
+
Info: 0
|
|
615
|
+
"""
|
|
616
|
+
|
|
617
|
+
# Determine status icon based on error/warning counts
|
|
618
|
+
if summary["errors"] > 0:
|
|
619
|
+
status_icon = "❌"
|
|
620
|
+
elif summary["warnings"] > 0:
|
|
621
|
+
status_icon = "⚠️"
|
|
622
|
+
else:
|
|
623
|
+
status_icon = "ℹ️"
|
|
624
|
+
|
|
625
|
+
# Determine status message based on error/warning counts
|
|
626
|
+
if summary["errors"] > 0:
|
|
627
|
+
status = "Failed"
|
|
628
|
+
elif summary["warnings"] > 0:
|
|
629
|
+
status = "Warning"
|
|
630
|
+
else:
|
|
631
|
+
status = "Passed with info"
|
|
632
|
+
|
|
633
|
+
return f"""# Validation Summary for {conversion_type} Conversion
|
|
634
|
+
|
|
635
|
+
{status_icon} **Validation Results:**
|
|
636
|
+
• Errors: {summary["errors"]}
|
|
637
|
+
• Warnings: {summary["warnings"]}
|
|
638
|
+
• Info: {summary["info"]}
|
|
639
|
+
|
|
640
|
+
**Status:** {status}
|
|
641
|
+
"""
|
souschef/deployment.py
CHANGED
|
@@ -10,6 +10,7 @@ import json
|
|
|
10
10
|
import re
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any
|
|
13
|
+
from urllib.parse import urlparse
|
|
13
14
|
|
|
14
15
|
from souschef.core.constants import (
|
|
15
16
|
CHEF_RECIPE_PREFIX,
|
|
@@ -21,6 +22,11 @@ from souschef.core.errors import (
|
|
|
21
22
|
validate_cookbook_structure,
|
|
22
23
|
validate_directory_exists,
|
|
23
24
|
)
|
|
25
|
+
from souschef.core.metrics import (
|
|
26
|
+
ComplexityLevel,
|
|
27
|
+
EffortMetrics,
|
|
28
|
+
categorize_complexity,
|
|
29
|
+
)
|
|
24
30
|
from souschef.core.path_utils import _safe_join
|
|
25
31
|
|
|
26
32
|
# Maximum length for attribute values in Chef attribute parsing
|
|
@@ -253,10 +259,11 @@ def generate_awx_inventory_source_from_chef(
|
|
|
253
259
|
"(e.g., https://chef.example.com)"
|
|
254
260
|
)
|
|
255
261
|
|
|
256
|
-
|
|
262
|
+
parsed_url = urlparse(chef_server_url)
|
|
263
|
+
if parsed_url.scheme != "https" or not parsed_url.netloc:
|
|
257
264
|
return (
|
|
258
265
|
f"Error: Invalid Chef server URL: {chef_server_url}\n\n"
|
|
259
|
-
"Suggestion: URL must use HTTPS protocol
|
|
266
|
+
"Suggestion: URL must use HTTPS protocol with a valid host "
|
|
260
267
|
"(e.g., https://chef.example.com)"
|
|
261
268
|
)
|
|
262
269
|
|
|
@@ -978,7 +985,12 @@ def main():
|
|
|
978
985
|
# Chef server configuration
|
|
979
986
|
chef_server_url = os.environ.get('CHEF_SERVER_URL', '{chef_server_url}')
|
|
980
987
|
client_name = os.environ.get('CHEF_NODE_NAME', 'admin')
|
|
981
|
-
|
|
988
|
+
# Client key path should be customizable - use environment variable with
|
|
989
|
+
# home directory default instead of hardcoded /etc/chef/client.pem
|
|
990
|
+
client_key = os.environ.get(
|
|
991
|
+
'CHEF_CLIENT_KEY',
|
|
992
|
+
os.path.expanduser('~/.chef/client.pem')
|
|
993
|
+
)
|
|
982
994
|
|
|
983
995
|
# Initialize Chef API
|
|
984
996
|
try:
|
|
@@ -1496,13 +1508,56 @@ def _detect_patterns_from_content(content: str) -> list[str]:
|
|
|
1496
1508
|
return patterns
|
|
1497
1509
|
|
|
1498
1510
|
|
|
1499
|
-
def _assess_complexity_from_resource_count(
|
|
1500
|
-
|
|
1511
|
+
def _assess_complexity_from_resource_count(
|
|
1512
|
+
resource_count: int,
|
|
1513
|
+
) -> tuple[ComplexityLevel, str, str]:
|
|
1514
|
+
"""
|
|
1515
|
+
Assess complexity, effort estimate, and risk based on resource count.
|
|
1516
|
+
|
|
1517
|
+
Uses centralized metrics for consistent complexity categorization.
|
|
1518
|
+
|
|
1519
|
+
Args:
|
|
1520
|
+
resource_count: Number of resources in cookbook
|
|
1521
|
+
|
|
1522
|
+
Returns:
|
|
1523
|
+
Tuple of (complexity_level, effort_estimate_weeks, risk_level)
|
|
1524
|
+
|
|
1525
|
+
"""
|
|
1526
|
+
# Map resource count to complexity score (0-100 scale)
|
|
1527
|
+
# 50+ resources = high complexity (70-100)
|
|
1528
|
+
# 20-50 resources = medium complexity (30-69)
|
|
1529
|
+
# <20 resources = low complexity (0-29)
|
|
1501
1530
|
if resource_count > 50:
|
|
1502
|
-
|
|
1503
|
-
elif resource_count
|
|
1504
|
-
|
|
1505
|
-
|
|
1531
|
+
complexity_score = 80
|
|
1532
|
+
elif resource_count > 30:
|
|
1533
|
+
complexity_score = 50
|
|
1534
|
+
elif resource_count >= 20:
|
|
1535
|
+
complexity_score = 40
|
|
1536
|
+
else:
|
|
1537
|
+
complexity_score = 15
|
|
1538
|
+
|
|
1539
|
+
# Use centralized categorization
|
|
1540
|
+
complexity_level = categorize_complexity(complexity_score)
|
|
1541
|
+
|
|
1542
|
+
# Estimate effort based on resource count and complexity
|
|
1543
|
+
# Base: 0.2 days per resource (2.5 hours)
|
|
1544
|
+
base_days = resource_count * 0.2
|
|
1545
|
+
complexity_multiplier = 1 + (complexity_score / 100)
|
|
1546
|
+
estimated_days = round(base_days * complexity_multiplier, 1)
|
|
1547
|
+
|
|
1548
|
+
# Create metrics object for consistent week calculation
|
|
1549
|
+
metrics = EffortMetrics(estimated_days=estimated_days)
|
|
1550
|
+
effort_estimate = metrics.estimated_weeks_range
|
|
1551
|
+
|
|
1552
|
+
# Risk mapping based on complexity level
|
|
1553
|
+
if complexity_level == ComplexityLevel.HIGH:
|
|
1554
|
+
risk_level = "high"
|
|
1555
|
+
elif complexity_level == ComplexityLevel.MEDIUM:
|
|
1556
|
+
risk_level = "medium"
|
|
1557
|
+
else:
|
|
1558
|
+
risk_level = "low"
|
|
1559
|
+
|
|
1560
|
+
return complexity_level, effort_estimate, risk_level
|
|
1506
1561
|
|
|
1507
1562
|
|
|
1508
1563
|
def _analyse_application_cookbook(cookbook_path: Path, app_type: str) -> dict:
|
|
@@ -1536,10 +1591,14 @@ def _analyse_application_cookbook(cookbook_path: Path, app_type: str) -> dict:
|
|
|
1536
1591
|
# Silently skip malformed files
|
|
1537
1592
|
pass
|
|
1538
1593
|
|
|
1539
|
-
# Assess complexity
|
|
1594
|
+
# Assess complexity using centralized function
|
|
1540
1595
|
resource_count = len(analysis["resources"])
|
|
1541
|
-
|
|
1542
|
-
|
|
1596
|
+
complexity_level, effort, risk = _assess_complexity_from_resource_count(
|
|
1597
|
+
resource_count
|
|
1598
|
+
)
|
|
1599
|
+
|
|
1600
|
+
# Convert complexity level enum to string for backward compatibility
|
|
1601
|
+
analysis["complexity"] = complexity_level.value
|
|
1543
1602
|
analysis["effort_estimate"] = effort
|
|
1544
1603
|
analysis["risk_level"] = risk
|
|
1545
1604
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Ansible artifact generators."""
|
|
2
|
+
|
|
3
|
+
from souschef.generators.repo import (
|
|
4
|
+
RepoType,
|
|
5
|
+
analyse_conversion_output,
|
|
6
|
+
generate_ansible_repository,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"RepoType",
|
|
11
|
+
"analyse_conversion_output",
|
|
12
|
+
"generate_ansible_repository",
|
|
13
|
+
]
|